diff --git a/B-progress.md b/B-progress.md index 7d9b31f..f13b06d 100644 --- a/B-progress.md +++ b/B-progress.md @@ -4,6 +4,21 @@ plus the game shell. It runs on my hand-written fixture **and** auto-integrates with Lane A's `generatePlan` (which landed mid-session). Everything below was verified live in a browser. +## Round 6 (Fable's ROUND6 → Lane B) — placed the orphaned street props · qa GREEN + +Wired the two published-but-unconsumed street GLBs (`street_bin_01`, `bus_shelter_01`) via the +use-if-ready furniture path (fail-soft; `?noassets` → primitives, 0 network). Files: `furniture.js`, +`chunks.js` (collider merge). +- **Bins**: sparse (~1/block, `s += 78 m`, seeded), decoupled from benches — country town not a mall. +- **Bus shelters on the street graph** (future tram/bus stops): deterministic rule = midpoint of every + MAIN edge with `hashEdge(id)%3===0`, road-side footpath, long axis along the edge, open front to road + → **1–3/town** (2 in seed 20261990). **Solid collider** (furniture now returns colliders → `chunks` + merges into `getColliders`); walk-into-it is blocked. **Exposed `busShelterStops(plan)` → the stop + list contract** for whoever builds the vehicle loop. +- **Perf**: worst view WITH rig-fleet citizens **94k tris / 148 draws** (≤200k / ≤300, big margin); + deterministic; all-flags-on composes clean (0 errors). Flagged the 3k-tri `street_bin` GLB to Lane E as + a decimation candidate (bounded by the sparse cadence for now). Shot: `docs/shots/laneB/bus_shelter.png`. + ## Round 5 (Fable's ROUND5 → Lane B) — v2 window trick, behind `?winmap=1` · qa GREEN (4/4) - **Parallax interior-mapping window glass** (V2_IDEAS "Vuntra window trick"), behind **`?winmap=1` diff --git a/docs/LANES/LANE_B_NOTES.md b/docs/LANES/LANE_B_NOTES.md index 0ae3c28..80721f5 100644 --- a/docs/LANES/LANE_B_NOTES.md +++ b/docs/LANES/LANE_B_NOTES.md @@ -1,5 +1,33 @@ # LANE B — NOTES (measured) +## Round 6 (Fable's ROUND6 → Lane B) — place the orphaned street props + +Placed the two published-but-unconsumed street GLBs (`procity_street_bin_01`, `procity_street_bus_shelter_01`) +via the existing use-if-ready furniture GLB path (fail-soft to primitives; `?noassets` → primitives, +**0 network** verified). All in `furniture.js` (+ a 3-line collider merge in `chunks.js`). + +- **Bins** — genuinely sparse now (one loop, ~1 per block: `s += 78 m`, seeded side per edge), a country + town not a mall. Decoupled from benches. GLB (3.0k tris) on its 0.57×0.68 footprint, primitive fallback. +- **Bus shelters — on the street graph, and this is the future tram/bus-stop contract.** Deterministic + rule: **the midpoint of every MAIN edge whose `hashEdge(id) % 3 === 0`, on the road-side footpath by the + kerb, long axis along the edge, open front to the road** — yields **1–3 per town** (2 in seed 20261990). + The GLB (2.33×2.4×1.41, 8.0k tris) gets a **solid oriented-rect collider** so the player rounds it + (furniture now returns `colliders`; `chunks.js` merges them into `getColliders`). Collision-verified: a + straight walk into it is blocked. + - **→ whoever builds the V2 tram/bus loop:** call **`busShelterStops(plan)`** (exported from + `furniture.js`) → `[{ edgeId, x, z, yaw }]`. It re-derives the stops from the exact same rule the + shelters are placed by — pure function of the plan, zero side effects. That is the stop list; don't + re-invent it. To move/space stops, change the one rule + `busShelterStops` together. +- **Perf (measured, worst view WITH the rig-fleet citizens, midday):** **94k tris · 148 draws** — both + well under the ≤200k / ≤300 gates (~100k tri and ~150 draw margin). Determinism holds (seed → same + props). All-flags-on (`?winmap=1&dig=1&roster=stream`) composes clean, 0 console errors. Shot: + `docs/shots/laneB/bus_shelter.png`. `qa.sh --strict` GREEN. + - **→ Lane E (decimation candidate, not blocking):** the `street_bin` GLB is **3.0k tris** — heavy for + a prop placed ~15×; I bounded it with the sparse cadence, but a decimate to ~500 tris would give back + tri headroom if the combined-lane budget tightens. (Same class as the deferred `novelty_record`.) + +--- + ## Round 5 (Fable's ROUND5 → Lane B) — v2: the Vuntra window trick, behind `?winmap=1` - **Parallax interior-mapping glass** (V2_IDEAS "window trick"). Behind **`?winmap=1`, default-off.** diff --git a/docs/shots/laneB/bus_shelter.png b/docs/shots/laneB/bus_shelter.png new file mode 100644 index 0000000..243996b Binary files /dev/null and b/docs/shots/laneB/bus_shelter.png differ diff --git a/web/js/world/chunks.js b/web/js/world/chunks.js index 956ff10..94fbf1f 100644 --- a/web/js/world/chunks.js +++ b/web/js/world/chunks.js @@ -35,7 +35,10 @@ export function createChunkManager(plan, scene, ctx) { lastBuildMs = performance.now() - t0; scene.add(buildings.group, furniture.group); if (buildings.doorMesh) doorMeshes.push(buildings.doorMesh); - live.set(key, { buildings, furniture, colliders: buildings.colliders, cx, cz }); + // player colliders = building/yard rects + any furniture keep-outs (bus shelters) + const colliders = furniture.colliders && furniture.colliders.length + ? buildings.colliders.concat(furniture.colliders) : buildings.colliders; + live.set(key, { buildings, furniture, colliders, cx, cz }); // optional lifecycle hook (Lane D/F per-chunk spawning, ambient, LOD — LANE_F_NOTES §8). // Inert unless a consumer sets ctx.onChunkBuilt; citizens currently drive off plan.streets instead. if (ctx.onChunkBuilt) ctx.onChunkBuilt(key, { cx, cz, buildings: buildings.group, furniture: furniture.group, data }); diff --git a/web/js/world/furniture.js b/web/js/world/furniture.js index 53c4ad0..7406e56 100644 --- a/web/js/world/furniture.js +++ b/web/js/world/furniture.js @@ -15,19 +15,24 @@ import { rng, frange } from '../core/prng.js'; import { chunkOf, resolveEdges } from './planutil.js'; const FOOT = 3.5; +// bus-shelter footprint (from manifest: 2.33 × 1.41 m) + a facing tweak applied to the along-edge +// yaw so the open front reads toward the road (verified in-browser; π flips it if the GLB is mirrored). +const SHELTER_W = 2.33, SHELTER_D = 1.41, SHELTER_YAW = 0; // ── 3GOD-depot furniture GLB upgrade (fail-soft; ?noassets → primitives only, zero network) ────── // Each mapped type loads ONCE at module init. buildChunkFurniture checks `_glbReady[type]` // SYNCHRONOUSLY at build time — so a chunk built before the GLB lands uses its primitive, and every // chunk built after uses the GLB (streaming rebuilds pick it up). No async write into a live chunk. const NOASSETS = (() => { try { const p = new URLSearchParams(location.search); return p.has('noassets') && p.get('noassets') !== '0'; } catch { return false; } })(); -// Only LIGHT GLBs (≤5k tris/glb_law) are enabled — a heavy prop × many instances blows the ≤200k -// tri gate. Measured: bench 1.9k ✓, food_cart 5.0k ✓, streetlight 5.0k ✓, but novelty_record is -// **26.5k tris** (5× over) → it stays on the low-poly PRIMITIVE until Lane E decimates it (same -// treatment as the ped rigs — see LANE_B_NOTES). Placement (record-store adjacency) is unchanged. +// Only LIGHT GLBs (≤~8k tris) are enabled — a heavy prop × many instances blows the ≤200k tri gate. +// Measured: bench 1.9k ✓, food_cart 5.0k ✓, bin 3.0k ✓, bus_shelter 8.0k ✓ (only 1–3 per town), but +// novelty_record is **26.5k tris** (5× over) → it stays on the low-poly PRIMITIVE until Lane E +// decimates it (same treatment as the ped rigs — see LANE_B_NOTES). const GLB_FILES = { - bench: 'procity_street_bench_01.glb', - food_cart: 'procity_street_food_cart_01.glb', + bench: 'procity_street_bench_01.glb', + food_cart: 'procity_street_food_cart_01.glb', + bin: 'procity_street_bin_01.glb', + bus_shelter: 'procity_street_bus_shelter_01.glb', }; const _glbReady = {}; // type → { geo, mat } once (and if) the GLB loads and merges to one mesh @@ -161,6 +166,7 @@ export function buildChunkFurniture(chunkData, ctx, cx, cz) { const edges = ctx._edges || (ctx._edges = resolveEdges(ctx.plan)); const lists = { pole: [], lamp: [], bench: [], bin: [], busstop: [], tree: [], novelty: [], foodcart: [] }; + const colliders = []; // oriented-rect keep-outs (bus shelters) → player collision (via chunks.js) const here = (x, z) => { const c = chunkOf(x, z); return c.cx === cx && c.cz === cz; }; const pushYaw = (list, x, y, z, yaw, sx = 1, sy = 1, sz = 1) => { @@ -189,14 +195,21 @@ export function buildChunkFurniture(chunkData, ctx, cx, cz) { } side = -side; } - // benches + bins every ~26m on the building side of the footpath, facing the road + // benches every ~26m on the building side of the footpath, facing the road for (let s = 14; s < len - 6; s += 26) { const bside = (Math.floor(s / 26) % 2) ? 1 : -1; const off = (halfRoad + FOOT - 0.8) * bside; const bx = e.ax + ux * s + px * off, bz = e.az + uz * s + pz * off; if (here(bx, bz)) pushYaw(lists.bench, bx, 0, bz, yaw + (bside > 0 ? Math.PI : 0)); - const nx = bx + ux * 1.4, nz = bz + uz * 1.4; - if (here(nx, nz)) pushYaw(lists.bin, nx, 0, nz, 0); + } + // bins: genuinely sparse (~1 per block), seeded, one side of the footpath — a country town, not a + // mall. Wide cadence also bounds the 3k-tri bin GLB's contribution to the ≤200k tri gate. + const binH = hashEdge(e.id); + for (let s = 30; s < len - 8; s += 78) { + const bside = ((binH + Math.floor(s / 78)) % 2) ? 1 : -1; + const off = (halfRoad + FOOT - 0.6) * bside; + const x = e.ax + ux * s + px * off, z = e.az + uz * s + pz * off; + if (here(x, z)) pushYaw(lists.bin, x, 0, z, 0); } // gum trees only along side/lane verges (main street has no verge room) if (e.kind === 'side' || e.kind === 'lane') { @@ -208,11 +221,18 @@ export function buildChunkFurniture(chunkData, ctx, cx, cz) { } } } - // one bus stop near each main-street edge's near end - if (e.kind === 'main') { - const s = 10, off = (halfRoad + FOOT - 0.9); - const x = e.ax + ux * s + px * off, z = e.az + uz * s + pz * off; - if (here(x, z)) pushYaw(lists.busstop, x, 0, z, yaw + Math.PI); + // Bus shelters — 1–3 per town, ON THE STREET GRAPH (they are the future tram/bus stops). + // Deterministic rule (see busShelterStops() + LANE_B_NOTES): at the MIDPOINT of every MAIN edge + // whose id hashes to 0 mod 3, on the road-side footpath near the kerb, long axis (2.33 m) ALONG + // the edge, open front to the road. A solid collider so the player rounds it (collision-verified). + if (e.kind === 'main' && hashEdge(e.id) % 3 === 0) { + const off = halfRoad + 1.7; // on the footpath, hard by the kerb + const sx = e.ax + ux * (len * 0.5) + px * off, sz = e.az + uz * (len * 0.5) + pz * off; + const syaw = Math.atan2(-uz, ux) + SHELTER_YAW; // long axis along the edge (+ facing tweak) + if (here(sx, sz)) { + pushYaw(lists.busstop, sx, 0, sz, syaw); + colliders.push({ x: sx, z: sz, ry: syaw, hw: SHELTER_W / 2, hd: SHELTER_D / 2, use: 'shelter' }); + } } } @@ -247,8 +267,8 @@ export function buildChunkFurniture(chunkData, ctx, cx, cz) { emit(t.pole, mat.dark, lists.pole); emit(t.lamp, mat.lamp, lists.lamp, false); emit(...glb('bench', t.bench, mat.wood), lists.bench); - emit(t.bin, mat.bin, lists.bin); - emit(t.busstop, mat.shelter, lists.busstop); + emit(...glb('bin', t.bin, mat.bin), lists.bin); + emit(...glb('bus_shelter', t.busstop, mat.shelter), lists.busstop); emit(t.treeGeo, mat.tree, lists.tree, false); emit(...glb('novelty_record', t.novelty, mat.novelty), lists.novelty); emit(...glb('food_cart', t.foodcart, mat.foodcart), lists.foodcart); @@ -261,7 +281,22 @@ export function buildChunkFurniture(chunkData, ctx, cx, cz) { // shared templates/materials intentionally NOT disposed (reused by other chunks) group.clear(); } - return { group, applyNight, dispose }; + return { group, applyNight, dispose, colliders }; } function hashEdge(id) { const s = String(id); let h = 0; for (let i = 0; i < s.length; i++) h = (h * 31 + s.charCodeAt(i)) | 0; return h >>> 0; } // String(): generatePlan uses numeric edge ids + +// The town's bus/tram stops, derived from the SAME deterministic rule buildChunkFurniture places the +// shelters by, so the future vehicle loop can enumerate stops without touching furniture internals. +// Rule: midpoint of every MAIN edge whose id hashes to 0 mod 3, on the road-side footpath (kerb). +// Returns [{ edgeId, x, z, yaw }] in plan order. Zero side effects; pure function of the plan. +export function busShelterStops(plan) { + const stops = []; + for (const e of resolveEdges(plan)) { + if (e.kind !== 'main' || hashEdge(e.id) % 3 !== 0) continue; + const dx = e.bx - e.ax, dz = e.bz - e.az, len = Math.hypot(dx, dz) || 1; + const ux = dx / len, uz = dz / len, px = -uz, pz = ux, off = e.width / 2 + 1.7; + stops.push({ edgeId: e.id, x: e.ax + ux * (len * 0.5) + px * off, z: e.az + uz * (len * 0.5) + pz * off, yaw: Math.atan2(-uz, ux) + SHELTER_YAW }); + } + return stops; +}