🎯

implementing-game-skill-parsers

🎯Skill

from aclinia/torchlight-of-building

VibeIndex|
What it does

implementing-game-skill-parsers skill from aclinia/torchlight-of-building

implementing-game-skill-parsers

Installation

pnpmRun with pnpm
pnpm install
pnpmRun with pnpm
pnpm dev
πŸ“– Extracted from docs: aclinia/torchlight-of-building
1
-
Last UpdatedJan 24, 2026

Skill Details

SKILL.md

Use when implementing skill data generation from HTML sources for game build planners - guides the parser-factory-generation pattern for extracting level-scaling values for active and passive skills (project)

Overview

# Implementing Game Skill Parsers

Overview

Skill data generation follows a parser-factory-generation pattern:

  1. Parser extracts numeric values from HTML/data sources with named keys
  2. Factory defines how to build Mod objects using those named values
  3. Generation script combines parsed values into levelValues output

Critical: Parser keys MUST match factory key usage exactly.

Note: This skill covers active and passive skills only. For support skills, see the adding-support-mod-parsers skill.

When to Use

  • Adding new active or passive skills with level-scaling properties
  • Extracting values from game data HTML pages

Project File Locations

| Purpose | File Path |

|---------|-----------|

| Active factories | src/tli/skills/active-factories.ts |

| Passive factories | src/tli/skills/passive-factories.ts |

| Factory types & helpers | src/tli/skills/types.ts |

| Active parsers | src/scripts/skills/active-parsers.ts |

| Passive parsers | src/scripts/skills/passive-parsers.ts |

| Parser registry | src/scripts/skills/index.ts |

| Generation script | src/scripts/generate-skill-data.ts |

| HTML data sources | .garbage/tlidb/skill/{category}/{Skill_Name}.html |

Categories: active, passive, activation_medium

Implementation Checklist

1. Identify Data Source

  • HTML file at .garbage/tlidb/skill/{category}/{Skill_Name}.html
  • Find Progression /40 table - columns are: level, col0, col1, col2 (Descript)
  • Column indexing: values[0] = first column after level, values[2] = Descript
  • Input is clean text (HTML already stripped by buildProgressionTableInput)

2. Define Factory (structure + key names)

```typescript

// In active-factories.ts or passive-factories.ts

import { v } from "./types";

"Ice Bond": (l, vals) => ({

buffMods: [

{

type: "DmgPct",

value: v(vals.coldDmgPctVsFrostbitten, l), // Define key name here

addn: true,

dmgModType: "cold",

cond: "enemy_frostbitten",

},

],

}),

```

Factory return types:

  • Active skills: { offense?: SkillOffense; mods?: Mod[]; buffMods?: Mod[] }
  • Passive skills: { mods?: Mod[]; buffMods?: Mod[] }

SkillOffense is a structured interface, NOT an array:

```typescript

interface SkillOffense {

weaponAtkDmgPct?: { value: number };

addedDmgEffPct?: { value: number };

persistentDmg?: { value: number; dmgType: DmgChunkType; duration: number };

spellDmg?: { value: DmgRange; dmgType: DmgChunkType; castTime: number };

// Multi-phase attack skills (e.g., Berserking Blade)

sweepWeaponAtkDmgPct?: { value: number };

sweepAddedDmgEffPct?: { value: number };

steepWeaponAtkDmgPct?: { value: number };

steepAddedDmgEffPct?: { value: number };

}

```

The v(arr, level) helper safely accesses arr[level - 1] with bounds checking.

Key naming conventions:

  • Use descriptive camelCase names
  • Include context: dmgPctPerProjectile not just dmgPct

3. Create Parser (extract values for those keys)

