Forty-eight sprites: every prop in every room is real art now

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>
This commit is contained in:
m3ultra 2026-07-28 21:14:33 +10:00
parent 22bd43857c
commit 7225eed349
45 changed files with 166 additions and 1 deletions

View File

@ -2121,3 +2121,125 @@ flicker). Recorded in SPACES.md so nobody burns farm time chasing a preview.
### Verified in pane
Dance floor at game scale: disco ball has real mirror-tile detail, decks show
their platters, speakers read as cabinets, truss reads overhead. Screenshot.
## SESSION — FABLE-SOLO-26 · 2026-07-28
**Branch:** main (solo, Fable) · **Gate:** lint ✓ build ✓ test ✓ (821 tests, 56 files)
Three jobs: four rooms instead of one, 48 new sprites, and a review pass that
found two bugs in my own refactor.
### 1. The venue ladder gets four actual rooms (docs/VENUES.md)
`data/venues.ts` has always listed four venues; they shared ONE floor map and
differed only by difficulty knobs, so a promotion you survived a 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/` — the new rooms
import primitives from it, and a `layouts/voltage.ts` would close an import
cycle for no benefit.
- Three new rooms in `src/scenes/floor/layouts/`, authored in parallel against a
written brief: **The Royal** (horseshoe public bar you work from inside, 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
`void` sky, the DJ is on an unwalled plinth, you arrive by lift so the entry is
a lobby), **ROOM** (concrete warehouse, pillars you path around, central booth,
loading-dock smoking area, twelve lights in the whole venue).
- Per-venue **`palette`** gives each room its own materials without inventing
TileKinds — The Royal's floor is pub carpet, ROOM's is bare concrete, but both
are still `'floor'` to the sim, so pathing and every existing test are untouched.
### 2. The station coordinates were Voltage's, and nothing said so
`FloorDemoScene` had six module constants — `TAPS_SPOT`, `DECKS_SPOT`,
`RACK_SPOT`, `DISH_SPOT`, `WATER_SPOT`, `TAPS_FRONT` — holding Voltage's tile
coordinates. Fine with one room; at four, a hardcoded `(47,3)` puts the bar shift
inside The Royal's pool room. `POSTS`/`PROBES` existed for exactly this and were
never wired to anything. Now each layout names its own stations and the scene
reads them from the layout.
### 3. `tests/floor/layoutInvariants.ts` — the rulebook is executable
Perimeter holds · every walkable tile reachable from the entry (cubicle
interiors excepted, they are sealed on purpose) · anchors on walkable ground ·
**posts not sealed** · probes have somewhere to stand · props on-grid · no staff
prop on a post tile. **Proved against Voltage BEFORE the three new rooms were
authored against it** — if the rulebook is wrong it is wrong on the known-good
room first. Add a room, add it to `LAYOUTS`, it gets checked.
### 4. What the review pass found — two of them mine
Three independent reviewers checked each room against its brief. All three rooms
genuinely distinct, no brief items missing, no blockers. But `FloorView` was
still drawing two things at Voltage's coordinates, in the file the layout authors
were told not to touch:
- **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, ~25 tiles from
that venue's dance floor. Now derived from the layout's own `discoball`; 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`. The beat-bob had the same constant in it.
- Elevate's **bartender stood on the exact tile the bar shift teleports you to**
you would have played the shift from inside her. That is now an invariant.
- Elevate's two **mirrors were both `poster3`** (the author raised it as a contract
request and stubbed it honestly). `mirror` and `graffiti` kinds added.
### 5. Assets: 37 generated props → 86
`tools/gen/`: `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** (10k free neurons/day, resets 00:00 UTC) falling
back to MODELBEAST `flux_local` automatically — the free tier was already spent
when this session started, confirmed from Cloudflare's own error body rather than
guessed, so the whole batch ran on the farm.
**The mesh tier is the thing worth knowing.** TRELLIS's defaults
(`1024_cascade` + 2048px metal PBR bake) cost 512 min each on a queue that runs
ONE gpu job at a time — four hours for a 37-prop tileset. `mb.FAST_MESH`
(`512` + `vertex` baker + 200k faces) lands in ~90s, and A/B'd at ship size the
difference does not survive the downscale to 32px. Judge at ship size, as
SPACES.md already said.
### 6. Three concurrency bugs, all introduced by parallelising the batch
Recorded because the serial version could not have had any of them:
1. **`props_import.py` manifest was a read-modify-write.** Two imports finishing
together each read the same list and the second write dropped the first's
entry — which ships a prop that is on disk but absent from the manifest, so
the game renders a placeholder over perfectly good art. Now `flock`ed.
`tools/gen/rebuild_manifest.py` repairs a manifest from the directory.
2. **`glb_to_sprite.py` wrote its Blender script to one shared path** and unlinked
it after — a finishing render deletes the script a starting one is about to
read. Now per-kind.
3. **`mb._req` raised `SystemExit`**, which is a `BaseException`, so `except
Exception` in the worker threads did not catch it: one stray 401 during a poll
killed a 43-asset run after a single asset. Now a normal `MBError`, with
backoff on transient 401/429/5xx.
### How to see it
`npm run dev`**`#floor:theRoyal`**, **`#floor:elevate`**, **`#floor:room`**
(plain `#floor`/F is still Voltage). Or play: week 1 IS The Royal now.
In Claude's preview pane the tab reports `document.hidden`, so drive frames by
hand — `const b=performance.now(); for(let i=1;i<=900;i++) window.game.loop.step(b+i*16.67)`
— and note the world only builds after the loader finishes 86 textures, which
needs real wall-clock time, not more `loop.step` calls.
### Broke / known-wonky
- **ROOM's barkeep stands in a one-tile-wide service lane**, so the player walks
through him during a bar shift. Inherent to props-as-set-dressing in a lane
that narrow; not on a post tile, so the invariant passes. A bar with no
bartender looked worse.
- Reviewers noted **The Royal's central floor is sparse** (~500 walkable tiles,
four props) and **ROOM's east hall has no lights or anchors** (~23% of its
interior). Both are consistent with their briefs — the pub's dance floor is
meant to be an afterthought, ROOM is meant to be hard to navigate — but a
future pass could furnish them.
- ROOM's sodium signature (`0xe08420`) differs from bar amber (`0xd8a020`) only
in the green channel; under additive glow they are close. Deliberate but worth
an eye.
- `picnicTable` failed once mid-batch on a farm-side `404 no such job`; retried
clean. 86/86 kinds placed across the four venues have art — no placeholders.
### Next session should
- Furnish The Royal's middle and light ROOM's east hall.
- Consider body-frames-under-outfits for patrons (ASSETS.md §2 / FABLE-SOLO-23) —
still the only patron-art path that does not blind the dress-code game.
- Cloudflare's neurons reset daily; a batch started after 00:00 UTC gets ~150
free images before it ever touches the farm.

