ecspresso
Version:
A minimal Entity-Component-System library for typescript and javascript.
244 lines (243 loc) • 8.5 kB
TypeScript
/**
* 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 {};