import Phaser from 'phaser'; import type { Card } from '@/game/onitama'; import type { Spawner } from 'boardgame-phaser'; import type { OnitamaScene } from '@/scenes/OnitamaScene'; import { BOARD_OFFSET, CELL_SIZE } from './PawnSpawner'; export const CARD_WIDTH = 100; export const CARD_HEIGHT = 140; const BOARD_SIZE = 5; export interface CardSpawnData { cardId: string; position: 'red' | 'black' | 'spare'; index: number; } export class CardSpawner implements Spawner { private previousData = new Map(); constructor(public readonly scene: OnitamaScene) {} *getData(): Iterable { const state = this.scene.state; // 红方卡牌 for (let i = 0; i < state.redCards.length; i++) { yield { cardId: state.redCards[i], position: 'red', index: i }; } // 黑方卡牌 for (let i = 0; i < state.blackCards.length; i++) { yield { cardId: state.blackCards[i], position: 'black', index: i }; } // 备用卡牌 yield { cardId: state.spareCard, position: 'spare', index: 0 }; } getKey(data: CardSpawnData): string { return data.cardId; } private getCardPosition(data: CardSpawnData): { x: number, y: number } { const boardLeft = BOARD_OFFSET.x; const boardTop = BOARD_OFFSET.y; const boardRight = BOARD_OFFSET.x + BOARD_SIZE * CELL_SIZE; const boardCenterX = boardLeft + (BOARD_SIZE * CELL_SIZE) / 2; if (data.position === 'red') { return { x: boardLeft - CARD_WIDTH - 60 + 60, y: boardTop + 80 + data.index * (CARD_HEIGHT + 15), }; } else if (data.position === 'black') { return { x: boardRight + 60 + 40, y: boardTop + 80 + data.index * (CARD_HEIGHT + 15), }; } else { return { x: boardCenterX, y: boardTop - CARD_HEIGHT - 20, }; } } private hasPositionChanged(data: CardSpawnData): boolean { const prev = this.previousData.get(data.cardId); if (!prev) return true; return prev.position !== data.position || prev.index !== data.index; } onUpdate(data: CardSpawnData, obj: Phaser.GameObjects.Container): void { // 检查是否是选中的卡牌 const isSelected = this.scene.uiState.value.selectedCard === data.cardId; // 高亮选中的卡牌 if (isSelected) { this.highlightCard(obj, 0xfbbf24, 3); } else { this.unhighlightCard(obj); } // 只在位置实际变化时才播放移动动画 if (!this.hasPositionChanged(data)) { this.previousData.set(data.cardId, { ...data }); return; } const pos = this.getCardPosition(data); // 播放移动动画并添加中断 const tween = this.scene.tweens.add({ targets: obj, x: pos.x, y: pos.y, duration: 350, ease: 'Back.easeOut', }); this.scene.addTweenInterruption(tween); this.previousData.set(data.cardId, { ...data }); } private highlightCard(container: Phaser.GameObjects.Container, color: number, lineWidth: number): void { // 检查是否已经有高亮边框 let highlight = container.list.find( child => child instanceof Phaser.GameObjects.Rectangle && child.getData('isHighlight') ) as Phaser.GameObjects.Rectangle; if (!highlight) { // 创建高亮边框 highlight = this.scene.add.rectangle(0, 0, CARD_WIDTH + 8, CARD_HEIGHT + 8, color, 0) .setStrokeStyle(lineWidth, color) .setData('isHighlight', true); container.addAt(highlight, 0); } else { // 更新现有高亮边框 highlight.setStrokeStyle(lineWidth, color); highlight.setAlpha(1); } } private unhighlightCard(container: Phaser.GameObjects.Container): void { const highlight = container.list.find( child => child instanceof Phaser.GameObjects.Rectangle && child.getData('isHighlight') ) as Phaser.GameObjects.Rectangle; if (highlight) { highlight.setAlpha(0); } } onSpawn(data: CardSpawnData): Phaser.GameObjects.Container { const card = this.scene.state.cards[data.cardId]; if (!card) { this.previousData.set(data.cardId, { ...data }); return this.scene.add.container(0, 0); } const container = this.scene.add.container(0, 0); const pos = this.getCardPosition(data); container.x = pos.x; container.y = pos.y; // 创建卡牌视觉 const cardVisual = this.createCardVisual(card); container.add(cardVisual); // 使卡牌可点击(设置矩形点击区域) const hitArea = new Phaser.Geom.Rectangle(-CARD_WIDTH / 2, -CARD_HEIGHT / 2, CARD_WIDTH, CARD_HEIGHT); container.setInteractive(hitArea, Phaser.Geom.Rectangle.Contains); // 悬停效果 container.on('pointerover', () => { if (this.scene.uiState.value.selectedCard !== data.cardId) { container.setAlpha(0.8); } }); container.on('pointerout', () => { container.setAlpha(1); }); container.on('pointerdown', () => { this.scene.onCardClick(data.cardId); }); // 初始状态为透明,然后淡入 container.setAlpha(0); const tween = this.scene.tweens.add({ targets: container, alpha: 1, duration: 300, ease: 'Power2', }); this.scene.addTweenInterruption(tween); this.previousData.set(data.cardId, { ...data }); return container; } onDespawn(obj: Phaser.GameObjects.Container): void { const tween = this.scene.tweens.add({ targets: obj, alpha: 0, scale: 0.8, duration: 200, ease: 'Power2', onComplete: () => obj.destroy(), }); this.scene.addTweenInterruption(tween); } private createCardVisual(card: Card): Phaser.GameObjects.Container { const container = this.scene.add.container(0, 0); const bg = this.scene.add.rectangle(0, 0, CARD_WIDTH, CARD_HEIGHT, 0xf9fafb, 1) .setStrokeStyle(2, 0x6b7280); container.add(bg); const title = this.scene.add.text(0, -CARD_HEIGHT / 2 + 15, card.id, { fontSize: '12px', fontFamily: 'Arial', color: '#1f2937', }).setOrigin(0.5); container.add(title); const grid = this.scene.add.graphics(); const cellSize = 16; const gridWidth = 5 * cellSize; const gridHeight = 5 * cellSize; const gridStartX = -gridWidth / 2; const gridStartY = -gridHeight / 2 + 30; for (let row = 0; row < 5; row++) { for (let col = 0; col < 5; col++) { const x = gridStartX + col * cellSize; const y = gridStartY + row * cellSize; if (row === 2 && col === 2) { grid.fillStyle(0x3b82f6, 1); grid.fillCircle(x + cellSize / 2, y + cellSize / 2, cellSize / 3); } else { const isTarget = card.moveCandidates.some(m => m.dx === col - 2 && m.dy === 2 - row); if (isTarget) { grid.fillStyle(0xef4444, 0.6); grid.fillCircle(x + cellSize / 2, y + cellSize / 2, cellSize / 3); } } } } container.add(grid); const playerText = this.scene.add.text(0, CARD_HEIGHT / 2 - 15, card.startingPlayer, { fontSize: '10px', fontFamily: 'Arial', color: '#6b7280', }).setOrigin(0.5); container.add(playerText); return container; } }