Builds the last missing v0.2 floor mechanic (design §3.2). A fight is not a modal: two patrons square up, a 7s fuse burns through five escalating shove rungs, and you stop it by physically standing between them — it spends the floor's only real resource, where you are. The tell (shoving silhouettes + pulsing marker) deliberately reads OUTSIDE the torch cone, because spotting it is the gameplay. Adversarially reviewed the whole lane, then hand-adjudicated the findings. Six real defects fixed, several in code from round 1: - PatDownOverlay compared 'Escape' against the scene's uppercased 'ESCAPE', so ESC was dead there while the on-screen hint promised it worked. - The spawn cap counted departed agents (CrowdSim keeps them so find() works), so every ejection permanently shrank the venue and the floor drained. - FloorView's sprite cache is keyed by patron id and outlived resetRun, while _resetPatronSerial recycles ids from p0 — reseeding gave the new crowd the old run's dolls. - escalation's per-incident `resolved` was consumed as a permanent per-patron `handled`, retiring a correctly-watered `loose` patron for the whole night while they kept drinking. Now settles per stage and re-arms when they worsen. - Stall busts retired the captured occupant list, but the crowd keeps simulating behind the modal. Re-queries at resolution. - Stall infractions put a stall id in the frozen `patronId` field. Also wired teardown to Phaser's SHUTDOWN event; a `shutdown()` method is never called, so leaving the floor leaked the view, sim, overlays and bus listeners. Fight balance was measured, not guessed: at a 20s cooldown an idle night brewed 28 fights and 28 heat strikes against a run cap of 3. Now 150s (~5/night) with a deferred heat strike, matching §3.2's "possible Heat". An idle night ends at vibe 0 / 5 strikes; an attentive one at vibe 54 / 0 strikes. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
274 lines
16 KiB
Markdown
274 lines
16 KiB
Markdown
# LANE HANDOVER LOG
|
||
|
||
Coordination file between lanes (Opus 4.8 execution sessions) and the reviewer
|
||
(Fable). **Append-only** — never edit or delete previous blocks. Newest block at the
|
||
BOTTOM. Push after every session so the reviewer can see state remotely.
|
||
|
||
## Protocol
|
||
|
||
1. Before starting work: read this whole file + your lane file in `lanes/` +
|
||
`docs/CONTRACTS.md`. Check for a REVIEW block addressed to your lane — it
|
||
overrides your lane file where they conflict.
|
||
2. Work only inside your lane's owned directories (see BUILD_PLAN.md §2).
|
||
3. End of session: append a SESSION block (template below), commit, push your
|
||
`lane/<name>` branch.
|
||
4. Never merge to main — the reviewer does that.
|
||
5. Blocked on a contract ambiguity? Don't stall and don't silently change the
|
||
contract: stub locally with `// TODO(contract):`, log a CONTRACT CHANGE REQUEST
|
||
below, keep moving.
|
||
|
||
## SESSION block template
|
||
|
||
```markdown
|
||
---
|
||
### SESSION — <LANE-NAME> — <date> <time>
|
||
**Branch/commits:** lane/<name> @ <short-sha>..<short-sha>
|
||
**Done:** bullet list of what actually works now (be honest — "renders but untested" ≠ "done")
|
||
**How to see it:** exact command + what to click (e.g. `npm run dev` → press D → deny 3 patrons)
|
||
**Tests:** X passing / Y failing (name the failures)
|
||
**Broke / known-wonky:** anything you left imperfect
|
||
**Contract change requests:** none | numbered list with justification
|
||
**Questions for reviewer:** none | numbered list
|
||
**Next session should:** bullet list
|
||
```
|
||
|
||
## REVIEW block template (reviewer only)
|
||
|
||
```markdown
|
||
---
|
||
### REVIEW — Fable — <date>
|
||
**Reviewed:** <lanes/commits>
|
||
**Merged to main:** <yes/no + sha>
|
||
**Contract decisions:** rulings on any open change requests
|
||
**Instructions for <LANE>:** what to do next / what to fix first
|
||
```
|
||
|
||
---
|
||
|
||
*(log starts below)*
|
||
|
||
---
|
||
### SESSION — LANE-0 (executed by Fable, reviewer) — 2026-07-19 16:30
|
||
**Branch/commits:** main (reviewer executed Phase 0 directly; no lane branch)
|
||
**Done:**
|
||
- Vite + strict-TS + Phaser 3.90 + Vitest + ESLint scaffold; `Math.random` banned by lint rule.
|
||
- All CONTRACTS.md types verbatim in `src/data/types.ts`.
|
||
- `core/`: SeededRNG (mulberry32, named streams), typed EventBus (snapshot-emit
|
||
semantics), GameClock (real-ms driven, `ageOn` birthday math incl. leap-day),
|
||
Meters (single-writer, hype multiplies positive vibe only, clicker drift),
|
||
StubBeatClock (128 BPM), versioned corrupt-safe localStorage save.
|
||
- `data/`: outfit vocabulary (+PALETTE), 9 archetypes, 10 contraband items,
|
||
17 Dazza texts (8 rule-bearing with ruleIds).
|
||
- `patrons/`: generator (fake-ID tell system, DOBs clustered near the 18
|
||
boundary, late-arrival drunk drift), pure `dollPlan` + Phaser `renderDoll`
|
||
(3 poses, drunk tells baked), regulars' memory store.
|
||
- `rules/drunk.ts`: drunkStage thresholds.
|
||
- ParadeScene demo: rainy street, kebab-shop + venue dressing, 24-walker parade
|
||
with ID cards rendered from the same data, SPACE reseed / D toggle cards.
|
||
- 49 tests green; lint/build/test gate clean.
|
||
**How to see it:** `npm run dev` → http://localhost:5199 (in the Claude preview
|
||
pane, drive frames per CLAUDE.md "Dev notes"). Watch for an `almost18` label
|
||
walking under an ID card claiming 18+ — that's the whole game in one image.
|
||
**Tests:** 49 passing / 0 failing.
|
||
**Broke / known-wonky:**
|
||
- ID cards overlap when the footpath is crowded — cosmetic, parade-only.
|
||
- `window.game` debug handle exposed in main.ts (deliberate, for pane-driving).
|
||
- Spec deviations: doll renderer split into `dollPlan.ts` (pure) + `doll.ts`
|
||
(Phaser) — better than spec'd, plans are pixel-testable without a GPU; added
|
||
a 5th fake-ID tell (expired real card, no boolean tells); no MainMenu scene
|
||
(Boot → Parade direct) — menu is Phase 2 shell work.
|
||
**Contract change requests:** none (contracts implemented as written).
|
||
**Questions for reviewer:** n/a (self-reviewed).
|
||
**Next session should:** Phase 1 lanes are OPEN — see REVIEW block below.
|
||
|
||
---
|
||
### REVIEW — Fable — 2026-07-19
|
||
**Reviewed:** Phase 0 (self-executed; verified via test gate + in-browser parade).
|
||
**Merged to main:** yes (direct).
|
||
**Contract decisions:** CONTRACTS.md now frozen. Extension requests via this file.
|
||
**Instructions for Phase 1 (LANE-DOOR, LANE-FLOOR, LANE-JUICE):**
|
||
1. Branch from current main into `lane/door` / `lane/floor` / `lane/juice`
|
||
(worktrees if sharing a machine). Read your lane file's new
|
||
"Phase-0 reality" section FIRST — it corrects the original spec where the
|
||
landed code differs.
|
||
2. LANE-DOOR is the critical path; if only one Opus session is available, run
|
||
DOOR first, JUICE second, FLOOR third.
|
||
3. Every session ends with the gate + a SESSION block here + a push. Fable
|
||
reviews on request ("review the lanes").
|
||
|
||
---
|
||
### SESSION — LANE-FLOOR — 2026-07-19 18:50
|
||
**Branch/commits:** lane/floor @ 9805ff8..(this commit) — branched from main, worktree `../not-tonight-floor`
|
||
**Done:**
|
||
- `FloorDemoScene` (menu key **F** from Parade): 80x45-tile venue (1280x720, 2x screen,
|
||
camera-follow), 44-patron crowd sim, flashlight cone, UV mode, all three v0.2
|
||
infractions + the stamp check, playable end-to-end.
|
||
- Pure logic, all tested: `venueMap` (programmatic layout + anchors + stalls),
|
||
`cone` (vision maths), `player` (heavy accel + wall slide), `crowdSim`
|
||
(archetype-weighted anchor wandering, clock-driven intoxication drift, beat-driven
|
||
dance bob, stall double-occupancy, escort-out), `bangJudge` (nearest-beat-boundary
|
||
timing), `escalation` (5x3 cut-off matrix), `patDown` (pocket layout + scoring).
|
||
- Phaser layer: `FloorView` (baked tile texture, silhouette/detail split with a
|
||
cone GeometryMask, neon glow pools, UV stamp dots) + three overlays
|
||
(`CutOffOverlay`, `StallOverlay`, `PatDownOverlay`).
|
||
- All floor dialogue in `src/scenes/floor/strings.ts` (see deviation #1).
|
||
**How to see it:** `npm run dev` → press **F** at the Parade → WASD patrol, **TAB** UV,
|
||
**E** interact, **SPACE** bang, **ESC** cancel, **R** reseed. Walk into the dark until
|
||
the torch finds someone. In Claude's preview pane drive frames per CLAUDE.md.
|
||
**Tests:** 169 passing / 0 failing (49 pre-existing + 120 new floor tests).
|
||
**Verified in-browser, not just green:** cut-off (correct rung ejects, one rung too
|
||
soft on a maggot lets them stagger free), escort walks to the exit and logs an
|
||
ejection, stall busts on 3 on-beat bangs and *never* busts on 8 off-beat ones,
|
||
pat-down bins an item (+2 vibe, right flavour line) and leaks it on timeout (-1.5),
|
||
UV shows the stamp glow and marches an unstamped patron out with a Kayden radio line.
|
||
**Broke / known-wonky:**
|
||
- **Two bugs the test suite could not catch, both found by running it and both fixed:**
|
||
1. *Ambient meter collapse.* The bump penalty had a per-agent cooldown but no
|
||
sim-wide bound, so leak rate scaled with crowd size — an unattended venue
|
||
pinned vibe to 0 and aggro to 100 by ~1AM before the player touched anything.
|
||
Added a sim-wide gate (`BUMP_GATE_MS`); an unattended night now drifts
|
||
50→27 vibe / 0→34 aggro. Regression test added.
|
||
2. *Frozen beat behind modals.* `beat.update()` sat below the overlay early-return
|
||
in the scene loop, so opening the stall minigame stopped the beat clock and the
|
||
rhythm game could never receive a tick. Clock + beat now run regardless of overlays.
|
||
- Dwell times raised (2.5-9s → 7-22s): at the old values ~68% of the crowd was
|
||
permanently in transit and the bar/dance floor read as empty.
|
||
- Tile palette + darkness alpha retuned; at the original values the venue
|
||
multiplied down to near-black and you could not tell a dance floor from a wall.
|
||
- Light2D dropped in `FloorView` (documented in-file) — the pipeline swap fights the
|
||
cone GeometryMask, and the mask is the actual gameplay. The look is carried by a
|
||
darkness sheet + additive cone + neon glow pools instead.
|
||
- `GeometryMask` on a container is a WebGL-first path; fine under `Phaser.AUTO`,
|
||
would misbehave if anything ever forced the Canvas renderer.
|
||
- The 16x16 top-down doll reads as a coloured ring from above (dark hair over
|
||
outer-layer shoulders). Correct per Phase 0's `dollPlan`, just noting it.
|
||
- Stall interiors are deliberately sealed (`stall` walkable, `stallDoor` not), so
|
||
patrons are moved in/out via `StallDef.inside`/`.door` rather than pathed.
|
||
**Contract change requests:**
|
||
1. **`incident:log` event.** `core` exposes no incident API and `EventMap` is frozen,
|
||
so `FloorDemoScene` appends to `NightState.incidents` directly (marked
|
||
`// TODO(contract):`). Three lanes writing that array directly is exactly the
|
||
shared-mutable-state fight the bus discipline exists to prevent, and Phase 3's
|
||
inspector/audit needs a single writer. Requesting
|
||
`'incident:log': IncidentRecord` plus a handler in `core/meters.ts`.
|
||
2. **Aggro has no decay.** `Meters` only ratchets aggro upward, so it is a one-way
|
||
trip to 100 over a long night. Ambient pressure is now bounded (above), but a slow
|
||
downward drift on a quiet floor would make the meter a *state* rather than a
|
||
countdown. Flagging as a design/contract question, not patching it unilaterally.
|
||
**Questions for reviewer:**
|
||
1. **Floor strings placement.** CONTRACTS.md §7 says Aussie dialogue lives in
|
||
`src/data/strings/`, but `data/` is LANE-0's and lanes commit only in their own
|
||
dirs. Parked in `src/scenes/floor/strings.ts`. Move it to
|
||
`src/data/strings/floor.ts` on your word — it is a pure file move.
|
||
2. **`.gitignore` gap (affects every lane using a worktree).** It lists
|
||
`node_modules/` with a trailing slash, which does not match a *symlinked*
|
||
`node_modules` — worktrees that symlink it show `?? node_modules` as untracked.
|
||
I staged paths explicitly, but a lane running `git add -A` would commit a symlink
|
||
pointing at an absolute path on John's machine. One-char fix (drop the slash);
|
||
left alone since `.gitignore` is not in my lane.
|
||
3. Escalation numbers (clean +4 vibe, scene -3/+5 per rung of overreach) are
|
||
deliberately small so aggression never feels rewarded. Confirm the balance intent
|
||
before LANE-DOOR mirrors it at the rope.
|
||
**Next session should:**
|
||
- Phase 2 integration: replace the self-populating spawner with the real admitted-patron
|
||
list from LANE-DOOR (the scene already only ever consumes `Patron[]`).
|
||
- Swap `StubBeatClock` for LANE-JUICE's `TechnoEngine` — `StallOverlay` takes
|
||
`beatIntervalMs` in its constructor and reads `beat:tick` off the bus, so it should
|
||
be a one-line change.
|
||
- Fight-brewing infraction (design §3.2) is specced but NOT built — the only v0.2
|
||
floor mechanic still missing.
|
||
- Replace `HudStub` with LANE-JUICE's real meter HUD.
|
||
|
||
---
|
||
### SESSION — LANE-FLOOR (round 2) — 2026-07-19 20:56
|
||
**Branch/commits:** lane/floor @ 3eee843..(this commit)
|
||
**Done:**
|
||
- **Fight-brewing infraction built** — the last missing v0.2 floor mechanic (design §3.2).
|
||
Not a modal: two patrons square up, a 7s fuse burns through five escalating shove
|
||
rungs, and you stop it by physically standing between them. `fight.ts` (pure:
|
||
`isBetween` segment maths, `stepFight`, `scoreFight`, eligibility) +
|
||
`fightDirector.ts` (lifecycle, one fight at a time, cooldown, pin/release) +
|
||
a `setFight` tell in `FloorView` + scene wiring. `crowdSim` gained a
|
||
`'squaringUp'` activity that pins an agent, plus `pinForFight`/`releaseFromFight`.
|
||
- **Adversarial review of the whole lane** (5 dimensions x ~3k lines), then I
|
||
hand-adjudicated the findings and fixed six real defects — see below.
|
||
**How to see it:** `npm run dev` → **F** → patrol. A fight announces itself with a
|
||
pulsing marker and shoving silhouettes **visible outside the torch cone** (verified
|
||
by screenshot with the beam pointed away) — spotting it is the gameplay. Prompt
|
||
reads GET BETWEEN THEM; stand in the gap before the fuse burns.
|
||
**Tests:** 225 passing / 0 failing (+56 this round: `fight.test.ts`, `fightDirector.test.ts`).
|
||
**Verified in-browser:** fight breaks up (+6 vibe, both released, no heat) and swings
|
||
(−12 vibe, +20 aggro, exactly one heat strike, neither marked handled); shove rungs
|
||
escalate 0→4; the tell reads with the torch facing away; all four round-1 mechanics
|
||
still work after the fixes.
|
||
|
||
**Balance, measured rather than guessed** — same seed, whole night:
|
||
| | idle player | attentive player |
|
||
|---|---|---|
|
||
| vibe | 0 | 54 |
|
||
| aggro | 100 | 45 |
|
||
| heat strikes | 5 | 0 |
|
||
Doing nothing is ruinous; doing the job holds the room. That gradient is the point.
|
||
|
||
**Bugs found and fixed this round (all confirmed by reading the code, several in
|
||
code I wrote myself last session):**
|
||
1. `PatDownOverlay` checked `'Escape'` while the scene routes `'ESCAPE'` (it
|
||
uppercases) — ESC was dead in the pat-down while the on-screen hint promised it.
|
||
The other two overlays handled both spellings, which is why round 1 missed it.
|
||
2. The spawn cap and HUD counted `crowd.agents.length`, but CrowdSim deliberately
|
||
keeps departed agents so `find()` works — so **every ejection permanently shrank
|
||
the venue** and the floor drained over the night. Now counts live bodies.
|
||
(Verified: culled 15 → refilled to 44.)
|
||
3. `FloorView`'s sprite cache is keyed by `patron.id` and survives `resetRun`, but
|
||
`_resetPatronSerial()` recycles ids from `p0` — so **R** gave the new crowd the
|
||
previous run's dolls, sway and stamps. Added `FloorView.reset()`.
|
||
4. `escalation`'s per-incident `resolved` was consumed as a permanent per-patron
|
||
`handled`, so correctly watering a `loose` patron **retired them from all
|
||
tracking for the night while they kept drinking**. The Quiet Messy One could
|
||
never come back around. Now settled per-stage: they re-arm when they get worse.
|
||
5. Stall busts marked the **captured** occupant list handled, but the crowd keeps
|
||
simulating behind the modal — unrelated patrons who had wandered off got retired.
|
||
Now re-queries occupancy at resolution.
|
||
6. `floor:infractionSpotted` for a stall put a **stall id in the frozen `patronId`
|
||
field** (resolves to nobody), and the dedupe key meant one report per cubicle per
|
||
night. Now reports an occupant, so a fresh pair reports again.
|
||
7. Also wired `teardown()` to `Phaser.Scenes.Events.SHUTDOWN` — a `shutdown()`
|
||
*method* is not invoked by Phaser, so the previous version leaked FloorView,
|
||
CrowdSim, overlays and bus listeners every time you left the floor.
|
||
**Fight rebalance (found by running it, not by tests):** at a 20s cooldown an idle
|
||
night brewed **28 fights, all missed, 28 heat strikes** against a run cap of 3 —
|
||
fights swamped every other meter. Cooldown → 150s (≈5/night) and the heat strike is
|
||
now `deferred: true`, matching §3.2's "possible Heat" and the contract's
|
||
inspection/audit semantics. Two tests updated to match; the cooldown test now derives
|
||
its window from the constants so it can't silently pass vacuously again.
|
||
**Broke / known-wonky:**
|
||
- **My review harness under-reported and I overrode it.** 16 raw findings, and the
|
||
adversarial verify stage confirmed **zero** — I had required both skeptics to fail
|
||
to refute *and* told them to default to "refuted" when uncertain. That combination
|
||
produced a 100% false-negative rate: six of the sixteen were real, including two
|
||
found independently by three reviewers. I adjudicated them by hand instead.
|
||
Flagging because the *pattern* matters for anyone reusing that harness: a
|
||
refute-biased verifier plus unanimity is not a quality bar, it is an off switch.
|
||
- Fights burn their fuse through overlays. Deliberate — you can't be in a cubicle
|
||
and between two blokes at once, and ESC always exits — but it means a fight
|
||
starting during a 10s pat-down is a forced choice. Confirm you want that.
|
||
- An idle night still pins aggro at 100 by ~1AM. Aggro has no decay (see round 1
|
||
request #2); with that in place the idle curve would breathe rather than saturate.
|
||
**Contract change requests:** both from round 1 still open and unchanged —
|
||
(1) `incident:log` event + single writer in `core/meters.ts`; (2) aggro decay.
|
||
No new ones: the fight reuses the existing `'fight'` infraction kind and `heat:strike`.
|
||
**Questions for reviewer:**
|
||
1. `patrons/doll.ts` caches textures by key and never releases them, so each reseed
|
||
mints a fresh set with no upper bound (a long session leaks canvases). It is
|
||
LANE-0's file and frozen, so I left it — worth a Phase 2 ticket.
|
||
2. Fight numbers (broken +6 / swung −12 vibe, +20 aggro, deferred heat) and the
|
||
150s cooldown are my calls from the measured table above. Confirm before
|
||
LANE-DOOR mirrors any of it.
|
||
3. Round 1's questions on floor strings placement and the `.gitignore` symlink gap
|
||
are both still open.
|
||
**Next session should:**
|
||
- Phase 2 integration: real admitted-patron list from LANE-DOOR, LANE-JUICE's
|
||
`TechnoEngine` for `beat:tick`, real meter HUD.
|
||
- All five v0.2 floor infractions are now built — the floor is feature-complete
|
||
against design §3.2.
|