The door stops being 28 rectangles

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>
This commit is contained in:
m3ultra 2026-07-29 00:10:02 +10:00
parent e0ead6698b
commit 4ee4cf343b
27 changed files with 194 additions and 18 deletions

View File

@ -2294,3 +2294,45 @@ missing art**. 26/26 this wave, no failures.
- Cloudflare's 10k neurons reset 00:00 UTC — a batch started after that gets
~150 free images before it ever touches the farm. Both waves today ran entirely
on MODELBEAST because the allocation was already spent.
## SESSION — FABLE-SOLO-26c · 2026-07-29 (wave 3, same session)
**Branch:** main · **Gate:** lint ✓ build ✓ test ✓ (821 tests) · **Deployed** + verified.
John: "we can use local flux as well cloudflare creds is just a bonus."
### The door scene stops being 28 rectangles
The rope is where the player spends most of the night and it was still coloured
primitives behind one street plate. Now:
- **`kebabShop`** — the moral centre of the game (design §5) was three rects and
the word KEBABS. The warm spill onto the footpath stays procedural either way;
it is the only warm light in the scene and it is what says OPEN.
- **`front:<venueId>`** — one venue façade per rung. The Royal is a pub front,
ROOM is a blank wall with one steel door. The promotion is now visible from the
footpath before you walk in. ROOM also suppresses the procedural NOT TONIGHT
sign, because "no sign. no socials." is literally its entry in data/venues.ts.
- Footpath dressing: `streetLamp`, `streetBin`, `taxi`, and `stanchion` posts on
the red carpet. The taxi is alpha'd back and depth-1 so it never reads as
clickable next to the kebab shop, which is.
- Six more posters (`poster4``poster9`) so four venues stop sharing one wall of
three. Mangled lettering on these is CORRECT (docs/SPACES.md) — the poster
prompts are the only ones that do not say "no text".
### The composition bug I shipped into the same wave
First pass prompted the per-venue street plates as **"frontage"**, so The Royal's
backdrop was a whole pub with its own central door — and then the venue façade
was drawn 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 ("NO doors, NO entrance") and regenerated: 50s each, free, local.
Caught by looking at the screen, not by any test — there is no assertion that
would have found it.
### Totals
**127 sprites.** 95 prop kinds + 10 materials + the door-scene art, none missing.
15/15 this wave plus 3 regenerated plates, no failures.
### Note on Cloudflare
Still spent both times today: the free 10k neurons reset at **00:00 UTC**, which
is 10:00 AEST — so an evening session in Australia will essentially always find
the allocation gone and run local. That is fine (the farm is free and unlimited),
but it means the CF-first tier only ever pays off for a morning batch. Every
sprite this session is MODELBEAST-rendered.

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

BIN
public/props/front_room.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

BIN
public/props/kebabShop.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

View File

@ -39,6 +39,10 @@
"fireHose",
"firepit",
"flightCase",
"front_elevate",
"front_room",
"front_theRoyal",
"front_voltage",
"glassRack",
"glassStack",
"graffiti",
@ -46,6 +50,7 @@
"highTable",
"iceWell",
"jukebox",
"kebabShop",
"keg",
"liftDoor",
"loungeChair",
@ -69,6 +74,12 @@
"poster1",
"poster2",
"poster3",
"poster4",
"poster5",
"poster6",
"poster7",
"poster8",
"poster9",
"raffleDrum",
"rollerDoor",
"ropePost",
@ -86,6 +97,9 @@
"staffGlassie",
"staffSecurity",
"stampPodium",
"stanchion",
"streetBin",
"streetLamp",
"street_backdrop",
"street_elevate",
"street_room",
@ -94,6 +108,7 @@
"subBass",
"tabBoard",
"taps",
"taxi",
"tex_brick",
"tex_carpet",
"tex_concrete",

BIN
public/props/poster4.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

BIN
public/props/poster5.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1009 B

BIN
public/props/poster6.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

BIN
public/props/poster7.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 395 B

BIN
public/props/poster8.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 924 B

BIN
public/props/poster9.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 701 B

BIN
public/props/stanchion.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 291 B

BIN
public/props/streetBin.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

BIN
public/props/streetLamp.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 183 KiB

After

Width:  |  Height:  |  Size: 153 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 208 KiB

After

Width:  |  Height:  |  Size: 208 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 191 KiB

After

Width:  |  Height:  |  Size: 209 KiB

BIN
public/props/taxi.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

View File

