Agent skill
add-mod-resolver
Use when adding new push* resolver functions inside resolveModsForOffenseSkill to handle mod-based combat mechanics like tangles, debuffs, or conditional damage bonuses (project)
Install this agent skill to your Project
npx add-skill https://github.com/majiayu000/claude-skill-registry/tree/main/skills/other/other/add-mod-resolver
SKILL.md
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) - Adding enemy debuff calculations (e.g., numbed stacks, frostbite, frail)
- Adding buff/aura effect calculations with effect multipliers
Project File Locations
| Purpose | File Path |
|---|---|
| Resolver function location | src/tli/calcs/offense.ts (inside resolveModsForOffenseSkill) |
| Mod type definitions | src/tli/mod.ts (ModDefinitions interface) |
| Configuration interface & defaults | src/tli/core.ts (Configuration, DEFAULT_CONFIGURATION) |
| Mod helper utilities | src/tli/calcs/mod-utils.ts |
| Tests | src/tli/calcs/offense.test.ts |
Architecture
applyModFilters preprocesses all mods into four groups:
mods— non-per mods withoutcondThresholdorresolvedCond(ready to use immediately)prenormMods— all mods withoutresolvedCond(source for per-stackable normalization)condThresholdMods— non-per mods withcondThreshold(pushed back bynormalize()when threshold is met)resolvedCondMods— mods withresolvedCond(pushed back by individualpush*resolvers when condition is met)
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 modsprenormMods— mods needing per-stackable normalizationcondThresholdMods— mods held back until their stackable threshold is evaluated bynormalize()resolvedCondMods— mods held back until their resolved condition is evaluated by apush*resolverconfig: Configuration— user-configured combat parametersresourcePool— stats, mana, blessings, etc.defenses— armor, evasion, resistances, etc.loadout: Loadout— full parsed loadout- Helper functions:
pm()(push mods),normalize()(normalize stackables),step()(dependency tracking)
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 formods.push(...ms)normalize(stackable, value)— normalizes per-stackable mods fromprenormModsand pushes satisfiedcondThresholdModsfor that stackablenormalizeFromConfig()— callsnormalize()for all stackables whose values come purely fromconfigfields; called once at the start of the execution sequence beforepushStatNorms()step(stepName)— registers a step for dependency tracking (only needed if other steps depend on this one)resolvedCondMods: Mod[]— mods withresolvedCond, separated out byapplyModFilters; push matching ones intomodsviapm()when the condition is metcondThresholdMods: Mod[]— non-per mods withcondThreshold, separated out byapplyModFilters; pushed back automatically bynormalize()when their stackable threshold is met
From src/tli/calcs/mod-utils.ts:
modExists(mods, "ModType")— returnsboolean, checks if any mod of that type existsfindMod(mods, "ModType")— returns first mod of type orundefinedfilterMods(mods, "ModType")— returns all mods of type asModT<T>[]sumByValue(mods)— sums.valueof all mods in arraycalcEffMult(mods, "ModType")— calculates(1 + sum_of_inc) * product_of_addnmultipliermultModValue(mod, multiplier)— returns new mod with.valuemultiplied
Implementation Checklist
1. Ensure the Trigger Mod Type Exists
Check src/tli/mod.ts under ModDefinitions. If the flag mod (e.g., IsTangle) doesn't exist, add it:
// In ModDefinitions
IsTangle: object; // flag mod, no fields
For mods with data:
NumbedEffPct: { value: number };
2. Ensure Configuration Fields Exist (if needed)
If the resolver needs user-configurable values, ensure they exist in src/tli/core.ts. Use the /add-configuration skill if they don't.
3. Write the push* Function
Define the function inside resolveModsForOffenseSkill, near related resolvers. Follow these patterns:
Simple flag-based resolver (multiplies damage by a config value):
const pushTangle = (): void => {
if (!modExists(mods, "IsTangle") || config.numActiveTangles <= 1) return;
mods.push({
type: "DmgPct",
dmgModType: "global",
addn: true,
value: (config.numActiveTangles - 1) * 100,
src: "Tangle",
});
};
Debuff with effect multiplier:
const pushNumbed = (): void => {
if (!config.enemyNumbed) return;
const numbedStacks = config.enemyNumbedStacks ?? 10;
const numbedEffMult = calcEffMult(mods, "NumbedEffPct");
const baseValPerStack = 5;
const numbedVal = baseValPerStack * numbedEffMult * numbedStacks;
mods.push({
type: "DmgPct",
value: numbedVal,
dmgModType: "lightning",
addn: true,
isEnemyDebuff: true,
src: "Numbed",
});
};
Buff with effect multiplier (e.g., aggression, mark):
const pushMark = (): void => {
if (!config.targetEnemyMarked) return;
const markEffMult = calcEffMult(mods, "MarkEffPct");
const baseValue = 20;
mods.push({
type: "CritDmgPct",
value: baseValue * markEffMult,
addn: true,
modType: "global",
isEnemyDebuff: true,
src: "Mark",
});
};
Resolver with per-stackable normalization:
const pushPactspirits = () => {
const addedMaxStacks = sumByValue(filterMods(mods, "MaxPureHeartStacks"));
const maxStacks = 5 + addedMaxStacks;
const stacks = config.pureHeartStacks ?? maxStacks;
normalize("pure_heart", stacks);
};
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:
const normalizeFromConfig = (): void => {
// ... existing config-based normalizes ...
normalize("new_stackable", config.newStackableValue ?? defaultValue);
};
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.
const pushHasSealedLifeAndManaCond = (): void => {
const { sealedManaPct, sealedLifePct } = resourcePool.sealedResources;
if (sealedManaPct <= 0 || sealedLifePct <= 0) return;
pm(
...resolvedCondMods.filter(
(m) => m.resolvedCond === "have_both_sealed_mana_and_life",
),
);
};
To add a new resolved condition:
- Add the condition string to
ResolvedConditionValuesinsrc/tli/mod.ts - In the mod parser template (
src/tli/mod-parser/templates.ts), useresolvedCond: RC.condition_name(imported asResolvedConditions as RC) instead ofcond: C.condition_name - Write a
push*resolver that filtersresolvedCondModsand pushes matching mods viapm(), and call it at the appropriate point in the execution sequence
Resolver with step dependencies (rare, only when other resolvers depend on this one):
const pushSpellAggression = (): void => {
step("spellAggression");
if (!config.hasSpellAggression && !modExists(mods, "HasSpellAggression")) return;
const mult = calcEffMult(mods, "SpellAggressionEffPct");
mods.push({
type: "CspdPct",
value: 7 * mult,
addn: true,
src: "Spell Aggression",
});
};
If using step(), also register the step in stepDeps (above resolveModsForOffenseSkill):
const stepDeps = createSelfReferential({
// ... existing steps ...
newStep: ["dependency1"], // list steps that must run before this one
});
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. Alladdn: falsemods sum together into one multiplier:(1 + sum).addn: true— More damage. Eachaddn: truemod 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 |
Adding step() unnecessarily |
Only use step() if other resolvers depend on this one running first |
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 |
Recommended Agent Skills
Expand your agent's capabilities with these related and highly-rated skills.
agent-ops-spec
Manage specification documents in .agent/specs/. Use when user provides requirements, acceptance criteria, or feature descriptions that need to be tracked and validated against implementation.
agent-ops-state
Maintain .agent state files. Use at session start, after meaningful steps, and before concluding: read/update constitution/memory/focus/issues/baseline consistently.
agent-ops-spec
Manage specification documents in .agent/specs/. Use when user provides requirements, acceptance criteria, or feature descriptions that need to be tracked and validated against implementation.
agent-ops-testing
Test strategy, execution, and coverage analysis. Use when designing tests, running test suites, or analyzing test results beyond baseline checks.
agent-ops-testing
Test strategy, execution, and coverage analysis. Use when designing tests, running test suites, or analyzing test results beyond baseline checks.
agent-ops-state
Maintain .agent state files. Use at session start, after meaningful steps, and before concluding: read/update constitution/memory/focus/issues/baseline consistently.
Didn't find tool you were looking for?