refactor: more regicide stuff
This commit is contained in:
parent
a0412ddd88
commit
cedb69e55e
|
|
@ -32,6 +32,7 @@ export function getCardValue(rank: CardRank): number {
|
|||
}
|
||||
|
||||
// 创建城堡牌堆 (J/Q/K 作为敌人)
|
||||
// TODO 不要全部洗混,把J/Q/K分别洗混再合并,并且J先翻出K最后翻出
|
||||
export function createCastleDeck(): EnemyCard[] {
|
||||
const suits: CardSuit[] = ['hearts', 'diamonds', 'clubs', 'spades'];
|
||||
const castleCards: EnemyCard[] = [];
|
||||
|
|
|
|||
|
|
@ -251,6 +251,16 @@ async function counterattack(game: RegicideGame, cardIds: string[]) {
|
|||
if (state.phase !== 'enemyCounterattack') throw new Error('不是反击阶段');
|
||||
if (!state.currentEnemy) throw new Error('没有活跃的敌人');
|
||||
|
||||
// 空牌提交 = 认输
|
||||
if (cardIds.length === 0) {
|
||||
game.produce((state: RegicideState) => {
|
||||
state.isGameOver = true;
|
||||
state.victoryLevel = null;
|
||||
state.phase = 'gameOver';
|
||||
});
|
||||
return { success: false, required: state.currentEnemy.counterDamage, available: 0, isSurrender: true };
|
||||
}
|
||||
|
||||
const selectedCards: Card[] = [];
|
||||
for (const cardId of cardIds) {
|
||||
const card = state.hand.find(c => c.id === cardId);
|
||||
|
|
@ -408,6 +418,11 @@ export async function start(game: RegicideGame) {
|
|||
const { cardIds } = await game.prompt(
|
||||
prompts.counterattack,
|
||||
(cardIds: string[]) => {
|
||||
// 允许空牌提交(认输)
|
||||
if (cardIds.length === 0) {
|
||||
return { cardIds: [], totalValue: 0 };
|
||||
}
|
||||
|
||||
const selectedCards: Card[] = [];
|
||||
for (const cardId of cardIds) {
|
||||
const card = state.hand.find(c => c.id === cardId);
|
||||
|
|
@ -418,11 +433,8 @@ export async function start(game: RegicideGame) {
|
|||
}
|
||||
|
||||
const totalValue = selectedCards.reduce((sum, c) => sum + c.value, 0);
|
||||
if (totalValue < requiredValue) {
|
||||
throw `牌的总点数 ${totalValue} 不足以抵消反击伤害 ${requiredValue}`;
|
||||
}
|
||||
|
||||
return { cardIds };
|
||||
// 不再抛出错误,允许提交不足的牌,由 counterattack 处理失败逻辑
|
||||
return { cardIds, totalValue };
|
||||
},
|
||||
state.currentEnemy.id
|
||||
);
|
||||
|
|
@ -444,9 +456,21 @@ export async function start(game: RegicideGame) {
|
|||
isDefeated: false,
|
||||
};
|
||||
state.currentEnemy = nextEnemy;
|
||||
}
|
||||
}
|
||||
state.phase = 'playerTurn';
|
||||
} else {
|
||||
// 城堡牌堆没有有效的敌人卡牌,游戏胜利
|
||||
state.isGameOver = true;
|
||||
state.victoryLevel = calculateVictoryLevel(state.jestersUsed);
|
||||
state.currentEnemy = null;
|
||||
state.phase = 'gameOver';
|
||||
}
|
||||
} else {
|
||||
// 城堡牌堆已空,游戏胜利
|
||||
state.isGameOver = true;
|
||||
state.victoryLevel = calculateVictoryLevel(state.jestersUsed);
|
||||
state.currentEnemy = null;
|
||||
state.phase = 'gameOver';
|
||||
}
|
||||
state.currentPlayed = null;
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,6 +3,8 @@ import type { RegicideState, Card, Enemy } from '@/game/types';
|
|||
import { GameHostScene } from 'boardgame-phaser';
|
||||
import { spawnEffect, type Spawner } from 'boardgame-phaser';
|
||||
import { GameUI, CardView, EnemyView, PromptDisplay } from './views';
|
||||
import { prompts } from '@/game/regicide';
|
||||
import { counterattackInfo, setGameSceneRef, deckInfo } from '@/ui/App';
|
||||
|
||||
const GAME_WIDTH = 800;
|
||||
const GAME_HEIGHT = 700;
|
||||
|
|
@ -15,6 +17,8 @@ export class GameScene extends GameHostScene<RegicideState> {
|
|||
private promptDisplay!: PromptDisplay;
|
||||
// 追踪当前卡牌容器,用于点击检测
|
||||
public cardContainers: Phaser.GameObjects.Container[] = [];
|
||||
// 卡牌选择状态(反击阶段)
|
||||
private selectedCardIds: Set<string> = new Set();
|
||||
|
||||
constructor() {
|
||||
super('RegicideGameScene');
|
||||
|
|
@ -22,6 +26,7 @@ export class GameScene extends GameHostScene<RegicideState> {
|
|||
|
||||
create(): void {
|
||||
super.create();
|
||||
setGameSceneRef(this);
|
||||
|
||||
// 创建 UI 组件
|
||||
this.gameUI = new GameUI(this);
|
||||
|
|
@ -67,12 +72,11 @@ export class GameScene extends GameHostScene<RegicideState> {
|
|||
let view = cardViews.get(card.id);
|
||||
|
||||
if (!view) {
|
||||
// 新卡牌 - 创建时先放在中心,然后用 tween 动画移到目标位置
|
||||
view = new CardView(this, card, CENTER_X, HAND_Y);
|
||||
cardViews.set(card.id, view);
|
||||
this.cardContainers.push(view.container);
|
||||
|
||||
// 入场动画:从中心移到目标位置
|
||||
// 入场动画
|
||||
this.tweens.add({
|
||||
targets: view.container,
|
||||
x,
|
||||
|
|
@ -81,13 +85,12 @@ export class GameScene extends GameHostScene<RegicideState> {
|
|||
ease: 'Back.easeOut',
|
||||
});
|
||||
} else {
|
||||
// 已有卡牌 - 更新位置
|
||||
view.setPosition(x, HAND_Y);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// 添加状态效果
|
||||
// 状态效果
|
||||
this.addEffect(() => {
|
||||
this.gameUI.updateEnemyDisplay(this, this.state.currentEnemy);
|
||||
});
|
||||
|
|
@ -111,27 +114,106 @@ export class GameScene extends GameHostScene<RegicideState> {
|
|||
this.promptDisplay.update(schema, player);
|
||||
});
|
||||
|
||||
// 监听阶段变化,更新反击信息
|
||||
this.addEffect(() => {
|
||||
const phase = this.state.phase;
|
||||
const enemy = this.state.currentEnemy;
|
||||
const totalValue = this.getTotalSelectedCardValue();
|
||||
const required = enemy?.counterDamage ?? 0;
|
||||
const maxHandValue = this.getMaxHandValue();
|
||||
|
||||
counterattackInfo.value = {
|
||||
phase,
|
||||
selectedCards: Array.from(this.selectedCardIds),
|
||||
totalValue,
|
||||
maxHandValue,
|
||||
requiredValue: required,
|
||||
canSubmit: this.selectedCardIds.size > 0 && totalValue >= required,
|
||||
canWin: maxHandValue >= required,
|
||||
error: null,
|
||||
};
|
||||
|
||||
if (phase !== 'enemyCounterattack') {
|
||||
this.selectedCardIds.clear();
|
||||
}
|
||||
});
|
||||
|
||||
// 监听牌堆变化,更新牌堆信息
|
||||
this.addEffect(() => {
|
||||
const state = this.state;
|
||||
deckInfo.value = {
|
||||
castleDeck: state.castleDeck.length,
|
||||
tavernDeck: state.tavernDeck.length,
|
||||
discardPile: state.discardPile.length,
|
||||
hand: state.hand.length,
|
||||
defeatedEnemies: state.defeatedEnemies.length,
|
||||
jestersUsed: state.jestersUsed,
|
||||
};
|
||||
});
|
||||
|
||||
// 设置背景点击
|
||||
this.setupBackgroundInput();
|
||||
}
|
||||
|
||||
private setupBackgroundInput(): void {
|
||||
this.input.on('pointerdown', (pointer: Phaser.Input.Pointer) => {
|
||||
// 如果点击的是卡牌,不处理
|
||||
if (this.isPointerOnCard(pointer)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 游戏结束时点击任意位置重新开始
|
||||
// 点击空白区域取消选中(反击阶段)
|
||||
if (this.state.phase === 'enemyCounterattack' && this.selectedCardIds.size > 0) {
|
||||
this.selectedCardIds.clear();
|
||||
for (const container of this.cardContainers) {
|
||||
const view = container.getData('cardView') as CardView | undefined;
|
||||
if (view) view.setSelected(false, this);
|
||||
}
|
||||
this.updateCounterattackInfo();
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.state.isGameOver) {
|
||||
this.gameHost.start();
|
||||
} else if (!this.state.currentEnemy && this.state.phase === 'playerTurn') {
|
||||
// 开始游戏
|
||||
this.gameHost.start();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 公共方法:供 Preact UI 调用提交反击
|
||||
submitCounterattack(): void {
|
||||
const cardIds = Array.from(this.selectedCardIds);
|
||||
if (cardIds.length === 0) return;
|
||||
|
||||
const error = this.gameHost.tryAnswerPrompt(prompts.counterattack, cardIds);
|
||||
if (error) {
|
||||
counterattackInfo.value = {
|
||||
...counterattackInfo.value,
|
||||
error,
|
||||
};
|
||||
}
|
||||
|
||||
this.selectedCardIds.clear();
|
||||
for (const container of this.cardContainers) {
|
||||
const view = container.getData('cardView') as CardView | undefined;
|
||||
if (view) view.setSelected(false, this);
|
||||
}
|
||||
this.updateCounterattackInfo();
|
||||
}
|
||||
|
||||
// 公共方法:反击阶段认输
|
||||
surrenderCounterattack(): void {
|
||||
// 直接提交空牌,触发游戏结束
|
||||
this.gameHost.tryAnswerPrompt(prompts.counterattack, []);
|
||||
|
||||
this.selectedCardIds.clear();
|
||||
for (const container of this.cardContainers) {
|
||||
const view = container.getData('cardView') as CardView | undefined;
|
||||
if (view) view.setSelected(false, this);
|
||||
}
|
||||
this.updateCounterattackInfo();
|
||||
}
|
||||
|
||||
private isPointerOnCard(pointer: Phaser.Input.Pointer): boolean {
|
||||
for (const container of this.cardContainers) {
|
||||
if (!container || !container.active) continue;
|
||||
|
|
@ -147,6 +229,48 @@ export class GameScene extends GameHostScene<RegicideState> {
|
|||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// 卡牌点击回调(反击阶段)
|
||||
onCardClicked(cardView: CardView): void {
|
||||
const card = cardView.getCard();
|
||||
if (this.selectedCardIds.has(card.id)) {
|
||||
this.selectedCardIds.delete(card.id);
|
||||
} else {
|
||||
this.selectedCardIds.add(card.id);
|
||||
}
|
||||
this.updateCounterattackInfo();
|
||||
}
|
||||
|
||||
private updateCounterattackInfo(): void {
|
||||
const enemy = this.state.currentEnemy;
|
||||
const totalValue = this.getTotalSelectedCardValue();
|
||||
const required = enemy?.counterDamage ?? 0;
|
||||
const maxHandValue = this.getMaxHandValue();
|
||||
|
||||
counterattackInfo.value = {
|
||||
phase: this.state.phase,
|
||||
selectedCards: Array.from(this.selectedCardIds),
|
||||
totalValue,
|
||||
maxHandValue,
|
||||
requiredValue: required,
|
||||
canSubmit: this.selectedCardIds.size > 0 && totalValue >= required,
|
||||
canWin: maxHandValue >= required,
|
||||
error: null,
|
||||
};
|
||||
}
|
||||
|
||||
private getTotalSelectedCardValue(): number {
|
||||
let total = 0;
|
||||
for (const id of this.selectedCardIds) {
|
||||
const card = this.state.hand.find(c => c.id === id);
|
||||
if (card) total += card.value;
|
||||
}
|
||||
return total;
|
||||
}
|
||||
|
||||
private getMaxHandValue(): number {
|
||||
return this.state.hand.reduce((sum, c) => sum + c.value, 0);
|
||||
}
|
||||
}
|
||||
|
||||
// 敌人生成器
|
||||
|
|
|
|||
|
|
@ -14,7 +14,9 @@ export class CardView {
|
|||
private readonly valueText: Phaser.GameObjects.Text;
|
||||
private readonly card: Card;
|
||||
private isHovering = false;
|
||||
private isSelected = false;
|
||||
private baseY: number;
|
||||
private selectedBorderColor = 0xfbbf24; // 金色边框表示选中
|
||||
|
||||
constructor(scene: GameScene, card: Card, x: number, y: number) {
|
||||
this.card = card;
|
||||
|
|
@ -73,7 +75,7 @@ export class CardView {
|
|||
const scene = this.container.scene as Phaser.Scene;
|
||||
scene.tweens.add({
|
||||
targets: this.container,
|
||||
y: this.baseY + offset,
|
||||
y: this.baseY + offset + (this.isSelected ? -15 : 0),
|
||||
duration: 100,
|
||||
ease: 'Power2',
|
||||
});
|
||||
|
|
@ -86,13 +88,37 @@ export class CardView {
|
|||
const scene = this.container.scene as Phaser.Scene;
|
||||
scene.tweens.add({
|
||||
targets: this.container,
|
||||
y: this.baseY,
|
||||
y: this.baseY + (this.isSelected ? -15 : 0),
|
||||
duration: 100,
|
||||
ease: 'Power2',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
setSelected(selected: boolean, scene: GameScene): void {
|
||||
if (this.isSelected === selected) return;
|
||||
this.isSelected = selected;
|
||||
|
||||
// 更新边框颜色
|
||||
const color = selected ? this.selectedBorderColor : getSuitColor(this.card.suit);
|
||||
this.bg.setStrokeStyle(3, Phaser.Display.Color.HexStringToColor(
|
||||
selected ? '#fbbf24' : getSuitColor(this.card.suit)
|
||||
).color);
|
||||
|
||||
// 更新位置
|
||||
const targetY = this.baseY + (selected ? -15 : 0);
|
||||
scene.tweens.add({
|
||||
targets: this.container,
|
||||
y: targetY,
|
||||
duration: 100,
|
||||
ease: 'Power2',
|
||||
});
|
||||
}
|
||||
|
||||
getSelected(): boolean {
|
||||
return this.isSelected;
|
||||
}
|
||||
|
||||
getCard(): Card {
|
||||
return this.card;
|
||||
}
|
||||
|
|
@ -114,7 +140,11 @@ export class CardView {
|
|||
if (scene.state.phase === 'playerTurn') {
|
||||
scene.gameHost.tryAnswerPrompt(prompts.playerAction, 'play', [this.card.id]);
|
||||
} else if (scene.state.phase === 'enemyCounterattack') {
|
||||
scene.gameHost.tryAnswerPrompt(prompts.counterattack, [this.card.id]);
|
||||
// 切换选中状态
|
||||
const newSelected = !this.isSelected;
|
||||
this.setSelected(newSelected, scene);
|
||||
// 通知 GameScene 更新选中状态
|
||||
scene.onCardClicked(this);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,9 +1,53 @@
|
|||
import { useComputed } from '@preact/signals';
|
||||
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(() => {
|
||||
|
|
@ -22,6 +66,20 @@ export default function App<TState extends Record<string, unknown>>(props: { gam
|
|||
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,
|
||||
|
|
@ -40,10 +98,103 @@ export default function App<TState extends Record<string, unknown>>(props: { gam
|
|||
</div>
|
||||
|
||||
{/* 底部控制栏 */}
|
||||
<div class="p-4 bg-gray-900 border-t border-gray-700 flex justify-between items-center">
|
||||
<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"
|
||||
|
|
@ -52,5 +203,7 @@ export default function App<TState extends Record<string, unknown>>(props: { gam
|
|||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue