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 作为敌人)
|
// 创建城堡牌堆 (J/Q/K 作为敌人)
|
||||||
|
// TODO 不要全部洗混,把J/Q/K分别洗混再合并,并且J先翻出K最后翻出
|
||||||
export function createCastleDeck(): EnemyCard[] {
|
export function createCastleDeck(): EnemyCard[] {
|
||||||
const suits: CardSuit[] = ['hearts', 'diamonds', 'clubs', 'spades'];
|
const suits: CardSuit[] = ['hearts', 'diamonds', 'clubs', 'spades'];
|
||||||
const castleCards: EnemyCard[] = [];
|
const castleCards: EnemyCard[] = [];
|
||||||
|
|
|
||||||
|
|
@ -247,10 +247,20 @@ registry.register('yield', yieldTurn);
|
||||||
// counterattack <cardIds> - 反击时弃牌
|
// counterattack <cardIds> - 反击时弃牌
|
||||||
async function counterattack(game: RegicideGame, cardIds: string[]) {
|
async function counterattack(game: RegicideGame, cardIds: string[]) {
|
||||||
const state = game.value;
|
const state = game.value;
|
||||||
|
|
||||||
if (state.phase !== 'enemyCounterattack') throw new Error('不是反击阶段');
|
if (state.phase !== 'enemyCounterattack') throw new Error('不是反击阶段');
|
||||||
if (!state.currentEnemy) 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[] = [];
|
const selectedCards: Card[] = [];
|
||||||
for (const cardId of cardIds) {
|
for (const cardId of cardIds) {
|
||||||
const card = state.hand.find(c => c.id === cardId);
|
const card = state.hand.find(c => c.id === cardId);
|
||||||
|
|
@ -408,6 +418,11 @@ export async function start(game: RegicideGame) {
|
||||||
const { cardIds } = await game.prompt(
|
const { cardIds } = await game.prompt(
|
||||||
prompts.counterattack,
|
prompts.counterattack,
|
||||||
(cardIds: string[]) => {
|
(cardIds: string[]) => {
|
||||||
|
// 允许空牌提交(认输)
|
||||||
|
if (cardIds.length === 0) {
|
||||||
|
return { cardIds: [], totalValue: 0 };
|
||||||
|
}
|
||||||
|
|
||||||
const selectedCards: Card[] = [];
|
const selectedCards: Card[] = [];
|
||||||
for (const cardId of cardIds) {
|
for (const cardId of cardIds) {
|
||||||
const card = state.hand.find(c => c.id === cardId);
|
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);
|
const totalValue = selectedCards.reduce((sum, c) => sum + c.value, 0);
|
||||||
if (totalValue < requiredValue) {
|
// 不再抛出错误,允许提交不足的牌,由 counterattack 处理失败逻辑
|
||||||
throw `牌的总点数 ${totalValue} 不足以抵消反击伤害 ${requiredValue}`;
|
return { cardIds, totalValue };
|
||||||
}
|
|
||||||
|
|
||||||
return { cardIds };
|
|
||||||
},
|
},
|
||||||
state.currentEnemy.id
|
state.currentEnemy.id
|
||||||
);
|
);
|
||||||
|
|
@ -444,9 +456,21 @@ export async function start(game: RegicideGame) {
|
||||||
isDefeated: false,
|
isDefeated: false,
|
||||||
};
|
};
|
||||||
state.currentEnemy = nextEnemy;
|
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.phase = 'playerTurn';
|
|
||||||
state.currentPlayed = null;
|
state.currentPlayed = null;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,8 @@ import type { RegicideState, Card, Enemy } from '@/game/types';
|
||||||
import { GameHostScene } from 'boardgame-phaser';
|
import { GameHostScene } from 'boardgame-phaser';
|
||||||
import { spawnEffect, type Spawner } from 'boardgame-phaser';
|
import { spawnEffect, type Spawner } from 'boardgame-phaser';
|
||||||
import { GameUI, CardView, EnemyView, PromptDisplay } from './views';
|
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_WIDTH = 800;
|
||||||
const GAME_HEIGHT = 700;
|
const GAME_HEIGHT = 700;
|
||||||
|
|
@ -15,6 +17,8 @@ export class GameScene extends GameHostScene<RegicideState> {
|
||||||
private promptDisplay!: PromptDisplay;
|
private promptDisplay!: PromptDisplay;
|
||||||
// 追踪当前卡牌容器,用于点击检测
|
// 追踪当前卡牌容器,用于点击检测
|
||||||
public cardContainers: Phaser.GameObjects.Container[] = [];
|
public cardContainers: Phaser.GameObjects.Container[] = [];
|
||||||
|
// 卡牌选择状态(反击阶段)
|
||||||
|
private selectedCardIds: Set<string> = new Set();
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
super('RegicideGameScene');
|
super('RegicideGameScene');
|
||||||
|
|
@ -22,6 +26,7 @@ export class GameScene extends GameHostScene<RegicideState> {
|
||||||
|
|
||||||
create(): void {
|
create(): void {
|
||||||
super.create();
|
super.create();
|
||||||
|
setGameSceneRef(this);
|
||||||
|
|
||||||
// 创建 UI 组件
|
// 创建 UI 组件
|
||||||
this.gameUI = new GameUI(this);
|
this.gameUI = new GameUI(this);
|
||||||
|
|
@ -67,12 +72,11 @@ export class GameScene extends GameHostScene<RegicideState> {
|
||||||
let view = cardViews.get(card.id);
|
let view = cardViews.get(card.id);
|
||||||
|
|
||||||
if (!view) {
|
if (!view) {
|
||||||
// 新卡牌 - 创建时先放在中心,然后用 tween 动画移到目标位置
|
|
||||||
view = new CardView(this, card, CENTER_X, HAND_Y);
|
view = new CardView(this, card, CENTER_X, HAND_Y);
|
||||||
cardViews.set(card.id, view);
|
cardViews.set(card.id, view);
|
||||||
this.cardContainers.push(view.container);
|
this.cardContainers.push(view.container);
|
||||||
|
|
||||||
// 入场动画:从中心移到目标位置
|
// 入场动画
|
||||||
this.tweens.add({
|
this.tweens.add({
|
||||||
targets: view.container,
|
targets: view.container,
|
||||||
x,
|
x,
|
||||||
|
|
@ -81,13 +85,12 @@ export class GameScene extends GameHostScene<RegicideState> {
|
||||||
ease: 'Back.easeOut',
|
ease: 'Back.easeOut',
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
// 已有卡牌 - 更新位置
|
|
||||||
view.setPosition(x, HAND_Y);
|
view.setPosition(x, HAND_Y);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// 添加状态效果
|
// 状态效果
|
||||||
this.addEffect(() => {
|
this.addEffect(() => {
|
||||||
this.gameUI.updateEnemyDisplay(this, this.state.currentEnemy);
|
this.gameUI.updateEnemyDisplay(this, this.state.currentEnemy);
|
||||||
});
|
});
|
||||||
|
|
@ -111,27 +114,106 @@ export class GameScene extends GameHostScene<RegicideState> {
|
||||||
this.promptDisplay.update(schema, player);
|
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();
|
this.setupBackgroundInput();
|
||||||
}
|
}
|
||||||
|
|
||||||
private setupBackgroundInput(): void {
|
private setupBackgroundInput(): void {
|
||||||
this.input.on('pointerdown', (pointer: Phaser.Input.Pointer) => {
|
this.input.on('pointerdown', (pointer: Phaser.Input.Pointer) => {
|
||||||
// 如果点击的是卡牌,不处理
|
|
||||||
if (this.isPointerOnCard(pointer)) {
|
if (this.isPointerOnCard(pointer)) {
|
||||||
return;
|
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) {
|
if (this.state.isGameOver) {
|
||||||
this.gameHost.start();
|
this.gameHost.start();
|
||||||
} else if (!this.state.currentEnemy && this.state.phase === 'playerTurn') {
|
} else if (!this.state.currentEnemy && this.state.phase === 'playerTurn') {
|
||||||
// 开始游戏
|
|
||||||
this.gameHost.start();
|
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 {
|
private isPointerOnCard(pointer: Phaser.Input.Pointer): boolean {
|
||||||
for (const container of this.cardContainers) {
|
for (const container of this.cardContainers) {
|
||||||
if (!container || !container.active) continue;
|
if (!container || !container.active) continue;
|
||||||
|
|
@ -147,6 +229,48 @@ export class GameScene extends GameHostScene<RegicideState> {
|
||||||
}
|
}
|
||||||
return false;
|
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 valueText: Phaser.GameObjects.Text;
|
||||||
private readonly card: Card;
|
private readonly card: Card;
|
||||||
private isHovering = false;
|
private isHovering = false;
|
||||||
|
private isSelected = false;
|
||||||
private baseY: number;
|
private baseY: number;
|
||||||
|
private selectedBorderColor = 0xfbbf24; // 金色边框表示选中
|
||||||
|
|
||||||
constructor(scene: GameScene, card: Card, x: number, y: number) {
|
constructor(scene: GameScene, card: Card, x: number, y: number) {
|
||||||
this.card = card;
|
this.card = card;
|
||||||
|
|
@ -73,7 +75,7 @@ export class CardView {
|
||||||
const scene = this.container.scene as Phaser.Scene;
|
const scene = this.container.scene as Phaser.Scene;
|
||||||
scene.tweens.add({
|
scene.tweens.add({
|
||||||
targets: this.container,
|
targets: this.container,
|
||||||
y: this.baseY + offset,
|
y: this.baseY + offset + (this.isSelected ? -15 : 0),
|
||||||
duration: 100,
|
duration: 100,
|
||||||
ease: 'Power2',
|
ease: 'Power2',
|
||||||
});
|
});
|
||||||
|
|
@ -86,13 +88,37 @@ export class CardView {
|
||||||
const scene = this.container.scene as Phaser.Scene;
|
const scene = this.container.scene as Phaser.Scene;
|
||||||
scene.tweens.add({
|
scene.tweens.add({
|
||||||
targets: this.container,
|
targets: this.container,
|
||||||
y: this.baseY,
|
y: this.baseY + (this.isSelected ? -15 : 0),
|
||||||
duration: 100,
|
duration: 100,
|
||||||
ease: 'Power2',
|
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 {
|
getCard(): Card {
|
||||||
return this.card;
|
return this.card;
|
||||||
}
|
}
|
||||||
|
|
@ -114,7 +140,11 @@ export class CardView {
|
||||||
if (scene.state.phase === 'playerTurn') {
|
if (scene.state.phase === 'playerTurn') {
|
||||||
scene.gameHost.tryAnswerPrompt(prompts.playerAction, 'play', [this.card.id]);
|
scene.gameHost.tryAnswerPrompt(prompts.playerAction, 'play', [this.card.id]);
|
||||||
} else if (scene.state.phase === 'enemyCounterattack') {
|
} 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 { createGameHost, type GameModule } from 'boardgame-core';
|
||||||
import Phaser from 'phaser';
|
import Phaser from 'phaser';
|
||||||
import { h } from 'preact';
|
import { h } from 'preact';
|
||||||
import { PhaserGame, PhaserScene } from 'boardgame-phaser';
|
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 } }) {
|
export default function App<TState extends Record<string, unknown>>(props: { gameModule: GameModule<TState>, gameScene: { new(): Phaser.Scene } }) {
|
||||||
|
|
||||||
const gameHost = useComputed(() => {
|
const gameHost = useComputed(() => {
|
||||||
|
|
@ -18,10 +62,24 @@ export default function App<TState extends Record<string, unknown>>(props: { gam
|
||||||
console.log('Game finished!', result);
|
console.log('Game finished!', result);
|
||||||
};
|
};
|
||||||
|
|
||||||
const label = useComputed(() =>
|
const label = useComputed(() =>
|
||||||
gameHost.value.gameHost.status.value === 'running' ? '重新开始' : '开始游戏'
|
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 画布配置
|
// Phaser 画布配置
|
||||||
const phaserConfig = {
|
const phaserConfig = {
|
||||||
type: Phaser.AUTO,
|
type: Phaser.AUTO,
|
||||||
|
|
@ -40,16 +98,111 @@ export default function App<TState extends Record<string, unknown>>(props: { gam
|
||||||
</div>
|
</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="text-sm text-gray-400">
|
{/* 牌堆信息 */}
|
||||||
⚔️ Regicide - 击败所有12个敌人
|
<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>
|
||||||
<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>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue