import { GameHostScene, spawnEffect } from "boardgame-phaser"; import type { OnitamaState, Pawn } from "@/game/onitama"; import type { HighlightData } from "@/spawners/HighlightSpawner"; import type { OnitamaUIState } from "@/state"; import type { MutableSignal } from "boardgame-core"; import type Phaser from "phaser"; import { COLORS, FONTS, MENU_BUTTON, TEXT_POSITION, VISUAL, getBoardCenter, getCardLabelPosition, colorToStr, createWinnerPulseTween, } from "@/config"; import { prompts } from "@/game/onitama"; import { PawnSpawner, CardSpawner, BOARD_OFFSET, CELL_SIZE, BOARD_SIZE, boardToScreen, HighlightSpawner, } from "@/spawners"; import { createUIState, clearSelection, selectPiece, selectCard, } 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( TEXT_POSITION.infoX, BOARD_OFFSET.y, "", FONTS.info, ); // 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 { // Red cards label - 棋盘下方 const redPos = getCardLabelPosition("red"); const redLabel = this.add .text(redPos.x, redPos.y, "RED", { ...FONTS.cardLabel, color: colorToStr(COLORS.red), }) .setOrigin(redPos.originX, redPos.originY); this.cardLabelContainers.set("red", redLabel); // Black cards label - 棋盘上方 const blackPos = getCardLabelPosition("black"); const blackLabel = this.add .text(blackPos.x, blackPos.y, "BLACK", { ...FONTS.cardLabel, color: colorToStr(COLORS.black), }) .setOrigin(blackPos.originX, blackPos.originY); 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, COLORS.gridLine); 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( TEXT_POSITION.titleX, TEXT_POSITION.titleY, "Onitama", FONTS.title, ); } 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 { this.menuButtonBg = this.add .rectangle( MENU_BUTTON.x, MENU_BUTTON.y, MENU_BUTTON.width, MENU_BUTTON.height, COLORS.menuButton, ) .setInteractive({ useHandCursor: true }); this.menuButtonText = this.add .text(MENU_BUTTON.x, MENU_BUTTON.y, "Menu", FONTS.menuButton) .setOrigin(0.5); this.menuButtonContainer = this.add.container( MENU_BUTTON.x, MENU_BUTTON.y, [this.menuButtonBg, this.menuButtonText], ); this.menuButtonBg.on("pointerover", () => { this.menuButtonBg.setFillStyle(COLORS.menuButtonHover); }); this.menuButtonBg.on("pointerout", () => { this.menuButtonBg.setFillStyle(COLORS.menuButton); }); 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 center = getBoardCenter(); const boardWidth = BOARD_SIZE * CELL_SIZE; const boardHeight = BOARD_SIZE * CELL_SIZE; const bg = this.add .rectangle( center.x, center.y, boardWidth, boardHeight, COLORS.overlayBg, VISUAL.overlayAlpha, ) .setInteractive({ useHandCursor: true }); bg.on("pointerdown", () => { this.gameHost.start(); }); this.winnerOverlay.add(bg); const winText = this.add .text(center.x, center.y, text, FONTS.winner) .setOrigin(0.5); this.winnerOverlay.add(winText); createWinnerPulseTween(this, winText); } }