127 lines
7.2 KiB
Markdown
127 lines
7.2 KiB
Markdown
# 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.
|