not-tonight/LANEHANDOVER.md
type-two 906a366ba8 LANE-FLOOR round 2: fight-brewing infraction + six defect fixes
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>
2026-07-19 20:57:28 +10:00

274 lines
16 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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.