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>
6.4 KiB
6.4 KiB
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
- Read
LANEHANDOVER.md— find the latest REVIEW block and any block for your lane. Review instructions override lane files. - Read your lane file in
lanes/(John/Fable will tell you which lane you are; if not stated, ask — do not guess). - Skim
docs/GAME_DESIGN.md,docs/BUILD_PLAN.md, and readdocs/CONTRACTS.mdfully. 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/, orsrc/data/types.tsafter 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 testclean, append a SESSION block toLANEHANDOVER.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 insrc/scenes/floor/layouts/(theRoyal,elevate,room); Voltage stays invenueMap.tsso the others can import primitives from it without an import cycle. A night picks its room fromNightContext.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/ LIGHTSstill 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.tsis 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 toLAYOUTS, it gets checked.- Asset generation is
tools/gen/—assets.pyis the manifest (one entry per sprite),run_batch.pywalks it, resumable,--workers N. Cloudflare Workers AI first (10k free neurons/day, resets 00:00 UTC), falling back to MODELBEASTflux_localautomatically 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.pytakes its sizes FROMtools/gen/assets.py, so sizes cannot drift between the farm and the game. It locksmanifest.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.pyrepairs.
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:tickhonest. - Venue spaces/props/lights are DATA (
venueMap.PROPS/LIGHTS, plan in docs/SPACES.md). Generated art drops intopublic/props/viatools/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(onreturns an unsubscribe fn),core/SeededRNG(rng.stream(name)— NEVERMath.random, eslint blocks it),core/GameClock(feed real ms viaupdate(delta)from your scene;ageOn(dob, nightDate)is the ID math),core/meters(emitmeters: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).StubBeatClockneeds an owning scene callingupdate(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-tonightis 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).