adding-mod-parsers

The mod parser converts raw mod strings (e.g., "+10% all stats" ) into typed Mod objects used by the calculation engine. It uses a template-based system for pattern matching.

Safety Notice

This listing is imported from skills.sh public index metadata. Review upstream SKILL.md and repository scripts before running.

Copy this and send it to your AI assistant to learn

Install skill "adding-mod-parsers" with this command: npx skills add aclinia/torchlight-of-building/aclinia-torchlight-of-building-adding-mod-parsers

Adding Mod Parsers

Overview

The mod parser converts raw mod strings (e.g., "+10% all stats" ) into typed Mod objects used by the calculation engine. It uses a template-based system for pattern matching.

When to Use

  • Adding support for new mod string patterns

  • Extending existing mod types to handle new variants

  • Adding new mod types to the engine

Project File Locations

Purpose File Path

Mod type definitions src/tli/mod.ts

Parser templates src/tli/mod-parser/templates.ts

Enum registrations src/tli/mod-parser/enums.ts

Calculation handlers src/tli/calcs/offense.ts

Tests src/tli/mod-parser.test.ts

Implementation Checklist

  1. Check if Mod Type Exists

Look in src/tli/mod.ts under ModDefinitions . If the mod type doesn't exist, add it:

interface ModDefinitions { // ... existing types ... NewModType: { value: number; someField: string }; }

  1. Add Template in templates.ts

Templates use a DSL for pattern matching. Do not add comments to templates.ts - the template string itself is self-documenting.

t("{value:dec%} all stats").output((c) => ({ type: "StatPct", value: c.value, statModType: "all", })), t("{value:dec%} {statModType:StatWord}") .enum("StatWord", StatWordMapping) .output((c) => ({ type: "StatPct", value: c.value, statModType: c.statModType })), t("{value:dec%} [additional] [{modType:DmgModType}] damage").output((c) => ({ type: "DmgPct", value: c.value, dmgModType: c.modType ?? "global", addn: c.additional !== undefined, })), t("{value:dec%} attack and cast speed").outputMany([ spec((c) => ({ type: "AspdPct", value: c.value, addn: false })), spec((c) => ({ type: "CspdPct", value: c.value, addn: false })), ]),

Template capture types:

Type Matches Example Input → Output

{name:int}

Unsigned integer "5" → 5

{name:dec}

Unsigned decimal "21.5" → 21.5

{name:int%}

Unsigned integer percent "30%" → 30

{name:dec%}

Unsigned decimal percent "96%" → 96

{name:+int}

Signed integer (requires + or - ) "+5" → 5 , "-3" → -3

{name:+dec}

Signed decimal (requires + or - ) "+21.5" → 21.5

{name:+int%}

Signed integer percent "+30%" → 30 , "-15%" → -15

{name:+dec%}

Signed decimal percent "+96%" → 96

{name:?int}

Optional-sign integer (matches with or without + /- ) "5" → 5 , "+5" → 5 , "-3" → -3

{name:?dec}

Optional-sign decimal "21.5" → 21.5 , "+21.5" → 21.5

{name:?int%}

Optional-sign integer percent "30%" → 30 , "+30%" → 30

{name:?dec%}

Optional-sign decimal percent "96%" → 96 , "+96%" → 96

{name:EnumType}

Enum lookup {dmgType:DmgChunkType}

Signed vs Unsigned vs Optional-sign Types:

  • Use unsigned (dec% , int ) when input NEVER has + or - (e.g., "8% additional damage applied to Life" )

  • Use signed (+dec% , +int ) when input ALWAYS has + or - (e.g., "+25% additional damage" )

  • Use optional-sign (?dec% , ?int ) when input MAY OR MAY NOT have a sign — this avoids needing two separate templates for signed/unsigned variants

  • Signed types will NOT match unsigned inputs, and unsigned will NOT match signed inputs

  • Prefer ?dec% over two separate dec% /+dec% templates when the same mod can appear with or without a sign

Optional syntax:

  • [additional]

  • Optional literal, sets c.additional?: true

  • [{modType:DmgModType}]

  • Optional capture, sets c.modType?: DmgModType

  • {(effect|damage)}

  • Alternation (regex-style)

  1. Add Enum Mapping (if needed)

If you need custom word → value mapping, add to enums.ts :

export const StatWordMapping: Record<string, string> = { strength: "str", dexterity: "dex", intelligence: "int", };

registerEnum("StatWord", ["strength", "dexterity", "intelligence"]);

  1. Add Handler in offense.ts (if new mod type)

If you added a new mod type, add handling in calculateOffense() or relevant helper:

case "NewModType": { break; }

For existing mod types with new variants (like adding statModType: "all" ), update existing handlers to also filter for the new variant:

const flat = sumByValue( statMods.filter((m) => m.statModType === statType || m.statModType === "all"), );

