// ==UserScript==
// @name [MWI] Ultimate Enhancement Notifier v3.2.0
// @namespace http://tampermonkey.net/
// @version 3.2.0
// @description Added persistence between reloads and option to clear data.
// @author Truth_Light (modified by Nex, styled by Cosmic, cost tracking by Assistant)
// @match https://www.milkywayidle.com/*
// @match https://test.milkywayidle.com/*
// @license MIT
// @grant GM_registerMenuCommand
// @icon https://www.google.com/s2/favicons?sz=64&domain=milkywayidle.com
// ==/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
*/
(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');
let enhancementLevel;
// 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;
cleanSessionIndices();
// Function to save enhancementData to localStorage
function saveEnhancementData() {
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');
updateFloatingUI();
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["其他数据"]["物品名称"]
);
}
});
updateFloatingUI();
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;
}
// 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;
}
}
// ======================
// 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) {
try {
const materials = getEnhancementMaterials(itemHRID);
if (!materials || materials.length === 0) {
console.log('[Cost] No materials to track for:', itemHRID);
return 0;
}
let totalCost = 0;
const session = enhancementData[currentTrackingIndex];
if (!session["材料消耗"]) {
session["材料消耗"] = {};
}
materials.forEach(material => {
let materialHrid, count;
if (Array.isArray(material)) {
materialHrid = material[0];
count = material[1] || 0;
} else {
console.log('[Cost] Invalid material format:', material);
return;
}
if (!materialHrid || count <= 0) {
console.log('[Cost] Invalid material entry:', {materialHrid, count});
return;
}
const cost = getMarketPrice(materialHrid) * count;
totalCost += cost;
if (!session["材料消耗"][materialHrid]) {
session["材料消耗"][materialHrid] = {
name: item_hrid_to_name[materialHrid] || materialHrid,
count: 0,
totalCost: 0
};
}
session["材料消耗"][materialHrid].count += count;
session["材料消耗"][materialHrid].totalCost += cost;
// console.log('[Cost] Tracked:', count, 'x', materialHrid, '=', cost, 'gold');
});
session["总成本"] = (session["总成本"] || 0) + totalCost;
// console.log('[Cost] Total for this attempt:', totalCost, 'gold');
return totalCost;
} catch (e) {
console.error('[Cost] Error tracking materials:', e);
return 0;
}
}
// ======================
// 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 extractBaseItemHrid(primaryItemHash) {
try {
// Handle different possible formats:
// 1. "/item_locations/inventory::/items/enhancers_bottoms::0"
// 2. "161296::/item_locations/inventory::/items/enhancers_bottoms::0"
// 3. Direct HRID like "/items/enhancers_bottoms"
// Split by :: and find the part that starts with /items/
const parts = primaryItemHash.split('::');
const itemPart = parts.find(part => part.startsWith('/items/'));
if (itemPart) {
return itemPart;
}
// If no /items/ found but it's a direct HRID
if (primaryItemHash.startsWith('/items/')) {
return primaryItemHash;
}
console.log('[Extract] Could not find item HRID in:', primaryItemHash);
return null;
} catch (e) {
console.error('[Extract] Error parsing primaryItemHash:', e);
return null;
}
}
// Updated handleEnhancement with cost tracking and localStorage saving
function handleEnhancement(action) {
const newLevel = parseInt(action.primaryItemHash.match(/::(\d+)$/)[1]);
const currentCount = action.currentCount;
const wasBlessed = enhancementLevel !== undefined && (newLevel - enhancementLevel) >= 2;
const itemHRID = extractBaseItemHrid(action.primaryItemHash);
if (!itemHRID) return;
// Determine previous level properly
const previousLevel = (enhancementLevel !== undefined) ? enhancementLevel : (currentCount === 1 ? newLevel : 0);
// Update the current level immediately so it's accurate for next run
enhancementLevel = newLevel;
// First enhancement detection
if (currentTrackingIndex === 0) {
// Properly initialize first session
currentTrackingIndex = 1;
enhancementData[currentTrackingIndex] = {
"强化数据": {},
"其他数据": {
"物品HRID": itemHRID,
"物品名称": item_hrid_to_name[itemHRID] || "Unknown",
"目标强化等级": action.enhancingMaxLevel,
"是否保护": action.enhancingProtectionMinLevel >= 2,
"保护物品HRID": getProtectionItemHrid(action),
"保护物品名称": item_hrid_to_name[getProtectionItemHrid(action)] || null,
"保护消耗总数": 0,
"保护总成本": 0,
"保护最小等级": action.enhancingProtectionMinLevel
},
"材料消耗": {},
"保护消耗": {},
"总成本": 0,
"强化次数": 0,
"强化状态": "进行中"
};
currentViewingIndex = currentTrackingIndex;
saveEnhancementData(); // Save new session
} // Session initialization logic (unchanged)
else if (currentCount === 1 || !enhancementData[currentTrackingIndex] ||
currentCount <= enhancementData[currentTrackingIndex]["强化次数"]) {
startNewEnhancementSession(action);
initializeNewItem(action, itemHRID, newLevel);
saveEnhancementData(); // Save new session
}
// Get current session reference
const session = enhancementData[currentTrackingIndex];
// Initialize data for the new level if it doesn't exist
if (!enhancementData[currentTrackingIndex]["强化数据"][newLevel]) {
enhancementData[currentTrackingIndex]["强化数据"][newLevel] = {
"成功次数": 0,
"失败次数": 0,
"成功率": 0
};
saveEnhancementData(); // Save new level data
}
if(currentCount == 1){
const cost = trackMaterialCosts(itemHRID);
session["总成本"] += cost;
saveEnhancementData(); // Save after material cost tracking
}
// Skip enhancement logic if currentCount is 1 or enhancementLevel is undefined
if (currentCount !== 1 && previousLevel !== undefined) {
// Initialize level data for the previous level if needed
if (!enhancementData[currentTrackingIndex]["强化数据"][previousLevel]) {
enhancementData[currentTrackingIndex]["强化数据"][previousLevel] = {
"成功次数": 0,
"失败次数": 0,
"成功率": 0
};
saveEnhancementData(); // Save new level data
}
const levelData = enhancementData[currentTrackingIndex]["强化数据"][previousLevel];
const session = enhancementData[currentTrackingIndex];
const materialCost = trackMaterialCosts(itemHRID);
session["总成本"] += materialCost;
saveEnhancementData(); // Save after material cost tracking
if (newLevel > previousLevel) {
handleSuccess(levelData, newLevel, wasBlessed);
} else {
handleFailure(action, levelData, session);
// Protection cost handling
if (session["其他数据"]["是否保护"] &&
previousLevel >= session["其他数据"]["保护最小等级"]) {
const protectionHrid = session["其他数据"]["保护物品HRID"];
const protectionCost = getMarketPrice(protectionHrid);
if (!session["保护消耗"][protectionHrid]) {
session["保护消耗"][protectionHrid] = {
name: session["其他数据"]["保护物品名称"],
count: 0,
totalCost: 0
};
}
session["保护消耗"][protectionHrid].count++;
session["保护消耗"][protectionHrid].totalCost += protectionCost;
session["其他数据"]["保护消耗总数"]++;
session["其他数据"]["保护总成本"] += protectionCost;
session["总成本"] += protectionCost;
saveEnhancementData(); // Save after protection cost tracking
}
}
updateStats(levelData);
session["强化次数"] = currentCount;
saveEnhancementData(); // Save after stats update
}
// Final updates
if (newLevel >= enhancementData[currentTrackingIndex]["其他数据"]["目标强化等级"]) {
showTargetAchievedCelebration(newLevel, enhancementData[currentTrackingIndex]["其他数据"]["目标强化等级"]);
}
updateDisplay();
updateFloatingUI();
}
function startNewEnhancementSession(action) {
// Only create new sessions after first one exists
if (currentTrackingIndex === 0) return;
currentTrackingIndex++;
enhancementData[currentTrackingIndex] = {
"强化数据": {},
"其他数据": {
"物品HRID": "",
"物品名称": "Unknown",
"目标强化等级": 0,
"是否保护": false,
"保护物品HRID": null,
"保护物品名称": null,
"保护消耗总数": 0,
"保护总成本": 0,
"保护最小等级": 0
},
"材料消耗": {}, // Material costs storage
"保护消耗": {}, // Protection costs storage
"总成本": 0, // Total gold cost
"强化次数": 0,
"强化状态": "进行中"
};
}
// Keep handleSuccess as is but ensure it's safe
function handleSuccess(levelData, newLevel, wasBlessed) {
levelData["成功次数"] = (levelData["成功次数"] || 0) + 1;
const message = isZH ? `强化成功 +${newLevel}` : `Success +${newLevel}`;
showNotification(
wasBlessed ? (isZH ? `祝福强化! +${newLevel}` : `BLESSED! +${newLevel}`) : message,
"success",
newLevel,
wasBlessed
);
}
// Updated handleFailure with proper safety checks
function handleFailure(action, levelData, session) {
// Initialize if undefined
levelData["失败次数"] = (levelData["失败次数"] || 0) + 1;
// Only process protection if session data exists
if (session["其他数据"]?.["是否保护"] &&
enhancementLevel >= session["其他数据"]?.["保护最小等级"]) {
const protectionHrid = session["其他数据"]["保护物品HRID"];
if (protectionHrid) {
// Initialize protection tracking if needed
if (!session["保护消耗"]) {
session["保护消耗"] = {};
}
if (!session["保护消耗"][protectionHrid]) {
session["保护消耗"][protectionHrid] = {
name: session["其他数据"]["保护物品名称"],
count: 0,
totalCost: 0
};
}
// Update protection stats
const protectionCost = getMarketPrice(protectionHrid);
session["保护消耗"][protectionHrid].count++;
session["保护消耗"][protectionHrid].totalCost += protectionCost;
session["其他数据"]["保护消耗总数"] = (session["其他数据"]["保护消耗总数"] || 0) + 1;
session["其他数据"]["保护总成本"] = (session["其他数据"]["保护总成本"] || 0) + protectionCost;
session["总成本"] = (session["总成本"] || 0) + protectionCost;
}
}
showNotification(isZH ? "强化失败!" : "Failed!", "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]["其他数据"]["目标强化等级"]) ? "强化成功" : "强化失败";
}
// Updated initializeNewItem():
function initializeNewItem(action, itemHRID, newLevel) {
const rawItemName = item_hrid_to_name[itemHRID] || "Unknown";
const protectionHrid = getProtectionItemHrid(action);
const isProtected = protectionHrid !== null;
enhancementData[currentTrackingIndex]["其他数据"] = {
"物品HRID": itemHRID,
"物品名称": rawItemName,
"目标强化等级": action.enhancingMaxLevel,
"是否保护": isProtected,
"保护物品HRID": protectionHrid,
"保护物品名称": isProtected ? (item_hrid_to_name[protectionHrid] || protectionHrid) : null,
"保护消耗总数": 0,
"保护总成本": 0,
"保护最小等级": action.enhancingProtectionMinLevel
};
// Initialize current level data with all required properties
enhancementData[currentTrackingIndex]["强化数据"][newLevel] = {
"成功次数": 0,
"失败次数": 0,
"成功率": 0
};
}
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)];
}
// ======================
// DISPLAY SYSTEM
// ======================
function updateDisplay() {
const targetElement = document.querySelector(".SkillActionDetail_enhancingComponent__17bOx");
if (!targetElement) return;
let parentContainer = document.querySelector("#enhancementParentContainer");
if (!parentContainer) {
parentContainer = createDisplayContainer(targetElement);
}
updateDropdown(parentContainer);
}
function createDisplayContainer(targetElement) {
const parentContainer = document.createElement("div");
parentContainer.id = "enhancementParentContainer";
Object.assign(parentContainer.style, {
display: "block",
borderLeft: `2px solid ${STYLE.colors.border}`,
padding: "0 4px"
});
const title = document.createElement("div");
title.textContent = isZH ? "强化数据" : "Enhancement Data";
Object.assign(title.style, {
fontWeight: "bold",
marginBottom: "10px",
textAlign: "center",
color: STYLE.colors.textSecondary,
font: STYLE.fonts.header
});
parentContainer.appendChild(title);
const dropdownContainer = document.createElement("div");
dropdownContainer.style.marginBottom = "10px";
const dropdown = document.createElement("select");
dropdown.id = "enhancementDropdown";
Object.assign(dropdown.style, {
width: "100%",
padding: "4px",
borderRadius: STYLE.borderRadius.small,
background: STYLE.colors.background,
color: STYLE.colors.textPrimary,
border: STYLE.colors.border
});
dropdown.addEventListener("change", function() {
renderStats(this.value);
updateDropdownColor();
});
dropdownContainer.appendChild(dropdown);
parentContainer.appendChild(dropdownContainer);
const statsContainer = document.createElement("div");
statsContainer.id = "enhancementStatsContainer";
Object.assign(statsContainer.style, {
display: "grid",
gridTemplateColumns: "repeat(4, 1fr)",
gap: "10px",
textAlign: "center",
marginTop: "10px",
font: STYLE.fonts.secondary
});
parentContainer.appendChild(statsContainer);
targetElement.appendChild(parentContainer);
return parentContainer;
}
function updateDropdown(parentContainer) {
const dropdown = parentContainer.querySelector("#enhancementDropdown");
const previousSelectedValue = dropdown.value;
dropdown.innerHTML = "";
Object.keys(enhancementData).forEach(key => {
const item = enhancementData[key];
if (!item["其他数据"]) return;
const option = document.createElement("option");
const itemName = translateItemName(
item["其他数据"]["物品HRID"],
item["其他数据"]["物品名称"]
);
const targetLevel = item["其他数据"]["目标强化等级"] || 0;
const currentLevel = Math.max(...Object.keys(item["强化数据"] || {}).map(Number).filter(n => !isNaN(n))) || 0;
const enhancementState = item["强化状态"] || "";
option.text = isZH
? `${itemName} (目标: ${targetLevel}, 总计: ${item["强化次数"] || 0}${item["其他数据"]["保护消耗总数"] > 0 ? `, 垫子: ${item["其他数据"]["保护消耗总数"]}` : ""})`
: `${itemName} (Target: ${targetLevel}, Total: ${item["强化次数"] || 0}${item["其他数据"]["保护消耗总数"] > 0 ? `, Protectors: ${item["其他数据"]["保护消耗总数"]}` : ""})`;
option.value = key;
option.style.color = enhancementState === "强化成功" ? STYLE.colors.success
: (currentLevel < targetLevel && Object.keys(enhancementData).indexOf(key) === Object.keys(enhancementData).length - 1) ? STYLE.colors.accent
: STYLE.colors.danger;
dropdown.appendChild(option);
});
if (Object.keys(enhancementData).length > 0) {
dropdown.value = previousSelectedValue || Object.keys(enhancementData)[0];
updateDropdownColor();
renderStats(dropdown.value);
}
}
function updateDropdownColor() {
const dropdown = document.querySelector("#enhancementDropdown");
if (!dropdown) return;
const selectedOption = dropdown.options[dropdown.selectedIndex];
dropdown.style.color = selectedOption ? selectedOption.style.color : STYLE.colors.textPrimary;
}
function renderStats(selectedKey) {
const statsContainer = document.querySelector("#enhancementStatsContainer");
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: "320px",
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 elements to header
header.appendChild(titleContainer);
navContainer.appendChild(clearButton);
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();
updateFloatingUI();
}
}
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}) [${currentViewingIndex}]`
: `(${currentPosition}/${totalSessions}) [${currentViewingIndex}]`;
// Visual indicator
sessionCounter.style.color = currentViewingIndex === currentTrackingIndex ?
STYLE.colors.accent : STYLE.colors.textSecondary;
sessionCounter.style.fontWeight = currentViewingIndex === currentTrackingIndex ?
"bold" : "normal";
}
function refreshFloatingUIHeight() {
const header = document.getElementById("enhancementPanelHeader");
const content = document.getElementById("enhancementPanelContent");
if (!header || !content || !floatingUI) return;
const headerHeight = header.offsetHeight;
const contentHeight = content.scrollHeight;
floatingUI.style.height = `min(${headerHeight + contentHeight}px, 80vh)`;
}
// 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};
`;
function updateFloatingUI() {
updateSessionCounter();
const floatingUI = document.getElementById("enhancementFloatingUI") || createFloatingUI();
const content = document.getElementById("enhancementPanelContent");
// Clear existing content
content.innerHTML = '';
if (currentViewingIndex === 0 || !enhancementData[currentViewingIndex]) {
floatingUI.classList.toggle('has-data', false);
content.innerHTML = `
<div class="empty-state">
<div class="empty-icon">✧</div>
<div class="empty-text">${isZH ? "开始强化以记录数据" : "Begin enhancing to populate data"}</div>
</div>
`;
floatingUI.style.height = 'auto';
return;
}
floatingUI.classList.toggle('has-data', true);
const session = enhancementData[currentViewingIndex];
if (!session || !session["其他数据"]) {
content.innerHTML = isZH ? "没有活跃的强化数据" : "No active enhancement data";
floatingUI.style.height = 'auto';
return;
}
// Get market status
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;
// Create main container with fixed height
const container = document.createElement("div");
container.style.display = 'flex';
container.style.flexDirection = 'column';
container.style.height = '100%';
// Create fixed header content with market status
const headerContent = document.createElement("div");
headerContent.style.flexShrink = '0';
headerContent.innerHTML = `
<div style="margin-bottom: 5px; font-size: 12px; color: ${lastMarketUpdate > 0 ? STYLE.colors.success : STYLE.colors.danger};">
${marketStatus}
</div>
<div style="margin-bottom: 10px; font-size: 13px;">
<div style="display: flex; justify-content: space-between;">
<span>${isZH ? "物品" : "Item"}:</span>
<strong>${itemName}</strong>
</div>
<div style="display: flex; justify-content: space-between;">
<span>${isZH ? "目标" : "Target"}:</span>
<span>+${targetLevel}</span>
</div>
</div>
`;
// Create scrollable content area
const scrollContent = document.createElement("div");
scrollContent.style.flexGrow = '1';
scrollContent.style.overflowY = 'auto';
scrollContent.style.minHeight = '333px';
scrollContent.style.maxHeight = '500px';
scrollContent.style.paddingRight = '5px';
// Build content using the proper functions
scrollContent.innerHTML = buildTableHTML(session);
if (session["材料消耗"] && Object.keys(session["材料消耗"]).length > 0) {
scrollContent.innerHTML += generateMaterialCostsHTML(session);
}
// Assemble the UI
container.appendChild(headerContent);
container.appendChild(scrollContent);
content.appendChild(container);
// Set floating UI dimensions
floatingUI.style.height = '600px';
floatingUI.style.minHeight = '150px';
floatingUI.style.maxHeight = '80vh';
floatingUI.style.overflow = 'hidden';
}
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;
return `
<table style="${compactTableStyle}">
<thead>
<tr>
<th style="${compactHeaderStyle}">${isZH ? "等级" : "Lvl"}</th>
<th style="${compactHeaderStyle}">${isZH ? "成功" : "Success"}</th>
<th style="${compactHeaderStyle}">${isZH ? "失败" : "Fail"}</th>
<th style="${compactHeaderStyle}">%</th>
</tr>
</thead>
<tbody>
${Object.keys(session["强化数据"])
.sort((a, b) => b - a)
.map(level => {
const levelData = session["强化数据"][level];
const rate = ((levelData["成功率"] || 0) * 100).toFixed(1);
const isCurrent = (level == enhancementLevel) && (currentTrackingIndex == currentViewingIndex);
return `
<tr ${isCurrent ? `
style="
background: linear-gradient(90deg, rgba(126, 87, 194, 0.25), rgba(0, 242, 255, 0.1));
box-shadow: 0 0 12px rgba(126, 87, 194, 0.5), inset 0 0 6px rgba(0, 242, 255, 0.3);
border-left: 3px solid ${STYLE.colors.accent};
font-weight: bold;
"` : ''}>
<td style="${compactCellStyle} text-align: center;">${level}</td>
<td style="${compactCellStyle} text-align: right;">${levelData["成功次数"] || 0}</td>
<td style="${compactCellStyle} text-align: right;">${levelData["失败次数"] || 0}</td>
<td style="${compactCellStyle} text-align: right;">${rate}%</td>
</tr>`;
}).join('')}
</tbody>
</table>
`;
}
function generateProtectionHTML(session) {
return `
<div style="margin-top: 8px; padding-top: 8px; border-top: 1px dashed ${STYLE.colors.border}">
<div style="display: flex; justify-content: space-between;">
<span>${isZH ? "保护物品" : "Protection Item"}:</span>
<span>${translateItemName(
session["其他数据"]["保护物品HRID"],
session["其他数据"]["保护物品名称"]
)}</span>
</div>
<div style="display: flex; justify-content: space-between;">
<span>${isZH ? "使用数量" : "Used"}:</span>
<span>${session["其他数据"]["保护消耗总数"]}</span>
</div>
<div style="display: flex; justify-content: space-between;">
<span>${isZH ? "保护总成本" : "Protection Cost"}:</span>
<strong style="color: ${STYLE.colors.gold}">${formatNumber(session["其他数据"]["保护总成本"])}</strong>
</div>
</div>
`;
}
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();
updateFloatingUI();
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 = `
<div style="margin-top: 10px; border-top: 1px solid ${STYLE.colors.border}; padding-top: 8px;">
<div style="display: flex; justify-content: space-between; margin-bottom: 5px;">
<span>${isZH ? "材料成本" : "Material Costs"}:</span>
<strong style="color: ${STYLE.colors.gold}; text-shadow: ${STYLE.shadows.gold};">${formatNumber(totalCost)}</strong>
</div>
<table style="${compactTableStyle}">
<thead>
<tr>
<th style="${compactHeaderStyle}">${isZH ? "材料" : "Material"}</th>
<th style="${compactHeaderStyle}">${isZH ? "数量" : "Qty"}</th>
<th style="${compactHeaderStyle}">${isZH ? "成本" : "Cost"}</th>
</tr>
</thead>
<tbody>
${Object.entries(session["材料消耗"])
.sort(([hridA, dataA], [hridB, dataB]) => {
if (hridA.includes('/items/coin')) return 1;
if (hridB.includes('/items/coin')) return -1;
return dataB.totalCost - dataA.totalCost;
})
.map(([hrid, data]) => {
const isCoin = hrid.includes('/items/coin');
return `
<tr>
<td style="${compactCellStyle}">${translateItemName(hrid, data.name)}</td>
<td style="${compactCellStyle} text-align: right;">${isCoin ? '' : data.count}</td>
<td style="${compactCellStyle} text-align: right; color: ${STYLE.colors.gold}">${formatNumber(data.totalCost)}</td>
</tr>
`;
}).join('')}
</tbody>
</table>
</div>
`;
// Add protection cost to the materials display if used
if (session["其他数据"]["保护消耗总数"] > 0) {
html += `
<div style="margin-top: 8px; padding-top: 8px; border-top: 1px dashed ${STYLE.colors.border}">
<div style="display: flex; justify-content: space-between;">
<span>${isZH ? "保护物品" : "Protection Item"}:</span>
<span>${translateItemName(
session["其他数据"]["保护物品HRID"],
session["其他数据"]["保护物品名称"]
)}</span>
</div>
<div style="display: flex; justify-content: space-between;">
<span>${isZH ? "使用数量" : "Used"}:</span>
<span>${session["其他数据"]["保护消耗总数"]}</span>
</div>
<div style="display: flex; justify-content: space-between;">
<span>${isZH ? "保护总成本" : "Protection Cost"}:</span>
<strong style="color: ${STYLE.colors.gold}">${formatNumber(session["其他数据"]["保护总成本"])}</strong>
</div>
</div>
`;
}
return html;
}
function refreshFloatingUIHeight() {
const floatingUI = document.getElementById("enhancementFloatingUI");
if (!floatingUI) return;
// Keep the fixed height but ensure it's within bounds
floatingUI.style.height = '400px';
floatingUI.style.minHeight = '200px';
floatingUI.style.maxHeight = '70vh';
}
function cleanupFloatingUI() {
if (floatingUI && document.body.contains(floatingUI)) {
floatingUI.remove();
}
cleanupFunctions.forEach(fn => fn());
cleanupFunctions = [];
floatingUI = null;
}
// ======================
// 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();
updateFloatingUI();
}
}
}, 500);
}
// Start everything
addGlobalStyles();
initializeTesting();
initializeFloatingUI();
// Initialize market data loading when script starts
loadMarketData();
hookWS();
console.log("Enhancement Notifier v3.2.0 loaded - With Cost Tracking/Session Nav and Persistent Data");
})();