Claude Code Plugins

Community-maintained marketplace

Feedback

implementing-game-skill-parsers

@aclinia/torchlight-of-building
15
0

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)

Install Skill

1Download skill
2Enable skills in Claude

Open claude.ai/settings/capabilities and find the "Skills" section

3Upload to Claude

Click "Upload skill" and select the downloaded ZIP file

Note: Please verify skill by going through its instructions before using it.

SKILL.md

name implementing-game-skill-parsers
description 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)

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)

// 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: dmgPctPerProjectile not just dmgPct

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

  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