双手盾装备DPS对比工具
// ==UserScript== // @name MWI-Bulwark-Equipment-Diff // @namespace http://tampermonkey.net/ // @version 1.6.0 // @description 双手盾装备DPS对比工具 // @author wangchyan // @match https://*.milkywayidle.com/* // @match https://*.milkywayidlecn.com/* // @icon https://www.google.com/s2/favicons?sz=64&domain=milkywayidle.com // @run-at document-start // @require https://cdn.jsdelivr.net/npm/[email protected]/libs/lz-string.min.js // @grant GM_getValue // @license MIT // ==/UserScript== (function() { 'use strict'; let selfData = {}; let importedStats = null; // 存储导入的玩家属性(用于动态系数计算) let importedEquipmentStats = {}; // 存储导入时各位置装备的词条数据 let importedBaseValues = {}; // 存储导入时各属性的基础值(不含装备加成) let importedTotalValues = {}; // 存储导入时各属性的总值(用于固定增益转相对值) let currentEquipmentMap = {}; // 存储当前装备item信息 let itemDetailMap = {}; // 存储游戏物品详细信息 let isButtonInSuccessState = false; const colors = { info: 'rgb(0, 108, 158)', smaller: 'rgb(199, 21, 21)', greater: 'rgb(23, 151, 12)', disabled: 'rgb(128, 128, 128)', success: 'rgb(34, 197, 94)', }; // 强化加成倍数表(索引=强化等级,值=加成倍数) const ENHANCEMENT_MULTIPLIER_TABLE = [ 0, 1, 2.1, 3.3, 4.6, 6, 7.5, 9.1, 10.8, 12.6, 14.5, 16.7, 19.2, 22, 25.1, 28.5, 32.2, 36.2, 40.5, 45.1, 50 ]; // 动态检查当前语言设置 function isZH() { return localStorage.getItem("i18nextLng")?.toLowerCase()?.startsWith("zh") || false; } // 双手盾DPS模型权重 const DPS_WEIGHTS = { autoAttack: 0.40, retaliation: 0.30, thorns: 0.20, other: 0.10 }; // 论文中提到的属性列表及其收益系数 // type: 'percent' = 比例增益(+x%),直接用百分比差值 // type: 'flat' = 固定增益(+x),需要转换为相对基础值的百分比 const PAPER_STATS = { '物理增幅': { en: 'Physical Amplify', coefficient: 0.50, dynamic: false, type: 'percent' }, '防御伤害': { en: 'Defensive Damage', coefficient: 0.40, dynamic: true, type: 'percent' }, '自动攻击伤害': { en: 'Auto Attack Damage', coefficient: 0.40, dynamic: false, type: 'percent' }, '攻击速度': { en: 'Attack Speed', coefficient: 0.40, dynamic: false, type: 'percent' }, '钝击精准度': { en: 'Smash Accuracy', coefficient: 0.40, dynamic: false, type: 'percent' }, '护甲': { en: 'Armor', coefficient: 0.20, dynamic: false, type: 'flat' }, '暴击率': { en: 'Critical Rate', coefficient: 0.20, dynamic: false, type: 'percent' }, '最大MP': { en: 'Max Manapoints', coefficient: 0.02, dynamic: false, type: 'absolute' }, '钝击伤害': { en: 'Smash Damage', coefficient: 0.10, dynamic: true, type: 'percent' }, }; // 装备位置映射 const EQUIPMENT_SLOTS = { '/item_locations/head': 'head', '/item_locations/body': 'body', '/item_locations/legs': 'legs', '/item_locations/feet': 'feet', '/item_locations/hands': 'hands', '/item_locations/main_hand': 'main_hand', '/item_locations/off_hand': 'off_hand', '/item_locations/two_hand': 'two_hand', '/item_locations/neck': 'neck', '/item_locations/earrings': 'earrings', '/item_locations/ring': 'ring', '/item_locations/back': 'back', '/item_locations/pouch': 'pouch', '/item_locations/trinket': 'trinket', }; function transZH(zh) { if (isZH()) return zh; return { "战斗风格": "Combat Style", "伤害类型": "Damage Type", "自动攻击伤害": "Auto Attack Damage", "攻击速度": "Attack Speed", "攻击间隔": "Attack Interval", "暴击率": "Critical Rate", "钝击精准度": "Smash Accuracy", "钝击伤害": "Smash Damage", "物理增幅": "Physical Amplify", "防御伤害": "Defensive Damage", "护甲": "Armor", "最大MP": "Max Manapoints", }[zh] || zh; } function getStatZHName(key) { if (PAPER_STATS[key]) return key; for (const zhName in PAPER_STATS) { if (PAPER_STATS[zhName].en === key || PAPER_STATS[zhName].en.toLowerCase() === key.toLowerCase()) { return zhName; } } return null; } function isStatInPaper(key) { return getStatZHName(key) !== null; } // 检查是否装备了双手盾 function isBulwarkEquipped() { const twoHandItem = currentEquipmentMap['/item_locations/two_hand']; if (!twoHandItem) return false; const itemHrid = (twoHandItem.itemHrid || '').toLowerCase(); return itemHrid.includes('bulwark'); } // 检查itemHrid是否是双手盾 function isBulwarkItem(itemHrid) { if (!itemHrid) return false; return itemHrid.toLowerCase().includes('bulwark'); } // 获取装备位置类型 function getEquipmentSlot(itemHrid) { if (!itemHrid || !itemDetailMap[itemHrid]) return null; const detail = itemDetailMap[itemHrid]; return detail?.equipmentDetail?.type || null; } // 获取技能等级 function getSkillLevel(skillName, skillMap) { if (!skillMap) return 0; for (const skill of skillMap) { if (skill.skillHrid === skillName) { return skill.level; } } return 0; } // 获取玩家当前属性用于动态系数计算 function getPlayerStats() { if (!selfData || !selfData.combatUnit) return null; const combatDetails = selfData.combatUnit.combatDetails; const combatStats = combatDetails?.combatStats || {}; const defenseLevel = getSkillLevel('/skills/defense', selfData.characterSkills); const meleeLevel = getSkillLevel('/skills/attack', selfData.characterSkills); const defensiveDamageBonus = combatStats.defensiveDamage || 0; const smashDamageBonus = combatStats.smashDamage || 0; const S = (10 + meleeLevel) * (1 + smashDamageBonus); const D = (10 + defenseLevel) * (1 + defensiveDamageBonus); const totalSmashDamage = S + D; return { defenseLevel, meleeLevel, defensiveDamageBonus, smashDamageBonus, totalSmashDamage, }; } // 计算装备的属性(从itemDetailMap获取基础属性,根据强化等级计算实际属性) // 公式:最终属性 = 基础属性 + 强化倍数 × 强化加成基础值 function getEquipmentStats(itemHrid, enhancementLevel) { if (!itemHrid || !itemDetailMap[itemHrid]) return {}; const detail = itemDetailMap[itemHrid]; const equipDetail = detail?.equipmentDetail; if (!equipDetail) return {}; const stats = {}; const combatStats = equipDetail.combatStats || {}; const combatEnhancementBonuses = equipDetail.combatEnhancementBonuses || {}; // 获取强化倍数 const level = Math.min(Math.max(enhancementLevel || 0, 0), 20); const multiplier = ENHANCEMENT_MULTIPLIER_TABLE[level] || 0; // 提取战斗属性 for (const [key, value] of Object.entries(combatStats)) { if (value !== undefined && value !== null && typeof value === 'number') { const zhName = combatStatKeyToZHName(key); if (zhName && isStatInPaper(zhName)) { // 计算强化后的属性:基础属性 + 强化倍数 × 强化加成基础值 const enhancementBonus = combatEnhancementBonuses[key] || 0; const finalValue = value + multiplier * enhancementBonus; // 根据属性类型决定是否转换为百分比格式 const statInfo = PAPER_STATS[zhName]; if (statInfo && (statInfo.type === 'flat' || statInfo.type === 'absolute')) { // flat/absolute 类型属性(如护甲、最大MP)直接使用实际值 stats[zhName] = finalValue; } else { // 百分比类型属性,转为百分比格式(游戏数据是小数形式) stats[zhName] = finalValue * 100; } } } } return stats; } // 将combatStats的key转换为中文名称(内部使用) function combatStatKeyToZHName(key) { const mapping = { 'physicalAmplify': '物理增幅', 'defensiveDamage': '防御伤害', 'autoAttackDamage': '自动攻击伤害', 'attackSpeed': '攻击速度', 'smashAccuracy': '钝击精准度', 'armor': '护甲', 'criticalRate': '暴击率', 'maxManapoints': '最大MP', 'smashDamage': '钝击伤害', }; return mapping[key] || null; } // 将combatStats的key转换为显示名称(根据当前语言) function combatStatKeyToName(key) { const zhName = combatStatKeyToZHName(key); if (!zhName) return null; return isZH() ? zhName : (PAPER_STATS[zhName]?.en || null); } // 导入当前属性(直接从游戏数据计算装备属性) function importAllEquipmentStats() { importedEquipmentStats = {}; importedTotalValues = {}; // 确保 itemDetailMap 已加载 if (Object.keys(itemDetailMap).length === 0) { loadGameData(); } // 获取角色等级(用于基础值稀释计算) const defenseLevel = getSkillLevel('/skills/defense', selfData?.characterSkills); const meleeLevel = getSkillLevel('/skills/attack', selfData?.characterSkills); const attackLevel = getSkillLevel('/skills/attack', selfData?.characterSkills); // 1. 获取当前总属性值(从combatStats) const combatStats = selfData?.combatUnit?.combatDetails?.combatStats || {}; // 记录当前总值 for (const [key, value] of Object.entries(combatStats)) { const zhName = combatStatKeyToZHName(key); if (zhName && isStatInPaper(zhName)) { const statInfo = PAPER_STATS[zhName]; if (statInfo && (statInfo.type === 'flat' || statInfo.type === 'absolute')) { // flat/absolute 类型属性(如护甲、最大MP)直接使用实际值 importedTotalValues[zhName] = value || 0; } else { // 百分比类型属性,转为百分比格式 importedTotalValues[zhName] = (value || 0) * 100; } } } // 2. 等级基础值稀释: // 防御伤害公式:D = (10 + 防御等级) × (1 + 防御伤害词条),基础 "1" 需要加入 // 钝击伤害公式:S = (10 + 近战等级) × (1 + 钝击伤害词条),基础 "1" 需要加入 // 攻击速度:每级提升 0.05%,需要加入等级贡献 if (importedTotalValues['防御伤害'] !== undefined) { importedTotalValues['防御伤害'] += 100; // 加上基础 1(100%) } if (importedTotalValues['钝击伤害'] !== undefined) { importedTotalValues['钝击伤害'] += 100; // 加上基础 1(100%) } if (importedTotalValues['攻击速度'] !== undefined) { importedTotalValues['攻击速度'] += attackLevel * 0.05; // 加上等级贡献 } // 3. 直接从数据计算每件装备的属性(使用正确的强化公式) for (const [location, item] of Object.entries(currentEquipmentMap)) { if (!item || !item.itemHrid) continue; const stats = getEquipmentStats(item.itemHrid, item.enhancementLevel); if (Object.keys(stats).length > 0) { importedEquipmentStats[location] = stats; } } // console.log('Bulwark Diff: Total values:', importedTotalValues); // console.log('Bulwark Diff: Equipment stats:', importedEquipmentStats); } // 获取属性的DPS收益系数 function getStatCoefficient(statKey, stats) { const zhName = getStatZHName(statKey); if (!zhName) return null; const statInfo = PAPER_STATS[zhName]; if (statInfo.dynamic) { if (zhName === '防御伤害') { const currentDefDmg = stats.defensiveDamageBonus || 0; const autoAttackContrib = (10 + stats.defenseLevel) / stats.totalSmashDamage; const thornsContrib = 1 / (1 + currentDefDmg); const weighted = autoAttackContrib * (DPS_WEIGHTS.autoAttack + DPS_WEIGHTS.other) + thornsContrib * (DPS_WEIGHTS.retaliation + DPS_WEIGHTS.thorns); return weighted; } if (zhName === '钝击伤害') { const contrib = (10 + stats.meleeLevel) / stats.totalSmashDamage; return contrib * (DPS_WEIGHTS.autoAttack + DPS_WEIGHTS.other); } } return statInfo.coefficient; } // 创建导入按钮 function createImportButton() { const buttonContainer = document.querySelector('[class*="EquipmentPanel_buttonContainer"]'); if (!buttonContainer) return; if (buttonContainer.querySelector('.BulwarkImportButton')) return; const originalButton = buttonContainer.querySelector('button'); if (!originalButton) return; const importButton = originalButton.cloneNode(true); importButton.classList.add('BulwarkImportButton'); importButton.style.marginLeft = '8px'; importButton.style.display = 'inline-flex'; importButton.onclick = null; buttonContainer.style.display = 'flex'; buttonContainer.style.flexWrap = 'wrap'; buttonContainer.style.gap = '8px'; buttonContainer.style.alignItems = 'center'; updateButtonDisplay(importButton); importButton.addEventListener('click', (e) => { e.preventDefault(); e.stopPropagation(); if (!isBulwarkEquipped() || isButtonInSuccessState) return; const stats = getPlayerStats(); if (stats) { importedStats = stats; isButtonInSuccessState = true; // 直接从数据计算装备属性 importAllEquipmentStats(); // 显示成功状态 const scannedCount = Object.keys(importedEquipmentStats).length; importButton.textContent = isZH() ? `导入成功 (${scannedCount}件)` : `Success (${scannedCount})`; importButton.style.backgroundColor = colors.success; importButton.style.color = '#fff'; setTimeout(() => { isButtonInSuccessState = false; importButton.textContent = isZH() ? '重新导入当前属性' : 'Re-import Stats'; importButton.style.backgroundColor = ''; importButton.style.color = ''; }, 3000); } }); buttonContainer.appendChild(importButton); // 如果按钮被灰掉(没有装备数据),设置定时器定期检查 if (!isBulwarkEquipped()) { const retryInterval = setInterval(() => { if (Object.keys(currentEquipmentMap).length > 0) { updateButtonDisplay(importButton); clearInterval(retryInterval); } }, 500); // 10秒后停止检查,避免无限循环 setTimeout(() => { clearInterval(retryInterval); }, 10000); } } function updateButtonDisplay(button) { if (isButtonInSuccessState) return; const hasBulwark = isBulwarkEquipped(); if (!hasBulwark) { button.textContent = isZH() ? '未装备双手盾' : 'No Bulwark Equipped'; button.style.backgroundColor = colors.disabled; button.style.opacity = '0.6'; button.style.cursor = 'not-allowed'; button.disabled = true; } else if (importedStats) { button.textContent = isZH() ? '重新导入当前属性' : 'Re-import Stats'; button.style.backgroundColor = ''; button.style.opacity = ''; button.style.cursor = ''; button.disabled = false; } else { button.textContent = isZH() ? '导入当前属性' : 'Import Current Stats'; button.style.backgroundColor = ''; button.style.opacity = ''; button.style.cursor = ''; button.disabled = false; } } // 解析装备模态框数据 function parseEquipmentModal(element) { const equipmentDetail = {}; const detailLines = element.querySelectorAll('[class*="EquipmentStatsText_stat"]'); for (const line of detailLines) { if (line.querySelector('[class*="EquipmentStatsText_uniqueStat"]')) continue; const data = line.textContent.split(':'); if (data.length === 2) { const key = data[0].trim(); const value = data[1].split('(')[0].trim(); if (value === 'N/A') continue; equipmentDetail[key] = value; } } return equipmentDetail; } // 装备类型文本到位置的映射 const EQUIPMENT_TYPE_TO_LOCATION = { // 中文 '头部': '/item_locations/head', '身体': '/item_locations/body', '腿部': '/item_locations/legs', '脚部': '/item_locations/feet', '手部': '/item_locations/hands', '主手': '/item_locations/main_hand', '副手': '/item_locations/off_hand', '双手': '/item_locations/two_hand', '项链': '/item_locations/neck', '耳环': '/item_locations/earrings', '戒指': '/item_locations/ring', '背部': '/item_locations/back', '口袋': '/item_locations/pouch', '袋子': '/item_locations/pouch', '饰品': '/item_locations/trinket', // 英文 'Head': '/item_locations/head', 'Body': '/item_locations/body', 'Legs': '/item_locations/legs', 'Feet': '/item_locations/feet', 'Hands': '/item_locations/hands', 'Main Hand': '/item_locations/main_hand', 'Off Hand': '/item_locations/off_hand', 'Two Hand': '/item_locations/two_hand', 'Neck': '/item_locations/neck', 'Earrings': '/item_locations/earrings', 'Ring': '/item_locations/ring', 'Back': '/item_locations/back', 'Pouch': '/item_locations/pouch', 'Trinket': '/item_locations/trinket', }; // 从tooltip中获取装备位置 function getEquipmentLocationFromModal(modal) { // 查找装备类型文本 const allText = modal.textContent; // 调试:打印tooltip中的所有文本 // console.log('Bulwark Diff: Modal text sample:', allText.substring(0, 500)); // 查找包含装备类型的行 const lines = modal.querySelectorAll('[class*="ItemTooltipText_infoText"], [class*="EquipmentStatsText_stat"], div'); for (const line of lines) { const text = line.textContent.trim(); // 检查是否包含装备类型关键词 if (text.includes('装备类型') || text.includes('Equipment Type') || text.includes('类型') || text.includes('Type:')) { // console.log('Bulwark Diff: Found type line:', text); const parts = text.split(':'); if (parts.length >= 2) { const typeName = parts[parts.length - 1].trim(); const location = EQUIPMENT_TYPE_TO_LOCATION[typeName]; if (location) { // console.log('Bulwark Diff: Found location from type text:', typeName, '->', location); return location; } } } } // 备用方法:直接在文本中查找装备类型名 for (const [typeName, location] of Object.entries(EQUIPMENT_TYPE_TO_LOCATION)) { // 检查各种可能的格式 if (allText.includes(`装备类型: ${typeName}`) || allText.includes(`Equipment Type: ${typeName}`) || allText.includes(`类型: ${typeName}`) || allText.includes(`Type: ${typeName}`)) { // console.log('Bulwark Diff: Found location from full text:', typeName, '->', location); return location; } } // console.log('Bulwark Diff: Could not find equipment location'); return null; } // 检查是否是双手盾类型的武器(从tooltip属性判断) // 双手盾的特征:有 "防御伤害" 属性 function isBulwarkFromModal(modal) { const allText = modal.textContent; // 检查是否有防御伤害属性(双手盾特有) return allText.includes('防御伤害') || allText.includes('Defensive Damage'); } function parseStatValue(valueStr) { if (!valueStr) return 0; const cleaned = valueStr.replace(',', '').replace(' ', '').replace('+', '').replace('_', ''); if (cleaned.endsWith('%')) { return parseFloat(cleaned.replace('%', '')); } else if (cleaned.endsWith('s')) { return parseFloat(cleaned.replace('s', '')) * 100; } return parseFloat(cleaned) || 0; } // 计算相对差值 // 论文定义:1%相对词条提升 = 当前总属性的1% // 相对提升 = (装备属性差) / 当前总属性 × 100% // absolute类型:直接用绝对差值(如最大MP:每1点 = 0.02% DPS) function calculateRelativeDiff(statName, newValue, currentValue) { const zhName = getStatZHName(statName); if (!zhName) return { diff: 0, relativeDiff: 0 }; const statInfo = PAPER_STATS[zhName]; const absoluteDiff = newValue - currentValue; // absolute类型:直接使用绝对差值 if (statInfo && statInfo.type === 'absolute') { // console.log('Bulwark Diff: calculateRelativeDiff (absolute)', { // statName, // zhName, // newValue, // currentValue, // absoluteDiff, // relativeDiff: absoluteDiff // }); return { diff: absoluteDiff, relativeDiff: absoluteDiff }; } // 获取当前总属性值 const totalValue = importedTotalValues[statName] || 100; // console.log('Bulwark Diff: calculateRelativeDiff', { // statName, // zhName, // newValue, // currentValue, // absoluteDiff, // totalValue, // relativeDiff: (absoluteDiff / totalValue) * 100 // }); if (totalValue === 0) return { diff: absoluteDiff, relativeDiff: 0 }; // 相对提升 = 属性差 / 总属性 × 100% const relativeDiff = (absoluteDiff / totalValue) * 100; return { diff: absoluteDiff, relativeDiff }; } // 计算DPS差异(新装备 vs 当前同位置装备) // equipmentLocation: 直接传入装备位置(如 '/item_locations/neck') // isBulwark: 是否是双手盾 function calculateDPSDiff(newEquipmentData, equipmentLocation, isBulwark) { if (!importedStats) { return { error: isZH() ? '请先导入当前属性' : 'Please import stats first' }; } if (!equipmentLocation) { return { error: isZH() ? '无法识别装备类型' : 'Unknown equipment type' }; } // console.log('Bulwark Diff: calculateDPSDiff', { // equipmentLocation, // isBulwark, // newEquipmentData, // importedEquipmentStats // }); // 如果是双手武器位置,检查是否是bulwark if (equipmentLocation === '/item_locations/two_hand') { if (!isBulwark) { return { error: isZH() ? '非双手盾装备' : 'Not a Bulwark' }; } } // 主手武器不参与比对(双手盾不使用主手装备) if (equipmentLocation === '/item_locations/main_hand') { return { error: isZH() ? '主手装备不参与比对' : 'Main hand not compared' }; } // 副手武器不参与比对(双手盾不使用副手装备) if (equipmentLocation === '/item_locations/off_hand') { return { error: isZH() ? '副手装备不参与比对' : 'Off hand not compared' }; } // 获取当前位置的装备属性 const currentSlotStats = importedEquipmentStats[equipmentLocation] || {}; // console.log('Bulwark Diff: Current slot stats', { equipmentLocation, currentSlotStats }); // 标准化 newEquipmentData:将属性名称(可能是中文或英文)转换为中文名称 const normalizedNewEquipmentData = {}; for (const [key, value] of Object.entries(newEquipmentData)) { if (!isStatInPaper(key)) continue; const zhName = getStatZHName(key); if (zhName) { normalizedNewEquipmentData[zhName] = value; } } let totalDPSDiff = 0; const details = {}; // 计算差异 for (const key in normalizedNewEquipmentData) { if (!isStatInPaper(key)) continue; // newEquipmentData 现在直接是数字格式 const newValue = typeof normalizedNewEquipmentData[key] === 'number' ? normalizedNewEquipmentData[key] : parseStatValue(normalizedNewEquipmentData[key]); const currentValue = currentSlotStats[key] || 0; // 检查是否两方不全为0 const hasCurrentValue = currentValue && currentValue !== 0; const hasNewValue = newValue && newValue !== 0; if (!hasCurrentValue && !hasNewValue) continue; // 两方都为0,跳过 const { diff, relativeDiff } = calculateRelativeDiff(key, newValue, currentValue); if (isNaN(relativeDiff)) continue; const coefficient = getStatCoefficient(key, importedStats); // console.log('Bulwark Diff: Stat contribution', { // key, // newValue, // currentValue, // diff, // relativeDiff, // coefficient, // contribution: coefficient !== null ? relativeDiff * coefficient : null // }); if (coefficient !== null) { const contribution = relativeDiff * coefficient; // 误差小于0.01%的算作没变化 if (Math.abs(contribution) < 0.01) continue; totalDPSDiff += contribution; details[key] = { newValue, currentValue, diff, // 绝对差值 relativeDiff, // 相对差值(用于计算DPS) contribution }; } } // 处理当前装备有但新装备没有的属性(会减少) for (const key in currentSlotStats) { if (!isStatInPaper(key)) continue; if (normalizedNewEquipmentData[key]) continue; // 已经处理过 const currentValue = currentSlotStats[key]; // 检查是否两方不全为0(当前装备有值,新装备为0) const hasCurrentValue = currentValue && currentValue !== 0; if (!hasCurrentValue) continue; // 当前装备也为0,跳过 const { diff, relativeDiff } = calculateRelativeDiff(key, 0, currentValue); if (isNaN(relativeDiff)) continue; const coefficient = getStatCoefficient(key, importedStats); if (coefficient !== null) { const contribution = relativeDiff * coefficient; // 误差小于0.01%的算作没变化 if (Math.abs(contribution) < 0.01) continue; totalDPSDiff += contribution; details[key] = { newValue: 0, currentValue, diff, relativeDiff, contribution }; } } // 总DPS变化小于0.01%的算作没变化 if (Math.abs(totalDPSDiff) < 0.01) { totalDPSDiff = 0; } // console.log('Bulwark Diff: Final result', { totalDPSDiff, details }); return { dpsDiff: totalDPSDiff, details }; } // 添加DPS显示到模态框 function addDiffToModal(element, equipmentData, equipmentLocation, isBulwark, price) { if (!importedStats) return; if (element.querySelector('.bulwark-diff-header')) return; const parentArea = element.querySelector('[class*="EquipmentStatsText_equipmentStatsText"]'); if (!parentArea) return; const TextArea = parentArea.firstChild; const dpsResult = calculateDPSDiff(equipmentData, equipmentLocation, isBulwark); const headerLine = document.createElement('div'); headerLine.className = 'bulwark-diff-header'; headerLine.style = 'display: flex; grid-gap: 6px; gap: 6px; justify-content: space-between; border-top: 1px solid #666; padding-top: 6px; margin-top: 6px;'; const titleSpan = document.createElement('span'); titleSpan.textContent = isZH() ? '双手盾DPS变化:' : 'Bulwark DPS Change:'; titleSpan.style = `color: ${colors.info}; font-weight: bold;`; headerLine.appendChild(titleSpan); if (dpsResult.error) { const errorSpan = document.createElement('span'); errorSpan.textContent = `[${dpsResult.error}]`; errorSpan.style = `color: ${colors.info}; font-weight: bold;`; headerLine.appendChild(errorSpan); } else { const dpsDiff = dpsResult.dpsDiff; const diffSpan = document.createElement('span'); diffSpan.textContent = dpsDiff > 0 ? `+${dpsDiff.toFixed(3)}%` : `${dpsDiff.toFixed(3)}%`; diffSpan.style = `color: ${dpsDiff > 0 ? colors.greater : dpsDiff < 0 ? colors.smaller : colors.info}; font-weight: bold;`; headerLine.appendChild(diffSpan); } parentArea.insertBefore(headerLine, TextArea); // 性价比 if (!dpsResult.error && dpsResult.dpsDiff !== 0 && price > 0) { const priceLine = document.createElement('div'); priceLine.className = 'bulwark-diff-header'; priceLine.style = 'display: flex; grid-gap: 6px; gap: 6px; justify-content: space-between;'; const priceLabel = document.createElement('span'); priceLabel.textContent = isZH() ? '每10M价格DPS:' : 'DPS% per 10M:'; priceLabel.style = `color: ${colors.info}; font-weight: bold;`; priceLine.appendChild(priceLabel); const dpsPerMil = Math.abs(dpsResult.dpsDiff) / (price / 1e7); const dpsPerMilSpan = document.createElement('span'); dpsPerMilSpan.textContent = `${dpsPerMil.toFixed(5)}%`; dpsPerMilSpan.style = `color: ${colors.info}; font-weight: bold;`; priceLine.appendChild(dpsPerMilSpan); parentArea.insertBefore(priceLine, TextArea); } // 显示各属性变化 if (dpsResult.details) { const detailLines = element.querySelectorAll('[class*="EquipmentStatsText_stat"]'); const displayedStats = new Set(); // 记录已显示的属性(中文名称) // 1. 先显示 tooltip 中存在的属性 for (const line of detailLines) { const keyFromTooltip = line.textContent.split(':')[0].trim(); if (!isStatInPaper(keyFromTooltip)) continue; // 将 tooltip 中的属性名称(可能是中文或英文)转换为中文名称 const zhName = getStatZHName(keyFromTooltip); if (!zhName || !dpsResult.details[zhName]) continue; const detail = dpsResult.details[zhName]; // 不显示没变化的属性 if (detail.relativeDiff === 0) continue; const valueElement = line.querySelectorAll('span')[1]; if (!valueElement) continue; const diffSpan = document.createElement('span'); diffSpan.className = 'bulwark-diff-value'; // 显示格式:只显示DPS贡献% const dpsText = detail.contribution > 0 ? `+${detail.contribution.toFixed(3)}%` : `${detail.contribution.toFixed(3)}%`; diffSpan.textContent = ` (${dpsText} DPS)`; diffSpan.style = `color: ${detail.relativeDiff > 0 ? colors.greater : colors.smaller}; font-weight: bold;`; valueElement.appendChild(diffSpan); displayedStats.add(zhName); } // 2. 对于 details 中存在但 tooltip 中不存在的属性,创建新的显示行 for (const [zhName, detail] of Object.entries(dpsResult.details)) { if (displayedStats.has(zhName)) continue; // 已经显示过 // 不显示没变化的属性 if (detail.relativeDiff === 0) continue; // 检查是否两方不全为0 const hasCurrentValue = detail.currentValue && detail.currentValue !== 0; const hasNewValue = detail.newValue && detail.newValue !== 0; if (!hasCurrentValue && !hasNewValue) continue; // 两方都为0,不显示 // 创建新的属性行(克隆现有行的结构) const existingLine = detailLines[0]; const newLine = existingLine ? existingLine.cloneNode(false) : document.createElement('div'); if (!existingLine) { newLine.className = 'EquipmentStatsText_stat'; } newLine.innerHTML = ''; // 清空内容 const nameSpan = document.createElement('span'); const displayName = isZH() ? zhName : (PAPER_STATS[zhName]?.en || zhName); nameSpan.textContent = displayName + ':'; const valueSpan = document.createElement('span'); const statInfo = PAPER_STATS[zhName]; const isNonPercent = statInfo && (statInfo.type === 'flat' || statInfo.type === 'absolute'); let newDisplayValue; if (detail.newValue) { if (isNonPercent) { newDisplayValue = detail.newValue > 0 ? `+${detail.newValue.toFixed(2)}` : `${detail.newValue.toFixed(2)}`; } else { newDisplayValue = detail.newValue > 0 ? `+${detail.newValue.toFixed(2)}%` : `${detail.newValue.toFixed(2)}%`; } } else { newDisplayValue = isNonPercent ? '0' : '0%'; } const dpsText = detail.contribution > 0 ? `+${detail.contribution.toFixed(3)}%` : `${detail.contribution.toFixed(3)}%`; valueSpan.innerHTML = `${newDisplayValue} <span style="color: ${detail.relativeDiff > 0 ? colors.greater : colors.smaller}; font-weight: bold;">(${dpsText} DPS)</span>`; newLine.appendChild(nameSpan); newLine.appendChild(valueSpan); // 插入到 headerLine 之后(下方) if (parentArea) { const headerLine = parentArea.querySelector('.bulwark-diff-header'); if (headerLine && headerLine.nextSibling) { parentArea.insertBefore(newLine, headerLine.nextSibling); } else if (headerLine) { parentArea.appendChild(newLine); } else { // 如果没有 headerLine,追加到末尾 parentArea.appendChild(newLine); } } } } } function parsePrice(costText) { if (!costText) return 0; if (costText.endsWith('M')) return parseFloat(costText.replace('M', '').replace(',', '')) * 1e6; if (costText.endsWith('k') || costText.endsWith('K')) return parseFloat(costText.replace(/[kK]/, '').replace(',', '')) * 1e3; if (costText.endsWith('B')) return parseFloat(costText.replace('B', '').replace(',', '')) * 1e9; if (costText.endsWith('T')) return parseFloat(costText.replace('T', '').replace(',', '')) * 1e12; return parseFloat(costText.replace(',', '')) || 0; } function getMWIToolsPrice(modal) { const enhancedPriceText = isZH() ? '总成本' : 'Total cost'; let costNodes = Array.from(modal.querySelectorAll('*')).filter(el => { if (!el.textContent || !el.textContent.includes(enhancedPriceText)) return false; return Array.from(el.childNodes).every(node => node.nodeType === Node.TEXT_NODE); }); if (costNodes.length > 0) { const costText = costNodes[0].textContent.replace(enhancedPriceText, '').trim(); return parsePrice(costText); } const normalPriceText = isZH() ? '日均价' : 'Daily average price'; costNodes = Array.from(modal.querySelectorAll('*')).filter(el => { if (!el.textContent || !el.textContent.includes(normalPriceText)) return false; return Array.from(el.childNodes).every(node => node.nodeType === Node.TEXT_NODE); }); if (costNodes.length > 0) { const costText = costNodes[0].textContent.split('/')[0].split(' ')[1]?.trim(); return parsePrice(costText); } return 0; } // 解压initClientData(使用LZString) function decompressInitClientData(compressedData) { try { if (typeof LZString === 'undefined') { // console.log('Bulwark Diff: LZString not available'); return null; } const decompressedJson = LZString.decompressFromUTF16(compressedData); if (!decompressedJson) { // console.log('Bulwark Diff: decompressFromUTF16 returned null'); return null; } return JSON.parse(decompressedJson); } catch (e) { // console.log('Bulwark Diff: decompress error', e); return null; } } // 加载游戏数据 function loadGameData() { // console.log('Bulwark Diff: loadGameData called, LZString:', typeof LZString !== 'undefined'); try { const initClientData = localStorage.getItem("initClientData"); // console.log('Bulwark Diff: initClientData exists:', !!initClientData); if (initClientData) { const obj = decompressInitClientData(initClientData); // console.log('Bulwark Diff: decompressed obj:', !!obj, obj ? Object.keys(obj).slice(0, 5) : null); if (obj && obj.itemDetailMap) { itemDetailMap = obj.itemDetailMap; // console.log('Bulwark Diff: itemDetailMap loaded with', Object.keys(itemDetailMap).length, 'items'); return true; } } } catch (e) { // console.log('Bulwark Diff: Error loading itemDetailMap', e); } return false; } // 从 MWITools 的 GM 存储读取角色数据 function loadCharacterDataFromMWITools() { try { if (typeof GM_getValue === 'undefined') { return false; } const characterDataStr = GM_getValue("init_character_data", null); if (!characterDataStr) { return false; } const characterData = JSON.parse(characterDataStr); if (characterData && characterData.characterItems) { selfData = characterData; updateEquipmentMap(characterData.characterItems); return true; } } catch (e) { // ignore } return false; } function updateEquipmentMap(characterItems) { if (!characterItems) return; currentEquipmentMap = {}; for (const item of characterItems) { if (item.itemLocationHrid !== "/item_locations/inventory") { currentEquipmentMap[item.itemLocationHrid] = item; } } } // WebSocket Hook function hookWS() { const dataProperty = Object.getOwnPropertyDescriptor(MessageEvent.prototype, "data"); if (!dataProperty || !dataProperty.get) { return; } const oriGet = dataProperty.get; dataProperty.get = function hookedGet() { const socket = this.currentTarget; if (!(socket instanceof WebSocket)) return oriGet.call(this); // 支持国际服和中国服 if (!socket.url.includes("milkywayidle") || !socket.url.includes("/ws")) { return oriGet.call(this); } const message = oriGet.call(this); Object.defineProperty(this, "data", { value: message }); try { handleMessage(message); } catch (error) { // console.log("Bulwark Diff: Error in handleMessage:", error); } return message; }; Object.defineProperty(MessageEvent.prototype, "data", dataProperty); } function handleMessage(message) { if (typeof message !== "string") return; try { const parsedMessage = JSON.parse(message); if (parsedMessage?.type === "init_client_data") { if (parsedMessage.itemDetailMap) { itemDetailMap = parsedMessage.itemDetailMap; // console.log('Bulwark Diff: itemDetailMap loaded from WS'); } } if (parsedMessage?.type === "init_character_data") { selfData = parsedMessage; updateEquipmentMap(parsedMessage.characterItems); // 更新按钮状态 const btn = document.querySelector('.BulwarkImportButton'); if (btn) updateButtonDisplay(btn); } if (parsedMessage?.type === "items_updated" && parsedMessage.endCharacterItems) { for (const item of parsedMessage.endCharacterItems) { if (item.itemLocationHrid !== "/item_locations/inventory") { if (item.count === 0) { currentEquipmentMap[item.itemLocationHrid] = null; } else { currentEquipmentMap[item.itemLocationHrid] = item; } } } // 更新按钮状态 const btn = document.querySelector('.BulwarkImportButton'); if (btn) updateButtonDisplay(btn); } } catch (error) { // Ignore } } // 使用 MutationObserver 监听DOM变化 function setupObserver() { const observer = new MutationObserver((mutations) => { // 检查按钮 createImportButton(); // 检查tooltip if (!importedStats) return; const modals = document.querySelectorAll('.MuiPopper-root'); for (const modal of modals) { const equipmentDetail = modal.querySelector('[class*="ItemTooltipText_equipmentDetail"]'); if (!equipmentDetail) continue; if (equipmentDetail.querySelector('.bulwark-diff-header')) continue; const equipmentLocation = getEquipmentLocationFromModal(modal); const isBulwark = isBulwarkFromModal(modal); const price = getMWIToolsPrice(modal); // 直接从 tooltip 读取属性进行比对 const equipmentData = parseEquipmentModal(equipmentDetail); const parsedStats = {}; for (const [keyFromTooltip, value] of Object.entries(equipmentData)) { if (!isStatInPaper(keyFromTooltip)) continue; // 将 tooltip 中的属性名称(可能是中文或英文)转换为中文名称 const zhName = getStatZHName(keyFromTooltip); if (zhName) { parsedStats[zhName] = parseStatValue(value); } } addDiffToModal(equipmentDetail, parsedStats, equipmentLocation, isBulwark, price); } }); observer.observe(document.body, { childList: true, subtree: true }); } // 初始化 - 立即安装 WebSocket hook(在游戏建立连接之前) hookWS(); // 等待 DOM 准备好后再进行其他初始化 function initWhenReady() { if (document.body) { loadGameData(); loadCharacterDataFromMWITools(); // 延迟启动 observer setTimeout(() => { if (Object.keys(currentEquipmentMap).length === 0) { loadCharacterDataFromMWITools(); } setupObserver(); }, 1000); } else { setTimeout(initWhenReady, 50); } } // 开始检查 DOM 是否准备好 initWhenReady(); })();