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
|
||||
`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**
|
||||
|
||||
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)
|
||||
|
||||
## 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
|
||||
|
||||
**`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
|
||||
})();
|
||||
|
||||
// [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
|
||||
// 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
|
||||
@ -632,17 +650,24 @@ export function buildChunkBuildings(chunkData, ctx) {
|
||||
|
||||
// posted verandah + awning over the footpath (main-street shopfronts only)
|
||||
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
|
||||
// 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, [])));
|
||||
_s.set(w, 0.16, AWNING_DEPTH);
|
||||
_p.copy(new THREE.Vector3().fromArray(toWorld(lot, 0, AWNING_Y, d / 2 + depth / 2, [])));
|
||||
_s.set(w, 0.16, depth);
|
||||
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
|
||||
const postY = AWNING_Y / 2;
|
||||
for (const ox of [-w / 2 + 0.25, w / 2 - 0.25]) {
|
||||
_p.fromArray(toWorld(lot, ox, postY, d / 2 + AWNING_DEPTH - 0.2, []));
|
||||
_s.set(0.12, AWNING_Y, 0.12);
|
||||
boxMats.push({ matrix: new THREE.Matrix4().compose(_p, _q, _s), color: new THREE.Color('#2b2b2b') });
|
||||
// two posts at the awning's outer edge — never inside a spanned arcade
|
||||
if (!inArcade) {
|
||||
const postY = AWNING_Y / 2;
|
||||
for (const ox of [-w / 2 + 0.25, w / 2 - 0.25]) {
|
||||
_p.fromArray(toWorld(lot, ox, postY, d / 2 + AWNING_DEPTH - 0.2, []));
|
||||
_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);
|
||||
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 doorMeshes = []; // live door meshes (for HUD raycast)
|
||||
let queue = []; // keys pending build, nearest-first
|
||||
|
||||
@ -40,6 +40,11 @@ export function installDBG(deps) {
|
||||
geometries: r.memory.geometries, textures: r.memory.textures,
|
||||
chunk: `${Math.round(player.position.x / CS)},${Math.round(player.position.z / CS)}`,
|
||||
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;
|
||||
}
|
||||
|
||||
// [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.
|
||||
// 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).
|
||||
|
||||
@ -43,7 +43,11 @@ const CSS = `
|
||||
.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')) {
|
||||
const st = document.createElement('style'); st.id = 'pc-hud-style'; st.textContent = CSS;
|
||||
document.head.appendChild(st);
|
||||
@ -74,6 +78,14 @@ export function createHUD({ camera, renderer, plan, getDoorMeshes, onEnterShop }
|
||||
$('pc-seed').textContent = plan.citySeed;
|
||||
|
||||
// ── 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
|
||||
// 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
|
||||
@ -304,6 +316,49 @@ export function createHUD({ camera, renderer, plan, getDoorMeshes, onEnterShop }
|
||||
let hovered = null; // { shopId, name }
|
||||
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) {
|
||||
let best = null, bd = Infinity;
|
||||
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; }
|
||||
|
||||
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
|
||||
// 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 now = window.PROCITY && window.PROCITY.currentHour ? window.PROCITY.currentHour() : null;
|
||||
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
|
||||
? `🔒 ${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;
|
||||
} 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 ? ' 🌙' : ''}`;
|
||||
elFps.textContent = fpsShown;
|
||||
const draws = renderer.info.render.calls, tris = renderer.info.render.triangles;
|
||||
elDraws.innerHTML = draws > 300 ? `<span class="warn">${draws}</span>` : draws;
|
||||
elTris.innerHTML = tris > 200000 ? `<span class="warn">${(tris / 1000 | 0)}k</span>` : `${(tris / 1000 | 0)}k`;
|
||||
elDraws.innerHTML = draws > budget.draws ? `<span class="warn">${draws}</span>` : draws; // [Lane B R40] the boot's own law
|
||||
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;
|
||||
}
|
||||
}
|
||||
@ -399,5 +457,6 @@ export function createHUD({ camera, renderer, plan, getDoorMeshes, onEnterShop }
|
||||
}
|
||||
|
||||
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 = {}) {
|
||||
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');
|
||||
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); }
|
||||
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'}`)
|
||||
+ ` 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) {
|
||||
@ -277,6 +304,7 @@ export function createMinimap(plan, known = null, opts = {}) {
|
||||
}
|
||||
ctx.clearRect(0, 0, W, H);
|
||||
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);
|
||||
// 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'; },
|
||||
dispose: () => wrap.remove(),
|
||||
// [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.
|
||||
setAddresses(a) { addresses = a || null; cacheKey = null; lastCounts = null; },
|
||||
// createAddresses, and it can land mid-session without a reload. [R40] + the optional street
|
||||
// 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
|
||||
get view() {
|
||||
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