$var Value Resolution

Effect action params accept either literal values (numbers, booleans, strings) or $key references that resolve against the owning EffectInstance.values bag at execution time. This is how artifact tier-scaled values, passive level-scaled values, and mod-tier values get plugged into otherwise-static action definitions.

What lives in values

Every EffectInstance carries a values: Record<string, number> dict (see types.ts). The dict is populated when the effect is registered at run start, drawn from:

  • Artifact tier values — each artifact tier ships a named numeric bag (e.g. damage, radius, duration).
  • Passive level values — ship passives scale with level, exposed as named numbers.
  • Mod ability values — ship-mod abilities have tier-scaled numbers.

Action params reference these by name. The action definition itself stays content-independent — only the values dict changes per tier/level.

Resolution rules

Logic lives in engine/effects/resolve-params.ts:

Raw param valueBehavior
String beginning with $ (charCode 36)Slice the $, look up the key in inst.values. Throws if the key is missing.
Number, boolean, or any other stringReturns the raw value unchanged.

Three public resolvers:

  • resolveParam(raw, values) — generic. Returns whatever type the lookup or pass-through produces.
  • resolveNumber(raw, values) — calls resolveParam, then asserts the result is typeof 'number'. Throws otherwise.
  • resolveString(raw, values) — calls resolveParam, then String(...)-coerces. Numbers stringify, booleans become "true"/"false".

There’s also resolveAllParams(params, values) which walks every key in a param dict and applies resolveParam to each, returning a new object. Used by custom actions which forward the whole resolved bag to a registered handler.

Single-level lookup only

$var resolution is one level deep. The lookup reads values[key] and returns the number directly — there is no recursion, no $foo chaining where the result is itself another $bar. Values are always concrete numbers by the time the effect runs.

Where it’s consumed

actions.ts calls resolveNumber / resolveString on every parameter the action reads — stat, value, mode, duration, source, radius, damage, upgradeId, delta, etc. Conditions and triggers also support $var for fields like counterThreshold (see types.ts).

The contract: any field declared as number | string | boolean in a ConditionDef.params or ActionDef.params accepts a $key reference. The author of the action handler decides which resolver (number vs. string) to call, and an enforced type check fires if a $key resolves to the wrong shape.

Failure modes

  • Missing keyEffect param $<key> not found in values dict. Thrown synchronously; the effect doesn’t fire and the whole action chain aborts. Indicates the artifact/passive/mod content didn’t populate the value, or the action definition referenced a typo’d key.
  • Wrong typeresolveNumber throws if a $key resolves to anything but a number. Since the values dict is typed Record<string, number>, this only triggers on direct literal misuse (e.g. passing a non-numeric string where a number is required).