boardgame-phaser/packages/sample-game/src/main.tsx

168 lines
5.1 KiB
TypeScript
Raw Normal View History

2026-04-03 15:18:47 +08:00
import { h, render } from 'preact';
import { signal } from '@preact/signals-core';
2026-04-03 19:39:07 +08:00
import { useEffect, useState, useCallback } from 'preact/hooks';
2026-04-03 15:18:47 +08:00
import Phaser from 'phaser';
import { createGameHost } from 'boardgame-core';
import { GameUI, PromptDialog, CommandLog } from 'boardgame-phaser';
2026-04-03 15:18:47 +08:00
import { createInitialState, registry, type TicTacToeState } from './game/tic-tac-toe';
import { GameScene } from './scenes/GameScene';
2026-04-03 15:18:47 +08:00
import './style.css';
// 创建 GameHost 实例,自动管理状态和 prompt
const gameHost = createGameHost(
{ registry, createInitialState },
'setup',
{ autoStart: false }
);
2026-04-03 15:18:47 +08:00
const commandLog = signal<Array<{ input: string; result: string; timestamp: number }>>([]);
2026-04-03 19:39:07 +08:00
// 记录命令日志的辅助函数
function logCommand(input: string, result: { success: boolean; result?: unknown; error?: string }) {
2026-04-03 15:18:47 +08:00
commandLog.value = [
...commandLog.value,
{
input,
result: result.success ? `OK: ${JSON.stringify(result.result)}` : `ERR: ${result.error}`,
timestamp: Date.now(),
},
];
2026-04-03 19:39:07 +08:00
}
2026-04-03 15:18:47 +08:00
2026-04-03 16:18:44 +08:00
function App() {
const [phaserReady, setPhaserReady] = useState(false);
const [game, setGame] = useState<Phaser.Game | null>(null);
2026-04-03 19:39:07 +08:00
const [scene, setScene] = useState<GameScene | null>(null);
const [gameState, setGameState] = useState<TicTacToeState | null>(null);
const [promptSchema, setPromptSchema] = useState<any>(null);
2026-04-03 16:18:44 +08:00
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);
// 通过 init 传递 gameHost
2026-04-03 19:39:07 +08:00
const gameScene = new GameScene();
phaserGame.scene.add('GameScene', gameScene, true, { gameHost });
2026-04-03 15:18:47 +08:00
2026-04-03 16:18:44 +08:00
setGame(phaserGame);
2026-04-03 19:39:07 +08:00
setScene(gameScene);
2026-04-03 16:18:44 +08:00
setPhaserReady(true);
2026-04-03 15:18:47 +08:00
2026-04-03 16:18:44 +08:00
return () => {
gameHost.dispose();
2026-04-03 16:18:44 +08:00
phaserGame.destroy(true);
};
}, []);
useEffect(() => {
2026-04-03 19:39:07 +08:00
if (phaserReady && scene) {
// 监听 prompt 状态变化
const disposePromptSchema = gameHost.activePromptSchema.subscribe((schema) => {
setPromptSchema(schema);
scene.promptSchema.current = schema;
2026-04-04 00:16:30 +08:00
});
2026-04-03 19:39:07 +08:00
// 监听状态变化
const disposeState = gameHost.state.subscribe(() => {
setGameState(gameHost.state.value as TicTacToeState);
2026-04-03 19:39:07 +08:00
});
// 运行游戏设置
gameHost.setup('setup').then(() => {
logCommand('setup', { success: true });
}).catch(err => {
logCommand('setup', { success: false, error: err.message });
2026-04-03 19:39:07 +08:00
});
return () => {
disposePromptSchema();
disposeState();
2026-04-03 19:39:07 +08:00
};
}
}, [phaserReady, scene]);
const handlePromptSubmit = useCallback((input: string) => {
const error = gameHost.onInput(input);
if (error === null) {
logCommand(input, { success: true });
setPromptSchema(null);
if (scene) {
scene.promptSchema.current = null;
}
} else {
logCommand(input, { success: false, error });
2026-04-03 19:39:07 +08:00
}
}, []);
const handlePromptCancel = useCallback(() => {
gameHost.commands._cancel('User cancelled');
setPromptSchema(null);
if (scene) {
scene.promptSchema.current = null;
2026-04-03 16:18:44 +08:00
}
2026-04-03 19:39:07 +08:00
}, []);
const handleReset = useCallback(() => {
gameHost.commands.run('reset').then(result => {
2026-04-03 19:39:07 +08:00
logCommand('reset', result);
});
}, []);
2026-04-03 16:18:44 +08:00
return (
<div className="flex flex-col h-screen">
<div className="flex-1 relative">
<div id="phaser-container" className="w-full h-full" />
2026-04-03 19:39:07 +08:00
{/* 游戏状态显示 */}
{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">
<span className="text-lg font-semibold text-gray-800">
{gameState.currentPlayer}'s Turn
</span>
</div>
)}
2026-04-03 19:39:07 +08:00
{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">
<span className="text-lg font-semibold text-yellow-600">
{gameState.winner === 'draw' ? "It's a Draw!" : `${gameState.winner} Wins!`}
</span>
</div>
)}
2026-04-03 16:18:44 +08:00
<PromptDialog
prompt={promptSchema ? { schema: promptSchema, tryCommit: () => null, cancel: () => {} } : null}
2026-04-03 19:39:07 +08:00
onSubmit={handlePromptSubmit}
onCancel={handlePromptCancel}
2026-04-03 16:18:44 +08:00
/>
</div>
<div className="p-4 bg-gray-100 border-t">
2026-04-03 19:39:07 +08:00
<div className="flex justify-between items-center mb-2">
<span className="text-sm font-medium text-gray-700">Command Log</span>
<button
className="px-3 py-1 text-xs font-medium text-white bg-blue-600 rounded-md hover:bg-blue-700"
onClick={handleReset}
>
Reset Game
</button>
</div>
2026-04-03 16:18:44 +08:00
<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();