# LANE-DOOR — The Door (Phase 1, parallel)

**Executor:** Opus 4.8. **Branch:** `lane/door` (from main after Phase 0 merge).
**Owns:** `src/scenes/door/`, `src/rules/` (shared with reviewer approval — you are
its primary author in Phase 1), `tests/` mirrors. **Prereq reading:**
`docs/GAME_DESIGN.md` §3.1 → `docs/CONTRACTS.md` → `LANEHANDOVER.md` (check for
REVIEW blocks addressed to you).

**You are the v0.1 critical path.** When this lane is done, the game is shippable
as a Door-only night. Bias every decision toward "playable and funny" over
"architecturally complete".

## Phase-0 reality (read before coding)
Phase 0 landed 2026-07-19 — see CLAUDE.md "Dev notes" for the API map and the
browser-pane verification trick. Door-relevant specifics:
- `rules/drunk.ts` already exists (drunkStage thresholds + `hasVisibleTells`) —
  build `dressCode.ts`, `idCheck.ts`, `judge.ts` beside it.
- ID date math: `ageOn(dob, nightDate)` in `core/GameClock` and
  `idAge(patron, nightDate)` in `patrons/generator` exist and are tested —
  `idCheck.ts` should build on them, not reimplement.
- Fake-ID tells now include a fifth: an **expired real-looking card** (fake
  present, all tell booleans false, expiry in the past). Your `idVerdict` must
  catch it — see `tests/generator.test.ts` "every fake has a tell".
- Patron generation: `generatePatron({rng, nightDate}, clockMin, override?)`;
  ids are serial (`p0…`), reset per run via `_resetPatronSerial()`.
- `renderDoll(scene, patron, 'queue')` returns a texture key; drunk tells are
  baked per sway-bucket (see `dollTextureKey`). For the ×3 patron-up view just
  scale the image — pixelArt rendering keeps it crisp.
- `ParadeScene` shows the working pattern for bus/clock/meters lifecycle
  (`resetRun`) — copy that shape, don't invent one. `StubBeatClock` requires
  your scene to call `update(delta)` each frame.
- Dazza strings live in `data/strings/dazza.ts` keyed with `ruleId`s — your
  `dressCode.ts` rule ids must match them (8 rule-bearing texts are already
  written: noThongsSinglets, noLogos, noWhiteSneakers, noSongRequesters,
  noBucketHats, noSunnies, noBlazers, noHandHolders).

## Deliverable

`npm run dev` → full Door-only night: 9:00 PM to 3:00 AM, patrons queue and step
up, you inspect and rule on them, Dazza texts land, meters move, night ends in a
summary screen (win) or a fail state (Vibe 0 / Aggro 100). A stranger can play it
with zero explanation.

## Scene layout (640×360, static tableau)

Street view: queue snaking from the left, rope + patron-up position centre, venue
door right, rain + kebab-shop glow (reuse ParadeScene ambience). Desk UI along the
bottom: ID tray · dress-code card (latest rules) · clicker · phone · verdict
buttons. Patron steps up BIG (queue-pose doll scaled ×3) so outfit details read.

## Systems to build (in rough order)

### 1. Queue manager (`door/QueueManager.ts`)
Spawns patrons from the generator on a clock-driven arrival curve (quiet 9 PM,
slams 10:30–12:30). Maintains visible queue of ≤8 dolls + an offscreen count.
**The Rope:** next patron only steps up when the player clicks the rope hook.
Idle time while queue is non-empty and no patron is up: +hype (capped), +aggro
(uncapped, accelerating). Wire via `meters:delta`. Phone prop: clicking your phone
while someone waits gives bonus hype and a little animation — reward the theatre.

