feat: add error display & upgrade pick

This commit is contained in:
hypercross 2026-04-05 00:24:06 +08:00
parent df1c0cbb81
commit c678974489
4 changed files with 167 additions and 4 deletions

View File

@ -241,7 +241,10 @@ async function turn(game: BoopGame, turnPlayer: PlayerType) {
}
const turnCommand = registry.register('turn <player>', turn);
export const commands = {
play: (player: PlayerType, row: number, col: number, type: PieceType) => {
play: (player: PlayerType, row: number, col: number, type?: PieceType) => {
if (type) {
return `play ${player} ${row} ${col} ${type}`;
}
return `play ${player} ${row} ${col}`;
},
};

View File

@ -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();

View File

@ -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();
}
}

View File

@ -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<BoopState> {
private boardRenderer!: BoardRenderer;
@ -14,6 +15,7 @@ export class GameScene extends GameHostScene<BoopState> {
private pieceTypeSelector!: PieceTypeSelector;
private winnerOverlay!: WinnerOverlay;
private startOverlay!: StartOverlay;
private errorOverlay!: ErrorOverlay;
constructor() {
super('GameScene');
@ -28,6 +30,7 @@ export class GameScene extends GameHostScene<BoopState> {
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<BoopState> {
() => 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<BoopState> {
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);
}
}