refactor: reformat code and fix type signatures in GameHost
Reformat `src/core/game-host.ts` to use double quotes and consistent spacing. Update `createGameHost` to correctly propagate the `TResult` generic from `GameModule`.
This commit is contained in:
parent
8142fbfa60
commit
1e1d04777f
|
|
@ -1,180 +1,188 @@
|
||||||
import { ReadonlySignal, Signal } from '@preact/signals-core';
|
import { ReadonlySignal, Signal } from "@preact/signals-core";
|
||||||
|
import { CommandSchema, CommandRegistry, PromptEvent } from "@/utils/command";
|
||||||
import {
|
import {
|
||||||
CommandSchema,
|
createGameCommandRegistry,
|
||||||
CommandRegistry,
|
createGameContext,
|
||||||
PromptEvent,
|
IGameContext,
|
||||||
} from '@/utils/command';
|
PromptDef,
|
||||||
import {createGameCommandRegistry, createGameContext, IGameContext, PromptDef} from './game';
|
} from "./game";
|
||||||
|
|
||||||
export type GameHostStatus = 'created' | 'running' | 'disposed';
|
export type GameHostStatus = "created" | "running" | "disposed";
|
||||||
|
|
||||||
export class GameHost<TState extends Record<string, unknown>, TResult=unknown> {
|
export class GameHost<
|
||||||
readonly state: ReadonlySignal<TState>;
|
TState extends Record<string, unknown>,
|
||||||
readonly status: ReadonlySignal<GameHostStatus>;
|
TResult = unknown,
|
||||||
readonly activePromptSchema: ReadonlySignal<CommandSchema | null>;
|
> {
|
||||||
readonly activePromptPlayer: ReadonlySignal<string | null>;
|
readonly state: ReadonlySignal<TState>;
|
||||||
readonly activePromptHint: ReadonlySignal<string | null>;
|
readonly status: ReadonlySignal<GameHostStatus>;
|
||||||
|
readonly activePromptSchema: ReadonlySignal<CommandSchema | null>;
|
||||||
|
readonly activePromptPlayer: ReadonlySignal<string | null>;
|
||||||
|
readonly activePromptHint: ReadonlySignal<string | null>;
|
||||||
|
|
||||||
private _context: IGameContext<TState>;
|
private _context: IGameContext<TState>;
|
||||||
private _start: (ctx: IGameContext<TState>) => Promise<TResult>;
|
private _start: (ctx: IGameContext<TState>) => Promise<TResult>;
|
||||||
private _status: Signal<GameHostStatus>;
|
private _status: Signal<GameHostStatus>;
|
||||||
private _activePromptSchema: Signal<CommandSchema | null>;
|
private _activePromptSchema: Signal<CommandSchema | null>;
|
||||||
private _activePromptPlayer: Signal<string | null>;
|
private _activePromptPlayer: Signal<string | null>;
|
||||||
private _activePromptHint: Signal<string | null>;
|
private _activePromptHint: Signal<string | null>;
|
||||||
private _createInitialState: () => TState;
|
private _createInitialState: () => TState;
|
||||||
private _eventListeners: Map<'start' | 'dispose', Set<() => void>>;
|
private _eventListeners: Map<"start" | "dispose", Set<() => void>>;
|
||||||
private _isDisposed = false;
|
private _isDisposed = false;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
registry: CommandRegistry<IGameContext<TState>>,
|
registry: CommandRegistry<IGameContext<TState>>,
|
||||||
createInitialState: () => TState,
|
createInitialState: () => TState,
|
||||||
start: (ctx: IGameContext<TState>) => Promise<TResult>
|
start: (ctx: IGameContext<TState>) => Promise<TResult>,
|
||||||
) {
|
) {
|
||||||
this._createInitialState = createInitialState;
|
this._createInitialState = createInitialState;
|
||||||
this._eventListeners = new Map();
|
this._eventListeners = new Map();
|
||||||
|
|
||||||
const initialState = createInitialState();
|
const initialState = createInitialState();
|
||||||
this._context = createGameContext(registry, initialState);
|
this._context = createGameContext(registry, initialState);
|
||||||
this._start = start;
|
this._start = start;
|
||||||
this.state = this._context._state;
|
this.state = this._context._state;
|
||||||
|
|
||||||
this._status = new Signal<GameHostStatus>('created');
|
this._status = new Signal<GameHostStatus>("created");
|
||||||
this.status = this._status;
|
this.status = this._status;
|
||||||
|
|
||||||
this._activePromptSchema = new Signal<CommandSchema | null>(null);
|
this._activePromptSchema = new Signal<CommandSchema | null>(null);
|
||||||
this.activePromptSchema = this._activePromptSchema;
|
this.activePromptSchema = this._activePromptSchema;
|
||||||
|
|
||||||
this._activePromptPlayer = new Signal<string | null>(null);
|
this._activePromptPlayer = new Signal<string | null>(null);
|
||||||
this.activePromptPlayer = this._activePromptPlayer;
|
this.activePromptPlayer = this._activePromptPlayer;
|
||||||
|
|
||||||
this._activePromptHint = new Signal<string | null>(null);
|
this._activePromptHint = new Signal<string | null>(null);
|
||||||
this.activePromptHint = this._activePromptHint;
|
this.activePromptHint = this._activePromptHint;
|
||||||
|
|
||||||
this._setupPromptTracking();
|
this._setupPromptTracking();
|
||||||
|
}
|
||||||
|
|
||||||
|
private _setupPromptTracking() {
|
||||||
|
let currentPromptEvent: PromptEvent | null = null;
|
||||||
|
|
||||||
|
this._context._commands.on("prompt", (e) => {
|
||||||
|
currentPromptEvent = e as PromptEvent;
|
||||||
|
this._activePromptSchema.value = currentPromptEvent.schema;
|
||||||
|
this._activePromptPlayer.value = currentPromptEvent.currentPlayer;
|
||||||
|
this._activePromptHint.value = currentPromptEvent.hintText || null;
|
||||||
|
});
|
||||||
|
|
||||||
|
this._context._commands.on("promptEnd", () => {
|
||||||
|
currentPromptEvent = null;
|
||||||
|
this._activePromptSchema.value = null;
|
||||||
|
this._activePromptPlayer.value = null;
|
||||||
|
this._activePromptHint.value = null;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Initial state
|
||||||
|
this._activePromptSchema.value = null;
|
||||||
|
this._activePromptPlayer.value = null;
|
||||||
|
this._activePromptHint.value = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
tryInput(input: string): string | null {
|
||||||
|
if (this._isDisposed) {
|
||||||
|
return "GameHost is disposed";
|
||||||
|
}
|
||||||
|
return this._context._commands._tryCommit(input);
|
||||||
|
}
|
||||||
|
|
||||||
|
tryAnswerPrompt<TArgs extends any[]>(def: PromptDef<TArgs>, ...args: TArgs) {
|
||||||
|
return this._context._commands._tryCommit({
|
||||||
|
name: def.schema.name,
|
||||||
|
params: args,
|
||||||
|
options: {},
|
||||||
|
flags: {},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 为下一个 produceAsync 注册中断 Promise(通常用于 UI 动画)。
|
||||||
|
* @see MutableSignal.addInterruption
|
||||||
|
*/
|
||||||
|
addInterruption(promise: Promise<void>): void {
|
||||||
|
this._context._state.addInterruption(promise);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 清除所有未完成的中断。
|
||||||
|
* @see MutableSignal.clearInterruptions
|
||||||
|
*/
|
||||||
|
clearInterruptions(): void {
|
||||||
|
this._context._state.clearInterruptions();
|
||||||
|
}
|
||||||
|
|
||||||
|
start(seed?: number): Promise<TResult> {
|
||||||
|
if (this._isDisposed) {
|
||||||
|
throw new Error("GameHost is disposed");
|
||||||
}
|
}
|
||||||
|
|
||||||
private _setupPromptTracking() {
|
this._context._commands._cancel();
|
||||||
let currentPromptEvent: PromptEvent | null = null;
|
|
||||||
|
|
||||||
this._context._commands.on('prompt', (e) => {
|
const initialState = this._createInitialState();
|
||||||
currentPromptEvent = e as PromptEvent;
|
this._context._state.value = initialState as any;
|
||||||
this._activePromptSchema.value = currentPromptEvent.schema;
|
|
||||||
this._activePromptPlayer.value = currentPromptEvent.currentPlayer;
|
|
||||||
this._activePromptHint.value = currentPromptEvent.hintText || null;
|
|
||||||
});
|
|
||||||
|
|
||||||
this._context._commands.on('promptEnd', () => {
|
seed = seed || Math.floor(Math.random() * Number.MAX_SAFE_INTEGER);
|
||||||
currentPromptEvent = null;
|
this._context._rng.setSeed(seed);
|
||||||
this._activePromptSchema.value = null;
|
const promise = this._start(this._context);
|
||||||
this._activePromptPlayer.value = null;
|
|
||||||
this._activePromptHint.value = null;
|
|
||||||
});
|
|
||||||
|
|
||||||
// Initial state
|
this._status.value = "running";
|
||||||
this._activePromptSchema.value = null;
|
this._emitEvent("start");
|
||||||
this._activePromptPlayer.value = null;
|
|
||||||
this._activePromptHint.value = null;
|
return promise;
|
||||||
|
}
|
||||||
|
|
||||||
|
dispose(): void {
|
||||||
|
if (this._isDisposed) {
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
tryInput(input: string): string | null {
|
this._isDisposed = true;
|
||||||
if (this._isDisposed) {
|
this._context._commands._cancel();
|
||||||
return 'GameHost is disposed';
|
this._status.value = "disposed";
|
||||||
}
|
|
||||||
return this._context._commands._tryCommit(input);
|
// Emit dispose event BEFORE clearing listeners
|
||||||
|
this._emitEvent("dispose");
|
||||||
|
this._eventListeners.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
on(event: "start" | "dispose", listener: () => void): () => void {
|
||||||
|
if (!this._eventListeners.has(event)) {
|
||||||
|
this._eventListeners.set(event, new Set());
|
||||||
}
|
}
|
||||||
|
this._eventListeners.get(event)!.add(listener);
|
||||||
|
|
||||||
tryAnswerPrompt<TArgs extends any[]>(def: PromptDef<TArgs>, ...args: TArgs){
|
return () => {
|
||||||
return this._context._commands._tryCommit({
|
this._eventListeners.get(event)?.delete(listener);
|
||||||
name: def.schema.name,
|
};
|
||||||
params: args,
|
}
|
||||||
options: {},
|
|
||||||
flags: {}
|
private _emitEvent(event: "start" | "dispose") {
|
||||||
});
|
const listeners = this._eventListeners.get(event);
|
||||||
}
|
if (listeners) {
|
||||||
|
for (const listener of listeners) {
|
||||||
/**
|
listener();
|
||||||
* 为下一个 produceAsync 注册中断 Promise(通常用于 UI 动画)。
|
}
|
||||||
* @see MutableSignal.addInterruption
|
|
||||||
*/
|
|
||||||
addInterruption(promise: Promise<void>): void {
|
|
||||||
this._context._state.addInterruption(promise);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 清除所有未完成的中断。
|
|
||||||
* @see MutableSignal.clearInterruptions
|
|
||||||
*/
|
|
||||||
clearInterruptions(): void {
|
|
||||||
this._context._state.clearInterruptions();
|
|
||||||
}
|
|
||||||
|
|
||||||
start(seed?: number): Promise<TResult> {
|
|
||||||
if (this._isDisposed) {
|
|
||||||
throw new Error('GameHost is disposed');
|
|
||||||
}
|
|
||||||
|
|
||||||
this._context._commands._cancel();
|
|
||||||
|
|
||||||
const initialState = this._createInitialState();
|
|
||||||
this._context._state.value = initialState as any;
|
|
||||||
|
|
||||||
seed = seed || Math.floor(Math.random() * Number.MAX_SAFE_INTEGER);
|
|
||||||
this._context._rng.setSeed(seed);
|
|
||||||
const promise = this._start(this._context);
|
|
||||||
|
|
||||||
this._status.value = 'running';
|
|
||||||
this._emitEvent('start');
|
|
||||||
|
|
||||||
return promise;
|
|
||||||
}
|
|
||||||
|
|
||||||
dispose(): void {
|
|
||||||
if (this._isDisposed) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this._isDisposed = true;
|
|
||||||
this._context._commands._cancel();
|
|
||||||
this._status.value = 'disposed';
|
|
||||||
|
|
||||||
// Emit dispose event BEFORE clearing listeners
|
|
||||||
this._emitEvent('dispose');
|
|
||||||
this._eventListeners.clear();
|
|
||||||
}
|
|
||||||
|
|
||||||
on(event: 'start' | 'dispose', listener: () => void): () => void {
|
|
||||||
if (!this._eventListeners.has(event)) {
|
|
||||||
this._eventListeners.set(event, new Set());
|
|
||||||
}
|
|
||||||
this._eventListeners.get(event)!.add(listener);
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
this._eventListeners.get(event)?.delete(listener);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
private _emitEvent(event: 'start' | 'dispose') {
|
|
||||||
const listeners = this._eventListeners.get(event);
|
|
||||||
if (listeners) {
|
|
||||||
for (const listener of listeners) {
|
|
||||||
listener();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export type GameModule<TState extends Record<string, unknown>, TResult=unknown> = {
|
export type GameModule<
|
||||||
registry?: CommandRegistry<IGameContext<TState>>;
|
TState extends Record<string, unknown>,
|
||||||
createInitialState: () => TState;
|
TResult = unknown,
|
||||||
start: (ctx: IGameContext<TState>) => Promise<TResult>;
|
> = {
|
||||||
}
|
registry?: CommandRegistry<IGameContext<TState>>;
|
||||||
|
createInitialState: () => TState;
|
||||||
|
start: (ctx: IGameContext<TState>) => Promise<TResult>;
|
||||||
|
};
|
||||||
|
|
||||||
export function createGameHost<TState extends Record<string, unknown>>(
|
export function createGameHost<
|
||||||
gameModule: GameModule<TState>
|
TState extends Record<string, unknown>,
|
||||||
): GameHost<TState> {
|
TResult = unknown,
|
||||||
return new GameHost(
|
>(gameModule: GameModule<TState, TResult>): GameHost<TState, TResult> {
|
||||||
gameModule.registry || createGameCommandRegistry(),
|
return new GameHost(
|
||||||
gameModule.createInitialState,
|
gameModule.registry || createGameCommandRegistry(),
|
||||||
gameModule.start
|
gameModule.createInitialState,
|
||||||
);
|
gameModule.start,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue