engine/effects/conditions

PURPOSE

Pure read-only predicates that gate effect actions. Each condition function inspects the signal snapshot, game state, ship state, or effect instance values and returns a boolean. The dispatcher evaluates a condition by type via a string-keyed registry; the batch evaluator short-circuits on the first failing condition so callers get all-or-nothing semantics for an effect’s condition list.

OWNS

  • evaluateCondition — dispatches a single ConditionDef to its handler by cond.type string, throws on unknown types.
  • evaluateAllConditions — iterates an EffectInstance.def.conditions array and returns true only if every condition passes; bails on first false.
  • ConditionFn — internal function shape shared by all condition implementations.
  • CONDITION_MAP — string-to-function registry that is the source of truth for which condition types are supported.
  • Condition implementations grouped into three buckets:
    • Signal context checks: condRandom, condSignalStr1Eq, condSignalStr1Neq, condSignalNum1Gte, condDamageTagEq.
    • Ship state checks: condHpBelow, condHpAbove, condShieldActive, condShieldBroken, condShieldEmpty, condHasArtifact, condHasUpgrade, condHasBuff.
    • Game state checks: condKillStreakAbove, condElapsedAbove, condElapsedBelow, condTierAtLeast, condHeatAbove, condSpeedAbove, condSpeedBelow, condBossActive, condBossPhaseGte.

READS FROM

  • ./typesConditionDef, EffectInstance, SignalSnapshot type imports.
  • ../core/typesGameState, ShipState type imports.
  • ./resolve-paramsresolveNumber and resolveString for resolving condition parameter values against inst.values.
  • ../core/modifiersModifiers._modsByTarget map read lazily via require inside condHasBuff to avoid a circular dependency.
  • SignalSnapshot fields read: snap.str1, snap.num1.
  • ShipState fields read: ship.hp, ship.shield, ship.vx, ship.vy, plus untyped casts to hpMax, _base.hpMax, heat, and eid.
  • GameState fields read: game.artifacts, game.upgradeCounts, game.killStreak, game.time, game.bossArena, plus an untyped cast to _currentLevel.
  • EffectInstance.values — used by every parameter resolution call.
  • EffectInstance.def.conditions — iterated by evaluateAllConditions.

PUSHES TO

  • Returns boolean to the caller; no writes, no state mutation, no event emission.
  • Throws Error("Unknown condition type: ...") from evaluateCondition when the type string is missing from CONDITION_MAP.

DOES NOT

  • Does not modify GameState, ShipState, EffectInstance, or the signal snapshot.
  • Does not register effects, schedule actions, or evaluate triggers — only checks gating predicates.
  • Does not execute the actions of an effect; that responsibility lies in the actions module.
  • Does not maintain its own cooldown timers — cooldown semantics for triggers live elsewhere; conditions here are stateless reads.
  • Does not implement condBossPhaseGte — the handler is registered but currently returns false unconditionally.
  • Does not validate condition definitions at load time; unknown types only surface at evaluation.

Signals

  • SignalSnapshot.str1 is read by signal_str1_eq, signal_str1_neq, and damage_tag_eq (the last treats the tag param as the expected string).
  • SignalSnapshot.num1 is read by signal_num1_gte and compared against the resolved value threshold.
  • When snap is null, signal_str1_eq, signal_num1_gte, and damage_tag_eq return false; signal_str1_neq returns true (no snapshot means the value cannot equal the expected string).
  • condRandom ignores the snapshot entirely and rolls Math.random() against the resolved chance parameter (default 1).
  • Ship and game state conditions ignore the snapshot.

Entry points

  • evaluateCondition(cond, inst, snap, game, ship) — single-condition evaluation entry; used internally by the all-conditions helper and exported for direct use.
  • evaluateAllConditions(inst, snap, game, ship) — batch evaluator over inst.def.conditions; the standard entry point for effect-trigger gating.
  • CONDITION_MAP is module-private; new condition types must be added by extending this object.

Pattern notes

  • Evaluation order in evaluateAllConditions is the array order on the effect definition; the file comment instructs authors to put cheapest checks first because the loop short-circuits on first failure.
  • All handlers are pure reads — conditions never mutate state, matching the file-level invariant.
  • Parameter values are resolved through resolveNumber and resolveString so that condition params can reference variables in EffectInstance.values; literal string params (artifactId, upgradeId) are coerced via String(...) without resolution.
  • HP-ratio conditions (hp_below, hp_above) fall back through ship.hpMax, then ship._base.hpMax, then 1 to avoid division by zero on partially-initialized ships.
  • shield_broken and shield_empty are aliases — both return ship.shield <= 0.
  • condHasBuff uses a runtime require('../core/modifiers') instead of a top-level import to break a circular dependency between effects and modifiers; it reads the private Modifiers._modsByTarget map keyed by entity id and matches any modifier whose source equals the param.
  • condHasArtifact walks game.artifacts as an any[] and matches by id; condHasUpgrade checks game.upgradeCounts[upgradeId] >= minLevel (default minLevel 1).
  • condSpeedAbove and condSpeedBelow compute speed as sqrt(vx^2 + vy^2) each call rather than relying on a cached value.
  • condElapsedBelow defaults its seconds param to 999999 so an unset threshold effectively passes; condElapsedAbove defaults to 0.
  • The dispatcher throws on unknown condition types rather than failing silently, surfacing typos in effect definitions immediately at evaluation time.
  • condBossPhaseGte is a stub: registered in CONDITION_MAP as boss_phase_gte but its body returns false, so any effect gated on it never fires.