The venue ladder has always listed four venues in data/venues.ts, but they shared ONE floor map and differed only by difficulty knobs — so a promotion you survived a whole week for looked exactly like the room you had just left. - venueMap.ts grows a FloorLayout contract (map + props + lights + posts + probes + palette) and the newGrid() primitives every room is painted with. Voltage stays IN venueMap rather than moving to layouts/, so the new rooms can import primitives from it without closing an import cycle. - Three new rooms in src/scenes/floor/layouts/: The Royal (horseshoe public bar, pool room, pokies corner, trough + two cubicles, a beer garden that is plainly the nicest room in the pub, and a DJ "corner" that is a folding table because this pub never built a booth), Elevate (open-air rooftop — most of the grid is sky, the DJ is on an unwalled plinth, you arrive by lift), ROOM (concrete warehouse, pillars you path around, central booth, loading-dock smoking area, twelve lights in the whole venue). - Nothing about a room is a module singleton any more. FloorView, sweep and FloorDemoScene take the layout; the door picks its street plate by venue id. - FloorDemoScene's six hardcoded station spots (TAPS_SPOT, DECKS_SPOT, ...) were Voltage's tile coordinates. At four venues a hardcoded (47,3) puts the bar shift inside The Royal's pool room, so each layout now names its own stations and the scene reads them from there. - tests/floor/layoutInvariants.ts is an executable rulebook: perimeter holds, every walkable tile reachable from the entry, anchors on walkable ground, posts not sealed (a post on a sealed tile freezes the player for the night — that has shipped twice), probes reachable, props on-grid. Proved against Voltage BEFORE the new rooms were authored against it. Dev route #floor:<venueId> boots the floor straight into one room, because otherwise seeing ROOM means surviving three weeks of the ladder. Gate: lint clean, build clean, 821 tests passing (was 784). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
103 lines
6.4 KiB
Markdown
103 lines
6.4 KiB
Markdown
# NOT TONIGHT — instructions for Claude sessions in this repo
|
|
|
|
Papers, Please-style bouncer sim. Browser game: Phaser 3 + TypeScript (strict) +
|
|
Vite + Vitest. You are probably an **Opus 4.8 lane executor** — this repo is built
|
|
by parallel "lanes" coordinated through files, reviewed by Fable.
|
|
|
|
## Before writing any code
|
|
1. Read `LANEHANDOVER.md` — find the latest REVIEW block and any block for your
|
|
lane. Review instructions override lane files.
|
|
2. Read your lane file in `lanes/` (John/Fable will tell you which lane you are;
|
|
if not stated, ask — do not guess).
|
|
3. Skim `docs/GAME_DESIGN.md`, `docs/BUILD_PLAN.md`, and read `docs/CONTRACTS.md`
|
|
fully. Contracts are law.
|
|
|
|
## Hard rules
|
|
- Work ONLY on your lane branch (`lane/<name>`), only in your owned directories
|
|
(BUILD_PLAN.md §2) + your tests + LANEHANDOVER.md. **Never merge or push main.**
|
|
- Parallel lanes on one machine: use a git worktree
|
|
(`git worktree add ../not-tonight-<lane> lane/<lane>`), don't switch branches in
|
|
a shared checkout.
|
|
- Don't modify `docs/CONTRACTS.md`, `src/core/`, or `src/data/types.ts` after
|
|
Phase 0 — file a CONTRACT CHANGE REQUEST in the handover instead, stub locally
|
|
with `// TODO(contract):`, keep working.
|
|
- No `Math.random()` — SeededRNG streams only. No cross-lane scene imports.
|
|
Game logic in pure tested functions (`src/rules/`, per-lane logic modules);
|
|
scenes stay thin.
|
|
- End EVERY session: `npm run lint && npm run build && npm test` clean, append a
|
|
SESSION block to `LANEHANDOVER.md` (template in that file), commit, push your
|
|
branch. An unpushed session didn't happen.
|
|
- Remote is Gitea: `ssh://git@100.71.119.27:222/monster/not-tonight.git`.
|
|
|
|
## Dev notes (venue expansion landed 2026-07-28)
|
|
- **The ladder's four venues now have four different rooms** (`docs/VENUES.md`).
|
|
Layouts live in `src/scenes/floor/layouts/` (`theRoyal`, `elevate`, `room`);
|
|
**Voltage stays in `venueMap.ts`** so the others can import primitives from it
|
|
without an import cycle. A night picks its room from `NightContext.venue.id`.
|
|
- Nothing about a room may be a module singleton any more. Props, lights, posts,
|
|
probes and the tile palette all hang off `FloorLayout`. `venueMap.VENUE/PROPS/
|
|
LIGHTS` still exist and are still Voltage's — they are the reference room, not
|
|
"the venue".
|
|
- **Dev route `#floor:<venueId>`** boots the floor demo straight into one room
|
|
(`#floor:room`, `#floor:theRoyal`, `#floor:elevate`). Plain `#floor`/F = Voltage.
|
|
- `tests/floor/layoutInvariants.ts` is the executable rulebook every layout runs:
|
|
perimeter, reachability, anchors on walkable tiles, posts not sealed, probes
|
|
reachable, props on-grid. Add a room, add it to `LAYOUTS`, it gets checked.
|
|
- Asset generation is `tools/gen/` — `assets.py` is the manifest (one entry per
|
|
sprite), `run_batch.py` walks it, resumable, `--workers N`. **Cloudflare Workers
|
|
AI first** (10k free neurons/day, resets 00:00 UTC), falling back to MODELBEAST
|
|
`flux_local` automatically when the day's allocation is spent. Meshes use the
|
|
**512 tier + vertex baker** (`mb.FAST_MESH`): ~90s vs ~6min, and the difference
|
|
does not survive the downscale to 32px. Heartbeat: `~/.jobs/not-tonight-assets.json`.
|
|
- `props_import.py` takes its sizes FROM `tools/gen/assets.py`, so sizes cannot
|
|
drift between the farm and the game. It locks `manifest.json` — parallel imports
|
|
used to lose entries to a read-modify-write race, which shows up as a prop that
|
|
is on disk but renders as a placeholder. `tools/gen/rebuild_manifest.py` repairs.
|
|
|
|
## Dev notes (Phase 2 landed 2026-07-19)
|
|
- The game boots into the NIGHT (Thu→Fri→Sat run, saved). Controls: click the
|
|
rope/verdicts at the door; click the venue doorway to step inside; on the
|
|
floor WASD + E (E at the entry exits back to the door). While you're inside,
|
|
Kayden works the door — badly, on purpose.
|
|
- Dev routes: N night · P parade · F floor demo · J juice demo (+ same-named
|
|
URL hashes) · M toggles the MIX DESK overlay anywhere (ear-pass rig).
|
|
`localStorage.removeItem('not-tonight-save')` for a clean run.
|
|
- Audio unlocks on first click (TechnoEngine + location filter). Before that a
|
|
StubBeatClock keeps `beat:tick` honest.
|
|
- Venue spaces/props/lights are DATA (`venueMap.PROPS`/`LIGHTS`, plan in
|
|
docs/SPACES.md). Generated art drops into `public/props/` via
|
|
`tools/props_import.py` + manifest — placeholders render until then.
|
|
|
|
## Dev notes (Phase 0, still true)
|
|
- Gate: `npm run lint && npm run build && npm test` — all three must pass before
|
|
any push. Dev server: `npm run dev` (port 5199, config in `.claude/launch.json`).
|
|
- Key APIs (read the source, it's short): `core/EventBus` (`on` returns an
|
|
unsubscribe fn), `core/SeededRNG` (`rng.stream(name)` — NEVER `Math.random`,
|
|
eslint blocks it), `core/GameClock` (feed real ms via `update(delta)` from your
|
|
scene; `ageOn(dob, nightDate)` is the ID math), `core/meters` (emit
|
|
`meters:delta`, never mutate NightState), `patrons/generator`
|
|
(`generatePatron(ctx, clockMin, override?)`), `patrons/dollPlan` (pure,
|
|
testable) + `patrons/doll` (`renderDoll(scene, patron, pose)` → texture key),
|
|
`rules/drunk` (`drunkStage`). `StubBeatClock` needs an owning scene calling
|
|
`update(delta)` each frame.
|
|
- **Verifying in Claude's browser preview pane:** the pane tab reports
|
|
`document.hidden = true`, so Chrome suspends rAF and the Phaser loop freezes at
|
|
frame 0. Drive frames manually via the exposed debug handle:
|
|
`const b = performance.now(); for (let i = 1; i <= 900; i++) window.game.loop.step(b + i * 16.67)`
|
|
then screenshot/inspect. In a normal browser the game runs fine.
|
|
|
|
## Tone guard
|
|
The game is Aussie hospitality satire — affectionate, funny, with a moral undertow
|
|
(design doc §4.3). If content you're writing reads as mean-spirited rather than
|
|
absurd, soften toward absurd.
|
|
|
|
## Deploy (live since 2026-07-19)
|
|
- Live at **https://monsterrobot.games/not-tonight/** (botchat VPS, forum-nginx
|
|
bind mount — host path `/home/humanjing/monsterrobot.games/games/not-tonight/`,
|
|
`/not-tonight` is a symlink to it; landing tile in `../index.html`).
|
|
- Ship: `npm run build && rsync -az --delete dist/ humanjing@100.71.119.27:/home/humanjing/monsterrobot.games/games/not-tonight/`
|
|
then cache-busted curl to verify. Hashed asset names make CF cache a non-issue;
|
|
index.html is text/html (uncached by CF default).
|
|
- Cover art: `covers/nottonight.jpg` + `_riso.jpg` (MODELBEAST flux_local seed
|
|
4208 + PIL stamp text/riso pass — recipe in LANEHANDOVER Phase-3 deploy note).
|