refactor: more regicide stuff
This commit is contained in:
@@ -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);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user