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:
- Skips instances where
inst.enabled === false. - For each
custom_tickaction on the instance, readsparams.handlerand looks it up inCustomTickHandlers. Missing handlers are silently skipped. - Resolves numeric params via
resolveNumber()so$varNamereferences in the action params get substituted with the instance’svaluestable (booleans become1/0; non-numeric params are skipped). - 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
_tickHandlersatregister(). Rebuilt duringunregister(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:
signaltrigger with a periodic source signal,timertrigger (one-shot or repeating at a fixed interval), orauratrigger (re-evaluated everyauraIntervalseconds 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.
Related
- Effect Engine — register / init / tick / clear lifecycle
- Action Catalog — full list of action types including
custom_tick - Trigger System — signal / aura / timer / counter / run_start triggers