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(); // 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( BOARD_OFFSET.x + (BOARD_SIZE * CELL_SIZE) / 2, BOARD_OFFSET.y + BOARD_SIZE * CELL_SIZE + 30, '', { fontSize: '20px', fontFamily: 'Arial', color: '#4b5563', } ).setOrigin(0.5); // Update info text when UI state changes this.addEffect(() => { this.updateInfoText(); }); // Input handling this.setupInput(); } private createCardLabels(): void { const boardLeft = BOARD_OFFSET.x; const boardTop = BOARD_OFFSET.y; const boardRight = BOARD_OFFSET.x + BOARD_SIZE * CELL_SIZE; // Red cards label const redLabel = this.add.text( boardLeft - CARD_WIDTH - 60 + CARD_WIDTH / 2, boardTop + 50 + 2 * (CARD_HEIGHT + 15) + 10, "RED's Cards", { fontSize: '16px', fontFamily: 'Arial', color: '#ef4444', } ).setOrigin(0.5, 0); this.cardLabelContainers.set('red', redLabel); // Black cards label const blackLabel = this.add.text( boardRight + 60 + CARD_WIDTH / 2, boardTop + 50 + 2 * (CARD_HEIGHT + 15) + 10, "BLACK's Cards", { fontSize: '16px', fontFamily: 'Arial', color: '#3b82f6', } ).setOrigin(0.5, 0); this.cardLabelContainers.set('black', blackLabel); // Spare card label const boardCenterX = boardLeft + (BOARD_SIZE * CELL_SIZE) / 2; const spareLabel = this.add.text( boardCenterX, boardTop - 50, 'Spare Card', { fontSize: '16px', fontFamily: 'Arial', color: '#6b7280', } ).setOrigin(0.5, 0); this.cardLabelContainers.set('spare', spareLabel); } private updateInfoText(): void { const currentPlayer = this.state.currentPlayer; const selectedCard = this.uiState.value.selectedCard; const selectedPiece = this.uiState.value.selectedPiece; if (this.state.winner) { this.infoText.setText(`${this.state.winner} wins!`); } else if (!selectedCard) { this.infoText.setText(`${currentPlayer}'s turn - Select a card first`); } else if (!selectedPiece) { this.infoText.setText(`Card: ${selectedCard} - Select a piece to move`); } else { this.infoText.setText(`${currentPlayer}'s turn (Turn ${this.state.turn + 1})`); } } 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( BOARD_OFFSET.x + (BOARD_SIZE * CELL_SIZE) / 2, BOARD_OFFSET.y - 40, 'Onitama', { fontSize: '28px', fontFamily: 'Arial', color: '#1f2937', } ).setOrigin(0.5); } 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 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, }); } }