107 lines
3.5 KiB
TypeScript
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();
|
|
}
|
|
}
|