refactor: use GameHost for simplification
This commit is contained in:
parent
32509d7812
commit
4fa06098ba
|
|
@ -1,21 +1,22 @@
|
||||||
import { h, render } from 'preact';
|
import { h, render } from 'preact';
|
||||||
import { signal, computed } from '@preact/signals-core';
|
import { signal } from '@preact/signals-core';
|
||||||
import { useEffect, useState, useCallback } from 'preact/hooks';
|
import { useEffect, useState, useCallback } from 'preact/hooks';
|
||||||
import Phaser from 'phaser';
|
import Phaser from 'phaser';
|
||||||
import { createGameContext } from 'boardgame-core';
|
import { createGameHost } from 'boardgame-core';
|
||||||
import { GameUI, PromptDialog, CommandLog, createPromptHandler } from 'boardgame-phaser';
|
import { GameUI, PromptDialog, CommandLog } from 'boardgame-phaser';
|
||||||
import { createInitialState, registry, type TicTacToeState } from './game/tic-tac-toe';
|
import { createInitialState, registry, type TicTacToeState } from './game/tic-tac-toe';
|
||||||
import { GameScene } from './scenes/GameScene';
|
import { GameScene } from './scenes/GameScene';
|
||||||
import './style.css';
|
import './style.css';
|
||||||
|
|
||||||
const gameContext = createGameContext<TicTacToeState>(registry, createInitialState);
|
// 创建 GameHost 实例,自动管理状态和 prompt
|
||||||
|
const gameHost = createGameHost(
|
||||||
|
{ registry, createInitialState },
|
||||||
|
'setup',
|
||||||
|
{ autoStart: false }
|
||||||
|
);
|
||||||
|
|
||||||
const commandLog = signal<Array<{ input: string; result: string; timestamp: number }>>([]);
|
const commandLog = signal<Array<{ input: string; result: string; timestamp: number }>>([]);
|
||||||
|
|
||||||
// Single PromptHandler — the only consumer of promptQueue
|
|
||||||
let promptHandler: ReturnType<typeof createPromptHandler> | null = null;
|
|
||||||
const promptSignal = signal<import('boardgame-core').PromptEvent | null>(null);
|
|
||||||
|
|
||||||
// 记录命令日志的辅助函数
|
// 记录命令日志的辅助函数
|
||||||
function logCommand(input: string, result: { success: boolean; result?: unknown; error?: string }) {
|
function logCommand(input: string, result: { success: boolean; result?: unknown; error?: string }) {
|
||||||
commandLog.value = [
|
commandLog.value = [
|
||||||
|
|
@ -33,6 +34,7 @@ function App() {
|
||||||
const [game, setGame] = useState<Phaser.Game | null>(null);
|
const [game, setGame] = useState<Phaser.Game | null>(null);
|
||||||
const [scene, setScene] = useState<GameScene | null>(null);
|
const [scene, setScene] = useState<GameScene | null>(null);
|
||||||
const [gameState, setGameState] = useState<TicTacToeState | null>(null);
|
const [gameState, setGameState] = useState<TicTacToeState | null>(null);
|
||||||
|
const [promptSchema, setPromptSchema] = useState<any>(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const phaserConfig: Phaser.Types.Core.GameConfig = {
|
const phaserConfig: Phaser.Types.Core.GameConfig = {
|
||||||
|
|
@ -45,81 +47,70 @@ function App() {
|
||||||
};
|
};
|
||||||
|
|
||||||
const phaserGame = new Phaser.Game(phaserConfig);
|
const phaserGame = new Phaser.Game(phaserConfig);
|
||||||
// 通过 init 传递 gameContext
|
// 通过 init 传递 gameHost
|
||||||
const gameScene = new GameScene();
|
const gameScene = new GameScene();
|
||||||
phaserGame.scene.add('GameScene', gameScene, true, { gameContext });
|
phaserGame.scene.add('GameScene', gameScene, true, { gameHost });
|
||||||
|
|
||||||
setGame(phaserGame);
|
setGame(phaserGame);
|
||||||
setScene(gameScene);
|
setScene(gameScene);
|
||||||
setPhaserReady(true);
|
setPhaserReady(true);
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
|
gameHost.dispose();
|
||||||
phaserGame.destroy(true);
|
phaserGame.destroy(true);
|
||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (phaserReady && scene) {
|
if (phaserReady && scene) {
|
||||||
// Initialize the single PromptHandler
|
// 监听 prompt 状态变化
|
||||||
promptHandler = createPromptHandler({
|
const disposePromptSchema = gameHost.activePromptSchema.subscribe((schema) => {
|
||||||
commands: gameContext.commands,
|
setPromptSchema(schema);
|
||||||
onPrompt: (prompt) => {
|
scene.promptSchema.current = schema;
|
||||||
promptSignal.value = prompt;
|
|
||||||
// Also update the scene's prompt reference
|
|
||||||
scene.promptSignal.current = prompt;
|
|
||||||
},
|
|
||||||
onCancel: () => {
|
|
||||||
promptSignal.value = null;
|
|
||||||
scene.promptSignal.current = null;
|
|
||||||
},
|
|
||||||
});
|
|
||||||
promptHandler.start();
|
|
||||||
|
|
||||||
// Wire the scene's submit function to this PromptHandler
|
|
||||||
scene.setSubmitPrompt((cmd: string) => {
|
|
||||||
const error = promptHandler!.submit(cmd);
|
|
||||||
if (error === null) {
|
|
||||||
logCommand(cmd, { success: true });
|
|
||||||
promptSignal.value = null;
|
|
||||||
scene.promptSignal.current = null;
|
|
||||||
} else {
|
|
||||||
logCommand(cmd, { success: false, error });
|
|
||||||
}
|
|
||||||
return error;
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// 监听状态变化
|
// 监听状态变化
|
||||||
const dispose = gameContext.state.subscribe(() => {
|
const disposeState = gameHost.state.subscribe(() => {
|
||||||
setGameState({ ...gameContext.state.value });
|
setGameState(gameHost.state.value as TicTacToeState);
|
||||||
});
|
});
|
||||||
|
|
||||||
// 运行游戏设置
|
// 运行游戏设置
|
||||||
gameContext.commands.run('setup').then(result => {
|
gameHost.setup('setup').then(() => {
|
||||||
logCommand('setup', result);
|
logCommand('setup', { success: true });
|
||||||
|
}).catch(err => {
|
||||||
|
logCommand('setup', { success: false, error: err.message });
|
||||||
});
|
});
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
dispose();
|
disposePromptSchema();
|
||||||
promptHandler?.destroy();
|
disposeState();
|
||||||
promptHandler = null;
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}, [phaserReady, scene]);
|
}, [phaserReady, scene]);
|
||||||
|
|
||||||
const handlePromptSubmit = useCallback((input: string) => {
|
const handlePromptSubmit = useCallback((input: string) => {
|
||||||
if (promptHandler) {
|
const error = gameHost.onInput(input);
|
||||||
promptHandler.submit(input);
|
if (error === null) {
|
||||||
|
logCommand(input, { success: true });
|
||||||
|
setPromptSchema(null);
|
||||||
|
if (scene) {
|
||||||
|
scene.promptSchema.current = null;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
logCommand(input, { success: false, error });
|
||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const handlePromptCancel = useCallback(() => {
|
const handlePromptCancel = useCallback(() => {
|
||||||
if (promptHandler) {
|
gameHost.commands._cancel('User cancelled');
|
||||||
promptHandler.cancel('User cancelled');
|
setPromptSchema(null);
|
||||||
|
if (scene) {
|
||||||
|
scene.promptSchema.current = null;
|
||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const handleReset = useCallback(() => {
|
const handleReset = useCallback(() => {
|
||||||
gameContext.commands.run('reset').then(result => {
|
gameHost.commands.run('reset').then(result => {
|
||||||
logCommand('reset', result);
|
logCommand('reset', result);
|
||||||
});
|
});
|
||||||
}, []);
|
}, []);
|
||||||
|
|
@ -128,7 +119,7 @@ function App() {
|
||||||
<div className="flex flex-col h-screen">
|
<div className="flex flex-col h-screen">
|
||||||
<div className="flex-1 relative">
|
<div className="flex-1 relative">
|
||||||
<div id="phaser-container" className="w-full h-full" />
|
<div id="phaser-container" className="w-full h-full" />
|
||||||
|
|
||||||
{/* 游戏状态显示 */}
|
{/* 游戏状态显示 */}
|
||||||
{gameState && !gameState.winner && (
|
{gameState && !gameState.winner && (
|
||||||
<div className="absolute top-4 left-1/2 transform -translate-x-1/2 bg-white px-4 py-2 rounded-lg shadow-lg z-10">
|
<div className="absolute top-4 left-1/2 transform -translate-x-1/2 bg-white px-4 py-2 rounded-lg shadow-lg z-10">
|
||||||
|
|
@ -137,7 +128,7 @@ function App() {
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{gameState?.winner && (
|
{gameState?.winner && (
|
||||||
<div className="absolute top-4 left-1/2 transform -translate-x-1/2 bg-white px-4 py-2 rounded-lg shadow-lg z-10">
|
<div className="absolute top-4 left-1/2 transform -translate-x-1/2 bg-white px-4 py-2 rounded-lg shadow-lg z-10">
|
||||||
<span className="text-lg font-semibold text-yellow-600">
|
<span className="text-lg font-semibold text-yellow-600">
|
||||||
|
|
@ -147,7 +138,7 @@ function App() {
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<PromptDialog
|
<PromptDialog
|
||||||
prompt={promptSignal.value}
|
prompt={promptSchema ? { schema: promptSchema, tryCommit: () => null, cancel: () => {} } : null}
|
||||||
onSubmit={handlePromptSubmit}
|
onSubmit={handlePromptSubmit}
|
||||||
onCancel={handlePromptCancel}
|
onCancel={handlePromptCancel}
|
||||||
/>
|
/>
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
import Phaser from 'phaser';
|
import Phaser from 'phaser';
|
||||||
import type { TicTacToeState, TicTacToePart, PlayerType } from '@/game/tic-tac-toe';
|
import type { TicTacToeState, TicTacToePart, PlayerType } from '@/game/tic-tac-toe';
|
||||||
|
import type { GameHost, CommandSchema, IGameContext, MutableSignal } from 'boardgame-core';
|
||||||
import { ReactiveScene, bindRegion, createInputMapper, InputMapper } from 'boardgame-phaser';
|
import { ReactiveScene, bindRegion, createInputMapper, InputMapper } from 'boardgame-phaser';
|
||||||
|
|
||||||
const CELL_SIZE = 120;
|
const CELL_SIZE = 120;
|
||||||
|
|
@ -11,13 +12,31 @@ export class GameScene extends ReactiveScene<TicTacToeState> {
|
||||||
private gridGraphics!: Phaser.GameObjects.Graphics;
|
private gridGraphics!: Phaser.GameObjects.Graphics;
|
||||||
private inputMapper!: InputMapper;
|
private inputMapper!: InputMapper;
|
||||||
private turnText!: Phaser.GameObjects.Text;
|
private turnText!: Phaser.GameObjects.Text;
|
||||||
/** Receives the active prompt from the single PromptHandler in main.tsx */
|
/** Receives the active prompt schema from main.tsx */
|
||||||
promptSignal: { current: any } = { current: null };
|
promptSchema: { current: CommandSchema | null } = { current: null };
|
||||||
|
/** GameHost instance passed from main.tsx */
|
||||||
|
private gameHost!: GameHost<TicTacToeState>;
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
super('GameScene');
|
super('GameScene');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
init(data: { gameHost: GameHost<TicTacToeState> } | { gameContext: IGameContext<TicTacToeState> }): void {
|
||||||
|
if ('gameHost' in data) {
|
||||||
|
this.gameHost = data.gameHost;
|
||||||
|
// Create a compatible gameContext from GameHost
|
||||||
|
this.gameContext = {
|
||||||
|
state: this.gameHost.state as MutableSignal<TicTacToeState>,
|
||||||
|
commands: this.gameHost.commands,
|
||||||
|
} as IGameContext<TicTacToeState>;
|
||||||
|
this.state = this.gameContext.state;
|
||||||
|
this.commands = this.gameContext.commands;
|
||||||
|
} else {
|
||||||
|
// Fallback for direct gameContext passing
|
||||||
|
super.init(data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
protected onStateReady(_state: TicTacToeState): void {
|
protected onStateReady(_state: TicTacToeState): void {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -84,10 +103,8 @@ export class GameScene extends ReactiveScene<TicTacToeState> {
|
||||||
private setupInput(): void {
|
private setupInput(): void {
|
||||||
this.inputMapper = createInputMapper(this, {
|
this.inputMapper = createInputMapper(this, {
|
||||||
onSubmit: (cmd: string) => {
|
onSubmit: (cmd: string) => {
|
||||||
// Delegate to the single PromptHandler via the shared commands reference.
|
// Directly submit to GameHost
|
||||||
// The actual PromptHandler instance lives in main.tsx and is set up once.
|
return this.gameHost.onInput(cmd);
|
||||||
// We call through a callback that main.tsx provides via the scene's public interface.
|
|
||||||
return this.submitToPrompt(cmd);
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -106,21 +123,6 @@ export class GameScene extends ReactiveScene<TicTacToeState> {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Called by main.tsx to wire up the single PromptHandler's submit function.
|
|
||||||
*/
|
|
||||||
private _submitToPrompt: ((cmd: string) => string | null) | null = null;
|
|
||||||
|
|
||||||
setSubmitPrompt(fn: (cmd: string) => string | null): void {
|
|
||||||
this._submitToPrompt = fn;
|
|
||||||
}
|
|
||||||
|
|
||||||
private submitToPrompt(cmd: string): string | null {
|
|
||||||
return this._submitToPrompt
|
|
||||||
? this._submitToPrompt(cmd)
|
|
||||||
: null; // no handler wired yet, accept silently
|
|
||||||
}
|
|
||||||
|
|
||||||
private drawGrid(): void {
|
private drawGrid(): void {
|
||||||
const g = this.gridGraphics;
|
const g = this.gridGraphics;
|
||||||
g.lineStyle(3, 0x6b7280);
|
g.lineStyle(3, 0x6b7280);
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue