import Phaser from 'phaser'; import type { OnitamaState, Pawn } from '@/game/onitama'; import {getAvailableMoves, prompts} from '@/game/onitama'; import { GameHostScene } from 'boardgame-phaser'; import { spawnEffect } from 'boardgame-phaser'; import type { MutableSignal } from 'boardgame-core'; import { PawnSpawner, CardSpawner, BOARD_OFFSET, CELL_SIZE, CARD_WIDTH, CARD_HEIGHT, boardToScreen, BOARD_SIZE, HighlightSpawner } from '@/spawners'; import type { HighlightData } from '@/spawners/HighlightSpawner'; import {createUIState, clearSelection, selectPiece, selectCard, createValidMoves} from '@/state'; import type { OnitamaUIState, ValidMove } from '@/state'; export class OnitamaScene extends GameHostScene { private boardContainer!: Phaser.GameObjects.Container; private gridGraphics!: Phaser.GameObjects.Graphics; private infoText!: Phaser.GameObjects.Text; private winnerOverlay?: Phaser.GameObjects.Container; private cardLabelContainers: Map = new Map(); private menuButtonContainer!: Phaser.GameObjects.Container; private menuButtonBg!: Phaser.GameObjects.Rectangle; private menuButtonText!: Phaser.GameObjects.Text; // UI State managed by MutableSignal public uiState!: MutableSignal; constructor() { super('OnitamaScene'); } create(): void { super.create(); // Create UI state signal this.uiState = createUIState(); this.boardContainer = this.add.container(0, 0); this.gridGraphics = this.add.graphics(); this.drawBoard(); // Add spawners this.disposables.add(spawnEffect(new PawnSpawner(this))); this.disposables.add(spawnEffect(new CardSpawner(this))); this.disposables.add(spawnEffect(new HighlightSpawner(this))); // Create card labels this.createCardLabels(); // Winner overlay effect this.addEffect(() => { const winner = this.state.winner; if (winner) { this.showWinner(winner); } else if (this.winnerOverlay) { this.winnerOverlay.destroy(); this.winnerOverlay = undefined; } }); // Info text this.infoText = this.add.text( 40, BOARD_OFFSET.y, '', { fontSize: '16px', fontFamily: 'Arial', color: '#4b5563', } ); // Update info text when UI state changes this.addEffect(() => { this.updateInfoText(); }); // Input handling this.setupInput(); // Menu button this.createMenuButton(); // Start the game this.gameHost.start(); } private createCardLabels(): void { const boardLeft = BOARD_OFFSET.x; const boardTop = BOARD_OFFSET.y; const boardRight = BOARD_OFFSET.x + BOARD_SIZE * CELL_SIZE; const boardBottom = BOARD_OFFSET.y + BOARD_SIZE * CELL_SIZE; // Red cards label - 棋盘下方 const redLabel = this.add.text( boardLeft + (BOARD_SIZE * CELL_SIZE) / 2, boardBottom + 40, "RED", { fontSize: '16px', fontFamily: 'Arial', color: '#ef4444', } ).setOrigin(0.5, 0); this.cardLabelContainers.set('red', redLabel); // Black cards label - 棋盘上方 const blackLabel = this.add.text( boardLeft + (BOARD_SIZE * CELL_SIZE) / 2, boardTop - 40, "BLACK", { fontSize: '16px', fontFamily: 'Arial', color: '#3b82f6', } ).setOrigin(0.5, 1); this.cardLabelContainers.set('black', blackLabel); } private updateInfoText(): void { const currentPlayer = this.state.currentPlayer; if (this.state.winner) { this.infoText.setText(`${this.state.winner} wins!`); } else { this.infoText.setText(`Turn ${this.state.turn + 1}\n\n${currentPlayer}`); } } private drawBoard(): void { const g = this.gridGraphics; g.lineStyle(2, 0x6b7280); for (let i = 0; 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( 40, 40, 'Onitama', { fontSize: '28px', fontFamily: 'Arial', color: '#1f2937', } ); } private setupInput(): void { // Board cell clicks for (let row = 0; row < BOARD_SIZE; row++) { for (let col = 0; col < BOARD_SIZE; col++) { const pos = boardToScreen(col, row); const zone = this.add.zone(pos.x, pos.y, CELL_SIZE, CELL_SIZE).setInteractive(); zone.on('pointerdown', () => { if (this.state.winner) return; this.handleCellClick(col, row); }); } } } private handleCellClick(x: number, y: number): void { const pawn = this.getPawnAtPosition(x, y); if(pawn?.owner !== this.state.currentPlayer){ return; } selectPiece(this.uiState, x, y); } public onCardClick(cardId: string): void { // 只能选择当前玩家的手牌 const currentPlayer = this.state.currentPlayer; const playerCards = currentPlayer === 'red' ? this.state.redCards : this.state.blackCards; if (!playerCards.includes(cardId)) { return; } selectCard(this.uiState, cardId); } public onHighlightClick(data: HighlightData): void { clearSelection(this.uiState); this.executeMove({ card: data.card, fromX: data.fromX, fromY: data.fromY, toX: data.toX, toY: data.toY, }); } private executeMove(move: { card: string; fromX: number; fromY: number; toX: number; toY: number }): void { const error = this.gameHost.tryAnswerPrompt( prompts.move, this.state.currentPlayer, move.card, move.fromX, move.fromY, move.toX, move.toY ); if (error) { console.warn('Invalid move:', error); } } private getPawnAtPosition(x: number, y: number): Pawn | null { const key = `${x},${y}`; const pawnId = this.state.regions.board.partMap[key]; return pawnId ? this.state.pawns[pawnId] : null; } /** 创建菜单按钮 */ private createMenuButton(): void { const buttonX = 680; const buttonY = 40; this.menuButtonBg = this.add.rectangle(buttonX, buttonY, 120, 40, 0x6b7280) .setInteractive({ useHandCursor: true }); this.menuButtonText = this.add.text(buttonX, buttonY, 'Menu', { fontSize: '18px', fontFamily: 'Arial', color: '#ffffff', }).setOrigin(0.5); this.menuButtonContainer = this.add.container(buttonX, buttonY, [ this.menuButtonBg, this.menuButtonText, ]); this.menuButtonBg.on('pointerover', () => { this.menuButtonBg.setFillStyle(0x4b5563); }); this.menuButtonBg.on('pointerout', () => { this.menuButtonBg.setFillStyle(0x6b7280); }); this.menuButtonBg.on('pointerdown', () => { this.goToMenu(); }); } /** 跳转到菜单场景 */ private async goToMenu(): Promise { await this.sceneController.launch('MenuScene'); } 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!`; const bg = 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 ).setInteractive({ useHandCursor: true }); bg.on('pointerdown', () => { this.gameHost.start(); }); this.winnerOverlay.add(bg); 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, }); } }