boardgame-phaser/packages/sample-game/src/scenes/GameScene.ts

188 lines
5.0 KiB
TypeScript
Raw Normal View History

2026-04-03 15:18:47 +08:00
import Phaser from 'phaser';
2026-04-03 16:18:44 +08:00
import type { TicTacToeState, TicTacToePart, PlayerType } from '@/game/tic-tac-toe';
import { isCellOccupied } from 'boardgame-core';
2026-04-03 15:18:47 +08:00
import { ReactiveScene, bindRegion, createInputMapper, createPromptHandler } from 'boardgame-phaser';
import type { PromptEvent, MutableSignal, IGameContext } from 'boardgame-core';
const CELL_SIZE = 120;
const BOARD_OFFSET = { x: 100, y: 100 };
const BOARD_SIZE = 3;
export interface GameSceneData {
state: MutableSignal<TicTacToeState>;
commands: IGameContext<TicTacToeState>['commands'];
}
export class GameScene extends ReactiveScene<TicTacToeState> {
private boardContainer!: Phaser.GameObjects.Container;
private gridGraphics!: Phaser.GameObjects.Graphics;
private inputMapper!: ReturnType<typeof createInputMapper<TicTacToeState>>;
private promptHandler!: ReturnType<typeof createPromptHandler<TicTacToeState>>;
private activePrompt: PromptEvent | null = null;
private turnText!: Phaser.GameObjects.Text;
constructor() {
super('GameScene');
}
init(data: GameSceneData): void {
this.state = data.state;
this.commands = data.commands;
}
protected onStateReady(_state: TicTacToeState): void {
}
create(): void {
2026-04-03 16:18:44 +08:00
super.create();
2026-04-03 15:18:47 +08:00
this.boardContainer = this.add.container(0, 0);
this.gridGraphics = this.add.graphics();
this.drawGrid();
this.watch(() => {
const winner = this.state.value.winner;
if (winner) {
this.showWinner(winner);
}
});
this.watch(() => {
const currentPlayer = this.state.value.currentPlayer;
this.updateTurnText(currentPlayer);
});
this.setupInput();
}
protected setupBindings(): void {
2026-04-03 16:18:44 +08:00
bindRegion<TicTacToeState, { player: PlayerType }>(
this.state,
(state) => state.parts,
2026-04-03 15:18:47 +08:00
this.state.value.board,
{
cellSize: { x: CELL_SIZE, y: CELL_SIZE },
offset: BOARD_OFFSET,
2026-04-03 16:18:44 +08:00
factory: (part, pos: Phaser.Math.Vector2) => {
2026-04-03 15:18:47 +08:00
const text = this.add.text(pos.x + CELL_SIZE / 2, pos.y + CELL_SIZE / 2, part.player, {
fontSize: '64px',
fontFamily: 'Arial',
color: part.player === 'X' ? '#3b82f6' : '#ef4444',
}).setOrigin(0.5);
return text;
},
},
this.boardContainer,
);
}
private setupInput(): void {
this.inputMapper = createInputMapper(this, this.commands);
this.inputMapper.mapGridClick(
{ x: CELL_SIZE, y: CELL_SIZE },
BOARD_OFFSET,
{ cols: BOARD_SIZE, rows: BOARD_SIZE },
(col, row) => {
if (this.state.value.winner) return null;
const currentPlayer = this.state.value.currentPlayer;
2026-04-03 16:18:44 +08:00
if (isCellOccupied(this.state.value.parts, 'board', [row, col])) return null;
2026-04-03 15:18:47 +08:00
return `play ${currentPlayer} ${row} ${col}`;
},
);
this.promptHandler = createPromptHandler(this, this.commands, {
onPrompt: (prompt) => {
this.activePrompt = prompt;
},
onSubmit: (input) => {
if (this.activePrompt) {
return this.activePrompt.tryCommit(input);
}
return null;
},
onCancel: () => {
this.activePrompt = null;
},
});
this.promptHandler.start();
}
private drawGrid(): void {
const g = this.gridGraphics;
g.lineStyle(3, 0x6b7280);
for (let i = 1; i < BOARD_SIZE; i++) {
g.lineBetween(
BOARD_OFFSET.x + i * CELL_SIZE,
BOARD_OFFSET.y,
BOARD_OFFSET.x + i * CELL_SIZE,
BOARD_OFFSET.y + BOARD_SIZE * CELL_SIZE,
);
g.lineBetween(
BOARD_OFFSET.x,
BOARD_OFFSET.y + i * CELL_SIZE,
BOARD_OFFSET.x + BOARD_SIZE * CELL_SIZE,
BOARD_OFFSET.y + i * CELL_SIZE,
);
}
g.strokePath();
this.add.text(BOARD_OFFSET.x + (BOARD_SIZE * CELL_SIZE) / 2, BOARD_OFFSET.y - 40, 'Tic-Tac-Toe', {
fontSize: '28px',
fontFamily: 'Arial',
color: '#1f2937',
}).setOrigin(0.5);
this.turnText = this.add.text(BOARD_OFFSET.x + (BOARD_SIZE * CELL_SIZE) / 2, BOARD_OFFSET.y + BOARD_SIZE * CELL_SIZE + 20, '', {
fontSize: '20px',
fontFamily: 'Arial',
color: '#4b5563',
}).setOrigin(0.5);
this.updateTurnText(this.state.value.currentPlayer);
}
private updateTurnText(player: string): void {
if (this.turnText) {
this.turnText.setText(`${player}'s turn`);
}
}
private showWinner(winner: string): void {
const text = winner === 'draw' ? "It's a draw!" : `${winner} wins!`;
this.add.rectangle(
BOARD_OFFSET.x + (BOARD_SIZE * CELL_SIZE) / 2,
BOARD_OFFSET.y + (BOARD_SIZE * CELL_SIZE) / 2,
BOARD_SIZE * CELL_SIZE,
BOARD_SIZE * CELL_SIZE,
0x000000,
0.6,
);
const winText = this.add.text(
BOARD_OFFSET.x + (BOARD_SIZE * CELL_SIZE) / 2,
BOARD_OFFSET.y + (BOARD_SIZE * CELL_SIZE) / 2,
text,
{
fontSize: '36px',
fontFamily: 'Arial',
color: '#fbbf24',
},
).setOrigin(0.5);
this.tweens.add({
targets: winText,
scale: 1.2,
duration: 500,
yoyo: true,
repeat: 1,
});
}
}