/** * Diablo II Item Tooltip and Formatted Attribute Generator. * * Formats dropped Item instances into rich, canonical Diablo II tooltip data: * - Quality colors and tiered title (Unique gold, Set green, Rare yellow, Magic blue, Rune orange, Gem purple) * - Base equipment stats (Defense, 1H/2H Damage, Speed, Durability, Required Str/Dex/Level, ilvl) * - Rolled magic modifiers, affix lines, and unique/set fixed modifiers formatted with authentic D2 strings * - Sockets, rune effects, and gem socketing bonuses */ import type { Item, ItemBase } from './items.ts' import type { UniqueItem, UniqueItemProp } from './unique-items.ts' import type { SetItem, SetItemProp } from './set-items.ts' import type { GeneratedAffixes, GeneratedRareAffixes, RolledAffix, RolledMod } from './affix-generator.ts' export interface FormattedStatLine { text: string color?: 'blue' | 'white' | 'gold' | 'green' | 'orange' | 'yellow' | 'red' | 'gray' } export interface FormattedItemTooltip { title: string subTitle?: string | undefined quality: string qualityColor: string baseType: string ilvl: number reqLevel?: number | undefined reqStr?: number | undefined reqDex?: number | undefined defense?: number | undefined oneHandDamage?: { min: number; max: number } | undefined twoHandDamage?: { min: number; max: number } | undefined durability?: { current: number; max: number } | undefined sockets?: number | undefined lines: FormattedStatLine[] setPieces?: string[] | undefined setBonuses?: FormattedStatLine[] | undefined flavorText?: string | undefined } // Canonical Chinese Translations for Common Base Names const BASE_NAMES_CN: Record = { // Common Boss Drops & Favorites uap: '军帽 (Shako)', xap: '战帽 (War Hat)', cap: '便帽 (Cap)', uit: '统治者大盾 (Monarch)', xit: '冷酷之盾 (Grim Shield)', kit: '轻盾 (Kite Shield)', uui: '海蛇皮甲 (Serpentskin Armor)', xui: '鬼魂战甲 (Ghost Armor)', qui: '皮甲 (Leather Armor)', aar: '古代装甲 (Ancient Armor)', uar: '神圣盔甲 (Sacred Armor)', xar: '法师铠甲 (Mage Plate)', ful: '全铠甲 (Full Plate Mail)', gth: '哥特战甲 (Gothic Plate)', rin: '戒指 (Ring)', amu: '项链 (Amulet)', jew: '珠宝 (Jewel)', cm1: '小型护身符 (Small Charm)', cm2: '大型护身符 (Large Charm)', cm3: '超大型护身符 (Grand Charm)', ob4: '涡流水晶 (Swirling Crystal)', '7cr': '幻化之刃 (Phase Blade)', crs: '水晶剑 (Crystal Sword)', '9cr': '空间之刃 (Dimensional Blade)', '7wa': '狂战士斧 (Berserker Axe)', hax: '手斧 (Hand Axe)', '9ha': '长柄斧 (Hatchet)', '7gd': '巨神之刃 (Colossus Blade)', gsd: '巨剑 (Great Sword)', '7fl': '连枷 (Flail)', fla: '连枷 (Flail)', '9fl': '铁刺锤 (Knout)', '7ws': '天罚之锤 (Scourge)', gld: '金币 (Gold)', tsc: '城镇传送卷轴 (Town Portal Scroll)', isc: '辨识卷轴 (Identify Scroll)', hp1: '微型生命药剂', hp2: '轻型生命药剂', hp3: '中型生命药剂', hp4: '强效生命药剂', hp5: '超级生命药剂', mp1: '微型法力药剂', mp2: '轻型法力药剂', mp3: '中型法力药剂', mp4: '强效法力药剂', mp5: '超级法力药剂', rvs: '恢复药剂 (35%)', rvl: '全面恢复药剂 (100%)', aqv: '箭矢 (Arrows)', cqv: '十字弓弹 (Bolts)', key: '钥匙 (Key)', } // Rune Names & Socket Effects const RUNE_INFO: Record = { r01: { num: 1, nameCn: '艾尔 (El)', nameEn: 'El Rune', lvl: 11, weapon: '+50 命中率, +1 点照亮范围', armor: '+15 防御, +1 点照亮范围' }, r02: { num: 2, nameCn: '艾德 (Eld)', nameEn: 'Eld Rune', lvl: 11, weapon: '+75% 对不死生物伤害, +50 针对不死生物命中率', armor: '降低体力消耗 15% (盾牌: +7% 格挡率)' }, r03: { num: 3, nameCn: '特尔 (Tir)', nameEn: 'Tir Rune', lvl: 13, weapon: '+2 点法力在每杀一个敌人后获得', armor: '+2 点法力在每杀一个敌人后获得' }, r04: { num: 4, nameCn: '那夫 (Nef)', nameEn: 'Nef Rune', lvl: 13, weapon: '击退目标 (Knockback)', armor: '+30 对远距离攻击防御' }, r05: { num: 5, nameCn: '爱斯 (Eth)', nameEn: 'Eth Rune', lvl: 15, weapon: '-25% 目标防御', armor: '法力回复速度 +15%' }, r06: { num: 6, nameCn: '伊夫 (Ith)', nameEn: 'Ith Rune', lvl: 15, weapon: '+9 最大伤害', armor: '15% 受损生命转化为法力' }, r07: { num: 7, nameCn: '塔尔 (Tal)', nameEn: 'Tal Rune', lvl: 17, weapon: '+75 毒素伤害持续 5 秒', armor: '抗毒 +30% (盾牌: 抗毒 +35%)' }, r08: { num: 8, nameCn: '拉尔 (Ral)', nameEn: 'Ral Rune', lvl: 19, weapon: '增加 5-30 火焰伤害', armor: '抗火 +30% (盾牌: 抗火 +35%)' }, r09: { num: 9, nameCn: '欧特 (Ort)', nameEn: 'Ort Rune', lvl: 21, weapon: '增加 1-50 闪电伤害', armor: '抗电 +30% (盾牌: 抗电 +35%)' }, r10: { num: 10, nameCn: '书尔 (Thul)', nameEn: 'Thul Rune', lvl: 23, weapon: '增加 3-14 冰冷伤害 (持续 3 秒)', armor: '抗寒 +30% (盾牌: 抗寒 +35%)' }, r11: { num: 11, nameCn: '安姆 (Amn)', nameEn: 'Amn Rune', lvl: 25, weapon: '每次命中偷取 7% 生命', armor: '攻击者受到 14 点反弹伤害' }, r12: { num: 12, nameCn: '索尔 (Sol)', nameEn: 'Sol Rune', lvl: 27, weapon: '+9 最小伤害', armor: '物理伤害减少 7' }, r13: { num: 13, nameCn: '夏 (Shael)', nameEn: 'Shael Rune', lvl: 29, weapon: '20% 提升攻击速度 (IAS)', armor: '20% 快速打击恢复 (盾牌: 20% 快速格挡率)' }, r14: { num: 14, nameCn: '多尔 (Dol)', nameEn: 'Dol Rune', lvl: 31, weapon: '击中使怪物逃跑 (25%)', armor: '生命恢复 +7' }, r15: { num: 15, nameCn: '海尔 (Hel)', nameEn: 'Hel Rune', lvl: 0, weapon: '装备需求 -20%', armor: '装备需求 -15%' }, r16: { num: 16, nameCn: '埃欧 (Io)', nameEn: 'Io Rune', lvl: 35, weapon: '+10 体力', armor: '+10 体力' }, r17: { num: 17, nameCn: '卢姆 (Lum)', nameEn: 'Lum Rune', lvl: 37, weapon: '+10 精力', armor: '+10 精力' }, r18: { num: 18, nameCn: '科 (Ko)', nameEn: 'Ko Rune', lvl: 39, weapon: '+10 敏捷', armor: '+10 敏捷' }, r19: { num: 19, nameCn: '法尔 (Fal)', nameEn: 'Fal Rune', lvl: 41, weapon: '+10 力量', armor: '+10 力量' }, r20: { num: 20, nameCn: '蓝姆 (Lem)', nameEn: 'Lem Rune', lvl: 43, weapon: '75% 额外金币从怪物身上获得', armor: '50% 额外金币从怪物身上获得' }, r21: { num: 21, nameCn: '普尔 (Pul)', nameEn: 'Pul Rune', lvl: 45, weapon: '+75% 对恶魔伤害, +100 对恶魔命中率', armor: '+30% 增强防御' }, r22: { num: 22, nameCn: '乌姆 (Um)', nameEn: 'Um Rune', lvl: 47, weapon: '25% 几率造成伤口流血 (OW)', armor: '所有抗性 +15 (盾牌: 所有抗性 +22)' }, r23: { num: 23, nameCn: '马尔 (Mal)', nameEn: 'Mal Rune', lvl: 49, weapon: '防止怪物自愈', armor: '魔法伤害减少 7' }, r24: { num: 24, nameCn: '伊司特 (Ist)', nameEn: 'Ist Rune', lvl: 51, weapon: '30% 更加寻获魔法装备 (MF)', armor: '25% 更加寻获魔法装备 (MF)' }, r25: { num: 25, nameCn: '古尔 (Gul)', nameEn: 'Gul Rune', lvl: 53, weapon: '20% 额外攻击命中率', armor: '+5% 最大毒素抗性' }, r26: { num: 26, nameCn: '伐克斯 (Vex)', nameEn: 'Vex Rune', lvl: 55, weapon: '每次命中偷取 7% 法力', armor: '+5% 最大火焰抗性' }, r27: { num: 27, nameCn: '欧姆 (Ohm)', nameEn: 'Ohm Rune', lvl: 57, weapon: '+50% 增强伤害 (ED)', armor: '+5% 最大冰冷抗性' }, r28: { num: 28, nameCn: '罗 (Lo)', nameEn: 'Lo Rune', lvl: 59, weapon: '20% 几率造成双倍打击 (DS)', armor: '+5% 最大闪电抗性' }, r29: { num: 29, nameCn: '瑟 (Sur)', nameEn: 'Sur Rune', lvl: 61, weapon: '击中使目标目盲', armor: '最大法力提升 5% (盾牌: +50 法力)' }, r30: { num: 30, nameCn: '贝 (Ber)', nameEn: 'Ber Rune', lvl: 63, weapon: '20% 几率产生压碎打击 (CB)', armor: '物理伤害减少 8% (DR)' }, r31: { num: 31, nameCn: '乔 (Jah)', nameEn: 'Jah Rune', lvl: 65, weapon: '无视目标防御力 (ITD)', armor: '最大生命提升 5% (盾牌: +50 生命)' }, r32: { num: 32, nameCn: '查姆 (Cham)', nameEn: 'Cham Rune', lvl: 67, weapon: '击中使目标冻结 +3', armor: '无法被冻结 (Cannot be Frozen)' }, r33: { num: 33, nameCn: '萨德 (Zod)', nameEn: 'Zod Rune', lvl: 69, weapon: '无法破坏 (Indestructible)', armor: '无法破坏 (Indestructible)' }, } // Property Code Formatter export function formatPropertyCode(code: string, min: number, max: number, par?: string | number): string { const isRange = max > min const valStr = isRange ? `${min}-${max}` : `${min}` const plusVal = min > 0 ? `+${valStr}` : `${valStr}` switch (code.toLowerCase()) { case 'allskills': return `${plusVal} 所有技能` case 'skilltab': return `${plusVal} 技能分类 (${par ?? ''})` case 'classskills': return `${plusVal} 职业技能 (${par ?? ''})` case 'skill': return `${plusVal} 到技能 [${par ?? ''}]` case 'str': return `${plusVal} 力量` case 'dex': return `${plusVal} 敏捷` case 'vit': return `${plusVal} 体力` case 'enr': return `${plusVal} 精力` case 'all-stats': return `${plusVal} 所有属性` case 'hp': return `${plusVal} 生命` case 'mana': return `${plusVal} 法力` case 'hp/lvl': return `+${(min / 8).toFixed(1)}~${((min / 8) * 99).toFixed(0)} 生命 (依角色等级决定)` case 'mana/lvl': return `+${(min / 8).toFixed(1)}~${((min / 8) * 99).toFixed(0)} 法力 (依角色等级决定)` case 'ac': return `${plusVal} 防御力` case 'ac%': return `${plusVal}% 增强防御` case 'dmg%': return `${plusVal}% 增强伤害 (ED)` case 'dmg-min': return `+${valStr} 最小伤害值` case 'dmg-max': return `+${valStr} 最大伤害值` case 'res-all': return `所有抗性 ${plusVal}%` case 'res-fire': case 'fireresist': return `抗火 ${plusVal}%` case 'res-cold': case 'coldresist': return `抗寒 ${plusVal}%` case 'res-ltng': case 'lightresist': return `抗电 ${plusVal}%` case 'res-pois': case 'poisonresist': return `抗毒 ${plusVal}%` case 'fcr': return `${plusVal}% 快速施法速度 (FCR)` case 'fhr': return `${plusVal}% 快速打击恢复 (FHR)` case 'ias': return `${plusVal}% 提升攻击速度 (IAS)` case 'frw': return `${plusVal}% 快速移动/奔跑 (FRW)` case 'fbr': return `${plusVal}% 快速格挡率 (FBR)` case 'mag%': return `${plusVal}% 更加寻获魔法装备 (MF)` case 'gold%': return `${plusVal}% 额外金币从怪物身上获得` case 'lifesteal': case 'leech': return `每次击中偷取 ${valStr}% 生命` case 'manasteal': return `每次击中偷取 ${valStr}% 法力` case 'crush': return `${valStr}% 几率产生压碎打击 (CB)` case 'openwounds': return `${valStr}% 几率造成伤口流血 (OW)` case 'deadly': return `${valStr}% 几率造成双倍打击 (DS)` case 'dmag': return `伤害减少 ${valStr}% (物免)` case 'dmag-ac': return `物理伤害减少 ${valStr}` case 'mag-ac': return `魔法伤害减少 ${valStr}` case 'sock': return `凹槽 (${valStr})` case 'indestruct': return `无法破坏` case 'no-freeze': case 'nofreeze': return `无法被冻结` case 'half-freeze': return `冻结时间减半` case 'rep-life': case 'regen': return `生命恢复 +${valStr}` case 'regen-mana': case 'manaregen': return `法力恢复速度 +${valStr}%` case 'pierce': return `穿透攻击 (${valStr}%)` case 'ignore-ac': return `无视目标防御力` case 'freeze': return `击中使目标冰冻 +${valStr}` case 'slow': return `使目标减速 ${valStr}%` case 'att': return `${plusVal} 攻击命中率` case 'att%': return `${plusVal}% 额外攻击命中率` case 'dmg-fire': return `增加 ${min}-${max} 点火焰伤害` case 'dmg-cold': return `增加 ${min}-${max} 点冰冷伤害` case 'dmg-ltng': return `增加 ${min}-${max} 点闪电伤害` case 'dmg-pois': return `增加 ${min} 点毒素伤害,持续 ${max} 秒` case 'dmg-mag': return `增加 ${min}-${max} 点魔法伤害` default: return `${code}: ${plusVal}${par ? ` (${par})` : ''}` } } /** * Generates formatted tooltip structure for any dropped Diablo II Item. */ export function formatItemTooltip(item: Item): FormattedItemTooltip { const base = item.base const quality = (item.rarity || 'normal').toLowerCase() const ilvl = item.level ?? item.ilvl ?? 1 let title = item.name let subTitle: string | undefined = undefined let qualityColor: string = '#ffffff' const lines: FormattedStatLine[] = [] // Check if this is a Rune const isRune = item.code?.trim().startsWith('r') && Number(item.code.trim().slice(1)) >= 1 && Number(item.code.trim().slice(1)) <= 33 const runeKey = item.code?.trim().toLowerCase() const runeData = isRune && runeKey ? RUNE_INFO[runeKey] : undefined if (runeData) { title = `${runeData.num}# ${runeData.nameCn}` subTitle = runeData.nameEn qualityColor = '#ff8c00' lines.push({ text: `可以镶嵌在有凹槽的装备中`, color: 'white' }) lines.push({ text: `武器: ${runeData.weapon}`, color: 'blue' }) lines.push({ text: `防具/盾牌: ${runeData.armor}`, color: 'blue' }) return { title, subTitle, quality: '符文 (Rune)', qualityColor, baseType: '符文', ilvl, reqLevel: runeData.lvl > 0 ? runeData.lvl : undefined, lines, } } // Check if this is Gold if (base.id === 'gld' || item.name.toLowerCase().includes('gold')) { title = `${(item.stats.amount ?? item.value ?? 100).toLocaleString()} 金币 (Gold)` qualityColor = '#ffd700' return { title, quality: '金币', qualityColor, baseType: '金币', ilvl: 1, lines: [{ text: '可用于向城镇商贩交易、赌博或雇佣兵复活', color: 'white' }], } } // Resolve Base Chinese Name const baseNameCn = BASE_NAMES_CN[base.id] ?? base.name // Determine Quality Presentation if (quality === 'unique') { qualityColor = '#c8a15a' const uniqDef = item.uniqueItemDef as UniqueItem | undefined if (uniqDef) { title = uniqDef.index subTitle = `${baseNameCn}` // Add Unique Properties for (const prop of uniqDef.props) { if (prop.code) { lines.push({ text: formatPropertyCode(prop.code, prop.min, prop.max, prop.par), color: 'blue', }) } } } else { subTitle = baseNameCn } } else if (quality === 'set') { qualityColor = '#00e600' const setDef = item.setItemDef as SetItem | undefined if (setDef) { title = setDef.index subTitle = `${baseNameCn} · [${setDef.set}]` for (const prop of setDef.props) { if (prop.code) { lines.push({ text: formatPropertyCode(prop.code, prop.min, prop.max, prop.par), color: 'blue', }) } } } else { subTitle = baseNameCn } } else if (quality === 'rare') { qualityColor = '#ffff60' subTitle = baseNameCn const rareDef = item.rolledRareAffixes as GeneratedRareAffixes | undefined if (rareDef && rareDef.affixes) { title = rareDef.name for (const aff of rareDef.affixes) { for (const mod of aff.mods) { lines.push({ text: formatPropertyCode(mod.code, mod.min, mod.max), color: 'blue', }) } } } } else if (quality === 'magic') { qualityColor = '#4880ff' const magicDef = item.rolledMagicAffixes as GeneratedAffixes | undefined if (magicDef) { title = magicDef.name subTitle = baseNameCn if (magicDef.prefix) { for (const m of magicDef.prefix.mods) { lines.push({ text: formatPropertyCode(m.code, m.min, m.max), color: 'blue' }) } } if (magicDef.suffix) { for (const m of magicDef.suffix.mods) { lines.push({ text: formatPropertyCode(m.code, m.min, m.max), color: 'blue' }) } } } else { subTitle = baseNameCn } } else if (quality === 'superior') { qualityColor = '#ffffff' title = `超强的 ${baseNameCn}` lines.push({ text: '+15% 增强防御 / +15% 增强伤害', color: 'blue' }) } else { qualityColor = '#e0d5c1' title = baseNameCn } // Equipment Base Stats const reqLevel = (base as any).levelreq > 0 ? (base as any).levelreq : undefined const reqStr = (base as any).reqstr > 0 ? (base as any).reqstr : undefined const reqDex = (base as any).reqdex > 0 ? (base as any).reqdex : undefined const defense = base.defense > 0 ? (item.stats.defense ?? base.defense) : undefined const oneHandDamage = (base as any).mindam && (base as any).maxdam ? { min: (base as any).mindam, max: (base as any).maxdam } : undefined const twoHandDamage = (base as any).twoHandedMindam && (base as any).twoHandedMaxdam ? { min: (base as any).twoHandedMindam, max: (base as any).twoHandedMaxdam } : undefined const durability = item.durability && item.maxDurability ? { current: item.durability, max: item.maxDurability } : undefined const sockets = (base as any).gemsockets > 0 ? (base as any).gemsockets : undefined return { title, subTitle, quality, qualityColor, baseType: base.kind === 'weapon' ? '武器' : base.kind === 'armor' ? '防具' : '杂项/首饰', ilvl, reqLevel, reqStr, reqDex, defense, oneHandDamage, twoHandDamage, durability, sockets, lines, } }