SOURCE / PINNED RELEASE
Made of little things.
Outbreak: Soycoon City
- Release
- db7750b1d009…
- Author-recorded commit
- 3ee3f5fd27a6…
- License
- LICENSE
- Author’s source reference
- nostr://npub182jczunncwe0jn6frpqwq3e0qjws7yqqnc3auccqv9nte2dnd63scjm4rf/wss%3A%2F%2Fgit.napplet.soy%2F/n-b87c04571a9
Archive hash verified: dbebdc772b142a1c…. The source-to-build association is the author’s claim; it has not been independently rebuilt.
import { PerspectiveCamera, Vector3 } from 'three';
import { blocked, cameraAt, cutWall, doorArrival, inZone, polygonIssue, zonePoints, type Camera, type Door, type Point, type Room } from './world.js';
export function view(camera: Camera): PerspectiveCamera {
const result=new PerspectiveCamera(camera.fov,4/3,.1,80);result.position.set(...camera.position);result.lookAt(...camera.target);result.updateMatrixWorld();return result;
}
export function onScreen(camera: Camera, point: Point, height=1, margin=.92): boolean {
const p=new Vector3(point.x,height,point.z).project(view(camera));return p.z>-1&&p.z<1&&Math.abs(p.x)<=margin&&Math.abs(p.y)<=margin;
}
export function actorFramed(camera: Camera, point: Point): boolean { return onScreen(camera,point,.08,.97)&&onScreen(camera,point,1.85,.97); }
function clearRay(room: Room, camera: Camera, point: Point, height: number): boolean {
for(let i=1;i<50;i++) {
const t=i/50,x=camera.position[0]+(point.x-camera.position[0])*t,z=camera.position[2]+(point.z-camera.position[2])*t,y=camera.position[1]+(height-camera.position[1])*t;
if(room.props.some(p=>Math.abs(x-p.x)<p.w/2&&Math.abs(z-p.z)<p.d/2&&y<p.h&&y>0))return false;
}
return true;
}
export function doorwayVisible(room: Room, camera: Camera, door: Door): boolean {
return !cutWall(camera,door.side)&&onScreen(camera,door,.1)&&onScreen(camera,door,2.1)&&clearRay(room,camera,door,1.45);
}
export function cameraWarnings(room: Room): string[] {
const warnings:string[]=[];
for(const door of room.doors){const camera=cameraAt(room,doorArrival(door));if(!doorwayVisible(room,camera,door))warnings.push(`${door.side} door needs a clearer approach shot`);}
let unseen=0,total=0;
for(let x=-room.w/2+.4;x<room.w/2;x+=.75)for(let z=-room.d/2+.4;z<room.d/2;z+=.75){const p={x,z};if(blocked(room,p))continue;total++;if(!actorFramed(cameraAt(room,p),p))unseen++;}
if(unseen)warnings.push(`${unseen}/${total} sampled floor positions crop the character`);
return warnings;
}
export function doorZone(room: Room,door: Door): {points: Point[]} {
const inward=door.side==='west'?[1,0]:door.side==='east'?[-1,0]:door.side==='north'?[0,1]:[0,-1],tangent=[-inward[1],inward[0]];
const point=(u:number,v:number)=>({x:Math.round(Math.max(-room.w/2,Math.min(room.w/2,door.x+u*inward[0]+v*tangent[0]))*1000)/1000,z:Math.round(Math.max(-room.d/2,Math.min(room.d/2,door.z+u*inward[1]+v*tangent[1]))*1000)/1000});
const points=[point(-.35,-1.65),...Array.from({length:9},(_,i)=>point(Math.sin(i*Math.PI/8)*1.65,-Math.cos(i*Math.PI/8)*1.65)),point(-.35,1.65)].filter((p,i,a)=>i===0||Math.hypot(p.x-a[i-1].x,p.z-a[i-1].z)>.05);
if(Math.hypot(points[0].x-points.at(-1)!.x,points[0].z-points.at(-1)!.z)<.05)points.pop();
const issue=polygonIssue(points);if(issue)throw Error(issue);return {points};
}
export function doorCamera(room: Room,door: Door,name: string): Camera {
const inward=door.side==='west'?[1,0]:door.side==='east'?[-1,0]:door.side==='north'?[0,1]:[0,-1],tangent=[-inward[1],inward[0]],extent=inward[0]?room.w:room.d;
const zone=doorZone(room,door),samples:Point[]=[doorArrival(door)];
const bounds=zonePoints(zone);
for(let x=Math.min(...bounds.map(p=>p.x))+.2;x<=Math.max(...bounds.map(p=>p.x));x+=.4)for(let z=Math.min(...bounds.map(p=>p.z))+.2;z<=Math.max(...bounds.map(p=>p.z));z+=.4){const p={x,z};if(inZone(p,zone)&&!blocked(room,p))samples.push(p);}
let best:Camera|undefined,bestScore=-Infinity;
for(const height of [2.8,3.05,3.6])for(const offset of [.8,-.8,0,1.8,-1.8])for(const range of [Math.min(5.5,extent-.8),Math.min(4.4,extent-.8)]){
const c:Camera={id:`door-${door.id}`,name:`Door → ${name}`.slice(0,180),position:[Math.max(-room.w/2+.25,Math.min(room.w/2-.25,door.x+inward[0]*range+tangent[0]*offset)),height,Math.max(-room.d/2+.25,Math.min(room.d/2-.25,door.z+inward[1]*range+tangent[1]*offset))],target:[door.x,1.05,door.z],fov:64,zone,priority:20,door:door.id};
const framed=samples.filter(p=>actorFramed(c,p)).length,clear=samples.filter(p=>clearRay(room,c,p,1.5)).length;
const score=(doorwayVisible(room,c,door)?10000:0)+framed*50+clear*20-height*.1-Math.abs(offset)*.01;
if(score>bestScore){best=c;bestScore=score;}
}
return best!;
}
