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>