refactor: more regicide stuff

This commit is contained in:
2026-04-06 17:17:04 +08:00
parent a0412ddd88
commit cedb69e55e
5 changed files with 360 additions and 28 deletions
+131 -7
View File
@@ -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);
}
});
}