ecspresso
    Preparing search index...

    Plugins

    Organize related systems and resources into reusable plugins:

    import ECSpresso, { definePlugin } from 'ecspresso';

    interface PhysicsComponents {
    position: { x: number; y: number };
    velocity: { x: number; y: number };
    }

    interface PhysicsResources {
    gravity: { value: number };
    }

    const physicsPlugin = definePlugin('physics')
    .withComponentTypes<PhysicsComponents>()
    .withResourceTypes<PhysicsResources>()
    .install((world) => {
    world.addSystem('applyVelocity')
    .addQuery('moving', { with: ['position', 'velocity'] })
    .setProcess(({ queries, dt }) => {
    for (const entity of queries.moving) {
    entity.components.position.x += entity.components.velocity.x * dt;
    entity.components.position.y += entity.components.velocity.y * dt;
    }
    });

    world.addSystem('applyGravity')
    .addQuery('falling', { with: ['velocity'] })
    .setProcess(({ queries, dt, ecs }) => {
    const gravity = ecs.getResource('gravity');
    for (const entity of queries.falling) {
    entity.components.velocity.y += gravity.value * dt;
    }
    });

    world.addResource('gravity', { value: 9.8 });
    });

    // Register plugins with the world — types merge automatically
    const game = ECSpresso.create()
    .withPlugin(physicsPlugin)
    .build();

    The install function receives a second argument, onCleanup, for registering disposers that run when the plugin is uninstalled or the world is torn down. Use it to remove event listeners, cancel timers, or release any external resources the plugin acquired.

    const inputPlugin = definePlugin('input')
    .install((world, onCleanup) => {
    const handler = (e: KeyboardEvent) => { /* ... */ };
    window.addEventListener('keydown', handler);
    onCleanup(() => window.removeEventListener('keydown', handler));

    const off = world.on('someEvent', () => { /* ... */ });
    onCleanup(off);
    });

    Disposers run in reverse registration order. A failing disposer does not prevent later ones from running.

    world.uninstallPlugin('input');  // runs cleanup disposers, returns true if found
    world.dispose(); // uninstalls all plugins, then cleans up world state

    When multiple plugins share the same types (common in application code), use pluginFactory() on the builder or built world to capture types automatically:

    // types.ts — builder accumulates all types
    export const builder = ECSpresso.create()
    .withPlugin(createPhysicsPlugin())
    .withComponentTypes<{ player: boolean; enemy: EnemyData }>()
    .withResourceTypes<{ score: number }>();

    // Types flow from the builder — no manual imports or extends chains
    export const definePlugin = builder.pluginFactory();

    // movement-plugin.ts — no type params needed
    import { definePlugin } from './types';

    export const movementPlugin = definePlugin({
    id: 'movement',
    install(world) {
    world.addSystem('movement')
    .addQuery('moving', { with: ['position', 'velocity'] })
    .setProcess(({ queries, dt }) => { /* ... */ });
    },
    });

    Use slot-specific config helpers when a plugin requires existing world types but does not provide them itself. This avoids spelling empty WorldConfigFrom slots just to reach resources, assets, events, or screens.

    import {
    definePlugin,
    type ComponentsConfig,
    type ResourcesConfig,
    type ScreensConfig,
    type ScreenDefinition,
    } from 'ecspresso';

    type TransformComponents = {
    worldTransform: { x: number; y: number };
    };

    type InputResources = {
    input: { isPressed(action: string): boolean };
    };

    type RequiredScreens = {
    playing: ScreenDefinition<{ level: number }>;
    };

    type MovementRequires =
    ComponentsConfig<TransformComponents>
    & ResourcesConfig<InputResources>
    & ScreensConfig<RequiredScreens>;

    export const movementPlugin = definePlugin('movement')
    .requires<MovementRequires>()
    .install((world) => {
    world.addSystem('movement')
    .inScreens(['playing'])
    .setProcess(({ ecs }) => {
    const input = ecs.getResource('input');
    if (!input.isPressed('right')) return;
    });
    });

    Available helpers are ComponentsConfig<T>, EventsConfig<T>, ResourcesConfig<T>, AssetsConfig<T>, and ScreensConfig<T>. Use WorldConfigFrom directly when a type naturally spans several slots and the positional form is still clearer.

    Plugins can declare that certain components depend on others. When an entity gains a trigger component, any required components that aren't already present are auto-added with default values:

    const transformPlugin = definePlugin('transform')
    .withComponentTypes<TransformComponents>()
    .install((world) => {
    world.registerRequired('localTransform', 'worldTransform', () => ({
    x: 0, y: 0, rotation: 0, scaleX: 1, scaleY: 1,
    }));
    });

    const world = ECSpresso.create()
    .withPlugin(transformPlugin)
    .build();

    // worldTransform is auto-added with defaults
    const entity = world.spawn({
    localTransform: { x: 100, y: 200, rotation: 0, scaleX: 1, scaleY: 1 },
    });

    // Explicit values always win — no auto-add if already provided
    const entity2 = world.spawn({
    localTransform: { x: 100, y: 200, rotation: 0, scaleX: 1, scaleY: 1 },
    worldTransform: { x: 50, y: 50, rotation: 0, scaleX: 2, scaleY: 2 }, // used as-is
    });

    Requirements can also be registered via the builder or at runtime:

    // Builder
    const world = ECSpresso.create()
    .withComponentTypes<Components>()
    .withRequired('rigidBody', 'velocity', () => ({ x: 0, y: 0 }))
    .withRequired('rigidBody', 'force', () => ({ x: 0, y: 0 }))
    .build();

    // Runtime
    world.registerRequired('position', 'velocity', () => ({ x: 0, y: 0 }));
    • Enforced at insertion time (spawn, addComponent, addComponents, spawnChild, command buffer)
    • Removal is unrestricted — removing a required component does not cascade
    • Transitive requirements resolve automatically (A requires B, B requires C → all three added)
    • Circular dependencies are detected and rejected at registration time
    • Auto-added components are marked as changed and trigger reactive queries
    • Component names and factory return types are fully type-checked

    The Transform plugin registers localTransformworldTransform. The Physics 2D plugin registers rigidBodyvelocity and rigidBodyforce.