2026-04-03 15:18:47 +08:00
|
|
|
import { h, render } from 'preact';
|
|
|
|
|
import { signal } from '@preact/signals-core';
|
2026-04-03 16:18:44 +08:00
|
|
|
import { useEffect, useState } from 'preact/hooks';
|
2026-04-03 15:18:47 +08:00
|
|
|
import Phaser from 'phaser';
|
|
|
|
|
import { createGameContext } from 'boardgame-core';
|
|
|
|
|
import { GameUI, PromptDialog, CommandLog } from 'boardgame-phaser';
|
|
|
|
|
import { createInitialState, registry, type TicTacToeState } from './game/tic-tac-toe';
|
|
|
|
|
import { GameScene, type GameSceneData } from './scenes/GameScene';
|
|
|
|
|
import './style.css';
|
|
|
|
|
|
|
|
|
|
const gameContext = createGameContext<TicTacToeState>(registry, createInitialState);
|
|
|
|
|
|
|
|
|
|
const promptSignal = signal<null | Awaited<ReturnType<typeof gameContext.commands.promptQueue.pop>>>(null);
|
|
|
|
|
const commandLog = signal<Array<{ input: string; result: string; timestamp: number }>>([]);
|
|
|
|
|
|
2026-04-03 16:18:44 +08:00
|
|
|
// 监听 prompt 事件
|
2026-04-03 15:18:47 +08:00
|
|
|
gameContext.commands.on('prompt', (event) => {
|
|
|
|
|
promptSignal.value = event;
|
|
|
|
|
});
|
|
|
|
|
|
2026-04-03 16:18:44 +08:00
|
|
|
// 包装 run 方法以记录命令日志
|
2026-04-03 15:18:47 +08:00
|
|
|
const originalRun = gameContext.commands.run.bind(gameContext.commands);
|
|
|
|
|
(gameContext.commands as any).run = async (input: string) => {
|
|
|
|
|
const result = await originalRun(input);
|
|
|
|
|
commandLog.value = [
|
|
|
|
|
...commandLog.value,
|
|
|
|
|
{
|
|
|
|
|
input,
|
|
|
|
|
result: result.success ? `OK: ${JSON.stringify(result.result)}` : `ERR: ${result.error}`,
|
|
|
|
|
timestamp: Date.now(),
|
|
|
|
|
},
|
|
|
|
|
];
|
|
|
|
|
return result;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const sceneData: GameSceneData = {
|
|
|
|
|
state: gameContext.state,
|
|
|
|
|
commands: gameContext.commands,
|
|
|
|
|
};
|
|
|
|
|
|
2026-04-03 16:18:44 +08:00
|
|
|
function App() {
|
|
|
|
|
const [phaserReady, setPhaserReady] = useState(false);
|
|
|
|
|
const [game, setGame] = useState<Phaser.Game | null>(null);
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
const phaserConfig: Phaser.Types.Core.GameConfig = {
|
|
|
|
|
type: Phaser.AUTO,
|
|
|
|
|
width: 560,
|
|
|
|
|
height: 560,
|
|
|
|
|
parent: 'phaser-container',
|
|
|
|
|
backgroundColor: '#f9fafb',
|
|
|
|
|
scene: [],
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const phaserGame = new Phaser.Game(phaserConfig);
|
|
|
|
|
phaserGame.scene.add('GameScene', GameScene, true, sceneData);
|
2026-04-03 15:18:47 +08:00
|
|
|
|
2026-04-03 16:18:44 +08:00
|
|
|
setGame(phaserGame);
|
|
|
|
|
setPhaserReady(true);
|
2026-04-03 15:18:47 +08:00
|
|
|
|
2026-04-03 16:18:44 +08:00
|
|
|
return () => {
|
|
|
|
|
phaserGame.destroy(true);
|
|
|
|
|
};
|
|
|
|
|
}, []);
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
if (phaserReady) {
|
|
|
|
|
gameContext.commands.run('setup');
|
|
|
|
|
}
|
|
|
|
|
}, [phaserReady]);
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<div className="flex flex-col h-screen">
|
|
|
|
|
<div className="flex-1 relative">
|
|
|
|
|
<div id="phaser-container" className="w-full h-full" />
|
|
|
|
|
<PromptDialog
|
|
|
|
|
prompt={promptSignal.value}
|
|
|
|
|
onSubmit={(input: string) => {
|
|
|
|
|
gameContext.commands._tryCommit(input);
|
|
|
|
|
promptSignal.value = null;
|
|
|
|
|
}}
|
|
|
|
|
onCancel={() => {
|
|
|
|
|
gameContext.commands._cancel('cancelled');
|
|
|
|
|
promptSignal.value = null;
|
|
|
|
|
}}
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
<div className="p-4 bg-gray-100 border-t">
|
|
|
|
|
<CommandLog entries={commandLog} />
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|
2026-04-03 15:18:47 +08:00
|
|
|
|
|
|
|
|
const ui = new GameUI({
|
|
|
|
|
container: document.getElementById('ui-root')!,
|
2026-04-03 16:18:44 +08:00
|
|
|
root: <App />,
|
2026-04-03 15:18:47 +08:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
ui.mount();
|