// ==UserScript== // @name [MWI] Ultimate Enhancement Tracker v3.5.0 // @namespace http://tampermonkey.net/ // @version 3.5.1 // @description Now with XP and XP/hr tracking // @author Nex (Base tracker by Truth_Light, enhancelator by Dohnuts) // @match https://www.milkywayidle.com/* // @match https://test.milkywayidle.com/* // @require https://cdnjs.cloudflare.com/ajax/libs/mathjs/10.6.4/math.min.js // @license MIT // @grant GM_registerMenuCommand // @icon https://www.google.com/s2/favicons?sz=64&domain=milkywayidle.com // @downloadURL none // ==/UserScript== /* Market price fetching relies on having MWI Tools installed, get it here: https://greasyfork.org/en/scripts/494467-mwitools 获取市场价格取决于是否安装了 MWI 工具,在此获取: https://greasyfork.cc/zh-CN/scripts/494467-mwitools MilkyWayIdle Steam game client players should also install this script: https://raw.githubusercontent.com/YangLeda/Userscripts-For-MilkyWayIdle/refs/heads/main/MWITools%20addon%20for%20Steam%20version.js (Web browser players do NOT need to install the above script.) */ (function() { 'use strict'; // ====================== // STYLE CONFIGURATION // ====================== const STYLE = { colors: { primary: '#00ffe7', // Bright neon cyan background: 'rgba(5, 5, 15, 0.95)', // Deep black-blue border: '1px solid rgba(0, 255, 234, 0.4)', // Glowing border textPrimary: '#e0f7ff', // Soft neon white-blue textSecondary: '#9b9bff', // Neon purple tint accent: '#ff00d4', // Vibrant pink-magenta danger: '#ff0055', // Electric red success: '#00ff99', // Neon green headerBg: 'rgba(15, 5, 35, 0.7)', // Dark purple gradient tint epic: '#c63dff', // Deep neon purple legendary: '#ff6f1a', // Bright orange neon mythic: '#ff0033', // Intense mythic red blessed: 'linear-gradient(135deg, #ff00d4, #c63dff, #00ffe7)', // Trippy neon gradient gold: '#FFD700' // Gold color for cost display }, fonts: { primary: '14px "Orbitron", "Segoe UI", Roboto, sans-serif', // Futuristic font secondary: '12px "Orbitron", "Segoe UI", Roboto, sans-serif', header: 'bold 16px "Orbitron", "Segoe UI", Roboto, sans-serif', milestone: 'bold 18px "Orbitron", "Segoe UI", Roboto, sans-serif', cost: 'bold 13px "Orbitron", "Segoe UI", Roboto, sans-serif' }, shadows: { panel: '0 0 20px rgba(0, 255, 234, 0.3)', // Neon glow panel notification: '0 0 15px rgba(255, 0, 212, 0.3)', // Pink neon soft glow milestone: '0 0 25px rgba(198, 61, 255, 0.4)', // Epic purple glow text: '0 0 6px rgba(0, 255, 234, 0.3)', // Soft text glow gold: '0 0 8px rgba(255, 215, 0, 0.7)' // Gold glow }, borderRadius: { medium: '14px', // Rounded with a modern edge small: '8px' }, transitions: { fast: 'all 0.15s ease-in-out', medium: 'all 0.3s ease-in-out', slow: 'all 0.5s ease-in-out' }, }; STYLE.scrollable = { maxHeight: '50vh', // Maximum height before scrolling kicks in overflowY: 'auto', scrollbarWidth: 'thin', scrollbarColor: `${STYLE.colors.primary} transparent` }; // Core Variables const userLanguage = localStorage.getItem('i18nextLng'); // Load enhancementData from localStorage if available let enhancementData = JSON.parse(localStorage.getItem('enhancementData')) || {}; let currentTrackingIndex = parseInt(localStorage.getItem('enhancementCurrentTrackingIndex')) || 0; let currentViewingIndex = parseInt(localStorage.getItem('enhancementCurrentViewingIndex')) || 0; // Add session validation function validateSession(session) { if (!session) return; // Ensure all required fields exist if (!session["硬币消耗"]) session["硬币消耗"] = { count: 0, totalCost: 0 }; if (!session["总成本"]) session["总成本"] = 0; // Reconstruct total from components if needed const calculatedTotal = Object.values(session["材料消耗"] || {}).reduce((sum, m) => sum + (m.totalCost || 0), 0) + (session["硬币消耗"]?.totalCost || 0) + (session["其他数据"]["保护总成本"] || 0); if (Math.abs(calculatedTotal - session["总成本"]) > 1) { console.log(`Correcting total cost drift: ${session["总成本"]} → ${calculatedTotal}`); session["总成本"] = calculatedTotal; } } // Validate all sessions on load Object.values(enhancementData).forEach(validateSession); // Add data migration for existing sessions Object.values(enhancementData).forEach(session => { if (!session["硬币消耗"]) { session["硬币消耗"] = { count: 0, totalCost: 0 }; } // Recalculate total cost if needed if (session["总成本"] === undefined) { const materialsCost = Object.values(session["材料消耗"] || {}).reduce((sum, m) => sum + m.totalCost, 0); const coinsCost = session["硬币消耗"]?.totalCost || 0; const protectionCost = session["其他数据"]["保护总成本"] || 0; session["总成本"] = materialsCost + coinsCost + protectionCost; } }); cleanSessionIndices(); // Function to save enhancementData to localStorage function saveEnhancementData() { // Ensure all sessions have the correct structure Object.values(enhancementData).forEach(session => { if (!session["硬币消耗"]) session["硬币消耗"] = { count: 0, totalCost: 0 }; }); localStorage.setItem('enhancementData', JSON.stringify(enhancementData)); localStorage.setItem('enhancementCurrentTrackingIndex', currentTrackingIndex); localStorage.setItem('enhancementCurrentViewingIndex', currentViewingIndex); } function clearAllSessions() { if (confirm(isZH ? "确定要清除所有强化会话数据吗?此操作不可撤销。" : "Are you sure you want to clear all enhancement sessions? This cannot be undone.")) { enhancementData = {}; currentTrackingIndex = 0; currentViewingIndex = 0; localStorage.removeItem('enhancementData'); debouncedUpdateFloatingUI(); showUINotification(isZH ? "已清除所有会话数据" : "All session data cleared"); } } currentViewingIndex = currentTrackingIndex; // Start viewing current session let item_name_to_hrid; let item_hrid_to_name; var isZH = userLanguage.startsWith("zh"); // Add this near your language detection let lastLangCheck = userLanguage; setInterval(() => { const currentLang = localStorage.getItem('i18nextLng'); if (currentLang !== lastLangCheck) { lastLangCheck = currentLang; isZH = currentLang.startsWith("zh"); // Force refresh all displayed names Object.keys(enhancementData).forEach(key => { const session = enhancementData[key]; if (session["其他数据"]) { session["其他数据"]["物品名称"] = translateItemName( session["其他数据"]["物品HRID"], session["其他数据"]["物品名称"] ); } }); debouncedUpdateFloatingUI(); saveEnhancementData(); // Save after updating names } }, 1000); cleanSessionIndices(); function cleanSessionIndices() { const sortedIndices = Object.keys(enhancementData) .map(Number) .sort((a, b) => a - b); if (sortedIndices.some((v, i) => v !== i + 1)) { const newData = {}; sortedIndices.forEach((oldIndex, i) => { newData[i + 1] = enhancementData[oldIndex]; }); enhancementData = newData; currentTrackingIndex = sortedIndices.length > 0 ? Math.max(...Object.keys(newData).map(Number)) : 1; currentViewingIndex = currentTrackingIndex; saveEnhancementData(); } // Ensure loaded indices are valid if (!enhancementData[currentViewingIndex]) { currentViewingIndex = currentTrackingIndex; } if (!enhancementData[currentTrackingIndex]) { currentTrackingIndex = sortedIndices.length > 0 ? Math.max(...sortedIndices) : 0; } } // ====================== // MARKET DATA HANDLING // ====================== let MWITools_marketAPI_json = null; let lastMarketUpdate = 0; const MARKET_REFRESH_INTERVAL = 30000; // 30 seconds function loadMarketData() { try { // Try to load from localStorage const marketDataStr = localStorage.getItem('MWITools_marketAPI_json'); if (marketDataStr) { MWITools_marketAPI_json = JSON.parse(marketDataStr); lastMarketUpdate = MWITools_marketAPI_json?.time || 0; // console.log('[Market] Loaded market data from storage', lastMarketUpdate); } // Set up periodic refresh setInterval(refreshMarketData, MARKET_REFRESH_INTERVAL); // Initial refresh refreshMarketData(); } catch (e) { console.error('[Market] Error loading market data:', e); } } function refreshMarketData() { try { // Check if we have the market data object in memory if (typeof window.MWITools_marketAPI_json !== 'undefined') { MWITools_marketAPI_json = window.MWITools_marketAPI_json; lastMarketUpdate = MWITools_marketAPI_json?.time || 0; localStorage.setItem('MWITools_marketAPI_json', JSON.stringify(MWITools_marketAPI_json)); // console.log('[Market] Updated market data from memory', lastMarketUpdate); } } catch (e) { console.error('[Market] Error refreshing market data:', e); } } function getMarketPrice(itemHRID) { try { // Special case for coins - always worth 1 gold if (itemHRID === '/items/coin' || itemHRID === '/items/coins') { return 1; // Coins are tracked separately } // Validate input if (!itemHRID || typeof itemHRID !== 'string') { console.log('[Market] Invalid item HRID:', itemHRID); return 0; } // Refresh market data if stale if (Date.now() - lastMarketUpdate > MARKET_REFRESH_INTERVAL) { refreshMarketData(); } // Check if market data is available if (!MWITools_marketAPI_json?.market) { console.log('[Market] No market data available'); return 0; } // Try to get the display name (either from dictionary or HRID) let itemName = item_hrid_to_name?.[itemHRID]; if (!itemName) { // Extract from HRID format: /items/aqua_essence → Aqua Essence const parts = itemHRID.split('/'); const lastPart = parts[parts.length - 1] || ''; itemName = lastPart .replace(/_/g, ' ') .replace(/(^|\s)\S/g, t => t.toUpperCase()) .trim(); } // Return 0 if we still don't have a name if (!itemName) { console.log('[Market] Could not determine item name for:', itemHRID); return 0; } // Find market entry (case insensitive) const marketEntry = Object.entries(MWITools_marketAPI_json.market).find( ([name]) => name.toLowerCase() === itemName.toLowerCase() ); if (!marketEntry) { console.log('[Market] No entry for:', itemName); return 0; } // Use ask price if available, otherwise bid price const price = marketEntry[1]?.ask || marketEntry[1]?.bid || 0; // console.log('[Market] Price for', itemName, ':', price); return price; } catch (e) { console.error("[Market] Error getting price for", itemHRID, ":", e); return 0; } } // ====================== // XP GAIN TRACKING // ====================== function getBaseItemLevel(itemHRID) { try { // Get initClientData from localStorage const initClientData = JSON.parse(localStorage.getItem('initClientData')); if (!initClientData?.itemDetailMap) return 0; // Find the item in the detail map const itemDetails = initClientData.itemDetailMap[itemHRID]; if (!itemDetails) return 0; // Return the item level if available return itemDetails.itemLevel || 0; } catch (e) { console.error("Error getting base item level:", e); return 0; } } function getWisdomBuff() { try { const charData = JSON.parse(localStorage.getItem('init_character_data')); if (!charData) return { flatBoost: 0 }; let totalFlatBoost = 0; // 1. Community Buffs const communityEnhancingBuffs = charData.communityActionTypeBuffsMap?.["/action_types/enhancing"]; if (Array.isArray(communityEnhancingBuffs)) { communityEnhancingBuffs.forEach(buff => { if (buff.typeHrid === "/buff_types/wisdom") { totalFlatBoost += buff.flatBoost || 0; } }); } // 2. Equipment Buffs const equipmentEnhancingBuffs = charData.equipmentActionTypeBuffsMap?.["/action_types/enhancing"]; if (Array.isArray(equipmentEnhancingBuffs)) { equipmentEnhancingBuffs.forEach(buff => { if (buff.typeHrid === "/buff_types/wisdom") { totalFlatBoost += buff.flatBoost || 0; } }); } // 3. House Buffs const houseEnhancingBuffs = charData.houseActionTypeBuffsMap?.["/action_types/enhancing"]; if (Array.isArray(houseEnhancingBuffs)) { houseEnhancingBuffs.forEach(buff => { if (buff.typeHrid === "/buff_types/wisdom") { totalFlatBoost += buff.flatBoost || 0; } }); } // 4. Consumable Buffs (NEW - from wisdom tea etc) const consumableEnhancingBuffs = charData.consumableActionTypeBuffsMap?.["/action_types/enhancing"]; if (Array.isArray(consumableEnhancingBuffs)) { consumableEnhancingBuffs.forEach(buff => { if (buff.typeHrid === "/buff_types/wisdom") { totalFlatBoost += buff.flatBoost || 0; } }); } return { flatBoost: totalFlatBoost } } catch (e) { console.error("Error calculating wisdom buff:", e); return { flatBoost: 0 }; } } function calculateSuccessXP(previousLevel, itemHrid) { const baseLevel = getBaseItemLevel(itemHrid); const wisdomBuff = getWisdomBuff(); // Special handling for enhancement level 0 (base items) const enhancementMultiplier = previousLevel === 0 ? 1.0 // Base value for unenhanced items : (previousLevel + 1); // Normal progression return Math.floor( 1.4 * (1 + (wisdomBuff?.flatBoost || 0)) * enhancementMultiplier * (10 + baseLevel) ); } function calculateFailureXP(previousLevel, itemHrid) { return Math.floor(calculateSuccessXP(previousLevel, itemHrid) * 0.1); // 10% of success XP } // ====================== // ENHANCEMENT COST TRACKING (updated to use new market functions) // ====================== function getEnhancementMaterials(itemHRID) { try { const initData = JSON.parse(localStorage.getItem('initClientData')); const itemData = initData.itemDetailMap?.[itemHRID]; if (!itemData) { console.log('[Materials] Item not found:', itemHRID); return null; } // Get the costs array (try different possible property names) const costs = itemData.enhancementCosts || itemData.upgradeCosts || itemData.enhanceCosts || itemData.enhanceCostArray; if (!costs) { console.log('[Materials] No enhancement costs found for:', itemHRID); return null; } // Convert to our desired format let materials = []; // Case 1: Array of objects (current format) if (Array.isArray(costs) && costs.length > 0 && typeof costs[0] === 'object') { materials = costs.map(cost => [cost.itemHrid, cost.count]); } // Case 2: Already in correct format [["/items/foo", 30], ["/items/bar", 20]] else if (Array.isArray(costs) && costs.length > 0 && Array.isArray(costs[0])) { materials = costs; } // Case 3: Simple array ["/items/foo", 30] else if (Array.isArray(costs) && costs.length === 2 && typeof costs[0] === 'string') { materials = [costs]; } // Case 4: Object format {"/items/foo": 30, "/items/bar": 20} else if (typeof costs === 'object' && !Array.isArray(costs)) { materials = Object.entries(costs); } // Filter out any invalid entries materials = materials.filter(m => Array.isArray(m) && m.length === 2 && typeof m[0] === 'string' && typeof m[1] === 'number' ); // console.log('[Materials] Processed costs:', materials); return materials.length > 0 ? materials : null; } catch (e) { console.error('[Materials] Error:', e); return null; } } // Updated trackMaterialCosts(): function trackMaterialCosts(itemHRID) { const session = enhancementData[currentTrackingIndex]; const materials = getEnhancementMaterials(itemHRID) || []; let materialCost = 0; let coinCost = 0; materials.forEach(([hrid, count]) => { if (hrid.includes('/items/coin')) { // Track coins for THIS ATTEMPT ONLY coinCost = count; // Coins are 1:1 value (1 coin = 1 gold) session["硬币消耗"].count += count; session["硬币消耗"].totalCost += count; return; } const cost = getMarketPrice(hrid) * count; materialCost += cost; if (!session["材料消耗"][hrid]) { session["材料消耗"][hrid] = { name: item_hrid_to_name[hrid] || hrid, count: 0, totalCost: 0 }; } session["材料消耗"][hrid].count += count; session["材料消耗"][hrid].totalCost += cost; }); return { materialCost, coinCost }; // Return both values } // ====================== // NOTIFICATION SYSTEM // ====================== function createNotificationContainer(headerElement) { const container = document.createElement("div"); container.id = "enhancementNotificationContainer"; Object.assign(container.style, { position: "absolute", top: "calc(100% + 5px)", left: "50%", transform: "translateX(-50%)", zIndex: "9999", display: "flex", flexDirection: "column", gap: "5px", width: "220px", pointerEvents: "none" }); if (getComputedStyle(headerElement).position === "static") { headerElement.style.position = "relative"; } headerElement.appendChild(container); return container; } function showNotification(message, type, level, isBlessed = false) { const headerElement = document.querySelector('[class^="Header_myActions"]'); if (!headerElement) return; let container = document.getElementById("enhancementNotificationContainer"); if (!container) { container = createNotificationContainer(headerElement); } if (type === "success") { createStandardNotification(container, level, isBlessed); if (level >= 10) { setTimeout(() => { createMilestoneNotification(container, level); }, 300); } } else { createFailureNotification(container); } } function createStandardNotification(container, level, isBlessed) { const notification = document.createElement("div"); notification.className = "enhancement-notification standard"; Object.assign(notification.style, { padding: "10px 15px", borderRadius: STYLE.borderRadius.small, color: "white", fontWeight: "bold", boxShadow: STYLE.shadows.notification, transform: "translateY(-20px)", opacity: "0", transition: STYLE.transitions.medium, textAlign: "center", marginBottom: "5px", position: "relative", overflow: "hidden", pointerEvents: "auto", textShadow: STYLE.shadows.text }); if (isBlessed) { Object.assign(notification.style, { background: STYLE.colors.blessed, color: "#8B4513" }); notification.textContent = isZH ? `祝福强化! +${level}` : `BLESSED! +${level}`; addHolyEffects(notification); } else { notification.style.background = getLevelGradient(level); notification.textContent = isZH ? `强化成功 +${level}` : `Success +${level}`; } animateNotification(notification, container, level, isBlessed); } function createMilestoneNotification(container, level) { const milestone = document.createElement("div"); milestone.className = "enhancement-notification milestone"; Object.assign(milestone.style, { padding: "12px 15px", borderRadius: STYLE.borderRadius.small, color: "white", fontWeight: "bolder", boxShadow: STYLE.shadows.milestone, transform: "translateY(-20px)", opacity: "0", transition: STYLE.transitions.medium, textAlign: "center", marginBottom: "5px", animation: "pulse 2s infinite", position: "relative", overflow: "hidden", pointerEvents: "auto", textShadow: STYLE.shadows.text, font: STYLE.fonts.milestone }); if (level >= 20) { milestone.style.background = ` linear-gradient( 135deg, ${STYLE.colors.mythic}, #ff0044, #ff2200, #ff0055 ) `; milestone.style.backgroundSize = "400% 400%"; milestone.style.animation = "mythicPulse 2.5s ease-in-out infinite"; milestone.style.color = "#fff"; milestone.style.fontWeight = "bold"; milestone.style.textShadow = ` 0 0 4px #ff0044, 0 0 8px #ff0044, 0 0 12px #ff0044 `; milestone.textContent = isZH ? "命中注定!" : "IT. IS. DESTINY."; } else if (level >= 15) { milestone.style.background = `linear-gradient(135deg, ${STYLE.colors.legendary}, #FF6600)`; milestone.style.textShadow = "0 0 8px rgba(255, 102, 0, 0.8)"; milestone.textContent = isZH ? "奇迹发生!" : "IT'S A MIRACLE!"; } else if (level >= 10) { milestone.style.background = `linear-gradient(135deg, ${STYLE.colors.epic}, #8000FF)`; milestone.style.textShadow = "0 0 8px rgba(153, 51, 255, 0.8)"; milestone.textContent = isZH ? "运气不错!" : "NICE LUCK!"; } animateNotification(milestone, container, level, false); } function createFailureNotification(container) { const notification = document.createElement("div"); notification.className = "enhancement-notification failure"; Object.assign(notification.style, { padding: "10px 15px", borderRadius: STYLE.borderRadius.small, color: "white", fontWeight: "bold", boxShadow: STYLE.shadows.notification, transform: "translateY(-20px)", opacity: "0", transition: STYLE.transitions.medium, textAlign: "center", marginBottom: "5px", position: "relative", overflow: "hidden", pointerEvents: "auto", textShadow: STYLE.shadows.text, backgroundColor: STYLE.colors.danger, borderTop: "3px solid #C62828" }); notification.textContent = isZH ? "强化失败!" : "Failed!"; animateNotification(notification, container, 0, false); } function animateNotification(notification, container, level, isBlessed) { container.appendChild(notification); setTimeout(() => { notification.style.transform = "translateY(0)"; notification.style.opacity = "1"; }, 10); setTimeout(() => { notification.style.transform = "translateY(20px)"; notification.style.opacity = "0"; setTimeout(() => notification.remove(), 300); }, getNotificationDuration(level, isBlessed)); notification.addEventListener("click", () => { notification.style.transform = "translateY(20px)"; notification.style.opacity = "0"; setTimeout(() => notification.remove(), 300); }); } function addHolyEffects(notification) { const rays = document.createElement("div"); rays.className = "holy-rays"; Object.assign(rays.style, { position: "absolute", top: "0", left: "0", width: "100%", height: "100%", background: "radial-gradient(circle, transparent 20%, rgba(255,215,0,0.3) 70%)", pointerEvents: "none", animation: "raysRotate 4s linear infinite" }); notification.appendChild(rays); for (let i = 0; i < 4; i++) { const cross = document.createElement("div"); cross.textContent = "✝"; Object.assign(cross.style, { position: "absolute", fontSize: "16px", animation: `floatUp ${3 + Math.random() * 2}s linear infinite`, opacity: "0.7", left: `${10 + (i * 25)}%`, top: "100%" }); notification.appendChild(cross); } } function getLevelGradient(level) { if (level >= 20) { return `linear-gradient(135deg, ${STYLE.colors.mythic}, #FF0000)`; } else if (level >= 15) { return `linear-gradient(135deg, ${STYLE.colors.legendary}, #FF6600)`; } else if (level >= 10) { return `linear-gradient(135deg, ${STYLE.colors.epic}, #8000FF)`; } else if (level >= 5) { return "linear-gradient(135deg, #3399FF, #0066FF)"; } else if (level >= 1) { return "linear-gradient(135deg, #33CC33, #009900)"; } else { return "linear-gradient(135deg, #CCCCCC, #999999)"; } } function getNotificationDuration(level, isBlessed) { if (isBlessed) return 5000; if (level >= 20) return 5000; if (level >= 15) return 4000; if (level >= 10) return 3000; return 2500; } // ====================== // ENHANCEMENT TRACKING // ====================== function hookWS() { const dataProperty = Object.getOwnPropertyDescriptor(MessageEvent.prototype, "data"); const oriGet = dataProperty.get; dataProperty.get = hookedGet; Object.defineProperty(MessageEvent.prototype, "data", dataProperty); function hookedGet() { const socket = this.currentTarget; if (!(socket instanceof WebSocket)) return oriGet.call(this); if (!socket.url.includes("api.milkywayidle.com/ws") && !socket.url.includes("api-test.milkywayidle.com/ws")) { return oriGet.call(this); } const message = oriGet.call(this); Object.defineProperty(this, "data", { value: message }); return handleMessage(message); } } function handleMessage(message) { try { const obj = JSON.parse(message); if (!obj) return message; if (obj.type === "init_character_data") { handleInitData(); } else if (obj.type === "action_completed" && obj.endCharacterAction?.actionHrid === "/actions/enhancing/enhance") { handleEnhancement(obj.endCharacterAction); } } catch (error) { console.error("Error processing message:", error); } return message; } function handleInitData() { const initClientData = localStorage.getItem('initClientData'); if (!initClientData) { setTimeout(handleInitData, 500); // Retry if not ready return; } try { const data = JSON.parse(initClientData); item_hrid_to_name = {}; // Properly map all item names for (const [hrid, details] of Object.entries(data.itemDetailMap)) { item_hrid_to_name[hrid] = details.name; } console.log("Translations loaded:", item_hrid_to_name); } catch (error) { console.error('Data parsing failed:', error); } } function parseItemHash(primaryItemHash) { try { // Handle different possible formats: // 1. "/item_locations/inventory::/items/enhancers_bottoms::0" (level 0) // 2. "161296::/item_locations/inventory::/items/enhancers_bottoms::5" (level 5) // 3. Direct HRID like "/items/enhancers_bottoms" (no level) let itemHrid = null; let enhancementLevel = 0; // Default to 0 if not specified // Split by :: to parse components const parts = primaryItemHash.split('::'); // Find the part that starts with /items/ const itemPart = parts.find(part => part.startsWith('/items/')); if (itemPart) { itemHrid = itemPart; } // If no /items/ found but it's a direct HRID else if (primaryItemHash.startsWith('/items/')) { itemHrid = primaryItemHash; } // Try to extract enhancement level (last part after ::) const lastPart = parts[parts.length - 1]; if (lastPart && !isNaN(lastPart)) { enhancementLevel = parseInt(lastPart, 10); } if (!itemHrid) { console.log('[Parse] Could not find item HRID in:', primaryItemHash); return { hrid: null, level: 0 }; } return { hrid: itemHrid, level: enhancementLevel }; } catch (e) { console.error('[Parse] Error parsing primaryItemHash:', e); return { hrid: null, level: 0 }; } } function calculateSessionDuration(session) { if (!session["会话数据"]?.开始时间) return 0; const now = Date.now(); return now - session["会话数据"].开始时间; } function calculateXpPerHour(session) { // Don't calculate for completed sessions if (session["强化状态"] === "已完成") { return session["会话数据"]?.["每小时经验"] || 0; } const durationMs = calculateSessionDuration(session); const totalXP = session["会话数据"]?.["总经验"] || 0; // Don't show rate until we have meaningful data if (durationMs < 60000 || totalXP < 1000) { // First minute or <1k XP return 0; // Or return null and hide the display } // Weighted average that becomes more accurate over time const weight = Math.min(1, session["尝试历史"]?.length / 10); // 0-1 based on 10 attempts const rawRate = (totalXP / (durationMs / (1000 * 60 * 60))); // For first few attempts, blend with a conservative estimate if (session["尝试历史"]?.length < 5) { const baseRate = 50000; // Conservative base rate (adjust per your game) return Math.floor((rawRate * weight) + (baseRate * (1 - weight))); } return Math.floor(rawRate); } function formatDuration(ms) { const seconds = Math.floor((ms / 1000) % 60); const minutes = Math.floor((ms / (1000 * 60)) % 60); const hours = Math.floor(ms / (1000 * 60 * 60)); return `${hours}h ${minutes}m ${seconds}s`; } // Source: https://doh-nuts.github.io/Enhancelator/ function Enhancelate(input_data, protect_at) { const success_rate = [ 50, //+1 45, //+2 45, //+3 40, //+4 40, //+5 40, //+6 35, //+7 35, //+8 35, //+9 35, //+10 30, //+11 30, //+12 30, //+13 30, //+14 30, //+15 30, //+16 30, //+17 30, //+18 30, //+19 30, //+20 ]; // Get base item level const itemLevel = getBaseItemLevel(input_data.itemHRID); const guzzlingTemp = input_data.guzzling_bonus * 100; const guzzling_bonus = input_data.use_guzzling ? Number(1 + guzzlingTemp / 100) : 1; // Calculate enhancer bonus (matches update_values() logic) const enhancerTemp = input_data.enhancer_bonus; const enhancer_bonus = Number(enhancerTemp); // Calculate effective level with tea bonuses (matches update_values() logic) const effective_level = Math.floor(input_data.enhancing_level + (input_data.tea_enhancing ? 3 * guzzling_bonus : 0) + (input_data.tea_super_enhancing ? 6 * guzzling_bonus : 0) + (input_data.tea_ultra_enhancing ? 8 * guzzling_bonus : 0)); // Calculate total bonus (matches update_values() logic) let total_bonus; if (effective_level >= itemLevel) { total_bonus = 1 + (0.05 * (effective_level + input_data.observatory_level - itemLevel) + enhancer_bonus) / 100; } else { total_bonus = (1 - (0.5 * (1 - (effective_level) / itemLevel))) + ((0.05 * input_data.observatory_level) + enhancer_bonus) / 100; } // Simulation - create 21x21 matrix (0-20) let markov = math.zeros(21, 21); const success_chances = success_rate.map(rate => (rate / 100.0) * total_bonus); for (let i = 0; i < input_data.stop_at; i++) { const success_chance = success_chances[i]; let remaining_success_chance = success_chance; let remaining_fail_chance = 1.0 - success_chance; // If protect_at < 2, we skip protections and set the destination to 0 const destination = (protect_at >= 2 && i >= protect_at) ? i - 1 : 0; // Handle blessed tea (+2 enhancement chance) if (input_data.tea_blessed) { const plus2_chance = success_chance * 0.01 * guzzling_bonus; markov.set([i, Math.min(i + 2, input_data.stop_at)], plus2_chance); remaining_success_chance -= plus2_chance; } // Set remaining probabilities markov.set([i, Math.min(i + 1, input_data.stop_at)], remaining_success_chance); markov.set([i, destination], remaining_fail_chance); } // Set absorbing state markov.set([input_data.stop_at, input_data.stop_at], 1.0); // Calculate expected attempts let Q = markov.subset(math.index(math.range(0, input_data.stop_at), math.range(0, input_data.stop_at))); let M = math.inv(math.subtract(math.identity(input_data.stop_at), Q)); let attemptsArray = M.subset(math.index(math.range(0, 1), math.range(0, input_data.stop_at))); let attempts = math.sum(attemptsArray); // Calculate expected protections let protects = 0; if (protect_at >= 2) { let protectAttempts = M.subset(math.index(math.range(0, 1), math.range(protect_at, input_data.stop_at))); let protectAttemptsArray = (typeof protectAttempts === 'number') ? [protectAttempts] : math.flatten(math.row(protectAttempts, 0).valueOf()); protects = protectAttemptsArray.map((a, i) => a * markov.get([i + protect_at, i + protect_at - 1]) ).reduce((a, b) => a + b, 0); } // Calculate action time (matches original logic) const tea_speed_bonus = input_data.tea_enhancing ? 2 * guzzling_bonus : input_data.tea_super_enhancing ? 4 * guzzling_bonus : input_data.tea_ultra_enhancing ? 6 * guzzling_bonus : 0; const perActionTimeSec = ( 12 / (1 + (input_data.enhancing_level > itemLevel ? ((input_data.effective_level + input_data.observatory_level - itemLevel + input_data.glove_bonus + tea_speed_bonus) / 100) : ((input_data.observatory_level + input_data.glove_bonus + tea_speed_bonus) / 100) ) ) ).toFixed(2); const result = { actions: attempts, protect_count: protects, totalActionTimeSec: perActionTimeSec * attempts, totalActionTimeStr: formatDuration(perActionTimeSec * attempts * 1000), success_rate: success_chances[input_data.stop_at - 1] // Success rate at target level }; return result; } function initializeEnhancelation(itemHRID, action) { const charData = JSON.parse(localStorage.getItem('init_character_data')); const clientData = JSON.parse(localStorage.getItem('initClientData')); // Extract active enhancing teas const enhancingTeas = charData.actionTypeDrinkSlotsMap?.['/action_types/enhancing'] || []; const activeTeas = enhancingTeas.filter(tea => tea?.isActive); // Map tea HRIDs to their types const teaMap = { '/items/wisdom_tea': 'wisdom', '/items/tea_enhancing': 'regular', '/items/super_enhancing_tea': 'super', '/items/ultra_enhancing_tea': 'ultra', '/items/blessed_tea': 'blessed' }; // Check for specific teas let hasRegular = false; let hasSuper = false; let hasUltra = false; let hasBlessed = false; activeTeas.forEach(tea => { const teaType = teaMap[tea.itemHrid]; if (teaType === 'regular') hasRegular = true; if (teaType === 'super') hasSuper = true; if (teaType === 'ultra') hasUltra = true; if (teaType === 'blessed') hasBlessed = true; }); // Map player data to input_data structure let input_data = { item_hrid: itemHRID, stop_at: action.enhancingMaxLevel, enhancing_level: charData?.characterSkills.find(skill => skill.skillHrid === '/skills/enhancing').level || 100, observatory_level: charData?.characterHouseRoomMap['/house_rooms/observatory'].level || 0, enhancer_bonus: calculateEnhancerBonus(charData), glove_bonus: calculateGloveBonus(charData), tea_enhancing: hasRegular, tea_super_enhancing: hasSuper, tea_ultra_enhancing: hasUltra, tea_blessed: hasBlessed, guzzling_bonus: charData.noncombatStats.drinkConcentration ?? 0.0, priceAskBidRatio: 1 }; // Helper functions to calculate equipment bonuses function calculateEnhancerBonus(charData) { // Check for enhancing tool bonuses const equipmentBuffs = charData.equipmentActionTypeBuffsMap?.['/action_types/enhancing'] || []; const enhancerBuff = equipmentBuffs.find(buff => buff.typeHrid === '/buff_types/enhancing_success' ); // Default values if no buff found return enhancerBuff?.ratioBoost || 0.0; // no bonus from tool } function calculateGloveBonus(charData) { // Check for glove speed bonuses const equipmentBuffs = charData.equipmentActionTypeBuffsMap?.['/action_types/enhancing'] || []; const speedBuff = equipmentBuffs.find(buff => buff.typeHrid === '/buff_types/action_speed' ); // Default values if no buff found return speedBuff?.flatBoost || 10; // Base value for level 0 gloves } return input_data } function calculateEnhancementProbabilities(input_data, itemHrid, currentLevel, targetLevel, protectAtLevel) { // Validate inputs if (!itemHrid || targetLevel <= currentLevel) return null; try { const results = Enhancelate( { ...input_data, item_hrid: itemHrid, stop_at: targetLevel }, protectAtLevel ); return { expectedAttempts: Math.round(results.actions), expectedProtections: Math.round(results.protect_count), expectedTime: results.totalActionTimeStr, successChance: `${((results.successRate || 0) * 100).toFixed(1)}%`, rawData: results // Keep original for debugging }; } catch (error) { console.error("[Enhancement] Calculation failed", { itemHrid, currentLevel, targetLevel, error }); return null; } } // Updated handleEnhancement with cost tracking and localStorage saving function handleEnhancement(action) { 'use strict'; // console.log(action); // 1. Extract enhancement data const { hrid: itemHRID, level: newLevel } = parseItemHash(action.primaryItemHash); const currentCount = action.currentCount; if (!itemHRID) return; // 2. Check if this is a new item (which means new session) const isNewItemSession = currentCount === 1 || !enhancementData[currentTrackingIndex] || currentCount <= enhancementData[currentTrackingIndex]["强化次数"]; // 3. Start new session if needed if (currentTrackingIndex === 0 || isNewItemSession) { startNewItemSession(action); } const session = enhancementData[currentTrackingIndex]; // 8. Track costs const preUpdateTotal = session["总成本"]; const { materialCost, coinCost } = trackMaterialCosts(itemHRID); const existingProtectionCost = session["其他数据"]["保护总成本"] || 0; // 4. Special case: First attempt should be a no-op (just record the initial state) if (currentCount === 1) { // Record initial state but don't count as success/failure session["尝试历史"].push({ attemptNumber: currentCount, previousLevel: newLevel, newLevel: newLevel, timestamp: Date.now(), wasSuccess: false, wasBlessed: false, isInitialState: true // New flag to mark initial state }); // Initialize level data if (!session["强化数据"][newLevel]) { session["强化数据"][newLevel] = { "成功次数": 0, "失败次数": 0, "成功率": 0 }; } // Update session metadata session["强化次数"] = currentCount; session["最后更新时间"] = Date.now(); session["会话数据"].最后更新时间 = Date.now(); session["会话数据"].持续时间 = calculateSessionDuration(session); saveEnhancementData(); updateStatsOnly(); return; // Early return for first attempt } // 5. Determine previous level for subsequent attempts const previousLevel = session["尝试历史"]?.length > 0 ? session["尝试历史"][session["尝试历史"].length - 1].newLevel : newLevel; // Fallback to current level // 6. Record this attempt const wasBlessed = (newLevel - previousLevel) >= 2; const isFailure = (newLevel < previousLevel) || (previousLevel === 0 && newLevel === 0); session["尝试历史"].push({ attemptNumber: currentCount, previousLevel: previousLevel, newLevel: newLevel, timestamp: Date.now(), wasSuccess: newLevel > previousLevel, wasBlessed: wasBlessed }); // 7. Initialize level data if needed if (!session["强化数据"][newLevel]) { session["强化数据"][newLevel] = { "成功次数": 0, "失败次数": 0, "成功率": 0 }; } // 9. Handle success/failure if (newLevel > previousLevel) { handleSuccess(session["强化数据"][previousLevel], newLevel, wasBlessed, session); } else if (isFailure) { handleFailure(action, session["强化数据"][previousLevel], session); } // No else case - if newLevel === previousLevel and not level 0, it's neither success nor failure // 10. Update protection costs if failure occurred const newProtectionCost = session["其他数据"]["保护总成本"] || 0; const protectionCostDelta = newProtectionCost - existingProtectionCost; // 11. Update session data session["总成本"] = preUpdateTotal + materialCost + coinCost + protectionCostDelta; session["强化次数"] = currentCount; session["最后更新时间"] = Date.now(); session["会话数据"].最后更新时间 = Date.now(); session["会话数据"].持续时间 = calculateSessionDuration(session); // 12. Check for target achievement if (newLevel >= session["其他数据"]["目标强化等级"]) { finalizeCurrentSession(); showTargetAchievedCelebration(newLevel, session["其他数据"]["目标强化等级"]); session["强化状态"] = "已完成"; } updateStats(session["强化数据"][previousLevel]); // 13. Save and update UI saveEnhancementData(); updateStatsOnly(); } function finalizeCurrentSession() { const session = enhancementData[currentTrackingIndex]; if (!session) return; // Calculate final stats const lastAttempt = session["尝试历史"].slice(-1)[0]; const finalDuration = lastAttempt.timestamp - session["会话数据"].开始时间; const totalXP = session["会话数据"].总经验 || 0; const xpPerHour = Math.floor(totalXP / (finalDuration / (1000 * 60 * 60))) || 0; // Freeze the session data session["会话数据"] = { ...session["会话数据"], "finalDuration": finalDuration, "finalXpPerHour": xpPerHour, "持续时间": finalDuration, "每小时经验": xpPerHour, "最后更新时间": Date.now() }; session["强化状态"] = "已完成"; session["isLive"] = false; saveEnhancementData(); } function startNewItemSession(action) { // Finalize previous session if exists if (enhancementData[currentTrackingIndex]) { finalizeCurrentSession(); } const { hrid: itemHRID, level: newLevel } = parseItemHash(action.primaryItemHash); if (!itemHRID) return; currentTrackingIndex++; enhancementData[currentTrackingIndex] = createItemSessionData(action, itemHRID, newLevel); currentViewingIndex = currentTrackingIndex; debouncedUpdateFloatingUI(); } function createItemSessionData(action, itemHRID, initialLevel) { const protectionHrid = getProtectionItemHrid(action); const isProtected = protectionHrid !== null; const now = Date.now(); let input_data = initializeEnhancelation(itemHRID, action); // Calculate initial probabilities const probabilities = calculateEnhancementProbabilities( input_data, itemHRID, 0, // Starting from +0 action.enhancingMaxLevel, action.enhancingProtectionMinLevel ); return { "强化数据": { [initialLevel]: { "成功次数": 0, "失败次数": 0, "成功率": 0 } }, "其他数据": { "物品HRID": itemHRID, "物品名称": item_hrid_to_name[itemHRID] || "Unknown", "目标强化等级": action.enhancingMaxLevel, "是否保护": isProtected, "保护物品HRID": protectionHrid, "保护物品名称": isProtected ? (item_hrid_to_name[protectionHrid] || protectionHrid) : null, "保护消耗总数": 0, "保护总成本": 0, "保护最小等级": action.enhancingProtectionMinLevel, "初始概率预测": probabilities // Store the initial calculation }, "材料消耗": {}, "硬币消耗": { count: 0, totalCost: 0 }, "总成本": 0, "强化次数": 0, "尝试历史": [], "会话数据": { "开始时间": now, "最后更新时间": now, "总经验": 0, "持续时间": 0, "每小时经验": 0, // Add this new field "finalDuration": null, // Add this for completed sessions "finalXpPerHour": null // Add this for completed sessions }, "强化状态": "进行中", "isLive": true // Add this flag to track live sessions }; } // Keep handleSuccess as is but ensure it's safe function handleSuccess(levelData, newLevel, wasBlessed, session) { try { // 1. Update success count levelData["成功次数"] = (levelData["成功次数"] || 0) + 1; // 2. Calculate XP gain from last attempt const xpGain = session["尝试历史"]?.length > 0 ? calculateSuccessXP( session["尝试历史"][session["尝试历史"].length - 1].previousLevel, session["其他数据"]["物品HRID"] ) : calculateSuccessXP(newLevel, session["其他数据"]["物品HRID"]); // Fallback for first attempt // 3. Update XP tracking (using new 会话数据 structure) session["会话数据"].总经验 = (session["会话数据"].总经验 || 0) + xpGain; // 4. Show appropriate notification showNotification( wasBlessed ? (isZH ? `祝福强化! +${newLevel}` : `BLESSED! +${newLevel}`) : (isZH ? `强化成功 +${newLevel}` : `Success +${newLevel}`), "success", newLevel, wasBlessed ); } catch (e) { console.error("Error in handleSuccess:", e); showNotification(isZH ? "强化跟踪错误" : "Enhancement tracking error", "failure", 0); } } function handleFailure(action, levelData, session) { try { // 1. Update failure count levelData["失败次数"] = (levelData["失败次数"] || 0) + 1; // 2. Calculate XP gain from last attempt const xpGain = session["尝试历史"]?.length > 0 ? calculateFailureXP( session["尝试历史"][session["尝试历史"].length - 1].previousLevel, session["其他数据"]["物品HRID"] ) : calculateFailureXP(0, session["其他数据"]["物品HRID"]); // Fallback for first attempt // 3. Update XP tracking session["会话数据"].总经验 = (session["会话数据"].总经验 || 0) + xpGain; const currentLevel = session["尝试历史"]?.slice(-1)[0]?.previousLevel || 0; // 4. Handle protection if enabled if (session["其他数据"]?.["是否保护"] && currentLevel >= session["其他数据"]?.["保护最小等级"]) { const protectionHrid = session["其他数据"]["保护物品HRID"]; if (protectionHrid) { // Initialize protection tracking if needed session["保护消耗"] = session["保护消耗"] || {}; session["保护消耗"][protectionHrid] = session["保护消耗"][protectionHrid] || { name: session["其他数据"]["保护物品名称"] || protectionHrid, count: 0, totalCost: 0 }; // Update protection costs const protectionCost = getMarketPrice(protectionHrid) || 0; session["其他数据"]["保护消耗总数"] = (session["其他数据"]["保护消耗总数"] || 0) + 1; session["其他数据"]["保护总成本"] = (session["其他数据"]["保护总成本"] || 0) + protectionCost; session["保护消耗"][protectionHrid].count += 1; session["保护消耗"][protectionHrid].totalCost += protectionCost; } } showNotification(isZH ? "强化失败!" : "Failed!", "failure", 0); } catch (e) { console.error("Error in handleFailure:", e); showNotification(isZH ? "强化跟踪错误" : "Enhancement tracking error", "failure", 0); } } function updateStats(levelData) { // Safe access with default values const success = levelData["成功次数"] || 0; const failure = levelData["失败次数"] || 0; levelData["成功率"] = (success + failure) > 0 ? success / (success + failure) : 0; } function getEnhancementState(currentItem) { const highestSuccessLevel = Math.max(...Object.keys(currentItem).filter(level => currentItem[level]["成功次数"] > 0)); return (highestSuccessLevel + 1 >= enhancementData[currentTrackingIndex]["其他数据"]["目标强化等级"]) ? "强化成功" : "强化失败"; } function getProtectionItemHrid(action) { // If protection is disabled (min level < 2) if (action.enhancingProtectionMinLevel < 2) { return null; } // Extract protection item from secondaryItemHash if (action.secondaryItemHash) { const parts = action.secondaryItemHash.split('::'); if (parts.length >= 3 && parts[2].startsWith('/items/')) { return parts[2]; } } // No protection being used return null; } function translateItemName(hrid, fallbackName) { if (!isZH) { return fallbackName; } try { const gameData = JSON.parse(localStorage.getItem('initClientData')); if (gameData?.itemDetailMap?.[hrid]?.name) { const translated = gameData.itemDetailMap[hrid].name; return translated; } } catch (e) { console.error("Translation error:", e); } return item_hrid_to_name?.[hrid] || fallbackName || "Unknown"; } function getCurrentItemName(session) { if (!session["其他数据"] || !session["其他数据"]["物品HRID"]) return "Unknown"; const itemHRID = session["其他数据"]["物品HRID"]; // Always get fresh translation from game data if (item_hrid_to_name && item_hrid_to_name[itemHRID]) { return item_hrid_to_name[itemHRID]; } // Fallback to English if needed const initData = JSON.parse(localStorage.getItem('initClientData') || '{}'); if (initData.itemDetailMap && initData.itemDetailMap[itemHRID]) { return initData.itemDetailMap[itemHRID].name; } return "Unknown"; } function showTargetAchievedCelebration(achievedLevel, targetLevel) { const celebration = document.createElement("div"); celebration.id = "enhancementCelebration"; Object.assign(celebration.style, { position: "fixed", top: "0", left: "0", width: "100%", height: "100%", zIndex: "99999", pointerEvents: "none", display: "flex", flexDirection: "column", justifyContent: "center", alignItems: "center", background: "radial-gradient(circle, rgba(0,0,0,0.7), transparent 70%)" }); const text = document.createElement("div"); text.textContent = isZH ? `目标达成! +${achievedLevel}` : `TARGET ACHIEVED! +${achievedLevel}`; Object.assign(text.style, { fontSize: "3rem", fontWeight: "900", color: "#FFD700", textShadow: "0 0 20px #FF0000, 0 0 30px #FF8C00", animation: "celebrateText 2s ease-out both" }); celebration.appendChild(text); for (let i = 0; i < 100; i++) { const confetti = document.createElement("div"); Object.assign(confetti.style, { position: "absolute", width: "10px", height: "10px", backgroundColor: getRandomColor(), borderRadius: "50%", left: "50%", top: "50%", opacity: "0", animation: `confettiFly ${Math.random() * 2 + 1}s ease-out ${Math.random() * 0.5}s both` }); celebration.appendChild(confetti); } for (let i = 0; i < 8; i++) { setTimeout(() => { createFireworkBurst(celebration); }, i * 300); } document.body.appendChild(celebration); setTimeout(() => { celebration.style.opacity = "0"; celebration.style.transition = "opacity 1s ease-out"; setTimeout(() => celebration.remove(), 1000); }, 4000); } function createFireworkBurst(container) { const burst = document.createElement("div"); Object.assign(burst.style, { position: "absolute", left: `${Math.random() * 80 + 10}%`, top: `${Math.random() * 80 + 10}%`, width: "5px", height: "5px", borderRadius: "50%", background: getRandomColor(), boxShadow: `0 0 10px 5px ${getRandomColor()}`, animation: `fireworkExpand 0.8s ease-out both` }); container.appendChild(burst); for (let i = 0; i < 30; i++) { setTimeout(() => { const particle = document.createElement("div"); Object.assign(particle.style, { position: "absolute", left: burst.style.left, top: burst.style.top, width: "3px", height: "3px", backgroundColor: burst.style.background, borderRadius: "50%", animation: `fireworkTrail ${Math.random() * 0.5 + 0.5}s ease-out both` }); container.appendChild(particle); }, i * 20); } } function getRandomColor() { const colors = ["#FF0000", "#FF8C00", "#FFD700", "#4CAF50", "#2196F3", "#9C27B0"]; return colors[Math.floor(Math.random() * colors.length)]; } function renderStats(selectedKey) { const statsContainer = document.querySelector("#ultimateEnhancementStatsContainer"); if (!statsContainer) return; statsContainer.innerHTML = ""; const item = enhancementData[selectedKey]; if (!item || !item["强化数据"]) return; const headers = ["等级", "成功", "失败", "概率"]; headers.forEach(headerText => { const headerDiv = document.createElement("div"); headerDiv.style.fontWeight = "bold"; headerDiv.textContent = isZH ? headerText : headerText === "等级" ? "Level" : headerText === "成功" ? "Success" : headerText === "失败" ? "Failure" : "Success Rate"; statsContainer.appendChild(headerDiv); }); const totalSuccess = Object.values(item["强化数据"]).reduce((acc, val) => acc + (val["成功次数"] || 0), 0); const totalFailure = Object.values(item["强化数据"]).reduce((acc, val) => acc + (val["失败次数"] || 0), 0); const totalCount = totalSuccess + totalFailure; const totalRate = totalCount > 0 ? (totalSuccess / totalCount * 100).toFixed(2) : "0.00"; ["总计", totalSuccess, totalFailure, `${totalRate}%`].forEach((totalText, index) => { const totalDiv = document.createElement("div"); totalDiv.textContent = isZH ? totalText : index === 0 ? "Total" : totalText; statsContainer.appendChild(totalDiv); }); Object.keys(item["强化数据"]) .map(Number) .sort((a, b) => b - a) .forEach(level => { const levelData = item["强化数据"][level]; const levelDivs = [ level, levelData["成功次数"] || 0, levelData["失败次数"] || 0, `${((levelData["成功率"] || 0) * 100).toFixed(2)}%` ]; levelDivs.forEach(data => { const dataDiv = document.createElement("div"); dataDiv.textContent = data; statsContainer.appendChild(dataDiv); }); }); } // ====================== // FLOATING UI SYSTEM (F9 TOGGLE) // ====================== let floatingUI = null; let cleanupFunctions = []; function createFloatingUI() { if (floatingUI && document.body.contains(floatingUI)) { return floatingUI; } // Create main container (existing code remains the same) floatingUI = document.createElement("div"); floatingUI.id = "enhancementFloatingUI"; Object.assign(floatingUI.style, { position: "fixed", top: "50px", left: "50px", zIndex: "9998", fontSize: "14px", padding: "0", borderRadius: STYLE.borderRadius.medium, boxShadow: '0 8px 32px rgba(0, 0, 0, 0.6)', overflow: "hidden", width: "350px", minHeight: "auto", background: 'rgba(25, 0, 35, 0.92)', backdropFilter: 'blur(12px)', border: `1px solid ${STYLE.colors.primary}`, color: STYLE.colors.textPrimary, willChange: "transform", transform: "translateZ(0)", backfaceVisibility: "hidden", perspective: "1000px", display: "flex", flexDirection: "column" }); // Create header const header = document.createElement("div"); header.id = "enhancementPanelHeader"; Object.assign(header.style, { display: "flex", justifyContent: "space-between", alignItems: "center", cursor: "move", padding: "10px 15px", background: STYLE.colors.headerBg, borderBottom: `1px solid ${STYLE.colors.border}`, userSelect: "none", WebkitUserSelect: "none", flexShrink: "0" }); // Create title with session counter const titleContainer = document.createElement("div"); titleContainer.style.display = "flex"; titleContainer.style.alignItems = "center"; titleContainer.style.gap = "10px"; const title = document.createElement("span"); title.textContent = isZH ? "强化追踪器" : "Enhancement Tracker"; title.style.fontWeight = "bold"; const sessionCounter = document.createElement("span"); sessionCounter.id = "enhancementSessionCounter"; sessionCounter.style.fontSize = "12px"; sessionCounter.style.opacity = "0.7"; sessionCounter.style.marginLeft = "5px"; titleContainer.appendChild(title); titleContainer.appendChild(sessionCounter); // Create navigation arrows container const navContainer = document.createElement("div"); Object.assign(navContainer.style, { display: "flex", gap: "5px", alignItems: "center", marginLeft: "auto" }); // Create clear sessions button const clearButton = document.createElement("button"); clearButton.innerHTML = "🗑️"; clearButton.title = isZH ? "清除所有会话" : "Clear all sessions"; Object.assign(clearButton.style, { background: "none", border: "none", color: STYLE.colors.textPrimary, cursor: "pointer", fontSize: "14px", padding: "2px 8px", borderRadius: "3px", transition: STYLE.transitions.fast, marginRight: "5px" }); clearButton.addEventListener("mouseover", () => { clearButton.style.color = STYLE.colors.danger; clearButton.style.background = "rgba(255, 0, 0, 0.1)"; }); clearButton.addEventListener("mouseout", () => { clearButton.style.color = STYLE.colors.textPrimary; clearButton.style.background = "none"; }); clearButton.addEventListener("click", (e) => { e.stopPropagation(); clearAllSessions(); }); // Create previous arrow const prevArrow = document.createElement("button"); prevArrow.innerHTML = "←"; Object.assign(prevArrow.style, { background: "none", border: "none", color: STYLE.colors.textPrimary, cursor: "pointer", fontSize: "14px", padding: "2px 8px", borderRadius: "3px", transition: STYLE.transitions.fast }); prevArrow.addEventListener("mouseover", () => { prevArrow.style.color = STYLE.colors.accent; prevArrow.style.background = "rgba(255, 255, 255, 0.1)"; }); prevArrow.addEventListener("mouseout", () => { prevArrow.style.color = STYLE.colors.textPrimary; prevArrow.style.background = "none"; }); prevArrow.addEventListener("click", (e) => { e.stopPropagation(); navigateSessions(-1); }); // Create next arrow const nextArrow = document.createElement("button"); nextArrow.innerHTML = "→"; Object.assign(nextArrow.style, { background: "none", border: "none", color: STYLE.colors.textPrimary, cursor: "pointer", fontSize: "14px", padding: "2px 8px", borderRadius: "3px", transition: STYLE.transitions.fast }); nextArrow.addEventListener("mouseover", () => { nextArrow.style.color = STYLE.colors.accent; nextArrow.style.background = "rgba(255, 255, 255, 0.1)"; }); nextArrow.addEventListener("mouseout", () => { nextArrow.style.color = STYLE.colors.textPrimary; nextArrow.style.background = "none"; }); nextArrow.addEventListener("click", (e) => { e.stopPropagation(); navigateSessions(1); }); // Add toggle button const toggleButton = document.createElement("button"); toggleButton.innerHTML = "👁️"; // Down arrow icon (will change based on state) toggleButton.title = isZH ? "切换面板显示" : "Toggle panel display"; Object.assign(toggleButton.style, { background: "none", border: "none", color: STYLE.colors.textPrimary, cursor: "pointer", fontSize: "14px", padding: "2px 8px", borderRadius: "3px", transition: STYLE.transitions.fast, marginLeft: "5px" }); toggleButton.addEventListener("mouseover", () => { toggleButton.style.color = STYLE.colors.accent; toggleButton.style.background = "rgba(255, 255, 255, 0.1)"; }); toggleButton.addEventListener("mouseout", () => { toggleButton.style.color = STYLE.colors.textPrimary; toggleButton.style.background = "none"; }); toggleButton.addEventListener("click", (e) => { e.stopPropagation(); toggleFloatingUI(); }); // Add elements to header header.appendChild(clearButton); header.appendChild(titleContainer); navContainer.appendChild(toggleButton); navContainer.appendChild(prevArrow); navContainer.appendChild(nextArrow); header.appendChild(navContainer); // Rest of the existing code (drag functionality, content area, etc.) remains the same let isDragging = false; let offsetX, offsetY; let animationFrameId; header.addEventListener("mousedown", (e) => { isDragging = true; offsetX = e.clientX - floatingUI.offsetLeft; offsetY = e.clientY - floatingUI.offsetTop; floatingUI.classList.add("dragging"); e.preventDefault(); }); const mouseMoveHandler = (e) => { if (!isDragging) return; cancelAnimationFrame(animationFrameId); animationFrameId = requestAnimationFrame(() => { floatingUI.style.left = `${e.clientX - offsetX}px`; floatingUI.style.top = `${e.clientY - offsetY}px`; }); }; const mouseUpHandler = () => { if (!isDragging) return; isDragging = false; floatingUI.classList.remove("dragging"); cancelAnimationFrame(animationFrameId); }; document.addEventListener("mousemove", mouseMoveHandler, { passive: true }); document.addEventListener("mouseup", mouseUpHandler, { passive: true }); cleanupFunctions.push(() => { document.removeEventListener("mousemove", mouseMoveHandler); document.removeEventListener("mouseup", mouseUpHandler); }); floatingUI.appendChild(header); // Create content area const content = document.createElement("div"); content.id = "enhancementPanelContent"; Object.assign(content.style, { padding: "8px", overflowY: "hidden", flexGrow: "1", minHeight: "0", contain: "strict", boxSizing: "border-box", display: "flex", flexDirection: "column" }); const resizeObserver = new ResizeObserver((entries) => { cancelAnimationFrame(animationFrameId); animationFrameId = requestAnimationFrame(() => { const headerHeight = header.offsetHeight; const contentHeight = content.scrollHeight; const newHeight = headerHeight + contentHeight; // Disable transitions temporarily when shrinking if (newHeight < parseInt(floatingUI.style.height || 0)) { floatingUI.style.transition = 'none'; floatingUI.style.height = `${newHeight}px`; // Force reflow before re-enabling transitions void floatingUI.offsetHeight; floatingUI.style.transition = STYLE.transitions.medium; } else { floatingUI.style.height = `${newHeight}px`; } }); }); resizeObserver.observe(content); cleanupFunctions.push(() => resizeObserver.disconnect()); floatingUI.appendChild(content); document.body.appendChild(floatingUI); // Initial class for empty state floatingUI.classList.toggle('has-data', false); return floatingUI; } function navigateSessions(direction) { const sessionKeys = Object.keys(enhancementData).map(Number).sort((a, b) => a - b); if (sessionKeys.length <= 1) return; const currentIndex = sessionKeys.indexOf(currentViewingIndex); const newIndex = currentIndex + direction; if (newIndex >= 0 && newIndex < sessionKeys.length) { currentViewingIndex = sessionKeys[newIndex]; saveEnhancementData(); // Save the new viewing index updateSessionCounter(); debouncedUpdateFloatingUI(); } } function updateSessionCounter() { const sessionCounter = document.getElementById("enhancementSessionCounter"); if (!sessionCounter) return; const sessionKeys = Object.keys(enhancementData).map(Number).sort((a, b) => a - b); const currentPosition = sessionKeys.indexOf(currentViewingIndex) + 1; const totalSessions = sessionKeys.length; sessionCounter.textContent = isZH ? `(${currentPosition}/${totalSessions})` : `(${currentPosition}/${totalSessions})`; // Visual indicator sessionCounter.style.color = currentViewingIndex === currentTrackingIndex ? STYLE.colors.accent : STYLE.colors.textSecondary; sessionCounter.style.fontWeight = currentViewingIndex === currentTrackingIndex ? "bold" : "normal"; } // Define table styles const compactTableStyle = ` width: 100%; border-collapse: separate; border-spacing: 0; font-size: 13px; margin: 5px 0; background: rgba(30, 0, 40, 0.6); border-radius: ${STYLE.borderRadius.small}; overflow: hidden; `; const compactCellStyle = ` padding: 4px 8px; line-height: 1.3; border-bottom: 1px solid rgba(126, 87, 194, 0.2); `; const compactHeaderStyle = ` ${compactCellStyle} font-weight: bold; text-align: center; background: ${STYLE.colors.headerBg}; color: ${STYLE.colors.textPrimary}; border-bottom: 2px solid ${STYLE.colors.primary}; `; let updateDebounce; function debouncedUpdateFloatingUI() { clearTimeout(updateDebounce); updateDebounce = setTimeout(updateFloatingUI, 100); } function updateFloatingUI() { updateSessionCounter(); const floatingUI = document.getElementById("enhancementFloatingUI") || createFloatingUI(); const content = document.getElementById("enhancementPanelContent"); const UI_DIMENSIONS = { scrollAreaMinHeight: '450px', scrollAreaMaxHeight: '700px', floatingUIHeight: '750px', floatingUIMinHeight: '200px', floatingUIMaxHeight: '85vh' }; // Save current scroll position before wiping content const scrollContainer = content.querySelector("div > div:last-child"); const oldScrollTop = scrollContainer?.scrollTop || 0; // Clear previous content content.innerHTML = ''; // No enhancement data to show if (currentViewingIndex === 0 || !enhancementData[currentViewingIndex]) { floatingUI.classList.toggle('has-data', false); content.innerHTML = `
${isZH ? "开始强化以记录数据" : "Begin enhancing to populate data"}
`; floatingUI.style.height = 'auto'; return; } const session = enhancementData[currentViewingIndex]; if (!session || !session["其他数据"]) { content.innerHTML = isZH ? "没有活跃的强化数据" : "No active enhancement data"; floatingUI.style.height = 'auto'; return; } floatingUI.classList.toggle('has-data', true); // Build header info const marketStatus = lastMarketUpdate > 0 ? `${isZH ? '市场数据' : 'Market data'} ${new Date(lastMarketUpdate * 1000).toLocaleTimeString()}` : `${isZH ? '无市场数据' : 'No market data'}`; const itemData = session["其他数据"] || {}; const itemName = translateItemName(itemData["物品HRID"], itemData["物品名称"]) || "Unknown"; const targetLevel = itemData["目标强化等级"] || 0; // Get the appropriate display data based on session state const displayData = getSessionDisplayData(session); // Main container const container = document.createElement("div"); container.style.display = 'flex'; container.style.flexDirection = 'column'; container.style.height = '100%'; // Header section const headerContent = document.createElement("div"); headerContent.style.flexShrink = '0'; // Status display logic const statusDisplay = session["强化状态"] === "已完成" ? `
${isZH ? "状态" : "Status"}: ${isZH ? "已完成" : "Completed"}
` : `
${isZH ? "状态" : "Status"}: ${isZH ? "进行中" : "In Progress"}
`; headerContent.innerHTML = `
${marketStatus}
${isZH ? "物品" : "Item"}: ${itemName}
${isZH ? "目标" : "Target"}: +${targetLevel}
${statusDisplay}
`; // Scrollable data area const scrollContent = document.createElement("div"); scrollContent.style.flexGrow = '1'; scrollContent.style.overflowY = 'auto'; scrollContent.style.minHeight = UI_DIMENSIONS.scrollAreaMinHeight; scrollContent.style.maxHeight = UI_DIMENSIONS.scrollAreaMaxHeight; scrollContent.style.paddingRight = '5px'; // Stats container with proper session data const statsContainer = document.createElement("div"); statsContainer.id = "ultimateEnhancementStatsContainer"; const totalAttempts = session["强化次数"]; const totalSuccess = Object.values(session["强化数据"]).reduce((sum, level) => sum + (level["成功次数"] || 0), 0); const totalFailure = Object.values(session["强化数据"]).reduce((sum, level) => sum + (level["失败次数"] || 0), 0); const totalRate = totalAttempts > 0 ? (totalSuccess / totalAttempts * 100).toFixed(2) : 0; const shouldDisplayRate = displayData.isLive ? (displayData.xpPerHour > 0) : (displayData.xpPerHour !== null); // Get stored probabilities if they exist const probabilities = session["其他数据"]["初始概率预测"] || null; statsContainer.innerHTML = ` ${Object.keys(session["强化数据"]) .sort((a, b) => b - a) .map(level => { const levelData = session["强化数据"][level]; const rate = ((levelData["成功率"] || 0) * 100).toFixed(1); const sessionLevel = session["尝试历史"]?.slice(-1)[0]?.newLevel || 0; const isCurrent = (level == sessionLevel) && (currentTrackingIndex == currentViewingIndex); return ` `; }).join('')}
${isZH ? "等级" : "Lvl"} ${isZH ? "成功" : "Success"} ${isZH ? "失败" : "Fail"} %
${level} ${levelData["成功次数"] || 0} ${levelData["失败次数"] || 0} ${rate}%
${isZH ? "总计尝试次数" : "Total Attempts"}: ${totalAttempts}
${isZH ? "保护石使用" : "Prots Used"}: ${session["其他数据"]["保护消耗总数"] || 0}
${probabilities ? `
${isZH ? "预计尝试" : "Expected Attempts"}: ${Math.round(probabilities.expectedAttempts)}
` : ''} ${probabilities ? `
${isZH ? "预计保护石" : "Expected Prots"}: ${Math.round(probabilities.expectedProtections)}
` : ''}
${isZH ? "总获得经验" : "Total XP Gained"}: ${formatNumber(displayData.totalXP)}
${isZH ? "会话时长" : "Session Duration"}: ${formatDuration(displayData.duration)}
${isZH ? "经验/小时" : "XP/Hour"}: ${shouldDisplayRate ? formatNumber(displayData.xpPerHour) : (isZH ? "计算中..." : "Calculating...")}
`; if (session["材料消耗"] && Object.keys(session["材料消耗"]).length > 0) { statsContainer.innerHTML += generateMaterialCostsHTML(session); } scrollContent.appendChild(statsContainer); // Build complete UI container.appendChild(headerContent); container.appendChild(scrollContent); content.appendChild(container); // Style the floating UI floatingUI.style.height = UI_DIMENSIONS.floatingUIHeight; floatingUI.style.minHeight = UI_DIMENSIONS.floatingUIMinHeight; floatingUI.style.maxHeight = UI_DIMENSIONS.floatingUIMaxHeight; floatingUI.style.overflow = 'hidden'; // Restore previous scroll position (after DOM renders) setTimeout(() => { scrollContent.scrollTop = oldScrollTop; }, 0); } // Helper function to get proper display data function getSessionDisplayData(session) { if (session["强化状态"] === "已完成") { // For completed sessions, use frozen values return { duration: session["会话数据"].finalDuration || session["会话数据"].持续时间, xpPerHour: session["会话数据"].finalXpPerHour || session["会话数据"].每小时经验, totalXP: session["会话数据"].总经验 || 0, isLive: false }; } else { // For live sessions, calculate current values return { duration: calculateSessionDuration(session), xpPerHour: calculateXpPerHour(session), totalXP: session["会话数据"].总经验 || 0, isLive: true }; } } function updateStatsOnly() { const session = enhancementData[currentViewingIndex]; if (!session || !session["其他数据"]) return; const statsContainer = document.getElementById("ultimateEnhancementStatsContainer"); if (!statsContainer) return; statsContainer.innerHTML = buildTableHTML(session); if (session["材料消耗"] && Object.keys(session["材料消耗"]).length > 0) { statsContainer.innerHTML += generateMaterialCostsHTML(session); } } function getSessionDisplayData(session) { if (session["强化状态"] === "已完成") { // For completed sessions, always use frozen values return { duration: session["会话数据"].finalDuration, xpPerHour: session["会话数据"].finalXpPerHour, totalXP: session["会话数据"].总经验, isLive: false }; } else { // For live sessions, calculate current values return { duration: calculateSessionDuration(session), xpPerHour: calculateXpPerHour(session), totalXP: session["会话数据"].总经验 || 0, isLive: true }; } } function buildTableHTML(session) { const totalAttempts = session["强化次数"]; const totalSuccess = Object.values(session["强化数据"]).reduce((sum, level) => sum + (level["成功次数"] || 0), 0); const totalFailure = Object.values(session["强化数据"]).reduce((sum, level) => sum + (level["失败次数"] || 0), 0); const totalRate = totalAttempts > 0 ? (totalSuccess / totalAttempts * 100).toFixed(2) : 0; // Get the appropriate display data const displayData = getSessionDisplayData(session); const shouldDisplayRate = displayData.isLive ? (displayData.xpPerHour > 0) : (displayData.xpPerHour !== null); // Get stored probabilities if they exist const probabilities = session["其他数据"]["初始概率预测"] || null; return ` ${Object.keys(session["强化数据"]) .sort((a, b) => b - a) .map(level => { const levelData = session["强化数据"][level]; const rate = ((levelData["成功率"] || 0) * 100).toFixed(1); const sessionLevel = session["尝试历史"]?.slice(-1)[0]?.newLevel || 0; const isCurrent = (level == sessionLevel) && (currentTrackingIndex == currentViewingIndex); return ` `; }).join('')}
${isZH ? "等级" : "Lvl"} ${isZH ? "成功" : "Success"} ${isZH ? "失败" : "Fail"} %
${level} ${levelData["成功次数"] || 0} ${levelData["失败次数"] || 0} ${rate}%
${isZH ? "总计尝试次数" : "Total Attempts"}: ${totalAttempts}
${isZH ? "保护石使用" : "Prots Used"}: ${session["其他数据"]["保护消耗总数"] || 0}
${probabilities ? `
${isZH ? "预计尝试" : "Expected Attempts"}: ${Math.round(probabilities.expectedAttempts)}
` : ''} ${probabilities ? `
${isZH ? "预计保护石" : "Expected Prots"}: ${Math.round(probabilities.expectedProtections)}
` : ''}
${isZH ? "总获得经验" : "Total XP Gained"}: ${formatNumber(session["会话数据"]?.["总经验"] || 0)}
${isZH ? "会话时长" : "Session Duration"}: ${formatDuration(displayData.duration)}
${isZH ? "经验/小时" : "XP/Hour"}: ${shouldDisplayRate ? formatNumber(displayData.xpPerHour) : (isZH ? "计算中..." : "Calculating...")}
`; } function formatNumber(num) { if (typeof num !== 'number') return '0'; return num.toLocaleString('en-US', { maximumFractionDigits: 0 }); } function toggleFloatingUI() { if (!floatingUI || !document.body.contains(floatingUI)) { createFloatingUI(); debouncedUpdateFloatingUI(); floatingUI.style.display = "block"; showUINotification( isZH ? "强化追踪器已启用" : "Enhancement Tracker Enabled" ); } else { const willShow = floatingUI.style.display === "none"; floatingUI.style.display = willShow ? "flex" : "none"; showUINotification( isZH ? `强化追踪器${willShow ? "已显示" : "已隐藏"}` : `Enhancement Tracker ${willShow ? "Shown" : "Hidden"}` ); } localStorage.setItem("enhancementUIVisible", floatingUI.style.display !== "none"); } function generateMaterialCostsHTML(session) { const totalCost = session["总成本"] || 0; let html = `
${isZH ? "材料成本" : "Material Costs"}: ${formatNumber(totalCost)}
`; // 1. Add regular materials sorted by cost (descending) html += Object.entries(session["材料消耗"] || {}) .sort(([hridA, dataA], [hridB, dataB]) => dataB.totalCost - dataA.totalCost) .map(([hrid, data]) => ` `).join(''); // 2. Add coins LAST if they exist if (session["硬币消耗"] && session["硬币消耗"].count > 0) { html += ` `; } html += `
${isZH ? "材料" : "Material"} ${isZH ? "数量" : "Qty"} ${isZH ? "成本" : "Cost"}
${translateItemName(hrid, data.name)} ${formatNumber(data.count)} ${formatNumber(data.totalCost)}
${isZH ? "金币" : "Coins"} ${formatNumber(session["硬币消耗"].totalCost)}
`; // Protection cost display remains unchanged if (session["其他数据"]["保护消耗总数"] > 0) { html += `
${isZH ? "保护物品" : "Protection Item"}: ${translateItemName( session["其他数据"]["保护物品HRID"], session["其他数据"]["保护物品名称"] )}
${isZH ? "使用数量" : "Used"}: ${session["其他数据"]["保护消耗总数"]}
${isZH ? "保护总成本" : "Protection Cost"}: ${formatNumber(session["其他数据"]["保护总成本"])}
`; } return html; } function cleanupFloatingUI() { if (floatingUI && document.body.contains(floatingUI)) { floatingUI.remove(); } cleanupFunctions.forEach(fn => fn()); cleanupFunctions = []; floatingUI = null; } // Add this function to create/manage the toggle button function addEnhancementPanelToggle() { // Find the target container const gamePanel = document.querySelector('[class^="EnhancingPanel_skillActionDetailContainer"]'); if (!gamePanel) return; // Check if we already added our button if (gamePanel.querySelector('.floating-ui-toggle-button')) return; // Create the toggle button const toggleBtn = document.createElement('button'); toggleBtn.className = 'floating-ui-toggle-button'; toggleBtn.innerHTML = '📊'; // Icon indicating stats/UI toggleBtn.title = isZH ? "切换浮动面板" : "Toggle Enhancement Tracker"; // Style the button to match the game's UI Object.assign(toggleBtn.style, { position: 'absolute', bottom: '5px', right: '5px', width: '24px', height: '24px', borderRadius: '4px', background: 'rgba(0, 0, 0, 0.7)', border: `1px solid ${STYLE.colors.primary}`, color: STYLE.colors.textPrimary, cursor: 'pointer', display: 'flex', alignItems: 'center', justifyContent: 'center', fontSize: '12px', zIndex: '10' }); // Hover effects toggleBtn.addEventListener('mouseenter', () => { toggleBtn.style.background = 'rgba(0, 0, 0, 0.9)'; toggleBtn.style.color = STYLE.colors.accent; }); toggleBtn.addEventListener('mouseleave', () => { toggleBtn.style.background = 'rgba(0, 0, 0, 0.7)'; toggleBtn.style.color = STYLE.colors.textPrimary; }); // Click handler: Toggles our floating UI toggleBtn.addEventListener('click', (e) => { e.stopPropagation(); toggleFloatingUI(); // This is the existing function from your script }); // Ensure the game panel has relative positioning if (getComputedStyle(gamePanel).position === 'static') { gamePanel.style.position = 'relative'; } // Add the button to the game panel gamePanel.appendChild(toggleBtn); } // ====================== // UI NOTIFICATION SYSTEM // ====================== function showUINotification(message, duration = 2000) { const notification = document.createElement("div"); Object.assign(notification.style, { position: "fixed", bottom: "20px", left: "50%", transform: "translateX(-50%)", padding: '8px 12px', borderRadius: "4px", backdropFilter: 'blur(4px)', border: `1px solid ${STYLE.colors.primary}`, background: 'rgba(40, 0, 60, 0.9)', backgroundColor: "rgba(0, 0, 0, 0.8)", color: "white", zIndex: "10000", fontSize: "14px", animation: "fadeInOut 2s ease-in-out", pointerEvents: "none" }); notification.textContent = message; document.body.appendChild(notification); setTimeout(() => { notification.style.opacity = "0"; setTimeout(() => notification.remove(), 300); }, duration); } // ====================== // KEYBOARD SHORTCUT // ====================== function setupKeyboardShortcut() { document.addEventListener("keydown", (e) => { if (e.key === "F9") { e.preventDefault(); toggleFloatingUI(); } }); } // ====================== // TESTING SYSTEM // ====================== function initializeTesting() { if (typeof GM_registerMenuCommand !== 'undefined') { GM_registerMenuCommand("🔧 Test Success Notifications", () => testNotifications("success")); GM_registerMenuCommand("🔧 Test Failure Notifications", () => testNotifications("failure")); GM_registerMenuCommand("✨ Test Blessed Success", () => testNotifications("blessed")); GM_registerMenuCommand("🌀 Test All Notifications", () => testNotifications("all")); GM_registerMenuCommand("💎 Test Hitting Target Level", () => { const level = 15; const type = "success"; const targetLevel = 15; const isBlessed = type === "blessed"; const isSuccess = type !== "failure"; showNotification( isBlessed ? "BLESSED!" : isSuccess ? "Success" : "Failed!", isSuccess ? "success" : "failure", level, isBlessed ); if (level >= targetLevel) { showTargetAchievedCelebration(level, targetLevel); } }); } window.testEnhancement = { success: () => testNotifications("success"), allSuccess: () => testNotifications("allSuccess"), failure: () => testNotifications("failure"), blessed: () => testNotifications("blessed"), all: () => testNotifications("all"), custom: (level, type, targetLevel) => { const isBlessed = type === "blessed"; const isSuccess = type !== "failure"; showNotification( isBlessed ? "BLESSED!" : isSuccess ? "Success" : "Failed!", isSuccess ? "success" : "failure", level, isBlessed ); if (level >= targetLevel) { showTargetAchievedCelebration(level, targetLevel); } } }; } function testNotifications(type) { const tests = { success: [ { level: 1, blessed: false }, { level: 5, blessed: false }, { level: 10, blessed: false }, { level: 15, blessed: false }, { level: 20, blessed: false } ], allSuccess: [ { level: 1, blessed: false }, { level: 2, blessed: false }, { level: 3, blessed: false }, { level: 4, blessed: false }, { level: 5, blessed: false }, { level: 6, blessed: false }, { level: 7, blessed: false }, { level: 8, blessed: false }, { level: 9, blessed: false }, { level: 10, blessed: false }, { level: 11, blessed: false }, { level: 12, blessed: false }, { level: 13, blessed: false }, { level: 14, blessed: false }, { level: 15, blessed: false }, { level: 16, blessed: false }, { level: 17, blessed: false }, { level: 18, blessed: false }, { level: 19, blessed: false }, { level: 20, blessed: false } ], blessed: [ { level: 3, blessed: true }, { level: 8, blessed: true }, { level: 12, blessed: true }, { level: 17, blessed: true }, { level: 22, blessed: true } ], failure: [ { level: 0, blessed: false } ], all: [ { level: 1, blessed: false }, { level: 0, blessed: false }, { level: 3, blessed: true }, { level: 0, blessed: false }, { level: 10, blessed: false }, { level: 12, blessed: true }, { level: 15, blessed: false }, { level: 0, blessed: false }, { level: 20, blessed: false } ] }; tests[type].forEach((test, i) => { setTimeout(() => { const message = test.blessed ? (isZH ? `祝福强化! +${test.level}` : `BLESSED! +${test.level}`) : (test.level > 0 ? (isZH ? `强化成功 +${test.level}` : `Success +${test.level}`) : (isZH ? "强化失败!" : "Failed!")); showNotification( message, test.level > 0 ? "success" : "failure", test.level, test.blessed ); }, i * 800); }); } // ====================== // INITIALIZATION // ====================== function addGlobalStyles() { const style = document.createElement("style"); style.textContent = ` @keyframes pulse { 0% { transform: scale(1); } 50% { transform: scale(1.05); } 100% { transform: scale(1); } } @keyframes holyGlow { 0% { box-shadow: 0 0 10px #FFD700; } 50% { box-shadow: 0 0 25px #FFD700, 0 0 40px white; } 100% { box-shadow: 0 0 10px #FFD700; } } @keyframes raysRotate { from { transform: rotate(0deg); } to { transform: rotate(360deg); } } @keyframes floatUp { 0% { transform: translateY(0) rotate(0deg); opacity: 0; } 10% { opacity: 0.7; } 90% { opacity: 0.7; } 100% { transform: translateY(-100px) rotate(20deg); opacity: 0; } } @keyframes celebrateText { 0% { transform: scale(0.5); opacity: 0; } 50% { transform: scale(1.2); opacity: 1; } 100% { transform: scale(1); } } @keyframes confettiFly { 0% { transform: translate(0,0) rotate(0deg); opacity: 1; } 100% { transform: translate(${Math.random() > 0.5 ? '-' : ''}${Math.random() * 300 + 100}px, ${Math.random() * 300 + 100}px) rotate(360deg); opacity: 0; } } @keyframes fireworkExpand { 0% { transform: scale(0); opacity: 1; } 100% { transform: scale(20); opacity: 0; } } @keyframes fireworkTrail { 0% { transform: translate(0,0); opacity: 1; } 100% { transform: translate(${Math.random() > 0.5 ? '-' : ''}${Math.random() * 100 + 50}px, ${Math.random() * 100 + 50}px); opacity: 0; } } .enhancement-notification { position: relative; overflow: hidden; transition: ${STYLE.transitions.medium}; backdrop-filter: blur(4px); -webkit-backdrop-filter: blur(4px); background: 'rgba(255, 255, 255, 0.1)', border: '1px solid rgba(255, 255, 255, 0.2)', } #enhancementFloatingUI { transition: height 0.15s ease-out, opacity 0.2s ease, transform 0.2s ease; height: auto; max-height: 80vh; backdrop-filter: blur(8px); -webkit-backdrop-filter: blur(8px); background: 'rgba(255, 255, 255, 0.1)', border: '1px solid rgba(255, 255, 255, 0.2)', } #enhancementFloatingUI.no-transition { transition: none !important; } #enhancementPanelContent { scrollbar-width: thin; scrollbar-color: ${STYLE.colors.border} transparent; } #enhancementPanelContent::-webkit-scrollbar { width: 6px; } #enhancementPanelContent::-webkit-scrollbar-thumb { background: ${STYLE.colors.primary}; border-radius: 3px; } #enhancementPanelContent::-webkit-scrollbar-track { background: rgba(30, 0, 40, 0.4); } #enhancementFloatingUI[style*="display: none"] { display: block !important; opacity: 0; pointer-events: none; transform: translateY(10px); } #enhancementFloatingUI.dragging { cursor: grabbing; box-shadow: 0 8px 24px rgba(0, 0, 0, 0.3); transition: none; } @keyframes floatIn { 0% { opacity: 0; transform: translateY(10px); } 100% { opacity: 1; transform: translateY(0); } } #enhancementFloatingUI:not([style*="display: none"]) { animation: floatIn 0.2s ease-out; } @keyframes fadeInOut { 0% { opacity: 0; transform: translateX(-50%) translateY(10px); } 20% { opacity: 1; transform: translateX(-50%) translateY(0); } 80% { opacity: 1; transform: translateX(-50%) translateY(0); } 100% { opacity: 0; transform: translateX(-50%) translateY(-10px); } } @keyframes mythicPulse { 0% { background-position: 0% 50%; transform: scale(1); box-shadow: 0 0 8px #ff0033; } 50% { background-position: 100% 50%; transform: scale(1.05); box-shadow: 0 0 16px #ff2200, 0 0 24px #ff2200; } 100% { background-position: 0% 50%; transform: scale(1); box-shadow: 0 0 8px #ff0033; } } #enhancementPanelHeader button { background: none; border: none; color: ${STYLE.colors.textPrimary}; cursor: pointer; font-size: 14px; padding: 2px 8px; border-radius: 3px; transition: ${STYLE.transitions.fast}; display: flex; align-items: center; justify-content: center; width: 24px; height: 24px; } #enhancementPanelHeader button:hover { color: ${STYLE.colors.accent}; background: rgba(255, 255, 255, 0.1); } #enhancementPanelHeader button:active { transform: scale(0.9); } #enhancementPanelHeader .nav-arrows { display: flex; gap: 5px; margin-left: auto; } /* Enhanced scrollbar styling */ #enhancementPanelContent > div::-webkit-scrollbar { width: 6px; height: 6px; } #enhancementPanelContent > div::-webkit-scrollbar-thumb { background-color: ${STYLE.colors.primary}; border-radius: 3px; } #enhancementPanelContent > div::-webkit-scrollbar-track { background-color: rgba(30, 0, 40, 0.4); border-radius: 3px; } #enhancementPanelContent > div { scrollbar-width: thin; scrollbar-color: ${STYLE.colors.primary} rgba(30, 0, 40, 0.4); } /* Better empty state for scroll container */ .empty-state { display: flex; flex-direction: column; align-items: center; justify-content: center; height: 100px; color: ${STYLE.colors.textSecondary}; padding: 20px; text-align: center; } .empty-icon { font-size: 32px; margin-bottom: 10px; opacity: 0.5; } .empty-text { font-size: 14px; } #enhancementFloatingUI:not(.has-data) { height: 150px !important; /* Fixed height for empty state */ } `; document.head.appendChild(style); } function initializeFloatingUI() { const checkReady = setInterval(() => { if (document.body && typeof item_hrid_to_name !== "undefined") { clearInterval(checkReady); setupKeyboardShortcut(); if (localStorage.getItem("enhancementUIVisible") !== "false") { createFloatingUI(); debouncedUpdateFloatingUI(); } } }, 500); } function initializeFloatingUIToggle() { // Try immediately in case the panel is already loaded addEnhancementPanelToggle(); // Set up an observer to detect when the panel loads dynamically const observer = new MutationObserver((mutations) => { if (document.querySelector('[class^="EnhancingPanel_skillActionDetailContainer"]')) { addEnhancementPanelToggle(); } }); observer.observe(document.body, { childList: true, subtree: true }); } // Start everything addGlobalStyles(); initializeTesting(); initializeFloatingUI(); // Initialize market data loading when script starts loadMarketData(); initializeFloatingUIToggle(); hookWS(); console.log("Enhancement Notifier v3.5.1 loaded"); })();