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; private whiteSupplyText!: Phaser.GameObjects.Text; private blackSupplyText!: Phaser.GameObjects.Text; private pieceTypeSelector!: Phaser.GameObjects.Container; private selectedPieceType: PieceType = 'kitten'; private kittenButton!: Phaser.GameObjects.Container; private catButton!: 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.createSupplyUI(); 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.updateSupplyUI(); this.updatePieceTypeSelector(); }); this.createPieceTypeSelector(); 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, this.selectedPieceType); 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 createSupplyUI(): void { const boardCenterX = BOARD_OFFSET.x + (BOARD_SIZE * CELL_SIZE) / 2; const uiY = BOARD_OFFSET.y - 20; // 白色玩家储备 this.whiteSupplyText = this.add.text( boardCenterX - 150, uiY, '', { fontSize: '16px', fontFamily: 'Arial', color: '#ffffff', backgroundColor: '#000000', padding: { x: 10, y: 5 }, } ).setOrigin(0.5).setDepth(100); // 黑色玩家储备 this.blackSupplyText = this.add.text( boardCenterX + 150, uiY, '', { fontSize: '16px', fontFamily: 'Arial', color: '#ffffff', backgroundColor: '#333333', padding: { x: 10, y: 5 }, } ).setOrigin(0.5).setDepth(100); this.updateSupplyUI(); } private updateSupplyUI(): void { const white = this.state.players.white; const black = this.state.players.black; this.whiteSupplyText.setText( `⚪ WHITE\n🐾 ${white.kitten.supply} | 🐱 ${white.cat.supply}` ); this.blackSupplyText.setText( `⚫ BLACK\n🐾 ${black.kitten.supply} | 🐱 ${black.cat.supply}` ); // 高亮当前玩家 const isWhiteTurn = this.state.currentPlayer === 'white'; this.whiteSupplyText.setStyle({ backgroundColor: isWhiteTurn ? '#fbbf24' : '#000000' }); this.blackSupplyText.setStyle({ backgroundColor: !isWhiteTurn ? '#fbbf24' : '#333333' }); } private createPieceTypeSelector(): void { const boardCenterX = BOARD_OFFSET.x + (BOARD_SIZE * CELL_SIZE) / 2; const selectorY = BOARD_OFFSET.y + BOARD_SIZE * CELL_SIZE + 100; this.pieceTypeSelector = this.add.container(boardCenterX, selectorY); // 标签文字 const label = this.add.text(-120, 0, '放置:', { fontSize: '18px', fontFamily: 'Arial', color: '#4b5563', }).setOrigin(0.5, 0.5); // 小猫按钮 this.kittenButton = this.createPieceButton('kitten', '🐾 小猫', -30); this.catButton = this.createPieceButton('cat', '🐱 大猫', 50); this.pieceTypeSelector.add([label, this.kittenButton, this.catButton]); this.updatePieceTypeSelector(); } private createPieceButton(type: PieceType, text: string, xOffset: number): Phaser.GameObjects.Container { const container = this.add.container(xOffset, 0); const bg = this.add.rectangle(0, 0, 100, 40, 0xe5e7eb) .setStrokeStyle(2, 0x9ca3af); const textObj = this.add.text(0, 0, text, { fontSize: '16px', fontFamily: 'Arial', color: '#1f2937', }).setOrigin(0.5); container.add([bg, textObj]); // 使按钮可交互 bg.setInteractive({ useHandCursor: true }); bg.on('pointerdown', () => { this.selectedPieceType = type; this.updatePieceTypeSelector(); }); // 存储引用以便后续更新 if (type === 'kitten') { this.kittenButton = container; } else { this.catButton = container; } return container; } private updatePieceTypeSelector(): void { if (!this.kittenButton || !this.catButton) return; const white = this.state.players.white; const black = this.state.players.black; const currentPlayer = this.state.players[this.state.currentPlayer]; const isWhiteTurn = this.state.currentPlayer === 'white'; // 更新按钮状态 const kittenAvailable = currentPlayer.kitten.supply > 0; const catAvailable = currentPlayer.cat.supply > 0; // 更新小猫按钮 const kittenBg = this.kittenButton.list[0] as Phaser.GameObjects.Rectangle; const kittenText = this.kittenButton.list[1] as Phaser.GameObjects.Text; const isKittenSelected = this.selectedPieceType === 'kitten'; kittenBg.setFillStyle(isKittenSelected ? 0xfbbf24 : (kittenAvailable ? 0xe5e7eb : 0xd1d5db)); kittenText.setText(`🐾 小猫 (${currentPlayer.kitten.supply})`); // 更新大猫按钮 const catBg = this.catButton.list[0] as Phaser.GameObjects.Rectangle; const catText = this.catButton.list[1] as Phaser.GameObjects.Text; const isCatSelected = this.selectedPieceType === 'cat'; catBg.setFillStyle(isCatSelected ? 0xfbbf24 : (catAvailable ? 0xe5e7eb : 0xd1d5db)); catText.setText(`🐱 大猫 (${currentPlayer.cat.supply})`); // 如果选中的类型不可用,切换到可用的 if (!kittenAvailable && this.selectedPieceType === 'kitten' && catAvailable) { this.selectedPieceType = 'cat'; this.updatePieceTypeSelector(); } else if (!catAvailable && this.selectedPieceType === 'cat' && kittenAvailable) { this.selectedPieceType = 'kitten'; this.updatePieceTypeSelector(); } } 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; // 使用 tween 动画平滑移动棋子 this.scene.tweens.add({ targets: obj, x: x, y: y, duration: 200, ease: 'Power2', }); } 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(), }); } }