Lane B R40 §40.3 (+E's §40.4 rule): the 307 was fiction, the town speaks its street names
THE ?r=3 BREACH — the carried number was stale by 84 draws. Re-measured with the pin
method of record (stepwalk 2m, 802 stations, fresh headless contexts, no-store server):
default 282/291 (≤300, 9 margin at night) · classic 269 byte-exact to the R38 pin incl.
90,580 tris · ?r=3 382/391, NOT 307. Decision: GATE, not shave — the 91-draw excess is
the R+1 live-chunk window (48 vs 31 chunks) on a per-chunk cost already collapsed to
~1 draw/kind/chunk, so shaving it means cutting default-boot content to legalise a
diagnostic. ?r= above auto now declares PROCITY.budget {draws:420, diagnostic:true} and
console-warns its own law; HUD threshold + DBG.info().budget read it; new
tools/qa/r40_lane_b.py enforces it (386 ≤ 420, and >300 so the exemption is provably
load-bearing).
?classic=1 TOWN SELECTOR (Fable ruled): true by construction, now deliberate and
qa-asserted — 27 options, one fetch (the named POST_V2_EXCEPTION), picks stay classic,
zero draws, 0 errors.
FOG SIGNAGE: three silent surfaces consume A's address layer via new
createStreetLocator(plan) — HUD street row, door tooltip ('Little Paris Cafe ·
Katoomba Street'), fog-map caption. Never branches on town type; classic-gated by
construction (#pc-street absent, tooltip pre-R40-byte).
E's arcade rule applied verbatim (§40.4): roof spans, a spanned roof has no posts;
district.kind keying, explicit ARCADEKIT classic gate + ?arcadekit=0 control.
-34 post instances exactly, lane draws 120→120 (+0). Shot pair vs E's reference.
Goldens 157,647/157,647, 0x5f76e76 unmoved after every wave.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
dab5ecd273
commit
066cc1775b
@ -4,6 +4,57 @@
|
|||||||
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 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,
|
||||||
|
headless fresh contexts, no-store servers, pin method (stepwalk 2 m · yaw 0 · laps 2 · 802 stations).
|
||||||
|
|
||||||
|
**1. THE ?r=3 BREACH — re-measured, then GATED (option b), because the carried 307 was fiction.**
|
||||||
|
|
||||||
|
| mode | before (carried) | re-measured (pin method, final tree) | law now |
|
||||||
|
|---|---|---|---|
|
||||||
|
| default noon / night | 282 / 292 (R38 pin) | **282 / 291** ✅ pin reproduced | ≤300 (unchanged) |
|
||||||
|
| `?classic=1` night | 269 | **269 / 90,580 tris — byte-exact** | ≤300 (unchanged) |
|
||||||
|
| `?r=3` noon / night | "307 / 317" (stale bookmark) | **382 / 391** at 45–48 live chunks | **≤420, its OWN declared law** |
|
||||||
|
|
||||||
|
Decision: **gate, not shave** — the 91-draw excess is the R+1 live-chunk window (48 vs 31) on a
|
||||||
|
per-chunk cost already collapsed to ~1 draw/kind/chunk; a shave that size is a content cut to the
|
||||||
|
DEFAULT town to legalise a diagnostic. So: `?r=` above auto now declares
|
||||||
|
`PROCITY.budget = {draws: 420, tris: 200k, diagnostic: true}`, console-warns its law, the HUD warn
|
||||||
|
threshold reads it, `DBG.info().budget` carries it, and **NEW `tools/qa/r40_lane_b.py`** asserts it
|
||||||
|
with both arms non-vacuous (measures 386 ≤ 420 at 8 m stations AND 386 > 300, so the exemption is
|
||||||
|
load-bearing; ≤300 someday ⇒ "retire the exemption", printed not failed). Ceiling 420 = 391 + ~7%
|
||||||
|
jitter margin; growth past it goes red and is re-pinned consciously. → F: one qa.sh line; README
|
||||||
|
`?r=N` row still says 307.
|
||||||
|
|
||||||
|
**2. ?classic=1 TOWN SELECTOR (Fable ruled yes) — verified true by construction, made deliberate.**
|
||||||
|
Classic boots with the selector present + visible, **all 27 options** (3 fixtures + 23 real/godverse
|
||||||
|
towns), one towns-fetch = exactly the named POST_V2_EXCEPTION (`assets/towns/index.json`), picks
|
||||||
|
FROM classic stay classic (query string preserved), zero draws (DOM chrome), 0 errors. Ruling
|
||||||
|
written into the selector block (hud.js) and asserted in r40_lane_b.py. → F: the flags_check
|
||||||
|
POST_V2_EXCEPTIONS label still calls this a debt-ledger question; it's ruled now.
|
||||||
|
|
||||||
|
**3. FOG SIGNAGE — three surfaces that said nothing now consume A's layer** (`street || label`,
|
||||||
|
null handled once, no town-type branch): **HUD `street` row** ("Lurline Street" on katoomba_real,
|
||||||
|
supplier `ways`; "the south end of the main street" on the synthetic) · **door tooltip**
|
||||||
|
("🚪 Little Paris Cafe · Katoomba Street — click to enter") · **fog-map caption**
|
||||||
|
("… 2 streets walked · Lurline Street · press M to close"). New `createStreetLocator(plan)` in
|
||||||
|
discovery.js (the probe's hash, read-only; signage ≠ discovery, so not fog-keyed); rides the
|
||||||
|
existing frame%6 throttles; zero draws. **Classic-gated by construction**: locator built only
|
||||||
|
`!CLASSIC`, so `#pc-street` doesn't exist in classic's DOM and the tooltip is the pre-R40 byte —
|
||||||
|
A/B'd on the same door, asserted both arms in r40_lane_b.py.
|
||||||
|
|
||||||
|
**4. §40.4 ARCADE KIT (routed mid-round by Fable, E's spec applied verbatim):** roof spans (depth
|
||||||
|
2.5 = half-lane, slabs meet flush), **a spanned roof has no posts**; `ctx.arcadeBlocks` keyed on
|
||||||
|
`district.kind` never `edge.kind`; explicit `ARCADEKIT` gate (classic forces off — mandatory;
|
||||||
|
`?arcadekit=0` = falsifiability control). Measured: **−34 post instances exactly, lane draws
|
||||||
|
120 → 120 (+0)**, shot pair `docs/shots/laneB_r40/`, classic pin cell byte-exact after the change.
|
||||||
|
|
||||||
|
Goldens **157,647/157,647, 0x5f76e76 unmoved** (selfcheck after every wave). r40_lane_b gate:
|
||||||
|
**all assertions green**. Files: `web/index.html` (`[Lane B R40]` marks), `hud.js`, `minimap.js`,
|
||||||
|
`discovery.js`, `dbg.js`, `chunks.js`, `buildings.js`, new `tools/qa/r40_lane_b.py`,
|
||||||
|
`docs/shots/laneB_r40/`, these docs. **No git commands run** (lane treaty; Fable commits).
|
||||||
|
|
||||||
## Round 39 (v9 Layer 2 — THE FOG, item 39.2) — **zero draws, and the map is finally readable**
|
## Round 39 (v9 Layer 2 — THE FOG, item 39.2) — **zero draws, and the map is finally readable**
|
||||||
|
|
||||||
Full detail + every number in LANE_B_NOTES §39. `minimap.js` drew all 493 synthetic shops from the
|
Full detail + every number in LANE_B_NOTES §39. `minimap.js` drew all 493 synthetic shops from the
|
||||||
|
|||||||
@ -1,5 +1,127 @@
|
|||||||
# LANE B — NOTES (measured)
|
# LANE B — NOTES (measured)
|
||||||
|
|
||||||
|
## 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
|
||||||
|
servers, 1280×720, seed 20261990 unless a town is named. Budget cells use **the pin method of
|
||||||
|
record** (R37/R38: `budget_walk.py --method stepwalk --stepm 2 --fixed-yaw 0 --laps 2` = 802
|
||||||
|
stations down the main spine at yaw 0, the worst bearing).
|
||||||
|
|
||||||
|
### §40.3-1 — THE ?r=3 BREACH: re-measured 391, and the ruling is GATE, not shave
|
||||||
|
|
||||||
|
**The carried "307" was a stale bookmark, as the round suspected — the true number is 391.** The
|
||||||
|
R37-era 307/317 pin predates five rounds of default-on content; nobody had re-measured the override
|
||||||
|
since. On today's tree, same method as the budget of record:
|
||||||
|
|
||||||
|
| synthetic, pin method | noon (seg 2) | NIGHT (seg 5) | live chunks max | tris worst |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| default (`r` auto = 2) | **282** | **291** | 32 | 120,093 ✅ pin reproduced (R38: 282/292) |
|
||||||
|
| `?classic=1` | — | **269** | 32 | 90,580 ✅ **byte-exact to the R38 pin** |
|
||||||
|
| **`?r=3`** | **382** | **391** | **48** | 132,829 (tris legal) |
|
||||||
|
|
||||||
|
**Why shave was refused, with the numbers:** the breach is structural, not fat. `?r=3` carries
|
||||||
|
45–48 live chunks against the default's 31 (R+1 dispose window over a 7×7 vs 5×5 set), and the
|
||||||
|
excess is 91–100 draws. Lane B's per-chunk cost is already collapsed to ~1 draw per kind per chunk
|
||||||
|
(facade atlas, merged instancing — R2 close-out); there is no 91-draw shave available in a
|
||||||
|
diagnostic boot that is not a content cut to the DEFAULT town. Shaving a diagnostic override by
|
||||||
|
making the shipped town poorer is backwards.
|
||||||
|
|
||||||
|
**So the flag now documents its own budget law and qa asserts it (the round's option b):**
|
||||||
|
|
||||||
|
- `index.html` (`[Lane B R40]`, the RADIUS block): any `?r=` **above** the auto radius declares
|
||||||
|
`PROCITY.budget = { draws: 420, tris: 200000, radius, auto, diagnostic: true }` and prints a
|
||||||
|
`console.warn` naming the law ("DIAGNOSTIC boot … ≤420 draws … not a supported boot"). At or
|
||||||
|
below auto: `{ draws: 300, diagnostic: false }` — the street law, unchanged.
|
||||||
|
- **The ceiling is 420** = 391 measured + ~7% for crowd/streamer jitter (the R37 lesson: settle
|
||||||
|
policy and station phase move a walked worst by single digits). If the town grows past it, the
|
||||||
|
gate goes red and the ceiling is re-pinned CONSCIOUSLY — that is what a pinned law is for.
|
||||||
|
- `hud.js` warn threshold reads the boot's own law (`budget.draws`), so the readout stops shaming a
|
||||||
|
documented diagnostic at 300 and starts telling the truth at 420. Default boots render identical
|
||||||
|
DOM (same 300).
|
||||||
|
- `dbg.js` `info().budget` carries the law, so **a harness asserts against the boot's declared
|
||||||
|
ceiling, never a hardcoded 300**.
|
||||||
|
- **`tools/qa/r40_lane_b.py` (NEW file — races no lane) is the enforcing gate**, all arms
|
||||||
|
non-vacuous: default declares 300/not-diagnostic · `?r=3` declares 420/diagnostic + the warn
|
||||||
|
fired · a night stepwalk under `?r=3` measures ≤ its own ceiling (read 386 ≤ 420 at 8 m
|
||||||
|
stations) **and** > 300 (the exemption is load-bearing, not vacuous — if it ever reads ≤300 the
|
||||||
|
gate prints "retire the exemption" rather than failing) · tri law unchanged (131,893 ≤ 200k) ·
|
||||||
|
0 console errors. **→ Lane F: one line wires it into qa.sh** — and README.md's `?r=N` row still
|
||||||
|
quotes 307; please update it to the law (391 measured R40, ceiling 420, gated + warned).
|
||||||
|
|
||||||
|
### §40.3-2 — CLASSIC'S TOWN SELECTOR: the ruling was already true by construction; now it is deliberate and gated
|
||||||
|
|
||||||
|
Fable ruled classic SHOWS the selector ("information, not gameplay state"). Verified on today's
|
||||||
|
tree: `?classic=1` boots with the selector **present and visible, all 27 options** (synthetic + 3
|
||||||
|
OSM fixtures + the 23-town real/godverse pack), its towns-fetch surface is **exactly the one named
|
||||||
|
POST_V2_EXCEPTION** (`assets/towns/index.json`, Fable's R37 ruling), navigation preserves the query
|
||||||
|
string so a town picked FROM classic STAYS classic, **zero draws** (a `<select>` the renderer never
|
||||||
|
sees — draws at spawn 148 with it, and `renderer.info` cannot see DOM), zero console errors. The
|
||||||
|
ruling is now written INTO the selector block (`hud.js`, `[Lane B R40]`) and **r40_lane_b.py
|
||||||
|
asserts it** (selector present, ≥20 towns, fetch surface = the named exception only).
|
||||||
|
**→ Lane F:** `flags_check.py`'s `POST_V2_EXCEPTIONS` label still carries "debt ledger R38+: should
|
||||||
|
classic show the selector?" — that question is now RULED (R40); update the label when convenient.
|
||||||
|
|
||||||
|
### §40.3-3 — FOG SIGNAGE: the HUD and the map now say where you are, and classic never grows the DOM
|
||||||
|
|
||||||
|
Three surfaces that said nothing now consume A's layer (LANE_A_NOTES §39 contract verbatim: print
|
||||||
|
`street || label`, handle null once, never branch on town type). All zero-draw DOM/canvas:
|
||||||
|
|
||||||
|
| surface | before | after (measured strings) |
|
||||||
|
|---|---|---|
|
||||||
|
| HUD readout | no location line | **`street` row**: `Lurline Street` (katoomba_real, supplier `ways`) · `the south end of the main street` (synthetic, district supplier) |
|
||||||
|
| door tooltip | `🚪 Little Paris Cafe — click to enter` | `🚪 Little Paris Cafe · Katoomba Street — click to enter` (and `· the south end of the main street` on the synthetic) |
|
||||||
|
| fog-map caption | `… 4 shops known · 2 streets walked · press M to close` | `… 4 shops known · 2 streets walked · Lurline Street · press M to close` |
|
||||||
|
|
||||||
|
- **`discovery.js` gains `createStreetLocator(plan)`** — the probe's own spatial hash, read-only:
|
||||||
|
`edgeAt(x,z)` (kerb + 4 m pad, nearest at intersections) and `shopNear(x,z)` (nearest shopfront
|
||||||
|
≤28 m, the locality fallback where streets are nameless). Signage is deliberately NOT keyed to
|
||||||
|
the fog ledger — knowing the name of the street you are standing on is information design, not
|
||||||
|
discovery. Resolution rides the existing `frame % 6` throttles (µs per lookup, DOM writes only on
|
||||||
|
change).
|
||||||
|
- **Classic-gated by construction, the house pattern:** the shell builds the locator only
|
||||||
|
`!CLASSIC` and `hud.setAddresses` is then never called ⇒ the `#pc-street` node **does not
|
||||||
|
exist** in classic's DOM and the tooltip string is the pre-R40 byte. A/B on the same door, same
|
||||||
|
seed: classic `🚪 Anglibarn Op Shop — click to enter` vs default
|
||||||
|
`🚪 Anglibarn Op Shop · the south end of the main street — click to enter`. The map's caption
|
||||||
|
suffix lives inside the fog arm, which classic (null store) never enters. r40_lane_b.py asserts
|
||||||
|
both arms (row resolves on default, node absent on classic).
|
||||||
|
|
||||||
|
### §40.4 (routed by Fable mid-round) — THE ARCADE KIT: E's placement rule applied in B's files
|
||||||
|
|
||||||
|
E measured, B applied, exactly as specced (LANE_E_NOTES §40): **the roof spans (slab depth 2.5 =
|
||||||
|
the half-lane, opposing slabs meet flush on the centreline) and a spanned roof has no posts.**
|
||||||
|
|
||||||
|
- `chunks.js`: `ctx.arcadeBlocks` derived once per manager from **`district.kind`, never
|
||||||
|
`edge.kind`** (real towns: 1,102 arcade-KIND edges = 39 km of ordinary footpath, zero arcade
|
||||||
|
districts).
|
||||||
|
- `buildings.js`: `ARCADEKIT` flag in the explicit VERGE/YARDS idiom — **`?classic=1` forces it
|
||||||
|
off** (mandatory: the synthetic IS the classic boot), `?arcadekit=0` is the standing
|
||||||
|
falsifiability control. In-arcade lots get depth 2.5 and skip the post loop; everywhere else the
|
||||||
|
code path is byte-identical pre-R40.
|
||||||
|
- **Measured on the final tree:** post instances **−34 exactly** (E's number), lane-interior view
|
||||||
|
**draws 120 → 120 (+0)**, 0 console errors; shot pair
|
||||||
|
`docs/shots/laneB_r40/r40_arcade_lane_{before,after}.png` (before = `?arcadekit=0`) matches E's
|
||||||
|
reference pair; classic lane view unchanged by construction (flag forced off ⇒ pre-R40 branch)
|
||||||
|
and classic's pin cell re-measured **byte-exact** (269 / 90,580) after the change.
|
||||||
|
- E's kit note (the `ground-arcade-roof` ceiling skin at +1 draw × 2 chunks) is NOT taken this
|
||||||
|
round — it is a visible-cost spend and the round's items didn't ask for it; filed as the next
|
||||||
|
arcade polish.
|
||||||
|
|
||||||
|
### The budget table of record, FINAL tree (all edits in), pin method
|
||||||
|
|
||||||
|
| mode | worst draws | law | margin | worst tris |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| default noon | 282 | ≤300 | 18 | 100,538 |
|
||||||
|
| **default night** | **291** | ≤300 | **9** | 120,093 |
|
||||||
|
| `?classic=1` night | **269** | ≤300 | 31 | **90,580 (byte-exact to R38 pin)** |
|
||||||
|
| `?r=3` night (diagnostic) | **391** → gate reads 386 @ 8 m | **≤420 (its own declared law)** | 29–34 | 132,421 |
|
||||||
|
|
||||||
|
Goldens: `selfcheck.js` **157,647/157,647, fingerprint 0x5f76e76 — unmoved** (run after every edit
|
||||||
|
wave). Zero console errors on every boot exercised. Files touched: `web/index.html` (RADIUS block,
|
||||||
|
createHUD budget, address wiring, PROCITY.budget — all `[Lane B R40]`), `web/js/world/hud.js`,
|
||||||
|
`minimap.js`, `discovery.js`, `dbg.js`, `chunks.js`, `buildings.js`, NEW `tools/qa/r40_lane_b.py`,
|
||||||
|
`docs/shots/laneB_r40/`.
|
||||||
|
|
||||||
## Round 39 (v9 Layer 2 — **THE FOG**, item 39.2) — the map costs what it always cost: nothing
|
## Round 39 (v9 Layer 2 — **THE FOG**, item 39.2) — the map costs what it always cost: nothing
|
||||||
|
|
||||||
**`minimap.js` drew every lot in the plan from the first frame. It now draws what you have walked past,
|
**`minimap.js` drew every lot in the plan from the first frame. It now draws what you have walked past,
|
||||||
|
|||||||
BIN
docs/shots/laneB_r40/r40_arcade_lane_after.png
Normal file
BIN
docs/shots/laneB_r40/r40_arcade_lane_after.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 522 KiB |
BIN
docs/shots/laneB_r40/r40_arcade_lane_before.png
Normal file
BIN
docs/shots/laneB_r40/r40_arcade_lane_before.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 510 KiB |
BIN
docs/shots/laneB_r40/r40_arcade_lane_classic.png
Normal file
BIN
docs/shots/laneB_r40/r40_arcade_lane_classic.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 561 KiB |
138
tools/qa/r40_lane_b.py
Normal file
138
tools/qa/r40_lane_b.py
Normal file
@ -0,0 +1,138 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""PROCITY Lane B — R40 §40.3 gate: the ?r= budget law · classic's town selector · fog signage.
|
||||||
|
|
||||||
|
[Lane B R40] NEW FILE (races no other lane's edits). Three rulings, each with both arms asserted so
|
||||||
|
none of them is vacuous. → Lane F: one line in qa.sh wires it (see LANE_B_NOTES §40).
|
||||||
|
|
||||||
|
1. THE ?r= BUDGET LAW (§40.3-1). `?r=` above the auto radius is a DIAGNOSTIC boot and now declares
|
||||||
|
its own law (index.html: `PROCITY.budget` = {draws: 420, tris: 200k, diagnostic: true} + a
|
||||||
|
console warn). Measured R40 (pin method, stepwalk 2 m yaw 0 laps 2, synthetic, seed 20261990):
|
||||||
|
default 282 noon / 291 night vs ?r=3 382 noon / 391 night at 45-48 live chunks — structural
|
||||||
|
(the live-chunk window, not a regression), so the round's ruling is GATE, not shave.
|
||||||
|
Asserted here: the declaration (both arms: default says 300/not-diagnostic, r=3 says
|
||||||
|
420/diagnostic + warns) AND the measurement — a night stepwalk under r=3 must stay ≤ the boot's
|
||||||
|
OWN declared ceiling. If the town grows past 420 this gate goes red and the ceiling is re-pinned
|
||||||
|
consciously. If r=3 ever measures ≤300 the exemption is unexercised — reported, not failed.
|
||||||
|
|
||||||
|
2. CLASSIC'S TOWN SELECTOR (§40.3-2, Fable's R40 ruling: classic SHOWS it — "information, not
|
||||||
|
gameplay state"). Asserted: `?classic=1` boots with the selector present, ≥20 town options, and
|
||||||
|
its towns-fetch surface is exactly the one named POST_V2_EXCEPTION (assets/towns/index.json).
|
||||||
|
|
||||||
|
3. FOG SIGNAGE (§40.3-3, classic-gated by construction). Asserted both arms: the default boot's HUD
|
||||||
|
carries the `#pc-street` row and it RESOLVES (non-"—" standing at a shopfront — on the synthetic
|
||||||
|
that is a locality label, LANE_A_NOTES §39: print `label`, never branch on town type); classic's
|
||||||
|
HUD contains NO `#pc-street` node at all (setAddresses never called ⇒ byte-identical DOM).
|
||||||
|
|
||||||
|
Run: tools/.venv/bin/python tools/qa/r40_lane_b.py (own no-store server, fresh contexts)
|
||||||
|
"""
|
||||||
|
import pathlib, sys
|
||||||
|
|
||||||
|
sys.path.insert(0, str(pathlib.Path(__file__).resolve().parent))
|
||||||
|
from budget_walk import SEED, SPINE_JS, STEPWALK_JS, boot, serve, summarise # the pinned method, not a reimplementation
|
||||||
|
|
||||||
|
try:
|
||||||
|
from playwright.sync_api import sync_playwright
|
||||||
|
except ImportError:
|
||||||
|
sys.exit("playwright not installed — tools/.venv/bin/pip install playwright")
|
||||||
|
|
||||||
|
ROOT = pathlib.Path(__file__).resolve().parent.parent.parent
|
||||||
|
PORT = 8967
|
||||||
|
DIAG_CAP = 420 # the ?r>auto diagnostic ceiling (index.html declares it; this must match)
|
||||||
|
STREET_CAP = 300
|
||||||
|
|
||||||
|
fails = []
|
||||||
|
def OK(m): print(f" \033[32m✓\033[0m {m}")
|
||||||
|
def FAIL(m): fails.append(m); print(f" \033[31m✗ FAIL\033[0m {m}")
|
||||||
|
def NOTE(m): print(f" \033[33m·\033[0m {m}")
|
||||||
|
|
||||||
|
|
||||||
|
def new_page(b):
|
||||||
|
pg = b.new_page(viewport={'width': 1280, 'height': 720})
|
||||||
|
state = {'errs': [], 'warns': [], 'reqs': []}
|
||||||
|
pg.on('console', lambda m: (state['errs'] if m.type == 'error' else state['warns']).append(m.text)
|
||||||
|
if m.type in ('error', 'warning') else None)
|
||||||
|
pg.on('request', lambda r: state['reqs'].append(r.url))
|
||||||
|
return pg, state
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
srv = serve(ROOT / 'web', PORT)
|
||||||
|
host = f'http://127.0.0.1:{PORT}'
|
||||||
|
with sync_playwright() as p:
|
||||||
|
b = p.chromium.launch()
|
||||||
|
|
||||||
|
# ── arm A: default boot declares the street law; signage row present and resolving ──
|
||||||
|
print("\n\033[1m▶ default boot — street law declared · fog signage resolves\033[0m")
|
||||||
|
pg, st = new_page(b)
|
||||||
|
boot(pg, host, '', None)
|
||||||
|
r = pg.evaluate("""() => {
|
||||||
|
const B = window.PROCITY.budget;
|
||||||
|
return { budget: B, streetRow: !!document.getElementById('pc-street'),
|
||||||
|
selector: !!document.getElementById('pc-town-select') }; }""")
|
||||||
|
(OK if r['budget'] and r['budget']['draws'] == STREET_CAP and not r['budget']['diagnostic'] else FAIL)(
|
||||||
|
f"default PROCITY.budget = {r['budget']} (expect draws {STREET_CAP}, diagnostic false)")
|
||||||
|
(OK if r['streetRow'] else FAIL)("default HUD has the #pc-street signage row")
|
||||||
|
# stand at a shopfront (the arcade bookmark fronts the dept anchor) and let the throttle run
|
||||||
|
pg.evaluate("() => window.DBG.shot('street_noon')")
|
||||||
|
pg.wait_for_timeout(700) # ≥ 6 street frames at any sane fps
|
||||||
|
txt = pg.evaluate("() => (document.getElementById('pc-street')||{}).textContent || ''")
|
||||||
|
(OK if txt and txt != '—' else FAIL)(f"street row RESOLVES at a shopfront: '{txt}'")
|
||||||
|
(OK if not st['errs'] else FAIL)(f"0 console errors (got {len(st['errs'])})")
|
||||||
|
pg.close()
|
||||||
|
|
||||||
|
# ── arm B: classic — selector shown (the R40 ruling), signage absent by construction ──
|
||||||
|
print("\n\033[1m▶ ?classic=1 — town selector shown · signage DOM absent\033[0m")
|
||||||
|
pg, st = new_page(b)
|
||||||
|
boot(pg, host, 'classic=1', None)
|
||||||
|
pg.wait_for_timeout(1200) # let the index.json upgrade land
|
||||||
|
r = pg.evaluate("""() => {
|
||||||
|
const sel = document.getElementById('pc-town-select');
|
||||||
|
return { nOpts: sel ? sel.options.length : 0,
|
||||||
|
streetRow: !!document.getElementById('pc-street'),
|
||||||
|
budget: window.PROCITY.budget }; }""")
|
||||||
|
(OK if r['nOpts'] >= 20 else FAIL)(f"classic selector present with {r['nOpts']} options (≥20)")
|
||||||
|
town_fetches = [u for u in st['reqs'] if '/assets/towns/' in u]
|
||||||
|
only_index = all(u.endswith('/assets/towns/index.json') for u in town_fetches)
|
||||||
|
(OK if town_fetches and only_index else FAIL)(
|
||||||
|
f"classic towns-fetch surface is exactly the named exception (index.json): {town_fetches}")
|
||||||
|
(OK if not r['streetRow'] else FAIL)("classic HUD has NO #pc-street node (byte-identical by construction)")
|
||||||
|
(OK if r['budget'] and not r['budget']['diagnostic'] else FAIL)("classic declares the street law")
|
||||||
|
(OK if not st['errs'] else FAIL)(f"0 console errors (got {len(st['errs'])})")
|
||||||
|
pg.close()
|
||||||
|
|
||||||
|
# ── arm C: ?r=3 — declares its own law, warns, and MEASURES inside it (night, worst class) ──
|
||||||
|
print("\n\033[1m▶ ?r=3 — diagnostic law declared + warned · measured ≤ its own ceiling\033[0m")
|
||||||
|
pg, st = new_page(b)
|
||||||
|
boot(pg, host, 'r=3', 5)
|
||||||
|
r = pg.evaluate("() => window.PROCITY.budget")
|
||||||
|
(OK if r and r['draws'] == DIAG_CAP and r['diagnostic'] else FAIL)(
|
||||||
|
f"?r=3 PROCITY.budget = {r} (expect draws {DIAG_CAP}, diagnostic true)")
|
||||||
|
warned = any('DIAGNOSTIC boot' in w for w in st['warns'])
|
||||||
|
(OK if warned else FAIL)("the ?r= console warning fired (the flag documents its own law)")
|
||||||
|
path = pg.evaluate(SPINE_JS, 800)
|
||||||
|
raw = pg.evaluate(STEPWALK_JS, {'path': path, 'stepm': 8, 'laps': 1, 'fixedYaw': 0})
|
||||||
|
s = summarise('r3/night/stepwalk8', raw)
|
||||||
|
worst = s['draws_worst']
|
||||||
|
(OK if worst <= DIAG_CAP else FAIL)(
|
||||||
|
f"?r=3 night worst {worst} draws ≤ its declared ceiling {DIAG_CAP} "
|
||||||
|
f"(p50 {s['draws_p50']}, tris worst {s['tris_worst']:,}, n={s['n']})")
|
||||||
|
(OK if s['tris_worst'] <= 200_000 else FAIL)(f"?r=3 tris worst {s['tris_worst']:,} ≤ 200,000 (tri law unchanged)")
|
||||||
|
if worst <= STREET_CAP:
|
||||||
|
NOTE(f"?r=3 measured ≤{STREET_CAP} — the exemption is unexercised; consider retiring it")
|
||||||
|
else:
|
||||||
|
OK(f"non-vacuous: ?r=3 exceeds the street law ({worst} > {STREET_CAP}) — the exemption is load-bearing")
|
||||||
|
(OK if not st['errs'] else FAIL)(f"0 console errors (got {len(st['errs'])})")
|
||||||
|
pg.close()
|
||||||
|
|
||||||
|
b.close()
|
||||||
|
srv.shutdown()
|
||||||
|
print()
|
||||||
|
if fails:
|
||||||
|
print(f"\033[31m✗ r40_lane_b: {len(fails)} assertion(s) failed\033[0m")
|
||||||
|
return 1
|
||||||
|
print("\033[32m✓ r40_lane_b: all assertions green\033[0m")
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
sys.exit(main())
|
||||||
@ -265,6 +265,24 @@ const YARDS = (() => {
|
|||||||
} catch (e) { return false; } // unparseable search ⇒ pre-R38 geometry, never a throw
|
} catch (e) { return false; } // unparseable search ⇒ pre-R38 geometry, never a throw
|
||||||
})();
|
})();
|
||||||
|
|
||||||
|
// [Lane B R40 §40.4 — THE ARCADE KIT (E's placement rule, Fable-routed)] The one arcade lane is
|
||||||
|
// 42.00 × 5 m with lot faces at ±2.50 m, and the R39-era awning rule planted two rows of posts
|
||||||
|
// 1.00 m apart down its centreline — 34 colliderless posts standing in the lane's own doorway.
|
||||||
|
// The kit designs posts + roof as ONE: a real arcade's roof SPANS (slab depth 2.5 = the half-lane,
|
||||||
|
// so opposing slabs meet flush and the 0.60 m centre slot closes) AND A SPANNED ROOF HAS NO POSTS.
|
||||||
|
// No new geometry — deeper awning instances, deleted post instances; E measured +0 draws.
|
||||||
|
// The gate is EXPLICIT, not by-absence (R39's warning): the arcade district exists only on the
|
||||||
|
// synthetic and the synthetic IS the classic boot, so ungated this moves the golden render.
|
||||||
|
// `?classic=1` keeps today's geometry byte-identical; `?arcadekit=0` is the standing
|
||||||
|
// falsifiability control (the `?verge=0` / `?yards=0` idiom).
|
||||||
|
const ARCADEKIT = (() => {
|
||||||
|
try {
|
||||||
|
const p = new URLSearchParams(location.search);
|
||||||
|
const classic = p.has('classic') && p.get('classic') !== '0';
|
||||||
|
return !classic && p.get('arcadekit') !== '0';
|
||||||
|
} catch (e) { return false; } // unparseable search ⇒ pre-R40 geometry, never a throw
|
||||||
|
})();
|
||||||
|
|
||||||
// Measured against the picture, not the spec sheet: a paling fence and a Hills Hoist are within
|
// Measured against the picture, not the spec sheet: a paling fence and a Hills Hoist are within
|
||||||
// 30 cm of each other in real life, and the first cut had the fence TALLER (1.75) than the washing
|
// 30 cm of each other in real life, and the first cut had the fence TALLER (1.75) than the washing
|
||||||
// line's drop, which hid an entire street of back yards behind their own fences from every angle a
|
// line's drop, which hid an entire street of back yards behind their own fences from every angle a
|
||||||
@ -632,17 +650,24 @@ export function buildChunkBuildings(chunkData, ctx) {
|
|||||||
|
|
||||||
// posted verandah + awning over the footpath (main-street shopfronts only)
|
// posted verandah + awning over the footpath (main-street shopfronts only)
|
||||||
if (lot.use === 'shop' || lot.use === 'anchor') {
|
if (lot.use === 'shop' || lot.use === 'anchor') {
|
||||||
|
// [Lane B R40 §40.4 — THE ARCADE KIT] in an arcade DISTRICT the roof spans the lane (2.5 =
|
||||||
|
// the half-lane, opposing slabs meet flush on the centreline) and a spanned roof has no
|
||||||
|
// posts. Everywhere else: the unchanged posted verandah. See ARCADEKIT above for the gate.
|
||||||
|
const inArcade = ARCADEKIT && ctx.arcadeBlocks && ctx.arcadeBlocks.has(lot.block);
|
||||||
|
const depth = inArcade ? 2.5 : AWNING_DEPTH;
|
||||||
const askin = AWNING_SKINS[(seed >>> 3) % AWNING_SKINS.length]; // unsigned: seed is uint32
|
const askin = AWNING_SKINS[(seed >>> 3) % AWNING_SKINS.length]; // unsigned: seed is uint32
|
||||||
// awning box: thin slab reaching over the footpath at the front
|
// awning box: thin slab reaching over the footpath at the front
|
||||||
_p.copy(new THREE.Vector3().fromArray(toWorld(lot, 0, AWNING_Y, d / 2 + AWNING_DEPTH / 2, [])));
|
_p.copy(new THREE.Vector3().fromArray(toWorld(lot, 0, AWNING_Y, d / 2 + depth / 2, [])));
|
||||||
_s.set(w, 0.16, AWNING_DEPTH);
|
_s.set(w, 0.16, depth);
|
||||||
awnings.push({ matrix: new THREE.Matrix4().compose(_p, _q, _s), color: new THREE.Color(skins.awningColor(askin)) });
|
awnings.push({ matrix: new THREE.Matrix4().compose(_p, _q, _s), color: new THREE.Color(skins.awningColor(askin)) });
|
||||||
// two posts at the awning's outer edge
|
// two posts at the awning's outer edge — never inside a spanned arcade
|
||||||
const postY = AWNING_Y / 2;
|
if (!inArcade) {
|
||||||
for (const ox of [-w / 2 + 0.25, w / 2 - 0.25]) {
|
const postY = AWNING_Y / 2;
|
||||||
_p.fromArray(toWorld(lot, ox, postY, d / 2 + AWNING_DEPTH - 0.2, []));
|
for (const ox of [-w / 2 + 0.25, w / 2 - 0.25]) {
|
||||||
_s.set(0.12, AWNING_Y, 0.12);
|
_p.fromArray(toWorld(lot, ox, postY, d / 2 + AWNING_DEPTH - 0.2, []));
|
||||||
boxMats.push({ matrix: new THREE.Matrix4().compose(_p, _q, _s), color: new THREE.Color('#2b2b2b') });
|
_s.set(0.12, AWNING_Y, 0.12);
|
||||||
|
boxMats.push({ matrix: new THREE.Matrix4().compose(_p, _q, _s), color: new THREE.Color('#2b2b2b') });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -16,6 +16,15 @@ export function createChunkManager(plan, scene, ctx) {
|
|||||||
const index = chunkIndex(plan);
|
const index = chunkIndex(plan);
|
||||||
prefetchStockIndex(plan); // R26 #5: keyed plans only — warm the stock manifest before any chunk builds
|
prefetchStockIndex(plan); // R26 #5: keyed plans only — warm the stock manifest before any chunk builds
|
||||||
|
|
||||||
|
// [Lane B R40 §40.4 — THE ARCADE KIT, district plumbing] The builder needs to know which BLOCKS
|
||||||
|
// sit in an arcade DISTRICT (lots carry `block`, but block→district lives only on the plan).
|
||||||
|
// Keyed on `district.kind`, NEVER `edge.kind` — the R39 rule: real towns carry 1,102 arcade-KIND
|
||||||
|
// edges (39 km of ordinary covered footpath) and zero arcade DISTRICTS, so an edge-kind test
|
||||||
|
// would re-roof half a real town. Derived once per manager, read by buildings.js's awning block.
|
||||||
|
const kindById = new Map((plan.districts || []).map((d) => [d.id, d.kind]));
|
||||||
|
ctx.arcadeBlocks = new Set((plan.blocks || [])
|
||||||
|
.filter((b) => kindById.get(b.district) === 'arcade').map((b) => b.id));
|
||||||
|
|
||||||
const live = new Map(); // key → { buildings, furniture, colliders, cx, cz }
|
const live = new Map(); // key → { buildings, furniture, colliders, cx, cz }
|
||||||
const doorMeshes = []; // live door meshes (for HUD raycast)
|
const doorMeshes = []; // live door meshes (for HUD raycast)
|
||||||
let queue = []; // keys pending build, nearest-first
|
let queue = []; // keys pending build, nearest-first
|
||||||
|
|||||||
@ -40,6 +40,11 @@ export function installDBG(deps) {
|
|||||||
geometries: r.memory.geometries, textures: r.memory.textures,
|
geometries: r.memory.geometries, textures: r.memory.textures,
|
||||||
chunk: `${Math.round(player.position.x / CS)},${Math.round(player.position.z / CS)}`,
|
chunk: `${Math.round(player.position.x / CS)},${Math.round(player.position.z / CS)}`,
|
||||||
chunks: chunks.count, mode: getMode(),
|
chunks: chunks.count, mode: getMode(),
|
||||||
|
// [Lane B R40 §40.3] the draw/tri law THIS boot answers to ({draws, tris, radius, auto,
|
||||||
|
// diagnostic}) — a harness asserts info().budget.draws, never a hardcoded 300, so a ?r=
|
||||||
|
// diagnostic boot is measured against its own declared ceiling (420) and a default boot
|
||||||
|
// against the street law. Read off window.PROCITY so DBG needs no new wiring.
|
||||||
|
budget: (window.PROCITY && window.PROCITY.budget) || null,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -73,6 +73,66 @@ function segDist2(px, pz, ax, az, bx, bz) {
|
|||||||
return dx * dx + dz * dz;
|
return dx * dx + dz * dz;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// [Lane B R40 §40.3 — FOG SIGNAGE] createStreetLocator(plan) → "where am I standing?" for the HUD
|
||||||
|
// and the map caption. The same plan-derived spatial hash the probe uses (edges by padded bbox,
|
||||||
|
// shopfronts by point), read-only: it answers with IDS — `edgeAt(x,z)` → the street edge under your
|
||||||
|
// feet (kerb + footpath tolerance, nearest wins at an intersection) and `shopNear(x,z)` → the
|
||||||
|
// nearest shopfront within `shopRadius` — and the consumer asks Lane A's address layer for the
|
||||||
|
// words (streetOf / localityOf, LANE_A_NOTES §39: print `label`, never branch on town type).
|
||||||
|
// Pure plan math, zero draws, no store, no probe side effects: knowing the name of the street you
|
||||||
|
// are on is signage, not discovery, so it is deliberately NOT keyed to the fog ledger. Classic
|
||||||
|
// never constructs it (the shell's address block is `!CLASSIC`-gated for signage), so classic's
|
||||||
|
// HUD/map DOM stays byte-identical by construction.
|
||||||
|
export function createStreetLocator(plan, { streetPad = FOG_STREET_PAD_M, shopRadius = 28 } = {}) {
|
||||||
|
const nodes = new Map((plan.streets?.nodes || []).map((n) => [n.id, n]));
|
||||||
|
const edges = [];
|
||||||
|
for (const e of plan.streets?.edges || []) {
|
||||||
|
const a = nodes.get(e.a), b = nodes.get(e.b);
|
||||||
|
if (!a || !b) continue;
|
||||||
|
const pad = (e.width || 0) / 2 + streetPad;
|
||||||
|
edges.push({ id: e.id, ax: a.x, az: a.z, bx: b.x, bz: b.z, pad2: pad * pad, pad });
|
||||||
|
}
|
||||||
|
const fronts = shopFronts(plan);
|
||||||
|
const cells = new Map();
|
||||||
|
const key = (cx, cz) => cx + ',' + cz;
|
||||||
|
const cellOf = (cx, cz) => { const k = key(cx, cz); let c = cells.get(k); if (!c) cells.set(k, c = { s: [], e: [] }); return c; };
|
||||||
|
for (const f of fronts) cellOf(Math.floor(f.x / CELL), Math.floor(f.z / CELL)).s.push(f);
|
||||||
|
for (const e of edges) {
|
||||||
|
const x0 = Math.floor((Math.min(e.ax, e.bx) - e.pad) / CELL), x1 = Math.floor((Math.max(e.ax, e.bx) + e.pad) / CELL);
|
||||||
|
const z0 = Math.floor((Math.min(e.az, e.bz) - e.pad) / CELL), z1 = Math.floor((Math.max(e.az, e.bz) + e.pad) / CELL);
|
||||||
|
for (let cx = x0; cx <= x1; cx++) for (let cz = z0; cz <= z1; cz++) cellOf(cx, cz).e.push(e);
|
||||||
|
}
|
||||||
|
const R2 = shopRadius * shopRadius; // shopRadius < CELL, so the 3×3 ring always covers it
|
||||||
|
return {
|
||||||
|
edgeAt(x, z) {
|
||||||
|
let best = null, bd = Infinity;
|
||||||
|
const cx = Math.floor(x / CELL), cz = Math.floor(z / CELL);
|
||||||
|
for (let ix = cx - 1; ix <= cx + 1; ix++) for (let iz = cz - 1; iz <= cz + 1; iz++) {
|
||||||
|
const c = cells.get(key(ix, iz));
|
||||||
|
if (!c) continue;
|
||||||
|
for (const e of c.e) {
|
||||||
|
const d2 = segDist2(x, z, e.ax, e.az, e.bx, e.bz);
|
||||||
|
if (d2 <= e.pad2 && d2 < bd) { bd = d2; best = e.id; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return best;
|
||||||
|
},
|
||||||
|
shopNear(x, z) {
|
||||||
|
let best = null, bd = R2;
|
||||||
|
const cx = Math.floor(x / CELL), cz = Math.floor(z / CELL);
|
||||||
|
for (let ix = cx - 1; ix <= cx + 1; ix++) for (let iz = cz - 1; iz <= cz + 1; iz++) {
|
||||||
|
const c = cells.get(key(ix, iz));
|
||||||
|
if (!c) continue;
|
||||||
|
for (const f of c.s) {
|
||||||
|
const d2 = (x - f.x) ** 2 + (z - f.z) ** 2;
|
||||||
|
if (d2 <= bd) { bd = d2; best = f.id; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return best;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
// createDiscovery({ plan, known, radius? }) → the probe.
|
// createDiscovery({ plan, known, radius? }) → the probe.
|
||||||
// known — the KNOWN STORE (save.js `game.knownStore(fogKey)`): addShop/addEdge/hasShop/hasEdge/
|
// known — the KNOWN STORE (save.js `game.knownStore(fogKey)`): addShop/addEdge/hasShop/hasEdge/
|
||||||
// counts/version. Anything with that shape works (the headless gates pass a plain object).
|
// counts/version. Anything with that shape works (the headless gates pass a plain object).
|
||||||
|
|||||||
@ -43,7 +43,11 @@ const CSS = `
|
|||||||
.pc-crate-empty{opacity:.7;padding:4px 0 2px}
|
.pc-crate-empty{opacity:.7;padding:4px 0 2px}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export function createHUD({ camera, renderer, plan, getDoorMeshes, onEnterShop }) {
|
// [Lane B R40 §40.3] `budget` — the draw/tri law THIS boot answers to ({ draws, tris }). The default
|
||||||
|
// is the street law; the shell passes its diagnostic ceiling under a ?r= override so the readout's
|
||||||
|
// red warn tells the truth about the boot being run rather than shaming a documented diagnostic.
|
||||||
|
export function createHUD({ camera, renderer, plan, getDoorMeshes, onEnterShop,
|
||||||
|
budget = { draws: 300, tris: 200_000 } }) {
|
||||||
if (!document.getElementById('pc-hud-style')) {
|
if (!document.getElementById('pc-hud-style')) {
|
||||||
const st = document.createElement('style'); st.id = 'pc-hud-style'; st.textContent = CSS;
|
const st = document.createElement('style'); st.id = 'pc-hud-style'; st.textContent = CSS;
|
||||||
document.head.appendChild(st);
|
document.head.appendChild(st);
|
||||||
@ -74,6 +78,14 @@ export function createHUD({ camera, renderer, plan, getDoorMeshes, onEnterShop }
|
|||||||
$('pc-seed').textContent = plan.citySeed;
|
$('pc-seed').textContent = plan.citySeed;
|
||||||
|
|
||||||
// ── town selector (R19; R21 reads E's towns index) — synthetic + OSM fixtures + real-road towns.
|
// ── town selector (R19; R21 reads E's towns index) — synthetic + OSM fixtures + real-road towns.
|
||||||
|
// [Lane B R40 §40.3 — RULED, no longer a debt-ledger question] `?classic=1` SHOWS this selector:
|
||||||
|
// "classic is the shop-window mode; hiding the 23 towns from it serves nobody — it's information,
|
||||||
|
// not gameplay state" (ROUND40 rulings). It was already true by construction (this block has no
|
||||||
|
// flag test) and is now DELIBERATE: the selector is zero-draw DOM chrome (a <select> the renderer
|
||||||
|
// never sees), its one fetch (assets/towns/index.json) is the named POST_V2_EXCEPTION Fable's R37
|
||||||
|
// ruling carries for classic, and navigation preserves the full query string — so a town picked
|
||||||
|
// FROM classic stays classic. Verified R40: classic boots with all 27 options, 1 towns-fetch
|
||||||
|
// (index.json only), 0 console errors. qa asserts it: tools/qa/r40_lane_b.py.
|
||||||
// Navigates to the matching URL params, preserving everything else (seed, flags). Lives in the HUD
|
// Navigates to the matching URL params, preserving everything else (seed, flags). Lives in the HUD
|
||||||
// overlay, so it hides with the HUD for money shots; usable when the pointer is free (Esc). No
|
// overlay, so it hides with the HUD for money shots; usable when the pointer is free (Esc). No
|
||||||
// index.html seam. The real-town list is built SYNCHRONOUSLY from the fallback (so the picker always
|
// index.html seam. The real-town list is built SYNCHRONOUSLY from the fallback (so the picker always
|
||||||
@ -304,6 +316,49 @@ export function createHUD({ camera, renderer, plan, getDoorMeshes, onEnterShop }
|
|||||||
let hovered = null; // { shopId, name }
|
let hovered = null; // { shopId, name }
|
||||||
let frame = 0, fpsAcc = 0, fpsCount = 0, fpsShown = 0;
|
let frame = 0, fpsAcc = 0, fpsCount = 0, fpsShown = 0;
|
||||||
|
|
||||||
|
// ── [Lane B R40 §40.3 — FOG SIGNAGE] Lane A's address layer on the HUD ────────────────────────
|
||||||
|
// GATED BY CONSTRUCTION: none of this DOM exists until the shell calls setAddresses(), and the
|
||||||
|
// shell only does that on non-classic boots — so ?classic=1's HUD is byte-identical with no flag
|
||||||
|
// test here (the R19 selector / R30 game-UI pattern). Two surfaces, both zero-draw DOM:
|
||||||
|
// • a "street" row in the debug readout — the street you are standing on (locator.edgeAt →
|
||||||
|
// addr.streetOf), falling back to the nearest shopfront's locality label (addr.localityOf →
|
||||||
|
// street || label, LANE_A_NOTES §39: print `label`, never branch on town type);
|
||||||
|
// • the door tooltip carries the shop's street/locality between name and action.
|
||||||
|
// Resolution rides the existing frame%6 throttle (the door-raycast cadence); the address calls
|
||||||
|
// are wrapped once — a throwing layer disables itself rather than spamming (minimap.js's rule).
|
||||||
|
let addresses = null, locator = null, elStreet = null, shownStreet = null;
|
||||||
|
const whereCache = new Map(); // shopId → street/label string | null (localityOf is pure per boot)
|
||||||
|
function whereOf(shopId) {
|
||||||
|
if (!addresses || typeof addresses.localityOf !== 'function') return null;
|
||||||
|
if (whereCache.has(shopId)) return whereCache.get(shopId);
|
||||||
|
let text = null;
|
||||||
|
try { const loc = addresses.localityOf(shopId); text = (loc && (loc.street || loc.label)) || null; }
|
||||||
|
catch (err) { addresses = null; return null; }
|
||||||
|
whereCache.set(shopId, text);
|
||||||
|
return text;
|
||||||
|
}
|
||||||
|
function setAddresses(a, loc) {
|
||||||
|
addresses = a || null; locator = loc || null;
|
||||||
|
whereCache.clear(); shownStreet = null;
|
||||||
|
if (addresses && locator && !elStreet) {
|
||||||
|
const row = document.createElement('div');
|
||||||
|
row.innerHTML = 'street <b id="pc-street">—</b>';
|
||||||
|
$('pc-town').parentElement.insertAdjacentElement('afterend', row);
|
||||||
|
elStreet = row.querySelector('#pc-street');
|
||||||
|
}
|
||||||
|
if (elStreet) elStreet.textContent = '—';
|
||||||
|
}
|
||||||
|
function resolveStreet() {
|
||||||
|
if (!addresses || !locator || !elStreet) return;
|
||||||
|
let text = null;
|
||||||
|
try {
|
||||||
|
const eid = locator.edgeAt(camera.position.x, camera.position.z);
|
||||||
|
if (eid != null && typeof addresses.streetOf === 'function') text = addresses.streetOf(eid);
|
||||||
|
if (!text) { const sid = locator.shopNear(camera.position.x, camera.position.z); if (sid != null) text = whereOf(sid); }
|
||||||
|
} catch (err) { addresses = null; return; }
|
||||||
|
if (text !== shownStreet) { shownStreet = text; elStreet.textContent = text || '—'; }
|
||||||
|
}
|
||||||
|
|
||||||
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) {
|
||||||
@ -334,7 +389,7 @@ export function createHUD({ camera, renderer, plan, getDoorMeshes, onEnterShop }
|
|||||||
if (fpsAcc >= 0.4) { fpsShown = Math.round(fpsCount / fpsAcc); fpsAcc = 0; fpsCount = 0; }
|
if (fpsAcc >= 0.4) { fpsShown = Math.round(fpsCount / fpsAcc); fpsAcc = 0; fpsCount = 0; }
|
||||||
|
|
||||||
frame++;
|
frame++;
|
||||||
if (frame % 6 === 0) raycastDoor();
|
if (frame % 6 === 0) { raycastDoor(); resolveStreet(); } // [Lane B R40] signage rides the raycast cadence
|
||||||
|
|
||||||
// v7 game surface — lazy construction + refresh both live in tickGame() (R33 fix: the build used
|
// v7 game surface — lazy construction + refresh both live in tickGame() (R33 fix: the build used
|
||||||
// to happen ONLY here in street frames, so a door entered fast after boot had no crate/bar inside
|
// to happen ONLY here in street frames, so a door entered fast after boot had no crate/bar inside
|
||||||
@ -347,9 +402,12 @@ export function createHUD({ camera, renderer, plan, getDoorMeshes, onEnterShop }
|
|||||||
const h = hovered.hours;
|
const h = hovered.hours;
|
||||||
const now = window.PROCITY && window.PROCITY.currentHour ? window.PROCITY.currentHour() : null;
|
const now = window.PROCITY && window.PROCITY.currentHour ? window.PROCITY.currentHour() : null;
|
||||||
const closed = h && now != null && !(now >= h[0] && now < h[1]);
|
const closed = h && now != null && !(now >= h[0] && now < h[1]);
|
||||||
|
// [Lane B R40 §40.3] the shop's street/locality on the tooltip (null when unresolved or classic
|
||||||
|
// — setAddresses never called ⇒ `where` is always null ⇒ the pre-R40 string, by construction)
|
||||||
|
const where = hovered.shopId != null ? whereOf(hovered.shopId) : null;
|
||||||
elTip.textContent = closed
|
elTip.textContent = closed
|
||||||
? `🔒 ${hovered.name} — CLOSED · opens ${String(h[0]).padStart(2, '0')}:00`
|
? `🔒 ${hovered.name} — CLOSED · opens ${String(h[0]).padStart(2, '0')}:00`
|
||||||
: `🚪 ${hovered.name} — click to enter`;
|
: `🚪 ${hovered.name}${where ? ' · ' + where : ''} — click to enter`;
|
||||||
elTip.style.opacity = 1;
|
elTip.style.opacity = 1;
|
||||||
} else elTip.style.opacity = 0;
|
} else elTip.style.opacity = 0;
|
||||||
|
|
||||||
@ -358,8 +416,8 @@ export function createHUD({ camera, renderer, plan, getDoorMeshes, onEnterShop }
|
|||||||
if (info.clock) elClock.textContent = `${info.clock.label} ${info.clock.hour}${info.clock.night ? ' 🌙' : ''}`;
|
if (info.clock) elClock.textContent = `${info.clock.label} ${info.clock.hour}${info.clock.night ? ' 🌙' : ''}`;
|
||||||
elFps.textContent = fpsShown;
|
elFps.textContent = fpsShown;
|
||||||
const draws = renderer.info.render.calls, tris = renderer.info.render.triangles;
|
const draws = renderer.info.render.calls, tris = renderer.info.render.triangles;
|
||||||
elDraws.innerHTML = draws > 300 ? `<span class="warn">${draws}</span>` : draws;
|
elDraws.innerHTML = draws > budget.draws ? `<span class="warn">${draws}</span>` : draws; // [Lane B R40] the boot's own law
|
||||||
elTris.innerHTML = tris > 200000 ? `<span class="warn">${(tris / 1000 | 0)}k</span>` : `${(tris / 1000 | 0)}k`;
|
elTris.innerHTML = tris > budget.tris ? `<span class="warn">${(tris / 1000 | 0)}k</span>` : `${(tris / 1000 | 0)}k`;
|
||||||
if (info.chunks != null) elChunks.textContent = info.chunks;
|
if (info.chunks != null) elChunks.textContent = info.chunks;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -399,5 +457,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
|
||||||
}
|
}
|
||||||
|
|||||||
@ -35,6 +35,12 @@ const MAX_LABELS = 40;
|
|||||||
|
|
||||||
export function createMinimap(plan, known = null, opts = {}) {
|
export function createMinimap(plan, known = null, opts = {}) {
|
||||||
let addresses = opts.addresses || null; // Lane A's createAddresses(): streetOf / localityOf
|
let addresses = opts.addresses || null; // Lane A's createAddresses(): streetOf / localityOf
|
||||||
|
// [Lane B R40 §40.3 — FOG SIGNAGE] optional street locator (discovery.createStreetLocator): lets
|
||||||
|
// the fog-arm caption say where YOU are standing ("· Katoomba Street"). Consumed ONLY inside the
|
||||||
|
// fog arm (`known !== null`), so the classic/null-arm map — caption included — stays byte-for-byte
|
||||||
|
// pre-R39 with no flag test here, exactly like `known` itself. The shell passes null under classic.
|
||||||
|
let locator = opts.locator || null;
|
||||||
|
let capBase = null, capLoc = null; // caption = capBase + (' · ' + where-you-are) + tail
|
||||||
|
|
||||||
const wrap = document.createElement('div');
|
const wrap = document.createElement('div');
|
||||||
wrap.id = 'pc-map';
|
wrap.id = 'pc-map';
|
||||||
@ -258,9 +264,30 @@ export function createMinimap(plan, known = null, opts = {}) {
|
|||||||
for (const id of known.edgeIds()) { let n = null; try { n = addresses.streetOf(id); } catch (e) { names.clear(); break; } if (n) names.add(n); }
|
for (const id of known.edgeIds()) { let n = null; try { n = addresses.streetOf(id); } catch (e) { names.clear(); break; } if (n) names.add(n); }
|
||||||
if (names.size) streets = names.size;
|
if (names.size) streets = names.size;
|
||||||
}
|
}
|
||||||
cap.textContent = `${plan.name} — seed ${plan.citySeed} · ${c.shops} shop${c.shops === 1 ? '' : 's'} known · `
|
capBase = `${plan.name} — seed ${plan.citySeed} · ${c.shops} shop${c.shops === 1 ? '' : 's'} known · `
|
||||||
+ (streets != null ? `${streets} street${streets === 1 ? '' : 's'}` : `${c.edges} street segment${c.edges === 1 ? '' : 's'}`)
|
+ (streets != null ? `${streets} street${streets === 1 ? '' : 's'}` : `${c.edges} street segment${c.edges === 1 ? '' : 's'}`)
|
||||||
+ ` walked · press M to close`;
|
+ ` walked`;
|
||||||
|
cap.textContent = capBase + (capLoc ? ` · ${capLoc}` : '') + ` · press M to close`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// [Lane B R40 §40.3] where the player dot IS: the street under your feet by name, else the nearest
|
||||||
|
// shopfront's locality label — A's contract verbatim (`street || label`, print it, never test the
|
||||||
|
// town type; null is rendered by omission). Fog arm only; a throwing layer disables itself.
|
||||||
|
function refreshLocation(px, pz) {
|
||||||
|
if (!locator || !addresses || capBase == null) return;
|
||||||
|
let text = null;
|
||||||
|
try {
|
||||||
|
const eid = locator.edgeAt(px, pz);
|
||||||
|
if (eid != null && typeof addresses.streetOf === 'function') text = addresses.streetOf(eid);
|
||||||
|
if (!text && typeof addresses.localityOf === 'function') {
|
||||||
|
const sid = locator.shopNear(px, pz);
|
||||||
|
if (sid != null) { const loc = addresses.localityOf(sid); text = (loc && (loc.street || loc.label)) || null; }
|
||||||
|
}
|
||||||
|
} catch (err) { locator = null; return; }
|
||||||
|
if (text !== capLoc) {
|
||||||
|
capLoc = text;
|
||||||
|
cap.textContent = capBase + (capLoc ? ` · ${capLoc}` : '') + ` · press M to close`;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function draw(playerPos, fwd) {
|
function draw(playerPos, fwd) {
|
||||||
@ -277,6 +304,7 @@ export function createMinimap(plan, known = null, opts = {}) {
|
|||||||
}
|
}
|
||||||
ctx.clearRect(0, 0, W, H);
|
ctx.clearRect(0, 0, W, H);
|
||||||
ctx.drawImage(off, 0, 0);
|
ctx.drawImage(off, 0, 0);
|
||||||
|
refreshLocation(playerPos.x, playerPos.z); // [Lane B R40] caption: where the dot is (µs; writes DOM only on change)
|
||||||
}
|
}
|
||||||
const x = T.tx(playerPos.x), z = T.tz(playerPos.z);
|
const x = T.tx(playerPos.x), z = T.tz(playerPos.z);
|
||||||
// heading (world +X→right, +Z→down on the map)
|
// heading (world +X→right, +Z→down on the map)
|
||||||
@ -292,8 +320,9 @@ export function createMinimap(plan, known = null, opts = {}) {
|
|||||||
setVisible: (v) => { wrap.style.display = v ? 'flex' : 'none'; },
|
setVisible: (v) => { wrap.style.display = v ? 'flex' : 'none'; },
|
||||||
dispose: () => wrap.remove(),
|
dispose: () => wrap.remove(),
|
||||||
// [R39] Lane A's address layer, injectable — the shell wires it at boot when citygen exports
|
// [R39] Lane A's address layer, injectable — the shell wires it at boot when citygen exports
|
||||||
// createAddresses, and it can land mid-session without a reload.
|
// createAddresses, and it can land mid-session without a reload. [R40] + the optional street
|
||||||
setAddresses(a) { addresses = a || null; cacheKey = null; lastCounts = null; },
|
// locator (fog-arm caption signage; the shell passes null under classic — see the header note).
|
||||||
|
setAddresses(a, loc) { addresses = a || null; locator = loc || null; cacheKey = null; lastCounts = null; capLoc = null; },
|
||||||
// what the map is actually showing, for the gates: metres across, and the px a shop lot renders at
|
// what the map is actually showing, for the gates: metres across, and the px a shop lot renders at
|
||||||
get view() {
|
get view() {
|
||||||
const med = (arr) => { const b = arr.slice().sort((p, q) => p - q); return b.length ? b[b.length >> 1] : 0; };
|
const med = (arr) => { const b = arr.slice().sort((p, q) => p - q); return b.length ? b[b.length >> 1] : 0; };
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user