### 2. Inspection surface
Patron-up view with hover/click zones: face (compare to ID photo), each outfit
slot (tooltip: "white sneakers — look new"), eyes (bloodshot?), stance (sway tween
driven by intoxication tell from doll renderer). The ID card: click tray → card
zooms to half-screen; DOB, expiry, name, photo (rendered `idPhoto` pose from
`photoSeed` — mismatch tell = it's visibly a different doll). Fake tells per
CONTRACTS.md: peeling laminate (corner pixels), wrong hologram (wrong colour
shimmer), joke name.

### 3. Rules engine (`src/rules/` — PURE, tested, no Phaser imports)
- `dressCode.ts`: implement `DressCodeRule[]` for night 1 (the 4 escalations from
  the design doc) + 4 more of your own invention in the same escalating-absurdity
  register. `activeRules(clockMin)`, `violations(patron, clockMin)`.
- `idCheck.ts`: `idVerdict(idCard, nightDate)` → `{underage, expired, tells[]}`.
  All date math here, tested against boundary cases (18th birthday IS tonight;
  expiry tomorrow; leap-day DOB).
- `judge.ts`: `judge(patron, verdict, ctx) → VerdictOutcome` — the single scoring
  function. Encode the design's tensions:
  - deny a violator: +vibe; deny a clean punter: +vibe small, +aggro (arbitrary
    power is rewarded but heats the queue)
  - admit a violator: deferred −vibe ("Dazza does a lap" ~2–5 min later → dazza
    text + vibe hit)
  - admit underage: deferred `heat:strike` (fires on audit/inspection, Phase 2 —
    for v0.1 fire it at night end so it still hurts)
  - deny an actual owner's mate: big −vibe + furious Dazza text
  TEST every branch of this table.

### 4. Verdict flow & stamps
Verdict buttons: ADMIT / DENY / TEST / PAT-DOWN / (rope = wait). DENY = the big
red stamp: button press → stamp slams down over the patron with screen shake and a
dust ring, patron does the defeated-slouch walk-off. **Spend real time on this
feel** — it's the game's signature interaction. (LANE-JUICE will supply the stamp
widget + SFX later; build a local version now, swap on integration — mark it
`// TODO(juice): swap for ui/Stamp`.) ADMIT: rope unhooks, patron walks in the
door, **clicker increments only if the player clicks it** — the on-screen clicker
count vs true inside-count divergence is tracked in NightState per CONTRACTS.md.

### 5. Sobriety test (dialogue minigame)
Modal dialogue: pick one of 3 challenges (alphabet backwards / riddle / cursor
line-walk — implement the first two v0.1, line-walk stretch). Their answer quality
is derived from intoxication + rng, rendered as typed-out text with drunk typos.
Player then rules PASS/FAIL regardless of performance — failing someone who
passed: +vibe, +aggro. It's a power trip, remember.

### 6. Dazza & the phone
Phone widget (bottom corner): buzz animation + preview on `dazza:text`; click to
open thread history. Rule-bearing texts add the rule to the dress-code card with a
NEW badge. Strings from `data/strings/dazza.ts`; add more as needed (keep his
voice: aggressive lowercase, no punctuation, occasional fear of the owner).

### 7. Night shell (temporary, yours for v0.1)
Minimal `NightScene` wrapper: starts clock, runs Door, listens for fail states,
3 AM → `NightSummaryScene` (temporary version: vibe graph from sampled history,
cash = base + hypePeak bonus, verdict stats table). Phase 2 integration will
replace this shell — keep it thin and clearly marked `// TODO(integration)`.

## Constraints
- Consume `beat:tick` from StubBeatClock for any rhythm flourish; never your own timer.
- All scoring via `meters:delta` — never touch NightState directly.
- No imports from `scenes/floor/` or `audio/`.
- Dialogue/strings in `data/strings/`, Aussie register per design doc §5.

## Definition of done
Full night playable; both fail states reachable by playing badly on purpose;
`lint`/`build`/`test` clean; rules-engine coverage includes every judge branch;
SESSION block appended to LANEHANDOVER.md; branch pushed.