BIN
public/props/ashUrn.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 609 B

BIN
public/props/ashtray.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 608 B

BIN
public/props/bainMarie.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

BIN
public/props/balustrade.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

BIN
public/props/banquette.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

BIN
public/props/cableSnake.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 981 B

BIN
public/props/canStack.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 611 B

BIN
public/props/cdj.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 670 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

BIN
public/props/djPlinth.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

BIN
public/props/graffiti.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 853 B

BIN
public/props/liftDoor.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 611 B

View File

@ -1,14 +1,26 @@
{
"props": [
"arcade",
"ashUrn",
"ashtray",
"bainMarie",
"balustrade",
"banquette",
"barFridge",
"barShelf",
"beerUmbrella",
"bistroTable",
"boothLamp",
"boothTable",
"brokenBasin",
"buttBin",
"cableSnake",
"canStack",
"carpet",
"cdj",
"champBucket",
"cloak",
"cocktailBar",
"crate",
"cubicleDoor",
"cueRack",
@ -16,32 +28,63 @@
"deck",
"discoball",
"dishwasher",
"djPlinth",
"glassRack",
"glassStack",
"graffiti",
"handDryer",
"highTable",
"iceWell",
"jukebox",
"keg",
"liftDoor",
"loungeChair",
"marbleSink",
"mirror",
"mixer",
"monitor",
"mopBucket",
"oldCouch",
"pallet",
"patioHeater",
"picnicTable",
"pillar",
"plant",
"planter",
"plywoodBar",
"pokie",
"poolTable",
"poster1",
"poster2",
"poster3",
"raffleDrum",
"rollerDoor",
"ropePost",
"ruleBoard",
"sinkRow",
"skylineCard",
"smokeMachine",
"speaker",
"stackF1",
"staffBartender",
"staffDj",
"staffGlassie",
"staffSecurity",
"stampPodium",
"street_backdrop",
"street_elevate",
"street_room",
"street_theRoyal",
"strobe",
"subBass",
"tabBoard",
"taps",
"till",
"trough",
"truss",
"tvSport",
"urinal",
"wetFloor"
"wetFloor",
"wheelieBin"
]
}

BIN
public/props/marbleSink.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

BIN
public/props/mirror.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 893 B

BIN
public/props/oldCouch.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 948 B

BIN
public/props/pillar.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 486 B

BIN
public/props/planter.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

BIN
public/props/plywoodBar.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

BIN
public/props/pokie.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 903 B

BIN
public/props/raffleDrum.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 704 B

BIN
public/props/rollerDoor.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

BIN
public/props/ropePost.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 576 B

BIN
public/props/ruleBoard.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 605 B

BIN
public/props/stackF1.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

BIN
public/props/staffDj.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 183 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 208 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 191 KiB

BIN
public/props/strobe.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 538 B

BIN
public/props/subBass.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 800 B

BIN
public/props/tabBoard.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 561 B

BIN
public/props/tvSport.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 606 B

BIN
public/props/wheelieBin.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 632 B