import Phaser from 'phaser'; import type {TicTacToeState, TicTacToePart} from '@/game/tic-tac-toe'; import {ReadonlySignal} from "@preact/signals"; import {GameHostScene} from "@/scenes/GameHostScene"; import {spawnEffect, Spawner} from "@/utils/spawner"; const CELL_SIZE = 120; const BOARD_OFFSET = { x: 100, y: 100 }; const BOARD_SIZE = 3; export class GameScene extends GameHostScene { private boardContainer!: Phaser.GameObjects.Container; private gridGraphics!: Phaser.GameObjects.Graphics; private turnText!: Phaser.GameObjects.Text; private winnerOverlay?: Phaser.GameObjects.Container; constructor() { super('GameScene'); } create(): void { super.create(); this.boardContainer = this.add.container(0, 0); this.gridGraphics = this.add.graphics(); this.drawGrid(); this.disposables.add(spawnEffect(new TicTacToePartSpawner(this, this.gameHost.state))); this.watch(() => { const winner = this.state.winner; if (winner) { this.showWinner(winner); } }); this.watch(() => { const currentPlayer = this.state.currentPlayer; this.updateTurnText(currentPlayer); }); this.setupInput(); } private isCellOccupied(row: number, col: number): boolean { return !!this.state.board.partMap[`${row},${col}`]; } private setupInput(): void { // todo } 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.currentPlayer); } private updateTurnText(player: string): void { if (this.turnText) { this.turnText.setText(`${player}'s turn`); } } private showWinner(winner: string): void { // 清理旧的覆盖层防止叠加 if (this.winnerOverlay) { this.winnerOverlay.destroy(); } this.winnerOverlay = this.add.container(); const text = winner === 'draw' ? "It's a draw!" : `${winner} wins!`; this.winnerOverlay.add( 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.winnerOverlay.add(winText); this.tweens.add({ targets: winText, scale: 1.2, duration: 500, yoyo: true, repeat: 1, }); } } class TicTacToePartSpawner implements Spawner { constructor(public readonly scene: GameScene, public readonly state: ReadonlySignal) {} *getData() { for (const part of Object.values(this.state.value.parts)) { yield part; } } getKey(part: TicTacToePart): string { return part.id; } onUpdate(part: TicTacToePart, obj: Phaser.GameObjects.Text): void { const [xIndex, yIndex] = part.position; const x = xIndex * CELL_SIZE + BOARD_OFFSET.x; const y = yIndex * CELL_SIZE + BOARD_OFFSET.y; obj.x = x; obj.y = y; } onSpawn(part: TicTacToePart) { const [xIndex, yIndex] = part.position; const x = xIndex * CELL_SIZE + BOARD_OFFSET.x; const y = yIndex * CELL_SIZE + BOARD_OFFSET.y; const text = this.scene.add.text(x + CELL_SIZE / 2, y + CELL_SIZE / 2, part.player, { fontSize: '64px', fontFamily: 'Arial', color: part.player === 'X' ? '#3b82f6' : '#ef4444', }).setOrigin(0.5); // 添加落子动画 text.setScale(0); this.scene.tweens.add({ targets: text, scale: 1, duration: 200, ease: 'Back.easeOut', }); return text; } onDespawn(obj: Phaser.GameObjects.Text) { obj.removedFromScene(); } }