Spaces:
Running on Zero
Running on Zero
| /** | |
| * ============================================================================ | |
| * 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); | |
| }); | |
| } | |
| } | |