import Phaser from 'phaser'; import type { NightState } from '../../data/types'; import { DAZZA_SIGNOFF, FAIL_TEXT, SUMMARY_UI } from '../../data/strings/door'; import { MAX_HEAT_STRIKES, NIGHT_DATES, type NightLog, type RunInfo } from './NightScene'; import { DOOR_PALETTE, MONO, panel, text } from './ui'; // Phase-2: run-aware summary. Still owes Phase 3 the incident-report form and // the cross-night audit. const W = 640; const H = 360; const NIGHT_NAMES = ['thursday', 'friday', 'saturday'] as const; export function nightCash(log: NightLog, state: NightState): number { // Wage plus a hype bonus, minus a strike deduction. Vibe is not paid directly — // Dazza pays for the room feeling busy, not for you being right. const wage = log.wage; const hypeBonus = Math.round((log.hypePeak - 1) * 90); const doorBonus = log.admitted * 2; const strikes = state.heatStrikes.length * 60; return Math.max(0, wage + hypeBonus + doorBonus - strikes) + log.floorCash; } export function signoffFor(vibe: number, strikes: number): string { if (strikes >= 2) return DAZZA_SIGNOFF[0]!; const idx = vibe < 25 ? 0 : vibe < 50 ? 1 : vibe < 75 ? 2 : 3; return DAZZA_SIGNOFF[idx]!; } export class NightSummaryScene extends Phaser.Scene { private log!: NightLog; private state!: NightState; private run: RunInfo | null = null; constructor() { super('NightSummary'); } init(data: { log: NightLog; state: NightState; run?: RunInfo }): void { this.log = data.log; this.state = data.state; this.run = data.run ?? null; } create(): void { const reason = this.log.endReason; const failed = reason !== 'clock'; this.add.rectangle(W / 2, H / 2, W, H, failed ? 0x160a0e : 0x0a0a12); const title = reason !== 'clock' ? FAIL_TEXT[reason].title : SUMMARY_UI.title; const nightName = this.run ? (NIGHT_NAMES[this.run.nightIndex] ?? '') : ''; this.add .text(W / 2, 26, title, { fontFamily: MONO, fontSize: '18px', color: failed ? '#ff8080' : '#f0e4d0', }) .setOrigin(0.5); this.add .text( W / 2, 46, failed ? `${this.run?.venue.name ?? this.state.venueId} · ${nightName} · night over early` : `${SUMMARY_UI.subtitle}${nightName ? ` · ${nightName}` : ''} · shift: ${this.log.role.toLowerCase()} $${this.log.wage}`, { fontFamily: MONO, fontSize: '8px', color: DOOR_PALETTE.inkDim, }, ) .setOrigin(0.5); this.drawVibeGraph(30, 64, 300, 88); this.drawIncidents(30, 158, 300, 82); this.drawStats(354, 64); this.drawDazza(30, 250); const { prompt, next } = this.nextAction(); this.add .text(W / 2, H - 14, `${prompt} · seed ${this.log.seed}`, { fontFamily: MONO, fontSize: '8px', color: this.run?.runOver ? '#ff8080' : DOOR_PALETTE.inkDim, }) .setOrigin(0.5); // The paperwork sits between the night and the next shift (design §3.3). const toReport = (): void => { this.scene.start('IncidentReport', { state: this.state, run: this.run?.game ?? null, seed: this.log.seed, next, }); }; this.input.once('pointerdown', toReport); this.input.keyboard?.once('keydown-SPACE', toReport); } /** What clicking through leads to: the next shift, or a fresh run. */ private nextAction(): { prompt: string; next: () => void } { const r = this.run; if (!r) { return { prompt: SUMMARY_UI.replay, next: () => this.scene.start('Roster', { seed: this.log.seed + 1 }), }; } if (r.runOver) { return { prompt: 'licence gone · click to start a fresh week somewhere that has not heard of you', next: () => this.scene.start('Roster', { freshRun: true, seed: this.log.seed + 13 }), }; } if (r.seasonDone) { return { prompt: 'THE SEASON IS DONE. every door in the valley, held. click for a fresh run', next: () => this.scene.start('Roster', { freshRun: true, seed: this.log.seed + 7 }), }; } if (r.weekDone && r.promotedTo) { const v = r.promotedTo; return { // Dazza's pitch sells the next rung — the ladder is the reward. prompt: `WEEK SURVIVED — PROMOTED: ${v.name.toUpperCase()}. "${v.pitch}"`, next: () => this.scene.start('Roster', {}), }; } const failedNight = this.log.endReason !== 'clock'; const nextName = failedNight ? (NIGHT_NAMES[r.nightIndex] ?? 'the same night') : (NIGHT_NAMES[r.nightIndex + 1] ?? 'the next night'); return { prompt: failedNight ? `click to run ${nextName} back` : `click for ${nextName} · ${NIGHT_DATES[r.nightIndex + 1] ?? ''}`, next: () => this.scene.start('Roster', {}), }; } private drawVibeGraph(x: number, y: number, w: number, h: number): void { panel(this, x, y, w, h, 0x11111a, 0x2a2a38); text(this, x + 5, y + 4, 'VIBE ACROSS THE NIGHT', 7, DOOR_PALETTE.inkDim); // 50 is the line you started on — crossing below it is the story of the night const mid = y + h - 14 - (h - 34) * 0.5; this.add.rectangle(x + w / 2, mid, w - 10, 1, 0x33333f); const pts = this.log.vibeHistory; if (pts.length < 2) return; const g = this.add.graphics(); g.lineStyle(2, DOOR_PALETTE.greenHot, 1); g.beginPath(); pts.forEach((v, i) => { const px = x + 5 + ((w - 10) * i) / (pts.length - 1); const py = y + h - 14 - ((h - 34) * Math.max(0, Math.min(100, v))) / 100; if (i === 0) g.moveTo(px, py); else g.lineTo(px, py); }); g.strokePath(); text(this, x + 5, y + h - 12, '9PM', 7, DOOR_PALETTE.inkDim); text(this, x + w - 30, y + h - 12, '3AM', 7, DOOR_PALETTE.inkDim); } private drawStats(x: number, y: number): void { const w = 256; panel(this, x, y, w, 168, 0x11111a, 0x2a2a38); const cash = nightCash(this.log, this.state); const rows: Array<[string, string, string?]> = [ ['admitted', String(this.log.admitted)], ['knocked back', String(this.log.denied)], ['sobriety tests', String(this.log.tested)], ['pat-downs', String(this.log.pattedDown)], ['peak hype', `x${this.log.hypePeak.toFixed(1)}`], ['final vibe', String(Math.round(this.state.vibe))], ['final aggro', String(Math.round(this.state.aggro))], [ 'clicker vs reality', `${this.state.capacity.clickerShown} vs ${this.state.capacity.inside}`, this.state.capacity.clickerShown === this.state.capacity.inside ? undefined : '#ffc060', ], [ 'heat strikes', `${this.state.heatStrikes.length}/3`, this.state.heatStrikes.length > 0 ? '#ff8080' : undefined, ], ['floor score', `$${this.log.floorCash}`, this.log.floorCash > 0 ? '#f0d060' : undefined], ]; rows.forEach(([label, value, colour], i) => { const ry = y + 10 + i * 13; text(this, x + 8, ry, label, 8, DOOR_PALETTE.inkDim); this.add .text(x + w - 8, ry, value, { fontFamily: MONO, fontSize: '8px', color: colour ?? DOOR_PALETTE.inkHot, }) .setOrigin(1, 0); }); this.add.rectangle(x + w / 2, y + 138, w - 16, 1, 0x2a2a38); text(this, x + 8, y + 145, 'PAY', 9, '#d0b060'); this.add .text(x + w - 8, y + 143, `$${cash}`, { fontFamily: MONO, fontSize: '14px', color: '#f0d060' }) .setOrigin(1, 0); } /** * The strikes spelled out. A count alone tells you the run is over; the * reasons tell you which call did it, which is the only way the next night is * played differently. Lives in its own panel because the reasons are long * sentences and used to run straight off the right edge of the stats table. */ private drawIncidents(x: number, y: number, w: number, h: number): void { panel(this, x, y, w, h, 0x11111a, 0x2a2a38); const strikes = this.state.heatStrikes; text(this, x + 5, y + 4, strikes.length > 0 ? 'THE INSPECTOR WROTE UP' : 'NOTHING ON THE RECORD', 7, strikes.length > 0 ? '#ff8080' : DOOR_PALETTE.inkDim); if (strikes.length === 0) { this.add.text(x + 5, y + 18, 'a clean sheet. no strikes, no questions.\nthe licence survives to friday.', { fontFamily: MONO, fontSize: '7px', color: DOOR_PALETTE.inkDim, wordWrap: { width: w - 12 }, lineSpacing: 2, }); return; } let ry = y + 16; for (const s of strikes.slice(0, MAX_HEAT_STRIKES)) { const line = this.add.text(x + 5, ry, `· ${s.reason}`, { fontFamily: MONO, fontSize: '7px', color: '#ff9090', wordWrap: { width: w - 12 }, }); ry += line.height + 3; } } private drawDazza(x: number, y: number): void { const w = W - 60; panel(this, x, y, w, 74, 0x0d1116, 0x2c4a38); text(this, x + 8, y + 6, 'DAZZA', 8, '#7de08a'); const reason = this.log.endReason; const body = reason !== 'clock' ? FAIL_TEXT[reason].dazza : signoffFor(this.state.vibe, this.state.heatStrikes.length); this.add.text(x + 8, y + 20, body, { fontFamily: MONO, fontSize: '9px', color: reason !== 'clock' ? '#ffb0b0' : '#bfe8c8', wordWrap: { width: w - 16 }, }); if (this.state.heatStrikes.length >= 3) { this.add.text(x + 8, y + 56, 'THREE STRIKES. THE LICENCE IS GONE. RUN OVER.', { fontFamily: MONO, fontSize: '9px', color: '#ff6060', }); } } }