diff --git a/docs/VENUES.md b/docs/VENUES.md index 733deb8..e066510 100644 --- a/docs/VENUES.md +++ b/docs/VENUES.md @@ -30,6 +30,19 @@ bouncer actually learns about a room. sealed tile freezes the player for the night — this has shipped twice); 4. every `PROBES` entry has a walkable tile within reach; 5. no prop hangs off the grid. +- **Materials are grain, the palette is hue.** `FloorLayout.materials` names a + generated plate per tile kind (`tex_carpet`, `tex_concrete`, …) and the baker + stamps it **tinted to the palette colour**. That is deliberate: an untinted + photo-real plate blows straight past the brightness band everything else was + tuned in, and the room stops being dark. Plates are 16×16 — the game's tile — + because at that size "carpet" is structured noise, which is also what pub + carpet looks like from head height. No plate named, or art not landed: flat + palette colour, exactly as before. +- **The baked floor is keyed per venue** (`floor:tiles:`). Textures outlive a + scene, so a single global key meant the first venue you played baked the floor + for every venue after it — you would survive a week at The Royal, get promoted, + and walk into Voltage still standing on the pub's carpet. It only shows on + promotion, which is precisely when the ladder is supposed to pay off. - **Light signatures survive** (SPACES.md §1): bar = amber, dance = pulsing pink on the real beat, DJ = deep blue, toilets = flickering green, smoking = cool blue, entry = venue pink. A venue may add ONE signature colour of its diff --git a/public/props/manifest.json b/public/props/manifest.json index 9a34a12..52b0755 100644 --- a/public/props/manifest.json +++ b/public/props/manifest.json @@ -79,6 +79,16 @@ "subBass", "tabBoard", "taps", + "tex_brick", + "tex_carpet", + "tex_concrete", + "tex_deck", + "tex_looTile", + "tex_parquet", + "tex_paving", + "tex_plaster", + "tex_steel", + "tex_timber", "till", "trough", "truss", diff --git a/public/props/tex_brick.png b/public/props/tex_brick.png new file mode 100644 index 0000000..32726de Binary files /dev/null and b/public/props/tex_brick.png differ diff --git a/public/props/tex_carpet.png b/public/props/tex_carpet.png new file mode 100644 index 0000000..d03b634 Binary files /dev/null and b/public/props/tex_carpet.png differ diff --git a/public/props/tex_concrete.png b/public/props/tex_concrete.png new file mode 100644 index 0000000..2d37f6e Binary files /dev/null and b/public/props/tex_concrete.png differ diff --git a/public/props/tex_deck.png b/public/props/tex_deck.png new file mode 100644 index 0000000..dfb986e Binary files /dev/null and b/public/props/tex_deck.png differ diff --git a/public/props/tex_looTile.png b/public/props/tex_looTile.png new file mode 100644 index 0000000..8997122 Binary files /dev/null and b/public/props/tex_looTile.png differ diff --git a/public/props/tex_parquet.png b/public/props/tex_parquet.png new file mode 100644 index 0000000..70e83d4 Binary files /dev/null and b/public/props/tex_parquet.png differ diff --git a/public/props/tex_paving.png b/public/props/tex_paving.png new file mode 100644 index 0000000..8b7b9b2 Binary files /dev/null and b/public/props/tex_paving.png differ diff --git a/public/props/tex_plaster.png b/public/props/tex_plaster.png new file mode 100644 index 0000000..f846957 Binary files /dev/null and b/public/props/tex_plaster.png differ diff --git a/public/props/tex_steel.png b/public/props/tex_steel.png new file mode 100644 index 0000000..d71cf3e Binary files /dev/null and b/public/props/tex_steel.png differ diff --git a/public/props/tex_timber.png b/public/props/tex_timber.png new file mode 100644 index 0000000..724ce8b Binary files /dev/null and b/public/props/tex_timber.png differ diff --git a/src/scenes/floor/FloorView.ts b/src/scenes/floor/FloorView.ts index ac489b4..c82b096 100644 --- a/src/scenes/floor/FloorView.ts +++ b/src/scenes/floor/FloorView.ts @@ -3,7 +3,7 @@ import { renderDoll } from '../../patrons/doll'; import { dollPlan } from '../../patrons/dollPlan'; import { conePolygon, type ConeSpec } from './cone'; import { FIGHT_FUSE_MS } from './fight'; -import { TILE, tileAt, tileColour, type FloorLayout, type PropDef, type PropKind, type VenueMap, type ZoneLight } from './venueMap'; +import { TILE, tileAt, tileColour, type FloorLayout, type PropDef, type PropKind, type TileKind, type VenueMap, type ZoneLight } from './venueMap'; import type { Agent } from './crowdSim'; // Pure drawing for the floor. Darkness is the mechanic: the venue is painted @@ -69,6 +69,9 @@ export class FloorView { private readonly posts: FloorLayout['posts']; private readonly lights: readonly ZoneLight[]; private readonly palette: FloorLayout['palette']; + private readonly materials: FloorLayout['materials']; + /** Per-venue, so one venue's floor cannot be reused for the next. */ + private readonly tilesKey: string; private readonly tileImg: Phaser.GameObjects.Image; private readonly neon: Phaser.GameObjects.Image[] = []; @@ -118,6 +121,8 @@ export class FloorView { this.posts = layout.posts; this.lights = layout.lights; this.palette = layout.palette; + this.materials = layout.materials; + this.tilesKey = `${TILES_TEX}:${layout.id}`; const worldW = map.width * TILE; const worldH = map.height * TILE; @@ -126,7 +131,7 @@ export class FloorView { this.bakeMarker(); this.bakeTiles(worldW, worldH); - this.tileImg = scene.add.image(0, 0, TILES_TEX).setOrigin(0, 0).setDepth(D_TILES); + this.tileImg = scene.add.image(0, 0, this.tilesKey).setOrigin(0, 0).setDepth(D_TILES); this.dark = scene.add .rectangle(0, 0, worldW, worldH, 0x000000, 0.5) @@ -630,8 +635,36 @@ export class FloorView { g.uvDot.destroy(); } + /** + * Paint the whole room into one texture. + * + * Keyed PER VENUE. It used to bake to a single global 'floor:tiles' and + * early-return if that existed — correct while there was one room, and a + * silent disaster with four: textures outlive a scene, so the first venue you + * played baked the floor for every venue after it. You would survive a week at + * The Royal, get promoted, and walk into Voltage still standing on the pub's + * carpet. It only shows on promotion, which is exactly the moment the whole + * venue ladder is supposed to pay off. + * + * Materials: where the layout names one for a tile kind and its art has + * loaded, the tile is STAMPED with that plate tinted to the palette colour — + * the plate carries the grain, the palette keeps the venue's hue and the + * brightness band the darkness sheet was tuned against. No plate, or art not + * landed: flat colour exactly as before. + */ private bakeTiles(worldW: number, worldH: number): void { - if (this.scene.textures.exists(TILES_TEX)) return; + if (this.scene.textures.exists(this.tilesKey)) return; + + const materials = this.materials ?? {}; + const plateFor = (kind: TileKind): string | null => { + const name = materials[kind]; + if (!name) return null; + const key = `gen:prop:${name}`; + return this.scene.textures.exists(key) ? key : null; + }; + + // Flat colour underneath everything — it is the fallback AND the base the + // stamped plates sit on, so a plate with any alpha still reads in-venue. const g = this.scene.make.graphics({}, false); for (let ty = 0; ty < this.map.height; ty++) { for (let tx = 0; tx < this.map.width; tx++) { @@ -639,8 +672,26 @@ export class FloorView { g.fillRect(tx * TILE, ty * TILE, TILE, TILE); } } - g.generateTexture(TILES_TEX, worldW, worldH); + + if (Object.keys(materials).length === 0) { + g.generateTexture(this.tilesKey, worldW, worldH); + g.destroy(); + return; + } + + const rt = this.scene.make.renderTexture({ width: worldW, height: worldH }, false); + rt.draw(g); g.destroy(); + for (let ty = 0; ty < this.map.height; ty++) { + for (let tx = 0; tx < this.map.width; tx++) { + const kind = tileAt(this.map, tx, ty); + const plate = plateFor(kind); + if (!plate) continue; + rt.draw(plate, tx * TILE, ty * TILE, 1, tileColour(kind, this.palette)); + } + } + rt.saveTexture(this.tilesKey); + rt.destroy(); } private bakeGlow(): void { @@ -716,6 +767,12 @@ const PROP_SIZES: Record = { staffBartender: [16, 32], staffDj: [16, 32], staffGlassie: [16, 32], staffSecurity: [16, 32], // Wall surfaces mirror: [32, 16], graffiti: [32, 16], + // Wave 2 filler + venue dressing + pieWarmer: [32, 16], binRound: [16, 16], sandwichBoard: [16, 16], + noticeBoard: [32, 16], ceilingFan: [32, 32], flightCase: [32, 16], + 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], }; function bakePropPlaceholder(scene: Phaser.Scene, kind: PropKind): string { diff --git a/src/scenes/floor/layouts/elevate.ts b/src/scenes/floor/layouts/elevate.ts index 4ca7b23..3039fbf 100644 --- a/src/scenes/floor/layouts/elevate.ts +++ b/src/scenes/floor/layouts/elevate.ts @@ -265,6 +265,16 @@ const PROPS: readonly PropDef[] = Object.freeze([ P('loungeC', 'loungeChair', 41, 32, 2, 1), P('terraceArt', 'poster2', 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. + P('firepit', 'firepit', 40, 31, 2, 1), + P('topiaryA', 'topiary', 17, 12, 1, 1), + P('topiaryB', 'topiary', 44, 12, 1, 1), + P('topiaryC', 'topiary', 44, 34, 1, 1), + P('extRoof', 'fireExt', 15, 30, 1, 1), + P('exitLobby', 'exitSign', 2, 20, 1, 1), + P('camLobby', 'cctv', 12, 20, 1, 1), + P('runner', 'staffGlassie', 24, 20, 1, 2), ]); const L = ( @@ -359,4 +369,15 @@ export const ELEVATE: FloorLayout = Object.freeze({ posts: POSTS, probes: PROBES, palette: PALETTE, + // Materials: the roof is decking, the two roofed pockets are poured concrete, + // and the bar is marble-ish steel. Tinted, so the terrace stays cool and dark. + materials: Object.freeze({ + yard: 'tex_deck', + floor: 'tex_concrete', + wall: 'tex_plaster', + bar: 'tex_steel', + dance: 'tex_parquet', + sink: 'tex_looTile', + stall: 'tex_looTile', + }), }); diff --git a/src/scenes/floor/layouts/room.ts b/src/scenes/floor/layouts/room.ts index 62aab89..ac6f0e0 100644 --- a/src/scenes/floor/layouts/room.ts +++ b/src/scenes/floor/layouts/room.ts @@ -244,6 +244,22 @@ const ROOM_PROPS: readonly PropDef[] = Object.freeze([ P('bin', 'wheelieBin', 47, 40, 1, 2), P('butts', 'buttBin', 36, 41, 1, 1), P('dockCrate', 'crate', 43, 38, 1, 1), + + // Wave 2. The review found the east hall — the route from the floor to the + // dunnies, ~300 walkable tiles — carrying zero lights and three props. It is + // where a warehouse party actually stores its gear, so that is what is in it. + P('caseA', 'flightCase', 60, 18, 2, 1), + P('caseB', 'flightCase', 66, 24, 2, 1), + P('caseC', 'flightCase', 61, 32, 2, 1), + P('barrierA', 'barrier', 58, 22, 2, 1), + P('barrierB', 'barrier', 68, 28, 2, 1), + P('distroA', 'distroBox', 66, 17, 1, 1), + P('distroB', 'distroBox', 59, 27, 1, 1), + P('hose', 'fireHose', 71, 21, 1, 1), + P('extHall', 'fireExt', 71, 30, 1, 1), + P('extBar', 'fireExt', 9, 22, 1, 1), + P('exitHall', 'exitSign', 9, 18, 1, 1), + P('cleaner', 'staffCleaner', 64, 33, 1, 2), ]); const L = ( @@ -256,6 +272,12 @@ const L = ( // dunnies and the way out by colour — but between those pools there is nothing, // and that is the point of the venue. const ROOM_LIGHTS: readonly ZoneLight[] = Object.freeze([ + // Two low sodium spills down the east hall. Deliberately dimmer and smaller + // than anything on the main floor — this is the corridor to the dunnies, not a + // room, and ROOM is meant to stay hard to navigate without the torch. The + // review's point was that the hall had NO light at all, not that it was dark. + L('hallN', 63, 19, 0xe08420, 26), + L('hallS', 63, 30, 0xe08420, 26), // Two work lamps clipped to the scaffolding. Amber, because a bar is amber. L('bar0', 10, 26, 0xd8a020, 26), L('bar1', 10, 31, 0xd8a020, 26), @@ -347,4 +369,15 @@ export const ROOM: FloorLayout = Object.freeze({ posts: ROOM_POSTS, probes: ROOM_PROBES, palette: ROOM_PALETTE, + // Materials: concrete, concrete, concrete. The bar is scaffold plank, the dock + // is wet paving, and the dunnies are the only tiled surface in the building. + materials: Object.freeze({ + floor: 'tex_concrete', + dance: 'tex_concrete', + wall: 'tex_brick', + bar: 'tex_timber', + yard: 'tex_paving', + sink: 'tex_looTile', + stall: 'tex_looTile', + }), }); diff --git a/src/scenes/floor/layouts/theRoyal.ts b/src/scenes/floor/layouts/theRoyal.ts index 01eaec7..fa1b2d5 100644 --- a/src/scenes/floor/layouts/theRoyal.ts +++ b/src/scenes/floor/layouts/theRoyal.ts @@ -272,6 +272,22 @@ const PROPS: readonly PropDef[] = Object.freeze([ P('poster1', 'poster1', 3, 13, 2, 1), P('poster2', 'poster2', 6, 13, 2, 1), P('poster3', 'poster3', 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 + // bins, signage and a fan every few metres; now so does this one. + P('aFrame', 'sandwichBoard', 34, 19, 1, 1), + P('binMidW', 'binRound', 36, 26, 1, 1), + P('binMidE', 'binRound', 50, 30, 1, 1), + P('fanMidW', 'ceilingFan', 41, 21, 2, 2), + P('fanMidE', 'ceilingFan', 52, 27, 2, 2), + P('cleaner', 'staffCleaner', 46, 18, 1, 2), + P('notices', 'noticeBoard', 28, 12, 2, 1), + P('pies', 'pieWarmer', 52, 2, 2, 1), + P('extMain', 'fireExt', 32, 13, 1, 1), + P('extGarden', 'fireExt', 45, 33, 1, 1), + P('exitMain', 'exitSign', 12, 13, 1, 1), + P('camMain', 'cctv', 40, 16, 1, 1), ]); const L = ( @@ -374,4 +390,16 @@ export const THE_ROYAL: FloorLayout = Object.freeze({ posts: POSTS, probes: PROBES, palette: PALETTE, + // Materials (docs/VENUES.md §1). Stamped tinted, so these carry grain and the + // palette above still decides hue — pub carpet, nicotine plaster, a varnished + // counter, and the 2004 parquet nobody has re-laid. + materials: Object.freeze({ + floor: 'tex_carpet', + wall: 'tex_plaster', + bar: 'tex_timber', + dance: 'tex_parquet', + yard: 'tex_paving', + sink: 'tex_looTile', + stall: 'tex_looTile', + }), }); diff --git a/src/scenes/floor/venueMap.ts b/src/scenes/floor/venueMap.ts index e38f38a..02a1525 100644 --- a/src/scenes/floor/venueMap.ts +++ b/src/scenes/floor/venueMap.ts @@ -230,7 +230,13 @@ export type PropKind = // Wall surfaces. Added after review: Elevate was standing BOTH its mirrors up // as `poster3`, so the lift lobby and the marble bathroom would have rendered // the same torn gig poster and called it a mirror. - | 'mirror' | 'graffiti'; + | 'mirror' | 'graffiti' + // Wave 2: the small dressing every real room is full of and no game room has — + // the fire kit, the signage, the bins — plus filler for the two spaces the + // 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'; export interface PropEmissive { colour: number; @@ -333,6 +339,18 @@ export const PROPS: readonly PropDef[] = Object.freeze([ P('heater', 'patioHeater', 21, 39, 1, 2, { colour: 0xe07030, radius: 34, pulse: 'flicker' }), P('trussN', 'truss', 34, 15, 3, 1), P('trussS', 'truss', 34, 30, 3, 1), + + // Wave 2 (2026-07-28). Voltage is the reference room, which made it the one + // venue that quietly missed the dressing pass the other three got — the fire + // kit, the signage and the bins every real room is full of. + P('extCorridor', 'fireExt', 15, 21, 1, 1), + P('extFloor', 'fireExt', 55, 35, 1, 1), + P('exitCorridor', 'exitSign', 2, 20, 1, 1), + P('camFloor', 'cctv', 30, 10, 1, 1), + P('fanFloor', 'ceilingFan', 33, 12, 2, 2), + P('binFloor', 'binRound', 24, 33, 1, 1), + P('cleaner', 'staffCleaner', 58, 34, 1, 2), + P('barkeep', 'staffBartender', 30, 3, 1, 1), ]); const L = ( @@ -417,6 +435,15 @@ export interface FloorLayout { probes: Readonly>; /** Per-venue tile colours, merged over COLOURS. */ palette?: Readonly>>; + /** + * Generated material plates per tile kind, by manifest name (e.g. 'tex_carpet' + * for public/props/tex_carpet.png). The plate supplies grain; `palette` + * supplies hue, because the plate is stamped TINTED — that keeps a venue + * inside the brightness band the darkness sheet was tuned against instead of + * letting a bright texture wash the room out. Unlisted kinds, or art that has + * not landed, fall back to flat palette colour. + */ + materials?: Readonly>>; } export function tileAt(map: VenueMap, tx: number, ty: number): TileKind { @@ -458,4 +485,15 @@ export const VOLTAGE: FloorLayout = Object.freeze({ lights: LIGHTS, posts: POSTS, probes: PROBES, + // Voltage's colours are the tuned baseline and do not move; the plates only + // add grain under them (materials are stamped TINTED to these same colours). + materials: Object.freeze({ + floor: 'tex_concrete', + wall: 'tex_plaster', + bar: 'tex_timber', + dance: 'tex_parquet', + yard: 'tex_paving', + sink: 'tex_looTile', + stall: 'tex_looTile', + }), }); diff --git a/tests/floor/layout-room.test.ts b/tests/floor/layout-room.test.ts index 106e2fa..3373f2f 100644 --- a/tests/floor/layout-room.test.ts +++ b/tests/floor/layout-room.test.ts @@ -111,10 +111,17 @@ describe('ROOM layout', () => { const dance = ROOM.lights.filter((l) => l.colour === 0xd03470 && l.pulse === 'beat'); expect(dance.length).toBeGreaterThanOrEqual(4); for (const l of dance) expect(l.radius).toBeLessThan(26); - // The one signature colour ROOM is allowed, and it belongs to the dock. - const sodium = ROOM.lights.find((l) => l.colour === 0xe08420); - expect(sodium).toBeDefined(); - expect(tileAt(ROOM.map, sodium!.tx, sodium!.ty)).toBe('yard'); + // The one signature colour ROOM is allowed, and the dock is what it is FOR. + // Asserted as "at least one sodium light stands over the dock" rather than + // "the first one does": the east hall legitimately burns the same colour + // (it is the same building's lighting), and keying on array order made this + // fail the moment another sodium light was added anywhere earlier. + const sodium = ROOM.lights.filter((l) => l.colour === 0xe08420); + expect(sodium.length).toBeGreaterThan(0); + expect( + sodium.some((l) => tileAt(ROOM.map, l.tx, l.ty) === 'yard'), + 'no sodium light stands over the loading dock', + ).toBe(true); }); it('runs the rig on the floor, taped down', () => { diff --git a/tools/gen/assets.py b/tools/gen/assets.py index 57f6d0c..09c2980 100644 --- a/tools/gen/assets.py +++ b/tools/gen/assets.py @@ -164,6 +164,35 @@ ROOM = [ "cryptic house rules, chipped frame"), ] +# ---- Material plates (docs/VENUES.md §1: `FloorLayout.materials`) ------------- +# Stamped one-per-tile and TINTED to the venue palette, so these want to be +# grain and pattern, not colour — the hue is applied at bake time. 16x16 is the +# game's tile, and a plate that reads as "carpet" at 16px is coloured noise with +# structure, which is also what pub carpet looks like from head height. +TEXTURE_TAIL = ( + "seamless repeating texture, flat lay, straight overhead, evenly lit, " + "no objects, no text, no watermark, tileable" +) + + +def T(kind: str, seed: int, subject: str) -> Asset: + return Asset(f"tex:{kind}", "flat", (16, 16), f"{subject}, {TEXTURE_TAIL}", + seed, gen=(1024, 1024), quantise=False, keep_bg=True) + + +TEXTURES = [ + T("carpet", 4490, "gaudy patterned red and navy pub carpet, worn, swirling 1990s design"), + T("concrete", 4491, "raw grey polished concrete warehouse floor, cracks and stains"), + T("deck", 4492, "pale grey composite timber rooftop decking boards"), + T("parquet", 4493, "scuffed wooden parquet dance floor, herringbone blocks"), + T("looTile", 4494, "small square white ceramic bathroom floor tiles with grimy grout"), + T("paving", 4495, "grey concrete paver slabs, wet, outdoor courtyard"), + T("plaster", 4496, "flat painted plaster wall, scuffed and nicotine stained"), + T("brick", 4497, "dark painted brick wall, mortar lines"), + T("timber", 4498, "varnished dark timber bar counter top, grain visible"), + T("steel", 4499, "brushed stainless steel panel, faint scratches"), +] + # ---- Wall surfaces (flat by nature — flux draws these directly) --------------- WALLS = [ F("mirror", (32, 16), 4480, @@ -200,7 +229,53 @@ STREETS = [ gen=(2048, 576), quantise=False, keep_bg=True), ] -ALL: list[Asset] = [*ROYAL, *ELEVATE, *ROOM, *WALLS, *STAFF, *STREETS] +# ---- Wave 2: filling the rooms out ------------------------------------------ +# Reviewers flagged two genuinely empty spaces (The Royal's central carpet, ROOM's +# east hall) and the venues share almost no small dressing — the fire kit, signage +# and bins every real room is full of and no game room ever has. +FILLER = [ + # The Royal's middle + M("pieWarmer", (32, 16), 4500, 60, + "a glass fronted heated pie warmer cabinet on a pub counter"), + M("binRound", (16, 16), 4501, 76, + "a round stainless steel swing top rubbish bin"), + M("sandwichBoard", (16, 16), 4502, 60, + "a wooden A-frame chalkboard sandwich board sign standing open"), + F("noticeBoard", (32, 16), 4503, + "a cork pub notice board covered in pinned paper flyers and raffle tickets"), + F("ceilingFan", (32, 32), 4504, + "a dusty four blade ceiling fan seen from directly below, dark"), + # ROOM's east hall and general industrial + M("flightCase", (32, 16), 4505, 60, + "a black road flight case with metal corners and latches"), + M("barrier", (32, 16), 4506, 76, + "a galvanised steel crowd control barrier fence section"), + M("distroBox", (16, 16), 4507, 76, + "a grey industrial electrical distribution box with thick cables"), + F("fireHose", (16, 16), 4508, + "a red fire hose reel mounted on a wall"), + # Elevate + M("firepit", (32, 16), 4509, 60, + "a modern rectangular gas fire pit table with glass surround, lit"), + M("topiary", (16, 16), 4510, 76, + "a neatly clipped ball topiary shrub in a tall planter"), + # Every venue: the kit a real room has and a game room never does + M("fireExt", (16, 16), 4511, 76, + "a red fire extinguisher on a wall bracket"), + F("exitSign", (16, 16), 4512, + "a glowing green illuminated fire exit sign, running man pictogram"), + F("cctv", (16, 16), 4513, + "a small white dome CCTV security camera on a ceiling mount"), + # More staff figures (set dressing, never patrons) + M("staffCleaner", (16, 32), 4514, 76, + "a single cleaner standing holding a mop, hi vis vest, full body"), + M("staffChef", (16, 32), 4515, 76, + "a single kitchen cook standing in whites and apron, arms at sides, full body"), +] + +ALL: list[Asset] = [ + *ROYAL, *ELEVATE, *ROOM, *TEXTURES, *WALLS, *STAFF, *FILLER, *STREETS, +] BY_KIND = {a.kind: a for a in ALL}