UNPKG

ecspresso

Version:

A minimal Entity-Component-System library for typescript and javascript.

244 lines (243 loc) 8.5 kB
/** * State Machine Plugin for ECSpresso * * Provides ECS-native finite state machines with guard-based transitions, * event-driven transitions, and lifecycle hooks (onEnter, onExit, onUpdate). * * Each entity gets a `stateMachine` component referencing a shared definition. * One system processes all state machine entities each tick. */ import { type BasePluginOptions } from 'ecspresso'; import type { BaseWorld } from 'ecspresso'; /** BaseWorld narrowed to state-machine components for typed access in helpers. */ type StateMachineWorld = BaseWorld<StateMachineComponentTypes>; /** * Configuration for a single state in a state machine definition. * * @template S - Union of state name strings * @template W - World interface type for hooks/guards (default: StateMachineWorld) */ export interface StateConfig<S extends string, W extends BaseWorld<StateMachineComponentTypes> = StateMachineWorld> { /** Called when entering this state */ onEnter?(ctx: { ecs: W; entityId: number; }): void; /** Called when exiting this state */ onExit?(ctx: { ecs: W; entityId: number; }): void; /** Called each tick while in this state */ onUpdate?(ctx: { ecs: W; entityId: number; dt: number; }): void; /** Guard-based transitions evaluated each tick. First passing guard wins. */ transitions?: ReadonlyArray<{ target: S; guard(ctx: { ecs: W; entityId: number; }): boolean; }>; /** Event-based transition map: eventName → target state or guarded transition */ on?: Record<string, S | { target: S; guard(ctx: { ecs: W; entityId: number; }): boolean; }>; } /** * Immutable definition of a state machine. Shared across entities. * * @template S - Union of state name strings */ export interface StateMachineDefinition<S extends string> { readonly id: string; readonly initial: S; readonly states: { readonly [K in S]: StateConfig<S>; }; } /** * Runtime state machine component stored on each entity. * * @template S - Union of state name strings (default: string) */ export interface StateMachine<S extends string = string> { readonly definition: StateMachineDefinition<string>; current: S; previous: S | null; stateTime: number; } /** * Component types provided by the state machine plugin. * * @template S - Union of state name strings (default: string) */ export interface StateMachineComponentTypes<S extends string = string> { stateMachine: StateMachine<S>; } /** * Event published on every state transition. * * @template S - Union of state name strings (default: string) */ export interface StateTransitionEvent<S extends string = string> { entityId: number; from: S; to: S; definitionId: string; } /** * Event types provided by the state machine plugin. * * @template S - Union of state name strings (default: string) */ export interface StateMachineEventTypes<S extends string = string> { stateTransition: StateTransitionEvent<S>; } /** * Extract the state name union from a StateMachineDefinition. * * @example * ```typescript * const enemyFSM = defineStateMachine('enemy', { initial: 'idle', states: { idle: {}, chase: {} } }); * type EnemyStates = StatesOf<typeof enemyFSM>; // 'idle' | 'chase' * type AllStates = StatesOf<typeof enemyFSM> | StatesOf<typeof playerFSM>; * ``` */ export type StatesOf<D> = D extends StateMachineDefinition<infer S> ? S : never; /** * Configuration options for the state machine plugin. */ export interface StateMachinePluginOptions<G extends string = 'stateMachine'> extends BasePluginOptions<G> { } /** * Define a state machine with type-safe state names. * * @template S - Union of state name strings, inferred from `states` keys * @param id - Unique identifier for this definition * @param config - Initial state and state configurations * @returns A frozen StateMachineDefinition * * @example * ```typescript * const enemyFSM = defineStateMachine('enemy', { * initial: 'idle', * states: { * idle: { * onEnter: ({ ecs, entityId }) => { ... }, * transitions: [{ target: 'chase', guard: ({ ecs, entityId }) => playerNearby(ecs, entityId) }], * }, * chase: { * onUpdate: ({ ecs, entityId, dt }) => { ... }, * on: { playerLost: 'idle' }, * }, * }, * }); * ``` */ export declare function defineStateMachine<S extends string>(id: string, config: { initial: NoInfer<S>; states: Record<S, StateConfig<NoInfer<S>>>; }): StateMachineDefinition<S>; /** * Create a stateMachine component from a definition. * * @param definition - The state machine definition to use * @param options - Optional overrides (e.g., initial state) * @returns Component object suitable for spreading into spawn() * * @example * ```typescript * ecs.spawn({ * ...createStateMachine(enemyFSM), * position: { x: 100, y: 200 }, * }); * ``` */ export declare function createStateMachine<S extends string>(definition: StateMachineDefinition<S>, options?: { initial?: S; }): Pick<StateMachineComponentTypes<S>, 'stateMachine'>; /** * Directly transition an entity's state machine to a target state. * Fires onExit, onEnter hooks and publishes stateTransition event. * * @param ecs - ECS instance (structural typing) * @param entityId - Entity to transition * @param targetState - State to transition to * @returns true if transition succeeded, false if entity has no stateMachine or target state doesn't exist */ export declare function transitionTo(ecs: StateMachineWorld, entityId: number, targetState: string): boolean; /** * Send a named event to an entity's state machine. * Checks the current state's `on` handlers for a matching event. * * @param ecs - ECS instance (structural typing) * @param entityId - Entity to send event to * @param eventName - Event name to match against `on` handlers * @returns true if a transition occurred, false otherwise */ export declare function sendEvent(ecs: StateMachineWorld, entityId: number, eventName: string): boolean; /** * Get the current state of an entity's state machine. * * @param ecs - ECS instance (structural typing) * @param entityId - Entity to query * @returns The current state string, or undefined if entity has no stateMachine */ export declare function getStateMachineState(ecs: StateMachineWorld, entityId: number): string | undefined; /** * Typed helpers for the state machine plugin. * Creates helpers that validate hook parameters against the world type W. * Call after .build() using typeof ecs. */ export interface StateMachineHelpers<W extends BaseWorld<StateMachineComponentTypes>> { defineStateMachine: <S extends string>(id: string, config: { initial: NoInfer<S>; states: Record<S, StateConfig<NoInfer<S>, W>>; }) => StateMachineDefinition<S>; } export declare function createStateMachineHelpers<W extends BaseWorld<StateMachineComponentTypes> = StateMachineWorld>(_world?: W): StateMachineHelpers<W>; /** * Create a state machine plugin for ECSpresso. * * Provides: * - Lifecycle hooks (onEnter, onExit, onUpdate) per state * - Guard-based automatic transitions evaluated each tick * - Event-based transitions via `sendEvent()` * - Direct transitions via `transitionTo()` * - stateTransition events published on every transition * * @example * ```typescript * const ecs = ECSpresso.create() * .withPlugin(createStateMachinePlugin()) * .build(); * * const fsm = defineStateMachine('enemy', { * initial: 'idle', * states: { * idle: { * transitions: [{ target: 'chase', guard: ({ ecs, entityId }) => playerNearby(ecs, entityId) }], * }, * chase: { * onUpdate: ({ ecs, entityId, dt }) => moveTowardPlayer(ecs, entityId, dt), * on: { playerLost: 'idle' }, * }, * }, * }); * * ecs.spawn({ * ...createStateMachine(fsm), * position: { x: 0, y: 0 }, * }); * ``` */ export declare function createStateMachinePlugin<S extends string = string, G extends string = 'stateMachine'>(options?: StateMachinePluginOptions<G>): import("ecspresso").Plugin<import("ecspresso").WithEvents<import("ecspresso").WithComponents<import("ecspresso").EmptyConfig, StateMachineComponentTypes<S>>, StateMachineEventTypes<S>>, import("ecspresso").EmptyConfig, "state-machine-update", G, never, never>; export {};