mafia / frontend /src /ui /DialogueBox.ts
Alfaxad's picture
Migrate Mafia game to ZeroGPU Gradio Space
c670567 verified
Raw
History Blame Contribute Delete
6.77 kB
/**
* ============================================================================
* DIALOGUE BOX - Text display with typewriter effect
* ============================================================================
*
* A Phaser Container that displays dialogue text with:
* - Typewriter (character-by-character) text reveal
* - Speaker name label
* - Background panel (solid color rectangle)
* - Click/Enter to complete or advance
* - Configurable text speed, font, colors
*
* EVENTS:
* - 'typeComplete': () => void -- typewriter finished current text
* - 'advance': () => void -- player clicked to advance
* - 'skip': () => void -- player skipped typewriter
*
* USAGE:
* const box = new DialogueBox(scene, { x: 512, y: 650, width: 900, height: 150 });
* box.showText('Alaric', 'Hello there!');
* box.on('typeComplete', () => { ... });
* // On click: box.handleInput(); // completes or advances
*/
import Phaser from 'phaser';
export interface DialogueBoxConfig {
/** X position (center) */
x: number;
/** Y position (center) */
y: number;
/** Box width */
width: number;
/** Box height */
height: number;
/** Background texture key (optional, uses solid color if not set) */
backgroundKey?: string;
/** Background color (hex, default: 0x000000) */
backgroundColor?: number;
/** Background alpha (default: 0.8) */
backgroundAlpha?: number;
/** Text style overrides */
textStyle?: Phaser.Types.GameObjects.Text.TextStyle;
/** Speaker name style overrides */
nameStyle?: Phaser.Types.GameObjects.Text.TextStyle;
/** Typewriter speed in ms per character (default: 30) */
typeSpeed?: number;
/** Padding inside the box */
padding?: number;
}
export class DialogueBox extends Phaser.GameObjects.Container {
private boxConfig: DialogueBoxConfig;
private background!: Phaser.GameObjects.Rectangle | Phaser.GameObjects.Image;
private nameText!: Phaser.GameObjects.Text;
private bodyText!: Phaser.GameObjects.Text;
private continueIndicator!: Phaser.GameObjects.Text;
private typeTimer?: Phaser.Time.TimerEvent;
private fullText: string = '';
private currentCharIndex: number = 0;
private isTyping: boolean = false;
constructor(scene: Phaser.Scene, config: DialogueBoxConfig) {
super(scene, 0, 0);
this.boxConfig = config;
scene.add.existing(this);
this.setDepth(100);
this.createElements();
}
// -- Public API --
/** Show text with typewriter effect. */
showText(speaker: string, text: string): void {
this.setVisible(true);
// Set speaker name
if (speaker && speaker !== 'narrator') {
this.nameText.setText(speaker);
this.nameText.setVisible(true);
} else {
this.nameText.setText('');
this.nameText.setVisible(false);
}
// Start typewriter
this.fullText = text;
this.currentCharIndex = 0;
this.bodyText.setText('');
this.continueIndicator.setVisible(false);
this.isTyping = true;
this.startTypewriter(text);
}
/** Handle player input (complete typewriter or signal advance). */
handleInput(): void {
if (this.isTyping) {
this.completeTypewriter();
} else {
this.emit('advance');
}
}
/** Complete typewriter immediately (show full text). */
completeTypewriter(): void {
if (this.typeTimer) {
this.typeTimer.destroy();
this.typeTimer = undefined;
}
this.bodyText.setText(this.fullText);
this.isTyping = false;
this.continueIndicator.setVisible(true);
this.emit('typeComplete');
}
/** Show/hide the dialogue box. */
setBoxVisible(visible: boolean): void {
this.setVisible(visible);
}
/** Check if typewriter is currently animating. */
getIsTyping(): boolean {
return this.isTyping;
}
// -- Internal --
private createElements(): void {
const cfg = this.boxConfig;
const pad = cfg.padding ?? 20;
// Background
if (cfg.backgroundKey && this.scene.textures.exists(cfg.backgroundKey)) {
this.background = this.scene.add.image(cfg.x, cfg.y, cfg.backgroundKey);
this.background.setDisplaySize(cfg.width, cfg.height);
} else {
this.background = this.scene.add.rectangle(
cfg.x,
cfg.y,
cfg.width,
cfg.height,
cfg.backgroundColor ?? 0x000000,
cfg.backgroundAlpha ?? 0.8,
);
}
this.background.setOrigin(0.5);
this.add(this.background);
// Add border
const border = this.scene.add.rectangle(
cfg.x,
cfg.y,
cfg.width + 4,
cfg.height + 4,
);
border.setStrokeStyle(2, 0x888888);
border.setFillStyle(0x000000, 0);
border.setOrigin(0.5);
border.setDepth(-1);
this.add(border);
// Speaker name
const nameLeft = cfg.x - cfg.width / 2 + pad;
const nameTop = cfg.y - cfg.height / 2 + pad;
this.nameText = this.scene.add.text(nameLeft, nameTop, '', {
fontSize: '20px',
fontFamily: 'Arial',
color: '#C8A2C8',
fontStyle: 'bold',
...(cfg.nameStyle ?? {}),
});
this.add(this.nameText);
// Body text
const textTop = nameTop + 28;
const textWidth = cfg.width - pad * 2;
this.bodyText = this.scene.add.text(nameLeft, textTop, '', {
fontSize: '18px',
fontFamily: 'Arial',
color: '#E8D8A0',
wordWrap: { width: textWidth },
lineSpacing: 4,
...(cfg.textStyle ?? {}),
});
this.add(this.bodyText);
// Continue indicator (blinking triangle)
const indicatorX = cfg.x + cfg.width / 2 - pad;
const indicatorY = cfg.y + cfg.height / 2 - pad;
this.continueIndicator = this.scene.add
.text(indicatorX, indicatorY, '\u25BC', {
fontSize: '16px',
color: '#ffffff',
})
.setOrigin(1, 1);
this.continueIndicator.setVisible(false);
this.add(this.continueIndicator);
// Blink animation for indicator
this.scene.tweens.add({
targets: this.continueIndicator,
alpha: { from: 1, to: 0.3 },
duration: 500,
yoyo: true,
repeat: -1,
});
}
private startTypewriter(text: string): void {
const speed = this.boxConfig.typeSpeed ?? 30;
this.currentCharIndex = 0;
this.typeTimer = this.scene.time.addEvent({
delay: speed,
callback: () => {
this.currentCharIndex++;
this.bodyText.setText(text.substring(0, this.currentCharIndex));
if (this.currentCharIndex >= text.length) {
this.isTyping = false;
this.continueIndicator.setVisible(true);
if (this.typeTimer) {
this.typeTimer.destroy();
this.typeTimer = undefined;
}
this.emit('typeComplete');
}
},
repeat: text.length - 1,
});
}
}