The rope is where the player spends most of the night and it was still coloured
primitives behind a single street plate.
- kebabShop: the moral centre of the game (design §5) was three rects and the
word KEBABS. The warm spill on the footpath stays procedural — it is the only
warm light in the scene and it is what says the shop is open.
- front:<venueId>: one facade per rung of the ladder, so a promotion is visible
from the footpath. ROOM also loses the procedural NOT TONIGHT sign, because
"no sign. no socials." is its actual entry in data/venues.ts.
- Footpath: a street lamp, a bin, a waiting cab, and brass stanchions on the
carpet. The cab is alpha'd back and depth-1 so it never looks clickable next
to the kebab shop, which is.
- poster4..poster9, so four venues stop sharing one wall of three. Mangled
lettering is correct on posters and always has been.
And a composition bug I put in this same wave: the per-venue street plates were
first prompted as "frontage", so The Royal's backdrop was a whole pub with its
own central door and the venue facade then sat on top of it — two pub fronts,
two doors. Voltage's original plate has always been a WALL, which is what a
backdrop has to be. Re-prompted all three as blank walls and regenerated. No
test would have caught that; I found it by looking at the screen.
127 sprites now, none missing.
Gate: lint clean, build clean, 821 tests passing.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Wave 2 lands complete — 26/26, no failures. 112 sprites in public/props, and
every one of the 91 prop kinds and 10 materials the four venues reference has
art. Nothing anywhere renders as a placeholder.
Ten material plates (carpet, concrete, deck, parquet, looTile, paving, plaster,
brick, timber, steel) and sixteen props: the filler for The Royal's central
carpet and ROOM's east hall, the fire kit and signage every venue now carries,
and two more staff figures.
Gate: lint clean, build clean, 821 tests passing.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Two things, one of them a real bug.
THE BUG: bakeTiles baked to a single global 'floor:tiles' and early-returned if
that key existed. Textures outlive a scene, so the first venue you played baked
the floor for every venue after it — you would survive a week at The Royal, get
promoted, walk into Voltage and still be standing on the pub's carpet. It only
shows on promotion, which is exactly the moment the venue ladder is supposed to
pay off, and never in a dev route because those reload the page. The baked
texture is now keyed per venue; verified by switching venues inside one session
and watching the concrete arrive.
MATERIALS: FloorLayout.materials names a generated plate per tile kind and the
baker stamps it TINTED to the palette colour. The plate carries grain, the
palette keeps hue — an untinted photo-real plate blows straight past the
brightness band the whole room was tuned in, and darkness is the mechanic.
Plates are 16x16, the game's own tile, because at that size "carpet" is
structured noise, which is also what pub carpet looks like from head height.
Ten of them: carpet, concrete, deck, parquet, looTile, paving, plaster, brick,
timber, steel. No plate named, or art not landed, falls back to flat colour
exactly as before.
Wave 2 filler for the two spaces the review found genuinely empty — The Royal's
central carpet (four props across ~500 tiles) and ROOM's east hall (~23% of its
interior with no lights and three props) — plus the kit every real room has and
no game room does: fire extinguishers, exit signs, CCTV, bins, a notice board,
ceiling fans. Voltage gets the same pass; being the reference room is how it
quietly missed the last one.
The invariant caught a duplicate prop id I introduced while placing these, which
is what it is for.
Also relaxed one ROOM assertion that keyed on array order: it checked that the
FIRST sodium light stands over the loading dock, which broke the moment the east
hall legitimately burned the same colour. It now checks that one of them does.
Gate: lint clean, tsc clean, 821 tests passing.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
37 generated props -> 86. Every one of the 76 prop kinds placed across the four
venues has a sprite; nothing renders as a placeholder.
tools/gen/ is the pipeline: assets.py is the manifest (one entry per sprite —
prompt, ship size, rake, route), run_batch.py walks it, resumable, --workers N.
Cloudflare Workers AI first for its 10k free daily neurons, falling back to
MODELBEAST flux_local automatically. Today's allocation was already spent when
the run started — confirmed from Cloudflare's own error body rather than assumed
— so this batch is entirely farm-rendered.
The mesh tier is the part worth keeping: TRELLIS's defaults (1024_cascade + a
2048px metal PBR bake) cost 5-12 minutes each on a queue that runs one gpu job
at a time, which is four hours for a tileset. mb.FAST_MESH (512 + vertex baker)
lands in ~90s, and A/B'd at ship size the difference does not survive the
downscale to 32px.
Also adds the door's per-venue street plates, keyed on the venue id exactly —
'theRoyal', not a case fold, because props_import writes ':' as '_' and a
lowercased key looks for a plate nobody made.
Gate: lint clean, build clean, 821 tests passing.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Three independent reviewers checked each new venue against its brief. No
blockers, all three rooms genuinely distinct — but two of the findings were
mine, in the file the layout authors were told not to touch.
FloorView was still drawing two things at Voltage's coordinates:
- The scan beams pivoted on a hardcoded (38,23) — Voltage's mirror ball. In The
Royal that swept two beams across the empty middle of the pub, twenty-five
tiles from that venue's dance floor. Now derived from the layout's own
discoball, and a venue without one gets no beams, because the beams ARE the
ball's light.
- The resident DJ stood at a hardcoded (55.5, 22.5) — Voltage's booth, which in
The Royal is the middle of the bistro. Now placed from the venue's own gear
and stepped clear of `posts.decks`, so a player on a DJ shift is never
standing inside him. The beat-bob had the same constant baked into it.
Elevate's staffDj prop is dropped as a consequence: with a resident DJ derived
per venue it would have made two DJs on the plinth, and three once the player
took the shift.
New invariant: a staff prop may not stand on a post tile. Elevate's bartender
was on the exact tile the bar shift teleports the player to, so the shift would
have been played from inside her. Caught by the rule, then fixed.
`mirror` and `graffiti` kinds added. Elevate was standing BOTH its mirrors up as
`poster3` (the author raised it as a contract request and stubbed it honestly),
which would have hung the same torn gig poster in a marble bathroom and called
it a mirror. ROOM's toilets get the graffiti its brief always asked for.
The Royal: TAB screens moved onto the wall they belong on rather than floating
mid-carpet, the trough given its own fluoro (the one fixture that room is known
for was rendering unlit), the beer garden's dark middle band lit — 288 tiles on
two lights left the gate everyone walks through in the dark — the out-of-order
sign moved off the tile the queue stands on, and two comments corrected to
describe what the code actually does.
Also hardened the farm client: `_req` raised SystemExit, which is a
BaseException, so `except Exception` in the batch's worker threads did not catch
it and one stray 401 during a poll killed a 43-asset run after a single asset.
It now raises a normal error and retries transient 401/429/5xx with backoff.
Gate: lint clean, tsc clean, floor suite 271 passing.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
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>
9/9 through flux product shot -> trellis2_mlx -> glb_to_sprite, unattended.
Rake corrected after the first pass: tall objects need a STEEPER angle (76), not
a shallower one — an arcade at 55 is a front-on slab. Re-angled from the meshes
already on disk, no re-generation, which is the point of keeping the GLB.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
tools/glb_to_sprite.py completes SPACES.md's 3D route — fixed ortho camera fitted
to the mesh bounds, flat light, transparent film, straight into props_import.
A/B'd on one pool table: TRELLIS wins for objects with volume (worn felt, real
pockets, rails), flux stays for flat things. Rake is 60 not 90 — straight down
turns a fridge into a grey box.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Audit found the dunnies were the biggest room in the venue with ONE prop in it
(and a minigame happening there). Nine new kinds placed across the five thinnest
spaces. House-style note recorded: props are raked three-quarter top-down, not
strict orthographic — a fridge from directly overhead is a grey box.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- scenes/floor/sweep.ts: the closing sweep extracted behind an accessor host.
Scene 2100 -> 1981 lines, behaviour verified identical.
- data/roleBeats.ts: 18 beats across 6 roles x 3 nights, landing in the venue
group chat. The glassie is spoken about; the HoS is spoken to.
- dollPlan(p, pose, frame): 4-frame idle on the queue pose. NOT pre-rendered
sheets — those would hide the outfit tells the dress code convicts on, which
design 4.1 forbids; the pre-render path must go UNDER the outfit rects.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
authority (eject/pat-down/cut-off/busts) and stations (taps/decks) are now per
role. Gated verbs become 'radio it in' — reusing the staff competence roll, so
~2 in 3 calls bring somebody over and the rest just hang in the air. Doorgirl
keeps floor authority on purpose: that loop is the shipped game.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- pourAdjust pays hype: clean 0.03, heavy 0.05, short nothing. The crowd-pleaser
is also what walks the room toward the RSA line — same lever, both ways.
Role pay gap closes from ~$180 to $45.
- breachSeenChance(inspectorWatching): 0.35 alone, 0.9 with a lanyard in the room
(design §6). Corrects SOLO-19: detection was never the weak part — dedupe caps
strikes at one a night and 0.35 already saturates across several pours.
- Sim harness was missing the 3 AM audit and under-reporting late-night strikes.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A bar shift logs ~26 serves and buildReport takes the top 3 with no dedupe, so
bartender nights filed three identical generic pour questions. Weights now price
frequency as well as interest, and the loaded kinds have real questions.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
vitest does not typecheck, so these passed under `npm test` while `npm run build`
failed. The gate is lint AND build AND test for exactly this reason — run it in
that order after touching tests, not before.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- DJ post stood the player inside the sealed djbooth (same class as the taps
softlock): every direction blocked, and since the sweep needs you to walk to
the entry, the night could never be paid. DECKS_SPOT moves to the booth's west
counter, which is where the map comment always said the gear faces from.
- Role stations are now DATA (venueMap POSTS/PROBES) with tests/floor/stations.test.ts
pinning the whole bug class: posts must be walkable AND escapable, probes must
have somewhere walkable within interact range. Written as a MOVEMENT test, not
a tile-name test — the question is whether the player can leave.
- DoorScene.busy is released by a delayedCall on the scene clock, and stopping a
scene discards pending events, so a ruling in the last 2.6s of a night left it
stuck true: a dead door — no rope, no verdicts, no way inside — for the rest
of the run. Now reset per night, along with seenCards/kebab/franko/retiredRules.
- Bar WATER and CUT OFF never settled the patron, so watering a maggot from
behind the taps left them ticking as unhandled and re-logging every grace
period: the call that fixed the problem kept being charged for it.
- Every bar verb logged as 'barServe', putting drinks in the record that were
never poured. One kind per verb; the report system takes unknown kinds by design.
- carpetMsFor() is consumed on read: a regular parked, recalled and knocked back
came round again later and was paid the entrance dividend a second time.
- Sweep state joins resetNightState() so an interrupted floor score cannot carry
its cash, or its previous-night pay callback, into the next night.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
toggleTaps() teleported the player to TAPS_FRONT on exit, and TAPS_FRONT was
tile (47,6) — inside venueMap's stool rect, which is not in WALKABLE. Verified
in the live build: after handing the taps back the player sat at (760,104) and
W/A/S/D each moved them exactly 0px, for the rest of the night. The bartender
roster role mounts the taps automatically, so one keypress ended the run.
Exit no longer teleports at all: you stay behind the bar and walk out around
its east end, which is both walkable (verified end to end) and how staff
actually leave a rail. TAPS_FRONT moves to row 7 so the approach probe sits on
the punter side rather than inside the furniture.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
applyLocation() only moved flags — it never announced the move. FloorDemoScene
latches `present` when it binds the night context, which (now the night preloads
every prop texture) happens BEFORE the initial applyLocation runs, so every
floor-start shift — bartender, glassie, DJ — came up visible but completely
inert, with the audio filter still in door mode. The night now emits
night:phaseChange and audio:location for the starting post.
Phaser reuses scene instances across restarts, so class-field initialisers run
once per GAME, not once per night: a DJ shift left djMode set and the next
night's floor booted with movement locked and the keyboard eaten, last night's
puddles kept slipping people through destroyed sprites, and the handled-patron
sets marked fresh arrivals as already dealt with. resetNightState() now wipes
all of it in create().
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
showScene() stopped 'Door' (it was in the stop list) and then declined to
restart the already-active 'Night', leaving an unplayable black screen with no
way back. Route only ROOT scenes, and restart the target when it is already
running so N means 'fresh night'. NightScene.teardown() now stops Door and
Floor, making real the ownership main.ts always claimed.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Ported from claude/epic-faraday-332679 (6c5889c) — merge was permission-blocked,
same diff applied directly.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>