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>
@ -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.
|
||||
|
||||
BIN
public/props/front_elevate.png
Normal file
|
After Width: | Height: | Size: 32 KiB |
BIN
public/props/front_room.png
Normal file
|
After Width: | Height: | Size: 27 KiB |
BIN
public/props/front_theRoyal.png
Normal file
|
After Width: | Height: | Size: 32 KiB |
BIN
public/props/front_voltage.png
Normal file
|
After Width: | Height: | Size: 27 KiB |
BIN
public/props/kebabShop.png
Normal file
|
After Width: | Height: | Size: 24 KiB |
@ -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
|
After Width: | Height: | Size: 1.2 KiB |
BIN
public/props/poster5.png
Normal file
|
After Width: | Height: | Size: 1009 B |
BIN
public/props/poster6.png
Normal file
|
After Width: | Height: | Size: 1.1 KiB |
BIN
public/props/poster7.png
Normal file
|
After Width: | Height: | Size: 395 B |
BIN
public/props/poster8.png
Normal file
|
After Width: | Height: | Size: 924 B |
BIN
public/props/poster9.png
Normal file
|
After Width: | Height: | Size: 701 B |
BIN
public/props/stanchion.png
Normal file
|
After Width: | Height: | Size: 291 B |
BIN
public/props/streetBin.png
Normal file
|
After Width: | Height: | Size: 1.0 KiB |
BIN
public/props/streetLamp.png
Normal file
|
After Width: | Height: | Size: 1.0 KiB |
|
Before Width: | Height: | Size: 183 KiB After Width: | Height: | Size: 153 KiB |
|
Before Width: | Height: | Size: 208 KiB After Width: | Height: | Size: 208 KiB |
|
Before Width: | Height: | Size: 191 KiB After Width: | Height: | Size: 209 KiB |
BIN
public/props/taxi.png
Normal file
|
After Width: | Height: | Size: 1.8 KiB |
@ -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);
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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.
|
||||
|
||||
@ -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 = (
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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}
|
||||
|
||||