dragon-item-parser: introduce item parser library
This commit is contained in:
3
dragon-item-parser/.gitignore
vendored
Normal file
3
dragon-item-parser/.gitignore
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
node_modules/
|
||||
dist/
|
||||
*.log
|
||||
10
dragon-item-parser/.prettierrc
Normal file
10
dragon-item-parser/.prettierrc
Normal file
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"semi": false,
|
||||
"singleQuote": true,
|
||||
"tabWidth": 2,
|
||||
"trailingComma": "none",
|
||||
"printWidth": 100,
|
||||
"bracketSpacing": true,
|
||||
"arrowParens": "avoid",
|
||||
"endOfLine": "lf"
|
||||
}
|
||||
19
dragon-item-parser/eslint.config.js
Normal file
19
dragon-item-parser/eslint.config.js
Normal file
@@ -0,0 +1,19 @@
|
||||
import { defineConfig } from 'eslint/config'
|
||||
import js from '@eslint/js'
|
||||
import tseslint from 'typescript-eslint'
|
||||
import prettier from 'eslint-config-prettier'
|
||||
|
||||
export default defineConfig([
|
||||
js.configs.recommended,
|
||||
tseslint.configs.recommended,
|
||||
prettier,
|
||||
{
|
||||
ignores: ['dist/**', 'node_modules/**']
|
||||
},
|
||||
{
|
||||
rules: {
|
||||
semi: 'off',
|
||||
'prefer-const': 'error'
|
||||
}
|
||||
}
|
||||
])
|
||||
2886
dragon-item-parser/package-lock.json
generated
Normal file
2886
dragon-item-parser/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
53
dragon-item-parser/package.json
Normal file
53
dragon-item-parser/package.json
Normal file
@@ -0,0 +1,53 @@
|
||||
{
|
||||
"name": "dragon-item-parser",
|
||||
"version": "1.0.0",
|
||||
"description": "Parse League of Legends item stats from CommunityDragon data",
|
||||
"type": "module",
|
||||
"main": "dist/index.js",
|
||||
"types": "dist/index.d.ts",
|
||||
"exports": {
|
||||
".": {
|
||||
"import": "./dist/index.js",
|
||||
"types": "./dist/index.d.ts"
|
||||
}
|
||||
},
|
||||
"files": [
|
||||
"dist"
|
||||
],
|
||||
"scripts": {
|
||||
"build": "tsc",
|
||||
"dev": "tsc --watch",
|
||||
"test": "vitest run",
|
||||
"test:watch": "vitest",
|
||||
"lint": "eslint .",
|
||||
"lint:fix": "eslint --fix .",
|
||||
"format": "prettier --write .",
|
||||
"format:check": "prettier --check .",
|
||||
"prepublishOnly": "npm run build"
|
||||
},
|
||||
"keywords": [
|
||||
"league-of-legends",
|
||||
"cdragon",
|
||||
"communitydragon",
|
||||
"item",
|
||||
"parser",
|
||||
"lol"
|
||||
],
|
||||
"author": "",
|
||||
"license": "MIT",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": ""
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.39.2",
|
||||
"@typescript-eslint/eslint-plugin": "^8.53.1",
|
||||
"@typescript-eslint/parser": "^8.53.1",
|
||||
"eslint": "^9.39.2",
|
||||
"eslint-config-prettier": "^10.1.8",
|
||||
"prettier": "^3.8.3",
|
||||
"typescript": "^5.0.0",
|
||||
"typescript-eslint": "^8.53.1",
|
||||
"vitest": "^3.0.0"
|
||||
}
|
||||
}
|
||||
2
dragon-item-parser/src/index.ts
Normal file
2
dragon-item-parser/src/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export type { ItemStats, CDragonItem, ItemWithStats } from './item.js'
|
||||
export { parseItemStats, parseItem, parseItems } from './item.js'
|
||||
202
dragon-item-parser/src/item.ts
Normal file
202
dragon-item-parser/src/item.ts
Normal file
@@ -0,0 +1,202 @@
|
||||
/**
|
||||
* Item stats parsed from CommunityDragon item description HTML
|
||||
*/
|
||||
export interface ItemStats {
|
||||
// Offensive stats
|
||||
attackDamage?: number
|
||||
abilityPower?: number
|
||||
attackSpeed?: number
|
||||
criticalStrikeChance?: number
|
||||
lifeSteal?: number
|
||||
omnivamp?: number
|
||||
physicalVamp?: number
|
||||
spellVamp?: number
|
||||
|
||||
// Defensive stats
|
||||
health?: number
|
||||
armor?: number
|
||||
magicResist?: number
|
||||
|
||||
// Resource stats
|
||||
mana?: number
|
||||
baseManaRegen?: number
|
||||
baseHealthRegen?: number
|
||||
|
||||
// Movement stats
|
||||
moveSpeed?: number
|
||||
|
||||
// Ability stats
|
||||
abilityHaste?: number
|
||||
|
||||
// Penetration stats (usually percentages)
|
||||
armorPenetration?: number
|
||||
magicPenetration?: number
|
||||
lethality?: number
|
||||
magicPenetrationFlat?: number
|
||||
|
||||
// Other percentage stats
|
||||
healAndShieldPower?: number
|
||||
tenacity?: number
|
||||
slowResist?: number
|
||||
}
|
||||
|
||||
/**
|
||||
* Stat name mappings from CDragon description text to stat keys
|
||||
*/
|
||||
const STAT_MAPPINGS: Record<string, keyof ItemStats> = {
|
||||
'Attack Damage': 'attackDamage',
|
||||
'Ability Power': 'abilityPower',
|
||||
'Attack Speed': 'attackSpeed',
|
||||
'Critical Strike Chance': 'criticalStrikeChance',
|
||||
'Life Steal': 'lifeSteal',
|
||||
Omnivamp: 'omnivamp',
|
||||
'Physical Vamp': 'physicalVamp',
|
||||
'Spell Vamp': 'spellVamp',
|
||||
Health: 'health',
|
||||
Armor: 'armor',
|
||||
'Magic Resist': 'magicResist',
|
||||
Mana: 'mana',
|
||||
'Base Mana Regen': 'baseManaRegen',
|
||||
'Base Health Regen': 'baseHealthRegen',
|
||||
'Move Speed': 'moveSpeed',
|
||||
'Ability Haste': 'abilityHaste',
|
||||
'Armor Penetration': 'armorPenetration',
|
||||
'Magic Penetration': 'magicPenetration',
|
||||
Lethality: 'lethality',
|
||||
'Heal and Shield Power': 'healAndShieldPower',
|
||||
Tenacity: 'tenacity',
|
||||
'Slow Resist': 'slowResist'
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse a stat value string to a number
|
||||
* Handles both flat values (e.g., "25") and percentages (e.g., "25%")
|
||||
*/
|
||||
function parseStatValue(valueStr: string): { value: number; isPercentage: boolean } {
|
||||
const trimmed = valueStr.trim()
|
||||
const isPercentage = trimmed.includes('%')
|
||||
const numStr = trimmed.replace('%', '').replace(',', '').trim()
|
||||
const value = parseFloat(numStr)
|
||||
return { value, isPercentage }
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract stats section from the description HTML
|
||||
*/
|
||||
function extractStatsSection(description: string): string | null {
|
||||
const statsMatch = description.match(/<stats>(.*?)<\/stats>/s)
|
||||
return statsMatch ? statsMatch[1] : null
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse individual stat lines from the stats section
|
||||
* Format: <attention> value </attention> statName
|
||||
*/
|
||||
function parseStatLines(
|
||||
statsSection: string
|
||||
): Array<{ value: number; isPercentage: boolean; statName: string }> {
|
||||
const results: Array<{ value: number; isPercentage: boolean; statName: string }> = []
|
||||
|
||||
// Match patterns like: <attention> 25</attention> Move Speed
|
||||
// or: <attention> 25%</attention> Attack Speed
|
||||
const statRegex = /<attention>\s*([^<]+)<\/attention>\s*([^<]+)/g
|
||||
let match
|
||||
|
||||
while ((match = statRegex.exec(statsSection)) !== null) {
|
||||
const valueStr = match[1]
|
||||
const statName = match[2].trim()
|
||||
|
||||
const { value, isPercentage } = parseStatValue(valueStr)
|
||||
|
||||
if (!isNaN(value) && statName) {
|
||||
results.push({ value, isPercentage, statName })
|
||||
}
|
||||
}
|
||||
|
||||
return results
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse item stats from CDragon description HTML
|
||||
*
|
||||
* @param description - The HTML description string from CDragon items.json
|
||||
* @returns Parsed ItemStats object with all recognized stats
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* const stats = parseItemStats(
|
||||
* '<mainText><stats><attention> 25</attention> Move Speed</stats><br><br></mainText>'
|
||||
* )
|
||||
* // Returns: { moveSpeed: 25 }
|
||||
* ```
|
||||
*/
|
||||
export function parseItemStats(description: string): ItemStats {
|
||||
const stats: ItemStats = {}
|
||||
|
||||
if (!description) {
|
||||
return stats
|
||||
}
|
||||
|
||||
const statsSection = extractStatsSection(description)
|
||||
if (!statsSection) {
|
||||
return stats
|
||||
}
|
||||
|
||||
const statLines = parseStatLines(statsSection)
|
||||
|
||||
for (const { value, statName } of statLines) {
|
||||
const statKey = STAT_MAPPINGS[statName]
|
||||
|
||||
if (statKey) {
|
||||
// For percentage stats that are stored as decimals (e.g., 25% -> 25)
|
||||
// We store the percentage value as-is for consistency with game data
|
||||
stats[statKey] = value
|
||||
}
|
||||
}
|
||||
|
||||
return stats
|
||||
}
|
||||
|
||||
/**
|
||||
* Item data structure from CDragon
|
||||
*/
|
||||
export interface CDragonItem {
|
||||
id: number
|
||||
name: string
|
||||
description: string
|
||||
active?: boolean
|
||||
inStore?: boolean
|
||||
from?: number[]
|
||||
to?: number[]
|
||||
categories?: string[]
|
||||
maxStacks?: number
|
||||
requiredChampion?: string
|
||||
requiredAlly?: string
|
||||
price?: number
|
||||
priceTotal?: number
|
||||
iconPath: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Item with parsed stats
|
||||
*/
|
||||
export interface ItemWithStats extends CDragonItem {
|
||||
stats: ItemStats
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse a CDragon item and add parsed stats
|
||||
*/
|
||||
export function parseItem(item: CDragonItem): ItemWithStats {
|
||||
return {
|
||||
...item,
|
||||
stats: parseItemStats(item.description)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse an array of CDragon items and add parsed stats
|
||||
*/
|
||||
export function parseItems(items: CDragonItem[]): ItemWithStats[] {
|
||||
return items.map(parseItem)
|
||||
}
|
||||
235
dragon-item-parser/test/item.test.ts
Normal file
235
dragon-item-parser/test/item.test.ts
Normal file
@@ -0,0 +1,235 @@
|
||||
import { describe, it, expect } from 'vitest'
|
||||
import { parseItemStats, parseItem, parseItems } from '../src/item.js'
|
||||
|
||||
describe('parseItemStats', () => {
|
||||
describe('basic stats', () => {
|
||||
it('should parse Move Speed', () => {
|
||||
// Boots (id: 1001)
|
||||
const description =
|
||||
'<mainText><stats><attention> 25</attention> Move Speed</stats><br><br></mainText>'
|
||||
const stats = parseItemStats(description)
|
||||
expect(stats.moveSpeed).toBe(25)
|
||||
})
|
||||
|
||||
it('should parse Attack Damage', () => {
|
||||
// Long Sword (id: 1036)
|
||||
const description =
|
||||
'<mainText><stats><attention> 10</attention> Attack Damage</stats><br><br></mainText>'
|
||||
const stats = parseItemStats(description)
|
||||
expect(stats.attackDamage).toBe(10)
|
||||
})
|
||||
|
||||
it('should parse Ability Power', () => {
|
||||
// Amplifying Tome (id: 1052)
|
||||
const description =
|
||||
'<mainText><stats><attention> 20</attention> Ability Power</stats><br><br></mainText>'
|
||||
const stats = parseItemStats(description)
|
||||
expect(stats.abilityPower).toBe(20)
|
||||
})
|
||||
|
||||
it('should parse Health', () => {
|
||||
// Ruby Crystal (id: 1028)
|
||||
const description =
|
||||
'<mainText><stats><attention> 150</attention> Health</stats><br><br></mainText>'
|
||||
const stats = parseItemStats(description)
|
||||
expect(stats.health).toBe(150)
|
||||
})
|
||||
|
||||
it('should parse Armor', () => {
|
||||
// Cloth Armor (id: 1029)
|
||||
const description =
|
||||
'<mainText><stats><attention> 15</attention> Armor</stats><br><br></mainText>'
|
||||
const stats = parseItemStats(description)
|
||||
expect(stats.armor).toBe(15)
|
||||
})
|
||||
|
||||
it('should parse Magic Resist', () => {
|
||||
// Null-Magic Mantle (id: 1033)
|
||||
const description =
|
||||
'<mainText><stats><attention> 20</attention> Magic Resist</stats><br><br></mainText>'
|
||||
const stats = parseItemStats(description)
|
||||
expect(stats.magicResist).toBe(20)
|
||||
})
|
||||
|
||||
it('should parse Mana', () => {
|
||||
// Sapphire Crystal (id: 1027)
|
||||
const description =
|
||||
'<mainText><stats><attention> 300</attention> Mana</stats><br><br></mainText>'
|
||||
const stats = parseItemStats(description)
|
||||
expect(stats.mana).toBe(300)
|
||||
})
|
||||
})
|
||||
|
||||
describe('percentage stats', () => {
|
||||
it('should parse Attack Speed percentage', () => {
|
||||
// Dagger (id: 1042)
|
||||
const description =
|
||||
'<mainText><stats><attention> 10%</attention> Attack Speed</stats><br><br></mainText>'
|
||||
const stats = parseItemStats(description)
|
||||
expect(stats.attackSpeed).toBe(10)
|
||||
})
|
||||
|
||||
it('should parse Critical Strike Chance', () => {
|
||||
// Cloak of Agility (id: 1018)
|
||||
const description =
|
||||
'<mainText><stats><attention> 15%</attention> Critical Strike Chance</stats><br><br></mainText>'
|
||||
const stats = parseItemStats(description)
|
||||
expect(stats.criticalStrikeChance).toBe(15)
|
||||
})
|
||||
|
||||
it('should parse Life Steal', () => {
|
||||
// Vampiric Scepter (id: 1053)
|
||||
const description =
|
||||
'<mainText><stats><attention> 15</attention> Attack Damage<br><attention> 7%</attention> Life Steal</stats><br><br></mainText>'
|
||||
const stats = parseItemStats(description)
|
||||
expect(stats.attackDamage).toBe(15)
|
||||
expect(stats.lifeSteal).toBe(7)
|
||||
})
|
||||
|
||||
it('should parse Base Mana Regen percentage', () => {
|
||||
// Faerie Charm (id: 1004)
|
||||
const description =
|
||||
'<mainText><stats><attention> 50%</attention> Base Mana Regen</stats><br><br></mainText>'
|
||||
const stats = parseItemStats(description)
|
||||
expect(stats.baseManaRegen).toBe(50)
|
||||
})
|
||||
|
||||
it('should parse Base Health Regen percentage', () => {
|
||||
// Rejuvenation Bead (id: 1006)
|
||||
const description =
|
||||
'<mainText><stats><attention> 100%</attention> Base Health Regen</stats><br><br></mainText>'
|
||||
const stats = parseItemStats(description)
|
||||
expect(stats.baseHealthRegen).toBe(100)
|
||||
})
|
||||
})
|
||||
|
||||
describe('multiple stats', () => {
|
||||
it("should parse multiple stats from Doran's Blade", () => {
|
||||
// Doran's Blade (id: 1055)
|
||||
const description =
|
||||
'<mainText><stats><attention> 10</attention> Attack Damage<br><attention> 80</attention> Health<br><attention> 2.5%</attention> Omnivamp</stats><br><br></mainText>'
|
||||
const stats = parseItemStats(description)
|
||||
expect(stats.attackDamage).toBe(10)
|
||||
expect(stats.health).toBe(80)
|
||||
expect(stats.omnivamp).toBe(2.5)
|
||||
})
|
||||
|
||||
it("should parse multiple stats from Doran's Ring", () => {
|
||||
// Doran's Ring (id: 1056)
|
||||
const description =
|
||||
'<mainText><stats><attention> 18</attention> Ability Power<br><attention> 90</attention> Health</stats><br><br></mainText>'
|
||||
const stats = parseItemStats(description)
|
||||
expect(stats.abilityPower).toBe(18)
|
||||
expect(stats.health).toBe(90)
|
||||
})
|
||||
|
||||
it("should parse multiple stats from Seeker's Armguard", () => {
|
||||
// Seeker's Armguard (id: 2420)
|
||||
const description =
|
||||
'<mainText><stats><attention> 40</attention> Ability Power<br><attention> 25</attention> Armor</stats><br><br></mainText>'
|
||||
const stats = parseItemStats(description)
|
||||
expect(stats.abilityPower).toBe(40)
|
||||
expect(stats.armor).toBe(25)
|
||||
})
|
||||
|
||||
it('should parse complex item with many stats', () => {
|
||||
// Overlord's Bloodmail (id: 2501)
|
||||
const description =
|
||||
'<mainText><stats><attention> 30</attention> Attack Damage<br><attention> 550</attention> Health</stats><br><br></mainText>'
|
||||
const stats = parseItemStats(description)
|
||||
expect(stats.attackDamage).toBe(30)
|
||||
expect(stats.health).toBe(550)
|
||||
})
|
||||
|
||||
it('should parse item with Ability Haste', () => {
|
||||
// Unending Despair (id: 2502)
|
||||
const description =
|
||||
'<mainText><stats><attention> 400</attention> Health<br><attention> 50</attention> Armor<br><attention> 15</attention> Ability Haste</stats><br><br></mainText>'
|
||||
const stats = parseItemStats(description)
|
||||
expect(stats.health).toBe(400)
|
||||
expect(stats.armor).toBe(50)
|
||||
expect(stats.abilityHaste).toBe(15)
|
||||
})
|
||||
|
||||
it('should parse item with Mana and Ability Haste', () => {
|
||||
// Blackfire Torch (id: 2503)
|
||||
const description =
|
||||
'<mainText><stats><attention> 80</attention> Ability Power<br><attention> 600</attention> Mana<br><attention> 20</attention> Ability Haste</stats><br><br></mainText>'
|
||||
const stats = parseItemStats(description)
|
||||
expect(stats.abilityPower).toBe(80)
|
||||
expect(stats.mana).toBe(600)
|
||||
expect(stats.abilityHaste).toBe(20)
|
||||
})
|
||||
})
|
||||
|
||||
describe('edge cases', () => {
|
||||
it('should return empty object for empty description', () => {
|
||||
const stats = parseItemStats('')
|
||||
expect(stats).toEqual({})
|
||||
})
|
||||
|
||||
it('should return empty object for description without stats', () => {
|
||||
const description = '<mainText><stats></stats><br><br></mainText>'
|
||||
const stats = parseItemStats(description)
|
||||
expect(stats).toEqual({})
|
||||
})
|
||||
|
||||
it('should handle description with only passive text', () => {
|
||||
// Emberknife (id: 1035) - no stats, only passives
|
||||
const description =
|
||||
'<mainText><stats></stats><br><br> <passive>7%</passive> Omnivamp against jungle monsters<br><li><passive>Sear:</passive> Damaging jungle monsters burns them for <magicDamage> magic damage</magicDamage> over 5 seconds.</mainText>'
|
||||
const stats = parseItemStats(description)
|
||||
expect(stats).toEqual({})
|
||||
})
|
||||
|
||||
it('should handle decimal values', () => {
|
||||
const description =
|
||||
'<mainText><stats><attention> 2.5%</attention> Omnivamp</stats><br><br></mainText>'
|
||||
const stats = parseItemStats(description)
|
||||
expect(stats.omnivamp).toBe(2.5)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('parseItem', () => {
|
||||
it('should parse item and add stats', () => {
|
||||
const item = {
|
||||
id: 1001,
|
||||
name: 'Boots',
|
||||
description:
|
||||
'<mainText><stats><attention> 25</attention> Move Speed</stats><br><br></mainText>',
|
||||
iconPath: '/path/to/icon.png'
|
||||
}
|
||||
|
||||
const result = parseItem(item)
|
||||
expect(result.id).toBe(1001)
|
||||
expect(result.name).toBe('Boots')
|
||||
expect(result.stats.moveSpeed).toBe(25)
|
||||
})
|
||||
})
|
||||
|
||||
describe('parseItems', () => {
|
||||
it('should parse multiple items', () => {
|
||||
const items = [
|
||||
{
|
||||
id: 1001,
|
||||
name: 'Boots',
|
||||
description:
|
||||
'<mainText><stats><attention> 25</attention> Move Speed</stats><br><br></mainText>',
|
||||
iconPath: '/path/to/boots.png'
|
||||
},
|
||||
{
|
||||
id: 1036,
|
||||
name: 'Long Sword',
|
||||
description:
|
||||
'<mainText><stats><attention> 10</attention> Attack Damage</stats><br><br></mainText>',
|
||||
iconPath: '/path/to/sword.png'
|
||||
}
|
||||
]
|
||||
|
||||
const results = parseItems(items)
|
||||
expect(results).toHaveLength(2)
|
||||
expect(results[0].stats.moveSpeed).toBe(25)
|
||||
expect(results[1].stats.attackDamage).toBe(10)
|
||||
})
|
||||
})
|
||||
17
dragon-item-parser/tsconfig.json
Normal file
17
dragon-item-parser/tsconfig.json
Normal file
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2020",
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "bundler",
|
||||
"declaration": true,
|
||||
"declarationMap": true,
|
||||
"outDir": "./dist",
|
||||
"rootDir": "./src",
|
||||
"strict": true,
|
||||
"esModuleInterop": true,
|
||||
"skipLibCheck": true,
|
||||
"forceConsistentCasingInFileNames": true
|
||||
},
|
||||
"include": ["src/**/*"],
|
||||
"exclude": ["node_modules", "dist", "test"]
|
||||
}
|
||||
7
dragon-item-parser/vitest.config.ts
Normal file
7
dragon-item-parser/vitest.config.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import { defineConfig } from 'vitest/config'
|
||||
|
||||
export default defineConfig({
|
||||
test: {
|
||||
include: ['test/**/*.test.ts']
|
||||
}
|
||||
})
|
||||
Reference in New Issue
Block a user