import Phaser from 'phaser'; import { renderDoll } from '../../patrons/doll'; import { dollPlan } from '../../patrons/dollPlan'; import { drunkStage } from '../../rules/drunk'; import type { Patron } from '../../data/types'; import { CLAIM_LINES, DRUNK_GREETINGS, PATRON_GREETINGS } from '../../data/strings/door'; import { MONO } from './ui'; import { ZONE_BANDS, ZONE_ORDER, describeZone, greetingIndex, type InspectZone } from './inspect'; // The patron at the rope, three times life size so the outfit actually reads. // Hover zones map 1:1 onto the doll's y bands — you look at the shoes by // looking at the shoes. const SCALE = 3; const DOLL_W = 32; const DOLL_H = 48; export class PatronUpView { private root: Phaser.GameObjects.Container | null = null; private doll: Phaser.GameObjects.Image | null = null; private tooltip: Phaser.GameObjects.Container | null = null; private patron: Patron | null = null; private swayT = 0; private swayPx = 0; constructor( private readonly scene: Phaser.Scene, private readonly x: number, /** y of the patron's FEET */ private readonly footY: number, ) {} get current(): Patron | null { return this.patron; } show(p: Patron): void { this.clear(); this.patron = p; const { scene } = this; const root = scene.add.container(this.x, this.footY).setDepth(300); this.root = root; // a puddle-ish shadow so they are standing ON something root.add(scene.add.ellipse(0, 2, DOLL_W * SCALE * 0.6, 10, 0x000000, 0.45)); const key = renderDoll(scene, p, 'queue'); const doll = scene.add.image(0, 0, key).setOrigin(0.5, 1).setScale(SCALE); this.doll = doll; this.swayPx = dollPlan(p, 'queue').swayPx; root.add(doll); // hover zones, in doll-local coordinates scaled up for (const zone of ZONE_ORDER) { const band = ZONE_BANDS[zone]; const h = (band.y1 - band.y0) * SCALE; const cy = -DOLL_H * SCALE + (band.y0 + (band.y1 - band.y0) / 2) * SCALE; const hit = scene.add .rectangle(0, cy, DOLL_W * SCALE * 0.8, h, 0xffffff, 0) .setInteractive({ useHandCursor: true }); hit.on('pointerover', () => this.showTooltip(zone, cy)); hit.on('pointerout', () => this.hideTooltip()); root.add(hit); } this.speak(p); // step-up: they walk in from the queue side and settle root.x = this.x - 90; root.setAlpha(0); scene.tweens.add({ targets: root, x: this.x, alpha: 1, duration: 380, ease: 'Quad.easeOut' }); } private speak(p: Patron): void { if (!this.root) return; const { scene } = this; const drunk = drunkStage(p.intoxication); const pool = p.flags.claimsGuestList || p.flags.knowsOwner ? CLAIM_LINES : drunk === 'messy' || drunk === 'maggot' || drunk === 'loose' ? DRUNK_GREETINGS : PATRON_GREETINGS; const line = pool[greetingIndex(p, pool.length)] ?? PATRON_GREETINGS[0]!; const bubbleY = -DOLL_H * SCALE - 16; const label = scene.add .text(0, bubbleY, `"${line}"`, { fontFamily: MONO, fontSize: '8px', color: '#d8d0c0', align: 'center', wordWrap: { width: 150 }, }) .setOrigin(0.5, 1); const bg = scene.add .rectangle(0, bubbleY - label.height / 2 + 1, label.width + 10, label.height + 6, 0x14121a, 0.85) .setStrokeStyle(1, 0x3a3448); this.root.add([bg, label]); scene.tweens.add({ targets: [bg, label], alpha: 0, delay: 3600, duration: 700 }); } private showTooltip(zone: InspectZone, localY: number): void { this.hideTooltip(); if (!this.root || !this.patron) return; const { scene } = this; const lines = describeZone(this.patron, zone); const c = scene.add.container(56, localY).setDepth(310); const label = scene.add .text(6, 0, lines.join('\n'), { fontFamily: MONO, fontSize: '7px', color: '#f0e4d0', wordWrap: { width: 120 }, }) .setOrigin(0, 0.5); const bg = scene.add .rectangle(0, 0, label.width + 14, label.height + 8, 0x0d0b12, 0.94) .setOrigin(0, 0.5) .setStrokeStyle(1, 0x5a4a68); c.add([bg, label]); this.root.add(c); this.tooltip = c; } private hideTooltip(): void { this.tooltip?.destroy(); this.tooltip = null; } /** Called every frame — the sway is the drunk tell you watch for. */ update(deltaMs: number): void { if (!this.doll || this.swayPx <= 0) return; this.swayT += deltaMs / 1000; const speed = 2.2 - this.swayPx * 0.35; this.doll.x = Math.sin(this.swayT * speed) * this.swayPx * SCALE * 0.5; this.doll.setAngle(Math.sin(this.swayT * speed * 0.7) * this.swayPx * 0.9); } /** They walk off left after a denial: the defeated slouch. */ walkOffDenied(onDone?: () => void): void { const root = this.root; this.patron = null; this.hideTooltip(); if (!root) { onDone?.(); return; } this.root = null; this.doll?.setAngle(9); // the slouch this.scene.tweens.add({ targets: root, x: root.x - 170, y: root.y + 6, alpha: 0, duration: 900, ease: 'Quad.easeIn', onComplete: () => { root.destroy(); onDone?.(); }, }); } /** They walk right, into the venue. */ walkInAdmitted(doorX: number, onDone?: () => void): void { const root = this.root; this.patron = null; this.hideTooltip(); if (!root) { onDone?.(); return; } this.root = null; this.scene.tweens.add({ targets: root, x: doorX, y: root.y - 26, scale: 0.55, alpha: 0, duration: 820, ease: 'Quad.easeIn', onComplete: () => { root.destroy(); onDone?.(); }, }); } clear(): void { this.hideTooltip(); this.root?.destroy(); this.root = null; this.doll = null; this.patron = null; this.swayT = 0; } }