@ -285,20 +285,39 @@ export class DoorScene extends Phaser.Scene {
this.add.rectangle(W / 2, 176, W, 2, 0x2a2a38);
// kebab shop, stage left — visible all night, the moral centre (design §5)
this.add.rectangle(62, 120, 112, 106, 0x2a1c08);
this.add.rectangle(62, 88, 96, 18, DOOR_PALETTE.kebab, 0.9);
this.add.text(28, 82, 'KEBABS', { fontFamily: MONO, fontSize: '12px', color: '#3a2404' });
if (this.textures.exists('gen:prop:kebabShop')) {
this.add.image(62, 120, 'gen:prop:kebabShop').setDisplaySize(112, 106);
} else {
this.add.rectangle(62, 120, 112, 106, 0x2a1c08);
this.add.rectangle(62, 88, 96, 18, DOOR_PALETTE.kebab, 0.9);
this.add.text(28, 82, 'KEBABS', { fontFamily: MONO, fontSize: '12px', color: '#3a2404' });
}
// The warm spill onto the footpath stays either way — it is what tells you
// the shop is OPEN, and it is the only warm light in the scene.
this.add.rectangle(62, 148, 92, 46, 0xe8c060, 0.12);
// The whole shopfront becomes a button when Dazza wants feeding.
this.kebabGlow = this.add.rectangle(62, 120, 112, 106, 0xe8c060, 0).setInteractive();
this.kebabGlow.on('pointerdown', () => this.startKebabRun());
this.add.rectangle(62, 214, 96, 8, DOOR_PALETTE.kebab, 0.13);
// the venue
this.add.rectangle(DOOR_X, 108, 116, 140, 0x181828);
this.add.rectangle(DOOR_X, 58, 92, 16, DOOR_PALETTE.neon, 0.92);
this.add.text(DOOR_X - 40, 51, 'NOT TONIGHT', { fontFamily: MONO, fontSize: '10px', color: '#2a0818' });
this.add.rectangle(DOOR_X, 140, 34, 74, 0x05050a);
// The venue you are working tonight, standing in front of its own street.
// The Royal is a pub front, ROOM is a blank wall with one steel door — the
// promotion is meant to be visible from the footpath before you go in.
const front = this.textures.exists(`gen:prop:front_${this.night.venue.id}`)
? `gen:prop:front_${this.night.venue.id}`
: null;
if (front) {
this.add.image(DOOR_X, 108, front).setDisplaySize(116, 140);
} else {
this.add.rectangle(DOOR_X, 108, 116, 140, 0x181828);
}
// The venue's own sign stays procedural: it carries the game's title and
// ROOM is defined by NOT having one (data/venues.ts — "no sign. no socials").
if (this.night.venue.id !== 'room') {
this.add.rectangle(DOOR_X, 58, 92, 16, DOOR_PALETTE.neon, 0.92);
this.add.text(DOOR_X - 40, 51, 'NOT TONIGHT', { fontFamily: MONO, fontSize: '10px', color: '#2a0818' });
}
if (!front) this.add.rectangle(DOOR_X, 140, 34, 74, 0x05050a);
this.add.rectangle(DOOR_X, 214, 66, 8, DOOR_PALETTE.neon, 0.16);
// The doorway is how you get to the floor — click it to step inside.
@ -319,8 +338,28 @@ export class DoorScene extends Phaser.Scene {
// The red carpet: a strip of somewhere-better between rope and door,
// flanked by brass posts. Bodies parked here are rendered by
// syncCarpetSprites; the strip is set dressing.
// Footpath dressing. The street had one backdrop plate and 76px of empty
// grey between the kebab shop and the rope; a lamp, a bin and a waiting cab
// are what is actually outside a venue at 1am.
if (this.textures.exists('gen:prop:streetLamp')) {
this.add.image(196, 214, 'gen:prop:streetLamp').setOrigin(0.5, 1).setDisplaySize(24, 96).setDepth(1);
}
if (this.textures.exists('gen:prop:streetBin')) {
this.add.image(232, 216, 'gen:prop:streetBin').setOrigin(0.5, 1).setDisplaySize(24, 32).setDepth(1);
}
if (this.textures.exists('gen:prop:taxi')) {
// Parked far left, behind everything — it is scenery, not a destination,
// and it must never look clickable next to the kebab shop, which is.
this.add.image(96, 244, 'gen:prop:taxi').setOrigin(0.5, 1).setDisplaySize(96, 48).setDepth(1).setAlpha(0.85);
}
if (this.textures.exists('gen:prop:carpet')) {
this.add.image(420, 236, 'gen:prop:carpet').setOrigin(0.5, 1).setDisplaySize(132, 34).setDepth(2);
if (this.textures.exists('gen:prop:stanchion')) {
for (const px of [356, 484]) {
this.add.image(px, 232, 'gen:prop:stanchion').setOrigin(0.5, 1).setDisplaySize(14, 30).setDepth(3);
}
}
} else {
this.add.rectangle(420, 232, 124, 12, 0x8a1428).setDepth(2);
this.add.rectangle(420, 227, 124, 2, 0xa8283c).setDepth(2);

View File

@ -773,6 +773,8 @@ const PROP_SIZES: Record<PropKind, [number, number]> = {
barrier: [32, 16], distroBox: [16, 16], fireHose: [16, 16], firepit: [32, 16],
topiary: [16, 16], fireExt: [16, 16], exitSign: [16, 16], cctv: [16, 16],
staffCleaner: [16, 32], staffChef: [16, 32],
poster4: [32, 16], poster5: [32, 16], poster6: [32, 16],
poster7: [32, 16], poster8: [32, 16], poster9: [32, 16],
};
function bakePropPlaceholder(scene: Phaser.Scene, kind: PropKind): string {

View File

@ -263,7 +263,7 @@ const PROPS: readonly PropDef[] = Object.freeze([
P('highC', 'highTable', 43, 24, 1, 1),
P('highD', 'highTable', 20, 24, 1, 1),
P('loungeC', 'loungeChair', 41, 32, 2, 1),
P('terraceArt', 'poster2', 15, 14, 1, 2),
P('terraceArt', 'poster7', 15, 14, 1, 2),
P('glassie', 'staffGlassie', 44, 20, 1, 1),
// Wave 2: a rooftop bar's actual furniture, and the safety kit every venue has.

View File

@ -260,6 +260,11 @@ const ROOM_PROPS: readonly PropDef[] = Object.freeze([
P('extBar', 'fireExt', 9, 22, 1, 1),
P('exitHall', 'exitSign', 9, 18, 1, 1),
P('cleaner', 'staffCleaner', 64, 33, 1, 2),
// Flyposting by the entry. ROOM has no signage of its own (that is the whole
// conceit) but nothing stops other people's parties going up on its wall.
P('flyA', 'poster5', 14, 16, 2, 1),
P('flyB', 'poster7', 14, 21, 2, 1),
]);
const L = (

View File

@ -270,8 +270,8 @@ const PROPS: readonly PropDef[] = Object.freeze([
// entry corridor
P('poster1', 'poster1', 3, 13, 2, 1),
P('poster2', 'poster2', 6, 13, 2, 1),
P('poster3', 'poster3', 9, 13, 2, 1),
P('poster2', 'poster8', 6, 13, 2, 1),
P('poster3', 'poster9', 9, 13, 2, 1),
// Wave 2. The review counted four props across ~500 tiles of central carpet —
// "more genuinely empty carpet than any shipped room". A pub that size has

View File

@ -236,7 +236,12 @@ export type PropKind =
// review found genuinely empty (The Royal's middle, ROOM's east hall).
| 'pieWarmer' | 'binRound' | 'sandwichBoard' | 'noticeBoard' | 'ceilingFan'
| 'flightCase' | 'barrier' | 'distroBox' | 'fireHose' | 'firepit' | 'topiary'
| 'fireExt' | 'exitSign' | 'cctv' | 'staffCleaner' | 'staffChef';
| 'fireExt' | 'exitSign' | 'cctv' | 'staffCleaner' | 'staffChef'
// Six more gig posters, so four venues stop sharing one wall of three.
// Mangled lettering is CORRECT on these (docs/SPACES.md): unreadable print
// is what a real pub poster wall looks like, which is why the poster prompts
// are the only ones that do not say "no text".
| 'poster4' | 'poster5' | 'poster6' | 'poster7' | 'poster8' | 'poster9';
export interface PropEmissive {
colour: number;

View File

@ -216,16 +216,16 @@ STAFF = [
# ---- Door-scene street plates, one per venue ---------------------------------
STREETS = [
F("street:theRoyal", (640, 176), 4470,
"a wide suburban Australian pub frontage at night, painted brick, tiled dado, "
"a lit beer sign, bins, wet footpath, warm amber light",
"a wide blank painted brick pub wall at night, green tiled dado, torn posters, "
"a downpipe, wet footpath, warm amber spill, NO doors, NO entrance, no windows",
gen=(2048, 576), quantise=False, keep_bg=True),
F("street:elevate", (640, 176), 4471,
"a wide polished lobby entrance of a rooftop bar at night, brushed steel lift doors, "
"marble, planters, cool white downlight, glass",
"a wide polished marble and dark glass building wall at night, planters along the base, "
"cool white downlight, NO doors, NO entrance",
gen=(2048, 576), quantise=False, keep_bg=True),
F("street:room", (640, 176), 4472,
"a wide unmarked concrete warehouse wall at night, no signage, a single steel door, "
"graffiti, sodium orange streetlight, puddles, rain",
"a wide blank unmarked concrete warehouse wall at night, no signage, heavy graffiti, "
"sodium orange light, puddles, rain, NO doors, NO entrance",
gen=(2048, 576), quantise=False, keep_bg=True),
]
@ -273,8 +273,76 @@ FILLER = [
"a single kitchen cook standing in whites and apron, arms at sides, full body"),
]
# ---- Wave 3: the door scene -------------------------------------------------
# The rope is where the player spends most of the night and it was still 28
# coloured rectangles behind the one street plate. All FRONT-ON and all flat:
# this is a tableau, not a top-down room, so flux picking its own camera —
# the thing that forces furniture onto the mesh route — does not apply.
DOOR_TAIL = (
"front-on flat elevation, night, wet, video game background art, "
"no people, no watermark, no logo"
)
def D(kind: str, size: tuple[int, int], seed: int, subject: str,
gen: tuple[int, int] = (1024, 1024)) -> Asset:
return Asset(kind, "flat", size, f"{subject}, {DOOR_TAIL}", seed,
gen=gen, quantise=False, keep_bg=True)
def DCUT(kind: str, size: tuple[int, int], seed: int, subject: str) -> Asset:
"""Door-scene object that wants cutting out of its background."""
return Asset(kind, "flat", size, f"{subject}, {DOOR_TAIL}", seed,
gen=(1024, 1024), quantise=True, keep_bg=False)
DOORSCENE = [
# The kebab shop is the moral centre of the whole game (design §5) and it was
# three rectangles and the word KEBABS.
D("kebabShop", (112, 106), 4520,
"a late night kebab takeaway shopfront, glowing yellow sign board, "
"steamy window, vertical rotisserie inside, tiled counter"),
# One venue façade per rung of the ladder — the first thing you see of a
# promotion, standing in front of that venue's street plate.
D("front:theRoyal", (116, 140), 4521,
"a suburban Australian pub facade at night, painted brick, frosted windows, "
"a lit beer brand sign, single timber door"),
D("front:voltage", (116, 140), 4522,
"a nightclub facade at night, black painted brick, pink neon strip, "
"a dark recessed doorway"),
D("front:elevate", (116, 140), 4523,
"a polished hotel lobby entrance at night, glass and brushed steel, "
"warm downlight, a lift lobby visible inside"),
D("front:room", (116, 140), 4524,
"a blank concrete warehouse facade at night, no signage at all, "
"one steel service door, sodium light above it"),
DCUT("stanchion", (16, 32), 4525,
"a single brass rope barrier stanchion post, front view, isolated on black"),
DCUT("streetBin", (24, 32), 4526,
"a battered council street rubbish bin, front view, isolated on black"),
DCUT("streetLamp", (24, 96), 4527,
"a tall street lamp post lit with orange sodium light, front view, isolated on black"),
DCUT("taxi", (96, 48), 4528,
"a waiting taxi cab seen from the side at night, headlights on, isolated on black"),
# Six more gig posters. Mangled text is CORRECT here and always has been —
# docs/SPACES.md keeps "no text" out of the poster prompts on purpose,
# because unreadable print is what a real pub poster wall looks like.
*[
Asset(f"poster{i}", "flat", (32, 16), p, 4530 + i, gen=(512, 768), quantise=True)
for i, p in enumerate([
"a grimy 90s Australian pub rock gig poster, portrait, torn edges",
"a rave flyer poster, acid green and magenta, portrait, torn edges",
"a missing cat poster stapled to a pole, portrait, weathered",
"a techno club night poster, stark black and white, portrait",
"a meat raffle and poker night notice, portrait, photocopied",
"a covers band poster with a bad photo of the band, portrait",
], start=4)
],
]
ALL: list[Asset] = [
*ROYAL, *ELEVATE, *ROOM, *TEXTURES, *WALLS, *STAFF, *FILLER, *STREETS,
*DOORSCENE,
]
BY_KIND = {a.kind: a for a in ALL}