$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 value | Behavior |
|---|---|
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 string | Returns the raw value unchanged. |
Three public resolvers:
resolveParam(raw, values)— generic. Returns whatever type the lookup or pass-through produces.resolveNumber(raw, values)— callsresolveParam, then asserts the result istypeof 'number'. Throws otherwise.resolveString(raw, values)— callsresolveParam, thenString(...)-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 key —
Effect 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 type —
resolveNumberthrows if a$keyresolves to anything but a number. Since the values dict is typedRecord<string, number>, this only triggers on direct literal misuse (e.g. passing a non-numeric string where a number is required).
Related
- Action catalog — which params each action reads.
- Condition catalog —
$varworks in condition params too. - Effect engine — how
EffectInstance.valuesgets populated. - Artifact tier mechanics — primary producer of tier-scaled values.