engine/effects/resolve-params

PURPOSE

Tiny $var substitution layer for the unified effect pipeline. Replaces strings of the form "$key" in action and condition param dicts with numeric values pulled from an EffectInstance’s values dict (artifact tier values, passive level values, runtime-bound stat values), and provides typed wrappers that coerce the resolved result to number or string for call sites that need a hard cast.

OWNS

  • The four exported pure functions: resolveParam, resolveAllParams, resolveNumber, resolveString.
  • The $-prefix recognition rule itself — a string whose first code unit is 36 ('$') is treated as a value-dict lookup; every other input passes through.
  • The “missing key throws” contract — when a $key lookup hits undefined in the values dict, the resolver raises Error("Effect param $<key> not found in values dict") rather than returning a default.
  • The “expected number” contract on resolveNumber — when the resolved value is not a typeof === 'number', it raises Error("Expected number for param, got <type>: <value>").
  • The String(v) coercion used by resolveString for non-string resolved values.

READS FROM

  • A caller-supplied values: Record<string, number> dict — for every EffectInstance in the runtime that is the instance’s own values field, populated when the def is registered with tier / level values bound in.
  • The raw param value itself — number, string, or boolean — passed through from action.params[k] or cond.params[k] (or any literal default the caller supplied via ??).

Has no imports. Touches no module-level state and no engine module.

PUSHES TO

  • Nothing. The functions are pure: they return a fresh value (or a fresh object, for resolveAllParams) and never mutate the input params or values dicts.
  • Errors thrown propagate to the caller — typically executeAllActions or evaluateAllConditions — which surfaces them up through EffectEngine._tryExecute and out to the per-frame tick.

DOES NOT

  • Does not own the values dict — that lives on EffectInstance and is populated by whichever loader registers the effect.
  • Does not own param schema or validation — it does not know which actions or conditions accept which keys; callers pass action.params[k] ?? <default> and ask for whichever type they need.
  • Does not memoize or cache — every call re-walks the input dict.
  • Does not handle nested templates, arithmetic, or multi-$var interpolation. The entire $var syntax is “string starts with $, the rest is the key” — "$a + $b" is not a substitution, it is passed through as a literal string.
  • Does not handle escaping a literal leading $. Any string whose first character is $ is treated as a lookup.
  • Does not return undefined on a missing key — it throws. There is no silent fallback.
  • Does not coerce booleans to numbers itself. resolveNumber will throw on a boolean input that was passed through unchanged. Callers that need the boolean-to-0/1 coercion do it before calling (typeof v === 'boolean' ? (v ? 1 : 0) : v) — effect-engine.ts and actions.ts both follow that pattern at their custom_tick and registry-action dispatch sites.
  • Does not call console.log or emit telemetry.

Signals

Subscribes to no signals. Emits no signals. Has no awareness of Sig.

Entry points

  • resolveParam(raw, values) — single-value resolver. Returns the number from values[raw.slice(1)] when raw is a $-prefixed string, throws when the key is missing, otherwise returns raw unchanged. Used internally by the other three exports and not commonly called directly by action / condition handlers.
  • resolveAllParams(params, values) — bulk resolver. Builds a fresh object, walks every own key of params with a for…in loop, and stores resolveParam(params[k], values) at the same key. Used by effect-engine.ts for the telemetry log payload (so the [EFFECT] line shows post-substitution numbers) and re-exported through actions.ts.
  • resolveNumber(raw, values) — number-typed resolver. Calls resolveParam then asserts typeof === 'number'. The dominant entry point in the codebase: condition handlers (conditions.tschance, value, minLevel, threshold, seconds, tier) and action handlers (actions.tsdamage, radius, duration, value, count, speed, lifetime, spread, force, damageMult, etc.) call it on every numeric param.
  • resolveString(raw, values) — string-typed resolver. Calls resolveParam then String(v). Used for tag, signal-name, status-type, stat-id, source-owner, and color params (actions.tsstat, source, signal, status, color, upgradeId; conditions.tsvalue, tag).

Pattern notes

  • The $ check is raw.charCodeAt(0) === 36 rather than raw.startsWith('$') or raw[0] === '$'. Same semantics, slightly cheaper — this code is on the per-frame tick path for every action param of every effect that fires.
  • $var substitution is the only way to thread per-tier / per-level scaling values into effect params. The author of an EffectDef writes params: { damage: "$dmg", radius: "$r" } once, and the loader that registers the effect at tier 2 binds values: { dmg: 75, r: 240 } onto the instance. The same def can be registered at multiple tiers without duplication.
  • Resolution happens on every fire, not once at registration. Callers always pass inst.values from the live EffectInstance. This keeps the resolver pure and lets callers update values between fires if needed.
  • The four exports are re-surfaced through engine/effects/index.ts so consumers can import them as import { resolveNumber } from '../engine/effects' without reaching into the file directly.
  • All three typed wrappers go through resolveParam rather than re-implementing the $ check, so the “missing key throws” contract is enforced in exactly one place.
  • The ?? <default> idiom at call sites — resolveNumber(cond.params.chance ?? 1, inst.values) — supplies the default before the resolver runs. A $var that resolves to a defined number is used as-is; a missing params key falls back to the literal default and passes through unchanged. A $var whose key is missing from values is the error case and throws.
  • resolveAllParams is currently used only by the telemetry-log path in effect-engine.ts. Action and condition handlers prefer the typed resolveNumber / resolveString wrappers because they know the expected type per param.