600 lines
39 KiB
Markdown
600 lines
39 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-DOOR — 2026-07-19 18:50
|
||
**Branch/commits:** lane/door @ 9805ff8..(this commit) — branched from main after Phase 0,
|
||
worked in worktree `../not-tonight-door`.
|
||
|
||
**Done** (all verified by playing it, not just by tests):
|
||
- **Full Door-only night is playable start→summary.** `npm run dev` boots straight into
|
||
9:00 PM, patrons arrive on a curve, you work the rope, rule on them, Dazza texts land,
|
||
meters move, 3:00 AM → summary. Both fail states reachable.
|
||
- **Rules engine** (`src/rules/`, pure, no Phaser):
|
||
- `dressCode.ts` — 8 rules. `text`/`activeFrom` are READ OUT of `data/strings/dazza.ts`
|
||
by ruleId rather than retyped, and the module throws at load if a rule and its
|
||
announcing text ever disagree, so the card can't drift from the message.
|
||
- `idCheck.ts` — all the ID date maths (`idVerdict`, `idDisplayLines`). Builds on
|
||
`ageOn`, does not reimplement it. Catches the 5th tell (expired-but-otherwise-real).
|
||
- `judge.ts` — the single scoring function + `JUDGE_TUNING` (every magic number named).
|
||
- `sobriety.ts` — challenge content + the drunk-performance model.
|
||
- `doorTypes.ts` — this lane's contract EXTENSIONS (see change requests below).
|
||
- **Scene layer** (`src/scenes/door/`): `NightScene` (thin night shell — clock, meters,
|
||
deferred consequences, fail states, marked `TODO(integration)`), `DoorScene` (tableau +
|
||
desk + all input), `QueueManager` + `arrivalCurve` (pure, no Phaser), `PatronUpView`
|
||
(×3 doll, 5 hover zones), `IdCardView` (tray thumbnail + half-screen zoom),
|
||
`PhoneWidget` (buzz, preview, scrollback thread), `DressCodeCard` (NEW badges),
|
||
`SobrietyModal`, `StampFx` (marked `TODO(juice)`), `NightSummaryScene`, `inspect.ts`,
|
||
`dazzaSchedule.ts`, `ui.ts`.
|
||
- **Tests: 49 → 229** (+180). Every judge branch, every ID boundary (18th birthday
|
||
tonight, leap-day DOB, expiry today vs tomorrow), every dress-code predicate, the queue
|
||
economy, the Dazza schedule, and the inspection vocabulary.
|
||
|
||
**How to see it:** `npm run dev` → http://localhost:5199.
|
||
Click the rope (or NEXT!) → patron steps up ×3 → **hover their shoes/torso/face** for
|
||
tooltips → click the ID in the tray to zoom it → ADMIT or NOT TONIGHT. Stamp someone to
|
||
see the signature interaction. Wait ~30 in-game min for Dazza's first rule text.
|
||
To fail on purpose: deny everyone (aggro riots by ~10:30 PM), or admit everyone (three
|
||
heat strikes, licence gone at 3 AM).
|
||
In the Claude preview pane, drive frames per CLAUDE.md "Dev notes" — `document.hidden`
|
||
freezes the Phaser loop at frame 0 there. **Drive a whole run inside ONE `javascript_exec`
|
||
call**: splitting the frame stepping across calls hands Phaser a multi-second delta and
|
||
its tweens/timers land in a state that looks like a game bug but isn't. Cost me an hour.
|
||
|
||
**Tests:** 229 passing / 0 failing.
|
||
|
||
**Bugs found by playing it that the test gate could never have caught** (all fixed):
|
||
1. Game opened on ~45 real seconds of empty street — the 9 PM arrival rate was honest and
|
||
unplayable. Raised the curve, capped the max gap, and the QueueManager now seeds one
|
||
patron at construction so somebody is always at the rope on the first frame.
|
||
2. `noLogos` fired on 47.6% of patrons, but `dollPlan.ts` only draws a logo for the TOP
|
||
slot — ~28% of all patrons were deniable for a logo that was never on screen. Narrowed
|
||
to visibly-rendered slots (see CCR-3). Dazza's own text says "no *visible* logos".
|
||
3. **One patron could be ruled up to 11 times.** Game state was being mutated inside the
|
||
stamp's tween callback; if the tween didn't land, the patron stayed at a rope the
|
||
player could keep scoring. All state now settles synchronously in `rule()` and the
|
||
tweens only move pixels. This class of bug is why `busy` also has a token-guarded
|
||
watchdog.
|
||
4. A correctly-played night still rioted at 1:37 AM. Aggro relief only fired on a
|
||
completely empty street, which never happens during the slam. Relief now scales with
|
||
queue slack — the pressure is "keep it moving", not "never deny".
|
||
5. Admit-everything logged **40** heat strikes (one per patron over capacity).
|
||
CONTRACTS.md §3 says heatStrikes is "run-scoped, max 3"; now enforced, and a persistent
|
||
breach is one write-up rather than one per patron.
|
||
6. One stubborn regular reappeared 6 times and ate a third of the night's denials.
|
||
Comebacks capped at 2.
|
||
7. Dazza sent the identical lap text 5 times in a row. Seven phrasings now, picked by the
|
||
same roll that sets the delay.
|
||
8. Determinism leak: denying a regular drew from the `arrivals` stream, so one player
|
||
decision reshuffled every remaining arrival time. Moved to its own `comebacks` stream
|
||
(CONTRACTS.md §6 exists precisely to stop this).
|
||
9. ID card modal covered the patron, making photo-vs-face impossible. Moved right.
|
||
The genuine-hologram reference swatch now sits on the same cream backing as the card —
|
||
a translucent teal reads as a different colour over dark laminate than over card stock,
|
||
so the reference was lying.
|
||
10. Phone preview text spilled off the screen onto the desk; strike reasons on the summary
|
||
ran off the right edge of the canvas. Both clamped.
|
||
|
||
**Broke / known-wonky:**
|
||
- `src/main.ts` is the one file I touched outside my lane — 8 lines, to register the three
|
||
door scenes and boot into `Night` instead of `Parade`. Unavoidable for the "npm run dev
|
||
→ playable night" deliverable. Boot/Parade are still registered; `game.scene.start('Parade')`
|
||
from the console still works. Flagging it explicitly for the merge.
|
||
- `src/data/strings/door.ts` is a NEW file in `data/strings/` — CONTRACTS.md §7 says Aussie
|
||
strings live there, and it adds no types, but `data/` is nominally frozen-ish. Move it if
|
||
you'd rather.
|
||
- With placeholder art, `noThongsSinglets` / `noWhiteSneakers` / `noBlazers` are not
|
||
distinguishable by silhouette (a thong and a boot are both a 3×4 rect). They ARE
|
||
readable via the hover tooltips, which is why I built the inspection surface before the
|
||
stamp. It costs the player time, which costs aggro — that tradeoff is fine, arguably
|
||
good. It stops being a compromise at the v0.4 art pass (CCR-3).
|
||
- Vibe pins at 100 for a competent player by ~midnight, so late-night vibe stops being a
|
||
live pressure. Aggro and heat still are. Worth a tuning pass in Phase 2; I did not want
|
||
to over-tune against a bot that plays with perfect information and zero deliberation
|
||
time. A human is much slower and will not see these numbers.
|
||
- `NightScene` renders nothing and `DoorScene` draws the HUD. Deliberate, but it means the
|
||
HUD moves to wherever Phase 2 puts the shell.
|
||
- No audio (LANE-JUICE). `StubBeatClock` runs and is consumed for nothing yet — no rhythm
|
||
flourish landed; the stamp is tween-timed, not beat-timed.
|
||
|
||
**Contract change requests:**
|
||
1. **CCR-1 — `PatronFlags.handHolding?: boolean`.** Needed by `noHandHolders`, which is one
|
||
of the 8 already-written Dazza rule texts and otherwise has no data to predicate on.
|
||
Implemented as a `declare module` interface augmentation in `src/rules/doorTypes.ts`, so
|
||
`src/data/types.ts` is byte-identical to main. QueueManager sets it on couples and
|
||
DoorScene draws a linked-hands pixel between them so the rule is visible, not psychic.
|
||
Request: fold the field into `types.ts` properly at integration and delete the augmentation.
|
||
2. **CCR-2 — `VerdictOutcome.hypeDelta?: number`.** Design §3.1 gives rope theatre a hype
|
||
reward but `VerdictOutcome` has no hype channel, so `judge()` cannot express the 'wait'
|
||
verdict's payoff. Same augmentation pattern, in `src/rules/judge.ts`. Same request.
|
||
3. **CCR-3 — renderer request, for LANE-0/art rather than contracts.** `dollPlan.ts` draws
|
||
a logo rect for the TOP slot only, and gives thongs/boots, blazer/bomber, and
|
||
sneaker/heel identical geometry. Four dress-code rules are therefore silhouette-invisible
|
||
and I narrowed `noLogos` to compensate (`LOGO_VISIBLE_SLOTS` in `dressCode.ts`, one
|
||
constant, widen it when the art lands). Requesting per-slot logo marks and distinct
|
||
shoe/outer silhouettes in the v0.4 art pass.
|
||
|
||
**Questions for reviewer:**
|
||
1. Is the `declare module` augmentation pattern acceptable as the standing mechanism for
|
||
additive contract extensions, or do you want every extension to go through you and land
|
||
in `types.ts`? It kept the frozen files clean and I'd use it again, but it is a
|
||
precedent and two other lanes will copy it.
|
||
2. `src/main.ts` and `src/data/strings/door.ts` — ruling on ownership, please. If either is
|
||
off-limits I'll move the strings into `scenes/door/` and hand you a patch for main.ts.
|
||
3. Should heat strikes end the night immediately at 3, or accumulate to the summary as
|
||
they do now? Design §2 says "3 strikes = licence pulled, run over" — I read that as
|
||
run-scoped, so the night still finishes and the summary declares it. Say the word and
|
||
it becomes a third fail state.
|
||
4. Vibe pinning at 100 (above) — worth retuning now, or leave it until there is a real
|
||
playtester rather than a bot?
|
||
|
||
**Next session should:**
|
||
- Take the review findings from the four-lens pass (lane discipline, scene lifecycle,
|
||
playability, balance) — I ran it but landed only what I could verify in-session.
|
||
- Swap `StampFx` for `ui/Stamp` + SFX once LANE-JUICE lands; hook a beat flourish to
|
||
`beat:tick` (currently consumed by nothing).
|
||
- Guest-list clipboard and the UV pass-out stamp are specced in design §3.1 and NOT built —
|
||
they were the right things to cut to get a full playable night.
|
||
- Sobriety line-walk (the third challenge) is still a stretch item; alphabet + riddle ship.
|
||
- Phase 2 integration should lift clock/meters/fail-state ownership out of `NightScene`
|
||
into the real night-flow machine and replace the temporary summary.
|
||
|
||
---
|
||
### SESSION — LANE-DOOR (review-fix pass) — 2026-07-19 19:05
|
||
**Branch/commits:** lane/door @ 30162d3..(this commit)
|
||
|
||
Ran a four-lens adversarial review over the lane (discipline / scene lifecycle /
|
||
playability / balance) and landed the findings I could verify in-session.
|
||
|
||
**Fixed:**
|
||
1. **The game was answering its own question.** `DoorScene` lit up every rule a patron
|
||
broke on the dress-code card the moment they stepped up. Optimal play was "read the red
|
||
line", which deletes the inspection loop the entire scene exists for — all 8 dress-code
|
||
rules were free. The highlight now fires AFTER the ruling, for ~2s, as feedback.
|
||
This is the single most important fix in this pass.
|
||
2. **Tonight's date appeared nowhere in the game.** The card showed `04/12/2000` and the
|
||
HUD showed `11:42 PM`; the player was asked to subtract a birthdate from a date they
|
||
were never given. `underage` is the tell that files a heat strike and costs the
|
||
licence, and it was pure guesswork. Now stamped on the card beside the DOB and under
|
||
the HUD clock. Every boundary case `idCheck.ts` was carefully written for
|
||
(`turnsEighteenTonight`, leap-day DOB) is finally reachable by a player.
|
||
3. **Determinism, again, worse than the one I already fixed.** In `spawnArrival` the
|
||
partner's `generatePatron` call sat behind `if (enqueued)`, and enqueue fails at
|
||
`maxQueue` — which is player-controlled. From the first overflow onward every
|
||
remaining patron of the night differed. All draws are now unconditional. Same class as
|
||
the comeback-stream bug; I fixed it for `couples` and left it in `patrons`.
|
||
4. **You could rule straight through the open sobriety modal.** Its backdrop was not
|
||
interactive, so ADMIT/NOT TONIGHT stayed live underneath. Now eats clicks.
|
||
5. **Tooltips read as broken text** — `navy a blazer`, `white a singlet`, `cream a bucket
|
||
hat`. Articles were baked into the noun tables and then colour-prefixed. Since the
|
||
tooltips are the ONLY detection channel for four rules under placeholder art, the one
|
||
system the player has to trust looked broken. Nouns are bare now.
|
||
6. `bumbag` and `chain` are drawn on the torso/waist but were described only in the head
|
||
zone — hovering the visible item told you about a jacket. Both body zones now name them.
|
||
7. `NightScene.endNight` pushed to `state.heatStrikes` directly, bypassing `core/meters.ts`
|
||
fifty lines after correctly emitting `heat:strike`. Now emits. CONTRACTS.md §4.
|
||
8. Dropped the one `as never` in the diff (`DoorScene`'s `dazza:text` handler) for an
|
||
honest type guard — see CCR-4.
|
||
9. Test quality: `tests/inspect.test.ts` proved its vocabulary coverage by string-comparing
|
||
output against the raw type id, which silently stopped working once a legitimate word
|
||
('skirt', 'cap') matched its own id. Replaced with an exact `hasWordFor` check.
|
||
|
||
**Tests:** 231 passing / 0 failing (+2). Re-verified in-browser after the changes:
|
||
competent play still runs to 3:00 AM; deny-everything still riots (aggro 100 @ 10:42 PM);
|
||
one patron can still only be ruled once, plus up to 2 regular comebacks.
|
||
|
||
**New contract change request:**
|
||
4. **CCR-4 — `dazza:text` payload should carry the lane's rule type.** `EventMap` types it
|
||
`rule?: DressCodeRule`, so a rule this lane emits comes back without the `short` card
|
||
label the lane added to it. Currently narrowed with a runtime type guard in
|
||
`DoorScene`. Either widen the payload or accept the guard.
|
||
|
||
**Reviewer findings I did NOT land, with reasons:**
|
||
- **The economy is slack and dawdling is dominant.** Measured over 5 seeds: a competent
|
||
night pours 749–1115 raw vibe into 50 points of headroom (≈94% discarded by the clamp),
|
||
vibe pins at 100 by ~10:00 PM and stays there 81–86% of the night, and aggro peaks at
|
||
5/100 because relief refunds ~10:1. `hype` is a pure ratchet — nothing in `src/` ever
|
||
emits a negative hype delta — so stalling is paid twice (vibe multiplier + cash bonus)
|
||
and a player who plays *slower* earns 24% more at identical accuracy. Recommended:
|
||
`denyViolatorVibe` 6→1, `admitCleanVibe` 3→1, `QUEUE_TUNING.calmPerSec` 0.9→0.25.
|
||
I did not apply these: it is a whole-economy retune, the measurements come from a bot
|
||
with perfect information and zero deliberation time, and I would rather you tune it
|
||
against a human than have me chase a number I can't feel. Flagging it as the top item
|
||
for the next pass.
|
||
- **The queue never actually backs up.** Player throughput ≈0.83 arrivals/in-game-min
|
||
against a curve that peaks at 0.42 — the "10:30–12:30 slam" is currently fiction
|
||
(measured avg queue 0.1–0.2, max 3–5 against 8 visible slots). Fixing it means roughly
|
||
doubling the curve to ~170 arrivals a night, which is a big feel change; wants a human
|
||
playtest first.
|
||
- **`noSongRequesters` (activeFrom 240) and `noHandHolders` (280) are nearly decorative** —
|
||
they arrive after 84% and 93% of the night's patrons have been served, and fire ~1.3 and
|
||
~0 times respectively. Their `fromMin` values come from `data/strings/dazza.ts`, which
|
||
is not mine to edit. Suggest 240→~105 and 280→~135.
|
||
- **`noHandHolders` is still unfair even when it fires.** The linked-hands pixel only draws
|
||
between adjacent QUEUE sprites; once a patron steps up to the rope there is no partner
|
||
drawn and no tooltip mentions it, so at the moment of judgement the tell is invisible.
|
||
CCR-1's justification ("rendered as a linked-hands pixel ... so the rule stays one the
|
||
player can SEE") overclaims. Either draw the partner at the rope, add it to
|
||
`describeZone`, or cut the rule.
|
||
- **Intoxication and contraband are worth zero points.** `judge()` reads neither. Denying
|
||
a maggot is scored as denying a clean punter (+7 aggro), and a pat-down that finds a
|
||
baggie changes nothing. The drunk channel is the most legible signal the game has — sway
|
||
tween, bloodshot pixels, a whole sobriety minigame — and it does not score. This is the
|
||
biggest *design* gap in the lane and I'd want your ruling on the intended numbers before
|
||
inventing them.
|
||
- **The clicker is inert.** `clickerShown` is read only for a text colour and one summary
|
||
row; `judge` uses the true `insideCount` for capacity. The brief calls the divergence
|
||
"the game", so it needs a consequence — presumably the inspector reads the clicker, not
|
||
reality, which is Phase-2 inspector work.
|
||
- Minor, unlanded: the ID modal's shade dims the desk hologram swatch but not the card's
|
||
(the teal/magenta hue gap carries it today, but a subtler fake colour would break);
|
||
`peelingLaminate` is cream-on-cream and near-invisible; `photoMismatch` degrades to a 1/6
|
||
coin flip for hatted patrons; three player-facing strings are still inline in `judge.ts`
|
||
against CONTRACTS.md §7, one of which ("Good story for them one day") reads as narrator
|
||
rather than the game's voice.
|
||
### 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-JUICE — 2026-07-19 18:55
|
||
**Branch/commits:** lane/juice @ c262ac8..2eb3c94
|
||
**Done:**
|
||
- `audio/TechnoEngine.ts` — synthesized 128 BPM track (kick / off-beat hats /
|
||
2-bar 16th bassline / 8-bar pad), look-ahead scheduler (25ms timer, 100ms
|
||
horizon) as the beat authority. Emits `beat:tick {beatIndex, audioTimeMs}`
|
||
with the **scheduled** time, plus `beat:bar` on downbeats. Drop-in replacement
|
||
for `core/StubBeatClock`. Music → lowpass → master; SFX bypass the filter.
|
||
- `audio/Sfx.ts` — 10 procedural one-shots + `startRain`/`stopRain`. One shared
|
||
noise buffer; voices disconnect on `ended`.
|
||
- `audio/scheduling.ts` + `audio/filterCurve.ts` — pure, node-testable beat grid
|
||
and cutoff-ramp math (the WebAudio classes are thin wrappers over these).
|
||
- `ui/` — `Stamp`, `Phone`, `DialogueBox`, `Clicker`, `MeterHud`, plus
|
||
`style.ts` (chunky widget kit) and `typo.ts` (deterministic drunk typos).
|
||
- `ui/JuiceDemoScene.ts` — exercises every widget and SFX; log-mapped cutoff
|
||
slider, DOOR/FLOOR toggle routed through the bus, beat pad that flashes when
|
||
`ctx.currentTime` reaches the scheduled time (not on event receipt).
|
||
**How to see it:** `npm run dev` → http://localhost:5199/#juice (or press **J**;
|
||
**P** returns to the parade). Click once to unlock audio, then: drag MUFFLE to
|
||
sweep the filter by hand, hit STEP INSIDE for the door→floor sweep, mash the
|
||
SOUNDS column, stamp someone with NOT TONIGHT.
|
||
**Tests:** 111 passing / 0 failing (was 49 at Phase 0; +62 this lane).
|
||
**Verified by ear:** NOTHING. No audio output available in this environment.
|
||
Everything below was logic-checked in a real browser via the exposed engine:
|
||
- door→floor sweep ramps 250Hz→18kHz over 600ms, monotonic, exponential
|
||
(t=0.5 lands ~2.1kHz vs the ~9.1kHz a linear ramp gives).
|
||
- under a genuinely hidden/throttled tab: 52 consecutive beats, **zero**
|
||
scheduled in the past, 75–98ms lead, max 1 beat scheduled per timer wake.
|
||
**John/Fable still need to ear-check:** does the kick read as a kick, is the
|
||
bassline musical, is `typeTick` quiet enough at the real mix level (it fires per
|
||
character), and does the sweep *feel* like a door rather than a filter.
|
||
**Broke / known-wonky:**
|
||
- `src/main.ts` is edited (2 lines + a ~12-line demo route). **Outside LANE-JUICE
|
||
ownership** — see request 2 below. Without it the demo scene is unreachable
|
||
and Vite tree-shakes it out entirely.
|
||
- `TechnoEngine` has no test file — it needs a real `AudioContext` and the suite
|
||
runs in node. Its scheduling/filter math is tested via the pure modules, but
|
||
the `resync` arithmetic is only reachable through the class. Extracting
|
||
`resyncBeat(grid, nextBeat, nowMs)` into `scheduling.ts` would close that gap.
|
||
- `Phone` thread truncates an over-tall newest message rather than scrolling.
|
||
- `Stamp` decals are caller-owned now; the demo caps them at 5. The door scene
|
||
must destroy them (or pass `parent`) or they accumulate over a night.
|
||
**Contract change requests:**
|
||
1. **Add `door:phoneTheatre: { durationMs: number }` to `EventMap`.** Emitted by
|
||
`ui/Phone` when the player closes the phone, carrying how long they stared at
|
||
it while a queue waited — the door scene rewards it with hype. It is a door
|
||
event, not an audio one, so it is outside my namespace. **I did not edit
|
||
`data/types.ts`.** `ui/juiceEvents.ts` declaration-merges it into `EventMap`
|
||
instead: fully typed, zero edits to the frozen file, and folding it into the
|
||
real contract later is a copy-paste. Same file also adds `audio:unlocked` and
|
||
`beat:bar`, which ARE mine per LANE_JUICE.md but are declared together so the
|
||
whole juice surface reads in one place.
|
||
2. **Sign-off requested on the `src/main.ts` edit** (scene registration + J/#juice
|
||
route, marked `// TODO(integration)`). Happy to revert it if you would rather
|
||
wire the demo from the Phase 2 shell yourself.
|
||
**Questions for reviewer:**
|
||
1. `TechnoEngine.unlock()` anchors beat 0 at `currentTime + 60ms` so beat 0 is as
|
||
honest as every other beat. If a consumer assumes beat 0 lands exactly at
|
||
`audio:unlocked.atMs`, that is a 60ms discrepancy — is that acceptable, or
|
||
should `atMs` carry the anchored origin instead?
|
||
2. `MeterHud` counts heat strikes from `heat:strike` events it receives. On a
|
||
mid-run scene swap it would restart from 0. Should it take an initial count in
|
||
its options, or read `NightState.heatStrikes.length` at construction?
|
||
**Next session should:**
|
||
- LANE-DOOR: swap the local stamp for `ui/Stamp` (`// TODO(juice)`), and take
|
||
`MeterHud` in place of `HudStub`.
|
||
- Someone with speakers should ear-check the mix and tell me what to retune.
|
||
- Consider extracting `resyncBeat` into `scheduling.ts` so the throttled-tab
|
||
resync gets direct test coverage.
|
||
|
||
---
|
||
### REVIEW — Fable — 2026-07-19 (Phase 1 review & integration)
|
||
**Reviewed:** lane/door @ e0b5831 (231 tests), lane/floor @ 3eee843 (169),
|
||
lane/juice @ f5e1151 (111). All three gates verified green per-branch before merge.
|
||
**Merged to main:** yes — door, then floor, then juice; 413 tests green post-merge.
|
||
Verified in-browser after integration: Door night boots and plays (called a patron
|
||
up, stamped her, meters moved per the judge table), FloorDemo cone/crowd runs (F),
|
||
JuiceDemo runs with AUDIO LIVE @ 128 BPM after unlock (J).
|
||
|
||
**Quality note to all three lanes:** outstanding sessions. The play-found bug
|
||
lists (door's 10 + review pass, floor's meter-collapse and frozen-beat catches)
|
||
and juice's honest "verified by ear: NOTHING" are exactly what this file is for.
|
||
|
||
**Contract decisions (all folded into `src/data/types.ts` + CONTRACTS.md §4 by me):**
|
||
1. CCR-1 `PatronFlags.handHolding` — ACCEPTED, folded in; doorTypes.ts augmentation removed.
|
||
2. CCR-2 `hypeDelta` — ACCEPTED onto `VerdictOutcome` (not just DoorOutcome); judge.ts augmentation removed.
|
||
3. CCR-3 (per-slot logos, distinct shoe/outer silhouettes) — ACCEPTED for the
|
||
v0.4 art pass; logged. `LOGO_VISIBLE_SLOTS` stays the guard until then.
|
||
4. CCR-4 — resolved by adding optional `short?: string` to `DressCodeRule`.
|
||
Door's runtime guard may stay or simplify; either is fine.
|
||
5. JUICE-1 `door:phoneTheatre` + `audio:unlocked` + `beat:bar` — ACCEPTED, folded in;
|
||
`ui/juiceEvents.ts` deleted (its three importers updated).
|
||
6. FLOOR-1 `incident:log` — ACCEPTED; `core/meters.ts` is now the single incident
|
||
writer and all three direct `incidents.push` sites (floor demo, DoorScene,
|
||
NightScene deferred) now emit the event instead.
|
||
7. FLOOR-2 aggro decay — deferred to the Phase-2 tuning pass, alongside door's
|
||
economy findings (vibe pinning, hype ratchet, queue slack). One tuning pass,
|
||
one owner, against a human player.
|
||
|
||
**Rulings on questions:**
|
||
- `declare module` augmentation: it was the right unblocking move, and both lanes
|
||
executed it cleanly — but it does NOT become standing practice. Contract
|
||
extensions land in `types.ts` at each review, augmentations are deleted on
|
||
folding. One source of truth wins over lane autonomy here.
|
||
- `src/main.ts`: now integration-owned (reviewer/Phase-2). Lanes request routing
|
||
changes via handover. Current dev routes: N night · P parade · F floor · J juice,
|
||
same hashes.
|
||
- `data/strings/<lane>.ts`: ACCEPTED as the standing convention — new string
|
||
files there are lane-owned, no sign-off needed (floor's moved:
|
||
`src/data/strings/floor.ts`). `types.ts` stays frozen.
|
||
- Heat 3-strikes: keep summary-declared for v0.1; Phase 2's shell makes it an
|
||
immediate "licence pulled" cut — logged for integration.
|
||
- Vibe-pins-at-100 / economy retune: HELD for a human playtest (John). Door's
|
||
measured numbers are the starting point for that session, not auto-applied.
|
||
- Juice beat-0 anchor: `audio:unlocked.atMs` should carry the anchored beat-0
|
||
origin — implement next juice session.
|
||
- MeterHud heat count: read `NightState.heatStrikes.length` at construction
|
||
(single source of truth) — implement next juice session.
|
||
- Floor escalation balance ("aggression never feels rewarded"): CONFIRMED as
|
||
design intent; door should mirror the shape at the rope.
|
||
- `.gitignore` slash: fixed.
|
||
|
||
**Also applied at integration:** dazza fromMin retunes per door's measurements
|
||
(noSongRequesters 240→105, noHandHolders 280→135; two pinned-order tests updated),
|
||
floor strings moved to `data/strings/floor.ts`, vite port now respects `$PORT`
|
||
(preview autoPort), 413 tests + lint + build green on main.
|
||
|
||
**Next:** Phase 2 integration is OPEN — one lane, `lanes/` file to follow. Big
|
||
rocks: night-flow state machine (door⇄floor interleave + radio pulls), TechnoEngine
|
||
replacing StubBeatClock in the night, ui/Stamp + MeterHud replacing door's local
|
||
versions, save/progression wiring, then the tuning pass with a human.
|
||
|
||
---
|
||
### SESSION — PHASE-2 INTEGRATION (executed by Fable, reviewer) — 2026-07-19 20:40
|
||
**Branch/commits:** main (integration is reviewer-owned per the Phase-1 ruling)
|
||
**Done:**
|
||
- **The night is whole.** Door and Floor run CONCURRENTLY for the entire night;
|
||
the player toggles by clicking the venue doorway (it glows on radio calls) and
|
||
pressing E at the floor's entry. `NightScene` is the flow machine: it owns
|
||
clock, meters, audio, radio, Kayden's stamp accuracy, and the run save.
|
||
- **Kayden.** While you're inside, `DoorScene.kaydenRule()` works the REAL queue
|
||
through the REAL `judge()` — admits 4 in 5, never tests, never clicks the
|
||
clicker, forgets one UV stamp in seven. His mistakes cost actual vibe/heat and
|
||
his unstamped admits are the floor's noStamp supply. Leaving him alone all
|
||
night reliably ends in LICENCE PULLED (verified: 97 admits, 3 audit strikes).
|
||
- **Floor in night mode:** `FloorDemoScene` gained a second registration
|
||
('Floor') that adopts the NightContext — fed by real door admissions with a
|
||
6–14s walk-in, MeterHud (initialHeat per ruling), keeps simulating while
|
||
you're at the rope (maggots ripen → radio pulls), demo mode untouched.
|
||
- **Audio in the night:** TechnoEngine + Sfx created by NightScene; unlock on
|
||
first click; StubBeatClock hands over on `audio:unlocked`; the lowpass sweeps
|
||
on every door⇄floor toggle; stamp/clicker/rope/phone/radio SFX wired.
|
||
`audio:unlocked.atMs` now carries the anchored beat-0 origin (juice Q1 ruling).
|
||
- **Run progression:** Thu 07-16 → Fri 07-17 → Sat 07-18 at The Royal, night
|
||
seed = runSeed + index*101, heat carried run-scoped via save.ts, third strike
|
||
= immediate LICENCE PULLED cut (new endReason + FAIL_TEXT.licence), run-over
|
||
and week-done both clear the save into a fresh week.
|
||
- **Widget swaps:** door deny path now uses ui/Stamp (decal destroyed ~900ms
|
||
post-slam); StampFx.ts deleted.
|
||
**How to see it:** `npm run dev` → play. Doorway = step inside; E at entry =
|
||
step out. To watch Kayden: step inside and just... stand there. Check INSIDE
|
||
count and the incident log fill up. Preview-pane note in CLAUDE.md still applies
|
||
(drive frames in ONE javascript_exec call).
|
||
**Tests:** 413 passing / 0 failing (no new pure logic — flow wiring; the night
|
||
was verified by driving full nights in-browser, including a complete 6-hour
|
||
Kayden-only night, the licence-pulled cut, and the Thu→Fri chain with carried
|
||
heat).
|
||
**Bugs found while verifying (both fixed):**
|
||
1. `kaydenRule` emitted `door:verdict` on top of `applyOutcome`'s own emission —
|
||
every Kayden admission spawned TWO pending floor walk-ins. Caught by
|
||
inside-vs-crowd arithmetic, not by tests.
|
||
2. Pre-existing: a deferred hit's vibe delta could end the night, and
|
||
`endNight()` reassigns `this.deferred` while `fireDeferred` iterates it →
|
||
crash at scale. Collect-then-process now; surfaced only by simulating a full
|
||
unattended night.
|
||
**Broke / known-wonky:**
|
||
- Balance: a Kayden-only night ends licence-pulled (intended), but the
|
||
alternation rhythm (how long CAN you stay inside) is untuned — that's the
|
||
John-playtest tuning pass, together with the Phase-1 economy findings.
|
||
- Floor overlays (stall/pat-down) still silent — LANE-JUICE2/FLOOR2 item.
|
||
- DoorScene keeps its bespoke top-bar meters; MeterHud lives on the floor side
|
||
only. Cosmetic unification optional (LANE_JUICE2 §4).
|
||
- `radio:call` maggot trigger only fires while at the door (by design — you can
|
||
hear the floor, you can't see it).
|
||
**Contract changes:** none needed — Phase-1 folds covered everything.
|
||
**Next:** Phase 3 lanes are OPEN — LANE_CONTENT / LANE_FLOOR2 / LANE_JUICE2
|
||
(files in lanes/). All three parallel, worktrees, own branches, usual protocol.
|
||
If only one session: CONTENT first (it's the game's heart), FLOOR2, then JUICE2
|
||
— except run JUICE2 first if John is available for the ear pass.
|