// ==UserScript== // @name 艾德尔修仙传 · 锻造天书(全自动捕获版·增强) // @namespace http://tampermonkey.net/ // @version 3.1 // @description 自动捕获锻造材料与成品装备,支持多选、批量复制、可最小化面板 // @author 最中幻想 // @match *://idlexiuxianzhuan.cn/* // @match *://*.idlexiuxianzhuan.cn/* // @grant GM_addStyle // @grant GM_setValue // @grant GM_getValue // @run-at document-idle // ==/UserScript== (function() { 'use strict'; // ---------- 存储 ---------- const STORAGE_KEY = 'ForgeTomePro'; let records = GM_getValue(STORAGE_KEY, []); let pendingForge = null; // ---------- 辅助函数 ---------- function saveRecords() { GM_setValue(STORAGE_KEY, records); renderList(); } // 捕获锻造材料(同原逻辑) function captureCurrentMaterials() { let equipSelect = document.querySelector('.forge-form select.input-sm'); let equipType = equipSelect ? equipSelect.value : '未知'; let mainBtn = Array.from(document.querySelectorAll('.forge-form .form-row button.btn-sm')).find(btn => btn.innerText && /x\d+/.test(btn.innerText)); let mainMaterial = mainBtn ? mainBtn.innerText.trim() : '未选择'; let quantityMatch = mainMaterial.match(/x(\d+)/); let quantity = quantityMatch ? quantityMatch[1] : '1'; let spiritBtn = Array.from(document.querySelectorAll('.forge-form .form-row button.btn-sm')).find(btn => { let txt = btn.innerText; return txt !== mainMaterial && !txt.includes('选择主装备') && !txt.includes('选择被继承'); }); let spirit = spiritBtn ? spiritBtn.innerText.trim() : '未选择'; let catalystBtn = Array.from(document.querySelectorAll('.forge-form .form-row button.btn-sm')).find(btn => btn.innerText.includes('催化') || btn.innerText.includes('寒潭沙') || (btn !== mainBtn && btn !== spiritBtn && !btn.innerText.includes('选择')) ); let catalyst = catalystBtn ? catalystBtn.innerText.trim() : '未选择'; return { equipType, mainMaterial, quantity: parseInt(quantity) || 1, spirit, catalyst, timestamp: Date.now() }; } // 从详情面板解析装备(同原逻辑) function parseEquipmentFromDetailPanel() { const detailDiv = document.querySelector('.item-detail:not(.item-detail-compare)'); if (!detailDiv) return null; const nameEl = detailDiv.querySelector('.item-detail-name'); const qualityEl = detailDiv.querySelector('.item-detail-quality'); if (!nameEl) return null; let name = nameEl.innerText.trim(); let qualityText = qualityEl ? qualityEl.innerText : ''; let tier = '未知'; let element = ''; if (qualityText) { let tierMatch = qualityText.match(/([一二三四五六七八]阶|EX|神兵)/); if (tierMatch) tier = tierMatch[1]; let elemMatch = qualityText.match(/[金银铜铁水火土木混元]+$/); if (elemMatch) element = elemMatch[0]; } const setDiv = detailDiv.querySelector('.item-detail-set'); let isSet = !!setDiv; let setBonus = ''; if (setDiv) { const setTitle = setDiv.querySelector('.set-title'); if (setTitle) setBonus = setTitle.innerText.replace('效果', '').trim(); } let affixes = []; const statDivs = detailDiv.querySelectorAll('.item-detail-stats div'); statDivs.forEach(div => { let text = div.innerText.trim(); let match = text.match(/^(.+?):\s*([\d\.%]+)(?:\s*\((.+?)\))?$/); if (match) { let key = match[1]; let val = match[2]; let extra = match[3] || ''; if (extra) extra = `(${extra})`; affixes.push(`${key} ${val} ${extra}`.trim()); } else { let simple = text.match(/^(.+?):\s*(.+)$/); if (simple) affixes.push(`${simple[1]} ${simple[2]}`); } }); let slot = ''; const slotMap = { '背部':'cloak', '手部':'gloves', '胸甲':'chest', '头部':'helmet', '腿部':'legs', '肩部':'shoulder', '武器':'weapon', '戒指':'ring', '项链':'necklace', '符箓':'talisman' }; for (let [cn, en] of Object.entries(slotMap)) { if (qualityText.includes(cn) || detailDiv.innerText.includes(cn)) { slot = en; break; } } return { name, tier, element, isSet, setBonus, slot, affixes }; } function matchWithPending(equipData) { if (!pendingForge) return false; let timeDiff = Date.now() - pendingForge.timestamp; if (timeDiff < 5 * 60 * 1000) { let record = { id: Date.now(), timestamp: pendingForge.timestamp, materials: { equipType: pendingForge.equipType, main: pendingForge.mainMaterial, quantity: pendingForge.quantity, spirit: pendingForge.spirit, catalyst: pendingForge.catalyst }, result: { name: equipData.name, tier: equipData.tier, element: equipData.element || '', isSet: equipData.isSet || false, setBonus: equipData.setBonus || '', affixes: equipData.affixes || [] }, matched: true, dateStr: new Date(pendingForge.timestamp).toLocaleString() }; records.unshift(record); saveRecords(); pendingForge = null; showToast(`✅ 关联成功:${equipData.name}`); return true; } return false; } function saveAlone(equipData) { let record = { id: Date.now(), timestamp: Date.now(), materials: null, result: { name: equipData.name, tier: equipData.tier, element: equipData.element || '', isSet: equipData.isSet || false, setBonus: equipData.setBonus || '', affixes: equipData.affixes || [] }, matched: false, dateStr: new Date().toLocaleString() }; records.unshift(record); saveRecords(); showToast(`📦 单独捕获:${equipData.name}`); } // 监听锻造开始 function bindForgeStart() { function hook() { let forgeBtn = document.querySelector('.forge-form button.btn-action.gold'); if (forgeBtn && !forgeBtn.hasAttribute('data-forge-auto')) { forgeBtn.setAttribute('data-forge-auto', 'true'); forgeBtn.addEventListener('click', () => { let mats = captureCurrentMaterials(); pendingForge = mats; showToast('📝 已记录配方,等待成品'); }); } } hook(); const observer = new MutationObserver(() => hook()); observer.observe(document.body, { childList: true, subtree: true }); } // 邮件接口拦截(简版) function interceptMailList() { const originalFetch = window.fetch; window.fetch = function(...args) { return originalFetch.apply(this, args).then(async (response) => { const url = args[0]; if (typeof url === 'string' && url.includes('/mail/list')) { const clone = response.clone(); const text = await clone.text(); setTimeout(() => processMailText(text), 100); } return response; }); }; } function processMailText(text) { let matches = text.matchAll(/炼器完成通知.*?装备已投递邮件:([^<\n]+)/g); for (let match of matches) { let equipName = match[1].trim(); if (equipName) { let tier = '未知'; if (equipName.includes('一阶')) tier = '1阶'; else if (equipName.includes('二阶')) tier = '2阶'; else if (equipName.includes('三阶')) tier = '3阶'; else if (equipName.includes('四阶')) tier = '4阶'; else if (equipName.includes('五阶')) tier = '5阶'; else if (equipName.includes('六阶')) tier = '6阶'; else if (equipName.includes('七阶')) tier = '7阶'; else if (equipName.includes('八阶')) tier = '8阶'; else if (equipName.includes('EX')) tier = 'EX'; let affixList = ['裂空','开山','撼岳','破军','聚气','法威','灵涌','铁壁','不灭','圣躯','锋芒','灵犀','回灵','凝神','逐影','厚甲','灵息','镇岳','强体','凝元','归一','道韵']; let affixes = affixList.filter(af => equipName.includes(af)); let equipData = { name: equipName, tier, affixes, isSet: equipName.includes('套装') || equipName.includes('异界') }; let matched = matchWithPending(equipData); if (!matched) saveAlone(equipData); } } } function observeEquipmentSelection() { let lastSelectedItem = null; setInterval(() => { const selected = document.querySelector('.inv-list-item.selected'); if (selected && (!lastSelectedItem || lastSelectedItem !== selected)) { lastSelectedItem = selected; setTimeout(() => { const equipData = parseEquipmentFromDetailPanel(); if (equipData) { let matched = matchWithPending(equipData); if (!matched) saveAlone(equipData); } }, 500); } }, 1000); } // ---------- UI 增强版 ---------- let isMinimized = false; // 是否最小化为一条 let currentSelectedIds = new Set(); // 存储选中记录的 id function createUI() { GM_addStyle(` #forge-tome-pro { position: fixed; bottom: 20px; left: 20px; width: calc(100vw - 40px); max-width: 420px; background: #1a1d2ecc; backdrop-filter: blur(12px); border-radius: 24px; border: 1px solid #4a5a7a; z-index: 99999; font-family: system-ui; font-size: 13px; color: #e0e0e0; display: flex; flex-direction: column; overflow: hidden; box-shadow: 0 8px 20px rgba(0,0,0,0.3); transition: all 0.2s ease; } #forge-tome-pro.minimized { max-width: 200px; min-width: 140px; } .tome-header { display: flex; justify-content: space-between; align-items: center; padding: 8px 12px; background: #0f1222; cursor: move; user-select: none; border-bottom: 1px solid #2a3a5a; } .tome-title { font-weight: bold; font-size: 14px; display: flex; align-items: center; gap: 6px; } .tome-buttons { display: flex; gap: 6px; align-items: center; } .tome-btn { background: none; border: none; color: #aaa; cursor: pointer; font-size: 14px; padding: 0 4px; touch-action: manipulation; } .tome-btn:hover { color: #ffd700; } .tome-list { max-height: 300px; overflow-y: auto; padding: 4px 0; } .record-item { display: flex; align-items: flex-start; gap: 8px; padding: 8px 12px; border-bottom: 1px solid #2a3a5a; font-size: 12px; } .record-checkbox { margin-top: 2px; width: 18px; height: 18px; cursor: pointer; accent-color: #ffd700; } .record-content { flex: 1; } .record-materials { color: #8a9bb0; font-size: 11px; margin: 4px 0; } .record-result { font-weight: 500; color: #ffd966; } .record-affixes { color: #6c8fc0; font-size: 11px; } .record-set { color: #c084fc; font-size: 11px; } .tome-footer { display: flex; gap: 8px; padding: 8px 12px; border-top: 1px solid #2a3a5a; background: #0f1222; } .footer-btn { flex: 1; background: #2a3a5a; border: none; border-radius: 20px; padding: 6px 0; color: #eee; font-size: 12px; cursor: pointer; text-align: center; touch-action: manipulation; } .footer-btn:active { background: #3a4a6a; } .minimized .tome-list, .minimized .tome-footer { display: none; } .minimized .tome-header { border-bottom: none; } .toast-msg { position: fixed; bottom: 80px; left: 50%; transform: translateX(-50%); background: #1e2a3a; color: #ffd966; padding: 4px 12px; border-radius: 20px; font-size: 12px; z-index: 100000; white-space: nowrap; } @media (max-width: 640px) { #forge-tome-pro { left: 10px; bottom: 70px; width: calc(100vw - 20px); max-width: 400px; } .record-item { padding: 6px 8px; } .record-checkbox { width: 20px; height: 20px; } } `); let panel = document.createElement('div'); panel.id = 'forge-tome-pro'; panel.innerHTML = `