feat: better item tooltips
Some checks failed
Dragon Item Parser CI / build-and-test (push) Successful in 13s
pipeline / lint-and-format (push) Successful in 4m35s
pipeline / build-and-push-images (push) Failing after 25s

This commit is contained in:
2026-04-27 00:31:31 +02:00
parent 0e0a12513e
commit 0b2d00ad0b
6 changed files with 1307 additions and 266 deletions

View File

@@ -1,5 +1,11 @@
import { describe, it, expect } from 'vitest'
import { parseItemStats, parseItem, parseItems } from '../src/item.js'
import {
parseItemStats,
parseItem,
parseItems,
parseItemDescription,
parseItemFull
} from '../src/item.js'
describe('parseItemStats', () => {
describe('basic stats', () => {
@@ -191,6 +197,189 @@ describe('parseItemStats', () => {
})
})
describe('parseItemDescription', () => {
describe('stats parsing', () => {
it('should parse stats from description', () => {
const description =
'<mainText><stats><attention> 75</attention> Attack Damage<br><attention> 25%</attention> Critical Strike Chance</stats><br><br></mainText>'
const parsed = parseItemDescription(description)
expect(parsed.stats.attackDamage).toBe(75)
expect(parsed.stats.criticalStrikeChance).toBe(25)
})
it('should parse Infinity Edge stats', () => {
// Infinity Edge (id: 3031)
const description =
'<mainText><stats><attention> 75</attention> Attack Damage<br><attention> 25%</attention> Critical Strike Chance<br><attention> 30%</attention> Critical Strike Damage</stats><br><br></mainText>'
const parsed = parseItemDescription(description)
expect(parsed.stats.attackDamage).toBe(75)
expect(parsed.stats.criticalStrikeChance).toBe(25)
// Critical Strike Damage is not in our mappings, but should not crash
})
})
describe('effects parsing', () => {
it('should parse passive effect', () => {
// Rabadon's Deathcap (id: 3089)
const description =
'<mainText><stats><attention> 130</attention> Ability Power</stats><br><br><passive>Magical Opus</passive><br>Increases your total <scaleAP>Ability Power by 30%</scaleAP>.</mainText>'
const parsed = parseItemDescription(description)
expect(parsed.effects).toHaveLength(1)
expect(parsed.effects[0].type).toBe('passive')
expect(parsed.effects[0].name).toBe('Magical Opus')
})
it('should parse active effect', () => {
// Zhonya's Hourglass (id: 3157)
const description =
'<mainText><stats><attention> 105</attention> Ability Power<br><attention> 50</attention> Armor</stats><br><br><br> <br><active>Time Stop</active><br>Enter <keyword>Stasis</keyword> for 2.5 seconds.</mainText>'
const parsed = parseItemDescription(description)
expect(parsed.effects).toHaveLength(1)
expect(parsed.effects[0].type).toBe('active')
expect(parsed.effects[0].name).toBe('Time Stop')
})
it('should parse multiple passives', () => {
// Trinity Force (id: 3078)
const description =
'<mainText><stats><attention> 36</attention> Attack Damage<br><attention> 30%</attention> Attack Speed<br><attention> 333</attention> Health<br><attention> 15</attention> Ability Haste</stats><br><br><passive>Spellblade</passive><br>After using an Ability, your next Attack deals <physicalDamage>bonus physical damage</physicalDamage> <OnHit>On-Hit</OnHit>.<br> <br><passive>Quicken</passive><br>Attacking grants <speed>20 Move Speed</speed> for 2 seconds.</mainText>'
const parsed = parseItemDescription(description)
expect(parsed.effects).toHaveLength(2)
expect(parsed.effects[0].type).toBe('passive')
expect(parsed.effects[0].name).toBe('Spellblade')
expect(parsed.effects[1].type).toBe('passive')
expect(parsed.effects[1].name).toBe('Quicken')
})
it('should parse passive with status tag', () => {
// Serylda's Grudge (id: 6694)
const description =
'<mainText><stats><attention> 45</attention> Attack Damage<br><attention> 35%</attention> Armor Penetration<br><attention> 15</attention> Ability Haste</stats><br><br><passive>Bitter Cold</passive><br>Damaging Abilities <status>Slow</status> enemies below 50% Health by 30% for 1 second.</mainText>'
const parsed = parseItemDescription(description)
expect(parsed.effects).toHaveLength(1)
expect(parsed.effects[0].type).toBe('passive')
expect(parsed.effects[0].name).toBe('Bitter Cold')
})
it('should handle item with no effects', () => {
// Infinity Edge has no passive/active tags
const description =
'<mainText><stats><attention> 75</attention> Attack Damage<br><attention> 25%</attention> Critical Strike Chance<br><attention> 30%</attention> Critical Strike Damage</stats><br><br></mainText>'
const parsed = parseItemDescription(description)
expect(parsed.effects).toHaveLength(0)
})
})
describe('text segments', () => {
it('should parse scaleAP tag', () => {
const description =
'<mainText><stats><attention> 130</attention> Ability Power</stats><br><br><passive>Magical Opus</passive><br>Increases your total <scaleAP>Ability Power by 30%</scaleAP>.</mainText>'
const parsed = parseItemDescription(description)
// Find the scaleAP segment
const effect = parsed.effects[0]
const scaleSegment = effect.description.find(s => s.type === 'scaleAP')
expect(scaleSegment).toBeDefined()
expect(scaleSegment?.content).toContain('Ability Power by 30%')
})
it('should parse physicalDamage tag', () => {
const description =
'<mainText><stats><attention> 36</attention> Attack Damage</stats><br><br><passive>Spellblade</passive><br>Deals <physicalDamage>bonus physical damage</physicalDamage>.</mainText>'
const parsed = parseItemDescription(description)
const effect = parsed.effects[0]
const damageSegment = effect.description.find(s => s.type === 'physicalDamage')
expect(damageSegment).toBeDefined()
expect(damageSegment?.content).toContain('bonus physical damage')
})
it('should parse keyword tag', () => {
const description =
'<mainText><stats><attention> 105</attention> Ability Power</stats><br><br><active>Time Stop</active><br>Enter <keyword>Stasis</keyword> for 2.5 seconds.</mainText>'
const parsed = parseItemDescription(description)
const effect = parsed.effects[0]
const keywordSegment = effect.description.find(s => s.type === 'keyword')
expect(keywordSegment).toBeDefined()
expect(keywordSegment?.content).toBe('Stasis')
})
it('should parse speed tag', () => {
const description =
'<mainText><stats><attention> 36</attention> Attack Damage</stats><br><br><passive>Quicken</passive><br>Grants <speed>20 Move Speed</speed>.</mainText>'
const parsed = parseItemDescription(description)
const effect = parsed.effects[0]
const speedSegment = effect.description.find(s => s.type === 'speed')
expect(speedSegment).toBeDefined()
expect(speedSegment?.content).toContain('20 Move Speed')
})
it('should parse status tag', () => {
const description =
'<mainText><stats><attention> 45</attention> Attack Damage</stats><br><br><passive>Bitter Cold</passive><br><status>Slow</status> enemies.</mainText>'
const parsed = parseItemDescription(description)
const effect = parsed.effects[0]
const statusSegment = effect.description.find(s => s.type === 'status')
expect(statusSegment).toBeDefined()
expect(statusSegment?.content).toBe('Slow')
})
})
describe('rarity detection', () => {
it('should detect mythic rarity', () => {
const description =
'<mainText><stats><attention> 65</attention> Attack Damage</stats><br><br><rarityMythic>Mythic</rarityMythic></mainText>'
const parsed = parseItemDescription(description)
expect(parsed.rarity).toBe('mythic')
})
it('should detect legendary rarity', () => {
const description =
'<mainText><stats><attention> 65</attention> Attack Damage</stats><br><br><rarityLegendary>Legendary</rarityLegendary></mainText>'
const parsed = parseItemDescription(description)
expect(parsed.rarity).toBe('legendary')
})
it('should detect epic rarity', () => {
const description =
'<mainText><stats><attention> 65</attention> Attack Damage</stats><br><br><rarityGeneric>Epic</rarityGeneric></mainText>'
const parsed = parseItemDescription(description)
expect(parsed.rarity).toBe('epic')
})
})
describe('rules and flavor text', () => {
it('should parse rules section', () => {
const description =
'<mainText><stats><attention> 25</attention> Move Speed</stats><br><br><rules>Ornn upgrade only</rules></mainText>'
const parsed = parseItemDescription(description)
expect(parsed.rules).toBeDefined()
expect(parsed.rules?.length).toBeGreaterThan(0)
})
it('should parse flavor text section', () => {
const description =
'<mainText><stats><attention> 25</attention> Move Speed</stats><br><br><flavorText>A ancient artifact</flavorText></mainText>'
const parsed = parseItemDescription(description)
expect(parsed.flavorText).toBeDefined()
expect(parsed.flavorText?.length).toBeGreaterThan(0)
})
})
})
describe('parseItem', () => {
it('should parse item and add stats', () => {
const item = {
@@ -233,3 +422,22 @@ describe('parseItems', () => {
expect(results[1].stats.attackDamage).toBe(10)
})
})
describe('parseItemFull', () => {
it('should parse item with full description', () => {
const item = {
id: 3089,
name: "Rabadon's Deathcap",
description:
'<mainText><stats><attention> 130</attention> Ability Power</stats><br><br><passive>Magical Opus</passive><br>Increases your total <scaleAP>Ability Power by 30%</scaleAP>.</mainText>',
iconPath: '/path/to/icon.png'
}
const result = parseItemFull(item)
expect(result.id).toBe(3089)
expect(result.name).toBe("Rabadon's Deathcap")
expect(result.parsedDescription.stats.abilityPower).toBe(130)
expect(result.parsedDescription.effects).toHaveLength(1)
expect(result.parsedDescription.effects[0].name).toBe('Magical Opus')
})
})