PURPOSE

Backing store for the unified effect system’s named code-callbacks. Effects whose behavior cannot be expressed as data-driven trigger / condition / action params declare an action.type of custom or custom_tick and reference a handler by name string; this registry is the lookup table that resolves those names to functions. Splitting the maps and the registration functions out of custom-handlers.ts lets actions.ts and effect-engine.ts import the lookup tables without pulling in every handler implementation as a dependency.

OWNS

  • CustomHandlers: Record<string, CustomActionFn> — module-level map from handler name to signal-triggered action function. Mutable, populated at import time. Starts empty.
  • CustomTickHandlers: Record<string, CustomTickFn> — module-level map from handler name to per-frame tick function. Mutable, populated at import time. Starts empty.
  • registerCustomAction(name, fn) — adds a CustomActionFn under name. Throws Duplicate custom action handler: <name> if name is already present.
  • registerCustomTick(name, fn) — adds a CustomTickFn under name. Throws Duplicate custom tick handler: <name> if name is already present.

READS FROM

  • ./typesCustomActionFn and CustomTickFn function-signature types (for the map value types and the register-function parameter types).

This file performs no runtime reads — its only inputs are the values passed to the two register functions.

PUSHES TO

The two exported maps are read by the rest of the effect system:

  • effects/actions.tsactCustom looks up CustomHandlers[handlerName] to execute a custom action. Throws Unknown custom action handler: <name> on miss.
  • effects/effect-engine.ts — the per-frame tick loop reads CustomTickHandlers[handlerName] for every custom_tick action on registered effects. Silently skips on miss.
  • effects/index.ts — re-exports both maps and both register functions as the public surface of the effects barrel.

DOES NOT

  • Does not define any handlers itself. All handler bodies live in effects/custom-handlers.ts.
  • Does not resolve $var params, the handler action param, or snapshot capture — those happen at the call sites in actions.ts and effect-engine.ts.
  • Does not gate by owner, charges, cooldown, or enabled flag — those are the effect engine’s responsibility.
  • Does not provide an unregister, override, or clear API. Once a name is registered for the lifetime of the module, it is fixed; the duplicate-name throw enforces this.
  • Does not validate the function signature beyond TypeScript’s compile-time check, and does not assert the name is non-empty or matches any naming convention.
  • Does not initialize itself with a built-in handler set; the maps are empty until custom-handlers.ts is imported (which happens via the effects barrel).

Signals

This file does not subscribe to or emit any Sig signals. Signals reach registered custom-action handlers via the call site in actions.ts, which forwards a SignalSnapshot argument captured by the effect engine before action dispatch. Custom tick handlers receive dt instead of a snapshot.

Entry points

  • registerCustomAction(name: string, fn: CustomActionFn): void — called once per signal-triggered handler at module-load time from custom-handlers.ts.
  • registerCustomTick(name: string, fn: CustomTickFn): void — called once per per-frame handler at module-load time from custom-handlers.ts.
  • CustomHandlers[name] — read by actCustom in actions.ts during action execution.
  • CustomTickHandlers[name] — read by the engine tick loop in effect-engine.ts once per frame per registered custom_tick action.

The CustomActionFn signature is (snap: SignalSnapshot | null, params: Record<string, number>, game: GameState, ship: ShipState, world: WorldState) => void. The CustomTickFn signature is (dt: number, params: Record<string, number>, game: GameState, ship: ShipState, world: WorldState) => void. Both signatures are owned by types.ts.

Pattern notes

  • Two categories, two maps. The split exists because the call paths differ: custom flows through the action-executor pipeline (snapshot, conditions, cooldown, charges all already applied), while custom_tick is pulled by the engine’s tick loop every frame on a separate _tickHandlers list. Mixing them in one map would lose that dispatch-time distinction.
  • Registration happens at module-import time as a side effect of importing custom-handlers.ts. There is no explicit init() step. The effects barrel index.ts is responsible for ordering imports so the handlers are present before the first effect runs.
  • Duplicate-name throws are intentional and load-bearing. They turn a typo or copy-paste mistake into a hard startup error rather than a silently overridden handler. There is no production case where overwriting a registered handler is correct, so no override API is provided.
  • Handlers cannot be removed. The maps live for the lifetime of the process; per-run lifecycle is managed by the effect engine’s instance list, not by mutating these maps.
  • The handler action param is the lookup key. Action authors pick a string in the effect data file (data/artifacts/<id>.ts, etc.) and actions.ts / effect-engine.ts strip it from the resolved param dict before calling the handler. Handler code therefore never sees its own name in params.
  • Param resolution is the caller’s job. Both call sites walk action.params, run them through resolveNumber against inst.values (the $var table), coerce booleans to 0/1, and pass the resulting Record<string, number> to the handler. Non-numeric params are dropped silently — handlers receive only numeric params.
  • Miss behavior diverges by category: actCustom throws on an unknown name (treats it as a content bug), while the tick loop silently skips. The asymmetry is deliberate — a missing action handler is a defect, but a missing tick handler is treated as a no-op so a partially-loaded build still ticks.
  • This is the only place in the effect system that holds executable code keyed by string. Conditions and built-in actions are dispatched by switch in conditions.ts and actions.ts; only the custom / custom_tick escape hatch routes through a name lookup.