From 5d4c169fea29b04a12d458ab2e8096546e6a4ce5 Mon Sep 17 00:00:00 2001 From: hypercross Date: Fri, 3 Apr 2026 19:13:12 +0800 Subject: [PATCH] fix: update according to boardgame-core --- packages/framework/src/bindings/index.ts | 18 ++++-- packages/framework/src/input/index.ts | 55 +++++++++++++++---- .../framework/src/scenes/ReactiveScene.ts | 10 +++- packages/framework/src/ui/PromptDialog.tsx | 4 +- packages/sample-game/src/game/tic-tac-toe.ts | 16 +++--- packages/sample-game/src/main.tsx | 10 +--- packages/sample-game/src/scenes/GameScene.ts | 21 ++----- 7 files changed, 83 insertions(+), 51 deletions(-) diff --git a/packages/framework/src/bindings/index.ts b/packages/framework/src/bindings/index.ts index fbe4b57..fd15ea2 100644 --- a/packages/framework/src/bindings/index.ts +++ b/packages/framework/src/bindings/index.ts @@ -29,11 +29,12 @@ export interface BindRegionOptions { cellSize: { x: number; y: number }; offset?: { x: number; y: number }; factory: (part: TPart, position: Phaser.Math.Vector2) => Phaser.GameObjects.GameObject; + update?: (part: TPart, obj: Phaser.GameObjects.GameObject) => void; } export function bindRegion( state: MutableSignal, - partsGetter: (state: TState) => Part[], + partsGetter: (state: TState) => Record>, region: Region, options: BindRegionOptions>, container: Phaser.GameObjects.Container, @@ -45,6 +46,8 @@ export function bindRegion( const dispose = effect(function(this: { dispose: () => void }) { const parts = partsGetter(state.value); const currentIds = new Set(region.childIds); + + // 移除不在 region 中的对象 for (const [id, obj] of objects) { if (!currentIds.has(id)) { obj.destroy(); @@ -52,13 +55,15 @@ export function bindRegion( } } + // 同步 region 中的 parts for (const childId of region.childIds) { - const part = parts.find(p => p.id === childId); + const part = parts[childId]; if (!part) continue; + // 支持动态维度:取前两个维度作为 x, y const pos = new Phaser.Math.Vector2( - part.position[0] * options.cellSize.x + offset.x, - part.position[1] * options.cellSize.y + offset.y, + (part.position[0] ?? 0) * options.cellSize.x + offset.x, + (part.position[1] ?? 0) * options.cellSize.y + offset.y, ); let obj = objects.get(childId); @@ -67,9 +72,14 @@ export function bindRegion( objects.set(childId, obj); container.add(obj); } else { + // 更新位置 if ('setPosition' in obj && typeof obj.setPosition === 'function') { (obj as any).setPosition(pos.x, pos.y); } + // 调用自定义更新函数 + if (options.update) { + options.update(part, obj); + } } } }); diff --git a/packages/framework/src/input/index.ts b/packages/framework/src/input/index.ts index cdf2667..6fe9f5a 100644 --- a/packages/framework/src/input/index.ts +++ b/packages/framework/src/input/index.ts @@ -35,7 +35,7 @@ export class InputMapper> { const cmd = onCellClick(col, row); if (cmd) { - this.commands._tryCommit(cmd); + this.commands.run(cmd); } }; @@ -73,7 +73,6 @@ export interface PromptHandlerOptions> { scene: Phaser.Scene; commands: IGameContext['commands']; onPrompt: (prompt: PromptEvent) => void; - onSubmit: (input: string) => string | null; onCancel: (reason?: string) => void; } @@ -81,35 +80,68 @@ export class PromptHandler> { private scene: Phaser.Scene; private commands: IGameContext['commands']; private onPrompt: (prompt: PromptEvent) => void; - private onSubmit: (input: string) => string | null; private onCancel: (reason?: string) => void; + private activePrompt: PromptEvent | null = null; + private isListening = false; constructor(options: PromptHandlerOptions) { this.scene = options.scene; this.commands = options.commands; this.onPrompt = options.onPrompt; - this.onSubmit = options.onSubmit; this.onCancel = options.onCancel; } start(): void { - this.commands.promptQueue.pop().then((promptEvent) => { - this.onPrompt(promptEvent); - }).catch(() => { - // prompt was cancelled - }); + this.isListening = true; + this.listenForPrompt(); + } + + private listenForPrompt(): void { + if (!this.isListening) return; + + this.commands.promptQueue.pop() + .then((promptEvent) => { + this.activePrompt = promptEvent; + this.onPrompt(promptEvent); + }) + .catch((reason) => { + this.activePrompt = null; + this.onCancel(reason?.message || 'Cancelled'); + }); } submit(input: string): string | null { - return this.onSubmit(input); + if (!this.activePrompt) { + return 'No active prompt'; + } + + const error = this.activePrompt.tryCommit(input); + if (error === null) { + // 提交成功,重置并监听下一个 prompt + this.activePrompt = null; + this.listenForPrompt(); + } + return error; } cancel(reason?: string): void { + if (this.activePrompt) { + this.activePrompt.cancel(reason); + this.activePrompt = null; + } this.onCancel(reason); } + stop(): void { + this.isListening = false; + if (this.activePrompt) { + this.activePrompt.cancel('Handler stopped'); + this.activePrompt = null; + } + } + destroy(): void { - // No cleanup needed - promptQueue handles its own lifecycle + this.stop(); } } @@ -125,7 +157,6 @@ export function createPromptHandler>( commands: IGameContext['commands'], callbacks: { onPrompt: (prompt: PromptEvent) => void; - onSubmit: (input: string) => string | null; onCancel: (reason?: string) => void; }, ): PromptHandler { diff --git a/packages/framework/src/scenes/ReactiveScene.ts b/packages/framework/src/scenes/ReactiveScene.ts index 5b247e6..6e48046 100644 --- a/packages/framework/src/scenes/ReactiveScene.ts +++ b/packages/framework/src/scenes/ReactiveScene.ts @@ -5,11 +5,11 @@ import type { MutableSignal, IGameContext, CommandResult } from 'boardgame-core' type DisposeFn = () => void; export interface ReactiveSceneOptions> { - state: MutableSignal; - commands: IGameContext['commands']; + gameContext: IGameContext; } export abstract class ReactiveScene> extends Phaser.Scene { + protected gameContext!: IGameContext; protected state!: MutableSignal; protected commands!: IGameContext['commands']; private effects: DisposeFn[] = []; @@ -18,6 +18,12 @@ export abstract class ReactiveScene> exte super(key); } + init(data: ReactiveSceneOptions): void { + this.gameContext = data.gameContext; + this.state = data.gameContext.state; + this.commands = data.gameContext.commands; + } + protected watch(fn: () => void): DisposeFn { const e = effect(fn); this.effects.push(e); diff --git a/packages/framework/src/ui/PromptDialog.tsx b/packages/framework/src/ui/PromptDialog.tsx index 4787111..4605bf4 100644 --- a/packages/framework/src/ui/PromptDialog.tsx +++ b/packages/framework/src/ui/PromptDialog.tsx @@ -65,7 +65,7 @@ export function PromptDialog({ prompt, onSubmit, onCancel }: PromptDialogProps) {fields.length > 0 ? (
- {fields.map(({ param, label }) => ( + {fields.map(({ param, label }, index) => (
))} diff --git a/packages/sample-game/src/game/tic-tac-toe.ts b/packages/sample-game/src/game/tic-tac-toe.ts index 82c8b71..0625e97 100644 --- a/packages/sample-game/src/game/tic-tac-toe.ts +++ b/packages/sample-game/src/game/tic-tac-toe.ts @@ -30,7 +30,7 @@ export function createInitialState() { { name: 'x', min: 0, max: BOARD_SIZE - 1 }, { name: 'y', min: 0, max: BOARD_SIZE - 1 }, ]), - parts: [] as TicTacToePart[], + parts: {} as Record, currentPlayer: 'X' as PlayerType, winner: null as WinnerType, turn: 0, @@ -76,7 +76,9 @@ registration.add('turn ', async function (cmd) { if (row < 0 || row >= BOARD_SIZE || col < 0 || col >= BOARD_SIZE) { return `Invalid position: (${row}, ${col}).`; } - if (isCellOccupied(this.context, row, col)) { + const state = this.context.value; + const partId = state.board.partMap[`${row},${col}`]; + if (partId) { return `Cell (${row}, ${col}) is already occupied.`; } return null; @@ -108,18 +110,18 @@ export function hasWinningLine(positions: number[][]): boolean { export function checkWinner(host: MutableSignal): WinnerType { const parts = host.value.parts; - const xPositions = parts.filter((p: TicTacToePart) => p.player === 'X').map((p: TicTacToePart) => p.position); - const oPositions = parts.filter((p: TicTacToePart) => p.player === 'O').map((p: TicTacToePart) => p.position); + const xPositions = Object.values(parts).filter((p: TicTacToePart) => p.player === 'X').map((p: TicTacToePart) => p.position); + const oPositions = Object.values(parts).filter((p: TicTacToePart) => p.player === 'O').map((p: TicTacToePart) => p.position); if (hasWinningLine(xPositions)) return 'X'; if (hasWinningLine(oPositions)) return 'O'; - if (parts.length >= MAX_TURNS) return 'draw'; + if (Object.keys(parts).length >= MAX_TURNS) return 'draw'; return null; } export function placePiece(host: MutableSignal, row: number, col: number, player: PlayerType) { - const moveNumber = host.value.parts.length + 1; + const moveNumber = Object.keys(host.value.parts).length + 1; const piece: TicTacToePart = { id: `piece-${player}-${moveNumber}`, regionId: 'board', @@ -127,7 +129,7 @@ export function placePiece(host: MutableSignal, row: number, col player, }; host.produce(state => { - state.parts.push(piece); + state.parts[piece.id] = piece; state.board.childIds.push(piece.id); state.board.partMap[`${row},${col}`] = piece.id; }); diff --git a/packages/sample-game/src/main.tsx b/packages/sample-game/src/main.tsx index f54c551..43455dc 100644 --- a/packages/sample-game/src/main.tsx +++ b/packages/sample-game/src/main.tsx @@ -5,7 +5,7 @@ import Phaser from 'phaser'; import { createGameContext } from 'boardgame-core'; import { GameUI, PromptDialog, CommandLog } from 'boardgame-phaser'; import { createInitialState, registry, type TicTacToeState } from './game/tic-tac-toe'; -import { GameScene, type GameSceneData } from './scenes/GameScene'; +import { GameScene } from './scenes/GameScene'; import './style.css'; const gameContext = createGameContext(registry, createInitialState); @@ -33,11 +33,6 @@ const originalRun = gameContext.commands.run.bind(gameContext.commands); return result; }; -const sceneData: GameSceneData = { - state: gameContext.state, - commands: gameContext.commands, -}; - function App() { const [phaserReady, setPhaserReady] = useState(false); const [game, setGame] = useState(null); @@ -53,7 +48,8 @@ function App() { }; const phaserGame = new Phaser.Game(phaserConfig); - phaserGame.scene.add('GameScene', GameScene, true, sceneData); + // 通过 init 传递 gameContext + phaserGame.scene.add('GameScene', GameScene, true, { gameContext }); setGame(phaserGame); setPhaserReady(true); diff --git a/packages/sample-game/src/scenes/GameScene.ts b/packages/sample-game/src/scenes/GameScene.ts index f11b8df..77c1e36 100644 --- a/packages/sample-game/src/scenes/GameScene.ts +++ b/packages/sample-game/src/scenes/GameScene.ts @@ -2,17 +2,12 @@ import Phaser from 'phaser'; import type { TicTacToeState, TicTacToePart, PlayerType } from '@/game/tic-tac-toe'; import { isCellOccupied } from 'boardgame-core'; import { ReactiveScene, bindRegion, createInputMapper, createPromptHandler } from 'boardgame-phaser'; -import type { PromptEvent, MutableSignal, IGameContext } from 'boardgame-core'; +import type { PromptEvent } from 'boardgame-core'; const CELL_SIZE = 120; const BOARD_OFFSET = { x: 100, y: 100 }; const BOARD_SIZE = 3; -export interface GameSceneData { - state: MutableSignal; - commands: IGameContext['commands']; -} - export class GameScene extends ReactiveScene { private boardContainer!: Phaser.GameObjects.Container; private gridGraphics!: Phaser.GameObjects.Graphics; @@ -25,11 +20,6 @@ export class GameScene extends ReactiveScene { super('GameScene'); } - init(data: GameSceneData): void { - this.state = data.state; - this.commands = data.commands; - } - protected onStateReady(_state: TicTacToeState): void { } @@ -71,6 +61,9 @@ export class GameScene extends ReactiveScene { return text; }, + update: (part, obj) => { + // 可以在这里更新部件的视觉状态 + }, }, this.boardContainer, ); @@ -97,12 +90,6 @@ export class GameScene extends ReactiveScene { onPrompt: (prompt) => { this.activePrompt = prompt; }, - onSubmit: (input) => { - if (this.activePrompt) { - return this.activePrompt.tryCommit(input); - } - return null; - }, onCancel: () => { this.activePrompt = null; },