import Phaser from 'phaser'; import type {BoopState, BoopPart, PlayerType, PieceType} from '@/game/boop'; import { GameHostScene } from 'boardgame-phaser'; import { spawnEffect, type Spawner } from 'boardgame-phaser'; import type { ReadonlySignal } from '@preact/signals-core'; import {commands} from "@/game/boop"; const BOARD_SIZE = 6; const CELL_SIZE = 80; const BOARD_OFFSET = { x: 80, y: 100 }; export class GameScene extends GameHostScene { private boardContainer!: Phaser.GameObjects.Container; private gridGraphics!: Phaser.GameObjects.Graphics; private turnText!: Phaser.GameObjects.Text; private infoText!: 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 BoopPartSpawner(this, this.gameHost.state))); this.watch(() => { const winner = this.state.winner; if (winner) { this.showWinner(winner); } else if (this.winnerOverlay) { this.winnerOverlay.destroy(); this.winnerOverlay = undefined; } }); this.watch(() => { const currentPlayer = this.state.currentPlayer; this.updateTurnText(currentPlayer); }); this.setupInput(); } private setupInput(): void { for (let row = 0; row < BOARD_SIZE; row++) { for (let col = 0; col < BOARD_SIZE; col++) { const x = BOARD_OFFSET.x + col * CELL_SIZE + CELL_SIZE / 2; const y = BOARD_OFFSET.y + row * CELL_SIZE + CELL_SIZE / 2; const zone = this.add.zone(x, y, CELL_SIZE, CELL_SIZE).setInteractive(); zone.on('pointerdown', () => { if (this.state.winner) return; if (this.isCellOccupied(row, col)) return; const cmd = commands.play(this.state.currentPlayer, row, col, 'kitten'); const error = this.gameHost.onInput(cmd); if (error) { console.warn('Invalid move:', error); } }); } } } private drawGrid(): void { const g = this.gridGraphics; g.lineStyle(2, 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 - 50, 'Boop Game', { fontSize: '32px', 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 + 30, '', { fontSize: '22px', fontFamily: 'Arial', color: '#4b5563', } ).setOrigin(0.5); this.infoText = this.add.text( BOARD_OFFSET.x + (BOARD_SIZE * CELL_SIZE) / 2, BOARD_OFFSET.y + BOARD_SIZE * CELL_SIZE + 60, 'Click to place kitten. Cats win with 3 in a row!', { fontSize: '16px', fontFamily: 'Arial', color: '#6b7280', } ).setOrigin(0.5); this.updateTurnText(this.state.currentPlayer); } private updateTurnText(player: PlayerType): void { if (this.turnText) { const whitePieces = this.state.players.white; const blackPieces = this.state.players.black; const current = player === 'white' ? whitePieces : blackPieces; this.turnText.setText( `${player.toUpperCase()}'s turn | Kittens: ${current.kitten.supply} | Cats: ${current.cat.supply}` ); } } private showWinner(winner: PlayerType | 'draw' | null): void { if (this.winnerOverlay) { this.winnerOverlay.destroy(); } this.winnerOverlay = this.add.container(); const text = winner === 'draw' ? "It's a draw!" : winner ? `${winner.toUpperCase()} 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.setup('setup'); }); 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: '40px', 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, }); } private isCellOccupied(row: number, col: number): boolean { return !!this.state.board.partMap[`${row},${col}`]; } } class BoopPartSpawner implements Spawner { constructor(public readonly scene: GameScene, public readonly state: ReadonlySignal) {} *getData() { for (const part of Object.values(this.state.value.pieces)) { yield part; } } getKey(part: BoopPart): string { return part.id; } onUpdate(part: BoopPart, obj: Phaser.GameObjects.Container): void { const [row, col] = part.position; const x = BOARD_OFFSET.x + col * CELL_SIZE + CELL_SIZE / 2; const y = BOARD_OFFSET.y + row * CELL_SIZE + CELL_SIZE / 2; obj.x = x; obj.y = y; } onSpawn(part: BoopPart) { const [row, col] = part.position; const x = BOARD_OFFSET.x + col * CELL_SIZE + CELL_SIZE / 2; const y = BOARD_OFFSET.y + row * CELL_SIZE + CELL_SIZE / 2; const container = this.scene.add.container(x, y); const isCat = part.pieceType === 'cat'; const baseColor = part.player === 'white' ? 0xffffff : 0x333333; const strokeColor = part.player === 'white' ? 0x000000 : 0xffffff; // 绘制圆形背景 const circle = this.scene.add.circle(0, 0, CELL_SIZE * 0.4, baseColor) .setStrokeStyle(3, strokeColor); // 添加文字标识 const text = isCat ? '🐱' : '🐾'; const textObj = this.scene.add.text(0, 0, text, { fontSize: `${isCat ? 40 : 32}px`, fontFamily: 'Arial', }).setOrigin(0.5); container.add([circle, textObj]); // 添加落子动画 container.setScale(0); this.scene.tweens.add({ targets: container, scale: 1, duration: 200, ease: 'Back.easeOut', }); return container; } onDespawn(obj: Phaser.GameObjects.Container) { this.scene.tweens.add({ targets: obj, alpha: 0, scale: 0.5, duration: 200, ease: 'Back.easeIn', onComplete: () => obj.destroy(), }); } }