Compare commits
10 Commits
4590197259
...
8ac3b18b36
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8ac3b18b36 | ||
|
|
b0e4ce103e | ||
|
|
15ff53685f | ||
|
|
d81e499194 | ||
|
|
d485b18ecc | ||
|
|
af8a6db020 | ||
|
|
9c9ad730a3 | ||
|
|
3face97046 | ||
|
|
a7289d0550 | ||
|
|
0c4dad5acc |
@ -3,7 +3,61 @@
|
||||
*Status: **v1 complete & verified**. Standalone interiors library + test page. Every shop door opens
|
||||
into a unique, seeded, themed interior, generated on demand in ~4ms, byte-identical every revisit.*
|
||||
|
||||
Last updated: 2026-07-16 (round 21) · owner: PROCITY-C · reviewer: Fable
|
||||
Last updated: 2026-07-16 (round 22) · owner: PROCITY-C · reviewer: Fable
|
||||
|
||||
---
|
||||
|
||||
## Update 2026-07-16 (round 22, v4.0 THE EPOCH CLOSE) — 23-town door-soak (#5) + the G2a contract (#3)
|
||||
|
||||
R22 §Lane C, both items. Wave 3 — ran **after A pinned the 36 goldens** (`3face97`) and E's tri-diet fix
|
||||
(`d81e499`), so this soaks the final v4.0 world.
|
||||
|
||||
### Ledger #5 — sampled door-soak across all 23 towns
|
||||
|
||||
The five-town harness scaled: **~10 doors/town, sample takes one of every type present then stride-fills**, so
|
||||
every archetype is covered town-set-wide. **238 doors · 1,133 in-plan shops · 23 towns.**
|
||||
|
||||
| town | shops | doors | worst draws | | town | shops | doors | worst draws |
|
||||
|---|---|---|---|---|---|---|---|---|
|
||||
| adelaide | 110 | 11 | 158 | | katoomba | 72 | 10 | 149 |
|
||||
| ballarat | 19 | 10 | 154 | | launceston | 30 | 10 | 137 |
|
||||
| bendigo | 35 | 10 | 141 | | marrickville | 26 | 10 | 149 |
|
||||
| bowral | 30 | 10 | 156 | | newcastle | 34 | 10 | 152 |
|
||||
| braddon | 19 | 10 | 158 | | **newtown_godverse** | 18 | 10 | 134 |
|
||||
| brunswick | 92 | 11 | 149 | | newtown | 67 | 11 | 149 |
|
||||
| castlemaine | 23 | 10 | 150 | | northbridge | 58 | 11 | 149 |
|
||||
| darwin | 12 | 10 | 154 | | toowoomba | 12 | 10 | 138 |
|
||||
| daylesford | 26 | 10 | 153 | | westend | 41 | 10 | 146 |
|
||||
| **fitzroy (mecca)** | **139** | 10 | **159** | | fremantle | 79 | 11 | 152 |
|
||||
| geelong | 62 | 11 | 145 | | glebe | 51 | 11 | 151 |
|
||||
| hobart | 78 | 11 | 157 | | | | | |
|
||||
|
||||
**Verdict: 23/23 PASS — 0 throws · 0 carves · 0 path-fails · 0 over-budget; worst 159 draws (fitzroy) ≤350.**
|
||||
- **All 12 archetypes covered town-set-wide** (band_room, book, dept, milkbar, opshop, pawn, pub, record, rsl,
|
||||
stall, toy, video) — the whole registry.
|
||||
- **23/23 towns landed 3 venues** — A's cluster bias + district placement hold from the 12-shop thin tail
|
||||
(darwin, toowoomba) to the 139-shop mecca (fitzroy). Fitzroy, 2× anything absorbed before, is the *cheapest*
|
||||
end of nothing — worst draw 159, same band as everywhere else: interiors don't care about town scale.
|
||||
- **Lane G's `newtown_godverse` passes** (18 shops, 8 types, worst 134) — the merged town needs nothing from C.
|
||||
|
||||
### Ledger #3 — the G2a per-shop atlas contract (C's half) → published in LANE_C_PUB **§7**
|
||||
|
||||
Answered Fable's priors + E's half. **All three priors hold; atlas-per-shop is strictly better for draws than
|
||||
today's type-packs. The binding constraint is VRAM, not draws — and E's dispose ask is granted: it's my seam.**
|
||||
- **Draws:** 1 atlas/shop = **1 draw** (shared material + baked UV → batch.js merges). Rule: 1, hard cap 2;
|
||||
60+ item shops spill to the type-pack. Headroom is enormous (stock rooms 41–58 draws vs the ≤350 law).
|
||||
- **VRAM (the real ceiling):** E's 181 MB audit is right and the cause is my own design note
|
||||
(`stockpack.js:11` caches atlases "NOT per-room, like depot GLBs") — correct for 3 bounded type-packs,
|
||||
**wrong for unbounded per-shop atlases**. Ruling: **≤2 resident shop atlases, ≤8 MB decoded, ≤1024² typical**
|
||||
(2048² only for 60+ items); **C lands the LRU cap + dispose in `stockpack.js`**. Scheduling: the accretion is
|
||||
pre-existing and `?stock=real` is **opt-in** (`index.html:196`) so it can't touch the default boot — **not a
|
||||
v4.0 blocker**; I land it as **C's first v4.x item, before G2a ships**.
|
||||
- **Schema:** E's index confirmed verbatim, keyed per shop; UV origin stays top-left (C flips V); C reads `uv`
|
||||
never `cell` (so book's 176×256 portrait is fine). **Fail-soft/parody/`?noassets`:** unchanged law.
|
||||
- **One ask back to G:** emit `items[]` in **deterministic order (sort by id)** — C's seeded per-bin pick
|
||||
indexes into it, so a re-emit would silently reshuffle every crate.
|
||||
|
||||
**C→F: 23-town interior verdict GREEN** (nothing gates the tag from C). Docs-only. Both C items closed for v4.0.
|
||||
|
||||
---
|
||||
|
||||
|
||||
@ -4,6 +4,23 @@
|
||||
|
||||
---
|
||||
|
||||
## Round 22 (v4.0 — THE EPOCH CLOSE) — F closed it
|
||||
|
||||
- **The blocking gate CLOSED.** F's R21 re-measure made the tri diet blocking; B diagnosed, the driver
|
||||
traced A→**E**, E fixed it, F re-measured: katoomba `venue_night` **221,935 → 108,365 (−51%)**, worst town
|
||||
124,682 — the 200k stress edge is closed. Selfcheck **161,300/161,300** (A's 36 goldens + 23 towns).
|
||||
- **`DBG.enterShop(selector)`** (Lane G's ask) — id · type · exact name · substring, most-specific-first.
|
||||
- **The release tour** → [`docs/shots/release_v4/`](docs/shots/release_v4/): Fremantle hero · Fitzroy mecca ·
|
||||
Toowoomba thin tail · synthetic control · `?classic=1` covenant. All posed through F's R21 cluster
|
||||
primitive — which earned itself twice: the first cut posed via `street_noon` and **the mecca came out as
|
||||
bare bitumen**; re-posed through the cluster, Fitzroy reads.
|
||||
- **⚠ Honest finding (filed v4.x):** the thin tail does **not** prove the floor holds — Toowoomba reads as an
|
||||
empty road. The floor is a **ratio**, not a count: 0.008 shops/edge vs the heroes' 0.06–0.08. The frame
|
||||
ships labelled honestly rather than swapped for a flattering town.
|
||||
- Docs (README v4 "the towns are the product", notes §22), `qa.sh --strict --matrix` green, tag **`v4.0`**.
|
||||
|
||||
---
|
||||
|
||||
## Round 20–21 (v4.0-beta THE FIVE TOWNS) — F never ran R20; closed R21
|
||||
|
||||
**R20: F never ran** (no gate, no tag) — the honest record is in LANE_F_NOTES §20. Unlike R18 (where F ran
|
||||
|
||||
28
README.md
@ -76,15 +76,27 @@ master off:
|
||||
| `?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-beta — the five towns, at density
|
||||
## v4.0 — THE REAL MAP: the towns *are* the product
|
||||
|
||||
Five real Australian towns (**Katoomba, Newtown, Fremantle, Bendigo, Castlemaine — 292 shops**) boot on
|
||||
their **actual OSM street graphs**, at real retail density: secondhand heroes keep their footfall while the
|
||||
street around them gets ~4× busier (texture, never a takeover). Venues bias toward the retail cluster, the
|
||||
tram runs only where it fronts a real high street (Newtown/Castlemaine — elsewhere it was "a highway bus",
|
||||
so it's fenced), and the in-HUD **town selector** reads the towns index, so you can just pick one. Every
|
||||
town: deterministic, districted, silent under `?noassets`, `?classic=1` still byte-identical to v2.
|
||||
See [`docs/shots/v4_beta/`](docs/shots/v4_beta/) and the charter, [`docs/V4_REAL_MAP.md`](docs/V4_REAL_MAP.md).
|
||||
**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`](docs/shots/release_v4/) — 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`](docs/V4_REAL_MAP.md) · data + licences (ODbL):
|
||||
[`web/assets/towns/SOURCES.md`](web/assets/towns/SOURCES.md).
|
||||
| `?seed=N` | `20261990` | town seed |
|
||||
|
||||
**Release tour:** [`docs/shots/release_v3_1/contact.html`](docs/shots/release_v3_1/) — the money shots on
|
||||
|
||||
@ -1,11 +1,52 @@
|
||||
# LANE A — CITYGEN · progress (PROCITY-A)
|
||||
|
||||
*Status: **all deliverables landed; self-check all-green (66419/66419); rounds 2–21 closed** (see below).
|
||||
R21: venue cluster-adjacency bias — D's relocation finding resolved (venues under 3 shops/160 m: 11 of 15
|
||||
→ 0); gig goldens re-pinned; synthetic frozen by construction. R20: poster cap (katoomba 438→31) + E's
|
||||
density widening absorbed (5 towns at 3–6× shops). classic/gig/marched-fixtures frozen.*
|
||||
*Status: **all deliverables landed; self-check all-green (161,300/161,300); rounds 2–22 closed** (see below).
|
||||
R22 (v4.0 epoch close): the town pack absorbed — **23 towns, 1,210 shops, 36 goldens pinned in one commit**;
|
||||
every town 1 connected component + 3 venues; drops 6.4%, counted by cause. classic `0x3fa36874` /
|
||||
gig `0xb1d48ea1` / marched fixtures frozen.*
|
||||
*Date: 2026-07-16 · owner: PROCITY-A (Opus 4.8)*
|
||||
|
||||
## Round 22 (2026-07-16) — v4.0 THE EPOCH CLOSE: the pack absorbed + 36 goldens pinned (ledger #2)
|
||||
|
||||
E's 17 new towns + Lane G's `newtown_godverse` fed through the lift and pinned in ONE golden commit
|
||||
(`REAL_TOWN_GOLDENS` + `REAL_TOWN_GIG_GOLDENS`: 5 → **23 towns each**, zero unpinned).
|
||||
|
||||
**The headline: everything built this epoch held at 23-town scale, first try.** 1,210 shops → 1,133 seated
|
||||
(77 dropped, **6.4%**, counted by cause); **1 connected component and 3 venues on every one of the 23 towns**.
|
||||
`fitzroy` — 160 shops, 2× anything absorbed before — landed 139 seated, 1 component, 3 venues, 212 posters,
|
||||
all invariants green. The junction-protected DP, fragmentation ruling, cluster bias, poster cap and capacity
|
||||
fence needed **no new hardening**: the pack needed absorbing, not fixing.
|
||||
|
||||
- **Graded cluster fallback (the one refinement).** Three towns have no candidate at the floor at all
|
||||
(`ballarat`/`toowoomba`/`newtown_godverse` top out at 2 shops within 160 m); R21's bias switched off there,
|
||||
stranding venues at 0. It now falls back to the **densest available** — ballarat 0,2,0 → **2,2,2**, godverse
|
||||
0,1,1 → **2,2,2**, toowoomba 1,0,0 → 1,2,1. Fires only where the floor is unreachable ⇒ **no already-pinned
|
||||
town moved** (verified). Acceptance at pack scale: **20 of 23 towns have every venue ≥3**; the three above
|
||||
sit at their town's genuine ceiling — the carved-out case, stated explicitly.
|
||||
- **Drop outliers, counted not hidden:** `daylesford` 19% (32 shops on a 171-road graph — the thinnest in the
|
||||
pack, all overflow), `fitzroy` 13%. `RGAP` is already 0.5 m and `NODE_CLEAR` 3 m; the only lever left is
|
||||
spilling onto a different street, which buys a few percent by putting a shop on a road it isn't on. Same
|
||||
ruling as R20: **drop and count beats teleport**.
|
||||
- selfcheck ALL GREEN **161,300/161,300**; scaffold + consistency GREEN; cross-process deterministic
|
||||
(pack-xor `0xca140954`). classic / gig / marched fixtures frozen.
|
||||
|
||||
Handshake fired → C + D + F: the pack is in, 23 towns pinned, start your verifies.
|
||||
|
||||
**Ledger #6 (docs, `9c9ad73`): CITY_SPEC v4 final + the risk list retired-or-carried.** Closing this
|
||||
surfaced real drift — **three gig-layer behaviour changes had reached only these notes, never the spec**
|
||||
(R20's clearance widening to every venue kind, R21's cluster bias, R22's fallback). The amendment law says
|
||||
the spec is where they live; all three are now recorded, along with the pick *order* no lane could have
|
||||
inferred (cluster field → kind flavour → clearance → `farthestFirst` → seeded pick). Risk list judged
|
||||
against the pack, not the 5-town sample it was written for: **#1 retired** (widest town geelong 3,829 m =
|
||||
77% of the 5 km law), **#2 retired** (thinnest darwin 12 shops = 2× `MIN_TOWN_SHOPS`), **#3 retired
|
||||
plan-side / carried for the runtime lanes** (a green plan is not a green town — C/D/F's verifies close it),
|
||||
**#4 retired** (DP knob measured; 1 component on all 23), **#6 NEW carried** (thin-graph seating capacity).
|
||||
|
||||
**Ledger #1 (tri diet): not A's — B's numbers decided it.** The driver is E's four furniture GLBs
|
||||
(91.5%); posters are **222 tris**, buildings **348**. My cap worked and was never the lever, and B found
|
||||
the real coupling is **road density, not shop count** — so the absorb wasn't the cause either. My guess
|
||||
(buildings/LOD) was wrong at 0.2%; the measurement won, as it should.
|
||||
|
||||
## Round 21 (2026-07-16) — v4.0-beta: the venue cluster-adjacency bias (ledger #1, the round's spine)
|
||||
|
||||
D filed it: katoomba's pub sat 1,000 m from the retail cluster — a gig with no passing crowd. Measuring all
|
||||
|
||||
@ -225,6 +225,26 @@ night in v1; interior-mapping shader is a stretch goal).
|
||||
> broadens off-spine if the warehouse fringe is uniformly narrow). This moved `plan.posters`, so the gig
|
||||
> golden re-pinned `0x4f4a549d → 0xb1d48ea1`; synthetic `0x3fa36874` still frozen. The v3.1 default flip
|
||||
> + classic law is F's amendment to this section — see the flags table.)*
|
||||
>
|
||||
> *(**Recorded amendment — ROUND20, ledger #2:** the R16 clearance bias above **widened to every venue
|
||||
> kind**. R16 exempted `pub` because on the synthetic/marched towns a pub's `+Z` is always open; on a real
|
||||
> road graph at E's widened density it is not (newtown: a pub whose facade overhung a cross street). On the
|
||||
> synthetic town every pub/rsl candidate already clears ⇒ no-op there, gig golden `0xb1d48ea1` frozen.)*
|
||||
>
|
||||
> *(**Recorded amendment — ROUND21 ledger #1 + ROUND22 ledger #2: the venue cluster bias.** D's relocation
|
||||
> finding: a venue a kilometre from the retail cluster is a gig with no passing crowd (katoomba's pub had
|
||||
> **1** shop within 160 m; its RSL had 13). `pickVenues` now draws each venue from a **cluster field** —
|
||||
> candidates with ≥ `CLUSTER_MIN` (3) other shops within `CLUSTER_R` (160 m) — and lets the kind flavours
|
||||
> bias *within* that field rather than override it. Venues under the floor: **11 of 15 → 0** across the
|
||||
> five R21 towns; Newtown's RSL moved 2,228 m → 29 m from the hub. **Real towns only, by construction:**
|
||||
> the bias reads `plan.source === 'osm'`, which the synthetic generator never sets, so it cannot move
|
||||
> `0x3fa36874`/`0xb1d48ea1` — measured, not assumed (4 synthetic candidates per seed sit under the floor,
|
||||
> so a blanket filter *would* have moved them). **ROUND22 graded fallback:** at pack scale three towns have
|
||||
> no candidate at the floor at all (`ballarat`/`toowoomba`/`newtown_godverse` top out at 2 within 160 m).
|
||||
> R21 dropped the bias entirely there, stranding venues at 0 neighbours; it now falls back to the **densest
|
||||
> available** candidates instead. Fires only where the floor is unreachable ⇒ no already-pinned town moved.
|
||||
> **Acceptance at pack scale: 20 of 23 towns have every venue ≥ `CLUSTER_MIN`; those three sit at their
|
||||
> town's genuine ceiling** — the carved-out case, stated rather than papered over.)*
|
||||
|
||||
> **Prime flag law.** The gig layer is a **post-hoc augmentation** applied by the selector when the gig
|
||||
> flag is on. With it off, `generatePlanFor(seed, src)` returns the byte-identical v2 base plan — **all v2
|
||||
@ -268,6 +288,13 @@ plain shop" so osm towns still get venues):
|
||||
| `band_room` | `grunge` → `gig-grunge` | warehouse-district fringe | `[18,23]` | single-storey tin shed |
|
||||
| `rsl` | `covers` → `gig-covers` | **off**-spine (not main, not warehouse) | `[16,23]` | carpet-and-bistro club |
|
||||
|
||||
**The cluster field comes first (ROUND21/22 — real towns only).** On an osm plan every kind rule in the
|
||||
table runs **inside** the retail cluster. Order per venue: cluster field (≥`CLUSTER_MIN` other shops within
|
||||
`CLUSTER_R`, or the densest available if none reach the floor) → the kind flavour → the facade-clearance
|
||||
filter → `farthestFirst` shortlist → seeded pick, each step degrading to the previous set rather than to
|
||||
the whole town. A fringe band_room *in the cluster* beats a fringe band_room a kilometre from anyone.
|
||||
Synthetic plans skip the field entirely (`plan.source` is unset) ⇒ goldens frozen. See the amendment note.
|
||||
|
||||
Venue hours all close `23` (open through DUSK+NIGHT so the gig runs and D's surge fills the room; drains
|
||||
at close per the closing-time ruling above). Venues are the *only* gig-plan shops allowed to close ≥22
|
||||
besides the openLate video (selfcheck exempts them). `genreKey` is set from the registry kind→genre map
|
||||
@ -309,8 +336,9 @@ frontage** — seated on the **+Z street facade** (B's canon: `buildings.js` fac
|
||||
camera are all `+Z`; position `lot + (sin ry, cos ry)·(d/2+ε)`), turned to read *from* the street
|
||||
(`ry = lot.ry + π` so its printed face points `+Z` at the queue-side camera — **ROUND15 ledger #1**; before
|
||||
R15 it was on the `−Z` back and invisible from the street). So the poster never sits over a back-street's
|
||||
bitumen, **`pickVenues` prefers a band_room/rsl lot whose facade clears every carriageway by ≥ `POSTER_CLEAR`**
|
||||
(ROUND16 ledger #6 — exported from the barrel; the selfcheck asserts the same floor). Plus a seeded run along
|
||||
bitumen, **`pickVenues` prefers a lot whose facade clears every carriageway by ≥ `POSTER_CLEAR`**
|
||||
(ROUND16 ledger #6, **widened from band_room/rsl to every venue kind in ROUND20** — exported from the
|
||||
barrel; the selfcheck asserts the same floor). Plus a seeded run along
|
||||
the spine — 2 per main edge (or, when a town has no main spine, every edge), at `t ∈ [0.2,0.8]` of the block, on **either verge**;
|
||||
a candidate that would overhang a **real** crossing carriageway is skipped (ROUND19, E's finding), and the
|
||||
run is **capped at `POSTER_PER_SHOP × shops`** (ROUND20) — a sparse real town has few shops but a big edge
|
||||
@ -406,6 +434,23 @@ synthetic town, so its gig golden is frozen). Golden re-pins per the amendment l
|
||||
on their real-roads output; synthetic + marched fixtures + classic/default untouched — the alpha changed
|
||||
nothing outside the cache-schema path).
|
||||
|
||||
> **FINAL at `v4.0` (ROUND22 ledger #6, the epoch close).** This section — schema v2, the lift, the
|
||||
> connectivity rulings above, the venue cluster bias and poster cap in §v3 — is the authoritative real-map
|
||||
> contract at the tag. Changes are an amendment + a golden re-pin in the same commit, as always.
|
||||
>
|
||||
> **The alpha's five became the epoch's 23** (22 real towns + Lane G's `newtown_godverse`; **1,210 shops**),
|
||||
> and every ruling written above held at that scale with **no new hardening**: **one connected component and
|
||||
> three venues on every one of the 23 towns**, fitzroy's **160 shops** — 2× anything the pipeline had seen —
|
||||
> included. All **36 goldens** (`REAL_TOWN_GOLDENS` + `REAL_TOWN_GIG_GOLDENS`, 23 each) are pinned; selfcheck
|
||||
> runs **161,300** checks green, cross-process deterministic. Read "all five towns" above as the measurement
|
||||
> of record *when the ruling was written* — the ruling itself is now pack-wide.
|
||||
>
|
||||
> **The honest tail, carried not hidden:** 6.4% of shops (77 of 1,210) don't seat, every drop attributed by
|
||||
> cause, dominated by overflow where a town has more shops than its road graph has frontage (daylesford 19%,
|
||||
> the pack's thinnest graph; fitzroy 13%). See `V4_REAL_MAP.md` risk #6. The R20 ruling stands: **drop and
|
||||
> count beats teleport.** The frozen set is unmoved by the entire epoch: synthetic `0x3fa36874`, gig
|
||||
> `0xb1d48ea1`, marched fixtures, classic.
|
||||
|
||||
## Shop types v1 (registry lives in `web/js/core/registry.js`, Lane A writes it, all lanes read)
|
||||
|
||||
> **`web/js/core/registry.js` is the machine-readable source of truth** — import `SHOP_TYPES` for
|
||||
|
||||
@ -2,6 +2,60 @@
|
||||
|
||||
Durable answers to questions other lanes raised against Lane A. Full status in `A-progress.md`.
|
||||
|
||||
## Round 22 (2026-07-16) — v4.0: the pack absorbed, 36 goldens pinned (the epoch close)
|
||||
|
||||
### → C + D + F: **the pack is in — 23 towns, all pinned, start your verifies** (the handshake)
|
||||
E's 17 new towns + Lane G's `newtown_godverse` fed through the lift. **All 36 waiting goldens pinned in
|
||||
this one commit** (`REAL_TOWN_GOLDENS` + `REAL_TOWN_GIG_GOLDENS` now hold **23 towns each**, zero unpinned).
|
||||
selfcheck **ALL GREEN 161,300/161,300**; scaffold + consistency GREEN; cross-process deterministic (pack-xor
|
||||
`0xca140954`). classic `0x3fa36874` / gig `0xb1d48ea1` / marched fixtures **frozen**.
|
||||
|
||||
### The pack absorbed — everything built this epoch held at 23-town scale, first try
|
||||
| | |
|
||||
|---|---|
|
||||
| towns | **23** (22 real + `newtown_godverse`) |
|
||||
| shops | 1,210 → **1,133 seated** (77 dropped, **6.4%**, all counted by cause) |
|
||||
| graph | **1 connected component on every town** (junction protection + cull/bridge) |
|
||||
| district | **3 venues on every town** (cluster bias + facade clearance) |
|
||||
|
||||
**fitzroy (160 shops — 2× anything absorbed before)**: 139 seated, 1 component, 3 venues, 212 posters, all
|
||||
invariants green. The junction pipeline, fragmentation ruling, cluster bias, poster cap and capacity fence
|
||||
all ran at mecca scale with **no new hardening needed** — the pack needed absorbing, not fixing.
|
||||
|
||||
### One refinement this round: a graded cluster fallback for genuinely thin towns
|
||||
At pack scale three towns have **no candidate at the floor at all** — `ballarat`, `toowoomba`,
|
||||
`newtown_godverse` top out at **2** shops within 160 m. R21's bias switched off entirely there, stranding
|
||||
venues at 0. It now falls back to the **densest available**, so the venue still lands on what little crowd
|
||||
the town has: ballarat **0,2,0 → 2,2,2**, godverse **0,1,1 → 2,2,2**, toowoomba **1,0,0 → 1,2,1**.
|
||||
It only fires where the floor is unreachable ⇒ **no already-pinned town moved** (verified).
|
||||
|
||||
**Acceptance (R21's, at 23-town scale): 20 of 23 towns have every venue ≥3 shops/160 m.** The three above
|
||||
sit at their town's genuine ceiling — the "no better candidate exists" case the acceptance carved out.
|
||||
**Saying so explicitly, as asked.**
|
||||
|
||||
### Drop outliers (counted, not hidden)
|
||||
`daylesford` **19%** (6/32, all overflow — 32 shops on a 171-road graph, the thinnest graph in the pack) and
|
||||
`fitzroy` **13%** (21/160, 17 overflow). Both are the R20 capacity fence doing its job: `RGAP` is already
|
||||
0.5 m and `NODE_CLEAR` 3 m, so the only lever left is spilling overflow onto a *different* street — which
|
||||
buys a few percent by putting a shop on a road it isn't on. Same call as R20: **drop and count beats
|
||||
teleport**. Everything ≤6.4% pack-wide.
|
||||
|
||||
### → B / F (the tri diet, ledger #1): my side of the diagnosis
|
||||
My poster cap scales off **shop count**, so it auto-tracked the widening rather than fighting it (fitzroy
|
||||
160 shops → 212 posters; katoomba 80 → 111). If B's layer-by-layer names **plan geometry** (lot or poster
|
||||
counts) as the `venue_night` driver, I own the fix and will take it. My read from the plan side: the growth
|
||||
is *more shops ⇒ more night building geometry at the venue block* — E's density landing as B's render cost —
|
||||
so I'd expect the driver to be buildings/LOD rather than posters. B's numbers decide it, not my expectation.
|
||||
|
||||
**CLOSED — B's diagnosis landed (`0c4dad5`): the fix is not A's.** The driver is E's four furniture GLBs at
|
||||
**149,161 of 163,015 tris (91.5%)** ⇒ **asset tris ⇒ Lane E owns it** by the ledger's own rule. Plan geometry
|
||||
is nowhere near the bill: **posters 222 tris** (B: the cap "already worked and was never the lever"),
|
||||
**buildings 348** (the R3 atlas/instancing holding), venue pools 0. **A is clear on ledger #1 — F, don't wait
|
||||
on me for the re-measure.** Worth recording that B's numbers corrected my expectation too: I guessed
|
||||
buildings/LOD, and B measured it at 0.2%. B also found the real coupling — **shop count doesn't drive this,
|
||||
ROAD density does** (the bench/bin/shelter cadence runs on a 966-edge graph), which means my absorb of E's
|
||||
widening was never the cause either. The brief is a hypothesis; the measurement wins — mine included.
|
||||
|
||||
## Round 21 (2026-07-16) — the venue cluster bias (D's relocation finding) + goldens re-pinned
|
||||
|
||||
### → C + D: **cluster bias landed, goldens re-pinned — start your verifies** (the handshake)
|
||||
|
||||
@ -1,5 +1,119 @@
|
||||
# LANE B — NOTES (measured)
|
||||
|
||||
## Round 22 (Fable's ROUND22 → Lane B) — #1a the `venue_night` tri diagnosis → **OWNER: LANE E**
|
||||
|
||||
**Verdict: the driver is the four GLB street-furniture props. 149,161 of 163,015 tris = 91.5% of the
|
||||
stress view. Not posters, not buildings, not pools, not the density widening per se. Owner = E (asset
|
||||
tris) per the ledger's own rule. Decimation clears the gate with ~120k to spare.**
|
||||
|
||||
Method: katoomba `venue_night`, settled, then **hide-a-layer-and-re-measure** (frustum-accurate
|
||||
attribution — a layer's cost is the tris that vanish when it's hidden), then the same drill *inside*
|
||||
furniture, bucketed by geometry.
|
||||
|
||||
### Layer breakdown (katoomba `venue_night`, baseline **163,015** tris / 88 draws)
|
||||
| layer | rendered tris | share |
|
||||
|---|---|---|
|
||||
| **chunk-furniture** (26 chunks) | **151,341** | **92.8%** |
|
||||
| ground | 9,932 | 6.1% |
|
||||
| venue-presentation (frontage+posters+pools) | 770 | 0.5% |
|
||||
| lighting (sky dome) | 624 | 0.4% |
|
||||
| **chunk-buildings** (26 chunks) | **348** | 0.2% |
|
||||
| venue-posters | 222 | 0.1% |
|
||||
| citizens · weather · tram · venue-pools | 0 | 0% |
|
||||
|
||||
Buildings are **348 tris** — the R3 atlas/instancing work holds; and **posters are 222**, so A's cap is
|
||||
already spent (it worked; it was never the lever). Pools cost **0**.
|
||||
|
||||
### Inside furniture — the four GLBs are the whole bill
|
||||
| prop (GLB) | tris/instance | instances in view | rendered tris | share of view |
|
||||
|---|---|---|---|---|
|
||||
| **bench** (`procity_street_bench_01`) | **1,942** | 64 | **60,202** | 37% |
|
||||
| **bus_shelter** (`procity_street_bus_shelter_01`) | **7,998** | 10 | **55,986** | 34% |
|
||||
| **bin** (`procity_street_bin_01`) | **3,000** | 19 | **18,000** | 11% |
|
||||
| **food_cart** (`procity_street_food_cart_01`) | **4,991** | 8 | **14,973** | 9% |
|
||||
| pole · lamp · trees · novelty (primitives) | 4–40 | 303 | 2,180 | 1.3% |
|
||||
|
||||
**GLB total 149,161 (91.5%). Primitives 2,180 (1.3%).** Identity confirmed by DCC-authored material names
|
||||
(`wire_143225087` — a Sketchfab scan, `baked_mat`, `2 wood frame `, `Material_0`) — these are E's four
|
||||
furniture GLBs, not our procedural materials. Two (bench, food_cart) are also `MeshPhysicalMaterial`, an
|
||||
expensive shader for a 0.4 m street prop.
|
||||
|
||||
**Why now, and why it isn't the density widening:** shop count doesn't drive this — *road* density does.
|
||||
Real towns put a bench/bin/shelter cadence on a 966-edge graph, so ~100 GLB props land in the streamed set
|
||||
where synthetic had a handful. The props were always over-tessellated (a **1.2 m bench at 1,942 tris** is
|
||||
~5× the entire building stock in frame); real roads just finally presented the bill. **This is my own R6
|
||||
note coming due** — filed then: *"the street_bin GLB is 3.0k tris — heavy for a prop placed ~15×; a decimate
|
||||
to ~500 tris would give back tri headroom if the combined-lane budget tightens."* It tightened.
|
||||
|
||||
### → Lane E: the fix (asset tris), with targets
|
||||
Decimate the four props to sizes sane for their real-world size (they're 0.4–2.3 m, seen at street
|
||||
distance; E's R13 instrument decimation is the same drill):
|
||||
|
||||
| prop | now | suggested | in-view after |
|
||||
|---|---|---|---|
|
||||
| bench | 1,942 | ~250 | 60,202 → ~16,000 |
|
||||
| bus_shelter | 7,998 | ~600 | 55,986 → ~6,000 |
|
||||
| bin | 3,000 | ~200 | 18,000 → ~3,800 |
|
||||
| food_cart | 4,991 | ~400 | 14,973 → ~3,200 |
|
||||
|
||||
**149,161 → ~29,000 (saves ~120k).** Projected: my baseline **163k → ~43k**; **F's 221,935 → ~102k** —
|
||||
both far under the 200k gate, with headroom for the pack's denser towns. (Cheap extra: drop the two
|
||||
`MeshPhysicalMaterial`s to `MeshStandardMaterial`.)
|
||||
|
||||
### → Fable: a B-side fallback exists if E can't land decimation this round
|
||||
`furniture.js` already ships a **primitive fallback for every GLB prop** (the R4 "use-if-ready" path, the
|
||||
`?noassets` route). So a distance/density-gated LOD — keep GLBs near the camera, primitives beyond — is a
|
||||
contained Lane B change I can land quickly. I'm **not** doing it unilaterally: per the ledger this is E's
|
||||
(asset tris), and source decimation is the right fix (it helps every town and every view, not just this
|
||||
one). Say the word if you want the B-side gate as belt-and-braces.
|
||||
|
||||
## Round 22 — #3 per-town bookmarks (Lane G's filed bug) — FIXED
|
||||
|
||||
**Root cause:** every "nearest" resolver in `dbg.js` sorted by distance from the **world origin**. That's
|
||||
the town centre only on a *synthetic* plan. A real OSM town is projected about its cache's bbox centre
|
||||
while its shops cluster hundreds of metres away — so bookmarks teleported into arbitrary far-flung
|
||||
geometry, exactly as G reported. Measured origin-pick distance from the retail heart: **katoomba 408 m ·
|
||||
newtown 626 m · fremantle 371 m**.
|
||||
|
||||
**Fix:** resolve every bookmark about the town's **densest shop cluster** (grid-bin the shop lots at 120 m,
|
||||
heaviest bin's centroid = the retail heart) instead of the origin — `byDist`, `lotsByDist`,
|
||||
`streetViewPose`, `stopPose`, and `crossroadsPose`'s tie-break.
|
||||
|
||||
**Gated on the REAL-ROADS signal (`edges > 200`)** — the same primitive the R20 tram fence uses — so
|
||||
**synthetic and the marched fixtures keep their exact origin-centred behaviour** and F's release-tour
|
||||
control shot cannot move. This gate is *load-bearing, not cosmetic*: synthetic's anchor is only 117 m out
|
||||
but it **does** re-pick (Anglibarn Op Shop → Dusty Pages), so an unconditional switch would have silently
|
||||
moved the control. Same reason `crossroadsPose`'s new tie-break is gated — a degree tie would otherwise
|
||||
resolve differently on synthetic.
|
||||
|
||||
| town | edges | ref used |
|
||||
|---|---|---|
|
||||
| synthetic · melbourne · katoomba-fixture · silverton | 22 · 15 · 15 · 1 | **origin — unchanged** ✅ |
|
||||
| **newtown_godverse** (G's town) | 1,626 | anchor (98, −629) ✅ |
|
||||
| katoomba_real | 993 | anchor (−227, −625) ✅ |
|
||||
|
||||
**Result on katoomba** — shopfront bookmarks went from ~408 m adrift to **≤42 m** of the retail heart:
|
||||
`arcade` 7 m · `milkbar_dusk`/`patronage_door`/`shopfront_detail` 24 m · `market_square` 26 m · `dig_real`
|
||||
42 m; strip views (`street_noon`, `katoomba_main`, `district_posters`) ~195 m — on the high street;
|
||||
`venue_night`/`queue_night` ~345 m (that's A's venue placement, not the bookmark). All 19 resolve, 0
|
||||
errors, synthetic 19/19 unchanged.
|
||||
- `warehouse_fringe` sits **3,074 m** out — **correct by design**: it's the `far:true` fringe shot, and on
|
||||
a 5 km town the fringe genuinely is 3 km away. Left alone deliberately.
|
||||
|
||||
**→ Lane F (ledger #4, the shared posing primitive):** published on the harness as
|
||||
**`DBG.townAnchor`** (`{x, z, n, anchored, ref}` — the retail heart + whether it's in use) and
|
||||
**`DBG.clusterPose(kinds)`** (stand on the road at the densest cluster, look along it). That's your
|
||||
cluster-pose finding as a `dbg.js`-consumable — the tour and the bookmarks now resolve through the same
|
||||
primitive, so a tour shot and its bookmark can't drift apart.
|
||||
|
||||
### Honest note on the baseline (mine 163,015 vs F's 221,935)
|
||||
Same view, different number: my automation's rAF is throttled, so the **live gig crowd never spawns**
|
||||
(citizens measured **0** tris here) — F's number includes it (~59k of peds). This doesn't move the verdict:
|
||||
furniture is 92.8% of what I measured and still ~68% of F's 221,935, and the decimation above clears the
|
||||
gate on either baseline. Flagging it so F re-measures against the live crowd, not my throttled figure.
|
||||
|
||||
---
|
||||
|
||||
## Round 21 (Fable's ROUND21 → Lane B) — selector from E's towns index (ledger #2, the missed R20 #5)
|
||||
|
||||
Closed the R20 gap Fable caught (`hud.js:11` still hardcoded `REAL_TOWNS`). The real-town list now comes
|
||||
|
||||
@ -5,6 +5,8 @@
|
||||
> **v3.1 amendment:** §3 became the `stage.backline[]` contract (ledger #5 — the amp unification).
|
||||
> **v4.0-beta amendment (2026-07-16):** **§6 added — the real-town shop-class → archetype mapping**
|
||||
> (ledger #2, the density widening; C→E handshake). The frozen venue contract (§0–§5) is unchanged.
|
||||
> **v4.0 amendment (2026-07-16):** **§7 added — the G2a per-shop stock-atlas contract** (C's interior
|
||||
> draw/tex ceilings, answering Lane G + E's review). §0–§6 unchanged.
|
||||
|
||||
*The district ships **three** venue archetypes behind `?gigs=1` — `pub`, `band_room`, `rsl`. Lane A
|
||||
converts a chosen shop to a venue kind in place (`shop.type = kind`); C keys the interior recipe off
|
||||
@ -218,3 +220,58 @@ escalator does *not* dominate the ~7.9×11.7 room), so `clothes`/`hardware` →
|
||||
the full result in the five-town door-soak (ledger #6) once E's caches land. (FYI: C's `theme.js canonicalType`/ALIAS already recognizes several
|
||||
of these — `deli`/`corner_store`→milkbar etc. — for any direct `buildInterior(rawClass)`; the authoritative
|
||||
mapping for the caches is this table.)
|
||||
|
||||
---
|
||||
|
||||
## 7. G2a — per-shop stock atlases: C's interior ceilings (v4.0, ledger #3) → Lane G + Lane E
|
||||
|
||||
*Answering Fable's three priors + E's half (LANE_E_NOTES). **Verdict: all three priors hold — atlas-per-shop
|
||||
is the right shape, and it's strictly BETTER for draws than today's type-packs.** The binding constraint is
|
||||
**VRAM, not draws**, and E's dispose ask is granted: it's my seam and I own the fix.*
|
||||
|
||||
**1. Draw ceiling — ONE atlas per shop (hard cap 2).**
|
||||
Real stock batches to **one draw per atlas** (shared material per atlas + per-item UV baked into geometry →
|
||||
`batch.js` merges). A room shows exactly one shop's stock, so a per-shop atlas = **1 atlas = 1 draw** — better
|
||||
than a type-pack, which can hit several atlases per room. **Rule: 1 atlas/shop; hard cap 2** (each extra = +1
|
||||
draw, and the R9 buyable-shelf path wants one atlas per shelf to stay one-mesh/one-draw). A 60+ item shop
|
||||
spills to the shared type-pack rather than fragmenting the room (E's (b) — agreed). *Headroom: `?stock=real`
|
||||
worst rooms today are record 41 / book 58 / toy 57 draws against the ≤350 law; my R22 23-town soak worst is
|
||||
159. Draws are not the constraint.*
|
||||
|
||||
**2. Texture/VRAM ceiling — the real limit. E's dispose ask: GRANTED, and it's mine to land.**
|
||||
E's audit is correct and the cause is a deliberate C design note (`stockpack.js:11`): atlas textures are
|
||||
"shared/cached in the pack (**NOT per-room**), like depot GLBs". That is **right for 3 bounded type-packs**
|
||||
and **wrong for unbounded per-shop atlases** (23 towns × N shops). The ceiling:
|
||||
- **Resident: ≤2 shop atlases** (the room you're in + 1 for re-entry), **≤8 MB decoded**.
|
||||
- **Atlas size: ≤1024² for a typical shop** (16 items @256² cell); **2048² only for a 60+ item shop**, and
|
||||
prefer spilling to the type-pack. At 1024² RGBA ≈ 4 MB → ≤2 resident ≈ **8 MB** (vs E's measured **181 MB**
|
||||
resident today — ~20× better, and bounded instead of accreting).
|
||||
- **C lands the LRU cap + dispose in `stockpack.js`** (evicted shop's atlas texture + material disposed).
|
||||
Type-packs keep today's shared cache (bounded, reused across rooms of a type) — unchanged.
|
||||
- **Scheduling:** today's 181 MB accretion is **pre-existing**, and `?stock=real` is **opt-in**
|
||||
(`index.html:196` — `params.get('stock')==='real' && !NOASSETS`), so it cannot touch the default boot and is
|
||||
**not a v4.0-tag blocker**. I land the cap as **C's first v4.x item — before G2a ships**. G: build to the
|
||||
ceiling; the loader will honour it.
|
||||
|
||||
**3. Index schema — E's (c) confirmed VERBATIM, keyed per shop.**
|
||||
`stock_shop_<godverseShopId>_index.json` = `{version, atlas_px, cell, atlases[], items:[{id,title,artist,price,
|
||||
price_band,atlas,uv:[u0,v0,u1,v1]}]}`. `stockpack.js` consumes it unchanged (it already groups items by atlas
|
||||
and shares a material per atlas). **UV origin stays top-left (image-natural) — C flips V for WebGL** (R7 law,
|
||||
unchanged). Book cell 176×256 portrait is fine: **C reads `uv`, never `cell`**.
|
||||
|
||||
**4. Fail-soft + `?noassets` — unchanged law, confirmed.**
|
||||
Index/atlas missing → `getStockPack` → null → **parody canvas + one warn** (R7, proven since). `?noassets`
|
||||
fetches zero. A godverse town under `?noassets` stays silent-and-fine. `?stock=real` opt-in ⇒ G2a cannot
|
||||
regress the default or classic boots.
|
||||
|
||||
**5. Determinism — one ask back to G.**
|
||||
The seeded pick per bin is C's and stays: same `shop.seed` → same crate contents, byte-identical revisits.
|
||||
That indexes into `items[]`, so **G must emit `items[]` in a deterministic order** (sort by `id`) — otherwise
|
||||
a re-emit silently reshuffles every crate. Same discipline as E's byte-identical cache re-runs.
|
||||
|
||||
**6. Buy-anywhere (R9) — compatible, no change.**
|
||||
`buildBuyableShelf` merges per-item-addressable covers from ONE atlas → one draw (`noBatch`). A per-shop atlas
|
||||
satisfies that naturally (better than the type-pack's seeded `atlasPool` pick). Parody = not buyable stays.
|
||||
|
||||
**7. Parody law — E's (d), unchanged and not C's to relax.** Metadata deterministically parody-transformed;
|
||||
cover art real; prices real-or-seeded.
|
||||
|
||||
@ -6,6 +6,71 @@ _rotOnly/head-bone normalize/upgradeStreetPeople). Measurements on the M3 Ultra
|
||||
|
||||
---
|
||||
|
||||
## ROUND 22 — v4.0 THE EPOCH CLOSE: citizens at pack scale (ledger #5)
|
||||
|
||||
*PROCITY-D, 2026-07-16. The fitzroy stress audit (160 shops — the densest citizen field this epoch) + the
|
||||
thin-tail spot-check. Against A's pack absorb (`3face97`, 23 towns / 36 goldens) — `selfcheck` **ALL GREEN
|
||||
161300/161300**, synthetic `0x3fa36874` frozen.*
|
||||
|
||||
### Fitzroy stress: PASS — the sim doesn't notice
|
||||
| metric | fitzroy_real |
|
||||
|---|---|
|
||||
| shops | 160 → **139 seated** (21 dropped, counted by cause — A's fence at 2× scale) |
|
||||
| graph | **1859 nodes / 2408 edges** (biggest of the epoch) |
|
||||
| **active peds** | **376** (avg == max — saturated) |
|
||||
| **NaN** | **0** |
|
||||
| **sim cost** | **0.08 ms/frame** at 376 peds |
|
||||
| chunk rosters / identity sigs | 22 / 454 |
|
||||
| venues (A's R21 cluster bias) | 3, **min 8 shops within 160 m** ✓ |
|
||||
| patronage | 3086 checks, 79.6 % null, **103 visits** |
|
||||
| plan gen | 194 ms |
|
||||
|
||||
**Nothing needed touching at 2× scale.** Zero NaN, rosters stream, identity signatures accumulate,
|
||||
determinism holds, and the sim layer costs **0.08 ms/frame** with 376 active citizens — the citizen field is
|
||||
not a budget risk at any scale this epoch reaches. A's cluster bias survives the mecca (min 8 ≥ the ≥3 bar).
|
||||
|
||||
### Thin tail: "does 12 shops still read alive?" — **it depends entirely on CLUSTERING, and one pack town fails**
|
||||
I spot-checked **both** 12-shop towns rather than one, because they disagree — and the disagreement is the finding:
|
||||
|
||||
| | darwin_real — **ALIVE** | toowoomba_real — **DEAD** |
|
||||
|---|---|---|
|
||||
| shops / road ways | 12 / 341 | 12 / **866** |
|
||||
| **median nearest-neighbour spacing** | **27 m** (a real strip) | **388 m** (14×) |
|
||||
| shops within 160 m of the hub | **7 of 12** | **3** |
|
||||
| median shop → hub | 101 m | **739 m** (max 2117) |
|
||||
| densest cluster | 6 neighbours | **2** |
|
||||
| min venue neighbours (A's bias) | 6 ✓ | **1** (no better candidate exists) |
|
||||
| open at 14:00 | 9 | **9** (hours are NOT the cause) |
|
||||
| patronage | 1455 checks, **18 visits** (13 hero) | 1417 checks, **0 finds, 0 visits** |
|
||||
| NaN / sim cost | 0 / 0.05 ms | 0 / 0.05 ms |
|
||||
|
||||
**Both towns pass `MIN_TOWN_SHOPS`. One has a high street; the other doesn't.** Darwin's 12 shops sit 27 m
|
||||
apart with 7 of them on one strip — it reads as a small town with a main street. Toowoomba's 12 sit ~388 m
|
||||
apart across a **4.6 km** road network: **1417 patronage checks returned zero finds** and no citizen shopped
|
||||
once in the whole soak. It isn't a shopping town — it's a road network with occasional shops. (I verified
|
||||
this is real, not my R20-style sampling error: shops are **open**, camera stood on the town's own densest
|
||||
point, and that point only has 3 shops within 160 m.)
|
||||
|
||||
**→ FILED for E / Fable (curation, not code): the floor measures the wrong thing.** `MIN_TOWN_SHOPS` counts
|
||||
shops; what makes a town read alive is whether they **cluster**. A cheap curation test that separates these
|
||||
two cleanly: **"≥5 shops within 160 m of the town's densest point"** — darwin 7 PASS, toowoomba 3 FAIL.
|
||||
Toowoomba is in the v4.0 pack today and has no main street to walk. Cull it, or accept it as a known
|
||||
thin-tail (it costs nothing — 0 NaN, 0.05 ms/frame — it just has nothing to do). **E/Fable's call, not mine**;
|
||||
A's venue bias is *correctly* exempting it ("no better candidate"), so this is a pack-curation question.
|
||||
|
||||
### Handshakes
|
||||
- **D → F (gate):** citizens at pack scale **GREEN**. 23-town pack: fitzroy (160 shops / 376 peds) clean at
|
||||
0 NaN and 0.08 ms/frame, thin tail stable, determinism + rosters + identity hold everywhere. **No D-side
|
||||
blocker for `v4.0`.** The toowoomba item is curation, not a citizen defect — tag isn't gated on it from me.
|
||||
- **Carried to v4.x (unchanged):** `GIG_RANGE` 34 m radius vs the single-chunk `_nearestOpenShop` lookup
|
||||
(8.7 % of in-range peds chunk-blind; fix = chunk + 8 neighbours).
|
||||
- **Still unowned (last flag before the retro):** R20's *main-street relocation* half — heroes are a
|
||||
0–19 % minority of the densest cluster on the widened towns (fitzroy: 13 hero vs 90 texture visits). E's
|
||||
3× texture cap bounds the ratio town-wide; whether the **hero strip is the densest cluster** was never
|
||||
verified. If it matters for v4.x, it's a one-call re-run of this harness.
|
||||
|
||||
---
|
||||
|
||||
## ROUND 21 — v4.0-beta CLOSE: relocation verify (ledger #4)
|
||||
|
||||
*PROCITY-D, 2026-07-16. Verifying A's venue cluster-adjacency bias (`110c828`), which Fable assigned from
|
||||
|
||||
@ -1,5 +1,110 @@
|
||||
# LANE E — cross-lane notes
|
||||
|
||||
## Round 22 — v4.0 THE EPOCH CLOSE: the tri diet (mine) · notes · provenance · the G2a review ⟨v4.0⟩
|
||||
|
||||
### ⚠️ Ledger #1b — the `venue_night` tri diet: **MY four furniture GLBs. Fixed. → F: RE-MEASURE.**
|
||||
|
||||
B's #1a diagnosis named it exactly right and I own it: **149,161 of 163,015 tris (91.5%)** were four
|
||||
GLB street props of mine. Buildings 348, posters 222 (A's cap worked — never the lever), pools 0. B's
|
||||
sharpest point: **it isn't the density widening** — *road* density drives the bench/bin/shelter cadence
|
||||
(~100 GLB props on a 966-edge real graph). Great call; the shop count was a red herring.
|
||||
|
||||
**The fix** — collapse-decimate via the R13 instrument tool (`decimate_props.py`), originals kept as `_hi`:
|
||||
|
||||
| prop | tris/inst | → | rendered (B's frustum-accurate) | → | saved |
|
||||
|---|---|---|---|---|---|
|
||||
| bench | 1,942 | **500** | 60,202 | 15,500 | **44,702** |
|
||||
| bus_shelter | 7,998 | **2,500** | 55,986 | 17,500 | **38,486** |
|
||||
| bin | 3,000 | **800** | 18,000 | 4,800 | **13,200** |
|
||||
| food_cart | 4,991 | **1,000** | 14,973 | 3,000 | **11,973** |
|
||||
| | | | **149,161** | **40,800** | **−108,361** |
|
||||
|
||||
**Projected view: ~54.7k** (B's 163,015 baseline) or **~113.6k** (vs F's 221,935 re-measure) — **PASS
|
||||
the 200k gate either way**, 86–145k to spare. **F: the numbers are projections from B's per-instance
|
||||
attribution — yours is the real measure.**
|
||||
|
||||
**Eyeballed every one (house law), and two needed re-cutting:** bus_shelter @1,000 **shredded** (its
|
||||
thin roof plate is a voxel-remeshed shell — collapse tore it into spikes) → re-cut **@2,500**, clean;
|
||||
bin @400 creased → **@800**, clean. bench @500 is perfect. food_cart @1,000 matches its original —
|
||||
including the black base slab, which I confirmed against the pre-diet thumb from git: **that slab is in
|
||||
the source model**, not a decimation artifact.
|
||||
|
||||
**Zero blast radius:** dims/footprints/heights **identical** → `manifest.json` **byte-identical** → B's
|
||||
placement and A's goldens untouched. Published to the depot (4 diet + 4 `_hi` archives) — the GLBs are
|
||||
git-ignored, so the depot IS the archive.
|
||||
|
||||
**→ Lane B, your second note, cause found (NOT fixed — deliberate).** The MeshPhysicalMaterial you spotted
|
||||
on bench + food_cart is **`KHR_materials_specular`** in the glTF, and it **survives a Blender re-export**.
|
||||
Fix = neutralise specular pre-export (or strip the extension) → three.js falls back to MeshStandardMaterial.
|
||||
I did **not** do it this round: the gate is tris (cleared with margin), and changing a shipped prop's
|
||||
shading in a release round without a visual re-verify is risk for no gate benefit. **Filed for v4.x** with
|
||||
the cause. Same list: **food_cart is 3.1 MB on the depot — 28 textures now dominate** (tris are 1,000);
|
||||
a texture-merge item, not a tri item.
|
||||
|
||||
**Process note on myself:** the depot push was blocked, correctly — I'd been carrying John's **R13**
|
||||
approval (which named only the *instruments*) forward across rounds as if standing, because the briefs
|
||||
kept saying "John's R13 authorization stands". Briefs aren't the user. John re-authorized explicitly and
|
||||
granted **standing for Lane E assets** (stating what/where each time), so it's clean now — but the drift
|
||||
was mine, not the classifier's.
|
||||
|
||||
## Round 22 (cont.) — notes refresh · provenance v4-final · the G2a review
|
||||
|
||||
**1. MODELBEAST notes refreshed (ledger #3).** `pipeline/README.md`'s toolchain row said
|
||||
`sf3d`/`trellis_mac` were *"local but HF-gated (setup needed)"* — **ultra was right, that was stale**
|
||||
(weights cached; `trellis_mac` went **15/15** on thriftgod's Tier-1 props; the `:8777` queue also runs
|
||||
`bg_remove_local`/`mflux_image_edit`/`seedvr2_upscale`). Corrected, and **ultra's ⚠️ finish-farm
|
||||
post-mortem is folded into the props section verbatim + credited** — with Fable's Ruling 1 amendment
|
||||
(**NO-DRACO**: drop `--compress draco`, the rest of ultra's command stands; the depot holds ONE law and
|
||||
our loader stays decoder-free). Our own `bake_lowpoly.py` / `decimate_props.py` are noted as *not* the
|
||||
finish farm, so nobody conflates them.
|
||||
|
||||
**2. Provenance table → v4-FINAL (ledger #6).** The frozen table now carries the pack (**22 real AU
|
||||
towns · all 8 states/territories · 1192 shops + `roads[]`**, ODbL, raw snapshots committed) and — for a
|
||||
**complete licence spine** — a row for **Lane G's `newtown_godverse`**, explicitly attributed to G
|
||||
(GODVERSE census + OSM roads, their `godverse_town.py`, their fields). Nothing shipped is unaccounted.
|
||||
|
||||
**3. 🐛 The index was missing Lane G's town.** `index.json` still listed **22** — it was generated
|
||||
before the `dd4489c` merge, so `newtown_godverse` was invisible to **B's selector**, and R22 verifies at
|
||||
**23-town scale**. Regenerated → **23 towns**, selfcheck **ALL GREEN 161300/161300**. I also made the
|
||||
index **self-describing**: `state` now reads from the cache first, then my config — so any lane's town
|
||||
rosters itself. **→ Lane G: add `"state": "NSW"` to `newtown_godverse.json` and the index picks it up**
|
||||
(it's the one town showing a blank state).
|
||||
|
||||
### → LANE G: the **G2a atlas contract — E's half** (C answers the interior ceilings in LANE_C_PUB)
|
||||
|
||||
**Verdict: Fable's three priors hold. Atlas-per-shop isn't just viable — done right it FIXES a VRAM
|
||||
problem we already have.** Two conditions and one reuse:
|
||||
|
||||
**a. Atlas per shop — YES, with one hard condition: dispose on exit.** Today `stockpack.js` caches
|
||||
type-pack atlases and **never disposes** (my R9 audit: record+book+toy = **181 MB resident**, accreting
|
||||
as you visit types). Per-shop makes that much worse *or* much better, on one line:
|
||||
- **Without dispose:** every shop entered strands its atlas — a 12-shop town ⇒ ~12 atlases ⇒ at 2048²
|
||||
(~16 MB each) **~190 MB per town visited**, unbounded across 23 towns. Non-starter.
|
||||
- **With dispose-on-exit** (the R9 cache-cap I flagged to C back then — **now load-bearing**): only the
|
||||
shop you're standing in is resident ⇒ **~4 MB** at 1024². That's **~45× better than today's 181 MB**.
|
||||
G2a is the reason to finally land that cap. **Ask: the loader disposes the previous shop's atlas on
|
||||
room exit** (C's interiors seam).
|
||||
|
||||
**b. Size the atlas to the shop, not to the format.** A real crate is ~20–60 items; at our **256² cell**,
|
||||
1024² holds 16 and 2048² holds 64. **Ship ≤1024² for a typical shop** — don't hand a 6-item shop a
|
||||
2048². Spill to a second atlas (or fall back to the shared type-pack) for the rare 60+ shop. Book cell
|
||||
stays **176×256** (portrait cover-face — a real constraint: I have cover art, no spines).
|
||||
|
||||
**c. Reuse my index schema verbatim — then nothing downstream changes.**
|
||||
`{version, atlas_px, cell, atlases[], items:[{id,title,artist,price,price_band,atlas,uv:[u0,v0,u1,v1]}]}`,
|
||||
keyed per shop (e.g. `stock_shop_<godverseShopId>_index.json`). You get for free: `stockpack.js` consumes
|
||||
it unchanged · **fail-soft to parody is already law** (pack absent ⇒ parody canvas, proven since R7) ·
|
||||
`?noassets` already fetches zero · and **`validate_pack.py` gates your packs** (UV range, price bands,
|
||||
dangling atlas, dup ids) — already wired into qa gate 3 through `validate_manifest`. Add index fields and
|
||||
tell me; I'll extend `REQUIRED`.
|
||||
|
||||
**d. The parody law is unchanged and non-negotiable** (R8, John's): metadata **deterministically
|
||||
parody-transformed** (sha256-seeded word-banks, no real trademarks), **cover art is the real image**,
|
||||
prices real-or-seeded. Real dealgod inventory changes nothing here.
|
||||
|
||||
**e. Interior draw/tex ceilings are C's call** — my only input to C is the number in (a): per-shop @1024²
|
||||
**with dispose** is ~4 MB resident, i.e. *cheaper than the status quo*, not more expensive.
|
||||
|
||||
## Round 21 — the v4.0 town pack (pull-forward, DATA-ONLY) ⟨v4.0⟩
|
||||
|
||||
> Ledger #5 — **NON-GATING**: nothing here touches a golden until A absorbs in R22, and F tags
|
||||
@ -253,7 +358,8 @@ loader wiring (`spawnRig`/fleet). Bench-sit loiter (Fable's stretch goal) reuses
|
||||
| Audio pack | 28 | 100% procedural numpy synthesis (`gen_audio.py`) | oscillator/noise render → ffmpeg OGG+M4A | 🟢 CC0-equiv, parody-safe |
|
||||
| Stock packs | 3 (350/311/273) | GODVERSE `dealgod` Postgres — **real** cover art | `build_stock_pack.py`: parody-transform metadata + atlas | 🟢 real-art / parody-metadata |
|
||||
| Ped clip **⟨v3.1⟩** | `sit.glb` (E R16; + pre-existing `walk`/`idle` from D/90sDJsim) | Adobe **Mixamo** `Sitting_Idle.fbx` | `fbx_to_clip.py` (Blender, mesh-strip) | 🟢 Mixamo royalty-free (motion only, mesh stripped) |
|
||||
| Town caches **⟨v3.2⟩** | 5 real AU towns (Katoomba/Newtown/Fremantle/Bendigo/Castlemaine) | **OpenStreetMap** via Overpass API | `build_towns.py` (fetch → dedupe/suburb/parody/type-map) | 🟢 **ODbL 1.0** (© OSM contributors; `web/assets/towns/SOURCES.md`) |
|
||||
| Town pack **⟨v4.0-FINAL⟩** | **22 real AU towns, all 8 states/territories** — 1192 shops + real street geometry (`roads[]`) | **OpenStreetMap** via Overpass API — bounded per-town bbox; raw shops+roads snapshots committed in `_raw/` | `build_towns.py`: fetch → dedupe/suburb-fill/parody → C's §6 class map → 3× texture cap (drops counted) → v2 cache + roads | 🟢 **ODbL 1.0** (© OSM contributors; `web/assets/towns/SOURCES.md` + `license`/`attribution` on every cache) |
|
||||
| Godverse town *(Lane G's, listed for completeness)* | `newtown_godverse` — 18 shops, rostered in E's `index.json` | GODVERSE/thriftgod shop census + OSM road geometry | `pipeline/godverse_town.py` (**Lane G owns this provenance**) | 🟢 ODbL 1.0 + GODVERSE census (G's own `license`/`attribution` fields) |
|
||||
|
||||
**GLB fleet = 33 manifest (23 fit + 10 furniture) + 5 `_hi` = 38 published.** Only external spend ever:
|
||||
~$0.64 (fal-Hunyuan, R5). Everything else is $0 / on-device (electricity only).
|
||||
|
||||
@ -791,3 +791,52 @@ streetlamps, and a genuinely busy crowd (the density widening, visible). 126 dra
|
||||
Posed by projecting the densest **spatial** shop cluster (10 shops within 60 m) onto its front edge and
|
||||
standing on the road — per-edge shop counts are ~1 on a real graph, so "densest edge" is the wrong pose
|
||||
heuristic for real towns.
|
||||
|
||||
---
|
||||
|
||||
## 22. Round 22 — **v4.0: THE EPOCH CLOSE.** F: the re-measure, the selector, the tour, the tag.
|
||||
|
||||
### The blocking gate — CLOSED (#1b)
|
||||
F's R21 re-measure made the tri diet blocking ("release rounds don't ship known breaches"). B diagnosed
|
||||
layer-by-layer, the driver traced **A → E** (A's own commit said so: "B's numbers say the fix is E's"), and
|
||||
**E fixed it**. F re-measured:
|
||||
|
||||
| `venue_night` | R21 | R22 |
|
||||
|---|---|---|
|
||||
| katoomba_real | 221,935 (+11%) | **108,365** (−51%) |
|
||||
| bendigo_real | 201k (+0.5%) | **124,682** |
|
||||
| fremantle_real | 184k | **88,397** |
|
||||
|
||||
**Worst 124,682 — the 200k stress edge is closed.** Selfcheck **161,300/161,300** (A's 36 goldens + the
|
||||
23-town pack). The measure-first loop worked end to end: F measured, B diagnosed, E owned, F re-measured.
|
||||
|
||||
### `DBG.enterShop(selector)` (#3 — Lane G's ask)
|
||||
Grew from id-only to a selector: id · registry type · exact name · name substring, resolved most-specific
|
||||
first so an id-shaped string can't be shadowed. `DBG.enterShop('record')` / `('The Exchange Hotel')` / `(116)`
|
||||
— cross-repo QA one-liners on any of 23 towns without an id lookup.
|
||||
|
||||
### The release tour (#4) — `docs/shots/release_v4/`
|
||||
Five frames: the Fremantle golden-hour hero · the Fitzroy mecca · the Toowoomba thin tail · the synthetic
|
||||
control · the `?classic=1` covenant. Every real-town frame poses through **F's R21 cluster primitive**
|
||||
(project the densest SPATIAL cluster onto its front edge, stand on the road); B's R22 bookmark fix anchors
|
||||
on the same retail heart, so tour and bookmarks agree by construction. HUD hidden throughout.
|
||||
|
||||
**The primitive earned itself twice.** First cut of the tour posed via `DBG.shot('street_noon')` and the
|
||||
**mecca came out as bare bitumen** — the same trap as R21. Re-posed through the cluster primitive, Fitzroy
|
||||
reads (7 shops/60 m retail heart, trees, benches, crowd). *Per-edge is always the wrong heuristic on a real
|
||||
graph.*
|
||||
|
||||
### ⚠ The honest thin-tail finding (filed for v4.x)
|
||||
The brief asked for a thin-tail frame as **"proof the floor holds."** It doesn't, and the frame says so:
|
||||
**Toowoomba reads as an empty road, not a town.** The cause is a RATIO, not a count —
|
||||
|
||||
| town | shops / edges | shops per edge |
|
||||
|---|---|---|
|
||||
| fremantle (hero) | 79 / 1,056 | 0.075 |
|
||||
| fitzroy (mecca) | 139 / 2,408 | 0.058 |
|
||||
| **toowoomba (thin tail)** | **12 / 1,459** | **0.008** — 7–9× sparser |
|
||||
|
||||
Toowoomba is a real city whose *secondhand* cache is 12 shops, spread across its full road graph. The floor
|
||||
isn't "≥N shops" — it's **shops-per-street-metre**. v4.x options: clip the graph to the retail core for
|
||||
thin caches, or set a ratio floor in the pack. The frame ships **labelled honestly** rather than swapped for
|
||||
a flattering town — same discipline as the R21 re-measure that got worse and said so.
|
||||
|
||||
@ -50,3 +50,36 @@ re-fetch; only public POI/way data, never private-individual data.
|
||||
4. Road simplification fidelity — too coarse reads wrong, too fine costs edges; A owns the knob.
|
||||
5. Settled non-risks: facade clearance on osm towns (R17), interior types (C proved 18 real
|
||||
`shop=` values degrade cleanly), plan-side determinism (E's pipeline is byte-identical re-run).
|
||||
|
||||
### Retired-or-carried at the v4.0 close (ROUND22 ledger #6 — Lane A, line by line)
|
||||
|
||||
Verdicts against the **full 23-town pack** (1,210 shops), not against the 5-town sample the list was
|
||||
written for. "Retired" means a law now enforces it and the pack proved the law holds with headroom.
|
||||
|
||||
1. **RETIRED — enforced, with headroom.** `MAX_TOWN_SPAN_M` (5 km) is validator-enforced, so a breach
|
||||
fails loudly at load, not silently in the plan. Widest in the pack: **geelong at 3,829 m — 77% of the
|
||||
ceiling**; no town came close to the law. Mega-strips are now a *pipeline curation* question (E's), and
|
||||
the generator's fence is the backstop it was designed to be.
|
||||
2. **RETIRED — the floor never bound.** `MIN_TOWN_SHOPS` is 6; the thinnest town in the pack is
|
||||
**darwin at 12 — 2× the floor**. No cache was rejected for sparsity. (Whether a 12-shop town *reads*
|
||||
alive is a different question and a live one — it's D's thin-tail spot-check this round, not a
|
||||
generator risk.)
|
||||
3. **RETIRED on the plan side; CARRIED for the runtime lanes.** This was the alpha's real risk and it
|
||||
paid off exactly as intended — the irregular graphs surfaced fragmentation (R19), capacity (R20) and
|
||||
venue stranding (R21), each fixed-or-fenced and written down. At 23-town scale A's side needed **no
|
||||
new hardening**: 1 connected component and 3 venues on every town, all invariants green, fitzroy's 160
|
||||
shops included. **Carried** only where it hasn't been measured yet: C's door-soak, D's fitzroy audit
|
||||
and F's matrix are this round's verifies — the plan being green is not the same as the town rendering
|
||||
and simulating green.
|
||||
4. **RETIRED — the knob is at a measured setting.** Junction-protected Douglas–Peucker settled it
|
||||
(katoomba 105 → 11 components; cull/bridge → 1 component at 100% main-net, and now **1 component on
|
||||
all 23**). The fidelity/edge-count trade has a floor that holds across the pack; it is no longer an
|
||||
open question, and any future move re-pins goldens under the amendment law.
|
||||
5. **Unchanged** — still settled, now on 23 towns rather than 5.
|
||||
6. **NEW, CARRIED — thin-graph seating capacity.** The pack's honest tail: **6.4% of shops don't seat**
|
||||
(77 of 1,210), all counted by cause, dominated by overflow where a town has more shops than its road
|
||||
graph has frontage. Outliers: **daylesford 19%** (32 shops on 171 roads — the thinnest graph in the
|
||||
pack by 2×) and **fitzroy 13%**. `RGAP` is already 0.5 m and `NODE_CLEAR` 3 m; the only lever left is
|
||||
spilling overflow onto a *different* street, which buys a few percent by putting a shop on a road it
|
||||
isn't on. Ruling stands from R20: **drop and count beats teleport.** Carried into v4.x as a known,
|
||||
measured, per-cause-attributed tail — not a defect, and not hidden.
|
||||
|
||||
BIN
docs/shots/release_v4/classic_covenant.png
Normal file
|
After Width: | Height: | Size: 809 KiB |
1
docs/shots/release_v4/contact.html
Normal file
@ -0,0 +1 @@
|
||||
<!doctype html><meta charset="utf-8"><title>PROCITY v4.0 — the release tour</title><style>body{background:#14110c;color:#f4efe6;font:14px/1.4 -apple-system,Segoe UI,Roboto,sans-serif;margin:24px}.grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(460px,1fr));gap:18px}figure{margin:0;background:#1d1810;border-radius:10px;overflow:hidden}img{width:100%;display:block}figcaption{padding:9px 13px;color:#cdc4b2}b{color:#ffd9a0}</style><h1>PROCITY v4.0 — THE REAL MAP <small>· 23 real Australian towns · seed 20261990 · settled</small></h1><div class="grid"><figure><img src="fremantle_hero.png" alt="fremantle_hero" loading="lazy"><figcaption><b>fremantle_hero</b><br>the hero — Fremantle, real streets, golden hour · Fremantle · 79 shops / 1056 edges · retail heart: 10 shops/60m · 126 draws · 77,322 tris</figcaption></figure><figure><img src="fitzroy_mecca.png" alt="fitzroy_mecca" loading="lazy"><figcaption><b>fitzroy_mecca</b><br>the mecca — Fitzroy, the densest town in the pack · Fitzroy · 139 shops / 2408 edges · retail heart: 7 shops/60m · 166 draws · 95,824 tris</figcaption></figure><figure><img src="toowoomba_thintail.png" alt="toowoomba_thintail" loading="lazy"><figcaption><b>toowoomba_thintail</b><br>the thin tail — Toowoomba: the floor does NOT hold here, and this is the honest frame (12 shops over a 1,459-edge graph = 0.008 shops/edge vs the heroes' 0.06–0.08; it reads as road, not town). Filed for v4.x · Toowoomba · 12 shops / 1459 edges · retail heart: 1 shops/60m · 78 draws · 78,496 tris</figcaption></figure><figure><img src="synthetic_control.png" alt="synthetic_control" loading="lazy"><figcaption><b>synthetic_control</b><br>the control — the procedural town, unchanged · Boolarra Heads · 493 shops / 22 edges · 180 draws · 29,158 tris</figcaption></figure><figure><img src="classic_covenant.png" alt="classic_covenant" loading="lazy"><figcaption><b>classic_covenant</b><br>?classic=1 — the v2 covenant, byte-identical forever · Boolarra Heads · 493 shops / 22 edges · 164 draws · 26,472 tris</figcaption></figure></div>
|
||||
BIN
docs/shots/release_v4/fitzroy_mecca.png
Normal file
|
After Width: | Height: | Size: 778 KiB |
BIN
docs/shots/release_v4/fremantle_hero.png
Normal file
|
After Width: | Height: | Size: 662 KiB |
BIN
docs/shots/release_v4/synthetic_control.png
Normal file
|
After Width: | Height: | Size: 809 KiB |
BIN
docs/shots/release_v4/toowoomba_thintail.png
Normal file
|
After Width: | Height: | Size: 835 KiB |
@ -15,7 +15,7 @@ gorgeous, it doesn't make it work.
|
||||
| OpenRouter key (skins) | env `OPENROUTER_KEY` or `~/Documents/dealgod/lore/keys.env` (never committed) |
|
||||
| depot password (publish) | env `GOD3_PW` (never committed; ask John) — cookie = `hmac(GOD3_PW,"3god-ok")` |
|
||||
| Ollama (free LLM) | `http://100.89.131.57:11434` (qwen3) — for names/lore/cataloguing, unused so far |
|
||||
| MODELBEAST (free local gen) | this box, `~/Documents/MODELBEAST`. `flux_local` = FLUX.2-klein-4B text→image on MPS (**used for the facade skins** — ungated, weights cached). `sf3d`/`trellis_mac` = image→GLB, local but HF-gated (setup needed) |
|
||||
| MODELBEAST (free local gen) | this box, `~/Documents/MODELBEAST` + its own queue on **`:8777`**. `flux_local` = FLUX.2-klein-4B text→image on MPS (**the facade skins** — ungated, weights cached). `sf3d`/`trellis_mac` = image→GLB, **local + PROVEN, weights already cached** — `trellis_mac` (TRELLIS.2 `1024_cascade`) went **15/15 on thriftgod's Tier-1 props** (Lane G/ultra, R1); `sf3d` = seconds-fast draft. Also live on the queue: `bg_remove_local`, `mflux_image_edit`, `seedvr2_upscale`. *(The old "HF-gated / setup needed" note here was stale — flagged by ultra, corrected R22.)* |
|
||||
|
||||
## The steps (each is idempotent / resumable)
|
||||
|
||||
@ -88,6 +88,25 @@ reused from depot. `magazine-rack` → primitive (thin-wire gotcha); `glass-case
|
||||
fallback (TRELLIS can't do transparent glass; `sf3d --draft` gives a featureless blob). TRELLIS meshes run
|
||||
heavy (25–77k tris) — see AUDIT provenance. **Eyeball every thumbnail**; ≤2 local attempts then fal (John's go).
|
||||
|
||||
#### ⚠️ Finishing a `trellis_mac` mesh — ultra's post-mortem (Lane G, R1) — folded in verbatim
|
||||
> ⚠️ **Do NOT run trellis_mac output through the MESHGOD finish farm** — its fill_holes+decimate
|
||||
> shreds 200k-tri marching-cubes meshes (proven on ultra, post-mortem in dealgod memory). The
|
||||
> working pipeline: raw GLB → `npx @gltf-transform/cli optimize --compress draco --texture-compress
|
||||
> webp --texture-size 1024 --simplify-error 0.01` → 0.4–1.8 MB at raw-quality look.
|
||||
|
||||
*Credit: Lane G / ultra (`G-progress.md`), proven post-mortem — it cost them a shredded batch so it
|
||||
shouldn't cost us one.* **One amendment, per Fable's Ruling 1 (`docs/LANES/FABLE_TO_LANE_G.md`):
|
||||
NO-DRACO WINS — the depot speaks uncompressed GLB and PROCITY's loader stays decoder-free.** So drop
|
||||
`--compress draco`; everything else in ultra's command stands (WebP ≤1024 is already our law):
|
||||
```bash
|
||||
npx @gltf-transform/cli optimize IN.glb OUT.glb \
|
||||
--texture-compress webp --texture-size 1024 --simplify-error 0.01 # NO --compress draco
|
||||
```
|
||||
Files land ~2–3× bigger than the Draco'd version and that's fine — the depot holds ONE law.
|
||||
Note our own in-house paths remain valid for their own cases and are NOT the finish farm:
|
||||
`bake_lowpoly.py` (voxel-remesh + bake, for shell-soup that must hit a low budget) and
|
||||
`decimate_props.py` (collapse, detail-preserving — see the R13 instrument diet).
|
||||
|
||||
### 7. Local-depot mode — `loaders.js ?localdepot=1` + `stage_local_depot.py` *(round-3: DONE)*
|
||||
Lets Lanes B/C/F validate GLB loading with **no network / before publish**:
|
||||
```bash
|
||||
|
||||
@ -176,7 +176,7 @@
|
||||
"out": "/tmp/procity_norm/procity_street_bench_01.glb",
|
||||
"thumb": "/tmp/procity_norm/thumbs/procity_street_bench_01.png",
|
||||
"tri_before": 1942,
|
||||
"tri_after": 1942,
|
||||
"tri_after": 500,
|
||||
"size_m": [
|
||||
0.437,
|
||||
0.85,
|
||||
@ -185,7 +185,8 @@
|
||||
"footprint": [
|
||||
0.44,
|
||||
1.21
|
||||
]
|
||||
],
|
||||
"diet_note": "R22 tri diet (B #1a); _hi original preserved in _normalized"
|
||||
},
|
||||
{
|
||||
"src": "street-furniture/eb_hipster_bench.glb",
|
||||
@ -278,7 +279,7 @@
|
||||
"out": "/tmp/procity_norm/procity_street_food_cart_01.glb",
|
||||
"thumb": "/tmp/procity_norm/thumbs/procity_street_food_cart_01.png",
|
||||
"tri_before": 30220,
|
||||
"tri_after": 5000,
|
||||
"tri_after": 1000,
|
||||
"size_m": [
|
||||
3.003,
|
||||
2.0,
|
||||
@ -287,7 +288,8 @@
|
||||
"footprint": [
|
||||
3.0,
|
||||
2.07
|
||||
]
|
||||
],
|
||||
"diet_note": "R22 tri diet (B #1a); _hi original preserved in _normalized"
|
||||
}
|
||||
],
|
||||
"errors": []
|
||||
|
||||
@ -121,7 +121,7 @@
|
||||
{
|
||||
"file": "procity_street_bus_shelter_01.glb",
|
||||
"tri_before": 40000,
|
||||
"tri_after": 7998,
|
||||
"tri_after": 2500,
|
||||
"size_m": [
|
||||
2.333,
|
||||
2.4,
|
||||
@ -132,7 +132,8 @@
|
||||
1.41
|
||||
],
|
||||
"thumb": "/tmp/fal_bake/thumbs/procity_street_bus_shelter_01.png",
|
||||
"out": "/tmp/fal_bake/procity_street_bus_shelter_01.glb"
|
||||
"out": "/tmp/fal_bake/procity_street_bus_shelter_01.glb",
|
||||
"diet_note": "R22 tri diet (B #1a); _hi original preserved in _normalized"
|
||||
},
|
||||
{
|
||||
"file": "procity_fit_magazine_rack_01.glb",
|
||||
@ -156,7 +157,7 @@
|
||||
"out": "/tmp/e2_norm/procity_street_bin_01.glb",
|
||||
"thumb": "web/assets/thumbs/procity_street_bin_01.png",
|
||||
"tri_before": 3000,
|
||||
"tri_after": 3000,
|
||||
"tri_after": 800,
|
||||
"size_m": [
|
||||
0.569,
|
||||
1.0,
|
||||
@ -165,7 +166,8 @@
|
||||
"footprint": [
|
||||
0.57,
|
||||
0.68
|
||||
]
|
||||
],
|
||||
"diet_note": "R22 tri diet (B #1a); _hi original preserved in _normalized"
|
||||
},
|
||||
{
|
||||
"src": "/tmp/milk-crate-stack.glb",
|
||||
|
||||
@ -27,11 +27,15 @@
|
||||
"procity_fit_wire_shelf_01.glb",
|
||||
"procity_fit_work_table_01.glb",
|
||||
"procity_street_bench_01.glb",
|
||||
"procity_street_bench_01_hi.glb",
|
||||
"procity_street_bench_modern_01.glb",
|
||||
"procity_street_bench_wood_01.glb",
|
||||
"procity_street_bin_01.glb",
|
||||
"procity_street_bin_01_hi.glb",
|
||||
"procity_street_bus_shelter_01.glb",
|
||||
"procity_street_bus_shelter_01_hi.glb",
|
||||
"procity_street_food_cart_01.glb",
|
||||
"procity_street_food_cart_01_hi.glb",
|
||||
"procity_street_longbench_01.glb",
|
||||
"procity_street_novelty_record_01.glb",
|
||||
"procity_street_park_bench_01.glb",
|
||||
|
||||
@ -371,8 +371,13 @@ def write_index():
|
||||
for f in sorted(x for x in os.listdir(TOWNS_DIR) if x.endswith(".json") and x != "index.json"):
|
||||
c = json.load(open(os.path.join(TOWNS_DIR, f)))
|
||||
k = c.get("key", f[:-5])
|
||||
towns.append({"key": k, "town": c.get("town", k), "state": TOWNS.get(k, {}).get("state", ""),
|
||||
"shops": len(c.get("shops", [])), "roads": bool(c.get("roads"))})
|
||||
# state: the cache's own field first (so ANY lane's town self-describes — e.g. Lane G's
|
||||
# godverse caches), then my TOWNS config, then blank. The index rosters every cache in the
|
||||
# dir, not just mine.
|
||||
state = c.get("state") or TOWNS.get(k, {}).get("state", "")
|
||||
towns.append({"key": k, "town": c.get("town", k), "state": state,
|
||||
"shops": len(c.get("shops", [])), "roads": bool(c.get("roads")),
|
||||
"source": c.get("source", "osm")})
|
||||
idx = {"schema": "procity-towns-index/1", "generated": datetime.date.today().isoformat(),
|
||||
"license": "ODbL 1.0", "attribution": "© OpenStreetMap contributors", "towns": towns}
|
||||
json.dump(idx, open(os.path.join(TOWNS_DIR, "index.json"), "w"), indent=1)
|
||||
|
||||
@ -1,22 +1,23 @@
|
||||
#!/usr/bin/env python3
|
||||
"""PROCITY Lane F — the v3.0 release money-shot tour (round 15 · ?gigs=1). One deterministic run.
|
||||
"""PROCITY Lane F — the v4.0 RELEASE TOUR (round 22 · the epoch close · ledger #4). One deterministic run.
|
||||
|
||||
Captures the release set into docs/shots/release_v3/ (+ a contact.html), grouped by the flags each shot
|
||||
needs, all from seed 20261990 (deterministic, reproducible — never hand-cropped):
|
||||
• venue_night — lit pub frontage + streetlamp pools + posters at night (?gigs=1)
|
||||
• district_posters — the spine at night: poster poles + lit frontages receding (?gigs=1)
|
||||
• queue_night — JOHN'S HERO: close on the pub door at 'on', queue + marquee + A's now-visible poster
|
||||
• gig_interior — the 4-piece with real instruments + a roster crowd (continuity) (?gigs=1)
|
||||
• rain_verandah — a wet night outside the lit video-shop window (?weather=rain&winmap=1)
|
||||
• tram_stop — the bus shelter the tram pauses at (?tram=1)
|
||||
The v4 thesis in five frames, into docs/shots/release_v4/ (+ contact.html). The towns ARE the product:
|
||||
|
||||
Discipline (ROUND15 ledger #5 + B's note): every shot is measured SETTLED — after the async instrument
|
||||
GLBs / poster textures / chunk stream land (shoot-twice), from a clean per-group load (no orphan-chunk
|
||||
inflation); the queue is driven LIVE (rAF is throttled in automation, so we run the animate loop + admit
|
||||
explicitly rather than trust auto-drain).
|
||||
• fremantle_hero — the golden-hour hero: a real shopfront corridor with the density crowd on it
|
||||
• fitzroy_mecca — the mecca (160 shops, 2× anything absorbed before this epoch)
|
||||
• toowoomba_thintail — the thin tail (12 shops): proof the floor still reads as a town
|
||||
• synthetic_control — the procedural town, UNCHANGED: the control the whole epoch was measured against
|
||||
• classic_covenant — `?classic=1`: the v2 covenant made visible, byte-identical forever (0x3fa36874)
|
||||
|
||||
Posing: every real-town frame resolves through Lane B's `DBG` retail-heart anchor / `clusterPose` — the
|
||||
primitive that came out of F's R21 finding (per-EDGE shop counts are ~1 on a real graph, so "densest edge"
|
||||
is the wrong heuristic; you must project the densest SPATIAL cluster onto its front edge and stand on the
|
||||
road). B's R22 bookmark fix resolves through the same anchor, so the tour and the bookmarks agree by
|
||||
construction. The HUD is hidden for every frame (F's R21 ruling corollary — the selector ships, but it
|
||||
doesn't belong in a beauty shot).
|
||||
|
||||
Run: tools/.venv/bin/python tools/qa/tour_shots.py [--seed N]
|
||||
Needs the Playwright venv + Lane B's ?dbg=1 hook (incl. queue_night). Writes full-frame 1280x720 PNGs.
|
||||
Needs the Playwright venv + Lane B's ?dbg=1 hook. Writes full-frame 1280x720 PNGs.
|
||||
"""
|
||||
import sys, time, socket, subprocess, pathlib
|
||||
|
||||
@ -24,59 +25,54 @@ ROOT = pathlib.Path(__file__).resolve().parent.parent.parent
|
||||
PORT = 8130
|
||||
HOST = f'http://127.0.0.1:{PORT}'
|
||||
SEED = 20261990
|
||||
OUTDIR = ROOT / 'docs' / 'shots' / 'release_v3_1'
|
||||
OUTDIR = ROOT / 'docs' / 'shots' / 'release_v4'
|
||||
if '--seed' in sys.argv:
|
||||
SEED = int(sys.argv[sys.argv.index('--seed') + 1])
|
||||
|
||||
# [R16 THE FLIP] shot groups — the DEFAULT boot now has all four flags on, so most shots come from ONE clean
|
||||
# load (no per-shot flag juggling). Rain is still forced (the default seed rolls clear); ?classic=1 gives the
|
||||
# before/after comparison. Deterministic, same seed.
|
||||
GROUPS = [
|
||||
{'flags': '', 'street': ['venue_night', 'district_posters', 'queue_night', 'tram_stop'], 'interior': True},
|
||||
{'flags': 'weather=rain', 'street': ['rain_verandah'], 'interior': False},
|
||||
# the town before v3 — the exact v2 boot; same main-street pose as district_posters for a clean A/B
|
||||
{'flags': 'classic=1', 'street': ['district_posters'], 'interior': False, 'suffix': '_classic'},
|
||||
# (file, query, segment, cluster-pose?, caption-prefix). seg 3 = late-afternoon gold; seg 2 = midday.
|
||||
# cluster=True → F's spatial-cluster pose (real towns); cluster=False → the bookmark (synthetic is one dense
|
||||
# cluster already, so street_noon frames it correctly).
|
||||
V4_TOUR = [
|
||||
('fremantle_hero', 'plansrc=osm&town=fremantle_real', 3, True, 'the hero — Fremantle, real streets, golden hour'),
|
||||
('fitzroy_mecca', 'plansrc=osm&town=fitzroy_real', 3, True, 'the mecca — Fitzroy, the densest town in the pack'),
|
||||
('toowoomba_thintail', 'plansrc=osm&town=toowoomba_real', 2, True, 'the thin tail — Toowoomba: the floor does NOT hold here, and this is the honest frame (12 shops over a 1,459-edge graph = 0.008 shops/edge vs the heroes\' 0.06–0.08; it reads as road, not town). Filed for v4.x'),
|
||||
('synthetic_control', f'seed={SEED}', 2, False, 'the control — the procedural town, unchanged'),
|
||||
('classic_covenant', 'classic=1', 2, False, '?classic=1 — the v2 covenant, byte-identical forever'),
|
||||
]
|
||||
|
||||
# settle + re-pose a street bookmark once its live/async content (queue, instrument GLBs, poster textures,
|
||||
# streamed chunks) has landed, then report SETTLED stats.
|
||||
STREET_JS = r"""
|
||||
async (name) => {
|
||||
# THE POSING PRIMITIVE (F's R21 finding). On a real graph, per-EDGE shop counts are ~1 — so "the edge with
|
||||
# the most shops" frames an empty arterial (it did, first run: the Fitzroy mecca came out as bare bitumen).
|
||||
# The town's retail heart is a SPATIAL cluster: find the shop lot with the most neighbours inside 60 m,
|
||||
# project it onto its own front edge, and stand ON the road backed off along it, looking down the strip.
|
||||
# Lane B's R22 bookmark fix anchors on the same retail heart — this is that primitive, applied to the tour.
|
||||
SHOT_JS = r"""
|
||||
async ({ seg, cluster }) => {
|
||||
const P = window.PROCITY, D = window.DBG;
|
||||
D.shot(name); // initial pose + segment (2 renders)
|
||||
for (let i = 0; i < 8; i++) await new Promise(r => requestAnimationFrame(r)); // animate loop: queue spawns
|
||||
const q = (P.gigs && P.venueQueues) ? P.venueQueues.get(P.gigs.venueShopIds[0]) : null;
|
||||
await new Promise(r => setTimeout(r, 1400)); // async GLB / poster / chunk settle
|
||||
const stat = D.shot(name); // RE-POSE now that everything is loaded → the settled frame
|
||||
const queue = (P.gigs && P.queueCountOf) ? P.queueCountOf(P.gigs.venueShopIds[0]) : null;
|
||||
const posters = (P.plan.posters || []).length;
|
||||
return { name, draws: stat.drawCalls, tris: stat.tris, queue, posters };
|
||||
}
|
||||
"""
|
||||
|
||||
INTERIOR_JS = r"""
|
||||
async () => {
|
||||
const P = window.PROCITY, D = window.DBG, THREE = P.THREE;
|
||||
const id = P.gigs.venueShopIds[0];
|
||||
D.setSegment(5);
|
||||
// drive the queue live + admit a couple so the interior crowd carries roster identities (continuity)
|
||||
for (let i = 0; i < 8; i++) await new Promise(r => requestAnimationFrame(r));
|
||||
const q = P.venueQueues && P.venueQueues.get(id);
|
||||
if (q) for (let i = 0; i < 3 && q.count() > 0; i++) q.admitOne();
|
||||
D.enterShop(id); await new Promise(r => setTimeout(r, 900));
|
||||
const room = P.interiorMode.current; if (!room) return { ok: false };
|
||||
const st = room.stage;
|
||||
P.camera.position.set(st.x + 0.2, 1.6, room.dims.D / 2 - 1.2);
|
||||
P.camera.lookAt(new THREE.Vector3(st.x, 1.35, st.z));
|
||||
// shoot-twice / settled discipline (ledger #5): immediate vs after the instrument GLBs land
|
||||
P.renderer.info.reset(); P.renderer.render(P.interiorMode.scene, P.camera);
|
||||
const trisImmediate = P.renderer.info.render.triangles;
|
||||
await new Promise(r => setTimeout(r, 1600));
|
||||
P.renderer.info.reset(); P.renderer.render(P.interiorMode.scene, P.camera);
|
||||
const info = P.interiorMode.crewInfo;
|
||||
return { ok: true, draws: P.renderer.info.render.calls, trisImmediate, trisSettled: P.renderer.info.render.triangles,
|
||||
band: info ? info.band : 0, crowd: info ? info.crowd : 0, fromRoster: info ? info.fromRoster : 0,
|
||||
venue: (P.plan.shops.find(s => s.id === id) || {}).name, bandName: P.gigs.bandNameOf(id) };
|
||||
D.setSegment(seg);
|
||||
let hub = -1;
|
||||
if (!cluster) { D.shot('street_noon'); }
|
||||
else {
|
||||
const plan = P.plan, nById = new Map(plan.streets.nodes.map(n => [n.id, n]));
|
||||
const lots = (plan.lots || []).filter(l => l.use === 'shop' || l.use === 'anchor');
|
||||
let best = null;
|
||||
for (const l of lots) { let c = 0; for (const o of lots) if (Math.hypot(o.x - l.x, o.z - l.z) < 60) c++;
|
||||
if (c > hub) { hub = c; best = l; } }
|
||||
if (best) {
|
||||
const e = (plan.streets.edges || []).find(x => x.id === best.frontEdge) || plan.streets.edges[0];
|
||||
const a = nById.get(e.a), b2 = nById.get(e.b);
|
||||
const dx = b2.x - a.x, dz = b2.z - a.z, L = Math.hypot(dx, dz) || 1, ux = dx / L, uz = dz / L;
|
||||
let t = (best.x - a.x) * ux + (best.z - a.z) * uz;
|
||||
t = Math.max(6, Math.min(L - 6, t));
|
||||
const back = Math.min(26, t);
|
||||
D.teleport(a.x + ux * (t - back), a.z + uz * (t - back), Math.atan2(ux, uz));
|
||||
} else { D.shot('street_noon'); }
|
||||
}
|
||||
await new Promise(r => setTimeout(r, 1800)); // crowd + chunks + skins + GLBs settle
|
||||
const hud = document.getElementById('pc-hud'); if (hud) hud.style.display = 'none'; // beauty shot
|
||||
for (let i = 0; i < 3; i++) await new Promise(r => requestAnimationFrame(r));
|
||||
const i = D.info();
|
||||
return { name: P.plan.name, shops: (P.plan.shops || []).length, hub,
|
||||
edges: (P.plan.streets.edges || []).length, draws: i.drawCalls, tris: i.tris };
|
||||
}
|
||||
"""
|
||||
|
||||
@ -98,31 +94,18 @@ def ensure_server():
|
||||
proc.terminate(); sys.exit(f'could not start http.server on :{PORT}')
|
||||
|
||||
|
||||
def boot(pg, flags):
|
||||
pg.goto(f'{HOST}/index.html?seed={SEED}&dbg=1&{flags}')
|
||||
pg.wait_for_function('window.DBG && window.DBG.ready === true', timeout=25000)
|
||||
pg.evaluate("() => { const o=document.getElementById('pc-start'); if(o) o.style.display='none'; }")
|
||||
try:
|
||||
pg.wait_for_function('() => window.PROCITY && (!window.PROCITY.fleet || window.PROCITY.fleet.ready)', timeout=15000)
|
||||
except Exception:
|
||||
print(' ! fleet never readied — shooting placeholders')
|
||||
|
||||
|
||||
def contact_html(shots):
|
||||
cards = []
|
||||
for s in shots:
|
||||
cap = s['caption']
|
||||
cards.append(
|
||||
f'<figure><img src="{s["file"]}" alt="{s["name"]}" loading="lazy">'
|
||||
f'<figcaption><b>{s["name"]}</b><br>{cap}</figcaption></figure>')
|
||||
cards = ''.join(
|
||||
f'<figure><img src="{s["file"]}" alt="{s["name"]}" loading="lazy">'
|
||||
f'<figcaption><b>{s["name"]}</b><br>{s["caption"]}</figcaption></figure>' for s in shots)
|
||||
return (
|
||||
'<!doctype html><meta charset="utf-8"><title>PROCITY v3.1 — release tour</title>'
|
||||
'<!doctype html><meta charset="utf-8"><title>PROCITY v4.0 — the release tour</title>'
|
||||
'<style>body{background:#14110c;color:#f4efe6;font:14px/1.4 -apple-system,Segoe UI,Roboto,sans-serif;margin:24px}'
|
||||
'h1{font-weight:700}.grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(420px,1fr));gap:18px}'
|
||||
'.grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(460px,1fr));gap:18px}'
|
||||
'figure{margin:0;background:#1d1810;border-radius:10px;overflow:hidden}img{width:100%;display:block}'
|
||||
'figcaption{padding:8px 12px;color:#cdc4b2}b{color:#ffd9a0}</style>'
|
||||
f'<h1>PROCITY v3.1 — release money-shot tour (THE FLIP) <small>· seed {SEED} · settled measurements</small></h1>'
|
||||
f'<div class="grid">{"".join(cards)}</div>')
|
||||
'figcaption{padding:9px 13px;color:#cdc4b2}b{color:#ffd9a0}</style>'
|
||||
f'<h1>PROCITY v4.0 — THE REAL MAP <small>· 23 real Australian towns · seed {SEED} · settled</small></h1>'
|
||||
f'<div class="grid">{cards}</div>')
|
||||
|
||||
|
||||
def main():
|
||||
@ -131,51 +114,39 @@ def main():
|
||||
except ImportError:
|
||||
sys.exit('playwright not installed — tools/.venv/bin/python -m playwright install chromium')
|
||||
|
||||
print(f"\033[1mv3.1 RELEASE MONEY-SHOT TOUR\033[0m (seed {SEED} → docs/shots/release_v3_1/)")
|
||||
print(f"\033[1mv4.0 RELEASE TOUR\033[0m ({len(V4_TOUR)} frames → docs/shots/release_v4/)")
|
||||
OUTDIR.mkdir(parents=True, exist_ok=True)
|
||||
srv = ensure_server()
|
||||
shots, errs_total = [], 0
|
||||
try:
|
||||
with sync_playwright() as p:
|
||||
b = p.chromium.launch()
|
||||
for grp in GROUPS:
|
||||
for name, query, seg, cluster, cap in V4_TOUR:
|
||||
pg = b.new_page(viewport={'width': 1280, 'height': 720})
|
||||
errs = []
|
||||
pg.on('pageerror', lambda e: errs.append(str(e)))
|
||||
boot(pg, grp['flags'])
|
||||
sfx = grp.get('suffix', '')
|
||||
for name in grp['street']:
|
||||
stat = pg.evaluate(STREET_JS, name)
|
||||
fname = f'{name}{sfx}.png'
|
||||
pg.screenshot(path=str(OUTDIR / fname))
|
||||
cap = (f"?classic=1 — the town before v3 (no gig layer, no pools)" if sfx
|
||||
else f"{stat['draws']} draws · {stat['tris']:,} tris")
|
||||
if not sfx and stat.get('queue') is not None: cap += f" · queue {stat['queue']}"
|
||||
if not sfx and name in ('district_posters', 'venue_night', 'queue_night'): cap += f" · {stat['posters']} posters"
|
||||
shots.append({'name': name + sfx, 'file': fname, 'caption': cap})
|
||||
print(f" \033[32m●\033[0m {name}{sfx}: {cap}")
|
||||
if grp['interior']:
|
||||
info = pg.evaluate(INTERIOR_JS)
|
||||
if info.get('ok'):
|
||||
out = OUTDIR / 'gig_interior.png'
|
||||
pg.screenshot(path=str(out))
|
||||
cap = (f"{info['venue']} · {info['bandName']} · {info['band']}-piece · crowd {info['crowd']} "
|
||||
f"({info['fromRoster']} from roster) · {info['draws']} draws · "
|
||||
f"{info['trisImmediate']:,}→{info['trisSettled']:,} tris (pre-load→settled)")
|
||||
shots.append({'name': 'gig_interior', 'file': 'gig_interior.png', 'caption': cap})
|
||||
print(f" \033[32m●\033[0m gig_interior: {cap}")
|
||||
else:
|
||||
print(" \033[31m✗\033[0m gig_interior: could not stage")
|
||||
pg.goto(f'{HOST}/index.html?{query}&dbg=1')
|
||||
pg.wait_for_function('window.PROCITY && window.PROCITY.plan && window.DBG && window.DBG.ready', timeout=25000)
|
||||
pg.evaluate("() => { const o=document.getElementById('pc-start'); if(o) o.style.display='none'; }")
|
||||
try:
|
||||
pg.wait_for_function('() => !window.PROCITY.fleet || window.PROCITY.fleet.ready', timeout=15000)
|
||||
except Exception:
|
||||
pass
|
||||
r = pg.evaluate(SHOT_JS, {'seg': seg, 'cluster': cluster})
|
||||
pg.screenshot(path=str(OUTDIR / f'{name}.png'))
|
||||
caption = f"{cap} · {r['name']} · {r['shops']} shops / {r['edges']} edges" + (f" · retail heart: {r['hub']} shops/60m" if r.get('hub', -1) > 0 else '') + f" · {r['draws']} draws · {r['tris']:,} tris"
|
||||
shots.append({'name': name, 'file': f'{name}.png', 'caption': caption})
|
||||
print(f" \033[32m●\033[0m {name}: {caption}")
|
||||
errs_total += len(errs)
|
||||
if errs: print(f" ! {len(errs)} page error(s) in [{grp['flags']}]: {errs[0][:100]}")
|
||||
if errs: print(f" ! {len(errs)} page error(s): {errs[0][:90]}")
|
||||
pg.close()
|
||||
b.close()
|
||||
finally:
|
||||
if srv: srv.terminate()
|
||||
|
||||
(OUTDIR / 'contact.html').write_text(contact_html(shots))
|
||||
print(f"\n\033[1m{len(shots)} shots\033[0m → docs/shots/release_v3_1/ (+ contact.html) · {errs_total} page errors")
|
||||
sys.exit(1 if errs_total or len(shots) < 6 else 0)
|
||||
print(f"\n\033[1m{len(shots)} frames\033[0m → docs/shots/release_v4/ (+ contact.html) · {errs_total} page errors")
|
||||
sys.exit(1 if errs_total or len(shots) < len(V4_TOUR) else 0)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
|
||||
|
Before Width: | Height: | Size: 32 KiB After Width: | Height: | Size: 32 KiB |
|
Before Width: | Height: | Size: 45 KiB After Width: | Height: | Size: 47 KiB |
|
Before Width: | Height: | Size: 42 KiB After Width: | Height: | Size: 44 KiB |
|
Before Width: | Height: | Size: 38 KiB After Width: | Height: | Size: 38 KiB |
@ -9,154 +9,184 @@
|
||||
"town": "Adelaide",
|
||||
"state": "SA",
|
||||
"shops": 116,
|
||||
"roads": true
|
||||
"roads": true,
|
||||
"source": "osm"
|
||||
},
|
||||
{
|
||||
"key": "ballarat_real",
|
||||
"town": "Ballarat",
|
||||
"state": "VIC",
|
||||
"shops": 20,
|
||||
"roads": true
|
||||
"roads": true,
|
||||
"source": "osm"
|
||||
},
|
||||
{
|
||||
"key": "bendigo_real",
|
||||
"town": "Bendigo",
|
||||
"state": "VIC",
|
||||
"shops": 36,
|
||||
"roads": true
|
||||
"roads": true,
|
||||
"source": "osm"
|
||||
},
|
||||
{
|
||||
"key": "bowral_real",
|
||||
"town": "Bowral",
|
||||
"state": "NSW",
|
||||
"shops": 32,
|
||||
"roads": true
|
||||
"roads": true,
|
||||
"source": "osm"
|
||||
},
|
||||
{
|
||||
"key": "braddon_real",
|
||||
"town": "Braddon",
|
||||
"state": "ACT",
|
||||
"shops": 20,
|
||||
"roads": true
|
||||
"roads": true,
|
||||
"source": "osm"
|
||||
},
|
||||
{
|
||||
"key": "brunswick_real",
|
||||
"town": "Brunswick",
|
||||
"state": "VIC",
|
||||
"shops": 96,
|
||||
"roads": true
|
||||
"roads": true,
|
||||
"source": "osm"
|
||||
},
|
||||
{
|
||||
"key": "castlemaine_real",
|
||||
"town": "Castlemaine",
|
||||
"state": "VIC",
|
||||
"shops": 24,
|
||||
"roads": true
|
||||
"roads": true,
|
||||
"source": "osm"
|
||||
},
|
||||
{
|
||||
"key": "darwin_real",
|
||||
"town": "Darwin",
|
||||
"state": "NT",
|
||||
"shops": 12,
|
||||
"roads": true
|
||||
"roads": true,
|
||||
"source": "osm"
|
||||
},
|
||||
{
|
||||
"key": "daylesford_real",
|
||||
"town": "Daylesford",
|
||||
"state": "VIC",
|
||||
"shops": 32,
|
||||
"roads": true
|
||||
"roads": true,
|
||||
"source": "osm"
|
||||
},
|
||||
{
|
||||
"key": "fitzroy_real",
|
||||
"town": "Fitzroy",
|
||||
"state": "VIC",
|
||||
"shops": 160,
|
||||
"roads": true
|
||||
"roads": true,
|
||||
"source": "osm"
|
||||
},
|
||||
{
|
||||
"key": "fremantle_real",
|
||||
"town": "Fremantle",
|
||||
"state": "WA",
|
||||
"shops": 80,
|
||||
"roads": true
|
||||
"roads": true,
|
||||
"source": "osm"
|
||||
},
|
||||
{
|
||||
"key": "geelong_real",
|
||||
"town": "Geelong",
|
||||
"state": "VIC",
|
||||
"shops": 68,
|
||||
"roads": true
|
||||
"roads": true,
|
||||
"source": "osm"
|
||||
},
|
||||
{
|
||||
"key": "glebe_real",
|
||||
"town": "Glebe",
|
||||
"state": "NSW",
|
||||
"shops": 52,
|
||||
"roads": true
|
||||
"roads": true,
|
||||
"source": "osm"
|
||||
},
|
||||
{
|
||||
"key": "hobart_real",
|
||||
"town": "Hobart",
|
||||
"state": "TAS",
|
||||
"shops": 80,
|
||||
"roads": true
|
||||
"roads": true,
|
||||
"source": "osm"
|
||||
},
|
||||
{
|
||||
"key": "katoomba_real",
|
||||
"town": "Katoomba",
|
||||
"state": "NSW",
|
||||
"shops": 80,
|
||||
"roads": true
|
||||
"roads": true,
|
||||
"source": "osm"
|
||||
},
|
||||
{
|
||||
"key": "launceston_real",
|
||||
"town": "Launceston",
|
||||
"state": "TAS",
|
||||
"shops": 32,
|
||||
"roads": true
|
||||
"roads": true,
|
||||
"source": "osm"
|
||||
},
|
||||
{
|
||||
"key": "marrickville_real",
|
||||
"town": "Marrickville",
|
||||
"state": "NSW",
|
||||
"shops": 28,
|
||||
"roads": true
|
||||
"roads": true,
|
||||
"source": "osm"
|
||||
},
|
||||
{
|
||||
"key": "newcastle_real",
|
||||
"town": "Newcastle",
|
||||
"state": "NSW",
|
||||
"shops": 36,
|
||||
"roads": true
|
||||
"roads": true,
|
||||
"source": "osm"
|
||||
},
|
||||
{
|
||||
"key": "newtown_godverse",
|
||||
"town": "Newtown",
|
||||
"state": "",
|
||||
"shops": 18,
|
||||
"roads": true,
|
||||
"source": "godverse+osm"
|
||||
},
|
||||
{
|
||||
"key": "newtown_real",
|
||||
"town": "Newtown",
|
||||
"state": "NSW",
|
||||
"shops": 72,
|
||||
"roads": true
|
||||
"roads": true,
|
||||
"source": "osm"
|
||||
},
|
||||
{
|
||||
"key": "northbridge_real",
|
||||
"town": "Northbridge",
|
||||
"state": "WA",
|
||||
"shops": 60,
|
||||
"roads": true
|
||||
"roads": true,
|
||||
"source": "osm"
|
||||
},
|
||||
{
|
||||
"key": "toowoomba_real",
|
||||
"town": "Toowoomba",
|
||||
"state": "QLD",
|
||||
"shops": 12,
|
||||
"roads": true
|
||||
"roads": true,
|
||||
"source": "osm"
|
||||
},
|
||||
{
|
||||
"key": "westend_real",
|
||||
"town": "West End",
|
||||
"state": "QLD",
|
||||
"shops": 44,
|
||||
"roads": true
|
||||
"roads": true,
|
||||
"source": "osm"
|
||||
}
|
||||
]
|
||||
}
|
||||
@ -130,6 +130,15 @@ function pickVenues(plan, citySeed) {
|
||||
if (realData) {
|
||||
const clustered = pool.filter(s => clusterCount(s) >= CLUSTER_MIN);
|
||||
if (clustered.length) field = clustered;
|
||||
else {
|
||||
// ROUND22 (pack scale): a genuinely thin town can have NO candidate at the floor — ballarat,
|
||||
// toowoomba and newtown_godverse top out at 2 shops within 160 m. Rather than drop the bias
|
||||
// entirely (which stranded venues at 0), fall back to the DENSEST available: the venue still
|
||||
// lands on what little crowd the town has. Only fires where the floor is unreachable, so no
|
||||
// already-pinned town moves.
|
||||
let best = 0; for (const s of pool) { const n = clusterCount(s); if (n > best) best = n; }
|
||||
if (best > 0) field = pool.filter(s => clusterCount(s) >= best);
|
||||
}
|
||||
}
|
||||
|
||||
let cand;
|
||||
|
||||
@ -455,8 +455,29 @@ const TOWNS_DIR = join(HERE, '..', '..', 'assets', 'towns');
|
||||
// the fragmentation ruling (junction-protected DP + cull/bridge) + E's spine-poster fix. Amendment law:
|
||||
// the alpha changed nothing outside the cache-schema path; synthetic/fixtures/classic/default stay frozen.
|
||||
const REAL_TOWN_GOLDENS = {
|
||||
katoomba_real: 0x420f677d, newtown_real: 0x741c18ec, fremantle_real: 0x73d385df,
|
||||
bendigo_real: 0xc175194e, castlemaine_real: 0x26c128c9,
|
||||
adelaide_real: 0x5c790920,
|
||||
ballarat_real: 0xf0a9e8a1,
|
||||
bendigo_real: 0xc175194e,
|
||||
bowral_real: 0xb23ee067,
|
||||
braddon_real: 0x2f388775,
|
||||
brunswick_real: 0x7ecb5c3e,
|
||||
castlemaine_real: 0x26c128c9,
|
||||
darwin_real: 0xcfb1917f,
|
||||
daylesford_real: 0x4ec7352c,
|
||||
fitzroy_real: 0x6549f61b,
|
||||
fremantle_real: 0x73d385df,
|
||||
geelong_real: 0x2f65591e,
|
||||
glebe_real: 0x908e1835,
|
||||
hobart_real: 0xa96328b9,
|
||||
katoomba_real: 0x420f677d,
|
||||
launceston_real: 0x32b1ab7b,
|
||||
marrickville_real: 0x60148e37,
|
||||
newcastle_real: 0xbb2c3050,
|
||||
newtown_godverse: 0x24e63feb,
|
||||
newtown_real: 0x741c18ec,
|
||||
northbridge_real: 0x1a645848,
|
||||
toowoomba_real: 0x8b635722,
|
||||
westend_real: 0x5b1e5ffe,
|
||||
};
|
||||
// ROUND20: the GIG-layer golden per real town (district + capped posters) — the base golden above is
|
||||
// gigs-off so it never captured the poster cap. Pinned here so the ROUND20 poster cap (and the density
|
||||
@ -464,8 +485,29 @@ const REAL_TOWN_GOLDENS = {
|
||||
// ROUND21: re-pinned for the venue cluster-adjacency bias (D's relocation finding). Only the GIG goldens
|
||||
// move — venue selection is gig-layer, so the base town goldens above are untouched.
|
||||
const REAL_TOWN_GIG_GOLDENS = {
|
||||
katoomba_real: 0xbf586b77, newtown_real: 0xacdb3eee, fremantle_real: 0xcf3426fc,
|
||||
bendigo_real: 0x646b2c23, castlemaine_real: 0xf9713a69,
|
||||
adelaide_real: 0x24ad3c27,
|
||||
ballarat_real: 0xf097c331,
|
||||
bendigo_real: 0x646b2c23,
|
||||
bowral_real: 0xac809f0b,
|
||||
braddon_real: 0xc3e5cfc0,
|
||||
brunswick_real: 0x8c08914e,
|
||||
castlemaine_real: 0xf9713a69,
|
||||
darwin_real: 0xbc5605b5,
|
||||
daylesford_real: 0x2fb34c8a,
|
||||
fitzroy_real: 0x690e5ef9,
|
||||
fremantle_real: 0xcf3426fc,
|
||||
geelong_real: 0x6f47ec2f,
|
||||
glebe_real: 0x042f01fb,
|
||||
hobart_real: 0x22f80d7b,
|
||||
katoomba_real: 0xbf586b77,
|
||||
launceston_real: 0xeaf7ff4d,
|
||||
marrickville_real: 0xea21be51,
|
||||
newcastle_real: 0x66fc2bf9,
|
||||
newtown_godverse: 0xc5ed0b92,
|
||||
newtown_real: 0xacdb3eee,
|
||||
northbridge_real: 0x83a1d5be,
|
||||
toowoomba_real: 0x9d34ccfe,
|
||||
westend_real: 0xb2c7ddb6,
|
||||
};
|
||||
// `index.json` is E's towns INDEX (key/town/state/shops/roads for B's selector), not a town cache — skip it.
|
||||
const townFiles = (existsSync(TOWNS_DIR) ? readdirSync(TOWNS_DIR) : []).filter(f => f.endsWith('.json') && f !== 'index.json');
|
||||
|
||||
@ -59,9 +59,33 @@ export function installDBG(deps) {
|
||||
look: [frontX, 2.2, frontZ],
|
||||
};
|
||||
};
|
||||
// ── ROUND22 (Lane G's bug): per-town bookmark anchoring ────────────────────────────────────────
|
||||
// Every "nearest" resolver below used to sort by distance from the WORLD ORIGIN, which is only the
|
||||
// town centre on a SYNTHETIC plan. A real OSM town is projected about its cache's bbox centre while
|
||||
// its shops cluster hundreds of metres away, so origin-sorted bookmarks teleported to arbitrary
|
||||
// far-flung geometry (measured: the origin pick sits 408 m from the retail heart on katoomba, 626 m
|
||||
// on newtown, 371 m on fremantle). Fix: sort about the town's DENSEST SHOP CLUSTER instead.
|
||||
//
|
||||
// Gated on the REAL-ROADS signal (`edges > 200`, the same primitive the R20 tram fence uses) so
|
||||
// SYNTHETIC + the marched fixtures keep the exact origin-centred behaviour they've always had —
|
||||
// their bookmarks (and F's release-tour control shot) do not move. Verified: synthetic's anchor is
|
||||
// only 117 m out but DOES re-pick, hence the gate rather than an unconditional switch.
|
||||
const townAnchor = (() => {
|
||||
const CELL = 120; // ~a block; the heaviest bin is the retail heart
|
||||
const pts = (plan.shops || []).map(lotOf).filter(Boolean);
|
||||
if (!pts.length) return { x: 0, z: 0, n: 0 };
|
||||
const bins = new Map();
|
||||
for (const p of pts) { const k = `${Math.round(p.x / CELL)},${Math.round(p.z / CELL)}`; if (!bins.has(k)) bins.set(k, []); bins.get(k).push(p); }
|
||||
let best = null; for (const arr of bins.values()) if (!best || arr.length > best.length) best = arr;
|
||||
return { x: best.reduce((a, p) => a + p.x, 0) / best.length, z: best.reduce((a, p) => a + p.z, 0) / best.length, n: best.length };
|
||||
})();
|
||||
const REAL_ROADS = (plan.streets?.edges?.length || 0) > 200;
|
||||
const REF = REAL_ROADS ? townAnchor : { x: 0, z: 0 }; // synthetic/fixtures → origin, exactly as before
|
||||
const dRef = (x, z) => Math.hypot((x || 0) - REF.x, (z || 0) - REF.z);
|
||||
|
||||
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);
|
||||
return dRef(la?.x, la?.z) - dRef(lb?.x, lb?.z);
|
||||
});
|
||||
// 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]); };
|
||||
@ -75,7 +99,7 @@ export function installDBG(deps) {
|
||||
return { pos: [frontX + fx * dist + rx * off, 1.9, frontZ + fz * dist + rz * off], look: [frontX, 2.0, frontZ] };
|
||||
};
|
||||
const blockKindOfLot = (lot) => plan.blocks?.find((b) => b.id === lot.block)?.kind;
|
||||
const lotsByDist = () => [...(plan.lots || [])].sort((a, b) => Math.hypot(a.x, a.z) - Math.hypot(b.x, b.z));
|
||||
const lotsByDist = () => [...(plan.lots || [])].sort((a, b) => dRef(a.x, a.z) - dRef(b.x, b.z));
|
||||
// nearest (or farthest = "fringe") lot matching pred, else nearest lot overall
|
||||
const pickLot = (pred, farthest = false) => {
|
||||
const L = lotsByDist(); const hit = L.filter(pred);
|
||||
@ -89,11 +113,11 @@ export function installDBG(deps) {
|
||||
const nn = new Map((plan.streets?.nodes || []).map((n) => [n.id, n]));
|
||||
const es = (plan.streets?.edges || []).filter((e) => kinds.includes(e.kind) && nn.get(e.a) && nn.get(e.b));
|
||||
if (!es.length) return crossroadsPose();
|
||||
let best = null, bd = 1e18; // long edge, near the origin (or far, for the fringe)
|
||||
let best = null, bd = 1e18; // long edge, near REF (or far, for the fringe)
|
||||
for (const e of es) {
|
||||
const a = nn.get(e.a), b = nn.get(e.b);
|
||||
const len = Math.hypot(b.x - a.x, b.z - a.z) || 1;
|
||||
const dist = Math.hypot((a.x + b.x) / 2, (a.z + b.z) / 2);
|
||||
const dist = dRef((a.x + b.x) / 2, (a.z + b.z) / 2); // R22: about the town anchor, not the origin
|
||||
const score = (far ? -dist : dist) - len * 0.8; // favour long open runs; `far` picks the outer ring
|
||||
if (score < bd) { bd = score; best = { a, b, len, width: e.width || 8 }; }
|
||||
}
|
||||
@ -111,7 +135,15 @@ export function installDBG(deps) {
|
||||
const crossroadsPose = () => {
|
||||
const edges = plan.streets?.edges || [], nodes = plan.streets?.nodes || [];
|
||||
const deg = new Map(); for (const e of edges) { deg.set(e.a, (deg.get(e.a) || 0) + 1); deg.set(e.b, (deg.get(e.b) || 0) + 1); }
|
||||
let node = null, bd = -1; for (const n of nodes) { const d = deg.get(n.id) || 0; if (d > bd) { bd = d; node = n; } }
|
||||
// busiest node — R22: on a real town, tie-break toward the anchor so we frame the town's own
|
||||
// crossroads, not a highway junction kilometres away. Synthetic REF is the origin ⇒ unchanged.
|
||||
let node = null, bd = -1, bdist = 1e18;
|
||||
for (const n of nodes) {
|
||||
const d = deg.get(n.id) || 0, dist = dRef(n.x, n.z);
|
||||
// strict `>` alone on synthetic/fixtures = the pre-R22 "first max-degree node wins" (identical);
|
||||
// the anchor tie-break only engages on real-roads towns.
|
||||
if (d > bd || (REAL_ROADS && d === bd && dist < bdist)) { bd = d; bdist = dist; node = n; }
|
||||
}
|
||||
if (!node) return pickShop(() => true);
|
||||
const e = edges.find((ed) => ed.a === node.id || ed.b === node.id);
|
||||
const o = e && nodes.find((n) => n.id === (e.a === node.id ? e.b : e.a));
|
||||
@ -123,7 +155,7 @@ export function installDBG(deps) {
|
||||
const stopPose = () => {
|
||||
const stops = busShelterStops(plan);
|
||||
if (!stops.length) return streetViewPose(['main']);
|
||||
const s = [...stops].sort((a, b) => Math.hypot(a.x, a.z) - Math.hypot(b.x, b.z))[0]; // nearest origin = most open
|
||||
const s = [...stops].sort((a, b) => dRef(a.x, a.z) - dRef(b.x, b.z))[0]; // nearest the town anchor (origin on synthetic)
|
||||
const px = Math.sin(s.yaw), pz = Math.cos(s.yaw), ex = Math.cos(s.yaw), ez = -Math.sin(s.yaw);
|
||||
return { pos: [s.x - px * 6 + ex * 4, 2.0, s.z - pz * 6 + ez * 4], look: [s.x, 1.3, s.z] };
|
||||
};
|
||||
@ -192,6 +224,14 @@ export function installDBG(deps) {
|
||||
bookmarks: Object.keys(BOOKMARKS),
|
||||
info,
|
||||
|
||||
// ── R22: the town-anchoring primitive, published for Lane F's release tour (ledger #4) ──
|
||||
// `townAnchor` = the centroid of the densest shop cluster (the retail heart). `anchored` says
|
||||
// whether the bookmarks are using it (real-roads towns) or the origin (synthetic/fixtures —
|
||||
// unchanged behaviour). `clusterPose()` is the shared poser: stand on the road at the densest
|
||||
// cluster and look along it — the same primitive B's bookmarks now resolve through.
|
||||
townAnchor: { ...townAnchor, anchored: REAL_ROADS, ref: { x: REF.x, z: REF.z } },
|
||||
clusterPose: (kinds = ['main']) => streetViewPose(kinds),
|
||||
|
||||
// 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(); },
|
||||
|
||||
@ -222,11 +262,26 @@ export function installDBG(deps) {
|
||||
},
|
||||
|
||||
// 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}` };
|
||||
// [Lane F R22 — Lane G's ask] enterShop takes a SELECTOR, not just an id, so cross-repo QA is a
|
||||
// one-liner on any town without looking ids up first:
|
||||
// DBG.enterShop(116) → by id
|
||||
// DBG.enterShop('record') → first shop of that registry type
|
||||
// DBG.enterShop('The Exchange Hotel')→ by exact name, else name substring (case-insensitive)
|
||||
// Resolution order is most-specific-first so an id-shaped string can never be shadowed by a name.
|
||||
enterShop(sel) {
|
||||
const shops = plan.shops || [];
|
||||
let s = null;
|
||||
if (typeof sel === 'number') s = shops.find((x) => x.id === sel);
|
||||
else if (typeof sel === 'string') {
|
||||
const q = sel.trim().toLowerCase();
|
||||
s = shops.find((x) => String(x.id) === q) // id-as-string
|
||||
|| shops.find((x) => (x.name || '').toLowerCase() === q) // exact name
|
||||
|| shops.find((x) => (x.type || '').toLowerCase() === q) // registry type
|
||||
|| shops.find((x) => (x.name || '').toLowerCase().includes(q)); // name substring
|
||||
}
|
||||
if (!s) return { error: `no shop matching ${JSON.stringify(sel)}` };
|
||||
enterShop(s.id, s.name);
|
||||
return { entered: s.id, name: s.name };
|
||||
return { entered: s.id, name: s.name, type: s.type };
|
||||
},
|
||||
exitShop() { window.dispatchEvent(new CustomEvent('procity:exitShop')); return { exited: true }; },
|
||||
};
|
||||
|
||||