Modifier Source Tags

Every entry in the central Modifiers stat-stack carries a human-readable source string. The tag identifies the origin of the modifier so the system can find and remove the right entries without disturbing unrelated ones.

Signature

Modifiers.add(target, stat, mode, value, duration, creator, source, stackOpts?)

Defined in engine/core/modifiers.ts. The source parameter is the seventh argument — a free-form string, defaulting to ''. It is stored on the Modifier record alongside target (entity id), stat, mode (flat | percent | set), value, duration, and creator (entity id of the owner).

Why source tags exist

The modifier stack is shared across the player ship. Multiple subsystems (artifacts, level-up picks, passives, run mods) all add entries against the same entity. When one of those subsystems wants to undo its own contribution — for example, an artifact retiering or being removed — it needs a way to delete just its modifiers and leave everything else intact.

Modifiers.removeBySource(targetId, source) walks the stack and removes every modifier on the given target whose source string matches exactly. The match is literal — no prefix matching, no wildcards. The tag itself encodes the scope.

There are three deletion verbs in the system:

  • remove(modId) — by exact modifier id, when a caller kept the id from add.
  • removeByCreator(creatorId) — by owning entity, used for cleanup when an entity dies.
  • removeBySource(targetId, source) — by string tag, the workhorse for “undo my contribution.”

removeBySource is the only one that lets a subsystem clean up without holding state.

Conventions

The format is a colon-delimited namespace: <system>:<id>[:<sub>]. Lowercase, no spaces. The first segment names the subsystem, the second narrows to a specific entity, and the third (optional) names a sub-effect when one entity contributes multiple distinct modifiers.

artifact:<id>:flatbonus

Used by the artifact flat-bonus path in engine/world/artifacts.ts:

Modifiers.add(
  (sh as any).eid ?? 0,
  fb.stat,
  fb.mode,
  value,
  0, 0,
  `artifact:${inst.id}:flatbonus`,
);

When the artifact tiers up, the existing flat-bonus modifier must be replaced with the new tier’s value. The path calls Modifiers.removeBySource(sh.eid, \artifact:${inst.id}:flatbonus`)to clear the old contribution, then re-adds at the new tier. Onlyflatbonus` entries for this specific artifact id get removed — other artifacts and other contributions from this artifact (via the EffectEngine) are untouched.

The :flatbonus sub-segment is what makes this safe to namespace: if the artifact later adds a second stat through a different mechanism (e.g. artifact:<id>:auraregen), each can be retiered independently.

level_up:<modId>:<stat>

Used by _applyModifierPick in engine/world/leveling.ts when the player picks a level-up modifier card:

Modifiers.add(
  ship.eid ?? 0,
  eff.stat,
  eff.mode,
  val,
  0, 0,
  `level_up:${modId}:${eff.stat}`,
);

A single modifier (e.g. health) can fan out to multiple stats (hpMax, armor, hpRegen) — one Modifiers.add call per stat, each tagged with its own :<stat> suffix. Each pick adds an independent entry so a player who picks health five times accumulates five tagged modifiers per stat. The tag does not encode the rank — it identifies which subsystem and which modifier slot the entry belongs to.

Level-up modifiers are permanent for the run (duration: 0) and are never explicitly removed; the tag exists primarily so they can be inspected and so that future cleanup or respec systems can target them.

mod:<modId>

Reserved for general modifier-system entries that are not tied to the level-up pick flow — for example, modifiers applied by run-mods or event scripts that touch a stat directly. Same shape as level-up tags but without the per-stat suffix; one entry per (modId, target) pair.

passive:<id>

Reserved for passive abilities that contribute permanent stat changes. Removed wholesale via removeBySource when the passive is disabled or the entity loses the passive.

Rules of thumb for new sources

  1. Pick a stable prefix. <system>: should match exactly one subsystem (artifact, level_up, mod, passive). Don’t reuse artifact: for something that isn’t an artifact.
  2. Encode the entity id second. <system>:<id> — the id is the artifact id, modifier id, ability id, etc. Stable across runs.
  3. Use the third segment to split sub-effects. Only when one entity contributes more than one logically distinct modifier (different stat, different lifecycle). Otherwise leave it off.
  4. Match exactly when removing. removeBySource does literal string comparison. Build the same tag string for removal as for add.
  5. Tag every Modifiers.add call. Even modifiers that are never explicitly removed benefit from the tag: it shows up in Modifiers.dump(targetId) and makes debugging the stack tractable.