import Phaser from 'phaser'; import { EventBus } from '../../core/EventBus'; import { GameClock } from '../../core/GameClock'; import { SeededRNG } from '../../core/SeededRNG'; import { Meters, freshNightState } from '../../core/meters'; import { drunkStage } from '../../rules/drunk'; import { generatePatron, idAge, _resetPatronSerial } from '../../patrons/generator'; import { renderDoll } from '../../patrons/doll'; import { dollPlan } from '../../patrons/dollPlan'; import type { Patron } from '../../data/types'; import { HudStub } from './HudStub'; const W = 640; const H = 360; interface Walker { patron: Patron; root: Phaser.GameObjects.Container; doll: Phaser.GameObjects.Image; speed: number; } // Phase-0 proof scene: generator + doll renderer + RNG determinism + clock all // visibly working. Patrons stream across a rainy street; their ID card renders // from the same data record. SPACE reseeds, D toggles ID cards. export class ParadeScene extends Phaser.Scene { private bus!: EventBus; private clock!: GameClock; private rng!: SeededRNG; private meters!: Meters; private hud!: HudStub; private walkers: Walker[] = []; private seed = 4207; private spawnAccMs = 0; private showIds = true; private seedText!: Phaser.GameObjects.Text; private clockText!: Phaser.GameObjects.Text; constructor() { super('Parade'); } create(): void { this.buildWorld(); this.resetRun(this.seed); this.input.keyboard?.on('keydown-SPACE', () => this.resetRun(this.seed + 1)); this.input.keyboard?.on('keydown-F', () => this.scene.start('FloorDemo')); // LANE-FLOOR demo this.input.keyboard?.on('keydown-D', () => { this.showIds = !this.showIds; for (const w of this.walkers) { (w.root.getByName('card') as Phaser.GameObjects.Container | null)?.setVisible(this.showIds); } }); } private buildWorld(): void { // street this.add.rectangle(W / 2, H - 40, W, 80, 0x1a1a24); // footpath this.add.rectangle(W / 2, H / 2 - 40, W, H - 160, 0x0d0d16); // wall of night // kebab shop glow, stage left — the moral centre of the game this.add.rectangle(70, 120, 120, 90, 0x2a1c08); this.add.rectangle(70, 96, 100, 18, 0xd8a020, 0.9); this.add.text(34, 90, 'KEBABS', { fontFamily: 'monospace', fontSize: '12px', color: '#3a2404' }); this.add.rectangle(70, 150, 100, 40, 0xe8c060, 0.12); // venue door, stage right this.add.rectangle(W - 60, 130, 90, 110, 0x181828); this.add.rectangle(W - 60, 90, 70, 14, 0xd03470, 0.9); this.add.text(W - 88, 84, 'NOT TONIGHT', { fontFamily: 'monospace', fontSize: '9px', color: '#2a0818' }); this.add.rectangle(W - 60, 150, 26, 54, 0x05050a); // neon reflections on the footpath this.add.rectangle(70, H - 60, 90, 6, 0xd8a020, 0.15); this.add.rectangle(W - 60, H - 60, 60, 6, 0xd03470, 0.15); // rain const rainG = this.add.graphics(); rainG.fillStyle(0x8899bb, 1); rainG.fillRect(0, 0, 1, 4); rainG.generateTexture('rainDrop', 1, 4); rainG.destroy(); this.add.particles(0, 0, 'rainDrop', { x: { min: 0, max: W }, y: -6, lifespan: 900, speedY: { min: 220, max: 300 }, speedX: { min: -30, max: -10 }, quantity: 2, alpha: { start: 0.5, end: 0.15 }, }); this.seedText = this.add.text(4, H - 12, '', { fontFamily: 'monospace', fontSize: '8px', color: '#667' }); this.clockText = this.add.text(W - 60, 4, '', { fontFamily: 'monospace', fontSize: '8px', color: '#9fe8a0' }); this.add.text(4, 14, 'SPACE reseed · D toggle IDs · F floor demo', { fontFamily: 'monospace', fontSize: '8px', color: '#556' }); } private resetRun(seed: number): void { this.seed = seed; for (const w of this.walkers) w.root.destroy(); this.walkers = []; _resetPatronSerial(); this.bus?.removeAll(); this.bus = new EventBus(); this.rng = new SeededRNG(seed); this.clock = new GameClock(this.bus); this.meters?.destroy(); this.meters = new Meters(this.bus, freshNightState('theRoyal', 0, 80)); this.hud?.destroy(); this.hud = new HudStub(this, this.bus); this.clock.start(); this.bus.on('clock:tick', () => { // idle proof that the meter pipeline works: hype breathes with the clock this.bus.emit('meters:delta', { vibe: 0 }); }); this.seedText.setText(`seed ${seed}`); } private spawnWalker(): void { const patron = generatePatron({ rng: this.rng, nightDate: this.clock.nightDate }, this.clock.clockMin); const plan = dollPlan(patron, 'queue'); const key = renderDoll(this, patron, 'queue'); const y = H - 78 + (this.walkers.length % 5) * 6; const root = this.add.container(-40, y); const doll = this.add.image(0, 0, key).setOrigin(0.5, 1); root.add(doll); const stage = drunkStage(patron.intoxication); const label = this.add .text(0, 4, `${patron.archetype} ${patron.age} ${stage !== 'sober' ? stage : ''}`.trim(), { fontFamily: 'monospace', fontSize: '7px', color: stage === 'maggot' || stage === 'messy' ? '#e06060' : '#556', }) .setOrigin(0.5, 0); root.add(label); const card = this.add.container(0, -plan.height - 18); card.setName('card'); const idKey = renderDoll(this, patron, 'idPhoto'); card.add(this.add.rectangle(0, 0, 58, 30, 0xdad4c4).setStrokeStyle(1, 0x555555)); card.add(this.add.image(-22, 0, idKey).setScale(1)); const fakeMark = patron.idCard.fake ? ' ?' : ''; card.add( this.add .text(-8, -11, `${patron.idCard.name}\nID ${idAge(patron, this.clock.nightDate)}${fakeMark}`, { fontFamily: 'monospace', fontSize: '7px', color: '#222', }) .setOrigin(0, 0), ); card.setVisible(this.showIds); root.add(card); root.setDepth(y); this.walkers.push({ patron, root, doll, speed: 26 + (patron.dollSeed % 20) - plan.swayPx * 5 }); } override update(_time: number, deltaMs: number): void { this.clock.update(deltaMs); this.clockText.setText(this.clock.label); this.spawnAccMs += deltaMs; const spawnEvery = 700; while (this.spawnAccMs > spawnEvery && this.walkers.length < 24) { this.spawnAccMs -= spawnEvery; this.spawnWalker(); } for (const w of [...this.walkers]) { w.root.x += (w.speed * deltaMs) / 1000; const sway = dollPlan(w.patron, 'queue').swayPx; if (sway > 0) w.doll.x = Math.sin(w.root.x / (14 - sway * 3)) * sway; if (w.root.x > W + 40) { w.root.destroy(); this.walkers.splice(this.walkers.indexOf(w), 1); } } } }