```typescript

// In active-parsers.ts or passive-parsers.ts

import { findColumn, validateAllLevels } from "./progression-table";

import { template } from "./template-compiler";

import type { SupportLevelParser } from "./types";

import { createConstantLevels } from "./utils";

export const iceBondParser: SupportLevelParser = (input) => {

const { skillName, progressionTable } = input;

// Find column by header (uses substring matching)

const descriptCol = findColumn(progressionTable, "descript", skillName);

const coldDmgPctVsFrostbitten: Record = {};

// Iterate over column rows (level β†’ text)

for (const [levelStr, text] of Object.entries(descriptCol.rows)) {

const level = Number(levelStr);

// Use template() for pattern matching - cleaner than regex

const match = template("{value:dec%} additional cold damage").match(

text,

skillName,

);

coldDmgPctVsFrostbitten[level] = match.value;

}

validateAllLevels(coldDmgPctVsFrostbitten, skillName);

// Return named keys matching factory expectations

return { coldDmgPctVsFrostbitten };

};

```

Template syntax for value extraction:

  • {name:int} - Integer (e.g., "5" β†’ 5)
  • {name:dec} - Decimal (e.g., "21.5" β†’ 21.5)
  • {name:dec%} - Percentage as decimal (e.g., "96%" β†’ 96, NOT 0.96)
  • {name:int%} - Percentage as integer (e.g., "-30%" β†’ -30)

For constant values (same across all levels): use createConstantLevels(value)

4. Register Parser

```typescript

// In index.ts

{ skillName: "Ice Bond", categories: ["active"], parser: iceBondParser }

```

5. Regenerate & Verify

```bash

pnpm exec tsx src/scripts/generate_skill_data.ts

pnpm test

```

Check generated output for levels 1, 20, 40 against source HTML.

Example: Complex Skill (Frost Spike)

Parser extracts multiple named values:

```typescript

export const frostSpikeParser: SupportLevelParser = (input) => {

const weaponAtkDmgPct: Record = {};

const addedDmgEffPct: Record = {};

// ... extract from columns ...

return {

weaponAtkDmgPct,

addedDmgEffPct,

convertPhysicalToColdPct: createConstantLevels(convertValue),

maxProjectile: createConstantLevels(maxProjValue),

projectilePerFrostbiteRating: createConstantLevels(projPerRating),

baseProjectile: createConstantLevels(baseProj),

dmgPctPerProjectile: createConstantLevels(dmgPerProj),

};

};

```

Factory uses those keys:

```typescript

"Frost Spike": (l, vals) => ({

offense: {

weaponAtkDmgPct: { value: v(vals.weaponAtkDmgPct, l) },

addedDmgEffPct: { value: v(vals.addedDmgEffPct, l) },

},

mods: [

{ type: "ConvertDmgPct", value: v(vals.convertPhysicalToColdPct, l), from: "physical", to: "cold" },

{ type: "MaxProjectile", value: v(vals.maxProjectile, l), override: true },

{ type: "Projectile", value: v(vals.projectilePerFrostbiteRating, l), per: { stackable: "frostbite_rating", amt: 35 } },

{ type: "BaseProjectileQuant", value: v(vals.baseProjectile, l) },

{ type: "DmgPct", value: v(vals.dmgPctPerProjectile, l), dmgModType: "global", addn: true, per: { stackable: "projectile" } },

],

}),

```

Generated output:

```typescript

levelValues: {

weaponAtkDmgPct: [1.49, 1.51, 1.54, ...],

addedDmgEffPct: [1.49, 1.51, 1.54, ...],

convertPhysicalToColdPct: [1, 1, 1, ...],

maxProjectile: [5, 5, 5, ...],

projectilePerFrostbiteRating: [1, 1, 1, ...],

baseProjectile: [2, 2, 2, ...],

dmgPctPerProjectile: [0.08, 0.08, 0.08, ...],

}

```

Example: Multi-Phase Attack Skill (Berserking Blade)

For skills with multiple attack phases, use the dedicated offense properties:

