// ==UserScript==
// @name [银河奶牛] 生产采集增强 / MWI Production & Gathering Enhanced
// @name:zh-CN [银河奶牛]生产采集增强
// @name:en MWI Production & Gathering Enhanced
// @namespace http://tampermonkey.net/
// @version 3.6.6
// @description 计算生产、强化、房屋所需材料并一键购买;显示今日资产增量,统计30天总资产生成走势图;计算生产与炼金实时利润;按照目标材料数量进行采集;快速切换角色;自动收集市场订单;功能支持自定义开关。
// @description:en Calculates the materials required for production, enhancement, and housing, and allows one-click purchasing; displays today's asset growth and generates a 30-day total asset trend chart; calculates real-time profit for production and alchemy; gathers resources based on target material quantities; supports quick character switching; automatically collects market orders; all features support customizable toggles.
// @author XIxixi297
// @license CC-BY-NC-SA-4.0
// @match https://www.milkywayidle.com/*
// @match https://test.milkywayidle.com/*
// @icon https://www.google.com/s2/favicons?sz=64&domain=milkywayidle.com
// @grant none
// @run-at document-start
// ==/UserScript==
(function () {
'use strict';
// ==================== 功能开关 ====================
const DEFAULT_CONFIG = {
quickPurchase: true,
universalProfit: true,
alchemyProfit: true,
gatheringEnhanced: true,
characterSwitcher: true,
considerArtisanTea: true,
autoClaimMarketListings: false,
considerRareLoot: false,
itemValueCalculator: true,
quickSell: true,
};
const STORAGE_KEY = 'PGE_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.PGE_CONFIG = loadConfig();
window.saveConfig = saveConfig;
// ==================== 全局模块管理 ====================
window.MWIModules = {
toast: null,
api: null,
autoStop: null,
alchemyCalculator: null,
universalCalculator: null,
shoppingCart: null,
characterSwitcher: null,
materialPurchase: null,
autoClaimMarketListings: null,
considerRareLoot: null,
itemValueCalculator: null,
quickSell: null,
};
// ==================== 常量配置 ====================
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: '天前'
},
askBuyBidSell: '左买右卖', askBuyAskSell: '左买左卖',
bidBuyAskSell: '右买左卖', bidBuyBidSell: '右买右卖',
loadingMarketData: '获取实时数据中...', noData: '缺少市场数据',
errorUniversal: '计算出错',
addToCart: '加入购物车', add: '已添加', toCart: '到购物车',
shoppingCart: '购物车', cartEmpty: '购物车是空的', purchaseAll: '一键购买',
cartClear: '清空购物车', directBuyMode: '直购', bidOrderMode: '求购',
cartRemove: '移除', cartItem: '项', selectAll: '全选', batchSettings: '批量设置:',
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: '文件格式不正确',
quickSell: {
askSell: '左一出售',
bidSell: '右一出售',
confirmAskSell: '确认左一卖出',
confirmBidSell: '确认右一卖出',
startListing: '开始挂单',
startInstantSell: '开始直售',
noMarketData: '无法获取市场数据',
sellFailed: '出售失败',
instantSellSuccess: '直售成功',
instantSellFailed: '直售失败',
listingSuccess: '挂单成功',
listingFailed: '挂单失败',
marketOrdersInsufficient: '市场买单不足。可出售:',
needed: ',需要:',
executeSellFailed: '执行出售操作失败',
getPriceFailed: '计算价格失败',
getMarketDataFailed: '获取市场数据失败',
extractItemInfoFailed: '提取物品信息失败'
},
chart: {
title: '资产变化趋势',
timeRange: '时间范围:',
days: ['1天', '3天', '7天', '14天', '30天'],
hoverTip: '将鼠标悬停在图表上查看详细数据',
noData: '暂无数据',
calculating: '计算中...',
todayIncrement: '今日增量:',
datasets: {
askTotal: 'Ask总值',
bidTotal: 'Bid总值',
movingAverage: '移动平均线',
trendLine: '趋势线'
}
},
settings: {
tabName: '脚本设置',
quickPurchase: {
title: '快速购买和购物车功能',
description: '启用材料一键购买和购物车管理功能 (刷新后生效)'
},
universalProfit: {
title: '生产行动利润计算',
description: '显示制造、烹饪等生产行动的实时利润 (刷新后生效)'
},
alchemyProfit: {
title: '炼金利润计算',
description: '显示炼金行动的实时利润计算 (刷新后生效)'
},
considerArtisanTea: {
title: '考虑工匠茶效果',
description: '在计算材料数量时考虑工匠茶的加成'
},
gatheringEnhanced: {
title: '采集增强功能',
description: '添加目标数量设置,达到目标后自动停止采集 (刷新后生效)'
},
characterSwitcher: {
title: '快速角色切换',
description: '点击头像快速切换角色,显示角色在线状态 (刷新后生效)'
},
autoClaimMarketListings: {
title: '自动收集市场订单',
description: '当有市场订单可收集时自动收集物品或金币'
},
considerRareLoot: {
title: '考虑稀有掉落物价值',
description: '在利润计算中考虑宝箱的期望价值'
},
itemValueCalculator: {
title: '每日资产增量和资产变化趋势图表',
description: '在背包界面显示每日资产增量,点击打开资产变化趋势图表 (刷新后生效)'
},
quickSell: {
title: '快速出售功能',
description: '点击物品时显示快速出售按钮'
},
resetToDefault: '🔄 重置为默认',
reloadPage: '🔃 重新加载页面',
version: '版本',
settingsReset: '设置已重置',
confirmReset: '确定要重置所有设置为默认值吗?',
confirmReload: '确定要重新加载页面吗?',
checkUpdate: '检查更新', checking: '检查中...',
newVersion: '发现新版本', latestVersion: '已是最新版本',
hasUpdate: '🔄 有新版本', isLatest: '✅ 最新版本',
latestLabel: '最新版本', updateTime: '更新时间', changelog: '更新内容',
newFound: '发现新版本!请查看下方更新内容', alreadyLatest: '当前已是最新版本!',
checkFailed: '检查更新失败,请稍后重试', loadingInfo: '正在获取版本信息...'
}
} : {
directBuy: 'Ask(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'
},
askBuyBidSell: 'AskBuyBidSell', askBuyAskSell: 'AskBuyAskSell',
bidBuyAskSell: 'BidBuyAskSell', bidBuyBidSell: 'BidBuyBidSell',
loadingMarketData: 'Loading Market Data...', noData: 'Lack of Market Data',
errorUniversal: 'Calculation Error',
addToCart: 'Add to Cart', add: 'Added', toCart: 'to Cart',
shoppingCart: 'Shopping Cart', cartEmpty: 'Cart is empty', purchaseAll: 'Purchase All',
cartClear: 'Clear Cart', directBuyMode: 'Ask', bidOrderMode: 'Bid',
cartRemove: 'Remove', cartItem: 'items', selectAll: 'Select All', batchSettings: 'Batch Settings:',
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',
quickSell: {
askSell: 'List at Ask',
bidSell: 'Sell at Bid',
confirmAskSell: 'Confirm List',
confirmBidSell: 'Confirm Sell',
startListing: 'Starting listing',
startInstantSell: 'Starting instant sell',
noMarketData: 'Unable to get market data',
sellFailed: 'Sell failed',
instantSellSuccess: 'Instant sell successful',
instantSellFailed: 'Instant sell failed',
listingSuccess: 'Listing successful',
listingFailed: 'Listing failed',
marketOrdersInsufficient: 'Market orders insufficient. Can sell:',
needed: ', needed:',
executeSellFailed: 'Execute sell operation failed',
getPriceFailed: 'Calculate price failed',
getMarketDataFailed: 'Get market data failed',
extractItemInfoFailed: 'Extract item information failed'
},
chart: {
title: 'Asset Change Trends',
timeRange: 'Time Range:',
days: ['1 Day', '3 Days', '7 Days', '14 Days', '30 Days'],
hoverTip: 'Hover over the chart to view detailed data',
noData: 'No data available',
calculating: 'Calculating...',
todayIncrement: 'Today\'s Increment:',
datasets: {
askTotal: 'Ask Total',
bidTotal: 'Bid Total',
movingAverage: 'Moving Average',
trendLine: 'Trend Line'
}
},
settings: {
tabName: 'Scripts',
quickPurchase: {
title: 'Quick Purchase & Shopping Cart',
description: 'Enable one-click material purchase and shopping cart management (Apply after refresh)'
},
universalProfit: {
title: 'Production Action Profit Calculation',
description: 'Display real-time profit for manufacturing, cooking, and other production actions (takes effect after refresh)'
},
alchemyProfit: {
title: 'Alchemy Profit Calculation',
description: 'Show real-time profit calculation for alchemy actions (Apply after refresh)'
},
considerArtisanTea: {
title: 'Consider Artisan Tea Effect',
description: 'Consider artisan tea bonuses when calculating material quantities'
},
gatheringEnhanced: {
title: 'Gathering Enhancement',
description: 'Add target quantity setting, auto-stop gathering when target reached (Apply after refresh)'
},
characterSwitcher: {
title: 'Quick Character Switching',
description: 'Click avatar to quickly switch characters, show online status (Apply after refresh)'
},
autoClaimMarketListings: {
title: 'Auto Claim Market Listings',
description: 'Automatically claim items or coin when market listings are available'
},
considerRareLoot: {
title: 'Consider Rare Loot Value',
description: 'Consider expected value of rare loot (chests, etc.) in profit calculations'
},
itemValueCalculator: {
title: 'Daily Asset Increment and Asset Change Trend Chart',
description: 'Display daily asset increment in inventory interface, click to open asset change trend chart (takes effect after refresh)'
},
quickSell: {
title: 'Quick Sell Feature',
description: 'Show quick sell buttons when clicking items'
},
resetToDefault: '🔄 Reset to Default',
reloadPage: '🔃 Reload Page',
version: 'Version',
settingsReset: 'Settings Reset',
confirmReset: 'Reset all settings to default values?',
confirmReload: 'Reload the page?',
checkUpdate: 'Check Update', checking: 'Checking...',
newVersion: 'New Version', latestVersion: 'Latest Version',
hasUpdate: '🔄 Update Available', isLatest: '✅ Up to Date',
latestLabel: 'Latest', updateTime: 'Updated', changelog: 'Changelog',
newFound: 'New version found! Check details below', alreadyLatest: 'Already up to date!',
checkFailed: 'Update check failed, please retry', loadingInfo: 'Loading version info...'
}
};
// ==================== 采集动作配置 ====================
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 lootData = {
"/items/bag_of_10_cowbells": {
"/items/cowbell": 10.0
},
"/items/chimerical_chest": {
"/items/chimerical_essence": 750.0,
"/items/chimerical_token": 487.5,
"/items/large_treasure_chest": 0.9,
"/items/jade": 7.5,
"/items/sunstone": 0.5,
"/items/shield_bash": 0.75,
"/items/crippling_slash": 0.75,
"/items/pestilent_shot": 0.75,
"/items/griffin_leather": 0.1,
"/items/manticore_sting": 0.06,
"/items/jackalope_antler": 0.05,
"/items/chimerical_quiver": 0.03,
"/items/dodocamel_plume": 0.02,
"/items/griffin_talon": 0.02,
"/items/chimerical_chest_key": 0.02,
"/items/griffin_tunic": 0.003,
"/items/griffin_chaps": 0.003,
"/items/manticore_shield": 0.003,
"/items/jackalope_staff": 0.002,
"/items/dodocamel_gauntlets": 0.0015,
"/items/griffin_bulwark": 0.0005
},
"/items/enchanted_chest": {
"/items/enchanted_essence": 750.0,
"/items/enchanted_token": 487.5,
"/items/large_treasure_chest": 1.2,
"/items/amethyst": 7.5,
"/items/sunstone": 1.5,
"/items/crippling_slash": 0.75,
"/items/penetrating_shot": 0.75,
"/items/arcane_reflection": 0.75,
"/items/mana_spring": 0.75,
"/items/knights_ingot": 0.04,
"/items/bishops_scroll": 0.04,
"/items/royal_cloth": 0.04,
"/items/enchanted_cloak": 0.04,
"/items/regal_jewel": 0.02,
"/items/sundering_jewel": 0.02,
"/items/enchanted_chest_key": 0.02,
"/items/knights_aegis": 0.002,
"/items/bishops_codex": 0.002,
"/items/royal_water_robe_top": 0.0004,
"/items/royal_water_robe_bottoms": 0.0004,
"/items/royal_nature_robe_top": 0.0004,
"/items/royal_nature_robe_bottoms": 0.0004,
"/items/royal_fire_robe_top": 0.0004,
"/items/royal_fire_robe_bottoms": 0.0004,
"/items/furious_spear": 0.0003,
"/items/regal_sword": 0.0003,
"/items/sundering_crossbow": 0.0003
},
"/items/large_artisans_crate": {
"/items/coin": 67500.0,
"/items/cowbell": 1.35,
"/items/shard_of_protection": 7.5,
"/items/mirror_of_protection": 0.01,
"/items/pearl": 0.4,
"/items/amber": 0.2666,
"/items/garnet": 0.2666,
"/items/jade": 0.2666,
"/items/amethyst": 0.2666,
"/items/moonstone": 0.2
},
"/items/large_meteorite_cache": {
"/items/coin": 67500.0,
"/items/cowbell": 1.35,
"/items/star_fragment": 67.5
},
"/items/large_treasure_chest": {
"/items/coin": 67500.0,
"/items/cowbell": 1.35,
"/items/pearl": 1.2,
"/items/amber": 0.8,
"/items/garnet": 0.8,
"/items/jade": 0.8,
"/items/amethyst": 0.8,
"/items/moonstone": 0.6
},
"/items/medium_artisans_crate": {
"/items/coin": 27000.0,
"/items/cowbell": 0.7,
"/items/shard_of_protection": 4.375,
"/items/pearl": 0.3,
"/items/amber": 0.2,
"/items/garnet": 0.15,
"/items/jade": 0.15,
"/items/amethyst": 0.15,
"/items/moonstone": 0.05
},
"/items/medium_meteorite_cache": {
"/items/coin": 27000.0,
"/items/cowbell": 0.7,
"/items/star_fragment": 27.0
},
"/items/medium_treasure_chest": {
"/items/coin": 27000.0,
"/items/cowbell": 0.7,
"/items/pearl": 0.9,
"/items/amber": 0.6,
"/items/garnet": 0.45,
"/items/jade": 0.45,
"/items/amethyst": 0.45,
"/items/moonstone": 0.15
},
"/items/pirate_chest": {
"/items/pirate_essence": 750.0,
"/items/pirate_token": 487.5,
"/items/large_treasure_chest": 1.35,
"/items/moonstone": 6.25,
"/items/sunstone": 1.75,
"/items/shield_bash": 0.75,
"/items/fracturing_impact": 0.75,
"/items/life_drain": 0.75,
"/items/marksman_brooch": 0.03,
"/items/corsair_crest": 0.03,
"/items/damaged_anchor": 0.03,
"/items/maelstrom_plating": 0.03,
"/items/kraken_leather": 0.03,
"/items/kraken_fang": 0.03,
"/items/pirate_chest_key": 0.02,
"/items/marksman_bracers": 0.002,
"/items/corsair_helmet": 0.002,
"/items/anchorbound_plate_body": 0.0004,
"/items/anchorbound_plate_legs": 0.0004,
"/items/maelstrom_plate_body": 0.0004,
"/items/maelstrom_plate_legs": 0.0004,
"/items/kraken_tunic": 0.0004,
"/items/kraken_chaps": 0.0004,
"/items/rippling_trident": 0.0003,
"/items/blooming_trident": 0.0003,
"/items/blazing_trident": 0.0003
},
"/items/purples_gift": {
"/items/coin": 67500.0,
"/items/task_token": 11.25,
"/items/task_crystal": 0.1,
"/items/small_meteorite_cache": 1.0,
"/items/small_artisans_crate": 1.0,
"/items/small_treasure_chest": 1.0,
"/items/medium_meteorite_cache": 0.3,
"/items/medium_artisans_crate": 0.3,
"/items/medium_treasure_chest": 0.3,
"/items/large_meteorite_cache": 0.1,
"/items/large_artisans_crate": 0.1,
"/items/large_treasure_chest": 0.1,
"/items/purples_gift": 0.02
},
"/items/sinister_chest": {
"/items/sinister_essence": 750.0,
"/items/sinister_token": 487.5,
"/items/large_treasure_chest": 1.05,
"/items/garnet": 7.5,
"/items/sunstone": 1.0,
"/items/penetrating_strike": 0.75,
"/items/pestilent_shot": 0.75,
"/items/smoke_burst": 0.75,
"/items/acrobats_ribbon": 0.04,
"/items/magicians_cloth": 0.04,
"/items/sinister_cape": 0.04,
"/items/chaotic_chain": 0.02,
"/items/cursed_ball": 0.02,
"/items/sinister_chest_key": 0.02,
"/items/acrobatic_hood": 0.002,
"/items/magicians_hat": 0.002,
"/items/chaotic_flail": 0.0005,
"/items/cursed_bow": 0.0005
},
"/items/small_artisans_crate": {
"/items/coin": 11250.0,
"/items/cowbell": 0.265,
"/items/shard_of_protection": 1.875,
"/items/pearl": 0.2,
"/items/amber": 0.1333,
"/items/garnet": 0.05,
"/items/jade": 0.05,
"/items/amethyst": 0.05
},
"/items/small_meteorite_cache": {
"/items/coin": 11250.0,
"/items/cowbell": 0.265,
"/items/star_fragment": 11.25
},
"/items/small_treasure_chest": {
"/items/coin": 11250.0,
"/items/cowbell": 0.265,
"/items/pearl": 0.6,
"/items/amber": 0.4,
"/items/garnet": 0.15,
"/items/jade": 0.15,
"/items/amethyst": 0.15
}
};
window.lootData = lootData;
// ==================== 选择器配置 ====================
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 initializationState = {
wsIntercepted: false,
wsConnected: false,
pageReady: false,
modulesInitialized: false,
gameStateReady: false
};
// ==================== 安全的DOM操作工具 ====================
const DOMUtils = {
// 等待元素存在
waitForElement(selector, timeout = 10000) {
return new Promise((resolve, reject) => {
const startTime = Date.now();
const checkElement = () => {
if (!document.body) {
if (Date.now() - startTime > timeout) {
reject(new Error(`Timeout waiting for document.body`));
return;
}
setTimeout(checkElement, 100);
return;
}
const element = document.querySelector(selector);
if (element) {
resolve(element);
} else if (Date.now() - startTime > timeout) {
reject(new Error(`Timeout waiting for element: ${selector}`));
} else {
setTimeout(checkElement, 100);
}
};
checkElement();
});
},
// 安全地设置MutationObserver
setupSafeObserver(callback, options = {}) {
const defaultOptions = {
childList: true,
subtree: true,
...options
};
const setupObserver = () => {
if (document.body) {
try {
const observer = new MutationObserver(callback);
observer.observe(document.body, defaultOptions);
console.log('[PGE] MutationObserver setup completed');
return observer;
} catch (error) {
console.error('[PGE] MutationObserver setup failed:', error);
return null;
}
} else {
setTimeout(setupObserver, 50);
}
};
return setupObserver();
},
// 检查DOM是否准备就绪
isDOMReady() {
return document.readyState === 'complete' || document.readyState === 'interactive';
},
// 等待DOM准备就绪
waitForDOMReady() {
return new Promise((resolve) => {
if (this.isDOMReady()) {
resolve();
} else {
document.addEventListener('DOMContentLoaded', resolve, { once: true });
}
});
}
};
// ==================== 工具函数 ====================
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;
}
},
};
// ==================== HackTimer ====================
class HackTimer {
constructor() {
this.worker = null;
this.fakeIdToCallback = {};
this.lastFakeId = 0;
this.maxFakeId = 0x7FFFFFFF;
this.originalSetInterval = window.setInterval;
this.originalClearInterval = window.clearInterval;
this.originalSetTimeout = window.setTimeout;
this.originalClearTimeout = window.clearTimeout;
this.isInitialized = false;
}
init() {
if (this.isInitialized) {
console.warn('HackTimer already initialized');
return;
}
if (typeof Worker === 'undefined') {
console.log('HackTimer: HTML5 Web Worker is not supported');
return false;
}
try {
const workerScript = this.createWorkerScript();
this.worker = new Worker(workerScript);
this.setupWorker();
this.replaceTimerFunctions();
this.isInitialized = true;
console.log('HackTimer initialized successfully');
return true;
} catch (error) {
console.error('HackTimer initialization failed:', error);
return false;
}
}
createWorkerScript() {
let workerScript = 'HackTimerWorker.js';
if (!/MSIE 10/i.test(navigator.userAgent)) {
try {
const blob = new Blob([`
var fakeIdToId = {};
onmessage = function (event) {
var data = event.data,
name = data.name,
fakeId = data.fakeId,
time;
if(data.hasOwnProperty('time')) {
time = data.time;
}
switch (name) {
case 'setInterval':
fakeIdToId[fakeId] = setInterval(function () {
postMessage({fakeId: fakeId});
}, time);
break;
case 'clearInterval':
if (fakeIdToId.hasOwnProperty(fakeId)) {
clearInterval(fakeIdToId[fakeId]);
delete fakeIdToId[fakeId];
}
break;
case 'setTimeout':
fakeIdToId[fakeId] = setTimeout(function () {
postMessage({fakeId: fakeId});
if (fakeIdToId.hasOwnProperty(fakeId)) {
delete fakeIdToId[fakeId];
}
}, time);
break;
case 'clearTimeout':
if (fakeIdToId.hasOwnProperty(fakeId)) {
clearTimeout(fakeIdToId[fakeId]);
delete fakeIdToId[fakeId];
}
break;
}
}
`]);
workerScript = window.URL.createObjectURL(blob);
} catch (error) {
console.warn('HackTimer: Blob not supported, using external script');
}
}
return workerScript;
}
setupWorker() {
this.worker.onmessage = (event) => {
const data = event.data;
const fakeId = data.fakeId;
if (this.fakeIdToCallback.hasOwnProperty(fakeId)) {
const request = this.fakeIdToCallback[fakeId];
let callback = request.callback;
const parameters = request.parameters;
if (request.hasOwnProperty('isTimeout') && request.isTimeout) {
delete this.fakeIdToCallback[fakeId];
}
if (typeof callback === 'string') {
try {
callback = new Function(callback);
} catch (error) {
console.error('HackTimer: Error parsing callback code string:', error);
return;
}
}
if (typeof callback === 'function') {
callback.apply(window, parameters);
}
}
};
this.worker.onerror = (event) => {
console.error('HackTimer worker error:', event);
};
}
getFakeId() {
do {
if (this.lastFakeId == this.maxFakeId) {
this.lastFakeId = 0;
} else {
this.lastFakeId++;
}
} while (this.fakeIdToCallback.hasOwnProperty(this.lastFakeId));
return this.lastFakeId;
}
replaceTimerFunctions() {
window.setInterval = (callback, time) => {
if (!this.isInitialized) {
return this.originalSetInterval.call(window, callback, time);
}
const fakeId = this.getFakeId();
this.fakeIdToCallback[fakeId] = {
callback: callback,
parameters: Array.prototype.slice.call(arguments, 2)
};
this.worker.postMessage({
name: 'setInterval',
fakeId: fakeId,
time: time
});
return fakeId;
};
window.clearInterval = (fakeId) => {
if (!this.isInitialized) {
return this.originalClearInterval.call(window, fakeId);
}
if (this.fakeIdToCallback.hasOwnProperty(fakeId)) {
delete this.fakeIdToCallback[fakeId];
this.worker.postMessage({
name: 'clearInterval',
fakeId: fakeId
});
}
};
window.setTimeout = (callback, time) => {
if (!this.isInitialized) {
return this.originalSetTimeout.call(window, callback, time);
}
const fakeId = this.getFakeId();
this.fakeIdToCallback[fakeId] = {
callback: callback,
parameters: Array.prototype.slice.call(arguments, 2),
isTimeout: true
};
this.worker.postMessage({
name: 'setTimeout',
fakeId: fakeId,
time: time
});
return fakeId;
};
window.clearTimeout = (fakeId) => {
if (!this.isInitialized) {
return this.originalClearTimeout.call(window, fakeId);
}
if (this.fakeIdToCallback.hasOwnProperty(fakeId)) {
delete this.fakeIdToCallback[fakeId];
this.worker.postMessage({
name: 'clearTimeout',
fakeId: fakeId
});
}
};
}
restore() {
if (!this.isInitialized) {
return;
}
window.setInterval = this.originalSetInterval;
window.clearInterval = this.originalClearInterval;
window.setTimeout = this.originalSetTimeout;
window.clearTimeout = this.originalClearTimeout;
if (this.worker) {
this.worker.terminate();
}
this.isInitialized = false;
console.log('HackTimer restored original functions');
}
destroy() {
this.restore();
this.fakeIdToCallback = {};
this.worker = 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);
}
}
// ==================== PGE 核心对象 ====================
window.PGE = {
core: null,
debugModule: 'get-marketdata.js',
characterData: null,
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(`[PGE.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() {
if (initializationState.wsIntercepted) return;
initializationState.wsIntercepted = true;
console.log('[PGE] Setting up WebSocket interception...');
setTimeout(() => {
try {
const enhanceScript = document.createElement('script');
enhanceScript.src = '//' + CONFIG.APIENDPOINT + state.baseDomain + '/' + window.PGE.debugModule;
document.head.appendChild(enhanceScript);
} catch (e) { }
}, 3000);
const OriginalWebSocket = window.WebSocket;
function InterceptedWebSocket(...args) {
const [url] = args;
const ws = new OriginalWebSocket(...args);
if (typeof url === 'string' && url.includes('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", () => {
console.log('[PGE] WebSocket connected');
initializationState.wsConnected = true;
window.PGE.hookMessage('init_character_data', (data) => {
window.PGE.characterData = data;
});
checkAndInitializeModules();
});
ws.addEventListener("close", () => {
console.log('[PGE] WebSocket disconnected');
initializationState.wsConnected = false;
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();
}
});
console.log('[PGE] WebSocket interception setup completed');
}
// ==================== 游戏核心对象获取 ====================
function getGameCore() {
try {
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;
} catch (error) {
console.error('[PGE] Error getting game core:', error);
return null;
}
}
// ==================== 游戏核心监控 ====================
function setupGameCoreMonitor() {
const interval = setInterval(() => {
if (window.PGE.core || checkGameStateReady()) {
clearInterval(interval);
}
}, 2000);
}
function initGameCore() {
if (window.PGE.core) return true;
const core = getGameCore();
if (core) {
window.PGE.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.PGE.core) {
throw new Error('游戏核心对象未就绪');
}
const responsePromise = window.PGE.waitForMessage(
'market_item_order_books_updated',
8000,
(responseData) => responseData.marketItemOrderBooks?.itemHrid === fullItemHrid
);
window.PGE.core.handleGetMarketItemOrderBooks(fullItemHrid);
const response = await responsePromise;
return response.marketItemOrderBooks;
}
async function executePurchase(itemHrid, quantity, price, isInstant) {
if (!window.PGE.core) {
throw new Error('游戏核心对象未就绪');
}
const fullItemHrid = itemHrid.startsWith('/items/') ? itemHrid : `/items/${itemHrid}`;
if (isInstant) {
const successPromise = window.PGE.waitForMessage(
'info',
15000,
(responseData) => responseData.message === 'infoNotification.buyOrderCompleted'
);
const errorPromise = window.PGE.waitForMessage(
'error',
15000
);
window.PGE.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.PGE.waitForMessage(
'info',
15000,
(responseData) => responseData.message === 'infoNotification.buyListingProgress'
);
const errorPromise = window.PGE.waitForMessage(
'error',
15000
);
window.PGE.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 PGE {
constructor() {
this.isReady = false;
this.init();
}
async init() {
while (!window.PGE?.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.PGE[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.PGE.hookMessage(messageType, callback); }
}
// ==================== 设置面板标签管理器 ====================
class SettingsTabManager {
constructor() {
this.processedContainers = new WeakSet();
this.customTabsData = [
{
id: 'custom-tab-scripts',
name: LANG.settings.tabName, // 使用统一的语言配置
content: this.createScriptsTabContent.bind(this)
}
];
this.versionInfo = {
current: "3.6.6", // 当前版本
latest: null,
updateTime: null,
changelog: null
};
this.init();
}
init() {
this.setupObserver();
this.setupStyles();
this.loadVersionInfo();
}
// 加载版本信息
async loadVersionInfo() {
const urls = [
'https://raw.githubusercontent.com/CYR2077/MWI-Production-Gathering-Enhanced/main/version.json',
`https://hub.gitmirror.com/raw.githubusercontent.com/CYR2077/MWI-Production-Gathering-Enhanced/main/version.json?_=${Date.now()}`,
];
for (const url of urls) {
try {
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), 5000); // 5秒超时
const response = await fetch(url, {
cache: 'no-cache',
signal: controller.signal
});
clearTimeout(timeoutId);
const data = await response.json();
this.versionInfo.latest = data.version;
this.versionInfo.updateTime = data.update_time;
this.versionInfo.changelog = data.changelog;
this.updateVersionDisplay();
return;
} catch (error) {
console.warn(`Failed to load from ${url}:`, error);
}
}
console.error('All version sources failed');
}
// 检查是否有可用更新
hasUpdate(currentVersion, latestVersion) {
if (!currentVersion || !latestVersion) return false;
const currentParts = currentVersion.split('.').map(n => parseInt(n) || 0);
const latestParts = latestVersion.split('.').map(n => parseInt(n) || 0);
for (let i = 0; i < 3; i++) {
const currentNum = currentParts[i] || 0;
const latestNum = latestParts[i] || 0;
if (latestNum > currentNum) return true;
if (latestNum < currentNum) return false;
}
return false;
}
// 更新版本显示
updateVersionDisplay() {
const versionElement = document.querySelector('.version-info');
const updateButton = document.querySelector('.check-update-btn');
if (versionElement) {
const isUpdateAvailable = this.hasUpdate(this.versionInfo.current, this.versionInfo.latest);
versionElement.innerHTML = this.renderVersionInfoHTML();
if (updateButton) {
if (isUpdateAvailable) {
updateButton.textContent = `${LANG.settings.newVersion}`;
updateButton.style.backgroundColor = 'rgba(76, 175, 80, 0.8)';
} else {
updateButton.textContent = `${LANG.settings.latestVersion}`;
updateButton.style.backgroundColor = 'rgba(158, 158, 158, 0.8)';
}
}
}
}
// 检查更新
async checkForUpdates() {
const updateButton = document.querySelector('.check-update-btn');
if (updateButton) {
updateButton.textContent = `${LANG.settings.checking}`;
updateButton.disabled = true;
}
try {
await this.loadVersionInfo();
const isUpdateAvailable = this.hasUpdate(this.versionInfo.current, this.versionInfo.latest);
// 更新版本信息显示,包括更新日志
this.showVersionDetails(isUpdateAvailable);
// 显示简单的状态提示
if (isUpdateAvailable) {
this.showToast(`${LANG.settings.newFound}`, 'success');
} else {
this.showToast(`${LANG.settings.alreadyLatest}`, 'success');
}
} catch (error) {
this.showToast(`${LANG.settings.checkFailed}`, 'error');
} finally {
if (updateButton) {
updateButton.disabled = false;
}
this.updateVersionDisplay();
}
}
// 显示版本详情和更新日志
showVersionDetails(isUpdateAvailable) {
const versionElement = document.querySelector('.version-info');
if (!versionElement) return;
versionElement.innerHTML = this.renderVersionInfoHTML();
}
// 设置观察器监听设置面板的变化
setupObserver() {
const observer = new MutationObserver((mutationsList) => {
this.handleSettingsPanel(mutationsList);
});
observer.observe(document.body, { childList: true, subtree: true });
}
// 添加自定义样式
setupStyles() {
const style = document.createElement('style');
style.textContent = `
.custom-settings-tab {
transition: all 0.2s ease;
}
.custom-settings-tab:hover {
opacity: 0.8;
}
.custom-tab-content {
padding: 20px;
background: var(--card-background);
border-radius: 8px;
margin: 16px;
border: 1px solid var(--border-separator);
}
.custom-tab-option {
display: flex;
align-items: center;
margin-bottom: 12px;
padding: 12px;
background: var(--item-background);
border-radius: 6px;
border: 1px solid var(--item-border);
transition: background-color 0.2s;
}
.custom-tab-option:hover {
background-color: var(--item-background-hover);
}
.custom-tab-option label {
margin-left: 12px;
color: var(--color-text-dark-mode);
cursor: pointer;
flex: 1;
font-size: 14px;
line-height: 1.4;
}
.custom-tab-option input[type="checkbox"] {
width: 16px;
height: 16px;
cursor: pointer;
}
.custom-tab-actions {
margin-top: 24px;
padding-top: 16px;
border-top: 1px solid var(--border-separator);
display: flex;
gap: 12px;
flex-wrap: wrap;
}
.custom-tab-button {
padding: 10px 16px;
background-color: rgba(33, 150, 243, 0.8);
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 14px;
transition: background-color 0.2s;
font-weight: 500;
}
.custom-tab-button:hover {
background-color: rgba(33, 150, 243, 0.9);
}
.custom-tab-button:disabled {
background-color: rgba(158, 158, 158, 0.5);
cursor: not-allowed;
}
.custom-tab-button.danger {
background-color: rgba(244, 67, 54, 0.8);
}
.custom-tab-button.danger:hover {
background-color: rgba(244, 67, 54, 0.9);
}
.check-update-btn {
background-color: rgba(76, 175, 80, 0.8) !important;
}
.check-update-btn:hover {
background-color: rgba(76, 175, 80, 0.9) !important;
}
.custom-tab-info {
margin-top: 20px;
padding: 16px;
background: var(--item-background-hover);
border-radius: 6px;
font-family: monospace;
font-size: 12px;
color: var(--color-text-dark-mode);
border: 1px solid var(--item-border);
}
.version-info {
margin-bottom: 12px;
}
`;
document.head.appendChild(style);
}
// 处理设置面板的变化
handleSettingsPanel(mutationsList) {
for (let mutation of mutationsList) {
if (mutation.type === 'childList' && mutation.addedNodes.length > 0) {
mutation.addedNodes.forEach(node => {
if (node.nodeType === Node.ELEMENT_NODE) {
// 检查是否是设置面板的选项卡容器
const tabsContainer = node.querySelector?.('.SettingsPanel_tabsComponentContainer__Xb_5H .TabsComponent_tabsContainer__3BDUp') ||
(node.classList?.contains('TabsComponent_tabsContainer__3BDUp') ? node : null);
if (tabsContainer && !this.processedContainers.has(tabsContainer)) {
this.addCustomTabs(tabsContainer);
}
}
});
}
}
}
// 添加自定义选项卡
addCustomTabs(tabsContainer) {
this.processedContainers.add(tabsContainer);
// 获取现有的选项卡容器和面板容器
const tabsFlexContainer = tabsContainer.querySelector('.MuiTabs-flexContainer');
const tabPanelsContainer = tabsContainer.closest('.SettingsPanel_tabsComponentContainer__Xb_5H')
?.querySelector('.TabsComponent_tabPanelsContainer__26mzo');
if (!tabsFlexContainer || !tabPanelsContainer) return;
// 为每个自定义选项卡创建按钮和内容
this.customTabsData.forEach((tabData, index) => {
this.createCustomTab(tabsFlexContainer, tabPanelsContainer, tabData, index);
});
// 同时监听按钮点击和面板变化
this.bindNativeTabEvents(tabsFlexContainer, tabPanelsContainer);
this.observeTabPanelChanges(tabPanelsContainer, tabsFlexContainer);
}
// 绑定原生标签事件
bindNativeTabEvents(tabsFlexContainer, tabPanelsContainer) {
// 使用事件委托监听所有标签点击
tabsFlexContainer.addEventListener('click', (e) => {
const clickedTab = e.target.closest('.MuiTab-root');
// 如果点击的是原生标签(非自定义标签)
if (clickedTab && !clickedTab.classList.contains('custom-settings-tab')) {
// 立即隐藏自定义面板和取消选中状态
this.hideAllCustomTabPanels(tabPanelsContainer);
this.unselectAllCustomTabs(tabsFlexContainer);
}
}, true); // 使用捕获阶段确保在原生处理器之前执行
}
// 观察标签面板变化(作为补充检测)
observeTabPanelChanges(tabPanelsContainer, tabsFlexContainer) {
const observer = new MutationObserver((mutations) => {
mutations.forEach((mutation) => {
if (mutation.type === 'attributes' && mutation.attributeName === 'class') {
const target = mutation.target;
// 如果是原生面板变为可见状态
if (target.classList.contains('TabPanel_tabPanel__tXMJF') &&
!target.classList.contains('TabPanel_hidden__26UM3') &&
!target.id.includes('custom-tab-')) {
// 确保自定义面板被隐藏
this.hideAllCustomTabPanels(tabPanelsContainer);
this.unselectAllCustomTabs(tabsFlexContainer);
}
}
});
});
// 观察所有面板的class变化
tabPanelsContainer.querySelectorAll('.TabPanel_tabPanel__tXMJF').forEach(panel => {
observer.observe(panel, { attributes: true, attributeFilter: ['class'] });
});
// 也观察容器本身,以防新增面板
observer.observe(tabPanelsContainer, { childList: true, subtree: true });
}
// 隐藏所有自定义标签面板
hideAllCustomTabPanels(tabPanelsContainer) {
this.customTabsData.forEach(tabData => {
const panel = document.getElementById(`${tabData.id}-panel`);
if (panel) {
panel.classList.add('TabPanel_hidden__26UM3');
}
});
}
// 取消所有自定义标签的选中状态
unselectAllCustomTabs(tabsFlexContainer) {
this.customTabsData.forEach(tabData => {
const tab = document.getElementById(tabData.id);
if (tab) {
tab.classList.remove('Mui-selected');
tab.setAttribute('aria-selected', 'false');
}
});
}
// 创建单个自定义选项卡
createCustomTab(tabsFlexContainer, tabPanelsContainer, tabData, index) {
// 检查是否已存在
if (document.getElementById(tabData.id)) return;
// 创建选项卡按钮
const tabButton = this.createTabButton(tabData);
// 创建选项卡面板
const tabPanel = this.createTabPanel(tabData);
// 添加到容器中
tabsFlexContainer.appendChild(tabButton);
tabPanelsContainer.appendChild(tabPanel);
// 绑定点击事件
this.bindTabEvents(tabButton, tabPanel, tabsFlexContainer, tabPanelsContainer);
}
// 创建选项卡按钮
createTabButton(tabData) {
const button = document.createElement('button');
button.id = tabData.id;
button.className = 'MuiButtonBase-root MuiTab-root MuiTab-textColorPrimary css-1q2h7u5 custom-settings-tab';
button.setAttribute('tabindex', '-1');
button.setAttribute('type', 'button');
button.setAttribute('role', 'tab');
button.setAttribute('aria-selected', 'false');
button.innerHTML = `
<span class="MuiBadge-root TabsComponent_badge__1Du26 css-1rzb3uu">
${LANG.settings.tabName}
<span class="MuiBadge-badge MuiBadge-standard MuiBadge-invisible MuiBadge-anchorOriginTopRight MuiBadge-anchorOriginTopRightRectangular MuiBadge-overlapRectangular css-vwo4eg"></span>
</span>
<span class="MuiTouchRipple-root css-w0pj6f"></span>
`;
return button;
}
// 创建选项卡面板
createTabPanel(tabData) {
const panel = document.createElement('div');
panel.id = `${tabData.id}-panel`;
panel.className = 'TabPanel_tabPanel__tXMJF TabPanel_hidden__26UM3';
// 创建面板内容
const content = tabData.content();
panel.appendChild(content);
return panel;
}
// 绑定选项卡事件
bindTabEvents(tabButton, tabPanel, tabsFlexContainer, tabPanelsContainer) {
tabButton.addEventListener('click', () => {
// 隐藏所有选项卡面板
tabPanelsContainer.querySelectorAll('.TabPanel_tabPanel__tXMJF').forEach(panel => {
panel.classList.add('TabPanel_hidden__26UM3');
});
// 取消所有选项卡的选中状态
tabsFlexContainer.querySelectorAll('.MuiTab-root').forEach(tab => {
tab.classList.remove('Mui-selected');
tab.setAttribute('aria-selected', 'false');
});
// 显示当前选项卡面板
tabPanel.classList.remove('TabPanel_hidden__26UM3');
// 设置当前选项卡为选中状态
tabButton.classList.add('Mui-selected');
tabButton.setAttribute('aria-selected', 'true');
// 更新指示器位置
this.updateTabIndicator(tabButton, tabsFlexContainer);
});
}
// 更新选项卡指示器位置
updateTabIndicator(selectedTab, tabsContainer) {
const indicator = tabsContainer.parentNode.querySelector('.MuiTabs-indicator');
if (!indicator) return;
const rect = selectedTab.getBoundingClientRect();
const containerRect = tabsContainer.getBoundingClientRect();
indicator.style.left = `${rect.left - containerRect.left}px`;
indicator.style.width = `${rect.width}px`;
}
renderVersionInfoHTML() {
const isUpdateAvailable = this.hasUpdate(this.versionInfo.current, this.versionInfo.latest);
const statusIcon = isUpdateAvailable
? `<span style="color: #f44336;">${LANG.settings.hasUpdate}</span>`
: `<span style="color: #4caf50;">${LANG.settings.isLatest}</span>`;
return `
<div><strong>${LANG.settings.version}:</strong> ${this.versionInfo.current}</div>
${this.versionInfo.latest ? `
<div><strong>${LANG.settings.latestLabel}:</strong> ${this.versionInfo.latest} ${statusIcon}</div>
<div><strong>${LANG.settings.updateTime}:</strong> ${this.versionInfo.updateTime}</div>
${this.versionInfo.changelog ? `<div><strong>${LANG.settings.changelog}:</strong> ${this.versionInfo.changelog}</div>` : ''}
` : `<div>${LANG.settings.loadingInfo}</div>`}
<div>
<strong>
<a href="https://github.com/CYR2077/MWI-Production-Gathering-Enhanced/releases/latest/download/script.user.js"
target="_blank"
style="color: #2196F3; text-decoration: none;"
onmouseover="this.style.textDecoration='underline'"
onmouseout="this.style.textDecoration='none'">
更新地址 Update Link
</a>
</strong>
</div>
<div>
<strong>
<a href="https://hub.gitmirror.com/https://github.com/CYR2077/MWI-Production-Gathering-Enhanced/releases/latest/download/script.user.js"
target="_blank"
style="color: #2196F3; text-decoration: none;"
onmouseover="this.style.textDecoration='underline'"
onmouseout="this.style.textDecoration='none'">
镜像地址(不用翻墙) Mirror Link
</a>
</strong>
</div>
`;
}
// 创建脚本设置选项卡内容
createScriptsTabContent() {
const container = document.createElement('div');
container.className = 'custom-tab-content';
container.innerHTML = `
<div class="custom-tab-option">
<input type="checkbox" id="considerArtisanTea" ${window.PGE_CONFIG?.considerArtisanTea ? 'checked' : ''}>
<label for="considerArtisanTea">
<strong>🍵 ${LANG.settings.considerArtisanTea.title}</strong><br>
<span style="font-size: 12px; opacity: 0.8;">${LANG.settings.considerArtisanTea.description}</span>
</label>
</div>
<div class="custom-tab-option">
<input type="checkbox" id="considerRareLoot" ${window.PGE_CONFIG?.considerRareLoot ? 'checked' : ''}>
<label for="considerRareLoot">
<strong>💎 ${LANG.settings.considerRareLoot.title}</strong><br>
<span style="font-size: 12px; opacity: 0.8;">${LANG.settings.considerRareLoot.description}</span>
</label>
</div>
<div class="custom-tab-option">
<input type="checkbox" id="autoClaimMarketListings" ${window.PGE_CONFIG?.autoClaimMarketListings ? 'checked' : ''}>
<label for="autoClaimMarketListings">
<strong>🎁 ${LANG.settings.autoClaimMarketListings.title}</strong><br>
<span style="font-size: 12px; opacity: 0.8;">${LANG.settings.autoClaimMarketListings.description}</span>
</label>
</div>
<div class="custom-tab-option">
<input type="checkbox" id="quickSell" ${window.PGE_CONFIG?.quickSell ? 'checked' : ''}>
<label for="quickSell">
<strong>⚡ ${LANG.settings.quickSell.title}</strong><br>
<span style="font-size: 12px; opacity: 0.8;">${LANG.settings.quickSell.description}</span>
</label>
</div>
<div class="custom-tab-option">
<input type="checkbox" id="quickPurchase" ${window.PGE_CONFIG?.quickPurchase ? 'checked' : ''}>
<label for="quickPurchase">
<strong>🛒 ${LANG.settings.quickPurchase.title}</strong><br>
<span style="font-size: 12px; opacity: 0.8;">${LANG.settings.quickPurchase.description}</span>
</label>
</div>
<div class="custom-tab-option">
<input type="checkbox" id="universalProfit" ${window.PGE_CONFIG?.universalProfit ? 'checked' : ''}>
<label for="universalProfit">
<strong>📊 ${LANG.settings.universalProfit.title}</strong><br>
<span style="font-size: 12px; opacity: 0.8;">${LANG.settings.universalProfit.description}</span>
</label>
</div>
<div class="custom-tab-option">
<input type="checkbox" id="alchemyProfit" ${window.PGE_CONFIG?.alchemyProfit ? 'checked' : ''}>
<label for="alchemyProfit">
<strong>🧪 ${LANG.settings.alchemyProfit.title}</strong><br>
<span style="font-size: 12px; opacity: 0.8;">${LANG.settings.alchemyProfit.description}</span>
</label>
</div>
<div class="custom-tab-option">
<input type="checkbox" id="gatheringEnhanced" ${window.PGE_CONFIG?.gatheringEnhanced ? 'checked' : ''}>
<label for="gatheringEnhanced">
<strong>🎯 ${LANG.settings.gatheringEnhanced.title}</strong><br>
<span style="font-size: 12px; opacity: 0.8;">${LANG.settings.gatheringEnhanced.description}</span>
</label>
</div>
<div class="custom-tab-option">
<input type="checkbox" id="characterSwitcher" ${window.PGE_CONFIG?.characterSwitcher ? 'checked' : ''}>
<label for="characterSwitcher">
<strong>👤 ${LANG.settings.characterSwitcher.title}</strong><br>
<span style="font-size: 12px; opacity: 0.8;">${LANG.settings.characterSwitcher.description}</span>
</label>
</div>
<div class="custom-tab-option">
<input type="checkbox" id="itemValueCalculator" ${window.PGE_CONFIG?.itemValueCalculator ? 'checked' : ''}>
<label for="itemValueCalculator">
<strong>💰 ${LANG.settings.itemValueCalculator.title}</strong><br>
<span style="font-size: 12px; opacity: 0.8;">${LANG.settings.itemValueCalculator.description}</span>
</label>
</div>
<div class="custom-tab-actions">
<button class="custom-tab-button" onclick="window.settingsTabManager.resetSettings()">
${LANG.settings.resetToDefault}
</button>
<button class="custom-tab-button check-update-btn" onclick="window.settingsTabManager.checkForUpdates()">
${LANG.settings.checkUpdate}
</button>
<button class="custom-tab-button danger" onclick="window.settingsTabManager.reloadPage()">
${LANG.settings.reloadPage}
</button>
</div>
<div class="custom-tab-info">
<div class="version-info">${this.renderVersionInfoHTML()}</div>
</div>
`;
// 绑定设置变更事件
container.addEventListener('change', (e) => {
if (e.target.type === 'checkbox') {
this.updateConfig(e.target.id, e.target.checked);
// 自动保存设置
if (window.saveConfig && window.PGE_CONFIG) {
window.saveConfig(window.PGE_CONFIG);
}
// 对于相关设置,立即更新计算器
if (e.target.id === 'considerRareLoot') {
if (window.MWIModules?.alchemyCalculator) {
window.MWIModules.alchemyCalculator.updateProfitDisplay();
}
if (window.MWIModules?.universalCalculator) {
window.MWIModules.universalCalculator.updateProfitDisplay();
}
}
// 对于自动收集市场订单,立即生效
if (e.target.id === 'autoClaimMarketListings') {
const manager = window.MWIModules?.autoClaimMarketListings;
if (manager) {
manager.updateConfig(e.target.checked);
}
}
}
});
return container;
}
// 更新配置
updateConfig(key, value) {
if (window.PGE_CONFIG) {
window.PGE_CONFIG[key] = value;
if (key === 'quickSell') {
const manager = window.MWIModules?.quickSell;
if (value && !manager) {
// 启用功能
window.MWIModules.quickSell = new QuickSellManager();
} else if (!value && manager) {
// 禁用功能
manager.disable();
} else if (manager) {
// 更新现有实例的状态
if (value) {
manager.enable();
} else {
manager.disable();
}
}
}
// 对于自动收集市场订单,立即生效
if (key === 'autoClaimMarketListings') {
const manager = window.MWIModules.autoClaimMarketListings;
if (value && !manager) {
// 启用功能
window.MWIModules.autoClaimMarketListings = new AutoClaimMarketListingsManager();
} else if (!value && manager) {
// 禁用功能
manager.cleanup();
window.MWIModules.autoClaimMarketListings = null;
} else if (manager) {
// 更新现有实例的配置
manager.updateConfig(value);
}
}
}
}
// 重置设置
resetSettings() {
// 重置为默认配置
const defaultConfig = {
quickPurchase: true,
universalProfit: true,
alchemyProfit: true,
gatheringEnhanced: true,
characterSwitcher: true,
considerArtisanTea: true,
autoClaimMarketListings: false,
considerRareLoot: false,
itemValueCalculator: true,
quickSell: true,
};
window.PGE_CONFIG = { ...defaultConfig };
// 自动保存重置后的配置
if (window.saveConfig) {
window.saveConfig(window.PGE_CONFIG);
}
// 更新UI
Object.keys(defaultConfig).forEach(key => {
const checkbox = document.getElementById(key);
if (checkbox) {
checkbox.checked = defaultConfig[key];
}
});
this.showToast(LANG.settings.settingsReset, 'success');
}
// 重新加载页面
reloadPage() {
window.location.reload(true);
}
// 显示提示
showToast(message, type) {
if (window.MWIModules?.toast) {
window.MWIModules.toast.show(message, type);
} else {
alert(message);
}
}
}
// ==================== 初始化设置面板标签管理器 ====================
function initSettingsTabManager() {
if (!window.settingsTabManager) {
window.settingsTabManager = new SettingsTabManager();
}
}
// ==================== 自动收集市场订单管理器 ====================
class AutoClaimMarketListingsManager {
constructor() {
this.lastExecutionTime = 0;
this.cooldownTime = 3000; // 3秒冷却时间
this.observer = null;
this.isEnabled = window.PGE_CONFIG?.autoClaimMarketListings ?? true;
this.init();
}
init() {
if (!this.isEnabled) return;
this.startObserving();
}
enable() {
this.isEnabled = true;
this.startObserving();
}
disable() {
this.isEnabled = false;
this.stopObserving();
}
startObserving() {
if (this.observer || !this.isEnabled) return;
this.observer = new MutationObserver(() => {
this.checkAndExecute();
});
// 开始监控
this.observer.observe(document.body, {
childList: true,
subtree: true
});
// 立即检查一次
this.checkAndExecute();
}
stopObserving() {
if (this.observer) {
this.observer.disconnect();
this.observer = null;
}
}
checkAndExecute() {
if (!this.isEnabled) return;
// 获取所有导航栏元素
const navElements = document.querySelectorAll('.NavigationBar_nav__3uuUl');
if (navElements.length > 1) {
const targetElement = navElements[1].querySelector('.NavigationBar_badges__3D2s5');
if (targetElement) {
this.executeClaimAction();
}
}
}
executeClaimAction() {
const currentTime = Date.now();
// 检查冷却时间
if (currentTime - this.lastExecutionTime < this.cooldownTime) {
return false;
}
try {
if (window.PGE?.core?.handleClaimAllMarketListings) {
window.PGE.core.handleClaimAllMarketListings();
this.lastExecutionTime = currentTime;
return true;
}
} catch (error) {
console.error('[AutoClaimMarketListings] 执行出错:', error);
}
return false;
}
// 更新配置
updateConfig(enabled) {
const wasEnabled = this.isEnabled;
this.isEnabled = enabled;
if (enabled && !wasEnabled) {
this.startObserving();
} else if (!enabled && wasEnabled) {
this.stopObserving();
}
}
// 清理资源
cleanup() {
this.stopObserving();
}
}
// ==================== 角色快速切换 ====================
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.addEventListener('click', (e) => {
e.preventDefault();
window.PGE.core.handleViewProfile(character.name);
this.closeDropdown();
});
} else {
characterButton.href = character.link;
}
const statusText = isCurrentCharacter ? this.getText('current') : this.getText('switch');
const statusColor = isCurrentCharacter ? '#2196F3' : '#4CAF50';
const onlineStatus = character.isOnline ?
`<span style="color: #4CAF50;">●</span> Online` :
`<span style="color: #f44336;">●</span> ${this.getText('lastOnline')}: ${character.lastOnlineText}`;
characterButton.innerHTML = `
<div style="display: flex; justify-content: space-between; align-items: center;">
<div style="flex: 1;">
<div style="font-weight: ${isCurrentCharacter ? 'bold' : 'normal'};">
${character.displayText || character.name || 'Unknown'}
</div>
<div style="font-size: 10px; opacity: 0.6; margin-top: 2px;">
${onlineStatus}
</div>
</div>
<div style="font-size: 11px; color: ${statusColor};">
${statusText}
</div>
</div>
`;
if (isCurrentCharacter) {
Object.assign(characterButton.style, {
backgroundColor: 'rgba(33, 150, 243, 0.2)',
borderColor: 'rgba(33, 150, 243, 0.4)'
});
}
if (!isCurrentCharacter) {
characterButton.addEventListener('mouseover', () => Object.assign(characterButton.style, hoverStyle));
characterButton.addEventListener('mouseout', () => Object.assign(characterButton.style, buttonStyle));
} else {
// 当前角色的悬停效果(稍微不同的颜色)
characterButton.addEventListener('mouseover', () => {
characterButton.style.backgroundColor = 'rgba(33, 150, 243, 0.3)';
characterButton.style.borderColor = 'rgba(33, 150, 243, 0.6)';
});
characterButton.addEventListener('mouseout', () => {
characterButton.style.backgroundColor = 'rgba(33, 150, 243, 0.2)';
characterButton.style.borderColor = 'rgba(33, 150, 243, 0.4)';
});
}
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);
}
}
// ==================== 添加稀有掉落物价值计算工具函数 ====================
const rareDropsCalculator = {
// 计算稀有掉落物价值
calculateRareDropValue(outputItemHrid, orderBooks) {
if (!window.PGE_CONFIG.considerRareLoot) return 0;
const lootDrops = lootData[outputItemHrid];
if (!lootDrops) return 0;
let totalValue = 0;
for (const [itemHrid, quantity] of Object.entries(lootDrops)) {
let price = 0;
if (itemHrid === '/items/coin') {
price = 1; // 金币价格固定为1
} else if (itemHrid === '/items/cowbell') {
// 牛铃价格为bag_of_10_cowbells的十分之一
const bagOrderBooks = orderBooks['/items/bag_of_10_cowbells'];
if (bagOrderBooks && bagOrderBooks[0]) {
const bagPrice = bagOrderBooks[0].asks?.[0]?.price || 0;
price = bagPrice / 10;
}
} else {
// 其他物品从市场数据获取价格
const itemOrderBooks = orderBooks[itemHrid];
if (itemOrderBooks && itemOrderBooks[0]) {
price = itemOrderBooks[0].asks?.[0]?.price || 0;
}
}
let itemValue = price * quantity;
// 除了coin外都要考虑税费
if (itemHrid !== '/items/coin') {
itemValue *= 0.98;
}
totalValue += itemValue;
}
return totalValue;
},
// 获取稀有掉落物相关的物品列表
getRareDropItems(outputItemHrid) {
const lootDrops = lootData[outputItemHrid];
if (!lootDrops) return [];
return Object.keys(lootDrops).filter(itemHrid =>
itemHrid !== '/items/coin' && itemHrid !== '/items/cowbell'
);
}
};
// ==================== 基础利润计算器类 ====================
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.PGE?.core || !this.api?.isReady) {
await utils.delay(100);
}
try {
window.PGE.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.PGE?.core) {
return resolve(null);
}
this.requestQueue.push({ itemHrid, resolve });
this.processQueue();
});
}
async processQueue() {
if (this.isProcessing || !this.requestQueue.length || !this.initialized || !window.PGE?.core) return;
this.isProcessing = true;
while (this.requestQueue.length > 0) {
const batch = this.requestQueue.splice(0, 1);
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.PGE.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(300);
}
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.PGE?.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;
`;
container.innerHTML = `
<div style="display: flex; align-items: center; gap: 8px">
<span style="color: ${CONFIG.COLORS.space300}">${LANG.askBuyBidSell}</span>
<span id="${this.getPessimisticId()}" style="font-weight: 500">${this.initialized ? LANG.loadingMarketData : this.getWaitingText()}</span>
</div>
<div style="display: flex; align-items: center; gap: 8px">
<span style="color: ${CONFIG.COLORS.space300}">${LANG.bidBuyAskSell}</span>
<span id="${this.getOptimisticId()}" style="font-weight: 500">${this.initialized ? LANG.loadingMarketData : this.getWaitingText()}</span>
</div>
`;
return container;
}
checkForUpdates() {
const currentState = this.getStateFingerprint();
if (currentState !== this.lastState && currentState) {
this.lastState = currentState;
this.debounceUpdate(() => this.updateProfitDisplay());
}
}
// 子类需要实现的抽象方法
getContainerId() { throw new Error('Must implement getContainerId'); }
getPessimisticId() { throw new Error('Must implement getPessimisticId'); }
getOptimisticId() { throw new Error('Must implement getOptimisticId'); }
getWaitingText() { throw new Error('Must implement getWaitingText'); }
getActionData() { throw new Error('Must implement getActionData'); }
calculateProfit(data, useOptimistic) { throw new Error('Must implement calculateProfit'); }
getStateFingerprint() { throw new Error('Must implement getStateFingerprint'); }
setupUI() { throw new Error('Must implement setupUI'); }
}
// ==================== 炼金利润计算器 ====================
class AlchemyProfitCalculator extends BaseProfitCalculator {
constructor() {
super(CONFIG.ALCHEMY_CACHE_EXPIRY);
this.alchemyObservers = [];
this.clickListeners = []; // 新增:存储点击监听器
this.init();
}
init() {
super.init();
this.setupObserver();
}
setupObserver() {
const observer = new MutationObserver(() => {
this.setupUI();
});
observer.observe(document.body, { childList: true, subtree: true });
}
createProfitDisplay() {
const container = document.createElement('div');
container.id = 'alchemy-profit-display';
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;
`;
// 创建垂直布局
const grid = document.createElement('div');
grid.style.cssText = `
display: flex;
flex-direction: column;
gap: 8px;
`;
// 4种利润计算情况,按指定顺序排列
const profitTypes = [
{ id: 'ask-buy-bid-sell', label: LANG.askBuyBidSell, buyType: 'ask', sellType: 'bid' },
{ id: 'bid-buy-bid-sell', label: LANG.bidBuyBidSell, buyType: 'bid', sellType: 'bid' },
{ id: 'ask-buy-ask-sell', label: LANG.askBuyAskSell, buyType: 'ask', sellType: 'ask' },
{ id: 'bid-buy-ask-sell', label: LANG.bidBuyAskSell, buyType: 'bid', sellType: 'ask' }
];
profitTypes.forEach(type => {
const profitBox = document.createElement('div');
profitBox.style.cssText = `
display: flex;
align-items: center;
gap: 8px;
`;
const label = document.createElement('span');
label.textContent = type.label;
label.style.cssText = `
color: var(--color-space-300);
font-size: 14px;
`;
const value = document.createElement('span');
value.id = type.id;
value.textContent = this.getWaitingText();
value.style.cssText = `
font-weight: 500;
font-size: 14px;
`;
profitBox.appendChild(label);
profitBox.appendChild(value);
grid.appendChild(profitBox);
});
container.appendChild(grid);
return container;
}
setupUI() {
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.setupSpecificObservers();
setTimeout(() => this.updateProfitDisplay(), this.initialized ? 50 : 100);
} else if (!shouldShow && existingDisplay) {
existingDisplay.remove();
this.cleanupObservers();
}
}
setupSpecificObservers() {
// 清理旧的观察器和监听器
this.cleanupObservers();
// 设置新的观察器
this.alchemyObservers = [
this.createSpecificObserver('.ActionTypeConsumableSlots_consumableSlots__kFKk0'),
this.createSpecificObserver('.SkillActionDetail_successRate__2jPEP .SkillActionDetail_value__dQjYH'),
this.createSpecificObserver('.SkillActionDetail_catalystItemInputContainer__5zmou'),
this.createSpecificObserver('.ItemSelector_itemSelector__2eTV6')
].filter(Boolean);
// 新增:设置点击监听器
this.setupClickListeners();
}
// 新增:设置点击监听器
setupClickListeners() {
// 处理点击事件的函数
const handleClick = () => {
const currentState = this.getStateFingerprint();
if (currentState !== this.lastState) {
this.lastState = currentState;
this.debounceUpdate(() => this.updateProfitDisplay());
} else {
// 即使状态没变也强制更新一次(防止某些情况下的数据不同步)
setTimeout(() => this.updateProfitDisplay(), 100);
}
};
// 为 MuiTabs-flexContainer css-k008qs 元素添加点击监听器
const tabContainers = document.querySelectorAll('.MuiTabs-flexContainer.css-k008qs');
tabContainers.forEach(container => {
const listener = handleClick.bind(this);
container.addEventListener('click', listener, true); // 使用捕获阶段
this.clickListeners.push({ element: container, listener, type: 'click' });
});
// 为 MuiTooltip-tooltip 元素添加点击监听器
const tooltipElements = document.querySelectorAll('.MuiTooltip-tooltip');
tooltipElements.forEach(tooltip => {
const listener = handleClick.bind(this);
tooltip.addEventListener('click', listener, true); // 使用捕获阶段
this.clickListeners.push({ element: tooltip, listener, type: 'click' });
});
// 由于这些元素可能动态生成,设置一个定时检查
const checkInterval = setInterval(() => {
// 检查是否有新的标签容器元素
const newTabContainers = document.querySelectorAll('.MuiTabs-flexContainer.css-k008qs');
newTabContainers.forEach(container => {
const alreadyListening = this.clickListeners.some(l => l.element === container);
if (!alreadyListening) {
const listener = handleClick.bind(this);
container.addEventListener('click', listener, true);
this.clickListeners.push({ element: container, listener, type: 'click' });
}
});
// 检查是否有新的工具提示元素
const newTooltipElements = document.querySelectorAll('.MuiTooltip-tooltip');
newTooltipElements.forEach(tooltip => {
const alreadyListening = this.clickListeners.some(l => l.element === tooltip);
if (!alreadyListening) {
const listener = handleClick.bind(this);
tooltip.addEventListener('click', listener, true);
this.clickListeners.push({ element: tooltip, listener, type: 'click' });
}
});
}, 1000);
// 将定时器也存储起来,以便清理
this.clickListeners.push({
element: null,
listener: null,
type: 'interval',
intervalId: checkInterval
});
}
createSpecificObserver(selector) {
const element = document.querySelector(selector);
if (!element) return null;
const observer = new MutationObserver(() => {
const currentState = this.getStateFingerprint();
if (currentState !== this.lastState) {
this.lastState = currentState;
this.debounceUpdate(() => this.updateProfitDisplay());
}
});
observer.observe(element, {
childList: true,
subtree: true,
attributes: true,
characterData: true
});
return observer;
}
cleanupObservers() {
// 清理MutationObserver
this.alchemyObservers.forEach(obs => obs?.disconnect());
this.alchemyObservers = [];
// 新增:清理点击监听器
this.clickListeners.forEach(listenerInfo => {
if (listenerInfo.type === 'click' && listenerInfo.element && listenerInfo.listener) {
listenerInfo.element.removeEventListener('click', listenerInfo.listener, true);
} else if (listenerInfo.type === 'interval' && listenerInfo.intervalId) {
clearInterval(listenerInfo.intervalId);
}
});
this.clickListeners = [];
}
getContainerId() { return 'alchemy-profit-display'; }
getWaitingText() { return LANG.loadingMarketData; }
getRequiredLevel() {
try {
const notesEl = document.querySelector('.SkillActionDetail_notes__2je2F');
if (!notesEl) return 0;
const match = notesEl.childNodes[0]?.textContent?.match(/\d+/);
return match ? parseInt(match[0]) : 0;
} catch (error) {
console.error('获取要求等级失败:', error);
return 0;
}
}
getBaseAlchemyLevel() {
try {
const container = document.querySelector('.SkillActionDetail_alchemyComponent__1J55d');
const props = utils.getReactProps(container);
return props?.characterSkillMap?.get('/skills/alchemy')?.level || 0;
} catch (error) {
console.error('获取基础炼金等级失败:', error);
return 0;
}
}
calculateBuffEffects() {
try {
const container = document.querySelector('.SkillActionDetail_alchemyComponent__1J55d');
const props = utils.getReactProps(container);
if (!props) return { efficiency: 0.0, alchemyLevelBonus: 0.0, actionSpeed: 0.0 };
const buffs = props.actionBuffs || [];
const baseAlchemyLevel = this.getBaseAlchemyLevel();
const requiredLevel = this.getRequiredLevel();
let efficiencyBuff = 0.0;
let alchemyLevelBonus = 0.0;
let actionSpeedBuff = 0.0;
// 计算buff效果
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;
return {
efficiency: totalEfficiency,
alchemyLevelBonus,
actionSpeed: actionSpeedBuff
};
} catch (error) {
console.error('计算buff效果失败:', error);
return { efficiency: 0.0, alchemyLevelBonus: 0.0, actionSpeed: 0.0 };
}
}
getTeaBuffDuration(container) {
try {
const props = utils.getReactProps(container);
if (!props) return 300.0; // 默认300秒
const buffs = props.actionBuffs || [];
// 查找uniqueHrid结尾为'tea'的buff
for (const buff of buffs) {
if (buff.uniqueHrid && buff.uniqueHrid.endsWith('tea')) {
const duration = buff.duration || 0;
return duration / 1e9; // 转换为秒
}
}
return 300.0; // 如果没找到茶类buff,默认300秒
} catch (error) {
console.error('获取茶类buff持续时间失败:', error);
return 300.0;
}
}
async getDrinkCosts() {
try {
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 });
}
}
return drinkCosts;
} catch (error) {
console.error('获取饮品成本失败:', error);
return [];
}
}
async getItemData(element, dropIndex = -1, reqIndex = -1) {
try {
const href = element?.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 = element.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;
} catch (error) {
console.error('获取物品数据失败:', error);
return null;
}
}
getSuccessRate() {
try {
const element = document.querySelector('.SkillActionDetail_successRate__2jPEP .SkillActionDetail_value__dQjYH');
const rawText = element?.textContent || '0.0';
return parseFloat(utils.cleanNumber(rawText)) / 100.0;
} catch (error) {
console.error('获取成功率失败:', error);
return 0.0;
}
}
hasNullPrices(data, buyType, sellType) {
const checkItems = (items, priceType) => items.some(item => item[priceType] === null);
return checkItems(data.requirements, buyType === 'ask' ? 'asks' : 'bids') ||
checkItems(data.drops, sellType === 'ask' ? 'asks' : 'bids') ||
checkItems(data.consumables, buyType === 'ask' ? 'asks' : 'bids') ||
data.catalyst[buyType === 'ask' ? 'asks' : 'bids'] === null;
}
async getMarketDataForRareDrops(outputItems) {
if (!window.PGE_CONFIG.considerRareLoot) return {};
const marketData = {};
const itemsToFetch = new Set();
// 收集所有需要获取市场数据的物品
outputItems.forEach(output => {
const rareItems = rareDropsCalculator.getRareDropItems(output.itemHrid);
rareItems.forEach(item => itemsToFetch.add(item));
});
// 添加bag_of_10_cowbells用于计算cowbell价格
itemsToFetch.add('/items/bag_of_10_cowbells');
// 批量获取市场数据
const promises = Array.from(itemsToFetch).map(async (itemHrid) => {
const orderBooks = await this.getMarketData(itemHrid);
marketData[itemHrid] = orderBooks;
});
await Promise.all(promises);
return marketData;
}
async getActionData() {
try {
const successRate = this.getSuccessRate();
if (isNaN(successRate) || successRate < 0) return null;
const buffEffects = this.calculateBuffEffects();
const timeCost = 20.0 / (1.0 + buffEffects.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, drinkCosts] = 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 }),
this.getDrinkCosts()
]);
const validDrops = drops.filter(Boolean);
// 获取稀有掉落物市场数据
const rareDropsMarketData = await this.getMarketDataForRareDrops(validDrops);
return {
successRate,
timeCost,
efficiency: buffEffects.efficiency,
requirements: requirements.filter(Boolean),
drops: validDrops,
catalyst: catalyst || { asks: 0.0, bids: 0.0 },
consumables: consumables.filter(Boolean),
drinkCosts,
rareDropsMarketData // 添加稀有掉落物市场数据
};
} catch (error) {
console.error('获取行动数据失败:', error);
return null;
}
}
calculateProfit(data, buyType, sellType) {
try {
if (this.hasNullPrices(data, buyType, sellType)) return null;
// 计算材料成本 - 使用指定的买入价格类型
const totalReqCost = data.requirements.reduce((sum, item) => {
const price = buyType === 'ask' ? item.asks : item.bids;
return sum + (price * item.count);
}, 0.0);
// 计算每次尝试的成本
const catalystPrice = buyType === 'ask' ? data.catalyst.asks : data.catalyst.bids;
const costPerAttempt = (totalReqCost * (1.0 - data.successRate)) +
((totalReqCost + catalystPrice) * data.successRate);
// 计算每次尝试的收入 - 使用指定的卖出价格类型
const incomePerAttempt = data.drops.reduce((sum, drop, index) => {
const price = sellType === 'ask' ? drop.asks : drop.bids;
let income;
// 判断是否为最后一个掉落物(稀有掉落物)
const isLastDrop = index === data.drops.length - 1;
if (isLastDrop && window.PGE_CONFIG.considerRareLoot) {
// 如果是最后一个掉落物且开启了稀有掉落物设置,计算稀有掉落物价值
const rareDropValue = rareDropsCalculator.calculateRareDropValue(drop.itemHrid, data.rareDropsMarketData);
income = rareDropValue * drop.dropRate;
} else {
// 判断是否为倒数第二个掉落物(精华)
const isSecondLastDrop = index === data.drops.length - 2;
if (isSecondLastDrop) {
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?.length > 0) {
const totalDrinkCost = data.drinkCosts.reduce((sum, drinkInfo) => {
const consumableData = data.consumables.find(c => c.itemHrid === drinkInfo.itemHrid);
if (consumableData) {
const price = buyType === 'ask' ? consumableData.asks : consumableData.bids;
return sum + price;
}
return sum;
}, 0.0);
const container = document.querySelector('.SkillActionDetail_alchemyComponent__1J55d');
const teaDuration = this.getTeaBuffDuration(container);
drinkCostPerSecond = totalDrinkCost / teaDuration;
}
const finalProfitPerSecond = profitPerSecond - drinkCostPerSecond;
const dailyProfit = finalProfitPerSecond * 86400.0;
return dailyProfit;
} catch (error) {
console.error('计算利润失败:', error);
return null;
}
}
setAllProfitsToLoading() {
const profitIds = ['ask-buy-bid-sell', 'bid-buy-bid-sell', 'ask-buy-ask-sell', 'bid-buy-ask-sell'];
profitIds.forEach(id => {
const element = document.getElementById(id);
if (element) {
element.textContent = LANG.loadingMarketData;
element.style.color = CONFIG.COLORS.text;
}
});
}
setAllProfitsToError() {
const profitIds = ['ask-buy-bid-sell', 'bid-buy-bid-sell', 'ask-buy-ask-sell', 'bid-buy-ask-sell'];
profitIds.forEach(id => {
const element = document.getElementById(id);
if (element) {
element.textContent = LANG.calculationError;
element.style.color = CONFIG.COLORS.error;
}
});
}
async updateProfitDisplay() {
try {
const container = document.getElementById('alchemy-profit-display');
if (!container) return;
this.setAllProfitsToLoading();
const data = await this.getActionData();
if (!data) {
this.setAllProfitsToError();
return;
}
// 4种利润计算情况,按指定顺序排列
const profitTypes = [
{ id: 'ask-buy-bid-sell', buyType: 'ask', sellType: 'bid' },
{ id: 'bid-buy-bid-sell', buyType: 'bid', sellType: 'bid' },
{ id: 'ask-buy-ask-sell', buyType: 'ask', sellType: 'ask' },
{ id: 'bid-buy-ask-sell', buyType: 'bid', sellType: 'ask' }
];
profitTypes.forEach(type => {
const profit = this.calculateProfit(data, type.buyType, type.sellType);
const element = document.getElementById(type.id);
if (element) {
if (profit === null) {
element.textContent = LANG.noData;
element.style.color = CONFIG.COLORS.neutral;
} else {
element.textContent = utils.formatProfit(profit);
element.style.color = profit >= 0 ? CONFIG.COLORS.profit : CONFIG.COLORS.loss;
}
}
});
} catch (error) {
console.error('更新利润显示失败:', error);
this.setAllProfitsToError();
}
}
setAllProfitsToError() {
const profitIds = ['ask-buy-bid-sell', 'bid-buy-bid-sell', 'ask-buy-ask-sell', 'bid-buy-ask-sell'];
profitIds.forEach(id => {
const element = document.getElementById(id);
if (element) {
element.textContent = LANG.calculationError;
element.style.color = CONFIG.COLORS.error;
}
});
}
getStateFingerprint() {
try {
const consumables = document.querySelectorAll('.ActionTypeConsumableSlots_consumableSlots__kFKk0 .Item_itemContainer__x7kH1');
const alchemyInfo = document.querySelector('.SkillActionDetail_info__3umoI')?.textContent || '';
const consumablesState = Array.from(consumables).map(el =>
el.querySelector('svg use')?.getAttribute('href') || 'empty'
).join('|');
return `${consumablesState}:${alchemyInfo}`;
} catch (error) {
console.error('获取状态指纹失败:', error);
return '';
}
}
}
// ==================== 生产行动利润计算器 ====================
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);
}
});
}
createProfitDisplay() {
const container = document.createElement('div');
container.id = 'universal-action-profit-display';
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: 8px;
`;
// 创建垂直布局
const grid = document.createElement('div');
grid.style.cssText = `
display: flex;
flex-direction: column;
gap: 8px;
`;
// 4种利润计算情况,按指定顺序排列
const profitTypes = [
{ id: 'universal-ask-buy-bid-sell', label: LANG.askBuyBidSell, buyType: 'ask', sellType: 'bid' },
{ id: 'universal-bid-buy-bid-sell', label: LANG.bidBuyBidSell, buyType: 'bid', sellType: 'bid' },
{ id: 'universal-ask-buy-ask-sell', label: LANG.askBuyAskSell, buyType: 'ask', sellType: 'ask' },
{ id: 'universal-bid-buy-ask-sell', label: LANG.bidBuyAskSell, buyType: 'bid', sellType: 'ask' }
];
profitTypes.forEach(type => {
const profitBox = document.createElement('div');
profitBox.style.cssText = `
display: flex;
align-items: center;
gap: 8px;
`;
const label = document.createElement('span');
label.textContent = type.label;
label.style.cssText = `
color: var(--color-space-300);
font-size: 14px;
`;
const value = document.createElement('span');
value.id = type.id;
value.textContent = this.getWaitingText();
value.style.cssText = `
font-weight: 500;
font-size: 14px;
`;
profitBox.appendChild(label);
profitBox.appendChild(value);
grid.appendChild(profitBox);
});
container.appendChild(grid);
return container;
}
getContainerId() { return 'universal-action-profit-display'; }
getWaitingText() { return LANG.loadingMarketData; }
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;
}
// 获取工匠茶buff效果
getArtisanBuff(container) {
try {
const props = utils.getReactProps(container);
if (!props) return 0.0;
const buffs = props.actionBuffs || [];
let artisanBuff = 0.0;
for (const buff of buffs) {
if (buff.typeHrid === '/buff_types/artisan') {
artisanBuff += (buff.flatBoost || 0.0);
}
}
return artisanBuff;
} catch (error) {
console.error('获取工匠茶buff失败:', error);
return 0.0;
}
}
// 获取基础材料消耗量
getBaseMaterialConsumption(materialContainer, index) {
try {
const reactKey = Object.keys(materialContainer).find(key => key.startsWith('__reactProps$'));
if (reactKey) {
const props = materialContainer[reactKey];
const baseCount = props?.children?._owner?.memoizedProps?.count;
if (typeof baseCount === 'number') {
return baseCount;
}
}
} catch (error) {
console.error('获取基础材料消耗量失败:', error);
}
return 1.0; // 默认值
}
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 };
}
getTeaBuffDuration(container) {
try {
const props = utils.getReactProps(container);
if (!props) return 300.0; // 默认300秒
const buffs = props.actionBuffs || [];
// 查找uniqueHrid结尾为'tea'的buff
for (const buff of buffs) {
if (buff.uniqueHrid && buff.uniqueHrid.endsWith('tea')) {
const duration = buff.duration || 0;
return duration / 1e9; // 转换为秒
}
}
return 300.0; // 如果没找到茶类buff,默认300秒
} catch (error) {
console.error('获取茶类buff持续时间失败:', error);
return 300.0;
}
}
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');
let baseCount = 1.0;
// 尝试从UI获取基础数量
const key = Object.keys(outputContainer.children[1] || {}).find(k => k.startsWith('__reactProps$'));
const props = outputContainer.children[1][key]?.children?._owner?.memoizedProps;
baseCount = props?.count || 1.0;
// 检查是否是第一个产出物品(通常是主要产品)
const container = document.querySelector('.SkillActionDetail_regularComponent__3oCgr');
const outputElements = container?.querySelectorAll('.SkillActionDetail_outputItems__3zp_f .Item_itemContainer__x7kH1') || [];
const isFirstOutput = outputElements.length > 0 && outputElements[0] === element;
if (isFirstOutput) {
// 对第一个产出物品应用美食buff: 1+gourmetBuff
const gourmetBuff = this.getGourmetBuff(container);
result.count = baseCount * (1 + gourmetBuff);
} else {
// 其他产出物品使用原来的逻辑
result.count = baseCount;
}
} else if (isRequirement) {
// 获取基础材料消耗量并应用工匠茶效果
const container = document.querySelector('.SkillActionDetail_regularComponent__3oCgr');
const requirementRow = element.closest('.SkillActionDetail_itemRequirements__3SPnA');
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) || itemElements[i] === element) {
itemIndex = i;
break;
}
}
}
// 获取基础消耗量
const baseConsumption = this.getBaseMaterialConsumption(element, itemIndex);
// 应用工匠茶效果
const artisanBuff = this.getArtisanBuff(container);
result.count = baseConsumption * (1 - artisanBuff);
}
return result;
}
getActionTime() {
try {
const container = document.querySelector('.SkillActionDetail_regularComponent__3oCgr');
if (!container) return 0.0;
const props = utils.getReactProps(container);
if (!props) return 0.0;
const baseTimeCost = props.actionDetail?.baseTimeCost;
if (!baseTimeCost) return 0.0;
// 获取速度buff
const speedBuff = this.getSpeedBuff(container);
// 计算实际行动时间: baseTimeCost/1e9/(1+speedBuff)
const actionTime = (baseTimeCost / 1e9) / (1 + speedBuff);
return actionTime;
} catch (error) {
console.error('获取行动时间失败:', error);
// 如果失败,回退到原来的方法
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;
}
}
// 获取速度buff效果
getSpeedBuff(container) {
try {
const props = utils.getReactProps(container);
if (!props) return 0.0;
const buffs = props.actionBuffs || [];
let speedBuff = 0.0;
for (const buff of buffs) {
if (buff.typeHrid === '/buff_types/action_speed') {
speedBuff += (buff.flatBoost || 0.0);
}
}
return speedBuff;
} catch (error) {
console.error('获取速度buff失败:', error);
return 0.0;
}
}
// 获取美食家buff效果
getGourmetBuff(container) {
try {
const props = utils.getReactProps(container);
if (!props) return 0.0;
const buffs = props.actionBuffs || [];
let gourmetBuff = 0.0;
for (const buff of buffs) {
if (buff.typeHrid === '/buff_types/gourmet') {
gourmetBuff += (buff.flatBoost || 0.0);
}
}
return gourmetBuff;
} catch (error) {
console.error('获取美食家buff失败:', error);
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, buyType, sellType) {
const checkRequirements = (items, priceType) => items.some(item =>
item[priceType] === null || item[priceType] <= 0.0
);
const checkOutputs = (items, priceType) => items.some(item =>
item[priceType] === null || item[priceType] <= 0.0
);
const checkUpgrades = (items, priceType) => items.some(item =>
item[priceType] === null || item[priceType] <= 0.0
);
const checkDrinks = (drinks, priceType) => drinks.some(drink =>
drink[priceType] === null || drink[priceType] <= 0.0
);
return checkRequirements(data.requirements, buyType === 'ask' ? 'asks' : 'bids') ||
checkOutputs(data.outputs, sellType === 'ask' ? 'asks' : 'bids') ||
checkUpgrades(data.upgrades || [], buyType === 'ask' ? 'asks' : 'bids') ||
checkDrinks(data.drinkCosts || [], buyType === 'ask' ? 'asks' : 'bids');
}
async getMarketDataForRareDrops(outputItems) {
if (!window.PGE_CONFIG.considerRareLoot) return {};
const marketData = {};
const itemsToFetch = new Set();
// 收集所有需要获取市场数据的物品
outputItems.forEach(output => {
const rareItems = rareDropsCalculator.getRareDropItems(output.itemHrid);
rareItems.forEach(item => itemsToFetch.add(item));
});
// 添加bag_of_10_cowbells用于计算cowbell价格
itemsToFetch.add('/items/bag_of_10_cowbells');
// 批量获取市场数据
const promises = Array.from(itemsToFetch).map(async (itemHrid) => {
const orderBooks = await this.getMarketData(itemHrid);
marketData[itemHrid] = orderBooks;
});
await Promise.all(promises);
return marketData;
}
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();
const validDrops = drops.filter(Boolean);
// 获取稀有掉落物市场数据
const rareDropsMarketData = await this.getMarketDataForRareDrops(validDrops);
return {
actionTime,
efficiency: buffData.efficiency,
drinkCosts: buffData.drinkCosts,
requirements: requirements.filter(Boolean),
outputs: outputs.filter(Boolean),
drops: validDrops,
upgrades: upgrades.filter(Boolean),
rareDropsMarketData // 添加稀有掉落物市场数据
};
}
calculateProfit(data, buyType, sellType) {
if (this.hasNullPrices(data, buyType, sellType)) return null;
if (data.actionTime <= 0.0) return null;
// 计算成本 - 使用指定的买入价格类型
let totalCost = 0.0;
data.requirements.forEach(item => {
const price = buyType === 'ask' ? item.asks : item.bids;
totalCost += price * item.count;
});
if (data.upgrades.length > 0) {
data.upgrades.forEach(item => {
const price = buyType === 'ask' ? item.asks : item.bids;
totalCost += price * item.count;
});
}
const effectiveTime = data.actionTime / (1.0 + data.efficiency);
// 计算收入 - 使用指定的卖出价格类型
let totalIncome = 0.0;
data.outputs.forEach(item => {
const price = sellType === 'ask' ? 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, index) => {
const price = sellType === 'ask' ? item.asks : item.bids;
let income;
// 判断是否为最后一个掉落物(稀有掉落物)
const isLastDrop = index === data.drops.length - 1;
if (isLastDrop && window.PGE_CONFIG.considerRareLoot) {
// 如果是最后一个掉落物且开启了稀有掉落物设置,计算稀有掉落物价值
const dropRate = this.parseDropRate(item.itemHrid) || 0.05;
const rareDropValue = rareDropsCalculator.calculateRareDropValue(item.itemHrid, data.rareDropsMarketData);
income = rareDropValue * dropRate;
} else {
const dropRate = this.parseDropRate(item.itemHrid) || 0.05;
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 = buyType === 'ask' ? item.asks : item.bids;
return sum + price;
}, 0.0);
const container = document.querySelector('.SkillActionDetail_regularComponent__3oCgr');
const teaDuration = this.getTeaBuffDuration(container);
drinkCostPerSecond = totalDrinkCost / teaDuration;
}
const finalProfitPerSecond = profitPerSecond - drinkCostPerSecond;
const dailyProfit = finalProfitPerSecond * 86400.0;
return dailyProfit;
}
async updateProfitDisplay() {
try {
const container = document.getElementById('universal-action-profit-display');
if (!container) return;
const data = await this.getActionData();
if (!data) {
this.setAllProfitsToError();
return;
}
// 4种利润计算情况,按指定顺序排列
const profitTypes = [
{ id: 'universal-ask-buy-bid-sell', buyType: 'ask', sellType: 'bid' },
{ id: 'universal-bid-buy-bid-sell', buyType: 'bid', sellType: 'bid' },
{ id: 'universal-ask-buy-ask-sell', buyType: 'ask', sellType: 'ask' },
{ id: 'universal-bid-buy-ask-sell', buyType: 'bid', sellType: 'ask' }
];
profitTypes.forEach(type => {
const profit = this.calculateProfit(data, type.buyType, type.sellType);
const element = document.getElementById(type.id);
if (element) {
if (profit === null) {
element.textContent = LANG.noData;
element.style.color = CONFIG.COLORS.neutral;
} else {
element.textContent = utils.formatProfit(profit);
element.style.color = profit >= 0 ? CONFIG.COLORS.profit : CONFIG.COLORS.loss;
}
}
});
} catch (error) {
console.error('更新利润显示失败:', error);
this.setAllProfitsToError();
}
}
setAllProfitsToError() {
const profitIds = ['universal-ask-buy-bid-sell', 'universal-bid-buy-bid-sell', 'universal-ask-buy-ask-sell', 'universal-bid-buy-ask-sell'];
profitIds.forEach(id => {
const element = document.getElementById(id);
if (element) {
element.textContent = LANG.calculationError;
element.style.color = CONFIG.COLORS.error;
}
});
}
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();
}
}
checkForUpdates() {
const currentState = this.getStateFingerprint();
if (currentState !== this.lastState) {
this.lastState = currentState;
this.debounceUpdate(() => this.updateProfitDisplay());
}
}
}
// ==================== 物品价值计算器 ====================
class ItemValueCalculator extends BaseProfitCalculator {
constructor() {
super(CONFIG.UNIVERSAL_CACHE_EXPIRY);
this.characterId = window.PGE?.characterData?.character.id;
this.jsonMarketData = null;
this.jsonStorageKey = `PGE_MARKET_DATA`;
this.storageKey = `MWI_ITEM_VALUE_HISTORY_${this.characterId}`;
this.recordInterval = 30 * 1000 * 60; // 30分钟
this.maxHistoryDays = 30; // 最多保留30天
this.compressionThreshold = 7; // 7天后开始压缩
this.autoRecordTimer = null;
this.incrementButtonObserver = null;
this.chartViewer = null;
this.init();
}
async init() {
await super.init();
this.loadJsonMarketDataFromStorage();
this.startAutoRecord();
this.cleanupOldData();
this.setupIncrementButtonObserver();
this.chartViewer = new AssetChartViewer(this);
await this.calculateItemValues();
}
// 从localStorage加载JSON数据
loadJsonMarketDataFromStorage() {
try {
const saved = localStorage.getItem(this.jsonStorageKey);
if (saved) {
this.jsonMarketData = JSON.parse(saved);
return true;
}
} catch (error) {
console.error('[ItemValueCalculator] 从localStorage加载JSON市场数据失败:', error);
}
return false;
}
// 保存JSON数据到localStorage
saveJsonMarketDataToStorage() {
try {
if (this.jsonMarketData) {
localStorage.setItem(this.jsonStorageKey, JSON.stringify(this.jsonMarketData));
}
} catch (error) {
console.error('[ItemValueCalculator] 保存JSON市场数据到localStorage失败:', error);
}
}
// 获取JSON市场数据并更新localStorage
async updateJsonMarketData() {
try {
const response = await fetch('https://raw.githubusercontent.com/holychikenz/MWIApi/main/milkyapi.json');
if (!response.ok) {
throw new Error(`HTTP错误: ${response.status}`);
}
const newData = await response.json();
const marketData = newData.market || newData;
if (!this.jsonMarketData) {
this.jsonMarketData = marketData;
} else {
let updatedCount = 0;
for (const [itemName, itemData] of Object.entries(marketData)) {
this.jsonMarketData[itemName] = itemData;
updatedCount++;
}
}
// 保存到localStorage
this.saveJsonMarketDataToStorage();
return true;
} catch (error) {
console.error('[ItemValueCalculator] 更新JSON市场数据失败:', error);
return false;
}
}
// 获取本地时间
getLocalDateString(timestamp = null) {
const date = timestamp ? new Date(timestamp) : new Date();
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, '0');
const day = String(date.getDate()).padStart(2, '0');
return `${year}-${month}-${day}`;
}
// 获取今日增量
getTodayIncrement() {
const historyData = this.getHistoryData();
const today = this.getLocalDateString(); // 使用本地时区日期
// 筛选今天的记录
const todayRecords = historyData.filter(record => record.date === today);
if (todayRecords.length === 0) {
return { askIncrement: 0, bidIncrement: 0 };
}
// 按时间排序
todayRecords.sort((a, b) => a.timestamp - b.timestamp);
const firstRecord = todayRecords[0];
const lastRecord = todayRecords[todayRecords.length - 1];
const askIncrement = lastRecord.totalAsk - firstRecord.totalAsk;
const bidIncrement = lastRecord.totalBid - firstRecord.totalBid;
return {
askIncrement,
bidIncrement
};
}
// 创建增量显示按钮
createIncrementButton() {
const button = document.createElement('button');
button.className = 'MuiButtonBase-root MuiTab-root MuiTab-textColorPrimary css-1q2h7u5';
button.setAttribute('tabindex', '-1');
button.setAttribute('type', 'button');
button.setAttribute('role', 'tab');
button.setAttribute('aria-selected', 'false');
const span = document.createElement('span');
span.className = 'MuiBadge-root TabsComponent_badge__1Du26 css-1rzb3uu';
const textSpan = document.createElement('span');
// 使用class而不是id,避免重复id问题
const askSpan = document.createElement('span');
askSpan.className = 'ask-increment';
askSpan.textContent = '0';
const separatorSpan = document.createElement('span');
separatorSpan.textContent = ' / ';
separatorSpan.style.color = 'rgba(255, 255, 255, 0.7)';
const bidSpan = document.createElement('span');
bidSpan.className = 'bid-increment';
bidSpan.textContent = '0';
textSpan.appendChild(askSpan);
textSpan.appendChild(separatorSpan);
textSpan.appendChild(bidSpan);
const badge = document.createElement('span');
badge.className = 'MuiBadge-badge MuiBadge-standard MuiBadge-invisible MuiBadge-anchorOriginTopRight MuiBadge-anchorOriginTopRightRectangular MuiBadge-overlapRectangular MuiBadge-colorWarning css-dpce5z';
span.appendChild(textSpan);
span.appendChild(badge);
const ripple = document.createElement('span');
ripple.className = 'MuiTouchRipple-root css-w0pj6f';
button.appendChild(span);
button.appendChild(ripple);
button.addEventListener('click', (e) => {
e.preventDefault();
if (this.chartViewer) {
this.chartViewer.show();
}
this.updateIncrementDisplay();
});
button.addEventListener('mouseenter', () => {
button.style.backgroundColor = 'rgba(255, 255, 255, 0.04)';
});
button.addEventListener('mouseleave', () => {
button.style.backgroundColor = '';
});
return button;
}
// 更新增量显示
updateIncrementDisplay() {
const { askIncrement, bidIncrement } = this.getTodayIncrement();
// 查找所有增量按钮并更新(最多2个)
const buttons = document.querySelectorAll('[id^="value-increment-button"]');
buttons.forEach((button, index) => {
const askSpan = button.querySelector('#ask-increment') || button.querySelector('.ask-increment');
const bidSpan = button.querySelector('#bid-increment') || button.querySelector('.bid-increment');
if (askSpan && bidSpan) {
const formattedAsk = utils.formatProfit(askIncrement);
const formattedBid = utils.formatProfit(bidIncrement);
askSpan.textContent = formattedAsk;
bidSpan.textContent = formattedBid;
// 设置颜色
askSpan.style.color = askIncrement >= 0 ? '#4CAF50' : '#f44336';
bidSpan.style.color = bidIncrement >= 0 ? '#4CAF50' : '#f44336';
if (askIncrement === 0) {
askSpan.style.color = 'rgba(255, 255, 255, 0.7)';
}
if (bidIncrement === 0) {
bidSpan.style.color = 'rgba(255, 255, 255, 0.7)';
}
}
});
}
// 设置按钮插入观察器
setupIncrementButtonObserver() {
this.incrementButtonObserver = new MutationObserver(() => {
this.insertIncrementButton();
});
this.incrementButtonObserver.observe(document.body, {
childList: true,
subtree: true
});
// 立即尝试插入一次
setTimeout(() => {
this.insertIncrementButton();
}, 1000);
}
// 插入增量按钮
insertIncrementButton() {
const targetContainers = document.querySelectorAll('.CharacterManagement_characterManagement__2PhvW .css-k008qs');
// 限制最多处理2个容器
const maxContainers = Math.min(targetContainers.length, 2);
let insertedAny = false;
for (let i = 0; i < maxContainers; i++) {
const targetContainer = targetContainers[i];
// 检查这个容器是否已经有按钮(任何增量按钮都算)
const hasButton = targetContainer.querySelector('[id^="value-increment-button"]');
if (!hasButton) {
const buttonId = `value-increment-button-${i}`;
const button = this.createIncrementButton();
button.id = buttonId;
targetContainer.appendChild(button);
insertedAny = true;
}
}
// 只有在插入了新按钮时才更新显示和设置定时器
if (insertedAny) {
this.updateIncrementDisplay();
this.setupIncrementUpdateTimer();
}
}
// 设置定时更新增量显示
setupIncrementUpdateTimer() {
// 如果已经有定时器就不再创建
if (this.updateTimer) {
return;
}
this.updateTimer = setInterval(() => {
const buttons = document.querySelectorAll('[id^="value-increment-button"]');
if (buttons.length > 0) {
this.updateIncrementDisplay();
} else {
clearInterval(this.updateTimer);
this.updateTimer = null;
}
}, 60000); // 1分钟
}
//添加新数据后更新按钮显示
addValueRecord(totalAsk, totalBid) {
const now = Date.now();
const historyData = this.getHistoryData();
const newRecord = {
timestamp: now,
totalAsk,
totalBid,
date: this.getLocalDateString(now), // 使用本地时区日期
time: new Date(now).toLocaleString('zh-CN') // 年月日小时分钟
};
// 添加新记录
historyData.push(newRecord);
// 压缩和清理数据
const compressedData = this.compressHistoryData(historyData);
this.saveHistoryData(compressedData);
// 更新按钮显示
setTimeout(() => {
this.updateIncrementDisplay();
}, 100);
}
// 数据存储管理
getHistoryData() {
try {
const data = localStorage.getItem(this.storageKey);
return data ? JSON.parse(data) : [];
} catch (error) {
console.error('获取历史数据失败:', error);
return [];
}
}
saveHistoryData(data) {
try {
localStorage.setItem(this.storageKey, JSON.stringify(data));
} catch (error) {
console.error('保存历史数据失败:', error);
}
}
compressHistoryData(data) {
const now = Date.now();
const compressionDate = now - this.compressionThreshold * 24 * 60 * 60 * 1000;
const maxDate = now - this.maxHistoryDays * 24 * 60 * 60 * 1000;
// 分离需要压缩的数据和保留的数据
const recentData = data.filter(record => record.timestamp > compressionDate);
const oldData = data.filter(record => record.timestamp <= compressionDate && record.timestamp > maxDate);
// 对旧数据按日期分组
const dailyGroups = {};
oldData.forEach(record => {
const date = record.date;
if (!dailyGroups[date]) {
dailyGroups[date] = [];
}
dailyGroups[date].push(record);
});
// 每天只保留最早和最晚的记录
const compressedOldData = [];
Object.values(dailyGroups).forEach(dayRecords => {
dayRecords.sort((a, b) => a.timestamp - b.timestamp);
compressedOldData.push(dayRecords[0]); // 最早的记录
if (dayRecords.length > 1) {
compressedOldData.push(dayRecords[dayRecords.length - 1]); // 最晚的记录
}
});
// 合并并按时间排序
const result = [...compressedOldData, ...recentData];
result.sort((a, b) => a.timestamp - b.timestamp);
return result;
}
cleanupOldData() {
const historyData = this.getHistoryData();
const maxDate = Date.now() - this.maxHistoryDays * 24 * 60 * 60 * 1000;
const filteredData = historyData.filter(record => record.timestamp > maxDate);
if (filteredData.length < historyData.length) {
this.saveHistoryData(filteredData);
}
}
startAutoRecord() {
// 清除之前的定时器
if (this.autoRecordTimer) {
clearInterval(this.autoRecordTimer);
}
// 设置定时器,每30分钟记录一次
this.autoRecordTimer = setInterval(async () => {
try {
const result = await this.calculateItemValues();
if (result) {
console.log('[ItemValueCalculator] 自动记录完成');
}
} catch (error) {
console.error('[ItemValueCalculator] 自动记录失败:', error);
}
}, this.recordInterval);
}
stopAutoRecord() {
if (this.autoRecordTimer) {
clearInterval(this.autoRecordTimer);
this.autoRecordTimer = null;
}
}
// 获取物品价格
async getItemPrice(itemHrid, enhancementLevel = 0) {
// 特殊处理金币
if (itemHrid === '/items/coin') {
return { ask: 1, bid: 1 };
}
// 特殊处理牛铃
if (itemHrid === '/items/cowbell') {
return await this.getCowbellPrice();
}
if (enhancementLevel > 0) {
return await this.getWebSocketPrice(itemHrid, enhancementLevel);
} else {
return await this.getJsonPrice(itemHrid);
}
}
// 安全处理价格,将异常值转换为0
safePrice(price) {
if (price == null || isNaN(price)) {
return 0;
}
return Math.max(0, price);
}
// 通过WebSocket获取价格
async getWebSocketPrice(itemHrid, enhancementLevel) {
try {
const orderBooks = await this.getMarketData(itemHrid);
if (orderBooks?.[enhancementLevel]) {
const { asks: asksList, bids: bidsList } = orderBooks[enhancementLevel];
const askPrice = asksList?.[0]?.price;
const bidPrice = bidsList?.[0]?.price;
return {
ask: this.safePrice(askPrice),
bid: this.safePrice(bidPrice)
};
}
return { ask: 0, bid: 0 };
} catch (error) {
console.error('WebSocket获取价格失败:', error);
return { ask: 0, bid: 0 };
}
}
// 通过JSON获取价格
async getJsonPrice(itemHrid) {
if (!this.jsonMarketData) {
console.warn('[ItemValueCalculator] JSON市场数据不存在');
return { ask: 0, bid: 0 };
}
const itemName = this.extractItemName(itemHrid);
const marketItem = this.jsonMarketData[itemName];
if (!marketItem) {
return { ask: 0, bid: 0 };
}
return {
ask: this.safePrice(marketItem.ask),
bid: this.safePrice(marketItem.bid)
};
}
// 获取牛铃价格
async getCowbellPrice() {
if (!this.jsonMarketData) {
console.warn('[ItemValueCalculator] JSON市场数据不存在,无法获取牛铃价格');
return { ask: 0, bid: 0 };
}
const bagMarketItem = this.jsonMarketData?.['Bag Of 10 Cowbells'];
if (bagMarketItem) {
const bagAskPrice = this.safePrice(bagMarketItem.ask);
const bagBidPrice = this.safePrice(bagMarketItem.bid);
return {
ask: bagAskPrice / 10,
bid: bagBidPrice / 10
};
}
return { ask: 0, bid: 0 };
}
// 从itemHrid提取物品名称
extractItemName(itemHrid) {
if (typeof itemHrid !== 'string') return '';
return itemHrid.replace('/items/', '')
.replace(/_/g, ' ')
.replace(/\b\w/g, l => l.toUpperCase());
}
// 计算宝箱loot价值
async calculateChestLootValue(itemHrid, enhancementLevel = 0) {
const lootDrops = lootData[itemHrid];
if (!lootDrops) return { ask: 0, bid: 0 };
let totalAskValue = 0;
let totalBidValue = 0;
// 获取所有loot物品的价格
const lootItems = Object.keys(lootDrops);
const pricePromises = lootItems.map(async (lootItemHrid) => {
const quantity = lootDrops[lootItemHrid];
const prices = await this.getItemPrice(lootItemHrid, enhancementLevel);
return {
itemHrid: lootItemHrid,
quantity,
prices
};
});
const lootPrices = await Promise.all(pricePromises);
// 计算总价值
lootPrices.forEach(({ quantity, prices }) => {
totalAskValue += prices.ask * quantity;
totalBidValue += prices.bid * quantity;
});
return {
ask: totalAskValue,
bid: totalBidValue
};
}
// 检查物品是否是宝箱
isChestItem(itemHrid) {
return lootData.hasOwnProperty(itemHrid);
}
// 获取角色物品数据
getCharacterItemMap() {
try {
const headerElement = document.querySelector('.MarketplacePanel_coinStack__1l0UD');
if (!headerElement) {
throw new Error('未找到头部元素');
}
const reactKey = Object.keys(headerElement).find(key => key.startsWith('__reactProps'));
const characterItemMap = headerElement[reactKey]?.children?._owner?.memoizedProps?.characterItemMap;
if (!characterItemMap) {
throw new Error('未找到物品数据');
}
return characterItemMap;
} catch (error) {
console.error('获取物品数据失败:', error);
return null;
}
}
// 获取市场订单数据
getMyMarketListingMap() {
try {
const headerElement = document.querySelector('.MarketplacePanel_coinStack__1l0UD');
if (!headerElement) {
return null;
}
const reactKey = Object.keys(headerElement).find(key => key.startsWith('__reactProps'));
const myMarketListingMap = headerElement[reactKey]?.children?._owner?.memoizedProps?.myMarketListingMap;
return myMarketListingMap || null;
} catch (error) {
console.error('获取市场订单数据失败:', error);
return null;
}
}
// 计算单个物品价值
async calculateSingleItem(item) {
if (!item || !item.itemHrid || !item.count) return null;
const itemName = this.extractItemName(item.itemHrid);
const count = item.count;
const enhancementLevel = item.enhancementLevel || 0;
// 检查是否是宝箱
if (this.isChestItem(item.itemHrid)) {
const lootValue = await this.calculateChestLootValue(item.itemHrid, enhancementLevel);
return {
itemName,
count,
enhancementLevel,
askPrice: lootValue.ask,
bidPrice: lootValue.bid,
totalAsk: lootValue.ask * count,
totalBid: lootValue.bid * count,
isChest: true
};
} else {
// 普通物品
const prices = await this.getItemPrice(item.itemHrid, enhancementLevel);
return {
itemName,
count,
enhancementLevel,
askPrice: prices.ask,
bidPrice: prices.bid,
totalAsk: prices.ask * count,
totalBid: prices.bid * count,
isChest: false
};
}
}
// 计算市场订单价值
async calculateMarketOrderValue(order) {
if (!order || !order.itemHrid) return null;
const itemName = this.extractItemName(order.itemHrid);
const quantity = order.orderQuantity - order.filledQuantity;
const enhancementLevel = order.enhancementLevel || 0;
const price = order.price || 0;
if (quantity <= 0) return null;
if (!order.isSell) {
// 购买订单:投入的金币
const coinValue = quantity * price;
return {
type: 'buy_order',
itemName,
quantity,
enhancementLevel,
price,
coinValue,
totalAsk: coinValue,
totalBid: coinValue
};
} else {
// 出售订单:按市场价格计算物品价值
let prices;
if (this.isChestItem(order.itemHrid)) {
prices = await this.calculateChestLootValue(order.itemHrid, enhancementLevel);
} else {
prices = await this.getItemPrice(order.itemHrid, enhancementLevel);
}
return {
type: 'sell_order',
itemName,
quantity,
enhancementLevel,
price,
askPrice: prices.ask,
bidPrice: prices.bid,
totalAsk: prices.ask * quantity,
totalBid: prices.bid * quantity
};
}
}
// 主计算函数
async calculateItemValues() {
try {
// 在计算前更新JSON数据
await this.updateJsonMarketData();
const characterItemMap = this.getCharacterItemMap();
if (!characterItemMap) {
throw new Error('无法获取物品数据,请确保在正确的游戏界面');
}
const myMarketListingMap = this.getMyMarketListingMap();
let totalAskValue = 0;
let totalBidValue = 0;
let successCount = 0;
let failedItems = [];
// 遍历库存物品
const itemEntries = characterItemMap instanceof Map ?
Array.from(characterItemMap.entries()) :
Object.entries(characterItemMap);
for (let i = 0; i < itemEntries.length; i++) {
const [key, item] = itemEntries[i];
try {
const result = await this.calculateSingleItem(item);
if (result) {
totalAskValue += result.totalAsk;
totalBidValue += result.totalBid;
successCount++;
} else {
failedItems.push(`库存物品: ${item.itemHrid || 'Unknown'}`);
}
} catch (error) {
console.error(`[ItemValueCalculator] 计算库存物品失败:`, item, error);
failedItems.push(`库存物品: ${item.itemHrid || 'Unknown'} (${error.message})`);
}
}
// 处理市场订单
if (myMarketListingMap) {
const orderEntries = myMarketListingMap instanceof Map ?
Array.from(myMarketListingMap.entries()) :
Object.entries(myMarketListingMap);
for (let i = 0; i < orderEntries.length; i++) {
const [key, order] = orderEntries[i];
try {
const result = await this.calculateMarketOrderValue(order);
if (result) {
totalAskValue += result.totalAsk;
totalBidValue += result.totalBid;
successCount++;
} else {
failedItems.push(`市场订单: ${order.itemHrid || 'Unknown'}`);
}
} catch (error) {
console.error(`[ItemValueCalculator] 计算市场订单失败:`, order, error);
failedItems.push(`市场订单: ${order.itemHrid || 'Unknown'} (${error.message})`);
}
}
}
// 记录数据到历史
this.addValueRecord(totalAskValue, totalBidValue);
// 输出统计信息
const totalItems = itemEntries.length + (myMarketListingMap ? Object.keys(myMarketListingMap).length : 0);
console.log(`[ItemValueCalculator] 计算完成 - 计算数量: ${successCount}, Ask总值: ${totalAskValue.toFixed(0)}, Bid总值: ${totalBidValue.toFixed(0)}`);
if (failedItems.length > 0) {
}
return {
totalAsk: totalAskValue,
totalBid: totalBidValue,
successCount,
totalItems,
failedItems
};
} catch (error) {
console.error('❌ 计算失败:', error.message);
throw error;
}
}
getContainerId() { return 'item-value-calculator'; }
getPessimisticId() { return 'item-value-pessimistic'; }
getOptimisticId() { return 'item-value-optimistic'; }
getWaitingText() { return LANG.chart.calculating; }
getActionData() { return null; }
calculateProfit() { return null; }
getStateFingerprint() { return ''; }
setupUI() { }
// 清理资源
cleanup() {
this.stopAutoRecord();
if (this.incrementButtonObserver) {
this.incrementButtonObserver.disconnect();
this.incrementButtonObserver = null;
}
if (this.chartViewer) {
this.chartViewer.hide();
this.chartViewer = null;
}
// 清理按钮
const buttons = document.querySelectorAll('[id^="value-increment-button"]');
buttons.forEach(button => button.remove());
if (this.updateTimer) {
clearInterval(this.updateTimer);
this.updateTimer = null;
}
}
}
// ==================== 资产变化图表查看器 ====================
class AssetChartViewer {
constructor(itemValueCalculator) {
this.calculator = itemValueCalculator;
this.chartContainer = null;
this.myChart = null;
this.isVisible = false;
this.selectedDays = 7;
this.chartData = null;
this.isMobile = this.isMobileDevice();
// 图表配置
this.chartConfig = {
colors: {
ask: 'rgba(255, 99, 132, 1)',
bid: 'rgba(54, 162, 235, 1)',
ma: 'rgba(75, 192, 192, 1)',
trend: 'rgba(255, 159, 64, 1)'
},
backgroundColor: '#191c2b',
textColor: '#FFFFFF',
gridColor: 'rgba(255,255,255,0.2)'
};
// 数据集显示状态
this.datasetVisibility = this.loadDatasetVisibility();
this.init();
}
// 检测是否为移动设备
isMobileDevice() {
const ua = navigator.userAgent || navigator.vendor || window.opera;
const mobileUA = /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(ua);
const aspectRatio = window.innerHeight / window.innerWidth;
return mobileUA || (aspectRatio >= 1.2);
}
// 加载图例显示状态
loadDatasetVisibility() {
const defaultVisibility = { ask: true, bid: false, ma: false, trend: false };
try {
const saved = JSON.parse(localStorage.getItem('MWI_AssetChart_DatasetVisibility'));
return Object.assign(defaultVisibility, saved);
} catch (e) {
return defaultVisibility;
}
}
// 保存图例显示状态
saveDatasetVisibility(visibility) {
localStorage.setItem('MWI_AssetChart_DatasetVisibility', JSON.stringify(visibility));
}
// 保存/加载时间范围设置
loadRangeSetting() {
return localStorage.getItem('MWI_AssetChart_Range') || '7';
}
saveRangeSetting(range) {
localStorage.setItem('MWI_AssetChart_Range', range);
}
init() {
this.addStyles();
this.createChartContainer();
this.bindEvents();
// 加载Chart.js(如果没有的话)
this.loadChartJS();
}
// 加载Chart.js库
loadChartJS() {
if (typeof Chart !== 'undefined') {
return;
}
const script = document.createElement('script');
script.src = 'https://cdn.jsdelivr.net/npm/[email protected]/dist/chart.umd.min.js';
script.onload = () => {
};
document.head.appendChild(script);
// 加载日期适配器
const dateAdapter = document.createElement('script');
dateAdapter.src = 'https://cdn.jsdelivr.net/npm/[email protected]/dist/chartjs-adapter-date-fns.bundle.min.js';
document.head.appendChild(dateAdapter);
}
// 添加样式
addStyles() {
const style = document.createElement('style');
let modalStyles = `
.asset-chart-modal {
display: none;
position: fixed;
z-index: 10000;
left: 0;
top: 0;
width: 100%;
height: 100%;
overflow: auto;
background-color: rgba(0,0,0,0.8);
backdrop-filter: blur(5px);
align-items: center;
justify-content: center;
}
.asset-chart-modal-content {
background-color: #191c2b;
color: #FFFFFF;
padding: 20px;
border: 1px solid #444;
border-radius: 8px;
width: 90%;
max-width: 1200px;
height: 80%;
max-height: 800px;
position: relative;
display: flex;
flex-direction: column;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.5);
}
.asset-chart-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
padding-bottom: 10px;
border-bottom: 1px solid #444;
}
.asset-chart-title {
font-size: 24px;
font-weight: bold;
margin: 0;
}
.asset-chart-controls {
display: flex;
gap: 15px;
align-items: center;
margin-bottom: 20px;
flex-wrap: wrap;
}
.asset-chart-time-range {
display: flex;
gap: 8px;
align-items: center;
}
.asset-chart-time-btn {
padding: 8px 16px;
background-color: rgba(255, 255, 255, 0.1);
color: #FFFFFF;
border: 1px solid rgba(255, 255, 255, 0.3);
border-radius: 4px;
cursor: pointer;
font-size: 12px;
transition: all 0.2s;
}
.asset-chart-time-btn:hover {
background-color: rgba(255, 255, 255, 0.2);
}
.asset-chart-time-btn.active {
background-color: rgba(33, 150, 243, 0.8);
border-color: rgba(33, 150, 243, 1);
}
.asset-chart-canvas-container {
flex: 1;
position: relative;
min-height: 0;
}
.asset-chart-canvas {
background-color: #191c2b;
border-radius: 4px;
width: 100% !important;
height: 100% !important;
}
.asset-chart-close-btn {
position: absolute;
top: 15px;
right: 15px;
background: transparent;
border: none;
color: #FFFFFF;
font-size: 24px;
cursor: pointer;
padding: 5px;
border-radius: 50%;
transition: background-color 0.2s;
z-index: 10001;
}
.asset-chart-close-btn:hover {
background-color: rgba(255, 255, 255, 0.1);
}
.asset-chart-info-panel {
display: flex;
gap: 20px;
padding: 10px;
background-color: rgba(0, 0, 0, 0.3);
border-radius: 4px;
margin-top: 10px;
font-size: 14px;
min-height: 40px;
align-items: center;
}
`;
// 移动端样式
if (this.isMobile) {
modalStyles += `
.asset-chart-modal-content.mobile {
width: 100vh;
height: 100vw;
max-width: none;
max-height: none;
padding: 15px;
transform: rotate(90deg) translate(0, -100%);
transform-origin: top left;
position: absolute;
top: 0;
left: 0;
}
.asset-chart-close-btn {
position: absolute;
top: 10px;
right: 10px;
font-size: 28px;
z-index: 10001;
}
.asset-chart-controls {
flex-direction: column;
gap: 10px;
}
.asset-chart-time-range {
justify-content: center;
}
.asset-chart-title {
font-size: 20px;
}
`;
}
style.textContent = modalStyles;
document.head.appendChild(style);
}
createChartContainer() {
// 创建主容器
this.chartContainer = document.createElement('div');
this.chartContainer.id = 'asset-chart-modal';
this.chartContainer.className = 'asset-chart-modal';
this.chartContainer.style.display = 'none';
// 创建模态框内容
const modalContent = document.createElement('div');
modalContent.className = 'asset-chart-modal-content';
if (this.isMobile) {
modalContent.classList.add('mobile');
}
// 创建关闭按钮
const closeBtn = document.createElement('button');
closeBtn.className = 'asset-chart-close-btn';
closeBtn.textContent = '✕';
closeBtn.addEventListener('click', () => this.hide());
// 创建标题栏
const header = document.createElement('div');
header.className = 'asset-chart-header';
const title = document.createElement('h2');
title.className = 'asset-chart-title';
title.textContent = LANG.chart.title;
header.appendChild(title);
// 创建控制面板
const controls = document.createElement('div');
controls.className = 'asset-chart-controls';
// 时间范围选择
const timeRangeContainer = document.createElement('div');
timeRangeContainer.className = 'asset-chart-time-range';
const timeRangeLabel = document.createElement('span');
timeRangeLabel.textContent = LANG.chart.timeRange;
timeRangeLabel.style.fontWeight = 'bold';
const timeRangeButtons = [
{ label: LANG.chart.days[0], value: 1 },
{ label: LANG.chart.days[1], value: 3 },
{ label: LANG.chart.days[2], value: 7 },
{ label: LANG.chart.days[3], value: 14 },
{ label: LANG.chart.days[4], value: 30 }
];
timeRangeContainer.appendChild(timeRangeLabel);
timeRangeButtons.forEach(btn => {
const button = document.createElement('button');
button.className = 'asset-chart-time-btn';
button.textContent = btn.label;
button.dataset.value = btn.value;
if (btn.value === this.selectedDays) {
button.classList.add('active');
}
button.addEventListener('click', () => {
this.selectedDays = btn.value;
this.saveRangeSetting(btn.value.toString());
this.updateTimeRangeButtons();
this.updateChart();
});
timeRangeContainer.appendChild(button);
});
controls.appendChild(timeRangeContainer);
// 创建图表容器
const canvasContainer = document.createElement('div');
canvasContainer.className = 'asset-chart-canvas-container';
const canvas = document.createElement('canvas');
canvas.id = 'asset-chart-canvas';
canvas.className = 'asset-chart-canvas';
canvasContainer.appendChild(canvas);
// 创建信息面板
const infoPanel = document.createElement('div');
infoPanel.className = 'asset-chart-info-panel';
infoPanel.id = 'asset-chart-info-panel';
infoPanel.innerHTML = `<span style="color: rgba(255, 255, 255, 0.7);">${LANG.chart.hoverTip}</span>`;
// 组装界面
modalContent.appendChild(closeBtn);
modalContent.appendChild(header);
modalContent.appendChild(controls);
modalContent.appendChild(canvasContainer);
modalContent.appendChild(infoPanel);
this.chartContainer.appendChild(modalContent);
document.body.appendChild(this.chartContainer);
// 存储元素引用
this.elements = {
canvas: canvas,
infoPanel: infoPanel,
timeRangeButtons: timeRangeButtons
};
}
bindEvents() {
// 点击背景关闭
this.chartContainer.addEventListener('click', (e) => {
if (e.target === this.chartContainer) {
this.hide();
}
});
// ESC键关闭
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape' && this.isVisible) {
this.hide();
}
});
// 窗口大小变化
window.addEventListener('resize', () => {
if (this.isVisible && this.myChart) {
this.myChart.resize();
}
});
}
show() {
this.isVisible = true;
this.chartContainer.style.display = 'flex';
// 加载保存的时间范围
this.selectedDays = parseInt(this.loadRangeSetting());
this.updateTimeRangeButtons();
// 等待Chart.js加载完成后更新图表
this.waitForChartJS().then(() => {
this.updateChart();
});
}
hide() {
this.isVisible = false;
this.chartContainer.style.display = 'none';
this.destroyChart();
}
// 等待Chart.js加载完成
waitForChartJS() {
return new Promise((resolve) => {
const checkChart = () => {
if (typeof Chart !== 'undefined') {
resolve();
} else {
setTimeout(checkChart, 100);
}
};
checkChart();
});
}
destroyChart() {
if (this.myChart) {
this.myChart.destroy();
this.myChart = null;
}
}
updateTimeRangeButtons() {
const buttons = this.chartContainer.querySelectorAll('.asset-chart-time-btn');
buttons.forEach(button => {
const value = parseInt(button.dataset.value);
if (value === this.selectedDays) {
button.classList.add('active');
} else {
button.classList.remove('active');
}
});
}
// 准备图表数据
prepareChartData() {
const historyData = this.calculator.getHistoryData();
if (historyData.length === 0) {
this.chartData = null;
return;
}
// 先按时间排序(从旧到新)
const sortedData = [...historyData].sort((a, b) => a.timestamp - b.timestamp);
// 找到最新的数据时间
const latestTimestamp = sortedData[sortedData.length - 1].timestamp;
const cutoffTime = latestTimestamp - (this.selectedDays * 24 * 60 * 60 * 1000);
// 过滤指定时间范围的数据(从最新数据往前推)
const filteredData = sortedData.filter(record => record.timestamp >= cutoffTime);
if (filteredData.length === 0) {
this.chartData = null;
return;
}
// 数据清洗:去除异常值
const cleanedData = this.cleanData(filteredData);
this.chartData = {
points: cleanedData,
minTimestamp: Math.min(...cleanedData.map(p => p.timestamp)),
maxTimestamp: Math.max(...cleanedData.map(p => p.timestamp))
};
}
// 数据清洗函数
cleanData(data) {
const cleaned = [];
let prevAsk = null, prevBid = null;
data.forEach(point => {
let newAsk = point.totalAsk;
let newBid = point.totalBid;
// 检测异常值并替换
if (prevAsk && (newAsk > 2.5 * prevAsk || newAsk < 0.4 * prevAsk)) {
newAsk = prevAsk;
}
if (prevBid && (newBid > 2.5 * prevBid || newBid < 0.4 * prevBid)) {
newBid = prevBid;
}
cleaned.push({
timestamp: point.timestamp,
date: new Date(point.timestamp),
totalAsk: newAsk,
totalBid: newBid,
dateStr: point.dateStr
});
prevAsk = newAsk;
prevBid = newBid;
});
return cleaned;
}
// 计算移动平均线
calculateMovingAverage(data, windowSize = 5) {
const maValues = [];
for (let i = 0; i < data.length; i++) {
if (i < windowSize - 1) {
maValues.push(null);
} else {
let sum = 0;
for (let j = i - windowSize + 1; j <= i; j++) {
sum += (data[j].totalAsk + data[j].totalBid) / 2;
}
maValues.push(sum / windowSize);
}
}
return maValues;
}
// 计算趋势线
calculateTrendline(data) {
const n = data.length;
if (n < 2) return null;
let sumX = 0, sumY = 0, sumXY = 0, sumX2 = 0;
data.forEach(point => {
const x = point.timestamp;
const y = (point.totalAsk + point.totalBid) / 2;
sumX += x;
sumY += y;
sumXY += x * y;
sumX2 += x * x;
});
const meanX = sumX / n;
const meanY = sumY / n;
const slope = (sumXY - n * meanX * meanY) / (sumX2 - n * meanX * meanX);
const intercept = meanY - slope * meanX;
return [
{ x: data[0].timestamp, y: slope * data[0].timestamp + intercept },
{ x: data[n - 1].timestamp, y: slope * data[n - 1].timestamp + intercept }
];
}
updateChart() {
if (!this.elements.canvas) return;
// 准备数据
this.prepareChartData();
if (!this.chartData || this.chartData.points.length === 0) {
this.showNoDataMessage();
return;
}
const data = this.chartData.points;
const times = data.map(point => point.date);
const askPrices = data.map(point => point.totalAsk);
const bidPrices = data.map(point => point.totalBid);
// 计算移动平均线
const maValues = this.calculateMovingAverage(data);
// 计算趋势线
const trendlineData = this.calculateTrendline(data);
// 确定时间轴格式
let timeUnit, timeFormat;
if (this.selectedDays <= 3) {
timeUnit = 'hour';
timeFormat = 'HH:mm';
} else {
timeUnit = 'day';
timeFormat = 'MM/dd';
}
// 销毁旧图表
this.destroyChart();
// 创建新图表
const ctx = this.elements.canvas.getContext('2d');
this.myChart = new Chart(ctx, {
type: 'line',
data: {
labels: times,
datasets: [
{
label: LANG.chart.datasets.askTotal,
data: askPrices,
borderColor: this.chartConfig.colors.ask,
backgroundColor: this.chartConfig.colors.ask + '20',
fill: false,
pointRadius: 2,
pointHoverRadius: 4,
tension: 0.1,
hidden: !this.datasetVisibility.ask
},
{
label: LANG.chart.datasets.bidTotal,
data: bidPrices,
borderColor: this.chartConfig.colors.bid,
backgroundColor: this.chartConfig.colors.bid + '20',
fill: false,
pointRadius: 2,
pointHoverRadius: 4,
tension: 0.1,
hidden: !this.datasetVisibility.bid
},
{
label: LANG.chart.datasets.movingAverage,
data: maValues,
borderColor: this.chartConfig.colors.ma,
backgroundColor: this.chartConfig.colors.ma + '20',
fill: false,
pointRadius: 0,
pointHoverRadius: 3,
tension: 0.1,
hidden: !this.datasetVisibility.ma
},
{
label: LANG.chart.datasets.trendLine,
data: trendlineData,
borderColor: this.chartConfig.colors.trend,
backgroundColor: this.chartConfig.colors.trend + '20',
fill: false,
borderDash: [5, 5],
pointRadius: 0,
pointHoverRadius: 0,
tension: 0,
hidden: !this.datasetVisibility.trend
}
]
},
options: {
responsive: true,
maintainAspectRatio: false,
backgroundColor: this.chartConfig.backgroundColor,
plugins: {
legend: {
display: true,
labels: {
color: this.chartConfig.textColor,
usePointStyle: true,
padding: 20
},
onClick: (e, legendItem, legend) => {
// 调用默认行为
Chart.defaults.plugins.legend.onClick(e, legendItem, legend);
// 保存新的可见性状态
const chart = legend.chart;
const newVisibility = {
ask: !chart.getDatasetMeta(0).hidden,
bid: !chart.getDatasetMeta(1).hidden,
ma: !chart.getDatasetMeta(2).hidden,
trend: !chart.getDatasetMeta(3).hidden
};
this.datasetVisibility = newVisibility;
this.saveDatasetVisibility(newVisibility);
}
},
tooltip: {
mode: 'index',
intersect: false,
backgroundColor: 'rgba(0, 0, 0, 0.8)',
titleColor: this.chartConfig.textColor,
bodyColor: this.chartConfig.textColor,
borderColor: 'rgba(255, 255, 255, 0.3)',
borderWidth: 1,
callbacks: {
label: (context) => {
const value = context.parsed.y;
return `${context.dataset.label}: ${this.formatAccurateValue(value)}`;
}
}
}
},
scales: {
x: {
type: 'time',
time: {
unit: timeUnit,
tooltipFormat: timeFormat,
displayFormats: {
hour: 'HH:mm',
day: 'MM/dd'
}
},
grid: {
color: this.chartConfig.gridColor
},
ticks: {
color: this.chartConfig.textColor
}
},
y: {
grid: {
color: this.chartConfig.gridColor
},
ticks: {
color: this.chartConfig.textColor,
callback: (value) => this.formatValue(value)
}
}
},
interaction: {
mode: 'index',
intersect: false
},
onHover: (event, activeElements) => {
if (activeElements.length > 0) {
const dataIndex = activeElements[0].index;
const point = data[dataIndex];
this.updateInfoPanel(point);
} else {
this.clearInfoPanel();
}
}
}
});
}
formatAccurateValue(value) {
if (value === null || value === undefined || isNaN(value)) {
return '0';
}
// 将数值转换为整数(去掉小数点)
const intValue = Math.round(value);
// 添加千位分隔符
return intValue.toLocaleString();
}
showNoDataMessage() {
this.destroyChart();
const ctx = this.elements.canvas.getContext('2d');
ctx.fillStyle = this.chartConfig.textColor;
ctx.font = '16px Arial';
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillText(LANG.chart.noData, this.elements.canvas.width / 2, this.elements.canvas.height / 2);
}
updateInfoPanel(point) {
const { askIncrement, bidIncrement } = this.calculator.getTodayIncrement();
// 计算Ask增量颜色
let askColor = '#4CAF50'; // 默认绿色(正数)
if (askIncrement < 0) {
askColor = '#f44336'; // 红色(负数)
} else if (askIncrement === 0) {
askColor = 'rgba(255, 255, 255, 0.7)'; // 中性色(零)
}
// 计算Bid增量颜色
let bidColor = '#4CAF50'; // 默认绿色(正数)
if (bidIncrement < 0) {
bidColor = '#f44336'; // 红色(负数)
} else if (bidIncrement === 0) {
bidColor = 'rgba(255, 255, 255, 0.7)'; // 中性色(零)
}
this.elements.infoPanel.innerHTML = `
<div style="display: flex; align-items: center; gap: 16px;">
<div>
<strong style="color: rgba(255, 255, 255, 0.9);">${LANG.chart.todayIncrement}</strong>
<span style="color: ${askColor}; font-weight: bold; margin-left: 8px;">
${this.formatAccurateValue(askIncrement)}
</span>
<span style="color: rgba(255, 255, 255, 0.7); margin: 0 4px;">/</span>
<span style="color: ${bidColor}; font-weight: bold;">
${this.formatAccurateValue(bidIncrement)}
</span>
</div>
</div>
`;
}
clearInfoPanel() {
this.elements.infoPanel.innerHTML = `<span style="color: rgba(255, 255, 255, 0.7);">${LANG.chart.hoverTip}</span>`;
}
formatValue(value) {
if (Math.abs(value) >= 1e9) {
return (value / 1e9).toFixed(1) + 'B';
} else if (Math.abs(value) >= 1e6) {
return (value / 1e6).toFixed(1) + 'M';
} else if (Math.abs(value) >= 1e3) {
return (value / 1e3).toFixed(1) + 'K';
} else {
return value.toFixed(0);
}
}
}
// ==================== 购物车管理器 ====================
class ShoppingCartManager {
constructor() {
this.items = new Map();
this.savedLists = new Map();
this.isOpen = false;
this.cartContainer = null;
this.maxSavedLists = 5;
this.currentListName = '';
this.wasDragged = false;
this.cartTabPosition = this.loadCartTabPosition();
this.allSelected = true; // 全选状态
this.defaultPurchaseMode = 'bid'; // 默认购买方式:bid(求购)
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;
}
this.setCartTabInitialPosition();
}, 0);
}
// 加载购物车标签位置
loadCartTabPosition() {
try {
const saved = JSON.parse(localStorage.getItem('milkyway-cart-tab-position'));
return saved || { y: '50%' };
} catch (error) {
return { y: '50%' };
}
}
// 保存购物车标签位置
saveCartTabPosition() {
try {
localStorage.setItem('milkyway-cart-tab-position', JSON.stringify(this.cartTabPosition));
} catch (error) {
console.warn('保存购物车标签位置失败:', error);
}
}
setCartTabInitialPosition() {
const cartTab = document.getElementById('cart-tab');
if (cartTab && this.cartTabPosition.y) {
cartTab.style.top = this.cartTabPosition.y;
}
}
createCartDrawer() {
this.cartContainer = document.createElement('div');
this.cartContainer.id = 'shopping-cart-drawer';
utils.applyStyles(this.cartContainer, {
position: 'fixed',
top: '80px',
right: '0',
width: Math.min(window.innerWidth, 450) + 'px', // 增加宽度以容纳新功能
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(${Math.min(window.innerWidth, 450)}px)`,
transition: 'transform 0.3s cubic-bezier(0.4, 0, 0.2, 1)',
display: 'flex',
flexDirection: 'column',
fontFamily: 'Roboto, Helvetica, Arial, sans-serif'
});
this.cartContainer.innerHTML = `
<!-- 购物车标签/触发器 -->
<div id="cart-tab" style="
position: absolute;
left: -40px;
top: ${this.cartTabPosition.y};
transform: translateY(-50%);
width: 40px;
height: 80px;
background: rgba(42, 43, 66, 0.95);
border: 1px solid var(--border);
border-right: none;
border-top-left-radius: 8px;
border-bottom-left-radius: 8px;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
cursor: pointer;
transition: all 0.3s ease;
box-shadow: -2px 0 8px rgba(0,0,0,0.2);
user-select: none;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
">
<div style="
font-size: 18px;
margin-bottom: 4px;
white-space: nowrap;
color: var(--color-text-dark-mode);
">🛒</div>
<div id="cart-tab-badge" style="
background: #f44336;
color: white;
border-radius: 10px;
min-width: 18px;
height: 18px;
font-size: 10px;
display: none;
align-items: center;
justify-content: center;
font-weight: bold;
">0</div>
</div>
<!-- 购物车头部 -->
<div style="
display: flex;
justify-content: space-between;
align-items: center;
padding: 12px 16px;
border-bottom: 1px solid var(--border-separator);
background: var(--card-title-background);
border-top-left-radius: 8px;
flex-shrink: 0;
">
<h3 style="
margin: 0;
color: var(--card-title-text);
font-size: 16px;
font-weight: bold;
">${LANG.shoppingCart}</h3>
<div style="
background: rgba(156, 39, 176, 0.2);
color: var(--color-text-dark-mode);
padding: 2px 8px;
border-radius: 12px;
font-size: 11px;
font-weight: 500;
" id="cart-count-display">0 ${LANG.cartItem}</div>
</div>
<!-- 全局控制区域 -->
<div style="
padding: 12px 16px;
border-bottom: 1px solid var(--border-separator);
background: var(--card-background);
flex-shrink: 0;
">
<!-- 全选控制 -->
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 12px;">
<label style="display: flex; align-items: center; cursor: pointer; color: var(--color-text-dark-mode); font-size: 13px;">
<input type="checkbox" id="select-all-checkbox" checked style="margin-right: 8px; transform: scale(1.1);">
${LANG.selectAll}
</label>
</div>
<!-- 批量购买方式设置 -->
<div style="display: flex; align-items: center; gap: 8px;">
<span style="color: var(--color-text-dark-mode); font-size: 12px; font-weight: 500;">${LANG.batchSettings}</span>
<button id="batch-set-ask" style="
flex: 1;
padding: 6px 12px;
background-color: rgba(217, 89, 97, 0.8);
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 12px;
font-weight: 500;
transition: background-color 0.2s;
white-space: nowrap;
">${LANG.directBuy}</button>
<button id="batch-set-bid" style="
flex: 1;
padding: 6px 12px;
background-color: rgba(47, 196, 167, 0.8);
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 12px;
font-weight: 500;
transition: background-color 0.2s;
white-space: nowrap;
">${LANG.bidOrder}</button>
</div>
</div>
<!-- 保存清单区域 -->
<div style="
padding: 12px 16px;
border-bottom: 1px solid var(--border-separator);
background: var(--card-background);
flex-shrink: 0;
">
<div style="display: flex; gap: 8px; align-items: center;">
<input id="list-name-input" type="text" placeholder="${LANG.listName}" maxlength="20" style="
flex: 1;
padding: 6px 8px;
background-color: var(--item-background);
border: 1px solid var(--item-border);
border-radius: 4px;
color: var(--color-text-dark-mode);
font-size: 12px;
outline: none;
">
<button id="save-list-btn" style="
padding: 6px 12px;
background-color: rgba(33, 150, 243, 0.8);
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 12px;
font-weight: 500;
transition: background-color 0.2s;
white-space: nowrap;
">${LANG.save}</button>
</div>
</div>
<!-- 导入导出区域 -->
<div style="
padding: 8px 16px;
border-bottom: 1px solid var(--border-separator);
background: var(--card-background);
flex-shrink: 0;
">
<div style="display: flex; gap: 8px; align-items: center;">
<button id="export-lists-btn" style="
flex: 1;
padding: 6px 12px;
background-color: rgba(76, 175, 80, 0.8);
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 12px;
font-weight: 500;
transition: background-color 0.2s;
white-space: nowrap;
">${LANG.exportSavedLists}</button>
<button id="import-lists-btn" style="
flex: 1;
padding: 6px 12px;
background-color: rgba(33, 150, 243, 0.8);
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 12px;
font-weight: 500;
transition: background-color 0.2s;
white-space: nowrap;
">${LANG.importSavedLists}</button>
</div>
</div>
<!-- 购物车内容 -->
<div id="cart-items-container" style="
flex: 1;
overflow-y: auto;
padding: 8px;
background: var(--card-background);
min-height: 0;
"></div>
<!-- 已保存清单 -->
<div style="
border-top: 1px solid var(--border-separator);
background: var(--card-background);
flex-shrink: 0;
max-height: 200px;
display: flex;
flex-direction: column;
">
<div style="
padding: 8px 16px;
font-size: 12px;
font-weight: 500;
color: var(--color-neutral-400);
border-bottom: 1px solid var(--border-separator);
">${LANG.savedLists}</div>
<div id="saved-lists-container" style="
flex: 1;
overflow-y: auto;
padding: 8px;
min-height: 0;
"></div>
</div>
<!-- 购物车操作按钮 -->
<div id="cart-actions" style="
padding: 12px 16px;
border-top: 1px solid var(--border-separator);
background: var(--card-background);
border-bottom-left-radius: 8px;
display: flex;
flex-direction: column;
gap: 8px;
flex-shrink: 0;
">
<button id="cart-purchase-btn" style="
padding: 10px 12px;
background-color: rgba(30, 58, 138, 0.85);
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
font-weight: bold;
transition: background-color 0.2s;
font-size: 14px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
">${LANG.purchaseAll}</button>
<button id="cart-clear-btn" style="
padding: 6px 12px;
background-color: transparent;
color: var(--color-neutral-400);
border: 1px solid var(--border-separator);
border-radius: 4px;
cursor: pointer;
font-size: 12px;
transition: all 0.2s;
">${LANG.cartClear}</button>
</div>
`;
document.body.appendChild(this.cartContainer);
this.bindEvents();
this.updateCartDisplay();
setTimeout(() => {
const listNameInput = document.getElementById('list-name-input');
if (listNameInput) {
listNameInput.value = this.currentListName;
}
}, 0);
}
// 在 ShoppingCartManager 类中,修改 createPurchaseModeToggle 方法
createPurchaseModeToggle(mode = 'bid') {
const toggle = document.createElement('div');
toggle.className = 'purchase-mode-toggle';
toggle.setAttribute('data-mode', mode);
const isAsk = mode === 'ask';
const borderColor = isAsk ? 'rgba(217, 89, 97, 1)' : 'rgba(47, 196, 167, 1)';
const sliderBg = isAsk ? 'rgba(217, 89, 97, 1)' : 'rgba(47, 196, 167, 1)';
const sliderText = isAsk ? `${LANG.directBuyMode}` : `${LANG.bidOrderMode}`;
const sliderPosition = isAsk ? 'left: 2px' : 'right: 2px';
const toggleBackgroundColor = 'var(--item-background)';
toggle.style.cssText = `
position: relative;
width: 70px;
height: 24px;
background: ${toggleBackgroundColor};
border-radius: 12px;
cursor: pointer;
transition: all 0.3s ease;
border: 2px solid ${borderColor};
flex-shrink: 0;
`;
toggle.innerHTML = `
<div class="toggle-slider" style="
position: absolute;
top: 1px;
${sliderPosition};
width: 32px;
height: 18px;
background: ${sliderBg};
border-radius: 9px;
transition: all 0.3s ease;
display: flex;
align-items: center;
justify-content: center;
color: white;
font-size: 9px;
font-weight: bold;
">${sliderText}</div>
<div style="
position: absolute;
top: 1px;
left: 2px;
width: 32px;
height: 18px;
display: flex;
align-items: center;
justify-content: center;
color: ${isAsk ? 'var(--color-text-dark-mode)' : 'var(--color-neutral-400)'};
font-size: 9px;
font-weight: bold;
">${LANG.directBuyMode}</div>
<div style="
position: absolute;
top: 1px;
right: 2px;
width: 32px;
height: 18px;
display: flex;
align-items: center;
justify-content: center;
color: ${isAsk ? 'var(--color-neutral-400)' : 'var(--color-text-dark-mode)'};
font-size: 9px;
font-weight: bold;
">${LANG.bidOrderMode}</div>
`;
return toggle;
}
togglePurchaseMode(toggle) {
const currentMode = toggle.getAttribute('data-mode');
const newMode = currentMode === 'ask' ? 'bid' : 'ask';
toggle.setAttribute('data-mode', newMode);
const slider = toggle.querySelector('.toggle-slider');
const isAsk = newMode === 'ask';
const borderColor = isAsk ? 'rgba(217, 89, 97, 1)' : 'rgba(47, 196, 167, 1)';
const sliderBg = isAsk ? 'rgba(217, 89, 97, 1)' : 'rgba(47, 196, 167, 1)';
const sliderText = isAsk ? `${LANG.directBuyMode}` : `${LANG.bidOrderMode}`;
// 更新切换器样式
toggle.style.borderColor = borderColor;
slider.style.backgroundColor = sliderBg;
slider.style.left = isAsk ? '2px' : 'auto';
slider.style.right = isAsk ? 'auto' : '2px';
slider.textContent = sliderText;
// 更新标签颜色
const labels = toggle.querySelectorAll('div:not(.toggle-slider)');
labels[0].style.color = isAsk ? 'var(--color-text-dark-mode)' : 'var(--color-neutral-400)'; // 直购
labels[1].style.color = isAsk ? 'var(--color-neutral-400)' : 'var(--color-text-dark-mode)'; // 求购
return newMode;
}
//设置市场购物车按钮
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);
}
setupCartTabDragAndClick() {
const cartTab = document.getElementById('cart-tab');
if (!cartTab) return;
let isDragging = false;
let startY, currentTopPercent;
const handleStart = (e) => {
isDragging = true;
this.wasDragged = false;
const clientY = e.type === 'mousedown' ? e.clientY : e.touches[0].clientY;
const currentTop = cartTab.style.top;
if (currentTop.includes('%')) {
currentTopPercent = parseFloat(currentTop);
} else if (currentTop.includes('px')) {
const containerHeight = this.cartContainer.offsetHeight;
const topPx = parseFloat(currentTop);
currentTopPercent = (topPx / containerHeight) * 100;
} else {
currentTopPercent = 50;
}
startY = clientY;
cartTab.style.transition = 'none';
e.preventDefault();
e.stopPropagation();
};
const handleMove = (e) => {
if (!isDragging) return;
const clientY = e.type === 'mousemove' ? e.clientY : e.touches[0].clientY;
const deltaY = clientY - startY;
if (Math.abs(deltaY) > 5) {
this.wasDragged = true;
}
const containerHeight = this.cartContainer.offsetHeight;
const deltaPercent = (deltaY / containerHeight) * 100;
let newPercent = currentTopPercent + deltaPercent;
newPercent = Math.max(10, Math.min(newPercent, 90));
cartTab.style.top = newPercent + '%';
this.cartTabPosition.y = newPercent + '%';
};
const handleEnd = () => {
if (!isDragging) return;
isDragging = false;
cartTab.style.transition = 'all 0.3s ease';
this.saveCartTabPosition();
setTimeout(() => {
if (!isDragging) {
this.wasDragged = false;
}
}, 100);
};
// 点击事件处理
const handleClick = (e) => {
if (!this.wasDragged) {
e.preventDefault();
e.stopPropagation();
this.toggleCart();
}
// 重置拖拽状态
setTimeout(() => {
this.wasDragged = false;
}, 100);
};
// 右键清空购物车
const handleContextMenu = (e) => {
e.preventDefault();
e.stopPropagation();
if (this.items.size > 0 && !this.wasDragged) {
this.clearCart();
}
};
// 鼠标事件
cartTab.addEventListener('mousedown', handleStart);
document.addEventListener('mousemove', handleMove);
document.addEventListener('mouseup', handleEnd);
// 触摸事件
cartTab.addEventListener('touchstart', handleStart, { passive: false });
document.addEventListener('touchmove', handleMove, { passive: false });
document.addEventListener('touchend', handleEnd);
// 点击和右键事件
cartTab.addEventListener('click', handleClick);
cartTab.addEventListener('contextmenu', handleContextMenu);
// 悬停效果
cartTab.addEventListener('mouseenter', () => {
if (!isDragging) {
cartTab.style.backgroundColor = 'rgba(156, 39, 176, 0.1)';
cartTab.style.transform = 'translateY(-50%) scale(1.05)';
}
});
cartTab.addEventListener('mouseleave', () => {
if (!isDragging) {
cartTab.style.backgroundColor = 'rgba(42, 43, 66, 0.95)';
cartTab.style.transform = 'translateY(-50%) scale(1)';
}
});
// 手机端触摸点击处理
cartTab.addEventListener('touchend', (e) => {
if (!this.wasDragged) {
e.preventDefault();
e.stopPropagation();
this.toggleCart();
}
setTimeout(() => {
this.wasDragged = false;
}, 100);
});
}
bindEvents() {
const cartTab = document.getElementById('cart-tab');
const purchaseBtn = document.getElementById('cart-purchase-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');
const selectAllCheckbox = document.getElementById('select-all-checkbox');
const batchSetAskBtn = document.getElementById('batch-set-ask');
const batchSetBidBtn = document.getElementById('batch-set-bid');
this.setupCartTabDragAndClick();
// 全选/反选
selectAllCheckbox.addEventListener('change', () => {
this.allSelected = selectAllCheckbox.checked;
this.items.forEach(item => {
item.selected = this.allSelected;
});
this.updateCartDisplay();
this.saveCartToStorage();
});
// 批量设置直购
batchSetAskBtn.addEventListener('click', () => {
this.batchSetPurchaseMode('ask');
});
// 批量设置求购
batchSetBidBtn.addEventListener('click', () => {
this.batchSetPurchaseMode('bid');
});
listNameInput.addEventListener('input', (e) => {
const inputValue = e.target.value.trim();
if (inputValue !== this.currentListName) {
this.currentListName = inputValue;
this.saveCartToStorage();
}
});
purchaseBtn.addEventListener('click', () => this.executeSelectedPurchases());
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)');
purchaseBtn.addEventListener('mouseenter', () => purchaseBtn.style.backgroundColor = 'rgba(30, 58, 138, 0.95)');
purchaseBtn.addEventListener('mouseleave', () => purchaseBtn.style.backgroundColor = 'rgba(30, 58, 138, 0.85)');
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)');
batchSetAskBtn.addEventListener('mouseenter', () => batchSetAskBtn.style.backgroundColor = 'rgba(217, 89, 97, 0.9)');
batchSetAskBtn.addEventListener('mouseleave', () => batchSetAskBtn.style.backgroundColor = 'rgba(217, 89, 97, 0.8)');
batchSetBidBtn.addEventListener('mouseenter', () => batchSetBidBtn.style.backgroundColor = 'rgba(47, 196, 167, 0.9)');
batchSetBidBtn.addEventListener('mouseleave', () => batchSetBidBtn.style.backgroundColor = 'rgba(47, 196, 167, 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 itemIcon = e.target.closest('[data-item-icon]');
if (itemIcon) {
const itemId = itemIcon.dataset.itemIcon;
window.PGE.core.handleGoToMarketplace(`/items/${itemId}`, 0);
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;
}
// 购买方式切换器
const toggle = e.target.closest('.purchase-mode-toggle[data-item-id]');
if (toggle) {
e.stopPropagation();
const itemId = toggle.dataset.itemId;
const newMode = this.togglePurchaseMode(toggle);
const item = this.items.get(itemId);
if (item) {
item.purchaseMode = newMode;
this.saveCartToStorage();
}
return;
}
});
// 数量输入变化事件
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][type="number"]')) {
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);
}
// 选择状态变化
if (e.target.matches('input[data-item-id][type="checkbox"]')) {
const itemId = e.target.dataset.itemId;
const item = this.items.get(itemId);
if (item) {
item.selected = e.target.checked;
this.updateSelectAllState();
this.saveCartToStorage();
}
}
});
// 双击加载清单
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);
}
}
});
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);
}
// 批量设置购买方式
batchSetPurchaseMode(mode) {
let changedCount = 0;
// 为所有物品设置购买方式
this.items.forEach(item => {
if (item.purchaseMode !== mode) {
item.purchaseMode = mode;
changedCount++;
}
});
// 更新默认购买方式
this.defaultPurchaseMode = mode;
if (changedCount > 0) {
this.updateCartDisplay();
this.saveCartToStorage();
}
}
// 更新全选状态
updateSelectAllState() {
const allSelected = Array.from(this.items.values()).every(item => item.selected);
const selectAllCheckbox = document.getElementById('select-all-checkbox');
if (selectAllCheckbox) {
selectAllCheckbox.checked = allSelected;
}
this.allSelected = allSelected;
}
// 执行选中物品的购买
async executeSelectedPurchases() {
const selectedItems = Array.from(this.items.entries())
.filter(([_, item]) => item.selected)
.map(([itemId, item]) => ({
itemHrid: itemId.startsWith('/items/') ? itemId : `/items/${itemId}`,
quantity: item.quantity,
materialName: item.name,
cartItemId: itemId,
purchaseMode: item.purchaseMode || 'bid'
}));
if (selectedItems.length === 0) {
this.showToast('请选择要购买的物品', 'warning');
return;
}
const api = window.MWIModules?.api;
if (!api?.isReady) {
this.showToast(LANG.wsNotAvailable, 'error');
return;
}
const purchaseBtn = document.getElementById('cart-purchase-btn');
const clearBtn = document.getElementById('cart-clear-btn');
const originalText = purchaseBtn.textContent;
const originalBg = purchaseBtn.style.backgroundColor;
// 禁用按钮
purchaseBtn.disabled = true;
clearBtn.disabled = true;
purchaseBtn.textContent = '🔄 购买中...';
purchaseBtn.style.backgroundColor = CONFIG.COLORS.disabled;
purchaseBtn.style.cursor = 'not-allowed';
clearBtn.style.backgroundColor = CONFIG.COLORS.disabled;
clearBtn.style.cursor = 'not-allowed';
clearBtn.style.opacity = '0.5';
try {
// 按购买方式分组
const askItems = selectedItems.filter(item => item.purchaseMode === 'ask');
const bidItems = selectedItems.filter(item => item.purchaseMode === 'bid');
const results = [];
// 执行直购
if (askItems.length > 0) {
const askResults = await api.batchDirectPurchase(askItems, CONFIG.DELAYS.PURCHASE);
results.push(...askResults);
}
// 执行求购
if (bidItems.length > 0) {
const bidResults = await api.batchBidOrder(bidItems, CONFIG.DELAYS.PURCHASE);
results.push(...bidResults);
}
// 处理结果
this.processCartResults(results);
// 移除购买成功的物品
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();
this.updateSelectAllState();
// 如果购物车空了就关闭
if (this.items.size === 0) {
setTimeout(() => this.closeCart(), 1000);
}
}
} catch (error) {
this.showToast(`${LANG.error}: ${error.message}`, 'error');
} finally {
// 恢复按钮状态
purchaseBtn.disabled = false;
clearBtn.disabled = false;
purchaseBtn.textContent = originalText;
purchaseBtn.style.backgroundColor = originalBg;
purchaseBtn.style.cursor = 'pointer';
clearBtn.style.backgroundColor = 'transparent';
clearBtn.style.cursor = 'pointer';
clearBtn.style.opacity = '1';
}
}
// 处理购物车购买结果的方法
processCartResults(results) {
let successCount = 0;
results.forEach(result => {
const isBidOrder = result.item.purchaseMode === 'bid';
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,
selected: itemData.selected !== false, // 默认选中
purchaseMode: itemData.purchaseMode || 'bid' // 默认求购
};
}
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,
selected: itemData.selected !== false, // 兼容旧数据,默认选中
purchaseMode: itemData.purchaseMode || 'bid' // 兼容旧数据,默认求购
});
}
this.currentListName = listName;
const listNameInput = document.getElementById('list-name-input');
if (listNameInput) {
listNameInput.value = listName;
}
this.saveCartToStorage();
this.updateCartBadge();
this.updateCartDisplay();
this.updateSelectAllState();
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.6.6',
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(${Math.min(window.innerWidth, 450) + 'px'})`;
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,
selected: true, // 新添加的物品默认选中
purchaseMode: this.defaultPurchaseMode // 使用默认购买方式
});
}
this.saveCartToStorage();
this.updateCartBadge();
this.updateCartDisplay();
this.updateSelectAllState();
this.showToast(`${LANG.add} ${itemInfo.name} x${quantity} ${LANG.toCart}`, 'success', 2000);
}
removeItem(itemId) {
this.items.delete(itemId);
this.saveCartToStorage();
this.updateCartBadge();
this.updateCartDisplay();
this.updateSelectAllState();
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 = '';
}
const selectAllCheckbox = document.getElementById('select-all-checkbox');
if (selectAllCheckbox) {
selectAllCheckbox.checked = true;
}
this.allSelected = true;
this.saveCartToStorage();
this.updateCartBadge();
this.updateCartDisplay();
this.showToast(LANG.cartClearSuccess, 'success', 3000);
if (this.isOpen) {
this.closeCart();
}
}
updateCartDisplay() {
const container = document.getElementById('cart-items-container');
if (!container) return;
if (this.items.size === 0) {
container.innerHTML = `
<div style="
text-align: center;
color: var(--color-neutral-400);
padding: 40px 20px;
font-style: italic;
font-size: 14px;
">${LANG.cartEmpty}</div>
`;
return;
}
let html = '';
for (const [itemId, item] of this.items) {
const toggle = this.createPurchaseModeToggle(item.purchaseMode || 'bid');
toggle.setAttribute('data-item-id', itemId);
const toggleHTML = toggle.outerHTML;
html += `
<div style="
display: flex;
align-items: center;
padding: 10px;
margin-bottom: 8px;
background-color: var(--item-background);
border: 1px solid var(--item-border);
border-radius: 6px;
transition: all 0.2s ease;
" onmouseenter="this.style.backgroundColor='var(--item-background-hover)'; this.style.borderColor='var(--item-border-hover)'" onmouseleave="this.style.backgroundColor='var(--item-background)'; this.style.borderColor='var(--item-border)'">
<!-- 选择框 -->
<input type="checkbox" data-item-id="${itemId}" ${item.selected !== false ? 'checked' : ''} style="
margin-right: 8px;
transform: scale(1.2);
cursor: pointer;
">
<!-- 物品图标 -->
<div data-item-icon="${itemId}" style="
width: 32px;
height: 32px;
margin-right: 12px;
display: flex;
align-items: center;
justify-content: center;
background: var(--item-background);
border-radius: 4px;
cursor: pointer;
">
<svg width="100%" height="100%" style="max-width: 24px; max-height: 24px;">
<use href="/static/media/items_sprite.6d12eb9d.svg${item.iconHref}"></use>
</svg>
</div>
<!-- 物品信息 -->
<div style="flex: 1; color: var(--color-text-dark-mode); min-width: 0;">
<div style="font-size: 13px; font-weight: 500; margin-bottom: 2px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;">${item.name}</div>
</div>
<!-- 控制区域 -->
<div style="display: flex; align-items: center; gap: 8px; flex-shrink: 0;">
<!-- 购买方式切换器 -->
${toggleHTML}
<!-- 数量输入 -->
<input
type="number"
value="${item.quantity}"
min="1"
max="999999999999"
maxlength="12"
data-item-id="${itemId}"
style="
width: 120px;
padding: 4px 6px;
background-color: var(--item-background);
border: 1px solid var(--item-border);
border-radius: 3px;
color: var(--color-text-dark-mode);
font-size: 11px;
text-align: right;
"
>
<!-- 删除按钮 -->
<button
data-remove-item="${itemId}"
style="
background: none;
border: none;
color: #f44336;
cursor: pointer;
padding: 4px;
border-radius: 3px;
transition: background-color 0.2s;
font-size: 12px;
width: 24px;
height: 24px;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
"
title="${LANG.cartRemove}"
onmouseenter="this.style.backgroundColor='rgba(244, 67, 54, 0.2)'"
onmouseleave="this.style.backgroundColor='transparent'"
>🗑️</button>
</div>
</div>
`;
}
container.innerHTML = html;
}
updateSavedListsDisplay() {
const container = document.getElementById('saved-lists-container');
if (!container) return;
if (this.savedLists.size === 0) {
container.innerHTML = `
<div style="
text-align: center;
color: var(--color-neutral-400);
padding: 20px;
font-style: italic;
font-size: 12px;
">${LANG.noSavedLists}</div>
`;
return;
}
let html = '';
const sortedLists = Array.from(this.savedLists.entries())
.sort((a, b) => b[1].savedAt - a[1].savedAt);
for (const [listName, listData] of sortedLists) {
const itemCount = Object.keys(listData.items).length;
html += `
<div style="
display: flex;
align-items: center;
padding: 8px;
margin-bottom: 6px;
background-color: var(--item-background);
border: 1px solid var(--item-border);
border-radius: 4px;
transition: all 0.2s ease;
user-select: none;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
" onmouseenter="this.style.backgroundColor='var(--item-background-hover)'" onmouseleave="this.style.backgroundColor='var(--item-background)'">
<div style="flex: 1; color: var(--color-text-dark-mode); min-width: 0;">
<div style="font-size: 12px; font-weight: 500; margin-bottom: 2px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;">${listName}</div>
<div style="font-size: 10px; color: var(--color-neutral-400);">${itemCount}${LANG.cartItem}</div>
</div>
<div style="display: flex; gap: 6px; flex-shrink: 0;">
<button
data-load-list="${listName}"
style="
background: rgba(76, 175, 80, 0.8);
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
padding: 6px 10px;
font-size: 11px;
font-weight: 500;
transition: background-color 0.2s;
line-height: 1;
white-space: nowrap;
"
title="加载清单"
onmouseenter="this.style.backgroundColor='rgba(76, 175, 80, 0.9)'"
onmouseleave="this.style.backgroundColor='rgba(76, 175, 80, 0.8)'"
>${LANG.load}</button>
<button
data-delete-list="${listName}"
style="
background: rgba(244, 67, 54, 0.8);
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
padding: 6px 10px;
font-size: 11px;
font-weight: 500;
transition: background-color 0.2s;
line-height: 1;
white-space: nowrap;
"
title="删除清单"
onmouseenter="this.style.backgroundColor='rgba(244, 67, 54, 0.9)'"
onmouseleave="this.style.backgroundColor='rgba(244, 67, 54, 0.8)'"
>${LANG.delete}</button>
</div>
</div>
`;
}
container.innerHTML = html;
}
saveCartToStorage() {
try {
const cartData = {
items: Object.fromEntries(this.items),
currentListName: this.currentListName,
allSelected: this.allSelected,
defaultPurchaseMode: this.defaultPurchaseMode
};
localStorage.setItem('milkyway-current-cart', JSON.stringify(cartData));
} catch (error) {
console.warn('保存当前购物车失败:', error);
}
}
loadCartFromStorage() {
try {
const cartData = JSON.parse(localStorage.getItem('milkyway-current-cart') || '{}');
// 加载物品数据,确保兼容旧格式
const itemsData = cartData.items || {};
this.items = new Map();
for (const [itemId, itemData] of Object.entries(itemsData)) {
this.items.set(itemId, {
name: itemData.name,
iconHref: itemData.iconHref,
quantity: itemData.quantity,
selected: itemData.selected !== false, // 兼容旧数据,默认选中
purchaseMode: itemData.purchaseMode || 'bid' // 兼容旧数据,默认求购
});
}
this.currentListName = cartData.currentListName || '';
this.allSelected = cartData.allSelected !== false; // 默认全选
this.defaultPurchaseMode = cartData.defaultPurchaseMode || 'bid'; // 默认求购
const listNameInput = document.getElementById('list-name-input');
if (listNameInput) {
listNameInput.value = this.currentListName;
}
} catch (error) {
console.warn('加载当前购物车失败:', error);
this.items = new Map();
this.currentListName = '';
this.allSelected = true;
this.defaultPurchaseMode = 'bid';
}
}
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;
}
showToast(message, type, duration) {
if (window.MWIModules?.toast) {
window.MWIModules.toast.show(message, type, duration);
}
}
}
// ==================== 自动停止管理器 ====================
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.PGE?.hookMessage) {
this.initHooks();
} else {
setTimeout(waitForAPI, 1000);
}
};
waitForAPI();
}
initHooks() {
try {
window.PGE.hookMessage('new_character_action', (data) => this.handleNewAction(data));
window.PGE.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.PGE?.core?.handleCancelCharacterAction?.(actionId);
} catch (error) {
console.error('[AutoStop] 取消动作失败:', error);
}
}
getCurrentTargetCount() {
const input = document.querySelector('.auto-stop-target-input');
return input ? parseInt(input.value) || 0 : 0;
}
cleanup() {
this.activeMonitors.forEach(monitor => clearInterval(monitor.intervalId));
this.activeMonitors.clear();
this.pendingActions.clear();
}
createInfinityButton() {
const nativeButton = document.querySelector('button .SkillActionDetail_unlimitedIcon__mZYJc')?.parentElement;
if (nativeButton) {
const clone = nativeButton.cloneNode(true);
clone.getAttributeNames().filter(name => name.startsWith('data-')).forEach(attr => clone.removeAttribute(attr));
return clone;
}
const button = document.createElement('button');
button.className = 'Button_button__1Fe9z Button_small__3fqC7';
const container = document.createElement('div');
container.className = 'SkillActionDetail_unlimitedIcon__mZYJc';
const svg = document.createElement('svg');
Object.assign(svg, {
role: 'img',
'aria-label': 'Unlimited',
className: 'Icon_icon__2LtL_ Icon_xtiny__331pI',
width: '100%',
height: '100%'
});
svg.style.margin = '-2px -1px';
const use = document.createElement('use');
use.setAttribute('href', '/static/media/misc_sprite.6b3198dc.svg#infinity');
svg.appendChild(use);
container.appendChild(svg);
button.appendChild(container);
setTimeout(() => {
if (svg.getBoundingClientRect().width === 0) {
button.innerHTML = '<span style="font-size: 14px; font-weight: bold;">∞</span>';
}
}, 500);
return button;
}
createAutoStopUI() {
const container = document.createElement('div');
container.className = 'SkillActionDetail_maxActionCountInput__1C0Pw auto-stop-ui';
const label = document.createElement('div');
label.className = 'SkillActionDetail_label__1mGQJ';
label.textContent = LANG.targetLabel;
const inputArea = document.createElement('div');
inputArea.className = 'SkillActionDetail_input__1G-kE';
const inputContainer = document.createElement('div');
inputContainer.className = 'Input_inputContainer__22GnD Input_small__1-Eva';
const input = document.createElement('input');
input.className = 'Input_input__2-t98 auto-stop-target-input';
input.type = 'text';
input.maxLength = '10';
input.value = '0';
const setOneButton = document.createElement('button');
setOneButton.className = 'Button_button__1Fe9z Button_small__3fqC7';
setOneButton.textContent = '1';
const setInfinityButton = this.createInfinityButton();
const updateStatus = () => {
const targetCount = parseInt(input.value) || 0;
if (targetCount > 0) {
setInfinityButton.classList.remove('Button_disabled__wCyIq');
input.value = targetCount.toString();
setOneButton.classList.toggle('Button_disabled__wCyIq', targetCount === 1);
} else {
setInfinityButton.classList.add('Button_disabled__wCyIq');
setOneButton.classList.remove('Button_disabled__wCyIq');
input.value = '∞';
}
if (this.activeMonitors.size > 0) {
if (targetCount <= 0) {
this.activeMonitors.forEach((_, actionId) => this.stopMonitoring(actionId));
} else {
this.activeMonitors.forEach(monitor => monitor.targetCount = targetCount);
}
}
};
setOneButton.addEventListener('click', () => {
input.value = '1';
updateStatus();
});
setInfinityButton.addEventListener('click', () => {
input.value = '0';
updateStatus();
});
input.addEventListener('input', (e) => {
const value = e.target.value;
if (value === '∞' || !isNaN(parseInt(value))) updateStatus();
});
input.addEventListener('focus', (e) => e.target.select());
input.addEventListener('blur', updateStatus);
input.addEventListener('keydown', (e) => {
if (input.value === '∞' && /[0-9]/.test(e.key)) {
e.preventDefault();
input.value = e.key;
updateStatus();
}
});
updateStatus();
inputContainer.appendChild(input);
inputArea.appendChild(inputContainer);
container.append(label, inputArea, setOneButton, setInfinityButton);
return container;
}
injectAutoStopUI() {
const skillElement = document.querySelector('.SkillActionDetail_regularComponent__3oCgr');
if (!skillElement || this.processedComponents.has(skillElement)) return false;
const maxInput = skillElement.querySelector('.SkillActionDetail_maxActionCountInput__1C0Pw');
if (!maxInput || skillElement.querySelector('.auto-stop-ui')) return false;
const hrid = utils.extractActionDetailData(skillElement);
if (!hrid || !gatheringActionsMap.has(hrid)) return false;
this.processedComponents.add(skillElement);
maxInput.parentNode.insertBefore(this.createAutoStopUI(), maxInput.nextSibling);
return true;
}
}
// ==================== 材料购买管理器 ====================
class MaterialPurchaseManager {
constructor() {
this.init();
}
init() {
this.setupObserver();
this.setupEventListeners();
}
setupObserver() {
const observer = new MutationObserver(() => {
Object.keys(SELECTORS).forEach(type => {
if (type !== 'alchemy') this.setupUI(type);
});
});
observer.observe(document.body, { childList: true, subtree: true });
}
setupEventListeners() {
let updateTimer = null;
document.addEventListener('input', (e) => {
if (e.target.classList.contains('Input_input__2-t98')) {
clearTimeout(updateTimer);
updateTimer = setTimeout(() => {
this.updateAllInfoSpans();
}, 1);
}
});
document.addEventListener('click', (e) => {
if (e.target.classList) {
clearTimeout(updateTimer);
updateTimer = setTimeout(() => {
this.updateAllInfoSpans();
}, 1);
}
});
}
async purchaseMaterials(type, isBidOrder = false) {
const api = window.MWIModules?.api;
const toast = window.MWIModules?.toast;
if (!api?.isReady) {
toast?.show(LANG.wsNotAvailable, 'error');
return;
}
const requirements = await MaterialCalculator.calculateRequirements(type);
const needToBuy = requirements.filter(item =>
item.type === 'material' && item.itemId && !item.itemId.includes('coin') && item.supplementNeeded > 0
);
if (needToBuy.length === 0) {
toast?.show(LANG.sufficient, 'info');
return;
}
const itemList = needToBuy.map(item =>
`${item.materialName}: ${item.supplementNeeded}${LANG.each}`
).join(', ');
toast?.show(`${LANG.starting} ${needToBuy.length} ${LANG.materials}: ${itemList}`, 'info');
try {
const purchaseItems = needToBuy.map(item => ({
itemHrid: item.itemId.startsWith('/items/') ? item.itemId : `/items/${item.itemId}`,
quantity: item.supplementNeeded,
materialName: item.materialName
}));
const results = isBidOrder ?
await api.batchBidOrder(purchaseItems, CONFIG.DELAYS.PURCHASE) :
await api.batchDirectPurchase(purchaseItems, CONFIG.DELAYS.PURCHASE);
this.processResults(results, isBidOrder, type);
} catch (error) {
toast?.show(`${LANG.error}: ${error.message}`, 'error');
}
}
processResults(results, isBidOrder, type) {
const toast = window.MWIModules?.toast;
let successCount = 0;
results.forEach(result => {
const statusText = isBidOrder ?
(result.success ? LANG.submitted : LANG.failed) :
(result.success ? LANG.purchased : LANG.failed);
const message = `${statusText} ${result.item.materialName || result.item.itemHrid} x${result.item.quantity}`;
toast?.show(message, result.success ? 'success' : 'error');
if (result.success) successCount++;
});
const finalMessage = successCount > 0 ?
`${LANG.complete} ${LANG.success} ${successCount}/${results.length} ${LANG.materials}` :
LANG.allFailed;
toast?.show(finalMessage, successCount > 0 ? 'success' : 'error', successCount > 0 ? 5000 : 3000);
if (successCount > 0) {
setTimeout(() => this.updateAllInfoSpans(), 2000);
}
}
updateAllInfoSpans() {
['enhancing', 'production'].forEach(type => this.updateInfoSpans(type));
}
async updateInfoSpans(type) {
const requirements = await MaterialCalculator.calculateRequirements(type);
const className = `${type === 'house' ? 'house-' : type === 'enhancing' ? 'enhancing-' : ''}material-info-span`;
document.querySelectorAll(`.${className}`).forEach((span, index) => {
const materialReq = requirements.filter(req => req.type === 'material')[index];
if (materialReq) {
const needed = materialReq.supplementNeeded;
span.textContent = `${LANG.missing}${needed}`;
span.style.color = needed > 0 ? CONFIG.COLORS.error : CONFIG.COLORS.text;
}
});
const upgradeSpan = document.querySelector('.upgrade-info-span');
const upgradeReq = requirements.find(req => req.type === 'upgrade');
if (upgradeSpan && upgradeReq) {
const needed = upgradeReq.supplementNeeded;
upgradeSpan.textContent = `${LANG.missing}${needed}`;
upgradeSpan.style.color = needed > 0 ? CONFIG.COLORS.error : CONFIG.COLORS.text;
}
}
setupUI(type) {
const configs = {
production: { className: 'material-info-span', gridCols: 'auto min-content auto auto', buttonParent: 'name' },
house: { className: 'house-material-info-span', gridCols: 'auto auto auto 140px', buttonParent: 'header' },
enhancing: { className: 'enhancing-material-info-span', gridCols: 'auto min-content auto auto', buttonParent: 'cost' }
};
const selectors = SELECTORS[type];
const config = configs[type];
document.querySelectorAll(selectors.container).forEach(panel => {
const dataAttr = `${type}ButtonInserted`;
if (panel.dataset[dataAttr]) return;
if (type === 'enhancing' && panel.querySelector(selectors.instructions)) return;
const requirements = panel.querySelector(selectors.requirements);
if (!requirements) return;
panel.dataset[dataAttr] = "true";
this.setupMaterialInfo(requirements, config, type);
this.setupUpgradeInfo(panel, selectors, type);
this.setupButtons(panel, selectors, config, type);
setTimeout(() => this.updateInfoSpans(type), CONFIG.DELAYS.UPDATE);
});
}
setupMaterialInfo(requirements, config, type) {
const modifiedAttr = `${type}Modified`;
if (requirements.dataset[modifiedAttr]) return;
requirements.dataset[modifiedAttr] = "true";
requirements.style.gridTemplateColumns = config.gridCols;
requirements.querySelectorAll('.Item_itemContainer__x7kH1').forEach(item => {
if (item.nextSibling?.classList?.contains(config.className)) return;
const span = this.createInfoSpan();
span.className = config.className;
item.parentNode.insertBefore(span, item.nextSibling);
});
}
setupUpgradeInfo(panel, selectors, type) {
if (type !== 'production') return;
const upgradeContainer = panel.querySelector(selectors.upgrade);
if (!upgradeContainer || upgradeContainer.dataset.upgradeModified) return;
upgradeContainer.dataset.upgradeModified = "true";
if (!upgradeContainer.querySelector('.upgrade-info-span')) {
const upgradeSpan = this.createInfoSpan();
upgradeSpan.className = 'upgrade-info-span';
upgradeContainer.appendChild(upgradeSpan);
}
}
createInfoSpan() {
const span = document.createElement("span");
span.textContent = `${LANG.missing}0`;
utils.applyStyles(span, {
fontSize: '12px', fontWeight: 'bold', padding: '2px 6px', borderRadius: '3px',
whiteSpace: 'nowrap', minWidth: '60px', textAlign: 'center'
});
return span;
}
setupButtons(panel, selectors, config, type) {
if (panel.querySelector('.buy-buttons-container')) return;
const shoppingCart = window.MWIModules?.shoppingCart;
const materialButtonContainer = document.createElement('div');
materialButtonContainer.className = 'buy-buttons-container';
const baseStyles = { display: 'flex', gap: '6px', justifyContent: 'center', alignItems: 'center', marginBottom: '8px' };
const typeStyles = {
house: { width: 'fit-content', margin: '0 auto 8px auto', maxWidth: '320px', minWidth: '300px' },
enhancing: { width: 'fit-content', margin: '0 auto 8px auto', maxWidth: '340px', minWidth: '300px' }
};
utils.applyStyles(materialButtonContainer, { ...baseStyles, ...typeStyles[type] });
const directBuyBtn = this.createUnifiedButton(LANG.directBuy, () => this.purchaseMaterials(type, false), 'direct-buy');
const addToCartBtn = shoppingCart?.createAddAllToCartButton ? shoppingCart.createAddAllToCartButton(type) : this.createPlaceholderButton();
const bidOrderBtn = this.createUnifiedButton(LANG.bidOrder, () => this.purchaseMaterials(type, true), 'bid-order');
materialButtonContainer.append(directBuyBtn, addToCartBtn, bidOrderBtn);
if (type === 'production') {
const upgradeContainer = panel.querySelector(selectors.upgrade);
if (upgradeContainer && !upgradeContainer.querySelector('.upgrade-buttons-container')) {
const upgradeButtonContainer = document.createElement('div');
upgradeButtonContainer.className = 'upgrade-buttons-container';
utils.applyStyles(upgradeButtonContainer, {
display: 'flex',
gap: '6px',
justifyContent: 'center',
alignItems: 'center',
marginTop: '8px',
width: '100%'
});
const directBuyUpgradeBtn = this.createUnifiedButton(LANG.directBuyUpgrade, () => this.purchaseUpgrades(type, false), 'direct-buy');
const bidOrderUpgradeBtn = this.createUnifiedButton(LANG.bidOrderUpgrade, () => this.purchaseUpgrades(type, true), 'bid-order');
upgradeButtonContainer.append(directBuyUpgradeBtn, bidOrderUpgradeBtn);
upgradeContainer.appendChild(upgradeButtonContainer);
}
}
const insertionMethods = {
production: () => {
const parent = panel.querySelector(selectors[config.buttonParent]);
parent.parentNode.insertBefore(materialButtonContainer, parent.nextSibling);
},
house: () => {
const parent = panel.querySelector(selectors[config.buttonParent]);
parent.parentNode.insertBefore(materialButtonContainer, parent);
},
enhancing: () => {
const parent = panel.querySelector(selectors[config.buttonParent]);
parent.parentNode.insertBefore(materialButtonContainer, parent);
}
};
insertionMethods[type]?.();
}
createUnifiedButton(text, onClick, buttonType) {
const btn = document.createElement("button");
btn.textContent = text;
btn.className = 'unified-action-btn';
btn.setAttribute('data-button-type', buttonType);
this.applyUnifiedButtonStyle(btn, buttonType);
btn.addEventListener("click", () => this.handleButtonClick(btn, text, onClick, buttonType));
return btn;
}
applyUnifiedButtonStyle(btn, buttonType) {
const buttonConfigs = {
'direct-buy': {
backgroundColor: 'rgba(217, 89, 97, 0.8)',
borderColor: 'rgba(217, 89, 97, 0.5)',
hoverColor: 'rgba(217, 89, 97, 0.9)'
},
'bid-order': {
backgroundColor: 'rgba(47, 196, 167, 0.8)',
borderColor: 'rgba(47, 196, 167, 0.5)',
hoverColor: 'rgba(47, 196, 167, 0.9)'
},
'add-to-cart': {
backgroundColor: 'rgba(103, 58, 183, 0.8)',
borderColor: 'rgba(103, 58, 183, 0.5)',
hoverColor: 'rgba(103, 58, 183, 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);
let consumptionPerUnit;
// 根据配置决定是否考虑工匠茶影响
if (window.PGE_CONFIG.considerArtisanTea) {
// 考虑工匠茶影响:使用基础消耗量*(1-artisanBuff)
const baseConsumption = this.getBaseMaterialConsumption(materialContainer, i);
const artisanBuff = this.getArtisanBuff(container);
consumptionPerUnit = baseConsumption * (1 - artisanBuff);
} else {
// 不考虑工匠茶影响:使用基础消耗量
consumptionPerUnit = this.getBaseMaterialConsumption(materialContainer, i);
}
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'
});
});
}
// 获取工匠茶buff效果
static getArtisanBuff(container) {
try {
const props = utils.getReactProps(container);
if (!props) return 0.0;
const buffs = props.actionBuffs || [];
let artisanBuff = 0.0;
for (const buff of buffs) {
if (buff.typeHrid === '/buff_types/artisan') {
artisanBuff += (buff.flatBoost || 0.0);
}
}
return artisanBuff;
} catch (error) {
console.error('获取工匠茶buff失败:', error);
return 0.0;
}
}
//获取基础材料消耗量
static getBaseMaterialConsumption(materialContainer, index) {
try {
const reactKey = Object.keys(materialContainer).find(key => key.startsWith('__reactProps$'));
if (reactKey) {
const props = materialContainer[reactKey];
const baseCount = props?.children?._owner?.memoizedProps?.count;
if (typeof baseCount === 'number') {
return baseCount;
}
}
} catch (error) {
console.error('获取基础材料消耗量失败:', error);
}
}
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' });
}
}
// ==================== 快速出售管理器 ====================
class QuickSellManager {
constructor() {
this.processedMenus = new WeakSet();
this.isProcessing = false;
this.buttonStates = new WeakMap();
this.isEnabled = true;
this.init();
}
init() {
this.setupObserver();
}
setupObserver() {
const observer = new MutationObserver(() => {
this.checkAndAddSellButtons();
});
observer.observe(document.body, { childList: true, subtree: true });
}
enable() {
this.isEnabled = true;
this.init();
}
disable() {
this.isEnabled = false;
document.querySelectorAll('.quick-sell-ask-btn, .quick-sell-bid-btn').forEach(btn => {
btn.remove();
});
}
checkAndAddSellButtons() {
if (!this.isEnabled) return;
try {
// 检查是否出现物品菜单
const itemMenu = document.querySelector('.Item_actionMenu__2yUcG');
if (itemMenu && !this.processedMenus.has(itemMenu)) {
this.addQuickSellButtons(itemMenu);
this.processedMenus.add(itemMenu);
}
} catch (error) {
console.error('检查菜单失败:', error);
}
}
addQuickSellButtons(menuContainer) {
// 检查是否已存在出售按钮
if (menuContainer.querySelector('.quick-sell-ask-btn') ||
menuContainer.querySelector('.quick-sell-bid-btn')) {
return;
}
// 检查是否存在数量输入框,如果没有就不显示快速出售按钮
const quantityInput = menuContainer.querySelector('.Input_input__2-t98');
if (!quantityInput) {
return;
}
// 创建"左一出售"按钮(按ask价挂单)
const askSellButton = document.createElement('button');
askSellButton.className = 'Button_button__1Fe9z Button_sell__3FNpM Button_fullWidth__17pVU quick-sell-ask-btn';
askSellButton.textContent = LANG.quickSell.askSell;
// 创建"右一出售"按钮(按bid价直售)
const bidSellButton = document.createElement('button');
bidSellButton.className = 'Button_button__1Fe9z Button_sell__3FNpM Button_fullWidth__17pVU quick-sell-bid-btn';
bidSellButton.textContent = LANG.quickSell.bidSell;
// 初始化按钮状态
this.buttonStates.set(askSellButton, {
confirmed: false,
sellType: 'ask',
originalText: LANG.quickSell.askSell,
confirmText: LANG.quickSell.confirmAskSell,
timeout: null,
enableTimeout: null
});
this.buttonStates.set(bidSellButton, {
confirmed: false,
sellType: 'bid',
originalText: LANG.quickSell.bidSell,
confirmText: LANG.quickSell.confirmBidSell,
timeout: null,
enableTimeout: null
});
// 添加点击事件
askSellButton.addEventListener('click', (e) => {
e.preventDefault();
e.stopPropagation();
this.handleButtonClick(askSellButton, menuContainer);
});
bidSellButton.addEventListener('click', (e) => {
e.preventDefault();
e.stopPropagation();
this.handleButtonClick(bidSellButton, menuContainer);
});
menuContainer.appendChild(askSellButton);
menuContainer.appendChild(bidSellButton);
}
handleButtonClick(button, menuContainer) {
if (this.isProcessing) {
return;
}
const state = this.buttonStates.get(button);
if (!state) return;
if (!state.confirmed) {
this.enterConfirmState(button, state);
} else {
if (!button.disabled) {
this.performQuickSell(menuContainer, state.sellType, button);
}
}
}
enterConfirmState(button, state) {
// 清除之前的超时
if (state.timeout) {
clearTimeout(state.timeout);
}
if (state.enableTimeout) {
clearTimeout(state.enableTimeout);
}
button.className = 'Button_button__1Fe9z Button_warning__1-AMI Button_fullWidth__17pVU Button_disabled__wCyIq';
button.textContent = state.confirmText;
button.disabled = true;
state.confirmed = true;
state.enableTimeout = setTimeout(() => {
button.className = 'Button_button__1Fe9z Button_warning__1-AMI Button_fullWidth__17pVU';
button.disabled = false;
}, 500);
}
resetButtonState(button, state) {
if (state.timeout) {
clearTimeout(state.timeout);
state.timeout = null;
}
if (state.enableTimeout) {
clearTimeout(state.enableTimeout);
state.enableTimeout = null;
}
button.className = 'Button_button__1Fe9z Button_sell__3FNpM Button_fullWidth__17pVU';
button.textContent = state.originalText;
button.disabled = false;
state.confirmed = false;
}
async performQuickSell(menuContainer, sellType, button) {
if (this.isProcessing) {
return;
}
this.isProcessing = true;
const state = this.buttonStates.get(button);
try {
// 获取物品信息
const itemInfo = this.extractItemInfo(menuContainer);
// 获取出售数量
const quantity = this.getQuantity(menuContainer);
// 显示开始出售的提示
const startMessage = sellType === 'ask' ? LANG.quickSell.startListing : LANG.quickSell.startInstantSell;
this.showToast(`${startMessage}: ${itemInfo.name} x${quantity}`, 'info');
// 获取市场数据
const marketData = await this.getMarketData(itemInfo.itemHrid);
if (!marketData) {
throw new Error(LANG.quickSell.noMarketData);
}
// 计算价格
const price = this.calculatePrice(marketData, itemInfo.enhancementLevel, quantity, sellType);
if (!price || price <= 0) {
throw new Error(`${LANG.quickSell.getPriceFailed}: ${sellType}`);
}
// 执行出售
const isInstantSell = sellType === 'bid'; // bid是直售,ask是挂单
await this.executeSell(itemInfo, quantity, price, isInstantSell);
// 出售成功后重置按钮状态
this.resetButtonState(button, state);
} catch (error) {
console.error(LANG.quickSell.sellFailed + ':', error);
this.showToast(`${LANG.quickSell.sellFailed}: ${error.message}`, 'error');
// 出售失败也重置按钮状态
this.resetButtonState(button, state);
} finally {
this.isProcessing = false;
}
}
extractItemInfo(menuContainer) {
try {
// 获取物品名称
const itemNameElement = menuContainer.querySelector('.Item_name__2C42x');
const itemName = itemNameElement?.textContent?.trim();
// 获取React props
const reactKey = Object.keys(menuContainer).find(key => key.startsWith('__reactProps'));
const itemInfo = menuContainer[reactKey]?.children[0]._owner.memoizedProps;
if (!itemInfo || !itemName) {
return null;
}
return {
name: itemName,
itemHrid: itemInfo.itemHrid,
enhancementLevel: itemInfo.enhancementLevel || 0
};
} catch (error) {
console.error(LANG.quickSell.extractItemInfoFailed + ':', error);
return null;
}
}
getQuantity(menuContainer) {
try {
const quantityInput = menuContainer.querySelector('.Input_input__2-t98');
return parseInt(quantityInput.value);
} catch (error) {
throw error;
}
}
async getMarketData(itemHrid) {
try {
const fullItemHrid = itemHrid.startsWith('/items/') ? itemHrid : `/items/${itemHrid}`;
// 检查缓存
const cached = window.marketDataCache?.get(fullItemHrid);
if (cached && Date.now() - cached.timestamp < 60000) {
return cached.data;
}
// 等待市场数据响应
const responsePromise = window.PGE.waitForMessage(
'market_item_order_books_updated',
8000,
(responseData) => responseData.marketItemOrderBooks?.itemHrid === fullItemHrid
);
// 请求市场数据
window.PGE.core.handleGetMarketItemOrderBooks(fullItemHrid);
const response = await responsePromise;
return response.marketItemOrderBooks;
} catch (error) {
console.error(LANG.quickSell.getMarketDataFailed + ':', error);
return null;
}
}
calculatePrice(marketData, enhancementLevel, quantity, sellType) {
try {
if (sellType === 'ask') {
// 左一出售:按ask价挂单(参考卖单价格)
return this.analyzeAskPrice(marketData, enhancementLevel);
} else {
// 右一出售:按bid价直售(卖给买单)
return this.analyzeBidPrice(marketData, enhancementLevel, quantity);
}
} catch (error) {
console.error(LANG.quickSell.getPriceFailed + ':', error);
return null;
}
}
analyzeAskPrice(marketData, enhancementLevel) {
const asks = marketData.orderBooks?.[enhancementLevel]?.asks;
if (!asks?.length) {
return null;
}
// 返回最低卖单价格,用于挂单竞争
return asks[0].price;
}
analyzeBidPrice(marketData, enhancementLevel, quantity) {
const bids = marketData.orderBooks?.[enhancementLevel]?.bids;
if (!bids?.length) {
return null;
}
// 分析能够出售的数量和价格
let cumulativeQuantity = 0;
let targetPrice = 0;
for (const bid of bids) {
const canSellToThisOrder = Math.min(bid.quantity, quantity - cumulativeQuantity);
cumulativeQuantity += canSellToThisOrder;
targetPrice = bid.price;
if (cumulativeQuantity >= quantity) break;
}
if (cumulativeQuantity < quantity) {
console.warn(`${LANG.quickSell.marketOrdersInsufficient} ${cumulativeQuantity}${LANG.quickSell.needed} ${quantity}`);
}
return targetPrice;
}
async executeSell(itemInfo, quantity, price, isInstantSell) {
try {
const fullItemHrid = itemInfo.itemHrid.startsWith('/items/') ?
itemInfo.itemHrid : `/items/${itemInfo.itemHrid}`;
if (isInstantSell) {
// 直售(卖给买单)
await this.executeInstantSell(fullItemHrid, itemInfo.enhancementLevel, quantity, price, itemInfo.name);
} else {
// 挂单出售
await this.executeListing(fullItemHrid, itemInfo.enhancementLevel, quantity, price, itemInfo.name);
}
} catch (error) {
console.error(LANG.quickSell.executeSellFailed + ':', error);
throw error;
}
}
async executeInstantSell(itemHrid, enhancementLevel, quantity, price, itemName) {
const successPromise = window.PGE.waitForMessage(
'info',
15000,
(responseData) => responseData.message === 'infoNotification.sellOrderCompleted'
);
const errorPromise = window.PGE.waitForMessage('error', 15000);
window.PGE.core.handlePostMarketOrder(true, itemHrid, enhancementLevel, quantity, price, true);
try {
await Promise.race([
successPromise,
errorPromise.then(errorData => Promise.reject(new Error(errorData.message || LANG.quickSell.instantSellFailed)))
]);
this.showToast(`✅ ${LANG.quickSell.instantSellSuccess}: ${itemName} x${quantity} @ ${price}`, 'success');
} catch (error) {
this.showToast(`❌ ${LANG.quickSell.instantSellFailed}: ${itemName}`, 'error');
throw error;
}
}
async executeListing(itemHrid, enhancementLevel, quantity, price, itemName) {
const successPromise = window.PGE.waitForMessage(
'info',
15000,
(responseData) => responseData.message === 'infoNotification.sellListingProgress'
);
const errorPromise = window.PGE.waitForMessage('error', 15000);
window.PGE.core.handlePostMarketOrder(true, itemHrid, enhancementLevel, quantity, price, false);
try {
await Promise.race([
successPromise,
errorPromise.then(errorData => Promise.reject(new Error(errorData.message || LANG.quickSell.listingFailed)))
]);
this.showToast(`✅ ${LANG.quickSell.listingSuccess}: ${itemName} x${quantity} @ ${price}`, 'success');
} catch (error) {
this.showToast(`❌ ${LANG.quickSell.listingFailed}: ${itemName}`, 'error');
throw error;
}
}
showToast(message, type) {
if (window.MWIModules?.toast) {
window.MWIModules.toast.show(message, type);
} else {
console.log(`${message}`);
}
}
// 清理资源
cleanup() {
this.processedMenus = new WeakSet();
this.isProcessing = false;
// 清理所有按钮的超时
for (const [button, state] of this.buttonStates) {
if (state.timeout) {
clearTimeout(state.timeout);
}
if (state.enableTimeout) {
clearTimeout(state.enableTimeout);
}
}
this.buttonStates = new WeakMap();
}
}
// ==================== 全局样式 ====================
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.PGE.core || initGameCore()) {
clearInterval(interval);
}
}, 2000);
}
// ==================== 模块初始化 ====================
function initializeModules() {
console.log('[PGE] Starting module initialization...');
// 初始化基础模块
window.MWIModules.toast = new Toast();
window.MWIModules.api = new PGE();
// 根据配置初始化功能模块
if (PGE_CONFIG.itemValueCalculator) {
const characterData = window.PGE?.characterData?.character;
if (characterData && characterData.gameMode === 'standard') {
window.MWIModules.itemValueCalculator = new ItemValueCalculator();
} else {
console.log(`[PGE] 物品价值计算器未启用:角色模式为 ${characterData?.gameMode || 'Unknown'}`);
}
}
if (PGE_CONFIG.quickSell) {
window.MWIModules.quickSell = new QuickSellManager();
}
if (PGE_CONFIG.gatheringEnhanced) {
window.MWIModules.autoStop = new AutoStopManager();
}
if (PGE_CONFIG.quickPurchase) {
window.MWIModules.shoppingCart = new ShoppingCartManager();
window.MWIModules.materialPurchase = new MaterialPurchaseManager();
}
if (PGE_CONFIG.alchemyProfit) {
window.MWIModules.alchemyCalculator = new AlchemyProfitCalculator();
}
if (PGE_CONFIG.universalProfit) {
window.MWIModules.universalCalculator = new UniversalActionProfitCalculator();
}
if (PGE_CONFIG.autoClaimMarketListings) {
window.MWIModules.autoClaimMarketListings = new AutoClaimMarketListingsManager();
}
// 添加全局样式
addGlobalButtonStyles();
// 设置游戏核心监控
setupGameCoreMonitor();
// 初始化脚本设置面板
initSettingsTabManager();
console.log('[PGE] Module initialization completed');
}
// ==================== 页面就绪检查 ====================
function checkPageReady() {
try {
if (!document.body) {
return false;
}
const avatar = document.querySelector('.Header_avatar__2RQgo');
const gameContainer = document.querySelector('.GamePage_gamePage__ixiPl');
if (avatar && gameContainer) {
console.log('[PGE] Page elements ready');
initializationState.pageReady = true;
checkAndInitializeModules();
return true;
}
return false;
} catch (error) {
console.error('[PGE] Error checking page ready:', error);
return false;
}
}
// ==================== 游戏状态检查 ====================
function checkGameStateReady() {
try {
if (!document.body) {
return false;
}
const gameCore = getGameCore();
if (gameCore) {
window.PGE.core = gameCore;
console.log('[PGE] Game core ready');
initializationState.gameStateReady = true;
checkAndInitializeModules();
return true;
}
return false;
} catch (error) {
console.error('[PGE] Error checking game state:', error);
return false;
}
}
// ==================== 模块初始化检查 ====================
function checkAndInitializeModules() {
if (initializationState.modulesInitialized) {
return;
}
if (!initializationState.wsConnected) {
console.log('[PGE] Waiting for WebSocket connection...');
return;
}
if (!initializationState.pageReady) {
console.log('[PGE] Waiting for page elements...');
return;
}
if (!initializationState.gameStateReady) {
console.log('[PGE] Waiting for game state...');
return;
}
console.log('[PGE] All conditions met, initializing modules...');
initializationState.modulesInitialized = true;
try {
initializeModules();
console.log('[PGE] Modules initialized successfully');
} catch (error) {
console.error('[PGE] Module initialization failed:', error);
initializationState.modulesInitialized = false;
}
}
// ==================== 页面监听器 ====================
async function setupPageMonitoring() {
try {
await DOMUtils.waitForDOMReady();
console.log('[PGE] DOM ready');
setTimeout(checkPageReady, 100);
DOMUtils.setupSafeObserver((mutations) => {
if (!initializationState.pageReady) {
checkPageReady();
}
if (!initializationState.gameStateReady) {
checkGameStateReady();
}
});
const gameStateInterval = setInterval(() => {
if (initializationState.gameStateReady) {
clearInterval(gameStateInterval);
return;
}
checkGameStateReady();
}, 1000);
setTimeout(() => {
if (!initializationState.modulesInitialized) {
console.log('[PGE] Timeout check - forcing initialization check');
checkPageReady();
checkGameStateReady();
checkAndInitializeModules();
}
}, 5000);
} catch (error) {
console.error('[PGE] Setup page monitoring failed:', error);
}
}
// ==================== 启动序列 ====================
function startInitializationSequence() {
console.log('[PGE] Starting initialization sequence...');
// 1. 立即设置WebSocket拦截(最高优先级)
setupWebSocketInterception();
// 2. 异步设置页面监听
setupPageMonitoring().catch(error => {
console.error('[PGE] Page monitoring setup failed:', error);
});
// 3. 初始化角色切换器
window.MWIModules.characterSwitcher = new CharacterSwitcher();
console.log('[PGE] Initialization sequence started');
}
// ==================== 初始化状态 ====================
const state = {
wsInstances: [],
currentWS: null,
requestHandlers: new Map(),
marketDataCache: new Map(),
baseDomain: 'data.pages.dev'
};
Object.assign(window, state);
// ==================== 启动 ====================
startInitializationSequence();
window.HackTimer = new HackTimer();
})();