ecspresso
Version:
A minimal Entity-Component-System library for typescript and javascript.
178 lines (177 loc) • 6.21 kB
TypeScript
/**
* Timer Plugin for ECSpresso
*
* ECS-native timers as pure data. An entity may carry multiple named timer
* slots; the plugin's update system ticks every slot each frame and exposes
* `justFinished` for the frame a slot crosses its duration. The plugin never
* touches entity lifecycle — callers despawn (or do anything else) themselves
* by reacting to `justFinished` or in the slot's `onComplete` callback.
*/
import { type BasePluginOptions } from 'ecspresso';
/**
* Data passed to a slot's `onComplete` callback when its timer completes.
*
* @example
* ```typescript
* timers: {
* launch: createTimer(1.5, {
* onComplete: ({ entityId, slot, elapsed }) => {
* console.log(`Slot ${slot} on entity ${entityId} finished after ${elapsed}s`);
* },
* }),
* }
* ```
*/
export interface TimerEventData<Slots extends string = string> {
/** The entity ID that owns the timer slot */
entityId: number;
/** The slot name within the entity's `timers` map */
slot: Slots;
/** The slot's configured duration in seconds */
duration: number;
/** The actual elapsed time (may exceed duration slightly) */
elapsed: number;
}
/**
* A single timer's data. Multiple of these can live on one entity, keyed by slot name.
* Use `justFinished` to detect completion in your systems.
*/
export interface Timer<Slots extends string = string> {
/** Time accumulated so far (seconds) */
elapsed: number;
/** Target duration (seconds) */
duration: number;
/** Whether the timer repeats after completion */
repeat: boolean;
/** Whether the timer is currently running */
active: boolean;
/** True for one frame after the timer completes */
justFinished: boolean;
/** Optional callback invoked when the timer completes */
onComplete?: (data: TimerEventData<Slots>) => void;
}
/**
* Component types provided by the timer plugin.
*
* Each entity carries a single `timers` component whose value is a map of
* named slots. This lets one entity host independent phase clocks
* (e.g. `{ launch: ..., shieldDepletion: ..., hangarCycle: ... }`) without
* one timer's lifecycle constraining another.
*
* @example
* ```typescript
* const ecs = ECSpresso.create()
* .withPlugin(createTimerPlugin())
* .withComponentTypes<{ fighter: true }>()
* .build();
*
* ecs.spawn({
* fighter: true,
* timers: { launch: createTimer(2.0) },
* });
* ```
*/
export interface TimerComponentTypes<Slots extends string = string> {
timers: Partial<Record<Slots, Timer<Slots>>>;
}
export interface TimerPluginOptions<G extends string = 'timers'> extends BasePluginOptions<G> {
}
export interface TimerOptions<Slots extends string = string> {
/** Callback invoked when the timer completes */
onComplete?: (data: TimerEventData<Slots>) => void;
}
/**
* Create a one-shot `Timer` to drop into a `timers` slot.
*
* The timer fires `justFinished` for one frame on completion and then idles
* (`active = false`). The entity is left alone — if the slot's lifetime
* coincides with the entity's lifetime (vfx, blasts, summon-anim), despawn
* the host yourself in `onComplete` or in a system that watches `justFinished`.
*
* @example
* ```typescript
* ecs.spawn({
* fighter: true,
* timers: { launch: createTimer(2.0) },
* });
*
* // Self-destructing vfx — caller owns the despawn:
* ecs.spawn({
* timers: {
* fade: createTimer(1.0, {
* onComplete: ({ entityId }) => ecs.commands.removeEntity(entityId),
* }),
* },
* });
* ```
*/
export declare function createTimer<Slots extends string = string>(duration: number, options?: TimerOptions<Slots>): Timer<Slots>;
/**
* Create a repeating `Timer` to drop into a `timers` slot. Fires
* `justFinished` once per cycle and continues running.
*
* @example
* ```typescript
* ecs.spawn({
* carrier: true,
* timers: { hangarCycle: createRepeatingTimer(5.0) },
* });
* ```
*/
export declare function createRepeatingTimer<Slots extends string = string>(duration: number, options?: TimerOptions<Slots>): Timer<Slots>;
/**
* Create a timer plugin for ECSpresso.
*
* The plugin installs one update system that ticks every slot of every
* `timers` component each frame. It does not touch entity lifecycle —
* react to `justFinished` (or use `onComplete`) and despawn yourself if needed.
*
* @example
* ```typescript
* const ecs = ECSpresso.create()
* .withPlugin(createTimerPlugin())
* .withComponentTypes<{ spawner: true }>()
* .build();
*
* ecs.spawn({
* spawner: true,
* timers: { wave: createRepeatingTimer(5.0) },
* });
*
* ecs.addSystem('spawn-on-timer')
* .addQuery('spawners', { with: ['timers', 'spawner'] })
* .setProcess(({ queries, ecs }) => {
* for (const { components } of queries.spawners) {
* if (components.timers.wave?.justFinished) {
* ecs.spawn({ enemy: true });
* }
* }
* });
* ```
*
* @example
* Typed slot names — pass a string-union generic to lock the set of legal
* slot names. Spawn sites reject typos, autocomplete works on slot access,
* and `slot` is narrowed in `onComplete` callbacks. Defaults to `string`
* (any slot name) when omitted.
*
* ```typescript
* const ecs = ECSpresso.create()
* .withPlugin(createTimerPlugin<'launch' | 'hangarCycle'>())
* .build();
*
* ecs.spawn({ timers: { launch: createTimer(2.0) } }); // ok
* ecs.spawn({ timers: { typo: createTimer(2.0) } }); // type error
*
* createTimer<'launch' | 'hangarCycle'>(1.0, {
* onComplete: ({ slot }) => {
* // slot is 'launch' | 'hangarCycle', not string
* },
* });
* ```
*
* Only one timer plugin can be installed per world. Feature plugins should
* re-export their slot union as a type so the app can assemble them:
* `createTimerPlugin<FighterSlots | CarrierSlots>()`.
*/
export declare function createTimerPlugin<Slots extends string = string, G extends string = 'timers'>(options?: TimerPluginOptions<G>): import("ecspresso").Plugin<import("ecspresso").WithComponents<import("ecspresso").EmptyConfig, TimerComponentTypes<Slots>>, import("ecspresso").EmptyConfig, "timer-update", G, never, never>;