Toolasha - Enhanced tools for Milky Way Idle.
// ==UserScript== // @name Toolasha // @namespace http://tampermonkey.net/ // @version 2.18.2 // @description Toolasha - Enhanced tools for Milky Way Idle. // @author Celasha and Claude, thank you to bot7420, DrDucky, Frotty, Truth_Light, AlphB, qu, and sentientmilk, for providing the basis for a lot of this. Thank you to Miku, Orvel, Jigglymoose, Incinarator, Knerd, and others for their time and help. Thank you to Steez for testing and helping me figure out where I'm wrong! Thank you to Tib for his generous contribution of the Character Cards. Thank you to Sapnas for -deeply- testing and singlehandedly help me improve performance. Special thanks to Zaeter for the name. // @license CC-BY-NC-SA-4.0 // @run-at document-start // @match https://www.milkywayidle.com/* // @match https://test.milkywayidle.com/* // @match https://shykai.github.io/MWICombatSimulatorTest/dist/* // @grant GM_addStyle // @grant GM.xmlHttpRequest // @grant GM_xmlhttpRequest // @grant GM_notification // @grant GM_getValue // @grant GM_setValue // @grant unsafeWindow // @require https://cdnjs.cloudflare.com/ajax/libs/mathjs/12.4.2/math.js // @require https://cdn.jsdelivr.net/npm/[email protected]/dist/chart.min.js // @require https://cdn.jsdelivr.net/npm/[email protected]/dist/chartjs-plugin-datalabels.min.js // @require https://cdn.jsdelivr.net/gh/Celasha/Toolasha@bf5b1c2b286d3ed25d3ff0b63bc234c4bd08f394/dist/libraries/toolasha-core.js // @require https://cdn.jsdelivr.net/gh/Celasha/Toolasha@bf5b1c2b286d3ed25d3ff0b63bc234c4bd08f394/dist/libraries/toolasha-utils.js // @require https://cdn.jsdelivr.net/gh/Celasha/Toolasha@bf5b1c2b286d3ed25d3ff0b63bc234c4bd08f394/dist/libraries/toolasha-market.js // @require https://cdn.jsdelivr.net/gh/Celasha/Toolasha@bf5b1c2b286d3ed25d3ff0b63bc234c4bd08f394/dist/libraries/toolasha-actions.js // @require https://cdn.jsdelivr.net/gh/Celasha/Toolasha@bf5b1c2b286d3ed25d3ff0b63bc234c4bd08f394/dist/libraries/toolasha-combat.js // @require https://cdn.jsdelivr.net/gh/Celasha/Toolasha@bf5b1c2b286d3ed25d3ff0b63bc234c4bd08f394/dist/libraries/toolasha-ui.js // ==/UserScript== // Note: Combat Sim auto-import requires Tampermonkey for cross-domain storage. Not available on Steam (use manual clipboard copy/paste instead). (function () { 'use strict'; window.Toolasha = window.Toolasha || {}; window.Toolasha.__buildTarget = "browser"; /** * Toolasha Entrypoint * Minimal bootstrap script that loads libraries and initializes features * * Libraries are loaded via @require in userscript header: * - Core (core modules, API) * - Utils (all utilities) * - Market (market, inventory, economy) * - Actions (production, gathering, alchemy) * - Combat (combat, stats, abilities) * - UI (tasks, skills, settings, misc) */ // Environment mismatch detection (function checkBuildEnvironment() { const buildTarget = window.Toolasha?.__buildTarget; // Check for GM_info specifically - the Steam polyfill provides GM but not GM_info const hasScriptManager = typeof GM_info !== 'undefined'; if (buildTarget === 'browser' && !hasScriptManager) { alert( 'Toolasha: Wrong build installed!\n\n' + 'You have the BROWSER build installed, but you are running on Steam.\n' + 'The browser build requires Tampermonkey and will not work on Steam.\n\n' + 'Please install the Steam build instead.' ); throw new Error('[Toolasha] Browser build cannot run on Steam. Install the Steam build.'); } if (buildTarget === 'steam' && hasScriptManager) { console.warn( '[Toolasha] Steam build detected in browser. ' + 'The Steam build is larger than necessary for browser use — consider switching to the browser build. ' + 'Continuing anyway.' ); } })(); // Access libraries from global namespace const Core = window.Toolasha.Core; const Utils = window.Toolasha.Utils; const Market = window.Toolasha.Market; const Actions = window.Toolasha.Actions; const Combat = window.Toolasha.Combat; const UI = window.Toolasha.UI; // Destructure core modules const { storage, config, webSocketHook, domObserver, dataManager, featureRegistry } = Core; const { setupScrollTooltipDismissal } = Utils.dom; /** * Detect if running on Combat Simulator page * @returns {boolean} True if on Combat Simulator */ function isCombatSimulatorPage() { const url = window.location.href; // Only work on test Combat Simulator for now return url.includes('shykai.github.io/MWICombatSimulatorTest/dist/'); } /** * Register all features from libraries into the feature registry */ function registerFeatures() { // Market Features const marketFeatures = [ { key: 'tooltipPrices', name: 'Tooltip Prices', category: 'Market', module: Market.tooltipPrices, async: true }, { key: 'expectedValueCalculator', name: 'Expected Value Calculator', category: 'Market', module: Market.expectedValueCalculator, async: true, }, { key: 'tooltipConsumables', name: 'Tooltip Consumables', category: 'Market', module: Market.tooltipConsumables, async: true, }, { key: 'dungeonTokenTooltips', name: 'Dungeon Token Tooltips', category: 'Inventory', module: Market.dungeonTokenTooltips, async: true, }, { key: 'marketFilter', name: 'Market Filter', category: 'Market', module: Market.marketFilter, async: false }, { key: 'marketSort', name: 'Market Sort', category: 'Market', module: Market.marketSort, async: false }, { key: 'autoFillPrice', name: 'Auto Fill Price', category: 'Market', module: Market.autoFillPrice, async: false, }, { key: 'autoClickMax', name: 'Auto Click Max', category: 'Market', module: Market.autoClickMax, async: false, }, { key: 'itemCountDisplay', name: 'Item Count Display', category: 'Market', module: Market.itemCountDisplay, async: false, }, { key: 'listingPriceDisplay', name: 'Listing Price Display', category: 'Market', module: Market.listingPriceDisplay, async: false, }, { key: 'estimatedListingAge', name: 'Estimated Listing Age', category: 'Market', module: Market.estimatedListingAge, async: false, }, { key: 'queueLengthEstimator', name: 'Queue Length Estimator', category: 'Market', module: Market.queueLengthEstimator, async: false, }, { key: 'marketOrderTotals', name: 'Market Order Totals', category: 'Market', module: Market.marketOrderTotals, async: false, }, { key: 'marketHistoryViewer', name: 'Market History Viewer', category: 'Market', module: Market.marketHistoryViewer, async: false, }, { key: 'philoCalculator', name: 'Philo Calculator', category: 'Market', module: Market.philoCalculator, async: false, }, { key: 'tradeHistory', name: 'Trade History', category: 'Market', module: Market.tradeHistory, async: false }, { key: 'tradeHistoryDisplay', name: 'Trade History Display', category: 'Market', module: Market.tradeHistoryDisplay, async: false, }, { key: 'networth', name: 'Net Worth', category: 'Economy', module: Market.networthFeature, async: false }, { key: 'inventoryBadgeManager', name: 'Inventory Badge Manager', category: 'Inventory', module: Market.inventoryBadgeManager, async: false, }, { key: 'inventorySort', name: 'Inventory Sort', category: 'Inventory', module: Market.inventorySort, async: false, }, { key: 'inventoryBadgePrices', name: 'Inventory Badge Prices', category: 'Inventory', module: Market.inventoryBadgePrices, async: false, }, { key: 'invCategoryTotals', name: 'Inventory Category Totals', category: 'Inventory', module: Market.inventoryCategoryTotals, async: false, }, { key: 'autoAllButton', name: 'Auto All Button', category: 'Inventory', module: Market.autoAllButton, async: false, }, { key: 'inventoryTabs', name: 'Custom Inventory Tabs', category: 'Inventory', module: Market.customTabsFeature, async: true, }, ]; // Actions Features const actionsFeatures = [ { key: 'actionTimeDisplay', name: 'Action Time Display', category: 'Actions', module: Actions.actionTimeDisplay, async: false, }, { key: 'quickInputButtons', name: 'Quick Input Buttons', category: 'Actions', module: Actions.quickInputButtons, async: false, }, { key: 'outputTotals', name: 'Output Totals', category: 'Actions', module: Actions.outputTotals, async: false }, { key: 'maxProduceable', name: 'Max Produceable', category: 'Actions', module: Actions.maxProduceable, async: false, }, { key: 'gatheringStats', name: 'Gathering Stats', category: 'Actions', module: Actions.gatheringStats, async: false, }, { key: 'requiredMaterials', name: 'Required Materials', category: 'Actions', module: Actions.requiredMaterials, async: false, }, { key: 'missingMaterialsButton', name: 'Missing Materials Button', category: 'Actions', module: Actions.missingMaterialsButton, async: false, }, { key: 'budgetCalculator', name: 'Budget Calculator', category: 'Actions', module: Actions.budgetCalculator, async: false, }, { key: 'alchemyProfitDisplay', name: 'Alchemy Profit Display', category: 'Alchemy', module: Actions.alchemyProfitDisplay, async: false, }, { key: 'teaRecommendation', name: 'Tea Recommendation', category: 'Actions', module: Actions.teaRecommendation, async: false, }, { key: 'lootLogStats', name: 'Loot Log Statistics', category: 'Actions', module: UI.lootLogStats, async: false, }, { key: 'inventoryCountDisplay', name: 'Inventory Count Display', category: 'Actions', module: Actions.inventoryCountDisplay, async: false, }, { key: 'pinnedActionsPage', name: 'Pinned Actions Page', category: 'Actions', module: Actions.pinnedActionsPage, async: false, }, ]; // Combat Features const combatFeatures = [ { key: 'abilityBookCalculator', name: 'Ability Book Calculator', category: 'Combat', module: Combat.abilityBookCalculator, async: false, }, { key: 'zoneIndices', name: 'Zone Indices', category: 'Combat', module: Combat.zoneIndices, async: false }, { key: 'combatScore', name: 'Combat Score', category: 'Profile', module: Combat.combatScore, async: false }, { key: 'characterCardButton', name: 'Character Card Button', category: 'Profile', module: Combat.characterCardButton, async: false, }, { key: 'loadoutEnhancementDisplay', name: 'Loadout Enhancement Display', category: 'Combat', module: Combat.loadoutEnhancementDisplay, async: false, }, { key: 'dungeonTracker', name: 'Dungeon Tracker', category: 'Combat', module: Combat.dungeonTracker, async: false, }, { key: 'dungeonTrackerUI', name: 'Dungeon Tracker UI', category: 'Combat', module: Combat.dungeonTrackerUI, async: false, }, { key: 'dungeonTrackerChatAnnotations', name: 'Dungeon Tracker Chat', category: 'Combat', module: Combat.dungeonTrackerChatAnnotations, async: false, }, { key: 'combatBattleCounter', name: 'Combat Battle Counter', category: 'Combat', module: Combat.combatBattleCounter, async: false, }, { key: 'combatSummary', name: 'Combat Summary', category: 'Combat', module: Combat.combatSummary, async: false, }, { key: 'combatStats', name: 'Combat Stats', category: 'Combat', module: Combat.combatStats, async: true }, { key: 'labyrinthTracker', name: 'Labyrinth Tracker', category: 'Combat', module: Combat.labyrinthTracker, async: false, }, { key: 'labyrinthBestLevel', name: 'Labyrinth Best Level', category: 'Combat', module: Combat.labyrinthBestLevel, async: false, }, { key: 'labyrinthShopPrices', name: 'Labyrinth Shop Prices', category: 'Combat', module: Combat.labyrinthShopPrices, async: false, }, { key: 'loadoutSort', name: 'Loadout Sort', category: 'Combat', module: Combat.loadoutSort, async: false, }, { key: 'loadoutSnapshot', name: 'Loadout Snapshots', category: 'Combat', module: Combat.loadoutSnapshot, async: false, }, { key: 'scrollSimulatorUI', name: 'Scroll Simulator UI', category: 'Combat', module: Combat.scrollSimulatorUI, async: false, }, { key: 'combatSim', name: 'Combat Simulator', category: 'Combat', module: Combat.combatSim, async: false, }, ]; // UI Features const uiFeatures = [ { key: 'equipmentLevelDisplay', name: 'Equipment Level Display', category: 'UI', module: UI.equipmentLevelDisplay, async: false, }, { key: 'alchemyItemDimming', name: 'Alchemy Item Dimming', category: 'UI', module: UI.alchemyItemDimming, async: false, }, { key: 'skillExperiencePercentage', name: 'Skill Experience Percentage', category: 'UI', module: UI.skillExperiencePercentage, async: false, }, { key: 'externalLinks', name: 'External Links', category: 'UI', module: UI.externalLinks, async: false }, { key: 'hideLabyrinthBadge', name: 'Hide Labyrinth Badge', category: 'UI', module: UI.hideLabyrinthBadge, async: false, }, { key: 'altClickNavigation', name: 'Alt+Click Navigation', category: 'Navigation', module: UI.altClickNavigation, async: false, }, { key: 'collectionNavigation', name: 'Collection Navigation', category: 'Navigation', module: UI.collectionNavigation, async: false, }, { key: 'collectionFilters', name: 'Collection Filters', category: 'Collection', module: UI.collectionFilters, async: true, }, { key: 'chatCommands', name: 'Chat Commands', category: 'Chat', module: UI.chatCommands, async: true }, { key: 'mentionTracker', name: 'Mention Tracker', category: 'Chat', module: UI.mentionTracker, async: true }, { key: 'popOutChat', name: 'Pop-Out Chat', category: 'Chat', module: UI.popOutChat, async: true }, { key: 'chatBlockList', name: 'Chat Block List', category: 'Chat', module: UI.chatBlockList, async: false }, { key: 'chatHistoryExtender', name: 'Chat History Extender', category: 'Chat', module: UI.chatHistoryExtender, async: false, }, { key: 'taskProfitDisplay', name: 'Task Profit Display', category: 'Tasks', module: UI.taskProfitDisplay, async: false, }, { key: 'taskRerollTracker', name: 'Task Reroll Tracker', category: 'Tasks', module: UI.taskRerollTracker, async: false, }, { key: 'taskSorter', name: 'Task Sorter', category: 'Tasks', module: UI.taskSorter, async: false }, { key: 'taskIcons', name: 'Task Icons', category: 'Tasks', module: UI.taskIcons, async: false }, { key: 'taskInventoryHighlighter', name: 'Task Inventory Highlighter', category: 'Tasks', module: UI.taskInventoryHighlighter, async: false, }, { key: 'taskStatistics', name: 'Task Statistics', category: 'Tasks', module: UI.taskStatistics, async: false, }, { key: 'taskClaimCollector', name: 'Task Claim Collector', category: 'Tasks', module: UI.taskClaimCollector, async: false, }, { key: 'skillRemainingXP', name: 'Remaining XP', category: 'Skills', module: UI.remainingXP, async: false }, { key: 'xpTracker', name: 'XP/hr Tracker', category: 'Skills', module: UI.xpTracker, async: false }, { key: 'housePanelObserver', name: 'House Panel Observer', category: 'House', module: UI.housePanelObserver, async: false, }, { key: 'transmuteRates', name: 'Transmute Rates', category: 'Dictionary', module: UI.transmuteRates, async: false, }, { key: 'alchemy_transmuteHistory', name: 'Transmute History Tracker', category: 'Alchemy', module: UI.transmuteHistoryTracker, async: false, }, { key: 'alchemy_transmuteHistoryViewer', name: 'Transmute History Viewer', category: 'Alchemy', module: UI.transmuteHistoryViewer, async: false, }, { key: 'alchemy_coinifyHistory', name: 'Coinify History Tracker', category: 'Alchemy', module: UI.coinifyHistoryTracker, async: false, }, { key: 'alchemy_coinifyHistoryViewer', name: 'Coinify History Viewer', category: 'Alchemy', module: UI.coinifyHistoryViewer, async: false, }, { key: 'enhancementFeature', name: 'Enhancement Tracker', category: 'Enhancement', module: UI.enhancementFeature, async: false, }, { key: 'enhancementXPH', name: 'Enhancement XPH Calculator', category: 'Enhancement', module: UI.xphCalculator, async: false, }, { key: 'guildXPTracker', name: 'Guild XP Tracker', category: 'Guild', module: UI.guildXPTracker, async: false, }, { key: 'guildXPDisplay', name: 'Guild XP Display', category: 'Guild', module: UI.guildXPDisplay, async: false, }, { key: 'emptyQueueNotification', name: 'Empty Queue Notification', category: 'Notifications', module: UI.emptyQueueNotification, async: false, }, ]; // Combine all features const allFeatures = [...marketFeatures, ...actionsFeatures, ...combatFeatures, ...uiFeatures]; // Convert to feature registry format const features = allFeatures.map((feature) => ({ key: feature.key, name: feature.name, category: feature.category, initialize: () => feature.module.initialize(), disable: typeof feature.module.disable === 'function' ? () => feature.module.disable() : undefined, async: feature.async, })); // Replace feature registry's features array featureRegistry.replaceFeatures(features); } if (isCombatSimulatorPage()) { // Initialize combat sim integration only Combat.combatSimIntegration.initialize(); // Skip all other initialization } else { // CRITICAL: Install WebSocket hook FIRST, before game connects webSocketHook.install(); // CRITICAL: Start centralized DOM observer SECOND, before features initialize domObserver.start(); // Set up scroll listener to dismiss stuck tooltips setupScrollTooltipDismissal(); // Initialize network alert (must be early, before market features) Market.networkAlert.initialize(); // Start capturing client data from localStorage (for Combat Sim export) webSocketHook.captureClientDataFromLocalStorage(); // Register all features from libraries registerFeatures(); // Initialize action panel observer (special case - not a regular feature) Actions.initActionPanelObserver(); // Initialize storage and config THIRD (async) // Store the promise so character_initialized can wait for storage readiness const storageReady = (async () => { try { // Initialize storage (opens IndexedDB) await storage.initialize(); // Initialize config (loads settings from storage) await config.initialize(); // Add beforeunload handler to flush all pending writes window.addEventListener('beforeunload', () => { storage.flushAll(); }); // Initialize Data Manager immediately // Don't wait for localStorageUtil - it handles missing data gracefully dataManager.initialize(); } catch (error) { console.error('[Toolasha] Storage/config initialization failed:', error); // Initialize anyway dataManager.initialize(); } })(); // Setup character switch handler once (NOT inside character_initialized listener) featureRegistry.setupCharacterSwitchHandler(); dataManager.on('character_initialized', (_data) => { // Skip full initialization during character switches // The character_switched handler in feature-registry already handles reinitialization if (_data._isCharacterSwitch) { return; } // Initialize all features using the feature registry setTimeout(async () => { try { // Ensure storage/config are initialized before loading character settings // On Steam, character data can arrive before IndexedDB is open await storageReady; // Reload config settings with character-specific data await config.loadSettings(); config.applyColorSettings(); // Initialize scroll simulator storage (character-specific) await Combat.scrollSimulator.initialize().catch((error) => { console.error('[Toolasha] Scroll simulator initialization failed:', error); }); // Initialize Settings UI after character data is loaded await UI.settingsUI.initialize().catch((error) => { console.error('[Toolasha] Settings UI initialization failed:', error); }); await featureRegistry.initializeFeatures(); // Health check after initialization setTimeout(async () => { const failedFeatures = featureRegistry.checkFeatureHealth(); // Note: Settings tab health check removed - tab only appears when user opens settings panel if (failedFeatures.length > 0) { console.warn( '[Toolasha] Health check found failed features:', failedFeatures.map((f) => f.name) ); setTimeout(async () => { await featureRegistry.retryFailedFeatures(failedFeatures); // Final health check const stillFailed = featureRegistry.checkFeatureHealth(); if (stillFailed.length > 0) { console.warn( '[Toolasha] These features could not initialize:', stillFailed.map((f) => f.name) ); console.warn( '[Toolasha] Try refreshing the page or reopening the relevant game panels' ); } }, 1000); } }, 500); // Wait 500ms after initialization to check health } catch (error) { console.error('[Toolasha] Feature initialization failed:', error); } }, 100); }); // Expose minimal user-facing API const targetWindow = typeof unsafeWindow !== 'undefined' ? unsafeWindow : window; targetWindow.Toolasha.version = '0.14.3'; // Feature toggle API (for users to manage settings via console) targetWindow.Toolasha.features = { list: () => config.getFeaturesByCategory(), enable: (key) => config.setFeatureEnabled(key, true), disable: (key) => config.setFeatureEnabled(key, false), toggle: (key) => config.toggleFeature(key), status: (key) => config.isFeatureEnabled(key), info: (key) => config.getFeatureInfo(key), }; // Guild XP data management targetWindow.Toolasha.guild = { resetMemberXP: () => UI.guildXPTracker.resetMemberData(), }; } })();