refactor: fix regicide, presumably?

This commit is contained in:
2026-04-06 16:45:52 +08:00
parent 20f722818d
commit a0412ddd88
14 changed files with 1248 additions and 62 deletions
@@ -0,0 +1,197 @@
import Phaser from 'phaser';
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';
const GAME_WIDTH = 800;
const GAME_HEIGHT = 700;
const CENTER_X = GAME_WIDTH / 2;
const HAND_Y = 580;
const CARD_SPACING = 70;
export class GameScene extends GameHostScene<RegicideState> {
private gameUI!: GameUI;
private promptDisplay!: PromptDisplay;
// 追踪当前卡牌容器,用于点击检测
public cardContainers: Phaser.GameObjects.Container[] = [];
constructor() {
super('RegicideGameScene');
}
create(): void {
super.create();
// 创建 UI 组件
this.gameUI = new GameUI(this);
this.promptDisplay = new PromptDisplay(this);
// 生成效果
this.disposables.add(spawnEffect(new EnemySpawner(this)));
// 手牌管理 - 手动跟踪卡牌容器
const cardViews = new Map<string, CardView>();
this.addEffect(() => {
const hand = this.state.hand;
const handSize = hand.length;
const spacing = Math.min(CARD_SPACING, 600 / Math.max(1, handSize));
const startX = CENTER_X - (handSize - 1) * spacing / 2;
const currentIds = new Set(hand.map(c => c.id));
// 移除不存在的卡牌
for (const [id, view] of cardViews) {
if (!currentIds.has(id)) {
this.tweens.add({
targets: view.container,
alpha: 0,
scale: 0.5,
y: view.container.y - 100,
duration: 200,
ease: 'Back.easeIn',
onComplete: () => {
const idx = this.cardContainers.indexOf(view.container);
if (idx !== -1) this.cardContainers.splice(idx, 1);
view.destroy();
},
});
cardViews.delete(id);
}
}
// 添加或更新卡牌
for (let i = 0; i < handSize; i++) {
const card = hand[i];
const x = startX + i * spacing;
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,
y: HAND_Y,
duration: 300,
ease: 'Back.easeOut',
});
} else {
// 已有卡牌 - 更新位置
view.setPosition(x, HAND_Y);
}
}
});
// 添加状态效果
this.addEffect(() => {
this.gameUI.updateEnemyDisplay(this, this.state.currentEnemy);
});
this.addEffect(() => {
this.gameUI.updatePhaseText(this.state.phase);
});
this.addEffect(() => {
if (this.state.isGameOver) {
this.gameUI.showGameOver(this, this.state.victoryLevel);
} else {
this.gameUI.hideGameOver();
}
});
// 监听 activePrompt 信号
this.addEffect(() => {
const schema = this.gameHost.activePromptSchema?.value;
const player = this.gameHost.activePromptPlayer?.value;
this.promptDisplay.update(schema, player);
});
// 设置背景点击
this.setupBackgroundInput();
}
private setupBackgroundInput(): void {
this.input.on('pointerdown', (pointer: Phaser.Input.Pointer) => {
// 如果点击的是卡牌,不处理
if (this.isPointerOnCard(pointer)) {
return;
}
// 游戏结束时点击任意位置重新开始
if (this.state.isGameOver) {
this.gameHost.start();
} else if (!this.state.currentEnemy && this.state.phase === 'playerTurn') {
// 开始游戏
this.gameHost.start();
}
});
}
private isPointerOnCard(pointer: Phaser.Input.Pointer): boolean {
for (const container of this.cardContainers) {
if (!container || !container.active) continue;
const bounds = container.getBounds();
if (
pointer.x >= bounds.left &&
pointer.x <= bounds.right &&
pointer.y >= bounds.top &&
pointer.y <= bounds.bottom
) {
return true;
}
}
return false;
}
}
// 敌人生成器
class EnemySpawner implements Spawner<Enemy, Phaser.GameObjects.Container> {
constructor(public readonly scene: GameScene) {}
*getData() {
if (this.scene.state.currentEnemy && !this.scene.state.currentEnemy.isDefeated) {
yield this.scene.state.currentEnemy;
}
}
getKey(enemy: Enemy): string {
return enemy.id;
}
onUpdate(enemy: Enemy, container: Phaser.GameObjects.Container): void {
const view = container.getData('enemyView') as EnemyView | undefined;
if (view) {
view.setPosition(CENTER_X, 200);
view.updateEnemy(enemy);
}
}
onSpawn(enemy: Enemy): Phaser.GameObjects.Container {
const view = new EnemyView(this.scene, enemy, CENTER_X, 200);
const container = view.container;
container.setData('enemyView', view);
return container;
}
onDespawn(container: Phaser.GameObjects.Container): void {
const view = container.getData('enemyView') as EnemyView | undefined;
if (view) {
this.scene.tweens.add({
targets: container,
alpha: 0,
y: container.y - 150,
scale: 0.3,
duration: 400,
ease: 'Back.easeIn',
onComplete: () => view.destroy(),
});
} else {
container.destroy();
}
}
}
@@ -0,0 +1,259 @@
import Phaser from 'phaser';
// 伤害数字特效
export function showDamageNumber(
scene: Phaser.Scene,
x: number,
y: number,
damage: number,
color: number = 0xef4444
): void {
const text = scene.add.text(x, y, `-${damage}`, {
fontSize: '32px',
fontFamily: 'Arial',
color: `#${color.toString(16).padStart(6, '0')}`,
fontStyle: 'bold',
}).setOrigin(0.5);
scene.tweens.add({
targets: text,
y: y - 100,
alpha: 0,
scale: 1.5,
duration: 800,
ease: 'Power2',
onComplete: () => text.destroy(),
});
}
// 治疗数字特效
export function showHealNumber(
scene: Phaser.Scene,
x: number,
y: number,
amount: number
): void {
const text = scene.add.text(x, y, `+${amount}`, {
fontSize: '28px',
fontFamily: 'Arial',
color: '#22c55e',
fontStyle: 'bold',
}).setOrigin(0.5);
scene.tweens.add({
targets: text,
y: y - 80,
alpha: 0,
duration: 800,
ease: 'Power2',
onComplete: () => text.destroy(),
});
}
// 卡牌飞行特效
export function flyCardToTarget(
scene: Phaser.Scene,
card: Phaser.GameObjects.Container,
targetX: number,
targetY: number,
duration: number = 400,
onComplete?: () => void
): void {
const startX = card.x;
const startY = card.y;
// 创建飞行路径
const path = [
{ x: startX, y: startY },
{ x: startX + (targetX - startX) / 2, y: startY - 100 },
{ x: targetX, y: targetY },
];
scene.tweens.add({
targets: card,
x: targetX,
y: targetY,
scale: 0.5,
alpha: 0,
duration,
ease: 'Back.easeIn',
onComplete: () => {
if (onComplete) onComplete();
},
});
}
// 敌人受击特效
export function enemyHitEffect(
scene: Phaser.Scene,
enemyContainer: Phaser.GameObjects.Container
): void {
// 红色闪烁
const flash = scene.add.rectangle(
enemyContainer.x,
enemyContainer.y,
150,
200,
0xff0000,
0.5
);
scene.tweens.add({
targets: flash,
alpha: 0,
duration: 300,
onComplete: () => flash.destroy(),
});
// 震动效果
scene.tweens.add({
targets: enemyContainer,
x: enemyContainer.x + 10,
duration: 50,
yoyo: true,
repeat: 3,
});
}
// 敌人死亡特效
export function enemyDeathEffect(
scene: Phaser.Scene,
enemyContainer: Phaser.GameObjects.Container
): void {
// 粒子爆炸
const particles = scene.add.particles(enemyContainer.x, enemyContainer.y, 'particle', {
speed: { min: 50, max: 150 },
angle: { min: 0, max: 360 },
scale: { start: 1, end: 0 },
lifespan: 1000,
gravityY: 100,
quantity: 20,
emitting: true,
});
// 淡出并上升
scene.tweens.add({
targets: enemyContainer,
y: enemyContainer.y - 200,
alpha: 0,
scale: 0.5,
duration: 800,
ease: 'Back.easeIn',
onComplete: () => {
enemyContainer.destroy();
particles.destroy();
},
});
}
// 抽牌特效
export function drawCardEffect(
scene: Phaser.Scene,
fromX: number,
fromY: number,
toX: number,
toY: number
): void {
// 创建临时卡牌
const tempCard = scene.add.rectangle(fromX, fromY, 60, 90, 0x3b82f6, 0.8)
.setStrokeStyle(2, 0x60a5fa);
scene.tweens.add({
targets: tempCard,
x: toX,
y: toY,
duration: 300,
ease: 'Back.easeOut',
onComplete: () => tempCard.destroy(),
});
}
// 反击警告特效
export function counterattackWarning(
scene: Phaser.Scene,
x: number,
y: number,
damage: number
): void {
const warning = scene.add.text(x, y, `⚠️ 反击伤害: ${damage}`, {
fontSize: '24px',
fontFamily: 'Arial',
color: '#ef4444',
fontStyle: 'bold',
}).setOrigin(0.5);
// 闪烁动画
scene.tweens.add({
targets: warning,
alpha: 0.3,
duration: 300,
yoyo: true,
repeat: 2,
});
// 消失
scene.time.delayedCall(1500, () => {
scene.tweens.add({
targets: warning,
alpha: 0,
y: y - 50,
duration: 300,
onComplete: () => warning.destroy(),
});
});
}
// 胜利特效
export function victoryEffect(
scene: Phaser.Scene,
x: number,
y: number
): void {
// 烟花粒子
for (let i = 0; i < 3; i++) {
scene.time.delayedCall(i * 300, () => {
const particles = scene.add.particles(
x + (Math.random() - 0.5) * 200,
y + (Math.random() - 0.5) * 100,
'particle',
{
speed: { min: 30, max: 100 },
angle: { min: 0, max: 360 },
scale: { start: 1.5, end: 0 },
lifespan: 1500,
gravityY: 50,
quantity: 30,
emitting: true,
}
);
scene.time.delayedCall(1500, () => particles.destroy());
});
}
}
// 闪烁文本
export function createFlashingText(
scene: Phaser.Scene,
x: number,
y: number,
text: string,
color: string = '#fbbf24'
): Phaser.GameObjects.Text {
const flashingText = scene.add.text(x, y, text, {
fontSize: '20px',
fontFamily: 'Arial',
color,
fontStyle: 'bold',
}).setOrigin(0.5);
scene.tweens.add({
targets: flashingText,
alpha: 0.3,
duration: 500,
yoyo: true,
repeat: -1,
});
return flashingText;
}
@@ -0,0 +1,121 @@
import Phaser from 'phaser';
import type { Card } from '@/game/types';
import { getCardDisplay, getSuitColor } from '@/game/card-utils';
import type { GameScene } from '../GameScene';
import { prompts } from '@/game/regicide';
export const CARD_WIDTH = 80;
export const CARD_HEIGHT = 120;
export class CardView {
public readonly container: Phaser.GameObjects.Container;
private readonly bg: Phaser.GameObjects.Rectangle;
private readonly text: Phaser.GameObjects.Text;
private readonly valueText: Phaser.GameObjects.Text;
private readonly card: Card;
private isHovering = false;
private baseY: number;
constructor(scene: GameScene, card: Card, x: number, y: number) {
this.card = card;
this.baseY = y;
this.container = scene.add.container(x, y);
// 卡牌背景
this.bg = scene.add.rectangle(0, 0, CARD_WIDTH, CARD_HEIGHT, 0xf9fafb)
.setStrokeStyle(3, 0x6b7280);
// 卡牌边框颜色
const borderColor = getSuitColor(card.suit);
this.bg.setStrokeStyle(3, Phaser.Display.Color.HexStringToColor(borderColor).color);
// 卡牌文本
const display = getCardDisplay(card);
const color = getSuitColor(card.suit);
this.text = scene.add.text(0, 0, display, {
fontSize: '24px',
fontFamily: 'Arial',
color,
fontStyle: 'bold',
}).setOrigin(0.5);
// 卡牌数值提示
this.valueText = scene.add.text(0, -CARD_HEIGHT / 2 + 15, `+${card.value}`, {
fontSize: '12px',
fontFamily: 'Arial',
color: '#6b7280',
}).setOrigin(0.5);
this.container.add([this.bg, this.valueText, this.text]);
// 设置交互
this.bg.setInteractive({ useHandCursor: true });
this.setupInteraction(scene);
// 出现动画
this.container.setScale(0);
scene.tweens.add({
targets: this.container,
scale: 1,
duration: 200,
ease: 'Back.easeOut',
});
}
setPosition(x: number, y: number): void {
this.baseY = y;
this.container.setPosition(x, y);
}
hover(offset: number = -30): void {
if (!this.isHovering) {
this.isHovering = true;
const scene = this.container.scene as Phaser.Scene;
scene.tweens.add({
targets: this.container,
y: this.baseY + offset,
duration: 100,
ease: 'Power2',
});
}
}
unhover(): void {
if (this.isHovering) {
this.isHovering = false;
const scene = this.container.scene as Phaser.Scene;
scene.tweens.add({
targets: this.container,
y: this.baseY,
duration: 100,
ease: 'Power2',
});
}
}
getCard(): Card {
return this.card;
}
destroy(): void {
this.container.destroy();
}
private setupInteraction(scene: GameScene): void {
this.bg.on('pointerover', () => {
this.hover();
});
this.bg.on('pointerout', () => {
this.unhover();
});
this.bg.on('pointerdown', () => {
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]);
}
});
}
}
@@ -0,0 +1,110 @@
import Phaser from 'phaser';
import type { Enemy } from '@/game/types';
import type { GameScene } from '../GameScene';
const ENEMY_CARD_WIDTH = 144;
const ENEMY_CARD_HEIGHT = 180;
export class EnemyView {
public readonly container: Phaser.GameObjects.Container;
private readonly bg: Phaser.GameObjects.Rectangle;
private readonly nameText: Phaser.GameObjects.Text;
private readonly immunityText: Phaser.GameObjects.Text;
private readonly counterText: Phaser.GameObjects.Text;
private readonly hpText: Phaser.GameObjects.Text;
private enemy: Enemy;
constructor(scene: GameScene, enemy: Enemy, x: number, y: number) {
this.enemy = enemy;
this.container = scene.add.container(x, y);
// 敌人卡牌背景
this.bg = scene.add.rectangle(0, 0, ENEMY_CARD_WIDTH, ENEMY_CARD_HEIGHT, 0x1f2937)
.setStrokeStyle(4, 0xef4444);
// 敌人名称
const suitSymbols: Record<string, string> = {
hearts: '♥',
diamonds: '♦',
clubs: '♣',
spades: '♠',
};
const suitSymbol = enemy.suit ? suitSymbols[enemy.suit] : '';
this.nameText = scene.add.text(0, -40, `${enemy.rank} ${suitSymbol}`, {
fontSize: '32px',
fontFamily: 'Arial',
color: '#fbbf24',
fontStyle: 'bold',
}).setOrigin(0.5);
// 花色免疫提示
const suitNames: Record<string, string> = {
hearts: '红桃 ♥',
diamonds: '方片 ♦',
clubs: '梅花 ♣',
spades: '黑桃 ♠',
};
const immunityText = enemy.immunitySuit
? `🛡️ 免疫: ${suitNames[enemy.immunitySuit]}`
: '';
this.immunityText = scene.add.text(0, 0, immunityText, {
fontSize: '14px',
fontFamily: 'Arial',
color: '#9ca3af',
}).setOrigin(0.5);
// 反击伤害提示
this.counterText = scene.add.text(0, 30, `⚔️ 反击伤害: ${enemy.counterDamage}`, {
fontSize: '16px',
fontFamily: 'Arial',
color: '#ef4444',
fontStyle: 'bold',
}).setOrigin(0.5);
// HP提示
this.hpText = scene.add.text(0, 60, `❤️ ${enemy.hp} HP`, {
fontSize: '16px',
fontFamily: 'Arial',
color: '#22c55e',
fontStyle: 'bold',
}).setOrigin(0.5);
this.container.add([this.bg, this.nameText, this.immunityText, this.counterText, this.hpText]);
// 出现动画
this.container.setScale(0);
scene.tweens.add({
targets: this.container,
scale: 1,
duration: 400,
ease: 'Back.easeOut',
});
// 抖动效果
scene.tweens.add({
targets: this.container,
x: x + 5,
duration: 100,
yoyo: true,
repeat: 3,
});
}
updateEnemy(enemy: Enemy): void {
this.enemy = enemy;
this.hpText.setText(`❤️ ${enemy.currentHp}/${enemy.hp} HP`);
this.counterText.setText(`⚔️ 反击伤害: ${enemy.counterDamage}`);
}
setPosition(x: number, y: number): void {
this.container.setPosition(x, y);
}
getEnemy(): Enemy {
return this.enemy;
}
destroy(): void {
this.container.destroy();
}
}
@@ -0,0 +1,171 @@
import Phaser from 'phaser';
import type { Enemy, VictoryLevel } from '@/game/types';
import type { GameScene } from '../GameScene';
import { EnemyView } from './EnemyView';
const GAME_WIDTH = 800;
const GAME_HEIGHT = 700;
const CENTER_X = GAME_WIDTH / 2;
const ENEMY_Y = 200;
const BAR_WIDTH = 200;
const BAR_HEIGHT = 20;
export class GameUI {
public readonly container: Phaser.GameObjects.Container;
private readonly hpBar: Phaser.GameObjects.Graphics;
private readonly infoText: Phaser.GameObjects.Text;
private readonly phaseText: Phaser.GameObjects.Text;
private gameOverOverlay?: Phaser.GameObjects.Container;
private enemyView?: EnemyView;
constructor(scene: GameScene) {
this.container = scene.add.container(0, 0);
// 创建HP条
this.hpBar = scene.add.graphics();
// 创建文本
this.infoText = scene.add.text(CENTER_X, 30, '⚔️ Regicide - 击败所有12个敌人!', {
fontSize: '22px',
fontFamily: 'Arial',
color: '#fbbf24',
}).setOrigin(0.5);
this.phaseText = scene.add.text(CENTER_X, 60, '点击任意位置开始游戏', {
fontSize: '16px',
fontFamily: 'Arial',
color: '#9ca3af',
}).setOrigin(0.5);
}
updateEnemyDisplay(scene: GameScene, enemy: Enemy | null): void {
this.hpBar.clear();
if (!enemy) {
if (this.enemyView) {
this.enemyView.destroy();
this.enemyView = undefined;
}
return;
}
// 更新或创建敌人视图
if (this.enemyView) {
this.enemyView.updateEnemy(enemy);
} else {
this.enemyView = new EnemyView(scene, enemy, CENTER_X, ENEMY_Y);
}
// 绘制HP条
this.drawHpBar(enemy);
}
updatePhaseText(phase: string): void {
const phaseNames: Record<string, string> = {
playerTurn: '🎯 你的回合 - 点击卡牌攻击敌人',
enemyCounterattack: '💥 敌人反击! - 点击卡牌抵消伤害',
enemyDefeated: '✨ 敌人被击败! 准备迎战下一个敌人',
gameOver: '🏁 游戏结束',
};
this.phaseText.setText(phaseNames[phase] || phase);
}
showGameOver(scene: GameScene, victoryLevel: VictoryLevel): void {
if (this.gameOverOverlay) {
this.gameOverOverlay.destroy();
}
this.gameOverOverlay = scene.add.container();
// 半透明背景
const bg = scene.add.rectangle(CENTER_X, GAME_HEIGHT / 2, GAME_WIDTH, GAME_HEIGHT, 0x000000, 0.85)
.setInteractive({ useHandCursor: true });
bg.on('pointerdown', () => {
scene.gameHost.start();
});
this.gameOverOverlay.add(bg);
// 游戏结束文本
let resultText = '游戏结束';
let resultColor = '#ef4444';
if (victoryLevel === 'gold') {
resultText = '🥇 金胜利! 完美通关!';
resultColor = '#fbbf24';
} else if (victoryLevel === 'silver') {
resultText = '🥈 银胜利! 使用1张小丑牌';
resultColor = '#9ca3af';
} else if (victoryLevel === 'bronze') {
resultText = '🥉 铜胜利! 使用2张小丑牌';
resultColor = '#b45309';
} else {
resultText = '💀 失败! 无法抵消反击伤害';
}
const text = scene.add.text(CENTER_X, 250, resultText, {
fontSize: '36px',
fontFamily: 'Arial',
color: resultColor,
}).setOrigin(0.5);
this.gameOverOverlay.add(text);
const restartText = scene.add.text(CENTER_X, 350, '点击重新开始', {
fontSize: '24px',
fontFamily: 'Arial',
color: '#6b7280',
}).setOrigin(0.5);
this.gameOverOverlay.add(restartText);
// 动画
scene.tweens.add({
targets: text,
scale: 1.1,
duration: 500,
yoyo: true,
repeat: 2,
});
}
hideGameOver(): void {
if (this.gameOverOverlay) {
this.gameOverOverlay.destroy();
this.gameOverOverlay = undefined;
}
}
dispose(): void {
this.hpBar.destroy();
this.infoText.destroy();
this.phaseText.destroy();
if (this.gameOverOverlay) {
this.gameOverOverlay.destroy();
}
if (this.enemyView) {
this.enemyView.destroy();
}
this.container.destroy();
}
private drawHpBar(enemy: Enemy): void {
const hpPercent = enemy.currentHp / enemy.hp;
const barX = CENTER_X - BAR_WIDTH / 2;
const barY = ENEMY_Y + 100;
// 背景
this.hpBar.fillStyle(0x374151);
this.hpBar.fillRoundedRect(barX, barY, BAR_WIDTH, BAR_HEIGHT, 5);
// 前景
const hpColor = hpPercent > 0.5 ? 0x22c55e : hpPercent > 0.25 ? 0xf59e0b : 0xef4444;
this.hpBar.fillStyle(hpColor);
this.hpBar.fillRoundedRect(barX, barY, BAR_WIDTH * hpPercent, BAR_HEIGHT, 5);
// 边框
this.hpBar.lineStyle(2, 0x6b7280);
this.hpBar.strokeRoundedRect(barX, barY, BAR_WIDTH, BAR_HEIGHT, 5);
}
}
@@ -0,0 +1,83 @@
import Phaser from 'phaser';
import type { Card } from '@/game/types';
import { CardView, CARD_WIDTH } from './CardView';
import type { GameScene } from '../GameScene';
const HAND_Y = 580;
const MAX_HAND_WIDTH = 600;
const CARD_SPACING = 70;
export class HandContainer {
public readonly container: Phaser.GameObjects.Container;
private cardViews: Map<string, CardView> = new Map();
private centerX: number;
private handY: number;
constructor(scene: GameScene, centerX: number, handY: number = HAND_Y) {
this.container = scene.add.container(0, 0);
this.centerX = centerX;
this.handY = handY;
}
updateCards(scene: GameScene, cards: Card[]): void {
const currentCardIds = new Set(cards.map(c => c.id));
// 移除不在手牌中的卡牌
for (const [cardId, view] of this.cardViews) {
if (!currentCardIds.has(cardId)) {
this.animateCardExit(scene, view);
this.cardViews.delete(cardId);
}
}
// 更新或创建卡牌
const handSize = cards.length;
const spacing = Math.min(CARD_SPACING, MAX_HAND_WIDTH / handSize);
const startX = this.centerX - (handSize - 1) * spacing / 2;
for (let i = 0; i < cards.length; i++) {
const card = cards[i];
const existingView = this.cardViews.get(card.id);
if (existingView) {
// 更新位置
const targetX = startX + i * spacing;
const targetY = this.handY;
existingView.setPosition(targetX, targetY);
} else {
// 创建新卡牌
const x = startX + i * spacing;
const view = new CardView(scene, card, x, this.handY);
this.cardViews.set(card.id, view);
}
}
}
getCardView(cardId: string): CardView | undefined {
return this.cardViews.get(cardId);
}
getAllViews(): CardView[] {
return Array.from(this.cardViews.values());
}
dispose(): void {
for (const view of this.cardViews.values()) {
view.destroy();
}
this.cardViews.clear();
this.container.destroy();
}
private animateCardExit(scene: GameScene, view: CardView): void {
scene.tweens.add({
targets: view.container,
alpha: 0,
scale: 0.5,
y: view.container.y - 100,
duration: 200,
ease: 'Back.easeIn',
onComplete: () => view.destroy(),
});
}
}
@@ -0,0 +1,133 @@
import Phaser from 'phaser';
import type { CommandSchema } from 'boardgame-core';
import type { GameScene } from '../GameScene';
const GAME_WIDTH = 800;
const CENTER_X = GAME_WIDTH / 2;
export class PromptDisplay {
public readonly container: Phaser.GameObjects.Container;
private readonly bg: Phaser.GameObjects.Graphics;
private readonly promptText: Phaser.GameObjects.Text;
private readonly playerText: Phaser.GameObjects.Text;
private isVisible = false;
constructor(scene: GameScene) {
this.container = scene.add.container(CENTER_X, 450);
this.container.setDepth(100);
// 创建背景
this.bg = scene.add.graphics();
// 提示文本
this.promptText = scene.add.text(0, -15, '', {
fontSize: '18px',
fontFamily: 'Arial',
color: '#fbbf24',
fontStyle: 'bold',
align: 'center',
}).setOrigin(0.5);
// 玩家文本
this.playerText = scene.add.text(0, 15, '', {
fontSize: '14px',
fontFamily: 'Arial',
color: '#9ca3af',
align: 'center',
}).setOrigin(0.5);
this.container.add([this.bg, this.promptText, this.playerText]);
this.container.setVisible(false);
}
update(schema: CommandSchema | null, player: string | null): void {
if (!schema) {
if (this.isVisible) {
this.hide();
}
return;
}
if (!this.isVisible) {
this.show();
}
// 格式化 schema 显示
const formattedSchema = this.formatSchema(schema);
this.promptText.setText(formattedSchema);
if (player) {
this.playerText.setText(`等待玩家: ${player}`);
} else {
this.playerText.setText('');
}
}
private show(): void {
this.isVisible = true;
this.container.setVisible(true);
this.container.setScale(0);
this.container.setAlpha(0);
const scene = this.container.scene as Phaser.Scene;
scene.tweens.add({
targets: this.container,
scale: 1,
alpha: 1,
duration: 200,
ease: 'Back.easeOut',
});
this.drawBackground();
}
private hide(): void {
if (!this.isVisible) return;
this.isVisible = false;
const scene = this.container.scene as Phaser.Scene;
scene.tweens.add({
targets: this.container,
scale: 0.8,
alpha: 0,
duration: 150,
ease: 'Power2',
onComplete: () => this.container.setVisible(false),
});
}
private drawBackground(): void {
this.bg.clear();
const padding = 20;
const width = Math.max(
this.promptText.width,
this.playerText.width
) + padding * 2;
const height = 60;
// 半透明背景
this.bg.fillStyle(0x1f2937, 0.9);
this.bg.fillRoundedRect(-width / 2, -height / 2, width, height, 10);
// 边框
this.bg.lineStyle(2, 0xfbbf24, 0.6);
this.bg.strokeRoundedRect(-width / 2, -height / 2, width, height, 10);
}
private formatSchema(schema: CommandSchema): string {
// 格式化命令 schema 为可读字符串
const params = schema.params.map(p => {
return p.variadic ? `<${p.name}...>` : `<${p.name}>`;
}).join(' ');
return `${schema.name} ${params}`;
}
destroy(): void {
this.bg.destroy();
this.promptText.destroy();
this.playerText.destroy();
this.container.destroy();
}
}
@@ -0,0 +1,5 @@
export { CardView, CARD_WIDTH, CARD_HEIGHT } from './CardView';
export { EnemyView } from './EnemyView';
export { HandContainer } from './HandContainer';
export { GameUI } from './GameUI';
export { PromptDisplay } from './PromptDisplay';