import Phaser from 'phaser'; import type { TicTacToeState, TicTacToePart, PlayerType } from '@/game/tic-tac-toe'; import { isCellOccupied } from 'boardgame-core'; import { ReactiveScene, bindRegion, createInputMapper, createPromptHandler } from 'boardgame-phaser'; import type { PromptEvent } from 'boardgame-core'; const CELL_SIZE = 120; const BOARD_OFFSET = { x: 100, y: 100 }; const BOARD_SIZE = 3; export class GameScene extends ReactiveScene { private boardContainer!: Phaser.GameObjects.Container; private gridGraphics!: Phaser.GameObjects.Graphics; private inputMapper!: ReturnType>; private promptHandler!: ReturnType>; private activePrompt: PromptEvent | null = null; private turnText!: Phaser.GameObjects.Text; constructor() { super('GameScene'); } protected onStateReady(_state: TicTacToeState): void { } create(): void { super.create(); 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 { bindRegion( this.state, (state) => state.parts, this.state.value.board, { cellSize: { x: CELL_SIZE, y: CELL_SIZE }, offset: BOARD_OFFSET, factory: (part, pos: Phaser.Math.Vector2) => { 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; }, update: (part, obj) => { // 可以在这里更新部件的视觉状态 }, }, 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; if (isCellOccupied(this.state.value.parts, 'board', [row, col])) return null; return `play ${currentPlayer} ${row} ${col}`; }, ); this.promptHandler = createPromptHandler(this, this.commands, { onPrompt: (prompt) => { this.activePrompt = prompt; }, 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, }); } }