Lane B R41 §41.5: the credits surface — and the game had never shown the OSM attribution it owes
THE CREDITS SURFACE. New web/assets/credits.json (schema procity-credits/1, the data file every lane appends to — documented in LANE_B_NOTES §41 with a paste-ready row) + credits.js (the panel, DOM only) + a HUD link. F2 or the link. Zero draws, zero boot fetches (0 before open, exactly 1 after), zero fetches under ?noassets=1. Seeding it found a LIVE OBLIGATION the game has never displayed: the 23 real-town caches are OpenStreetMap data under ODbL 1.0, whose own source note says keep the credit visible. It now rides the link and is context-aware — on an OSM boot the HUD reads '(c) OpenStreetMap contributors · credits (F2)'; on the synthetic town, where no OSM data is on screen, it does not claim it. three.js r175 MIT credited too. 14 entries seeded, all read off the repo. Field law: licence 'unverified' is legal and a guess is not. Two gaps filed to E as amber rather than invented: the 19-GLB base ped roster has NO per-asset licence record anywhere in this repo (and CITY_SPEC's declared zone includes CC-BY and CGTrader RF — a credit may be owed today), and the 18 house-library fittings still carry AUDIT.md's 38-round-old blanket caveat despite shipping since R3. A's D3 CLOSED. Re-measured over the shipped corpus at the golden seed: 27 towns, 1,838 shops, 81 venue lots, 81 of 81 taking the op-shop signage fallback — exactly 3 per town, in every town. A's number reproduces to the digit. Bite drifted :344 -> :465 -> :483, hence a named resolver: signStyleFor() tries B's nine palettes, then A's registry (gilt/stencil/club, carried since R13), then op-shop as genuine last resort. Venue treatment is venue-gated by construction. Deliberately NOT done: repainting the other nine types onto A's palettes (record would go muted-teal -> hot-pink neon) — an art call for John, filed. RULING 4 RESPECTED, NOTHING SPENT: frozen town, 136 station-yaw samples per arm, 544/544 identical on BOTH counters (draws and tris), 0 console errors. Pin method reproduces R40 exactly — default 282 noon / 291 NIGHT, classic 269 byte-exact. The nine-draw night margin is intact. Classic purity: HUD DOM 2354:8 both arms, fetch surface identical, no #pc-creditlink / overlay / F2 listener / PROCITY.credits. Filed to F: three classic bookmarks are not byte-reproducible across boots even at HEAD (2/5 HEAD-vs-HEAD) — a font race in the sign atlas. A screenshot golden on those cannot be a byte-hash. selfcheck ALL GREEN 157,647/157,647, fingerprint 0x5f76e76 unmoved. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@ -4,6 +4,97 @@
|
|||||||
plus the game shell. It runs on my hand-written fixture **and** auto-integrates with Lane A's
|
plus the game shell. It runs on my hand-written fixture **and** auto-integrates with Lane A's
|
||||||
`generatePlan` (which landed mid-session). Everything below was verified live in a browser.
|
`generatePlan` (which landed mid-session). Everything below was verified live in a browser.
|
||||||
|
|
||||||
|
## Round 41 (§41.5) — the game learns to say where it came from, and 81 pubs stop wearing op-shop paint
|
||||||
|
|
||||||
|
Full measurement detail in `docs/LANES/LANE_B_NOTES.md` §41. Everything below: today's tree, headless
|
||||||
|
fresh contexts, port-isolated no-store servers, pin method (stepwalk 2 m · yaw 0 · laps 2 · 802
|
||||||
|
stations), A/B against a HEAD control tree whose `assets/` `models/` `vendor/` are symlinked to the
|
||||||
|
live tree so another lane's mid-round wave is identical on both arms.
|
||||||
|
|
||||||
|
**1. THE CREDITS SURFACE — the schema is the product; the panel is 260 lines that never decide a licence.**
|
||||||
|
|
||||||
|
`web/assets/credits.json` (schema `procity-credits/1`) is the file **every lane appends to**;
|
||||||
|
`web/js/world/credits.js` renders it; `hud.js setCredits()` puts the way in; `index.html` gets one
|
||||||
|
`[Lane B R41]` dynamic-import block. Reachable on **F2** or the HUD link, bottom-left.
|
||||||
|
|
||||||
|
- **Schema, documented for C/D/E/G in LANE_B_NOTES §41** (with a paste-ready ACCAD row for R42):
|
||||||
|
`id · name · kind · licence · attribution · required · source · used_for · count · files · evidence`
|
||||||
|
(+ optional `flag`, `lane`). Three rules carry it: **`licence:"unverified"` is legal and a guess is
|
||||||
|
not**; **`attribution` is the exact string**, printed verbatim, `null` when nothing is owed;
|
||||||
|
**`required` is the licence question, not an editorial one** — Mixamo/Rokoko are `false` because
|
||||||
|
their licences ask for nothing, CC BY is always `true`. `evidence` must name a file in this repo.
|
||||||
|
- **Seeded with 14 entries read off the repo, nothing invented.** Two carry a **live obligation** the
|
||||||
|
game had never shown: **OpenStreetMap ODbL 1.0** (23 town caches — `SOURCES.md`'s own words are
|
||||||
|
*"keep the © OpenStreetMap contributors credit visible"*) and **three.js r175 MIT**. Plus Mixamo (4
|
||||||
|
clips), Rokoko (4 dance clips), 103 skins, 29 audio assets, the fitting/furniture fleet split by
|
||||||
|
provenance, the GODVERSE stock art, the generation stack, and "no font files ship".
|
||||||
|
**THE ATTRIBUTION RIDES THE LINK:** on an OSM boot it reads `© OpenStreetMap contributors · credits
|
||||||
|
(F2)`; on the synthetic town, where no OSM data is on screen, claiming it would be the opposite of
|
||||||
|
attribution, so it reads plainly.
|
||||||
|
- **Two findings filed to E, both shipped as `unverified` + amber flag rather than a guess:** the
|
||||||
|
**base ped roster (19 GLBs) has no per-asset licence record anywhere in this repo** (CITY_SPEC's
|
||||||
|
declared zone includes CC-BY and CGTrader RF — both can owe attribution, so a credit may be owed
|
||||||
|
*today*); and the **18 house-library fittings still carry AUDIT.md's own 38-round-old blanket
|
||||||
|
caveat** ("record a one-line source/licence per asset before publish"), having been published since
|
||||||
|
R3. AUDIT.md documents props in forensic detail and the people not at all.
|
||||||
|
- **Cost:** zero draws (DOM the composer never renders), **zero boot fetches** — `credits.json` is
|
||||||
|
fetched on FIRST OPEN, asserted 0 before and exactly 1 after — and **zero fetches under
|
||||||
|
`?noassets=1`**, where the panel says so honestly instead of shipping a second copy of the data.
|
||||||
|
|
||||||
|
**2. A's D3 — 81 venue lots, re-measured and fixed.** `SHOP_REGISTRY` carries 9 types and never
|
||||||
|
carried `pub`/`band_room`/`rsl`, so `SHOP_REGISTRY[type] || SHOP_REGISTRY.opshop` dressed every gig
|
||||||
|
venue in op-shop paint. Re-measured over the whole shipped corpus at the golden seed:
|
||||||
|
**27 towns · 1,838 shops · 81 venue lots · 81 of 81 taking the fallback — exactly 3 per town, in
|
||||||
|
every town.** A's R40 number reproduces to the digit. (The bite has drifted `:344`→`:465`→**`:483`**;
|
||||||
|
the line moves, the defect didn't — hence a named resolver, not another literal at a line number.)
|
||||||
|
`buildings.js signStyleFor()`: B's nine shipped palettes → **A's `core/registry.js`** (gilt/stencil/
|
||||||
|
club, carried since R13, and the source for any type A adds next) → op-shop, now genuinely
|
||||||
|
last-resort. Venue treatment = accent frame + top fascia band, **venue-gated by construction**
|
||||||
|
(`accent` exists only on registry-sourced palettes). **Not done, deliberately:** repainting the other
|
||||||
|
nine onto A's palettes — that is an art call for John (record would go muted-teal → hot-pink neon),
|
||||||
|
not a bug fix, and R41 is the round F photographs. Filed. The 28-round-stale `fixture_plan.js` header
|
||||||
|
A clocked is rewritten to say what that table honestly is.
|
||||||
|
|
||||||
|
**3. ZERO DRAWS, measured not claimed** — frozen town on both arms (`roster=v1&pop=0&weather=0&tram=0&
|
||||||
|
magpie=0&washing=0`; every one of those moves on wall-clock time), station lists compared as JSON
|
||||||
|
before either sweep runs:
|
||||||
|
|
||||||
|
| frozen, 136 station×yaw samples/arm | HEAD | R41 | delta |
|
||||||
|
|---|---|---|---|
|
||||||
|
| synthetic **NIGHT** (worst class) | 283 draws / 116,681 tris | **283 / 116,681** | **0 draws AND 0 tris on 136/136** |
|
||||||
|
| synthetic noon | 273 / 96,396 | **273 / 96,396** | **0 and 0 on 136/136** |
|
||||||
|
| `katoomba_real` NIGHT | 167 / 109,208 | **167 / 109,208** | **0 and 0 on 136/136** |
|
||||||
|
| `?classic=1` NIGHT | 264 / 90,578 | **264 / 90,578** | **0 and 0 on 136/136** |
|
||||||
|
|
||||||
|
**544 of 544 samples identical in both counters, 0 console errors on every arm.** Pin method on the
|
||||||
|
treatment tree reproduces R40 exactly: **default 282 noon / 291 NIGHT, classic 269 / 90,580 byte-exact**
|
||||||
|
— **ruling 4's nine-draw night margin is intact and Lane B spent none of it.** The new
|
||||||
|
`core/registry.js` import in `buildings.js` is free: `citygen/index.js → plan.js` already pulls it on
|
||||||
|
every boot, and the classic fetch surface is **121 URLs on both arms, `added []`, `removed []`**.
|
||||||
|
|
||||||
|
**4. CLASSIC IS BYTE-IDENTICAL.** A classic plan carries **0 venue-typed shops of 493**, so the
|
||||||
|
resolver's registry arm is *structurally* unreachable there; the credits module is never fetched
|
||||||
|
(null-provider idiom, no flag test in either file) so there is no `#pc-creditlink`, no overlay, no
|
||||||
|
injected CSS, no F2 listener, no `PROCITY.credits`, and the help line is unchanged. HUD DOM
|
||||||
|
**2354:8 on both arms**. Draw/tri delta **0 on 136/136**. → **F, a finding, not this round's defect:**
|
||||||
|
pixel-hashing five classic bookmarks gives 2/5 matches — and **HEAD-vs-HEAD gives the same 2/5**.
|
||||||
|
`street_noon`/`shopfront_detail`/`crossroads_busy` are not byte-reproducible across boots even at
|
||||||
|
HEAD (likely the sign atlas painting `bold Npx Arial` before the font resolves, in race-dependent
|
||||||
|
chunk order). A screenshot golden on those three cannot be a byte-hash.
|
||||||
|
|
||||||
|
Goldens **157,647/157,647, `0x5f76e76` unmoved** (no plan field is touched — signage is a canvas
|
||||||
|
texture, the credits surface is DOM). `tools/qa/r40_lane_b.py`: **all assertions green**;
|
||||||
|
`tools/flags_check.py`: **GREEN, 0 fails / 0 warns** (no new POST_V2 exception needed — classic never
|
||||||
|
requests either credits file). Shots: `docs/shots/laneB_r41/venue_signage_ab.png` is the contact sheet
|
||||||
|
— **1,088 / 9,645 / 14,160 changed pixels on the three pairs and every one of them inside the sign
|
||||||
|
band, nothing else in the town moves** — plus the full frames and the panel. Honest caveat: the pub is
|
||||||
|
the *least* visible of the three (op-shop `#2c2820` and gilt `#3a2416` are near-neighbour browns), so
|
||||||
|
its read comes mostly from the `#c0392b` accent frame; the band room and RSL are unmistakable.
|
||||||
|
Files: new `web/assets/credits.json`, new
|
||||||
|
`web/js/world/credits.js`, `hud.js`, `buildings.js`, `fixture_plan.js`, `web/index.html`
|
||||||
|
(`[Lane B R41]` marks), `docs/shots/laneB_r41/`, these docs. **No git commands run** (lane treaty;
|
||||||
|
Fable commits).
|
||||||
|
|
||||||
## Round 40 (§40.3) — the ?r= breach GATED at its true number, classic's selector ratified, the fog signed — plus §40.4's arcade kit (Fable-routed)
|
## Round 40 (§40.3) — the ?r= breach GATED at its true number, classic's selector ratified, the fog signed — plus §40.4's arcade kit (Fable-routed)
|
||||||
|
|
||||||
Full measurement detail in `docs/LANES/LANE_B_NOTES.md` §40. Everything below: today's tree,
|
Full measurement detail in `docs/LANES/LANE_B_NOTES.md` §40. Everything below: today's tree,
|
||||||
|
|||||||
@ -1,5 +1,296 @@
|
|||||||
# LANE B — NOTES (measured)
|
# LANE B — NOTES (measured)
|
||||||
|
|
||||||
|
## Round 41 (§41.5) — THE CREDITS SURFACE, and A's D3: 81 venue lots stop wearing op-shop paint
|
||||||
|
|
||||||
|
All numbers this section: today's tree, headless Chromium, **fresh contexts**, port-isolated
|
||||||
|
**no-store** servers, 1280×720, seed 20261990. Budget cells use the **pin method of record**
|
||||||
|
(`budget_walk.py --method stepwalk --stepm 2 --fixed-yaw 0 --laps 2` = 802 stations down the main
|
||||||
|
spine at yaw 0, the worst bearing). ROUND41 ruling 4 is binding — the street budget is spent
|
||||||
|
(291/300 at night, nine draws) — so both items are **zero-draw, measured as an A/B against a control
|
||||||
|
tree**, not asserted.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### §41.5-A — THE CREDITS SURFACE (`credits.js` + `assets/credits.json`)
|
||||||
|
|
||||||
|
PROCITY had **no attribution surface at all**, and two of the obligations were already live:
|
||||||
|
|
||||||
|
- **OpenStreetMap, ODbL 1.0.** `web/assets/towns/SOURCES.md` states the condition in its own words —
|
||||||
|
*"if a rendered/public build surfaces this data, keep the © OpenStreetMap contributors credit
|
||||||
|
visible"* — and 23 town caches ship. Nothing in the running game showed it.
|
||||||
|
- **three.js r175, MIT.** The notice lives in the vendored file header, which no player can reach.
|
||||||
|
|
||||||
|
And it is the gate in front of **4,472 more licence-clean motion clips** on the fleet that R41 could
|
||||||
|
not spend for exactly this reason (ruling 2): **100STYLE** (1,620 BVH, CC BY 4.0 — requires crediting
|
||||||
|
Mason et al. 2022), **CMU** (2,548 BVH — credit requested), **ACCAD** (304 BVH, CC BY 3.0 — requires
|
||||||
|
*"Open Motion Project by ACCAD/The Ohio State University"*). R42 can spend them by appending rows.
|
||||||
|
|
||||||
|
**Where it lives**
|
||||||
|
|
||||||
|
| file | role |
|
||||||
|
|---|---|
|
||||||
|
| `web/assets/credits.json` | **THE DATA FILE — every lane appends here.** Schema `procity-credits/1`. |
|
||||||
|
| `web/js/world/credits.js` | the panel. DOM only. No lane needs to touch it to be credited. |
|
||||||
|
| `web/js/world/hud.js` | `setCredits(credits, {osm})` — the link, bottom-left. Null-provider gated. |
|
||||||
|
| `web/index.html` | one `[Lane B R41]` dynamic-import block + one term in the `unlock` guard. |
|
||||||
|
|
||||||
|
**Reachable:** `F2`, or the HUD link at bottom-left. `Esc`, `F2`, the CLOSE button or a scrim click
|
||||||
|
closes it. Opening frees the pointer (the dig-card / sell-card / F8-note contract), which is why the
|
||||||
|
shell's `unlock` handler carries one more term — opening the credits inside a shop must not walk you
|
||||||
|
back out onto the street.
|
||||||
|
|
||||||
|
**THE ATTRIBUTION RIDES THE LINK.** On an OSM boot the link itself reads
|
||||||
|
`© OpenStreetMap contributors · credits (F2)`; on the synthetic town, where no OSM data is on screen,
|
||||||
|
claiming it would be the opposite of attribution, so it reads `credits & licences (F2)`. The shell
|
||||||
|
passes `osm: PLANSRC === 'osm'`; nothing is sniffed off the plan.
|
||||||
|
|
||||||
|
#### THE SCHEMA — `web/assets/credits.json`, `procity-credits/1` ⟨C · D · E · G: this is your bit⟩
|
||||||
|
|
||||||
|
Append an object to `entries`. **You never touch `credits.js`.** Order inside the file is preserved;
|
||||||
|
the panel only lifts `required: true` rows to the top.
|
||||||
|
|
||||||
|
```jsonc
|
||||||
|
{
|
||||||
|
"id": "one-hundred-style", // required · unique · stable kebab key
|
||||||
|
"name": "100STYLE", // required · what a player reads
|
||||||
|
"kind": "motion", // required · engine|data|motion|mesh|texture|audio|stock|tool|font
|
||||||
|
"licence": "CC BY 4.0", // required · the licence NAME, or the literal "unverified"
|
||||||
|
"attribution": "Mason et al. 2022, …", // required key · the EXACT text to show, or null if none is owed
|
||||||
|
"required": true, // required · true = a licence obligation, false = courtesy/none
|
||||||
|
"source": "…url or tool chain…", // required · where it came from
|
||||||
|
"used_for": "…what in the game…", // required · plain English, player-facing
|
||||||
|
"count": 1620, // required key · 0 if not a count
|
||||||
|
"files": ["models/clips/*.glb"], // required key · repo globs, [] if nothing ships
|
||||||
|
"evidence": "pipeline/AUDIT.md §…", // required · the in-repo file that RECORDS it, or "unverified"
|
||||||
|
"flag": "…", // OPTIONAL · an open licence question a human must answer
|
||||||
|
"lane": "E" // optional · who owns the row
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Field law, and why each one is the way it is:**
|
||||||
|
|
||||||
|
- **`licence: "unverified"` is a legal value and a guess is not.** Two rows ship unverified today
|
||||||
|
(below). A guessed licence is worse than an admitted gap: the gap gets closed, the guess gets cited.
|
||||||
|
- **`attribution` is the exact string, not a description of one.** The panel prints it verbatim inside
|
||||||
|
a box. `null` when nothing is owed — say so explicitly rather than omitting the key.
|
||||||
|
- **`required` is the licence question, not an editorial one.** `true` ⇒ the row sorts to the top and
|
||||||
|
the box is headed ATTRIBUTION REQUIRED. Mixamo and Rokoko are `false` **because their licences ask
|
||||||
|
for nothing** — they are recorded so nobody has to rediscover that. CC BY is always `true`.
|
||||||
|
- **`evidence` must name a file in this repo.** "E told me" is not evidence; `AUDIT.md §…` is.
|
||||||
|
- **`flag` is rendered in amber and counted in the footer.** Use it for a real open question (an
|
||||||
|
unrecorded source, art a human must clear). R25's lesson stands: *the print IS the mechanism* — a
|
||||||
|
flag that only exists in a lane note is a flag nobody sees.
|
||||||
|
|
||||||
|
Paste-ready when R42 spends the CC-BY banks:
|
||||||
|
|
||||||
|
```jsonc
|
||||||
|
{ "id": "accad-mocap", "name": "ACCAD Motion Capture Lab", "kind": "motion",
|
||||||
|
"licence": "CC BY 3.0",
|
||||||
|
"attribution": "Open Motion Project by ACCAD/The Ohio State University",
|
||||||
|
"required": true, "source": "https://accad.osu.edu/research/motion-lab/mocap-system-and-data",
|
||||||
|
"used_for": "…", "count": 304, "files": ["models/clips/*.glb"],
|
||||||
|
"evidence": "pipeline/AUDIT.md §R42", "lane": "E" }
|
||||||
|
```
|
||||||
|
|
||||||
|
#### What seeded it — 14 entries, read off this repo, nothing invented
|
||||||
|
|
||||||
|
| entry | licence | attribution required? |
|
||||||
|
|---|---|---|
|
||||||
|
| OpenStreetMap (23 town caches) | ODbL 1.0 | ✅ **YES** — © OpenStreetMap contributors |
|
||||||
|
| three.js r175 (vendored + 5 addons) | MIT | ✅ **YES** — © 2010–2025 three.js authors |
|
||||||
|
| Adobe Mixamo — `idle`/`walk`/`sit`/`look` | Mixamo royalty-free | no |
|
||||||
|
| Rokoko free mocap — the 4 dance clips | royalty-free incl. commercial | no |
|
||||||
|
| Citizen bodies, base roster (17+2) | **unverified** | **unknown — flagged** |
|
||||||
|
| Citizen bodies, the DJ five | on-device mesh + Mixamo auto-rig | no |
|
||||||
|
| Texture skins (103 in `assets/gen/`) | original, `flux_local` on-device | no |
|
||||||
|
| Sound and music (29 keyed, 34 files) | original, procedural synthesis | no |
|
||||||
|
| Fittings/furniture — house library (18) | **unverified** | **unknown — flagged** |
|
||||||
|
| Fittings/furniture — generated here (14) | original (CC0-equivalent) | no |
|
||||||
|
| Three props via fal-Hunyuan | original (~$0.64, the only outside spend) | no |
|
||||||
|
| GODVERSE stock art (15 atlases) | in-house only | **flagged — not cleared for release** |
|
||||||
|
| The generation stack (FLUX.2-klein-4B Apache-2.0, TRELLIS.2, RMBG-2.0) | see row | no |
|
||||||
|
| Typefaces | n/a — **no font files ship** (canvas text, house law) | no |
|
||||||
|
|
||||||
|
Sources: `web/assets/towns/SOURCES.md`, `pipeline/AUDIT.md` (R3 hero props · QUARANTINE + its blanket
|
||||||
|
caveat · R11/R12/R13 audio · R36 w1 · R38 §2/§3/§4/§6 · R39 §5/§6), `docs/LANES/LANE_E_NOTES.md`
|
||||||
|
(the by-source-zone provenance table), `web/assets/manifest.json`, the vendored three.js headers.
|
||||||
|
|
||||||
|
**→ LANE E, two findings the seeding turned up.** Both are recorded in the file as `unverified` +
|
||||||
|
`flag`, which is honest, but neither is *closed*:
|
||||||
|
|
||||||
|
1. **The base ped roster (19 GLBs) has no per-asset licence record anywhere in this repo.** They are
|
||||||
|
sha1 copies from 90sDJsim; `docs/CITY_SPEC.md` ("NPC contract") declares the zone as *"web-ok only
|
||||||
|
(Mixamo, CC0/CC-BY, CGTrader RF)"* — and **CC-BY and CGTrader RF can both carry an attribution
|
||||||
|
obligation**, so if one of the 19 is CC-BY the credit is owed *today* and unrecorded. These are the
|
||||||
|
most-seen assets in the game. AUDIT.md documents props in forensic detail and the people not at all.
|
||||||
|
2. **The 18 house-library fittings/furniture carry AUDIT.md's own blanket caveat** ("no explicit
|
||||||
|
licence/provenance record … before `publish.py` pushes to the shared CDN, record a one-line
|
||||||
|
source/licence per asset"). They have been published since R3. The caveat is 38 rounds old.
|
||||||
|
|
||||||
|
Neither is Lane B's to close, and neither blocks R41. Both now have a place to be closed *into*.
|
||||||
|
|
||||||
|
#### Cost — zero, by construction and by measurement
|
||||||
|
|
||||||
|
- **Zero draws, structurally.** Everything is DOM: no geometry, no material, no texture, no pass. The
|
||||||
|
renderer cannot see it (the R19 selector / R39 fog-map mechanism).
|
||||||
|
- **Zero boot fetches.** `credits.json` is fetched on **FIRST OPEN**, never at boot: a boot that never
|
||||||
|
opens the panel makes **0** requests for it (asserted). Opening it makes **exactly 1**.
|
||||||
|
- **Zero fetches under `?noassets=1`** — the asset law forbids it, so the panel says so honestly
|
||||||
|
rather than fetching anyway or shipping a second built-in copy of the data.
|
||||||
|
- **Classic-gated BY CONSTRUCTION, no flag test in either file.** The shell dynamic-imports
|
||||||
|
`credits.js` only when `!CLASSIC`; classic therefore fetches **neither `credits.js` nor
|
||||||
|
`credits.json`**, has **no `#pc-creditlink`**, **no overlay**, **no injected CSS**, **no `F2`
|
||||||
|
listener**, **no `PROCITY.credits`**, and an **unchanged help line** — all asserted on a live boot.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### §41.5-B — A's D3: the venue signage, re-measured at 81 and fixed
|
||||||
|
|
||||||
|
**The bite, re-measured on today's tree** (golden seed 20261990, gigs on, the whole shipped corpus =
|
||||||
|
synthetic + 3 osm fixtures + 23 town caches):
|
||||||
|
|
||||||
|
| | |
|
||||||
|
|---|---|
|
||||||
|
| towns | **27** |
|
||||||
|
| shops | **1,838** |
|
||||||
|
| venue lots (`pub` · `band_room` · `rsl`) | **81** — exactly **3 per town, in every town** |
|
||||||
|
| of those, falling through to op-shop signage | **81 of 81** |
|
||||||
|
| types missing from `SHOP_REGISTRY` | `band_room`, `pub`, `rsl` |
|
||||||
|
| `core/registry.js SHOP_TYPES` vs `fixture_plan.js SHOP_REGISTRY` | **12 vs 9** |
|
||||||
|
|
||||||
|
**A's R40 filing said 81; it reproduces to the digit.** The bite has drifted `:344` (A, R37) →
|
||||||
|
`:465` (A, R40) → **`:483`** on today's tree — the line moves, the defect didn't, which is why the fix
|
||||||
|
is a named resolver and not another literal at a line number.
|
||||||
|
|
||||||
|
**The fix — one resolver, explicit precedence, `buildings.js signStyleFor(type)`:**
|
||||||
|
|
||||||
|
1. `SHOP_REGISTRY[type]` — the **nine palettes Lane B has shipped since v1**, returned unchanged.
|
||||||
|
2. `SHOP_TYPES[type].sign` — **A's `core/registry.js`**, which has carried gilt (pub) / stencil
|
||||||
|
(band_room) / club (rsl) since R13, and is the source for **any type A adds next**.
|
||||||
|
3. `SHOP_REGISTRY.opshop` — a type in *neither* table. Unreachable for anything the generator emits.
|
||||||
|
|
||||||
|
**The venue treatment:** the sign frame is redrawn in the registry's `accent` and a fascia band runs
|
||||||
|
the top inside edge (a pub's painted head-board). `accent` exists **only** on registry-sourced
|
||||||
|
palettes, so this is **venue-gated by construction** — the nine cannot grow one by accident — and the
|
||||||
|
band sits at the TOP so it can never collide with the R26 crate mark at the sign's foot.
|
||||||
|
|
||||||
|
**What I deliberately did NOT do.** A's filing suggests `SHOP_REGISTRY` shrink to a fallback-only
|
||||||
|
table and all signage read A's palettes. That repaints the **whole town**: `record` would go from
|
||||||
|
muted teal `#1f2e2b` to hot-pink neon `#141428`/`#ff5470`, `stall` from dark to light `#e8e2d0`,
|
||||||
|
`pawn` from yellow-on-red to gold-on-black. That is an **art call for John**, not a bug fix, and R41 is
|
||||||
|
the round whose whole point is visible — landing it unasked inside a signage bug fix would put an
|
||||||
|
unrequested repaint into every tour frame F shoots. **Filed for the playtest session:** *unify the
|
||||||
|
nine sign palettes onto A's registry, yes or no?* One `signStyleFor` line either way.
|
||||||
|
`fixture_plan.js`'s header (the "registry.js hasn't landed yet" line A clocked at 25 rounds — it is
|
||||||
|
now 28) is rewritten to say what that table honestly is.
|
||||||
|
|
||||||
|
**Proof — a true A/B, control vs treatment, same poses, same frozen town:**
|
||||||
|
**`docs/shots/laneB_r41/venue_signage_ab.png`** is the contact sheet (crops are the *measured* diff
|
||||||
|
bounding box, not a hand-picked region); the full frames are
|
||||||
|
`{before,after}_{pub,band_room,rsl}_signage.png` + `_venue_night.png`. The control is a HEAD copy of
|
||||||
|
`web/` with `assets/`, `models/` and `vendor/` **symlinked to the live tree** (so another lane's
|
||||||
|
mid-round wave is identical on both arms), served on its own no-store port. The town is frozen on
|
||||||
|
both arms so the pair is strictly comparable.
|
||||||
|
|
||||||
|
| pair | changed pixels | where |
|
||||||
|
|---|---|---|
|
||||||
|
| pub — The Exchange Hotel | **1,088** (0.12% of frame) | **rows 336–365 only** |
|
||||||
|
| band_room — The Vinyl Shed | **9,645** (1.05%) | **rows 349–363 only** |
|
||||||
|
| rsl — Wangaratta RSL | **14,160** (1.54%) | **rows 336–365 only** |
|
||||||
|
|
||||||
|
**Every changed pixel in all three frames is inside the sign band. Nothing else in the town moves.**
|
||||||
|
|
||||||
|
**An honest note on the pub**, the venue F will photograph most: it is the *least* visible of the
|
||||||
|
three, because op-shop `#2c2820` and the registry's gilt `#3a2416` are near-neighbour dark browns and
|
||||||
|
cream `#f0e2c0` sits close to gilt `#f2d98a`. **The pub's read comes mostly from the `#c0392b` accent
|
||||||
|
frame** — which is a large part of why the venue treatment exists at all rather than a bare palette
|
||||||
|
swap. The band room (black + `#7ac74f` green) and the RSL (navy `#0f2d4a` + `#c9a227` gold) are
|
||||||
|
unmistakable at a glance.
|
||||||
|
|
||||||
|
**One measurement artefact found and fixed, worth recording:** the first A/B run showed a second diff
|
||||||
|
region at rows 670–707 on the RSL pair. It was **the v7 game bar** — `#pc-game` is built LAZILY on a
|
||||||
|
later frame, so a per-element `display:none` hide races it and one arm keeps the bar. Hiding via an
|
||||||
|
injected `!important` stylesheet at boot catches whatever is built afterwards. A hide that races the
|
||||||
|
thing it hides will quietly put a UI element into your "geometry changed" number.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### §41.5-C — THE BUDGET: nothing was spent (ruling 4)
|
||||||
|
|
||||||
|
**1. A/B against the HEAD control — the town FROZEN on both arms.** `roster=v1&pop=0` (no citizens) +
|
||||||
|
`weather=0&tram=0&magpie=0&washing=0`, because every one of those moves on **wall-clock** time
|
||||||
|
(R38's recorded trap) and a live town measured twice differs from itself. `gigs` stays ON — the venues
|
||||||
|
are the point. Station lists are computed from the plan on each arm and compared as JSON **before**
|
||||||
|
either sweep runs, so a drifted list fails the run instead of quietly voiding the comparison.
|
||||||
|
34 stations × 4 yaws = **136 samples per arm**:
|
||||||
|
|
||||||
|
| frozen town, 136 station×yaw samples/arm | HEAD worst | R41 worst | per-sample delta |
|
||||||
|
|---|---|---|---|
|
||||||
|
| synthetic, **NIGHT** (the budget's worst class) | 283 draws / 116,681 tris | **283 / 116,681** | **0 draws AND 0 tris on 136/136** |
|
||||||
|
| synthetic, noon | 273 / 96,396 | **273 / 96,396** | **0 and 0 on 136/136** |
|
||||||
|
| `katoomba_real`, NIGHT | 167 / 109,208 | **167 / 109,208** | **0 and 0 on 136/136** |
|
||||||
|
| `?classic=1`, NIGHT | 264 / 90,578 | **264 / 90,578** | **0 and 0 on 136/136** |
|
||||||
|
|
||||||
|
**544 of 544 samples identical in both counters, across four boot classes. 0 console errors on
|
||||||
|
either arm, every arm.**
|
||||||
|
|
||||||
|
**2. The pin method of record, on the treatment tree** — and it reproduces R40 exactly:
|
||||||
|
|
||||||
|
| synthetic, pin method (802 stations, yaw 0, laps 2) | R40 pin | R41 measured | law |
|
||||||
|
|---|---|---|---|
|
||||||
|
| default, noon (seg 2) | 282 | **282** / 100,346 tris | ≤300 ✅ |
|
||||||
|
| default, **NIGHT** (seg 5) | 291 | **291** / 120,093 tris | ≤300 ✅ — **the nine-draw margin is intact** |
|
||||||
|
| `?classic=1`, NIGHT | 269 / 90,580 | **269 / 90,580** | ≤300 ✅ **byte-exact** |
|
||||||
|
|
||||||
|
**Margin at night is still 9 draws. Lane B spent 0 of them.**
|
||||||
|
|
||||||
|
**3. The new import is free.** `buildings.js` now imports `SHOP_TYPES` from `core/registry.js` — which
|
||||||
|
`citygen/index.js → plan.js` already pulls on **every** boot including classic, so it adds **zero
|
||||||
|
fetches**: the classic fetch surface is **121 URLs on both arms, `added []`, `removed []`**.
|
||||||
|
|
||||||
|
### §41.5-D — CLASSIC PURITY, and a pre-existing non-determinism it exposed
|
||||||
|
|
||||||
|
| classic evidence | result |
|
||||||
|
|---|---|
|
||||||
|
| plan carries venue types | **0 of 493 shops** — the resolver's registry arm is *structurally* unreachable in classic |
|
||||||
|
| draws / tris, 136 station×yaw samples | **0 delta on 136/136**, both counters |
|
||||||
|
| pin, NIGHT | **269 / 90,580** — byte-exact to the R38/R40 pin |
|
||||||
|
| fetch surface | **121 URLs, added `[]`, removed `[]`** — `credits.js`/`credits.json` never requested |
|
||||||
|
| HUD DOM (`innerHTML` length : body children) | **2354:8 on both arms — IDENTICAL** |
|
||||||
|
| `#pc-creditlink` · `#pc-credits` · `#pc-credits-style` · `PROCITY.credits` · help-line text | **absent / absent / absent / absent / unchanged** |
|
||||||
|
|
||||||
|
**→ LANE F, a finding, not a defect of this round.** I also pixel-hashed five classic bookmark frames
|
||||||
|
(sha256 of the PNG) and only **2 of 5** matched — so I ran **HEAD against HEAD** as the noise floor,
|
||||||
|
and it is **the same 2 of 5**. `street_noon`, `shopfront_detail` and `crossroads_busy` are **not
|
||||||
|
byte-reproducible across boots at HEAD**, with citizens/weather/tram/washing/magpie all off and a
|
||||||
|
2.5 s settle; `night_neon` and `market_square` are rock-solid on every run. The likely mechanism is
|
||||||
|
the **sign atlas**: `buildings.js` paints `bold ${fs}px Arial` into a canvas while chunks build in a
|
||||||
|
race-dependent order, so a cell drawn before the font resolves gets different metrics. Consequence
|
||||||
|
for you: **a screenshot golden pinned on those three bookmarks cannot be a byte-hash** — it needs a
|
||||||
|
perceptual/threshold compare, or the atlas needs a font-ready await first. My treatment sits exactly
|
||||||
|
at the noise floor, which is why the draw/tri/DOM/fetch evidence above is the load-bearing part.
|
||||||
|
|
||||||
|
### §41.5-E — gates
|
||||||
|
|
||||||
|
- `node web/js/citygen/selfcheck.js` → **ALL GREEN 157,647/157,647**, fingerprint **`0x5f76e76`
|
||||||
|
UNMOVED**. No plan golden can move: signage is a canvas texture and the credits surface is DOM —
|
||||||
|
neither writes a plan field.
|
||||||
|
- `tools/qa/r40_lane_b.py` → all assertions green (the R40 rulings still hold under R41: default
|
||||||
|
declares 300/not-diagnostic, classic's selector shows 27 options with the one named towns-fetch
|
||||||
|
exception, `?r=3` declares 420 and measures 386 ≤ 420 while still > 300).
|
||||||
|
- `tools/flags_check.py` → **GREEN, 0 fails / 0 warns.** No new `POST_V2_EXCEPTIONS` entry is needed:
|
||||||
|
that gate inspects the **classic** fetch surface, and classic requests neither credits file.
|
||||||
|
- The credits surface asserted live on four boot classes (default / classic / `?noassets=1` /
|
||||||
|
`plansrc=osm`): 14 entries render, 2 required-attribution boxes, 3 open-licence-question boxes, the
|
||||||
|
ODbL string on screen verbatim, `licence unverified` shown as such, exactly 1 `credits.json` fetch
|
||||||
|
and only on first open, **0 console errors on every arm**.
|
||||||
|
|
||||||
|
**Files:** new `web/assets/credits.json` · new `web/js/world/credits.js` · `web/js/world/hud.js` ·
|
||||||
|
`web/js/world/buildings.js` · `web/js/world/fixture_plan.js` · `web/index.html` (`[Lane B R41]`
|
||||||
|
marks) · `docs/shots/laneB_r41/` · these docs. **No git commands run** (lane treaty; Fable commits).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## Round 40 (§40.3 + routed §40.4) — the ?r= flag learns its own law, classic keeps its shop window, and the fog gets its signage
|
## Round 40 (§40.3 + routed §40.4) — the ?r= flag learns its own law, classic keeps its shop window, and the fog gets its signage
|
||||||
|
|
||||||
All numbers this section: today's tree, headless Chromium, fresh contexts, port-isolated no-store
|
All numbers this section: today's tree, headless Chromium, fresh contexts, port-isolated no-store
|
||||||
|
|||||||
BIN
docs/shots/laneB_r41/after_band_room_signage.png
Normal file
|
After Width: | Height: | Size: 782 KiB |
BIN
docs/shots/laneB_r41/after_pub_signage.png
Normal file
|
After Width: | Height: | Size: 656 KiB |
BIN
docs/shots/laneB_r41/after_rsl_signage.png
Normal file
|
After Width: | Height: | Size: 725 KiB |
BIN
docs/shots/laneB_r41/after_venue_night.png
Normal file
|
After Width: | Height: | Size: 366 KiB |
BIN
docs/shots/laneB_r41/before_band_room_signage.png
Normal file
|
After Width: | Height: | Size: 782 KiB |
BIN
docs/shots/laneB_r41/before_pub_signage.png
Normal file
|
After Width: | Height: | Size: 656 KiB |
BIN
docs/shots/laneB_r41/before_rsl_signage.png
Normal file
|
After Width: | Height: | Size: 725 KiB |
BIN
docs/shots/laneB_r41/before_venue_night.png
Normal file
|
After Width: | Height: | Size: 366 KiB |
BIN
docs/shots/laneB_r41/credits_link_osm.png
Normal file
|
After Width: | Height: | Size: 768 KiB |
BIN
docs/shots/laneB_r41/credits_panel_flags.png
Normal file
|
After Width: | Height: | Size: 460 KiB |
BIN
docs/shots/laneB_r41/credits_panel_top.png
Normal file
|
After Width: | Height: | Size: 433 KiB |
BIN
docs/shots/laneB_r41/venue_signage_ab.png
Normal file
|
After Width: | Height: | Size: 148 KiB |
208
web/assets/credits.json
Normal file
@ -0,0 +1,208 @@
|
|||||||
|
{
|
||||||
|
"schema": "procity-credits/1",
|
||||||
|
"updated": "2026-08-07",
|
||||||
|
"round": 41,
|
||||||
|
"owner": "Lane B builds the surface; EVERY lane owns its own entries. Schema + how to append: docs/LANES/LANE_B_NOTES.md §41.",
|
||||||
|
"notice": "PROCITY is built from third-party libraries, open data and generated art. Everything below ships in this build. Entries marked REQUIRED carry an attribution the licence obliges us to show; the rest are recorded so the obligation can never arrive unnoticed. Where a source could not be established from this repo it says \"unverified\" — never a guess.",
|
||||||
|
"entries": [
|
||||||
|
{
|
||||||
|
"id": "openstreetmap",
|
||||||
|
"name": "OpenStreetMap",
|
||||||
|
"kind": "data",
|
||||||
|
"licence": "ODbL 1.0",
|
||||||
|
"attribution": "© OpenStreetMap contributors — data available under the Open Database License (ODbL) 1.0",
|
||||||
|
"required": true,
|
||||||
|
"source": "https://www.openstreetmap.org/copyright · fetched via the Overpass API (overpass-api.de), bounded per-town bbox",
|
||||||
|
"used_for": "Every real Australian town: 21 town caches (1,196 shops) plus their real street geometry, and the two GODVERSE towns built on the same road data. When you walk Fitzroy, those are Fitzroy's streets.",
|
||||||
|
"count": 23,
|
||||||
|
"files": ["assets/towns/*.json", "assets/towns/_raw/*.json"],
|
||||||
|
"evidence": "web/assets/towns/SOURCES.md · pipeline/build_towns.py · license/attribution fields on every cache",
|
||||||
|
"lane": "E"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "three-js",
|
||||||
|
"name": "three.js r175",
|
||||||
|
"kind": "engine",
|
||||||
|
"licence": "MIT",
|
||||||
|
"attribution": "Copyright © 2010–2025 three.js authors — MIT License",
|
||||||
|
"required": true,
|
||||||
|
"source": "https://threejs.org/ · vendored into web/vendor/ (no CDN at runtime)",
|
||||||
|
"used_for": "The renderer, the scene graph, the animation system, and the addons the shell uses: EffectComposer/RenderPass/UnrealBloomPass/OutputPass, GLTFLoader, PointerLockControls, BufferGeometryUtils, SkeletonUtils.",
|
||||||
|
"count": 1,
|
||||||
|
"files": ["vendor/three.module.js", "vendor/three.core.js", "vendor/addons/**"],
|
||||||
|
"evidence": "SPDX-License-Identifier: MIT in the vendored file headers · REVISION '175' in vendor/three.core.js",
|
||||||
|
"lane": "B"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "mixamo-clips",
|
||||||
|
"name": "Adobe Mixamo — animation clips",
|
||||||
|
"kind": "motion",
|
||||||
|
"licence": "Adobe Mixamo royalty-free licence (use inside a game is permitted; redistributing the clips as clips is not)",
|
||||||
|
"attribution": null,
|
||||||
|
"required": false,
|
||||||
|
"source": "Adobe Mixamo (Sitting_Idle, Looking_Around and the walk/idle family), converted mesh-free by pipeline fbx_to_clip.py",
|
||||||
|
"used_for": "Four of the eight motion clips every citizen in the town plays: idle, walk, sit (the seated drummer), look (the street glance). Motion only — the skeleton animation is retargeted onto PROCITY's own characters; no Mixamo geometry ships.",
|
||||||
|
"count": 4,
|
||||||
|
"files": ["models/peds/idle.glb", "models/peds/walk.glb", "models/peds/sit.glb", "models/peds/look.glb"],
|
||||||
|
"evidence": "docs/LANES/LANE_E_NOTES.md — provenance table, rows ⟨v3.1⟩ and ⟨v5.1⟩",
|
||||||
|
"lane": "E"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "rokoko-mocap",
|
||||||
|
"name": "Rokoko — free motion-capture library",
|
||||||
|
"kind": "motion",
|
||||||
|
"licence": "Rokoko free-pack licence — royalty-free including commercial use",
|
||||||
|
"attribution": null,
|
||||||
|
"required": false,
|
||||||
|
"source": "rokoko.com free 263-asset mocap pack (Rokoko Studio Mocap / Dancing), baked mesh-free",
|
||||||
|
"used_for": "The four dance clips the pub crowd and the DJs play: dance_party, dance_medium, dance_drink, dance_sway.",
|
||||||
|
"count": 4,
|
||||||
|
"files": ["models/peds/dance_*.glb"],
|
||||||
|
"evidence": "pipeline/AUDIT.md — 'Round 36 wave 1', the measured zone correction (these are Rokoko, not Mixamo, despite the _mixamo skeleton suffix)",
|
||||||
|
"lane": "E"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "ped-bodies-base",
|
||||||
|
"name": "Citizen bodies — the base roster (17 normal + 2 comical)",
|
||||||
|
"kind": "mesh",
|
||||||
|
"licence": "unverified",
|
||||||
|
"attribution": null,
|
||||||
|
"required": false,
|
||||||
|
"source": "Copied byte-identical from the house game 90sDJsim (web/world/models/peds/); the canonical originals live outside this repo.",
|
||||||
|
"used_for": "Every walking citizen you meet on the street and every shopkeeper behind a counter.",
|
||||||
|
"count": 19,
|
||||||
|
"files": ["models/peds/man_*.glb", "models/peds/woman_*.glb", "models/peds/comical_*.glb"],
|
||||||
|
"evidence": "unverified",
|
||||||
|
"flag": "NO PER-ASSET LICENCE RECORD EXISTS IN THIS REPO. docs/CITY_SPEC.md ('NPC contract') declares the roster's zone as 'web-ok only (Mixamo, CC0/CC-BY, CGTrader RF)' — and CC-BY and CGTrader RF can both carry an attribution obligation, so if any one of these 19 is CC-BY the credit is owed and unrecorded. Lane E: resolve per asset against the 90sDJsim/character_kit sources before any public release.",
|
||||||
|
"lane": "E"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "ped-bodies-dj",
|
||||||
|
"name": "Citizen bodies — the DJ five",
|
||||||
|
"kind": "mesh",
|
||||||
|
"licence": "Original work (CC0-equivalent mesh) + Adobe Mixamo auto-rig output (royalty-free)",
|
||||||
|
"attribution": null,
|
||||||
|
"required": false,
|
||||||
|
"source": "John's own DJ characters: an on-device FLUX.2-klein-4B concept sheet → TRELLIS.2 mesh on this box → Mixamo auto-rig → MESHGOD fbx_to_ped.py",
|
||||||
|
"used_for": "The dance-scene bodies in the venues (not in the ?classic=1 crowd).",
|
||||||
|
"count": 5,
|
||||||
|
"files": ["models/peds/dj_*.glb", "models/peds/woman_dj_*.glb"],
|
||||||
|
"evidence": "docs/LANES/LANE_E_NOTES.md — provenance table, row 'Ped bodies ⟨v7.0⟩' · pipeline/AUDIT.md 'Round 36 wave 1'",
|
||||||
|
"lane": "E"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "gen-skins",
|
||||||
|
"name": "Texture skins — facades, skies, grounds, wallpapers, interiors, posters",
|
||||||
|
"kind": "texture",
|
||||||
|
"licence": "Original work (CC0-equivalent)",
|
||||||
|
"attribution": null,
|
||||||
|
"required": false,
|
||||||
|
"source": "Generated on this machine by pipeline/gen_skins.py driving MODELBEAST flux_local (FLUX.2-klein-4B), plus ports of the house games 90sDJsim and thriftgod. Prompted with 'no text, no brands, no people'.",
|
||||||
|
"used_for": "Every shopfront in the town, every sky, every footpath and road, every interior wall and floor, and the gig posters.",
|
||||||
|
"count": 103,
|
||||||
|
"files": ["assets/gen/*.jpg"],
|
||||||
|
"evidence": "pipeline/AUDIT.md (§ skins, R38 §3, R39 §5) · docs/LANES/LANE_E_NOTES.md provenance table",
|
||||||
|
"lane": "E"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "audio-pack",
|
||||||
|
"name": "Sound and music",
|
||||||
|
"kind": "audio",
|
||||||
|
"licence": "Original work (CC0-equivalent)",
|
||||||
|
"attribution": null,
|
||||||
|
"required": false,
|
||||||
|
"source": "100% procedural synthesis — pipeline/gen_audio.py, numpy oscillators and noise renders through ffmpeg. No sample library, no recording, no neural model.",
|
||||||
|
"used_for": "Street and room ambience, the shop bells and till and door and footsteps, the tram, the magpie and the mower, and every music bed including the three gig genres.",
|
||||||
|
"count": 29,
|
||||||
|
"files": ["assets/audio/*.ogg", "assets/audio/*.m4a"],
|
||||||
|
"evidence": "pipeline/AUDIT.md — 'Round 11 audio pack provenance', R12, R13, R38 §2",
|
||||||
|
"lane": "E"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "props-house-library",
|
||||||
|
"name": "Shop fittings and street furniture — the house library set",
|
||||||
|
"kind": "mesh",
|
||||||
|
"licence": "unverified",
|
||||||
|
"attribution": null,
|
||||||
|
"required": false,
|
||||||
|
"source": "Generic props pre-sorted into John's house 3D library (benches, shelving, counter, crate, bin, streetlight, food cart …), normalized to the house GLB law by pipeline/normalize.py.",
|
||||||
|
"used_for": "The benches, bins, streetlights and bus stops on the footpath, and the shelving, racks, counters and crates inside the shops.",
|
||||||
|
"count": 18,
|
||||||
|
"files": ["manifest.json → fittings / furniture (served from the 3GOD depot)"],
|
||||||
|
"evidence": "pipeline/AUDIT.md — 'Normalized set' + the blanket caveat under QUARANTINE",
|
||||||
|
"flag": "No explicit per-asset licence record. All 18 source filenames were checked against known trademarks (0 hits) and branded/designer pieces were quarantined out of the manifest, but 'unclear ⇒ quarantine' is the house rule: record a one-line source per asset before any public release.",
|
||||||
|
"lane": "E"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "props-on-device",
|
||||||
|
"name": "Shop fittings, hero props and gig instruments — generated here",
|
||||||
|
"kind": "mesh",
|
||||||
|
"licence": "Original work (CC0-equivalent)",
|
||||||
|
"attribution": null,
|
||||||
|
"required": false,
|
||||||
|
"source": "MODELBEAST flux_local (FLUX.2-klein-4B) concept → RMBG-2.0 cutout → TRELLIS.2 reconstruction → pipeline/normalize.py + bake_lowpoly.py / decimate_props.py. All on this box, no cloud key, $0.",
|
||||||
|
"used_for": "The listening booth, drinks fridge, arcade cabinet, milkshake mixer and novelty record; the five band instruments on every stage; the magpie, the paling fence and the water tank in the backyards.",
|
||||||
|
"count": 14,
|
||||||
|
"files": ["manifest.json → fittings / furniture (served from the 3GOD depot)"],
|
||||||
|
"evidence": "pipeline/AUDIT.md — 'Provenance — round-3 hero props', R12/R13 gig instruments, R38 §4, R39 §6",
|
||||||
|
"lane": "E"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "props-fal-hunyuan",
|
||||||
|
"name": "Three props generated through fal.ai",
|
||||||
|
"kind": "mesh",
|
||||||
|
"licence": "Original work",
|
||||||
|
"attribution": null,
|
||||||
|
"required": false,
|
||||||
|
"source": "An on-device flux_local concept image reconstructed by fal-ai/hunyuan3d/v2 — the only outside spend in the project, about $0.64 in total.",
|
||||||
|
"used_for": "The glass display case, the magazine rack and the bus shelter (TRELLIS could not hold their thin or transparent geometry).",
|
||||||
|
"count": 3,
|
||||||
|
"files": ["manifest.json → fittings / furniture (served from the 3GOD depot)"],
|
||||||
|
"evidence": "pipeline/AUDIT.md — 'Round 5 — E3: fal.ai fallback props'",
|
||||||
|
"lane": "E"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "godverse-stock",
|
||||||
|
"name": "Record, book and toy cover art in the crates",
|
||||||
|
"kind": "stock",
|
||||||
|
"licence": "In-house only — cleared for this build, NOT cleared for public release",
|
||||||
|
"attribution": null,
|
||||||
|
"required": false,
|
||||||
|
"source": "Product photographs from the real shop (Monster Robot Party) via the recordgod point-of-sale, and from real dealgod seller listings — the sellers' own product shots, baked into atlases by pipeline/godverse_stock.py.",
|
||||||
|
"used_for": "The sleeves you flip past in a record bin and the spines and boxes on the shelves, when the stock layer is on.",
|
||||||
|
"count": 15,
|
||||||
|
"files": ["assets/stock_godverse/*/"],
|
||||||
|
"evidence": "docs/LANES/LANE_E_NOTES.md — provenance table, rows 'Real-stock atlas' and 'Mint atlases' (both amber)",
|
||||||
|
"flag": "FLAG BEFORE ANY PUBLIC OR COMMERCIAL RELEASE. These are real photographs taken by real sellers; minting changes whose stock the game claims it is, never whose photograph it is. Clearing them is a human decision, not a gate's.",
|
||||||
|
"lane": "G"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "gen-models",
|
||||||
|
"name": "The on-device generation stack",
|
||||||
|
"kind": "tool",
|
||||||
|
"licence": "FLUX.2-klein-4B — Apache-2.0 · TRELLIS.2 and RMBG-2.0 — used as reconstruction/segmentation tools, no model weights ship",
|
||||||
|
"attribution": null,
|
||||||
|
"required": false,
|
||||||
|
"source": "MODELBEAST, running locally on an Apple M3 Ultra. Nothing was downloaded from an asset pool and no cloud key was used except the one noted above.",
|
||||||
|
"used_for": "Not shipped in the game — recorded because it is what produced the skins, meshes and props credited above, and a generator's licence is where an obligation would hide.",
|
||||||
|
"count": 3,
|
||||||
|
"files": [],
|
||||||
|
"evidence": "pipeline/AUDIT.md — R38 §6, R39 §5 (licence tables) · pipeline/README.md",
|
||||||
|
"lane": "E"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "fonts",
|
||||||
|
"name": "Typefaces",
|
||||||
|
"kind": "font",
|
||||||
|
"licence": "n/a — no font files ship",
|
||||||
|
"attribution": null,
|
||||||
|
"required": false,
|
||||||
|
"source": "House law: canvas text only, no font files. Every sign, poster and menu is drawn with the operating system's own font stack (Arial / -apple-system / Segoe UI / Roboto), so nothing is embedded or redistributed.",
|
||||||
|
"used_for": "Every shop sign in the town, the band names on the posters, and this panel.",
|
||||||
|
"count": 0,
|
||||||
|
"files": [],
|
||||||
|
"evidence": "README.md — 'House laws' (canvas text, no font files)",
|
||||||
|
"lane": "B"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@ -13,6 +13,7 @@ import * as THREE from 'three';
|
|||||||
import { mergeGeometries } from 'three/addons/utils/BufferGeometryUtils.js';
|
import { mergeGeometries } from 'three/addons/utils/BufferGeometryUtils.js';
|
||||||
import { rng, pick, frange, irange } from '../core/prng.js';
|
import { rng, pick, frange, irange } from '../core/prng.js';
|
||||||
import { SHOP_REGISTRY } from './fixture_plan.js';
|
import { SHOP_REGISTRY } from './fixture_plan.js';
|
||||||
|
import { SHOP_TYPES } from '../core/registry.js'; // [Lane B R41 §41.5] A's registry — the venue sign palettes
|
||||||
import { lotCollider } from './planutil.js';
|
import { lotCollider } from './planutil.js';
|
||||||
|
|
||||||
const STOREY_H = 3.2;
|
const STOREY_H = 3.2;
|
||||||
@ -24,6 +25,43 @@ const WALL_TINTS = ['#c9b8a0', '#b0b8ab', '#c4b0b0', '#a8b4c0', '#c4bc9c', '#cab
|
|||||||
const ROOF_TINT = '#5b5148';
|
const ROOF_TINT = '#5b5148';
|
||||||
const AWNING_SKINS = ['red', 'green', 'blue'];
|
const AWNING_SKINS = ['red', 'green', 'blue'];
|
||||||
|
|
||||||
|
// ── [Lane B R41 §41.5] THE VENUE SIGNAGE FIX — A's D3, filed to B in R40 ─────────────────────────
|
||||||
|
// The bite (this file, one line, `SHOP_REGISTRY[shop.type] || SHOP_REGISTRY.opshop`): `SHOP_REGISTRY`
|
||||||
|
// in fixture_plan.js carries NINE types and has never carried `pub`/`band_room`/`rsl`, so every gig
|
||||||
|
// venue in the game fell to op-shop paint (#2c2820 / #f0e2c0) — the same sign as the Vinnies two doors
|
||||||
|
// down, on the buildings the game photographs most.
|
||||||
|
//
|
||||||
|
// RE-MEASURED on today's tree, golden seed 20261990, gigs on, over the whole shipped corpus
|
||||||
|
// (synthetic + 3 osm fixtures + 23 town caches = 27 towns, 1,838 shops):
|
||||||
|
// **81 venue lots — 3 per town, in every town — all 81 taking the op-shop fallback.**
|
||||||
|
// A's R40 filing said 81; it reproduces exactly. (The bite has now drifted from `:344` (A, R37) to
|
||||||
|
// `:465` (A, R40) to `:483` — the line moves, the defect didn't, which is why the fix is a named
|
||||||
|
// resolver and not another literal at a line number.)
|
||||||
|
//
|
||||||
|
// ONE resolver, explicit precedence, and the op-shop arm is now genuinely last-resort:
|
||||||
|
// 1. `SHOP_REGISTRY[type]` — the nine palettes Lane B has shipped since v1. Returned unchanged, so
|
||||||
|
// every non-venue sign in the game is byte-identical to R40. Repainting those nine to A's
|
||||||
|
// registry palettes is an ART call (record would go muted-teal → hot-pink neon, stall dark →
|
||||||
|
// light), not a bug fix — filed for John/Fable in LANE_B_NOTES §41, deliberately not taken here.
|
||||||
|
// 2. `SHOP_TYPES[type].sign` — A's `core/registry.js`, which has carried gilt (pub) / stencil
|
||||||
|
// (band_room) / club (rsl) since R13 and is the single source of truth for any type A adds next.
|
||||||
|
// A NEW registry type can no longer arrive silently wearing op-shop paint.
|
||||||
|
// 3. `SHOP_REGISTRY.opshop` — a type in NEITHER table. Unreachable for anything the generator emits.
|
||||||
|
//
|
||||||
|
// `accent` exists ONLY on registry-sourced palettes, so the venue treatment below (accent frame +
|
||||||
|
// fascia band) is venue-gated BY CONSTRUCTION — the nine can't grow one by accident.
|
||||||
|
// Cost: zero draws, zero tris, zero fetches — it is two more `fillStyle`s in a canvas cell that was
|
||||||
|
// already being painted into the chunk's existing sign atlas. No golden can move: signage is a canvas
|
||||||
|
// texture, never a plan field (selfcheck re-run and stated in B-progress).
|
||||||
|
const SIGN_FALLBACK = SHOP_REGISTRY.opshop;
|
||||||
|
function signStyleFor(type) {
|
||||||
|
const own = SHOP_REGISTRY[type];
|
||||||
|
if (own) return own; // the nine — shipped bytes, untouched
|
||||||
|
const a = SHOP_TYPES[type] && SHOP_TYPES[type].sign; // the venues, and anything A adds next
|
||||||
|
if (a && a.bg && a.fg) return { signBg: a.bg, signFg: a.fg, accent: a.accent || null, style: a.style || null };
|
||||||
|
return SIGN_FALLBACK; // in neither registry — honestly unknown
|
||||||
|
}
|
||||||
|
|
||||||
// ── R26 #5 — the stocked shopfront (B's polish pick) ──────────────────────────────────────────────
|
// ── R26 #5 — the stocked shopfront (B's polish pick) ──────────────────────────────────────────────
|
||||||
// A crate-diggers' shop should read as one FROM THE FOOTPATH. The beta makes every keyed crate
|
// A crate-diggers' shop should read as one FROM THE FOOTPATH. The beta makes every keyed crate
|
||||||
// distinct *inside*; this closes the loop outside, so the player can spot a shop worth digging while
|
// distinct *inside*; this closes the loop outside, so the player can spot a shop worth digging while
|
||||||
@ -480,11 +518,22 @@ export function buildChunkBuildings(chunkData, ctx) {
|
|||||||
}
|
}
|
||||||
const col = signCell % COLS, row = (signCell / COLS) | 0;
|
const col = signCell % COLS, row = (signCell / COLS) | 0;
|
||||||
const px = col * CELL_W, py = row * CELL_H;
|
const px = col * CELL_W, py = row * CELL_H;
|
||||||
const reg = SHOP_REGISTRY[shop.type] || SHOP_REGISTRY.opshop;
|
const reg = signStyleFor(shop.type); // [Lane B R41 §41.5] see the resolver at the top of this file
|
||||||
actx.fillStyle = reg.signBg; actx.fillRect(px + 4, py + 4, CELL_W - 8, CELL_H - 8);
|
actx.fillStyle = reg.signBg; actx.fillRect(px + 4, py + 4, CELL_W - 8, CELL_H - 8);
|
||||||
actx.fillStyle = reg.signFg;
|
actx.fillStyle = reg.signFg;
|
||||||
actx.strokeStyle = reg.signFg; actx.lineWidth = 3;
|
actx.strokeStyle = reg.signFg; actx.lineWidth = 3;
|
||||||
actx.strokeRect(px + 10, py + 10, CELL_W - 20, CELL_H - 20);
|
actx.strokeRect(px + 10, py + 10, CELL_W - 20, CELL_H - 20);
|
||||||
|
// [Lane B R41 §41.5] THE VENUE TREATMENT — a licensed house wears its own paint: the frame goes to
|
||||||
|
// the registry's `accent` and a fascia band runs the top inside edge (a pub's painted head-board).
|
||||||
|
// Only registry-sourced palettes carry `accent`, so this is venue-only by construction, and the
|
||||||
|
// band sits at the TOP so it can never collide with the R26 crate mark at the sign's foot.
|
||||||
|
if (reg.accent) {
|
||||||
|
actx.strokeStyle = reg.accent;
|
||||||
|
actx.strokeRect(px + 10, py + 10, CELL_W - 20, CELL_H - 20);
|
||||||
|
actx.fillStyle = reg.accent;
|
||||||
|
actx.fillRect(px + 14, py + 14, CELL_W - 28, 9);
|
||||||
|
actx.fillStyle = reg.signFg; // the name is still painted in `fg`
|
||||||
|
}
|
||||||
// fit text
|
// fit text
|
||||||
let fs = 74; const label = String(shop.name).slice(0, 22).toUpperCase();
|
let fs = 74; const label = String(shop.name).slice(0, 22).toUpperCase();
|
||||||
actx.textAlign = 'center'; actx.textBaseline = 'middle';
|
actx.textAlign = 'center'; actx.textBaseline = 'middle';
|
||||||
|
|||||||
200
web/js/world/credits.js
Normal file
@ -0,0 +1,200 @@
|
|||||||
|
// PROCITY Lane B — credits.js ⟨R41 §41.5, THE CREDITS SURFACE⟩
|
||||||
|
//
|
||||||
|
// PROCITY had no attribution surface at all. It ships OpenStreetMap data (ODbL 1.0 — "keep the
|
||||||
|
// © OpenStreetMap contributors credit visible", web/assets/towns/SOURCES.md's own words) and a
|
||||||
|
// vendored MIT three.js, and both obligations were live and undischarged. It is also the gate in
|
||||||
|
// front of 4,472 more licence-clean motion clips on the fleet (100STYLE CC BY 4.0 · CMU · ACCAD
|
||||||
|
// CC BY 3.0) that R41 could not spend for exactly this reason (ROUND41 ruling 2). So: build the
|
||||||
|
// surface, then a later round can spend them by appending rows to a data file.
|
||||||
|
//
|
||||||
|
// ── THE THREE PROPERTIES THIS FILE IS BUILT TO HOLD ──────────────────────────────────────────────
|
||||||
|
//
|
||||||
|
// 1. ZERO DRAWS, structurally. Everything here is DOM. The renderer never sees a credits object,
|
||||||
|
// there is no geometry, no material, no texture and no render pass — so the street draw budget
|
||||||
|
// (291/300 at night, ROUND41 ruling 4: nine draws of margin, no lane spends any) cannot move.
|
||||||
|
// It is the same mechanism the R19 town selector and the R39 fog map use: chrome the composer
|
||||||
|
// never renders. Measured as an A/B against a control tree anyway — see B-progress R41.
|
||||||
|
//
|
||||||
|
// 2. CLASSIC-GATED BY CONSTRUCTION, not by a flag test. There is NO `classic` check in this file.
|
||||||
|
// The shell dynamic-imports this module only on a non-classic boot and hands the result to
|
||||||
|
// `hud.setCredits()`; under `?classic=1` the module is never fetched, `setCredits` is never
|
||||||
|
// called, and the HUD's DOM, key handlers and fetch surface are byte-identical to R40. That is
|
||||||
|
// the null-provider idiom the R40 `#pc-street` row rides, and the R40 playtest harness before it.
|
||||||
|
//
|
||||||
|
// 3. LAZY, so an unopened panel costs nothing. `assets/credits.json` is fetched on FIRST OPEN, not
|
||||||
|
// at boot: a boot that never opens the panel makes no request at all, so the default boot's fetch
|
||||||
|
// surface is unchanged too. Under `?noassets=1` the asset law forbids the fetch outright and the
|
||||||
|
// panel says so honestly rather than fetching anyway or inventing a built-in copy — a boot with
|
||||||
|
// no assets loaded has, correctly, almost nothing to attribute.
|
||||||
|
//
|
||||||
|
// ── THE DATA FILE IS THE PRODUCT ─────────────────────────────────────────────────────────────────
|
||||||
|
// `web/assets/credits.json`, schema `procity-credits/1`. EVERY lane appends its own entries there;
|
||||||
|
// no lane needs to touch this file to be credited. The full schema, the field rules and a paste-ready
|
||||||
|
// row for the three CC-BY motion libraries are in docs/LANES/LANE_B_NOTES.md §41.
|
||||||
|
//
|
||||||
|
// This renderer makes exactly two editorial decisions and both are licence-driven:
|
||||||
|
// • entries whose attribution is a LICENCE OBLIGATION (`required: true`) sort to the top and are
|
||||||
|
// rendered with the required text verbatim, in a box, so it cannot be skimmed past;
|
||||||
|
// • entries carrying a `flag` (an open licence question — an unverified source, or art a human
|
||||||
|
// must clear before release) are rendered in amber. A flag that only exists in a lane note is a
|
||||||
|
// flag nobody sees; R25's lesson (the licence line that went silent while the gate still said OK)
|
||||||
|
// is that the print IS the mechanism.
|
||||||
|
// Everything else is presented as the data file states it. This file never decides a licence.
|
||||||
|
|
||||||
|
const CREDITS_URL = 'assets/credits.json';
|
||||||
|
|
||||||
|
const CSS = `
|
||||||
|
#pc-credits{position:fixed;inset:0;z-index:40;display:none;align-items:center;justify-content:center;
|
||||||
|
background:rgba(8,6,4,.72);font:13px/1.55 -apple-system,Segoe UI,Roboto,sans-serif;color:#f4efe6}
|
||||||
|
#pc-credits.on{display:flex}
|
||||||
|
#pc-credits-box{width:min(760px,92vw);max-height:86vh;overflow-y:auto;background:rgba(20,16,10,.97);
|
||||||
|
border:1px solid rgba(255,215,94,.35);border-radius:14px;padding:18px 20px 22px;pointer-events:auto;
|
||||||
|
box-shadow:0 18px 60px rgba(0,0,0,.6)}
|
||||||
|
#pc-credits h2{font-size:15px;color:#ffd75e;margin:0 0 2px;font-weight:600;letter-spacing:.06em}
|
||||||
|
#pc-credits .pc-cr-sub{opacity:.72;margin:0 0 14px;font-size:12px}
|
||||||
|
#pc-credits .pc-cr-note{opacity:.85;margin:0 0 16px;padding-bottom:14px;border-bottom:1px solid rgba(255,244,230,.13)}
|
||||||
|
#pc-credits .pc-cr-e{padding:11px 0;border-top:1px solid rgba(255,244,230,.09)}
|
||||||
|
#pc-credits .pc-cr-e:first-of-type{border-top:0}
|
||||||
|
#pc-credits .pc-cr-h{display:flex;gap:9px;align-items:baseline;flex-wrap:wrap}
|
||||||
|
#pc-credits .pc-cr-n{font-weight:600;color:#ffe9b0}
|
||||||
|
#pc-credits .pc-cr-k{font-size:10px;letter-spacing:.09em;text-transform:uppercase;opacity:.5}
|
||||||
|
#pc-credits .pc-cr-lic{font-size:12px;opacity:.8}
|
||||||
|
#pc-credits .pc-cr-u{opacity:.82;margin:4px 0 0}
|
||||||
|
#pc-credits .pc-cr-src{font-size:12px;opacity:.62;margin:3px 0 0}
|
||||||
|
#pc-credits .pc-cr-att{margin:7px 0 0;padding:7px 10px;border-left:3px solid #ffd75e;
|
||||||
|
background:rgba(255,215,94,.09);border-radius:0 6px 6px 0}
|
||||||
|
#pc-credits .pc-cr-att b{color:#ffd75e;font-size:10px;letter-spacing:.09em;display:block;margin-bottom:2px}
|
||||||
|
#pc-credits .pc-cr-flag{margin:7px 0 0;padding:7px 10px;border-left:3px solid #ff9a6a;
|
||||||
|
background:rgba(255,154,106,.09);border-radius:0 6px 6px 0;color:#ffcdb4;font-size:12px}
|
||||||
|
#pc-credits .pc-cr-flag b{color:#ff9a6a;font-size:10px;letter-spacing:.09em;display:block;margin-bottom:2px}
|
||||||
|
#pc-credits .pc-cr-foot{margin-top:16px;padding-top:12px;border-top:1px solid rgba(255,244,230,.13);
|
||||||
|
font-size:12px;opacity:.6;display:flex;justify-content:space-between;gap:12px;flex-wrap:wrap}
|
||||||
|
#pc-credits button{background:rgba(20,16,10,.7);color:#f4efe6;border:1px solid rgba(255,215,94,.35);
|
||||||
|
border-radius:8px;padding:4px 12px;font:12px/1.3 -apple-system,Segoe UI,Roboto,sans-serif;cursor:pointer}
|
||||||
|
#pc-credits button:hover{border-color:rgba(255,215,94,.7);color:#ffd75e}
|
||||||
|
`;
|
||||||
|
|
||||||
|
const KIND_LABEL = {
|
||||||
|
engine: 'engine', data: 'map data', motion: 'motion', mesh: 'models', texture: 'textures',
|
||||||
|
audio: 'audio', stock: 'cover art', tool: 'tools', font: 'type',
|
||||||
|
};
|
||||||
|
|
||||||
|
const esc = (s) => String(s == null ? '' : s)
|
||||||
|
.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* createCredits({ noassets, releasePointer }) → { toggle, open, close, dispose, get isOpen, state }
|
||||||
|
* Builds nothing but a hidden overlay shell; the list is rendered on first open, after the fetch.
|
||||||
|
* Never throws: a missing, 404ing or malformed data file degrades to a stated line, not an error.
|
||||||
|
*
|
||||||
|
* `releasePointer` frees the pointer lock on open, so the panel is clickable and WASD stops moving
|
||||||
|
* a player who is reading. It is the SAME contract the dig card, the sell card and the F8 note ride,
|
||||||
|
* which is why the shell's `unlock` handler carries one more term: an unlock that is us opening the
|
||||||
|
* credits must not walk you out of a shop (index.html, [Lane B R41]). F2/Esc work locked or not.
|
||||||
|
*/
|
||||||
|
export function createCredits({ noassets = false, releasePointer = null } = {}) {
|
||||||
|
if (!document.getElementById('pc-credits-style')) {
|
||||||
|
const st = document.createElement('style');
|
||||||
|
st.id = 'pc-credits-style'; st.textContent = CSS;
|
||||||
|
document.head.appendChild(st);
|
||||||
|
}
|
||||||
|
const root = document.createElement('div');
|
||||||
|
root.id = 'pc-credits';
|
||||||
|
root.innerHTML = '<div id="pc-credits-box"></div>';
|
||||||
|
document.body.appendChild(root);
|
||||||
|
const box = root.querySelector('#pc-credits-box');
|
||||||
|
|
||||||
|
let open = false, loaded = false, data = null, err = null;
|
||||||
|
|
||||||
|
// one fetch per session, on first open. `force-cache` matches the towns-index discipline.
|
||||||
|
function load() {
|
||||||
|
if (loaded) return Promise.resolve();
|
||||||
|
loaded = true;
|
||||||
|
if (noassets) { err = 'noassets'; return Promise.resolve(); }
|
||||||
|
return fetch(CREDITS_URL, { cache: 'force-cache' })
|
||||||
|
.then((r) => (r.ok ? r.json() : Promise.reject(new Error('HTTP ' + r.status))))
|
||||||
|
.then((j) => {
|
||||||
|
if (!j || !Array.isArray(j.entries)) throw new Error('bad shape');
|
||||||
|
data = j;
|
||||||
|
})
|
||||||
|
.catch((e) => { err = String((e && e.message) || e); }); // stated in the panel, never thrown
|
||||||
|
}
|
||||||
|
|
||||||
|
function entryHTML(e) {
|
||||||
|
const kind = KIND_LABEL[e.kind] || e.kind || '';
|
||||||
|
const lic = e.licence === 'unverified' ? 'licence unverified' : e.licence;
|
||||||
|
const n = Number.isFinite(e.count) && e.count > 0 ? ` · ${e.count}` : '';
|
||||||
|
let h = '<div class="pc-cr-e"><div class="pc-cr-h">'
|
||||||
|
+ `<span class="pc-cr-n">${esc(e.name)}</span>`
|
||||||
|
+ `<span class="pc-cr-k">${esc(kind)}${esc(n)}</span>`
|
||||||
|
+ `<span class="pc-cr-lic">${esc(lic)}</span></div>`;
|
||||||
|
if (e.used_for) h += `<div class="pc-cr-u">${esc(e.used_for)}</div>`;
|
||||||
|
if (e.attribution) {
|
||||||
|
h += '<div class="pc-cr-att"><b>' + (e.required ? 'ATTRIBUTION REQUIRED' : 'ATTRIBUTION')
|
||||||
|
+ `</b>${esc(e.attribution)}</div>`;
|
||||||
|
}
|
||||||
|
if (e.source) h += `<div class="pc-cr-src">${esc(e.source)}</div>`;
|
||||||
|
if (e.flag) h += `<div class="pc-cr-flag"><b>OPEN LICENCE QUESTION</b>${esc(e.flag)}</div>`;
|
||||||
|
return h + '</div>';
|
||||||
|
}
|
||||||
|
|
||||||
|
function render() {
|
||||||
|
let body;
|
||||||
|
if (data) {
|
||||||
|
// required attributions first, then everything else — data-file order inside each group.
|
||||||
|
const es = data.entries.slice();
|
||||||
|
const idx = new Map(es.map((e, i) => [e, i]));
|
||||||
|
es.sort((a, b) => (!!b.required - !!a.required) || (idx.get(a) - idx.get(b)));
|
||||||
|
const req = es.filter((e) => e.required).length;
|
||||||
|
const flags = es.filter((e) => e.flag).length;
|
||||||
|
body = `<p class="pc-cr-note">${esc(data.notice || '')}</p>`
|
||||||
|
+ es.map(entryHTML).join('')
|
||||||
|
+ '<div class="pc-cr-foot"><span>'
|
||||||
|
+ `${es.length} entries · ${req} with a required attribution`
|
||||||
|
+ (flags ? ` · ${flags} open licence question${flags === 1 ? '' : 's'}` : '')
|
||||||
|
+ `</span><span>${esc(data.schema || '')}${data.updated ? ' · ' + esc(data.updated) : ''}</span></div>`;
|
||||||
|
} else if (err === 'noassets') {
|
||||||
|
body = '<p class="pc-cr-note">This boot is running with <b>?noassets=1</b> — no textures, models,'
|
||||||
|
+ ' audio or town data have been loaded, and the asset law forbids fetching the credits file'
|
||||||
|
+ ' either. There is nothing on screen to attribute beyond three.js (MIT, vendored) and the'
|
||||||
|
+ ' game\'s own code. The full list ships at <b>web/assets/credits.json</b>.</p>';
|
||||||
|
} else {
|
||||||
|
body = `<p class="pc-cr-note">The credits file could not be read (<b>${esc(err || 'unknown')}</b>).`
|
||||||
|
+ ' It ships at <b>web/assets/credits.json</b>; nothing has been guessed to fill the gap.</p>';
|
||||||
|
}
|
||||||
|
box.innerHTML = '<h2>CREDITS & LICENCES</h2>'
|
||||||
|
+ '<p class="pc-cr-sub">What PROCITY is built from, and what each source asks for in return.</p>'
|
||||||
|
+ body
|
||||||
|
+ '<div class="pc-cr-foot"><span>Esc or F2 to close</span>'
|
||||||
|
+ '<button id="pc-credits-close">CLOSE</button></div>';
|
||||||
|
const btn = box.querySelector('#pc-credits-close');
|
||||||
|
if (btn) btn.onclick = () => setOpen(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
function setOpen(v) {
|
||||||
|
if (v === open) return;
|
||||||
|
open = v;
|
||||||
|
root.classList.toggle('on', open);
|
||||||
|
if (!open) return;
|
||||||
|
if (releasePointer) { try { releasePointer(); } catch { /* pointer lock is a nicety, never a throw */ } }
|
||||||
|
render(); // the "loading"/settled state, whichever we're in
|
||||||
|
load().then(() => { if (open) render(); }); // then the real list (or the stated failure)
|
||||||
|
}
|
||||||
|
|
||||||
|
function onKey(e) {
|
||||||
|
if (e.code === 'F2') { e.preventDefault(); setOpen(!open); }
|
||||||
|
else if (e.code === 'Escape' && open) setOpen(false);
|
||||||
|
}
|
||||||
|
addEventListener('keydown', onKey);
|
||||||
|
root.addEventListener('click', (e) => { if (e.target === root) setOpen(false); }); // click the scrim
|
||||||
|
|
||||||
|
return {
|
||||||
|
open: () => setOpen(true),
|
||||||
|
close: () => setOpen(false),
|
||||||
|
toggle: () => setOpen(!open),
|
||||||
|
get isOpen() { return open; },
|
||||||
|
// read-only diag for the QA harness: no fetch has happened until `loaded` is true
|
||||||
|
get state() { return { open, loaded, entries: data ? data.entries.length : 0, error: err }; },
|
||||||
|
dispose() { removeEventListener('keydown', onKey); root.remove(); },
|
||||||
|
};
|
||||||
|
}
|
||||||
@ -23,8 +23,19 @@
|
|||||||
|
|
||||||
import { rng, pick, irange } from '../core/prng.js';
|
import { rng, pick, irange } from '../core/prng.js';
|
||||||
|
|
||||||
// ── Shop-type registry (Lane A owns web/js/core/registry.js; it hasn't landed yet, so Lane B
|
// ── Shop-type registry — THE FIXTURE'S TABLE, and Lane B's shipped sign palettes ─────────────────
|
||||||
// carries the slice it needs. facade pools are the CITY_SPEC table. Lane F reconciles.) ──
|
// [Lane B R41 §41.5] The old header here said "Lane A owns web/js/core/registry.js; it hasn't landed
|
||||||
|
// yet" — registry.js landed in R13 and that line was still standing 28 rounds later, which is how
|
||||||
|
// `pub`/`band_room`/`rsl` stayed missing long enough to dress 81 venue lots corpus-wide in op-shop
|
||||||
|
// paint (A's D3; fixed in buildings.js `signStyleFor`, see its header for the measurement).
|
||||||
|
//
|
||||||
|
// What this table IS, honestly, today:
|
||||||
|
// • the FIXTURE's facade pools (bare skin keys — `pickFacade` below; the fixture has no venues), and
|
||||||
|
// • Lane B's nine SHIPPED sign palettes, which `buildings.js signStyleFor()` prefers over A's for
|
||||||
|
// exactly those nine so the town's signage bytes don't move under a bug fix.
|
||||||
|
// It is NOT the shop-type vocabulary. **`web/js/core/registry.js SHOP_TYPES` (12 types, each with a
|
||||||
|
// full `sign:{bg,fg,accent,style}`) is the single source of truth** — anything not in the nine below
|
||||||
|
// resolves there. Adding a type here is not how a new shop type enters the game.
|
||||||
export const SHOP_REGISTRY = {
|
export const SHOP_REGISTRY = {
|
||||||
record: { facades: ['timber-teal', 'arcade-tile', 'djsim-record'], signBg: '#1f2e2b', signFg: '#e8d9a0' },
|
record: { facades: ['timber-teal', 'arcade-tile', 'djsim-record'], signBg: '#1f2e2b', signFg: '#e8d9a0' },
|
||||||
opshop: { facades: ['weatherboard', 'fibro-blue', 'djsim-opshop'], signBg: '#2c2820', signFg: '#f0e2c0' },
|
opshop: { facades: ['weatherboard', 'fibro-blue', 'djsim-opshop'], signBg: '#2c2820', signFg: '#f0e2c0' },
|
||||||
|
|||||||
@ -27,6 +27,9 @@ const CSS = `
|
|||||||
#pc-townpick:hover{opacity:1}
|
#pc-townpick:hover{opacity:1}
|
||||||
#pc-townpick select{background:rgba(20,16,10,.7);color:#f4efe6;border:1px solid rgba(255,215,94,.35);border-radius:8px;padding:5px 9px;font:12px/1.3 -apple-system,Segoe UI,Roboto,sans-serif;max-width:230px;cursor:pointer}
|
#pc-townpick select{background:rgba(20,16,10,.7);color:#f4efe6;border:1px solid rgba(255,215,94,.35);border-radius:8px;padding:5px 9px;font:12px/1.3 -apple-system,Segoe UI,Roboto,sans-serif;max-width:230px;cursor:pointer}
|
||||||
#pc-townpick select:focus{outline:1px solid rgba(255,215,94,.6)}
|
#pc-townpick select:focus{outline:1px solid rgba(255,215,94,.6)}
|
||||||
|
#pc-creditlink{position:absolute;left:12px;bottom:34px;background:rgba(20,16,10,.5);padding:5px 10px;
|
||||||
|
border-radius:8px;opacity:.72;pointer-events:auto;cursor:pointer;transition:opacity .12s;font-size:12px}
|
||||||
|
#pc-creditlink:hover{opacity:1;color:#ffd75e}
|
||||||
#pc-game{position:fixed;right:10px;bottom:12px;z-index:30;display:flex;flex-direction:column;align-items:flex-end;gap:8px}
|
#pc-game{position:fixed;right:10px;bottom:12px;z-index:30;display:flex;flex-direction:column;align-items:flex-end;gap:8px}
|
||||||
#pc-gamebar{background:rgba(20,16,10,.72);padding:6px 10px;border-radius:10px;display:flex;gap:10px;align-items:center;pointer-events:auto;text-shadow:0 1px 2px #000}
|
#pc-gamebar{background:rgba(20,16,10,.72);padding:6px 10px;border-radius:10px;display:flex;gap:10px;align-items:center;pointer-events:auto;text-shadow:0 1px 2px #000}
|
||||||
#pc-gamebar b{color:#ffd75e;font-weight:600}
|
#pc-gamebar b{color:#ffd75e;font-weight:600}
|
||||||
@ -359,6 +362,30 @@ export function createHUD({ camera, renderer, plan, getDoorMeshes, onEnterShop,
|
|||||||
if (text !== shownStreet) { shownStreet = text; elStreet.textContent = text || '—'; }
|
if (text !== shownStreet) { shownStreet = text; elStreet.textContent = text || '—'; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── [Lane B R41 §41.5 — THE CREDITS SURFACE] the way in ───────────────────────────────────────
|
||||||
|
// GATED BY CONSTRUCTION, exactly like setAddresses above: none of this DOM or its listener exists
|
||||||
|
// until the shell calls setCredits(), and the shell only builds the credits module on a non-classic
|
||||||
|
// boot — so ?classic=1's HUD is byte-identical with NO flag test in this file. Zero draws (DOM).
|
||||||
|
//
|
||||||
|
// The link is not decoration. `web/assets/towns/SOURCES.md` states the ODbL condition in its own
|
||||||
|
// words — "if a rendered/public build surfaces this data, keep the © OpenStreetMap contributors
|
||||||
|
// credit visible" — and until this round nothing in the running game did. So on a boot that IS
|
||||||
|
// showing OSM-derived streets, the link itself carries the credit; on the synthetic town, where no
|
||||||
|
// OSM data is on screen, claiming it would be the opposite of attribution, so it reads plainly.
|
||||||
|
// `osm` is passed by the shell (PLANSRC), never sniffed off the plan.
|
||||||
|
let creditLink = null;
|
||||||
|
function setCredits(credits, { osm = false } = {}) {
|
||||||
|
if (!credits || creditLink) return;
|
||||||
|
creditLink = document.createElement('div');
|
||||||
|
creditLink.id = 'pc-creditlink';
|
||||||
|
creditLink.textContent = osm ? '© OpenStreetMap contributors · credits (F2)' : 'credits & licences (F2)';
|
||||||
|
creditLink.title = 'What PROCITY is built from, and what each source asks for in return';
|
||||||
|
creditLink.onclick = () => credits.open();
|
||||||
|
root.appendChild(creditLink);
|
||||||
|
const help = root.querySelector('#pc-help');
|
||||||
|
if (help) help.textContent += ' · F2 credits';
|
||||||
|
}
|
||||||
|
|
||||||
function nearestDoor(rects, point) {
|
function nearestDoor(rects, point) {
|
||||||
let best = null, bd = Infinity;
|
let best = null, bd = Infinity;
|
||||||
for (const r of rects) {
|
for (const r of rects) {
|
||||||
@ -458,5 +485,6 @@ export function createHUD({ camera, renderer, plan, getDoorMeshes, onEnterShop,
|
|||||||
|
|
||||||
return { update, tickToast, showToast, setVisible, tickGame, dispose, getHovered: () => hovered,
|
return { update, tickToast, showToast, setVisible, tickGame, dispose, getHovered: () => hovered,
|
||||||
getFps: () => fpsShown,
|
getFps: () => fpsShown,
|
||||||
setAddresses }; // [Lane B R40 §40.3] fog signage — the shell wires it on non-classic boots only
|
setAddresses, // [Lane B R40 §40.3] fog signage — the shell wires it on non-classic boots only
|
||||||
|
setCredits }; // [Lane B R41 §41.5] the credits surface — same null-provider gating
|
||||||
}
|
}
|
||||||
|
|||||||