refactor: simplify input handling?

This commit is contained in:
2026-04-04 00:17:52 +08:00
parent 7501b5f592
commit 3395a315a6
4 changed files with 111 additions and 91 deletions
+59 -51
View File
@@ -1,26 +1,25 @@
import Phaser from 'phaser';
import type { IGameContext, PromptEvent } from 'boardgame-core';
export interface InputMapperOptions<TState extends Record<string, unknown>> {
scene: Phaser.Scene;
commands: IGameContext<TState>['commands'];
activePrompt: { current: PromptEvent | null };
onSubmitPrompt: (input: string) => string | null;
// ─── InputMapper ───────────────────────────────────────────────────────────
export interface InputMapperOptions {
onSubmit: (input: string) => string | null;
}
export class InputMapper<TState extends Record<string, unknown>> {
export class InputMapper {
private scene: Phaser.Scene;
private commands: IGameContext<TState>['commands'];
private activePrompt: { current: PromptEvent | null };
private onSubmitPrompt: (input: string) => string | null;
private onSubmit: (input: string) => string | null;
private pointerDownCallback: ((pointer: Phaser.Input.Pointer) => void) | null = null;
private isWaitingForPrompt = false;
/** Track interactive objects registered via mapObjectClick for cleanup */
private trackedObjects: Array<{
obj: Phaser.GameObjects.GameObject;
handler: () => void;
}> = [];
constructor(options: InputMapperOptions<TState>) {
this.scene = options.scene;
this.commands = options.commands;
this.activePrompt = options.activePrompt;
this.onSubmitPrompt = options.onSubmitPrompt;
constructor(scene: Phaser.Scene, options: InputMapperOptions) {
this.scene = scene;
this.onSubmit = options.onSubmit;
}
mapGridClick(
@@ -42,9 +41,7 @@ export class InputMapper<TState extends Record<string, unknown>> {
const cmd = onCellClick(col, row);
if (cmd) {
// 总是尝试提交到 prompt
// 如果没有活跃 promptonSubmitPrompt 会返回错误,我们忽略它
this.onSubmitPrompt(cmd);
this.onSubmit(cmd);
}
};
@@ -60,42 +57,61 @@ export class InputMapper<TState extends Record<string, unknown>> {
if ('setInteractive' in obj && typeof (obj as any).setInteractive === 'function') {
const interactiveObj = obj as any;
interactiveObj.setInteractive({ useHandCursor: true });
interactiveObj.on('pointerdown', () => {
const handler = () => {
const cmd = onClick(obj as unknown as T);
if (cmd) {
this.commands.run(cmd);
this.onSubmit(cmd);
}
});
};
interactiveObj.on('pointerdown', handler);
this.trackedObjects.push({ obj, handler });
}
}
}
destroy(): void {
// Remove global pointerdown listener from mapGridClick
if (this.pointerDownCallback) {
this.scene.input.off('pointerdown', this.pointerDownCallback);
this.pointerDownCallback = null;
}
// Remove per-object pointerdown listeners from mapObjectClick
for (const { obj, handler } of this.trackedObjects) {
if ('off' in obj && typeof (obj as any).off === 'function') {
(obj as any).off('pointerdown', handler);
}
}
this.trackedObjects = [];
}
}
export interface PromptHandlerOptions<TState extends Record<string, unknown>> {
scene: Phaser.Scene;
commands: IGameContext<TState>['commands'];
export function createInputMapper(
scene: Phaser.Scene,
options: InputMapperOptions,
): InputMapper {
return new InputMapper(scene, options);
}
// ─── PromptHandler ─────────────────────────────────────────────────────────
export interface PromptHandlerOptions {
commands: IGameContext<any>['commands'];
onPrompt: (prompt: PromptEvent) => void;
onCancel: (reason?: string) => void;
}
export class PromptHandler<TState extends Record<string, unknown>> {
private scene: Phaser.Scene;
private commands: IGameContext<TState>['commands'];
export class PromptHandler {
private commands: IGameContext<any>['commands'];
private onPrompt: (prompt: PromptEvent) => void;
private onCancel: (reason?: string) => void;
private activePrompt: PromptEvent | null = null;
private isListening = false;
private pendingInput: string | null = null;
constructor(options: PromptHandlerOptions<TState>) {
this.scene = options.scene;
constructor(options: PromptHandlerOptions) {
this.commands = options.commands;
this.onPrompt = options.onPrompt;
this.onCancel = options.onCancel;
@@ -112,7 +128,7 @@ export class PromptHandler<TState extends Record<string, unknown>> {
this.commands.promptQueue.pop()
.then((promptEvent) => {
this.activePrompt = promptEvent;
// 如果有等待的输入,自动提交
if (this.pendingInput) {
const input = this.pendingInput;
@@ -121,10 +137,13 @@ export class PromptHandler<TState extends Record<string, unknown>> {
if (error === null) {
this.activePrompt = null;
this.listenForPrompt();
} else {
// 提交失败,把 prompt 交给 UI 显示错误
this.onPrompt(promptEvent);
}
return;
}
this.onPrompt(promptEvent);
})
.catch((reason) => {
@@ -133,16 +152,19 @@ export class PromptHandler<TState extends Record<string, unknown>> {
});
}
/**
* Submit an input string to the current prompt.
* @returns null on success (input accepted), error string on validation failure
*/
submit(input: string): string | null {
if (!this.activePrompt) {
// 没有活跃 prompt,保存为待处理输入
this.pendingInput = input;
return null; // 返回 null 表示已接受,会等待
return null;
}
const error = this.activePrompt.tryCommit(input);
if (error === null) {
// 提交成功,重置并监听下一个 prompt
this.activePrompt = null;
this.listenForPrompt();
}
@@ -170,22 +192,8 @@ export class PromptHandler<TState extends Record<string, unknown>> {
}
}
export function createInputMapper<TState extends Record<string, unknown>>(
scene: Phaser.Scene,
commands: IGameContext<TState>['commands'],
activePrompt: { current: PromptEvent | null },
onSubmitPrompt: (input: string) => string | null,
): InputMapper<TState> {
return new InputMapper({ scene, commands, activePrompt, onSubmitPrompt });
}
export function createPromptHandler<TState extends Record<string, unknown>>(
scene: Phaser.Scene,
commands: IGameContext<TState>['commands'],
callbacks: {
onPrompt: (prompt: PromptEvent) => void;
onCancel: (reason?: string) => void;
},
): PromptHandler<TState> {
return new PromptHandler({ scene, commands, ...callbacks });
export function createPromptHandler(
options: PromptHandlerOptions,
): PromptHandler {
return new PromptHandler(options);
}
+3 -4
View File
@@ -4,7 +4,7 @@ import type { PromptEvent, CommandSchema, CommandParamSchema } from 'boardgame-c
interface PromptDialogProps {
prompt: PromptEvent | null;
onSubmit: (input: string) => void;
onSubmit: (input: string) => string | null | void;
onCancel: () => void;
}
@@ -36,11 +36,10 @@ export function PromptDialog({ prompt, onSubmit, onCancel }: PromptDialogProps)
const fieldValues = schemaToFields(prompt.schema).map(f => values[f.label] || '');
const cmdString = [prompt.schema.name, ...fieldValues].join(' ');
const err = prompt.tryCommit(cmdString);
if (err) {
const err = onSubmit(cmdString);
if (err != null) {
setError(err);
} else {
onSubmit(cmdString);
setValues({});
setError(null);
}