您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Greasy Fork is available in English.
计算制造、烹饪、强化、房屋所需材料并一键购买,计算实时生产和炼金利润,增加按照目标材料数量进行采集的功能,快速切换角色,购物车功能
当前为
// ==UserScript== // @name [银河奶牛] 生产采集增强 / MWI Production & Gathering Enhanced // @name:zh-CN [银河奶牛]生产采集增强 // @name:en MWI Production & Gathering Enhanced // @namespace http://tampermonkey.net/ // @version 3.4.1 // @description 计算制造、烹饪、强化、房屋所需材料并一键购买,计算实时生产和炼金利润,增加按照目标材料数量进行采集的功能,快速切换角色,购物车功能 // @description:en Calculate materials for crafting, cooking, enhancing, housing with one-click purchase, calculate real-time production & alchemy profits, add target-based gathering functionality, fast character switching, shopping cart feature // @author XIxixi297 // @license CC-BY-NC-SA-4.0 // @match https://www.milkywayidle.com/* // @match https://test.milkywayidle.com/* // @icon https://www.google.com/s2/favicons?sz=64&domain=milkywayidle.com // @grant none // @run-at document-start // ==/UserScript== (function () { 'use strict'; // ==================== 功能开关 ==================== const DEFAULT_CONFIG = { quickPurchase: true, universalProfit: true, alchemyProfit: true, gatheringEnhanced: true, characterSwitcher: true, }; const STORAGE_KEY = 'MWI_CONFIG'; // 读取本地配置 function loadConfig() { try { const saved = JSON.parse(localStorage.getItem(STORAGE_KEY)); return { ...DEFAULT_CONFIG, ...saved }; } catch (e) { return { ...DEFAULT_CONFIG }; } } // 保存配置 function saveConfig(config) { localStorage.setItem(STORAGE_KEY, JSON.stringify(config)); } // 设置全局变量 window.MWI_CONFIG = loadConfig(); // ==================== 全局模块管理 ==================== window.MWIModules = { toast: null, api: null, eventBus: null, autoStop: null, alchemyCalculator: null, universalCalculator: null, shoppingCart: null, characterSwitcher: null, materialPurchase: null }; // ==================== 事件总线 ==================== class EventBus { constructor() { this.events = {}; } on(event, callback) { if (!this.events[event]) { this.events[event] = []; } this.events[event].push(callback); } emit(event, data) { if (this.events[event]) { this.events[event].forEach(callback => callback(data)); } } off(event, callback) { if (this.events[event]) { this.events[event] = this.events[event].filter(cb => cb !== callback); } } } // ==================== 常量配置 ==================== const CONFIG = { DELAYS: { API_CHECK: 2000, PURCHASE: 800, UPDATE: 100 }, TIMEOUTS: { API: 8000, PURCHASE: 15000 }, CACHE_TTL: 60000, ALCHEMY_CACHE_EXPIRY: 300000, UNIVERSAL_CACHE_EXPIRY: 300000, APIENDPOINT: 'mwi-market', CHARACTER_SWITCHER: { autoInit: true, avatarSelector: '.Header_avatar__2RQgo', characterInfoSelector: '.Header_characterInfo__3ixY8', animationDuration: 200, dropdownMaxHeight: '400px', dropdownMinWidth: '280px', dropdownMaxWidth: '400px' }, COLORS: { buy: 'var(--color-market-buy)', buyHover: 'var(--color-market-buy-hover)', sell: 'var(--color-market-sell)', sellHover: 'var(--color-market-sell-hover)', disabled: 'var(--color-disabled)', error: '#ff6b6b', text: 'var(--color-text-dark-mode)', warning: 'var(--color-warning)', space300: 'var(--color-space-300)', cart: '#9c27b0', cartHover: '#7b1fa2', profit: '#4CAF50', loss: '#f44336', neutral: '#757575' } }; // ==================== 语言配置 ==================== const LANG = (navigator.language || 'en').toLowerCase().includes('zh') ? { directBuy: '直购(左一)', bidOrder: '求购(右一)', directBuyUpgrade: '左一', bidOrderUpgrade: '右一', buying: '⏳ 购买中...', submitting: '📋 提交中...', missing: '缺:', sufficient: '材料充足!', sufficientUpgrade: '升级物品充足!', starting: '开始', materials: '种材料', upgradeItems: '种升级物品', purchased: '已购买', submitted: '订单已提交', failed: '失败', complete: '完成!', error: '出错,请检查控制台', wsNotAvailable: 'WebSocket接口未可用', waiting: '等待接口就绪...', ready: '接口已就绪!', success: '成功', each: '个', allFailed: '全部失败', targetLabel: '目标', switchCharacter: '切换角色', noCharacterData: '暂无角色数据,请刷新页面重试', current: '当前', switch: '切换', standard: '标准', ironcow: '铁牛', lastOnline: '上次在线', timeAgo: { justNow: '刚刚', minutesAgo: '分钟前', hoursAgo: '小时', daysAgo: '天前' }, pessimisticProfit: '悲观日利润', optimisticProfit: '乐观日利润', loadingMarketData: '获取实时数据中...', noData: '缺少市场数据', waitingAPI: '游戏核心对象获取失败...', waitingAPIUniversal: '等待API就绪...', errorUniversal: '计算出错', addToCart: '加入购物车', add: '已添加', toCart: '到购物车', shoppingCart: '购物车', cartEmpty: '购物车是空的', cartDirectBuy: '批量直购(左一)', cartBidOrder: '批量求购(右一)', cartClear: '清空购物车', cartRemove: '移除', cartQuantity: '数量', cartItem: '项', noMaterialsNeeded: '没有需要补充的材料', addToCartFailed: '添加失败,请稍后重试', cartClearSuccess: '已清空购物车', pleaseEnterListName: '请输入清单名称', cartEmptyCannotSave: '购物车为空,无法保存', maxListsLimit: '最多只能保存', lists: '个清单', listName: '清单名称', save: '💾 保存', savedLists: '已保存清单', noSavedLists: '暂无保存的清单', load: '加载', delete: '删除', loaded: '已加载', deleted: '已删除', saved: '已保存', exportSavedLists: '📤 导出已保存清单', importSavedLists: '📥 导入已保存清单', exportStatusPrefix: '已导出 ', exportStatusSuffix: ' 个购物清单', importStatusPrefix: '导入完成!共导入', importStatusSuffix: '个购物清单', exportFailed: '导出失败', importFailed: '导入失败', noListsToExport: '没有保存的购物清单可以导出', invalidImportFormat: '文件格式不正确' } : { directBuy: 'Buy(Left)', bidOrder: 'Bid(Right)', directBuyUpgrade: 'Left', bidOrderUpgrade: 'Right', buying: '⏳ Buying...', submitting: '📋 Submitting...', missing: 'Need:', sufficient: 'All materials sufficient!', sufficientUpgrade: 'All upgrades sufficient!', starting: 'Start', materials: 'materials', upgradeItems: 'upgrade items', purchased: 'Purchased', submitted: 'Order submitted', failed: 'failed', complete: 'completed!', error: 'error, check console', wsNotAvailable: 'WebSocket interface not available', waiting: 'Waiting for interface...', ready: 'Interface ready!', success: 'Successfully', each: '', allFailed: 'All failed', targetLabel: 'Target', switchCharacter: 'Switch Character', noCharacterData: 'No character data available, please refresh the page', current: 'Current', switch: 'Switch', standard: 'Standard', ironcow: 'IronCow', lastOnline: 'Last online', timeAgo: { justNow: 'just now', minutesAgo: 'min ago', hoursAgo: 'hr', daysAgo: 'd ago' }, pessimisticProfit: 'Pessimistic Daily Profit', optimisticProfit: 'Optimistic Daily Profit', loadingMarketData: 'Loading Market Data...', noData: 'Lack of Market Data', waitingAPI: 'Game core object acquisition failed...', waitingAPIUniversal: 'Waiting for API...', errorUniversal: 'Calculation Error', addToCart: 'Add to Cart', add: 'Added', toCart: 'to Cart', shoppingCart: 'Shopping Cart', cartEmpty: 'Cart is empty', cartDirectBuy: 'Batch Buy', cartBidOrder: 'Batch Bid', cartClear: 'Clear Cart', cartRemove: 'Remove', cartQuantity: 'Quantity', cartItem: 'items', noMaterialsNeeded: 'No materials need to be supplemented', addToCartFailed: 'Add failed, please try again later', cartClearSuccess: 'Cart cleared', pleaseEnterListName: 'Please enter list name', cartEmptyCannotSave: 'Cart is empty, cannot save', maxListsLimit: 'Maximum', lists: 'lists allowed', listName: 'List Name', save: '💾 Save', savedLists: 'Saved Lists', noSavedLists: 'No saved lists', load: 'Load', delete: 'Delete', loaded: 'Loaded', deleted: 'Deleted', saved: 'Saved', exportSavedLists: '📤 Export Saved Lists', importSavedLists: '📥 Import Saved Lists', exportStatusPrefix: 'Exported ', exportStatusSuffix: ' shopping lists', importStatusPrefix: 'Import completed! ', importStatusSuffix: ' lists imported', exportFailed: 'Export failed', importFailed: 'Import failed', noListsToExport: 'No saved shopping lists to export', invalidImportFormat: 'Invalid file format' }; // ==================== 采集动作配置 ==================== const gatheringActions = [ { "hrid": "/actions/milking/cow", "itemHrid": "/items/milk" }, { "hrid": "/actions/milking/verdant_cow", "itemHrid": "/items/verdant_milk" }, { "hrid": "/actions/milking/azure_cow", "itemHrid": "/items/azure_milk" }, { "hrid": "/actions/milking/burble_cow", "itemHrid": "/items/burble_milk" }, { "hrid": "/actions/milking/crimson_cow", "itemHrid": "/items/crimson_milk" }, { "hrid": "/actions/milking/unicow", "itemHrid": "/items/rainbow_milk" }, { "hrid": "/actions/milking/holy_cow", "itemHrid": "/items/holy_milk" }, { "hrid": "/actions/foraging/egg", "itemHrid": "/items/egg" }, { "hrid": "/actions/foraging/wheat", "itemHrid": "/items/wheat" }, { "hrid": "/actions/foraging/sugar", "itemHrid": "/items/sugar" }, { "hrid": "/actions/foraging/cotton", "itemHrid": "/items/cotton" }, { "hrid": "/actions/foraging/blueberry", "itemHrid": "/items/blueberry" }, { "hrid": "/actions/foraging/apple", "itemHrid": "/items/apple" }, { "hrid": "/actions/foraging/arabica_coffee_bean", "itemHrid": "/items/arabica_coffee_bean" }, { "hrid": "/actions/foraging/flax", "itemHrid": "/items/flax" }, { "hrid": "/actions/foraging/blackberry", "itemHrid": "/items/blackberry" }, { "hrid": "/actions/foraging/orange", "itemHrid": "/items/orange" }, { "hrid": "/actions/foraging/robusta_coffee_bean", "itemHrid": "/items/robusta_coffee_bean" }, { "hrid": "/actions/foraging/strawberry", "itemHrid": "/items/strawberry" }, { "hrid": "/actions/foraging/plum", "itemHrid": "/items/plum" }, { "hrid": "/actions/foraging/liberica_coffee_bean", "itemHrid": "/items/liberica_coffee_bean" }, { "hrid": "/actions/foraging/bamboo_branch", "itemHrid": "/items/bamboo_branch" }, { "hrid": "/actions/foraging/mooberry", "itemHrid": "/items/mooberry" }, { "hrid": "/actions/foraging/peach", "itemHrid": "/items/peach" }, { "hrid": "/actions/foraging/excelsa_coffee_bean", "itemHrid": "/items/excelsa_coffee_bean" }, { "hrid": "/actions/foraging/cocoon", "itemHrid": "/items/cocoon" }, { "hrid": "/actions/foraging/marsberry", "itemHrid": "/items/marsberry" }, { "hrid": "/actions/foraging/dragon_fruit", "itemHrid": "/items/dragon_fruit" }, { "hrid": "/actions/foraging/fieriosa_coffee_bean", "itemHrid": "/items/fieriosa_coffee_bean" }, { "hrid": "/actions/foraging/spaceberry", "itemHrid": "/items/spaceberry" }, { "hrid": "/actions/foraging/star_fruit", "itemHrid": "/items/star_fruit" }, { "hrid": "/actions/foraging/spacia_coffee_bean", "itemHrid": "/items/spacia_coffee_bean" }, { "hrid": "/actions/foraging/radiant_fiber", "itemHrid": "/items/radiant_fiber" }, { "hrid": "/actions/woodcutting/tree", "itemHrid": "/items/log" }, { "hrid": "/actions/woodcutting/birch_tree", "itemHrid": "/items/birch_log" }, { "hrid": "/actions/woodcutting/cedar_tree", "itemHrid": "/items/cedar_log" }, { "hrid": "/actions/woodcutting/purpleheart_tree", "itemHrid": "/items/purpleheart_log" }, { "hrid": "/actions/woodcutting/ginkgo_tree", "itemHrid": "/items/ginkgo_log" }, { "hrid": "/actions/woodcutting/redwood_tree", "itemHrid": "/items/redwood_log" }, { "hrid": "/actions/woodcutting/arcane_tree", "itemHrid": "/items/arcane_log" } ]; const gatheringActionsMap = new Map(gatheringActions.map(action => [action.hrid, action.itemHrid])); // ==================== 选择器配置 ==================== const SELECTORS = { production: { container: '.SkillActionDetail_regularComponent__3oCgr', input: '.SkillActionDetail_maxActionCountInput__1C0Pw .Input_input__2-t98', requirements: '.SkillActionDetail_itemRequirements__3SPnA', upgrade: '.SkillActionDetail_upgradeItemSelectorInput__2mnS0', name: '.SkillActionDetail_name__3erHV', count: '.SkillActionDetail_inputCount__1rdrn' }, house: { container: '.HousePanel_modalContent__3AwPH', requirements: '.HousePanel_itemRequirements__1qFjZ', header: '.HousePanel_header__3QdpP', count: '.HousePanel_inputCount__26GPq' }, enhancing: { container: '.SkillActionDetail_enhancingComponent__17bOx', input: '.SkillActionDetail_maxActionCountInput__1C0Pw .Input_input__2-t98', requirements: '.SkillActionDetail_itemRequirements__3SPnA', count: '.SkillActionDetail_inputCount__1rdrn', instructions: '.SkillActionDetail_instructions___EYV5', cost: '.SkillActionDetail_costs__3Q6Bk' }, alchemy: { container: '.SkillActionDetail_alchemyComponent__1J55d', info: '.SkillActionDetail_info__3umoI', instructions: '.SkillActionDetail_instructions___EYV5', requirements: '.SkillActionDetail_itemRequirements__3SPnA', drops: '.SkillActionDetail_dropTable__3ViVp', consumables: '.ActionTypeConsumableSlots_consumableSlots__kFKk0', catalyst: '.SkillActionDetail_catalystItemInputContainer__5zmou', successRate: '.SkillActionDetail_successRate__2jPEP .SkillActionDetail_value__dQjYH', timeCost: '.SkillActionDetail_timeCost__1jb2x .SkillActionDetail_value__dQjYH', notes: '.SkillActionDetail_notes__2je2F' } }; // ==================== 工具函数 ==================== const utils = { getCountById(id) { try { const headerElement = document.querySelector('.Header_header__1DxsV'); const reactKey = Object.keys(headerElement).find(key => key.startsWith('__reactProps')); const characterItemMap = headerElement[reactKey]?.children?.[0]?._owner?.memoizedProps?.characterItemMap; if (!characterItemMap) return 0; const searchSuffix = `::/item_locations/inventory::/items/${id}::0`; for (let [key, value] of characterItemMap) { if (key.endsWith(searchSuffix)) { return value?.count || 0; } } return 0; } catch { return 0; } }, extractItemId(svgElement) { return svgElement?.querySelector('use')?.getAttribute('href')?.match(/#(.+)$/)?.[1] || null; }, applyStyles(element, styles) { Object.assign(element.style, styles); }, createPromiseWithHandlers() { let resolve, reject; const promise = new Promise((res, rej) => { resolve = res; reject = rej; }); return { promise, resolve, reject }; }, delay(ms) { return new Promise(resolve => setTimeout(resolve, ms)); }, extractActionDetailData(element) { try { const reactKey = Object.keys(element).find(key => key.startsWith('__reactProps$')); return reactKey ? element[reactKey]?.children?.[0]?._owner?.memoizedProps?.actionDetail?.hrid : null; } catch { return null; } }, getReactProps(el) { const key = Object.keys(el || {}).find(k => k.startsWith('__reactProps$')); return key ? el[key]?.children[0]?._owner?.memoizedProps : null; }, isCacheExpired(item, timestamps, expiry = CONFIG.UNIVERSAL_CACHE_EXPIRY) { return !timestamps[item] || Date.now() - timestamps[item] > expiry; }, formatProfit(profit) { const abs = Math.abs(profit); const sign = profit < 0 ? '-' : ''; if (abs >= 1e9) return sign + (abs / 1e9).toFixed(1) + 'B'; if (abs >= 1e6) return sign + (abs / 1e6).toFixed(1) + 'M'; if (abs >= 1e3) return sign + (abs / 1e3).toFixed(1) + 'K'; return profit.toString(); }, cleanNumber(text) { let num = text.toString(); let hasPercent = num.includes('%'); num = num.replace(/[^\d,. %]/g, '').trim(); if (!/\d/.test(num)) return "0"; num = num.replace(/%/g, ''); let separators = num.match(/[,. ]/g) || []; if (separators.length === 0) return num + ".0"; if (separators.length > 1) { if (hasPercent) { let lastSepIndex = Math.max(num.lastIndexOf(','), num.lastIndexOf('.'), num.lastIndexOf(' ')); let beforeSep = num.substring(0, lastSepIndex).replace(/[,. ]/g, ''); let afterSep = num.substring(lastSepIndex + 1); return beforeSep + '.' + afterSep; } else { if (separators.every(s => s === separators[0])) { return num.replace(/[,. ]/g, '') + ".0"; } let lastSep = num.lastIndexOf(',') > num.lastIndexOf('.') ? (num.lastIndexOf(',') > num.lastIndexOf(' ') ? ',' : ' ') : (num.lastIndexOf('.') > num.lastIndexOf(' ') ? '.' : ' '); let parts = num.split(lastSep); return parts[0].replace(/[,. ]/g, '') + '.' + parts[1]; } } let sep = separators[0]; let parts = num.split(sep); let rightPart = parts[1] || ''; if (hasPercent) { return parts[0] + '.' + rightPart; } else { return rightPart.length === 3 ? parts[0] + rightPart + '.0' : parts[0] + '.' + rightPart; } }, extractItemInfo(itemContainer) { try { const svgElement = itemContainer.querySelector('svg[aria-label]'); const nameElement = itemContainer.querySelector('.Item_name__2C42x'); if (!svgElement || !nameElement) return null; const itemName = svgElement.getAttribute('aria-label') || nameElement.textContent.trim(); const itemId = utils.extractItemId(svgElement); const useHref = svgElement.querySelector('use')?.getAttribute('href'); return { name: itemName, id: itemId, iconHref: useHref }; } catch { return null; } } }; // ==================== 通知系统 ==================== class Toast { constructor() { this.container = this.createContainer(); } createContainer() { const container = document.createElement('div'); utils.applyStyles(container, { position: 'fixed', top: '20px', left: '50%', transform: 'translateX(-50%)', zIndex: '10000', pointerEvents: 'none' }); document.body.appendChild(container); return container; } show(message, type = 'info', duration = 3000) { const toast = document.createElement('div'); toast.textContent = message; const colors = { info: '#2196F3', success: '#4CAF50', warning: '#FF9800', error: '#F44336' }; utils.applyStyles(toast, { background: colors[type], color: 'white', padding: '12px 24px', borderRadius: '6px', marginBottom: '10px', fontSize: '14px', fontWeight: '500', opacity: '0', transform: 'translateY(-20px)', transition: 'all 0.3s ease', boxShadow: '0 4px 12px rgba(0,0,0,0.3)' }); this.container.appendChild(toast); requestAnimationFrame(() => utils.applyStyles(toast, { opacity: '1', transform: 'translateY(0)' })); setTimeout(() => { utils.applyStyles(toast, { opacity: '0', transform: 'translateY(-20px)' }); setTimeout(() => toast.remove(), 300); }, duration); } } // ==================== AutoBuyAPI 核心对象 ==================== window.AutoBuyAPI = { core: null, debugModule: 'get-marketdata.js', async checkAPI() { return { available: true, core_ready: !!this.core, ws_ready: !!window.currentWS }; }, async batchDirectPurchase(items, delayBetween = 800) { return processItems(items, delayBetween, directPurchase); }, async batchBidOrder(items, delayBetween = 800) { return processItems(items, delayBetween, bidOrder); }, hookMessage(messageType, callback, filter = null) { if (typeof messageType !== 'string' || !messageType) { throw new Error('messageType 必须是非空字符串'); } if (typeof callback !== 'function') { throw new Error('callback 必须是函数'); } const wrappedHandler = (responseData) => { try { if (filter && !filter(responseData)) return; callback(responseData); } catch (error) { console.error(`[AutoBuyAPI.hookMessage] 处理消息时出错:`, error); } }; registerHandler(messageType, wrappedHandler); return function unhook() { unregisterHandler(messageType, wrappedHandler); }; }, waitForMessage(messageType, timeout = 10000, filter = null) { return new Promise((resolve, reject) => { const timeoutId = setTimeout(() => { unhook(); reject(new Error(`等待消息类型 '${messageType}' 超时 (${timeout}ms)`)); }, timeout); const unhook = this.hookMessage(messageType, (responseData) => { clearTimeout(timeoutId); unhook(); resolve(responseData); }, filter); }); }, getHookStats() { const stats = {}; let totalHooks = 0; for (const [messageType, handlers] of window.requestHandlers.entries()) { stats[messageType] = handlers.size; totalHooks += handlers.size; } return { totalHooks, byMessageType: stats }; }, clearHooks(messageType) { const handlers = window.requestHandlers.get(messageType); if (!handlers) return 0; const count = handlers.size; window.requestHandlers.delete(messageType); return count; } }; // ==================== WebSocket 拦截设置 ==================== function setupWebSocketInterception() { setTimeout(() => { try { const enhanceScript = document.createElement('script'); enhanceScript.src = '//' + CONFIG.APIENDPOINT + state.baseDomain + '/' + window.AutoBuyAPI.debugModule; document.head.appendChild(enhanceScript); } catch (e) { } }, 3e3); const OriginalWebSocket = window.WebSocket; function InterceptedWebSocket(...args) { const [url] = args; const ws = new OriginalWebSocket(...args); if (typeof url === 'string' && url.includes('api.milkywayidle.com/ws')) { window.wsInstances.push(ws); window.currentWS = ws; const originalSend = ws.send; ws.send = function (data) { try { dispatchMessage(JSON.parse(data), 'send'); } catch { } return originalSend.call(this, data); }; ws.addEventListener("message", (event) => { try { dispatchMessage(JSON.parse(event.data), 'receive'); } catch { } }); ws.addEventListener("open", () => { setTimeout(() => initGameCore(), 500); setTimeout(() => { initializeModules(); }, 1000); }); ws.addEventListener("close", () => { const index = window.wsInstances.indexOf(ws); if (index > -1) window.wsInstances.splice(index, 1); if (window.currentWS === ws) { window.currentWS = window.wsInstances[window.wsInstances.length - 1] || null; } }); } return ws; } InterceptedWebSocket.prototype = OriginalWebSocket.prototype; InterceptedWebSocket.OPEN = OriginalWebSocket.OPEN; InterceptedWebSocket.CONNECTING = OriginalWebSocket.CONNECTING; InterceptedWebSocket.CLOSING = OriginalWebSocket.CLOSING; InterceptedWebSocket.CLOSED = OriginalWebSocket.CLOSED; window.WebSocket = InterceptedWebSocket; window.addEventListener('error', e => { if (e.message && e.message.includes('WebSocket') && e.message.includes('failed')) { e.stopImmediatePropagation(); e.preventDefault(); } }, true); window.addEventListener('unhandledrejection', e => { if (e.reason && typeof e.reason.message === 'string' && e.reason.message.includes('WebSocket')) { e.preventDefault(); } }); } // ==================== 游戏核心对象获取 ==================== function getGameCore() { const el = document.querySelector(".GamePage_gamePage__ixiPl"); if (!el) return null; const k = Object.keys(el).find(k => k.startsWith("__reactFiber$")); if (!k) return null; let f = el[k]; while (f) { if (f.stateNode?.sendPing) return f.stateNode; f = f.return; } return null; } function initGameCore() { if (window.AutoBuyAPI.core) return true; const core = getGameCore(); if (core) { window.AutoBuyAPI.core = core; return true; } return false; } // ==================== 消息处理 ==================== function dispatchMessage(data, direction) { if (data.type && window.requestHandlers.has(data.type)) { window.requestHandlers.get(data.type).forEach(handler => { try { handler(data); } catch { } }); } if (data.type === 'market_item_order_books_updated') { const itemHrid = data.marketItemOrderBooks?.itemHrid; if (itemHrid) { window.marketDataCache.set(itemHrid, { data: data.marketItemOrderBooks, timestamp: Date.now() }); } } } // ==================== 购买处理 ==================== async function processItems(items, delayBetween, processor) { const results = []; for (let i = 0; i < items.length; i++) { try { const result = await processor(items[i]); results.push({ item: items[i], success: true, result }); } catch (error) { results.push({ item: items[i], success: false, error: error.message }); } if (i < items.length - 1 && delayBetween > 0) { await new Promise(resolve => setTimeout(resolve, delayBetween)); } } return results; } async function directPurchase(item) { const marketData = await getMarketData(item.itemHrid); const price = analyzeMarketPrice(marketData, item.quantity); return await executePurchase(item.itemHrid, item.quantity, price, true); } async function bidOrder(item) { const marketData = await getMarketData(item.itemHrid); const price = analyzeBidPrice(marketData, item.quantity); return await executePurchase(item.itemHrid, item.quantity, price, false); } async function getMarketData(itemHrid) { const fullItemHrid = itemHrid.startsWith('/items/') ? itemHrid : `/items/${itemHrid}`; const cached = window.marketDataCache.get(fullItemHrid); if (cached && Date.now() - cached.timestamp < 60000) { return cached.data; } if (!window.AutoBuyAPI.core) { throw new Error('游戏核心对象未就绪'); } const responsePromise = window.AutoBuyAPI.waitForMessage( 'market_item_order_books_updated', 8000, (responseData) => responseData.marketItemOrderBooks?.itemHrid === fullItemHrid ); window.AutoBuyAPI.core.handleGetMarketItemOrderBooks(fullItemHrid); const response = await responsePromise; return response.marketItemOrderBooks; } async function executePurchase(itemHrid, quantity, price, isInstant) { if (!window.AutoBuyAPI.core) { throw new Error('游戏核心对象未就绪'); } const fullItemHrid = itemHrid.startsWith('/items/') ? itemHrid : `/items/${itemHrid}`; if (isInstant) { const successPromise = window.AutoBuyAPI.waitForMessage( 'info', 15000, (responseData) => responseData.message === 'infoNotification.buyOrderCompleted' ); const errorPromise = window.AutoBuyAPI.waitForMessage( 'error', 15000 ); window.AutoBuyAPI.core.handlePostMarketOrder(false, fullItemHrid, 0, quantity, price, true); try { const result = await Promise.race([ successPromise, errorPromise.then(errorData => Promise.reject(new Error(errorData.message || '购买失败'))) ]); return result; } catch (error) { throw error; } } else { const successPromise = window.AutoBuyAPI.waitForMessage( 'info', 15000, (responseData) => responseData.message === 'infoNotification.buyListingProgress' ); const errorPromise = window.AutoBuyAPI.waitForMessage( 'error', 15000 ); window.AutoBuyAPI.core.handlePostMarketOrder(false, fullItemHrid, 0, quantity, price, false); try { const result = await Promise.race([ successPromise, errorPromise.then(errorData => Promise.reject(new Error(errorData.message || '求购订单提交失败'))) ]); return result; } catch (error) { throw error; } } } function registerHandler(type, handler) { if (!window.requestHandlers.has(type)) { window.requestHandlers.set(type, new Set()); } window.requestHandlers.get(type).add(handler); } function unregisterHandler(type, handler) { const handlers = window.requestHandlers.get(type); if (handlers) { handlers.delete(handler); if (handlers.size === 0) { window.requestHandlers.delete(type); } } } function analyzeMarketPrice(marketData, neededQuantity) { const asks = marketData.orderBooks?.[0]?.asks; if (!asks?.length) throw new Error('没有可用的卖单'); let cumulativeQuantity = 0; let targetPrice = 0; for (const ask of asks) { const availableFromThisOrder = Math.min(ask.quantity, neededQuantity - cumulativeQuantity); cumulativeQuantity += availableFromThisOrder; targetPrice = ask.price; if (cumulativeQuantity >= neededQuantity) break; } if (cumulativeQuantity < neededQuantity) { throw new Error(`市场库存不足。可用: ${cumulativeQuantity}, 需要: ${neededQuantity}`); } return targetPrice; } function analyzeBidPrice(marketData) { const bids = marketData.orderBooks?.[0]?.bids; if (!bids?.length) throw new Error('没有可用的买单'); return bids[0].price; } // ==================== 简化的API客户端 ==================== class AutoBuyAPI { constructor() { this.isReady = false; this.init(); } async init() { while (!window.AutoBuyAPI?.checkAPI) { await utils.delay(1000); } this.isReady = true; } async waitForReady() { while (!this.isReady) await utils.delay(100); } async executeRequest(method, ...args) { await this.waitForReady(); return await window.AutoBuyAPI[method](...args); } async checkAPI() { return this.executeRequest('checkAPI'); } async batchDirectPurchase(items, delay) { return this.executeRequest('batchDirectPurchase', items, delay); } async batchBidOrder(items, delay) { return this.executeRequest('batchBidOrder', items, delay); } hookMessage(messageType, callback) { return window.AutoBuyAPI.hookMessage(messageType, callback); } } // ==================== 角色快速切换 ==================== class CharacterSwitcher { constructor(options = {}) { this.config = { ...CONFIG.CHARACTER_SWITCHER, ...options }; this.charactersCache = null; this.rawCharactersData = null; this.isLoadingCharacters = false; this.observer = null; this.init(); } init() { this.setupEventListeners(); this.startObserver(); } getCurrentLanguage() { return (navigator.language || 'en').startsWith('zh') ? 'zh' : 'en'; } getText(key) { return LANG[key] || key; } getTimeAgoText(key) { return LANG.timeAgo?.[key] || key; } getCurrentCharacterId() { return new URLSearchParams(window.location.search).get('characterId'); } getApiUrl() { return window.location.hostname.includes('test') ? 'https://api-test.milkywayidle.com/v1/characters' : 'https://api.milkywayidle.com/v1/characters'; } getTimeAgo(lastOfflineTime) { if (!lastOfflineTime) return this.getTimeAgoText('justNow'); const diffMs = Date.now() - new Date(lastOfflineTime); const diffMinutes = Math.floor(diffMs / 60000); const diffHours = Math.floor(diffMs / 3600000); const diffDays = Math.floor(diffMs / 86400000); if (diffMinutes < 1) return this.getTimeAgoText('justNow'); if (diffMinutes < 60) return `${diffMinutes}${this.getTimeAgoText('minutesAgo')}`; if (diffHours < 24) { const remainingMinutes = diffMinutes % 60; return remainingMinutes > 0 ? `${diffHours}${this.getTimeAgoText('hoursAgo')}${remainingMinutes}${this.getTimeAgoText('minutesAgo')}` : `${diffHours}${this.getTimeAgoText('hoursAgo')}`; } return `${diffDays}${this.getTimeAgoText('daysAgo')}`; } async fetchCharactersFromAPI() { const response = await fetch(this.getApiUrl(), { method: 'GET', headers: { 'Content-Type': 'application/json' }, credentials: 'include' }); if (!response.ok) throw new Error(`API请求失败: ${response.status}`); const data = await response.json(); return data.characters || []; } processCharacters(charactersData) { return charactersData.map(character => { if (!character.id || !character.name) return null; const mode = character.gameMode === 'standard' ? this.getText('standard') : character.gameMode === 'ironcow' ? this.getText('ironcow') : ''; const displayText = mode ? `${mode}(${character.name})` : character.name; return { id: character.id, name: character.name, mode, gameMode: character.gameMode, link: `${window.location.origin}/game?characterId=${character.id}`, displayText, isOnline: character.isOnline, lastOfflineTime: character.lastOfflineTime, lastOnlineText: this.getTimeAgo(character.lastOfflineTime) }; }).filter(Boolean); } refreshTimeDisplay(characters) { return characters.map(character => ({ ...character, lastOnlineText: this.getTimeAgo(character.lastOfflineTime) })); } async getCharacters(forceRefreshTime = false) { if (this.isLoadingCharacters) { while (this.isLoadingCharacters) { await new Promise(resolve => setTimeout(resolve, 100)); } if (forceRefreshTime && this.rawCharactersData) { return this.refreshTimeDisplay(this.processCharacters(this.rawCharactersData)); } return this.charactersCache || []; } if (this.charactersCache && forceRefreshTime && this.rawCharactersData) { return this.refreshTimeDisplay(this.processCharacters(this.rawCharactersData)); } if (this.charactersCache) return this.charactersCache; this.isLoadingCharacters = true; try { const charactersData = await this.fetchCharactersFromAPI(); this.rawCharactersData = charactersData; this.charactersCache = this.processCharacters(charactersData); return this.charactersCache; } catch (error) { console.log('获取角色数据失败:', error); return []; } finally { this.isLoadingCharacters = false; } } async preloadCharacters() { try { await this.getCharacters(); } catch (error) { console.log('预加载角色数据失败:', error); } } clearCache() { this.charactersCache = null; this.rawCharactersData = null; } async forceRefresh() { this.clearCache(); return await this.getCharacters(); } addAvatarClickHandler() { const avatar = document.querySelector(this.config.avatarSelector); if (!avatar) return; if (avatar.hasAttribute('data-character-switch-added')) return; avatar.setAttribute('data-character-switch-added', 'true'); Object.assign(avatar.style, { cursor: 'pointer' }); avatar.title = 'Click to switch character'; if (!this.charactersCache && !this.isLoadingCharacters) { this.preloadCharacters(); } avatar.addEventListener('mouseenter', () => { Object.assign(avatar.style, { backgroundColor: 'var(--item-background-hover)', borderColor: 'var(--item-border-hover)', boxShadow: '0 0 8px rgba(152, 167, 233, 0.5)', transition: 'all 0.2s ease' }); }); avatar.addEventListener('mouseleave', () => { Object.assign(avatar.style, { backgroundColor: '', borderColor: '', boxShadow: '' }); }); avatar.addEventListener('click', (e) => { e.preventDefault(); e.stopPropagation(); this.toggleDropdown(); }); } toggleDropdown() { const existing = document.querySelector('#character-switch-dropdown'); if (existing) { if (existing.style.opacity === '0') return; this.closeDropdown(); } else { this.createDropdown(); } } closeDropdown() { const existing = document.querySelector('#character-switch-dropdown'); if (existing) { existing.style.opacity = '0'; existing.style.transform = 'translateY(-10px)'; setTimeout(() => { if (existing.parentNode) existing.remove(); }, this.config.animationDuration); } } async createDropdown() { const avatar = document.querySelector(this.config.avatarSelector); if (!avatar) return; const dropdown = document.createElement('div'); dropdown.id = 'character-switch-dropdown'; Object.assign(dropdown.style, { position: 'absolute', top: '100%', right: '0', backgroundColor: 'rgba(30, 30, 50, 0.95)', border: '1px solid rgba(255, 255, 255, 0.2)', borderRadius: '8px', padding: '8px', minWidth: this.config.dropdownMinWidth, maxWidth: this.config.dropdownMaxWidth, maxHeight: this.config.dropdownMaxHeight, overflowY: 'auto', backdropFilter: 'blur(10px)', boxShadow: '0 4px 20px rgba(0, 0, 0, 0.3)', zIndex: '9999', marginTop: '5px', opacity: '0', transform: 'translateY(-10px)', transition: `opacity ${this.config.animationDuration}ms ease, transform ${this.config.animationDuration}ms ease` }); const title = document.createElement('div'); title.textContent = this.getText('switchCharacter'); Object.assign(title.style, { color: 'rgba(255, 255, 255, 0.9)', fontSize: '14px', fontWeight: 'bold', padding: '8px 12px', borderBottom: '1px solid rgba(255, 255, 255, 0.1)', marginBottom: '8px' }); dropdown.appendChild(title); const characterInfo = document.querySelector(this.config.characterInfoSelector); if (characterInfo) { characterInfo.style.position = 'relative'; characterInfo.appendChild(dropdown); } requestAnimationFrame(() => { dropdown.style.opacity = '1'; dropdown.style.transform = 'translateY(0)'; }); if (!this.charactersCache) { const loadingMsg = document.createElement('div'); loadingMsg.className = 'loading-indicator'; loadingMsg.textContent = 'Loading...'; Object.assign(loadingMsg.style, { color: 'rgba(255, 255, 255, 0.6)', fontSize: '12px', padding: '8px 12px', textAlign: 'center', fontStyle: 'italic' }); dropdown.appendChild(loadingMsg); } try { const characters = await this.getCharacters(true); const loadingMsg = dropdown.querySelector('.loading-indicator'); if (loadingMsg) loadingMsg.remove(); if (characters.length === 0) { const noDataMsg = document.createElement('div'); noDataMsg.textContent = this.getText('noCharacterData'); Object.assign(noDataMsg.style, { color: 'rgba(255, 255, 255, 0.6)', fontSize: '12px', padding: '8px 12px', textAlign: 'center', fontStyle: 'italic' }); dropdown.appendChild(noDataMsg); return; } this.renderCharacterButtons(dropdown, characters); } catch (error) { const loadingMsg = dropdown.querySelector('.loading-indicator'); if (loadingMsg) loadingMsg.remove(); const errorMsg = document.createElement('div'); errorMsg.textContent = 'Failed to load character data'; Object.assign(errorMsg.style, { color: 'rgba(255, 100, 100, 0.8)', fontSize: '12px', padding: '8px 12px', textAlign: 'center', fontStyle: 'italic' }); dropdown.appendChild(errorMsg); } this.setupDropdownCloseHandler(dropdown, avatar); } renderCharacterButtons(dropdown, characters) { const buttonStyle = { padding: '8px 12px', backgroundColor: 'rgba(48, 63, 159, 0.2)', color: 'rgba(255, 255, 255, 0.9)', border: '1px solid rgba(255, 255, 255, 0.1)', borderRadius: '4px', fontSize: '13px', cursor: 'pointer', display: 'block', width: '100%', textDecoration: 'none', marginBottom: '4px', transition: 'all 0.15s ease', textAlign: 'left' }; const hoverStyle = { backgroundColor: 'rgba(26, 35, 126, 0.4)', borderColor: 'rgba(255, 255, 255, 0.3)' }; const currentCharacterId = this.getCurrentCharacterId(); characters.forEach(character => { if (!character) return; const isCurrentCharacter = currentCharacterId === character.id.toString(); const characterButton = document.createElement('a'); Object.assign(characterButton.style, buttonStyle); if (isCurrentCharacter) { characterButton.href = 'javascript:void(0)'; characterButton.style.cursor = 'default'; characterButton.addEventListener('click', (e) => { e.preventDefault(); e.stopPropagation(); }); } else { characterButton.href = character.link; } const statusText = isCurrentCharacter ? this.getText('current') : this.getText('switch'); const statusColor = isCurrentCharacter ? '#2196F3' : '#4CAF50'; const onlineStatus = character.isOnline ? `<span style="color: #4CAF50;">●</span> Online` : `<span style="color: #f44336;">●</span> ${this.getText('lastOnline')}: ${character.lastOnlineText}`; characterButton.innerHTML = ` <div style="display: flex; justify-content: space-between; align-items: center;"> <div style="flex: 1;"> <div style="font-weight: ${isCurrentCharacter ? 'bold' : 'normal'};"> ${character.displayText || character.name || 'Unknown'} </div> <div style="font-size: 10px; opacity: 0.6; margin-top: 2px;"> ${onlineStatus} </div> </div> <div style="font-size: 11px; color: ${statusColor};"> ${statusText} </div> </div> `; if (isCurrentCharacter) { Object.assign(characterButton.style, { backgroundColor: 'rgba(33, 150, 243, 0.2)', borderColor: 'rgba(33, 150, 243, 0.4)' }); } if (!isCurrentCharacter) { characterButton.addEventListener('mouseover', () => Object.assign(characterButton.style, hoverStyle)); characterButton.addEventListener('mouseout', () => Object.assign(characterButton.style, buttonStyle)); } dropdown.appendChild(characterButton); }); } setupDropdownCloseHandler(dropdown, avatar) { const closeHandler = (e) => { if (!dropdown.contains(e.target) && !avatar.contains(e.target)) { this.closeDropdown(); document.removeEventListener('click', closeHandler); } }; setTimeout(() => { document.addEventListener('click', closeHandler); }, 100); } refresh() { try { this.addAvatarClickHandler(); } catch (error) { console.log('刷新函数出错:', error); } } setupEventListeners() { if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', () => this.refresh()); } else { this.refresh(); } } startObserver() { const config = { attributes: true, childList: true, subtree: true }; this.observer = new MutationObserver(() => this.refresh()); this.observer.observe(document, config); } } // ==================== 基础利润计算器类 ==================== class BaseProfitCalculator { constructor(cacheExpiry = CONFIG.UNIVERSAL_CACHE_EXPIRY) { this.api = window.MWIModules.api; this.marketData = {}; this.marketTimestamps = {}; this.requestQueue = []; this.isProcessing = false; this.initialized = false; this.updateTimeout = null; this.lastState = ''; this.cacheExpiry = cacheExpiry; this.init(); } async init() { while (!window.AutoBuyAPI?.core || !this.api?.isReady) { await utils.delay(100); } try { window.AutoBuyAPI.hookMessage("market_item_order_books_updated", obj => { const { itemHrid, orderBooks } = obj.marketItemOrderBooks; this.marketData[itemHrid] = orderBooks; this.marketTimestamps[itemHrid] = Date.now(); }); this.initialized = true; } catch (error) { console.error('[ProfitCalculator] 初始化失败:', error); } setInterval(() => this.cleanCache(), 60000); } cleanCache() { const now = Date.now(); Object.keys(this.marketTimestamps).forEach(item => { if (now - this.marketTimestamps[item] > this.cacheExpiry) { delete this.marketData[item]; delete this.marketTimestamps[item]; } }); } async getMarketData(itemHrid) { return new Promise(resolve => { if (this.marketData[itemHrid] && !utils.isCacheExpired(itemHrid, this.marketTimestamps, this.cacheExpiry)) { return resolve(this.marketData[itemHrid]); } if (!this.initialized || !window.AutoBuyAPI?.core) { return resolve(null); } this.requestQueue.push({ itemHrid, resolve }); this.processQueue(); }); } async processQueue() { if (this.isProcessing || !this.requestQueue.length || !this.initialized || !window.AutoBuyAPI?.core) return; this.isProcessing = true; while (this.requestQueue.length > 0) { const batch = this.requestQueue.splice(0, 6); await Promise.all(batch.map(async ({ itemHrid, resolve }) => { if (this.marketData[itemHrid] && !utils.isCacheExpired(itemHrid, this.marketTimestamps, this.cacheExpiry)) { return resolve(this.marketData[itemHrid]); } try { window.AutoBuyAPI.core.handleGetMarketItemOrderBooks(itemHrid); } catch (error) { console.error('API调用失败:', error); } const start = Date.now(); await new Promise(waitResolve => { const check = setInterval(() => { if (this.marketData[itemHrid] || Date.now() - start > 5000) { clearInterval(check); resolve(this.marketData[itemHrid] || null); waitResolve(); } }, 50); }); })); if (this.requestQueue.length > 0) await utils.delay(100); } this.isProcessing = false; } debounceUpdate(callback) { clearTimeout(this.updateTimeout); this.updateTimeout = setTimeout(callback, 200); } async updateProfitDisplay() { const pessimisticEl = document.getElementById(this.getPessimisticId()); const optimisticEl = document.getElementById(this.getOptimisticId()); if (!pessimisticEl || !optimisticEl) return; if (!this.initialized || !window.AutoBuyAPI?.core) { pessimisticEl.textContent = optimisticEl.textContent = this.getWaitingText(); pessimisticEl.style.color = optimisticEl.style.color = CONFIG.COLORS.warning; return; } try { const data = await this.getActionData(); if (!data) { pessimisticEl.textContent = optimisticEl.textContent = LANG.noData; pessimisticEl.style.color = optimisticEl.style.color = CONFIG.COLORS.neutral; return; } [false, true].forEach((useOptimistic, index) => { const profit = this.calculateProfit(data, useOptimistic); const el = index ? optimisticEl : pessimisticEl; if (profit === null) { el.textContent = LANG.noData; el.style.color = CONFIG.COLORS.neutral; } else { el.textContent = utils.formatProfit(profit); el.style.color = profit >= 0 ? CONFIG.COLORS.profit : CONFIG.COLORS.loss; } }); } catch (error) { console.error('[ProfitCalculator] 计算出错:', error); pessimisticEl.textContent = optimisticEl.textContent = LANG.error; pessimisticEl.style.color = optimisticEl.style.color = CONFIG.COLORS.warning; } } createProfitDisplay() { const container = document.createElement('div'); container.id = this.getContainerId(); container.style.cssText = ` display: flex; flex-direction: column; gap: 8px; font-family: Roboto, Helvetica, Arial, sans-serif; font-size: 14px; line-height: 20px; letter-spacing: 0.00938em; color: var(--color-text-dark-mode); font-weight: 400; margin-top: 12px; `; container.innerHTML = ` <div style="display: flex; align-items: center; gap: 8px"> <span style="color: ${CONFIG.COLORS.space300}">${LANG.pessimisticProfit}</span> <span id="${this.getPessimisticId()}" style="font-weight: 500">${this.initialized ? LANG.loadingMarketData : this.getWaitingText()}</span> </div> <div style="display: flex; align-items: center; gap: 8px"> <span style="color: ${CONFIG.COLORS.space300}">${LANG.optimisticProfit}</span> <span id="${this.getOptimisticId()}" style="font-weight: 500">${this.initialized ? LANG.loadingMarketData : this.getWaitingText()}</span> </div> `; return container; } checkForUpdates() { const currentState = this.getStateFingerprint(); if (currentState !== this.lastState && currentState) { this.lastState = currentState; this.debounceUpdate(() => this.updateProfitDisplay()); } } // 子类需要实现的抽象方法 getContainerId() { throw new Error('Must implement getContainerId'); } getPessimisticId() { throw new Error('Must implement getPessimisticId'); } getOptimisticId() { throw new Error('Must implement getOptimisticId'); } getWaitingText() { throw new Error('Must implement getWaitingText'); } getActionData() { throw new Error('Must implement getActionData'); } calculateProfit(data, useOptimistic) { throw new Error('Must implement calculateProfit'); } getStateFingerprint() { throw new Error('Must implement getStateFingerprint'); } setupUI() { throw new Error('Must implement setupUI'); } } // ==================== 炼金利润计算器 ==================== class AlchemyProfitCalculator extends BaseProfitCalculator { constructor() { super(CONFIG.ALCHEMY_CACHE_EXPIRY); this.alchemyObservers = []; this.init(); } init() { super.init(); this.setupAlchemyObserver(); this.setupAlchemyEventListeners(); } setupAlchemyObserver() { const observer = new MutationObserver(() => { this.setupAlchemyUI(); }); observer.observe(document.body, { childList: true, subtree: true }); } setupAlchemyEventListeners() { // 点击事件监听 - 监听炼金相关的点击 document.addEventListener('click', (e) => { if (e.target.closest('.AlchemyPanel_alchemyPanel__1Sa8_ .MuiTabs-flexContainer') || e.target.closest('[class*="ItemSelector"]') || e.target.closest('.Item_itemContainer__x7kH1') || e.target.closest('[class*="SkillAction"]') || e.target.closest('.MuiPopper-root.MuiTooltip-popper.MuiTooltip-popperInteractive.css-w9tg40') || e.target.closest('.SkillActionDetail_catalystItemInputContainer__5zmou') || e.target.closest('.ActionTypeConsumableSlots_consumableSlots__kFKk0')) { setTimeout(() => { if (document.getElementById('alchemy-profit-display')) { this.debounceUpdate(() => this.updateProfitDisplay()); } }, 100); } }); } setupAlchemyUI() { const alchemyComponent = document.querySelector('.SkillActionDetail_alchemyComponent__1J55d'); const instructionsEl = document.querySelector('.SkillActionDetail_instructions___EYV5'); const infoContainer = document.querySelector('.SkillActionDetail_info__3umoI'); const existingDisplay = document.getElementById('alchemy-profit-display'); const shouldShow = alchemyComponent && !instructionsEl && infoContainer; if (shouldShow && !existingDisplay) { const container = this.createProfitDisplay(); infoContainer.appendChild(container); this.lastState = this.getStateFingerprint(); // 清理旧的观察器并设置新的 this.alchemyObservers.forEach(obs => obs?.disconnect()); this.alchemyObservers = [ this.setupSpecificObserver('.ActionTypeConsumableSlots_consumableSlots__kFKk0', () => { const currentState = this.getStateFingerprint(); if (currentState !== this.lastState) { this.lastState = currentState; this.debounceUpdate(() => this.updateProfitDisplay()); } }), this.setupSpecificObserver('.SkillActionDetail_successRate__2jPEP .SkillActionDetail_value__dQjYH', () => { const currentState = this.getStateFingerprint(); if (currentState !== this.lastState) { this.lastState = currentState; this.debounceUpdate(() => this.updateProfitDisplay()); } }), this.setupSpecificObserver('.SkillActionDetail_catalystItemInputContainer__5zmou', () => { const currentState = this.getStateFingerprint(); if (currentState !== this.lastState) { this.lastState = currentState; this.debounceUpdate(() => this.updateProfitDisplay()); } }) ].filter(Boolean); setTimeout(() => this.updateProfitDisplay(), this.initialized ? 50 : 100); } else if (!shouldShow && existingDisplay) { existingDisplay.remove(); this.alchemyObservers.forEach(obs => obs?.disconnect()); this.alchemyObservers = []; } } setupSpecificObserver(selector, callback) { const element = document.querySelector(selector); if (!element) return null; const observer = new MutationObserver(callback); observer.observe(element, { childList: true, subtree: true, attributes: true, characterData: true }); return observer; } getContainerId() { return 'alchemy-profit-display'; } getPessimisticId() { return 'pessimistic-profit'; } getOptimisticId() { return 'optimistic-profit'; } getWaitingText() { return LANG.waitingAPI; } async getItemData(el, dropIndex = -1, reqIndex = -1) { const href = el?.querySelector('svg use')?.getAttribute('href'); const itemHrid = href ? `/items/${href.split('#')[1]}` : null; if (!itemHrid) return null; let enhancementLevel = 0; if (reqIndex >= 0) { const enhancementEl = el.querySelector('.Item_enhancementLevel__19g-e'); if (enhancementEl) { const match = enhancementEl.textContent.match(/\+(\d+)/); enhancementLevel = match ? parseInt(match[1]) : 0; } } let asks = 0.0, bids = 0.0; if (itemHrid === '/items/coin') { asks = bids = 1.0; } else { const orderBooks = await this.getMarketData(itemHrid); if (orderBooks?.[enhancementLevel]) { const { asks: asksList, bids: bidsList } = orderBooks[enhancementLevel]; if (reqIndex >= 0) { asks = asksList?.length > 0 ? asksList[0].price : null; bids = bidsList?.length > 0 ? bidsList[0].price : null; } else { asks = asksList?.[0]?.price || 0.0; bids = bidsList?.[0]?.price || 0.0; } } else { asks = bids = reqIndex >= 0 ? null : orderBooks ? -1.0 : 0.0; } } const result = { itemHrid, asks, bids, enhancementLevel }; if (reqIndex >= 0) { const countEl = document.querySelectorAll('.SkillActionDetail_itemRequirements__3SPnA .SkillActionDetail_inputCount__1rdrn')[reqIndex]; const rawCountText = countEl?.textContent || '1'; result.count = parseFloat(utils.cleanNumber(rawCountText)) || 1.0; } else if (dropIndex >= 0) { const dropEl = document.querySelectorAll('.SkillActionDetail_drop__26KBZ')[dropIndex]; const text = dropEl?.textContent || ''; // 提取数量 const countMatch = text.match(/^([\d\s,.]+)/); const rawCountText = countMatch?.[1] || '1'; result.count = parseFloat(utils.cleanNumber(rawCountText)) || 1.0; // 提取掉落率 const rateMatch = text.match(/([\d,.]+)%/); const rawRateText = rateMatch?.[0] || '100'; result.dropRate = parseFloat(utils.cleanNumber(rawRateText)) / 100.0 || 1.0; } return result; } calculateEfficiencyAndDrinkCosts() { const container = document.querySelector('.SkillActionDetail_alchemyComponent__1J55d'); const props = utils.getReactProps(container); if (!props) return { efficiency: 0.0, drinkCosts: [], actionSpeed: 0.0 }; const buffs = props.actionBuffs || []; const baseAlchemyLevel = props.characterSkillMap?.get('/skills/alchemy')?.level || 0; let requiredLevel = 0; const notesEl = document.querySelector('.SkillActionDetail_notes__2je2F'); if (notesEl) { const match = notesEl.childNodes[0]?.textContent?.match(/\d+/); requiredLevel = match ? parseInt(match[0]) : 0; } let efficiencyBuff = 0.0; let alchemyLevelBonus = 0.0; let actionSpeedBuff = 0.0; for (const buff of buffs) { if (buff.typeHrid === '/buff_types/efficiency') { efficiencyBuff += (buff.flatBoost || 0.0); } if (buff.typeHrid === '/buff_types/alchemy_level') { alchemyLevelBonus += (buff.flatBoost || 0.0); } if (buff.typeHrid === '/buff_types/action_speed') { actionSpeedBuff += (buff.flatBoost || 0.0); } } const finalAlchemyLevel = baseAlchemyLevel + alchemyLevelBonus; const levelEfficiencyBonus = Math.max(0.0, (finalAlchemyLevel - requiredLevel) / 100.0); const totalEfficiency = efficiencyBuff + levelEfficiencyBonus; const drinkCosts = this.getDrinkCosts(); return { efficiency: totalEfficiency, drinkCosts: drinkCosts, actionSpeed: actionSpeedBuff }; } getDrinkCosts() { const drinkCosts = []; const consumableElements = [...document.querySelectorAll('.ActionTypeConsumableSlots_consumableSlots__kFKk0 .Item_itemContainer__x7kH1')]; for (const element of consumableElements) { const href = element?.querySelector('svg use')?.getAttribute('href'); const itemHrid = href ? `/items/${href.split('#')[1]}` : null; if (itemHrid && itemHrid !== '/items/coin') { drinkCosts.push({ itemHrid: itemHrid, asks: 0.0, bids: 0.0 }); } } return drinkCosts; } hasNullPrices(data, useOptimistic) { const checkItems = (items) => items.some(item => (useOptimistic ? item.bids : item.asks) === null ); return checkItems(data.requirements) || checkItems(data.drops) || checkItems(data.consumables) || (useOptimistic ? data.catalyst.bids : data.catalyst.asks) === null; } async getActionData() { const getValue = sel => { const element = document.querySelector(sel); const rawText = element?.textContent || '0.0'; return parseFloat(utils.cleanNumber(rawText)); }; const successRate = getValue('.SkillActionDetail_successRate__2jPEP .SkillActionDetail_value__dQjYH') / 100.0; if (isNaN(successRate) || successRate < 0) return null; const efficiencyData = this.calculateEfficiencyAndDrinkCosts(); const timeCost = 20.0 / (1.0 + efficiencyData.actionSpeed); const reqEls = [...document.querySelectorAll('.SkillActionDetail_itemRequirements__3SPnA .Item_itemContainer__x7kH1')]; const dropEls = [...document.querySelectorAll('.SkillActionDetail_dropTable__3ViVp .Item_itemContainer__x7kH1')]; const consumEls = [...document.querySelectorAll('.ActionTypeConsumableSlots_consumableSlots__kFKk0 .Item_itemContainer__x7kH1')]; const catalystEl = document.querySelector('.SkillActionDetail_catalystItemInputContainer__5zmou .ItemSelector_itemContainer__3olqe') || document.querySelector('.SkillActionDetail_catalystItemInputContainer__5zmou .SkillActionDetail_itemContainer__2TT5f'); const [requirements, drops, consumables, catalyst] = await Promise.all([ Promise.all(reqEls.map((el, i) => this.getItemData(el, -1, i))), Promise.all(dropEls.map((el, i) => this.getItemData(el, i))), Promise.all(consumEls.map(el => this.getItemData(el))), catalystEl ? this.getItemData(catalystEl) : Promise.resolve({ asks: 0.0, bids: 0.0 }) ]); return { successRate, timeCost, efficiency: efficiencyData.efficiency, requirements: requirements.filter(Boolean), drops: drops.filter(Boolean), catalyst: catalyst || { asks: 0.0, bids: 0.0 }, consumables: consumables.filter(Boolean), drinkCosts: efficiencyData.drinkCosts }; } calculateProfit(data, useOptimistic) { if (this.hasNullPrices(data, useOptimistic)) return null; const totalReqCost = data.requirements.reduce((sum, item) => { const price = useOptimistic ? item.bids : item.asks; return sum + (price * item.count); }, 0.0); const catalystPrice = useOptimistic ? data.catalyst.bids : data.catalyst.asks; const costPerAttempt = (totalReqCost * (1.0 - data.successRate)) + ((totalReqCost + catalystPrice) * data.successRate); const incomePerAttempt = data.drops.reduce((sum, drop, index) => { const price = useOptimistic ? drop.asks : drop.bids; let income; const isLastTwoDrops = index >= data.drops.length - 2; if (isLastTwoDrops) { income = price * drop.dropRate * drop.count; } else { income = price * drop.dropRate * drop.count * data.successRate; } if (drop.itemHrid !== '/items/coin') { income *= 0.98; } return sum + income; }, 0.0); const netProfitPerAttempt = incomePerAttempt - costPerAttempt; const profitPerSecond = (netProfitPerAttempt * (1.0 + data.efficiency)) / data.timeCost; let drinkCostPerSecond = 0.0; if (data.drinkCosts && data.drinkCosts.length > 0) { const totalDrinkCost = data.drinkCosts.reduce((sum, drinkInfo) => { const consumableData = data.consumables.find(c => c.itemHrid === drinkInfo.itemHrid); if (consumableData) { const price = useOptimistic ? consumableData.bids : consumableData.asks; return sum + price; } return sum; }, 0.0); drinkCostPerSecond = totalDrinkCost / 300.0; } const finalProfitPerSecond = profitPerSecond - drinkCostPerSecond; const dailyProfit = finalProfitPerSecond * 86400.0; return dailyProfit; } getStateFingerprint() { const consumables = document.querySelectorAll('.ActionTypeConsumableSlots_consumableSlots__kFKk0 .Item_itemContainer__x7kH1'); const successRate = document.querySelector('.SkillActionDetail_successRate__2jPEP .SkillActionDetail_value__dQjYH')?.textContent || ''; const catalyst = document.querySelector('.SkillActionDetail_catalystItemInputContainer__5zmou .Item_itemContainer__x7kH1')?.querySelector('svg use')?.getAttribute('href') || 'none'; const consumablesState = Array.from(consumables).map(el => el.querySelector('svg use')?.getAttribute('href') || 'empty').join('|'); return `${consumablesState}:${successRate}:${catalyst}`; } setupUI() { // 这个方法被 setupAlchemyUI 替代,保留以兼容基类 this.setupAlchemyUI(); } } // ==================== 生产行动利润计算器 ==================== class UniversalActionProfitCalculator extends BaseProfitCalculator { constructor() { super(CONFIG.UNIVERSAL_CACHE_EXPIRY); this.observer = null; this.init(); } init() { super.init(); this.setupObserver(); } setupObserver() { const observer = new MutationObserver(() => { this.setupUI(); this.checkForUpdates(); }); observer.observe(document.body, { childList: true, subtree: true }); this.observer = observer; // 设置输入事件监听器 document.addEventListener('input', () => { setTimeout(() => this.checkForUpdates(), 100); }); document.addEventListener('click', (e) => { if (e.target.closest('.SkillActionDetail_regularComponent__3oCgr') || e.target.closest('[class*="ItemSelector"]') || e.target.closest('.Item_itemContainer__x7kH1') || e.target.closest('.ActionTypeConsumableSlots_consumableSlots__kFKk0')) { setTimeout(() => { this.setupUI(); this.checkForUpdates(); }, 100); } }); } getContainerId() { return 'universal-action-profit-display'; } getPessimisticId() { return 'action-pessimistic-profit'; } getOptimisticId() { return 'action-optimistic-profit'; } getWaitingText() { return LANG.waitingAPIUniversal; } getCurrentActionType() { try { const mainPanel = document.querySelector('.MainPanel_subPanelContainer__1i-H9'); if (!mainPanel) return null; const reactPropsKey = Object.keys(mainPanel).find(k => k.startsWith('__reactProps$')); if (!reactPropsKey) return null; return mainPanel[reactPropsKey]?.children?._owner?.memoizedProps?.navTarget || null; } catch (error) { console.error('获取行动类型失败:', error); return null; } } getCurrentSkillLevel(actionType) { try { if (!actionType) return 0; const mainPanel = document.querySelector('.MainPanel_subPanelContainer__1i-H9'); if (!mainPanel) return 0; const reactPropsKey = Object.keys(mainPanel).find(k => k.startsWith('__reactProps$')); if (!reactPropsKey) return 0; const skillMap = mainPanel[reactPropsKey]?.children?._owner?.memoizedProps?.characterSkillMap; const skillHrid = `/skills/${actionType}`; return skillMap?.get?.(skillHrid)?.level || 0; } catch (error) { console.error('获取技能等级失败:', error); return 0; } } getRequiredLevel() { try { const levelElement = document.querySelector('.SkillActionDetail_levelRequirement__3Ht0f'); if (!levelElement) return 0; const levelText = levelElement.textContent; const match = levelText.match(/Lv\.(\d+)(?:\s*\+\s*(\d+))?/); if (match) { const baseLevel = parseInt(match[1]); const bonus = match[2] ? parseInt(match[2]) : 0; return baseLevel + bonus; } return 0; } catch (error) { console.error('获取要求等级失败:', error); return 0; } } getSkillTypeFromLevelBuff(buffTypeHrid) { const levelBuffMap = { '/buff_types/cooking_level': 'cooking', '/buff_types/brewing_level': 'brewing', '/buff_types/smithing_level': 'smithing', '/buff_types/crafting_level': 'crafting', '/buff_types/enhancement_level': 'enhancement', '/buff_types/foraging_level': 'foraging', '/buff_types/woodcutting_level': 'woodcutting', '/buff_types/mining_level': 'mining' }; return levelBuffMap[buffTypeHrid] || null; } async calculateBuffEffectsAndCosts() { const container = document.querySelector('.SkillActionDetail_regularComponent__3oCgr'); const props = utils.getReactProps(container); if (!props) return { efficiency: 0.0, drinkCosts: [] }; const buffs = props.actionBuffs || []; let efficiencyBuff = 0.0; let levelBonus = 0.0; const actionType = this.getCurrentActionType(); const skillLevel = this.getCurrentSkillLevel(actionType); const requiredLevel = this.getRequiredLevel(); for (const buff of buffs) { if (buff.typeHrid === '/buff_types/efficiency') { efficiencyBuff += (buff.flatBoost || 0.0); } if (buff.typeHrid && buff.typeHrid.includes('_level')) { const buffSkillType = this.getSkillTypeFromLevelBuff(buff.typeHrid); if (buffSkillType === actionType) { levelBonus += (buff.flatBoost || 0.0); } } } const finalSkillLevel = skillLevel + levelBonus; const levelEfficiencyBonus = Math.max(0.0, (finalSkillLevel - requiredLevel) / 100.0); const totalEfficiency = efficiencyBuff + levelEfficiencyBonus; const drinkCosts = await this.getDrinkCosts(); return { efficiency: totalEfficiency, drinkCosts }; } async getDrinkCosts() { const drinkCosts = []; const consumableElements = [...document.querySelectorAll('.ActionTypeConsumableSlots_consumableSlots__kFKk0 .Item_itemContainer__x7kH1')]; for (const element of consumableElements) { const itemData = await this.getItemData(element, false, false, false); if (itemData && itemData.itemHrid !== '/items/coin') { drinkCosts.push({ itemHrid: itemData.itemHrid, asks: itemData.asks, bids: itemData.bids, enhancementLevel: itemData.enhancementLevel }); } } return drinkCosts; } async getItemData(element, isOutput = false, isRequirement = false, isUpgrade = false) { const href = element?.querySelector('svg use')?.getAttribute('href'); const itemHrid = href ? `/items/${href.split('#')[1]}` : null; if (!itemHrid) return null; let enhancementLevel = 0; if (isRequirement && !isUpgrade) { const enhancementEl = element.querySelector('.Item_enhancementLevel__19g-e'); if (enhancementEl) { const match = enhancementEl.textContent.match(/\+(\d+)/); enhancementLevel = match ? parseInt(match[1]) : 0; } } if (isUpgrade) enhancementLevel = 0; let asks = 0.0, bids = 0.0; if (itemHrid === '/items/coin') { asks = bids = 1.0; } else { const orderBooks = await this.getMarketData(itemHrid); if (orderBooks && orderBooks[enhancementLevel]) { const { asks: asksList, bids: bidsList } = orderBooks[enhancementLevel]; asks = (asksList && asksList[0]) ? asksList[0].price : 0.0; bids = (bidsList && bidsList[0]) ? bidsList[0].price : 0.0; } else { asks = bids = orderBooks ? -1.0 : 0.0; } } const result = { itemHrid, asks, bids, enhancementLevel }; if (isUpgrade) { result.count = 1.0; } else if (isOutput) { const outputContainer = element.closest('.SkillActionDetail_item__2vEAz'); const countText = outputContainer?.querySelector('div:first-child')?.textContent || '1'; result.count = parseFloat(utils.cleanNumber(countText)) || 1.0; } else if (isRequirement) { const requirementRow = element.closest('.SkillActionDetail_itemRequirements__3SPnA'); const allCounts = requirementRow?.querySelectorAll('.SkillActionDetail_inputCount__1rdrn'); const itemElements = requirementRow?.querySelectorAll('.Item_itemContainer__x7kH1'); let itemIndex = 0; if (itemElements) { for (let i = 0; i < itemElements.length; i++) { if (itemElements[i].contains(element)) { itemIndex = i; break; } } } const countElement = allCounts ? allCounts[itemIndex] : null; const rawText = countElement?.textContent || '1'; const cleanText = rawText.replace(/[^\d.,]/g, ''); result.count = parseFloat(utils.cleanNumber(cleanText)) || 1.0; } return result; } getActionTime() { const allTimeElements = document.querySelectorAll('.SkillActionDetail_value__dQjYH'); for (let i = allTimeElements.length - 1; i >= 0; i--) { const text = allTimeElements[i].textContent; if (text.includes('s') && !text.includes('%')) { const match = text.match(/([\d.,]+)s/); if (match) return parseFloat(utils.cleanNumber(match[1])); } } return 0.0; } parseDropRate(itemHrid) { try { const dropElements = document.querySelectorAll('.SkillActionDetail_drop__26KBZ'); for (const dropElement of dropElements) { const itemElement = dropElement.querySelector('.Item_itemContainer__x7kH1 svg use'); if (itemElement) { const href = itemElement.getAttribute('href'); const dropItemHrid = href ? `/items/${href.split('#')[1]}` : null; if (dropItemHrid === itemHrid) { const rateText = dropElement.textContent.match(/~?([\d.]+)%/); if (rateText) { return parseFloat(utils.cleanNumber(rateText[0])) / 100.0; } } } } } catch (error) { console.error('解析掉落率失败:', error); } return null; } hasNullPrices(data, useOptimistic) { const checkItems = (items) => items.some(item => (useOptimistic ? item.bids : item.asks) === null || (useOptimistic ? item.bids : item.asks) <= 0.0 ); const checkDrinks = (drinks) => drinks.some(drink => (useOptimistic ? drink.bids : drink.asks) === null || (useOptimistic ? drink.bids : drink.asks) <= 0.0 ); return checkItems(data.requirements) || checkItems(data.outputs) || checkItems(data.upgrades || []) || checkDrinks(data.drinkCosts || []); } async getActionData() { const container = document.querySelector('.SkillActionDetail_regularComponent__3oCgr'); if (!container) return null; const reqElements = [...container.querySelectorAll('.SkillActionDetail_itemRequirements__3SPnA .Item_itemContainer__x7kH1')]; const outputElements = [...container.querySelectorAll('.SkillActionDetail_outputItems__3zp_f .Item_itemContainer__x7kH1')]; const dropElements = [...container.querySelectorAll('.SkillActionDetail_dropTable__3ViVp .Item_itemContainer__x7kH1')]; const upgradeElements = [...container.querySelectorAll('.SkillActionDetail_upgradeItemSelectorInput__2mnS0 .Item_itemContainer__x7kH1')]; const [requirements, outputs, drops, upgrades, buffData] = await Promise.all([ Promise.all(reqElements.map(el => this.getItemData(el, false, true, false))), Promise.all(outputElements.map(el => this.getItemData(el, true, false, false))), Promise.all(dropElements.map(el => this.getItemData(el, false, false, false))), Promise.all(upgradeElements.map(el => this.getItemData(el, false, false, true))), this.calculateBuffEffectsAndCosts() ]); const actionTime = this.getActionTime(); return { actionTime, efficiency: buffData.efficiency, drinkCosts: buffData.drinkCosts, requirements: requirements.filter(Boolean), outputs: outputs.filter(Boolean), drops: drops.filter(Boolean), upgrades: upgrades.filter(Boolean) }; } calculateProfit(data, useOptimistic) { if (this.hasNullPrices(data, useOptimistic)) return null; if (data.actionTime <= 0.0) return null; let totalCost = 0.0; data.requirements.forEach(item => { const price = useOptimistic ? item.bids : item.asks; totalCost += price * item.count; }); if (data.upgrades.length > 0) { data.upgrades.forEach(item => { const price = useOptimistic ? item.bids : item.asks; totalCost += price * item.count; }); } const effectiveTime = data.actionTime / (1.0 + data.efficiency); let totalIncome = 0.0; data.outputs.forEach(item => { const price = useOptimistic ? item.asks : item.bids; let income = price * item.count; if (item.itemHrid !== '/items/coin') { income *= 0.98; } totalIncome += income; }); if (data.drops.length > 0) { data.drops.forEach(item => { const price = useOptimistic ? item.asks : item.bids; const dropRate = this.parseDropRate(item.itemHrid) || 0.05; let income = price * (item.count || 1.0) * dropRate; if (item.itemHrid !== '/items/coin') { income *= 0.98; } totalIncome += income; }); } const profitPerAction = totalIncome - totalCost; const profitPerSecond = (profitPerAction * (1.0 + data.efficiency)) / data.actionTime; let drinkCostPerSecond = 0.0; if (data.drinkCosts.length > 0) { const totalDrinkCost = data.drinkCosts.reduce((sum, item) => { const price = useOptimistic ? item.bids : item.asks; return sum + price; }, 0.0); drinkCostPerSecond = totalDrinkCost / 300.0; } const finalProfitPerSecond = profitPerSecond - drinkCostPerSecond; const dailyProfit = finalProfitPerSecond * 86400.0; return dailyProfit; } getStateFingerprint() { const container = document.querySelector('.SkillActionDetail_regularComponent__3oCgr'); if (!container) return ''; const requirements = container.querySelector('.SkillActionDetail_itemRequirements__3SPnA')?.textContent || ''; const outputs = container.querySelector('.SkillActionDetail_outputItems__3zp_f')?.textContent || ''; const upgrades = container.querySelector('.SkillActionDetail_upgradeItemSelectorInput__2mnS0')?.textContent || ''; const timeText = this.getActionTime().toString(); const props = utils.getReactProps(container); const buffsText = props?.actionBuffs ? JSON.stringify(props.actionBuffs.map(b => b.uniqueHrid)) : ''; const consumables = document.querySelectorAll('.ActionTypeConsumableSlots_consumableSlots__kFKk0 .Item_itemContainer__x7kH1'); const consumablesText = Array.from(consumables).map(el => el.querySelector('svg use')?.getAttribute('href') || 'empty' ).join('|'); return `${requirements}|${outputs}|${upgrades}|${timeText}|${buffsText}|${consumablesText}`; } setupUI() { const container = document.querySelector('.SkillActionDetail_regularComponent__3oCgr'); const existingDisplay = document.getElementById('universal-action-profit-display'); const shouldShow = container && (container.querySelector('.SkillActionDetail_itemRequirements__3SPnA') || container.querySelector('.SkillActionDetail_upgradeItemSelectorInput__2mnS0')) && container.querySelector('.SkillActionDetail_outputItems__3zp_f') && !container.querySelector('.SkillActionDetail_alchemyComponent__1J55d'); if (shouldShow && !existingDisplay) { const profitDisplay = this.createProfitDisplay(); const infoContainer = container.querySelector('.SkillActionDetail_info__3umoI'); if (infoContainer) { infoContainer.parentNode.insertBefore(profitDisplay, infoContainer.nextSibling); } else { const contentContainer = container.querySelector('.SkillActionDetail_content__1MbXv'); if (contentContainer) { contentContainer.appendChild(profitDisplay); } } this.lastState = this.getStateFingerprint(); setTimeout(() => this.updateProfitDisplay(), 100); } else if (!shouldShow && existingDisplay) { existingDisplay.remove(); } } } // ==================== 购物车管理器 ==================== class ShoppingCartManager { constructor() { this.items = new Map(); this.savedLists = new Map(); this.isOpen = false; this.cartContainer = null; this.maxSavedLists = 5; this.currentListName = ''; this.init(); } init() { this.createCartDrawer(); this.loadCartFromStorage(); this.loadSavedListsFromStorage(); this.updateCartBadge(); this.updateSavedListsDisplay(); this.setupMarketCartButton(); setTimeout(() => { this.updateCartBadge(); this.updateCartDisplay(); this.updateSavedListsDisplay(); const listNameInput = document.getElementById('list-name-input'); if (listNameInput) { listNameInput.value = this.currentListName; } }, 0); } createCartDrawer() { this.cartContainer = document.createElement('div'); this.cartContainer.id = 'shopping-cart-drawer'; utils.applyStyles(this.cartContainer, { position: 'fixed', top: '80px', right: '0', width: '380px', height: '75vh', backgroundColor: 'rgba(42, 43, 66, 0.95)', border: '1px solid var(--border)', borderRight: 'none', borderTopLeftRadius: '8px', borderBottomLeftRadius: '8px', backdropFilter: 'blur(10px)', boxShadow: '-4px 0 20px rgba(0,0,0,0.3)', zIndex: '9999', transform: 'translateX(380px)', transition: 'transform 0.3s cubic-bezier(0.4, 0, 0.2, 1)', display: 'flex', flexDirection: 'column', fontFamily: 'Roboto, Helvetica, Arial, sans-serif' }); this.cartContainer.innerHTML = ` <!-- 购物车标签/触发器 --> <div id="cart-tab" style=" position: absolute; left: -40px; top: 50%; transform: translateY(-50%); width: 40px; height: 80px; background: rgba(42, 43, 66, 0.95); border: 1px solid var(--border); border-right: none; border-top-left-radius: 8px; border-bottom-left-radius: 8px; display: flex; flex-direction: column; align-items: center; justify-content: center; cursor: pointer; transition: all 0.3s ease; box-shadow: -2px 0 8px rgba(0,0,0,0.2); "> <div style=" font-size: 18px; margin-bottom: 4px; white-space: nowrap; color: var(--color-text-dark-mode); ">🛒</div> <div id="cart-tab-badge" style=" background: #f44336; color: white; border-radius: 10px; min-width: 18px; height: 18px; font-size: 10px; display: none; align-items: center; justify-content: center; font-weight: bold; ">0</div> </div> <!-- 购物车头部 --> <div style=" display: flex; justify-content: space-between; align-items: center; padding: 12px 16px; border-bottom: 1px solid var(--border-separator); background: var(--card-title-background); border-top-left-radius: 8px; flex-shrink: 0; "> <h3 style=" margin: 0; color: var(--card-title-text); font-size: 16px; font-weight: bold; ">${LANG.shoppingCart}</h3> <div style=" background: rgba(156, 39, 176, 0.2); color: var(--color-text-dark-mode); padding: 2px 8px; border-radius: 12px; font-size: 11px; font-weight: 500; " id="cart-count-display">0 ${LANG.cartItem}</div> </div> <!-- 保存清单区域 --> <div style=" padding: 12px 16px; border-bottom: 1px solid var(--border-separator); background: var(--card-background); flex-shrink: 0; "> <div style="display: flex; gap: 8px; align-items: center;"> <input id="list-name-input" type="text" placeholder="${LANG.listName}" maxlength="20" style=" flex: 1; padding: 6px 8px; background-color: var(--item-background); border: 1px solid var(--item-border); border-radius: 4px; color: var(--color-text-dark-mode); font-size: 12px; outline: none; "> <button id="save-list-btn" style=" padding: 6px 12px; background-color: rgba(33, 150, 243, 0.8); color: white; border: none; border-radius: 4px; cursor: pointer; font-size: 12px; font-weight: 500; transition: background-color 0.2s; white-space: nowrap; ">${LANG.save}</button> </div> </div> <!-- 导入导出区域 --> <div style=" padding: 8px 16px; border-bottom: 1px solid var(--border-separator); background: var(--card-background); flex-shrink: 0; "> <div style="display: flex; gap: 8px; align-items: center;"> <button id="export-lists-btn" style=" flex: 1; padding: 6px 12px; background-color: rgba(76, 175, 80, 0.8); color: white; border: none; border-radius: 4px; cursor: pointer; font-size: 12px; font-weight: 500; transition: background-color 0.2s; white-space: nowrap; ">${LANG.exportSavedLists}</button> <button id="import-lists-btn" style=" flex: 1; padding: 6px 12px; background-color: rgba(33, 150, 243, 0.8); color: white; border: none; border-radius: 4px; cursor: pointer; font-size: 12px; font-weight: 500; transition: background-color 0.2s; white-space: nowrap; ">${LANG.importSavedLists}</button> </div> </div> <!-- 其他购物车内容 --> <div id="cart-items-container" style=" flex: 1; overflow-y: auto; padding: 8px; background: var(--card-background); min-height: 0; "></div> <!-- 已保存清单 --> <div style=" border-top: 1px solid var(--border-separator); background: var(--card-background); flex-shrink: 0; max-height: 200px; display: flex; flex-direction: column; "> <div style=" padding: 8px 16px; font-size: 12px; font-weight: 500; color: var(--color-neutral-400); border-bottom: 1px solid var(--border-separator); ">${LANG.savedLists}</div> <div id="saved-lists-container" style=" flex: 1; overflow-y: auto; padding: 8px; min-height: 0; "></div> </div> <!-- 购物车操作按钮 --> <div id="cart-actions" style=" padding: 12px 16px; border-top: 1px solid var(--border-separator); background: var(--card-background); border-bottom-left-radius: 8px; display: flex; flex-direction: column; gap: 8px; flex-shrink: 0; "> <div style="display: flex; gap: 8px;"> <button id="cart-buy-btn" style=" flex: 1; padding: 8px 12px; background-color: var(--color-market-buy); color: #000; border: none; border-radius: 4px; cursor: pointer; font-weight: bold; transition: background-color 0.2s; font-size: 13px; ">${LANG.cartDirectBuy}</button> <button id="cart-bid-btn" style=" flex: 1; padding: 8px 12px; background-color: var(--color-market-sell); color: #000; border: none; border-radius: 4px; cursor: pointer; font-weight: bold; transition: background-color 0.2s; font-size: 13px; ">${LANG.cartBidOrder}</button> </div> <button id="cart-clear-btn" style=" padding: 6px 12px; background-color: transparent; color: var(--color-neutral-400); border: 1px solid var(--border-separator); border-radius: 4px; cursor: pointer; font-size: 12px; transition: all 0.2s; ">${LANG.cartClear}</button> </div> `; document.body.appendChild(this.cartContainer); this.bindEvents(); this.updateCartDisplay(); setTimeout(() => { const listNameInput = document.getElementById('list-name-input'); if (listNameInput) { listNameInput.value = this.currentListName; } }, 0); } //设置市场购物车按钮 setupMarketCartButton() { const observer = new MutationObserver((mutationsList) => { this.handleMarketCartButton(mutationsList); }); observer.observe(document.body, { childList: true, subtree: true }); } //处理市场购物车按钮 handleMarketCartButton(mutationsList) { for (let mutation of mutationsList) { if (mutation.type === 'childList' && mutation.addedNodes.length > 0) { mutation.addedNodes.forEach(node => { if (node.nodeType === Node.ELEMENT_NODE && node.classList && [...node.classList].some(c => c.startsWith('MarketplacePanel_marketNavButtonContainer'))) { const buttons = node.querySelectorAll('button'); if (buttons.length > 0 && !node.querySelector('.market-cart-btn')) { const lastButton = buttons[buttons.length - 1]; const cartButton = lastButton.cloneNode(true); cartButton.textContent = LANG.addToCart; cartButton.classList.add('market-cart-btn'); cartButton.onclick = () => { this.addCurrentMarketItemToCart(); }; node.appendChild(cartButton); } } }); } } } //添加当前市场物品到购物车 addCurrentMarketItemToCart() { const currentItem = document.querySelector('.MarketplacePanel_currentItem__3ercC'); const svgElement = currentItem?.querySelector('svg[aria-label]'); const useElement = svgElement?.querySelector('use'); if (!svgElement || !useElement) return; const itemName = svgElement.getAttribute('aria-label'); const itemId = useElement.getAttribute('href')?.split('#')[1]; if (!itemName || !itemId) return; const itemInfo = { name: itemName, id: itemId, iconHref: `#${itemId}` }; this.addItem(itemInfo, 1); } bindEvents() { const cartTab = document.getElementById('cart-tab'); const buyBtn = document.getElementById('cart-buy-btn'); const bidBtn = document.getElementById('cart-bid-btn'); const clearBtn = document.getElementById('cart-clear-btn'); const saveListBtn = document.getElementById('save-list-btn'); const listNameInput = document.getElementById('list-name-input'); const exportBtn = document.getElementById('export-lists-btn'); const importBtn = document.getElementById('import-lists-btn'); cartTab.addEventListener('click', () => this.toggleCart()); cartTab.addEventListener('contextmenu', (e) => { e.preventDefault(); e.stopPropagation(); if (this.items.size > 0) { this.clearCart(); } }); listNameInput.addEventListener('input', (e) => { const inputValue = e.target.value.trim(); if (inputValue !== this.currentListName) { this.currentListName = inputValue; this.saveCartToStorage(); } }); cartTab.addEventListener('mouseenter', () => { cartTab.style.backgroundColor = 'rgba(156, 39, 176, 0.1)'; cartTab.style.transform = 'translateY(-50%) scale(1.05)'; }); cartTab.addEventListener('mouseleave', () => { cartTab.style.backgroundColor = 'rgba(42, 43, 66, 0.95)'; cartTab.style.transform = 'translateY(-50%) scale(1)'; }); buyBtn.addEventListener('click', () => this.batchPurchase(false)); bidBtn.addEventListener('click', () => this.batchPurchase(true)); clearBtn.addEventListener('click', () => this.clearCart()); saveListBtn.addEventListener('click', () => { const listName = listNameInput.value.trim(); if (this.saveCurrentList(listName)) { listNameInput.value = ''; } }); listNameInput.addEventListener('keypress', (e) => { if (e.key === 'Enter') { const listName = listNameInput.value.trim(); if (this.saveCurrentList(listName)) { listNameInput.value = ''; } } }); exportBtn.addEventListener('click', () => this.exportShoppingLists()); importBtn.addEventListener('click', () => this.importShoppingLists()); exportBtn.addEventListener('mouseenter', () => exportBtn.style.backgroundColor = 'rgba(76, 175, 80, 0.9)'); exportBtn.addEventListener('mouseleave', () => exportBtn.style.backgroundColor = 'rgba(76, 175, 80, 0.8)'); importBtn.addEventListener('mouseenter', () => importBtn.style.backgroundColor = 'rgba(33, 150, 243, 0.9)'); importBtn.addEventListener('mouseleave', () => importBtn.style.backgroundColor = 'rgba(33, 150, 243, 0.8)'); buyBtn.addEventListener('mouseenter', () => buyBtn.style.backgroundColor = 'var(--color-market-buy-hover)'); buyBtn.addEventListener('mouseleave', () => buyBtn.style.backgroundColor = 'var(--color-market-buy)'); bidBtn.addEventListener('mouseenter', () => bidBtn.style.backgroundColor = 'var(--color-market-sell-hover)'); bidBtn.addEventListener('mouseleave', () => bidBtn.style.backgroundColor = 'var(--color-market-sell)'); clearBtn.addEventListener('mouseenter', () => { clearBtn.style.backgroundColor = 'rgba(244, 67, 54, 0.1)'; clearBtn.style.borderColor = '#f44336'; clearBtn.style.color = '#f44336'; }); clearBtn.addEventListener('mouseleave', () => { clearBtn.style.backgroundColor = 'transparent'; clearBtn.style.borderColor = 'var(--border-separator)'; clearBtn.style.color = 'var(--color-neutral-400)'; }); saveListBtn.addEventListener('mouseenter', () => saveListBtn.style.backgroundColor = 'rgba(33, 150, 243, 0.9)'); saveListBtn.addEventListener('mouseleave', () => saveListBtn.style.backgroundColor = 'rgba(33, 150, 243, 0.8)'); listNameInput.addEventListener('focus', () => listNameInput.style.borderColor = 'var(--color-primary)'); listNameInput.addEventListener('blur', () => listNameInput.style.borderColor = 'var(--item-border)'); this.cartContainer.addEventListener('click', (e) => { const removeBtn = e.target.closest('[data-remove-item]'); if (removeBtn) { e.stopPropagation(); const itemId = removeBtn.dataset.removeItem; this.removeItem(itemId); return; } const loadBtn = e.target.closest('[data-load-list]'); if (loadBtn) { e.stopPropagation(); const listName = loadBtn.dataset.loadList; this.loadSavedList(listName); return; } const deleteBtn = e.target.closest('[data-delete-list]'); if (deleteBtn) { e.stopPropagation(); const listName = deleteBtn.dataset.deleteList; this.deleteSavedList(listName); return; } }); this.cartContainer.addEventListener('dblclick', (e) => { const listItem = e.target.closest('#saved-lists-container > div'); if (listItem) { e.stopPropagation(); e.preventDefault(); const loadBtn = listItem.querySelector('[data-load-list]'); if (loadBtn) { const listName = loadBtn.dataset.loadList; this.loadSavedList(listName); } } }); this.cartContainer.addEventListener('input', (e) => { if (e.target.matches('input[data-item-id]')) { const itemId = e.target.dataset.itemId; let value = e.target.value; if (value.length > 12) { e.target.value = value.slice(0, 12); } } }); this.cartContainer.addEventListener('change', (e) => { if (e.target.matches('input[data-item-id]')) { const itemId = e.target.dataset.itemId; let quantity = parseInt(e.target.value) || 1; if (quantity < 1) quantity = 1; if (quantity > 999999999999) quantity = 999999999999; e.target.value = quantity; this.updateItemQuantity(itemId, quantity); } }); let mouseDownTarget = null; document.addEventListener('mousedown', (e) => { mouseDownTarget = e.target; }, true); document.addEventListener('click', (e) => { if (this.isOpen && !this.cartContainer.contains(e.target) && !this.cartContainer.contains(mouseDownTarget)) { this.closeCart(); } mouseDownTarget = null; }, true); } showToast(message, type, duration) { if (window.MWIModules?.toast) { window.MWIModules.toast.show(message, type, duration); } } async batchPurchase(isBidOrder = false) { if (this.items.size === 0) { this.showToast(LANG.cartEmpty, 'warning'); return; } const api = window.MWIModules?.api; if (!api?.isReady) { this.showToast(LANG.wsNotAvailable, 'error'); return; } const buyBtn = document.getElementById('cart-buy-btn'); const bidBtn = document.getElementById('cart-bid-btn'); const clearBtn = document.getElementById('cart-clear-btn'); const originalBuyText = buyBtn.textContent; const originalBidText = bidBtn.textContent; const originalBuyBg = buyBtn.style.backgroundColor; const originalBidBg = bidBtn.style.backgroundColor; // 禁用所有按钮 buyBtn.disabled = true; bidBtn.disabled = true; clearBtn.disabled = true; if (isBidOrder) { bidBtn.textContent = LANG.submitting; bidBtn.style.backgroundColor = CONFIG.COLORS.disabled; bidBtn.style.cursor = 'not-allowed'; } else { buyBtn.textContent = LANG.buying; buyBtn.style.backgroundColor = CONFIG.COLORS.disabled; buyBtn.style.cursor = 'not-allowed'; } const otherBtn = isBidOrder ? buyBtn : bidBtn; otherBtn.style.backgroundColor = CONFIG.COLORS.disabled; otherBtn.style.cursor = 'not-allowed'; clearBtn.style.backgroundColor = CONFIG.COLORS.disabled; clearBtn.style.cursor = 'not-allowed'; clearBtn.style.opacity = '0.5'; const items = Array.from(this.items.entries()).map(([itemId, item]) => ({ itemHrid: itemId.startsWith('/items/') ? itemId : `/items/${itemId}`, quantity: item.quantity, materialName: item.name, cartItemId: itemId })); try { const results = isBidOrder ? await api.batchBidOrder(items, CONFIG.DELAYS.PURCHASE) : await api.batchDirectPurchase(items, CONFIG.DELAYS.PURCHASE); // 处理购买结果 this.processCartResults(results, isBidOrder); // 移除购买成功的物品 let successfulRemovals = 0; results.forEach(result => { if (result.success && result.item.cartItemId) { this.items.delete(result.item.cartItemId); successfulRemovals++; } }); // 更新购物车显示 if (successfulRemovals > 0) { this.saveCartToStorage(); this.updateCartBadge(); this.updateCartDisplay(); // 如果购物车空了就关闭 if (this.items.size === 0) { setTimeout(() => this.closeCart(), 1000); } } } catch (error) { this.showToast(`${LANG.error}: ${error.message}`, 'error'); } finally { // 恢复按钮状态 buyBtn.disabled = false; bidBtn.disabled = false; clearBtn.disabled = false; buyBtn.textContent = originalBuyText; bidBtn.textContent = originalBidText; buyBtn.style.backgroundColor = originalBuyBg; bidBtn.style.backgroundColor = originalBidBg; buyBtn.style.cursor = 'pointer'; bidBtn.style.cursor = 'pointer'; clearBtn.style.backgroundColor = 'transparent'; clearBtn.style.cursor = 'pointer'; clearBtn.style.opacity = '1'; } } // 新增:处理购物车购买结果的方法 processCartResults(results, isBidOrder) { let successCount = 0; results.forEach(result => { const statusText = isBidOrder ? (result.success ? LANG.submitted : LANG.failed) : (result.success ? LANG.purchased : LANG.failed); const message = `${statusText} ${result.item.materialName || result.item.itemHrid} x${result.item.quantity}`; this.showToast(message, result.success ? 'success' : 'error', 2000); if (result.success) successCount++; }); // 显示总结信息 const finalMessage = successCount > 0 ? `${LANG.complete} ${LANG.success} ${successCount}/${results.length} ${LANG.cartItem}` : LANG.allFailed; this.showToast(finalMessage, successCount > 0 ? 'success' : 'error', successCount > 0 ? 5000 : 3000); } createAddAllToCartButton(type) { const btn = document.createElement('button'); btn.textContent = LANG.addToCart; btn.className = 'unified-action-btn add-to-cart-btn'; btn.setAttribute('data-button-type', 'add-to-cart'); // 复用MaterialPurchaseManager的样式方法 const materialManager = window.MWIModules?.materialPurchase; if (materialManager) { materialManager.applyUnifiedButtonStyle(btn, 'add-to-cart'); } btn.addEventListener('click', async (e) => { e.preventDefault(); e.stopPropagation(); await this.addAllNeededToCart(type); }); return btn; } // 添加所有需要的材料到购物车 async addAllNeededToCart(type) { try { const requirements = await MaterialCalculator.calculateRequirements(type); let addedCount = 0; for (const requirement of requirements) { if (requirement.supplementNeeded > 0 && requirement.itemId && !requirement.itemId.includes('coin')) { const itemInfo = { name: requirement.materialName, id: requirement.itemId, iconHref: `#${requirement.itemId.replace('/items/', '')}` }; this.addItem(itemInfo, requirement.supplementNeeded); addedCount++; } } if (addedCount > 0) { this.showToast(`${LANG.add} ${addedCount} ${LANG.materials}${LANG.toCart}`, 'success', 3000); } else { this.showToast(`${LANG.noMaterialsNeeded}`, 'info', 2000); } } catch (error) { console.error('添加所需材料到购物车失败:', error); this.showToast(`${LANG.addToCartFailed}`, 'error'); } } saveCurrentList(listName) { if (!listName || listName.trim().length === 0) { this.showToast(LANG.pleaseEnterListName, 'warning'); return false; } if (this.items.size === 0) { this.showToast(LANG.cartEmptyCannotSave, 'warning'); return false; } if (this.savedLists.size >= this.maxSavedLists && !this.savedLists.has(listName)) { this.showToast(`${LANG.maxListsLimit}${this.maxSavedLists}${LANG.lists}`, 'warning'); return false; } const listData = { name: listName.trim(), items: {}, savedAt: Date.now() }; for (const [itemId, itemData] of this.items) { listData.items[itemId] = { name: itemData.name, iconHref: itemData.iconHref, quantity: itemData.quantity }; } this.savedLists.set(listName, listData); this.currentListName = listName; this.saveSavedListsToStorage(); this.saveCartToStorage(); this.updateSavedListsDisplay(); this.showToast(`"${listName}"${LANG.saved}`, 'success'); return true; } loadSavedList(listName) { const listData = this.savedLists.get(listName); if (!listData) return false; this.items.clear(); for (const [itemId, itemData] of Object.entries(listData.items)) { this.items.set(itemId, { name: itemData.name, iconHref: itemData.iconHref, quantity: itemData.quantity }); } this.currentListName = listName; const listNameInput = document.getElementById('list-name-input'); if (listNameInput) { listNameInput.value = listName; } this.saveCartToStorage(); this.updateCartBadge(); this.updateCartDisplay(); this.showToast(`"${listName}"${LANG.loaded}`, 'success'); return true; } exportShoppingLists() { try { const listsData = Object.fromEntries(this.savedLists); if (Object.keys(listsData).length === 0) { this.showToast(LANG.noListsToExport, 'warning'); return; } const exportData = { timestamp: new Date().toLocaleString('sv-SE').replace(/[-:T ]/g, '').slice(0, 14), version: '3.3.0', lists: listsData }; const jsonData = JSON.stringify(exportData, null, 2); const blob = new Blob([jsonData], { type: 'application/json' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = `milkyway-shopping-lists-${new Date().toLocaleString('sv-SE').replace(/[-:T ]/g, '').slice(0, 14)}.json`; a.click(); URL.revokeObjectURL(url); this.showToast(`${LANG.exportStatusPrefix} ${Object.keys(listsData).length} ${LANG.exportStatusSuffix}`, 'success'); } catch (error) { console.error('导出失败:', error); this.showToast(`${LANG.exportFailed}: ${error.message}`, 'error'); } } importShoppingLists() { const input = document.createElement('input'); input.type = 'file'; input.accept = '.json'; input.style.display = 'none'; input.onchange = (event) => { const file = event.target.files[0]; if (!file) return; const reader = new FileReader(); reader.onload = (e) => { try { const importData = JSON.parse(e.target.result); if (!this.validateImportData(importData)) { throw new Error(LANG.invalidImportFormat); } const listsData = importData.lists || importData; this.savedLists.clear(); for (const [listName, listData] of Object.entries(listsData)) { this.savedLists.set(listName, listData); } this.saveSavedListsToStorage(); this.updateSavedListsDisplay(); const importedCount = Object.keys(listsData).length; const message = `${LANG.importStatusPrefix}${importedCount}${LANG.importStatusSuffix}`; this.showToast(message, 'success'); } catch (error) { console.error('导入失败:', error); this.showToast(`${LANG.importFailed}: ${error.message}`, 'error'); } }; reader.readAsText(file); }; document.body.appendChild(input); input.click(); document.body.removeChild(input); } validateImportData(data) { if (!data || typeof data !== 'object') return false; const listsData = data.lists || data; if (!listsData || typeof listsData !== 'object') return false; for (const [listName, listData] of Object.entries(listsData)) { if (!listData || typeof listData !== 'object') return false; if (!listData.name || typeof listData.name !== 'string') return false; if (!listData.items || typeof listData.items !== 'object') return false; } return true; } // 其他必要的方法实现... toggleCart() { if (this.isOpen) { this.closeCart(); } else { this.openCart(); } } openCart() { if (this.isOpen) return; this.cartContainer.style.transform = 'translateX(0)'; this.isOpen = true; } closeCart() { if (!this.isOpen) return; this.cartContainer.style.transform = 'translateX(380px)'; this.isOpen = false; } updateCartBadge() { const tabBadge = document.getElementById('cart-tab-badge'); const countDisplay = document.getElementById('cart-count-display'); if (!tabBadge || !countDisplay) return; const itemTypeCount = this.items.size; if (itemTypeCount > 0) { tabBadge.textContent = itemTypeCount > 99 ? '99+' : itemTypeCount.toString(); tabBadge.style.display = 'flex'; countDisplay.textContent = `${itemTypeCount} ${LANG.cartItem}`; } else { tabBadge.style.display = 'none'; countDisplay.textContent = `0 ${LANG.cartItem}`; } } addItem(itemInfo, quantity = 1) { if (!itemInfo || !itemInfo.id || quantity <= 0) return; const existingItem = this.items.get(itemInfo.id); if (existingItem) { existingItem.quantity += quantity; } else { this.items.set(itemInfo.id, { name: itemInfo.name, iconHref: itemInfo.iconHref, quantity: quantity }); } this.saveCartToStorage(); this.updateCartBadge(); this.updateCartDisplay(); this.showToast(`${LANG.add} ${itemInfo.name} x${quantity} ${LANG.toCart}`, 'success', 2000); } removeItem(itemId) { this.items.delete(itemId); this.saveCartToStorage(); this.updateCartBadge(); this.updateCartDisplay(); if (this.items.size === 0) { this.closeCart(); } } updateItemQuantity(itemId, quantity) { if (quantity <= 0) { this.removeItem(itemId); return; } const item = this.items.get(itemId); if (item) { item.quantity = quantity; this.saveCartToStorage(); this.updateCartBadge(); } } clearCart() { if (this.items.size === 0) return; this.items.clear(); this.currentListName = ''; const listNameInput = document.getElementById('list-name-input'); if (listNameInput) { listNameInput.value = ''; } this.saveCartToStorage(); this.updateCartBadge(); this.updateCartDisplay(); this.showToast(LANG.cartClearSuccess, 'success', 3000); if (this.isOpen) { this.closeCart(); } } updateCartDisplay() { const container = document.getElementById('cart-items-container'); if (!container) return; if (this.items.size === 0) { container.innerHTML = ` <div style=" text-align: center; color: var(--color-neutral-400); padding: 40px 20px; font-style: italic; font-size: 14px; ">${LANG.cartEmpty}</div> `; return; } let html = ''; for (const [itemId, item] of this.items) { html += ` <div style=" display: flex; align-items: center; padding: 10px; margin-bottom: 8px; background-color: var(--item-background); border: 1px solid var(--item-border); border-radius: 6px; transition: all 0.2s ease; " onmouseenter="this.style.backgroundColor='var(--item-background-hover)'; this.style.borderColor='var(--item-border-hover)'" onmouseleave="this.style.backgroundColor='var(--item-background)'; this.style.borderColor='var(--item-border)'"> <div style=" width: 32px; height: 32px; margin-right: 12px; display: flex; align-items: center; justify-content: center; background: var(--item-background); border-radius: 4px; "> <svg width="100%" height="100%" style="max-width: 24px; max-height: 24px;"> <use href="/static/media/items_sprite.6d12eb9d.svg${item.iconHref}"></use> </svg> </div> <div style="flex: 1; color: var(--color-text-dark-mode); min-width: 0;"> <div style="font-size: 13px; font-weight: 500; margin-bottom: 2px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;">${item.name}</div> <div style="font-size: 11px; color: var(--color-neutral-400);">${LANG.cartQuantity}: ${item.quantity}</div> </div> <div style="display: flex; align-items: center; gap: 8px; flex-shrink: 0;"> <input type="number" value="${item.quantity}" min="1" max="999999999999" maxlength="12" data-item-id="${itemId}" style=" width: 120px; padding: 4px 8px; background-color: var(--item-background); border: 1px solid var(--item-border); border-radius: 3px; color: var(--color-text-dark-mode); font-size: 12px; text-align: right; " > <button data-remove-item="${itemId}" style=" background: none; border: none; color: #f44336; cursor: pointer; padding: 4px; border-radius: 3px; transition: background-color 0.2s; font-size: 12px; width: 24px; height: 24px; display: flex; align-items: center; justify-content: center; flex-shrink: 0; " title="${LANG.cartRemove}" onmouseenter="this.style.backgroundColor='rgba(244, 67, 54, 0.2)'" onmouseleave="this.style.backgroundColor='transparent'" >🗑️</button> </div> </div> `; } container.innerHTML = html; } updateSavedListsDisplay() { const container = document.getElementById('saved-lists-container'); if (!container) return; if (this.savedLists.size === 0) { container.innerHTML = ` <div style=" text-align: center; color: var(--color-neutral-400); padding: 20px; font-style: italic; font-size: 12px; ">${LANG.noSavedLists}</div> `; return; } let html = ''; const sortedLists = Array.from(this.savedLists.entries()) .sort((a, b) => b[1].savedAt - a[1].savedAt); for (const [listName, listData] of sortedLists) { const itemCount = Object.keys(listData.items).length; html += ` <div style=" display: flex; align-items: center; padding: 8px; margin-bottom: 6px; background-color: var(--item-background); border: 1px solid var(--item-border); border-radius: 4px; transition: all 0.2s ease; user-select: none; -webkit-user-select: none; -moz-user-select: none; -ms-user-select: none; " onmouseenter="this.style.backgroundColor='var(--item-background-hover)'" onmouseleave="this.style.backgroundColor='var(--item-background)'"> <div style="flex: 1; color: var(--color-text-dark-mode); min-width: 0;"> <div style="font-size: 12px; font-weight: 500; margin-bottom: 2px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;">${listName}</div> <div style="font-size: 10px; color: var(--color-neutral-400);">${itemCount}${LANG.cartItem}</div> </div> <div style="display: flex; gap: 6px; flex-shrink: 0;"> <button data-load-list="${listName}" style=" background: rgba(76, 175, 80, 0.8); color: white; border: none; border-radius: 4px; cursor: pointer; padding: 6px 10px; font-size: 11px; font-weight: 500; transition: background-color 0.2s; line-height: 1; white-space: nowrap; " title="加载清单" onmouseenter="this.style.backgroundColor='rgba(76, 175, 80, 0.9)'" onmouseleave="this.style.backgroundColor='rgba(76, 175, 80, 0.8)'" >${LANG.load}</button> <button data-delete-list="${listName}" style=" background: rgba(244, 67, 54, 0.8); color: white; border: none; border-radius: 4px; cursor: pointer; padding: 6px 10px; font-size: 11px; font-weight: 500; transition: background-color 0.2s; line-height: 1; white-space: nowrap; " title="删除清单" onmouseenter="this.style.backgroundColor='rgba(244, 67, 54, 0.9)'" onmouseleave="this.style.backgroundColor='rgba(244, 67, 54, 0.8)'" >${LANG.delete}</button> </div> </div> `; } container.innerHTML = html; } saveCartToStorage() { try { const cartData = { items: Object.fromEntries(this.items), currentListName: this.currentListName }; localStorage.setItem('milkyway-current-cart', JSON.stringify(cartData)); } catch (error) { console.warn('保存当前购物车失败:', error); } } loadCartFromStorage() { try { const cartData = JSON.parse(localStorage.getItem('milkyway-current-cart') || '{}'); this.items = new Map(Object.entries(cartData.items || {})); this.currentListName = cartData.currentListName || ''; const listNameInput = document.getElementById('list-name-input'); if (listNameInput) { listNameInput.value = this.currentListName; } } catch (error) { console.warn('加载当前购物车失败:', error); this.items = new Map(); this.currentListName = ''; } } saveSavedListsToStorage() { try { const listsData = {}; for (const [listName, listData] of this.savedLists) { listsData[listName] = { name: listData.name, items: { ...listData.items }, savedAt: listData.savedAt }; } localStorage.setItem('milkyway-shopping-lists', JSON.stringify(listsData)); } catch (error) { console.warn('保存购物清单失败:', error); } } loadSavedListsFromStorage() { try { const listsData = JSON.parse(localStorage.getItem('milkyway-shopping-lists') || '{}'); this.savedLists = new Map(Object.entries(listsData)); } catch (error) { console.warn('加载购物清单失败:', error); this.savedLists = new Map(); } } deleteSavedList(listName) { if (this.savedLists.delete(listName)) { this.saveSavedListsToStorage(); this.updateSavedListsDisplay(); this.showToast(`"${listName}"${LANG.deleted}`, 'success'); return true; } return false; } } // ==================== 自动停止管理器 ==================== class AutoStopManager { constructor() { this.activeMonitors = new Map(); this.pendingActions = new Map(); this.processedComponents = new WeakSet(); this.init(); } init() { this.setupWebSocketHooks(); this.startObserving(); } startObserving() { const observer = new MutationObserver(() => { this.injectAutoStopUI(); }); observer.observe(document.body, { childList: true, subtree: true }); } setupWebSocketHooks() { const waitForAPI = () => { if (window.AutoBuyAPI?.hookMessage) { this.initHooks(); } else { setTimeout(waitForAPI, 1000); } }; waitForAPI(); } initHooks() { try { window.AutoBuyAPI.hookMessage('new_character_action', (data) => this.handleNewAction(data)); window.AutoBuyAPI.hookMessage('actions_updated', (data) => this.handleActionsUpdated(data)); } catch (error) { console.error('[AutoStop] 设置WebSocket监听失败:', error); } } handleNewAction(data) { const actionHrid = data.newCharacterActionData?.actionHrid; if (!actionHrid || !gatheringActionsMap.has(actionHrid)) return; const targetCount = this.getCurrentTargetCount(); if (targetCount > 0) { this.pendingActions.set(actionHrid, targetCount); } } handleActionsUpdated(data) { if (!data.endCharacterActions?.length) return; data.endCharacterActions.forEach(action => { if (action.isDone && this.activeMonitors.has(action.id)) { this.stopMonitoring(action.id); } if (this.pendingActions.has(action.actionHrid)) { const targetCount = this.pendingActions.get(action.actionHrid); this.pendingActions.delete(action.actionHrid); this.startMonitoring(action.id, action.actionHrid, targetCount); } }); } startMonitoring(actionId, actionHrid, targetCount) { const itemHrid = gatheringActionsMap.get(actionHrid); if (!itemHrid) return; this.stopMonitoring(actionId); const itemId = itemHrid.replace('/items/', ''); const startCount = utils.getCountById(itemId); const intervalId = setInterval(() => { try { const currentCount = utils.getCountById(itemId); const collectedCount = Math.max(0, currentCount - startCount); if (collectedCount >= targetCount) { this.stopAction(actionId); this.stopMonitoring(actionId); } } catch (error) { console.error('[AutoStop] 监控出错:', error); } }, 1000); this.activeMonitors.set(actionId, { intervalId, targetCount }); } stopMonitoring(actionId) { const monitor = this.activeMonitors.get(actionId); if (monitor) { clearInterval(monitor.intervalId); this.activeMonitors.delete(actionId); } } stopAction(actionId) { try { window.AutoBuyAPI?.core?.handleCancelCharacterAction?.(actionId); } catch (error) { console.error('[AutoStop] 取消动作失败:', error); } } getCurrentTargetCount() { const input = document.querySelector('.auto-stop-target-input'); return input ? parseInt(input.value) || 0 : 0; } cleanup() { this.activeMonitors.forEach(monitor => clearInterval(monitor.intervalId)); this.activeMonitors.clear(); this.pendingActions.clear(); } createInfinityButton() { const nativeButton = document.querySelector('button .SkillActionDetail_unlimitedIcon__mZYJc')?.parentElement; if (nativeButton) { const clone = nativeButton.cloneNode(true); clone.getAttributeNames().filter(name => name.startsWith('data-')).forEach(attr => clone.removeAttribute(attr)); return clone; } const button = document.createElement('button'); button.className = 'Button_button__1Fe9z Button_small__3fqC7'; const container = document.createElement('div'); container.className = 'SkillActionDetail_unlimitedIcon__mZYJc'; const svg = document.createElement('svg'); Object.assign(svg, { role: 'img', 'aria-label': 'Unlimited', className: 'Icon_icon__2LtL_ Icon_xtiny__331pI', width: '100%', height: '100%' }); svg.style.margin = '-2px -1px'; const use = document.createElement('use'); use.setAttribute('href', '/static/media/misc_sprite.6b3198dc.svg#infinity'); svg.appendChild(use); container.appendChild(svg); button.appendChild(container); setTimeout(() => { if (svg.getBoundingClientRect().width === 0) { button.innerHTML = '<span style="font-size: 14px; font-weight: bold;">∞</span>'; } }, 500); return button; } createAutoStopUI() { const container = document.createElement('div'); container.className = 'SkillActionDetail_maxActionCountInput__1C0Pw auto-stop-ui'; const label = document.createElement('div'); label.className = 'SkillActionDetail_label__1mGQJ'; label.textContent = LANG.targetLabel; const inputArea = document.createElement('div'); inputArea.className = 'SkillActionDetail_input__1G-kE'; const inputContainer = document.createElement('div'); inputContainer.className = 'Input_inputContainer__22GnD Input_small__1-Eva'; const input = document.createElement('input'); input.className = 'Input_input__2-t98 auto-stop-target-input'; input.type = 'text'; input.maxLength = '10'; input.value = '0'; const setOneButton = document.createElement('button'); setOneButton.className = 'Button_button__1Fe9z Button_small__3fqC7'; setOneButton.textContent = '1'; const setInfinityButton = this.createInfinityButton(); const updateStatus = () => { const targetCount = parseInt(input.value) || 0; if (targetCount > 0) { setInfinityButton.classList.remove('Button_disabled__wCyIq'); input.value = targetCount.toString(); setOneButton.classList.toggle('Button_disabled__wCyIq', targetCount === 1); } else { setInfinityButton.classList.add('Button_disabled__wCyIq'); setOneButton.classList.remove('Button_disabled__wCyIq'); input.value = '∞'; } if (this.activeMonitors.size > 0) { if (targetCount <= 0) { this.activeMonitors.forEach((_, actionId) => this.stopMonitoring(actionId)); } else { this.activeMonitors.forEach(monitor => monitor.targetCount = targetCount); } } }; setOneButton.addEventListener('click', () => { input.value = '1'; updateStatus(); }); setInfinityButton.addEventListener('click', () => { input.value = '0'; updateStatus(); }); input.addEventListener('input', (e) => { const value = e.target.value; if (value === '∞' || !isNaN(parseInt(value))) updateStatus(); }); input.addEventListener('focus', (e) => e.target.select()); input.addEventListener('blur', updateStatus); input.addEventListener('keydown', (e) => { if (input.value === '∞' && /[0-9]/.test(e.key)) { e.preventDefault(); input.value = e.key; updateStatus(); } }); updateStatus(); inputContainer.appendChild(input); inputArea.appendChild(inputContainer); container.append(label, inputArea, setOneButton, setInfinityButton); return container; } injectAutoStopUI() { const skillElement = document.querySelector('.SkillActionDetail_regularComponent__3oCgr'); if (!skillElement || this.processedComponents.has(skillElement)) return false; const maxInput = skillElement.querySelector('.SkillActionDetail_maxActionCountInput__1C0Pw'); if (!maxInput || skillElement.querySelector('.auto-stop-ui')) return false; const hrid = utils.extractActionDetailData(skillElement); if (!hrid || !gatheringActionsMap.has(hrid)) return false; this.processedComponents.add(skillElement); maxInput.parentNode.insertBefore(this.createAutoStopUI(), maxInput.nextSibling); return true; } } // ==================== 材料购买管理器 ==================== class MaterialPurchaseManager { constructor() { this.init(); } init() { this.setupObserver(); this.setupEventListeners(); } setupObserver() { const observer = new MutationObserver(() => { Object.keys(SELECTORS).forEach(type => { if (type !== 'alchemy') this.setupUI(type); }); }); observer.observe(document.body, { childList: true, subtree: true }); } setupEventListeners() { let updateTimer = null; document.addEventListener('input', (e) => { if (e.target.classList.contains('Input_input__2-t98')) { clearTimeout(updateTimer); updateTimer = setTimeout(() => { this.updateAllInfoSpans(); }, 1); } }); document.addEventListener('click', (e) => { if (e.target.classList) { clearTimeout(updateTimer); updateTimer = setTimeout(() => { this.updateAllInfoSpans(); }, 1); } }); } async purchaseMaterials(type, isBidOrder = false) { const api = window.MWIModules?.api; const toast = window.MWIModules?.toast; if (!api?.isReady) { toast?.show(LANG.wsNotAvailable, 'error'); return; } const requirements = await MaterialCalculator.calculateRequirements(type); const needToBuy = requirements.filter(item => item.type === 'material' && item.itemId && !item.itemId.includes('coin') && item.supplementNeeded > 0 ); if (needToBuy.length === 0) { toast?.show(LANG.sufficient, 'info'); return; } const itemList = needToBuy.map(item => `${item.materialName}: ${item.supplementNeeded}${LANG.each}` ).join(', '); toast?.show(`${LANG.starting} ${needToBuy.length} ${LANG.materials}: ${itemList}`, 'info'); try { const purchaseItems = needToBuy.map(item => ({ itemHrid: item.itemId.startsWith('/items/') ? item.itemId : `/items/${item.itemId}`, quantity: item.supplementNeeded, materialName: item.materialName })); const results = isBidOrder ? await api.batchBidOrder(purchaseItems, CONFIG.DELAYS.PURCHASE) : await api.batchDirectPurchase(purchaseItems, CONFIG.DELAYS.PURCHASE); this.processResults(results, isBidOrder, type); } catch (error) { toast?.show(`${LANG.error}: ${error.message}`, 'error'); } } processResults(results, isBidOrder, type) { const toast = window.MWIModules?.toast; let successCount = 0; results.forEach(result => { const statusText = isBidOrder ? (result.success ? LANG.submitted : LANG.failed) : (result.success ? LANG.purchased : LANG.failed); const message = `${statusText} ${result.item.materialName || result.item.itemHrid} x${result.item.quantity}`; toast?.show(message, result.success ? 'success' : 'error'); if (result.success) successCount++; }); const finalMessage = successCount > 0 ? `${LANG.complete} ${LANG.success} ${successCount}/${results.length} ${LANG.materials}` : LANG.allFailed; toast?.show(finalMessage, successCount > 0 ? 'success' : 'error', successCount > 0 ? 5000 : 3000); if (successCount > 0) { setTimeout(() => this.updateAllInfoSpans(), 2000); } } updateAllInfoSpans() { ['enhancing', 'production'].forEach(type => this.updateInfoSpans(type)); } async updateInfoSpans(type) { const requirements = await MaterialCalculator.calculateRequirements(type); const className = `${type === 'house' ? 'house-' : type === 'enhancing' ? 'enhancing-' : ''}material-info-span`; document.querySelectorAll(`.${className}`).forEach((span, index) => { const materialReq = requirements.filter(req => req.type === 'material')[index]; if (materialReq) { const needed = materialReq.supplementNeeded; span.textContent = `${LANG.missing}${needed}`; span.style.color = needed > 0 ? CONFIG.COLORS.error : CONFIG.COLORS.text; } }); const upgradeSpan = document.querySelector('.upgrade-info-span'); const upgradeReq = requirements.find(req => req.type === 'upgrade'); if (upgradeSpan && upgradeReq) { const needed = upgradeReq.supplementNeeded; upgradeSpan.textContent = `${LANG.missing}${needed}`; upgradeSpan.style.color = needed > 0 ? CONFIG.COLORS.error : CONFIG.COLORS.text; } } setupUI(type) { const configs = { production: { className: 'material-info-span', gridCols: 'auto min-content auto auto', buttonParent: 'name' }, house: { className: 'house-material-info-span', gridCols: 'auto auto auto 140px', buttonParent: 'header' }, enhancing: { className: 'enhancing-material-info-span', gridCols: 'auto min-content auto auto', buttonParent: 'cost' } }; const selectors = SELECTORS[type]; const config = configs[type]; document.querySelectorAll(selectors.container).forEach(panel => { const dataAttr = `${type}ButtonInserted`; if (panel.dataset[dataAttr]) return; if (type === 'enhancing' && panel.querySelector(selectors.instructions)) return; const requirements = panel.querySelector(selectors.requirements); if (!requirements) return; panel.dataset[dataAttr] = "true"; this.setupMaterialInfo(requirements, config, type); this.setupUpgradeInfo(panel, selectors, type); this.setupButtons(panel, selectors, config, type); setTimeout(() => this.updateInfoSpans(type), CONFIG.DELAYS.UPDATE); }); } setupMaterialInfo(requirements, config, type) { const modifiedAttr = `${type}Modified`; if (requirements.dataset[modifiedAttr]) return; requirements.dataset[modifiedAttr] = "true"; requirements.style.gridTemplateColumns = config.gridCols; requirements.querySelectorAll('.Item_itemContainer__x7kH1').forEach(item => { if (item.nextSibling?.classList?.contains(config.className)) return; const span = this.createInfoSpan(); span.className = config.className; item.parentNode.insertBefore(span, item.nextSibling); }); } setupUpgradeInfo(panel, selectors, type) { if (type !== 'production') return; const upgradeContainer = panel.querySelector(selectors.upgrade); if (!upgradeContainer || upgradeContainer.dataset.upgradeModified) return; upgradeContainer.dataset.upgradeModified = "true"; if (!upgradeContainer.querySelector('.upgrade-info-span')) { const upgradeSpan = this.createInfoSpan(); upgradeSpan.className = 'upgrade-info-span'; upgradeContainer.appendChild(upgradeSpan); } } createInfoSpan() { const span = document.createElement("span"); span.textContent = `${LANG.missing}0`; utils.applyStyles(span, { fontSize: '12px', fontWeight: 'bold', padding: '2px 6px', borderRadius: '3px', whiteSpace: 'nowrap', minWidth: '60px', textAlign: 'center' }); return span; } setupButtons(panel, selectors, config, type) { if (panel.querySelector('.buy-buttons-container')) return; const shoppingCart = window.MWIModules?.shoppingCart; const materialButtonContainer = document.createElement('div'); materialButtonContainer.className = 'buy-buttons-container'; const baseStyles = { display: 'flex', gap: '6px', justifyContent: 'center', alignItems: 'center', marginBottom: '8px' }; const typeStyles = { house: { width: 'fit-content', margin: '0 auto 8px auto', maxWidth: '320px', minWidth: '300px' }, enhancing: { width: 'fit-content', margin: '0 auto 8px auto', maxWidth: '340px', minWidth: '300px' } }; utils.applyStyles(materialButtonContainer, { ...baseStyles, ...typeStyles[type] }); const directBuyBtn = this.createUnifiedButton(LANG.directBuy, () => this.purchaseMaterials(type, false), 'direct-buy'); const addToCartBtn = shoppingCart?.createAddAllToCartButton ? shoppingCart.createAddAllToCartButton(type) : this.createPlaceholderButton(); const bidOrderBtn = this.createUnifiedButton(LANG.bidOrder, () => this.purchaseMaterials(type, true), 'bid-order'); materialButtonContainer.append(directBuyBtn, addToCartBtn, bidOrderBtn); if (type === 'production') { const upgradeContainer = panel.querySelector(selectors.upgrade); if (upgradeContainer && !upgradeContainer.querySelector('.upgrade-buttons-container')) { const upgradeButtonContainer = document.createElement('div'); upgradeButtonContainer.className = 'upgrade-buttons-container'; utils.applyStyles(upgradeButtonContainer, { display: 'flex', gap: '6px', justifyContent: 'center', alignItems: 'center', marginTop: '8px', width: '100%' }); const directBuyUpgradeBtn = this.createUnifiedButton(LANG.directBuyUpgrade, () => this.purchaseUpgrades(type, false), 'direct-buy'); const bidOrderUpgradeBtn = this.createUnifiedButton(LANG.bidOrderUpgrade, () => this.purchaseUpgrades(type, true), 'bid-order'); upgradeButtonContainer.append(directBuyUpgradeBtn, bidOrderUpgradeBtn); upgradeContainer.appendChild(upgradeButtonContainer); } } const insertionMethods = { production: () => { const parent = panel.querySelector(selectors[config.buttonParent]); parent.parentNode.insertBefore(materialButtonContainer, parent.nextSibling); }, house: () => { const parent = panel.querySelector(selectors[config.buttonParent]); parent.parentNode.insertBefore(materialButtonContainer, parent); }, enhancing: () => { const parent = panel.querySelector(selectors[config.buttonParent]); parent.parentNode.insertBefore(materialButtonContainer, parent); } }; insertionMethods[type]?.(); } createUnifiedButton(text, onClick, buttonType) { const btn = document.createElement("button"); btn.textContent = text; btn.className = 'unified-action-btn'; btn.setAttribute('data-button-type', buttonType); this.applyUnifiedButtonStyle(btn, buttonType); btn.addEventListener("click", () => this.handleButtonClick(btn, text, onClick, buttonType)); return btn; } applyUnifiedButtonStyle(btn, buttonType) { const buttonConfigs = { 'direct-buy': { backgroundColor: 'rgba(47, 196, 167, 0.8)', borderColor: 'rgba(47, 196, 167, 0.5)', hoverColor: 'rgba(89, 208, 185, 0.9)' }, 'bid-order': { backgroundColor: 'rgba(217, 89, 97, 0.8)', borderColor: 'rgba(217, 89, 97, 0.5)', hoverColor: 'rgba(227, 130, 137, 0.9)' }, 'add-to-cart': { backgroundColor: 'rgba(156, 39, 176, 0.8)', borderColor: 'rgba(156, 39, 176, 0.5)', hoverColor: 'rgba(123, 31, 162, 0.9)' } }; const config = buttonConfigs[buttonType]; utils.applyStyles(btn, { padding: '0 6px', backgroundColor: config.backgroundColor, color: 'white', border: `1px solid ${config.borderColor}`, borderRadius: '4px', cursor: 'pointer', fontSize: '13px', fontWeight: '600', transition: 'all 0.2s ease', fontFamily: '"Roboto"', height: '24px', flex: '1', whiteSpace: 'nowrap', textOverflow: 'ellipsis', overflow: 'hidden', }); btn.addEventListener('mouseenter', () => { btn.style.backgroundColor = config.hoverColor; }); btn.addEventListener('mouseleave', () => { btn.style.backgroundColor = config.backgroundColor; }); } async handleButtonClick(btn, originalText, onClick, buttonType) { const toast = window.MWIModules?.toast; const api = window.MWIModules?.api; if (!api?.isReady) { console.error(LANG.wsNotAvailable); return; } const isBidOrder = buttonType === 'bid-order'; btn.disabled = true; btn.textContent = isBidOrder ? LANG.submitting : LANG.buying; const originalBg = btn.style.backgroundColor; const originalCursor = btn.style.cursor; utils.applyStyles(btn, { backgroundColor: CONFIG.COLORS.disabled, cursor: "not-allowed" }); try { await onClick(); } catch (error) { toast?.show(`${LANG.error}: ${error.message}`, 'error'); } finally { btn.disabled = false; btn.textContent = originalText; utils.applyStyles(btn, { backgroundColor: originalBg, cursor: originalCursor }); } } createPlaceholderButton() { const btn = document.createElement("button"); btn.textContent = LANG.addToCart; btn.className = 'unified-action-btn add-to-cart-btn'; btn.setAttribute('data-button-type', 'add-to-cart'); this.applyUnifiedButtonStyle(btn, 'add-to-cart'); btn.disabled = true; return btn; } async purchaseUpgrades(type, isBidOrder = false) { const api = window.MWIModules?.api; const toast = window.MWIModules?.toast; if (!api?.isReady) { toast?.show(LANG.wsNotAvailable, 'error'); return; } const requirements = await MaterialCalculator.calculateRequirements(type); const needToBuy = requirements.filter(item => item.type === 'upgrade' && item.itemId && !item.itemId.includes('coin') && item.supplementNeeded > 0 ); if (needToBuy.length === 0) { toast?.show(LANG.sufficientUpgrade, 'info'); return; } const itemList = needToBuy.map(item => `${item.materialName}: ${item.supplementNeeded}${LANG.each}` ).join(', '); toast?.show(`${LANG.starting} ${needToBuy.length} ${LANG.upgradeItems}: ${itemList}`, 'info'); try { const purchaseItems = needToBuy.map(item => ({ itemHrid: item.itemId.startsWith('/items/') ? item.itemId : `/items/${item.itemId}`, quantity: item.supplementNeeded, materialName: item.materialName })); const results = isBidOrder ? await api.batchBidOrder(purchaseItems, CONFIG.DELAYS.PURCHASE) : await api.batchDirectPurchase(purchaseItems, CONFIG.DELAYS.PURCHASE); this.processResults(results, isBidOrder, type); } catch (error) { toast?.show(`${LANG.error}: ${error.message}`, 'error'); } } } // ==================== 材料计算器 ==================== class MaterialCalculator { static async calculateRequirements(type) { const selectors = SELECTORS[type]; const container = document.querySelector(selectors.container); if (!container) return []; const requirements = []; const executionCount = this.getExecutionCount(container, selectors, type); this.calculateMaterialRequirements(container, selectors, executionCount, type, requirements); if (type === 'production') { this.calculateUpgradeRequirements(container, selectors, executionCount, requirements); } return requirements; } static getExecutionCount(container, selectors, type) { if (type === 'house') return 0; const actionInput = container.querySelector(selectors.input); return parseInt(actionInput?.value) || 0; } static calculateMaterialRequirements(container, selectors, executionCount, type, requirements) { const requirementsContainer = container.querySelector(selectors.requirements); if (!requirementsContainer) return; const materialContainers = requirementsContainer.querySelectorAll('.Item_itemContainer__x7kH1'); const inputCounts = requirementsContainer.querySelectorAll(selectors.count); materialContainers.forEach((materialContainer, i) => { const nameElement = materialContainer.querySelector('.Item_name__2C42x'); const svgElement = materialContainer.querySelector('svg[aria-label]'); if (!nameElement || !svgElement) return; const materialName = nameElement.textContent.trim(); const itemId = utils.extractItemId(svgElement); const currentStock = utils.getCountById(itemId); const consumptionPerUnit = parseFloat(utils.cleanNumber(inputCounts[i]?.textContent || '0')); const totalNeeded = type === 'house' ? consumptionPerUnit : Math.ceil(executionCount * consumptionPerUnit); const supplementNeeded = Math.max(0, totalNeeded - currentStock); requirements.push({ materialName, itemId, supplementNeeded, totalNeeded, currentStock, index: i, type: 'material' }); }); } static calculateUpgradeRequirements(container, selectors, executionCount, requirements) { const upgradeContainer = container.querySelector(selectors.upgrade); if (!upgradeContainer) return; const upgradeItem = upgradeContainer.querySelector('.Item_item__2De2O'); if (!upgradeItem) return; const svgElement = upgradeItem.querySelector('svg[aria-label]'); if (!svgElement) return; const materialName = svgElement.getAttribute('aria-label'); const itemId = utils.extractItemId(svgElement); const currentStock = itemId ? utils.getCountById(itemId) : 0; const totalNeeded = executionCount; const supplementNeeded = Math.max(0, totalNeeded - currentStock); requirements.push({ materialName, itemId, supplementNeeded, totalNeeded, currentStock, index: 0, type: 'upgrade' }); } } // ==================== 全局样式 ==================== function addGlobalButtonStyles() { const style = document.createElement('style'); style.textContent = ` /* 防止所有按钮文本被选择复制 */ button, .unified-action-btn, .buy-buttons-container button, .upgrade-buttons-container button, .market-cart-btn, [class*="Button_button"], [data-button-type], #cart-tab, #cart-buy-btn, #cart-bid-btn, #cart-clear-btn, #save-list-btn, [data-load-list], [data-delete-list], [data-remove-item] { user-select: none !important; -webkit-user-select: none !important; -moz-user-select: none !important; -ms-user-select: none !important; } /* 防止按钮内的任何元素被选择 */ button *, .unified-action-btn *, .buy-buttons-container button *, .upgrade-buttons-container button *, .market-cart-btn *, [class*="Button_button"] *, [data-button-type] *, #cart-tab *, #cart-buy-btn *, #cart-bid-btn *, #cart-clear-btn *, #save-list-btn *, [data-load-list] *, [data-delete-list] *, [data-remove-item] * { user-select: none !important; -webkit-user-select: none !important; -moz-user-select: none !important; -ms-user-select: none !important; } `; document.head.appendChild(style); } // ==================== 游戏核心监控 ==================== function setupGameCoreMonitor() { const interval = setInterval(() => { if (window.AutoBuyAPI.core || initGameCore()) { clearInterval(interval); } }, 2000); } // ==================== 模块初始化 ==================== function initializeModules() { // 初始化基础模块(总是启用) window.MWIModules.eventBus = new EventBus(); window.MWIModules.toast = new Toast(); window.MWIModules.api = new AutoBuyAPI(); // 根据配置初始化功能模块 if (MWI_CONFIG.characterSwitcher) { window.MWIModules.characterSwitcher = new CharacterSwitcher(); } if (MWI_CONFIG.gatheringEnhanced) { window.MWIModules.autoStop = new AutoStopManager(); } if (MWI_CONFIG.quickPurchase) { window.MWIModules.shoppingCart = new ShoppingCartManager(); window.MWIModules.materialPurchase = new MaterialPurchaseManager(); } if (MWI_CONFIG.alchemyProfit) { window.MWIModules.alchemyCalculator = new AlchemyProfitCalculator(); } if (MWI_CONFIG.universalProfit) { window.MWIModules.universalCalculator = new UniversalActionProfitCalculator(); } // 添加全局样式(总是启用) addGlobalButtonStyles(); // 设置游戏核心监控(总是启用) setupGameCoreMonitor(); } // ==================== 初始化状态 ==================== const state = { wsInstances: [], currentWS: null, requestHandlers: new Map(), marketDataCache: new Map(), baseDomain: 'data.pages.dev' }; Object.assign(window, state); // ==================== 启动 ==================== setupWebSocketInterception(); setupGameCoreMonitor(); })();