diff --git a/packages/onitama-game/src/scenes/OnitamaScene.ts b/packages/onitama-game/src/scenes/OnitamaScene.ts index 772c630..286d64f 100644 --- a/packages/onitama-game/src/scenes/OnitamaScene.ts +++ b/packages/onitama-game/src/scenes/OnitamaScene.ts @@ -1,13 +1,15 @@ import Phaser from 'phaser'; -import type { OnitamaState, PlayerType, Pawn } from '@/game/onitama'; -import { prompts } from '@/game/onitama'; +import type { OnitamaState, Pawn } from '@/game/onitama'; +import {getAvailableMoves, prompts} from '@/game/onitama'; import { GameHostScene } from 'boardgame-phaser'; import { spawnEffect } from 'boardgame-phaser'; -import { effect } from '@preact/signals-core'; import type { MutableSignal } from 'boardgame-core'; -import { PawnSpawner, CardSpawner, BOARD_OFFSET, CELL_SIZE, CARD_WIDTH, CARD_HEIGHT, boardToScreen, BOARD_SIZE } from '@/spawners'; +import { + PawnSpawner, CardSpawner, BOARD_OFFSET, CELL_SIZE, CARD_WIDTH, CARD_HEIGHT, boardToScreen, BOARD_SIZE, + HighlightSpawner +} from '@/spawners'; import type { HighlightData } from '@/spawners/HighlightSpawner'; -import { createOnitamaUIState, clearSelection, selectPiece, selectCard, deselectCard, setValidMoves } from '@/state'; +import {createUIState, clearSelection, selectPiece, selectCard, createValidMoves} from '@/state'; import type { OnitamaUIState, ValidMove } from '@/state'; export class OnitamaScene extends GameHostScene { @@ -19,8 +21,6 @@ export class OnitamaScene extends GameHostScene { // UI State managed by MutableSignal public uiState!: MutableSignal; - private highlightContainers: Map = new Map(); - private highlightDispose?: () => void; constructor() { super('OnitamaScene'); @@ -30,14 +30,7 @@ export class OnitamaScene extends GameHostScene { super.create(); // Create UI state signal - this.uiState = createOnitamaUIState(); - - // Cleanup effect on scene shutdown - this.events.once('shutdown', () => { - if (this.highlightDispose) { - this.highlightDispose(); - } - }); + this.uiState = createUIState(); this.boardContainer = this.add.container(0, 0); this.gridGraphics = this.add.graphics(); @@ -46,16 +39,11 @@ export class OnitamaScene extends GameHostScene { // Add spawners this.disposables.add(spawnEffect(new PawnSpawner(this))); this.disposables.add(spawnEffect(new CardSpawner(this))); + this.disposables.add(spawnEffect(new HighlightSpawner(this))); // Create card labels this.createCardLabels(); - // Setup highlight effect - react to validMoves changes - this.highlightDispose = effect(() => { - const validMoves = this.uiState.value.validMoves; - this.updateHighlights(validMoves); - }); - // Winner overlay effect this.addEffect(() => { const winner = this.state.winner; @@ -201,43 +189,10 @@ export class OnitamaScene extends GameHostScene { private handleCellClick(x: number, y: number): void { const pawn = this.getPawnAtPosition(x, y); - - // 如果没有选中卡牌,提示先选卡牌 - if (!this.uiState.value.selectedCard) { - console.log('请先选择一张卡牌'); + if(pawn?.owner !== this.state.currentPlayer){ return; } - - if (this.uiState.value.selectedPiece) { - // 已经选中了棋子 - if (pawn && pawn.owner === this.state.currentPlayer) { - // 点击了自己的另一个棋子,更新选择 - selectPiece(this.uiState, x, y); - this.updateValidMoves(); - return; - } - - const fromX = this.uiState.value.selectedPiece.x; - const fromY = this.uiState.value.selectedPiece.y; - - if (pawn && pawn.owner === this.state.currentPlayer) { - return; - } - - // 尝试移动到目标位置,必须使用选中的卡牌 - const validMoves = this.getValidMovesForPiece(fromX, fromY, [this.uiState.value.selectedCard]); - - const targetMove = validMoves.find(m => m.toX === x && m.toY === y); - if (targetMove) { - this.executeMove(targetMove); - } - } else { - // 还没有选中棋子 - if (pawn && pawn.owner === this.state.currentPlayer) { - selectPiece(this.uiState, x, y); - this.updateValidMoves(); - } - } + selectPiece(this.uiState, x, y); } public onCardClick(cardId: string): void { @@ -250,23 +205,6 @@ export class OnitamaScene extends GameHostScene { } selectCard(this.uiState, cardId); - // 如果已经选中了棋子,更新有效移动 - if (this.uiState.value.selectedPiece) { - this.updateValidMoves(); - } - } - - private updateValidMoves(): void { - const selectedPiece = this.uiState.value.selectedPiece; - const selectedCard = this.uiState.value.selectedCard; - - if (!selectedPiece || !selectedCard) { - setValidMoves(this.uiState, []); - return; - } - - const moves = this.getValidMovesForPiece(selectedPiece.x, selectedPiece.y, [selectedCard]); - setValidMoves(this.uiState, moves); } public onHighlightClick(data: HighlightData): void { @@ -295,80 +233,6 @@ export class OnitamaScene extends GameHostScene { } } - private updateHighlights(validMoves: ValidMove[]): void { - // Clear old highlights - for (const [, circle] of this.highlightContainers) { - circle.destroy(); - } - this.highlightContainers.clear(); - - // Create new highlights - for (const move of validMoves) { - const key = `${move.card}-${move.toX}-${move.toY}`; - const pos = boardToScreen(move.toX, move.toY); - - const circle = this.add.circle(pos.x, pos.y, CELL_SIZE / 3, 0x3b82f6, 0.3).setDepth(100); - circle.setInteractive({ useHandCursor: true }); - circle.on('pointerdown', () => { - this.onHighlightClick({ - key, - x: pos.x, - y: pos.y, - card: move.card, - fromX: move.fromX, - fromY: move.fromY, - toX: move.toX, - toY: move.toY, - }); - }); - - this.highlightContainers.set(key, circle as Phaser.GameObjects.GameObject); - } - } - - private getValidMovesForPiece( - fromX: number, - fromY: number, - cardNames: string[] - ): ValidMove[] { - const moves: ValidMove[] = []; - const player = this.state.currentPlayer; - - for (const cardName of cardNames) { - const card = this.state.cards[cardName]; - if (!card) continue; - - for (const move of card.moveCandidates) { - const toX = fromX + move.dx; - const toY = fromY + move.dy; - - if (this.isValidMove(fromX, fromY, toX, toY, player)) { - moves.push({ card: cardName, fromX, fromY, toX, toY }); - } - } - } - - return moves; - } - - private isValidMove(fromX: number, fromY: number, toX: number, toY: number, player: PlayerType): boolean { - if (toX < 0 || toX >= BOARD_SIZE || toY < 0 || toY >= BOARD_SIZE) { - return false; - } - - const targetPawn = this.getPawnAtPosition(toX, toY); - if (targetPawn && targetPawn.owner === player) { - return false; - } - - const pawn = this.getPawnAtPosition(fromX, fromY); - if (!pawn || pawn.owner !== player) { - return false; - } - - return true; - } - private getPawnAtPosition(x: number, y: number): Pawn | null { const key = `${x},${y}`; const pawnId = this.state.regions.board.partMap[key]; diff --git a/packages/onitama-game/src/spawners/HighlightSpawner.ts b/packages/onitama-game/src/spawners/HighlightSpawner.ts index d52b9ec..4d0a23e 100644 --- a/packages/onitama-game/src/spawners/HighlightSpawner.ts +++ b/packages/onitama-game/src/spawners/HighlightSpawner.ts @@ -1,8 +1,8 @@ import Phaser from 'phaser'; import type { Spawner } from 'boardgame-phaser'; import type { OnitamaScene } from '@/scenes/OnitamaScene'; -import type { OnitamaUIState } from '@/state/ui'; -import { BOARD_OFFSET, CELL_SIZE } from './PawnSpawner'; +import {getAvailableMoves} from "boardgame-core/samples/onitama"; +import {boardToScreen, CELL_SIZE} from './PawnSpawner'; export interface HighlightData { key: string; @@ -15,42 +15,77 @@ export interface HighlightData { toY: number; } -export class HighlightSpawner implements Spawner { +export class HighlightSpawner implements Spawner { constructor(public readonly scene: OnitamaScene) {} *getData(): Iterable { - // HighlightSpawner 的数据由 UI state 控制,不从这里生成 - // 我们会在 scene 中手动调用 spawnEffect 来更新 + const state = this.scene.state; + const uiState = this.scene.uiState.value; + + // 如果没有选择卡牌或棋子,不显示高亮 + if (!uiState.selectedCard || !uiState.selectedPiece) { + return; + } + + const currentPlayer = state.currentPlayer; + const availableMoves = getAvailableMoves(state, currentPlayer); + + // 过滤出符合当前选择的移动 + const filteredMoves = availableMoves.filter(move => + move.fromX === uiState.selectedPiece!.x && + move.fromY === uiState.selectedPiece!.y && + move.card === uiState.selectedCard + ); + + for(const move of filteredMoves){ + const pos = boardToScreen(move.toX, move.toY); + yield { + key: `${move.fromX}-${move.fromY}-${move.card}-${move.toX}-${move.toY}`, + x: pos.x, + y: pos.y, + card: move.card, + fromX: move.fromX, + fromY: move.fromY, + toX: move.toX, + toY: move.toY + } + } } getKey(data: HighlightData): string { return data.key; } - onUpdate(data: HighlightData, obj: Phaser.GameObjects.GameObject): void { - if (obj instanceof Phaser.GameObjects.Arc) { - obj.setPosition(data.x, data.y); - } + onUpdate(data: HighlightData, obj: Phaser.GameObjects.Container): void { + obj.setPosition(data.x, data.y); } - onSpawn(data: HighlightData): Phaser.GameObjects.GameObject { + onSpawn(data: HighlightData): Phaser.GameObjects.Container { + const container = this.scene.add.container(data.x, data.y); + const circle = this.scene.add.circle( - data.x, - data.y, + 0, + 0, CELL_SIZE / 3, 0x3b82f6, 0.3 - ).setDepth(100); + ); + container.add(circle); - circle.setInteractive({ useHandCursor: true }); - circle.on('pointerdown', () => { + const hitArea = new Phaser.Geom.Circle(0, 0, CELL_SIZE / 3); + container.setInteractive(hitArea, Phaser.Geom.Circle.Contains); + if (container.input) { + container.input.cursor = 'pointer'; + } + + container.on('pointerdown', () => { this.scene.onHighlightClick(data); }); - return circle; + return container; } - onDespawn(obj: Phaser.GameObjects.GameObject): void { + onDespawn(obj: Phaser.GameObjects.Container): void { obj.destroy(); } } diff --git a/packages/onitama-game/src/state/index.ts b/packages/onitama-game/src/state/index.ts index 1bd6a22..7659856 100644 --- a/packages/onitama-game/src/state/index.ts +++ b/packages/onitama-game/src/state/index.ts @@ -1,2 +1,2 @@ -export { createOnitamaUIState, clearSelection, selectPiece, selectCard, deselectCard, setValidMoves } from './ui'; +export { createUIState, clearSelection, selectPiece, selectCard, createValidMoves } from './ui'; export type { OnitamaUIState, ValidMove } from './ui'; diff --git a/packages/onitama-game/src/state/ui.ts b/packages/onitama-game/src/state/ui.ts index 0d0627d..47852a8 100644 --- a/packages/onitama-game/src/state/ui.ts +++ b/packages/onitama-game/src/state/ui.ts @@ -1,4 +1,5 @@ -import { MutableSignal, mutableSignal } from 'boardgame-core'; +import { MutableSignal, mutableSignal, computed, ReadonlySignal } from 'boardgame-core'; +import {getAvailableMoves, OnitamaState} from "boardgame-core/samples/onitama"; export interface ValidMove { card: string; @@ -12,14 +13,22 @@ export interface ValidMove { export interface OnitamaUIState { selectedPiece: { x: number; y: number } | null; selectedCard: string | null; - validMoves: ValidMove[]; } -export function createOnitamaUIState(): MutableSignal { +export function createUIState(): MutableSignal { return mutableSignal({ selectedPiece: null, selectedCard: null, - validMoves: [], + }); +} + +export function createValidMoves(state: ReadonlySignal, ui: ReadonlySignal){ + return computed(() => { + return getAvailableMoves(state.value, state.value.currentPlayer) + .filter(move => { + const {selectedCard, selectedPiece} = ui.value; + return selectedPiece?.x === move.fromX && selectedPiece?.y === move.fromY && selectedCard === move.card; + }) }); } @@ -27,7 +36,6 @@ export function clearSelection(uiState: MutableSignal): void { uiState.produce(state => { state.selectedPiece = null; state.selectedCard = null; - state.validMoves = []; }); } @@ -37,7 +45,12 @@ export function selectPiece( y: number ): void { uiState.produce(state => { - state.selectedPiece = { x, y }; + // 如果点击已选中的棋子,取消选择 + if(state.selectedPiece?.x === x && state.selectedPiece?.y === y){ + state.selectedPiece = null; + }else{ + state.selectedPiece = { x, y }; + } }); } @@ -48,34 +61,10 @@ export function selectCard( uiState.produce(state => { // 如果点击已选中的卡牌,取消选择 if (state.selectedCard === card) { - state.selectedPiece = null; state.selectedCard = null; - state.validMoves = []; } else { // 选择新卡牌,清除棋子选择 - state.selectedPiece = null; state.selectedCard = card; - state.validMoves = []; } }); } - -export function deselectCard( - uiState: MutableSignal -): void { - uiState.produce(state => { - state.selectedCard = null; - state.selectedPiece = null; - state.validMoves = []; - }); -} - -export function setValidMoves( - uiState: MutableSignal, - moves: ValidMove[] -): void { - uiState.produce(state => { - state.validMoves = moves; - }); -} -