Light.gg Bilingual Display Tool

命运2工具网站 light.gg 的增强脚本,将物品名显示为双语,并可选择性设置tooltip语言。

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

您需要先安裝使用者腳本管理器擴充功能後才能安裝該腳本。

(我已經安裝了使用者腳本管理器,讓我安裝!)

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

// ==UserScript==
// @name                Light.gg Bilingual Display Tool
// @version             3.0
// @description         命运2工具网站 light.gg 的增强脚本,将物品名显示为双语,并可选择性设置tooltip语言。
// @author              Eliver
// @match               https://www.light.gg/*
// @grant               GM_setValue
// @grant               GM_getValue
// @license             MIT
// @namespace https://greasyfork.org/users/1267935
// ==/UserScript==

(function () {
    'use strict';

    const CACHE_KEY = 'lightgg_item_list';
    const LAST_UPDATE_KEY = 'lightgg_last_update';
    const TOOLTIP_LANG_SETTING_KEY = 'lightgg_tooltip_lang_setting';
    const ITEM_LIST_URL = 'https://20xiji.github.io/Destiny-item-list/item-list-8-2-0.json';

    let setTooltipLang = GM_getValue(TOOLTIP_LANG_SETTING_KEY, true);
    let originalLang;

    // 性能优化:缓存和查找映射
    let cachedItemList = null;
    let itemLookupMap = new Map();
    let processedElements = new WeakSet();
    let isDataReady = false;

    // 性能优化:节流函数替代防抖
    function throttle(func, limit) {
        let inThrottle;
        return function(...args) {
            if (!inThrottle) {
                func.apply(this, args);
                inThrottle = true;
                setTimeout(() => inThrottle = false, limit);
            }
        };
    }

    // 构建O(1)查找的映射表
    function buildLookupMap(itemList) {
        itemLookupMap.clear();
        Object.keys(itemList).forEach(key => {
            const item = itemList[key];
            if (item.en) {
                itemLookupMap.set(item.en.toLowerCase(), { key, item });
            }
            if (item['zh-chs']) {
                itemLookupMap.set(item['zh-chs'].toLowerCase(), { key, item });
            }
        });
        console.log(`构建查找映射表完成,包含 ${itemLookupMap.size} 个条目`);
    }

    // 创建通知系统
    function createNotification(message, type = 'info', duration = 3000) {
        const notification = document.createElement('div');
        notification.className = 'lightgg-notification';
        notification.textContent = message;

        const colors = {
            success: '#4CAF50',
            error: '#f44336',
            info: '#2196F3',
            warning: '#ff9800'
        };

        notification.style.cssText = `
            position: fixed;
            top: 20px;
            right: 20px;
            background: ${colors[type]};
            color: white;
            padding: 12px 20px;
            border-radius: 8px;
            font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
            font-size: 14px;
            font-weight: 500;
            box-shadow: 0 4px 12px rgba(0,0,0,0.15);
            z-index: 10000;
            transform: translateX(100%);
            transition: transform 0.3s ease;
            max-width: 300px;
            word-wrap: break-word;
        `;

        document.body.appendChild(notification);

        // 动画进入
        setTimeout(() => {
            notification.style.transform = 'translateX(0)';
        }, 10);

        // 自动消失
        setTimeout(() => {
            notification.style.transform = 'translateX(100%)';
            setTimeout(() => notification.remove(), 300);
        }, duration);
    }

    function createSettingsUI() {
        // 创建可折叠的设置面板
        const container = document.createElement('div');
        container.className = 'lightgg-settings-container';
        container.style.cssText = `
            position: fixed;
            top: 20px;
            right: 20px;
            z-index: 9999;
            font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
        `;

        // 切换按钮
        const toggleButton = document.createElement('button');
        toggleButton.className = 'lightgg-toggle-btn';
        toggleButton.innerHTML = '⚙️';
        toggleButton.title = 'Light.gg 双语工具设置';
        toggleButton.style.cssText = `
            width: 44px;
            height: 44px;
            border-radius: 50%;
            border: none;
            background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
            color: white;
            font-size: 18px;
            cursor: pointer;
            box-shadow: 0 4px 12px rgba(0,0,0,0.15);
            transition: all 0.3s ease;
            display: flex;
            align-items: center;
            justify-content: center;
        `;

        // 设置面板
        const settingsPanel = document.createElement('div');
        settingsPanel.className = 'lightgg-settings-panel';
        settingsPanel.style.cssText = `
            position: absolute;
            top: 54px;
            right: 0;
            background: white;
            border-radius: 12px;
            box-shadow: 0 8px 32px rgba(0,0,0,0.12);
            padding: 20px;
            min-width: 280px;
            transform: translateY(-10px);
            opacity: 0;
            visibility: hidden;
            transition: all 0.3s ease;
            border: 1px solid #e1e5e9;
        `;

        // 面板标题
        const title = document.createElement('h3');
        title.textContent = 'Light.gg 双语工具';
        title.style.cssText = `
            margin: 0 0 16px 0;
            font-size: 16px;
            font-weight: 600;
            color: #1a1a1a;
            display: flex;
            align-items: center;
            gap: 8px;
        `;
        title.innerHTML = '🌐 Light.gg 双语工具';

        // 语言切换选项
        const langOption = document.createElement('div');
        langOption.style.cssText = `
            display: flex;
            align-items: center;
            justify-content: space-between;
            margin-bottom: 16px;
            padding: 12px;
            background: #f8f9fa;
            border-radius: 8px;
        `;

        const langLabel = document.createElement('label');
        langLabel.style.cssText = `
            display: flex;
            flex-direction: column;
            gap: 4px;
            cursor: pointer;
            flex: 1;
        `;

        const langTitle = document.createElement('span');
        langTitle.textContent = '中文 Perk 提示';
        langTitle.style.cssText = `
            font-weight: 500;
            color: #1a1a1a;
            font-size: 14px;
        `;

        const langDesc = document.createElement('span');
        langDesc.textContent = '将Perk提示框显示为中文';
        langDesc.style.cssText = `
            font-size: 12px;
            color: #6c757d;
        `;

        const toggleSwitch = document.createElement('div');
        toggleSwitch.className = 'lightgg-switch';
        toggleSwitch.style.cssText = `
            position: relative;
            width: 48px;
            height: 24px;
            background: ${setTooltipLang ? '#007bff' : '#dee2e6'};
            border-radius: 12px;
            cursor: pointer;
            transition: background 0.3s ease;
        `;

        const switchHandle = document.createElement('div');
        switchHandle.style.cssText = `
            position: absolute;
            top: 2px;
            left: ${setTooltipLang ? '26px' : '2px'};
            width: 20px;
            height: 20px;
            background: white;
            border-radius: 50%;
            transition: left 0.3s ease;
            box-shadow: 0 2px 4px rgba(0,0,0,0.2);
        `;

        toggleSwitch.appendChild(switchHandle);
        langLabel.appendChild(langTitle);
        langLabel.appendChild(langDesc);
        langOption.appendChild(langLabel);
        langOption.appendChild(toggleSwitch);

        // 更新按钮
        const updateButton = document.createElement('button');
        updateButton.innerHTML = '🔄 更新数据';
        updateButton.style.cssText = `
            width: 100%;
            background: linear-gradient(135deg, #28a745 0%, #20c997 100%);
            border: none;
            color: white;
            padding: 12px;
            border-radius: 8px;
            cursor: pointer;
            font-size: 14px;
            font-weight: 500;
            transition: all 0.3s ease;
            display: flex;
            align-items: center;
            justify-content: center;
            gap: 8px;
        `;

        // 状态指示器
        const statusIndicator = document.createElement('div');
        statusIndicator.style.cssText = `
            display: flex;
            align-items: center;
            gap: 8px;
            margin-top: 12px;
            padding: 8px 12px;
            background: #e8f5e8;
            border-radius: 6px;
            font-size: 12px;
            color: #155724;
        `;
        statusIndicator.innerHTML = '✅ 数据已加载';

        // 事件处理
        let isOpen = false;

        toggleButton.addEventListener('click', () => {
            isOpen = !isOpen;
            if (isOpen) {
                settingsPanel.style.opacity = '1';
                settingsPanel.style.visibility = 'visible';
                settingsPanel.style.transform = 'translateY(0)';
                toggleButton.style.transform = 'rotate(180deg)';
            } else {
                settingsPanel.style.opacity = '0';
                settingsPanel.style.visibility = 'hidden';
                settingsPanel.style.transform = 'translateY(-10px)';
                toggleButton.style.transform = 'rotate(0deg)';
            }
        });

        // 点击外部关闭
        document.addEventListener('click', (e) => {
            if (!container.contains(e.target) && isOpen) {
                isOpen = false;
                settingsPanel.style.opacity = '0';
                settingsPanel.style.visibility = 'hidden';
                settingsPanel.style.transform = 'translateY(-10px)';
                toggleButton.style.transform = 'rotate(0deg)';
            }
        });

        toggleSwitch.addEventListener('click', () => {
            setTooltipLang = !setTooltipLang;
            GM_setValue(TOOLTIP_LANG_SETTING_KEY, setTooltipLang);

            if (setTooltipLang) {
                lggTooltip.lang = "zh-chs";
                toggleSwitch.style.background = '#007bff';
                switchHandle.style.left = '26px';
                createNotification('已启用中文 Perk 提示', 'success');
            } else {
                lggTooltip.lang = originalLang;
                toggleSwitch.style.background = '#dee2e6';
                switchHandle.style.left = '2px';
                createNotification('已关闭中文 Perk 提示', 'info');
            }
        });

        updateButton.addEventListener('click', async () => {
            updateButton.disabled = true;
            updateButton.innerHTML = '⏳ 更新中...';
            updateButton.style.opacity = '0.7';
            statusIndicator.innerHTML = '🔄 正在更新数据...';
            statusIndicator.style.background = '#fff3cd';
            statusIndicator.style.color = '#856404';

            try {
                GM_setValue(CACHE_KEY, '');
                GM_setValue(LAST_UPDATE_KEY, '');
                cachedItemList = null;
                isDataReady = false;
                itemListPromise = loadItemList();
                await itemListPromise;
                optimizedTransformReviewItems();

                createNotification('数据更新成功!', 'success');
                statusIndicator.innerHTML = '✅ 数据已更新';
                statusIndicator.style.background = '#e8f5e8';
                statusIndicator.style.color = '#155724';
            } catch (error) {
                createNotification('更新失败:' + error.message, 'error');
                statusIndicator.innerHTML = '❌ 更新失败';
                statusIndicator.style.background = '#f8d7da';
                statusIndicator.style.color = '#721c24';
            } finally {
                updateButton.disabled = false;
                updateButton.innerHTML = '🔄 更新数据';
                updateButton.style.opacity = '1';
            }
        });

        // 组装UI
        settingsPanel.appendChild(title);
        settingsPanel.appendChild(langOption);
        settingsPanel.appendChild(updateButton);
        settingsPanel.appendChild(statusIndicator);

        container.appendChild(toggleButton);
        container.appendChild(settingsPanel);
        document.body.appendChild(container);

        // 悬停效果
        toggleButton.addEventListener('mouseenter', () => {
            toggleButton.style.transform = 'scale(1.1)';
            toggleButton.style.boxShadow = '0 6px 20px rgba(0,0,0,0.25)';
        });

        toggleButton.addEventListener('mouseleave', () => {
            if (!isOpen) {
                toggleButton.style.transform = 'scale(1)';
                toggleButton.style.boxShadow = '0 4px 12px rgba(0,0,0,0.15)';
            }
        });

        updateButton.addEventListener('mouseenter', () => {
            if (!updateButton.disabled) {
                updateButton.style.transform = 'translateY(-1px)';
                updateButton.style.boxShadow = '0 4px 12px rgba(40, 167, 69, 0.3)';
            }
        });

        updateButton.addEventListener('mouseleave', () => {
            updateButton.style.transform = 'translateY(0)';
            updateButton.style.boxShadow = 'none';
        });
    }

    async function loadItemList() {
        const now = new Date().toDateString();
        const lastUpdate = GM_getValue(LAST_UPDATE_KEY, '');

        if (lastUpdate !== now) {
            try {
                const response = await fetch(ITEM_LIST_URL);
                const data = await response.json();
                GM_setValue(CACHE_KEY, JSON.stringify(data.data));
                GM_setValue(LAST_UPDATE_KEY, now);
                cachedItemList = data.data;
            } catch (error) {
                console.error('更新失败:', error);
                cachedItemList = JSON.parse(GM_getValue(CACHE_KEY) || '{}');
            }
        } else {
            cachedItemList = JSON.parse(GM_getValue(CACHE_KEY) || '{}');
        }

        buildLookupMap(cachedItemList);
        isDataReady = true;
        return cachedItemList;
    }

    let itemListPromise = loadItemList();

    // 性能优化:只处理新元素,使用O(1)查找
    function processElements(elements, lang) {
        const newElements = Array.from(elements).filter(el => !processedElements.has(el));

        if (newElements.length === 0) return;

        newElements.forEach(element => {
            const originalText = element.textContent.trim();
            const lookupResult = itemLookupMap.get(originalText.toLowerCase());

            if (lookupResult) {
                const { item } = lookupResult;
                const translatedName = lang === 'zh-chs' ? item.en : item['zh-chs'];
                if (translatedName) {
                    element.textContent = `${originalText} | ${translatedName}`;
                    processedElements.add(element);
                }
            }
        });

        console.log(`处理了 ${newElements.length} 个新元素`);
    }

    function optimizedTransformReviewItems() {
        const elements = document.querySelectorAll('.item-name h2, .item-name a, .key-perk strong');
        const lang = window.location.pathname.includes('/zh-chs/') ? 'zh-chs' : 'en';

        // 性能优化:如果数据已准备好,直接同步处理
        if (isDataReady && itemLookupMap.size > 0) {
            processElements(elements, lang);
        } else {
            itemListPromise.then(() => {
                processElements(elements, lang);
            });
        }
    }

    // 性能优化:XHR拦截使用节流
    const originalOpen = XMLHttpRequest.prototype.open;
    XMLHttpRequest.prototype.open = function() {
        const url = arguments[1];
        if (/api\.light\.gg\/items\/\d*\/?/.test(url)) {
            this.addEventListener('load', throttle(optimizedTransformReviewItems, 200));
        }
        originalOpen.apply(this, arguments);
    };

    // 性能优化:更智能的DOM观察者
    const observer = new MutationObserver(throttle((mutations) => {
        let shouldProcess = false;

        // 只在添加了相关元素时才处理
        for (const mutation of mutations) {
            if (mutation.type === 'childList' && mutation.addedNodes.length > 0) {
                for (const node of mutation.addedNodes) {
                    if (node.nodeType === Node.ELEMENT_NODE) {
                        if (node.matches?.('.item-name, .key-perk') ||
                            node.querySelector?.('.item-name, .key-perk')) {
                            shouldProcess = true;
                            break;
                        }
                    }
                }
                if (shouldProcess) break;
            }
        }

        if (shouldProcess) {
            optimizedTransformReviewItems();
        }
    }, 200));

    observer.observe(document.body, { childList: true, subtree: true });

    // 初始化
    window.addEventListener('load', () => {
        createSettingsUI();
        originalLang = lggTooltip.lang;
        if (setTooltipLang) lggTooltip.lang = "zh-chs";

        // 只在主界面显示欢迎通知
        if (window.location.pathname === '/' || window.location.pathname === '') {
            setTimeout(() => {
                createNotification('Light.gg 双语工具已启动 🚀', 'success', 2000);
            }, 1000);
        }

        const reviewTab = document.getElementById('review-tab');
        reviewTab?.click();
        optimizedTransformReviewItems();
    });
})();