1005 lines
66 KiB
Markdown
1005 lines
66 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.
|
||
|
||
---
|
||
### SESSION — LANE-CONTENT — 2026-07-19 21:35
|
||
**Branch/commits:** lane/content @ 0f7b948..(this commit), worktree `../not-tonight-content`
|
||
|
||
**Done — all four deliverables land, in priority order:**
|
||
|
||
1. **Guest list clipboard.** `rules/guestList.ts` (pure) + `door/ClipboardPanel.ts`. The
|
||
desk's left panel is now tabbed **DAZZA'S RULES | GUEST LIST** — there is no spare
|
||
real estate at 640×360, and the tab is honest about the cost: you cannot read the
|
||
dress code and the list at once, so checking a name spends a beat of queue time.
|
||
The mechanic works as designed and is *measured*, not asserted: at the generator's
|
||
actual claim rates an **exact** match is a ~50/50, **near** leans legit (~66%), and
|
||
**none** leans liar (~97%). That posterior test is the module's real deliverable.
|
||
`knowsOwner` patrons now get their own line pool (`OWNER_HINTS`) that drops Terry's
|
||
name — the trap needs a tell that isn't just confidence.
|
||
2. **Scripted moral encounters.** `rules/encounters.ts` + `data/strings/encounters.ts`.
|
||
Four, three per night, injected by `QueueManager` ahead of the arrival curve (they are
|
||
people who turned up at a moment, not draws from a distribution). Multi-beat speech at
|
||
the rope. Verified in play: *"seven years ive been coming here. you knocked me back
|
||
once, back in nineteen. fair enough too."*
|
||
3. **The Inspector.** `rules/inspector.ts` + `NightScene` wiring. Reads as a boring
|
||
punter; admitted, they watch for 30 clock-min and convert any live breach's DEFERRED
|
||
strike to an IMMEDIATE one. Denied: nothing, ever — paranoia is the tax, and that
|
||
absent branch carries a comment so nobody "fixes" it. Verified in play: a night ended
|
||
LICENCE PULLED at 12:47 AM with two of the three strikes reading *"inspector saw the
|
||
room over licensed capacity"* / *"inspector saw a minor inside the venue"*.
|
||
4. **The Incident Report.** `rules/incidentReport.ts` + `door/IncidentReportScene.ts`,
|
||
sitting between the summary and the next shift. 3 questions, one truthful account and
|
||
one or two plausible-deniability ones, shuffled. Filed accounts go to
|
||
`GameState.pastReports` and re-save, so Phase 4 audits what the player *claimed*.
|
||
`NightScene` no longer dumps raw incidents there — a truth log has nothing to catch
|
||
anyone out with.
|
||
|
||
**Also landed (my Phase-1 finding, now fixable because I own `rules/`):**
|
||
- **`judge()` finally reads intoxication and contraband.** Denying a `messy`/`maggot`
|
||
patron, or one carrying severity-3 contraband, is now *justified* rather than scored as
|
||
an arbitrary denial (+7 aggro). Admitting anyone already showing tells schedules a
|
||
ripening hit 18–54 clock-min later with a Dazza line. `loose` is deliberately still a
|
||
judgement call. This closes the "the most legible signal in the game is worth zero
|
||
points" gap from the Phase-1 review, and it was the fix the encounters reviewer needed:
|
||
`quietBeer` had admit dominating deny on BOTH meters, which made the humane choice
|
||
secretly optimal — the one thing §4.3 forbids. Pinned by a test.
|
||
|
||
**How to see it:** `npm run dev` → play.
|
||
- Guest list: wait for someone who says *"i'm on the list"* — the GUEST LIST tab flashes,
|
||
click it, and the clipboard tells you whether the name matches. It never tells you
|
||
whether they are lying.
|
||
- Encounters: three a night, spaced 40+ min. They talk in beats and their name is not on
|
||
any list.
|
||
- Inspector: admit a `punter` who is secretly the inspector and then breach something
|
||
within 30 minutes. Or just play badly and find out.
|
||
- Report: play a night to 3 AM, click through the summary.
|
||
|
||
**Tests:** 518 passing / 0 failing (413 → 518, +105).
|
||
|
||
**Full 3-night run played** (careful player: refuse dodgy cards, minors, >0.62 intox, and
|
||
any dress breach). All three nights reached 3:00 AM; heat carried 1 → 2 → 2, so the week
|
||
was survived with one strike to spare. Encounters fired 3/3/3; guest-list claimants
|
||
13/19/13 a night; owner's mates 1–3.
|
||
**What it FELT like, honestly:** the door is much richer to *read* now — there is a reason
|
||
to look at a person rather than a checklist, and the clipboard genuinely creates doubt.
|
||
But **the Phase-1 economy findings are all still true and my content did not change them**:
|
||
vibe pinned at 97–98 on every night, aggro sat at 0 all three nights, hype maxed at ×3 every
|
||
night. The careful player is never under meter pressure; the only real tension left is heat.
|
||
Also, because late-night dress rules make most of the crowd deniable, the careful bot ends
|
||
up denying MORE than it admits (70 vs 55, 70 vs 41, 50 vs 39) — the room the player is
|
||
protecting stays half empty. That is the tuning pass John is holding, and it is now the
|
||
single biggest thing between this and a good game.
|
||
|
||
**Broke / known-wonky:**
|
||
- **The cross-night Kayden contradiction never fired in three nights of play**, because it
|
||
needs a `kayden` incident and Kayden only works the rope while you are on the FLOOR — my
|
||
playthrough never went inside. The path is unit-tested (5 tests) but has never been seen
|
||
in a real game. Someone should play a night from the floor and lie about Kayden.
|
||
- Encounter outcome objects are module singletons; I clone `heatStrike` before emitting so
|
||
a run cannot alias one strike, but the rest of the outcome is shared. Fine while nothing
|
||
annotates them.
|
||
- `settleId()` in encounters leaves a fake ID's fabricated DOB, so ~1 in 50 scripted
|
||
patrons carries a card whose age disagrees with them. Cosmetic today (nothing reads the
|
||
mismatch); the proper fix needs `nightDate`, which `dress()` isn't given.
|
||
- I removed a stale `import '../../rules/doorTypes'` from `QueueManager` — a leftover
|
||
side-effect import for the augmentation you folded in at review.
|
||
- One test run failed once and never reproduced across 4 subsequent runs; it coincided with
|
||
the dev server rewriting a file mid-run. Recording it rather than pretending it didn't
|
||
happen.
|
||
- Sobriety line-walk (deliverable 5, stretch) NOT built. 1–4 took the session.
|
||
|
||
**Contract change requests:** none. Everything went through existing events
|
||
(`meters:delta`, `heat:strike`, `dazza:text`, `incident:log`) and no `declare module`.
|
||
|
||
**Questions for reviewer:**
|
||
1. `RunInfo` gained a `game: GameState` field so the report form can file into the save and
|
||
persist it. That hands a scene a mutable reference to the run — happy with that, or
|
||
would you rather the report emit and let `NightScene` own the write?
|
||
2. The report re-saves after the night's own `saveGame`. Two writes per night. Cheap, but
|
||
if you want one, the night's save needs to move after the paperwork.
|
||
3. Encounters are 3-per-night on a 4-entry table, so a week shows the same people every
|
||
night with only the order changing. Worth gating repeats across nights via
|
||
`GameState`, or is a regular being a regular fine?
|
||
|
||
**Next session should:**
|
||
- The tuning pass with John. My run data above is the current baseline; the Phase-1
|
||
measurements still hold.
|
||
- Play a floor-side night and exercise the Kayden contradiction.
|
||
- Sobriety line-walk if anyone wants it.
|
||
- More encounters — the table wants 8–10 so a week does not repeat.
|
||
### SESSION — LANE-JUICE2 — 2026-07-19 21:50
|
||
**Branch/commits:** lane/juice2 @ d112bda..1a66c60 (worktree `../not-tonight-juice2`)
|
||
**Done:**
|
||
- **`audio/mix.ts` + `audio/arc.ts`** (new, pure, tested). The track's constants
|
||
are now a live `TrackMix`, and the night has a shape. Same surface serves the
|
||
ear pass, the arc, and the Phase-4 DJ control panel (BUILD_PLAN §4 rule).
|
||
- **`TechnoEngine`** drives every voice from the mix × arc scalars. Verified
|
||
behaviour-identical at `DEFAULT_MIX`, constant by constant. Silent voices are
|
||
skipped rather than clamped (9 PM bass builds zero nodes). `setMixValue`
|
||
quantises integer paths so the dump can't export a value nobody heard.
|
||
- **`ui/MixDeskScene`** — the ear-pass rig. Solo/mute per voice with a
|
||
per-voice listening prompt, every param on log-mapped sliders (5 pages), an
|
||
arc scrubber, and DUMP MIX → pasteable `TrackMix` literal + clipboard.
|
||
Restores the arc pin and solo state it found, so it can't un-tune its host.
|
||
- **`audio/Ambience`** — rain (outside), crowd murmur (inside, level follows the
|
||
room), kebab-shop fluoro hum. See routing + headcount notes below.
|
||
- **`Phone`** thread scrolls: visible-window culling, binary-searched range,
|
||
history cap, sticky-bottom, and a "NEW BELOW" cue when a text lands off-screen.
|
||
- **`JuiceDemoScene`** hosts all of it; arc pinned at PEAK on entry.
|
||
**How to see it:** the demo lives in this worktree, so
|
||
`cd ../not-tonight-juice2 && npx vite --port 5311` → http://localhost:5311/#juice
|
||
(or J). Click once to unlock audio, then **M** for the mix desk.
|
||
**Tests:** 460 passing / 0 failing (was 413; +45 across mix/arc, +2 from fixes).
|
||
**Verified by ear:** STILL NOTHING — no audio output in this environment. Logic
|
||
verified in a real browser: arc pins/restores across desk open+close, integer
|
||
quantisation, crowd headcount arithmetic (120 + 2 admits − 1 ejection = 121 with
|
||
the clicker event correctly ignored), zero console errors.
|
||
**THE ASK — the ear pass is now a ~10 minute job and I can't do it:**
|
||
Open the desk, SOLO each voice in turn, fix what's wrong, hit DUMP MIX, paste me
|
||
the literal. It goes straight over `DEFAULT_MIX` in `src/audio/mix.ts`.
|
||
`tests/mix.test.ts` pins the current values deliberately, so a retune will fail
|
||
that test — that's the tripwire working; update it in the same diff. Specifically
|
||
unjudged: does the kick read as a kick through the door filter, is the bassline
|
||
musical or just busy, is `typeTick` (fires per character) tolerable, does the
|
||
stamp sit right against the music, and does the door→floor sweep feel like a door.
|
||
**Broke / known-wonky:**
|
||
- `Ambience` rain uses `Sfx`'s own 1.2s/0.9s envelope, so it trails the 600/900ms
|
||
filter sweep by ~300ms. Fixing it properly means a `fadeS` param on
|
||
`Sfx.startRain/stopRain`. Reads fine; noting it rather than hiding it.
|
||
- `Ambience` assumes it is the sole owner of rain policy. If a door scene also
|
||
drives `sfx.startRain()` they will fight.
|
||
- `MixDesk` is reachable only from JuiceDemo (key M) — see request 1.
|
||
- No `TechnoEngine`/`Ambience` test files; both need a real `AudioContext` and
|
||
the suite is node. Their pure math is tested via mix/arc/scheduling/filterCurve.
|
||
**Contract change requests:** none. `door:phoneTheatre` was folded in at the
|
||
Phase-1 review; the crowd bed derives its headcount from existing events rather
|
||
than requesting `capacity.inside` in a payload (reasoning above).
|
||
**Requests for integration (main.ts is yours now, per the Phase-1 ruling):**
|
||
1. **A global route to the mix desk**, so the ear pass can also happen during a
|
||
REAL night rather than only in the demo. `MixDeskScene` already falls back to
|
||
`getActiveEngine()` when launched without data, so it attaches to whatever
|
||
engine is running — it just needs a key. Suggest **M**, alongside N/P/F/J.
|
||
2. Optional: `MeterHud` door-bar variant is still not done (LANE_JUICE2 §4.3,
|
||
explicitly "only if clean"). DoorScene's bespoke meters work; I left them.
|
||
**Questions for reviewer:**
|
||
1. The arc means the night no longer sounds the same at 9 PM and 1 AM — bass is
|
||
fully out at 9 PM by design. Worth confirming that's wanted before John tunes
|
||
against it, since it changes what "the mix" even means at a given hour.
|
||
2. `Ambience` currently has no owner in `NightScene` — I can't wire it there
|
||
(scenes aren't mine). Want it created alongside TechnoEngine at integration?
|
||
**Next session should:**
|
||
- Run the ear pass with John and land the retuned `DEFAULT_MIX`.
|
||
- Wire `Ambience` into `NightScene` (integration-owned).
|
||
- LANE-FLOOR2 owns the cut-off overlay call site; `DialogueBox` drunk-typo mode
|
||
is ready for it and needs no widget change — just pass `drunkenness` + `rng`.
|
||
|
||
---
|
||
### 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.
|
||
|
||
---
|
||
### REVIEW — Fable — 2026-07-19 (Phase 3 review & integration)
|
||
**Reviewed:** lane/content @ 37f9892 (518 tests), lane/floor @ 906a366 (225),
|
||
lane/juice2 @ 10462eb (460). All gates verified green per-branch. Merged
|
||
content → juice2 → floor; **621 tests green on main** post-merge + integration.
|
||
Verified in-browser: a Kayden-heavy night produced the full integrated strike
|
||
sheet (a fake-ID minor via Kayden, a hip flask, and an UNATTENDED FLOOR FIGHT —
|
||
cause: "someone said the kebab shop has gone downhill"), the incident-report
|
||
form runs between summary and next shift, and the mix desk overlays a live scene
|
||
via the new M route.
|
||
|
||
**Quality notes:**
|
||
- CONTENT: measured guest-list posteriors instead of asserting them, and the
|
||
judge() intoxication fix closed the worst scoring gap in the game — the exact
|
||
right person fixed it (rules owner). The §4.3 catch (humane choice must not be
|
||
secretly optimal) is the review of the phase.
|
||
- FLOOR2: six real defects found by re-reviewing YOUR OWN previous session is
|
||
the standard now set. The honest note that your verify harness had a 100%
|
||
false-negative rate (refute-biased + unanimity) is going in the reviewer
|
||
playbook: that pattern is an off switch, not a quality bar.
|
||
- JUICE2: the mix desk turns the ear-pass from a code task into a 10-minute
|
||
human task. Correct instinct throughout.
|
||
|
||
**Protocol breach (FLOOR2) — no harm, don't repeat:** the session branched from
|
||
its OLD lane tip (3eee843), not main — so it never saw Phase 2, its handover
|
||
re-requests CCRs that were folded a phase ago, and its round-2 code imported a
|
||
file main had moved. I ported everything at merge (fight files re-pointed at
|
||
data/strings/floor, fights wired into NIGHT mode incl. the unattended path,
|
||
live-body cap applied to the night spawn path). Next session: `git fetch &&
|
||
git switch -c lane/<name> origin/main`, and read the CURRENT handover first.
|
||
|
||
**Integration applied (beyond the merges):**
|
||
- Aggro physiology (approved round-1 request) implemented: `tickCalm` in the
|
||
floor — no brewing fight + no unhandled maggot → ~0.3 aggro/clock-min cools,
|
||
both modes, player present or not. Numbers are tuning-pass fodder.
|
||
- Fight swings radio the door (urgency 3) when the player is at the rope.
|
||
- `Ambience` created/destroyed by NightScene beside the engine; starts on
|
||
`audio:unlocked`.
|
||
- `MixDeskScene` registered globally; **M** toggles it as an overlay anywhere.
|
||
|
||
**Rulings:**
|
||
1. CONTENT Q1 (`RunInfo.game` mutable ref): ACCEPTED as-is. If a third writer
|
||
to the save ever appears, we revisit with an event; two is fine.
|
||
2. CONTENT Q2 (double save): accepted, cheap. Leave it.
|
||
3. CONTENT Q3 (encounter repeats): YES — gate repeats across nights via
|
||
GameState next content session, and grow the table to 8–10.
|
||
4. FLOOR2 (fight fuse burns through overlays): CONFIRMED as design. ESC exists;
|
||
choosing what to abandon is the floor.
|
||
5. FLOOR2 (doll texture cache growth): bounded per-night by the crowd cap;
|
||
acceptable until the v0.4 art pass, which owns texture lifecycle anyway.
|
||
6. JUICE2 Q1 (the arc changes what "the mix" means by hour): CONFIRMED wanted.
|
||
The desk pinning PEAK on entry is the right tuning default.
|
||
7. JUICE2 Q2 (Ambience owner): NightScene, done.
|
||
8. Fight heat strike as `deferred: true` + 150s cooldown: good rebalance,
|
||
approved retroactively.
|
||
|
||
**Open items carried:**
|
||
- The Kayden-contradiction path (lie about him in the report) has never fired in
|
||
real play — someone play a floor-heavy night and lie about it.
|
||
- Stall/pat-down/bin SFX still unwired (overlay constructors want the handle).
|
||
- Cut-off overlay ↔ DialogueBox drunk-typo integration (widget ready, call site
|
||
floor-owned).
|
||
|
||
**NO NEW LANES OPEN.** The bottleneck is no longer code: it is two sessions with
|
||
John — (1) the EAR PASS (press M in a real night, solo voices, DUMP MIX, paste
|
||
the literal back), and (2) the ECONOMY TUNING PASS (baselines: content's
|
||
3-night run data + door's Phase-1 measurements + floor's idle/attentive table).
|
||
After those land, the next lanes are content-expansion (encounters, repeat
|
||
gating), venue 2, and the v0.4 art pass per docs/ASSETS.md.
|
||
|
||
---
|
||
### SESSION — SOLO BUILD-OUT (Fable) — 2026-07-19 23:30
|
||
**Branch/commits:** main (three commits this session; deployed live)
|
||
**The headline: THE ECONOMY TUNING PASS IS DONE — against bots, held to bands.**
|
||
`tests/sim/economy.ts` is a full node-side bot night (QueueManager + judge +
|
||
Meters + deferred pipeline + churn model, no Phaser); `economy.test.ts` runs
|
||
careful/sloppy/dawdler bots over 5 seeds and asserts NINE feel-bands. Those
|
||
tests are now the economy's contract — a knob change that kills the feel fails
|
||
CI, not a playtest three weeks later. Where the numbers landed (careful bot):
|
||
admits 87 > denies 69 (was 48/68 — the room fills now), vibe pinned-share 0.34
|
||
(was 0.99 — the meter is alive), aggro peaks ~36 (was 6), slam queue avg 7
|
||
peak 16+ (was 1.0/8 — the 10:30 slam is REAL, verified live: 22 in the queue
|
||
at 12:33 AM), hype decays while you work (was a permanent x3 ratchet), sloppy
|
||
dies of vibe with 3 strikes, extreme dawdling riots before it profits.
|
||
**What the tuning changed (all commented at the definition):**
|
||
- judge: denyViolator 6→2, denyClean 2→1, admitClean 3→2, admitBadId 2→1.
|
||
- queue: calmPerSec 0.9→0.35, NEW hypeDecayPerSec 0.045 (theatre fades while
|
||
you serve — kills the ratchet).
|
||
- arrivals: slam peak 0.42→0.72/min, night total ~70→~120.
|
||
- generator: logo rate 0.35→0.18; sunnies/bucketHat/blazer weights halved.
|
||
- NEW `core/meters.vibeCoolingPerMin`: the room cools above 50 (-0.4) and
|
||
faster above 70 (-0.8), emitted per clock-minute — nobody stays impressed.
|
||
- **Dazza's attention span (`ACTIVE_RULE_CAP = 4`)**: only his last four rules
|
||
are live; older ones get greyed on the card + a retraction toast ("dazza:
|
||
forget the 'NO THONGS' thing. new priorities"). With all 8 cumulative, ~55%
|
||
of the late crowd was deniable and rule-following EMPTIED the room.
|
||
- **Natural churn**: patrons go home after 50-140 clock-min (`floor:leave`,
|
||
new contract event, CrowdSim emits, NightScene frees capacity). Without it
|
||
capacity was a one-way ratchet and the door locked shut from 1 AM.
|
||
- **Capacity strike bug FIXED**: the strike reason embedded the patron id, so
|
||
the reason-dedupe never collapsed and every over-80 admit filed its own
|
||
strike — 3 admits = licence gone. Now one stable reason per night. (This is
|
||
what was really killing the long Kayden nights in Phase 2.)
|
||
**Also this session:**
|
||
- Floor SFX wired: stall bangs (doorBang), pat-down bin drops (clickerClunk),
|
||
via optional sfx handles on the overlay constructors.
|
||
- Cut-off dialogue now wears its blood-alcohol: `drunkify` on the patron's
|
||
reply, deterministic per dollSeed.
|
||
- **Encounters 4 → 8** (eighteenToday, kaydensMate, bigNightOut, twoOfThem —
|
||
register per the §4.3 rules, humane never secretly optimal) + cross-night
|
||
repeat gating (`GameState.seenEncounters`, recorded off the encounter
|
||
incident, excluded by the scheduler until everyone's been met). Scheduler
|
||
reworked: end-order greedy + backward latest-safe pass — a random draw can
|
||
no longer strand a pick; overflow drops gracefully.
|
||
- nightShift window tightened to [150,195] (its noLogos dilemma dies when the
|
||
rule cap rescinds noLogos at 200).
|
||
- **Bug found & fixed in browser verification**: Dazza's texts lag the clock
|
||
(send cooldown), so a rule could be rescinded before its card row existed —
|
||
the one-shot guard then swallowed the retirement forever and the card LIED
|
||
about what was enforceable. Card sync is idempotent now; verified converged
|
||
under worst-case lag.
|
||
- **The Kayden contradiction has now fired in real play** (content's open
|
||
item): floor stint → his 9:15 PM influencer admit → lied on the report →
|
||
Friday's paperwork: "kayden wrote his version of p2 up too. it does not
|
||
match urs. he is very proud of the log". Screenshot in session notes.
|
||
**Tests:** 632 passing / 0 failing (was 621). Gate clean. **Deployed to
|
||
https://monsterrobot.games/not-tonight/ (bundle index-W0Kf_Kh7.js, verified 200).**
|
||
**Broke / known-wonky:**
|
||
- The sim bot cheats (perfect information, no inspection time beyond a fixed
|
||
cost) — bands are calibrated to it, not to humans. John's playtest may want
|
||
the arrival curve ±15%; the bands make retunes safe to try.
|
||
- Dawdler test covers EXTREME stalling (6s); mild-stall dominance is killed by
|
||
hype decay but has no dedicated band.
|
||
- Rule retractions land as toasts; a phone text would be richer (needs a dazza
|
||
schedule slot — next content session).
|
||
- Sloppy bot dies by VIBE before aggro ever moves — arguably right (admit-
|
||
everything tanks the room via laps/ripens) but worth a human sanity-check.
|
||
**Next:** John's ear pass (M in a live night) and a human feel-pass over the
|
||
new economy — the bands make it safe to turn knobs. Then venue 2.
|