您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
计算生产、强化、房屋所需材料并一键购买;显示今日资产增量,统计30天总资产生成走势图;计算生产与炼金实时利润;按照目标材料数量进行采集;快速切换角色;自动收集市场订单;功能支持自定义开关。
// ==UserScript== // @name [银河奶牛] 生产采集增强 / MWI Production & Gathering Enhanced // @name:zh-CN [银河奶牛]生产采集增强 // @name:en MWI Production & Gathering Enhanced // @namespace http://tampermonkey.net/ // @version 3.6.7 // @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.7", // 当前版本 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://github.com/CYR2077/MWI-Production-Gathering-Enhanced/releases/download/release-v3.6.7/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.7', 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.d4d08849.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(); })();