mafia / frontend /src /ui /ChoicePanel.ts
Alfaxad's picture
Migrate Mafia game to ZeroGPU Gradio Space
c670567 verified
Raw
History Blame Contribute Delete
7.59 kB
/**
* ============================================================================
* CHOICE PANEL - Branching choice buttons display
* ============================================================================
*
* Displays a set of choice buttons for player decision points.
* Used by both dialogue choices (BaseChapterScene) and quiz answers.
*
* FEATURES:
* - Vertical button layout
* - Hover highlight effects
* - Entrance animations (fade)
* - Keyboard navigation (Up/Down/Enter)
* - Dynamic button count
*
* EVENTS:
* - 'selected': (optionIndex: number, optionText: string) => void
*
* USAGE:
* const panel = new ChoicePanel(scene, 512, 400);
* panel.showChoices('What do you do?', [
* { text: 'Fight', enabled: true },
* { text: 'Run', enabled: true },
* ]);
* panel.on('selected', (index, text) => { ... });
*/
import Phaser from 'phaser';
export interface ChoiceDisplayOption {
/** Button text */
text: string;
/** Whether this option is clickable (default: true) */
enabled?: boolean;
/** Optional icon key */
iconKey?: string;
}
export interface ChoicePanelConfig {
/** Layout direction */
layout?: 'vertical' | 'horizontal';
/** Space between buttons (px) */
spacing?: number;
/** Button width */
buttonWidth?: number;
/** Button height */
buttonHeight?: number;
/** Button background color */
buttonColor?: number;
/** Button hover color */
buttonHoverColor?: number;
/** Text style */
textStyle?: Phaser.Types.GameObjects.Text.TextStyle;
/** Enable keyboard navigation (default: true) */
keyboardNav?: boolean;
}
export class ChoicePanel extends Phaser.GameObjects.Container {
private panelConfig: ChoicePanelConfig;
private buttons: Phaser.GameObjects.Container[] = [];
private currentOptions: ChoiceDisplayOption[] = [];
private selectedIndex: number = 0;
private promptText?: Phaser.GameObjects.Text;
private upKey?: Phaser.Input.Keyboard.Key;
private downKey?: Phaser.Input.Keyboard.Key;
private enterKey?: Phaser.Input.Keyboard.Key;
constructor(
scene: Phaser.Scene,
x: number,
y: number,
config?: ChoicePanelConfig,
) {
super(scene, x, y);
this.panelConfig = config ?? {};
scene.add.existing(this);
this.setDepth(200);
}
// -- Public API --
/** Show choice buttons with optional prompt text. */
showChoices(prompt: string, options: ChoiceDisplayOption[]): void {
this.clearButtons();
this.currentOptions = options;
this.setVisible(true);
const btnWidth = this.panelConfig.buttonWidth ?? 400;
const btnHeight = this.panelConfig.buttonHeight ?? 50;
const spacing = this.panelConfig.spacing ?? 15;
const btnColor = this.panelConfig.buttonColor ?? 0x333355;
const hoverColor = this.panelConfig.buttonHoverColor ?? 0x5555aa;
// Prompt text
if (prompt) {
this.promptText = this.scene.add
.text(0, 0, prompt, {
fontSize: '22px',
fontFamily: 'Arial',
color: '#ffffff',
fontStyle: 'bold',
stroke: '#000000',
strokeThickness: 3,
...(this.panelConfig.textStyle ?? {}),
})
.setOrigin(0.5, 0.5);
this.add(this.promptText);
}
// Create buttons
const startY = prompt ? 40 : 0;
options.forEach((option, index) => {
const yOffset = startY + index * (btnHeight + spacing);
const btn = this.createButton(
option,
index,
yOffset,
btnWidth,
btnHeight,
btnColor,
hoverColor,
);
this.buttons.push(btn);
this.add(btn);
});
// Keyboard navigation
if (this.panelConfig.keyboardNav !== false) {
this.setupKeyboardNav();
}
// Highlight first option
this.selectedIndex = 0;
this.highlightButton(0);
// Fade in
this.setAlpha(0);
this.scene.tweens.add({
targets: this,
alpha: 1,
duration: 200,
});
}
/** Hide and remove all buttons. */
hide(): void {
this.clearButtons();
this.setVisible(false);
}
/** Programmatically select an option (for keyboard navigation). */
selectByIndex(index: number): void {
if (index >= 0 && index < this.buttons.length) {
const text = this.currentOptions[index]?.text ?? '';
this.emit('selected', index, text);
}
}
// -- Internal --
private clearButtons(): void {
this.buttons.forEach((b) => b.destroy());
this.buttons = [];
this.currentOptions = [];
if (this.promptText) {
this.promptText.destroy();
this.promptText = undefined;
}
// Clean up keyboard
this.upKey?.destroy();
this.downKey?.destroy();
this.enterKey?.destroy();
this.upKey = undefined;
this.downKey = undefined;
this.enterKey = undefined;
}
private createButton(
option: ChoiceDisplayOption,
index: number,
yOffset: number,
width: number,
height: number,
bgColor: number,
hoverColor: number,
): Phaser.GameObjects.Container {
const container = this.scene.add.container(0, yOffset);
const enabled = option.enabled !== false;
// Background rectangle
const bg = this.scene.add.rectangle(
0,
0,
width,
height,
bgColor,
enabled ? 0.9 : 0.4,
);
bg.setOrigin(0.5);
bg.setStrokeStyle(2, 0x888888);
container.add(bg);
// Text
const text = this.scene.add
.text(0, 0, option.text, {
fontSize: '18px',
fontFamily: 'Arial',
color: enabled ? '#ffffff' : '#888888',
...(this.panelConfig.textStyle ?? {}),
})
.setOrigin(0.5);
container.add(text);
if (enabled) {
// Make interactive
bg.setInteractive({ useHandCursor: true });
bg.on('pointerover', () => {
bg.setFillStyle(hoverColor, 1);
this.selectedIndex = index;
this.highlightButton(index);
});
bg.on('pointerout', () => {
bg.setFillStyle(bgColor, 0.9);
});
bg.on('pointerdown', () => {
this.emit('selected', index, option.text);
});
}
// Store reference for highlighting
(container as any)._bg = bg;
(container as any)._defaultColor = bgColor;
(container as any)._hoverColor = hoverColor;
return container;
}
private highlightButton(index: number): void {
this.buttons.forEach((btn, i) => {
const bg = (btn as any)._bg as Phaser.GameObjects.Rectangle;
const defaultColor = (btn as any)._defaultColor as number;
const hoverColor = (btn as any)._hoverColor as number;
if (bg) {
bg.setFillStyle(i === index ? hoverColor : defaultColor, 0.9);
}
});
}
private setupKeyboardNav(): void {
this.upKey = this.scene.input.keyboard?.addKey(
Phaser.Input.Keyboard.KeyCodes.UP,
);
this.downKey = this.scene.input.keyboard?.addKey(
Phaser.Input.Keyboard.KeyCodes.DOWN,
);
this.enterKey = this.scene.input.keyboard?.addKey(
Phaser.Input.Keyboard.KeyCodes.ENTER,
);
this.upKey?.on('down', () => {
this.selectedIndex = Math.max(0, this.selectedIndex - 1);
this.highlightButton(this.selectedIndex);
});
this.downKey?.on('down', () => {
this.selectedIndex = Math.min(
this.buttons.length - 1,
this.selectedIndex + 1,
);
this.highlightButton(this.selectedIndex);
});
this.enterKey?.on('down', () => {
const text = this.currentOptions[this.selectedIndex]?.text ?? '';
this.emit('selected', this.selectedIndex, text);
});
}
}