feat: add error display & upgrade pick
This commit is contained in:
parent
df1c0cbb81
commit
c678974489
|
|
@ -241,7 +241,10 @@ async function turn(game: BoopGame, turnPlayer: PlayerType) {
|
||||||
}
|
}
|
||||||
const turnCommand = registry.register('turn <player>', turn);
|
const turnCommand = registry.register('turn <player>', turn);
|
||||||
export const commands = {
|
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} ${type}`;
|
||||||
}
|
}
|
||||||
|
return `play ${player} ${row} ${col}`;
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
@ -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 {
|
destroy(): void {
|
||||||
this.container.destroy();
|
this.container.destroy();
|
||||||
this.gridGraphics.destroy();
|
this.gridGraphics.destroy();
|
||||||
|
|
|
||||||
|
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -7,6 +7,7 @@ import { SupplyUI } from './SupplyUI';
|
||||||
import { PieceTypeSelector } from './PieceTypeSelector';
|
import { PieceTypeSelector } from './PieceTypeSelector';
|
||||||
import { WinnerOverlay } from './WinnerOverlay';
|
import { WinnerOverlay } from './WinnerOverlay';
|
||||||
import { StartOverlay } from './StartOverlay';
|
import { StartOverlay } from './StartOverlay';
|
||||||
|
import { ErrorOverlay } from './ErrorOverlay';
|
||||||
|
|
||||||
export class GameScene extends GameHostScene<BoopState> {
|
export class GameScene extends GameHostScene<BoopState> {
|
||||||
private boardRenderer!: BoardRenderer;
|
private boardRenderer!: BoardRenderer;
|
||||||
|
|
@ -14,6 +15,7 @@ export class GameScene extends GameHostScene<BoopState> {
|
||||||
private pieceTypeSelector!: PieceTypeSelector;
|
private pieceTypeSelector!: PieceTypeSelector;
|
||||||
private winnerOverlay!: WinnerOverlay;
|
private winnerOverlay!: WinnerOverlay;
|
||||||
private startOverlay!: StartOverlay;
|
private startOverlay!: StartOverlay;
|
||||||
|
private errorOverlay!: ErrorOverlay;
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
super('GameScene');
|
super('GameScene');
|
||||||
|
|
@ -28,6 +30,7 @@ export class GameScene extends GameHostScene<BoopState> {
|
||||||
this.pieceTypeSelector = new PieceTypeSelector(this);
|
this.pieceTypeSelector = new PieceTypeSelector(this);
|
||||||
this.winnerOverlay = new WinnerOverlay(this, () => this.restartGame());
|
this.winnerOverlay = new WinnerOverlay(this, () => this.restartGame());
|
||||||
this.startOverlay = new StartOverlay(this, () => this.startGame());
|
this.startOverlay = new StartOverlay(this, () => this.startGame());
|
||||||
|
this.errorOverlay = new ErrorOverlay(this);
|
||||||
|
|
||||||
// 设置棋子生成器
|
// 设置棋子生成器
|
||||||
this.disposables.add(createPieceSpawner(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.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(() => {
|
this.addEffect(() => {
|
||||||
const status = this.gameHost.status.value;
|
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 cmd = commands.play(this.state.currentPlayer, row, col, selectedType);
|
||||||
const error = this.gameHost.onInput(cmd);
|
const error = this.gameHost.onInput(cmd);
|
||||||
if (error) {
|
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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue