diff --git a/packages/boop-game/src/game/commands.ts b/packages/boop-game/src/game/commands.ts index bd848af..89d4e02 100644 --- a/packages/boop-game/src/game/commands.ts +++ b/packages/boop-game/src/game/commands.ts @@ -241,7 +241,10 @@ async function turn(game: BoopGame, turnPlayer: PlayerType) { } const turnCommand = registry.register('turn ', turn); export const commands = { - play: (player: PlayerType, row: number, col: number, type: PieceType) => { - return `play ${player} ${row} ${col} ${type}`; - } + play: (player: PlayerType, row: number, col: number, type?: PieceType) => { + if (type) { + return `play ${player} ${row} ${col} ${type}`; + } + return `play ${player} ${row} ${col}`; + }, }; \ No newline at end of file diff --git a/packages/boop-game/src/scenes/BoardRenderer.ts b/packages/boop-game/src/scenes/BoardRenderer.ts index e0a34e7..ba06587 100644 --- a/packages/boop-game/src/scenes/BoardRenderer.ts +++ b/packages/boop-game/src/scenes/BoardRenderer.ts @@ -111,6 +111,40 @@ export class BoardRenderer { } } + /** + * 设置棋盘上棋子的点击处理(用于选择要升级的棋子) + */ + setupPieceInput( + getState: () => BoopState, + onPieceClick: (row: number, col: number) => void, + checkWinner: () => boolean + ): 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.scene.add.zone(x, y, CELL_SIZE, CELL_SIZE).setInteractive(); + + zone.on('pointerdown', () => { + const state = getState(); + const isOccupied = !!state.regions.board.partMap[`${row},${col}`]; + if (isOccupied && !checkWinner()) { + onPieceClick(row, col); + } + }); + } + } + } + + /** + * 更新棋盘上棋子的交互状态 + */ + updatePieceInteraction(enabled: boolean): void { + // 可以通过此方法启用/禁用棋子点击 + // 暂时通过重新设置 zone 的 interactive 状态来实现 + } + destroy(): void { this.container.destroy(); this.gridGraphics.destroy(); diff --git a/packages/boop-game/src/scenes/ErrorOverlay.ts b/packages/boop-game/src/scenes/ErrorOverlay.ts new file mode 100644 index 0000000..064fca6 --- /dev/null +++ b/packages/boop-game/src/scenes/ErrorOverlay.ts @@ -0,0 +1,107 @@ +import Phaser from 'phaser'; +import { BOARD_OFFSET, CELL_SIZE, BOARD_SIZE } from './BoardRenderer'; + +export class ErrorOverlay { + private overlay?: Phaser.GameObjects.Container; + private hideTimeout?: Phaser.Time.TimerEvent; + + constructor(private scene: Phaser.Scene) { + // 初始时不显示 + } + + show(message: string, duration: number = 2000): void { + // 清除之前的定时器 + this.hideTimeout?.remove(); + + // 销毁之前的 overlay + if (this.overlay) { + this.overlay.destroy(); + } + + this.overlay = this.scene.add.container(); + + // 半透明背景(可点击关闭) + const bg = this.scene.add.rectangle( + BOARD_OFFSET.x + (BOARD_SIZE * CELL_SIZE) / 2, + BOARD_OFFSET.y + (BOARD_SIZE * CELL_SIZE) / 2, + BOARD_SIZE * CELL_SIZE + 200, + BOARD_SIZE * CELL_SIZE + 200, + 0x000000, + 0.4, + ).setInteractive(); + + bg.on('pointerdown', () => { + this.hide(); + }); + + this.overlay.add(bg); + + // 错误提示框 + const errorBoxY = BOARD_OFFSET.y + (BOARD_SIZE * CELL_SIZE) / 2 - 60; + const errorBox = this.scene.add.container( + BOARD_OFFSET.x + (BOARD_SIZE * CELL_SIZE) / 2, + errorBoxY + ); + + // 错误框背景 + const boxBg = this.scene.add.rectangle(0, 0, 450, 100, 0xef4444, 0.95) + .setStrokeStyle(4, 0xdc2626); + + // 错误图标 + const iconText = this.scene.add.text(-180, 0, '❌', { + fontSize: '36px', + }).setOrigin(0.5); + + // 错误文本 + const errorText = this.scene.add.text(30, 0, message, { + fontSize: '22px', + fontFamily: 'Arial', + color: '#ffffff', + align: 'center', + wordWrap: { width: 380 }, + }).setOrigin(0, 0.5); + + errorBox.add([boxBg, iconText, errorText]); + this.overlay.add(errorBox); + + // 出现动画 + errorBox.setScale(0); + errorBox.setAlpha(0); + this.scene.tweens.add({ + targets: errorBox, + scale: 1, + alpha: 1, + duration: 300, + ease: 'Back.easeOut', + }); + + // 自动隐藏 + this.hideTimeout = this.scene.time.delayedCall(duration, () => { + this.hide(); + }); + } + + hide(): void { + this.hideTimeout?.remove(); + this.hideTimeout = undefined; + + if (this.overlay) { + const overlay = this.overlay; + // 消失动画 + this.scene.tweens.add({ + targets: overlay, + alpha: 0, + duration: 200, + onComplete: () => { + overlay.destroy(); + this.overlay = undefined; + }, + }); + } + } + + destroy(): void { + this.hideTimeout?.remove(); + this.overlay?.destroy(); + } +} diff --git a/packages/boop-game/src/scenes/GameScene.ts b/packages/boop-game/src/scenes/GameScene.ts index 8a55593..71b9f95 100644 --- a/packages/boop-game/src/scenes/GameScene.ts +++ b/packages/boop-game/src/scenes/GameScene.ts @@ -7,6 +7,7 @@ import { SupplyUI } from './SupplyUI'; import { PieceTypeSelector } from './PieceTypeSelector'; import { WinnerOverlay } from './WinnerOverlay'; import { StartOverlay } from './StartOverlay'; +import { ErrorOverlay } from './ErrorOverlay'; export class GameScene extends GameHostScene { private boardRenderer!: BoardRenderer; @@ -14,6 +15,7 @@ export class GameScene extends GameHostScene { private pieceTypeSelector!: PieceTypeSelector; private winnerOverlay!: WinnerOverlay; private startOverlay!: StartOverlay; + private errorOverlay!: ErrorOverlay; constructor() { super('GameScene'); @@ -28,6 +30,7 @@ export class GameScene extends GameHostScene { this.pieceTypeSelector = new PieceTypeSelector(this); this.winnerOverlay = new WinnerOverlay(this, () => this.restartGame()); this.startOverlay = new StartOverlay(this, () => this.startGame()); + this.errorOverlay = new ErrorOverlay(this); // 设置棋子生成器 this.disposables.add(createPieceSpawner(this)); @@ -39,6 +42,13 @@ export class GameScene extends GameHostScene { () => this.gameHost.status.value !== 'running' || !!this.state.winner ); + // 设置棋子点击处理(用于棋盘满时选择要升级的棋子) + this.boardRenderer.setupPieceInput( + () => this.state, + (row, col) => this.handlePieceClick(row, col), + () => this.gameHost.status.value !== 'running' || !!this.state.winner + ); + // 监听游戏状态变化 this.addEffect(() => { const status = this.gameHost.status.value; @@ -72,7 +82,16 @@ export class GameScene extends GameHostScene { const cmd = commands.play(this.state.currentPlayer, row, col, selectedType); const error = this.gameHost.onInput(cmd); if (error) { - console.warn('Invalid move:', error); + this.errorOverlay.show(error); + } + } + + private handlePieceClick(row: number, col: number): void { + // 棋盘满时,点击棋子触发升级 + const cmd = commands.play(this.state.currentPlayer, row, col); + const error = this.gameHost.onInput(cmd); + if (error) { + this.errorOverlay.show(error); } }