fix: update according to boardgame-core
This commit is contained in:
@@ -29,11 +29,12 @@ export interface BindRegionOptions<TPart extends Part> {
|
||||
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<TState, TMeta>(
|
||||
state: MutableSignal<TState>,
|
||||
partsGetter: (state: TState) => Part<TMeta>[],
|
||||
partsGetter: (state: TState) => Record<string, Part<TMeta>>,
|
||||
region: Region,
|
||||
options: BindRegionOptions<Part<TMeta>>,
|
||||
container: Phaser.GameObjects.Container,
|
||||
@@ -45,6 +46,8 @@ export function bindRegion<TState, TMeta>(
|
||||
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<TState, TMeta>(
|
||||
}
|
||||
}
|
||||
|
||||
// 同步 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<TState, TMeta>(
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -35,7 +35,7 @@ export class InputMapper<TState extends Record<string, unknown>> {
|
||||
|
||||
const cmd = onCellClick(col, row);
|
||||
if (cmd) {
|
||||
this.commands._tryCommit(cmd);
|
||||
this.commands.run(cmd);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -73,7 +73,6 @@ export interface PromptHandlerOptions<TState extends Record<string, unknown>> {
|
||||
scene: Phaser.Scene;
|
||||
commands: IGameContext<TState>['commands'];
|
||||
onPrompt: (prompt: PromptEvent) => void;
|
||||
onSubmit: (input: string) => string | null;
|
||||
onCancel: (reason?: string) => void;
|
||||
}
|
||||
|
||||
@@ -81,35 +80,68 @@ export class PromptHandler<TState extends Record<string, unknown>> {
|
||||
private scene: Phaser.Scene;
|
||||
private commands: IGameContext<TState>['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<TState>) {
|
||||
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<TState extends Record<string, unknown>>(
|
||||
commands: IGameContext<TState>['commands'],
|
||||
callbacks: {
|
||||
onPrompt: (prompt: PromptEvent) => void;
|
||||
onSubmit: (input: string) => string | null;
|
||||
onCancel: (reason?: string) => void;
|
||||
},
|
||||
): PromptHandler<TState> {
|
||||
|
||||
@@ -5,11 +5,11 @@ import type { MutableSignal, IGameContext, CommandResult } from 'boardgame-core'
|
||||
type DisposeFn = () => void;
|
||||
|
||||
export interface ReactiveSceneOptions<TState extends Record<string, unknown>> {
|
||||
state: MutableSignal<TState>;
|
||||
commands: IGameContext<TState>['commands'];
|
||||
gameContext: IGameContext<TState>;
|
||||
}
|
||||
|
||||
export abstract class ReactiveScene<TState extends Record<string, unknown>> extends Phaser.Scene {
|
||||
protected gameContext!: IGameContext<TState>;
|
||||
protected state!: MutableSignal<TState>;
|
||||
protected commands!: IGameContext<TState>['commands'];
|
||||
private effects: DisposeFn[] = [];
|
||||
@@ -18,6 +18,12 @@ export abstract class ReactiveScene<TState extends Record<string, unknown>> exte
|
||||
super(key);
|
||||
}
|
||||
|
||||
init(data: ReactiveSceneOptions<TState>): 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);
|
||||
|
||||
@@ -65,7 +65,7 @@ export function PromptDialog({ prompt, onSubmit, onCancel }: PromptDialogProps)
|
||||
|
||||
{fields.length > 0 ? (
|
||||
<div className="space-y-3 mb-4">
|
||||
{fields.map(({ param, label }) => (
|
||||
{fields.map(({ param, label }, index) => (
|
||||
<div key={label}>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
{label}
|
||||
@@ -76,7 +76,7 @@ export function PromptDialog({ prompt, onSubmit, onCancel }: PromptDialogProps)
|
||||
value={values[label] || ''}
|
||||
onInput={(e) => setValues(prev => ({ ...prev, [label]: (e.target as HTMLInputElement).value }))}
|
||||
onKeyDown={(e) => { if (e.key === 'Enter') handleSubmit(); }}
|
||||
autoFocus={fields.indexOf({ param, label }) === 0}
|
||||
autoFocus={index === 0}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
|
||||
Reference in New Issue
Block a user