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

165 lines
5.1 KiB
TypeScript
Raw Normal View History

2026-04-03 15:18:47 +08:00
import { h, render } from 'preact';
2026-04-03 19:39:07 +08:00
import { signal, computed } from '@preact/signals-core';
import { useEffect, useState, useCallback } from 'preact/hooks';
2026-04-03 15:18:47 +08:00
import Phaser from 'phaser';
import { createGameContext } from 'boardgame-core';
2026-04-03 19:39:07 +08:00
import { GameUI, PromptDialog, CommandLog, createPromptHandler } 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';
const gameContext = createGameContext<TicTacToeState>(registry, createInitialState);
const commandLog = signal<Array<{ input: string; result: string; timestamp: number }>>([]);
2026-04-03 19:39:07 +08:00
// 创建 PromptHandler 用于处理 UI 层的 prompt
let promptHandler: ReturnType<typeof createPromptHandler<TicTacToeState>> | null = null;
const promptSignal = signal<null | Awaited<ReturnType<typeof gameContext.commands.promptQueue.pop>>>(null);
2026-04-03 15:18:47 +08:00
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);
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 传递 gameContext
2026-04-03 19:39:07 +08:00
const gameScene = new GameScene();
phaserGame.scene.add('GameScene', gameScene, true, { gameContext });
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 () => {
phaserGame.destroy(true);
};
}, []);
useEffect(() => {
2026-04-03 19:39:07 +08:00
if (phaserReady && scene) {
// 初始化 PromptHandler
promptHandler = createPromptHandler(scene, gameContext.commands, {
onPrompt: (prompt) => {
promptSignal.value = prompt;
},
onCancel: () => {
promptSignal.value = null;
},
});
promptHandler.start();
// 监听状态变化
const dispose = gameContext.state.subscribe(() => {
setGameState({ ...gameContext.state.value });
});
// 运行游戏设置
gameContext.commands.run('setup').then(result => {
logCommand('setup', result);
});
return () => {
dispose();
};
}
}, [phaserReady, scene]);
const handlePromptSubmit = useCallback((input: string) => {
if (promptHandler) {
const error = promptHandler.submit(input);
if (error === null) {
logCommand(input, { success: true });
promptSignal.value = null;
} else {
logCommand(input, { success: false, error });
}
}
}, []);
const handlePromptCancel = useCallback(() => {
if (promptHandler) {
promptHandler.cancel('User cancelled');
promptSignal.value = null;
2026-04-03 16:18:44 +08:00
}
2026-04-03 19:39:07 +08:00
}, []);
const handleReset = useCallback(() => {
gameContext.commands.run('reset').then(result => {
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>
)}
{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={promptSignal.value}
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();