implementing-game-skill-parsers
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)
Installation
Follow the repository instructions for this skill and your agent. Review the files and dependencies before adding it to your workflow.
View source ↗GuidesSkill instructions
Implementing Game Skill Parsers
Overview
Skill data generation follows a parser-factory-generation pattern:
- Parser extracts numeric values from HTML/data sources with named keys
- Factory defines how to build Mod objects using those named values
-
Generation script combines parsed values into
levelValuesoutput
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)
// 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:
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:
dmgPctPerProjectilenot justdmgPct
3. Create Parser (extract values for those keys)
// 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<number, number> = {};
// 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
// In index.ts
{ skillName: "Ice Bond", categories: ["active"], parser: iceBondParser }
5. Regenerate & Verify
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:
export const frostSpikeParser: SupportLevelParser = (input) => {
const weaponAtkDmgPct: Record<number, number> = {};
const addedDmgEffPct: Record<number, number> = {};
// ... 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:
"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:
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:
"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:
"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:
// 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:
// 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
-
Self-documenting:
vals.projectilePerFrostbiteRatingis clearer thanvals[4] - Order-independent: Parser and factory don't need to agree on array order
- Extensible: Adding new values doesn't shift existing indices
- Type-safe: TypeScript can catch typos in key names
Examples
Read the usage examples in the source repository.
View source ↗Source history
- Indexed on
- 2026-01-20
- Index updated
- 2026-01-26
- Branch
- main
Index and sync dates describe this directory. View the source repository for code history.
GitHub ↗chat Comments (0)
Sign in to join the discussion and leave a comment.
No comments yet. Be the first to share your thoughts!