// ==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
// @downloadURL none
// ==/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 ?
`● Online` :
`● ${this.getText('lastOnline')}: ${character.lastOnlineText}`;
characterButton.innerHTML = `
${character.displayText || character.name || 'Unknown'}
${onlineStatus}
${statusText}
`;
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 = `
${LANG.pessimisticProfit}
${this.initialized ? LANG.loadingMarketData : this.getWaitingText()}
${LANG.optimisticProfit}
${this.initialized ? LANG.loadingMarketData : this.getWaitingText()}
`;
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 = `
${LANG.shoppingCart}
0 ${LANG.cartItem}
`;
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 = `
${LANG.cartEmpty}
`;
return;
}
let html = '';
for (const [itemId, item] of this.items) {
html += `
${item.name}
${LANG.cartQuantity}: ${item.quantity}
`;
}
container.innerHTML = html;
}
updateSavedListsDisplay() {
const container = document.getElementById('saved-lists-container');
if (!container) return;
if (this.savedLists.size === 0) {
container.innerHTML = `
${LANG.noSavedLists}
`;
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 += `
${listName}
${itemCount}${LANG.cartItem}
`;
}
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 = '∞';
}
}, 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();
})();