import Phaser from 'phaser'; import { PALETTE } from '../data/outfits'; // Shared look for every juice widget. Chunky, high contrast, slightly grubby — // this club has not been deep-cleaned since the Olympics. Colours lean on the // doll PALETTE so UI and patrons live in the same world. export const UI = { ink: 0x0b0b12, panel: 0x1a1a24, panelLip: 0x2c2c3a, grime: 0x24242e, paper: PALETTE.paper ?? 0xdad4c4, neonPink: 0xd03470, neonGreen: 0x9fe8a0, kebabAmber: 0xd8a020, danger: PALETTE.red ?? 0xc03434, ok: PALETTE.green ?? 0x2e7d46, dim: 0x556, } as const; export const hex = (c: number): string => `#${c.toString(16).padStart(6, '0')}`; export const font = (size: number, colour: number): Phaser.Types.GameObjects.Text.TextStyle => ({ fontFamily: 'monospace', fontSize: `${size}px`, color: hex(colour), }); /** * A grubby recessed panel: dark fill, lit top lip, dark bottom shadow. Returns a * container positioned at (x, y) with the panel centred on its origin, so * callers can add children in local coordinates. */ export function panel( scene: Phaser.Scene, x: number, y: number, w: number, h: number, fill: number = UI.panel, ): Phaser.GameObjects.Container { const c = scene.add.container(x, y); c.add(scene.add.rectangle(0, 0, w, h, fill).setStrokeStyle(1, UI.ink)); c.add(scene.add.rectangle(0, -h / 2 + 1, w - 2, 1, UI.panelLip, 0.5)); c.add(scene.add.rectangle(0, h / 2 - 1, w - 2, 1, UI.ink, 0.6)); return c; } export interface ChunkyButtonOptions { w?: number; h?: number; fill?: number; textColour?: number; fontSize?: number; onClick: () => void; } /** * Chunky pixel button with a real press: it drops 1px and darkens on pointerdown * and springs back on release. The 1px travel is most of why it feels physical. */ export function chunkyButton( scene: Phaser.Scene, x: number, y: number, label: string, opts: ChunkyButtonOptions, ): Phaser.GameObjects.Container { const w = opts.w ?? 56; const h = opts.h ?? 16; const fill = opts.fill ?? UI.panelLip; const c = scene.add.container(x, y); const face = scene.add.rectangle(0, 0, w, h, fill).setStrokeStyle(1, UI.ink); const shadow = scene.add.rectangle(0, h / 2, w, 2, UI.ink, 0.8); const text = scene.add .text(0, 0, label, font(opts.fontSize ?? 8, opts.textColour ?? UI.paper)) .setOrigin(0.5); c.add([shadow, face, text]); face.setInteractive({ useHandCursor: true }); const press = (down: boolean): void => { face.y = down ? 1 : 0; text.y = down ? 1 : 0; shadow.setAlpha(down ? 0.2 : 0.8); face.setFillStyle(down ? Phaser.Display.Color.ValueToColor(fill).darken(20).color : fill); }; // Phaser fires 'pointerup' on any release over the object, even one whose // 'pointerdown' landed elsewhere. Arm on our own down and disarm the moment the // pointer leaves, so a stray release can't phantom-click (clicker tally is load-bearing). let armed = false; face.on('pointerdown', () => { armed = true; press(true); }); // Leaving the face is the only way to release elsewhere, so this covers the // press-drag-away-release case too. face.on('pointerout', () => { armed = false; press(false); }); face.on('pointerup', () => { const fire = armed; armed = false; press(false); if (fire) opts.onClick(); }); return c; } /** Short, sharp camera shake. Used by the stamp and anything else with weight. */ export function shake(scene: Phaser.Scene, px = 4, ms = 120): void { scene.cameras.main.shake(ms, px / 1000, true); }