not-tonight/src/scenes/door/DressCodeCard.ts

107 lines
3.5 KiB
TypeScript

import Phaser from 'phaser';
import type { DoorDressCodeRule } from '../../rules/dressCode';
import { DOOR_UI } from '../../data/strings/door';
import { DOOR_PALETTE, MONO, panel, text } from './ui';
// The laminated card on the desk. Rules land the moment Dazza's text arrives
// (design §3.1), so the NEW badge is the only warning the player gets that the
// punter they are already looking at just became a violation.
const NEW_BADGE_MS = 12_000;
interface Row {
rule: DoorDressCodeRule;
label: Phaser.GameObjects.Text;
badge: Phaser.GameObjects.Text;
addedAt: number;
/** Rescinded by Dazza (rules cap at ACTIVE_RULE_CAP) — greyed, never judged. */
retired?: boolean;
}
export class DressCodeCard {
private readonly root: Phaser.GameObjects.Container;
private readonly emptyText: Phaser.GameObjects.Text;
private readonly rows: Row[] = [];
private readonly x: number;
private readonly y: number;
constructor(scene: Phaser.Scene, x: number, y: number, w: number, h: number) {
this.x = x;
this.y = y;
this.root = scene.add.container(0, 0).setDepth(50);
this.root.add(panel(scene, x, y, w, h, 0x1a2018, 0x3c4a38));
this.emptyText = text(scene, x + 5, y + 6, DOOR_UI.dressCodeEmpty, 7, DOOR_PALETTE.inkDim);
this.root.add(this.emptyText);
}
add(scene: Phaser.Scene, rule: DoorDressCodeRule, nowMs: number): void {
if (this.rows.some((r) => r.rule.id === rule.id)) return;
this.emptyText.setVisible(false);
const y = this.y + 6 + this.rows.length * 11;
const label = scene.add.text(this.x + 5, y, `· ${rule.short}`, {
fontFamily: MONO,
fontSize: '7px',
color: '#e0e8c8',
wordWrap: { width: 150 },
});
const badge = scene.add.text(this.x + 5, y, 'NEW', {
fontFamily: MONO,
fontSize: '7px',
color: '#ffd050',
});
badge.x = this.x + 5 + label.width + 4;
this.root.add([label, badge]);
// A flash on arrival — the card is peripheral vision, it has to shout once.
scene.tweens.add({ targets: badge, alpha: { from: 1, to: 0.25 }, duration: 400, yoyo: true, repeat: 8 });
this.rows.push({ rule, label, badge, addedAt: nowMs });
}
/** ROOM shuffle: the board rewrites itself wholesale — clear and restate. */
setAll(scene: Phaser.Scene, rules: readonly DoorDressCodeRule[], nowMs: number): void {
for (const r of this.rows) {
r.label.destroy();
r.badge.destroy();
}
this.rows.length = 0;
this.emptyText.setVisible(rules.length === 0);
for (const rule of rules) this.add(scene, rule, nowMs);
}
/** Highlight the rules the patron currently in front of you is breaking. */
highlight(violatedIds: readonly string[]): void {
for (const r of this.rows) {
if (r.retired) continue;
const hit = violatedIds.includes(r.rule.id);
r.label.setColor(hit ? '#ff9090' : '#e0e8c8');
}
}
/** Dazza moved on: grey the rule out. It stays on the card as a monument. */
retire(ruleId: string): void {
const row = this.rows.find((r) => r.rule.id === ruleId && !r.retired);
if (!row) return;
row.retired = true;
row.label.setColor('#4a5a48');
row.label.setText(`- ${row.rule.short}`);
row.badge.setVisible(false);
}
update(nowMs: number): void {
for (const r of this.rows) {
if (r.badge.visible && nowMs - r.addedAt > NEW_BADGE_MS) r.badge.setVisible(false);
}
}
/** The clipboard tab shares this panel's footprint — see ClipboardPanel. */
setVisible(on: boolean): void {
this.root.setVisible(on);
}
destroy(): void {
this.root.destroy();
}
}