diff --git a/LANEHANDOVER.md b/LANEHANDOVER.md index cdd0763..c32d998 100644 --- a/LANEHANDOVER.md +++ b/LANEHANDOVER.md @@ -1114,3 +1114,46 @@ queue (SPACES §5), the two human passes (ear, economy feel) still open. non-code items. Code-side: regular step-up lines, Elevate VIP guest-list density (claims are archetype-driven so the mix already lifts them), and the smoking-door slip-in (FLOOR2 §2's second noStamp source) are the next nibbles. + +--- +### SESSION — THE CLUB GETS DRESSED (Fable, solo) — 2026-07-20 22:15 +**Branch/commits:** main; deployed live with the full art drop. +**Done:** +- **Ran the entire SPACES §5 generation queue myself on MODELBEAST** — 21 + flux_local jobs (all prop sprites + 3 posters + the street backdrop), + processed through tools/props_import.py on ultra (PIL), 20 sprites now live + in public/props/ + manifest. The drop-in contract worked exactly as designed: + zero code changes for the floor props. +- **Two new consumers wired**: NightScene now preloads the manifest art BEFORE + launching Door/Floor (textures are game-global), and DoorScene swaps its + flat wall-of-night rect for the generated STREET BACKDROP plate — brick, + layered gig posters, graffiti, rain streaks. The door scene went from + placeholder rects to a real street in one image. +- **Dance floor light show**: two scan beams pivoting on the disco ball + (baked wedge texture, additive, opposite rotations, beat-kicked brightness) + and the grid cells now COLOUR-CYCLE by bar (pink/blue/amber, phase-offset + per cell so the floor ripples instead of blinking). +- **Furniture pass**: pool table with low green cone (the Australian venue + underneath the techno), plants, wet-floor sign, standing tables by the dance + floor, third east booth table. New kinds registered end-to-end (map → sizes + → placeholders → importer targets). +- **Bug fixed (found live)**: stepping inside during the floor's prop-load + window meant the scene missed night:phaseChange and never learned the player + was present (camera dead, input ignored). `present` now initialises from + NightState.location at bind time. +- **Farm note**: first queue run lost all 20 downloads to a shell-quoting + mangle (ssh single-quote stripping around ['id'] — same trap as the cover + art). The renders survived on the farm; patched and refetched. If you script + MB over ssh: heredoc with QUOTED delimiter, double-quoted keys, always. +**Tests: 646. Gate clean. Deployed; game + backdrop both 200 live.** +**Broke / known-wonky:** +- Generated sprites are first-pass flux at 16-48px — most read beautifully + (decks/ball/backdrop/posters are transformative), a few are just fine + (glassRack, stampPodium). Re-rolls are one importer command away; judge on + John's screen, not mine. +- The GLB prerender path (real 90sDJsim gear via Blender) remains unused — + flux versions landed well enough that it's now an upgrade, not a need. +- Speaker sprite is slight-angle rather than pure top-down; reads good in situ. +**Next:** John plays it (economy feel + ear pass, M works live); re-roll any +sprite that bugs him; regular step-up lines and the smoking-door slip-in stay +the next code nibbles. diff --git a/art_incoming/arcade.png b/art_incoming/arcade.png new file mode 100644 index 0000000..bcb6e9a Binary files /dev/null and b/art_incoming/arcade.png differ diff --git a/art_incoming/barShelf.png b/art_incoming/barShelf.png new file mode 100644 index 0000000..6c6544f Binary files /dev/null and b/art_incoming/barShelf.png differ diff --git a/art_incoming/boothTable.png b/art_incoming/boothTable.png new file mode 100644 index 0000000..1f47f1b Binary files /dev/null and b/art_incoming/boothTable.png differ diff --git a/art_incoming/buttBin.png b/art_incoming/buttBin.png new file mode 100644 index 0000000..5536996 Binary files /dev/null and b/art_incoming/buttBin.png differ diff --git a/art_incoming/cloak.png b/art_incoming/cloak.png new file mode 100644 index 0000000..3a79478 Binary files /dev/null and b/art_incoming/cloak.png differ diff --git a/art_incoming/crate.png b/art_incoming/crate.png new file mode 100644 index 0000000..c766362 Binary files /dev/null and b/art_incoming/crate.png differ diff --git a/art_incoming/deck.png b/art_incoming/deck.png new file mode 100644 index 0000000..6d4f1de Binary files /dev/null and b/art_incoming/deck.png differ diff --git a/art_incoming/discoball.png b/art_incoming/discoball.png new file mode 100644 index 0000000..7a52771 Binary files /dev/null and b/art_incoming/discoball.png differ diff --git a/art_incoming/glassRack.png b/art_incoming/glassRack.png new file mode 100644 index 0000000..c00c83e Binary files /dev/null and b/art_incoming/glassRack.png differ diff --git a/art_incoming/glassStack.png b/art_incoming/glassStack.png new file mode 100644 index 0000000..01d3054 Binary files /dev/null and b/art_incoming/glassStack.png differ diff --git a/art_incoming/mixer.png b/art_incoming/mixer.png new file mode 100644 index 0000000..b3a2ae3 Binary files /dev/null and b/art_incoming/mixer.png differ diff --git a/art_incoming/poster1.png b/art_incoming/poster1.png new file mode 100644 index 0000000..d03f273 Binary files /dev/null and b/art_incoming/poster1.png differ diff --git a/art_incoming/poster2.png b/art_incoming/poster2.png new file mode 100644 index 0000000..c901cef Binary files /dev/null and b/art_incoming/poster2.png differ diff --git a/art_incoming/poster3.png b/art_incoming/poster3.png new file mode 100644 index 0000000..78bf40f Binary files /dev/null and b/art_incoming/poster3.png differ diff --git a/art_incoming/sinkRow.png b/art_incoming/sinkRow.png new file mode 100644 index 0000000..7bc284b Binary files /dev/null and b/art_incoming/sinkRow.png differ diff --git a/art_incoming/speaker.png b/art_incoming/speaker.png new file mode 100644 index 0000000..cccb3ae Binary files /dev/null and b/art_incoming/speaker.png differ diff --git a/art_incoming/stampPodium.png b/art_incoming/stampPodium.png new file mode 100644 index 0000000..1ab721c Binary files /dev/null and b/art_incoming/stampPodium.png differ diff --git a/art_incoming/street_backdrop.png b/art_incoming/street_backdrop.png new file mode 100644 index 0000000..7149abd Binary files /dev/null and b/art_incoming/street_backdrop.png differ diff --git a/art_incoming/taps.png b/art_incoming/taps.png new file mode 100644 index 0000000..71d3449 Binary files /dev/null and b/art_incoming/taps.png differ diff --git a/art_incoming/till.png b/art_incoming/till.png new file mode 100644 index 0000000..fe28386 Binary files /dev/null and b/art_incoming/till.png differ diff --git a/public/props/arcade.png b/public/props/arcade.png new file mode 100644 index 0000000..b80ae55 Binary files /dev/null and b/public/props/arcade.png differ diff --git a/public/props/barShelf.png b/public/props/barShelf.png new file mode 100644 index 0000000..5f0c06a Binary files /dev/null and b/public/props/barShelf.png differ diff --git a/public/props/boothTable.png b/public/props/boothTable.png new file mode 100644 index 0000000..16778e6 Binary files /dev/null and b/public/props/boothTable.png differ diff --git a/public/props/buttBin.png b/public/props/buttBin.png new file mode 100644 index 0000000..656fbe0 Binary files /dev/null and b/public/props/buttBin.png differ diff --git a/public/props/cloak.png b/public/props/cloak.png new file mode 100644 index 0000000..6544c80 Binary files /dev/null and b/public/props/cloak.png differ diff --git a/public/props/crate.png b/public/props/crate.png new file mode 100644 index 0000000..abae434 Binary files /dev/null and b/public/props/crate.png differ diff --git a/public/props/deck.png b/public/props/deck.png new file mode 100644 index 0000000..38935f8 Binary files /dev/null and b/public/props/deck.png differ diff --git a/public/props/discoball.png b/public/props/discoball.png new file mode 100644 index 0000000..8716bea Binary files /dev/null and b/public/props/discoball.png differ diff --git a/public/props/glassRack.png b/public/props/glassRack.png new file mode 100644 index 0000000..e33c6b6 Binary files /dev/null and b/public/props/glassRack.png differ diff --git a/public/props/glassStack.png b/public/props/glassStack.png new file mode 100644 index 0000000..01d02a5 Binary files /dev/null and b/public/props/glassStack.png differ diff --git a/public/props/manifest.json b/public/props/manifest.json index e08a06c..b993bca 100644 --- a/public/props/manifest.json +++ b/public/props/manifest.json @@ -1 +1,24 @@ -{ "props": [] } +{ + "props": [ + "arcade", + "barShelf", + "boothTable", + "buttBin", + "cloak", + "crate", + "deck", + "discoball", + "glassRack", + "glassStack", + "mixer", + "poster1", + "poster2", + "poster3", + "sinkRow", + "speaker", + "stampPodium", + "street_backdrop", + "taps", + "till" + ] +} diff --git a/public/props/mixer.png b/public/props/mixer.png new file mode 100644 index 0000000..a6c0e29 Binary files /dev/null and b/public/props/mixer.png differ diff --git a/public/props/poster1.png b/public/props/poster1.png new file mode 100644 index 0000000..54d5d78 Binary files /dev/null and b/public/props/poster1.png differ diff --git a/public/props/poster2.png b/public/props/poster2.png new file mode 100644 index 0000000..bca0e17 Binary files /dev/null and b/public/props/poster2.png differ diff --git a/public/props/poster3.png b/public/props/poster3.png new file mode 100644 index 0000000..764c1d4 Binary files /dev/null and b/public/props/poster3.png differ diff --git a/public/props/sinkRow.png b/public/props/sinkRow.png new file mode 100644 index 0000000..5f0ad58 Binary files /dev/null and b/public/props/sinkRow.png differ diff --git a/public/props/speaker.png b/public/props/speaker.png new file mode 100644 index 0000000..1be4420 Binary files /dev/null and b/public/props/speaker.png differ diff --git a/public/props/stampPodium.png b/public/props/stampPodium.png new file mode 100644 index 0000000..23070c0 Binary files /dev/null and b/public/props/stampPodium.png differ diff --git a/public/props/street_backdrop.png b/public/props/street_backdrop.png new file mode 100644 index 0000000..779e1db Binary files /dev/null and b/public/props/street_backdrop.png differ diff --git a/public/props/taps.png b/public/props/taps.png new file mode 100644 index 0000000..d50d7ca Binary files /dev/null and b/public/props/taps.png differ diff --git a/public/props/till.png b/public/props/till.png new file mode 100644 index 0000000..ff7fd1d Binary files /dev/null and b/public/props/till.png differ diff --git a/src/scenes/door/DoorScene.ts b/src/scenes/door/DoorScene.ts index 00ccca6..6107d1c 100644 --- a/src/scenes/door/DoorScene.ts +++ b/src/scenes/door/DoorScene.ts @@ -165,7 +165,13 @@ export class DoorScene extends Phaser.Scene { // ---------- construction ---------- private buildStreet(): void { - this.add.rectangle(W / 2, 88, W, 176, 0x0a0a12); // wall of night + if (this.textures.exists('gen:prop:street_backdrop')) { + // The generated plate (SPACES §5 item 11): brick, posters, grime. Sits + // where the flat wall-of-night rect went; everything else layers on top. + this.add.image(0, 0, 'gen:prop:street_backdrop').setOrigin(0, 0).setDisplaySize(W, 176).setAlpha(0.9); + } else { + this.add.rectangle(W / 2, 88, W, 176, 0x0a0a12); // wall of night + } this.add.rectangle(W / 2, 212, W, 76, 0x1a1a24); // footpath this.add.rectangle(W / 2, 176, W, 2, 0x2a2a38); diff --git a/src/scenes/door/NightScene.ts b/src/scenes/door/NightScene.ts index 3940fc4..f120f5d 100644 --- a/src/scenes/door/NightScene.ts +++ b/src/scenes/door/NightScene.ts @@ -267,11 +267,27 @@ export class NightScene extends Phaser.Scene { this.events.once('shutdown', () => this.teardown()); this.clock.start(); - this.scene.launch('Door', { night: this.ctx }); - this.scene.launch('Floor', { night: this.ctx }); - // Door and Floor BOTH run all night; the flags below decide which one the - // player is inhabiting. Floor starts hidden. - this.time.delayedCall(0, () => this.applyLocation('door')); + + // Generated art loads BEFORE the scenes that consume it (textures are + // game-global): the door's street backdrop and the floor's props both ride + // public/props + manifest. Missing manifest = placeholders, zero waiting. + this.load.json('propManifest', 'props/manifest.json'); + this.load.once(Phaser.Loader.Events.COMPLETE, () => { + const manifest = this.cache.json.get('propManifest') as { props?: string[] } | undefined; + const missing = (manifest?.props ?? []).filter((k) => !this.textures.exists(`gen:prop:${k}`)); + const launch = (): void => { + this.scene.launch('Door', { night: this.ctx }); + this.scene.launch('Floor', { night: this.ctx }); + // Door and Floor BOTH run all night; the flags below decide which one + // the player is inhabiting. Floor starts hidden. + this.time.delayedCall(0, () => this.applyLocation('door')); + }; + if (missing.length === 0) return launch(); + for (const k of missing) this.load.image(`gen:prop:${k}`, `props/${k}.png`); + this.load.once(Phaser.Loader.Events.COMPLETE, launch); + this.load.start(); + }); + this.load.start(); } // ---- location ---------------------------------------------------------- diff --git a/src/scenes/floor/FloorDemoScene.ts b/src/scenes/floor/FloorDemoScene.ts index 2e35ab6..ed29940 100644 --- a/src/scenes/floor/FloorDemoScene.ts +++ b/src/scenes/floor/FloorDemoScene.ts @@ -176,7 +176,10 @@ export class FloorDemoScene extends Phaser.Scene { this.target = null; this.liveFight = null; this.uvMode = false; - this.present = false; // the night starts at the rope + // From STATE, not an assumption: if the player stepped inside during the + // prop-manifest load window, the phaseChange event fired before our + // listener existed and 'false' would stick forever. + this.present = ctx.state.location === 'floor'; this.elapsedMs = 0; this.pendingSpawns = []; this.maggotSeen.clear(); diff --git a/src/scenes/floor/FloorView.ts b/src/scenes/floor/FloorView.ts index c7ab8e8..801dcf9 100644 --- a/src/scenes/floor/FloorView.ts +++ b/src/scenes/floor/FloorView.ts @@ -87,6 +87,8 @@ export class FloorView { private readonly flickerLights: Array<{ img: Phaser.GameObjects.Image; base: number; phase: number }> = []; private beatKick = 0; private yardRain: Phaser.GameObjects.Particles.ParticleEmitter | null = null; + private readonly beams: Phaser.GameObjects.Image[] = []; + private beamAngle = 0; private dj: Phaser.GameObjects.Image | null = null; private djBob = 0; // Render-only shove displacement by patron id. The sim owns agent.x/agent.y; @@ -147,6 +149,7 @@ export class FloorView { this.buildLights(); this.buildYardRain(); this.buildDj(); + this.buildScanBeams(); this.maskG = scene.make.graphics({}, false); this.mask = this.maskG.createGeometryMask(); @@ -219,6 +222,43 @@ export class FloorView { } } + /** + * The light rig: two scan beams pivoting on the disco ball, sweeping the + * dance floor in opposite directions. Baked wedge texture, additive blend — + * all show, no cost. + */ + private buildScanBeams(): void { + const key = 'floor:beam'; + if (!this.scene.textures.exists(key)) { + const canvas = this.scene.textures.createCanvas(key, 180, 36); + if (canvas) { + const c = canvas.getContext(); + const grad = c.createLinearGradient(0, 0, 180, 0); + grad.addColorStop(0, 'rgba(255,255,255,0.55)'); + grad.addColorStop(1, 'rgba(255,255,255,0)'); + c.fillStyle = grad; + c.beginPath(); + c.moveTo(0, 16); c.lineTo(180, 0); c.lineTo(180, 36); c.lineTo(0, 20); + c.closePath(); c.fill(); + canvas.refresh(); + } + } + // Pivot on the disco ball (PROPS: 'ball' at 37,22 with a 2x2 footprint). + const cx = 38 * TILE; + const cy = 23 * TILE; + for (let i = 0; i < 2; i++) { + this.beams.push( + this.scene.add + .image(cx, cy, key) + .setOrigin(0, 0.5) + .setDepth(D_NEON) + .setBlendMode(Phaser.BlendModes.ADD) + .setAlpha(0.16) + .setRotation(i * Math.PI), + ); + } + } + /** Rain falls INSIDE the smoking yard — it is the one open-sky pocket. */ private buildYardRain(): void { const yard = { x0: Infinity, y0: Infinity, x1: -Infinity, y1: -Infinity }; @@ -272,6 +312,13 @@ export class FloorView { onBeat(beatIndex: number): void { this.beatKick = beatIndex % 4 === 0 ? 1 : 0.6; this.djBob = 1; + // The dance grid cycles colour by bar — each cell offset so the floor + // ripples instead of blinking as one. + const bar = Math.floor(beatIndex / 4); + const PALETTE = [0xd03470, 0x3060c0, 0xd8a020]; + this.beatLights.forEach((img, i) => { + img.setTint(PALETTE[(bar + i) % PALETTE.length]!); + }); } /** Per-frame light life: beat decay + fluoro flicker. */ @@ -280,6 +327,11 @@ export class FloorView { this.beatKick = Math.max(0, this.beatKick - dtMs / 260); for (const img of this.beatLights) img.setAlpha(0.22 + 0.3 * this.beatKick); } + this.beamAngle += dtMs * 0.00035 * (1 + this.beatKick * 0.8); + this.beams.forEach((b, i) => { + b.setRotation(this.beamAngle * (i === 0 ? 1 : -1) + i * Math.PI); + b.setAlpha(0.1 + 0.14 * this.beatKick); + }); if (this.dj && this.djBob > 0) { this.djBob = Math.max(0, this.djBob - dtMs / 200); this.dj.setY(22.5 * TILE - 2 * this.djBob); @@ -441,6 +493,8 @@ export class FloorView { for (const n of this.neon) n.destroy(); this.yardRain?.destroy(); this.yardRain = null; + for (const b of this.beams) b.destroy(); + this.beams.length = 0; this.dj?.destroy(); this.dj = null; for (const pr of this.propImgs) pr.destroy(); @@ -564,6 +618,7 @@ const PROP_SIZES: Record = { buttBin: [16, 16], crate: [16, 16], poster1: [32, 16], poster2: [32, 16], poster3: [32, 16], cloak: [16, 48], stampPodium: [16, 16], arcade: [32, 32], + highTable: [16, 16], plant: [16, 16], wetFloor: [16, 16], poolTable: [48, 32], }; function bakePropPlaceholder(scene: Phaser.Scene, kind: PropKind): string { @@ -658,6 +713,24 @@ function bakePropPlaceholder(scene: Phaser.Scene, kind: PropKind): string { case 'arcade': f(3, 1, 26, 30, '#1c2a3c'); f(7, 5, 18, 12, '#40c0d0'); f(9, 20, 6, 4, '#c03434'); f(19, 20, 6, 4, '#d8c23a'); break; + case 'highTable': + c.beginPath(); c.arc(8, 8, 6, 0, Math.PI * 2); c.fillStyle = '#3c2e22'; c.fill(); + f(5, 5, 3, 3, '#d8a050'); f(9, 7, 3, 3, '#9fb2c8'); + break; + case 'plant': + f(6, 10, 4, 5, '#6e4a2e'); + f(4, 3, 3, 6, '#2e7d46'); f(9, 2, 3, 7, '#2e7d46'); f(6, 1, 3, 8, '#3a9456'); + break; + case 'wetFloor': + f(5, 2, 6, 12, '#d8c23a'); f(6, 4, 4, 4, '#22222c'); + break; + case 'poolTable': + f(2, 2, 44, 28, '#4a3226'); f(5, 5, 38, 22, '#1e6e3c'); + for (const [px, py] of [[6, 6], [42, 6], [6, 25], [42, 25], [24, 5], [24, 26]] as const) { + c.beginPath(); c.arc(px, py, 2, 0, Math.PI * 2); c.fillStyle = '#0c0c12'; c.fill(); + } + f(20, 14, 3, 3, '#f2f2f2'); f(28, 12, 3, 3, '#c03434'); f(31, 18, 3, 3, '#d8c23a'); + break; } canvas.refresh(); return key; diff --git a/src/scenes/floor/venueMap.ts b/src/scenes/floor/venueMap.ts index d74575f..6b108aa 100644 --- a/src/scenes/floor/venueMap.ts +++ b/src/scenes/floor/venueMap.ts @@ -177,8 +177,8 @@ export type PropKind = | 'deck' | 'mixer' | 'monitor' | 'boothLamp' | 'speaker' | 'discoball' | 'barShelf' | 'taps' | 'till' | 'glassStack' | 'glassRack' - | 'boothTable' | 'sinkRow' - | 'buttBin' | 'crate' + | 'boothTable' | 'highTable' | 'sinkRow' + | 'buttBin' | 'crate' | 'plant' | 'wetFloor' | 'poolTable' | 'poster1' | 'poster2' | 'poster3' | 'cloak' | 'stampPodium' | 'arcade'; export interface PropEmissive { @@ -253,6 +253,15 @@ export const PROPS: readonly PropDef[] = Object.freeze([ P('podium', 'stampPodium', 12, 22, 1, 1), // one arcade cabinet, because it is a venue in Australia P('arcade', 'arcade', 15, 34, 2, 2, { colour: 0x40c0d0, radius: 18, pulse: 'flicker' }), + // filling out the room (2026-07-20): the club stops being furniture-poor + P('tblE3', 'boothTable', 75, 33, 1, 1, { colour: 0xd8a050, radius: 12 }), + P('high1', 'highTable', 28, 34, 1, 1), + P('high2', 'highTable', 46, 34, 1, 1), + P('plant1', 'plant', 14, 25, 1, 1), + P('plant2', 'plant', 21, 36, 1, 1), + P('wet', 'wetFloor', 35, 8, 1, 1), + // the pool corner — it is still an Australian venue underneath the techno + P('pool', 'poolTable', 15, 10, 3, 2), ]); const L = ( @@ -273,6 +282,8 @@ export const LIGHTS: readonly ZoneLight[] = Object.freeze([ L('yard', 18, 40, 0x6080a0, 44), L('entry', 2, 22, 0xd03470, 40), L('glassie', 17, 5, 0xd8a020, 28), + // low green cone over the pool table, like every pool table ever + L('pool', 16, 10, 0x30a050, 34), ]); export const VENUE: VenueMap = Object.freeze({ diff --git a/tools/props_import.py b/tools/props_import.py index 32ad574..5427208 100644 --- a/tools/props_import.py +++ b/tools/props_import.py @@ -25,6 +25,7 @@ TARGETS = { "buttBin": (16, 16), "crate": (16, 16), "poster1": (32, 16), "poster2": (32, 16), "poster3": (32, 16), "cloak": (16, 48), "stampPodium": (16, 16), "arcade": (32, 32), + "highTable": (16, 16), "plant": (16, 16), "wetFloor": (16, 16), "poolTable": (48, 32), # the door-scene backdrop plate is special-cased: full size, no quantise "street:backdrop": (640, 176), }