implementing-game-skill-parsers

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

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.
技能数据生成遵循**parser-factory-generation(解析器-工厂-生成器)**模式:
  1. 解析器从HTML/数据源中提取带命名键的数值
  2. 工厂定义如何使用这些命名值构建Mod对象
  3. 生成脚本将解析后的值组合成
    levelValues
    输出
关键注意事项: 解析器的键必须与工厂中使用的键完全匹配。
说明: 本文档仅覆盖主动和被动技能。如需了解支援技能相关内容,请查看
adding-support-mod-parsers
文档。

When to Use

适用场景

  • Adding new active or passive skills with level-scaling properties
  • Extracting values from game data HTML pages
  • 添加带有等级缩放属性的新主动或被动技能
  • 从游戏数据HTML页面中提取数值

Project File Locations

项目文件位置

PurposeFile 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
用途文件路径
主动技能工厂
src/tli/skills/active-factories.ts
被动技能工厂
src/tli/skills/passive-factories.ts
工厂类型与工具函数
src/tli/skills/types.ts
主动技能解析器
src/scripts/skills/active-parsers.ts
被动技能解析器
src/scripts/skills/passive-parsers.ts
解析器注册表
src/scripts/skills/index.ts
生成脚本
src/scripts/generate-skill-data.ts
HTML数据源
.garbage/tlidb/skill/{category}/{Skill_Name}.html
分类:
active
(主动)、
passive
(被动)、
activation_medium
(触发媒介)

Implementation Checklist

实施步骤清单

1. Identify Data Source

1. 确定数据源

  • 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
    )
  • HTML文件路径:
    .garbage/tlidb/skill/{category}/{Skill_Name}.html
  • 找到“Progression /40”表格,列包括:等级、col0、col1、col2(Descript)
  • 列索引规则:
    values[0]
    = 等级列后的第一列,
    values[2]
    = Descript列
  • 输入为纯文本(已通过
    buildProgressionTableInput
    去除HTML标签)

2. Define Factory (structure + key names)

2. 定义工厂(结构 + 键名)

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
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",
    },
  ],
}),
工厂返回类型:
  • 主动技能:
    { offense?: SkillOffense; mods?: Mod[]; buffMods?: Mod[] }
  • 被动技能:
    { mods?: Mod[]; buffMods?: Mod[] }
SkillOffense是结构化接口,而非数组:
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 };
}
v(arr, level)
工具函数可通过边界检查安全访问
arr[level - 1]
键名命名规范:
  • 使用描述性小驼峰命名
  • 包含上下文信息:例如用
    dmgPctPerProjectile
    而非仅
    dmgPct

3. Create Parser (extract values for those keys)

3. 创建解析器(提取对应键的数值)

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<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)
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<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 };
};
数值提取模板语法:
  • {name:int}
    - 整数(例如:"5" → 5)
  • {name:dec}
    - 小数(例如:"21.5" → 21.5)
  • {name:dec%}
    - 百分比(保留数值,例如:"96%" → 96,而非0.96)
  • {name:int%}
    - 百分比整数(例如:"-30%" → -30)
对于全等级相同的数值:使用
createConstantLevels(value)

4. Register Parser

4. 注册解析器

typescript
// In index.ts
{ skillName: "Ice Bond", categories: ["active"], parser: iceBondParser }
typescript
// In index.ts
{ skillName: "Ice Bond", categories: ["active"], parser: iceBondParser }

5. Regenerate & Verify

5. 重新生成并验证

bash
pnpm exec tsx src/scripts/generate_skill_data.ts
pnpm test
Check generated output for levels 1, 20, 40 against source HTML.
bash
pnpm exec tsx src/scripts/generate_skill_data.ts
pnpm test
对照源HTML检查生成结果中1级、20级、40级的数据是否正确。

Example: Complex Skill (Frost Spike)

示例:复杂技能(Frost Spike)

Parser extracts multiple named values:
typescript
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:
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, ...],
}
解析器提取多个命名值:
typescript
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),
  };
};
工厂使用这些键:
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" } },
  ],
}),
生成输出:
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)

示例:多阶段攻击技能(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) },
  ],
}),
对于包含多阶段攻击的技能,使用专用的offense属性:
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)

示例:法术技能(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) }],
}),
法术技能使用
spellDmg
属性,包含伤害范围和施法时间:
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

常见错误

MistakeFix
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 textInput is already
.text().trim()
- no HTML tags
Parser key doesn't match factory keyKeys must match exactly:
vals.dmgPct
needs parser to return
{ dmgPct: ... }
Forgetting parser registrationAdd to SKILL_PARSERS array in
index.ts
Missing factoryMust 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-40Many skills only have data for levels 1-20; fill 21-40 with level 20 values
错误修复方案
为offense使用数组
offense
SkillOffense
对象,而非数组。正确写法:
offense: { weaponAtkDmgPct: { value: ... } }
在DmgPct类型Mod中使用
modType
应使用
dmgModType
替代
modType
对纯文本使用HTML正则表达式输入已通过
.text().trim()
处理,无HTML标签
解析器键与工厂键不匹配键必须完全一致:若工厂使用
vals.dmgPct
,解析器需返回
{ dmgPct: ... }
忘记注册解析器将解析器添加到
index.ts
中的SKILL_PARSERS数组
缺少工厂定义必须在
*-factories.ts
中添加工厂,否则Mod无法在运行时生效
findColumn
子字符串冲突
"damage"会优先匹配"Effectiveness of added damage"——使用精确匹配(见下文)
缺少21-40级数据许多技能仅提供1-20级数据;将21-40级填充为20级的值

findColumn Gotcha: Substring Matching

findColumn注意事项:子字符串匹配

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`);
}
findColumn
使用子字符串匹配。若列标题存在子字符串重叠,可能会匹配到错误列:
typescript
// PROBLEM: "damage"是"Effectiveness of added damage"的子字符串
// 这会返回错误的列!
const damageCol = findColumn(progressionTable, "damage", skillName);

// SOLUTION: 存在冲突时使用精确标题匹配
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

处理21-40级空数据

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;
}
许多技能仅提供1-20级的 progression 数据。将21-40级填充为20级的值:
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
HTML Source → buildProgressionTableInput (去除HTML标签)
           → Parser (提取带命名键的数值)
           → Generation Script (转换为levelValues数组)
           → 输出TypeScript文件
运行时:Factory + levelValues → Mod对象

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
  1. 自文档化:
    vals.projectilePerFrostbiteRating
    vals[4]
    更清晰
  2. 顺序无关: 解析器与工厂无需约定数组顺序
  3. 可扩展性: 添加新值不会影响现有索引
  4. 类型安全: TypeScript可捕获键名拼写错误