Use when adding new push* resolver functions inside resolveModsForOffenseSkill to handle mod-based combat mechanics like tangles, debuffs, or conditional damage bonuses (project)
Install with Codex or Claude Copy this prompt, paste it into Codex, Claude, or another assistant, and let it review the skill page and install it for you.
A direct command skips the review prompt. Inspect the source before running it.
Use when adding new push* resolver functions inside resolveModsForOffenseSkill to handle mod-based combat mechanics like tangles, debuffs, or conditional damage bonuses (project)
Adding Mod Resolvers
Overview
Mod resolvers are push* functions defined inside resolveModsForOffenseSkill in src/tli/calcs/offense.ts. They read from the mods array (and optionally config, prenormMods, resourcePool, defenses) and push new derived mods based on game mechanics. Each resolver handles one mechanic (e.g., frostbite, numbed, tangles, infiltrations).
When to Use
Adding a new combat mechanic that derives mods from existing mods or configuration
Adding conditional damage bonuses based on presence of a flag mod (e.g., IsTangle, WindStalker)
applyModFilters preprocesses all mods into four groups:
mods โ non-per mods without condThreshold or resolvedCond (ready to use immediately)
prenormMods โ all mods without resolvedCond (source for per-stackable normalization)
condThresholdMods โ non-per mods with condThreshold (pushed back by normalize() when threshold is met)
resolvedCondMods โ mods with resolvedCond (pushed back by individual push* resolvers when condition is met)
IMPORTANT: Mods with a per field are filtered into prenormMods only โ they do NOT appear in mods. The per field (from ModBase) triggers automatic per-stackable normalization via normalize(). If a mod needs custom resolver logic (e.g., applying an effect multiplier before scaling by stacks), do NOT use per on the mod type. Instead, store the scaling info in a custom field (e.g., perFervorAmt: number) so the mod stays in mods and the resolver can find it via filterMods().
resolveModsForOffenseSkill then runs a sequence of push* resolver functions that push new derived mods into mods via pm(). Each push* function is a closure that captures:
mods: Mod[] โ the shared mutable array of resolved mods
The execution sequence begins with normalizeFromConfig(), which normalizes all stackables whose values come purely from config fields (e.g., level, num_enemies_nearby, enemy_numbed_stacks). This runs before pushStatNorms() and all other resolvers. Normalize calls whose values depend on mods, stats, defenses, or resourcePool remain in their respective push* functions or inline in the execution sequence.
Available Helpers
From closure (defined in resolveModsForOffenseSkill):
pm(...ms: Mod[]) โ shorthand for mods.push(...ms)
normalize(stackable, value) โ normalizes per-stackable mods from prenormMods and pushes satisfied condThresholdMods for that stackable
normalizeFromConfig() โ calls normalize() for all stackables whose values come purely from config fields; called once at the start of the execution sequence before pushStatNorms()
step(stepName) โ registers a step for dependency tracking (only needed if other steps depend on this one)
resolvedCondMods: Mod[] โ mods with resolvedCond, separated out by applyModFilters; push matching ones into mods via pm() when the condition is met
condThresholdMods: Mod[] โ non-per mods with condThreshold, separated out by applyModFilters; pushed back automatically by normalize() when their stackable threshold is met
From src/tli/calcs/mod-utils.ts:
modExists(mods, "ModType") โ returns boolean, checks if any mod of that type exists
findMod(mods, "ModType") โ returns first mod of type or undefined
filterMods(mods, "ModType") โ returns all mods of type as ModT<T>[]
sumByValue(mods) โ sums .value of all mods in array
Config-only normalization (add to normalizeFromConfig):
If the stackable value comes purely from config fields (no dependency on mods, stats, defenses, or resourcePool), add the normalize() call inside normalizeFromConfig() instead of creating a separate push* function or placing it inline in the execution sequence:
Only use a separate push* function or inline normalize() when the value depends on computed data (mods, stats, etc.), or when the value has a config override with a mod-computed fallback (e.g., config.stacks ?? maxStacksFromMods).
Resolved condition resolver (conditions that depend on calculated values):
Some mod conditions can't be evaluated statically from configuration โ they depend on values calculated earlier in resolveModsForOffenseSkill (e.g., sealed mana/life percentages come from resourcePool.sealedResources, not config). These use resolvedCond on the mod (see ResolvedCondition in mod.ts) instead of cond (which is for static Configuration-based conditions evaluated in filterModsByCond).
Mods with resolvedCond are separated out by applyModFilters into resolvedCondMods. The push* resolver filters for its condition and pushes matching mods into mods via pm() when the condition is met.
Add the condition string to ResolvedConditions in src/tli/mod.ts
In the mod parser template (src/tli/mod-parser/templates.ts), use resolvedCond: "condition_name" instead of cond: "condition_name"
Write a push* resolver that filters resolvedCondMods and pushes matching mods via pm(), and call it at the appropriate point in the execution sequence
Resolver with step dependencies (when one resolver produces mods consumed by another):
Use step() and stepDeps whenever a resolver pushes mods that another resolver later reads. For example, pushFervor generates SkillAreaPct mods, so pushSkillArea depends on it. The dependency graph is validated at test time โ if pushSkillArea runs before pushFervor, an error is recorded.
Register both steps and their dependency in stepDeps (above resolveModsForOffenseSkill):
const stepDeps = createSelfReferential({
// ... existing steps ...fervor: [],
skillArea: ["fervor"], // skillArea must run after fervor
});
Ensure the call order in the execution sequence matches the dependency graph (dependent runs after dependency):
pushFervor(); // must come firstpushSkillArea(); // depends on fervor
step() always goes at the top of the resolver, before any early returns, so the step is registered even if the resolver short-circuits.
4. Call the Function
Add the call in the execution sequence inside resolveModsForOffenseSkill. Place it near related mechanics.
5. Verify
pnpm test
pnpm typecheck
pnpm check
Common Patterns
Pattern
When to Use
Key Helper
Check flag mod exists
Mechanic only applies when a specific support/skill mod is present
modExists(mods, "FlagMod")
Check config boolean
Mechanic depends on user toggle
if (!config.someToggle) return
Effect multiplier
Buff/debuff has mods that scale its effectiveness
calcEffMult(mods, "SomeEffPct")
Config stacks with default
User can override stack count, defaults to max
config.someStacks ?? maxStacks
Normalize stackable
Mechanic involves per-stackable scaling with computed value
normalize("stackable_name", value)
Config-only normalize
Stackable value comes purely from config
Add to normalizeFromConfig()
Filter by resolved condition
Condition depends on calculated values, not static config
pm(...resolvedCondMods.filter(...))
addn: true on DmgPct
More multiplier (multiplicative with other addn: true mods)
โ
addn: false on DmgPct
Increased multiplier (additive with other addn: false mods)
โ
isEnemyDebuff: true
Damage increase from enemy debuff (for display grouping)
โ
src: "Name"
Label for debug/display panel
โ
DmgPct addn Field
The addn (additional) field on DmgPct controls how the damage bonus stacks:
addn: false โ Increased damage. All addn: false mods sum together into one multiplier: (1 + sum).
addn: true โ More damage. Each addn: true mod is its own separate multiplier: (1 + value1) * (1 + value2) * ...
Most resolvers use addn: true because their effects are multiplicative with other damage sources.
Common Mistakes
Mistake
Fix
Forgetting early return when flag/config is absent
Always guard with if (!condition) return
Using addn: false when the mechanic should be multiplicative
Use addn: true for separate "more" multipliers
Pushing mods without src
Always include src for debug panel visibility
Forgetting to add config field
Use /add-configuration skill first
Missing step() when a resolver produces mods consumed by another
Add both steps to stepDeps with the dependency, and call step() at the top of each resolver
Not matching execution order to stepDeps
The call order must satisfy the dependency graph โ dependent resolvers run after their dependencies
Not handling undefined config with ??
Optional config values need fallback: config.stacks ?? defaultMax
Placing config-only normalize inline in execution sequence
Add to normalizeFromConfig() instead; only use inline/push* for computed values
Using per on a mod that needs custom resolver logic
Mods with per go to prenormMods, not mods, so filterMods(mods, ...) won't find them. Use a custom field (e.g., perFervorAmt: number) instead so the mod stays in mods for the resolver to read