implementing-game-skill-parsers
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseImplementing 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 output
levelValues
Critical: Parser keys MUST match factory key usage exactly.
Note: This skill covers active and passive skills only. For support skills, see the skill.
adding-support-mod-parsers技能数据生成遵循**parser-factory-generation(解析器-工厂-生成器)**模式:
- 解析器从HTML/数据源中提取带命名键的数值
- 工厂定义如何使用这些命名值构建Mod对象
- 生成脚本将解析后的值组合成输出
levelValues
关键注意事项: 解析器的键必须与工厂中使用的键完全匹配。
说明: 本文档仅覆盖主动和被动技能。如需了解支援技能相关内容,请查看文档。
adding-support-mod-parsersWhen to Use
适用场景
- Adding new active or passive skills with level-scaling properties
- Extracting values from game data HTML pages
- 添加带有等级缩放属性的新主动或被动技能
- 从游戏数据HTML页面中提取数值
Project File Locations
项目文件位置
| Purpose | File Path |
|---|---|
| Active factories | |
| Passive factories | |
| Factory types & helpers | |
| Active parsers | |
| Passive parsers | |
| Parser registry | |
| Generation script | |
| HTML data sources | |
Categories: , ,
activepassiveactivation_medium| 用途 | 文件路径 |
|---|---|
| 主动技能工厂 | |
| 被动技能工厂 | |
| 工厂类型与工具函数 | |
| 主动技能解析器 | |
| 被动技能解析器 | |
| 解析器注册表 | |
| 生成脚本 | |
| HTML数据源 | |
分类: (主动)、(被动)、(触发媒介)
activepassiveactivation_mediumImplementation 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: = first column after level,
values[0]= Descriptvalues[2] - 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]= Descript列values[2] - 输入为纯文本(已通过去除HTML标签)
buildProgressionTableInput
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 helper safely accesses with bounds checking.
v(arr, level)arr[level - 1]Key naming conventions:
- Use descriptive camelCase names
- Include context: not just
dmgPctPerProjectiledmgPct
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]键名命名规范:
- 使用描述性小驼峰命名
- 包含上下文信息:例如用而非仅
dmgPctPerProjectiledmgPct
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:
- - Integer (e.g., "5" → 5)
{name:int} - - Decimal (e.g., "21.5" → 21.5)
{name:dec} - - Percentage as decimal (e.g., "96%" → 96, NOT 0.96)
{name:dec%} - - Percentage as integer (e.g., "-30%" → -30)
{name:int%}
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 };
};数值提取模板语法:
- - 整数(例如:"5" → 5)
{name:int} - - 小数(例如:"21.5" → 21.5)
{name:dec} - - 百分比(保留数值,例如:"96%" → 96,而非0.96)
{name:dec%} - - 百分比整数(例如:"-30%" → -30)
{name:int%}
对于全等级相同的数值:使用
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 testCheck 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 with damage range and cast time:
spellDmgtypescript
"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) }],
}),法术技能使用属性,包含伤害范围和施法时间:
spellDmgtypescript
"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 | |
Using | Use |
| Using HTML regex on clean text | Input is already |
| Parser key doesn't match factory key | Keys must match exactly: |
| Forgetting parser registration | Add to SKILL_PARSERS array in |
| Missing factory | Must add factory in |
| "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 |
| 错误 | 修复方案 |
|---|---|
| 为offense使用数组 | |
在DmgPct类型Mod中使用 | 应使用 |
| 对纯文本使用HTML正则表达式 | 输入已通过 |
| 解析器键与工厂键不匹配 | 键必须完全一致:若工厂使用 |
| 忘记注册解析器 | 将解析器添加到 |
| 缺少工厂定义 | 必须在 |
| "damage"会优先匹配"Effectiveness of added damage"——使用精确匹配(见下文) |
| 缺少21-40级数据 | 许多技能仅提供1-20级数据;将21-40级填充为20级的值 |
findColumn Gotcha: Substring Matching
findColumn注意事项:子字符串匹配
findColumntypescript
// 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`);
}findColumntypescript
// 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 objectsHTML Source → buildProgressionTableInput (去除HTML标签)
→ Parser (提取带命名键的数值)
→ Generation Script (转换为levelValues数组)
→ 输出TypeScript文件
↓
运行时:Factory + levelValues → Mod对象Benefits of Named Keys
命名键的优势
- Self-documenting: is clearer than
vals.projectilePerFrostbiteRatingvals[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
- 自文档化: 比
vals.projectilePerFrostbiteRating更清晰vals[4] - 顺序无关: 解析器与工厂无需约定数组顺序
- 可扩展性: 添加新值不会影响现有索引
- 类型安全: TypeScript可捕获键名拼写错误