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>
122
LANEHANDOVER.md
@ -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 5–12 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
|
After Width: | Height: | Size: 609 B |
BIN
public/props/ashtray.png
Normal file
|
After Width: | Height: | Size: 608 B |
BIN
public/props/bainMarie.png
Normal file
|
After Width: | Height: | Size: 1.3 KiB |
BIN
public/props/balustrade.png
Normal file
|
After Width: | Height: | Size: 1.1 KiB |
BIN
public/props/banquette.png
Normal file
|
After Width: | Height: | Size: 1.8 KiB |
BIN
public/props/beerUmbrella.png
Normal file
|
After Width: | Height: | Size: 1.6 KiB |
BIN
public/props/brokenBasin.png
Normal file
|
After Width: | Height: | Size: 1.0 KiB |
BIN
public/props/cableSnake.png
Normal file
|
After Width: | Height: | Size: 981 B |
BIN
public/props/canStack.png
Normal file
|
After Width: | Height: | Size: 611 B |
BIN
public/props/cdj.png
Normal file
|
After Width: | Height: | Size: 1.7 KiB |
BIN
public/props/champBucket.png
Normal file
|
After Width: | Height: | Size: 670 B |
BIN
public/props/cocktailBar.png
Normal file
|
After Width: | Height: | Size: 1.5 KiB |
BIN
public/props/djPlinth.png
Normal file
|
After Width: | Height: | Size: 1.6 KiB |
BIN
public/props/graffiti.png
Normal file
|
After Width: | Height: | Size: 853 B |
BIN
public/props/liftDoor.png
Normal file
|
After Width: | Height: | Size: 1.2 KiB |
BIN
public/props/loungeChair.png
Normal file
|
After Width: | Height: | Size: 611 B |
@ -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
|
After Width: | Height: | Size: 1.1 KiB |
BIN
public/props/mirror.png
Normal file
|
After Width: | Height: | Size: 893 B |
BIN
public/props/oldCouch.png
Normal file
|
After Width: | Height: | Size: 1.5 KiB |
BIN
public/props/picnicTable.png
Normal file
|
After Width: | Height: | Size: 948 B |
BIN
public/props/pillar.png
Normal file
|
After Width: | Height: | Size: 486 B |
BIN
public/props/planter.png
Normal file
|
After Width: | Height: | Size: 1.1 KiB |
BIN
public/props/plywoodBar.png
Normal file
|
After Width: | Height: | Size: 2.1 KiB |
BIN
public/props/pokie.png
Normal file
|
After Width: | Height: | Size: 903 B |
BIN
public/props/raffleDrum.png
Normal file
|
After Width: | Height: | Size: 704 B |
BIN
public/props/rollerDoor.png
Normal file
|
After Width: | Height: | Size: 2.3 KiB |
BIN
public/props/ropePost.png
Normal file
|
After Width: | Height: | Size: 576 B |
BIN
public/props/ruleBoard.png
Normal file
|
After Width: | Height: | Size: 1.6 KiB |
BIN
public/props/skylineCard.png
Normal file
|
After Width: | Height: | Size: 6.4 KiB |
BIN
public/props/smokeMachine.png
Normal file
|
After Width: | Height: | Size: 605 B |
BIN
public/props/stackF1.png
Normal file
|
After Width: | Height: | Size: 2.2 KiB |
BIN
public/props/staffBartender.png
Normal file
|
After Width: | Height: | Size: 1.1 KiB |
BIN
public/props/staffDj.png
Normal file
|
After Width: | Height: | Size: 1.1 KiB |
BIN
public/props/staffGlassie.png
Normal file
|
After Width: | Height: | Size: 1.1 KiB |
BIN
public/props/staffSecurity.png
Normal file
|
After Width: | Height: | Size: 1.0 KiB |
BIN
public/props/street_elevate.png
Normal file
|
After Width: | Height: | Size: 183 KiB |
BIN
public/props/street_room.png
Normal file
|
After Width: | Height: | Size: 208 KiB |
BIN
public/props/street_theRoyal.png
Normal file
|
After Width: | Height: | Size: 191 KiB |
BIN
public/props/strobe.png
Normal file
|
After Width: | Height: | Size: 538 B |
BIN
public/props/subBass.png
Normal file
|
After Width: | Height: | Size: 800 B |
BIN
public/props/tabBoard.png
Normal file
|
After Width: | Height: | Size: 561 B |
BIN
public/props/tvSport.png
Normal file
|
After Width: | Height: | Size: 606 B |
BIN
public/props/wheelieBin.png
Normal file
|
After Width: | Height: | Size: 632 B |