```typescript

"Berserking Blade": (l, vals) => ({

offense: {

// Sweep phase stats

sweepWeaponAtkDmgPct: { value: v(vals.sweepWeaponAtkDmgPct, l) },

sweepAddedDmgEffPct: { value: v(vals.sweepAddedDmgEffPct, l) },

// Steep strike phase stats

steepWeaponAtkDmgPct: { value: v(vals.steepWeaponAtkDmgPct, l) },

steepAddedDmgEffPct: { value: v(vals.steepAddedDmgEffPct, l) },

},

mods: [

{

type: "SkillAreaPct",

skillAreaModType: "global" as const,

value: v(vals.skillAreaBuffPct, l),

per: { stackable: "berserking_blade_buff" },

},

{ type: "MaxBerserkingBladeStacks", value: v(vals.maxBerserkingBladeStacks, l) },

{ type: "SteepStrikeChancePct", value: v(vals.steepStrikeChancePct, l) },

],

}),

```

Example: Spell Skill (Chain Lightning)

Spell skills use spellDmg with damage range and cast time:

```typescript

"Chain Lightning": (l, vals) => ({

offense: {

addedDmgEffPct: { value: v(vals.addedDmgEffPct, l) },

spellDmg: {

value: { min: v(vals.spellDmgMin, l), max: v(vals.spellDmgMax, l) },

dmgType: "lightning",

castTime: v(vals.castTime, l),

},

},

mods: [{ type: "Jump", value: v(vals.jump, l) }],

}),

```

Common Mistakes

| Mistake | Fix |

|---------|-----|

| Using array for offense | offense is a SkillOffense object, NOT an array. Use offense: { weaponAtkDmgPct: { value: ... } } |

| Using modType in DmgPct mods | Use dmgModType instead of modType |

| Using HTML regex on clean text | Input is already .text().trim() - no HTML tags |

| Parser key doesn't match factory key | Keys must match exactly: vals.dmgPct needs parser to return { dmgPct: ... } |

| Forgetting parser registration | Add to SKILL_PARSERS array in index.ts |

| Missing factory | Must add factory in *-factories.ts for mods to be applied at runtime |

| findColumn substring collision | "damage" matches "Effectiveness of added damage" first - use exact matching (see below) |

| Missing levels 21-40 | Many skills only have data for levels 1-20; fill 21-40 with level 20 values |

findColumn Gotcha: Substring Matching

findColumn uses template substring matching. If column headers share substrings, you may get the wrong column:

```typescript

// PROBLEM: "damage" is a substring of "Effectiveness of added damage"

// This returns the WRONG column!

const damageCol = findColumn(progressionTable, "damage", skillName);

// SOLUTION: Use exact header matching when there's a collision

const damageCol = progressionTable.find(

(col) => col.header.toLowerCase() === "damage",

);

if (!damageCol) {

throw new Error(${skillName}: no "damage" column found);

}

```

Handling Levels 21-40 with Empty Data

Many skills only have progression data for levels 1-20. Fill levels 21-40 with level 20 values:

```typescript

// Extract levels 1-20

for (const [levelStr, text] of Object.entries(someCol.rows)) {

const level = Number(levelStr);

if (level <= 20 && text !== "") {

values[level] = parseValue(text);

}

}

// Fill levels 21-40 with level 20 value

const level20Value = values[20];

if (level20Value === undefined) {

throw new Error(${skillName}: level 20 value missing);

}

for (let level = 21; level <= 40; level++) {

values[level] = level20Value;

}

```

Data Flow

```

HTML Source β†’ buildProgressionTableInput (strips HTML)

β†’ Parser (extracts values with named keys)

β†’ Generation Script (converts to levelValues arrays)

β†’ Output TypeScript file

↓

Runtime: Factory + levelValues β†’ Mod objects

```

Benefits of Named Keys

  1. Self-documenting: vals.projectilePerFrostbiteRating is clearer than vals[4]
  2. Order-independent: Parser and factory don't need to agree on array order
  3. Extensible: Adding new values doesn't shift existing indices
  4. Type-safe: TypeScript can catch typos in key names