From 0b2d00ad0b310941df8094301c218f5631dd4c66 Mon Sep 17 00:00:00 2001 From: Valentin Haudiquet Date: Mon, 27 Apr 2026 00:31:31 +0200 Subject: [PATCH] feat: better item tooltips --- dragon-item-parser/src/index.ts | 21 +- dragon-item-parser/src/item.ts | 536 ++++++++++++++++ dragon-item-parser/test/item.test.ts | 210 ++++++- frontend/assets/css/main.css | 34 + frontend/components/item/ItemTooltip.vue | 769 +++++++++++++++-------- frontend/utils/buildHelpers.ts | 3 +- 6 files changed, 1307 insertions(+), 266 deletions(-) diff --git a/dragon-item-parser/src/index.ts b/dragon-item-parser/src/index.ts index beba903..a110255 100644 --- a/dragon-item-parser/src/index.ts +++ b/dragon-item-parser/src/index.ts @@ -1,2 +1,19 @@ -export type { ItemStats, CDragonItem, ItemWithStats } from './item.js' -export { parseItemStats, parseItem, parseItems } from './item.js' +export type { + ItemStats, + CDragonItem, + ItemWithStats, + ItemEffect, + TextSegment, + ScalingValue, + DamageValue, + ParsedDescription, + ItemWithParsedDescription +} from './item.js' +export { + parseItemStats, + parseItem, + parseItems, + parseItemDescription, + parseItemFull, + parseItemsFull +} from './item.js' diff --git a/dragon-item-parser/src/item.ts b/dragon-item-parser/src/item.ts index dbc5bc6..b860de5 100644 --- a/dragon-item-parser/src/item.ts +++ b/dragon-item-parser/src/item.ts @@ -7,6 +7,7 @@ export interface ItemStats { abilityPower?: number attackSpeed?: number criticalStrikeChance?: number + criticalStrikeDamage?: number lifeSteal?: number omnivamp?: number physicalVamp?: number @@ -40,6 +41,98 @@ export interface ItemStats { slowResist?: number } +/** + * Represents a scaling value (e.g., "5% bonus health") + */ +export interface ScalingValue { + value: number + isPercentage: boolean + scaleType: + | 'mana' + | 'health' + | 'ap' + | 'ad' + | 'armor' + | 'mr' + | 'level' + | 'bonusHealth' + | 'bonusMana' + | 'maxHealth' +} + +/** + * Represents a damage value with type + */ +export interface DamageValue { + value: number + isPercentage: boolean + damageType: 'magic' | 'physical' | 'true' +} + +/** + * Represents a colored text segment + */ +export interface TextSegment { + type: + | 'text' + | 'highlight' + | 'passive' + | 'active' + | 'keyword' + | 'keywordMajor' + | 'keywordStealth' + | 'status' + | 'speed' + | 'scaleMana' + | 'scaleHealth' + | 'scaleAP' + | 'scaleAD' + | 'scaleArmor' + | 'scaleMR' + | 'scaleLevel' + | 'scaleBonusHealth' + | 'scaleBonusMana' + | 'scaleMaxHealth' + | 'spellName' + | 'unique' + | 'rarityMythic' + | 'rarityLegendary' + | 'rarityGeneric' + | 'magicDamage' + | 'physicalDamage' + | 'trueDamage' + | 'healing' + | 'shield' + | 'attention' + | 'onHit' + | 'color' + content: string + color?: string // For custom color spans + scaling?: ScalingValue + damage?: DamageValue +} + +/** + * Represents an item effect (passive, active, unique, etc.) + */ +export interface ItemEffect { + type: 'passive' | 'active' | 'unique' | 'mythic' | 'legendary' | 'epic' + name?: string + description: TextSegment[] + isUnique?: boolean +} + +/** + * Parsed item description structure + */ +export interface ParsedDescription { + stats: ItemStats + effects: ItemEffect[] + rules?: TextSegment[] + flavorText?: TextSegment[] + rarity?: 'mythic' | 'legendary' | 'epic' +} + /** * Stat name mappings from CDragon description text to stat keys */ @@ -48,6 +141,7 @@ const STAT_MAPPINGS: Record = { 'Ability Power': 'abilityPower', 'Attack Speed': 'attackSpeed', 'Critical Strike Chance': 'criticalStrikeChance', + 'Critical Strike Damage': 'criticalStrikeDamage', 'Life Steal': 'lifeSteal', Omnivamp: 'omnivamp', 'Physical Vamp': 'physicalVamp', @@ -157,6 +251,424 @@ export function parseItemStats(description: string): ItemStats { return stats } +/** + * Remove HTML tags and get plain text + */ +function stripHtmlTags(html: string): string { + return html + .replace(//gi, '\n') + .replace(/<[^>]+>/g, '') + .replace(/\s+/g, ' ') + .trim() +} + +/** + * Parse scaling tags and extract scaling info + */ +function parseScalingTag( + tagName: string, + content: string +): { scaling?: ScalingValue; text: string } { + const scaleTypeMap: Record = { + scaleMana: 'mana', + scaleHealth: 'health', + scaleAP: 'ap', + scaleAD: 'ad', + scaleArmor: 'armor', + scaleMR: 'mr', + scaleLevel: 'level', + scaleBonusHealth: 'bonusHealth', + scaleBonusMana: 'bonusMana', + scaleMaxHealth: 'maxHealth' + } + + const scaleType = scaleTypeMap[tagName] + if (!scaleType) { + return { text: content } + } + + // Try to extract percentage or flat value + const percentMatch = content.match(/(\d+(?:\.\d+)?)\s*%/) + const flatMatch = content.match(/(\d+(?:\.\d+)?)/) + + if (percentMatch) { + return { + scaling: { + value: parseFloat(percentMatch[1]), + isPercentage: true, + scaleType + }, + text: content + } + } else if (flatMatch) { + return { + scaling: { + value: parseFloat(flatMatch[1]), + isPercentage: false, + scaleType + }, + text: content + } + } + + return { text: content } +} + +/** + * Parse damage tags (magicDamage, physicalDamage, trueDamage) + */ +function parseDamageTag(tagName: string, content: string): { damage?: DamageValue; text: string } { + const damageTypeMap: Record = { + magicDamage: 'magic', + physicalDamage: 'physical', + trueDamage: 'true' + } + + const damageType = damageTypeMap[tagName] + if (!damageType) { + return { text: content } + } + + // Try to extract damage value + const percentMatch = content.match(/(\d+(?:\.\d+)?)\s*%/) + const flatMatch = content.match(/(\d+(?:\.\d+)?)/) + + if (percentMatch) { + return { + damage: { + value: parseFloat(percentMatch[1]), + isPercentage: true, + damageType + }, + text: content + } + } else if (flatMatch) { + return { + damage: { + value: parseFloat(flatMatch[1]), + isPercentage: false, + damageType + }, + text: content + } + } + + return { text: content } +} + +/** + * Parse text content with HTML tags into TextSegments + */ +function parseTextSegments(html: string): TextSegment[] { + const segments: TextSegment[] = [] + + if (!html) return segments + + // Process the HTML and convert to segments + // We'll use a simple state machine approach + let remaining = html + let currentText = '' + + // Tag type mappings + const tagTypeMap: Record = { + passive: 'passive', + active: 'active', + keyword: 'keyword', + keywordMajor: 'keywordMajor', + keywordStealth: 'keywordStealth', + status: 'status', + speed: 'speed', + scaleMana: 'scaleMana', + scaleHealth: 'scaleHealth', + scaleAP: 'scaleAP', + scaleAD: 'scaleAD', + scaleArmor: 'scaleArmor', + scaleMR: 'scaleMR', + scaleLevel: 'scaleLevel', + scaleBonusHealth: 'scaleBonusHealth', + scaleBonusMana: 'scaleBonusMana', + scaleMaxHealth: 'scaleMaxHealth', + spellName: 'spellName', + attention: 'attention', + magicDamage: 'magicDamage', + physicalDamage: 'physicalDamage', + trueDamage: 'trueDamage', + healing: 'healing', + shield: 'shield', + onHit: 'onHit' + } + + // Process tags + while (remaining.length > 0) { + // Check for opening tag + const openMatch = remaining.match(/^<([a-zA-Z]+)(?:\s+color='([^']+)')?\s*>/) + + if (openMatch) { + // Push any accumulated text + if (currentText) { + segments.push({ type: 'text', content: currentText }) + currentText = '' + } + + const tagName = openMatch[1] + const color = openMatch[2] + const fullTag = openMatch[0] + + // Find closing tag + const closeTag = new RegExp(``, 'i') + const closeMatch = remaining.substring(fullTag.length).match(closeTag) + + if (closeMatch && closeMatch.index !== undefined) { + const content = remaining.substring(fullTag.length, fullTag.length + closeMatch.index) + const segmentType = tagTypeMap[tagName] || 'text' + + if (tagName === 'font' && color) { + segments.push({ type: 'color', content: stripHtmlTags(content), color }) + } else if (tagName.startsWith('scale') && tagTypeMap[tagName]) { + const { scaling, text } = parseScalingTag(tagName, content) + segments.push({ + type: segmentType, + content: stripHtmlTags(text), + scaling + }) + } else if (tagName.endsWith('Damage') && tagTypeMap[tagName]) { + const { damage, text } = parseDamageTag(tagName, content) + segments.push({ + type: segmentType, + content: stripHtmlTags(text), + damage + }) + } else if (segmentType !== 'text') { + segments.push({ type: segmentType, content: stripHtmlTags(content) }) + } else { + // Unknown tag, just add as text + currentText += stripHtmlTags(content) + } + + remaining = remaining.substring( + fullTag.length + (closeMatch.index || 0) + closeMatch[0].length + ) + } else { + // No closing tag found, skip the opening tag + remaining = remaining.substring(fullTag.length) + } + } else { + // Add character to current text + currentText += remaining[0] + remaining = remaining.substring(1) + } + } + + // Push any remaining text + if (currentText) { + segments.push({ type: 'text', content: currentText }) + } + + return segments +} + +/** + * Extract effects section from description (everything after stats) + */ +function extractEffectsSection(description: string): string { + // Remove stats section first + const withoutStats = description.replace(/.*?<\/stats>/s, '') + // Remove mainText wrapper + const effects = withoutStats + .replace(//gi, '') + .replace(/<\/mainText>/gi, '') + .replace(/.*?<\/rules>/gs, '') // Remove rules for now, handle separately + .replace(/.*?<\/flavorText>/gs, '') // Remove flavor for now, handle separately + .trim() + + return effects +} + +/** + * Parse effects from description HTML + */ +function parseEffects(description: string): ItemEffect[] { + const effects: ItemEffect[] = [] + + if (!description) return effects + + // Extract the effects section (after stats) + const effectsSection = extractEffectsSection(description) + + // Split by
tags to process line by line + const lines = effectsSection + .split(//gi) + .map(l => l.trim()) + .filter(l => l) + + let currentEffect: ItemEffect | null = null + + for (const line of lines) { + if (!line) continue + + // Check for passive tag + const passiveMatch = line.match(/([^<]*)<\/passive>/i) + if (passiveMatch) { + if (currentEffect) { + effects.push(currentEffect) + } + // Remove the passive tag from the line before parsing description + const lineWithoutTag = line.replace(/[^<]*<\/passive>/i, '').trim() + currentEffect = { + type: 'passive', + name: passiveMatch[1].trim(), + description: parseTextSegments(lineWithoutTag), + isUnique: false + } + continue + } + + // Check for active tag + const activeMatch = line.match(/([^<]*)<\/active>/i) + if (activeMatch) { + if (currentEffect) { + effects.push(currentEffect) + } + // Remove the active tag from the line before parsing description + const lineWithoutTag = line.replace(/[^<]*<\/active>/i, '').trim() + currentEffect = { + type: 'active', + name: activeMatch[1].trim(), + description: parseTextSegments(lineWithoutTag), + isUnique: false + } + continue + } + + // Check for unique tag + const uniqueMatch = line.match(/([^<]*)<\/unique>/i) + if (uniqueMatch) { + if (currentEffect) { + effects.push(currentEffect) + } + // Remove the unique tag from the line before parsing description + const lineWithoutTag = line.replace(/[^<]*<\/unique>/i, '').trim() + currentEffect = { + type: 'unique', + name: uniqueMatch[1].trim() || undefined, + description: parseTextSegments(lineWithoutTag), + isUnique: true + } + continue + } + + // Check for rarity tags + const mythicMatch = line.match(/([^<]*)<\/rarityMythic>/i) + if (mythicMatch) { + if (currentEffect) { + effects.push(currentEffect) + } + // Remove the rarity tag from the line before parsing description + const lineWithoutTag = line.replace(/[^<]*<\/rarityMythic>/i, '').trim() + currentEffect = { + type: 'mythic', + name: mythicMatch[1].trim() || undefined, + description: parseTextSegments(lineWithoutTag), + isUnique: false + } + continue + } + + const legendaryMatch = line.match(/([^<]*)<\/rarityLegendary>/i) + if (legendaryMatch) { + if (currentEffect) { + effects.push(currentEffect) + } + // Remove the rarity tag from the line before parsing description + const lineWithoutTag = line.replace(/[^<]*<\/rarityLegendary>/i, '').trim() + currentEffect = { + type: 'legendary', + name: legendaryMatch[1].trim() || undefined, + description: parseTextSegments(lineWithoutTag), + isUnique: false + } + continue + } + + // If we have a current effect, append this line to its description + if (currentEffect) { + const lineSegments = parseTextSegments(line) + currentEffect.description.push(...lineSegments) + } else if (line.trim()) { + // Standalone effect without explicit tag - create as passive + const segments = parseTextSegments(line) + if (segments.length > 0 && segments.some(s => s.content.trim())) { + currentEffect = { + type: 'passive', + description: segments, + isUnique: false + } + } + } + } + + // Don't forget the last effect + if (currentEffect) { + effects.push(currentEffect) + } + + return effects +} + +/** + * Parse rules section from description + */ +function parseRules(description: string): TextSegment[] | undefined { + const rulesMatch = description.match(/(.*?)<\/rules>/s) + if (!rulesMatch) return undefined + + return parseTextSegments(rulesMatch[1]) +} + +/** + * Parse flavor text section from description + */ +function parseFlavorText(description: string): TextSegment[] | undefined { + const flavorMatch = description.match(/(.*?)<\/flavorText>/s) + if (!flavorMatch) return undefined + + return parseTextSegments(flavorMatch[1]) +} + +/** + * Detect item rarity from description + */ +function detectRarity(description: string): 'mythic' | 'legendary' | 'epic' | undefined { + if (//i.test(description)) return 'mythic' + if (//i.test(description)) return 'legendary' + if (//i.test(description)) return 'epic' + return undefined +} + +/** + * Parse full item description into structured data + * + * @param description - The HTML description string from CDragon items.json + * @returns ParsedDescription object with stats, effects, rules, and flavor text + * + * @example + * ```ts + * const parsed = parseItemDescription( + * ' 25 Move Speed

Enhanced Movement: +25 Move Speed
' + * ) + * // Returns: { stats: { moveSpeed: 25 }, effects: [...], ... } + * ``` + */ +export function parseItemDescription(description: string): ParsedDescription { + return { + stats: parseItemStats(description), + effects: parseEffects(description), + rules: parseRules(description), + flavorText: parseFlavorText(description), + rarity: detectRarity(description) + } +} + /** * Item data structure from CDragon */ @@ -184,6 +696,13 @@ export interface ItemWithStats extends CDragonItem { stats: ItemStats } +/** + * Item with fully parsed description + */ +export interface ItemWithParsedDescription extends CDragonItem { + parsedDescription: ParsedDescription +} + /** * Parse a CDragon item and add parsed stats */ @@ -200,3 +719,20 @@ export function parseItem(item: CDragonItem): ItemWithStats { export function parseItems(items: CDragonItem[]): ItemWithStats[] { return items.map(parseItem) } + +/** + * Parse a CDragon item with full description parsing + */ +export function parseItemFull(item: CDragonItem): ItemWithParsedDescription { + return { + ...item, + parsedDescription: parseItemDescription(item.description) + } +} + +/** + * Parse an array of CDragon items with full description parsing + */ +export function parseItemsFull(items: CDragonItem[]): ItemWithParsedDescription[] { + return items.map(parseItemFull) +} diff --git a/dragon-item-parser/test/item.test.ts b/dragon-item-parser/test/item.test.ts index 21def79..393ba58 100644 --- a/dragon-item-parser/test/item.test.ts +++ b/dragon-item-parser/test/item.test.ts @@ -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 = + ' 75 Attack Damage
25% Critical Strike Chance


' + 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 = + ' 75 Attack Damage
25% Critical Strike Chance
30% Critical Strike Damage


' + 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 = + ' 130 Ability Power

Magical Opus
Increases your total Ability Power by 30%.
' + 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 = + ' 105 Ability Power
50 Armor




Time Stop
Enter Stasis for 2.5 seconds.
' + 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 = + ' 36 Attack Damage
30% Attack Speed
333 Health
15 Ability Haste


Spellblade
After using an Ability, your next Attack deals bonus physical damage On-Hit.

Quicken
Attacking grants 20 Move Speed for 2 seconds.
' + 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 = + ' 45 Attack Damage
35% Armor Penetration
15 Ability Haste


Bitter Cold
Damaging Abilities Slow enemies below 50% Health by 30% for 1 second.
' + 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 = + ' 75 Attack Damage
25% Critical Strike Chance
30% Critical Strike Damage


' + const parsed = parseItemDescription(description) + + expect(parsed.effects).toHaveLength(0) + }) + }) + + describe('text segments', () => { + it('should parse scaleAP tag', () => { + const description = + ' 130 Ability Power

Magical Opus
Increases your total Ability Power by 30%.
' + 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 = + ' 36 Attack Damage

Spellblade
Deals bonus physical damage.
' + 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 = + ' 105 Ability Power

Time Stop
Enter Stasis for 2.5 seconds.
' + 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 = + ' 36 Attack Damage

Quicken
Grants 20 Move Speed.
' + 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 = + ' 45 Attack Damage

Bitter Cold
Slow enemies.
' + 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 = + ' 65 Attack Damage

Mythic
' + const parsed = parseItemDescription(description) + + expect(parsed.rarity).toBe('mythic') + }) + + it('should detect legendary rarity', () => { + const description = + ' 65 Attack Damage

Legendary
' + const parsed = parseItemDescription(description) + + expect(parsed.rarity).toBe('legendary') + }) + + it('should detect epic rarity', () => { + const description = + ' 65 Attack Damage

Epic
' + const parsed = parseItemDescription(description) + + expect(parsed.rarity).toBe('epic') + }) + }) + + describe('rules and flavor text', () => { + it('should parse rules section', () => { + const description = + ' 25 Move Speed

Ornn upgrade only
' + const parsed = parseItemDescription(description) + + expect(parsed.rules).toBeDefined() + expect(parsed.rules?.length).toBeGreaterThan(0) + }) + + it('should parse flavor text section', () => { + const description = + ' 25 Move Speed

A ancient artifact
' + 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: + ' 130 Ability Power

Magical Opus
Increases your total Ability Power by 30%.
', + 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') + }) +}) diff --git a/frontend/assets/css/main.css b/frontend/assets/css/main.css index 8049550..d1bc11c 100644 --- a/frontend/assets/css/main.css +++ b/frontend/assets/css/main.css @@ -5,6 +5,40 @@ --color-on-surface: #b7b8e1; --color-surface-darker: #1f1d1c; --color-gold: #ffd700; + + /* Tooltip colors */ + --tooltip-bg: #312e2c; + --tooltip-border: #4a4543; + --tooltip-header-border: rgba(183, 184, 225, 0.2); + --tooltip-text: #b7b8e1; + --tooltip-text-dim: #8a8b9e; + --tooltip-stat-value: #ffd700; + --tooltip-stat-label: #b7b8e1; + + /* Effect type colors */ + --tooltip-effect-passive: #4a9eff; + --tooltip-effect-active: #ff6b6b; + --tooltip-effect-unique: #f39c12; + --tooltip-effect-mythic: #ff5252; + --tooltip-effect-legendary: #ff9800; + --tooltip-effect-epic: #ffd54f; + + /* Text segment colors */ + --tooltip-highlight: #ffd700; + --tooltip-keyword: #ffd700; + --tooltip-keyword-major: #ff8c00; + --tooltip-keyword-stealth: #9b59b6; + --tooltip-status: #e74c3c; + --tooltip-speed: #5dade2; + --tooltip-scaling: #5dade2; + --tooltip-magic-damage: #9b59b6; + --tooltip-physical-damage: #e67e22; + --tooltip-true-damage: #ffffff; + --tooltip-healing: #2ecc71; + --tooltip-shield: #3498db; + --tooltip-onhit: #5dade2; + --tooltip-spellname: #1abc9c; + --tooltip-flavor: #6a9fff; } /* Font setting */ diff --git a/frontend/components/item/ItemTooltip.vue b/frontend/components/item/ItemTooltip.vue index fecd89d..a0cdc02 100644 --- a/frontend/components/item/ItemTooltip.vue +++ b/frontend/components/item/ItemTooltip.vue @@ -1,5 +1,11 @@