First-person shell: chunk streaming, instanced buildings + facade texture atlas (22 facade materials -> 1; 5 skin materials city-wide), signs, awnings, furniture, day/night lighting, minimap, pixel-accurate collision (incl. arbitrary-angle lots), door raycast -> procity:enterShop, DBG harness, all-fallback mode. Renders Lane A's full generated town (seed 20261990 'Boolarra Heads', 493 shops); worst continuous- walk view ~261 draws (<=300 gate). 6 adversarial-review bugs fixed (collision sign, house doors, awning skin, sign atlas, furniture seed hash, shot-mode restore). index.html and skins.js include Lane F's inline integration seam edits (marked '[Lane F integration]'): interior/keeper/citizen wiring + facade filename fix. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
115 lines
5.7 KiB
JavaScript
115 lines
5.7 KiB
JavaScript
// PROCITY Lane B — dbg.js
|
|
// The QA debug harness hook (window.DBG), installed by the shell only with ?dbg=1. It drives Lane
|
|
// F's tools/shots.py + tools/soak.py (LANE_F_NOTES §4) and mirrors 90sDJsim's window.DBG.shot()
|
|
// contract: named camera bookmarks, teleport, time-of-day, scripted shop entry, and a perf/heap
|
|
// readout. Every pose is derived from the LIVE plan so bookmarks work on any seed; the day/night
|
|
// cycle is paused on the first scripted segment change so captures are deterministic.
|
|
//
|
|
// Lives in its own module (not the shell) so it can grow without touching index.html — the shell
|
|
// just calls installDBG(deps) with the systems it owns when ?dbg=1 is present.
|
|
|
|
export function installDBG(deps) {
|
|
const { plan, camera, renderer, composer, chunks, lighting, player, hud,
|
|
setMode, getMode, enterShop } = deps;
|
|
const CS = 64;
|
|
|
|
// reset the perf counters, then render one full composited frame (bloom + output) so the harness
|
|
// captures exactly this frame and renderer.info reflects it — robust even when rAF is throttled.
|
|
const render = () => { renderer.info.reset(); composer.render(); };
|
|
|
|
const info = () => {
|
|
const r = renderer.info;
|
|
return {
|
|
drawCalls: r.render.calls, tris: r.render.triangles,
|
|
fps: hud && hud.getFps ? hud.getFps() : null,
|
|
heapMB: (performance.memory ? +(performance.memory.usedJSHeapSize / 1048576).toFixed(1) : null),
|
|
geometries: r.memory.geometries, textures: r.memory.textures,
|
|
chunk: `${Math.round(player.position.x / CS)},${Math.round(player.position.z / CS)}`,
|
|
chunks: chunks.count, mode: getMode(),
|
|
};
|
|
};
|
|
|
|
// ── camera pose facing a shop's front ──
|
|
// Front normal = local +Z rotated by lot.ry; the shopfront sits d/2 out from the lot centre, so we
|
|
// stand `dist` m IN FRONT OF THE SHOPFRONT (not the centre — deep lots like the dept anchor would
|
|
// otherwise put the camera inside the facade), offset slightly for a 3/4 angle, looking at it.
|
|
const lotOf = (shop) => plan.lots?.find((l) => l.id === shop.lot);
|
|
const poseForShop = (shop, dist = 10) => {
|
|
const lot = shop && lotOf(shop); if (!lot) return null;
|
|
const ry = lot.ry || 0, fx = Math.sin(ry), fz = Math.cos(ry); // front normal
|
|
const rx = Math.cos(ry), rz = -Math.sin(ry); // right vector
|
|
const half = (lot.d || 8) / 2, off = 1.0; // near dead-on (terraced neighbours flank the frame)
|
|
const frontX = lot.x + fx * half, frontZ = lot.z + fz * half; // shopfront centre
|
|
return {
|
|
pos: [frontX + fx * dist + rx * off, 2.0, frontZ + fz * dist + rz * off],
|
|
look: [frontX, 2.2, frontZ],
|
|
};
|
|
};
|
|
const byDist = () => [...(plan.shops || [])].sort((a, b) => {
|
|
const la = lotOf(a), lb = lotOf(b);
|
|
return Math.hypot(la?.x || 0, la?.z || 0) - Math.hypot(lb?.x || 0, lb?.z || 0);
|
|
});
|
|
// nearest shop matching pred, else nearest shop overall → a pose is always produced
|
|
const pickShop = (pred) => { const s = byDist(); return poseForShop(s.find(pred) || s[0]); };
|
|
|
|
// Named bookmarks (LANE_F_NOTES §4). seg = day segment index (0 DAWN … 5 NIGHT).
|
|
// Predicates cover both Lane A's taxonomy (dept/stall/record/…) and the fixture's; each falls back
|
|
// to the nearest shop, so every bookmark always yields a valid street pose.
|
|
const BOOKMARKS = {
|
|
street_noon: { seg: 2, resolve: () => pickShop(() => true) },
|
|
arcade: { seg: 3, resolve: () => pickShop((s) => s.type === 'dept' || /arcade/.test(s.type)) },
|
|
market_square: { seg: 1, resolve: () => pickShop((s) => s.type === 'stall' || s.type === 'market') },
|
|
milkbar_dusk: { seg: 4, resolve: () => pickShop((s) => s.type === 'milkbar') },
|
|
night_neon: { seg: 5, resolve: () => pickShop((s) => s.type === 'record' || s.type === 'video') },
|
|
};
|
|
|
|
const DBG = {
|
|
// true once the initial chunks are built and the queue is drained (textures may still decode)
|
|
get ready() { return chunks.count > 0 && chunks.pending === 0; },
|
|
bookmarks: Object.keys(BOOKMARKS),
|
|
info,
|
|
|
|
// time-of-day (hands the day cycle to the harness so captures are deterministic)
|
|
setSegment(seg) { lighting.setPaused(true); lighting.setSegment(seg | 0); return lighting.getClock(); },
|
|
|
|
// drive the soak walk: place the player and stream the destination in before the next step/shot
|
|
teleport(x, z, ry = 0) {
|
|
if (getMode() !== 'street') setMode('street');
|
|
player.teleport(x, z, ry);
|
|
chunks.warmup(player.position);
|
|
render();
|
|
return info();
|
|
},
|
|
|
|
// snap to a named bookmark, settle, and return the frame's stats
|
|
shot(name) {
|
|
const bm = BOOKMARKS[name] || BOOKMARKS.street_noon;
|
|
const pose = bm.resolve();
|
|
if (getMode() !== 'street') setMode('street');
|
|
if (bm.seg != null) { lighting.setPaused(true); lighting.setSegment(bm.seg); }
|
|
if (pose) {
|
|
player.teleport(pose.pos[0], pose.pos[2], 0);
|
|
camera.position.set(pose.pos[0], pose.pos[1], pose.pos[2]);
|
|
chunks.warmup(camera.position);
|
|
camera.lookAt(pose.look[0], pose.look[1], pose.look[2]);
|
|
camera.updateMatrixWorld();
|
|
}
|
|
render(); render(); // two frames so bloom + the segment change settle before capture
|
|
return { name, ...info() };
|
|
},
|
|
|
|
// scripted interior visit (drives the record_interior shot + the soak memory-baseline gate)
|
|
enterShop(shopId) {
|
|
const s = (plan.shops || []).find((x) => x.id === shopId);
|
|
if (!s) return { error: `no shop ${shopId}` };
|
|
enterShop(s.id, s.name);
|
|
return { entered: s.id, name: s.name };
|
|
},
|
|
exitShop() { window.dispatchEvent(new CustomEvent('procity:exitShop')); return { exited: true }; },
|
|
};
|
|
|
|
window.DBG = DBG;
|
|
console.log('[procity] DBG harness ready (?dbg=1) — shot/teleport/setSegment/enterShop/exitShop/info/ready');
|
|
return DBG;
|
|
}
|