import { ReadonlySignal, Signal } from '@preact/signals-core'; import type { CommandSchema, CommandRegistry, PromptEvent, CommandRunnerContextExport, CommandResult } from '@/utils/command'; import type { MutableSignal } from '@/utils/mutable-signal'; import {createGameContext, IGameContext} from './game'; export type GameHostStatus = 'created' | 'running' | 'disposed'; export interface GameModule> { registry: CommandRegistry>; createInitialState: () => TState; } export class GameHost> { readonly context: IGameContext; readonly status: ReadonlySignal; readonly activePromptSchema: ReadonlySignal; readonly activePromptPlayer: ReadonlySignal; private _state: MutableSignal; private _commands: CommandRunnerContextExport>; private _status: Signal; private _activePromptSchema: Signal; private _activePromptPlayer: Signal; private _createInitialState: () => TState; private _eventListeners: Map<'setup' | 'dispose', Set<() => void>>; private _isDisposed = false; constructor( registry: CommandRegistry>, createInitialState: () => TState, ) { this._createInitialState = createInitialState; this._eventListeners = new Map(); const initialState = createInitialState(); this.context = createGameContext(registry, initialState); this._status = new Signal('created'); this.status = this._status; this._activePromptSchema = new Signal(null); this.activePromptSchema = this._activePromptSchema; this._activePromptPlayer = new Signal(null); this.activePromptPlayer = this._activePromptPlayer; this._state = this.context._state; this._commands = this.context._commands; this._setupPromptTracking(); } private _setupPromptTracking() { let currentPromptEvent: PromptEvent | null = null; this._commands.on('prompt', (e) => { currentPromptEvent = e as PromptEvent; this._activePromptSchema.value = currentPromptEvent.schema; this._activePromptPlayer.value = currentPromptEvent.currentPlayer; }); this._commands.on('promptEnd', () => { currentPromptEvent = null; this._activePromptSchema.value = null; this._activePromptPlayer.value = null; }); // Initial state this._activePromptSchema.value = null; this._activePromptPlayer.value = null; } onInput(input: string): string | null { if (this._isDisposed) { return 'GameHost is disposed'; } return this._commands._tryCommit(input); } /** * 为下一个 produceAsync 注册中断 Promise(通常用于 UI 动画)。 * @see MutableSignal.addInterruption */ addInterruption(promise: Promise): void { this._state.addInterruption(promise); } /** * 清除所有未完成的中断。 * @see MutableSignal.clearInterruptions */ clearInterruptions(): void { this._state.clearInterruptions(); } setup(setupCommand: string): Promise> { if (this._isDisposed) { throw new Error('GameHost is disposed'); } this._commands._cancel(); const initialState = this._createInitialState(); this._state.value = initialState as any; const promise = this._commands.run(setupCommand); this._status.value = 'running'; this._emitEvent('setup'); return promise; } dispose(): void { if (this._isDisposed) { return; } this._isDisposed = true; this._commands._cancel(); this._status.value = 'disposed'; // Emit dispose event BEFORE clearing listeners this._emitEvent('dispose'); this._eventListeners.clear(); } on(event: 'setup' | '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: 'setup' | 'dispose') { const listeners = this._eventListeners.get(event); if (listeners) { for (const listener of listeners) { listener(); } } } } export function createGameHost>( module: GameModule, ): GameHost { return new GameHost( module.registry, module.createInitialState, ); }