  1. Add Tests

Add test cases in src/tli/mod_parser.test.ts :

test("parse percentage all stats", () => { const result = parseMod("+10% all stats"); expect(result).toEqual([ { type: "StatPct", statModType: "all", value: 10, }, ]); });

  1. Verify

pnpm test src/tli/mod_parser.test.ts pnpm typecheck pnpm check

Template Ordering

IMPORTANT: More specific patterns must come before generic ones in allParsers array.

// Good: specific before generic t("{value:dec%} all stats").output(...), // Specific t("{value:dec%} {statModType:StatWord}").output(...), // Generic

// Bad: generic would match first and fail on "all stats"

Examples

Simple Value Parser (Signed)

Input: "+10% all stats" (starts with + )

t("{value:+dec%} all stats").output((c) => ({ type: "StatPct", value: c.value, statModType: "all", })),

Simple Value Parser (Unsigned)

Input: "8% additional damage applied to Life" (no sign)

t("{value:dec%} additional damage applied to life").output((c) => ({ type: "DmgPct", value: c.value, dmgModType: "global", addn: true, })),

Parser with Condition (Signed)

Input: "+40% damage if you have Blocked recently"

t("{value:+dec%} damage if you have blocked recently").output((c) => ({ type: "DmgPct", value: c.value, dmgModType: "global", addn: false, cond: "has_blocked_recently", })),

Parser with Per-Stackable (Signed in "deals" position)

Input: "Deals +1% additional damage to an enemy for every 2 points of Frostbite Rating the enemy has"

Note: The + appears AFTER "deals", so use {value:+dec%} :

t("deals {value:+dec%} additional damage to an enemy for every {amt:int} points of frostbite rating the enemy has") .output((c) => ({ type: "DmgPct", value: c.value, dmgModType: "global", addn: true, per: { stackable: "frostbite_rating", amt: c.amt }, })),

Multi-Output Parser (Signed)

Input: "+6% attack and cast speed"

t("{value:+dec%} [additional] attack and cast speed").outputMany([ spec((c) => ({ type: "AspdPct", value: c.value, addn: c.additional !== undefined })), spec((c) => ({ type: "CspdPct", value: c.value, addn: c.additional !== undefined })), ]),

Flat Stat Parser (Signed)

Input: "+166 Max Mana"

t("{value:+dec} max mana").output((c) => ({ type: "MaxMana", value: c.value })),

Optional-Sign Parser

Input: "12.5% Sealed Mana Compensation for Spirit Magus Skills" OR "+12.5% Sealed Mana Compensation for Spirit Magus Skills"

Use ?dec% when the same mod string can appear with or without a + /- sign, avoiding the need for two separate templates:

t("{value:?dec%} sealed mana compensation for spirit magus skills").output( (c) => ({ type: "SealedManaCompPct", value: c.value, addn: false, skillType: "spirit_magus" }), ),

No-Op Parser (Recognized but produces no mods)

Input: "Energy Shield starts to Charge when Blocking"

Use outputNone() when a mod string should be recognized (not flagged as unparsed) but has no effect on calculations:

t("energy shield starts to charge when blocking").outputNone(),

Common Mistakes

Mistake Fix

Using dec% for input with + prefix Use +dec% for inputs like "+25% damage" , or ?dec% if sign is optional

Using +dec% for input without sign Use dec% for inputs like "8% damage applied to life" , or ?dec% if sign is optional

Two templates for signed/unsigned variants of the same mod Use ?dec% to match both in a single template

Template doesn't match input case Templates are matched case-insensitively; input is normalized to lowercase

Missing type field in output mapper Include type: "ModType" in the returned object — contextual typing from the Mod discriminated union handles narrowing

Handler doesn't account for new variant Update offense.ts to handle new values (e.g., statModType === "all" )

Generic template before specific Move specific templates earlier in allParsers array

Data Flow

Raw string: "+10% all stats" ↓ normalize (lowercase, trim) "10% all stats" ↓ template matching (allParsers) { type: "StatPct", value: 10, statModType: "all" } ↓ calculateStats() in offense.ts Applied to str, dex, int calculations

Source Transparency

This detail page is rendered from real SKILL.md content. Trust labels are metadata-based hints, not a safety guarantee.

Related Skills

Related by shared tags or category signals.

Coding

adding-support-mod-parsers

No summary provided by upstream source.

Repository SourceNeeds Review
Coding

implementing-game-skill-parsers

No summary provided by upstream source.

Repository SourceNeeds Review
Coding

add-hero-trait

No summary provided by upstream source.

Repository SourceNeeds Review
Coding

add-mod-resolver

No summary provided by upstream source.

Repository SourceNeeds Review