validateTownCache now warns when medianShopSpacing(cache) > MAX_MEDIAN_SPACING_M (150 m). Metric + threshold exported from the barrel so no lane re-derives them. WARN, NEVER ERROR: a flagged cache still boots a valid district (toowoomba shipped green through the entire v4.0 pack) — E curates, A flags. selfcheck ALL GREEN 161,372/161,372 (+72); synthetic 0x3fa36874 frozen; NO golden moved (only E's toowoomba re-bbox may move one this round, per the round's law). THE THRESHOLD IS MEASURED, NOT PICKED. The brief offered darwin 27 m / toowoomba 388 m as "plenty of daylight for a line" — but that's 2 points. Measuring all 23 towns shows a near-continuum, and the line is FORCED into (119.3, 254.9]: braddon at 119.3 m is ALIVE by D's own hub test (7 shops within 160 m, identical to darwin's 7), ballarat at 254.9 m is not. 150 m sits in the pack's widest gap (x2.14), with 1.26x margin below and 1.70x above. Flags 2 of 23. TWO TOWNS FLAG, NOT ONE. The brief names only toowoomba (D spot-checked the two 12-shop towns). The measurement adds BALLARAT at 255 m — nobody has looked at it, and it's independently one of the three towns my R22 graded cluster fallback fired on (no venue candidate at the >=3-shops/160 m floor). Two signals agree. Filed for E as a curation call, counted either way. THE BRIEF'S ANCHORS WERE IN THE WRONG SPACE, AND IT MATTERS. D's 27 m is PLAN space (post-lift metres); validateTownCache is CACHE space (raw lat/lon — the only thing that exists at ingest, and the layer where a bad bbox must be caught). Darwin is 68.3 m in cache space, not 27. Interpolating a threshold between D's figures would have silently mixed spaces. I verified the hypothesis before trusting it: computing the metric plan-side reproduces D's numbers exactly (darwin 27.2 vs 27, toowoomba 386.8 vs 388, hubs 7 and 3). Cache space is correct HERE and provably safe: the lift marches shops at a regularised cadence, so it COMPRESSES dense towns (darwin plan/cache 0.40) but not sparse ones (toowoomba 0.98, ballarat 1.01) -- the error lives at the dense end, the warn only fires at the sparse end. Verified all 23 towns in both spaces: exactly ONE disagrees, newtown_godverse (94.4 cache passes / 158.3 plan would warn). Filed for G/Fable, NOT acted on: no cache-space line catches it without false-positiving braddon, and godverse is a census subset (18 thriftgod shops over the footprint that carries 72 OSM shops) so its sparsity is coverage, not a bad bbox — re-bboxing, the warn's only remedy, cannot fix it. Worth knowing since R23 stocks the real crate in that town and the beta asks "fitzroy_godverse if the census is dense there". Also fixed en route: the R17 silent-fallback trap bit again in my own scratch harness — an unregistered town key resolves to the melbourne fixture rather than erroring, so five towns silently measured identical. Registered the caches; noting it because it has now cost two rounds. Coverage: sparse cache warns + still boots (structural + district suites on a flagged town), compact cache does NOT warn (no false positive), metric edge cases (< 2 shops -> null, coincident -> 0, no throw on empty), and an exact-value check (a 100 m pair measures 100 m, projection sane). Cost: ~1 ms/town at registration (25.6 ms for all 23 incl. fitzroy's 160 shops) vs a <100 ms plan budget. E -> use the warn as your acceptance test for #4: re-bbox, re-run, warn goes silent = the town has a high street. If your fix moves toowoomba's or ballarat's cache, ping me — goldens are mine to re-pin, and only what you move. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> |
||
|---|---|---|
| docs | ||
| pipeline | ||
| priorwork | ||
| tools | ||
| web | ||
| .gitignore | ||
| B-progress.md | ||
| C-progress.md | ||
| D-progress.md | ||
| E-progress.md | ||
| F-progress.md | ||
| G-progress.md | ||
| README.md | ||
PROCITY
A standalone, procedurally generated, fully walkable 90s-Australian shopping town — every building enterable (the Vuntra City trick), every interior seeded and themed: record stores, op shops, toy shops, book barns, video rental, pawnbrokers, milk bars, market stalls. Built on the proven house stack (three.js r175 vendored, plain JS, no build step) and the asset DNA of 90sDJsim + thriftgod. v1 is the system; the GODVERSE/BaseGod real-item content plugs in later.
Run
cd web && python3 -m http.server 8130
# http://localhost:8130 (game shell — Lane B)
# http://localhost:8130/map.html (2D plan debugger — Lane A)
Read first
docs/CITY_SPEC.md— the treaty: schema, units, seeds, budgets, file ownership.docs/RESEARCH.md— what the parent repos already solved, with exact paths to port from.
The lanes (parallel Opus 4.8 sessions)
Each lane is a self-contained prompt. To run one, start an Opus session in this repo and say: "Read docs/LANES/LANE_X_….md and execute it." A–E are parallel-safe (disjoint files, standalone test pages); F integrates after they land.
| lane | mission | owns |
|---|---|---|
| A — CITYGEN | seed → CityPlan data (streets, blocks, lots, shops) + 2D map | js/citygen, js/core/registry.js, map.html |
| B — STREETSCAPE | chunk-streamed instanced 3D town + game shell | index.html, js/world |
| C — INTERIORS | every door opens: seeded themed shop interiors | js/interiors, interior_test.html |
| D — CITIZENS | rigged-near / impostor-far NPCs + shopkeepers | js/citizens, citizens_test.html, models/ |
| E — ASSETS | audit ultra's library, normalize GLBs, skins, manifest | pipeline/, assets/ |
| F — INTEGRATION | wire A–E into one seed-to-town game + QA gates | (after A–E) |
Already in the repo
web/vendor/— three.js r175 + addons (vendored from 90sDJsim; no CDN at runtime).web/assets/gen/— 69 generated texture skins (25 shopfront facades, 10 skies, 10 grounds, 8 wallpapers, 16 interior surfaces) inherited from thriftgod + 90sDJsim, style-locked.web/js/core/— frozen shared modules:prng.js(seeded-everything law),loaders.js(fail-soft promise-cached GLB/texture + 3GOD depot),canvas.js(text planes).
House laws (short form — CITY_SPEC has the fine print)
Seeded everything, Math.random banned in gen code · the game must run with zero assets ·
instancing from day one · canvas text, no font files · characters = shared base rigs + one
clip bank, canonical sources live outside this repo · 🟢 web-ok licensing only.
v3.1 — THE FLIP: the town shows its best by default
The Friday-night layer soaked through alpha, beta, and the v3.0 release — so v3.1 flips it on by default, and the mature v2 flags flip with it. Boot the plain URL and each town seeds 2–4 venues (pub, band room, RSL) with a full seeded week of gigs, town-wide posters, and a Friday night that reads: a 4-piece band (trio + a seated drummer) with real instruments, a mixed crowd, an outdoor queue, a per-venue cover charge, muffled gig spill + walla and applause, town-wide streetlamp pools, and identity continuity — the ped you followed in is in the crowd. Plus weather, the tram, and the parallax interior-window shader, all live by default.
The covenant moved, it didn't disappear. ?classic=1 boots the exact v2 town — byte-identical
forever (synthetic golden 0x3fa36874, zero gig layer, zero fetch delta). That is the new frozen
regression target; the default boot is deterministic to the gig golden 0xb1d48ea1. Asset-free
(?noassets=1) composes with the default boot and stays silent-and-fine.
Flags — the four headliners are ON by default, each with a =0 opt-out; ?classic=1 is the
master off:
| flag | default | what |
|---|---|---|
?classic=1 |
off | the exact v2 boot — all four below off, byte-identical to the frozen baseline |
?gigs=0 |
on | the gig district — venues, week schedule, band/crowd/queue, cover, posters, night pass |
?weather=0 · ?tram=0 · ?winmap=0 |
on | seeded weather · the tram loop · the parallax interior-window shader |
?roster=v1 · ?pop=N |
stream | chunk-streamed citizen density is the baseline; ?roster=v1 restores the fixed roster |
?dig=0 · ?stock=real |
dig on | crate-riffle on record bins · real GODVERSE sleeves in the stock |
?noassets=1 · ?plansrc=osm · ?mute=1 · ?dbg=1 |
off | asset-free (zero network) · OSM plan source · silence · the window.DBG capture/soak harness |
?plansrc=osm&town=<key> |
— | boot a real Australian town: fixtures melbourne/katoomba/silverton, or a real OSM cache bendigo_real/castlemaine_real/fremantle_real/katoomba_real/newtown_real (ODbL, web/assets/towns/SOURCES.md). REAL ROADS: a cache with roads[] (schema v2) builds the CityPlan street graph from the town's actual OSM streets; without roads[] a cache marches onto synthetic avenues. Or just pick a town from the HUD selector — no URL needed |
v4.0 — THE REAL MAP: the towns are the product
23 real Australian towns — 1,210 real shops, every state and territory — on their actual streets. Not "inspired by": Fitzroy's shops sit on Fitzroy's roads, at their real positions, because the pipeline eats OpenStreetMap and marches it through the same CityPlan contract the procedural town uses. Pick one from the in-HUD selector and walk it. Everything built across v1–v3 comes along unchanged — the venues and their seeded week of gigs, the 4-piece band with the seated drummer, the crowd that follows you into the pub, the interiors behind every door, weather, the tram — because they all hang off the plan contract and never had to know the geography got real.
The towns run from meccas (Fitzroy, 160 shops) to the thin tail (12), at real retail density: the
secondhand heroes keep their footfall while the street around them gets ~4× busier — texture, never a
takeover. Venues bias to the retail heart. The tram only runs where it fronts a real high street (elsewhere
it was honestly "a highway bus", so it's fenced). Everything stays deterministic from the seed, silent
under ?noassets=1, and ?classic=1 still boots the exact v2 town, byte-identical forever (0x3fa36874).
The tour: docs/shots/release_v4/contact.html — the Fremantle hero, the
Fitzroy mecca, the thin tail (labelled honestly: the floor is shops-per-street-metre, and Toowoomba is
below it), the synthetic control, and the ?classic=1 covenant made visible. Charter:
docs/V4_REAL_MAP.md · data + licences (ODbL):
web/assets/towns/SOURCES.md.
| ?seed=N | 20261990 | town seed |
Release tour: docs/shots/release_v3_1/contact.html — the money shots on
the default boot, plus a ?classic=1 before/after. QA: tools/qa.sh --strict (classic regression +
default-boot gate + smokes + no-giants) --soak (the full-week soak); the seated drummer passes the
no-giants gate at the seated stature band [0.9,1.5] m.