boardgame-phaser/packages/regicide-game/src/ui/App.tsx

210 lines
7.7 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { useComputed, signal } from '@preact/signals';
import { createGameHost, type GameModule } from 'boardgame-core';
import Phaser from 'phaser';
import { h } from 'preact';
import { PhaserGame, PhaserScene } from 'boardgame-phaser';
// 全局信号:从 GameScene 传递反击阶段的状态到 UI
export const counterattackInfo = signal<{
phase: string;
selectedCards: string[];
totalValue: number;
maxHandValue: number; // 手牌最大可提供的点数
requiredValue: number;
canSubmit: boolean;
canWin: boolean; // 手牌是否足以获胜
error: string | null;
}>({
phase: '',
selectedCards: [],
totalValue: 0,
maxHandValue: 0,
requiredValue: 0,
canSubmit: false,
canWin: false,
error: null,
});
// 全局信号:牌堆信息
export const deckInfo = signal<{
castleDeck: number; // 敌人牌堆剩余
tavernDeck: number; // 酒馆牌堆剩余
discardPile: number; // 弃牌堆数量
hand: number; // 手牌数
defeatedEnemies: number; // 已击败敌人数
jestersUsed: number; // 已用小丑牌数
}>({
castleDeck: 0,
tavernDeck: 0,
discardPile: 0,
hand: 0,
defeatedEnemies: 0,
jestersUsed: 0,
});
// 存储当前场景引用(由 GameScene 在 create 时设置)
let currentGameScene: any = null;
export function setGameSceneRef(scene: any) {
currentGameScene = scene;
}
export default function App<TState extends Record<string, unknown>>(props: { gameModule: GameModule<TState>, gameScene: { new(): Phaser.Scene } }) {
const gameHost = useComputed(() => {
const gameHost = createGameHost(props.gameModule);
return { gameHost };
});
const scene = useComputed(() => new props.gameScene());
const handleReset = async () => {
const result = await gameHost.value.gameHost.start();
console.log('Game finished!', result);
};
const label = useComputed(() =>
gameHost.value.gameHost.status.value === 'running' ? '重新开始' : '开始游戏'
);
// 反击阶段提交
const handleSubmitCounterattack = () => {
if (currentGameScene && typeof currentGameScene.submitCounterattack === 'function') {
currentGameScene.submitCounterattack();
}
};
// 反击阶段认输
const handleSurrender = () => {
if (currentGameScene && typeof currentGameScene.surrenderCounterattack === 'function') {
currentGameScene.surrenderCounterattack();
}
};
// Phaser 画布配置
const phaserConfig = {
type: Phaser.AUTO,
width: 800,
height: 700,
backgroundColor: '#111827',
};
return (
<div class="flex flex-col h-screen bg-gray-900">
{/* Phaser 游戏场景 */}
<div class="flex-1 relative flex items-center justify-center">
<PhaserGame config={phaserConfig}>
<PhaserScene sceneKey="RegicideGameScene" scene={scene.value} autoStart data={gameHost.value} />
</PhaserGame>
</div>
{/* 底部控制栏 */}
<div class="p-4 bg-gray-900 border-t border-gray-700 flex flex-col gap-3">
{/* 牌堆信息 */}
<div class="flex items-center justify-center gap-6 text-sm">
<div class="flex items-center gap-1.5">
<span class="text-lg">🏰</span>
<span class="text-gray-400">:</span>
<span class="text-purple-400 font-bold">{deckInfo.value.castleDeck}</span>
</div>
<div class="flex items-center gap-1.5">
<span class="text-lg">🍺</span>
<span class="text-gray-400">:</span>
<span class="text-amber-400 font-bold">{deckInfo.value.tavernDeck}</span>
</div>
<div class="flex items-center gap-1.5">
<span class="text-lg">🗑</span>
<span class="text-gray-400">:</span>
<span class="text-gray-300 font-bold">{deckInfo.value.discardPile}</span>
</div>
<div class="flex items-center gap-1.5">
<span class="text-lg">🃏</span>
<span class="text-gray-400">:</span>
<span class="text-blue-400 font-bold">{deckInfo.value.hand}</span>
</div>
<div class="flex items-center gap-1.5">
<span class="text-lg">💀</span>
<span class="text-gray-400">:</span>
<span class="text-red-400 font-bold">{deckInfo.value.defeatedEnemies}/12</span>
</div>
<div class="flex items-center gap-1.5">
<span class="text-lg">🃏</span>
<span class="text-gray-400">:</span>
<span class="text-green-400 font-bold">{2 - deckInfo.value.jestersUsed}/2</span>
</div>
</div>
{/* 反击阶段信息 + 按钮 */}
<div class="flex justify-between items-center">
<div class="text-sm text-gray-400">
Regicide - 12
</div>
{/* 反击阶段信息面板 */}
{counterattackInfo.value.phase === 'enemyCounterattack' && (
<div class="flex items-center gap-4">
<div class="text-sm text-gray-300">
<span class="text-yellow-400 font-bold">💥 </span>
<span class="ml-2">: </span>
<span class="text-red-400 font-bold">{counterattackInfo.value.requiredValue}</span>
<span class="ml-2">| : </span>
<span class={counterattackInfo.value.canWin ? 'text-green-400 font-bold' : 'text-red-400 font-bold'}>
{counterattackInfo.value.maxHandValue}
</span>
</div>
{counterattackInfo.value.selectedCards.length > 0 && (
<div class="text-sm text-gray-300">
<span>: </span>
<span class="text-blue-400 font-bold">{counterattackInfo.value.selectedCards.length}</span>
<span class="ml-2">: </span>
<span class={counterattackInfo.value.canSubmit ? 'text-green-400 font-bold' : 'text-orange-400 font-bold'}>
{counterattackInfo.value.totalValue}
</span>
</div>
)}
{counterattackInfo.value.error && (
<div class="text-sm text-red-400">
{counterattackInfo.value.error}
</div>
)}
</div>
)}
<div class="flex gap-2">
{/* 反击确认按钮 */}
{counterattackInfo.value.phase === 'enemyCounterattack' && counterattackInfo.value.selectedCards.length > 0 && (
<button
onClick={handleSubmitCounterattack}
disabled={!counterattackInfo.value.canSubmit}
class={`px-4 py-2 rounded font-medium transition-colors ${
counterattackInfo.value.canSubmit
? 'bg-green-600 text-white hover:bg-green-700'
: 'bg-gray-600 text-gray-400 cursor-not-allowed'
}`}
>
</button>
)}
{/* 认输按钮 */}
{counterattackInfo.value.phase === 'enemyCounterattack' && !counterattackInfo.value.canWin && (
<button
onClick={handleSurrender}
class="px-4 py-2 bg-red-600 text-white rounded hover:bg-red-700 transition-colors font-medium"
>
🏳
</button>
)}
<button
onClick={handleReset}
class="px-6 py-2 bg-yellow-600 text-white rounded hover:bg-yellow-700 disabled:bg-gray-600 disabled:cursor-not-allowed transition-colors font-medium"
>
{label}
</button>
</div>
</div>
</div>
</div>
);
}