diff --git a/src/core/game-host.ts b/src/core/game-host.ts index cdf7ab3..a41a9f5 100644 --- a/src/core/game-host.ts +++ b/src/core/game-host.ts @@ -1,23 +1,23 @@ import { ReadonlySignal, Signal } from '@preact/signals-core'; -import type { CommandSchema, CommandRegistry, PromptEvent } from '@/utils/command'; +import type {CommandSchema, CommandRegistry, PromptEvent, CommandRunnerContextExport} from '@/utils/command'; import type { MutableSignal } from '@/utils/mutable-signal'; -import { createGameContext } from './game'; +import {createGameContext, IGameContext} from './game'; export type GameHostStatus = 'created' | 'running' | 'disposed'; export interface GameModule> { - registry: CommandRegistry>; + registry: CommandRegistry>; createInitialState: () => TState; } export class GameHost> { - readonly state: ReadonlySignal; - readonly commands: ReturnType>['commands']; + 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; @@ -26,17 +26,14 @@ export class GameHost> { private _isDisposed = false; constructor( - registry: CommandRegistry>, + registry: CommandRegistry>, createInitialState: () => TState, ) { this._createInitialState = createInitialState; this._eventListeners = new Map(); const initialState = createInitialState(); - const context = createGameContext(registry, initialState); - - this._state = context.state; - this.commands = context.commands; + this.context = createGameContext(registry, initialState); this._status = new Signal('created'); this.status = this._status; @@ -47,7 +44,8 @@ export class GameHost> { this._activePromptPlayer = new Signal(null); this.activePromptPlayer = this._activePromptPlayer; - this.state = this._state; + this._state = this.context._state; + this._commands = this.context._commands; this._setupPromptTracking(); } @@ -55,13 +53,13 @@ export class GameHost> { private _setupPromptTracking() { let currentPromptEvent: PromptEvent | null = null; - this.commands.on('prompt', (e) => { + this._commands.on('prompt', (e) => { currentPromptEvent = e as PromptEvent; this._activePromptSchema.value = currentPromptEvent.schema; this._activePromptPlayer.value = currentPromptEvent.currentPlayer; }); - this.commands.on('promptEnd', () => { + this._commands.on('promptEnd', () => { currentPromptEvent = null; this._activePromptSchema.value = null; this._activePromptPlayer.value = null; @@ -76,7 +74,7 @@ export class GameHost> { if (this._isDisposed) { return 'GameHost is disposed'; } - return this.commands._tryCommit(input); + return this._commands._tryCommit(input); } /** @@ -100,14 +98,14 @@ export class GameHost> { throw new Error('GameHost is disposed'); } - this.commands._cancel(); + this._commands._cancel(); const initialState = this._createInitialState(); this._state.value = initialState as any; // Start the setup command but don't wait for it to complete // The command will run in the background and prompt for input - this.commands.run(setupCommand).catch(() => { + this._commands.run(setupCommand).catch(() => { // Command may be cancelled or fail, which is expected }); @@ -121,7 +119,7 @@ export class GameHost> { } this._isDisposed = true; - this.commands._cancel(); + this._commands._cancel(); this._status.value = 'disposed'; // Emit dispose event BEFORE clearing listeners diff --git a/src/core/game.ts b/src/core/game.ts index 1c3a9ae..72ced91 100644 --- a/src/core/game.ts +++ b/src/core/game.ts @@ -2,7 +2,7 @@ import {MutableSignal, mutableSignal} from "@/utils/mutable-signal"; import { Command, CommandRegistry, CommandResult, - CommandRunnerContext, + CommandRunnerContext, CommandRunnerContextExport, CommandSchema, createCommandRegistry, createCommandRunnerContext, @@ -22,7 +22,7 @@ export interface IGameContext = {} > { // test only _state: MutableSignal; - _commands: CommandRunnerContext>; + _commands: CommandRunnerContextExport>; } export function createGameContext = {} >( @@ -31,7 +31,7 @@ export function createGameContext = {} >( ): IGameContext { const stateValue = typeof initialState === 'function' ? initialState() : initialState ?? {} as TState; const state = mutableSignal(stateValue); - let commands: CommandRunnerContext> = null as any; + let commands: CommandRunnerContextExport> = null as any; const context: IGameContext = { get value(): TState { @@ -82,18 +82,30 @@ export function createGameCommandRegistry return createCommandRegistry>(); } -export function registerGameCommand = {}, TResult = unknown>( +type CmdFunc = {}> = (this: IGameContext, ...args: any[]) => Promise; + +export function registerGameCommand = {}, TFunc extends CmdFunc = CmdFunc>( registry: CommandRegistry>, schema: CommandSchema | string, - run: (this: IGameContext, ...args: any[]) => Promise + run: TFunc ) { + const parsedSchema = typeof schema === 'string' ? parseCommandSchema(schema) : schema; registerCommand(registry, { - schema: typeof schema === 'string' ? parseCommandSchema(schema) : schema, + schema: parsedSchema, async run(this: CommandRunnerContext>, command: Command){ const params = command.params; return await run.call(this.context, ...params); }, }); + + return function(game: IGameContext, ...args: Parameters){ + return game.runParsed({ + options: {}, + params: args, + flags: {}, + name: parsedSchema.name, + }); + } } export { GameHost, createGameHost } from './game-host'; diff --git a/src/utils/command/command-registry.ts b/src/utils/command/command-registry.ts index d93817d..24721cd 100644 --- a/src/utils/command/command-registry.ts +++ b/src/utils/command/command-registry.ts @@ -5,10 +5,36 @@ import { applyCommandSchema } from './command-validate'; import { parseCommandSchema } from './schema-parse'; import {AsyncQueue} from "@/utils/async-queue"; -export type CommandRegistry = Map>; +type CanRunParsed = { + runParsed(command: Command): Promise>, +} + +type CmdFunc = (this: TContext, ...args: any[]) => Promise; +export class CommandRegistry extends Map>{ + register = CmdFunc>(schema: CommandSchema | string, run: TFunc) { + const parsedSchema = typeof schema === 'string' ? parseCommandSchema(schema) : schema; + registerCommand(this, { + schema: parsedSchema, + async run(this: CommandRunnerContext, command: Command){ + const params = command.params; + return await run.call(this.context, ...params); + }, + }); + + type TResult = TFunc extends (this: TContext, ...args: any[]) => Promise ? X : null; + return function(ctx: TContext & CanRunParsed, ...args: Parameters){ + return ctx.runParsed({ + options: {}, + params: args, + flags: {}, + name: parsedSchema.name, + }) as Promise>; + } + } +} export function createCommandRegistry(): CommandRegistry { - return new Map(); + return new CommandRegistry(); } export function registerCommand( @@ -137,7 +163,7 @@ export function createCommandRunnerContext( registry, context, run: (input: string) => runCommandWithContext(runnerCtx, input) as Promise>, - runParsed: (command: Command) => runCommandParsedWithContext(runnerCtx, command), + runParsed: (command: Command) => runCommandParsedWithContext(runnerCtx, command) as Promise>, prompt, on, off,