Custom Tick Handler

Per-frame custom logic injected into the effect engine via the custom_tick action type. Used for sustained effects that need imperative code each frame instead of one-shot data-driven actions (e.g. Personal Space invulnerability, force fields, companion droids).

Pattern

A custom_tick action references a named handler registered in CustomTickHandlers (see engine/effects/custom-registry.ts). The effect engine calls the handler every frame while the owning effect instance is enabled.

Signature (from engine/effects/types.ts):

export type CustomTickFn = (
  dt: number,
  params: Record<string, number>,
  game: GameState,
  ship: ShipState,
  world: WorldState,
) => void;

Registration (see custom-registry.ts):

registerCustomTick('my_handler', (dt, params, game, ship, world) => { ... });

The registry rejects duplicate names — handlers are unique by string key.

Dispatch path

The effect engine indexes any instance whose definition has an action of type: 'custom_tick' into a _tickHandlers list at register() time. Each frame, EffectEngine.tick(dt, game, ship, world) walks this list and:

  1. Skips instances where inst.enabled === false.
  2. For each custom_tick action on the instance, reads params.handler and looks it up in CustomTickHandlers. Missing handlers are silently skipped.
  3. Resolves numeric params via resolveNumber() so $varName references in the action params get substituted with the instance’s values table (booleans become 1/0; non-numeric params are skipped).
  4. Calls handler(dt, resolved, game, ship, world).

The handler key in params is always stripped before being passed in — handlers receive only the resolved numeric params they need.

Lifecycle

  • Indexed into _tickHandlers at register(). Rebuilt during unregister(owner).
  • Cleared in EffectEngine.clear() at run start and end.
  • Tied to inst.enabled. A handler can be paused mid-run by flipping that flag elsewhere in the engine (e.g. timer one-shot completion).

Unlike signal-triggered effects (CustomHandlers), tick handlers do not consume charges, respect cooldowns, or evaluate conditions — they run unconditionally every frame while enabled. Any gating logic must live inside the handler body.

When to use

Use custom_tick only when the effect must run continuously every frame and cannot be expressed via:

  • signal trigger with a periodic source signal,
  • timer trigger (one-shot or repeating at a fixed interval), or
  • aura trigger (re-evaluated every auraInterval seconds for modifier application).

Typical fits: sustained invulnerability windows that need per-frame Modifiers.add refresh, companion entities that need to follow the ship and fire each frame, ongoing post-FX bound to ship position, custom physics integration.