Learn-skills.dev add-hero-trait
Use when adding hero trait mod implementations in hero-trait-mods.ts - guides reading trait descriptions, creating mod factories, adding stackables/config, and wiring up the calculation engine (project)
git clone https://github.com/NeverSight/learn-skills.dev
T=$(mktemp -d) && git clone --depth=1 https://github.com/NeverSight/learn-skills.dev "$T" && mkdir -p ~/.claude/skills && cp -r "$T/data/skills-md/aclinia/torchlight-of-building/add-hero-trait" ~/.claude/skills/neversight-learn-skills-dev-add-hero-trait && rm -rf "$T"
data/skills-md/aclinia/torchlight-of-building/add-hero-trait/SKILL.mdAdding Hero Trait Mods
Overview
Hero traits are implemented as mod factories in
src/tli/hero/hero-trait-mods.ts. Each factory takes a level index (0-4) and returns an array of Mod objects. The trait's description in src/data/hero-trait/hero-traits.ts is the source of truth for what mods and values to produce.
When to Use
- Implementing a hero trait that isn't yet in
heroTraitModFactories - Adding calculation support for a hero trait's mechanics
Step 0: Read the Trait Description
Always start here. Look up the trait in
src/data/hero-trait/hero-traits.ts and read its affix field. This determines:
- What mods to create and their types
- The per-level values (formatted as
for levels 1-5)(v1/v2/v3/v4/v5) - Whether it's a player buff or enemy debuff ("damage taken by the enemy" =
)isEnemyDebuff: true - Whether it stacks and the max stack count
- Any conditions for activation
Project File Locations
| Purpose | File Path |
|---|---|
| Trait descriptions (source of truth) | |
| Trait mod factories | |
| Mod type definitions | |
| Stackable types | () |
| Condition types | () |
| Configuration interface & defaults | |
| Configuration schema | |
| Configuration UI | |
| Calculation engine | |
Implementation Checklist
1. Add Mod Factory (src/tli/hero/hero-trait-mods.ts
)
src/tli/hero/hero-trait-mods.tsAdd an entry to
heroTraitModFactories. The key must match the trait's name field in hero-traits.ts exactly (it's typed as HeroTraitName).
Constant mods (no level scaling):
"Trait Name": () => [ { type: "SomeFlag" }, { type: "DmgPct", value: 20, dmgModType: "cold", addn: true, cond: "some_condition" }, ],
Level-scaled mods:
"Trait Name": (i) => [ { type: "FrostbiteEffPct", value: [65, 90, 110, 130, 150][i] }, ],
Stackable mods (per-stack scaling):
"Trait Name": (i) => [ { type: "DmgPct", value: [8, 10, 12, 15, 18][i], dmgModType: "cold", addn: true, isEnemyDebuff: true, per: { stackable: "dance_of_frost", limit: 4 }, }, ],
Place the factory near other traits for the same hero, using the comment format:
// Frostfire Gemma: Frostbitten Heart (#2)
2. Add New Mod Types (if needed, src/tli/mod.ts
)
src/tli/mod.tsIf the trait needs a mod type not in
ModDefinitions, add it:
interface ModDefinitions { // ... existing types ... NewModType: { value: number }; // or object for flag mods }
3. Add New Stackable (if needed, src/tli/mod.ts
)
src/tli/mod.tsIf the trait has a per-stack mechanic, add a stackable to
Stackables:
export const Stackables = [ // ... existing values ... // hero-specific "stalker", "twisted_spacetime", "dance_of_frost", // Add near other hero-specific stackables // ... ] as const;
4. Add New Condition (if needed, src/tli/mod.ts
)
src/tli/mod.tsIf the trait has a conditional activation, add to
Conditions:
export const Conditions = [ // ... existing values ... "frostbitten_heart_is_active", "new_condition_name", // Add here ] as const;
Then wire it up in
filterModsByCond in src/tli/calcs/offense.ts (the .with() chain).
5. Add Configuration Field (if needed)
If the trait introduces a user-configurable value (stack count, toggle, etc.), use the
/add-configuration skill or follow these steps:
a. Add to
interface (Configuration
):src/tli/core.ts
// hero-specific config section // default to 0 danceOfFrostStacks?: number;
b. Add default to
(DEFAULT_CONFIGURATION
):src/tli/core.ts
danceOfFrostStacks: undefined,
c. Add schema field (
):src/lib/schemas/config.schema.ts
danceOfFrostStacks: z.number().optional().catch(d.danceOfFrostStacks),
d. Add UI control (
):src/components/configuration/ConfigurationTab.tsx
<label className="text-right text-zinc-50"> Dance of Frost Stacks <InfoTooltip text="Frostfire Gemma: Dance of Frost trait stacks" /> </label> <NumberInput value={config.danceOfFrostStacks} onChange={(v) => onUpdate({ danceOfFrostStacks: v })} min={0} />
Place near other hero-specific config fields (after
frostbittenHeartIsActive).
6. Wire Up in Calculation Engine (src/tli/calcs/offense.ts
)
src/tli/calcs/offense.tsFor stackables: Add a
normalize() call in resolveModsForOffenseSkill:
normalize("dance_of_frost", config.danceOfFrostStacks ?? 0);
Place near other hero-specific normalizations (near
pushErika1, pushYouga2, etc.).
For conditions: Add a
.with() case in filterModsByCond:
.with("new_condition_name", () => config.newConditionField)
7. Verify
pnpm typecheck pnpm test pnpm check
Common Trait Patterns
| Pattern | Example Trait | Implementation |
|---|---|---|
| Simple constant buff | Frostbitten Heart | |
| Level-scaled value | Deepfreeze | |
| Per-stack with config | Dance of Frost | + config + normalize |
| Conditional activation | Frostbitten Heart | + config boolean |
| Flag mod (enables mechanic) | Wind Stalker | (object mod) |
| Override limit | Deepfreeze | |
Common Mistakes
| Mistake | Fix |
|---|---|
| Not reading the trait description first | Always check for the field |
| Guessing values instead of reading description | Values come from the format in the affix |
Missing | "damage taken by the enemy" = enemy debuff, not player buff |
Forgetting on per-stackable | "stacks up to N times" needs |
Not adding for new stackables | Per-stackable mods won't resolve without in offense.ts |
| Placing config UI in wrong section | Hero-specific config goes near other hero fields |
| Trait name doesn't match data | Key must exactly match in (typed as ) |