MWI-Bulwark-Equipment-Diff

双手盾装备DPS对比工具

スクリプトをインストールするには、Tampermonkey, GreasemonkeyViolentmonkey のような拡張機能のインストールが必要です。

You will need to install an extension such as Tampermonkey to install this script.

スクリプトをインストールするには、TampermonkeyViolentmonkey のような拡張機能のインストールが必要です。

スクリプトをインストールするには、TampermonkeyUserscripts のような拡張機能のインストールが必要です。

このスクリプトをインストールするには、Tampermonkeyなどの拡張機能をインストールする必要があります。

このスクリプトをインストールするには、ユーザースクリプト管理ツールの拡張機能をインストールする必要があります。

(ユーザースクリプト管理ツールは設定済みなのでインストール!)

このスタイルをインストールするには、Stylusなどの拡張機能をインストールする必要があります。

このスタイルをインストールするには、Stylus などの拡張機能をインストールする必要があります。

このスタイルをインストールするには、Stylus tなどの拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

(ユーザースタイル管理ツールは設定済みなのでインストール!)

このスクリプトの質問や評価の投稿はこちら通報はこちらへお寄せください
// ==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();

})();