// ==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();
});
})();