Greasy Fork is available in English.
Tools to assist you in combat droning to appease the Queen RNG.
// ==UserScript==
// @name MWI Combat Suite
// @namespace http://tampermonkey.net/
// @version 0.9.36073
// @description Tools to assist you in combat droning to appease the Queen RNG.
// @author Frotty
// @license MIB License
// @match https://www.milkywayidle.com/*
// @match https://shykai.github.io/MWICombatSimulatorTest/dist/*
// @grant GM_getValue
// @grant GM_setValue
// @grant unsafeWindow
// @run-at document-start
// ==/UserScript==
(function () {
'use strict';
const isShykaiSite = window.location.hostname === 'shykai.github.io';
const isMWISite = window.location.hostname.includes('milkywayidle.com');
window.MCS_MODULES_DISABLED = false;
window.MCS_MODULES_INITIALIZED = false;
window.MCS_ALL_PANEL_IDS = [];
if (!window.MCS_CHARACTER_DATA_CACHE) {
window.MCS_CHARACTER_DATA_CACHE = null;
}
if (!window.CharacterDataStorage) {
window.CharacterDataStorage = {
syncFromBridge() {
try {
const bridge = document.getElementById('equipspy-data-bridge');
if (bridge) {
const fullData = bridge.getAttribute('data-character-full');
if (fullData) {
const parsed = JSON.parse(fullData);
const newCharName = parsed?.character?.name;
const cachedCharName = window.MCS_CHARACTER_DATA_CACHE?.character?.name;
if (!window.MCS_CHARACTER_DATA_CACHE || (newCharName && newCharName !== cachedCharName)) {
window.MCS_CHARACTER_DATA_CACHE = parsed;
console.log('[MCS] Character loaded:', newCharName);
}
}
}
} catch (e) {
console.error('[CharacterDataStorage] Error syncing from bridge:', e);
}
},
getCurrentCharacterName() {
this.syncFromBridge();
if (window.MCS_CHARACTER_DATA_CACHE?.character?.name) {
return window.MCS_CHARACTER_DATA_CACHE.character.name;
}
return null;
},
get(characterName) {
return window.MCS_CHARACTER_DATA_CACHE || null;
},
set(value, characterName) {
if (typeof value === 'string') {
try {
window.MCS_CHARACTER_DATA_CACHE = JSON.parse(value);
} catch (e) {
console.error('[CharacterDataStorage] Error parsing data:', e);
}
} else if (value && typeof value === 'object') {
window.MCS_CHARACTER_DATA_CACHE = value;
}
},
getParsed(characterName) {
return window.MCS_CHARACTER_DATA_CACHE || null;
},
setParsed(obj, characterName) {
window.MCS_CHARACTER_DATA_CACHE = obj;
}
};
}
if (!window.ToolVisibilityStorage) {
window.ToolVisibilityStorage = {
getKey(characterName) {
const charName = characterName || window.CharacterDataStorage.getCurrentCharacterName();
return charName ? 'mcs__global_visibility_' + charName : 'mcs__global_visibility';
},
get(characterName) {
const key = this.getKey(characterName);
const data = localStorage.getItem(key);
try {
return data ? JSON.parse(data) : {};
} catch (e) {
console.error('[ToolVisibilityStorage] Error parsing visibility data:', e);
return {};
}
},
set(obj, characterName) {
const key = this.getKey(characterName);
try {
localStorage.setItem(key, JSON.stringify(obj));
} catch (e) {
console.error('[ToolVisibilityStorage] Error saving visibility data:', e);
}
}
};
}
if (!window.MCSEnabledStorage) {
window.MCSEnabledStorage = {
get(characterName) {
try {
const data = JSON.parse(localStorage.getItem('mcs__global_enabled') || '{}');
return data[characterName] !== false;
} catch (e) { return true; }
},
set(characterName, enabled) {
try {
const data = JSON.parse(localStorage.getItem('mcs__global_enabled') || '{}');
data[characterName] = enabled;
localStorage.setItem('mcs__global_enabled', JSON.stringify(data));
} catch (e) {}
}
};
}
if (!window.CharacterDataStorage) {
window.CharacterDataStorage = CharacterDataStorage;
}
window.ToolVisibilityStorage = ToolVisibilityStorage;
window.MCSEnabledStorage = MCSEnabledStorage;
try {
const currentCharacter = CharacterDataStorage.get();
const characterName = currentCharacter?.character?.name || null;
if (characterName) {
const sessionKey = 'mcs__global_pause_session_' + characterName;
const savedSession = localStorage.getItem(sessionKey);
if (savedSession) {
try {
const sessionData = JSON.parse(savedSession);
window.MCS_TOTAL_PAUSED_MS = sessionData.totalPausedMs ?? 0;
} catch (e) {
window.MCS_TOTAL_PAUSED_MS = 0;
}
} else {
window.MCS_TOTAL_PAUSED_MS = 0;
localStorage.setItem(sessionKey, JSON.stringify({ totalPausedMs: 0 }));
}
} else {
window.MCS_TOTAL_PAUSED_MS = 0;
}
} catch (e) {
window.MCS_TOTAL_PAUSED_MS = 0;
}
window.MCS_PAUSE_START_TIME = null;
window.MCS_TAB_HIDDEN_START = null;
window.MCS_TOTAL_TAB_HIDDEN_MS = 0;
function checkURL() {
if (document.hidden) return;
const currentURL = window.location.href;
const isCharacterSelectOrHome = currentURL === 'https://www.milkywayidle.com/characterSelect' ||
currentURL === 'https://www.milkywayidle.com/';
if (isCharacterSelectOrHome && !window.MCS_MODULES_DISABLED) {
if (window.MCS_MODULES_INITIALIZED) {
window.MCS_MODULES_DISABLED = true;
window.MCS_PAUSE_START_TIME = Date.now();
window.MCS_ALL_PANEL_IDS.forEach(panelId => {
const panel = document.getElementById(panelId);
if (panel) {
if (panel.classList.contains('visible')) {
panel.dataset.hadVisibleClass = 'true';
panel.classList.remove('visible');
}
panel.style.setProperty('display', 'none', 'important');
}
});
}
} else if (!isCharacterSelectOrHome && window.MCS_MODULES_DISABLED) {
if (window.MCS_PAUSE_START_TIME) {
const pauseDuration = Date.now() - window.MCS_PAUSE_START_TIME;
window.MCS_TOTAL_PAUSED_MS += pauseDuration;
try {
const currentCharacter = CharacterDataStorage.get();
const characterName = currentCharacter?.character?.name || null;
if (characterName) {
const sessionKey = 'mcs__global_pause_session_' + characterName;
localStorage.setItem(sessionKey, JSON.stringify({ totalPausedMs: window.MCS_TOTAL_PAUSED_MS }));
}
} catch (e) {
console.error('[MCS] Failed to save pause time:', e);
}
}
window.MCS_MODULES_DISABLED = false;
window.MCS_PAUSE_START_TIME = null;
window.MCS_ALL_PANEL_IDS.forEach(panelId => {
const panel = document.getElementById(panelId);
if (panel) {
if (panelId === 'mwi-combat-suite-panel') {
panel.classList.add('visible');
panel.style.removeProperty('display');
} else {
if (panel.dataset.hadVisibleClass === 'true') {
panel.classList.add('visible');
delete panel.dataset.hadVisibleClass;
}
panel.style.removeProperty('display');
}
}
});
}
return isCharacterSelectOrHome;
}
const isInitiallyPaused = checkURL();
if (isInitiallyPaused) {
}
setInterval(checkURL, 500);
window.mcs__global_equipment_tracker = {
allCharacterItems: null,
lastEquippedHash: null,
playerName: null,
processEquipmentTimeout: null,
init() {
console.log('[MCS] Initializing...');
this.listenForEvents();
},
listenForEvents() {
const tracker = this;
tracker._wsListener = (e) => {
const obj = e.detail;
if (!obj) return;
if (obj.type === 'battle_updated') {
window.MCS_IN_COMBAT = true;
}
try {
if (obj.type === 'items_updated' && obj.endCharacterItems && Array.isArray(obj.endCharacterItems)) {
const equippedUpdates = obj.endCharacterItems.filter(
item => item.itemLocationHrid && item.itemLocationHrid !== '/item_locations/inventory'
);
if (!tracker.allCharacterItems) {
tracker.allCharacterItems = [];
}
for (const updatedItem of obj.endCharacterItems) {
const existingIndex = tracker.allCharacterItems.findIndex(
item => item.itemHrid === updatedItem.itemHrid &&
item.itemLocationHrid === updatedItem.itemLocationHrid
);
if (existingIndex >= 0) {
tracker.allCharacterItems[existingIndex] = updatedItem;
} else {
tracker.allCharacterItems.push(updatedItem);
}
}
if (equippedUpdates.length > 0) {
tracker.processEquipment();
}
}
} catch (e) {
}
};
window.addEventListener('EquipSpyWebSocketMessage', tracker._wsListener);
tracker._charDataListener = (e) => {
const obj = e.detail;
if (!obj) return;
try {
if (obj.characterItems && Array.isArray(obj.characterItems)) {
tracker.allCharacterItems = obj.characterItems;
if (obj.character && obj.character.name) {
tracker.playerName = obj.character.name;
}
tracker.processEquipment();
}
} catch (e) {
}
};
window.addEventListener('LootTrackerCharacterData', tracker._charDataListener);
},
removeListeners() {
if (this._wsListener) {
window.removeEventListener('EquipSpyWebSocketMessage', this._wsListener);
this._wsListener = null;
}
if (this._charDataListener) {
window.removeEventListener('LootTrackerCharacterData', this._charDataListener);
this._charDataListener = null;
}
},
getEquippedItems() {
if (!this.allCharacterItems) return null;
const equipped = [];
for (const item of this.allCharacterItems) {
if (item.itemLocationHrid &&
item.itemLocationHrid !== '/item_locations/inventory' &&
item.count > 0) {
equipped.push(item);
}
}
return equipped;
},
buildEquipmentSlotMap() {
const equipped = this.getEquippedItems();
if (!equipped) return null;
const equipmentSlots = {
'/item_locations/head': 'Nothing',
'/item_locations/body': 'Nothing',
'/item_locations/legs': 'Nothing',
'/item_locations/hands': 'Nothing',
'/item_locations/feet': 'Nothing',
'/item_locations/main_hand': 'Nothing',
'/item_locations/off_hand': 'Nothing',
'/item_locations/pouch': 'Nothing',
'/item_locations/neck': 'Nothing',
'/item_locations/earrings': 'Nothing',
'/item_locations/ring': 'Nothing',
'/item_locations/back': 'Nothing',
'/item_locations/arrows': 'Nothing'
};
for (const item of equipped) {
if (equipmentSlots.hasOwnProperty(item.itemLocationHrid)) {
let itemName = item.itemHrid?.split('/').pop() || 'Unknown';
if (item.enhancementLevel > 0) {
itemName += ` +${item.enhancementLevel}`;
}
equipmentSlots[item.itemLocationHrid] = itemName;
}
}
return equipmentSlots;
},
processEquipment() {
if (window.MCS_MODULES_DISABLED) return;
if (this.processEquipmentTimeout) {
clearTimeout(this.processEquipmentTimeout);
}
this.processEquipmentTimeout = setTimeout(() => {
if (window.MCS_MODULES_DISABLED) return;
this.processEquipmentTimeout = null;
const equipped = this.getEquippedItems();
if (!equipped || !this.playerName) return;
const currentHash = JSON.stringify(equipped);
if (currentHash !== this.lastEquippedHash) {
this.lastEquippedHash = currentHash;
const storageKey = `mcs__global_equipment_${this.playerName}`;
localStorage.setItem(storageKey, JSON.stringify(equipped));
window[storageKey] = equipped;
const slotMap = this.buildEquipmentSlotMap();
for (const [slot, item] of Object.entries(slotMap)) {
const slotName = slot.split('/').pop().padEnd(15);
}
window.dispatchEvent(new CustomEvent('MCS_EquipmentChanged', {
detail: { equipped, playerName: this.playerName }
}));
}
}, 50);
}
};
window.mcs__global_equipment_tracker.init();
const VisibilityManager = {
intervals: new Map(),
isVisible: !document.hidden,
init() {
document.addEventListener('visibilitychange', () => {
this.isVisible = !document.hidden;
if (this.isVisible) {
if (window.MCS_TAB_HIDDEN_START) {
const hiddenDuration = Date.now() - window.MCS_TAB_HIDDEN_START;
window.MCS_TOTAL_PAUSED_MS += hiddenDuration;
window.MCS_TOTAL_TAB_HIDDEN_MS += hiddenDuration;
window.MCS_TAB_HIDDEN_START = null;
}
this.resumeAll();
} else {
window.MCS_TAB_HIDDEN_START = Date.now();
this.pauseAll();
}
});
},
register(name, callback, delay) {
if (this.intervals.has(name)) {
this.clear(name);
}
const wrappedCallback = PerformanceMonitor.wrap(name, callback);
const interval = {
callback: wrappedCallback,
delay,
intervalId: null,
paused: false
};
if (this.isVisible) {
interval.intervalId = setInterval(wrappedCallback, delay);
} else {
interval.paused = true;
}
this.intervals.set(name, interval);
return name;
},
clear(name) {
const interval = this.intervals.get(name);
if (interval) {
if (interval.intervalId) {
clearInterval(interval.intervalId);
}
this.intervals.delete(name);
}
},
pauseAll() {
for (const [name, interval] of this.intervals) {
if (interval.intervalId) {
clearInterval(interval.intervalId);
interval.intervalId = null;
interval.paused = true;
}
}
},
resumeAll() {
for (const [name, interval] of this.intervals) {
if (interval.paused) {
interval.intervalId = setInterval(interval.callback, interval.delay);
interval.paused = false;
}
}
}
};
VisibilityManager.init();
const PerformanceMonitor = {
enabled: false,
modules: new Map(),
measurementWindow: 5000,
init() {
setInterval(() => {
if (document.hidden) return;
if (this.enabled) this.cleanupOldMeasurements();
}, 1000);
},
cleanupOldMeasurements() {
const now = performance.now();
const cutoff = now - this.measurementWindow;
for (const [name, data] of this.modules) {
data.measurements = data.measurements.filter(m => m.time >= cutoff);
}
},
wrap(moduleName, fn) {
return (...args) => {
if (!this.enabled) return fn(...args);
const start = performance.now();
try {
const result = fn(...args);
if (result && typeof result.then === 'function') {
return result.finally(() => {
const elapsed = performance.now() - start;
this.record(moduleName, elapsed);
});
}
const elapsed = performance.now() - start;
this.record(moduleName, elapsed);
return result;
} catch (error) {
const elapsed = performance.now() - start;
this.record(moduleName, elapsed);
throw error;
}
};
},
record(moduleName, elapsedMs) {
if (!this.modules.has(moduleName)) {
this.modules.set(moduleName, {
measurements: []
});
}
const data = this.modules.get(moduleName);
data.measurements.push({
time: performance.now(),
duration: elapsedMs
});
},
getCpuPercent(moduleName) {
const data = this.modules.get(moduleName);
if (!data || data.measurements.length === 0) return 0;
const totalTime = data.measurements.reduce((sum, m) => sum + m.duration, 0);
const cpuPercent = (totalTime / this.measurementWindow) * 100;
return Math.min(cpuPercent, 100);
},
getAllStats() {
const stats = {};
for (const [name, data] of this.modules) {
const totalTime = data.measurements.reduce((sum, m) => sum + m.duration, 0);
const callCount = data.measurements.length;
stats[name] = {
cpuPercent: this.getCpuPercent(name),
totalTime: totalTime,
callCount: callCount,
avgTime: callCount > 0 ? totalTime / callCount : 0
};
}
return stats;
}
};
PerformanceMonitor.init();
const StorageMonitor = {
enabled: false,
keys: new Map(),
measurementWindow: 5000,
init() {
const originalGetItem = Storage.prototype.getItem;
Storage.prototype.getItem = function(key) {
if (StorageMonitor.enabled) StorageMonitor.recordRead(key);
return originalGetItem.call(this, key);
};
const originalSetItem = Storage.prototype.setItem;
Storage.prototype.setItem = function(key, value) {
if (StorageMonitor.enabled) StorageMonitor.recordWrite(key);
return originalSetItem.call(this, key, value);
};
if (typeof GM_getValue !== 'undefined') {
const originalGMGet = GM_getValue;
GM_getValue = function(key, defaultValue) {
if (StorageMonitor.enabled) StorageMonitor.recordRead('GM:' + key);
return originalGMGet(key, defaultValue);
};
}
if (typeof GM_setValue !== 'undefined') {
const originalGMSet = GM_setValue;
GM_setValue = function(key, value) {
if (StorageMonitor.enabled) StorageMonitor.recordWrite('GM:' + key);
return originalGMSet(key, value);
};
}
setInterval(() => {
if (document.hidden) return;
if (this.enabled) this.cleanupOldMeasurements();
}, 1000);
},
recordRead(key) {
if (!this.keys.has(key)) {
this.keys.set(key, { reads: [], writes: [] });
}
this.keys.get(key).reads.push({ time: performance.now() });
},
recordWrite(key) {
if (!this.keys.has(key)) {
this.keys.set(key, { reads: [], writes: [] });
}
this.keys.get(key).writes.push({ time: performance.now() });
},
cleanupOldMeasurements() {
const now = performance.now();
const cutoff = now - this.measurementWindow;
for (const [key, data] of this.keys) {
data.reads = data.reads.filter(r => r.time >= cutoff);
data.writes = data.writes.filter(w => w.time >= cutoff);
if (data.reads.length === 0 && data.writes.length === 0) {
this.keys.delete(key);
}
}
},
getAllStats() {
const stats = {};
for (const [key, data] of this.keys) {
stats[key] = {
reads: data.reads.length,
writes: data.writes.length
};
}
return stats;
}
};
StorageMonitor.init();
function mcsGoToMarketplace(itemHrid) {
function getGameObject() {
const rootEl = document.getElementById('root');
const rootFiber = rootEl?._reactRootContainer?.current
|| rootEl?._reactRootContainer?._internalRoot?.current;
if (!rootFiber) return null;
function find(fiber) {
if (!fiber) return null;
if (fiber.stateNode?.handleGoToMarketplace) return fiber.stateNode;
return find(fiber.child) || find(fiber.sibling);
}
return find(rootFiber);
}
const game = getGameObject();
if (game?.handleGoToMarketplace) {
game.handleGoToMarketplace(itemHrid, 0);
}
}
const StorageUtils = {
save(key, value) {
try {
localStorage.setItem(key, JSON.stringify(value));
return true;
} catch (e) {
return false;
}
},
load(key, defaultValue = null) {
try {
const item = localStorage.getItem(key);
return item ? JSON.parse(item) : defaultValue;
} catch (e) {
return defaultValue;
}
},
remove(key) {
try {
localStorage.removeItem(key);
return true;
} catch (e) {
return false;
}
}
};
const DragHandler = {
makeDraggable(pane, header, storageKey) {
let dragOffset = { x: 0, y: 0 };
const isModuleStorage = storageKey.match(/^mcs_[A-Z]{2}$/);
let savedPos;
if (isModuleStorage) {
const modulePrefix = storageKey.replace('mcs_', '');
const storage = createModuleStorage(modulePrefix);
savedPos = storage.get('position');
} else {
savedPos = CharacterStorageUtils.load(storageKey);
}
if (savedPos) {
if (savedPos.top !== undefined) pane.style.top = savedPos.top + 'px';
if (savedPos.left !== undefined) {
pane.style.left = savedPos.left + 'px';
pane.style.right = 'auto';
}
}
header.classList.add('mcs-cursor-move');
const onDragMove = (e) => {
let newLeft = e.clientX - dragOffset.x;
let newTop = e.clientY - dragOffset.y;
const headerRect = header.getBoundingClientRect();
const headerHeight = headerRect.height;
const paneRect = pane.getBoundingClientRect();
const minLeft = -(paneRect.width - 50);
const maxLeft = window.innerWidth - 50;
const minTop = 0;
const maxTop = window.innerHeight - headerHeight;
newLeft = Math.max(minLeft, Math.min(maxLeft, newLeft));
newTop = Math.max(minTop, Math.min(maxTop, newTop));
pane.style.left = newLeft + 'px';
pane.style.top = newTop + 'px';
pane.style.right = 'auto';
};
const onDragUp = () => {
document.removeEventListener('mousemove', onDragMove);
document.removeEventListener('mouseup', onDragUp);
header.classList.remove('mcs-cursor-grabbing');
header.classList.add('mcs-cursor-move');
const rect = pane.getBoundingClientRect();
const posData = {
top: rect.top,
left: rect.left
};
if (isModuleStorage) {
const modulePrefix = storageKey.replace('mcs_', '');
const storage = createModuleStorage(modulePrefix);
storage.set('position', posData);
} else {
CharacterStorageUtils.save(storageKey, posData);
}
};
header.addEventListener('mousedown', (e) => {
const rect = pane.getBoundingClientRect();
dragOffset.x = e.clientX - rect.left;
dragOffset.y = e.clientY - rect.top;
header.classList.remove('mcs-cursor-move');
header.classList.add('mcs-cursor-grabbing');
document.addEventListener('mousemove', onDragMove);
document.addEventListener('mouseup', onDragUp);
});
}
};
const ResizeHandler = {
makeResizable(pane, storageKey) {
const corners = [
{ name: 'nw', cursor: 'nw-resize' },
{ name: 'ne', cursor: 'ne-resize' },
{ name: 'sw', cursor: 'sw-resize' },
{ name: 'se', cursor: 'se-resize' }
];
corners.forEach(corner => {
const handle = document.createElement('div');
handle.className = `mcs-resize-handle mcs-resize-handle-${corner.name}`;
pane.appendChild(handle);
let startX, startY, startWidth, startHeight, startTop, startLeft;
const onResizeMove = (e) => {
const dx = e.clientX - startX;
const dy = e.clientY - startY;
let newWidth = startWidth;
let newHeight = startHeight;
let newTop = startTop;
let newLeft = startLeft;
if (corner.name.includes('e')) {
newWidth = startWidth + dx;
} else if (corner.name.includes('w')) {
newWidth = startWidth - dx;
newLeft = startLeft + dx;
}
if (corner.name.includes('s')) {
newHeight = startHeight + dy;
} else if (corner.name.includes('n')) {
newHeight = startHeight - dy;
newTop = startTop + dy;
}
if (newWidth >= 200) {
pane.style.width = newWidth + 'px';
if (corner.name.includes('w')) {
pane.style.left = newLeft + 'px';
pane.style.right = 'auto';
}
}
if (newHeight >= 100) {
pane.style.height = newHeight + 'px';
if (corner.name.includes('n')) {
pane.style.top = newTop + 'px';
}
}
};
const onResizeUp = () => {
document.removeEventListener('mousemove', onResizeMove);
document.removeEventListener('mouseup', onResizeUp);
document.body.style.cursor = '';
const rect = pane.getBoundingClientRect();
const sizeKey = storageKey ? `${storageKey}_size` : null;
if (sizeKey) {
CharacterStorageUtils.save(sizeKey, {
width: rect.width,
height: rect.height
});
}
if (storageKey && (corner.name.includes('w') || corner.name.includes('n'))) {
CharacterStorageUtils.save(storageKey, {
top: rect.top,
left: rect.left
});
}
};
handle.addEventListener('mousedown', (e) => {
e.preventDefault();
e.stopPropagation();
startX = e.clientX;
startY = e.clientY;
const rect = pane.getBoundingClientRect();
startWidth = rect.width;
startHeight = rect.height;
startTop = rect.top;
startLeft = rect.left;
document.body.style.cursor = corner.cursor;
document.addEventListener('mousemove', onResizeMove);
document.addEventListener('mouseup', onResizeUp);
});
});
const sizeKey = storageKey ? `${storageKey}_size` : null;
if (sizeKey) {
const savedSize = StorageUtils.load(sizeKey);
if (savedSize) {
if (savedSize.width !== undefined) pane.style.width = savedSize.width + 'px';
if (savedSize.height !== undefined) pane.style.height = savedSize.height + 'px';
}
}
}
};
const CharacterStorageUtils = {
_cachedPlayerKey: null,
getPlayerKey() {
try {
const cachedData = CharacterDataStorage.get();
if (cachedData) {
const name = cachedData?.character?.name;
if (name) {
if (this._cachedPlayerKey && this._cachedPlayerKey !== name) {
this._clearAllModuleCaches();
}
this._cachedPlayerKey = name;
return name;
}
}
if (typeof GM_getValue !== 'undefined') {
const gmCharData = GM_getValue('init_character_data', null);
if (gmCharData) {
const parsed = JSON.parse(gmCharData);
const name = parsed?.character?.name;
if (name) {
if (this._cachedPlayerKey && this._cachedPlayerKey !== name) {
this._clearAllModuleCaches();
}
this._cachedPlayerKey = name;
return name;
}
}
}
return 'default';
} catch (e) {
console.warn('[MCS] Failed to get player key:', e);
return 'default';
}
},
_clearAllModuleCaches() {
if (window._moduleStorageInstances) {
for (const key in window._moduleStorageInstances) {
if (window._moduleStorageInstances[key] && typeof window._moduleStorageInstances[key].clearCache === 'function') {
window._moduleStorageInstances[key].clearCache();
}
}
}
},
clearCache() {
this._cachedPlayerKey = null;
},
save(key, value) {
const playerKey = this.getPlayerKey();
const fullKey = `${key}_${playerKey}`;
return StorageUtils.save(fullKey, value);
},
load(key, defaultValue = null) {
const playerKey = this.getPlayerKey();
const fullKey = `${key}_${playerKey}`;
return StorageUtils.load(fullKey, defaultValue);
},
remove(key) {
const playerKey = this.getPlayerKey();
const fullKey = `${key}_${playerKey}`;
return StorageUtils.remove(fullKey);
},
setItem(key, value) {
try {
const playerKey = this.getPlayerKey();
if (playerKey === 'default') {
localStorage.setItem(key, value);
}
const fullKey = `${key}_${playerKey}`;
localStorage.setItem(fullKey, value);
return true;
} catch (e) {
return false;
}
},
getItem(key, defaultValue = null) {
try {
const playerKey = this.getPlayerKey();
const fullKey = `${key}_${playerKey}`;
const value = localStorage.getItem(fullKey);
return value !== null ? value : defaultValue;
} catch (e) {
return defaultValue;
}
},
removeItem(key) {
try {
const playerKey = this.getPlayerKey();
const fullKey = `${key}_${playerKey}`;
localStorage.removeItem(fullKey);
return true;
} catch (e) {
return false;
}
}
};
if (!window._moduleStorageInstances) {
window._moduleStorageInstances = {};
}
const _moduleStorageInstances = window._moduleStorageInstances;
window.createModuleStorage = function(modulePrefix) {
if (_moduleStorageInstances[modulePrefix]) {
return _moduleStorageInstances[modulePrefix];
}
let cache = null;
const instance = {
_load() {
if (!cache) {
const key = `mcs_${modulePrefix}`;
const data = CharacterStorageUtils.getItem(key);
cache = data ? JSON.parse(data) : {};
}
return cache;
},
get(key, defaultValue = null) {
const data = this._load();
return data[key] !== undefined ? data[key] : defaultValue;
},
set(key, value) {
const data = this._load();
data[key] = value;
cache = data;
CharacterStorageUtils.setItem(`mcs_${modulePrefix}`, JSON.stringify(data));
},
update(changes) {
const data = this._load();
Object.assign(data, changes);
cache = data;
CharacterStorageUtils.setItem(`mcs_${modulePrefix}`, JSON.stringify(data));
},
clearCache() {
cache = null;
}
};
_moduleStorageInstances[modulePrefix] = instance;
return instance;
};
const InitClientDataCache = {
_cachedData: null,
get() {
if (this._cachedData) {
return this._cachedData;
}
try {
if (typeof localStorageUtil === 'undefined' || !localStorageUtil) {
return null;
}
const clientData = localStorageUtil.getInitClientData();
if (!clientData) return null;
this._cachedData = clientData;
return clientData;
} catch (e) {
console.error('[InitClientDataCache] Error loading initClientData:', e);
return null;
}
},
clearCache() {
this._cachedData = null;
},
getAbilityDetailMap() {
try {
const clientData = this.get();
return clientData?.abilityDetailMap || {};
} catch (e) {
console.error('[InitClientDataCache] Error loading abilityDetailMap:', e);
return {};
}
},
getLevelExperienceTable() {
try {
const clientData = this.get();
return clientData?.levelExperienceTable || null;
} catch (e) {
console.error('[InitClientDataCache] Error loading levelExperienceTable:', e);
return null;
}
},
getItemDetailMap() {
try {
const clientData = this.get();
return clientData?.itemDetailMap || {};
} catch (e) {
console.error('[InitClientDataCache] Error loading itemDetailMap:', e);
return {};
}
},
getHouseRoomDetailMap() {
try {
const clientData = this.get();
return clientData?.houseRoomDetailMap || {};
} catch (e) {
console.error('[InitClientDataCache] Error loading houseRoomDetailMap:', e);
return {};
}
},
getActionDetailMap() {
try {
const clientData = this.get();
return clientData?.actionDetailMap || {};
} catch (e) {
console.error('[InitClientDataCache] Error loading actionDetailMap:', e);
return {};
}
},
getCombatMonsterDetailMap() {
try {
const clientData = this.get();
return clientData?.combatMonsterDetailMap || {};
} catch (e) {
console.error('[InitClientDataCache] Error loading combatMonsterDetailMap:', e);
return {};
}
}
};
function registerPanel(panelId) {
if (window.MCS_ALL_PANEL_IDS && !window.MCS_ALL_PANEL_IDS.includes(panelId)) {
window.MCS_ALL_PANEL_IDS.push(panelId);
}
}
function wrapPaneRemove(pane) {
if (pane && !pane._mcsRemoveWrapped) {
const originalRemove = pane.remove.bind(pane);
pane.remove = function() {
cleanupPaneListeners(pane);
originalRemove();
};
pane._mcsRemoveWrapped = true;
}
}
function cleanupPaneListeners(pane) {
if (pane && pane._mcsCleanupListeners) {
pane._mcsCleanupListeners.forEach(fn => fn());
pane._mcsCleanupListeners = null;
}
}
const FORMAT_PRESETS = {
price: { decimals: [2, 2, 1, 0], units: 'mixed', zero: '' },
short: { decimals: [1, 1, 1, 1], units: 'lower', zero: '-' },
precise: { decimals: [3, 3, 3, 3], units: 'upper', zero: '0' },
abbreviated: { decimals: [1, 1, 1, 0], units: 'mixed', zero: '0' },
cost: { decimals: [2, 2, 1, 0], units: 'upper', zero: '0', localeSub: true },
dps: { decimals: [2, 2, 2, 0], units: 'upper', zero: '0', localeSub: true },
compact: { decimals: [0, 1, 1, 0], units: 'mixed', zero: '0' },
detailed: { decimals: [0, 3, 3, 3], units: 'upper', zero: '0' },
gold: { decimals: [0, 1, 0, 0], units: 'upper', zero: '0' },
ihurt: { decimals: [0, 2, 1, 0], units: 'upper', zero: '0' },
ntally: { decimals: [0, 2, 1, 0], units: 'upper', zero: '0', localeSub: true },
};
const UNIT_MAP = {
upper: { b: 'B', m: 'M', k: 'K' },
lower: { b: 'b', m: 'm', k: 'k' },
mixed: { b: 'B', m: 'M', k: 'k' },
};
function mcsFormatCurrency(value, opts) {
if (typeof opts === 'string') opts = FORMAT_PRESETS[opts] || FORMAT_PRESETS.price;
const { decimals = [2, 2, 1, 0], units = 'mixed', zero = '', localeSub = false } = opts;
const u = UNIT_MAP[units] || UNIT_MAP.mixed;
if (value === null || value === undefined) return zero;
if (!value) return zero;
const abs = Math.abs(value);
const sign = value < 0 ? '-' : '';
if (abs >= 1e9) return sign + (abs / 1e9).toFixed(decimals[0]) + u.b;
if (abs >= 1e6) return sign + (abs / 1e6).toFixed(decimals[1]) + u.m;
if (abs >= 1e3) return sign + (abs / 1e3).toFixed(decimals[2]) + u.k;
if (localeSub) return sign + Math.round(abs).toLocaleString();
return sign + abs.toFixed(decimals[3]);
}
window.mcsFormatCurrency = mcsFormatCurrency;
let _marketDataCache = null;
let _marketDataRaw = null;
function mcsGetMarketData() {
try {
const raw = localStorage.getItem('mcs__global_marketAPI_json');
if (!raw) return _marketDataCache;
if (raw === _marketDataRaw) return _marketDataCache;
_marketDataRaw = raw;
_marketDataCache = JSON.parse(raw);
} catch (e) {
}
return _marketDataCache;
}
window.mcsGetMarketData = mcsGetMarketData;
const _spriteCache = {};
const _spriteDefaults = {
items: '/game-icons/items.svg',
items_sprite: '/static/media/items_sprite.328d6606.svg',
skills: '/static/media/skills_sprite.3bb4d936.svg',
};
const _spriteSelectors = {
items: 'svg use[href*="items"]',
items_sprite: 'svg use[href*="items_sprite"]',
skills: 'svg use[href*="skills_sprite"]',
};
function getIconSpriteUrl(sprite = 'items') {
if (_spriteCache[sprite]) return _spriteCache[sprite];
let url = _spriteDefaults[sprite] || _spriteDefaults.items;
try {
const existing = document.querySelector(_spriteSelectors[sprite] || _spriteSelectors.items);
if (existing) {
const href = existing.getAttribute('href');
if (href && href.includes('#')) url = href.split('#')[0];
}
} catch (e) { /* DOM query for sprite URL may fail before game loads */ }
_spriteCache[sprite] = url;
return url;
}
function createItemIcon(iconId, { width = 20, height = 20, className, clickable = false, sprite = 'items', itemHrid, title } = {}) {
const url = getIconSpriteUrl(sprite);
const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
svg.setAttribute('width', String(width));
svg.setAttribute('height', String(height));
if (className) svg.setAttribute('class', className);
if (clickable) {
svg.classList.add('mcs-clickable');
if (itemHrid) svg.setAttribute('data-item-hrid', itemHrid);
if (!title) title = 'Click to open in marketplace';
}
if (title) svg.setAttribute('title', title);
const use = document.createElementNS('http://www.w3.org/2000/svg', 'use');
use.setAttribute('href', url + '#' + iconId);
svg.appendChild(use);
return svg;
}
function createItemIconHtml(iconId, { width = 20, height = 20, className, clickable = false, sprite = 'items', itemHrid, style, title } = {}) {
const url = getIconSpriteUrl(sprite);
let attrs = `width="${width}" height="${height}"`;
const classes = [];
if (className) classes.push(className);
if (clickable) classes.push('mcs-clickable');
if (classes.length) attrs += ` class="${classes.join(' ')}"`;
const styles = [];
if (style) styles.push(style);
if (styles.length) attrs += ` style="${styles.join('; ')}"`;
if (clickable && itemHrid) attrs += ` data-item-hrid="${itemHrid}"`;
if (clickable && !title) title = 'Click to open in marketplace';
if (title) attrs += ` title="${title}"`;
return `<svg ${attrs}><use href="${url}#${iconId}"></use></svg>`;
}
window.getIconSpriteUrl = getIconSpriteUrl;
window.createItemIcon = createItemIcon;
window.createItemIconHtml = createItemIconHtml;
function mcsFormatDuration(seconds, format = 'clock') {
if (seconds === null || seconds === undefined || isNaN(seconds) || seconds < 0) seconds = 0;
const d = Math.floor(seconds / 86400);
const h = Math.floor((seconds % 86400) / 3600);
const m = Math.floor((seconds % 3600) / 60);
const s = Math.floor(seconds % 60);
if (format === 'eta') {
if (seconds >= 86400) return (seconds / 86400).toFixed(1) + 'd';
if (seconds >= 3600) return (seconds / 3600).toFixed(1) + 'h';
return (seconds / 60).toFixed(1) + 'm';
}
if (format === 'short') {
if (seconds < 60) return s + 's';
if (seconds < 3600) return Math.floor(seconds / 60) + 'm';
if (seconds < 86400) return Math.floor(seconds / 3600) + 'h';
return Math.floor(seconds / 86400) + 'd';
}
if (format === 'compact') {
if (seconds <= 0) return 'Ready!';
const mo = Math.floor(seconds / (30 * 86400));
const cd = Math.floor((seconds % (30 * 86400)) / 86400);
const ch = Math.floor((seconds % 86400) / 3600);
const cm = Math.floor((seconds % 3600) / 60);
const parts = [];
if (mo > 0) parts.push(mo + 'mo');
if (cd > 0) parts.push(cd + 'd');
if (ch > 0 && parts.length < 2) parts.push(ch + 'h');
if (cm > 0 && parts.length < 2) parts.push(cm + 'm');
return parts.slice(0, 2).join(' ');
}
if (format === 'elapsed') {
const hp = String(h).padStart(2, '0');
const mp = String(m).padStart(2, '0');
const sp = String(s).padStart(2, '0');
return d > 0 ? d + 'd ' + hp + ':' + mp + ':' + sp : hp + ':' + mp + ':' + sp;
}
return String(h + d * 24).padStart(2, '0') + ':' + String(m).padStart(2, '0') + ':' + String(s).padStart(2, '0');
}
window.mcsFormatDuration = mcsFormatDuration;
function mcsGetElapsedSeconds(startTime, endTime, savedPausedMs, accumulatedDuration, skipTabHidden, savedTabHiddenMs) {
if (!startTime) return accumulatedDuration || 0;
const now = endTime || Date.now();
const saved = savedPausedMs ?? 0;
let totalPausedMs = (window.MCS_TOTAL_PAUSED_MS ?? 0) - saved;
const currentPausedMs = window.MCS_MODULES_DISABLED && window.MCS_PAUSE_START_TIME
? (Date.now() - window.MCS_PAUSE_START_TIME) : 0;
let tabHiddenMs = 0;
if (skipTabHidden) {
const savedTH = savedTabHiddenMs ?? 0;
totalPausedMs -= ((window.MCS_TOTAL_TAB_HIDDEN_MS ?? 0) - savedTH);
} else {
tabHiddenMs = window.MCS_TAB_HIDDEN_START
? (Date.now() - window.MCS_TAB_HIDDEN_START) : 0;
}
const actualElapsedMs = (now - startTime) - totalPausedMs - currentPausedMs - tabHiddenMs;
return (accumulatedDuration || 0) + Math.max(0, actualElapsedMs / 1000);
}
window.mcsGetElapsedSeconds = mcsGetElapsedSeconds;
function mcsFormatHrid(hrid) {
if (!hrid) return 'Unknown';
return (hrid.split('/').pop() || '')
.split('_')
.map(w => w.charAt(0).toUpperCase() + w.slice(1))
.join(' ');
}
window.mcsFormatHrid = mcsFormatHrid;
// Utility end
// Shykai start
const ShykaiSimulator = {
isEnabled: false,
isLoaded: false,
websocketHooked: false,
init() {
if (typeof GM_info === 'undefined') return;
this.isEnabled = GM_getValue('shykai_simulator_enabled', 'false') === 'true';
if (isShykaiSite) {
this.loadSimulator();
} else if (this.isEnabled && isMWISite) {
this.loadSimulator();
}
},
toggle() {
this.isEnabled = !this.isEnabled;
GM_setValue('shykai_simulator_enabled', String(this.isEnabled));
if (isMWISite && this.isEnabled && !this.isLoaded) {
this.loadSimulator();
}
return this.isEnabled;
},
loadSimulator() {
if (this.isLoaded) {
return;
}
if (isMWISite) {
this.hookWebSocket();
this.captureClientDataFromLocalStorage();
}
if (isShykaiSite) {
this.addImportButton();
}
this.isLoaded = true;
},
hookWebSocket() {
if (this.websocketHooked) return;
const dataProperty = Object.getOwnPropertyDescriptor(MessageEvent.prototype, "data");
const oriGet = dataProperty.get;
dataProperty.get = function hookedGet() {
const socket = this.currentTarget;
if (!(socket instanceof WebSocket)) {
return oriGet.call(this);
}
const msg = oriGet.call(this);
let parsed = null;
try {
parsed = JSON.parse(msg);
} catch (e) {
return msg;
}
if (parsed?.type) {
if (parsed.type === 'init_character_data') {
GM_setValue("init_character_data", msg);
}
if (parsed.type === 'new_battle') {
GM_setValue("new_battle", msg);
}
if (parsed.type === 'profile_shared') {
let profileExportList = JSON.parse(GM_getValue("profile_export_list", "[]"));
parsed.characterID = parsed.profile.characterSkills[0].characterID;
parsed.characterName = parsed.profile.sharableCharacter.name;
parsed.timestamp = Date.now();
profileExportList = profileExportList.filter(
item => item.characterID !== parsed.characterID
);
profileExportList.unshift(parsed);
if (profileExportList.length > 20) {
profileExportList.pop();
}
GM_setValue("profile_export_list", JSON.stringify(profileExportList));
}
}
return msg;
};
Object.defineProperty(MessageEvent.prototype, "data", dataProperty);
this.websocketHooked = true;
},
_captureRetries: 0,
captureClientDataFromLocalStorage: PerformanceMonitor.wrap('Shykai', function() {
const self = ShykaiSimulator;
try {
const clientDataObj = InitClientDataCache.get();
if (!clientDataObj) {
if (self._captureRetries < 15) {
self._captureRetries++;
setTimeout(() => self.captureClientDataFromLocalStorage(), 2000);
}
return;
}
self._captureRetries = 0;
if (clientDataObj?.type === 'init_client_data') {
GM_setValue("init_client_data", JSON.stringify(clientDataObj));
}
} catch (e) {
if (self._captureRetries < 15) {
self._captureRetries++;
setTimeout(() => self.captureClientDataFromLocalStorage(), 2000);
}
}
}),
addImportButton() {
if (this._importButtonTimer) return;
const checkElem = () => {
if (!this.isEnabled) {
return;
}
const selectedElement = document.querySelector('button#buttonImportExport');
if (selectedElement) {
clearInterval(this._importButtonTimer);
this._importButtonTimer = null;
const button = document.createElement('button');
button.textContent = "Import solo/group";
button.style.marginLeft = "10px";
button.style.background = '#4CAF50';
button.style.color = 'white';
button.style.border = 'none';
button.style.padding = '10px 20px';
button.style.cursor = 'pointer';
button.style.borderRadius = '4px';
button.onclick = async () => {
await this.importData(button);
};
selectedElement.parentNode.insertBefore(button, selectedElement.nextSibling);
}
};
this._importButtonTimer = setInterval(checkElem, 200);
setTimeout(() => {
clearInterval(this._importButtonTimer);
this._importButtonTimer = null;
}, 30000);
},
async importData(button) {
const hasCharData = GM_getValue("init_character_data", null);
const hasClientData = GM_getValue("init_client_data", null);
if (!hasCharData || !hasClientData) {
const missing = [];
if (!hasCharData) missing.push('character data');
if (!hasClientData) missing.push('client data');
button.textContent = `Missing: ${missing.join(', ')}`;
button.style.background = '#f44336';
setTimeout(() => {
button.textContent = "Import solo/group";
button.style.background = '#4CAF50';
}, 3000);
return;
}
const [exportObj, playerIDs, importedPlayerPositions] = this.constructGroupExportObj();
const groupTab = document.querySelector('a#group-combat-tab');
if (groupTab) groupTab.click();
await new Promise(resolve => setTimeout(resolve, 100));
const importInputElem = document.querySelector('input#inputSetGroupCombatAll');
if (importInputElem) importInputElem.value = JSON.stringify(exportObj);
const importButton = document.querySelector('button#buttonImportSet');
if (importButton) importButton.click();
for (let i = 0; i < 5; i++) {
const tab = document.querySelector(`a#player${i + 1}-tab`);
if (tab) tab.textContent = playerIDs[i];
const checkbox = document.querySelector(`input#player${i + 1}.form-check-input.player-checkbox`);
if (checkbox) {
checkbox.checked = importedPlayerPositions[i];
checkbox.dispatchEvent(new Event("change"));
}
}
const simTimeInput = document.querySelector('input#inputSimulationTime');
if (simTimeInput) simTimeInput.value = 24;
button.textContent = "Imported!";
},
constructGroupExportObj() {
const characterObj = JSON.parse(GM_getValue("init_character_data", "{}"));
const clientObj = JSON.parse(GM_getValue("init_client_data", "{}"));
let battleObj = null;
if (GM_getValue("new_battle", "")) {
battleObj = JSON.parse(GM_getValue("new_battle", ""));
}
const storedProfileList = JSON.parse(GM_getValue("profile_export_list", "[]"));
const BLANK = `{"player":{"attackLevel":1,"magicLevel":1,"meleeLevel":1,"rangedLevel":1,"defenseLevel":1,"staminaLevel":1,"intelligenceLevel":1,"equipment":[]},"food":{"/action_types/combat":[{"itemHrid":""},{"itemHrid":""},{"itemHrid":""}]},"drinks":{"/action_types/combat":[{"itemHrid":""},{"itemHrid":""},{"itemHrid":""}]},"abilities":[{"abilityHrid":"","level":"1"},{"abilityHrid":"","level":"1"},{"abilityHrid":"","level":"1"},{"abilityHrid":"","level":"1"},{"abilityHrid":"","level":"1"}],"triggerMap":{},"zone":"/actions/combat/fly","simulationTime":"100","houseRooms":{"/house_rooms/dairy_barn":0,"/house_rooms/garden":0,"/house_rooms/log_shed":0,"/house_rooms/forge":0,"/house_rooms/workshop":0,"/house_rooms/sewing_parlor":0,"/house_rooms/kitchen":0,"/house_rooms/brewery":0,"/house_rooms/laboratory":0,"/house_rooms/observatory":0,"/house_rooms/dining_room":0,"/house_rooms/library":0,"/house_rooms/dojo":0,"/house_rooms/gym":0,"/house_rooms/armory":0,"/house_rooms/archery_range":0,"/house_rooms/mystical_study":0}}`;
const exportObj = {};
for (let i = 1; i <= 5; i++) exportObj[i] = BLANK;
const playerIDs = ["Player 1", "Player 2", "Player 3", "Player 4", "Player 5"];
const importedPlayerPositions = [false, false, false, false, false];
if (!characterObj?.partyInfo?.partySlotMap) {
exportObj[1] = JSON.stringify(this.constructSelfPlayer(characterObj, clientObj));
playerIDs[0] = characterObj.character?.name || "Player 1";
importedPlayerPositions[0] = true;
} else {
let i = 1;
for (const member of Object.values(characterObj.partyInfo.partySlotMap)) {
if (member.characterID) {
if (member.characterID === characterObj.character.id) {
exportObj[i] = JSON.stringify(this.constructSelfPlayer(characterObj, clientObj));
playerIDs[i - 1] = characterObj.character.name;
importedPlayerPositions[i - 1] = true;
} else {
const profile = storedProfileList.find(p => p.characterID === member.characterID);
if (profile) {
exportObj[i] = JSON.stringify(this.constructPartyPlayer(profile, clientObj, battleObj));
playerIDs[i - 1] = profile.characterName;
importedPlayerPositions[i - 1] = true;
} else {
playerIDs[i - 1] = "Open profile in game";
}
}
i++;
}
}
}
return [exportObj, playerIDs, importedPlayerPositions];
},
constructSelfPlayer(char, client) {
const p = {
player: { attackLevel: 1, magicLevel: 1, meleeLevel: 1, rangedLevel: 1, defenseLevel: 1, staminaLevel: 1, intelligenceLevel: 1, equipment: [] },
food: { "/action_types/combat": [] }, drinks: { "/action_types/combat": [] }, abilities: [], triggerMap: {}, houseRooms: {}
};
for (const skill of (char.characterSkills ?? [])) {
const type = skill.skillHrid?.split('/').pop();
if (type) p.player[type + 'Level'] = skill.level || 1;
}
if (Array.isArray(char.characterItems)) {
for (const item of char.characterItems) {
if (item.itemLocationHrid && !item.itemLocationHrid.includes("/item_locations/inventory")) {
p.player.equipment.push({
itemLocationHrid: item.itemLocationHrid,
itemHrid: item.itemHrid,
enhancementLevel: item.enhancementLevel ?? 0
});
}
}
} else if (char.characterEquipment) {
for (const key in char.characterEquipment) {
const item = char.characterEquipment[key];
p.player.equipment.push({
itemLocationHrid: item.itemLocationHrid,
itemHrid: item.itemHrid,
enhancementLevel: item.enhancementLevel ?? 0
});
}
}
for (let i = 0; i < 3; i++) {
p.food["/action_types/combat"][i] = { itemHrid: "" };
p.drinks["/action_types/combat"][i] = { itemHrid: "" };
}
const foods = char.actionTypeFoodSlotsMap?.["/action_types/combat"];
if (Array.isArray(foods)) {
foods.forEach((item, i) => {
if (i < 3 && item?.itemHrid) p.food["/action_types/combat"][i] = { itemHrid: item.itemHrid };
});
}
const drinks = char.actionTypeDrinkSlotsMap?.["/action_types/combat"];
if (Array.isArray(drinks)) {
drinks.forEach((item, i) => {
if (i < 3 && item?.itemHrid) p.drinks["/action_types/combat"][i] = { itemHrid: item.itemHrid };
});
}
for (let i = 0; i < 5; i++) p.abilities[i] = { abilityHrid: "", level: "1" };
let idx = 1;
const abilities = char.combatUnit?.combatAbilities ?? [];
for (const ab of abilities) {
if (ab && client.abilityDetailMap?.[ab.abilityHrid]?.isSpecialAbility) {
p.abilities[0] = { abilityHrid: ab.abilityHrid || "", level: String(ab.level || 1) };
} else if (ab?.abilityHrid && idx < 5) {
p.abilities[idx++] = { abilityHrid: ab.abilityHrid || "", level: String(ab.level || 1) };
}
}
p.triggerMap = { ...(char.abilityCombatTriggersMap ?? {}), ...(char.consumableCombatTriggersMap ?? {}) };
for (const house of Object.values(char.characterHouseRoomMap ?? {})) {
p.houseRooms[house.houseRoomHrid] = house.level;
}
p.achievements = {};
if (char.characterAchievements) {
for (const achievement of char.characterAchievements) {
if (achievement.achievementHrid && achievement.isCompleted) {
p.achievements[achievement.achievementHrid] = true;
}
}
}
return p;
},
constructPartyPlayer(profile, client, battle) {
const p = {
player: {
attackLevel: 1, magicLevel: 1, meleeLevel: 1, rangedLevel: 1,
defenseLevel: 1, staminaLevel: 1, intelligenceLevel: 1, equipment: []
},
food: { "/action_types/combat": [] },
drinks: { "/action_types/combat": [] },
abilities: [],
triggerMap: {},
houseRooms: {}
};
for (const skill of (profile.profile?.characterSkills ?? [])) {
const type = skill.skillHrid?.split('/').pop();
if (type) p.player[type + 'Level'] = skill.level || 1;
}
if (profile.profile?.wearableItemMap) {
for (const key in profile.profile.wearableItemMap) {
const item = profile.profile.wearableItemMap[key];
p.player.equipment.push({
itemLocationHrid: item.itemLocationHrid,
itemHrid: item.itemHrid,
enhancementLevel: item.enhancementLevel ?? 0
});
}
}
for (let i = 0; i < 3; i++) {
p.food["/action_types/combat"][i] = { itemHrid: "" };
p.drinks["/action_types/combat"][i] = { itemHrid: "" };
}
let battlePlayer = null;
if (battle?.players) {
battlePlayer = battle.players.find(player => player.character?.id === profile.characterID
);
}
if (battlePlayer?.combatConsumables) {
let foodIndex = 0;
let drinkIndex = 0;
battlePlayer.combatConsumables.forEach(consumable => {
const itemHrid = consumable.itemHrid;
const isDrink = itemHrid.includes('/drinks/') ||
itemHrid.includes('coffee') ||
client.itemDetailMap?.[itemHrid]?.type === 'drink';
if (isDrink && drinkIndex < 3) {
p.drinks["/action_types/combat"][drinkIndex++] = { itemHrid: itemHrid };
} else if (!isDrink && foodIndex < 3) {
p.food["/action_types/combat"][foodIndex++] = { itemHrid: itemHrid };
}
});
}
for (let i = 0; i < 5; i++) {
p.abilities[i] = { abilityHrid: "", level: "1" };
}
let idx = 1;
const abilities = profile.profile?.equippedAbilities ?? [];
for (const ab of abilities) {
if (ab && client.abilityDetailMap?.[ab.abilityHrid]?.isSpecialAbility) {
p.abilities[0] = {
abilityHrid: ab.abilityHrid || "",
level: String(ab.level || 1)
};
} else if (ab?.abilityHrid && idx < 5) {
p.abilities[idx++] = {
abilityHrid: ab.abilityHrid || "",
level: String(ab.level || 1)
};
}
}
p.triggerMap = {
...((battlePlayer?.abilityCombatTriggersMap ?? profile.profile?.abilityCombatTriggersMap) ?? {}),
...((battlePlayer?.consumableCombatTriggersMap ?? profile.profile?.consumableCombatTriggersMap) ?? {})
};
if (profile.profile?.characterHouseRoomMap) {
for (const house of Object.values(profile.profile.characterHouseRoomMap)) {
p.houseRooms[house.houseRoomHrid] = house.level;
}
}
p.achievements = {};
if (profile.profile?.characterAchievements) {
for (const achievement of profile.profile.characterAchievements) {
if (achievement.achievementHrid && achievement.isCompleted) {
p.achievements[achievement.achievementHrid] = true;
}
}
}
return p;
},
};
ShykaiSimulator.init();
// Shykai end
// MWI start
if (isMWISite) {
// Helper start
function createForceLoadEquipmentButton() {
return `
<button id="spy-force-load-btn" class="mcs-helper-force-btn">
🔄 Force Load Equipment
</button>
`;
}
function forceExtractEquipmentData() {
const loadingMessage = document.getElementById('spy-content');
if (loadingMessage) {
loadingMessage.innerHTML = `
<div class="mcs-helper-loading">
<div class="mcs-helper-spinner">⚙️</div>
<br><br>
Force loading equipment data...<br>
<small>Extracting from game UI...</small>
</div>
`;
}
if (window.lootDropsTrackerInstance && typeof window.lootDropsTrackerInstance.extractEquipmentFromUI === 'function') {
try {
const extracted = window.lootDropsTrackerInstance.extractEquipmentFromUI();
if (extracted && extracted.length > 0) {
handleForceLoadSuccess(extracted);
return;
}
} catch (e) {
console.error('[EquipSpy FORCE LOAD] extractEquipmentFromUI error:', e);
}
}
const reactItems = attemptReactFiberExtraction();
if (reactItems && reactItems.length > 0) {
handleForceLoadSuccess(reactItems);
return;
}
const bridgeItems = attemptBridgeDataExtraction();
if (bridgeItems && bridgeItems.length > 20) {
handleForceLoadSuccess(bridgeItems);
return;
}
setTimeout(() => {
const deepItems = attemptDeepReactSearch();
if (deepItems && deepItems.length > 0) {
handleForceLoadSuccess(deepItems);
} else {
console.error('[EquipSpy FORCE LOAD] All methods FAILED');
handleForceLoadFailure();
}
}, 500);
}
function attemptReactFiberExtraction() {
try {
const rootElement = document.querySelector('#root') || document.body;
if (!rootElement) return null;
const keys = Object.keys(rootElement);
for (const key of keys) {
if (key.startsWith('__reactContainer') || key.startsWith('__reactFiber') || key.startsWith('__reactInternalInstance')) {
const reactData = rootElement[key];
const items = deepSearchForCharacterItems(reactData, 0, 15);
if (items && Array.isArray(items) && items.length > 0) {
return items;
}
}
}
} catch (e) {
console.error('[EquipSpy FORCE LOAD] React Fiber extraction failed:', e);
}
return null;
}
function attemptDeepReactSearch() {
try {
const rootSelectors = ['#root', 'body', '[data-reactroot]', '.app'];
for (const selector of rootSelectors) {
const element = document.querySelector(selector);
if (!element) continue;
const allKeys = Object.getOwnPropertyNames(element);
for (const key of allKeys) {
if (key.includes('react') || key.includes('fiber') || key.includes('React')) {
try {
const items = deepSearchForCharacterItems(element[key], 0, 15);
if (items) return items;
} catch (e) { /* React internals may throw on property access */ }
}
}
}
} catch (e) {
console.error('[EquipSpy FORCE LOAD] Deep search failed:', e);
}
return null;
}
function attemptBridgeDataExtraction() {
try {
const bridge = document.getElementById('equipspy-data-bridge');
if (!bridge) return null;
const charData = bridge.getAttribute('data-character-items');
if (!charData) return null;
const items = JSON.parse(charData);
if (Array.isArray(items) && items.length > 0) {
return items;
}
} catch (e) {
console.error('[EquipSpy] Bridge extraction failed:', e);
}
return null;
}
function deepSearchForCharacterItems(node, depth, maxDepth, visited = new WeakSet()) {
if (depth > maxDepth || !node || typeof node !== 'object') return null;
if (visited.has(node)) return null;
visited.add(node);
if (node.characterItems && Array.isArray(node.characterItems)) {
return node.characterItems;
}
const fiberProps = ['child', 'sibling', 'return', 'alternate', 'memoizedState', 'memoizedProps', 'pendingProps', 'stateNode', '_owner', 'type'];
for (const prop of fiberProps) {
if (node[prop]) {
const result = deepSearchForCharacterItems(node[prop], depth + 1, maxDepth, visited);
if (result) return result;
}
}
try {
for (const key in node) {
if (key === 'characterItems' && Array.isArray(node[key])) {
return node[key];
}
if (typeof node[key] === 'object' && node[key] !== null && !key.startsWith('__') && depth < 10) {
const result = deepSearchForCharacterItems(node[key], depth + 1, maxDepth, visited);
if (result) return result;
}
}
} catch (e) { /* React fiber traversal may throw on circular refs */ }
return null;
}
function handleForceLoadSuccess(items) {
let characterData = null;
try {
characterData = CharacterDataStorage.get();
} catch (e) {
console.error('[EquipSpy] Error reading from storage:', e);
}
const sourceItems = characterData?.characterItems || items;
const equippedItems = [];
for (const item of sourceItems) {
if (!item.itemLocationHrid.includes("/item_locations/inventory")) {
equippedItems.push(item);
}
}
const event = new CustomEvent('EquipSpyForceLoadSuccess', {
detail: { items: equippedItems }
});
window.dispatchEvent(event);
}
function handleForceLoadFailure() {
const content = document.getElementById('spy-content');
if (!content) return;
content.innerHTML = `
<div class="mcs-helper-failure">
<div class="mcs-helper-failure-icon">❌</div>
<strong>Force load failed</strong>
<br><br>
Unable to extract equipment data.
<br><br>
<strong>Try:</strong><br>
1. Refresh the page (F5)<br>
2. Change combat zones<br>
3. Equip/unequip an item<br>
<br>
${createForceLoadEquipmentButton()}
</div>
`;
setTimeout(() => {
const btn = document.getElementById('spy-force-load-btn');
if (btn) {
btn.addEventListener('click', forceExtractEquipmentData);
}
}, 100);
}
// Helper end
if (window._MCS_FLOOT_INITIALIZED) {
} else {
window._MCS_FLOOT_INITIALIZED = true;
}
if (!window._equipSpyPageLoadTime) {
window._equipSpyPageLoadTime = Date.now();
}
const CUSTOM_SORT_KEY = 'lootDropsCustomSortPref';
const SORT_MODES = ['name', 'value', 'quantity'];
const fixedValueChests = new Set([
'medium treasure chest', 'large treasure chest', 'small treasure chest'
]);
let marketData = {};
let chestValueCache = {};
let itemPriceCache = {};
let dollarThreshold = null;
let dollarTagEnabled = false;
const viewModeState = {};
let inventoryPricesEnabled = true;
let inventoryState = new Map();
let marketWarningsEnabled = true;
let inventoryWarningsEnabled = true;
let _useAskPrice = false;
const flStorage = createModuleStorage('FL');
function initializePriceToggleButtons() {
const CONFIG = {
CONTAINER_ID: 'milt-loot-drops-display'
};
const priceToggleBtn = document.getElementById(`${CONFIG.CONTAINER_ID}-price-toggle`);
const priceToggleBtnHidden = document.getElementById(`${CONFIG.CONTAINER_ID}-price-toggle-hidden`);
function updateButtonAppearance(useAsk) {
const btnText = useAsk ? 'Showing Ask' : 'Showing Bid';
const btnColor = useAsk ? '#6495ED' : '#4CAF50';
const btnBg = useAsk ? 'rgba(100, 149, 237, 0.3)' : 'rgba(76, 175, 80, 0.3)';
if (priceToggleBtn) {
priceToggleBtn.textContent = btnText;
priceToggleBtn.style.color = btnColor;
priceToggleBtn.style.borderColor = btnColor;
priceToggleBtn.style.backgroundColor = btnBg;
}
if (priceToggleBtnHidden) {
priceToggleBtnHidden.textContent = btnText;
priceToggleBtnHidden.style.color = btnColor;
priceToggleBtnHidden.style.borderColor = btnColor;
priceToggleBtnHidden.style.backgroundColor = btnBg;
}
}
updateButtonAppearance(_useAskPrice);
if (priceToggleBtn) {
priceToggleBtn.onclick = () => {
_useAskPrice = !_useAskPrice;
flStorage.set('use_ask_price', _useAskPrice);
updateButtonAppearance(_useAskPrice);
recalculateAllPrices();
setTimeout(() => {
updateAllInventoryPrices(true);
}, 150);
};
}
if (priceToggleBtnHidden) {
priceToggleBtnHidden.onclick = () => {
_useAskPrice = !_useAskPrice;
flStorage.set('use_ask_price', _useAskPrice);
updateButtonAppearance(_useAskPrice);
recalculateAllPrices();
setTimeout(() => {
updateAllInventoryPrices(true);
}, 150);
};
}
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', () => {
setTimeout(initializePriceToggleButtons, 2500);
});
} else {
setTimeout(initializePriceToggleButtons, 2500);
}
let useFullNumbers = true;
function formatAbbreviated(value) {
return mcsFormatCurrency(value, 'abbreviated');
}
function formatCoins(value) {
const formatted = useFullNumbers ? Math.round(value).toLocaleString() : formatAbbreviated(value);
return `${formatted} coin`;
}
function formatNumberWithCommas(value) {
if (useFullNumbers) {
return Math.round(value).toLocaleString();
} else {
return formatAbbreviated(value);
}
}
function capitalizeEachWord(str) {
return str.split(' ').map(w => w.charAt(0).toUpperCase() + w.slice(1)).join(' ');
}
async function loadMarketData() {
try {
const res = await fetch('https://www.milkywayidle.com/game_data/marketplace.json');
const json = await res.json();
marketData = json.marketData ?? {};
localStorage.setItem('mcs__global_marketAPI_json', JSON.stringify(json));
await calculateChestValues();
} catch (e) {
console.error('[Frotty Loot] Failed to load market data:', e);
try {
const cached = mcsGetMarketData();
if (cached) {
marketData = cached.marketData ?? {};
}
} catch (cacheError) {
console.error('[Floot] Failed to recover from cache:', cacheError);
}
}
}
async function calculateChestValues() {
try {
const initData = InitClientDataCache.get();
if (!initData || !initData.openableLootDropMap) {
console.warn('[Frotty Loot] No openableLootDropMap found');
return;
}
const itemHridToName = {};
if (initData.itemDetailMap) {
for (const key in initData.itemDetailMap) {
const item = initData.itemDetailMap[key];
if (item && item.name) {
itemHridToName[key] = item.name;
}
}
}
const useCowbell0 = typeof window.getTreasureUseCowbell0 === 'function' ? window.getTreasureUseCowbell0() : false;
const specialItemPrices = {
'Coin': {
ask: 1,
bid: 1
},
'Cowbell': {
ask: useCowbell0 ? 0 : ((marketData['/items/bag_of_10_cowbells']?.['0']?.a || 360000 * 0.82)) / 10,
bid: useCowbell0 ? 0 : ((marketData['/items/bag_of_10_cowbells']?.['0']?.b || 350000 * 0.82)) / 10
}
};
const formattedChestDropData = {};
for (let iteration = 0; iteration < 4; iteration++) {
for (let [chestHrid, items] of Object.entries(initData.openableLootDropMap)) {
const chestName = itemHridToName[chestHrid] || formatItemName(chestHrid);
if (!formattedChestDropData[chestName]) {
formattedChestDropData[chestName] = { items: {} };
}
let totalAsk = 0;
let totalBid = 0;
items.forEach(item => {
const { itemHrid, dropRate, minCount, maxCount } = item;
if (dropRate < 0.01) return;
const itemName = itemHridToName[itemHrid] || formatItemName(itemHrid);
const expectedYield = ((minCount + maxCount) / 2) * dropRate;
let askPrice = 0;
let bidPrice = 0;
if (specialItemPrices[itemName]) {
askPrice = specialItemPrices[itemName].ask || 0;
bidPrice = specialItemPrices[itemName].bid || 0;
}
else if (marketData[itemHrid] && marketData[itemHrid]["0"]) {
askPrice = marketData[itemHrid]["0"].a || 0;
bidPrice = marketData[itemHrid]["0"].b || 0;
}
const taxFactor = (itemName in specialItemPrices) ? 1 : 0.98;
totalAsk += (askPrice * expectedYield) * taxFactor;
totalBid += (bidPrice * expectedYield) * taxFactor;
});
formattedChestDropData[chestName] = {
...formattedChestDropData[chestName],
expectedAsk: totalAsk,
expectedBid: totalBid
};
specialItemPrices[chestName] = {
ask: totalAsk,
bid: totalBid
};
}
}
for (let [chestHrid, items] of Object.entries(initData.openableLootDropMap)) {
const chestName = itemHridToName[chestHrid] || formatItemName(chestHrid);
if (formattedChestDropData[chestName]) {
chestValueCache[chestHrid] = formattedChestDropData[chestName];
}
}
} catch (error) {
console.error('[Frotty Loot] Error calculating chest values:', error);
}
}
function formatItemName(hrid) {
return mcsFormatHrid(hrid);
}
function getUnitValue(name, sessionKey = 'live', enhancementLevel = 0, priceType = null) {
const normalized = name;
if (normalized === '/items/coin') return 1;
if (normalized === '/items/cowbell') {
const useCowbell0 = typeof window.getTreasureUseCowbell0 === 'function' ? window.getTreasureUseCowbell0() : false;
if (useCowbell0) return 0;
const useAsk = (priceType !== null) ? (priceType === 'ask') : _useAskPrice;
const bagPrice = marketData['/items/bag_of_10_cowbells']?.['0'];
if (bagPrice) {
const price = useAsk ? (bagPrice.a || 360000) : (bagPrice.b || 350000);
return Math.floor((price / 10) * 0.98);
}
return useAsk ? 35280 : 34300;
}
const useAsk = (priceType !== null) ? (priceType === 'ask') : _useAskPrice;
const cacheKey = `${sessionKey}|${normalized}|${enhancementLevel}|${useAsk ? 'ask' : 'bid'}`;
if (itemPriceCache[cacheKey] !== undefined) {
if (normalized.includes('treasure_chest')) {
}
return itemPriceCache[cacheKey];
}
let calculatedPrice;
if (chestValueCache[normalized]) {
const chestData = chestValueCache[normalized];
calculatedPrice = useAsk ? chestData.expectedAsk : chestData.expectedBid;
}
else if (marketData[name] && marketData[name][enhancementLevel.toString()]) {
const priceValue = useAsk ?
marketData[name][enhancementLevel.toString()].a :
marketData[name][enhancementLevel.toString()].b;
if (priceValue <= 0) {
return null;
}
if (priceValue < 900) {
calculatedPrice = Math.floor(priceValue * 0.98);
} else {
calculatedPrice = Math.ceil(priceValue * 0.98);
}
} else {
return null;
}
itemPriceCache[cacheKey] = calculatedPrice;
return calculatedPrice;
}
window.getUnitValue = getUnitValue;
window.formatFlootCoins = formatCoins;
window.getFlootUseAskPrice = () => _useAskPrice;
window.updateAllInventoryPrices = updateAllInventoryPrices;
window.updateNtallyIndicators = updateNtallyIndicators;
function clearSessionPriceCache(sessionKey) {
if (!sessionKey) return;
const keysToDelete = Object.keys(itemPriceCache).filter(key => key.startsWith(sessionKey + '|'));
keysToDelete.forEach(key => delete itemPriceCache[key]);
}
let _cachedPlayerSections = null;
let _playerSectionsCacheTime = 0;
const PLAYER_SECTIONS_CACHE_TTL = 2000;
function getCachedPlayerSections() {
const now = Date.now();
if (!_cachedPlayerSections || now - _playerSectionsCacheTime > PLAYER_SECTIONS_CACHE_TTL) {
_cachedPlayerSections = document.querySelectorAll('.ldt-player-stats-section');
_playerSectionsCacheTime = now;
}
return _cachedPlayerSections;
}
function invalidatePlayerSectionsCache() {
_cachedPlayerSections = null;
_playerSectionsCacheTime = 0;
}
function recalculateAllPrices() {
itemPriceCache = {};
if (window.lootDropsTrackerInstance) {
window.lootDropsTrackerInstance.renderCurrentView();
}
setTimeout(() => {
if (window.lootDropsTrackerInstance) {
window.lootDropsTrackerInstance.updateSpyDisplay();
}
}, 100);
window.dispatchEvent(new CustomEvent('FlootPricesUpdated'));
}
function updateGoldPerDay() {
if (!window.lootDropsTrackerInstance) return;
let hoursElapsed = 0;
if (window.lootDropsTrackerInstance.startTime &&
window.lootDropsTrackerInstance.isLiveSessionActive) {
const elapsedMs = Date.now() - window.lootDropsTrackerInstance.startTime.getTime();
hoursElapsed = elapsedMs / (1000 * 60 * 60);
}
if (hoursElapsed < 0.01) return;
const userName = window.lootDropsTrackerInstance.userName;
const playerStats = window.lootDropsTrackerInstance.playerDropStats;
if (!playerStats) return;
const playerRevenue = {};
for (const playerName in playerStats) {
const items = playerStats[playerName].items ?? {};
let total = 0;
for (const hrid in items) {
const count = items[hrid];
const unitValue = getUnitValue(hrid, 'live');
if (unitValue !== null) {
total += unitValue * count;
}
}
const perDay = total > 0 ? (total / hoursElapsed) * 24 : 0;
playerRevenue[playerName] = { total, perDay };
}
window.lootDropsTrackerInstance.flootPlayerRevenue = playerRevenue;
const userRevenue = playerRevenue[userName];
if (userRevenue && userRevenue.total > 0) {
window.lootDropsTrackerInstance.goldPerDay = userRevenue.perDay;
window.lootDropsTrackerInstance.goldPerDayFormatted = formatCoins(userRevenue.perDay);
if (window.lootDropsTrackerInstance.updateCoinHeader) {
window.lootDropsTrackerInstance.updateCoinHeader();
}
}
}
function injectValuesAndSort() {
updateGoldPerDay();
if (window._isRendering) {
return;
}
const sortPref = 'value';
const playerSections = getCachedPlayerSections();
let hoursElapsed = 0;
const currentSession = window.lootDropsTrackerInstance?.getCurrentHistoryViewItem?.();
if (currentSession && currentSession.duration) {
hoursElapsed = currentSession.duration / (1000 * 60 * 60);
} else if (window.lootDropsTrackerInstance &&
window.lootDropsTrackerInstance.startTime &&
window.lootDropsTrackerInstance.isLiveSessionActive) {
const elapsedMs = Date.now() - window.lootDropsTrackerInstance.startTime.getTime();
hoursElapsed = elapsedMs / (1000 * 60 * 60);
}
const currentSessionKey = window.lootDropsTrackerInstance?.viewingLive ?
'live' :
(window.lootDropsTrackerInstance?.domRefs?.historySelect?.value || 'live');
let playerStats = window.lootDropsTrackerInstance?.playerDropStats ?? {};
if (currentSession && currentSession.stats) {
playerStats = currentSession.stats;
} else if (!window.lootDropsTrackerInstance?.viewingLive) {
const selectedValue = window.lootDropsTrackerInstance?.domRefs?.historySelect?.value;
if (selectedValue === 'combined' && window.lootDropsTrackerInstance?.aggregatedHistoryData) {
playerStats = window.lootDropsTrackerInstance.aggregatedHistoryData;
} else if (selectedValue && selectedValue !== 'live') {
const selectedSession = window.lootDropsTrackerInstance?.sessionHistory?.find(s => s.key === selectedValue);
if (selectedSession && selectedSession.stats) {
playerStats = selectedSession.stats;
}
}
}
setTimeout(() => {
const hiddenPlayersContainer = document.getElementById('milt-loot-drops-display-hidden-players');
if (hiddenPlayersContainer && window.lootDropsTrackerInstance) {
const userName = window.lootDropsTrackerInstance.userName;
let html = '';
if (playerStats && Object.keys(playerStats).length > 0) {
for (const playerName in playerStats) {
const items = playerStats[playerName].items ?? {};
let total = 0;
for (const hrid in items) {
const count = items[hrid];
const unitValue = getUnitValue(hrid, currentSessionKey);
if (unitValue !== null) {
total += unitValue * count;
}
}
const totalText = total > 0 ? formatCoins(total) : '--';
const goldPerDayText = (hoursElapsed > 0 && total > 0)
? formatCoins(Math.round((total / hoursElapsed) * 24))
: '';
const playerClass = (playerName === userName) ? 'is-current' : '';
html += `
<div class="ldt-hidden-player-row">
<div class="ldt-hidden-player-name ${playerClass}">${playerName}</div>
<div class="ldt-hidden-player-stats">
<span class="ldt-hidden-coin-emoji">💰</span>
<span class="ldt-hidden-coin-amount">${totalText}</span>
<span class="ldt-hidden-coin-rate">${goldPerDayText ? goldPerDayText + '/day' : ''}</span>
</div>
</div>
`;
}
} else {
html = '<div class="ldt-hidden-player-row"><span>No loot tracked</span></div>';
}
hiddenPlayersContainer.innerHTML = html;
}
}, 0);
playerSections.forEach((section) => {
const header = section.querySelector('.ldt-player-name-header');
header.querySelectorAll('span[data-value-injected]').forEach(el => el.remove());
const playerName = header?.textContent?.split('(')[0]?.trim() || 'Unknown';
const rows = Array.from(section.querySelectorAll('.ldt-loot-item-entry'));
rows.forEach(row => {
const nameEl = row.querySelector('.ldt-item-name');
const valueEl = row.querySelector('.ldt-item-value');
if (nameEl && valueEl) {
const name = nameEl.textContent.trim();
let unitValue = getUnitValue(name, currentSessionKey);
const scam = (!_useAskPrice && unitValue !== null) ? detectScamBid(name) : { isScamBid: false, isEqualToVendor: false };
if (scam.isScamBid) unitValue = scam.vendorValue;
valueEl.textContent = (unitValue !== null) ? formatNumberWithCommas(unitValue) : 'N/A';
if (scam.isScamBid) {
valueEl.style.color = '#ff1744';
valueEl.style.fontWeight = 'bold';
} else if (scam.isEqualToVendor) {
valueEl.style.color = '#FFD700';
valueEl.style.fontWeight = 'bold';
} else {
valueEl.style.color = '';
valueEl.style.fontWeight = '';
}
}
});
let total = 0;
const playerItems = playerStats[playerName]?.items ?? {};
const itemData = rows.map((row) => {
const nameEl = row.querySelector('.ldt-item-name');
const valueEl = row.querySelector('.ldt-item-value');
const countEl = row.querySelector('.ldt-item-count');
const name = nameEl?.textContent.trim() || '';
const count = playerItems[name] || 0;
let unitValue = getUnitValue(name, currentSessionKey);
const scam2 = (!_useAskPrice && unitValue !== null) ? detectScamBid(name) : { isScamBid: false, isEqualToVendor: false };
if (scam2.isScamBid) unitValue = scam2.vendorValue;
const itemValue = (unitValue !== null) ? (unitValue * count) : 0;
if (valueEl) {
valueEl.textContent = (unitValue !== null ? formatNumberWithCommas(unitValue) : 'N/A');
if (scam2.isScamBid) {
valueEl.style.color = '#ff1744';
valueEl.style.fontWeight = 'bold';
} else if (scam2.isEqualToVendor) {
valueEl.style.color = '#FFD700';
valueEl.style.fontWeight = 'bold';
} else {
valueEl.style.color = '';
valueEl.style.fontWeight = '';
}
}
if (unitValue !== null) {
total += itemValue;
}
row.style.border = '';
if (dollarTagEnabled && dollarThreshold && unitValue !== null && unitValue >= dollarThreshold) {
row.style.border = '3px solid rgba(0, 255, 0, 0.5)';
}
return { row, name, count, value: itemValue, nameEl, countEl, valueEl, unitValue };
});
const headerForTotal = header;
const playerNameForTotal = playerName;
itemData.sort((a, b) => {
if (sortPref === 'value') {
if (b.value > a.value) return 1;
if (b.value < a.value) return -1;
if (b.count > a.count) return 1;
if (b.count < a.count) return -1;
return a.name.localeCompare(b.name);
}
if (sortPref === 'quantity') return b.count - a.count || a.name.localeCompare(b.name);
return a.name.localeCompare(b.name);
});
itemData.forEach(({ valueEl, unitValue }) => {
if (valueEl) {
valueEl.textContent = (unitValue !== null) ? formatNumberWithCommas(unitValue) : 'N/A';
}
});
const list = section.querySelector('.ldt-loot-list');
if (!list) return;
itemData.forEach(({ row }) => list.appendChild(row));
let existingTotalSpan = headerForTotal.querySelector('span[data-value-injected="total"]');
if (existingTotalSpan) {
existingTotalSpan.remove();
}
let existingRateSpan = headerForTotal.querySelector('span[data-value-injected="rate"]');
if (existingRateSpan) {
existingRateSpan.remove();
}
const span = document.createElement('span');
span.setAttribute('data-value-injected', 'total');
span.style.fontWeight = 'normal';
span.style.cursor = 'pointer';
let displayText = ` (${formatCoins(total)})`;
span.style.color = 'gold';
span.style.fontSize = '0.9em';
if (hoursElapsed >= 0.01 && total > 0) {
const avg = total / hoursElapsed * 24;
displayText = ` ${formatCoins(total)}: ${formatCoins(avg)}/day`;
span.style.color = 'lightgreen';
if (playerName === window.lootDropsTrackerInstance.userName) {
window.lootDropsTrackerInstance.goldPerDay = avg;
window.lootDropsTrackerInstance.goldPerDayFormatted = formatCoins(avg);
}
}
span.textContent = displayText;
headerForTotal.appendChild(span);
});
}
const CONFIG = {
CONTAINER_ID: 'milt-loot-drops-display',
TOOLTIP_ID: 'milt-loot-drops-tooltip',
DEFAULT_POS: { top: '120px', right: '30px' },
STORAGE: {
sessionHistory: 'mcs__global_session_history'
},
PANEL_WIDTH_EXPANDED: '380px',
PANEL_MAX_HEIGHT: '70vh',
PLAYER_COLUMN_MIN_WIDTH: '160px',
USERNAME_SELECTOR: ".CharacterName_name__1amXp[data-name]",
SEARCH_DEBOUNCE_MS: 250,
HISTORY_LIMIT: 40,
DISPLAY_HISTORY_LIMIT: 10,
INJECTION_FLAG: 'lootDropsInjected'
};
const savedValue = flStorage.get('use_ask_price', false);
_useAskPrice = savedValue === true || savedValue === 'true';
function readSessionHistory() {
try {
const stored = localStorage.getItem(CONFIG.STORAGE.sessionHistory);
const parsed = stored ? JSON.parse(stored) : [];
return Array.isArray(parsed) ? parsed.filter(item => item && typeof item === 'object' && item.start && item.key) : [];
} catch (e) { console.error("LDT: Error reading session history", e); return []; }
}
function writeSessionHistory(history) {
try {
const MAX_SIZE = 4.0 * 1024 * 1024;
let historyString = JSON.stringify(history);
if (historyString.length > MAX_SIZE && history.length > 1) {
let lo = 1, hi = history.length - 1, trimCount = hi;
while (lo <= hi) {
const mid = (lo + hi) >>> 1;
const testString = JSON.stringify(history.slice(mid));
if (testString.length > MAX_SIZE) {
lo = mid + 1;
} else {
trimCount = mid;
hi = mid - 1;
}
}
history.splice(0, trimCount);
historyString = JSON.stringify(history);
}
localStorage.setItem(CONFIG.STORAGE.sessionHistory, historyString);
} catch (e) {
console.error("LDT: Error writing session history", e);
if (e.name === 'QuotaExceededError' && history.length > 0) {
history.shift();
try {
localStorage.setItem(CONFIG.STORAGE.sessionHistory, JSON.stringify(history));
} catch (e2) {
console.error("LDT: Failed to write history even after trimming.", e2);
}
}
}
}
function generateSessionKey(playerNames, startTimeString) {
if (!playerNames || playerNames.length === 0 || !startTimeString) return null;
const sortedNames = [...playerNames].sort().join(',');
return `${sortedNames}@${startTimeString}`;
}
function formatLocationName(actionHrid) {
if (!actionHrid || !actionHrid.startsWith('/actions/combat/')) {
return 'Unknown Location';
}
return actionHrid.replace('/actions/combat/', '')
.replace(/_/g, ' ')
.split(' ')
.map(word => word.charAt(0).toUpperCase() + word.slice(1))
.join(' ');
}
function debounce(func, wait) {
let timeout;
return function executedFunction(...args) {
const later = () => { clearTimeout(timeout); func.apply(this, args); };
clearTimeout(timeout);
timeout = setTimeout(later, wait);
};
};
function formatPrice(value) {
return mcsFormatCurrency(value, 'price');
}
function getRealItemCount(itemHrid, enhancementLevel = 0) {
const items = window.lootDropsTrackerInstance?.spyCharacterItems
|| CharacterDataStorage.get()?.characterItems;
if (!items) return null;
const match = items.find(i =>
i.itemHrid === itemHrid &&
(i.enhancementLevel ?? 0) === enhancementLevel &&
i.itemLocationHrid === '/item_locations/inventory'
);
return match ? match.count : null;
}
function getItemPrice(itemHrid, count, enhancementLevel = 0) {
if (!itemHrid) return null;
if (Object.keys(marketData).length === 0) {
console.warn('[Floot] Market data is empty, triggering reload...');
loadMarketData().then(() => {
if (inventoryPricesEnabled) {
setTimeout(() => updateAllInventoryPrices(true), 500);
}
});
return null;
}
const unitPrice = window.getUnitValue(itemHrid, 'live', enhancementLevel);
if (unitPrice !== null && unitPrice > 0) {
return unitPrice * (count || 1);
}
return null;
}
function detectScamBid(itemHrid) {
let isScamBid = false;
let isEqualToVendor = false;
let isNoBid = false;
let vendorValue = 0;
try {
const itemDetailMap = InitClientDataCache.getItemDetailMap();
vendorValue = itemDetailMap[itemHrid]?.sellPrice || 0;
if (marketData[itemHrid] && marketData[itemHrid]['0']) {
const rawBid = marketData[itemHrid]['0'].b || 0;
if (rawBid <= 0) {
isNoBid = true;
} else if (vendorValue > 0) {
const bidWithTax = rawBid < 900 ? Math.floor(rawBid * 0.98) : Math.ceil(rawBid * 0.98);
if (bidWithTax < vendorValue) {
isScamBid = true;
} else if (bidWithTax === vendorValue) {
isEqualToVendor = true;
}
}
}
} catch (e) {
}
return { isScamBid, isEqualToVendor, isNoBid, vendorValue };
}
function appendCenterOverlay(container, className, color, text) {
const el = document.createElement('div');
el.className = `${className} mcs-floot-center-overlay`;
el.style.color = color;
el.textContent = text;
container.classList.add('mcs-floot-overlay-container');
container.appendChild(el);
}
function addPriceOverlayToItem(itemElement) {
if (!itemElement) return;
if (itemElement.querySelector('.mcs-price-overlay') ||
itemElement.querySelector('.mcs-floot-center-overlay') ||
itemElement.querySelector('.mcs-ntally-indicator')) {
return;
}
const svgElement = itemElement.querySelector('svg');
if (!svgElement) {
return;
}
const ariaLabel = svgElement.getAttribute('aria-label') || '';
if (!ariaLabel) {
return;
}
const itemName = ariaLabel.toLowerCase().replace(/'/g, '').replace(/ /g, '_');
const fullItemHrid = '/items/' + itemName;
if (fullItemHrid === '/items/coins' || fullItemHrid === '/items/coin') {
return;
}
let enhancementLevel = 0;
const itemDiv = itemElement.querySelector('[class*="Item_item"]');
if (itemDiv) {
const enhancementDiv = itemDiv.querySelector('[class*="Item_enhancementLevel"]');
if (enhancementDiv) {
const enhancementText = enhancementDiv.textContent.trim();
const enhancementMatch = enhancementText.match(/\+(\d+)/);
if (enhancementMatch) {
enhancementLevel = parseInt(enhancementMatch[1], 10);
}
}
}
let count = getRealItemCount(fullItemHrid, enhancementLevel);
if (count === null) {
const countDiv = itemElement.querySelector('[class*="Item_count"]');
if (countDiv) {
const countMatch = countDiv.textContent.trim().match(/(\d+)/);
count = countMatch ? parseInt(countMatch[1], 10) : 1;
} else {
count = 1;
}
}
const totalPrice = getItemPrice(fullItemHrid, count, enhancementLevel);
let isTrackedByNtally = false;
try {
if (window.isItemTrackedByNtally) {
isTrackedByNtally = window.isItemTrackedByNtally(fullItemHrid);
}
} catch (e) {
}
const isMarketplace = !!itemElement.closest('[class*="Marketplace_"]');
const showWarnings = isMarketplace ? marketWarningsEnabled : inventoryWarningsEnabled;
const { isScamBid, isEqualToVendor, isNoBid } = (showWarnings && !_useAskPrice && enhancementLevel === 0)
? detectScamBid(fullItemHrid)
: { isScamBid: false, isEqualToVendor: false, isNoBid: false };
if (isNoBid) {
appendCenterOverlay(itemElement, 'mcs-none-overlay', '#999', 'NONE');
} else if (isScamBid) {
appendCenterOverlay(itemElement, 'mcs-scam-overlay', '#ff1744', 'SCAM');
} else if (isEqualToVendor) {
appendCenterOverlay(itemElement, 'mcs-vendor-equal-overlay', '#FFD700', 'VEND');
}
if (isTrackedByNtally) {
const ntallyIndicator = document.createElement('div');
ntallyIndicator.className = 'mcs-ntally-indicator';
itemElement.style.position = 'relative';
itemElement.appendChild(ntallyIndicator);
}
const priceOverlay = document.createElement('div');
priceOverlay.className = 'mcs-price-overlay';
priceOverlay.style.color = _useAskPrice ? '#6495ED' : '#4CAF50';
if (totalPrice !== null) {
priceOverlay.textContent = formatPrice(totalPrice);
}
itemElement.style.position = 'relative';
itemElement.appendChild(priceOverlay);
}
function captureInventoryState() {
const newState = new Map();
const categoryLabels = document.querySelectorAll('[class*="Inventory_label"]');
categoryLabels.forEach(labelElement => {
const categoryName = labelElement.textContent.replace(/\d+.*$/, '').trim();
if (categoryName.toLowerCase() === 'currencies') {
return;
}
const itemGrid = labelElement.closest('[class*="Inventory_itemGrid"]');
if (!itemGrid) return;
const categoryItems = new Map();
const itemContainers = itemGrid.querySelectorAll('[class*="Item_itemContainer"]');
itemContainers.forEach(itemElement => {
const svgElement = itemElement.querySelector('svg');
if (!svgElement) return;
const ariaLabel = svgElement.getAttribute('aria-label') || '';
if (!ariaLabel) return;
const itemName = ariaLabel.toLowerCase().replace(/'/g, '').replace(/ /g, '_');
const fullItemHrid = '/items/' + itemName;
if (fullItemHrid === '/items/coins' || fullItemHrid === '/items/coin') return;
let enhancementLevel = 0;
const itemDiv = itemElement.querySelector('[class*="Item_item"]');
if (itemDiv) {
const enhancementDiv = itemDiv.querySelector('[class*="Item_enhancementLevel"]');
if (enhancementDiv) {
const enhancementText = enhancementDiv.textContent.trim();
const enhancementMatch = enhancementText.match(/\+(\d+)/);
if (enhancementMatch) {
enhancementLevel = parseInt(enhancementMatch[1], 10);
}
}
}
const count = getRealItemCount(fullItemHrid, enhancementLevel) ?? 1;
const itemKey = `${fullItemHrid}|${enhancementLevel}`;
categoryItems.set(itemKey, {
hrid: fullItemHrid,
count: count,
enhLevel: enhancementLevel,
element: itemElement,
priceType: _useAskPrice ? 'ask' : 'bid'
});
});
if (categoryItems.size > 0) {
newState.set(categoryName, categoryItems);
}
});
return newState;
}
function compareInventoryStates(oldState, newState) {
const changedCategories = new Set();
for (const [categoryName, newItems] of newState) {
const oldItems = oldState.get(categoryName);
if (!oldItems) {
changedCategories.add(categoryName);
continue;
}
if (oldItems.size !== newItems.size) {
changedCategories.add(categoryName);
continue;
}
for (const [itemKey, newItemData] of newItems) {
const oldItemData = oldItems.get(itemKey);
if (!oldItemData ||
oldItemData.count !== newItemData.count ||
oldItemData.priceType !== newItemData.priceType ||
oldItemData.element !== newItemData.element ||
!newItemData.element.querySelector('.mcs-price-overlay')) {
changedCategories.add(categoryName);
break;
}
}
}
for (const categoryName of oldState.keys()) {
if (!newState.has(categoryName)) {
changedCategories.add(categoryName);
}
}
return changedCategories;
}
function updateCategoryTotals(changedCategoriesSet = null) {
const categoryLabels = document.querySelectorAll('[class*="Inventory_label"]');
categoryLabels.forEach(labelElement => {
const existingTotal = labelElement.querySelector('.mcs-category-total');
let categoryName = labelElement.textContent.trim();
if (existingTotal) {
const totalText = existingTotal.textContent;
categoryName = categoryName.replace(totalText, '').trim();
}
if (categoryName.toLowerCase() === 'currencies') {
return;
}
if (changedCategoriesSet && !changedCategoriesSet.has(categoryName)) {
return;
}
if (existingTotal) {
existingTotal.remove();
}
const itemGrid = labelElement.closest('[class*="Inventory_itemGrid"]');
if (!itemGrid) return;
const itemContainers = itemGrid.querySelectorAll('[class*="Item_itemContainer"]');
let categoryTotal = 0;
itemContainers.forEach(itemElement => {
const svgElement = itemElement.querySelector('svg');
if (!svgElement) return;
const ariaLabel = svgElement.getAttribute('aria-label') || '';
if (!ariaLabel) return;
const itemName = ariaLabel.toLowerCase().replace(/'/g, '').replace(/ /g, '_');
const fullItemHrid = '/items/' + itemName;
if (fullItemHrid === '/items/coins' || fullItemHrid === '/items/coin') return;
let count = 1;
const countDiv = itemElement.querySelector('[class*="Item_count"]');
if (countDiv) {
const countText = countDiv.textContent.trim();
const countMatch = countText.match(/(\d+)/);
if (countMatch) {
count = parseInt(countMatch[1], 10);
}
}
let enhancementLevel = 0;
const itemDiv = itemElement.querySelector('[class*="Item_item"]');
if (itemDiv) {
const enhancementDiv = itemDiv.querySelector('[class*="Item_enhancementLevel"]');
if (enhancementDiv) {
const enhancementText = enhancementDiv.textContent.trim();
const enhancementMatch = enhancementText.match(/\+(\d+)/);
if (enhancementMatch) {
enhancementLevel = parseInt(enhancementMatch[1], 10);
}
}
}
const itemPrice = getItemPrice(fullItemHrid, count, enhancementLevel);
if (itemPrice !== null && itemPrice > 0) {
categoryTotal += itemPrice;
}
});
if (categoryTotal > 0) {
const totalSpan = document.createElement('span');
totalSpan.className = 'mcs-category-total';
totalSpan.style.color = _useAskPrice ? '#6495ED' : '#4CAF50';
totalSpan.textContent = formatPrice(categoryTotal);
labelElement.appendChild(totalSpan);
}
});
}
function updateNtallyIndicators() {
const inventoryItems = document.querySelectorAll('[class*="Inventory_"] [class*="Item_itemContainer"], [class*="Marketplace_"] [class*="Item_itemContainer"]');
inventoryItems.forEach(itemElement => {
const svgElement = itemElement.querySelector('svg');
if (!svgElement) return;
const ariaLabel = svgElement.getAttribute('aria-label') || '';
if (!ariaLabel) return;
const itemName = ariaLabel.toLowerCase().replace(/'/g, '').replace(/ /g, '_');
const fullItemHrid = '/items/' + itemName;
let isTrackedByNtally = false;
try {
if (window.isItemTrackedByNtally) {
isTrackedByNtally = window.isItemTrackedByNtally(fullItemHrid);
}
} catch (e) {
}
const existingIndicator = itemElement.querySelector('.mcs-ntally-indicator');
if (existingIndicator) {
existingIndicator.remove();
}
if (isTrackedByNtally) {
const ntallyIndicator = document.createElement('div');
ntallyIndicator.className = 'mcs-ntally-indicator';
itemElement.style.position = 'relative';
itemElement.appendChild(ntallyIndicator);
}
});
}
function updateAllInventoryPrices(forceUpdate = false) {
if (!inventoryPricesEnabled) {
updateNtallyIndicators();
return;
}
if (Object.keys(marketData).length === 0) {
console.warn('[Floot] Market data unavailable during inventory update, attempting reload...');
loadMarketData().then(() => {
setTimeout(() => updateAllInventoryPrices(true), 500);
});
return;
}
const newState = captureInventoryState();
if (forceUpdate) {
inventoryState = newState;
let inventoryItems = document.querySelectorAll('[class*="Inventory_"] [class*="Item_itemContainer"], [class*="Marketplace_"] [class*="Item_itemContainer"]');
inventoryItems.forEach(itemElement => {
itemElement.querySelectorAll('.mcs-price-overlay, .mcs-floot-center-overlay, .mcs-ntally-indicator').forEach(el => el.remove());
const hasSvg = itemElement.querySelector('svg');
if (hasSvg) {
addPriceOverlayToItem(itemElement);
}
});
updateCategoryTotals();
} else {
const changedCategories = compareInventoryStates(inventoryState, newState);
if (changedCategories.size === 0) {
return;
}
inventoryState = newState;
changedCategories.forEach(categoryName => {
const categoryState = newState.get(categoryName);
if (categoryState) {
categoryState.forEach((itemData) => {
const itemElement = itemData.element;
if (itemElement) {
itemElement.querySelectorAll('.mcs-price-overlay, .mcs-floot-center-overlay, .mcs-ntally-indicator').forEach(el => el.remove());
addPriceOverlayToItem(itemElement);
}
});
}
});
updateCategoryTotals(changedCategories);
}
}
let inventoryObserver = null;
let observedInventoryPanel = null;
function observeInventoryChanges() {
const debouncedUpdate = debounce(() => {
updateAllInventoryPrices();
}, 750);
if (inventoryObserver) {
inventoryObserver.disconnect();
}
inventoryObserver = new MutationObserver((mutations) => {
if (document.hidden) return;
if (!inventoryPricesEnabled) {
return;
}
let shouldUpdate = false;
for (const mutation of mutations) {
const target = mutation.target;
if (mutation.type === 'characterData') {
const parent = target.parentElement;
if (parent && parent.className && typeof parent.className === 'string' &&
parent.className.includes('Item_count')) {
shouldUpdate = true;
break;
}
continue;
}
if (mutation.addedNodes.length > 0 || mutation.removedNodes.length > 0) {
shouldUpdate = true;
break;
}
if (target.className && typeof target.className === 'string') {
if (target.className.includes('Inventory_') || target.className.includes('Item_')) {
shouldUpdate = true;
break;
}
}
}
if (shouldUpdate) {
debouncedUpdate();
}
});
attachObserverToInventory();
setTimeout(() => updateAllInventoryPrices(true), 2000);
}
function attachObserverToInventory() {
const inventoryPanel = document.querySelector('[class*="Inventory_items"]');
if (inventoryPanel && inventoryPanel !== observedInventoryPanel) {
if (inventoryObserver) {
inventoryObserver.disconnect();
}
observedInventoryPanel = inventoryPanel;
inventoryObserver.observe(inventoryPanel, {
childList: true,
subtree: true,
characterData: true,
attributes: false
});
}
}
function checkAndRestoreInventoryValues() {
if (!inventoryPricesEnabled) {
return;
}
const inventoryPanel = document.querySelector('[class*="Inventory_items"]');
if (!inventoryPanel) {
observedInventoryPanel = null;
return;
}
if (inventoryPanel !== observedInventoryPanel) {
attachObserverToInventory();
}
const allItems = inventoryPanel.querySelectorAll('[class*="Item_itemContainer"] svg');
if (allItems.length === 0) {
return;
}
const itemsWithPrices = inventoryPanel.querySelectorAll('.mcs-price-overlay');
if (itemsWithPrices.length < allItems.length) {
inventoryState.clear();
updateAllInventoryPrices(true);
}
const categoryLabels = inventoryPanel.querySelectorAll('[class*="Inventory_label"]');
const labelsWithTotals = inventoryPanel.querySelectorAll('.mcs-category-total');
if (categoryLabels.length > 0 && labelsWithTotals.length < categoryLabels.length * 0.5) {
updateCategoryTotals();
}
}
window.toggleInventoryPrices = function(visible) {
inventoryPricesEnabled = visible;
if (visible) {
inventoryState.clear();
observeInventoryChanges();
updateAllInventoryPrices(true);
} else {
if (inventoryObserver) {
inventoryObserver.disconnect();
inventoryObserver = null;
observedInventoryPanel = null;
}
const priceOverlays = document.querySelectorAll('.mcs-price-overlay');
const categoryTotals = document.querySelectorAll('.mcs-category-total');
priceOverlays.forEach(overlay => {
overlay.remove();
});
categoryTotals.forEach(total => {
total.remove();
});
inventoryState.clear();
}
};
window.toggleFlootFullNumbers = function(showFull) {
useFullNumbers = showFull;
if (typeof injectValuesAndSort === 'function') {
injectValuesAndSort();
}
if (inventoryPricesEnabled) {
updateAllInventoryPrices(true);
}
if (window.skeletonInstance && typeof window.skeletonInstance.updateOPanelCombatRevenue === 'function') {
window.skeletonInstance.updateOPanelCombatRevenue();
}
};
window.toggleMarketWarnings = function(visible) {
marketWarningsEnabled = visible;
if (!visible) {
document.querySelectorAll('[class*="Marketplace_"] .mcs-floot-center-overlay').forEach(el => el.remove());
document.querySelectorAll('.mcs-nt-marketplace-scam-warning').forEach(el => el.remove());
}
};
window.toggleInventoryWarnings = function(visible) {
inventoryWarningsEnabled = visible;
if (!visible) {
document.querySelectorAll('[class*="Inventory_"] .mcs-floot-center-overlay').forEach(el => el.remove());
} else if (inventoryPricesEnabled) {
inventoryState.clear();
updateAllInventoryPrices(true);
}
};
const initMarketWarningsState = () => {
const savedStates = ToolVisibilityStorage.get();
marketWarningsEnabled = savedStates['floot-market-warnings'] !== false;
};
const initInventoryWarningsState = () => {
const savedStates = ToolVisibilityStorage.get();
inventoryWarningsEnabled = savedStates['floot-inventory-warnings'] !== false;
};
const initInventoryPriceVisibility = () => {
const savedStates = ToolVisibilityStorage.get();
const isVisible = savedStates['floot-inventory-value'] !== false;
inventoryPricesEnabled = isVisible;
if (!isVisible) {
const priceOverlays = document.querySelectorAll('.mcs-price-overlay');
const categoryTotals = document.querySelectorAll('.mcs-category-total');
priceOverlays.forEach(overlay => overlay.remove());
categoryTotals.forEach(total => total.remove());
}
};
const initFullNumbersState = () => {
const savedStates = ToolVisibilityStorage.get();
useFullNumbers = savedStates['floot-full-numbers'] !== false;
};
let lastWebSocketCheckTime = Date.now();
let webSocketReconnectDetected = false;
function flootHandleWsReconnect(event) {
if (window.MCS_MODULES_DISABLED) return;
const now = Date.now();
const timeSinceLastCheck = now - lastWebSocketCheckTime;
if (event.detail?.type === 'init_client_data' || event.detail?.type === 'init_character_data') {
if (timeSinceLastCheck > 30000 && !webSocketReconnectDetected) {
webSocketReconnectDetected = true;
loadMarketData().then(() => {
if (inventoryPricesEnabled) {
setTimeout(() => {
inventoryState.clear();
updateAllInventoryPrices(true);
}, 1000);
}
webSocketReconnectDetected = false;
});
}
}
lastWebSocketCheckTime = now;
}
function setupWebSocketReconnectHandler() {
window.addEventListener('EquipSpyWebSocketMessage', flootHandleWsReconnect);
}
function wrapRenderCurrentView() {
if (window.lootDropsTrackerInstance && window.lootDropsTrackerInstance.renderCurrentView) {
const originalRender = window.lootDropsTrackerInstance.renderCurrentView;
window.lootDropsTrackerInstance.renderCurrentView = function(...args) {
invalidatePlayerSectionsCache();
const result = originalRender.apply(this, args);
setTimeout(() => {
if (typeof injectValuesAndSort === 'function') {
injectValuesAndSort();
}
}, 50);
return result;
};
}
}
function tryWrapRenderCurrentView() {
if (window.lootDropsTrackerInstance && window.lootDropsTrackerInstance.renderCurrentView) {
wrapRenderCurrentView();
} else {
setTimeout(tryWrapRenderCurrentView, 500);
}
}
async function flootHandlePricesUpdated() {
await calculateChestValues();
itemPriceCache = {};
if (window.lootDropsTrackerInstance) {
window.lootDropsTrackerInstance.renderCurrentView();
}
if (inventoryPricesEnabled) {
updateAllInventoryPrices(true);
}
}
window.addEventListener('FlootPricesUpdated', flootHandlePricesUpdated);
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', () => {
setTimeout(() => {
initFullNumbersState();
initMarketWarningsState();
initInventoryWarningsState();
observeInventoryChanges();
initInventoryPriceVisibility();
setupWebSocketReconnectHandler();
loadMarketData();
tryWrapRenderCurrentView();
VisibilityManager.register('floot-inventory-check', () => {
checkAndRestoreInventoryValues();
}, 10000);
}, 2000);
});
} else {
setTimeout(() => {
initFullNumbersState();
initMarketWarningsState();
initInventoryWarningsState();
observeInventoryChanges();
initInventoryPriceVisibility();
setupWebSocketReconnectHandler();
loadMarketData();
tryWrapRenderCurrentView();
VisibilityManager.register('floot-inventory-check', () => {
checkAndRestoreInventoryValues();
}, 10000);
}, 2000);
}
// Class start
class LootDropsTracker {
constructor() {
if (document.getElementById(CONFIG.CONTAINER_ID)) {
return;
}
this.spyOpenComparisons = new Set();
this.marketDataCache = null;
this.marketDataCacheTime = 0;
this.CACHE_DURATION = 3000;
this.partyConsumableTracker = {};
this.lastMinTimes = {};
this.isUpdatingMinimizedSummary = false;
this.hwhatCowbellPollInterval = null;
this.sessionInitialCoins = {};
this.totalPerDay = 0;
this.totalPerDayFormatted = '';
this.consumableResetPending = false;
this.consumableResetBattleCount = 0;
this.consumableWaitingForFreshData = false;
this.userName = null;
this.playerDropStats = {};
this.encounterCount = 0;
this.sessionStartTime = null;
this.firstBattleSeenTime = null;
this.lastBattleTimestamp = 0;
this.sessionHistory = readSessionHistory();
this.viewingLive = true;
const startHiddenVal = flStorage.get('start_hidden');
this.startHiddenEnabled = startHiddenVal === true || startHiddenVal === 'true';
const minimizedVal = flStorage.get('minimized');
this.isHidden = this.startHiddenEnabled || minimizedVal === true || minimizedVal === 'true';
this.isMoving = false;
this.moveOffset = { x: 0, y: 0 };
this.initialRight = 0;
this.initialClientX = 0;
this.domRefs = {
panel: null, header: null, content: null, showButton: null, hideButton: null,
exportButton: null, tooltip: null, timerDisplay: null, searchInput: null,
settingsButton: null, settingsMenu: null, startHiddenCheckbox: null,
clearButton: null,
historySelect: null,
headerTopRow: null, headerControls: null, headerBottomRow: null
};
this.startTime = null;
this.sessionEndTime = null;
this.lastKnownActionHrid = null;
this.purchaseTimerInterval = null;
this.purchaseTimerIntervals = {};
this.coinHeaderInterval = null;
this.timerInterval = null;
this.isLiveSessionActive = false;
this.sortPreference = flStorage.get('sort', 'count');
this.currentSearchTerm = '';
this.currentSessionKey = null;
this.isSettingsMenuVisible = false;
this.aggregatedHistoryData = null;
this.aggregatedHistoryDuration = 0;
this.debouncedRender = debounce(this.renderCurrentView, CONFIG.SEARCH_DEBOUNCE_MS);
this.goldPerDay = 0;
this.ignoreNextStorageEvent = false;
this.spyMarketDataTimestamp = null;
this.spySimpleMode = false;
window.MCS_IN_COMBAT = false;
this.init();
this.comparisonOrder = [];
this.draggedSlot = null;
}
get spyCharacterItems() {
return this._spyCharacterItems ?? [];
}
set spyCharacterItems(value) {
if (this._spyCharacterItems && this._spyCharacterItems.length > 10 && value.length < 10) {
console.warn('[EquipSpy] WARNING: Character items being reduced from', this._spyCharacterItems.length, 'to', value.length);
console.warn('[EquipSpy] This looks like loot drops overwriting equipment!');
const oldEquipped = this._spyCharacterItems.filter(item => {
if (!item.itemLocationHrid) return false;
if (item.itemLocationHrid === '/item_locations/inventory') return false;
const slot = item.itemLocationHrid.replace('/item_locations/', '');
return this.spyConfig?.ALLOWED_SLOTS?.includes(slot);
});
const newInventory = value.filter(item => !item.itemLocationHrid || item.itemLocationHrid === '/item_locations/inventory'
);
const merged = [...oldEquipped, ...newInventory];
this._spyCharacterItems = merged;
const bridge = document.getElementById('equipspy-data-bridge');
if (bridge) {
bridge.setAttribute('data-character-items', JSON.stringify(merged));
}
return;
}
this._spyCharacterItems = value;
const bridge = document.getElementById('equipspy-data-bridge');
if (bridge) {
bridge.setAttribute('data-character-items', JSON.stringify(value));
}
}
createEquipSpyBridge() {
if (document.getElementById('equipspy-data-bridge')) return;
const bridge = document.createElement('div');
bridge.id = 'equipspy-data-bridge';
bridge.className = 'mcs-data-bridge';
document.body.appendChild(bridge);
}
init() {
this.injectCss();
this.createPanel();
this.setInitialCheckboxState();
this.createTooltipElement();
this.findUserName();
this.bindUiEvents();
this.createEquipSpyBridge();
this.setupFeedListener();
this.initFCB();
if (!this.isHidden) {
this.renderCurrentView();
this.updateTimerDisplay();
}
}
injectCss() {
const styleId = `${CONFIG.CONTAINER_ID}-style`;
if (document.getElementById(styleId)) return;
const css = `
#${CONFIG.CONTAINER_ID} {
--ldt-bg-primary: #333; --ldt-bg-header: #444; --ldt-bg-input: #222; --ldt-bg-hidden: #222; --ldt-bg-highlight: rgba(76, 175, 80, 0.18); --ldt-bg-settings-menu: #4a4a4a; --ldt-bg-select: #555; --ldt-bg-button-show: #4CAF50; --ldt-bg-button-show-hover: #5cb860; --ldt-bg-button-export: #0d6efd; --ldt-bg-button-export-border: #0b5ed7; --ldt-bg-button-hide: #6c757d; --ldt-bg-button-hide-border: #5c636a; --ldt-bg-button-sort: #7b4caf; --ldt-bg-button-sort-border: #6a3f9a; --ldt-bg-button-sort-hover: #8a5cb9; --ldt-bg-button-copied: #4CAF50; --ldt-bg-button-copied-border: #45a049; --ldt-bg-button-error: #f44336; --ldt-bg-button-error-border: #d32f2f; --ldt-bg-button-settings: #555; --ldt-bg-button-settings-border: #444; --ldt-bg-button-clear: #dc3545; --ldt-bg-button-clear-border: #b02a37;
--ldt-text-primary: #eee; --ldt-text-secondary: #bbb; --ldt-text-header: #fff; --ldt-text-button: #eee; --ldt-text-button-dark-bg: #fff; --ldt-text-player-current: #4CAF50; --ldt-text-player-other: #ddd; --ldt-text-select: #eee;
--ldt-border-primary: #555; --ldt-border-separator: #555; --ldt-border-header-bottom: #555; --ldt-border-item-dotted: #666; --ldt-border-item-dashed: #555; --ldt-border-settings-menu: #666; --ldt-border-select: #777;
--ldt-font-size-base: 13px; --ldt-font-size-small: 0.9em; --ldt-font-size-smaller: 0.85em; --ldt-font-size-large: 1.1em;
--ldt-radius: 5px; --ldt-radius-small: 3px;
--ldt-shadow: 0 4px 12px rgba(0, 0, 0, 0.5); --ldt-transition-duration: 0.25s; --ldt-transition-timing: ease-out;
}
#${CONFIG.CONTAINER_ID} { position: fixed; top: 0; right: 30px; left: auto; background-color: var(--ldt-bg-primary); color: var(--ldt-text-primary); font-family: sans-serif; font-size: var(--ldt-font-size-base); border: none; border-radius: var(--ldt-radius); z-index: 99997; user-select: none; box-shadow: var(--ldt-shadow); display: flex; flex-direction: column; overflow: hidden; transition: width var(--ldt-transition-duration) var(--ldt-transition-timing), height var(--ldt-transition-duration) var(--ldt-transition-timing), min-width var(--ldt-transition-duration) var(--ldt-transition-timing), opacity var(--ldt-transition-duration) var(--ldt-transition-timing); box-sizing: border-box; cursor: default; }
#${CONFIG.CONTAINER_ID}:not(.is-hidden) { width: auto; min-width: ${CONFIG.PANEL_WIDTH_EXPANDED}; height: auto; max-height: ${CONFIG.PANEL_MAX_HEIGHT}; opacity: 1; }
#${CONFIG.CONTAINER_ID}:not(.is-hidden) .hidden-state-only { display: none; }
#${CONFIG.CONTAINER_ID}:not(.is-hidden) .visible-state-only { display: flex; }
#${CONFIG.CONTAINER_ID}.is-hidden { width: auto; height: auto; min-width: 0; background: var(--ldt-bg-hidden); border-radius: var(--ldt-radius); padding: 4px 8px; cursor: move !important; flex-direction: row; align-items: center; opacity: 0.95; overflow: visible; max-height: none; }
#${CONFIG.CONTAINER_ID}.is-hidden:hover { opacity: 1; }
#${CONFIG.CONTAINER_ID}.is-hidden .visible-state-only { display: none; }
#${CONFIG.CONTAINER_ID}.is-hidden .hidden-state-only { display: flex; flex-direction: column; align-items: flex-start; gap: 3px; }
.ldt-hidden-header { display: flex; justify-content: space-between; align-items: center; width: 100%; margin-bottom: 5px; border-bottom: 1px solid var(--ldt-border-primary); padding-bottom: 3px; }
.ldt-hidden-title-row { display: flex; align-items: center; gap: 8px; flex: 1; }
.ldt-hidden-title { font-weight: bold; font-size: 1em; color: var(--ldt-text-header); }
.ldt-hidden-player-row { display: flex; align-items: center; gap: 8px; min-width: 250px; white-space: nowrap; }
.ldt-hidden-player-name { color: var(--ldt-text-primary); font-size: 0.9em; width: 100px; flex-shrink: 0; }
.ldt-hidden-player-name.is-current { color: var(--ldt-text-player-current); font-weight: bold; }
.ldt-hidden-player-stats { display: flex; align-items: center; gap: 4px; font-size: 0.85em; white-space: nowrap; }
.ldt-hidden-coin-emoji { flex-shrink: 0; }
.ldt-hidden-coin-amount { color: #cccccc; font-weight: bold; min-width: 60px; text-align: left; flex-shrink: 0; }
.ldt-hidden-coin-rate { color: #90EE90; font-weight: bold; min-width: 80px; text-align: right; flex-shrink: 0; }
#${CONFIG.CONTAINER_ID}.is-hidden .ldt-hide-label { font-weight: normal; color: var(--ldt-text-primary); font-size: 1em; cursor: move; user-select: none; }
#${CONFIG.CONTAINER_ID}.is-hidden .ldt-show-btn { color: var(--ldt-text-button-dark-bg); padding: 2px 5px; border-radius: var(--ldt-radius-small); text-decoration: none; font-size: var(--ldt-font-size-small); cursor: pointer; border: none; line-height: normal; vertical-align: middle; font-weight: normal; }
#${CONFIG.CONTAINER_ID}.is-hidden .ldt-show-btn:hover { filter: brightness(1.1); }
.ldt-panel-header { flex-direction: column; padding: 5px 8px 8px 8px; background: var(--ldt-bg-header); border-bottom: 1px solid var(--ldt-border-header-bottom); flex-shrink: 0; user-select: none; }
.ldt-header-top-row { display: flex; justify-content: space-between; align-items: center; width: 100%; margin-bottom: 5px; cursor: move; gap: 8px; }
.ldt-panel-title-area { display: flex; align-items: center; gap: 6px; margin-right: auto; min-width: 0; }
.ldt-timer { color: var(--ldt-text-primary); font-size: var(--ldt-font-size-small); font-weight: normal; white-space: nowrap; margin-left: auto; }
.ldt-hidden-timer { color: var(--ldt-text-primary); font-size: 0.85em; font-weight: normal; white-space: nowrap; }
.ldt-panel-title { font-weight: bold; font-size: var(--ldt-font-size-large); color: var(--ldt-text-header); flex-shrink: 0; }
.ldt-history-select { background-color: var(--ldt-bg-select); color: var(--ldt-text-select); border: 1px solid var(--ldt-border-select); border-radius: var(--ldt-radius-small); padding: 2px 4px; font-size: var(--ldt-font-size-smaller); max-width: 150px; cursor: pointer; flex-shrink: 1; }
.ldt-history-select optgroup { font-style: italic; font-weight: bold; }
.ldt-header-controls { position: relative; display: flex; align-items: center; gap: 4px; flex-shrink: 0; }
.ldt-header-controls button { position: relative; border-radius: var(--ldt-radius-small); padding: 3px 7px; font-size: var(--ldt-font-size-small); font-weight: normal; cursor: pointer; transition: filter 0.15s, background-color 0.15s, border-color 0.15s, color 0.15s; font-family: inherit; line-height: 1.2; border: 1px solid var(--ldt-border-primary); color: var(--ldt-text-button); display: inline-flex; align-items: center; justify-content: center; }
.ldt-header-controls button:hover { filter: brightness(1.15); }
.ldt-clear-btn { background-color: var(--ldt-bg-button-clear); border-color: var(--ldt-bg-button-clear-border); color: var(--ldt-text-button-dark-bg); }
.ldt-export-btn { background-color: var(--ldt-bg-button-export); border-color: var(--ldt-bg-button-export-border); color: var(--ldt-text-button-dark-bg); }
.ldt-hide-btn { background-color: var(--ldt-bg-button-hide); border-color: var(--ldt-bg-button-hide-border); color: var(--ldt-text-button-dark-bg); }
.ldt-btn-copied { background-color: var(--ldt-bg-button-copied) !important; border-color: var(--ldt-bg-button-copied-border) !important; color: var(--ldt-text-button-dark-bg) !important; }
.ldt-btn-error { background-color: var(--ldt-bg-button-error) !important; border-color: var(--ldt-bg-button-error-border) !important; color: var(--ldt-text-button-dark-bg) !important; }
.ldt-header-bottom-row { position: relative; display: flex; justify-content: space-between; align-items: center; width: 100%; gap: 8px; }
.ldt-search-input { background-color: var(--ldt-bg-input); color: var(--ldt-text-primary); border: 1px solid var(--ldt-border-primary); border-radius: var(--ldt-radius-small); padding: 3px 5px; font-size: var(--ldt-font-size-small); flex-grow: 1; min-width: 90px; max-width: 140px; cursor: text; }
.ldt-settings-btn { background-color: var(--ldt-bg-button-settings); border-color: var(--ldt-bg-button-settings-border); color: var(--ldt-text-button); padding: 3px 5px; margin-left: auto; order: 1; }
.ldt-settings-btn svg { width: 1em; height: 1em; fill: currentColor; display: block; }
.ldt-sort-btn { background-color: var(--ldt-bg-button-sort); border-color: var(--ldt-bg-button-sort-border); color: var(--ldt-text-button); padding: 3px 7px; font-size: var(--ldt-font-size-smaller); flex-shrink: 0; order: 2; }
.ldt-sort-btn:hover { filter: brightness(1.15); background-color: var(--ldt-bg-button-sort-hover); border-color: var(--ldt-bg-button-sort-border);}
.ldt-timer { font-size: var(--ldt-font-size-small); color: var(--ldt-text-secondary); flex-shrink: 0; padding: 0 5px; white-space: nowrap; min-width: 85px; text-align: right; order: 3; }
.ldt-settings-menu {display: none; position: absolute; top: 100%; left: 0; background-color: var(--ldt-bg-settings-menu); border: 1px solid var(--ldt-border-settings-menu); border-radius: var(--ldt-radius-small); padding: 8px; z-index: 10; box-shadow: 0 2px 5px rgba(0,0,0,0.3); margin-top: 4px; white-space: nowrap; }
.ldt-settings-menu.visible { display: block; }
.ldt-settings-menu label { display: flex; align-items: center; cursor: pointer; color: var(--ldt-text-primary); font-size: var(--ldt-font-size-small); }
.ldt-settings-menu input[type="checkbox"] { margin-right: 6px; cursor: pointer; }
.ldt-panel-body { display: flex; flex-direction: row; flex-grow: 1; overflow-y: auto; overflow-x: auto; background-color: var(--ldt-bg-primary); cursor: auto; padding: 0; }
.ldt-body-empty { padding: 15px; text-align: center; color: var(--ldt-text-secondary); font-style: italic; width: 100%; line-height: 1.4; }
.ldt-player-stats-section { flex: 1 1 auto; min-width: ${CONFIG.PLAYER_COLUMN_MIN_WIDTH}; padding: 8px 10px; box-sizing: border-box; }
.ldt-player-stats-section:not(:first-child) { border-left: 1px solid var(--ldt-border-separator); padding-left: 10px; }
.ldt-player-name-header { font-weight: bold; color: var(--ldt-text-player-other); background-color: transparent; padding: 4px 0 5px 0; margin-bottom: 5px; border-bottom: 1px dotted var(--ldt-border-item-dotted); font-size: 0.95em; }
.ldt-player-name-header.is-current-player { color: var(--ldt-text-player-current); }
.ldt-loot-list { padding: 0; list-style: none; margin: 0; }
.ldt-loot-item-entry { display: flex; justify-content: space-between; align-items: center; margin-bottom: 3px; padding: 2px 0px; font-size: 1em; border-bottom: 1px dashed var(--ldt-border-item-dashed); color: var(--ldt-text-primary); transition: background-color 0.2s linear; }
.ldt-loot-item-entry:last-child { border-bottom: none; }
.ldt-item-name { display:none; color: var(--ldt-text-primary); flex-grow: 1; margin-right: 10px; word-break: break-word; }
.ldt-vis-name { color: var(--ldt-text-primary); flex-grow: 1; margin-right: 10px; word-break: break-word; }
.ldt-item-value { color: yellow; margin-right:5px; font-weight: normal; white-space: nowrap; }
.ldt-item-count { color: var(--ldt-text-primary); font-weight: normal; white-space: nowrap; }
.ldt-loot-item-entry.highlight-match { background-color: var(--ldt-bg-highlight); }
#${CONFIG.TOOLTIP_ID} { position: fixed; background-color: #222; color: #eee; padding: 4px 8px; font-size: 11px; border-radius: var(--ldt-radius-small); white-space: nowrap; opacity: 0; visibility: hidden; pointer-events: none; z-index: 100002; transition: opacity 0.2s ease-out, visibility 0s linear 0.2s; will-change: transform, opacity; }
#${CONFIG.TOOLTIP_ID}.visible { opacity: 0.9; visibility: visible; transition: opacity 0.2s ease-out, visibility 0s linear 0s; }`;
const styleElement = document.createElement('style');
styleElement.id = styleId;
styleElement.textContent = css;
document.head.appendChild(styleElement);
}
createPanel() {
const panel = document.createElement('div');
panel.id = CONFIG.CONTAINER_ID;
registerPanel(CONFIG.CONTAINER_ID);
this.domRefs.panel = panel;
let savedPosition = {};
try {
savedPosition = flStorage.get('position') ?? {};
} catch (e) { console.error("LDT: Error parsing saved position", e); }
if (savedPosition.top || savedPosition.left || savedPosition.right) {
panel.style.top = savedPosition.top || CONFIG.DEFAULT_POS.top;
if (savedPosition.left) {
panel.style.left = savedPosition.left;
panel.style.right = 'auto';
} else {
panel.style.right = savedPosition.right || CONFIG.DEFAULT_POS.right;
panel.style.left = 'auto';
}
}
if (this.isHidden) panel.classList.add('is-hidden');
const settingsIconSvg = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16" fill="currentColor"><path d="M9.405 1.05c-.413-1.4-2.397-1.4-2.81 0l-.1.34a1.464 1.464 0 0 1-2.105.872l-.31-.17c-1.283-.698-2.686.705-1.987 1.987l.169.311c.446.82.023 1.841-.872 2.105l-.34.1c-1.4.413-1.4 2.397 0 2.81l.34.1a1.464 1.464 0 0 1 .872 2.105l-.17.31c-.698 1.283.705 2.686 1.987 1.987l.311-.169a1.464 1.464 0 0 1 2.105.872l.1.34c.413 1.4 2.397 1.4 2.81 0l.1-.34a1.464 1.464 0 0 1 2.105-.872l.31.17c1.283.698 2.686-.705 1.987-1.987l-.169-.311a1.464 1.464 0 0 1 .872-2.105l.34-.1c1.4-.413 1.4-2.397 0-2.81l-.34-.1a1.464 1.464 0 0 1-.872-2.105l.17-.31c.698-1.283-.705-2.686-1.987-1.987l-.311.169a1.464 1.464 0 0 1-2.105-.872l-.1-.34zM8 10.93a2.929 2.929 0 1 1 0-5.858 2.929 2.929 0 0 1 0 5.858z"/></svg>`;
const clearIconSvg = `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-trash" viewBox="0 0 16 16"><path d="M5.5 5.5A.5.5 0 0 1 6 6v6a.5.5 0 0 1-1 0V6a.5.5 0 0 1 .5-.5zm2.5 0a.5.5 0 0 1 .5.5v6a.5.5 0 0 1-1 0V6a.5.5 0 0 1 .5-.5zm3 .5a.5.5 0 0 0-1 0v6a.5.5 0 0 0 1 0V6z"/><path fill-rule="evenodd" d="M14.5 3a1 1 0 0 1-1 1H13v9a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V4h-.5a1 1 0 0 1-1-1V2a1 1 0 0 1 1-1H6a1 1 0 0 1 1-1h2a1 1 0 0 1 1 1h3.5a1 1 0 0 1 1 1v1zM4.118 4 4 4.059V13a1 1 0 0 0 1 1h6a1 1 0 0 0 1-1V4.059L11.882 4H4.118zM2.5 3V2h11v1h-11z"/></svg>`;
panel.innerHTML = `
<div class="hidden-state-only">
<div class="ldt-hidden-header">
<div class="ldt-hidden-title-row">
<span class="ldt-hidden-title">FLoot</span>
<span class="ldt-hidden-timer" id="${CONFIG.CONTAINER_ID}-timer-hidden">--:--:--</span>
<button class="ldt-price-toggle-btn-hidden" id="${CONFIG.CONTAINER_ID}-price-toggle-hidden">Showing Bid</button>
</div>
<button class="ldt-show-btn ldt-content-toggle-btn" id="${CONFIG.CONTAINER_ID}-minbtn-minimized">+</button>
</div>
<div id="${CONFIG.CONTAINER_ID}-hidden-players"></div>
</div>
<div class="visible-state-only ldt-panel-header" id="${CONFIG.CONTAINER_ID}-header">
<div class="ldt-header-top-row" id="${CONFIG.CONTAINER_ID}-header-top">
<div class="ldt-panel-title-area">
<span class="ldt-panel-title">FLoot</span>
<span class="ldt-timer" id="${CONFIG.CONTAINER_ID}-timer">--:--:--</span>
<select class="ldt-history-select" id="${CONFIG.CONTAINER_ID}-history-select" title="View session history"></select>
</div>
<div class="ldt-header-controls" id="${CONFIG.CONTAINER_ID}-header-controls-top">
<button class="ldt-price-toggle-btn" id="${CONFIG.CONTAINER_ID}-price-toggle" data-tooltip="Toggle between Bid and Ask prices">Showing Bid</button>
<button class="ldt-settings-btn" id="${CONFIG.CONTAINER_ID}-settingsbtn" data-tooltip="Settings">${settingsIconSvg}</button>
<div class="ldt-settings-menu" id="${CONFIG.CONTAINER_ID}-settings-menu">
<label>
<input type="checkbox" id="${CONFIG.CONTAINER_ID}-starthidden-check"> Start Hidden
</label>
</div>
<button class="ldt-clear-btn" id="${CONFIG.CONTAINER_ID}-clearbtn" data-tooltip="Clear Your History">${clearIconSvg}</button>
<button class="ldt-export-btn" id="${CONFIG.CONTAINER_ID}-exportbtn" data-tooltip="Copy loot as CSV to clipboard">Export</button>
</div>
<button class="ldt-minimize-content-btn ldt-content-toggle-btn" id="${CONFIG.CONTAINER_ID}-minimize-content-btn">−</button>
</div>
</div>
<div class="visible-state-only ldt-panel-body" id="${CONFIG.CONTAINER_ID}-content"></div>`;
document.body.appendChild(panel);
Object.assign(this.domRefs, {
panel: panel,
header: panel.querySelector(`#${CONFIG.CONTAINER_ID}-header`),
headerTopRow: panel.querySelector(`#${CONFIG.CONTAINER_ID}-header-top`),
headerControls: panel.querySelector(`#${CONFIG.CONTAINER_ID}-header-controls-top`),
content: panel.querySelector(`#${CONFIG.CONTAINER_ID}-content`),
showButton: panel.querySelector(`#${CONFIG.CONTAINER_ID}-minbtn-minimized`),
minimizeContentButton: panel.querySelector(`#${CONFIG.CONTAINER_ID}-minimize-content-btn`),
settingsButton: panel.querySelector(`#${CONFIG.CONTAINER_ID}-settingsbtn`),
settingsMenu: panel.querySelector(`#${CONFIG.CONTAINER_ID}-settings-menu`),
clearButton: panel.querySelector(`#${CONFIG.CONTAINER_ID}-clearbtn`),
exportButton: panel.querySelector(`#${CONFIG.CONTAINER_ID}-exportbtn`),
historySelect: panel.querySelector(`#${CONFIG.CONTAINER_ID}-history-select`),
timerDisplay: panel.querySelector(`#${CONFIG.CONTAINER_ID}-timer`),
timerDisplayHidden: panel.querySelector(`#${CONFIG.CONTAINER_ID}-timer-hidden`),
startHiddenCheckbox: panel.querySelector(`#${CONFIG.CONTAINER_ID}-starthidden-check`)
});
}
aggregateSessionHistory() {
if (!this.userName) {
this.aggregatedHistoryData = {};
this.aggregatedHistoryDuration = 0;
return;
}
const aggregatedStats = {
[this.userName]: { items: {} }
};
let totalDuration = 0;
const allRelevantHistory = this.sessionHistory.filter(s => s.key.split('@')[0].split(',').includes(this.userName)
);
const sortedRelevantHistory = allRelevantHistory.sort((a, b) => b.start - a.start);
const sessionsToCombine = sortedRelevantHistory.slice(0, CONFIG.DISPLAY_HISTORY_LIMIT);
sessionsToCombine.forEach(session => {
totalDuration += session.duration ?? 0;
if (session.stats && session.stats[this.userName] && session.stats[this.userName].items) {
Object.entries(session.stats[this.userName].items).forEach(([hrid, count]) => {
aggregatedStats[this.userName].items[hrid] = (aggregatedStats[this.userName].items[hrid] ?? 0) + count;
});
}
});
const liveSessionHasData = this.userName &&
this.playerDropStats[this.userName]?.items &&
Object.keys(this.playerDropStats[this.userName].items).length > 0;
if (liveSessionHasData && this.currentSessionKey) {
const liveSessionKey = this.currentSessionKey;
const liveSessionAlreadyIncluded = sessionsToCombine.some(s => s.key === liveSessionKey);
if (!liveSessionAlreadyIncluded) {
Object.entries(this.playerDropStats[this.userName].items).forEach(([hrid, count]) => {
aggregatedStats[this.userName].items[hrid] = (aggregatedStats[this.userName].items[hrid] ?? 0) + count;
});
if (this.startTime) {
const liveEndTime = this.sessionEndTime || Date.now();
const liveDuration = Math.max(0, liveEndTime - this.startTime.getTime());
totalDuration += liveDuration;
}
}
}
this.aggregatedHistoryData = aggregatedStats;
this.aggregatedHistoryDuration = totalDuration;
}
updateHistoryDropdown() {
if (!this.domRefs.historySelect || !this.userName) return;
const select = this.domRefs.historySelect;
const previousValue = select.value;
select.innerHTML = '';
const liveOption = document.createElement('option');
liveOption.value = 'live';
liveOption.textContent = 'Live Session';
select.appendChild(liveOption);
const relevantHistory = this.sessionHistory
.filter(s => s.key.split('@')[0].split(',').includes(this.userName))
.sort((a, b) => b.start - a.start)
.slice(0, CONFIG.DISPLAY_HISTORY_LIMIT);
if (relevantHistory.length > 0) {
const historyGroup = document.createElement('optgroup');
historyGroup.label = 'Past Sessions';
relevantHistory.forEach((session) => {
const option = document.createElement('option');
option.value = session.key;
const startTime = new Date(session.start).toLocaleTimeString([], { hour: 'numeric', minute: '2-digit' });
const durationStr = this.formatElapsedTime(session.duration).replace(/^00:/, '');
option.textContent = `${startTime} (${durationStr})`;
const locationStr = session.location ? `Location: ${session.location}\n` : '';
const playersStr = `Players: ${session.key.split('@')[0] || '?'}\n`;
option.title = `${locationStr}${playersStr}Started: ${new Date(session.start).toLocaleString()}\nEnded: ${new Date(session.end).toLocaleString()}`;
historyGroup.appendChild(option);
});
select.appendChild(historyGroup);
const combinedOption = document.createElement('option');
combinedOption.value = 'combined';
combinedOption.textContent = 'Combined History';
select.appendChild(combinedOption);
}
if (select.querySelector(`option[value="${previousValue}"]`)) {
select.value = previousValue;
} else {
select.value = 'live';
}
this.viewingLive = (select.value === 'live');
}
setInitialCheckboxState() {
if (this.domRefs.startHiddenCheckbox) {
this.domRefs.startHiddenCheckbox.checked = this.startHiddenEnabled;
}
}
createTooltipElement() {
const tooltip = document.createElement('div');
tooltip.id = CONFIG.TOOLTIP_ID;
document.body.appendChild(tooltip);
this.domRefs.tooltip = tooltip;
}
toggleVisibility() {
this.isHidden = !this.isHidden;
this.domRefs.panel.classList.toggle('is-hidden', this.isHidden);
flStorage.set('minimized', this.isHidden);
this.hideSettingsMenu();
if (!this.isHidden) {
this.renderCurrentView();
this.updateTimerDisplay();
if (this.viewingLive && this.isLiveSessionActive) {
this.startTimer();
}
setTimeout(() => {
const panel = this.domRefs.panel;
const rect = panel.getBoundingClientRect();
const winWidth = window.innerWidth;
const winHeight = window.innerHeight;
let needsAdjustment = false;
let newTop = parseFloat(panel.style.top) || rect.top;
let newRight = parseFloat(panel.style.right) || (winWidth - rect.right);
if (rect.right > winWidth) {
newRight = 10;
needsAdjustment = true;
}
if (rect.left < 0) {
const newLeft = 10;
newRight = winWidth - newLeft - rect.width;
needsAdjustment = true;
}
if (rect.bottom > winHeight) {
newTop = winHeight - rect.height - 10;
needsAdjustment = true;
}
if (rect.top < 0) {
newTop = 10;
needsAdjustment = true;
}
if (needsAdjustment) {
panel.style.top = newTop + 'px';
panel.style.right = newRight + 'px';
panel.style.left = 'auto';
const position = {
top: newTop + 'px',
right: newRight + 'px'
};
flStorage.set('position', position);
}
}, 50);
}
if (this.domRefs.minimizeContentButton) {
this.domRefs.minimizeContentButton.textContent = this.isHidden ? '+' : '−';
}
}
toggleContentMinimize() {
const content = this.domRefs.content;
const btn = this.domRefs.minimizeContentButton;
if (!content || !btn) return;
const isMinimized = content.style.display === 'none';
content.style.display = isMinimized ? 'block' : 'none';
btn.textContent = isMinimized ? '−' : '+';
localStorage.setItem('floot_contentMinimized', !isMinimized);
}
updatePlayerStats(playerInfo) {
const name = playerInfo.name;
if (!name) return;
if (!this.playerDropStats[name]) {
this.playerDropStats[name] = { items: {} };
}
const pStats = this.playerDropStats[name];
const currentLootMap = playerInfo.totalLootMap ?? {};
pStats.items = {};
for (const key in currentLootMap) {
const { itemHrid, count } = currentLootMap[key];
if (itemHrid && typeof count === 'number' && count > 0) {
pStats.items[itemHrid] = count;
}
}
}
renderLootSections(dataSource) {
window._isRendering = true;
if (!this.domRefs.content || this.isHidden) {
window._isRendering = false;
return;
}
const currentSessionKey = this.viewingLive ?
'live' :
(this.domRefs.historySelect?.value || 'live');
const playerNames = Object.keys(dataSource).sort((a, b) => {
if (a === this.userName) return -1;
if (b === this.userName) return 1;
return a.localeCompare(b);
});
this.domRefs.content.innerHTML = '';
if (playerNames.length === 0 || (this.isViewingCombined() && (!dataSource[this.userName] || Object.keys(dataSource[this.userName].items).length === 0))) {
let message = 'No active combat session.<br><small class="ldt-empty-help-text">If drops are happening but loot is not showing up here<br>Try refreshing the window<br><b>Browser</b>: F5 or Reload Button<br><b>Steam Client</b>: Top Menu > View > Reload</small>';
if (this.viewingLive && this.isLiveSessionActive) {
message = 'Waiting for loot data...';
} else if (this.viewingLive && !this.isLiveSessionActive && this.startTime) {
message = 'Session ended. No loot collected.';
} else if (!this.viewingLive && !this.isViewingCombined()) {
message = 'No data in selected session for this character.';
} else if (this.isViewingCombined()) {
message = `No loot found for ${this.userName} in the combined history view.`;
}
this.domRefs.content.innerHTML = `<div class="ldt-body-empty">${message}</div>`;
window._isRendering = false;
return;
}
const fragment = document.createDocumentFragment();
const searchTerm = this.currentSearchTerm.toLowerCase();
playerNames.forEach(playerName => {
if (this.isViewingCombined() && playerName !== this.userName) return;
const pStats = dataSource[playerName];
if (!pStats) return;
const items = pStats.items;
const sectionDiv = document.createElement('div');
sectionDiv.className = 'ldt-player-stats-section';
const headerDiv = document.createElement('div');
headerDiv.className = 'ldt-player-name-header';
headerDiv.textContent = playerName;
if (playerName === this.userName) {
headerDiv.classList.add('is-current-player');
}
const listDiv = document.createElement('ul');
listDiv.className = 'ldt-loot-list';
let sortedItems = Object.entries(items ?? {}).filter(([, count]) => count > 0);
if (this.sortPreference === 'name') {
sortedItems.sort((a, b) => a[0].localeCompare(b[0]));
} else {
sortedItems.sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0]));
}
if (sortedItems.length === 0) {
listDiv.innerHTML = `<li class="ldt-body-empty ldt-body-empty-item">No items collected.</li>`;
} else {
sortedItems.forEach(([hrid, count]) => {
const name = hrid;
const visname = hrid.replace("/items/",
"").replace(/_/g, " ");
const itemLi = document.createElement('li');
itemLi.className = 'ldt-loot-item-entry';
const visnameSpan = document.createElement('span');
visnameSpan.className = 'ldt-vis-name';
visnameSpan.textContent = visname;
const nameSpan = document.createElement('span');
nameSpan.className = 'ldt-item-name';
nameSpan.textContent = name;
const valueSpan = document.createElement('span');
valueSpan.className = 'ldt-item-value';
const calculatedValue = getUnitValue(name, currentSessionKey);
valueSpan.textContent = (calculatedValue !== null) ? calculatedValue : 'N/A';
const countSpan = document.createElement('span');
countSpan.className = 'ldt-item-count';
countSpan.textContent = ` × ${formatNumberWithCommas(count)}`;
if (searchTerm && name.toLowerCase().includes(searchTerm)) {
itemLi.classList.add('highlight-match');
}
itemLi.appendChild(visnameSpan);
itemLi.appendChild(nameSpan);
itemLi.appendChild(valueSpan);
itemLi.appendChild(countSpan);
listDiv.appendChild(itemLi);
setTimeout(() => {
}, 0);
});
}
sectionDiv.appendChild(headerDiv);
sectionDiv.appendChild(listDiv);
fragment.appendChild(sectionDiv);
});
this.domRefs.content.appendChild(fragment);
window._isRendering = false;
injectValuesAndSort();
}
renderCurrentView() {
if (this.isHidden) return;
const selectedValue = this.domRefs.historySelect?.value;
if (this.viewingLive || selectedValue === 'live' || !selectedValue) {
this.renderLootSections(this.playerDropStats);
} else if (selectedValue === 'combined') {
if (this.aggregatedHistoryData === null) {
this.aggregateSessionHistory();
}
this.renderLootSections(this.aggregatedHistoryData);
} else {
const selectedSession = this.sessionHistory.find(s => s.key === selectedValue);
if (selectedSession && selectedSession.key.split('@')[0].split(',').includes(this.userName)) {
this.renderLootSections(selectedSession.stats);
} else {
this.renderLootSections({});
}
}
}
exportLootCsv() {
let dataSource = {};
let sourceDescription = "Unknown View";
const selectedValue = this.domRefs.historySelect?.value;
if (this.viewingLive || selectedValue === 'live') {
dataSource = this.playerDropStats;
sourceDescription = "Live / Last Session";
if (this.lastKnownActionHrid) {
sourceDescription += ` (${formatLocationName(this.lastKnownActionHrid)})`;
}
} else if (selectedValue === 'combined') {
if (this.aggregatedHistoryData === null) this.aggregateSessionHistory();
dataSource = this.aggregatedHistoryData;
sourceDescription = `Combined History (${this.userName || 'Current User'})`;
} else {
const selectedSession = this.sessionHistory.find(s => s.key === selectedValue);
if (selectedSession && selectedSession.key.split('@')[0].split(',').includes(this.userName)) {
dataSource = selectedSession.stats;
const startTimeStr = new Date(selectedSession.start).toLocaleTimeString([], { hour: 'numeric', minute: '2-digit' });
const locationStr = selectedSession.location ? ` - ${selectedSession.location}` : '';
sourceDescription = `Session @ ${startTimeStr}${locationStr}`;
} else {
this.flashExportButtonState('Error', 'ldt-btn-error', 2500);
return;
}
}
const playerNames = Object.keys(dataSource);
const button = this.domRefs.exportButton;
button.disabled = true;
if (playerNames.length === 0 || (selectedValue === 'combined' && (!dataSource[this.userName] || Object.keys(dataSource[this.userName].items).length === 0))) {
this.flashExportButtonState('No Data', 'ldt-btn-error', 1500);
return;
}
let csvRows = [];
let hasItems = false;
playerNames.forEach(playerName => {
if (selectedValue === 'combined' && playerName !== this.userName) return;
const pStats = dataSource[playerName]; if (!pStats) return;
const items = pStats.items ?? {};
const sortedItems = Object.entries(items)
.filter(([, count]) => count > 0)
.sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0]));
sortedItems.forEach(([hrid, count]) => {
const itemName = hrid;
const visitemName = hrid.replace("/items/",
"").replace(/_/g, " ");
const safePlayerName = `"${playerName.replace(/"/g, '""')}"`;
const safeItemName = `"${itemName.replace(/"/g, '""')}"`;
csvRows.push(`${safePlayerName},${itemName},${count}`);
hasItems = true;
});
});
if (!hasItems) {
this.flashExportButtonState('No Items', 'ldt-btn-error', 1500);
return;
}
const csvHeader = "Player,Item Name,Count\n";
const csvContent = csvHeader + csvRows.join("\n");
navigator.clipboard.writeText(csvContent).then(() => {
this.flashExportButtonState('Copied!', 'ldt-btn-copied', 1800);
}).catch(err => {
console.error("LDT: Failed to copy CSV data to clipboard:", err);
this.flashExportButtonState('Error', 'ldt-btn-error', 2500);
});
}
flashExportButtonState(text, className, duration) {
const button = this.domRefs.exportButton;
if (!button) return;
const originalText = button.textContent;
const originalClasses = button.className;
button.textContent = text;
button.className = `ldt-export-btn ${className}`;
setTimeout(() => {
if (button) {
button.textContent = originalText;
button.className = originalClasses;
button.disabled = false;
}
}, duration);
}
ensureCombatSuiteVisible() {
const suiteBtn = document.getElementById('mwi-combat-suite-btn');
if (!suiteBtn) return;
const rect = suiteBtn.getBoundingClientRect();
const windowWidth = window.innerWidth;
if (rect.left > windowWidth - 100) {
const newLeft = Math.max(20, (windowWidth / 2) - 75);
suiteBtn.style.left = newLeft + 'px';
suiteBtn.style.right = 'auto';
localStorage.setItem('mcs__global_combat_suite_btn_position', JSON.stringify({
left: newLeft
}));
}
}
createEquipmentSpy() {
this.initializeGlobalStyles();
window.equipmentSpyInstance = this;
this.pageLoadTime = Date.now();
this.spyMarketData = {};
this._spyCharacterItems = [];
this.spyItemDetailMap = {};
const savedMinimized = this.ewStorage.get('minimized');
this.spyIsMinimized = savedMinimized === true || savedMinimized === 'true';
this.spyLockedComparisons = {};
this.spyPriceType = 'bid';
const CONFIG_SPY = {
PANE_ID: 'equipment-spy-pane',
MAIN_COLOR: '#4CAF50',
BG_COLOR: 'rgba(30, 30, 30, 0.95)',
BORDER_COLOR: '#555',
ALLOWED_SLOTS: [
'body', 'charm', 'earrings', 'feet', 'hands',
'head', 'legs', 'main_hand', 'neck', 'off_hand',
'pouch', 'ring', 'two_hand'
]
};
this.spyConfig = CONFIG_SPY;
this.loadSpyMarketData();
const savedTimestamp = localStorage.getItem('mcs__global_EW_market_timestamp');
if (savedTimestamp) {
this.spyMarketDataTimestamp = parseInt(savedTimestamp);
}
this.loadSpySettings();
const savedSimpleMode = this.ewStorage.get('simple_mode');
if (savedSimpleMode === true || savedSimpleMode === 'true') {
this.spySimpleMode = true;
}
this.spyNoSellMode = false;
const savedNoSellMode = this.ewStorage.get('no_sell_mode');
if (savedNoSellMode === true || savedNoSellMode === 'true') {
this.spyNoSellMode = true;
}
this.extractItemDetailMapFromPage();
this.setupPageContextBridge();
this.createSpyPane();
this.createConsumablesPane();
this.createJHousePane();
this.createKOllectionPane();
this.mcs_nt_createPane();
this.createPFormancePane();
this.createQCharmPane();
this.createTReasurePane();
this.createOPanel();
this.createBReadPane();
this.createGWhizPane();
this.createAMazingPane();
this.createDPsPane();
this.createMEatersPane();
this.createHWhatPanel();
this.createIHurtPane();
this.createMAnaPane();
this.createLuckyPanel();
setTimeout(() => {
this.ensureCombatSuiteVisible();
}, 500);
this.startEquipmentPolling();
this.interceptNetworkRequests();
this.waitForCharacterData();
}
startEquipmentPolling() {
const self = this;
self._equipChangedListener = (event) => {
if (window.MCS_MODULES_DISABLED) return;
const playerName = event.detail?.playerName;
if (self.spyIsInteracting) {
return;
}
const spyPane = document.getElementById('equipment-spy-pane');
if (!spyPane || spyPane.style.display === 'none') {
return;
}
try {
const newData = window.mcs__global_equipment_tracker?.allCharacterItems;
if (!newData) return;
self.spyCharacterItems = newData;
self.updateSpyDisplay();
self.updateCoinHeader();
} catch (e) {
console.error('[EWatch] Error updating from equipment change:', e);
}
};
window.addEventListener('MCS_EquipmentChanged', self._equipChangedListener);
VisibilityManager.register('ewatch-display-update', () => {
if (window.MCS_MODULES_DISABLED) return;
if (self.spyIsInteracting) return;
const spyPane = document.getElementById('equipment-spy-pane');
if (!spyPane || spyPane.style.display === 'none') return;
self.updateCoinHeader();
}, 2000);
}
equipmentHasChanged(newEquipment) {
const currentEquipped = this.spyCharacterItems.filter(item => item.itemLocationHrid && item.itemLocationHrid !== '/item_locations/inventory'
);
if (currentEquipped.length !== newEquipment.length) {
return true;
}
for (const newItem of newEquipment) {
const existing = currentEquipped.find(item => item.itemLocationHrid === newItem.itemLocationHrid
);
if (!existing || existing.hash !== newItem.hash) {
return true;
}
}
return false;
}
waitForCharacterData() {
let attempts = 0;
const maxAttempts = 200;
const checkInterval = setInterval(() => {
attempts++;
if (this.spyCharacterItems && this.spyCharacterItems.length > 0) {
clearInterval(checkInterval);
this.updateSpyDisplay();
if (!this.coinHeaderInterval) {
VisibilityManager.register('ewatch-coin-header', () => {
this.updateCoinHeader();
}, 1000);
this.coinHeaderInterval = true;
}
return;
}
const bridge = document.getElementById('equipspy-data-bridge');
if (bridge) {
const dataAttr = bridge.getAttribute('data-character-items');
if (dataAttr) {
try {
const items = JSON.parse(dataAttr);
if (items && items.length > 0) {
this.spyCharacterItems = items;
clearInterval(checkInterval);
this.updateSpyDisplay();
if (!this.coinHeaderInterval) {
VisibilityManager.register('ewatch-coin-header', () => {
this.updateCoinHeader();
}, 1000);
this.coinHeaderInterval = true;
}
return;
}
} catch (e) {
console.error('[EquipSpy] Error parsing bridge data:', e);
}
}
}
if (attempts >= maxAttempts) {
console.warn('[EquipSpy] Timed out waiting for character data');
clearInterval(checkInterval);
this.updateSpyDisplay();
}
}, 500);
}
constrainPanelToBoundaries(paneId, storageKey = null, useLeftTop = true) {
const pane = document.getElementById(paneId);
if (!pane) return;
requestAnimationFrame(() => {
const rect = pane.getBoundingClientRect();
const winWidth = window.innerWidth;
const winHeight = window.innerHeight;
let needsAdjustment = false;
let newLeft = rect.left;
let newTop = rect.top;
let newRight = winWidth - rect.right;
if (rect.right > winWidth) {
if (useLeftTop) {
newLeft = winWidth - rect.width;
} else {
newRight = 0;
}
needsAdjustment = true;
}
if (rect.left < 0 || newLeft < 0) {
newLeft = 0;
needsAdjustment = true;
}
if (rect.bottom > winHeight) {
newTop = winHeight - rect.height;
needsAdjustment = true;
}
if (newTop < 0) {
newTop = 0;
needsAdjustment = true;
}
if (useLeftTop) {
pane.style.left = newLeft + 'px';
pane.style.top = newTop + 'px';
pane.style.right = 'auto';
} else {
pane.style.right = newRight + 'px';
pane.style.top = newTop + 'px';
pane.style.left = 'auto';
}
if (storageKey) {
const positionData = useLeftTop
? { top: newTop, left: newLeft }
: { top: newTop, right: newRight };
if (storageKey.match(/^mcs_[A-Z]{2}$/)) {
const modulePrefix = storageKey.replace('mcs_', '');
const storage = createModuleStorage(modulePrefix);
storage.set('position', positionData);
} else {
CharacterStorageUtils.setItem(storageKey, JSON.stringify(positionData));
}
}
});
}
loadSpySettings() {
try {
const savedLocked = this.ewStorage.get('locked');
if (savedLocked) {
this.spyLockedComparisons = typeof savedLocked === 'string' ? JSON.parse(savedLocked) : savedLocked;
}
const savedMin = this.ewStorage.get('minimized');
if (savedMin === true || savedMin === 'true') {
this.spyIsMinimized = true;
}
const savedHeaderSlot = this.ewStorage.get('selected_header_slot');
if (savedHeaderSlot) {
this.spySelectedHeaderSlot = savedHeaderSlot;
}
this.loadComparisonOrder();
} catch (e) {
console.error('[EquipSpy] Failed to load settings:', e);
}
}
saveComparisonOrder() {
if (!this.comparisonOrder) return;
this.ewStorage.set('comparison_order', this.comparisonOrder);
}
loadComparisonOrder() {
try {
const saved = this.ewStorage.get('comparison_order');
if (saved) {
this.comparisonOrder = typeof saved === 'string' ? JSON.parse(saved) : saved;
} else {
this.comparisonOrder = [];
}
} catch (e) {
console.error('[EquipSpy] Failed to load comparison order:', e);
this.comparisonOrder = [];
}
}
getOrderedSlots(slots) {
const availableSlots = Object.keys(slots);
const orderedSlots = this.comparisonOrder.filter(slot => availableSlots.includes(slot));
availableSlots.forEach(slot => {
if (!orderedSlots.includes(slot)) {
orderedSlots.push(slot);
}
});
return orderedSlots;
}
// css start
initializeGlobalStyles() {
if (this.cssStyleSheet) return;
const styleElement = document.createElement('style');
styleElement.id = 'mcs-global-styles';
document.head.appendChild(styleElement);
this.cssStyleSheet = styleElement.sheet;
this.cssStyleElement = styleElement;
this.addGlobalStyles();
}
addGlobalStyles() {
const baseStyles = `
.mcs-pane {
position: fixed;
top: 0;
left: 0;
background: #2b2b2b;
border-radius: 6px;
box-shadow: 0 4px 12px rgba(0,0,0,0.5);
z-index: 99999;
font-family: sans-serif;
display: flex;
flex-direction: column;
}
.mcs-pane-dark {
background: #1a1a1a;
}
.mcs-pane-header {
background: #333333;
color: #eeeeee;
padding: 8px 12px;
font-weight: bold;
cursor: move;
border-radius: 6px 6px 0 0;
display: flex;
justify-content: space-between;
align-items: center;
user-select: none;
gap: 10px;
}
.mcs-pane-header.minimized {
border-radius: 6px;
}
.mcs-pane-title {
font-size: 14px;
white-space: nowrap;
}
.mcs-pane-content {
padding: 10px;
color: #e0e0e0;
font-size: 11px;
display: flex;
flex-direction: column;
gap: 8px;
align-items: stretch;
}
.mcs-pane-content-dark {
background: #0a0a0a;
}
/* Base button shared across all modules */
.mcs-btn,
.mcs-filter-btn,
.mcs-crack-reset-btn,
.mcs-crack-minimize-btn,
.mcs-dps-filter-btn,
.mcs-dps-reset-btn,
.mcs-qcharm-btn {
background: #555;
color: #fff;
border: 1px solid #666;
border-radius: 3px;
cursor: pointer;
font-weight: bold;
transition: background 0.2s;
}
/* Hover state for all base buttons */
.mcs-btn:hover,
.mcs-filter-btn:hover,
.mcs-crack-reset-btn:hover,
.mcs-crack-minimize-btn:hover,
.mcs-dps-filter-btn:hover,
.mcs-dps-reset-btn:hover,
.mcs-qcharm-btn:hover {
background: #666;
}
/* Size variants */
.mcs-btn {
padding: 6px 12px;
font-size: 12px;
}
.mcs-btn-small,
.mcs-filter-btn,
.mcs-dps-filter-btn,
.mcs-dps-reset-btn {
padding: 4px 8px;
font-size: 11px;
}
.mcs-btn-minimize,
.mcs-crack-minimize-btn,
.mcs-qcharm-btn {
padding: 4px 10px;
font-size: 14px;
line-height: 1;
}
.mcs-crack-reset-btn {
padding: 4px 8px;
font-size: 11px;
}
.mcs-btn-active {
background: #4CAF50;
color: #fff;
}
.mcs-btn-active:hover {
background: #66BB6A;
}
.mcs-btn-warning {
background: #DC143C;
color: #fff;
}
.mcs-btn-warning:hover {
background: #FF1744;
}
.mcs-btn-alert {
background: #FFA500;
color: #000;
}
.mcs-btn-alert:hover {
background: #FFB52E;
}
.mcs-container {
display: flex;
flex-direction: column;
gap: 8px;
padding: 8px;
background: #333;
border-radius: 4px;
}
.mcs-row {
display: flex;
align-items: center;
gap: 10px;
}
.mcs-text {
color: #e0e0e0;
font-size: 11px;
}
.mcs-text-description {
color: #bbb;
font-size: 11px;
flex: 1;
}
.mcs-text-highlight {
color: #FFD700;
}
.mcs-text-success {
color: #4CAF50;
}
.mcs-filter-container {
display: flex;
gap: 4px;
align-items: center;
}
/* Filter button states (base style now consolidated above) */
.mcs-filter-btn.active {
background: #4CAF50;
color: #fff;
}
.mcs-filter-btn.inactive {
background: #333333;
color: #888;
}
.mcs-overlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: #1a1a1a;
z-index: 99996;
pointer-events: none;
}
.mcs-meaters-info-dialog {
position: fixed;
top: 20px;
left: 50%;
transform: translateX(-50%);
background: rgba(30, 30, 40, 0.98);
border: 2px solid #4CAF50;
border-radius: 8px;
padding: 16px 20px;
z-index: 100000;
box-shadow: 0 4px 20px rgba(0,0,0,0.8);
max-width: 500px;
color: #fff;
font-family: sans-serif;
}
.mcs-hidden {
display: none !important;
}
.mcs-display-flex {
display: flex;
}
/* Gap utilities (sequential order) */
.mcs-gap-4 {
gap: 4px;
}
.mcs-gap-6 {
gap: 6px;
}
.mcs-gap-8 {
gap: 8px;
}
.mcs-gap-10 {
gap: 10px;
}
.mcs-gap-15 {
gap: 15px;
}
/* Font-size utilities (sequential order) */
.mcs-font-9 {
font-size: 9px;
}
.mcs-font-14 {
font-size: 14px;
}
.mcs-font-16 {
font-size: 16px;
}
.mcs-font-22 {
font-size: 22px;
}
/* Size utilities */
.mcs-width-fit {
width: fit-content;
}
.mcs-height-auto {
height: auto;
}
.mcs-amazing-pane {
width: auto;
}
.mcs-meaters-pane {
width: 700px;
}
.mcs-meaters-content {
padding: 10px;
overflow-y: auto;
overflow-x: hidden;
}
.mcs-meaters-empty {
color: #666;
text-align: center;
padding: 20px;
}
.mcs-meaters-info-title {
font-size: 14px;
font-weight: bold;
margin-bottom: 8px;
}
.mcs-meaters-info-text {
font-size: 12px;
line-height: 1.6;
color: #ddd;
}
.mcs-meaters-info-close {
margin-top: 12px;
}
.mcs-meaters-log-entry {
margin-bottom: 2px;
padding: 3px 8px;
background: rgba(255,255,255,0.02);
border-radius: 3px;
font-size: 11px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
line-height: 16px;
min-height: 22px;
display: flex;
align-items: center;
}
.mcs-meaters-log-player {
border-left: 3px solid transparent;
}
.mcs-meaters-log-enemy {
border-right: 3px solid transparent;
text-align: right;
justify-content: flex-end;
}
.mcs-meaters-timestamp {
color: #87CEEB;
font-weight: bold;
}
.mcs-meaters-separator {
color: #666;
}
.mcs-meaters-player-name {
color: #90EE90;
}
.mcs-meaters-ability {
color: #FFA500;
}
.mcs-meaters-damage {
font-weight: bold;
}
.mcs-meaters-monster-name {
color: #FF6B9D;
}
.mcs-bread-pane {
width: fit-content;
height: auto;
max-height: 80vh;
max-width: 95vw;
min-width: 200px;
}
.mcs-bread-content {
overflow-y: auto;
overflow-x: visible;
flex: 1 1 auto;
}
.mcs-bread-item {
display: flex;
flex-direction: row;
align-items: center;
gap: 10px;
padding: 3px;
min-width: 0;
flex-shrink: 0;
}
.mcs-bread-level {
color: #FFD700;
font-weight: bold;
text-align: right;
min-width: 35px;
flex-shrink: 0;
}
.mcs-bread-icon {
flex-shrink: 0;
}
.mcs-bread-exp-container {
display: flex;
flex-direction: column;
gap: 2px;
min-width: 60px;
flex: 0 1 auto;
}
.mcs-bread-exp {
color: #4CAF50;
font-weight: bold;
text-align: left;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.mcs-bread-rate {
color: #90EE90;
text-align: left;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.mcs-bread-time {
color: #87CEEB;
font-weight: bold;
text-align: center;
min-width: 50px;
flex: 0 0 60px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.mcs-bread-books {
color: #FFA500;
font-weight: bold;
text-align: right;
min-width: 60px;
flex: 0 0 auto;
white-space: nowrap;
overflow: visible;
}
.mcs-bread-market {
color: #FFD700;
font-weight: bold;
text-align: right;
min-width: 55px;
flex: 0 0 auto;
white-space: nowrap;
overflow: visible;
}
.mcs-bread-input {
width: 45px;
min-width: 45px;
padding: 2px 4px;
background: #333;
color: #fff;
border: 1px solid #555;
border-radius: 3px;
flex: 0 0 45px;
}
.mcs-bread-no-abilities {
padding: 20px;
text-align: center;
color: #999;
}
.mcs-button-section {
display: flex;
align-items: center;
gap: 6px;
}
.mcs-bread-header {
gap: 15px;
flex-shrink: 0;
}
.mcs-bread-title-section {
display: flex;
align-items: center;
gap: 4px;
}
.mcs-bread-header-info {
display: flex;
align-items: center;
gap: 4px;
font-size: 11px;
}
.mcs-bread-header-books {
color: #4CAF50;
font-weight: bold;
}
.mcs-bread-header-label {
color: #aaa;
}
.mcs-bread-header-cost {
color: #FFD700;
}
.mcs-bread-range-calc {
display: flex;
align-items: center;
gap: 8px;
padding: 6px 12px;
background: #2a2a2a;
border-bottom: 1px solid #444;
font-size: 11px;
}
.mcs-bread-range-input {
width: 50px;
padding: 4px 6px;
background: #333;
color: #fff;
border: 1px solid #555;
border-radius: 3px;
font-size: 11px;
text-align: center;
}
.mcs-bread-range-result {
display: flex;
flex-direction: column;
gap: 2px;
font-size: 10px;
color: #aaa;
margin-left: 8px;
}
.mcs-bread-range-result div {
white-space: nowrap;
}
.mcs-bread-range-result strong {
color: #4CAF50;
font-weight: bold;
}
/* Pane header when minimized */
.mcs-pane-header-minimized {
border-radius: 6px;
}
.mcs-crack-pane {
position: fixed;
top: 0;
left: 0;
width: fit-content;
min-width: 380px;
max-width: 600px;
max-height: 80vh;
background: #1a1a1a;
border: none;
border-radius: 8px;
z-index: 10001;
box-shadow: 0 4px 12px rgba(0,0,0,0.5);
display: flex;
flex-direction: column;
overflow: hidden;
}
.mcs-crack-header {
background: #2a2a2a;
padding: 6px 10px;
cursor: move;
user-select: none;
border-bottom: 1px solid #4a4a4a;
display: flex;
justify-content: space-between;
align-items: center;
font-size: 11px;
color: #90EE90;
font-weight: bold;
}
.mcs-crack-header-left {
display: flex;
align-items: center;
gap: 15px;
}
.mcs-crack-title {
font-size: 14px;
color: white;
font-weight: bold;
}
.mcs-crack-eta-container {
font-size: 14px;
}
.mcs-crack-eta-container #consumables-days-left {
color: inherit;
}
.mcs-crack-party-eta-container {
font-size: 14px;
}
.mcs-crack-button-container {
display: flex;
gap: 6px;
align-items: center;
}
/* CRack button overrides (base styles consolidated above) */
.mcs-crack-reset-btn {
background: #444;
padding: 2px 8px;
font-size: 14px;
line-height: 1;
}
.mcs-crack-reset-btn:hover {
background: #555;
}
.mcs-crack-content {
padding: 8px;
color: white;
font-size: 12px;
overflow-y: auto;
flex: 1;
}
.mcs-crack-minimized-summary {
padding: 8px 10px;
display: flex;
justify-content: space-between;
align-items: center;
border-top: 1px solid #4a4a4a;
background: #1a1a1a;
}
.mcs-crack-summary-left {
display: flex;
align-items: center;
gap: 6px;
}
.mcs-crack-summary-count {
color: #ff6666;
font-weight: bold;
font-size: 12px;
}
.mcs-crack-summary-name {
color: #e0e0e0;
font-size: 11px;
}
.mcs-crack-summary-right {
text-align: right;
font-size: 9px;
}
.mcs-crack-summary-cost-label {
color: #90EE90;
font-weight: bold;
}
.mcs-crack-summary-cost-value {
color: white;
}
/* CRack consumable row styles */
.mcs-crack-consumable-row {
display: flex;
align-items: center;
gap: 8px;
}
.mcs-crack-consumable-container {
display: flex;
flex-direction: column;
padding: 4px 0;
gap: 2px;
}
.mcs-crack-consumable-count {
min-width: 35px;
text-align: right;
font-size: 12px;
font-weight: bold;
}
.mcs-crack-consumable-icon {
flex-shrink: 0;
cursor: pointer;
}
.mcs-crack-consumable-name {
font-size: 12px;
flex: 1;
min-width: 0;
}
.mcs-crack-consumable-name-warning {
color: #ff6666;
}
.mcs-crack-count-placeholder {
color: #666;
}
.mcs-crack-name-placeholder {
color: #666;
}
.mcs-crack-stack-container {
display: flex;
flex-direction: column;
gap: 2px;
}
.mcs-crack-stack-row {
display: flex;
gap: 8px;
}
.mcs-crack-stat-actual {
color: #4CAF50;
font-size: 9px;
min-width: 45px;
text-align: right;
}
.mcs-crack-stat-combined {
color: #888;
font-size: 8px;
min-width: 45px;
text-align: right;
}
.mcs-crack-stat-count {
min-width: 35px;
text-align: right;
}
.mcs-crack-per-day {
color: white;
font-size: 10px;
min-width: 60px;
text-align: right;
}
.mcs-crack-time-remaining {
font-size: 10px;
min-width: 45px;
text-align: right;
}
.mcs-crack-waiting {
color: #999;
font-size: 10px;
min-width: 100px;
text-align: center;
font-style: italic;
}
.mcs-crack-tracker-error {
color: #f88;
font-size: 10px;
min-width: 100px;
text-align: center;
font-style: italic;
}
.mcs-crack-total-cost {
margin-top: 10px;
padding-top: 8px;
border-top: 1px solid #555;
font-size: 10px;
text-align: right;
}
.mcs-crack-party-section {
margin-top: 15px;
padding-top: 10px;
border-top: 1px solid #555;
}
.mcs-crack-party-name {
font-weight: bold;
margin-bottom: 8px;
color: #90EE90;
font-size: 11px;
}
.mcs-crack-reset-complete {
text-align: center;
color: #4CAF50;
padding: 10px;
background: #2a2a2a;
border-radius: 4px;
margin-bottom: 10px;
}
.mcs-crack-reset-title {
font-size: 14px;
font-weight: bold;
}
.mcs-crack-reset-waiting {
font-size: 12px;
margin-top: 5px;
}
.mcs-crack-reset-subtitle {
font-size: 11px;
margin-top: 3px;
color: #888;
}
.mcs-crack-no-data {
text-align: center;
color: #888;
padding: 10px;
}
.mcs-crack-price-text {
color: white;
font-size: 9px;
margin-left: 4px;
white-space: nowrap;
display: flex;
flex-direction: column;
line-height: 1.2;
text-align: right;
min-width: 80px;
}
/* Color states for ETA warnings */
.mcs-crack-eta-critical {
color: #c42323;
}
.mcs-crack-eta-warning {
color: #e8a738;
}
.mcs-crack-eta-good {
color: #65b83e;
}
.mcs-crack-time-critical {
color: #FF6B6B;
}
.mcs-crack-time-warning {
color: #FFA500;
}
.mcs-crack-time-normal {
color: #4CAF50;
}
.mcs-crack-count-critical {
color: #ff6666;
}
.mcs-crack-count-warning {
color: #FFA500;
}
/* Party ETA color states */
.mcs-crack-party-eta-disabled {
color: grey;
}
.mcs-crack-party-eta-critical {
color: #FF6B6B;
}
.mcs-crack-party-eta-warning {
color: #FFA500;
}
.mcs-crack-party-eta-good {
color: #90EE90;
}
.mcs-dps-pane {
width: auto;
}
.mcs-dps-header {
gap: 10px;
}
.mcs-dps-inactivity-timer {
margin-left: 10px;
font-size: 12px;
opacity: 0.7;
}
.mcs-dps-title {
font-size: 14px;
white-space: nowrap;
}
.mcs-dps-button-container {
display: flex;
gap: 6px;
align-items: center;
}
/* DPs button states (base styles consolidated above) */
.mcs-dps-filter-btn.enabled {
background: #4CAF50;
}
/* DPs info button (unique style) */
.mcs-dps-info-btn {
background: #2196F3;
color: #fff;
border: 1px solid #666;
border-radius: 3px;
padding: 4px 10px;
font-size: 11px;
cursor: pointer;
}
.mcs-dps-content {
padding: 10px;
color: #e0e0e0;
font-size: 11px;
display: flex;
flex-direction: column;
gap: 8px;
align-items: stretch;
max-height: 600px;
overflow-y: auto;
}
.mcs-dps-info-panel {
display: none;
padding: 12px;
background: rgba(33, 150, 243, 0.1);
border: 1px solid #2196F3;
border-radius: 4px;
color: #e0e0e0;
font-size: 12px;
line-height: 1.6;
margin-bottom: 10px;
}
.mcs-dps-info-title {
font-weight: bold;
font-size: 13px;
margin-bottom: 8px;
color: #2196F3;
}
.mcs-dps-info-text {
margin-bottom: 6px;
}
.mcs-dps-waiting {
padding: 40px 20px;
text-align: center;
color: #999;
display: flex;
flex-direction: column;
align-items: center;
gap: 12px;
}
.mcs-dps-waiting.minimized {
padding: 10px 20px;
gap: 6px;
}
.mcs-dps-waiting-icon {
font-size: 32px;
}
.mcs-dps-waiting-icon.minimized {
font-size: 16px;
}
.mcs-dps-waiting-text {
font-size: 13px;
}
.mcs-dps-waiting-text.minimized {
font-size: 11px;
}
.mcs-dps-header-row {
display: flex;
flex-direction: row;
align-items: center;
gap: 8px;
padding: 5px 3px;
font-weight: bold;
color: #FFD700;
border-bottom: 2px solid #555;
margin-bottom: 5px;
font-size: 10px;
}
.mcs-dps-header-col {
text-align: left;
}
.mcs-dps-header-char {
min-width: 120px;
}
.mcs-dps-header-dps {
min-width: 60px;
text-align: right;
}
.mcs-dps-header-damage {
min-width: 80px;
text-align: right;
}
.mcs-dps-header-atks {
min-width: 45px;
text-align: right;
}
.mcs-dps-header-hits {
min-width: 50px;
text-align: right;
}
.mcs-dps-header-crits {
min-width: 50px;
text-align: right;
}
.mcs-dps-header-misses {
min-width: 50px;
text-align: right;
}
.mcs-dps-player-item {
display: flex;
flex-direction: column;
gap: 2px;
padding: 5px 0;
}
.mcs-dps-player-item.bordered {
border-bottom: 1px solid #444;
}
.mcs-dps-player-row {
display: flex;
flex-direction: row;
align-items: center;
gap: 8px;
padding: 3px;
cursor: pointer;
}
.mcs-dps-player-row.minimized {
cursor: default;
}
.mcs-dps-expand-arrow {
font-size: 10px;
color: #FFD700;
min-width: 15px;
}
.mcs-dps-name-label {
font-size: 13px;
color: #87CEEB;
font-weight: bold;
text-align: left;
min-width: 105px;
}
.mcs-dps-dps-label {
font-size: 14px;
color: #4CAF50;
font-weight: bold;
text-align: right;
min-width: 60px;
}
.mcs-dps-accuracy-span {
color: #FF9800;
}
.mcs-dps-damage-label {
font-size: 13px;
color: #4CAF50;
font-weight: bold;
text-align: right;
min-width: 80px;
}
.mcs-dps-total-attacks-label {
font-size: 11px;
color: #CCC;
font-weight: bold;
text-align: right;
min-width: 45px;
}
.mcs-dps-total-hits-label {
font-size: 11px;
color: #90EE90;
font-weight: bold;
text-align: right;
min-width: 50px;
}
.mcs-dps-total-crits-label {
font-size: 11px;
color: #FFD700;
font-weight: bold;
text-align: right;
min-width: 50px;
}
.mcs-dps-total-misses-label {
font-size: 11px;
color: #FF6B6B;
font-weight: bold;
text-align: right;
min-width: 50px;
}
.mcs-dps-ability-container {
display: flex;
flex-direction: column;
gap: 1px;
padding-left: 15px;
}
.mcs-dps-ability-row {
display: flex;
flex-direction: row;
align-items: center;
gap: 8px;
padding: 2px 3px;
font-size: 10px;
}
.mcs-dps-ability-name {
font-size: 11px;
color: #AAA;
text-align: left;
min-width: 120px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.mcs-dps-ability-dps {
font-size: 10px;
color: #FFA500;
text-align: right;
min-width: 60px;
}
.mcs-dps-ability-damage {
font-size: 10px;
color: #90EE90;
text-align: right;
min-width: 80px;
}
.mcs-dps-ability-attacks {
font-size: 10px;
color: #CCC;
text-align: right;
min-width: 45px;
}
.mcs-dps-ability-hits {
font-size: 10px;
color: #90EE90;
text-align: right;
min-width: 50px;
}
.mcs-dps-ability-crits {
font-size: 10px;
color: #FFD700;
text-align: right;
min-width: 50px;
}
.mcs-dps-ability-misses {
font-size: 10px;
color: #FF6B6B;
text-align: right;
min-width: 50px;
}
.mcs-dps-monster-header {
display: flex;
flex-direction: row;
align-items: center;
gap: 8px;
padding: 4px 3px;
margin-top: 8px;
font-size: 10px;
background: rgba(255, 255, 255, 0.05);
border-radius: 3px;
cursor: pointer;
}
.mcs-dps-monster-arrow {
font-size: 9px;
color: #FF6B6B;
min-width: 12px;
}
.mcs-dps-monster-name {
font-size: 11px;
color: #FF6B6B;
font-weight: bold;
text-align: left;
min-width: 108px;
}
.mcs-dps-monster-dps {
font-size: 10px;
color: #FFA500;
text-align: right;
min-width: 60px;
}
.mcs-dps-monster-damage {
font-size: 10px;
color: #90EE90;
text-align: right;
min-width: 80px;
}
.mcs-dps-monster-attacks {
font-size: 10px;
color: #CCC;
text-align: right;
min-width: 45px;
}
.mcs-dps-monster-hits {
font-size: 10px;
color: #90EE90;
text-align: right;
min-width: 50px;
}
.mcs-dps-monster-crits {
font-size: 10px;
color: #FFD700;
text-align: right;
min-width: 50px;
}
.mcs-dps-monster-misses {
font-size: 10px;
color: #FF6B6B;
text-align: right;
min-width: 50px;
}
.mcs-dps-monster-ability-container {
display: flex;
flex-direction: column;
gap: 1px;
}
.mcs-dps-monster-ability-row {
display: flex;
flex-direction: row;
align-items: center;
gap: 8px;
padding: 2px 3px 2px 25px;
font-size: 10px;
}
.mcs-dps-monster-ability-name {
font-size: 10px;
color: #999;
text-align: left;
min-width: 120px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.mcs-dps-monster-ability-dps {
font-size: 9px;
color: #FFA500;
text-align: right;
min-width: 60px;
}
.mcs-dps-monster-ability-damage {
font-size: 9px;
color: #90EE90;
text-align: right;
min-width: 80px;
}
.mcs-dps-monster-ability-attacks {
font-size: 9px;
color: #CCC;
text-align: right;
min-width: 45px;
}
.mcs-dps-monster-ability-hits {
font-size: 9px;
color: #90EE90;
text-align: right;
min-width: 50px;
}
.mcs-dps-monster-ability-crits {
font-size: 9px;
color: #FFD700;
text-align: right;
min-width: 50px;
}
.mcs-dps-monster-ability-misses {
font-size: 9px;
color: #FF6B6B;
text-align: right;
min-width: 50px;
}
.mcs-dps-truedps-section {
margin-top: 12px;
padding: 10px;
background: rgba(255, 215, 0, 0.05);
border-radius: 4px;
border: 2px solid #FFD700;
}
.mcs-dps-truedps-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 8px;
}
.mcs-dps-truedps-title {
font-weight: bold;
color: #FFD700;
font-size: 13px;
}
.mcs-dps-truedps-container {
display: flex;
gap: 15px;
align-items: center;
}
.mcs-dps-truedps-value-container {
display: flex;
flex-direction: column;
align-items: flex-end;
}
.mcs-dps-truedps-value {
color: #FFD700;
font-weight: bold;
font-size: 18px;
}
.mcs-dps-truedps-label {
color: #999;
font-size: 9px;
font-style: italic;
}
.mcs-dps-loss-value {
color: #FF6B6B;
font-weight: bold;
font-size: 18px;
}
.mcs-dps-truedps-subtitle {
color: #999;
font-size: 9px;
margin-bottom: 8px;
}
.mcs-dps-truedps-stats {
display: flex;
justify-content: space-between;
margin-bottom: 8px;
font-size: 10px;
gap: 10px;
}
.mcs-dps-stat-label {
color: #aaa;
}
.mcs-dps-stat-time {
color: #4CAF50;
font-weight: bold;
}
.mcs-dps-stat-damage {
color: #87CEEB;
font-weight: bold;
}
.mcs-dps-stat-kills {
color: #90EE90;
font-weight: bold;
}
.mcs-dps-enemy-grid {
display: flex;
flex-wrap: wrap;
gap: 6px;
}
.mcs-dps-enemy-box {
flex: 1 1 calc(50% - 3px);
min-width: 200px;
padding: 6px 8px;
background: rgba(255, 107, 157, 0.1);
border-radius: 3px;
border-left: 3px solid #FF6B9D;
font-size: 10px;
}
.mcs-dps-enemy-name {
color: #FF6B9D;
font-weight: bold;
margin-bottom: 2px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.mcs-dps-enemy-stats {
color: #ccc;
font-size: 9px;
}
.mcs-dps-enemy-damage {
color: #90EE90;
font-weight: bold;
}
.mcs-dps-title-highlight {
color: white;
}
.mcs-dps-title-subtitle {
color: #888;
font-size: 10px;
}
.mcs-ew-pane {
position: fixed;
top: 0;
left: 0;
width: 450px;
border: none;
border-radius: 8px;
box-shadow: 0 4px 12px rgba(0,0,0,0.5);
z-index: 99998;
font-family: sans-serif;
display: flex;
flex-direction: column;
max-height: 85vh;
}
.mcs-ew-header {
background: #333333;
color: white;
padding: 8px 12px;
font-weight: bold;
cursor: move;
border-radius: 6px 6px 0 0;
display: flex;
justify-content: space-between;
align-items: center;
user-select: none;
}
.mcs-ew-header-left {
display: flex;
align-items: center;
gap: 8px;
flex: 1;
}
.mcs-ew-header-title {
font-size: 14px;
}
.mcs-ew-header-status {
font-size: 11px;
color: #aaa;
display: flex;
align-items: center;
gap: 4px;
}
.mcs-ew-header-buttons {
display: flex;
gap: 5px;
}
.mcs-ew-btn {
padding: 2px 8px;
border-radius: 3px;
cursor: pointer;
font-size: 11px;
border: 1px solid #6495ED;
}
.mcs-ew-btn-minimize {
background: rgba(255,255,255,0.2);
border: none;
color: white;
padding: 2px 8px;
border-radius: 3px;
cursor: pointer;
font-size: 14px;
}
.mcs-ew-content {
padding: 10px;
overflow-y: auto;
overflow-x: hidden;
max-height: 80vh;
color: #e0e0e0;
min-height: 200px;
}
.mcs-ew-status-msg {
text-align: center;
color: #888;
padding: 10px;
font-size: 12px;
}
.mcs-ew-error-msg {
text-align: center;
color: #ff6666;
padding: 20px;
font-size: 12px;
}
.mcs-ew-profit-section {
padding: 8px 12px;
background: rgba(0,0,0,0.2);
font-size: 11px;
display: none;
grid-template-columns: auto auto auto;
gap: 4px 8px;
align-items: center;
}
.mcs-ew-profit-label {
text-align: left;
font-weight: bold;
}
.mcs-ew-profit-type {
text-align: left;
color: #aaa;
font-size: 10px;
}
.mcs-ew-profit-value {
text-align: left;
font-weight: bold;
}
.mcs-ew-profit-empty {
text-align: left;
}
.mcs-ew-status-col {
display: flex;
flex-direction: column;
gap: 2px;
}
.mcs-ew-status-row {
display: flex;
align-items: center;
gap: 6px;
}
.mcs-ew-header-bar-track {
width: 255px;
height: 2px;
background: rgba(255,255,255,0.2);
border-radius: 1px;
overflow: hidden;
position: relative;
}
.mcs-ew-tick-mark {
position: absolute;
top: 0;
width: 1px;
height: 100%;
background: rgba(255,255,255,0.5);
}
.mcs-ew-nc-label {
color: #f44336;
font-weight: bold;
}
.mcs-ew-nc-time {
color: #999;
font-weight: normal;
}
.mcs-ew-gold-needed {
color: white;
font-weight: normal;
}
.mcs-ew-no-combat {
color: #f44336;
}
.mcs-ew-comparison {
margin-top: 4px;
padding: 6px;
background: rgba(0,0,0,0.3);
border-radius: 3px;
}
.mcs-ew-compare-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 4px;
}
.mcs-ew-compare-label {
font-size: 10px;
color: #aaa;
}
.mcs-ew-watch-btn {
background: rgba(255,215,0,0.3);
border: 1px solid #FFD700;
color: #FFD700;
padding: 1px 6px;
border-radius: 2px;
cursor: pointer;
font-size: 10px;
}
.mcs-ew-item-select {
width: 100%;
padding: 3px;
background: rgba(50,50,50,0.9);
color: #fff;
border: 1px solid #666;
border-radius: 2px;
font-size: 11px;
margin-bottom: 4px;
}
.mcs-ew-enh-container {
margin-top: 4px;
}
.mcs-ew-price-display {
margin-top: 4px;
font-size: 11px;
color: #FFD700;
font-weight: bold;
}
.mcs-ew-eta-display {
padding-top: 4px;
}
.mcs-ew-enh-row {
display: flex;
flex-wrap: wrap;
gap: 3px;
}
.mcs-ew-enh-col {
display: flex;
flex-direction: column;
gap: 3px;
}
.mcs-ew-enh-btn {
color: #fff;
padding: 2px 6px;
border-radius: 2px;
cursor: pointer;
font-size: 10px;
opacity: 1;
}
.mcs-ew-eta-text {
font-size: 10px;
text-align: right;
}
.mcs-ew-eta-nc {
font-size: 10px;
font-weight: bold;
color: #f44336;
text-align: right;
}
.mcs-ew-eta-waiting {
font-size: 10px;
font-style: italic;
color: #999;
text-align: right;
}
.mcs-ew-eta-affordable {
font-size: 10px;
font-weight: bold;
color: #66ff66;
text-align: right;
}
.mcs-ew-eta-timer {
font-size: 10px;
color: #6495ED;
text-align: right;
font-weight: bold;
}
.mcs-ew-eta-countdown {
font-size: 10px;
color: #f5a623;
}
.mcs-ew-eta-affordable-inline {
font-size: 10px;
color: #66ff66;
}
.mcs-ew-eta-nodata {
font-size: 10px;
font-style: italic;
color: #999;
}
.mcs-ew-price-lastseen {
color: white;
font-size: 9px;
font-style: italic;
}
.mcs-ew-price-nodata {
color: #FFD700;
font-size: 9px;
font-style: italic;
}
.mcs-ew-eye-btn {
padding: 2px 6px;
border-radius: 2px;
cursor: pointer;
font-size: 12px;
line-height: 1;
display: flex;
align-items: center;
justify-content: center;
}
.mcs-ew-progress-row {
display: flex;
align-items: center;
gap: 6px;
margin-top: 4px;
}
.mcs-ew-progress-track {
flex: 1;
background: rgba(0,0,0,0.3);
border-radius: 2px;
height: 4px;
overflow: hidden;
}
.mcs-ew-progress-text {
font-size: 10px;
min-width: 80px;
text-align: right;
}
.mcs-ew-content-wrap {
font-size: 11px;
line-height: 1.3;
}
.mcs-ew-gold-bar {
display: flex;
justify-content: space-between;
align-items: center;
padding: 6px 8px;
margin-bottom: 8px;
background: rgba(64, 64, 64, 0.1);
border: 1px solid rgba(255, 215, 0, 0.3);
border-radius: 3px;
}
.mcs-ew-gold-coin {
display: flex;
align-items: center;
gap: 4px;
color: #cccccc;
font-weight: bold;
font-size: 11px;
}
.mcs-ew-coin-icon {
width: 11px;
height: 11px;
display: inline-flex;
}
.mcs-ew-gold-stat {
display: flex;
align-items: center;
gap: 4px;
font-size: 11px;
}
.mcs-ew-market-bar {
display: flex;
justify-content: space-between;
align-items: center;
padding: 4px 8px;
margin-bottom: 8px;
background: rgba(64, 64, 64, 0.05);
border: 1px solid rgba(100, 149, 237, 0.2);
border-radius: 3px;
font-size: 10px;
}
.mcs-ew-market-label {
color: #999;
}
.mcs-ew-refresh-btn {
background: rgba(100, 149, 237, 0.3);
border: 1px solid #6495ED;
color: #6495ED;
padding: 2px 6px;
border-radius: 2px;
cursor: pointer;
font-size: 10px;
}
.mcs-ew-market-value-bar {
display: flex;
justify-content: space-between;
align-items: center;
padding: 4px 8px;
margin-bottom: 8px;
background: rgba(64, 64, 64, 0.05);
border: 1px solid rgba(100, 100, 100, 0.2);
border-radius: 3px;
font-size: 10px;
}
.mcs-ew-toggle-btn {
padding: 2px 6px;
border-radius: 2px;
border: 1px solid;
cursor: pointer;
font-size: 10px;
}
.mcs-ew-equip-row {
display: flex;
justify-content: space-between;
padding: 3px 6px;
background: rgba(255,255,255,0.03);
border-radius: 2px;
}
.mcs-ew-slot-name {
color: #999;
font-size: 10px;
min-width: 70px;
}
.mcs-ew-slot-item {
color: #ddd;
flex: 1;
padding: 0 6px;
}
.mcs-ew-slot-bid {
color: #FFD700;
font-weight: bold;
white-space: nowrap;
}
.mcs-ew-slot {
margin-bottom: 6px;
}
.mcs-ew-locked-nodata {
margin-top: 4px;
padding: 6px 6px 2px 6px;
background: rgba(100, 100, 100, 0.1);
border-radius: 3px;
border-left: 2px solid #999;
}
.mcs-ew-flex-between {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 4px;
}
.mcs-ew-flex-col {
display: flex;
flex-direction: column;
}
.mcs-ew-flex-row {
display: flex;
align-items: center;
gap: 6px;
}
.mcs-ew-locked-name-nodata {
font-size: 11px;
color: #ddd;
font-weight: bold;
}
.mcs-ew-waiting-market {
font-size: 9px;
color: #FFD700;
font-style: italic;
}
.mcs-ew-never-seen {
font-size: 10px;
color: #999;
}
.mcs-ew-watching-label {
font-size: 10px;
color: #999;
font-weight: bold;
}
.mcs-ew-unwatch-btn {
background: rgba(255,100,100,0.3);
border: 1px solid #ff6666;
color: #ff6666;
padding: 1px 6px;
border-radius: 2px;
cursor: pointer;
font-size: 10px;
}
.mcs-ew-locked-name {
font-size: 11px;
color: #ddd;
margin-bottom: 6px;
}
.mcs-ew-waiting-centered {
font-size: 10px;
color: #FFD700;
font-style: italic;
text-align: center;
padding: 8px 0;
}
.mcs-ew-locked-gold-name {
font-size: 11px;
color: #FFD700;
font-weight: bold;
}
.mcs-ew-last-price-note {
font-size: 8px;
color: #999;
font-style: italic;
}
.mcs-ew-locked-eta {
font-size: 10px;
color: #f5a623;
}
.mcs-ew-watching-title {
font-size: 10px;
color: #FFD700;
font-weight: bold;
}
.mcs-ew-locked-gold-name-mb {
font-size: 11px;
color: #FFD700;
font-weight: bold;
margin-bottom: 2px;
}
.mcs-ew-price-change {
font-size: 9px;
color: white;
margin-bottom: 4px;
font-style: italic;
}
.mcs-ew-price-row {
display: flex;
justify-content: space-between;
font-size: 10px;
margin-bottom: 2px;
}
.mcs-ew-price-row-mb6 {
display: flex;
justify-content: space-between;
font-size: 10px;
margin-bottom: 6px;
}
.mcs-ew-price-label {
color: #aaa;
}
.mcs-ew-ask-price-group {
display: flex;
align-items: center;
gap: 4px;
}
.mcs-ew-price-gold {
color: #FFD700;
font-weight: bold;
}
.mcs-ew-last-known-note {
color: #999;
font-size: 8px;
font-style: italic;
}
.mcs-ew-default-panel {
margin-top: 4px;
padding: 6px;
background: rgba(0,0,0,0.3);
border-radius: 3px;
cursor: pointer;
}
.mcs-ew-default-inner {
font-size: 10px;
color: #aaa;
text-align: center;
display: flex;
align-items: center;
justify-content: center;
gap: 6px;
}
.mcs-ew-total-row {
margin-top: 8px;
padding-top: 6px;
display: flex;
justify-content: space-between;
font-weight: bold;
font-size: 12px;
}
.mcs-ew-everything {
margin-top: 8px;
padding: 6px 6px 2px 6px;
border-radius: 3px;
cursor: default;
}
.mcs-ew-everything-title {
font-size: 11px;
color: #FFA500;
font-weight: bold;
}
.mcs-ew-everything-timer {
font-size: 10px;
color: #f5a623;
}
.mcs-ew-everything-affordable {
font-size: 10px;
font-weight: bold;
color: #66ff66;
}
.mcs-ew-everything-progress {
display: flex;
align-items: center;
gap: 6px;
margin-top: 4px;
}
.mcs-ew-everything-track {
flex: 1;
background: rgba(0,0,0,0.3);
border-radius: 2px;
height: 8px;
overflow: hidden;
position: relative;
}
.mcs-ew-no-equip {
text-align: center;
color: #888;
padding: 20px;
font-size: 12px;
}
.mcs-ew-no-equip-mb {
margin-bottom: 15px;
}
.mcs-ew-simple-empty {
text-align: center;
color: #888;
padding: 20px 10px;
font-size: 12px;
}
.mcs-ew-drop-zone {
height: 20px;
background: rgba(200, 200, 200, 0.3);
border: 1px dashed #999;
margin: 2px 0;
border-radius: 2px;
transition: all 0.2s ease;
position: relative;
z-index: 100;
}
.mcs-helper-force-btn {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
border: none;
color: white;
padding: 10px 20px;
border-radius: 6px;
cursor: pointer;
font-size: 13px;
font-weight: bold;
margin-top: 10px;
width: 90%;
transition: all 0.3s ease;
box-shadow: 0 2px 8px rgba(102, 126, 234, 0.3);
}
.mcs-helper-force-btn:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(102, 126, 234, 0.5);
}
.mcs-helper-loading {
text-align: center;
color: #667eea;
padding: 20px;
font-size: 12px;
}
.mcs-helper-spinner {
animation: spin 1s linear infinite;
display: inline-block;
font-size: 24px;
}
@keyframes spin {
from { transform: rotate(0deg); }
to { transform: rotate(360deg); }
}
.mcs-helper-failure {
text-align: center;
color: #ff6666;
padding: 20px;
font-size: 12px;
}
.mcs-helper-failure-icon {
font-size: 24px;
margin-bottom: 10px;
}
.floating-text-dps-container {
display: flex;
justify-content: space-around;
margin-top: 5px;
font-size: 12px;
font-weight: bold;
min-height: 18px;
}
.floating-text-player-dps-current {
text-align: center;
font-size: 11px;
font-weight: bold;
white-space: nowrap;
}
.floating-text-player-dps-total {
margin-bottom: 5px;
text-align: center;
font-size: 11px;
font-weight: bold;
white-space: nowrap;
}
.mcs-fcb-dps-column {
flex: 1;
text-align: center;
padding: 2px;
}
.fcb-floating-text {
position: fixed;
transform: translate(-50%, -50%);
pointer-events: none;
z-index: 10000;
font-family: sans-serif;
font-weight: bold;
font-size: 16.8px;
text-shadow:
-2px -2px 0 #000,
2px -2px 0 #000,
-2px 2px 0 #000,
2px 2px 0 #000,
0 0 12px rgba(0,0,0,0.9);
white-space: nowrap;
opacity: 1;
}
.mcs-fcb-text-shadow {
text-shadow: -1px -1px 0 #000, 1px -1px 0 #000, -1px 1px 0 #000, 1px 1px 0 #000;
}
.mcs-floot-overlay-container {
position: relative !important;
}
.mcs-floot-center-overlay {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
font-size: 10pt;
font-weight: bold;
text-shadow: -1px -1px 0 #000, 1px -1px 0 #000, -1px 1px 0 #000, 1px 1px 0 #000;
pointer-events: none;
z-index: 11;
line-height: 1;
}
.mcs-ntally-indicator {
position: absolute;
top: 2px;
left: 2px;
width: 8px;
height: 8px;
border-radius: 50%;
background-color: #4CAF50;
box-shadow: 0 0 3px rgba(76, 175, 80, 0.8);
pointer-events: none;
z-index: 12;
}
.mcs-price-overlay {
position: absolute;
top: 2px;
right: 2px;
font-size: 8pt;
font-weight: bold;
text-shadow: -1px -1px 0 #000, 1px -1px 0 #000, -1px 1px 0 #000, 1px 1px 0 #000;
pointer-events: none;
z-index: 10;
line-height: 1;
}
.mcs-category-total {
margin-left: 8px;
font-size: 10pt;
font-weight: bold;
}
.mcs-gw-pane {
width: auto;
}
.mcs-gw-left-section {
display: flex;
align-items: center;
gap: 10px;
}
.mcs-gw-total-exp {
font-size: 12px;
color: #90EE90;
font-weight: normal;
}
.mcs-gw-content {
align-items: stretch;
}
.mcs-gw-reset-btn {
padding: 4px 8px;
font-size: 11px;
}
.mcs-gw-section {
display: flex;
flex-direction: column;
gap: 4px;
padding: 6px;
background: #333;
border-radius: 4px;
}
.mcs-gw-section-mt {
margin-top: 8px;
}
.mcs-gw-section-mb {
margin-bottom: 8px;
}
.mcs-gw-session-row {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 4px 8px;
font-size: 11px;
}
.mcs-gw-session-label {
color: #999;
}
.mcs-gw-session-val {
color: #ddd;
font-weight: bold;
margin-right: 6px;
}
.mcs-gw-session-rate {
color: #90EE90;
}
.mcs-gw-top-row {
display: flex;
flex-direction: row;
align-items: center;
gap: 10px;
}
.mcs-gw-level-container {
display: flex;
flex-direction: column;
align-items: flex-end;
min-width: 35px;
}
.mcs-gw-combat-level-value {
font-size: 16px;
color: #ffa500;
font-weight: bold;
text-align: right;
}
.mcs-gw-name-container {
display: flex;
flex-direction: column;
gap: 2px;
flex: 1;
}
.mcs-gw-name-label {
font-size: 12px;
color: #fff;
font-weight: bold;
}
.mcs-gw-equation {
font-size: 18px;
color: #aaa;
font-family: monospace;
line-height: 1.2;
}
.mcs-gw-exp-label {
font-size: 11px;
color: #4CAF50;
font-weight: bold;
text-align: right;
flex: 1;
}
.mcs-gw-time-label {
font-size: 11px;
color: #87CEEB;
font-weight: bold;
text-align: right;
min-width: 80px;
}
.mcs-gw-progress-row {
display: flex;
flex-direction: row;
align-items: center;
gap: 8px;
}
.mcs-gw-progress-container {
flex: 1;
height: 10px;
background: #1a1a1a;
border-radius: 5px;
overflow: hidden;
position: relative;
}
.mcs-gw-combat-progress-bar {
height: 100%;
background: #ffa500;
width: 0%;
transition: width 0.3s ease;
position: relative;
}
.mcs-gw-combat-projected {
position: absolute;
left: 0;
top: 0;
height: 100%;
background: #259c85;
width: 0%;
transition: width 0.3s ease, background 0.3s ease, left 0.3s ease;
}
.mcs-gw-tickmark {
position: absolute;
top: 0;
height: 100%;
width: 1px;
background: #888;
pointer-events: none;
z-index: 100;
}
.mcs-gw-combat-progress-text {
font-size: 18px;
color: #ffa500;
font-weight: bold;
min-width: 60px;
text-align: right;
}
.mcs-gw-stat-item {
display: none;
flex-direction: column;
gap: 4px;
padding: 6px;
background: #333;
border-radius: 4px;
}
.mcs-gw-stat-level {
font-size: 16px;
font-weight: bold;
text-align: right;
min-width: 35px;
}
.mcs-gw-stat-name {
font-size: 12px;
color: #fff;
font-weight: bold;
min-width: 80px;
}
.mcs-gw-stat-progress-bar {
height: 100%;
width: 0%;
transition: width 0.3s ease;
}
.mcs-gw-stat-progress-text {
font-size: 18px;
font-weight: bold;
min-width: 60px;
text-align: right;
}
.mcs-gw-rate-label {
font-size: 9px;
color: #90EE90;
text-align: left;
}
.mcs-gw-section-title {
font-size: 12px;
color: #90EE90;
font-weight: bold;
text-align: left;
margin-bottom: 4px;
cursor: pointer;
user-select: none;
display: flex;
align-items: center;
gap: 6px;
}
.mcs-gw-ttl-title {
margin-bottom: 4px;
}
.mcs-gw-charms-title {
margin-bottom: 2px;
}
.mcs-gw-section-arrow {
font-size: 10px;
transition: transform 0.2s;
}
.mcs-gw-ttl-table {
display: flex;
flex-direction: column;
gap: 4px;
}
.mcs-gw-ttl-header {
display: grid;
grid-template-columns: 1fr 0.8fr 1fr 0.8fr 1fr;
gap: 4px;
padding: 4px;
font-size: 10px;
font-weight: bold;
color: #ccc;
border-bottom: 1px solid #555;
align-items: center;
}
.mcs-gw-ttl-time-header {
display: flex;
align-items: center;
justify-content: space-between;
padding-right: 4px;
}
.mcs-gw-ttl-time-text {
flex: 1;
text-align: center;
}
.mcs-gw-bulwark-container {
display: flex;
align-items: center;
gap: 2px;
font-size: 9px;
white-space: nowrap;
}
.mcs-gw-bulwark-checkbox {
cursor: pointer;
margin: 0;
width: 12px;
height: 12px;
}
.mcs-gw-bulwark-label {
cursor: pointer;
color: #95E1D3;
}
.mcs-gw-ttl-row {
display: grid;
grid-template-columns: 1fr 0.8fr 1fr 0.8fr 1fr;
gap: 4px;
padding: 4px;
font-size: 10px;
align-items: center;
}
.mcs-gw-ttl-name {
font-weight: bold;
text-align: left;
font-size: 12px;
}
.mcs-gw-ttl-current {
text-align: center;
color: #fff;
font-size: 12px;
}
.mcs-gw-ttl-exphr {
text-align: center;
color: #90EE90;
font-size: 11px;
}
.mcs-gw-ttl-target-cell {
text-align: center;
}
.mcs-gw-ttl-input {
width: 50px;
background: #2a2a2a;
border: 1px solid #555;
border-radius: 3px;
color: #fff;
padding: 2px 4px;
font-size: 12px;
text-align: center;
}
.mcs-gw-ttl-time {
text-align: center;
color: #87CEEB;
font-weight: bold;
font-size: 12px;
}
.mcs-gw-charms-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 4px;
}
.mcs-gw-charm-item {
display: flex;
flex-direction: row;
align-items: center;
gap: 4px;
padding: 3px 6px;
background: #2a2a2a;
border-radius: 3px;
}
.mcs-gw-charm-level {
font-size: 12px;
font-weight: bold;
min-width: 30px;
text-align: right;
}
.mcs-gw-charm-name {
font-size: 10px;
color: #ccc;
}
.mcs-gw-charm-percent {
font-size: 10px;
color: #90EE90;
margin-left: auto;
}
.gwhiz-combat-progress-bar-projected {
position: absolute;
top: 0;
height: 100%;
transition: width 0.3s ease, left 0.3s ease;
}
.mcs-hw-pane {
min-width: 300px;
}
.mcs-hw-header-left {
display: flex;
align-items: center;
gap: 10px;
flex: 1;
min-width: 0;
}
.mcs-hw-title {
white-space: nowrap;
}
.mcs-hw-combat-status {
font-size: 11px;
color: #ffa500;
display: none;
}
.mcs-hw-header-calc {
font-size: 11px;
white-space: nowrap;
}
.mcs-hw-color-green { color: #4CAF50; }
.mcs-hw-color-white { color: white; }
.mcs-hw-color-red { color: #f44336; }
.mcs-hw-color-blue { color: #2196F3; }
.mcs-hw-color-tax { color: #dc3545; }
.mcs-hw-color-gold { color: #FFD700; }
.mcs-hw-header-tax-section {
display: none;
}
.mcs-hw-header-tax-icon {
display: inline-block;
width: 16px;
height: 16px;
vertical-align: middle;
}
.mcs-hw-toggle-btn {
background: rgba(255, 255, 255, 0.1);
color: white;
border: 1px solid rgba(255, 255, 255, 0.3);
border-radius: 4px;
padding: 2px 8px;
cursor: pointer;
font-size: 11px;
white-space: nowrap;
}
.mcs-hw-content {
padding: 12px;
background: #2b2b2b;
border-radius: 0 0 6px 6px;
}
.mcs-hw-tax-section {
margin-bottom: 12px;
padding: 10px;
background: rgba(220, 53, 69, 0.1);
border: 1px solid rgba(220, 53, 69, 0.3);
border-radius: 6px;
display: flex;
align-items: center;
gap: 12px;
}
.mcs-hw-tax-icon {
width: 80px;
height: 80px;
flex-shrink: 0;
}
.mcs-hw-tax-details {
flex: 1;
}
.mcs-hw-tax-header-text {
font-weight: bold;
color: #dc3545;
margin-bottom: 6px;
}
.mcs-hw-tax-toggle {
padding: 6px 12px;
align-self: center;
}
.mcs-hw-profit-box {
margin-bottom: 12px;
padding: 10px;
border-radius: 6px;
}
.mcs-hw-lazy-box {
background: rgba(76, 175, 80, 0.1);
border: 1px solid rgba(76, 175, 80, 0.3);
}
.mcs-hw-mid-box {
background: rgba(33, 150, 243, 0.1);
border: 1px solid rgba(33, 150, 243, 0.3);
}
.mcs-hw-diff-box {
padding: 10px;
background: rgba(255, 215, 0, 0.1);
border: 1px solid rgba(255, 215, 0, 0.3);
border-radius: 6px;
margin-bottom: 0;
}
.mcs-hw-profit-title {
font-weight: bold;
margin-bottom: 6px;
}
.mcs-hw-profit-value {
font-size: 18px;
}
.mcs-hw-info-text {
font-size: 11px;
color: rgba(255, 255, 255, 0.6);
margin-top: 4px;
}
.mcs-hw-equation {
font-size: 10px;
font-family: monospace;
margin-top: 6px;
}
.mcs-hw-lazy-equation {
color: rgba(76, 175, 80, 0.7);
}
.mcs-hw-mid-equation {
color: rgba(33, 150, 243, 0.7);
}
.mcs-hw-diff-message {
font-size: 13px;
color: #FFD700;
margin-top: 6px;
font-weight: bold;
}
.mcs-hw-bags-needed {
color: #ffa500;
}
.mcs-jh-pane {
width: auto;
font-family: sans-serif;
}
.mcs-jh-header {
gap: 15px;
}
.mcs-jh-title-section {
display: flex;
align-items: center;
gap: 8px;
flex-wrap: wrap;
}
.mcs-jh-affordable-status {
font-size: 14px;
font-weight: bold;
display: none;
}
.mcs-jh-cheapest-status {
font-size: 11px;
color: #999;
display: none;
}
.mcs-jh-cheapest-room-name {
color: #FFD700;
}
.mcs-jh-status-pane {
padding: 20px;
color: #e0e0e0;
font-size: 12px;
display: flex;
flex-direction: column;
gap: 8px;
min-width: 300px;
}
.mcs-jh-status-line {
color: #FFA500;
font-family: monospace;
}
.mcs-jh-content {
padding: 10px;
color: #e0e0e0;
font-size: 11px;
display: none;
flex-direction: column;
gap: 8px;
align-items: stretch;
}
.mcs-jh-no-data {
padding: 10px;
text-align: center;
color: #999;
}
.mcs-jh-main-grid {
display: grid;
grid-template-columns: repeat(3, 60px) 1fr;
gap: 8px;
}
.mcs-jh-room-grid {
display: grid;
grid-template-columns: repeat(3, 60px);
gap: 8px;
grid-column: 1 / 4;
align-content: start;
}
.mcs-jh-room {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 2px;
padding: 4px;
background: #333;
border-radius: 4px;
cursor: pointer;
border: 1px solid transparent;
transition: all 0.2s ease;
width: 60px;
height: 60px;
min-width: 60px;
min-height: 60px;
max-width: 60px;
max-height: 60px;
position: relative;
}
.mcs-jh-room-level {
color: #90EE90;
font-size: 10px;
font-weight: bold;
text-align: center;
}
.mcs-jh-room-checkbox {
position: absolute;
bottom: 2px;
right: 2px;
width: 12px;
height: 12px;
cursor: pointer;
margin: 0;
z-index: 10;
}
.mcs-jh-detail-panel {
background: #2a2a2a;
border-radius: 4px;
padding: 12px;
border: 1px solid #444;
grid-column: 4;
display: flex;
flex-direction: column;
gap: 8px;
overflow-y: auto;
min-width: 220px;
}
.mcs-jh-detail-title {
font-size: 16px;
font-weight: bold;
color: #4CAF50;
text-align: left;
margin-bottom: 4px;
}
.mcs-jh-recipe-title {
font-size: 13px;
font-weight: bold;
color: #FFA500;
text-align: left;
margin-bottom: 4px;
}
.mcs-jh-section-label {
font-weight: bold;
font-size: 12px;
margin-bottom: 0px;
text-align: left;
line-height: 1;
}
.mcs-jh-label-materials-cost {
color: #FFA500;
}
.mcs-jh-label-gold {
color: #FFD700;
}
.mcs-jh-cost-values {
display: flex;
justify-content: flex-start;
gap: 16px;
font-size: 11px;
margin-bottom: 4px;
line-height: 1;
}
.mcs-jh-ask-value {
color: #ff6b6b;
font-weight: bold;
}
.mcs-jh-bid-value {
color: #4CAF50;
font-weight: bold;
}
.mcs-jh-gold-value {
font-weight: bold;
font-size: 11px;
margin-bottom: 2px;
text-align: left;
line-height: 1;
}
.mcs-jh-materials-title {
font-size: 12px;
font-weight: bold;
color: #90EE90;
margin-top: 0px;
margin-bottom: 4px;
}
.mcs-jh-materials-list {
display: flex;
flex-direction: column;
gap: 4px;
}
.mcs-jh-material-row {
display: flex;
justify-content: space-between;
padding: 4px 8px;
background: #333;
border-radius: 3px;
font-size: 11px;
cursor: pointer;
}
.mcs-jh-material-row:hover {
background: #444;
}
.mcs-jh-material-name {
color: #e0e0e0;
}
.mcs-jh-material-amount {
font-weight: bold;
}
.mcs-jh-max-level {
font-size: 14px;
color: #FFD700;
text-align: center;
padding: 20px;
font-weight: bold;
}
.mcs-jh-no-recipe {
font-size: 12px;
color: #888;
text-align: center;
padding: 20px;
}
.mcs-nt-pane {
min-width: 320px;
min-height: 300px;
}
.mcs-nt-title-section {
display: flex;
align-items: center;
gap: 8px;
}
.mcs-nt-column-header {
display: flex;
align-items: center;
padding: 8px 15px;
background: rgba(0,0,0,0.3);
border-bottom: 1px solid rgba(255,255,255,0.2);
gap: 8px;
}
.mcs-nt-col-icon-spacer {
width: 40px;
}
.mcs-nt-col-sort-name {
flex: 1;
cursor: pointer;
user-select: none;
color: #e0e0e0;
font-weight: bold;
font-size: 11px;
}
.mcs-nt-col-sort-value {
min-width: 160px;
text-align: right;
cursor: pointer;
user-select: none;
color: #999;
font-weight: bold;
font-size: 11px;
padding-right: 4px;
}
.mcs-nt-col-x-spacer {
min-width: 20px;
}
.mcs-nt-content {
padding: 10px 15px;
color: #e0e0e0;
font-size: 12px;
line-height: 1.6;
overflow-y: auto;
overflow-x: hidden;
height: calc(100% - 75px);
}
.mcs-nt-empty-state {
color: #999;
text-align: center;
padding: 20px;
}
.mcs-nt-section {
margin-bottom: 10px;
border: 1px solid #444;
border-radius: 4px;
background: #333;
}
.mcs-nt-section-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 8px 10px;
cursor: pointer;
user-select: none;
}
.mcs-nt-section-title-gold {
color: #FFD700;
font-weight: bold;
}
.mcs-nt-section-title-purple {
color: #9C27B0;
font-weight: bold;
}
.mcs-nt-section-summary {
font-size: 11px;
}
.mcs-nt-sell-value {
color: #4CAF50;
}
.mcs-nt-separator {
color: #888;
margin: 0 4px;
}
.mcs-nt-buy-value {
color: #FF6B6B;
}
.mcs-nt-total-value {
color: #FFD700;
}
.mcs-nt-expanded-content {
border-top: 1px solid #444;
max-height: 250px;
overflow-y: auto;
}
.mcs-nt-sell-orders-header {
padding: 4px 10px;
background: rgba(76, 175, 80, 0.1);
color: #4CAF50;
font-size: 10px;
font-weight: bold;
}
.mcs-nt-buy-orders-header {
padding: 4px 10px;
background: rgba(255, 107, 107, 0.1);
color: #FF6B6B;
font-size: 10px;
font-weight: bold;
}
.mcs-nt-order-row {
display: flex;
align-items: center;
padding: 6px 10px;
border-bottom: 1px solid rgba(255,255,255,0.05);
gap: 8px;
}
.mcs-nt-order-icon {
width: 32px;
height: 32px;
border: 1px solid rgba(255,255,255,0.2);
border-radius: 4px;
background: rgba(0,0,0,0.3);
padding: 2px;
}
.mcs-nt-order-details {
flex: 1;
min-width: 0;
}
.mcs-nt-order-name {
color: #e0e0e0;
font-size: 11px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.mcs-nt-order-unit-price {
font-size: 10px;
color: #888;
}
.mcs-nt-order-totals {
text-align: right;
min-width: 130px;
}
.mcs-nt-order-listing-total {
font-size: 11px;
}
.mcs-nt-order-market-totals {
font-size: 10px;
}
.mcs-nt-color-green {
color: #4CAF50;
}
.mcs-nt-color-blue {
color: #6495ED;
}
.mcs-nt-color-muted {
color: #888;
}
.mcs-nt-color-gold {
color: #FFD700;
}
.mcs-nt-section-content {
border-top: 1px solid #444;
padding: 8px 10px;
}
.mcs-nt-toggle-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 6px;
}
.mcs-nt-toggle-item {
display: flex;
align-items: center;
gap: 4px;
}
.mcs-nt-toggle-switch {
position: relative;
display: inline-block;
width: 28px;
height: 14px;
flex-shrink: 0;
}
.mcs-nt-toggle-checkbox {
opacity: 0;
width: 0;
height: 0;
}
.mcs-nt-toggle-slider {
position: absolute;
cursor: pointer;
top: 0;
left: 0;
right: 0;
bottom: 0;
transition: 0.3s;
border-radius: 14px;
}
.mcs-nt-toggle-knob {
position: absolute;
height: 10px;
width: 10px;
bottom: 2px;
background-color: white;
transition: 0.3s;
border-radius: 50%;
}
.mcs-nt-toggle-label {
font-size: 10px;
cursor: pointer;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.mcs-nt-item-row {
display: flex;
justify-content: space-between;
align-items: center;
padding: 6px;
border-bottom: 1px solid rgba(255,255,255,0.1);
gap: 8px;
}
.mcs-nt-item-icon {
width: 40px;
height: 40px;
border: 1px solid rgba(255,255,255,0.2);
border-radius: 4px;
background: rgba(0,0,0,0.3);
padding: 4px;
}
.mcs-nt-item-info {
flex: 1;
min-width: 0;
}
.mcs-nt-item-name {
color: #e0e0e0;
font-weight: bold;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.mcs-nt-item-qty {
color: #999;
font-size: 10px;
}
.mcs-nt-item-prices {
display: flex;
flex-direction: column;
align-items: flex-end;
gap: 2px;
min-width: 160px;
}
.mcs-nt-item-unit-price {
font-size: 10px;
color: #999;
}
.mcs-nt-item-total-price {
font-weight: bold;
color: #e0e0e0;
}
.mcs-nt-delete-btn {
background: #d32f2f;
color: white;
border: none;
border-radius: 3px;
padding: 2px 6px;
cursor: pointer;
font-size: 11px;
min-width: 20px;
}
.mcs-nt-market-info {
background: #424242;
color: #E0E0E0;
padding: 6px 8px;
border-radius: 4px;
text-align: center;
margin-top: 4px;
font-size: 11px;
font-family: monospace;
}
.mcs-nt-scam-warning {
background: #ff1744;
color: white;
padding: 8px;
border-radius: 4px;
font-weight: bold;
text-align: center;
margin-top: 4px;
font-size: 12px;
border: 2px solid #ff5252;
}
.mcs-nt-tally-btn {
color: white;
border: none;
padding: 8px 16px;
border-radius: 4px;
cursor: pointer;
font-weight: bold;
width: 100%;
margin-top: 4px;
}
.mcs-nt-marketplace-warning {
font-weight: bold;
font-size: 11px;
margin-top: 2px;
display: flex;
align-items: center;
justify-content: center;
gap: 4px;
}
.mcs-nt-scam-bid {
color: #ff1744;
font-weight: bold;
}
.mcs-nt-equal-vendor {
color: #FFD700;
font-weight: bold;
}
.mcs-nt-scam-total {
color: #ff1744;
}
.mcs-nt-equal-vendor-total {
color: #FFD700;
}
.mcs-ko-pane {
display: flex;
flex-direction: column;
width: 280px;
}
.mcs-ko-title-section {
display: flex;
align-items: center;
gap: 8px;
}
.mcs-ko-content {
display: flex;
flex-direction: column;
padding: 10px 15px;
color: #e0e0e0;
font-size: 12px;
line-height: 1.6;
}
.mcs-ko-player-name {
font-size: 13px;
font-weight: bold;
color: #4CAF50;
}
.mcs-ko-score-container {
margin-bottom: 5px;
}
.mcs-ko-score-row {
display: flex;
justify-content: space-between;
align-items: center;
cursor: pointer;
gap: 4px;
}
.mcs-ko-toggle-indicator {
color: #4CAF50;
font-weight: bold;
flex-shrink: 0;
}
.mcs-ko-label-value {
display: flex;
justify-content: space-between;
flex: 1;
}
.mcs-ko-score-label {
color: #FFA500;
}
.mcs-ko-score-value {
color: #4CAF50;
font-weight: bold;
}
.mcs-ko-details {
display: none;
margin-left: 20px;
margin-top: 5px;
font-size: 10px;
max-height: 200px;
overflow-y: auto;
}
.mcs-ko-total-row {
display: flex;
justify-content: space-between;
font-size: 13px;
margin-top: 5px;
padding-top: 5px;
border-top: 1px solid #444;
}
.mcs-ko-total-label {
color: #FFD700;
font-weight: bold;
}
.mcs-ko-total-value {
color: #FFD700;
font-weight: bold;
}
.mcs-ko-networth-container {
margin-top: 10px;
font-size: 12px;
}
.mcs-ko-networth-header {
display: flex;
justify-content: space-between;
cursor: pointer;
font-weight: bold;
color: #4CAF50;
}
.mcs-ko-networth-details {
display: none;
margin-left: 15px;
margin-top: 5px;
color: #aaa;
}
.mcs-ko-networth-row {
display: flex;
justify-content: space-between;
}
.mcs-ko-inspected-section {
margin-top: 15px;
border-top: 1px solid #444;
padding-top: 10px;
}
.mcs-ko-inspected-header {
cursor: pointer;
font-weight: bold;
color: #4CAF50;
font-size: 12px;
margin-bottom: 5px;
}
.mcs-ko-inspected-list {
display: none;
font-size: 11px;
max-height: 200px;
overflow-y: auto;
}
.mcs-ko-side-panel {
position: fixed;
background: rgba(30, 30, 30, 0.98);
border: 1px solid #444;
border-radius: 8px;
padding: 12px;
min-width: 180px;
max-width: 220px;
font-size: 0.875rem;
z-index: 10001;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.5);
}
.mcs-ko-score-section {
text-align: left;
color: #4CAF50;
}
.mcs-ko-side-name-row {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 6px;
}
.mcs-ko-side-player-name {
font-weight: bold;
color: #FFD700;
font-size: 0.9rem;
}
.mcs-ko-close-btn {
cursor: pointer;
font-size: 18px;
color: #aaa;
padding: 0 5px;
line-height: 1;
}
.mcs-ko-close-btn:hover {
color: #fff;
}
.mcs-ko-build-score-toggle {
cursor: pointer;
font-weight: bold;
margin-bottom: 8px;
}
.mcs-ko-side-score-details {
display: none;
margin-left: 10px;
margin-bottom: 10px;
}
.mcs-ko-buttons-container {
display: flex;
flex-direction: column;
gap: 8px;
margin-top: 10px;
}
.mcs-ko-action-btn {
border-radius: 5px;
height: 30px;
box-shadow: none;
border: 0px;
cursor: pointer;
margin-top: 10px;
padding: 0 12px;
font-weight: bold;
}
.mcs-ko-player-container {
border-bottom: 1px solid #333;
padding: 3px 0;
}
.mcs-ko-player-row {
color: #e0e0e0;
display: flex;
justify-content: space-between;
align-items: center;
gap: 4px;
}
.mcs-ko-delete-btn {
background: #ff5555;
color: #fff;
border: none;
border-radius: 3px;
padding: 0 6px;
font-size: 16px;
cursor: pointer;
line-height: 1;
font-weight: bold;
flex-shrink: 0;
}
.mcs-ko-delete-btn:hover {
background: #ff7777;
}
.mcs-ko-player-info {
display: flex;
justify-content: space-between;
flex: 1;
cursor: pointer;
}
.mcs-ko-player-details {
display: none;
margin-left: 20px;
margin-top: 5px;
font-size: 10px;
}
.mcs-ko-detail-subsection {
margin-bottom: 5px;
}
.mcs-ko-detail-sub-header {
display: flex;
align-items: center;
gap: 5px;
cursor: pointer;
color: #e0e0e0;
}
.mcs-ko-detail-sub-content {
display: none;
margin-left: 15px;
margin-top: 3px;
font-size: 9px;
}
.mcs-ko-detail-sub-content-scroll {
display: none;
margin-left: 15px;
margin-top: 3px;
font-size: 9px;
max-height: 150px;
overflow-y: auto;
}
.mcs-ko-detail-item {
padding: 1px 0;
margin-right: 4px;
}
.mcs-ko-detail-item-value {
color: #4CAF50;
margin-left: 5px;
}
.mcs-ko-detail-row {
display: flex;
justify-content: space-between;
color: #e0e0e0;
padding: 2px 0;
margin-right: 4px;
}
.mcs-ko-empty-message {
color: #888;
padding: 5px;
}
.mcs-ko-error-message {
color: #ff5555;
padding: 5px;
}
.lucky-panel {
position: fixed;
top: 0;
left: 0;
width: 80vw;
height: 80vh;
background: rgba(30, 30, 30, 0.98);
border: 1px solid rgba(80, 80, 80, 0.8);
border-radius: 0px;
z-index: 10100;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
color: white;
overflow: hidden;
min-width: 50px;
min-height: 50px;
}
.lucky-content {
overflow: hidden;
padding: 0;
position: relative;
width: 100%;
height: 100%;
}
.lucky-content-spacer {
position: absolute;
left: 0;
width: 100px;
height: 100px;
pointer-events: none;
opacity: 0;
}
.lucky-floating-panel {
position: absolute;
background: rgba(30, 30, 30, 0.95);
border: 1px solid transparent;
border-radius: 0px;
padding: 8px 12px 12px 12px;
z-index: 10001;
overflow: hidden;
display: flex;
flex-direction: column;
transition: border-color 0.2s;
}
.lucky-floating-panel:hover {
border-color: rgba(80, 80, 80, 0.8);
}
.lucky-resize-handle {
position: absolute;
user-select: none;
z-index: 10;
}
.lucky-resize-handle-right {
right: 0;
top: 0;
width: 10px;
height: 100%;
cursor: ew-resize;
}
.lucky-resize-handle-left {
left: 0;
top: 0;
width: 10px;
height: 100%;
cursor: ew-resize;
}
.lucky-resize-handle-bottom {
bottom: 0;
left: 0;
width: 100%;
height: 10px;
cursor: ns-resize;
}
.lucky-resize-handle-top {
top: 0;
left: 0;
width: 100%;
height: 10px;
cursor: ns-resize;
}
.lucky-resize-handle-corner {
right: 0;
bottom: 0;
width: 20px;
height: 20px;
cursor: nwse-resize;
}
.lucky-resize-handle-corner-topleft {
left: 0;
top: 0;
width: 20px;
height: 20px;
cursor: nwse-resize;
}
.lucky-resize-handle-corner-topright {
right: 0;
top: 0;
width: 20px;
height: 20px;
cursor: nesw-resize;
}
.lucky-resize-handle-corner-bottomleft {
left: 0;
bottom: 0;
width: 20px;
height: 20px;
cursor: nesw-resize;
}
.lucky-stats-section {
top: 60px;
right: 20px;
min-width: 50px;
max-width: 400px;
width: fit-content;
}
.lucky-data-panel {
min-width: 50px;
max-width: 800px;
width: 400px;
min-height: 50px;
max-height: 80vh;
}
.lucky-panel-header {
flex-shrink: 0;
overflow: visible;
}
.lucky-panel-content-scrollable {
flex: 1;
overflow-y: auto;
overflow-x: hidden;
min-height: 0;
}
.lucky-panel-header .lucky-drop-table {
margin-bottom: 0;
}
.lucky-panel-content-scrollable .lucky-drop-table {
margin-top: 0;
}
.lucky-panel-content-scrollable .lucky-drop-table tbody tr:first-child td {
border-top: none;
}
.lucky-player-stats-info {
font-size: 9px;
color: #a0b9ff;
margin-bottom: 8px;
padding-bottom: 8px;
border-bottom: 1px solid rgba(80, 80, 80, 0.3);
}
.lucky-revenue-panel {
min-width: 50px;
min-height: 50px;
width: fit-content;
height: fit-content;
}
.lucky-big-expected-panel {
min-width: 50px;
min-height: 50px;
max-width: 600px;
width: 400px;
height: fit-content;
}
.lucky-big-expected-header {
display: block;
margin-bottom: 8px;
}
.lucky-big-expected-content {
display: flex;
flex-wrap: wrap;
gap: 12px;
row-gap: 3px;
align-items: center;
}
.lucky-big-expected-item {
display: flex;
align-items: center;
gap: 8px;
font-size: 16px;
white-space: nowrap;
}
.lucky-big-expected-name {
color: #b0b0b0;
font-weight: 500;
}
.lucky-big-expected-percent {
font-weight: 600;
}
.lucky-big-expected-total {
font-weight: 600;
}
.lucky-big-luck-panel {
min-width: 50px;
min-height: 50px;
max-width: 600px;
width: 400px;
height: fit-content;
}
.lucky-big-luck-header {
display: block;
margin-bottom: 8px;
}
.lucky-big-luck-content {
display: flex;
flex-wrap: wrap;
gap: 12px;
row-gap: 3px;
align-items: center;
}
.lucky-big-luck-item {
display: flex;
align-items: center;
gap: 8px;
font-size: 16px;
white-space: nowrap;
}
.lucky-big-luck-name {
color: #b0b0b0;
font-weight: 500;
}
.lucky-big-luck-percent {
font-weight: 600;
}
.lucky-big-luck-total {
font-weight: 600;
}
.lucky-content-controls {
position: absolute;
top: 8px;
right: 8px;
display: flex;
gap: 8px;
z-index: 10102;
opacity: 0;
transition: opacity 0.2s;
}
.lucky-panel:hover .lucky-content-controls {
opacity: 1;
}
.lucky-control-icon {
width: 24px;
height: 24px;
background: rgba(40, 40, 40, 0.9);
border: 1px solid rgba(80, 80, 80, 0.6);
border-radius: 0px;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
color: rgba(255, 255, 255, 0.7);
font-size: 14px;
transition: all 0.2s;
user-select: none;
}
.lucky-control-icon:hover {
background: rgba(60, 60, 60, 0.9);
color: rgba(255, 255, 255, 0.9);
border-color: rgba(100, 100, 100, 0.8);
}
.lucky-control-icon.active {
background: rgba(84, 109, 219, 0.3);
border-color: rgba(84, 109, 219, 0.6);
color: #4ade80;
}
.lucky-options-panel {
position: fixed;
background: rgba(30, 30, 30, 0.98);
border: 1px solid rgba(80, 80, 80, 0.8);
border-radius: 0px;
padding: 12px;
z-index: 10103;
display: none;
}
.lucky-options-panel.visible {
display: block;
}
.lucky-options-title {
font-size: 14px;
font-weight: 600;
color: #e8e8e8;
margin-bottom: 12px;
border-bottom: 1px solid rgba(80, 80, 80, 0.5);
padding-bottom: 8px;
}
.lucky-options-title-mt {
margin-top: 12px;
}
.lucky-option-row {
display: flex;
align-items: center;
gap: 4px;
background: rgba(255,255,255,0.05);
padding: 4px 8px;
border-radius: 0px;
white-space: nowrap;
}
.lucky-option-checkbox {
cursor: pointer;
}
.lucky-option-label {
color: #e0e0e0;
font-size: 10px;
cursor: pointer;
user-select: none;
}
.lucky-stats-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 8px;
user-select: none;
cursor: move;
padding: 2px 0;
}
.lucky-stats-header:hover {
background: rgba(255, 255, 255, 0.05);
}
.lucky-stats-title {
font-size: 13px;
font-weight: 600;
flex-grow: 1;
color: #e8e8e8;
}
.lucky-stat-row {
display: flex;
flex-wrap: wrap;
align-items: baseline;
gap: 4px;
font-size: 12px;
padding: 3px 0;
color: #a8aed4;
}
.lucky-stat-label {
color: #b0b0b0;
white-space: nowrap;
}
.lucky-stat-value {
color: #4ade80;
font-weight: 500;
white-space: nowrap;
}
.lucky-revenue-row-container {
margin-bottom: 8px;
}
.lucky-revenue-row-container:last-child {
margin-bottom: 0;
}
.lucky-revenue-row-container:last-child .lucky-revenue-row {
font-weight: 600;
padding-top: 8px;
border-top: 1px solid rgba(80, 80, 80, 0.3);
}
.lucky-revenue-row {
display: flex;
flex-wrap: wrap;
gap: 8px;
row-gap: 2px;
align-items: baseline;
font-size: 11px;
}
.lucky-revenue-name {
color: #b0b0b0;
min-width: 100px;
flex-shrink: 0;
}
.lucky-revenue-stat {
display: flex;
gap: 4px;
align-items: baseline;
white-space: nowrap;
}
.lucky-revenue-stats-group {
display: flex;
gap: 8px;
flex-wrap: nowrap;
white-space: nowrap;
}
.lucky-revenue-stat-label {
color: #888;
font-size: 9px;
}
.lucky-revenue-stat-value {
color: #e0e0e0;
font-weight: 500;
}
.lucky-revenue-stat-value.colored {
color: inherit;
}
.lucky-player-section {
background: rgba(40, 40, 40, 0.6);
border: 1px solid rgba(80, 80, 80, 0.4);
border-radius: 0px;
margin-bottom: 12px;
overflow: hidden;
}
.lucky-player-header {
background: rgba(50, 50, 50, 0.8);
padding: 8px 12px;
font-weight: 600;
font-size: 13px;
border-bottom: 1px solid rgba(80, 80, 80, 0.3);
}
.lucky-drop-table {
width: 100%;
border-collapse: collapse;
table-layout: fixed;
}
.lucky-drop-table th {
padding: 8px;
text-align: left;
font-size: 11px;
color: #b0b0b0;
font-weight: 600;
border-bottom: 1px solid rgba(80, 80, 80, 0.3);
background: rgba(40, 40, 40, 0.8);
}
.lucky-drop-table td {
padding: 6px 8px;
font-size: 11px;
border-bottom: 1px solid rgba(80, 80, 80, 0.15);
}
.lucky-drop-table tr:hover {
background: rgba(60, 60, 60, 0.4);
}
.lucky-item-name {
color: #d0d0d0;
font-weight: 500;
}
.lucky-value-positive {
color: #4ade80;
}
.lucky-value-negative {
color: #f87171;
}
.lucky-value-neutral {
color: #a8aed4;
}
.lucky-no-data {
text-align: center;
padding: 30px;
color: #a8aed4;
font-size: 14px;
}
.lucky-grid-overlay {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
min-height: 1000px;
background-image:
repeating-linear-gradient(0deg, transparent, transparent 9px, rgba(200, 200, 200, 0.3) 9px, rgba(200, 200, 200, 0.3) 10px),
repeating-linear-gradient(90deg, transparent, transparent 9px, rgba(200, 200, 200, 0.3) 9px, rgba(200, 200, 200, 0.3) 10px);
pointer-events: none;
display: none;
z-index: 1000;
}
.lucky-options-container {
display: flex;
flex-wrap: wrap;
gap: 8px;
align-items: center;
}
.lucky-reset-btn {
margin-top: 12px;
padding: 6px 12px;
background: rgba(60, 60, 60, 0.9);
border: 1px solid rgba(80, 80, 80, 0.6);
border-radius: 0px;
color: #e8e8e8;
cursor: pointer;
font-size: 11px;
}
.lucky-auto-grid-btn {
margin-top: 8px;
padding: 8px;
width: 100%;
background: #22c55e;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 14px;
font-weight: bold;
}
.lucky-empty-state {
color: #888;
padding: 8px;
text-align: center;
}
.lucky-total-row {
background: rgba(84, 109, 219, 0.15);
font-weight: 600;
border-top: 2px solid rgba(113, 123, 169, 0.8);
}
.mcs-ma-pane {
width: 600px;
height: 550px;
resize: both;
overflow: hidden;
min-height: 300px;
}
.mcs-ma-title-section {
display: flex;
align-items: center;
gap: 12px;
}
.mcs-ma-time-display {
font-size: 12px;
color: #aaa;
padding: 4px 8px;
background: rgba(0, 0, 0, 0.3);
border-radius: 3px;
}
.mcs-ma-mana-display {
font-size: 12px;
color: #64b5f6;
padding: 4px 8px;
background: rgba(0, 0, 0, 0.3);
border-radius: 3px;
}
.mcs-ma-content {
padding: 10px 15px;
color: #e0e0e0;
font-size: 12px;
line-height: 1.6;
overflow-y: auto;
overflow-x: hidden;
height: calc(100% - 40px);
}
.mcs-ma-waiting {
color: #999;
text-align: center;
padding: 40px 20px;
font-size: 14px;
}
.mcs-ma-waiting-icon {
font-size: 18px;
margin-bottom: 10px;
}
.mcs-ma-waiting-sub {
font-size: 11px;
margin-top: 8px;
color: #666;
}
.mcs-ma-table-wrapper {
padding: 12px;
}
.mcs-ma-table {
width: 100%;
border-collapse: collapse;
}
.mcs-ma-th {
padding: 8px;
color: #aaa;
font-size: 11px;
font-weight: normal;
}
.mcs-ma-th-left {
text-align: left;
}
.mcs-ma-th-right {
text-align: right;
}
.mcs-ma-thead-row {
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
}
.mcs-ma-changes-section {
margin-top: 16px;
padding-top: 16px;
border-top: 1px solid rgba(74, 144, 226, 0.3);
}
.mcs-ma-ability-row {
border-bottom: 1px solid rgba(255, 255, 255, 0.05);
}
.mcs-ma-td {
padding: 8px;
}
.mcs-ma-td-right {
text-align: right;
padding: 8px;
}
.mcs-ma-ability-name {
padding: 8px;
color: #e0e0e0;
text-transform: capitalize;
}
.mcs-ma-color-green {
color: #81c784;
}
.mcs-ma-color-orange {
color: #ffb74d;
}
.mcs-ma-color-red {
color: #ef5350;
}
.mcs-ma-color-blue {
color: #64b5f6;
}
.mcs-ma-color-purple {
color: #9c27b0;
}
.mcs-ma-change-row {
border-bottom: 1px solid rgba(255, 255, 255, 0.05);
}
.mcs-ma-net-row {
border-top: 2px solid rgba(74, 144, 226, 0.3);
font-weight: bold;
}
.mcs-ma-full-mana-row {
border-top: 1px solid rgba(255, 255, 255, 0.1);
}
.mcs-ih-pane {
min-width: 400px;
max-width: 600px;
width: fit-content;
max-height: 80vh;
}
.mcs-ih-title-section {
display: flex;
align-items: center;
gap: 12px;
}
.mcs-ih-title {
color: #FF4444;
}
.mcs-ih-time-display {
font-size: 12px;
color: #aaa;
padding: 4px 8px;
background: rgba(0, 0, 0, 0.3);
border-radius: 3px;
}
.mcs-ih-deaths-display {
font-size: 12px;
color: #FFA07A;
padding: 4px 8px;
background: rgba(0, 0, 0, 0.3);
border-radius: 3px;
}
.mcs-ih-content {
padding: 12px;
color: white;
font-size: 12px;
overflow-y: auto;
flex: 1;
max-height: 600px;
}
.mcs-ih-waiting {
color: #999;
text-align: center;
padding: 40px 20px;
font-size: 14px;
}
.mcs-ih-waiting-icon {
font-size: 18px;
margin-bottom: 10px;
}
.mcs-ih-waiting-sub {
font-size: 11px;
margin-top: 8px;
color: #666;
}
.mcs-ih-main {
margin-bottom: 12px;
}
.mcs-ih-deaths-row {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 6px;
padding: 4px 8px;
background: rgba(255, 68, 68, 0.1);
border-radius: 4px;
font-size: 11px;
flex-wrap: wrap;
}
.mcs-ih-deaths-label {
color: #FFA07A;
font-weight: bold;
}
.mcs-ih-player-name {
color: #FFD700;
}
.mcs-ih-death-count {
color: #FF4444;
font-weight: bold;
}
.mcs-ih-separator {
color: #555;
}
.mcs-ih-total-label {
color: #aaa;
}
.mcs-ih-dph-row {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 10px;
padding: 4px 8px;
background: rgba(255, 160, 122, 0.1);
border-radius: 4px;
font-size: 11px;
flex-wrap: wrap;
}
.mcs-ih-dph-value {
color: #FFA07A;
}
.mcs-ih-dph-total {
color: #FFA07A;
font-weight: bold;
}
.mcs-ih-player-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 8px;
}
.mcs-ih-player-card {
background: rgba(255, 255, 255, 0.05);
padding: 10px;
border-radius: 4px;
border: 1px solid rgba(255, 255, 255, 0.1);
}
.mcs-ih-player-card-name {
color: #FFD700;
font-weight: bold;
margin-bottom: 8px;
text-align: center;
font-size: 13px;
}
.mcs-ih-player-stats {
display: flex;
flex-direction: column;
gap: 4px;
}
.mcs-ih-stat-row {
display: flex;
justify-content: space-between;
}
.mcs-ih-stat-label {
color: #aaa;
font-size: 10px;
}
.mcs-ih-damage-value {
color: #FF4444;
font-weight: bold;
}
.mcs-ih-regen-value {
color: #4CAF50;
font-weight: bold;
}
.mcs-ih-total-section {
margin-top: 4px;
padding-top: 4px;
border-top: 1px solid rgba(255, 255, 255, 0.1);
}
.mcs-ih-total-label-sm {
color: #888;
font-size: 9px;
}
.mcs-ih-total-dmg {
color: #FF6666;
font-size: 10px;
}
.mcs-ih-total-regen {
color: #4CAF50;
font-size: 10px;
}
.mcs-ih-death-section {
margin-top: 6px;
padding-top: 6px;
border-top: 1px solid rgba(255, 255, 255, 0.1);
}
.mcs-ih-death-count-sm {
color: #FF4444;
font-size: 10px;
font-weight: bold;
}
.mcs-ih-dph-value-sm {
color: #FFA07A;
font-size: 10px;
}
.mcs-ih-section {
margin-top: 16px;
padding-top: 12px;
border-top: 1px solid rgba(255, 255, 255, 0.1);
}
.mcs-ih-section-toggle {
display: flex;
align-items: center;
gap: 6px;
margin-bottom: 8px;
cursor: pointer;
padding: 4px;
border-radius: 3px;
background: rgba(255, 255, 255, 0.02);
}
.mcs-ih-enemy-toggle {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 8px;
cursor: pointer;
padding: 4px;
border-radius: 3px;
background: rgba(255, 255, 255, 0.02);
}
.mcs-ih-toggle-left {
display: flex;
align-items: center;
gap: 6px;
}
.mcs-ih-toggle-arrow {
font-size: 9px;
min-width: 12px;
}
.mcs-ih-toggle-arrow-red {
color: #FF6B6B;
}
.mcs-ih-toggle-arrow-gold {
color: #FFD700;
}
.mcs-ih-toggle-arrow-blue {
color: #87CEEB;
}
.mcs-ih-section-label {
color: #aaa;
font-size: 11px;
font-weight: bold;
}
.mcs-ih-enemy-total {
color: #FF6B6B;
font-weight: bold;
font-size: 14px;
}
.mcs-ih-enemy-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 8px;
}
.mcs-ih-enemy-card {
padding: 8px;
background: rgba(255, 107, 107, 0.1);
border-radius: 4px;
border-left: 3px solid #FF6B6B;
}
.mcs-ih-enemy-name {
font-weight: bold;
color: #FF6B6B;
margin-bottom: 6px;
font-size: 11px;
}
.mcs-ih-enemy-total-dmg {
font-size: 10px;
color: #FF8888;
margin-bottom: 4px;
}
.mcs-ih-enemy-range {
font-size: 9px;
color: #FFB6C1;
margin-bottom: 6px;
}
.mcs-ih-enemy-players {
display: flex;
flex-direction: column;
gap: 2px;
margin-top: 6px;
}
.mcs-ih-enemy-player-row {
display: flex;
justify-content: space-between;
align-items: center;
font-size: 9px;
background: rgba(255, 255, 255, 0.03);
padding: 3px 6px;
border-radius: 3px;
}
.mcs-ih-enemy-player-name {
color: #FF6666;
font-weight: bold;
}
.mcs-ih-enemy-player-dmg {
color: #FF8888;
}
.mcs-ih-enemy-player-range {
color: #FFB6C1;
font-size: 8px;
}
.mcs-ih-profile-card {
margin-bottom: 8px;
padding: 8px;
background: rgba(0, 0, 0, 0.2);
border-radius: 4px;
}
.mcs-ih-profile-name {
color: #FFD700;
font-size: 11px;
margin-bottom: 4px;
}
.mcs-ih-profile-stats {
font-size: 10px;
color: #aaa;
}
.mcs-ih-encounters-total {
color: #87CEEB;
font-weight: bold;
font-size: 12px;
}
.mcs-ih-kills-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 6px;
}
.mcs-ih-kill-card {
padding: 6px 8px;
background: rgba(135, 206, 235, 0.08);
border-radius: 4px;
border-left: 3px solid #87CEEB;
}
.mcs-ih-kill-card-dim {
padding: 6px 8px;
background: rgba(135, 206, 235, 0.05);
border-radius: 4px;
border-left: 3px solid #555;
opacity: 0.6;
}
.mcs-ih-kill-name {
font-weight: bold;
color: #87CEEB;
font-size: 11px;
margin-bottom: 4px;
}
.mcs-ih-kill-stats {
display: flex;
justify-content: space-between;
font-size: 10px;
}
.mcs-ih-kill-actual {
color: #ddd;
}
.mcs-ih-kill-expected {
color: #888;
}
.mcs-qcharm-pane {
position: fixed;
top: 0;
left: 0;
background: #2b2b2b;
border-radius: 6px;
box-shadow: 0 4px 12px rgba(0,0,0,0.5);
z-index: 99999;
font-family: sans-serif;
resize: both;
overflow: hidden;
min-width: 400px;
min-height: 250px;
}
/* QCharm pane when minimized */
.mcs-qcharm-pane-minimized {
min-height: 0;
resize: none;
}
.mcs-qcharm-title-section {
display: flex;
align-items: center;
gap: 8px;
}
.mcs-qcharm-title {
font-size: 14px;
}
.mcs-qcharm-button-section {
display: flex;
align-items: center;
gap: 6px;
}
.mcs-qcharm-content {
padding: 10px;
color: #e0e0e0;
font-size: 12px;
overflow-y: auto;
overflow-x: hidden;
height: calc(100% - 40px);
}
.mcs-qcharm-empty {
display: flex;
align-items: center;
justify-content: center;
height: 100%;
color: #999;
font-size: 14px;
}
.mcs-qcharm-table {
width: 100%;
border-collapse: collapse;
background: #1a1a1a;
border-radius: 4px;
overflow: hidden;
}
.mcs-qcharm-th {
background: #333;
color: #fff;
padding: 8px 10px;
text-align: left;
font-weight: bold;
font-size: 11px;
border-bottom: 2px solid #444;
}
.mcs-qcharm-sortable {
cursor: pointer;
user-select: none;
position: relative;
}
.mcs-qcharm-sortable:hover {
background: #3a3a3a;
}
.mcs-qcharm-sorted-asc::after {
content: ' ▲';
font-size: 10px;
color: #4CAF50;
}
.mcs-qcharm-sorted-desc::after {
content: ' ▼';
font-size: 10px;
color: #4CAF50;
}
.mcs-qcharm-row {
border-bottom: 1px solid #2a2a2a;
}
.mcs-qcharm-row:hover {
background: #252525;
}
.mcs-qcharm-equipped {
background: rgba(255, 152, 0, 0.15);
}
.mcs-qcharm-equipped:hover {
background: rgba(255, 152, 0, 0.25);
}
.mcs-qcharm-td {
padding: 8px 10px;
color: #e0e0e0;
font-size: 12px;
}
/* QCharm Guide Section */
.mcs-qcharm-guide-section {
margin-bottom: 12px;
border: 1px solid #444;
border-radius: 4px;
background: rgba(0, 0, 0, 0.3);
}
.mcs-qcharm-guide-header {
padding: 8px 12px;
background: rgba(0, 0, 0, 0.4);
cursor: pointer;
display: flex;
justify-content: space-between;
align-items: center;
font-weight: 600;
color: #ffd700;
font-size: 13px;
user-select: none;
}
.mcs-qcharm-guide-header:hover {
background: rgba(0, 0, 0, 0.5);
}
.mcs-qcharm-guide-toggle {
font-size: 10px;
color: #999;
}
.mcs-qcharm-guide-content {
padding: 10px;
max-height: 500px;
overflow: hidden;
transition: max-height 0.3s ease;
}
.mcs-qcharm-guide-collapsed {
max-height: 0;
padding: 0 10px;
}
.mcs-qcharm-guide-row {
display: flex;
flex-wrap: wrap;
gap: 8px;
margin-bottom: 10px;
}
.mcs-qcharm-guide-item {
padding: 4px 10px;
background: rgba(50, 50, 50, 0.6);
border: 1px solid #555;
border-radius: 3px;
font-size: 12px;
color: #e0e0e0;
white-space: nowrap;
}
.mcs-qcharm-guide-enh-table {
display: flex;
flex-direction: column;
gap: 4px;
}
.mcs-qcharm-guide-enh-row {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 6px;
}
.mcs-qcharm-guide-enh-cell {
padding: 4px 8px;
background: rgba(40, 40, 40, 0.6);
border: 1px solid #555;
border-radius: 3px;
font-size: 11px;
color: #d0d0d0;
text-align: center;
}
.mcs-qcharm-guide-enh-cell:empty {
visibility: hidden;
}
.mcs-qcharm-table-container {
/* Container for the actual table, separate from guide */
}
.mcs-qcharm-last-seen {
font-size: 11px;
color: #999;
font-style: italic;
}
/* QCharm Collapsible Table Sections */
.mcs-qcharm-section {
margin-bottom: 12px;
border: 1px solid #444;
border-radius: 4px;
background: rgba(0, 0, 0, 0.3);
}
.mcs-qcharm-section-header {
padding: 8px 12px;
background: rgba(0, 0, 0, 0.4);
cursor: pointer;
display: flex;
justify-content: space-between;
align-items: center;
font-weight: 600;
color: #87CEEB;
font-size: 12px;
user-select: none;
}
.mcs-qcharm-section-header:hover {
background: rgba(0, 0, 0, 0.5);
}
.mcs-qcharm-section-toggle {
font-size: 10px;
color: #999;
}
.mcs-qcharm-section-content {
max-height: 2000px;
overflow: hidden;
transition: max-height 0.3s ease;
}
.mcs-qcharm-section-collapsed {
max-height: 0;
overflow: hidden;
}
.mcs-resize-handle {
position: absolute;
width: 12px;
height: 12px;
z-index: 10;
}
.mcs-resize-handle-nw {
top: 0;
left: 0;
cursor: nw-resize;
}
.mcs-resize-handle-ne {
top: 0;
right: 0;
cursor: ne-resize;
}
.mcs-resize-handle-sw {
bottom: 0;
left: 0;
cursor: sw-resize;
}
.mcs-resize-handle-se {
bottom: 0;
right: 0;
cursor: se-resize;
}
.mcs-cursor-move {
cursor: move;
}
.mcs-cursor-grabbing {
cursor: grabbing;
}
.mcs-clickable {
cursor: pointer;
}
.mcs-tr-pane {
min-width: 400px;
min-height: 200px;
resize: both;
overflow: hidden;
}
.mcs-tr-title-section {
display: flex;
align-items: center;
gap: 8px;
}
.mcs-tr-title {
color: #FFD700;
}
.mcs-tr-header-btn {
cursor: pointer;
font-size: 11px;
padding: 2px 6px;
border-radius: 3px;
margin-left: 8px;
}
.mcs-tr-reset-all-btn {
color: #F44336;
background: #444;
}
.mcs-tr-reset-all-btn:hover {
background: #555;
}
.mcs-tr-configure-btn {
font-size: 14px;
color: #aaa;
background: #444;
}
.mcs-tr-configure-btn:hover {
background: #555;
color: #fff;
}
.mcs-tr-configure-btn.active {
background: #666;
color: #FFD700;
}
.mcs-tr-minimize-btn {
cursor: pointer;
font-size: 18px;
color: #aaa;
padding: 0 5px;
}
.mcs-tr-minimize-btn:hover {
color: #fff;
}
.mcs-tr-content {
padding: 10px;
color: #ddd;
font-size: 12px;
overflow-y: auto;
flex: 1;
}
.mcs-tr-content-minimized {
padding: 6px 10px;
color: #ddd;
font-size: 11px;
display: flex;
flex-wrap: wrap;
gap: 6px;
align-items: center;
}
.mcs-tr-side-panel {
position: fixed;
background: rgba(30, 30, 30, 0.98);
border: 1px solid #444;
border-radius: 8px;
padding: 12px;
min-width: 200px;
max-width: 280px;
z-index: 100000;
font-size: 11px;
color: #ddd;
box-shadow: 0 4px 12px rgba(0,0,0,0.5);
max-height: 80vh;
overflow-y: auto;
}
.mcs-tr-side-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 8px;
}
.mcs-tr-side-title {
font-weight: bold;
color: #FFD700;
font-size: 12px;
}
.mcs-tr-close-btn {
cursor: pointer;
font-size: 18px;
color: #aaa;
padding: 0 5px;
line-height: 1;
}
.mcs-tr-close-btn:hover {
color: #fff;
}
.mcs-tr-no-data {
color: #888;
text-align: center;
padding: 10px;
}
.mcs-tr-side-section {
margin-bottom: 8px;
padding-bottom: 8px;
border-bottom: 1px solid #444;
}
.mcs-tr-side-section-title {
color: #87CEEB;
font-weight: bold;
margin-bottom: 4px;
}
.mcs-tr-side-value-row {
display: flex;
justify-content: space-between;
align-items: center;
}
.mcs-tr-side-item-row {
display: flex;
align-items: center;
gap: 4px;
margin-bottom: 3px;
}
.mcs-tr-side-item-details {
display: flex;
flex-direction: column;
flex: 1;
}
.mcs-tr-side-item-values {
display: flex;
align-items: center;
gap: 4px;
}
.mcs-tr-min-30 {
min-width: 30px;
}
.mcs-tr-min-45 {
min-width: 45px;
font-size: 9px;
}
.mcs-tr-font-9 {
font-size: 9px;
}
.mcs-tr-side-expected-row {
display: flex;
align-items: center;
gap: 4px;
color: #666;
font-size: 9px;
}
.mcs-tr-side-total {
margin-top: 8px;
padding-top: 8px;
border-top: 1px solid #444;
}
.mcs-tr-side-total-text {
color: #888;
font-size: 10px;
}
.mcs-tr-side-action-row {
margin-top: 10px;
text-align: center;
}
.mcs-tr-view-stats-btn {
background: #444;
border: 1px solid #666;
color: #FFD700;
padding: 6px 12px;
border-radius: 4px;
cursor: pointer;
font-size: 10px;
width: 100%;
}
.mcs-tr-view-stats-btn:hover {
background: #555;
}
.mcs-tr-loading {
text-align: center;
color: #888;
padding: 20px;
}
.mcs-tr-configure-header {
background: #444;
padding: 8px;
margin-bottom: 10px;
border-radius: 4px;
text-align: center;
color: #FFD700;
}
.mcs-tr-reset-btn {
background: #555;
border: none;
color: #ddd;
padding: 4px 8px;
border-radius: 3px;
cursor: pointer;
font-size: 11px;
margin-left: 8px;
}
.mcs-tr-reset-btn:hover {
background: #666;
}
.mcs-tr-visibility-btn {
border: none;
padding: 4px 8px;
border-radius: 3px;
cursor: pointer;
font-size: 14px;
margin-left: auto;
}
.mcs-tr-visibility-btn-visible {
background: #4a6a4a;
color: #fff;
}
.mcs-tr-visibility-btn-hidden {
background: #555;
color: #888;
}
.mcs-tr-chest-row {
background: #3a3a3a;
border-radius: 4px;
margin-bottom: 8px;
overflow: hidden;
}
.mcs-tr-chest-header {
display: flex;
align-items: center;
padding: 8px;
gap: 8px;
}
.mcs-tr-chest-header-clickable {
cursor: pointer;
}
.mcs-tr-expand-icon {
font-weight: bold;
width: 15px;
}
.mcs-tr-expand-icon-green {
color: #4CAF50;
}
.mcs-tr-expand-icon-purple {
color: #9370DB;
}
.mcs-tr-chest-name {
flex: 1;
font-weight: bold;
}
.mcs-tr-chest-ev {
color: #87CEEB;
font-weight: normal;
font-size: 11px;
}
.mcs-tr-chest-count {
color: #888;
margin-right: 8px;
}
.mcs-tr-section-container {
margin-bottom: 12px;
border-bottom: 2px solid #555;
padding-bottom: 10px;
}
.mcs-tr-keys-grid {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 4px;
}
.mcs-tr-key-item {
display: flex;
align-items: center;
gap: 3px;
padding: 3px;
background: #3a3a3a;
border-radius: 3px;
}
.mcs-tr-key-details {
flex: 1;
min-width: 0;
}
.mcs-tr-key-name {
font-size: 9px;
color: #aaa;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.mcs-tr-key-price {
font-size: 10px;
font-weight: bold;
}
.mcs-tr-token-row {
background: #3a3a3a;
border-radius: 4px;
margin-bottom: 6px;
overflow: hidden;
}
.mcs-tr-token-header {
display: flex;
align-items: center;
padding: 6px 8px;
cursor: pointer;
gap: 6px;
}
.mcs-tr-token-name {
flex: 1;
font-weight: bold;
font-size: 12px;
}
.mcs-tr-token-value {
font-size: 11px;
}
.mcs-tr-token-items {
padding: 6px 8px 8px 30px;
border-top: 1px solid #555;
font-size: 11px;
}
.mcs-tr-token-item-row {
display: flex;
align-items: center;
gap: 6px;
margin-bottom: 4px;
}
.mcs-tr-best-value {
background: rgba(76, 175, 80, 0.2);
border-radius: 3px;
padding: 2px 4px;
margin: -2px -4px;
}
.mcs-tr-best-badge {
color: #4CAF50;
font-size: 9px;
margin-left: 4px;
}
.mcs-tr-token-item-name {
flex: 1;
}
.mcs-tr-token-item-cost {
color: #888;
min-width: 50px;
text-align: right;
}
.mcs-tr-token-item-price {
min-width: 60px;
text-align: right;
}
.mcs-tr-token-item-vpt {
color: #aaa;
min-width: 55px;
text-align: right;
}
.mcs-tr-mini-item {
display: flex;
align-items: center;
gap: 3px;
background: #3a3a3a;
padding: 3px 6px;
border-radius: 3px;
}
.mcs-tr-no-chests {
color: #888;
}
.mcs-tr-no-loot {
padding: 10px;
color: #888;
}
.mcs-tr-expanded {
padding: 10px;
border-top: 1px solid #555;
}
.mcs-tr-col-header {
font-weight: bold;
color: #87CEEB;
margin-bottom: 2px;
text-align: center;
}
.mcs-tr-grid-3col {
display: grid;
grid-template-columns: 1fr 1fr 1fr;
gap: 10px;
font-size: 11px;
}
.mcs-tr-col-value {
text-align: center;
margin-bottom: 6px;
font-size: 10px;
}
.mcs-tr-loot-row {
display: flex;
align-items: center;
gap: 4px;
margin-bottom: 3px;
}
.mcs-tr-loot-count {
min-width: 35px;
}
.mcs-tr-loot-value {
font-size: 10px;
min-width: 40px;
}
.mcs-tr-loot-diff {
font-size: 10px;
}
.mcs-tr-expected-row {
display: flex;
align-items: center;
gap: 3px;
margin-bottom: 3px;
font-size: 10px;
}
.mcs-tr-expected-count {
color: #888;
min-width: 28px;
}
.mcs-tr-expected-value {
min-width: 32px;
}
.mcs-tr-expected-sep {
color: #666;
}
.mcs-tr-expected-row-single {
display: flex;
align-items: center;
gap: 4px;
margin-bottom: 3px;
}
.mcs-tr-expected-count-single {
color: #888;
min-width: 35px;
}
.mcs-tr-expected-value-single {
font-size: 10px;
}
.mcs-data-bridge {
display: none;
}
.ldt-price-toggle-btn,
.ldt-price-toggle-btn-hidden {
background-color: rgba(76, 175, 80, 0.3);
border: 1px solid #4CAF50;
color: #4CAF50;
padding: 2px 6px;
border-radius: 3px;
cursor: pointer;
font-size: 11px;
}
.ldt-price-toggle-btn-hidden {
margin-left: 6px;
}
.ldt-content-toggle-btn {
background: rgba(255, 255, 255, 0.1);
color: white;
border: 1px solid rgba(255, 255, 255, 0.3);
border-radius: 4px;
padding: 2px 8px;
cursor: pointer;
font-size: 12px;
font-weight: bold;
}
.ldt-minimize-content-btn {
margin-left: auto;
}
.ldt-body-empty-item {
font-size: var(--ldt-font-size-small);
padding: 5px 0;
color: var(--ldt-text-secondary);
}
.ldt-empty-help-text {
color: var(--ldt-text-secondary);
}
.mcs-suite-header {
grid-column: 1 / -1;
display: flex;
justify-content: center;
align-items: center;
gap: 8px;
padding: 6px 8px 2px 8px;
background: rgba(0, 0, 0, 0.3);
border-radius: 4px 4px 0 0;
font-size: 11px;
text-align: center;
}
.mcs-suite-char-mode {
color: #87CEEB;
font-weight: 600;
}
.mcs-suite-char-name {
grid-column: 1 / -1;
margin-bottom: 8px;
padding: 2px 8px 6px 8px;
background: rgba(0, 0, 0, 0.3);
border-radius: 0 0 4px 4px;
font-size: 11px;
color: #FFD700;
text-align: center;
}
.mcs-suite-toggle-row {
grid-column: 1 / -1;
display: flex;
justify-content: center;
align-items: center;
padding: 6px 0;
margin-bottom: 6px;
border-bottom: 1px solid #444;
}
.mcs-suite-toggle-label {
display: flex;
align-items: center;
gap: 6px;
cursor: pointer;
font-size: 12px;
}
.mcs-suite-toggle-text {
color: #ccc;
}
.mcs-suite-refresh-msg {
color: #f44336;
font-size: 11px;
margin-left: 10px;
}
.mcs-suite-gm-available {
color: #4CAF50;
}
.mcs-suite-gm-unavailable {
color: #f44336;
}
.mcs-suite-columns {
display: flex;
gap: 16px;
}
.mcs-suite-col {
flex: 1;
}
.mcs-suite-separator {
margin: 10px 0;
border: none;
border-top: 1px solid #444;
}
.mcs-suite-tool-label {
display: flex;
align-items: center;
margin-bottom: 2px;
cursor: pointer;
user-select: none;
}
.mcs-suite-sub-label {
display: flex;
align-items: center;
margin-bottom: 3px;
cursor: pointer;
user-select: none;
margin-left: 16px;
font-size: 11px;
}
.mcs-suite-checkbox {
margin-right: 8px;
cursor: pointer;
}
.mcs-suite-tool-name {
font-size: 13px;
}
.mcs-pf-pane {
position: fixed;
top: 0;
left: 0;
background: #2b2b2b;
border-radius: 6px;
box-shadow: 0 4px 12px rgba(0,0,0,0.5);
z-index: 99999;
font-family: sans-serif;
resize: both;
overflow: hidden;
min-width: 320px;
min-height: 300px;
}
.mcs-pf-header {
background: #333333;
color: #eeeeee;
padding: 8px 12px;
font-weight: bold;
cursor: move;
border-radius: 6px 6px 0 0;
display: flex;
justify-content: space-between;
align-items: center;
user-select: none;
}
.mcs-pf-title-section {
display: flex;
align-items: center;
gap: 8px;
}
.mcs-pf-title {
font-size: 14px;
}
.mcs-pf-button-section {
display: flex;
align-items: center;
gap: 6px;
}
.mcs-pf-btn {
background: #555;
color: #fff;
border: 1px solid #666;
border-radius: 3px;
padding: 4px 8px;
font-size: 14px;
cursor: pointer;
line-height: 1;
}
.mcs-pf-btn:hover {
background: #666;
}
.mcs-pf-minimize-btn {
padding: 4px 10px;
}
.mcs-pf-content {
padding: 10px 15px;
color: #e0e0e0;
font-size: 12px;
line-height: 1.6;
overflow-y: auto;
overflow-x: hidden;
height: calc(100% - 40px);
}
.mcs-pf-loading {
color: #999;
}
.mcs-pf-header-minimized {
border-radius: 6px;
}
.mcs-pf-summary {
margin-bottom: 10px;
padding-bottom: 10px;
border-bottom: 1px solid #444;
}
.mcs-pf-total {
font-size: 13px;
font-weight: bold;
color: #4CAF50;
margin-bottom: 5px;
}
.mcs-pf-items-count {
font-size: 11px;
color: #999;
}
.mcs-pf-section {
margin-bottom: 10px;
}
.mcs-pf-section-bordered {
padding-bottom: 10px;
border-bottom: 1px solid #444;
}
.mcs-pf-toggle-header {
display: flex;
justify-content: space-between;
align-items: center;
cursor: pointer;
padding: 4px;
border-radius: 3px;
background: rgba(255,255,255,0.03);
}
.mcs-pf-toggle-indicator {
color: #4CAF50;
font-weight: bold;
margin-right: 4px;
}
.mcs-pf-cpu-label {
color: #FF9800;
font-weight: bold;
}
.mcs-pf-io-label {
color: #2196F3;
font-weight: bold;
}
.mcs-pf-section-subtitle {
color: #999;
font-size: 10px;
margin-left: 4px;
}
.mcs-pf-section-details {
margin-left: 20px;
margin-top: 5px;
}
.mcs-pf-category-label {
color: #FFA500;
font-weight: bold;
}
.mcs-pf-item-count {
color: #999;
font-size: 10px;
margin-left: 4px;
}
.mcs-pf-category-total {
color: #4CAF50;
font-weight: bold;
}
.mcs-pf-category-percent {
color: #999;
font-size: 10px;
}
.mcs-pf-category-items {
margin-left: 20px;
margin-top: 5px;
font-size: 10px;
}
.mcs-pf-item-row {
display: flex;
justify-content: space-between;
align-items: center;
padding: 2px 0;
gap: 8px;
border-bottom: 1px solid rgba(255,255,255,0.05);
}
.mcs-pf-item-key-active {
color: #4CAF50;
white-space: nowrap;
}
.mcs-pf-item-key-inactive {
color: #ccc;
white-space: nowrap;
}
.mcs-pf-item-size {
color: #888;
white-space: nowrap;
margin-left: auto;
}
.mcs-pf-delete-btn {
background: #d32f2f;
color: white;
border: none;
border-radius: 3px;
padding: 2px 6px;
cursor: pointer;
font-size: 10px;
font-weight: bold;
flex-shrink: 0;
}
.mcs-pf-quota-container {
margin-top: 10px;
padding-top: 10px;
border-top: 1px solid #444;
}
.mcs-pf-quota-title {
font-size: 11px;
color: #999;
margin-bottom: 5px;
}
.mcs-pf-quota-details {
font-size: 10px;
color: #ccc;
}
.mcs-pf-stat-row {
display: flex;
justify-content: space-between;
padding: 3px 0;
font-size: 11px;
}
.mcs-pf-stat-label {
color: #ccc;
}
.mcs-pf-cpu-percent {
font-weight: bold;
}
.mcs-pf-stat-meta {
color: #666;
margin-left: 8px;
font-size: 9px;
}
.mcs-pf-no-activity {
color: #999;
font-size: 11px;
}
.mcs-pf-ops-count {
font-weight: bold;
}
.mcs-opanel-pane {
position: fixed;
top: 0;
left: 0;
width: 500px;
height: 550px;
background: #1a1a1a;
border: 1px solid #4a4a4a;
border-radius: 4px;
box-shadow: 0 4px 12px rgba(0,0,0,0.5);
z-index: 8000;
display: flex;
flex-direction: column;
overflow: hidden;
min-width: 10px;
min-height: 10px;
}
.mcs-opanel-icons-bg {
position: absolute;
top: 2px;
right: 2px;
width: 96px;
height: 24px;
background: rgba(0, 0, 0, 0.7);
border-radius: 3px;
z-index: 9;
opacity: 0;
transition: opacity 0.2s ease;
}
.mcs-opanel-icon {
position: absolute;
top: 2px;
background: transparent;
border: none;
color: #aaa;
cursor: pointer;
font-size: 16px;
padding: 4px;
display: flex;
align-items: center;
justify-content: center;
z-index: 10;
transition: opacity 0.2s ease, color 0.2s ease;
opacity: 0;
}
.mcs-opanel-icon:hover {
color: #fff;
}
.mcs-opanel-computer-icon {
right: 74px;
}
.mcs-opanel-lock-icon {
right: 50px;
}
.mcs-opanel-gear-icon {
right: 26px;
}
.mcs-opanel-drag-handle {
right: 2px;
cursor: move;
user-select: none;
}
.mcs-opanel-config-menu {
display: none;
position: fixed;
background: #2a2a2a;
border: 1px solid #4a4a4a;
border-radius: 4px;
padding: 8px;
z-index: 8001;
box-shadow: 0 2px 8px rgba(0,0,0,0.5);
}
.mcs-opanel-close-btn {
position: absolute;
top: 4px;
right: 4px;
width: 20px;
height: 20px;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
color: #aaa;
font-size: 20px;
line-height: 1;
border-radius: 2px;
user-select: none;
}
.mcs-opanel-close-btn:hover {
background: rgba(255,255,255,0.1);
color: #fff;
}
.mcs-opanel-import-export-panel {
display: none;
position: fixed;
background: #2a2a2a;
border: 1px solid #4a4a4a;
border-radius: 4px;
padding: 4px 8px;
z-index: 8001;
box-shadow: 0 2px 8px rgba(0,0,0,0.5);
align-items: center;
gap: 6px;
box-sizing: border-box;
}
.mcs-opanel-ie-title {
color: #aaa;
font-size: 11px;
font-weight: bold;
white-space: nowrap;
}
.mcs-opanel-ie-btn {
padding: 3px 10px;
background: #3a3a3a;
border: 1px solid #555;
border-radius: 3px;
color: #ddd;
font-size: 11px;
cursor: pointer;
white-space: nowrap;
}
.mcs-opanel-ie-btn:hover {
background: #4a4a4a;
color: #fff;
}
.mcs-opanel-options-container {
display: flex;
flex-wrap: wrap;
gap: 8px;
align-items: center;
padding-right: 24px;
}
.mcs-opanel-checkbox-container {
display: flex;
align-items: center;
gap: 4px;
background: rgba(255,255,255,0.05);
padding: 4px 8px;
border-radius: 3px;
white-space: nowrap;
}
.mcs-opanel-checkbox {
cursor: pointer;
}
.mcs-opanel-label {
color: #e0e0e0;
font-size: 10px;
cursor: pointer;
user-select: none;
}
.mcs-opanel-action-btn-container {
display: flex;
align-items: center;
background: rgba(144,238,144,0.1);
padding: 4px 8px;
border-radius: 3px;
}
.mcs-opanel-action-btn {
padding: 4px 12px;
background: #3a3a3a;
border: 1px solid #5a5a5a;
border-radius: 3px;
color: #90EE90;
font-size: 10px;
cursor: pointer;
font-weight: bold;
}
.mcs-opanel-action-btn:hover {
background: #4a4a4a;
}
.mcs-opanel-reset-btn-container {
display: flex;
align-items: center;
background: rgba(255,100,100,0.1);
padding: 4px 8px;
border-radius: 3px;
}
.mcs-opanel-reset-btn {
padding: 4px 12px;
background: #3a3a3a;
border: 1px solid #5a5a5a;
border-radius: 3px;
color: #ff6b6b;
font-size: 10px;
cursor: pointer;
font-weight: bold;
}
.mcs-opanel-reset-btn:hover {
background: #4a4a4a;
}
.mcs-opanel-content {
flex: 1 1 auto;
padding: 0;
color: #e0e0e0;
font-size: 11px;
overflow: hidden;
min-height: 0;
max-height: 100%;
position: relative;
}
.mcs-opanel-content-grid {
position: relative;
width: 100%;
min-height: 1000px;
height: auto;
}
.mcs-opanel-grid-shim {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 1000px;
pointer-events: none;
opacity: 0;
z-index: 0;
}
.mcs-opanel-grid-overlay {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 1000px;
min-height: 1000px;
background-image:
repeating-linear-gradient(0deg, transparent, transparent 9px, rgba(200, 200, 200, 0.3) 9px, rgba(200, 200, 200, 0.3) 10px),
repeating-linear-gradient(90deg, transparent, transparent 9px, rgba(200, 200, 200, 0.3) 9px, rgba(200, 200, 200, 0.3) 10px);
pointer-events: none;
display: none;
z-index: 1000;
}
.mcs-opanel-spacer-wrapper {
position: relative;
width: 0;
height: 0;
overflow: visible;
pointer-events: none;
}
.mcs-opanel-spacer-box {
position: absolute;
left: 0px;
width: 100px;
height: 100px;
background: transparent;
border: 1px solid transparent;
box-sizing: border-box;
padding: 8px;
pointer-events: none;
z-index: 0;
opacity: 0;
}
.mcs-opanel-spacer-content {
color: #555;
font-size: 10px;
line-height: 1;
white-space: pre;
}
.mcs-opanel-height-forcer {
position: absolute;
top: 0;
left: 0;
height: 1000px;
width: 1px;
visibility: hidden;
pointer-events: none;
}
.mcs-opanel-corner-handle {
position: absolute;
width: 15px;
height: 15px;
z-index: 8001;
}
.mcs-opanel-option-box {
position: absolute;
background: transparent;
font-family: Arial, sans-serif;
box-sizing: border-box;
resize: none;
overflow: hidden;
cursor: move;
border: 1px solid transparent;
border-top: 1px solid #4a4a4a;
z-index: 1;
display: block;
visibility: visible;
transition: background 0.2s, border-color 0.2s;
}
.mcs-opanel-box-padded {
padding: 4px 0 0 0;
}
.mcs-opanel-spacer {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
border: 1px solid transparent;
box-sizing: border-box;
pointer-events: none;
}
.mcs-opanel-widget-wrapper {
position: absolute;
top: 4px;
left: 0;
right: 0;
pointer-events: none;
}
.mcs-opanel-timer-display {
color: #ffffff;
font-size: 11px;
line-height: 1;
font-family: Arial, sans-serif;
pointer-events: none;
white-space: nowrap;
}
.mcs-opanel-revenue-display {
color: #e0e0e0;
font-size: 10px;
line-height: 1;
pointer-events: none;
overflow: visible;
}
.mcs-opanel-consumables-display {
color: #e0e0e0;
font-size: 10px;
line-height: 1;
pointer-events: auto;
overflow: visible;
}
.mcs-opanel-exp-display {
color: #90EE90;
font-size: 11px;
line-height: 1;
gap: 4px;
font-family: Arial, sans-serif;
pointer-events: none;
white-space: nowrap;
}
.mcs-opanel-profit-display {
color: #e0e0e0;
font-size: 11px;
line-height: 1;
gap: 4px;
font-family: Arial, sans-serif;
pointer-events: none;
display: flex;
flex-wrap: wrap;
align-items: center;
}
.mcs-opanel-dps-display {
color: #e0e0e0;
font-size: 10px;
line-height: 1;
pointer-events: none;
overflow: visible;
gap: 4px;
}
.mcs-opanel-over-expected-display {
color: #e0e0e0;
font-size: 10px;
line-height: 1;
gap: 4px;
pointer-events: none;
overflow: visible;
}
.mcs-opanel-luck-display {
color: #e0e0e0;
font-size: 10px;
line-height: 1;
gap: 4px;
pointer-events: none;
overflow: visible;
}
.mcs-opanel-deaths-display {
color: #FFA07A;
font-size: 11px;
line-height: 1;
gap: 4px;
font-family: Arial, sans-serif;
pointer-events: none;
white-space: nowrap;
}
.mcs-opanel-houses-display {
color: #e0e0e0;
font-size: 11px;
line-height: 1;
font-family: Arial, sans-serif;
pointer-events: none;
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 4px;
}
.mcs-opanel-ewatch-display {
color: #e0e0e0;
font-size: 10px;
line-height: 1;
gap: 4px;
pointer-events: none;
}
.mcs-opanel-combat-status-display {
color: #e0e0e0;
font-size: 11px;
line-height: 1;
font-family: Arial, sans-serif;
pointer-events: none;
white-space: nowrap;
font-weight: bold;
gap: 4px;
}
.mcs-opanel-ntally-display {
color: #e0e0e0;
font-size: 11px;
line-height: 1;
font-family: Arial, sans-serif;
pointer-events: none;
white-space: nowrap;
display: flex;
align-items: center;
gap: 4px;
}
.mcs-opanel-build-score-display {
color: #FFD700;
font-size: 11px;
line-height: 1;
font-family: Arial, sans-serif;
pointer-events: none;
white-space: nowrap;
font-weight: bold;
}
.mcs-opanel-net-worth-display {
color: #4CAF50;
font-size: 11px;
line-height: 1;
font-family: Arial, sans-serif;
pointer-events: none;
white-space: nowrap;
font-weight: bold;
}
.mcs-opanel-coins-display {
color: #e0e0e0;
font-size: 11px;
line-height: 1;
font-family: Arial, sans-serif;
pointer-events: none;
white-space: nowrap;
display: flex;
align-items: center;
gap: 4px;
}
.mcs-opanel-market-display {
color: #e0e0e0;
font-size: 11px;
line-height: 1;
font-family: Arial, sans-serif;
pointer-events: none;
white-space: nowrap;
display: flex;
align-items: center;
gap: 4px;
}
.mcs-opanel-skill-books-display {
color: #e0e0e0;
font-size: 11px;
line-height: 1;
font-family: Arial, sans-serif;
pointer-events: none;
white-space: nowrap;
display: flex;
align-items: center;
gap: 4px;
}
.mcs-opanel-treasure-display {
color: #e0e0e0;
font-size: 11px;
line-height: 1;
font-family: Arial, sans-serif;
pointer-events: none;
white-space: nowrap;
display: flex;
align-items: center;
gap: 4px;
}
.mcs-opanel-box-resize-handle {
position: absolute;
bottom: 0;
right: 0;
width: 15px;
height: 15px;
cursor: se-resize;
opacity: 0;
transition: opacity 0.2s;
z-index: 10;
pointer-events: all;
}
.mcs-opanel-zoom-controls {
position: absolute;
bottom: 2px;
left: 2px;
display: flex;
gap: 2px;
opacity: 0;
transition: opacity 0.2s;
z-index: 10;
pointer-events: all;
}
.mcs-opanel-zoom-btn {
background: #3a3a3a;
border: 1px solid #555;
color: #e0e0e0;
width: 18px;
height: 18px;
font-size: 14px;
cursor: pointer;
border-radius: 2px;
display: flex;
align-items: center;
justify-content: center;
padding: 0;
line-height: 1;
}
.mcs-opanel-zoom-btn:hover {
background: #4a4a4a;
}
`;
if (this.cssStyleElement) {
this.cssStyleElement.textContent += baseStyles;
}
}
insertRule(cssText) {
try {
if (!this.cssStyleSheet) {
this.initializeGlobalStyles();
}
this.cssStyleSheet.insertRule(cssText, this.cssStyleSheet.cssRules.length);
} catch (e) {
console.error('[CSS] Failed to insert rule:', e);
}
}
createClass(styles, baseName = 'dynamic') {
const styleKey = JSON.stringify(styles);
if (this.cssClassMap.has(styleKey)) {
return this.cssClassMap.get(styleKey);
}
const className = `mcs-${baseName}-${this.cssClassCounter++}`;
const cssText = this.stylesToCSS(styles, className);
this.insertRule(cssText);
this.cssClassMap.set(styleKey, className);
return className;
}
stylesToCSS(styles, className) {
const cssProperties = Object.entries(styles)
.map(([key, value]) => {
const cssKey = key.replace(/([A-Z])/g, '-$1').toLowerCase();
return ` ${cssKey}: ${value};`;
})
.join('\n');
return `.${className} {\n${cssProperties}\n}`;
}
applyClass(element, className) {
if (element && className) {
element.classList.add(className);
}
}
applyClasses(element, ...classNames) {
if (element) {
element.classList.add(...classNames.filter(Boolean));
}
}
removeAllStyles() {
const styleElement = document.getElementById('mcs-global-styles');
if (styleElement) {
styleElement.remove();
this.cssStyleSheet = null;
this.cssClassCounter = 0;
this.cssClassMap.clear();
}
}
// css end
// IHurt Start
get ihStorage() {
if (!this._ihStorage) {
this._ihStorage = createModuleStorage('IH');
}
return this._ihStorage;
}
createIHurtPane() {
if (document.getElementById('ihurt-pane')) return;
const pane = document.createElement('div');
pane.id = 'ihurt-pane';
registerPanel('ihurt-pane');
pane.className = 'mcs-pane mcs-ih-pane';
const header = document.createElement('div');
header.className = 'mcs-pane-header';
const titleSection = document.createElement('div');
titleSection.className = 'mcs-ih-title-section';
const title = document.createElement('span');
title.className = 'mcs-pane-title mcs-ih-title';
title.textContent = 'IHurt';
const timeDisplay = document.createElement('div');
timeDisplay.id = 'ihurt-time-display';
timeDisplay.className = 'mcs-ih-time-display';
timeDisplay.textContent = '00:00:00';
const deathsDisplay = document.createElement('div');
deathsDisplay.id = 'ihurt-deaths-per-hour';
deathsDisplay.className = 'mcs-ih-deaths-display';
deathsDisplay.textContent = '0.0 deaths/hr';
titleSection.appendChild(title);
titleSection.appendChild(timeDisplay);
titleSection.appendChild(deathsDisplay);
const minimizeBtn = document.createElement('button');
minimizeBtn.id = 'ihurt-minimize-btn';
minimizeBtn.textContent = '−';
minimizeBtn.className = 'mcs-btn';
header.appendChild(titleSection);
header.appendChild(minimizeBtn);
const content = document.createElement('div');
content.id = 'ihurt-content';
content.className = 'mcs-ih-content';
pane.appendChild(header);
pane.appendChild(content);
document.body.appendChild(pane);
const spyContent = document.getElementById('spy-content');
if (spyContent && !this._ihWheelListener) {
let isScrolling = false;
this._ihWheelListener = function (e) {
if (isScrolling) return;
e.preventDefault();
e.stopPropagation();
const scrollAmount = e.deltaY / 3;
isScrolling = true;
spyContent.scrollTop += scrollAmount;
setTimeout(() => {
isScrolling = false;
}, 10);
};
spyContent.addEventListener('wheel', this._ihWheelListener, { passive: false });
}
const self = this;
const handleWebSocketMessage = PerformanceMonitor.wrap('IHurt', (event) => {
const data = event.detail;
if (data?.type === 'battle_updated') {
self.handleIHurtBattleUpdate(data);
if (self.lastBattleData) {
self.lastBattleData.pMap = data.pMap;
self.lastBattleData.mMap = data.mMap;
}
}
if (data?.type === 'new_battle') {
self.initializeIHurtTracking(data.players, data.monsters);
self.lastBattleData = data;
if (data.combatStartTime) {
self.ihurtTracking.combatStartTime = new Date(data.combatStartTime).getTime() / 1000;
}
if (data.players) {
self.ihurtTracking.serverDeathCounts = data.players.map(p => p.deathCount ?? 0);
}
}
});
window.addEventListener('EquipSpyWebSocketMessage', handleWebSocketMessage);
if (window.lootDropsTrackerInstance && window.lootDropsTrackerInstance.lastBattleData) {
const battleData = window.lootDropsTrackerInstance.lastBattleData;
if (battleData.players && battleData.monsters) {
self.initializeIHurtTracking(battleData.players, battleData.monsters);
}
}
this.ihurtIsMinimized = false;
minimizeBtn.onclick = () => {
this.ihurtIsMinimized = !this.ihurtIsMinimized;
if (this.ihurtIsMinimized) {
content.style.display = 'none';
minimizeBtn.textContent = '+';
header.style.borderBottom = 'none';
header.style.borderRadius = '8px';
this.ihStorage.set('minimized', true);
} else {
content.style.display = 'block';
minimizeBtn.textContent = '−';
header.style.borderBottom = '1px solid rgba(255, 255, 255, 0.1)';
header.style.borderRadius = '';
this.ihStorage.set('minimized', false);
setTimeout(() => {
const rect = pane.getBoundingClientRect();
const winWidth = window.innerWidth;
const winHeight = window.innerHeight;
let needsAdjustment = false;
let newTop = rect.top;
let newLeft = rect.left;
if (rect.right > winWidth) {
newLeft = winWidth - rect.width - 10;
needsAdjustment = true;
}
if (rect.left < 0 || newLeft < 0) {
newLeft = 10;
needsAdjustment = true;
}
if (rect.bottom > winHeight) {
newTop = winHeight - rect.height - 10;
needsAdjustment = true;
}
if (rect.top < 0 || newTop < 0) {
newTop = 10;
needsAdjustment = true;
}
if (needsAdjustment) {
pane.style.top = newTop + 'px';
pane.style.left = newLeft + 'px';
pane.style.right = 'auto';
this.ihStorage.set('position', {
top: newTop,
left: newLeft
});
}
}, 50);
}
};
const savedPos = this.ihStorage.get('position');
if (savedPos) {
pane.style.top = savedPos.top + 'px';
pane.style.left = savedPos.left + 'px';
pane.style.right = 'auto';
}
DragHandler.makeDraggable(pane, header, 'mcs_IH');
const savedIHurtMinimized = this.ihStorage.get('minimized');
this.ihurtIsMinimized = savedIHurtMinimized === true || savedIHurtMinimized === 'true';
if (this.ihurtIsMinimized) {
content.style.display = 'none';
minimizeBtn.textContent = '+';
header.style.borderBottom = 'none';
header.style.borderRadius = '8px';
}
this.ihurtTracking = {
players: [],
playerDamageTaken: [],
playerHealthRegen: [],
enemyDamageToPlayers: new Map(),
startTime: null,
endTime: null,
lastUpdateTime: null,
totalDuration: 0,
playersHP: [],
enemyAttackLog: [],
maxLogEntries: 100,
expandedEnemies: new Set(),
expandedProfiles: new Set(),
savedPausedMs: window.MCS_TOTAL_PAUSED_MS ?? 0
};
this.updateIHurtContent();
}
initializeIHurtTracking(players, monsters) {
if (!this.ihurtTracking.players || this.ihurtTracking.players.length === 0) {
this.ihurtTracking.players = players.map(p => p.name);
this.ihurtTracking.playerDamageTaken = new Array(players.length).fill(0);
this.ihurtTracking.playerHealthRegen = new Array(players.length).fill(0);
this.ihurtTracking.playerDeaths = new Array(players.length).fill(0);
this.ihurtTracking.enemyDamageToPlayers = new Map();
this.ihurtTracking.enemyHitStats = new Map();
this.ihurtTracking.damageProfiles = new Map();
this.ihurtTracking.startTime = Date.now();
this.ihurtTracking.totalDuration = 0;
this.ihurtTracking.enemyDamageExpanded = true;
this.ihurtTracking.profilesExpanded = true;
this.ihurtTracking.expandedEnemies = new Set();
this.ihurtTracking.expandedProfiles = new Set();
this.ihurtTracking.encounterCount = 0;
} else {
if (this.ihurtTracking.startTime) {
this.ihurtTracking.totalDuration = mcsGetElapsedSeconds(
this.ihurtTracking.startTime,
this.ihurtTracking.endTime,
this.ihurtTracking.savedPausedMs,
this.ihurtTracking.totalDuration
);
}
this.ihurtTracking.savedPausedMs = window.MCS_TOTAL_PAUSED_MS ?? 0;
this.ihurtTracking.startTime = Date.now();
}
const profileKey = this.createProfileKey(monsters);
if (!this.ihurtTracking.damageProfiles.has(profileKey)) {
this.ihurtTracking.damageProfiles.set(profileKey, {
composition: profileKey,
encounters: 0,
totalDamage: new Array(players.length).fill(0),
minHit: Infinity,
maxHit: 0,
playerMinHits: new Array(players.length).fill(Infinity),
playerMaxHits: new Array(players.length).fill(0)
});
}
this.ihurtTracking.damageProfiles.get(profileKey).encounters++;
if (this.ihurtTracking.encounterCount === undefined) this.ihurtTracking.encounterCount = 0;
this.ihurtTracking.encounterCount++;
this.ihurtTracking.currentProfile = profileKey;
monsters.forEach(monster => {
const enemyName = monster.name || 'Unknown Enemy';
if (!this.ihurtTracking.enemyDamageToPlayers.has(enemyName)) {
this.ihurtTracking.enemyDamageToPlayers.set(enemyName, new Array(players.length).fill(0));
}
if (!this.ihurtTracking.enemyHitStats.has(enemyName)) {
this.ihurtTracking.enemyHitStats.set(enemyName, {
minHit: Infinity,
maxHit: 0,
playerMinHits: new Array(players.length).fill(Infinity),
playerMaxHits: new Array(players.length).fill(0)
});
}
});
this.ihurtTracking.monsters = monsters;
this.ihurtTracking.playersHP = undefined;
this.ihurtTracking.monstersMP = undefined;
this.ihurtTracking.monstersDmgCounter = undefined;
this.ihurtTracking.playersDmgCounter = undefined;
this.ihurtTracking.endTime = null;
this.ihurtTracking.lastUpdateTime = Date.now();
}
createProfileKey(monsters) {
const enemyCounts = {};
monsters.forEach(monster => {
const name = monster.name || 'Unknown';
enemyCounts[name] = (enemyCounts[name] ?? 0) + 1;
});
const parts = Object.keys(enemyCounts)
.sort()
.map(name => {
const count = enemyCounts[name];
return count > 1 ? `${name} x${count}` : name;
});
return parts.join(' + ');
}
handleIHurtBattleUpdate(data) {
if (!this.ihurtTracking.players || this.ihurtTracking.players.length === 0) {
return;
}
const pMap = data.pMap;
const mMap = data.mMap;
const currentTime = Date.now();
if (!pMap || !mMap) {
return;
}
if (!this.ihurtTracking.monstersMP || !this.ihurtTracking.monstersDmgCounter) {
this.ihurtTracking.monstersMP = [];
this.ihurtTracking.monstersDmgCounter = [];
for (const mIndex of Object.keys(mMap)) {
this.ihurtTracking.monstersMP[mIndex] = mMap[mIndex].cMP;
this.ihurtTracking.monstersDmgCounter[mIndex] = mMap[mIndex].dmgCounter ?? 0;
}
}
if (!this.ihurtTracking.playersHP || !this.ihurtTracking.playersDmgCounter) {
this.ihurtTracking.playersHP = [];
this.ihurtTracking.playersDmgCounter = [];
for (const pIndex of Object.keys(pMap)) {
this.ihurtTracking.playersHP[pIndex] = pMap[pIndex].cHP;
this.ihurtTracking.playersDmgCounter[pIndex] = pMap[pIndex].dmgCounter ?? 0;
}
}
let castMonster = -1;
Object.keys(mMap).forEach((mIndex) => {
const monster = mMap[mIndex];
if (!monster) return;
const prevMP = this.ihurtTracking.monstersMP[mIndex];
if (prevMP !== undefined && monster.cMP < prevMP) {
castMonster = mIndex;
}
this.ihurtTracking.monstersMP[mIndex] = monster.cMP;
});
Object.keys(pMap).forEach(pIndex => {
const player = pMap[pIndex];
if (!player) return;
const prevHP = this.ihurtTracking.playersHP[pIndex];
if (prevHP === undefined) {
this.ihurtTracking.playersHP[pIndex] = player.cHP;
return;
}
const hpDiff = prevHP - player.cHP;
if (prevHP > 0 && player.cHP === 0) {
if (!this.ihurtTracking.playerDeaths) {
this.ihurtTracking.playerDeaths = new Array(this.ihurtTracking.players.length).fill(0);
}
this.ihurtTracking.playerDeaths[pIndex]++;
}
let dmgSplat = false;
const prevDmgCounter = this.ihurtTracking.playersDmgCounter[pIndex] || 0;
const currentDmgCounter = player.dmgCounter ?? 0;
if (currentDmgCounter > prevDmgCounter) {
dmgSplat = true;
}
this.ihurtTracking.playersDmgCounter[pIndex] = currentDmgCounter;
if (dmgSplat && hpDiff > 0) {
if (!this.ihurtTracking.playerDamageTaken[pIndex]) {
this.ihurtTracking.playerDamageTaken[pIndex] = 0;
}
this.ihurtTracking.playerDamageTaken[pIndex] += hpDiff;
let monsterName;
if (this.ihurtTracking.monsters && this.ihurtTracking.monsters.length > 0) {
if (castMonster !== -1 && this.ihurtTracking.monsters[castMonster]) {
monsterName = this.ihurtTracking.monsters[castMonster].name || `Monster ${parseInt(castMonster) + 1}`;
}
else if (this.ihurtTracking.monsters.length === 1) {
monsterName = this.ihurtTracking.monsters[0].name || 'Monster 1';
}
else {
let attackingMonster = -1;
Object.keys(mMap).forEach((mIndex) => {
const monster = mMap[mIndex];
if (!monster) return;
const prevMonsterDmgCounter = this.ihurtTracking.monstersDmgCounter[mIndex] || 0;
const currentMonsterDmgCounter = monster.dmgCounter ?? 0;
if (currentMonsterDmgCounter > prevMonsterDmgCounter) {
attackingMonster = mIndex;
}
this.ihurtTracking.monstersDmgCounter[mIndex] = currentMonsterDmgCounter;
});
if (attackingMonster !== -1 && this.ihurtTracking.monsters[attackingMonster]) {
monsterName = this.ihurtTracking.monsters[attackingMonster].name || `Monster ${parseInt(attackingMonster) + 1}`;
} else {
monsterName = 'Unknown Enemy';
}
}
if (monsterName) {
if (!this.ihurtTracking.enemyDamageToPlayers.has(monsterName)) {
this.ihurtTracking.enemyDamageToPlayers.set(monsterName, new Array(this.ihurtTracking.players.length).fill(0));
}
const damages = this.ihurtTracking.enemyDamageToPlayers.get(monsterName);
if (damages && damages[pIndex] !== undefined) {
damages[pIndex] = (damages[pIndex] ?? 0) + hpDiff;
}
if (!this.ihurtTracking.enemyHitStats.has(monsterName)) {
this.ihurtTracking.enemyHitStats.set(monsterName, {
minHit: Infinity,
maxHit: 0,
playerMinHits: new Array(this.ihurtTracking.players.length).fill(Infinity),
playerMaxHits: new Array(this.ihurtTracking.players.length).fill(0)
});
}
const hitStats = this.ihurtTracking.enemyHitStats.get(monsterName);
hitStats.minHit = Math.min(hitStats.minHit, hpDiff);
hitStats.maxHit = Math.max(hitStats.maxHit, hpDiff);
hitStats.playerMinHits[pIndex] = Math.min(hitStats.playerMinHits[pIndex], hpDiff);
hitStats.playerMaxHits[pIndex] = Math.max(hitStats.playerMaxHits[pIndex], hpDiff);
if (this.ihurtTracking.currentProfile && this.ihurtTracking.damageProfiles.has(this.ihurtTracking.currentProfile)) {
const profile = this.ihurtTracking.damageProfiles.get(this.ihurtTracking.currentProfile);
profile.totalDamage[pIndex] = (profile.totalDamage[pIndex] ?? 0) + hpDiff;
profile.minHit = Math.min(profile.minHit, hpDiff);
profile.maxHit = Math.max(profile.maxHit, hpDiff);
profile.playerMinHits[pIndex] = Math.min(profile.playerMinHits[pIndex], hpDiff);
profile.playerMaxHits[pIndex] = Math.max(profile.playerMaxHits[pIndex], hpDiff);
}
let attackingMonsterIndex = castMonster;
if (attackingMonsterIndex === -1) {
Object.keys(mMap).forEach((mIndex) => {
const monster = mMap[mIndex];
if (!monster) return;
const prevDmg = this.ihurtTracking.monstersDmgCounter[mIndex] || 0;
if ((monster.dmgCounter ?? 0) > prevDmg) {
attackingMonsterIndex = mIndex;
}
});
}
let abilityName = 'Unknown';
if (attackingMonsterIndex !== -1 && mMap[attackingMonsterIndex]) {
const monsterData = mMap[attackingMonsterIndex];
if (monsterData.preparingAbilityHrid) {
abilityName = monsterData.preparingAbilityHrid.split('/').pop().replace(/_/g, ' ');
} else if (monsterData.isPreparingAutoAttack) {
abilityName = 'Auto Attack';
}
}
const playerName = this.ihurtTracking.players[pIndex] || `Player ${parseInt(pIndex) + 1}`;
const isMiss = hpDiff === 0;
this.addIHurtAttackEntry(monsterName, playerName, abilityName, hpDiff, false, isMiss);
}
}
}
if (hpDiff < 0) {
if (!this.ihurtTracking.playerHealthRegen[pIndex]) {
this.ihurtTracking.playerHealthRegen[pIndex] = 0;
}
this.ihurtTracking.playerHealthRegen[pIndex] += Math.abs(hpDiff);
}
this.ihurtTracking.playersHP[pIndex] = player.cHP;
});
this.ihurtTracking.endTime = currentTime;
this.ihurtTracking.lastUpdateTime = currentTime;
this.updateIHurtContent();
}
addIHurtAttackEntry(enemyName, playerName, abilityName, damage, isCrit, isMiss) {
const timestamp = new Date().toLocaleTimeString();
this.ihurtTracking.enemyAttackLog.push({
timestamp,
enemyName,
playerName,
abilityName,
damage,
isCrit,
isMiss
});
if (this.ihurtTracking.enemyAttackLog.length > this.ihurtTracking.maxLogEntries) {
this.ihurtTracking.enemyAttackLog.shift();
}
this.updateIHurtContent();
}
updateIHurtContent() {
const content = document.getElementById('ihurt-content');
if (!content) return;
const tracking = this.ihurtTracking;
if (!tracking.players || tracking.players.length === 0) {
content.innerHTML = `<div class="mcs-ih-waiting">
<div class="mcs-ih-waiting-icon">⏳</div>
<div>Waiting for combat data</div>
<div class="mcs-ih-waiting-sub">IHurt will begin tracking shortly </div>
</div>`;
return;
}
let elapsedSeconds = tracking.startTime
? mcsGetElapsedSeconds(tracking.startTime, tracking.endTime, tracking.savedPausedMs, tracking.totalDuration || 0)
: (tracking.totalDuration || 0);
if (elapsedSeconds === 0) elapsedSeconds = 1;
const elapsedHours = elapsedSeconds / 3600;
let totalPartyDeaths = 0;
if (tracking.playerDeaths) {
totalPartyDeaths = tracking.playerDeaths.reduce((sum, deaths) => sum + deaths, 0);
}
const timeDisplay = document.getElementById('ihurt-time-display');
if (timeDisplay) {
const timeStr = mcsFormatDuration(elapsedSeconds, 'clock');
if (timeDisplay.textContent !== timeStr) {
timeDisplay.textContent = timeStr;
}
}
const headerServerDuration = tracking.combatStartTime ? (Date.now() / 1000 - tracking.combatStartTime) : 0;
const headerServerDeaths = (tracking.serverDeathCounts ?? []).reduce((s, d) => s + d, 0);
const headerDph = headerServerDuration > 0 ? (3600 * headerServerDeaths / headerServerDuration).toFixed(1) : '0.0';
const deathsPerHourSpan = document.getElementById('ihurt-deaths-per-hour');
if (deathsPerHourSpan) {
const deathsText = `${headerDph} deaths/hr`;
if (deathsPerHourSpan.textContent !== deathsText) {
deathsPerHourSpan.textContent = deathsText;
}
}
let html = '';
html += `<div class="mcs-ih-main">`;
const deathParts = tracking.players.map((name, i) => {
const d = tracking.playerDeaths ? (tracking.playerDeaths[i] ?? 0) : 0;
return `<span class="mcs-ih-player-name">${name}:</span> <span class="mcs-ih-death-count">${d}</span>`;
});
html += `<div class="mcs-ih-deaths-row">`;
html += `<span class="mcs-ih-deaths-label">Session Deaths:</span>`;
html += deathParts.join(`<span class="mcs-ih-separator"> | </span>`);
html += `<span class="mcs-ih-separator"> | </span><span class="mcs-ih-total-label">Total:</span> <span class="mcs-ih-death-count">${totalPartyDeaths}</span>`;
html += `</div>`;
const serverDuration = tracking.combatStartTime ? (Date.now() / 1000 - tracking.combatStartTime) : 0;
const serverDeathCounts = tracking.serverDeathCounts ?? [];
let totalServerDeaths = 0;
serverDeathCounts.forEach(d => { totalServerDeaths += d; });
const serverTotalDph = serverDuration > 0 ? (3600 * totalServerDeaths / serverDuration).toFixed(1) : '0.0';
const deathHrParts = tracking.players.map((name, i) => {
const d = serverDeathCounts[i] ?? 0;
const dhr = serverDuration > 0 ? (3600 * d / serverDuration).toFixed(1) : '0.0';
return `<span class="mcs-ih-player-name">${name}:</span> <span class="mcs-ih-dph-value">${dhr}</span>`;
});
html += `<div class="mcs-ih-dph-row">`;
html += `<span class="mcs-ih-deaths-label">Session Deaths/hr:</span>`;
html += deathHrParts.join(`<span class="mcs-ih-separator"> | </span>`);
html += `<span class="mcs-ih-separator"> | </span><span class="mcs-ih-total-label">Total:</span> <span class="mcs-ih-dph-total">${serverTotalDph}</span>`;
html += `</div>`;
html += `<div class="mcs-ih-player-grid">`;
tracking.players.forEach((playerName, index) => {
const damageTaken = tracking.playerDamageTaken[index] ?? 0;
const healthRegen = tracking.playerHealthRegen[index] ?? 0;
const deaths = tracking.playerDeaths ? (tracking.playerDeaths[index] ?? 0) : 0;
const dps = (damageTaken / elapsedSeconds).toFixed(1);
const hps = (healthRegen / elapsedSeconds).toFixed(1);
const deathsPerHour = elapsedHours > 0 ? (deaths / elapsedHours).toFixed(1) : '0.0';
html += `<div class="mcs-ih-player-card">`;
html += `<div class="mcs-ih-player-card-name">${playerName}</div>`;
html += `<div class="mcs-ih-player-stats">`;
html += `<div class="mcs-ih-stat-row">`;
html += `<span class="mcs-ih-stat-label">Damage/s:</span>`;
html += `<span class="mcs-ih-damage-value">${dps}</span>`;
html += `</div>`;
html += `<div class="mcs-ih-stat-row">`;
html += `<span class="mcs-ih-stat-label">Regen/s:</span>`;
html += `<span class="mcs-ih-regen-value">${hps}</span>`;
html += `</div>`;
html += `<div class="mcs-ih-stat-row mcs-ih-total-section">`;
html += `<span class="mcs-ih-total-label-sm">Total dmg:</span>`;
html += `<span class="mcs-ih-total-dmg">${this.formatIHurtNumber(damageTaken)}</span>`;
html += `</div>`;
html += `<div class="mcs-ih-stat-row">`;
html += `<span class="mcs-ih-total-label-sm">Total regen:</span>`;
html += `<span class="mcs-ih-total-regen">${this.formatIHurtNumber(healthRegen)}</span>`;
html += `</div>`;
html += `<div class="mcs-ih-death-section">`;
html += `<div class="mcs-ih-stat-row">`;
html += `<span class="mcs-ih-total-label-sm">Deaths:</span>`;
html += `<span class="mcs-ih-death-count-sm">${deaths}</span>`;
html += `</div>`;
html += `<div class="mcs-ih-stat-row">`;
html += `<span class="mcs-ih-total-label-sm">Deaths/hr:</span>`;
html += `<span class="mcs-ih-dph-value-sm">${deathsPerHour}</span>`;
html += `</div>`;
html += `</div>`;
html += `</div>`;
html += `</div>`;
});
html += `</div>`;
html += `</div>`;
if (tracking.enemyDamageToPlayers.size > 0) {
let totalEnemyDamageDealt = 0;
tracking.enemyDamageToPlayers.forEach((damages) => {
totalEnemyDamageDealt += damages.reduce((sum, dmg) => sum + dmg, 0);
});
const isExpanded = tracking.enemyDamageExpanded !== false;
html += `<div class="mcs-ih-section">`;
html += `<div class="ihurt-enemy-toggle mcs-ih-enemy-toggle">`;
html += `<div class="mcs-ih-toggle-left">`;
html += `<span class="mcs-ih-toggle-arrow mcs-ih-toggle-arrow-red">${isExpanded ? '▼' : '▶'}</span>`;
html += `<span class="mcs-ih-section-label">Enemy Damage to Party:</span>`;
html += `</div>`;
html += `<span class="mcs-ih-enemy-total">Total: ${this.formatIHurtNumber(totalEnemyDamageDealt)}</span>`;
html += `</div>`;
html += `<div class="mcs-ih-enemy-grid" style="display: ${isExpanded ? 'grid' : 'none'};">`;
const sortedEnemies = Array.from(tracking.enemyDamageToPlayers.entries()).sort((a, b) => {
const totalA = a[1].reduce((sum, dmg) => sum + dmg, 0);
const totalB = b[1].reduce((sum, dmg) => sum + dmg, 0);
return totalB - totalA;
});
sortedEnemies.forEach(([enemyName, damages]) => {
const totalDamage = damages.reduce((sum, dmg) => sum + dmg, 0);
const hitStats = tracking.enemyHitStats.get(enemyName);
html += `<div class="mcs-ih-enemy-card">`;
html += `<div class="mcs-ih-enemy-name">${enemyName}</div>`;
html += `<div class="mcs-ih-enemy-total-dmg">Total: ${this.formatIHurtNumber(totalDamage)}</div>`;
if (hitStats && hitStats.minHit !== Infinity && hitStats.maxHit > 0) {
html += `<div class="mcs-ih-enemy-range">Range: [${hitStats.minHit}-${hitStats.maxHit}]</div>`;
}
html += `<div class="mcs-ih-enemy-players">`;
damages.forEach((dmg, pIndex) => {
if (dmg > 0) {
const playerName = tracking.players[pIndex] || `Player ${parseInt(pIndex) + 1}`;
const playerMinHit = hitStats?.playerMinHits[pIndex];
const playerMaxHit = hitStats?.playerMaxHits[pIndex];
html += `<div class="mcs-ih-enemy-player-row">`;
html += `<span class="mcs-ih-enemy-player-name">${playerName}:</span>`;
html += `<span class="mcs-ih-enemy-player-dmg">${this.formatIHurtNumber(dmg)}</span>`;
if (playerMinHit !== undefined && playerMaxHit !== undefined && playerMinHit !== Infinity) {
html += `<span class="mcs-ih-enemy-player-range">[${playerMinHit}-${playerMaxHit}]</span>`;
}
html += `</div>`;
}
});
html += `</div>`;
html += `</div>`;
});
html += `</div>`;
html += `</div>`;
}
if (tracking.damageProfiles && tracking.damageProfiles.size > 0) {
const isExpanded = tracking.profilesExpanded !== false;
html += `<div class="mcs-ih-section">`;
html += `<div class="ihurt-profiles-toggle mcs-ih-section-toggle">`;
html += `<span class="mcs-ih-toggle-arrow mcs-ih-toggle-arrow-gold">${isExpanded ? '▼' : '▶'}</span>`;
html += `<span class="mcs-ih-section-label">Damage Profiles (Enemy Group Compositions):</span>`;
html += `</div>`;
html += `<div style="display: ${isExpanded ? 'block' : 'none'};">`;
const sortedProfiles = Array.from(tracking.damageProfiles.entries()).sort((a, b) => {
const totalA = a[1].totalDamage.reduce((sum, dmg) => sum + dmg, 0);
const totalB = b[1].totalDamage.reduce((sum, dmg) => sum + dmg, 0);
const avgA = a[1].encounters > 0 ? (totalA / a[1].encounters) : 0;
const avgB = b[1].encounters > 0 ? (totalB / b[1].encounters) : 0;
return avgB - avgA;
});
sortedProfiles.forEach(([profileKey, profile]) => {
const totalDamage = profile.totalDamage.reduce((sum, dmg) => sum + dmg, 0);
const avgDamagePerEncounter = profile.encounters > 0 ? (totalDamage / profile.encounters) : 0;
html += `<div class="mcs-ih-profile-card">`;
html += `<div class="mcs-ih-profile-name">${profileKey}</div>`;
html += `<div class="mcs-ih-profile-stats">`;
html += `Encounters: ${profile.encounters} | Total Damage: ${this.formatIHurtNumber(totalDamage)} | Avg/Encounter: <span style="color:red">${this.formatIHurtNumber(avgDamagePerEncounter)}</span>`;
if (profile.minHit !== Infinity && profile.maxHit > 0) {
html += ` | Hit Range: [${profile.minHit}-${profile.maxHit}]`;
}
html += `</div>`;
html += `</div>`;
});
html += `</div>`;
html += `</div>`;
}
const enemyKillEntries = this.trueDPSTracking?.enemyKills ? Object.entries(this.trueDPSTracking.enemyKills) : [];
if (enemyKillEntries.length > 0) {
const encounterKillsExpanded = tracking.encounterKillsExpanded !== false;
const battleCount = tracking.encounterCount ?? 0;
let totalActualKills = 0;
for (const [, e] of enemyKillEntries) { totalActualKills += e.kills; }
const expectedKillsPerName = {};
try {
if (typeof LuckyGameData !== 'undefined' && LuckyGameData.currentMapHrid && typeof LuckyDropAnalyzer !== 'undefined') {
const mapData = LuckyGameData.mapData[LuckyGameData.currentMapHrid];
if (mapData && mapData.spawnInfo && mapData.spawnInfo.spawns && mapData.spawnInfo.spawns.length > 0) {
const expectedSpawns = LuckyDropAnalyzer.computeExpectedSpawns(mapData.spawnInfo);
const monsterDetailMap = InitClientDataCache.getCombatMonsterDetailMap();
let bossWave = mapData.spawnInfo.bossWave ?? 0;
if (!bossWave && mapData.type === 'dungeon') {
bossWave = 1;
} else if (!bossWave && mapData.type === 'group' && mapData.bossDrops && Object.keys(mapData.bossDrops).length > 0) {
bossWave = 10;
}
const bossCount = bossWave ? Math.floor((battleCount - 1) / bossWave) : 0;
const normalCount = bossWave ?
bossCount * (bossWave - 1) + (battleCount - 1) % bossWave :
battleCount - 1;
for (const [hrid, countPerEncounter] of Object.entries(expectedSpawns)) {
const monsterDetail = monsterDetailMap[hrid];
const name = monsterDetail ? monsterDetail.name : hrid.split('/').pop().replace(/_/g, ' ');
expectedKillsPerName[name] = (expectedKillsPerName[name] ?? 0) + countPerEncounter * normalCount;
}
if (mapData.bossDrops && bossCount > 0) {
for (const bossHrid of Object.keys(mapData.bossDrops)) {
if (bossHrid === '_dungeon') continue;
const monsterDetail = monsterDetailMap[bossHrid];
const name = monsterDetail ? monsterDetail.name : bossHrid.split('/').pop().replace(/_/g, ' ');
expectedKillsPerName[name] = (expectedKillsPerName[name] ?? 0) + bossCount;
}
}
}
}
} catch (e) { /* silently ignore if Lucky not available */ }
let totalExpectedKills = 0;
Object.values(expectedKillsPerName).forEach(v => { totalExpectedKills += v; });
const hasExpected = totalExpectedKills > 0;
html += `<div class="mcs-ih-section">`;
html += `<div class="ihurt-encounters-toggle mcs-ih-enemy-toggle">`;
html += `<div class="mcs-ih-toggle-left">`;
html += `<span class="mcs-ih-toggle-arrow mcs-ih-toggle-arrow-blue">${encounterKillsExpanded ? '▼' : '▶'}</span>`;
html += `<span class="mcs-ih-section-label">Encounters & Kills:</span>`;
html += `</div>`;
html += `<span class="mcs-ih-encounters-total">Encounters: ${battleCount} | Kills: ${totalActualKills}</span>`;
html += `</div>`;
html += `<div style="display: ${encounterKillsExpanded ? 'block' : 'none'};">`;
const sortedKills = enemyKillEntries.sort((a, b) => b[1].kills - a[1].kills);
html += `<div class="mcs-ih-kills-grid">`;
sortedKills.forEach(([enemyName, data]) => {
const expected = expectedKillsPerName[enemyName] ?? 0;
const diffColor = data.kills >= expected ? '#4CAF50' : '#F44336';
const percentDiff = expected > 0 ? (((data.kills - expected) / expected) * 100).toFixed(1) : '';
const percentStr = percentDiff !== '' ? ` (${data.kills >= expected ? '+' : ''}${percentDiff}%)` : '';
html += `<div class="mcs-ih-kill-card">`;
html += `<div class="mcs-ih-kill-name">${enemyName}</div>`;
html += `<div class="mcs-ih-kill-stats">`;
html += `<span class="mcs-ih-kill-actual">Actual: <span style="font-weight: bold;">${data.kills}</span></span>`;
if (hasExpected) {
html += `<span class="mcs-ih-kill-expected">Expected: ${expected.toFixed(1)}</span>`;
html += `<span style="color: ${diffColor}; font-weight: bold;">${percentStr}</span>`;
}
html += `</div>`;
html += `</div>`;
});
if (hasExpected) {
for (const [name, expected] of Object.entries(expectedKillsPerName)) {
if (!this.trueDPSTracking.enemyKills[name] && expected > 0) {
html += `<div class="mcs-ih-kill-card-dim">`;
html += `<div class="mcs-ih-kill-name">${name}</div>`;
html += `<div class="mcs-ih-kill-stats">`;
html += `<span class="mcs-ih-kill-actual">Actual: <span style="font-weight: bold;">0</span></span>`;
html += `<span class="mcs-ih-kill-expected">Expected: ${expected.toFixed(1)}</span>`;
html += `<span style="color: #F44336; font-weight: bold;">(-100.0%)</span>`;
html += `</div>`;
html += `</div>`;
}
}
}
html += `</div>`;
html += `</div>`;
html += `</div>`;
}
content.innerHTML = html;
const enemyToggle = content.querySelector('.ihurt-enemy-toggle');
if (enemyToggle) {
enemyToggle.addEventListener('click', () => {
this.ihurtTracking.enemyDamageExpanded = !this.ihurtTracking.enemyDamageExpanded;
this.updateIHurtContent();
});
}
const profilesToggle = content.querySelector('.ihurt-profiles-toggle');
if (profilesToggle) {
profilesToggle.addEventListener('click', () => {
this.ihurtTracking.profilesExpanded = !this.ihurtTracking.profilesExpanded;
this.updateIHurtContent();
});
}
const encountersToggle = content.querySelector('.ihurt-encounters-toggle');
if (encountersToggle) {
encountersToggle.addEventListener('click', () => {
this.ihurtTracking.encounterKillsExpanded = !this.ihurtTracking.encounterKillsExpanded;
this.updateIHurtContent();
});
}
}
toggleIHurtEnemySection() {
if (!this.ihurtTracking.expandedEnemies) {
this.ihurtTracking.expandedEnemies = new Set();
}
if (this.ihurtTracking.expandedEnemies.has('enemy-damage')) {
this.ihurtTracking.expandedEnemies.delete('enemy-damage');
} else {
this.ihurtTracking.expandedEnemies.add('enemy-damage');
}
this.updateIHurtContent();
}
toggleIHurtProfilesSection() {
if (!this.ihurtTracking.expandedProfiles) {
this.ihurtTracking.expandedProfiles = new Set();
}
if (this.ihurtTracking.expandedProfiles.has('damage-profiles')) {
this.ihurtTracking.expandedProfiles.delete('damage-profiles');
} else {
this.ihurtTracking.expandedProfiles.add('damage-profiles');
}
this.updateIHurtContent();
}
formatIHurtNumber(num) {
return mcsFormatCurrency(num, 'ihurt');
}
// IHurt End
get gwStorage() {
if (!this._gwStorage) {
this._gwStorage = createModuleStorage('GW');
}
return this._gwStorage;
}
gwhizHandleCombatEnded() {
this.gwhizExpTracking = {};
this.updateGWhizExperience();
}
gwhizHandleWebSocketMessage(event) {
if (window.MCS_MODULES_DISABLED) return;
const data = event.detail;
if (data?.type === 'action_completed' && data.endCharacterSkills) {
for (const skill of data.endCharacterSkills) {
this.gwhizCurrentStatExp[skill.skillHrid] = {
level: skill.level,
experience: skill.experience,
skillHrid: skill.skillHrid
};
}
try {
const charData = CharacterDataStorage.get();
if (charData) {
if (charData.characterSkills) {
for (const skill of data.endCharacterSkills) {
const existingSkill = charData.characterSkills.find(s => s.skillHrid === skill.skillHrid);
if (existingSkill) {
existingSkill.level = skill.level;
existingSkill.experience = skill.experience;
}
}
CharacterDataStorage.set(charData);
}
}
} catch (e) {
console.error('[GWhiz] Failed to update localStorage:', e);
}
setTimeout(() => this.updateGWhizExperience(), 100);
}
if (data?.type === 'init_character_data' && data.characterSkills) {
this.gwhizExpTracking = {};
for (const skill of data.characterSkills) {
this.gwhizCurrentStatExp[skill.skillHrid] = {
level: skill.level,
experience: skill.experience,
skillHrid: skill.skillHrid
};
}
setTimeout(() => this.updateGWhizExperience(), 100);
}
if (data?.type === 'new_battle') {
if (data.combatStartTime) {
this.gwhizCombatStartTime = new Date(data.combatStartTime).getTime();
}
const playerData = data.players?.[0];
if (playerData?.totalSkillExperienceMap && typeof playerData.totalSkillExperienceMap === 'object') {
this.gwhizSessionExpMap = { ...playerData.totalSkillExperienceMap };
this.gwhizSessionExpSnapshot = {};
for (const skillHrid in this.gwhizCurrentStatExp) {
this.gwhizSessionExpSnapshot[skillHrid] = this.gwhizCurrentStatExp[skillHrid].experience;
}
}
}
if (data?.characterSkills && data.type !== 'init_character_data' && data.type !== 'action_completed') {
for (const skill of data.characterSkills) {
this.gwhizCurrentStatExp[skill.skillHrid] = {
level: skill.level,
experience: skill.experience,
skillHrid: skill.skillHrid
};
}
setTimeout(() => this.updateGWhizExperience(), 100);
}
}
createGWhizPane() {
if (document.getElementById('gwhiz-pane')) return;
const pane = document.createElement('div');
pane.id = 'gwhiz-pane';
registerPanel('gwhiz-pane');
pane.className = 'mcs-pane mcs-gw-pane';
const header = document.createElement('div');
header.className = 'mcs-pane-header';
const leftSection = document.createElement('div');
leftSection.className = 'mcs-gw-left-section';
const titleSpan = document.createElement('span');
titleSpan.className = 'mcs-pane-title';
titleSpan.textContent = 'GWhiz';
const totalExpSpan = document.createElement('span');
totalExpSpan.id = 'gwhiz-total-exp-hr';
totalExpSpan.className = 'mcs-gw-total-exp';
totalExpSpan.textContent = '0 exp/hr';
leftSection.appendChild(titleSpan);
leftSection.appendChild(totalExpSpan);
const buttonSection = document.createElement('div');
buttonSection.className = 'mcs-button-section';
const minimizeBtn = document.createElement('button');
minimizeBtn.id = 'gwhiz-minimize-btn';
minimizeBtn.textContent = '−';
minimizeBtn.className = 'mcs-btn';
const resetBtn = document.createElement('button');
resetBtn.id = 'gwhiz-reset-btn';
resetBtn.textContent = 'Reset';
resetBtn.className = 'mcs-btn mcs-gw-reset-btn';
buttonSection.appendChild(resetBtn);
buttonSection.appendChild(minimizeBtn);
header.appendChild(leftSection);
header.appendChild(buttonSection);
const content = document.createElement('div');
content.id = 'gwhiz-content';
content.className = 'mcs-pane-content mcs-gw-content';
pane.appendChild(header);
pane.appendChild(content);
document.body.appendChild(pane);
this.makeGWhizDraggable(pane, header);
this.gwhizExpTracking = {};
this.gwhizCurrentStatExp = {};
this.gwhizCombatStartTime = this.gwhizCombatStartTime || null;
this.gwhizSessionExpMap = this.gwhizSessionExpMap || null;
this.gwhizSessionExpSnapshot = this.gwhizSessionExpSnapshot || {};
this.gwhizLastCombatLevel = 0;
this.gwhizCachedLevelExpTable = null;
const savedGWhizMinimized = this.gwStorage.get('minimized');
this.gwhizIsMinimized = savedGWhizMinimized === true || savedGWhizMinimized === 'true';
if (this.gwhizIsMinimized) {
minimizeBtn.textContent = '+';
content.style.display = 'none';
header.style.borderRadius = '6px';
}
this.updateGWhizContent();
minimizeBtn.onclick = () => {
this.gwhizIsMinimized = !this.gwhizIsMinimized;
if (this.gwhizIsMinimized) {
content.style.display = 'none';
minimizeBtn.textContent = '+';
header.style.borderRadius = '6px';
this.gwStorage.set('minimized', true);
} else {
content.style.display = 'flex';
minimizeBtn.textContent = '−';
header.style.borderRadius = '6px 6px 0 0';
this.gwStorage.set('minimized', false);
this.constrainPanelToBoundaries('gwhiz-pane', 'mcs_GW', true);
this.updateGWhizExperience();
}
};
resetBtn.onclick = () => {
this.gwhizExpTracking = {};
this.updateGWhizExperience();
};
this._gwhizCombatEndedListener = this.gwhizHandleCombatEnded.bind(this);
this._gwhizWsListener = this.gwhizHandleWebSocketMessage.bind(this);
window.addEventListener('LootTrackerCombatEnded', this._gwhizCombatEndedListener);
window.addEventListener('EquipSpyWebSocketMessage', this._gwhizWsListener);
VisibilityManager.register('gwhiz-update', () => {
this.updateGWhizExperience();
}, 2000);
}
updateGWhizContent() {
const content = document.getElementById('gwhiz-content');
if (!content) return;
content.innerHTML = '';
const sessionDiv = document.createElement('div');
sessionDiv.id = 'gwhiz-session';
sessionDiv.className = 'mcs-gw-section mcs-gw-section-mb';
const sessionRow = document.createElement('div');
sessionRow.className = 'mcs-gw-session-row';
const sessionStartLabel = document.createElement('span');
sessionStartLabel.id = 'gwhiz-session-start';
sessionStartLabel.className = 'mcs-gw-session-val';
sessionStartLabel.textContent = '--';
const sessionDurLabel = document.createElement('span');
sessionDurLabel.id = 'gwhiz-session-duration';
sessionDurLabel.className = 'mcs-gw-session-val';
sessionDurLabel.textContent = '--';
const sessionExpLabel = document.createElement('span');
sessionExpLabel.id = 'gwhiz-session-exp';
sessionExpLabel.className = 'mcs-gw-session-val';
sessionExpLabel.textContent = '--';
const sessionRateLabel = document.createElement('span');
sessionRateLabel.id = 'gwhiz-session-rate';
sessionRateLabel.className = 'mcs-gw-session-val mcs-gw-session-rate';
sessionRateLabel.textContent = '--';
const mkLabel = (text) => { const s = document.createElement('span'); s.className = 'mcs-gw-session-label'; s.textContent = text; return s; };
sessionRow.appendChild(mkLabel('Start: '));
sessionRow.appendChild(sessionStartLabel);
sessionRow.appendChild(mkLabel('Duration: '));
sessionRow.appendChild(sessionDurLabel);
sessionRow.appendChild(mkLabel('Exp: '));
sessionRow.appendChild(sessionExpLabel);
sessionRow.appendChild(mkLabel('Exp/Hr: '));
sessionRow.appendChild(sessionRateLabel);
sessionDiv.appendChild(sessionRow);
content.appendChild(sessionDiv);
const combatLevelDiv = document.createElement('div');
combatLevelDiv.id = 'gwhiz-combat-level';
combatLevelDiv.className = 'mcs-gw-section mcs-gw-section-mb';
const combatTopRow = document.createElement('div');
combatTopRow.className = 'mcs-gw-top-row';
const combatLevelContainer = document.createElement('div');
combatLevelContainer.className = 'mcs-gw-level-container';
const combatLevelLabel = document.createElement('div');
combatLevelLabel.id = 'gwhiz-combat-level-value';
combatLevelLabel.className = 'mcs-gw-combat-level-value';
combatLevelLabel.textContent = '0.000';
combatLevelContainer.appendChild(combatLevelLabel);
const combatNameContainer = document.createElement('div');
combatNameContainer.className = 'mcs-gw-name-container';
const combatNameLabel = document.createElement('div');
combatNameLabel.className = 'mcs-gw-name-label';
combatNameLabel.textContent = 'Combat!';
const flooredEquationRow = document.createElement('div');
flooredEquationRow.id = 'gwhiz-floored-equation';
flooredEquationRow.className = 'mcs-gw-equation';
combatNameContainer.appendChild(combatNameLabel);
combatNameContainer.appendChild(flooredEquationRow);
const combatExpLabel = document.createElement('div');
combatExpLabel.className = 'mcs-gw-exp-label';
combatExpLabel.textContent = '';
const combatTimeLabel = document.createElement('div');
combatTimeLabel.id = 'gwhiz-combat-time-label';
combatTimeLabel.className = 'mcs-gw-time-label';
combatTimeLabel.textContent = '';
combatTopRow.appendChild(combatLevelContainer);
combatTopRow.appendChild(combatNameContainer);
combatTopRow.appendChild(combatExpLabel);
combatTopRow.appendChild(combatTimeLabel);
const combatProgressRow = document.createElement('div');
combatProgressRow.className = 'mcs-gw-progress-row';
const combatProgressContainer = document.createElement('div');
combatProgressContainer.className = 'mcs-gw-progress-container';
const combatProgressBar = document.createElement('div');
combatProgressBar.id = 'gwhiz-combat-progress-bar';
combatProgressBar.className = 'mcs-gw-combat-progress-bar';
const combatProgressBarProjected = document.createElement('div');
combatProgressBarProjected.id = 'gwhiz-combat-progress-bar-projected';
combatProgressBarProjected.className = 'mcs-gw-combat-projected';
combatProgressContainer.appendChild(combatProgressBar);
combatProgressContainer.appendChild(combatProgressBarProjected);
for (let i = 10; i <= 90; i += 10) {
const tickmark = document.createElement('div');
tickmark.className = 'mcs-gw-tickmark';
tickmark.style.left = `${i}%`;
combatProgressContainer.appendChild(tickmark);
}
const combatProgressText = document.createElement('div');
combatProgressText.id = 'gwhiz-combat-progress-text';
combatProgressText.className = 'mcs-gw-combat-progress-text';
combatProgressText.textContent = '0%';
combatProgressRow.appendChild(combatProgressContainer);
combatProgressRow.appendChild(combatProgressText);
combatLevelDiv.appendChild(combatTopRow);
combatLevelDiv.appendChild(combatProgressRow);
content.appendChild(combatLevelDiv);
const combatStats = [
{ name: 'Stamina', key: 'stamina', color: '#FF6B6B' },
{ name: 'Intelligence', key: 'intelligence', color: '#4ECDC4' },
{ name: 'Attack', key: 'attack', color: '#FFD93D' },
{ name: 'Defense', key: 'defense', color: '#95E1D3' },
{ name: 'Melee', key: 'melee', color: '#a62830' },
{ name: 'Ranged', key: 'ranged', color: '#259c85' },
{ name: 'Magic', key: 'magic', color: '#7445b8' }
];
combatStats.forEach(stat => {
const itemDiv = document.createElement('div');
itemDiv.className = 'gwhiz-stat-item mcs-gw-stat-item';
itemDiv.setAttribute('data-stat-key', stat.key);
const topRow = document.createElement('div');
topRow.className = 'mcs-gw-top-row';
const levelLabel = document.createElement('div');
levelLabel.className = 'gwhiz-level-label';
levelLabel.className = 'gwhiz-level-label mcs-gw-stat-level';
levelLabel.style.color = stat.color;
levelLabel.textContent = '...';
const nameLabel = document.createElement('div');
nameLabel.className = 'mcs-gw-stat-name';
nameLabel.textContent = stat.name;
const expLabel = document.createElement('div');
expLabel.className = 'gwhiz-exp-label';
expLabel.className = 'gwhiz-exp-label mcs-gw-exp-label';
expLabel.textContent = '0.0000000000';
const timeLabel = document.createElement('div');
timeLabel.className = 'gwhiz-time-label';
timeLabel.className = 'gwhiz-time-label mcs-gw-time-label';
timeLabel.textContent = '';
topRow.appendChild(levelLabel);
topRow.appendChild(nameLabel);
topRow.appendChild(expLabel);
topRow.appendChild(timeLabel);
const progressRow = document.createElement('div');
progressRow.className = 'mcs-gw-progress-row';
const progressContainer = document.createElement('div');
progressContainer.className = 'mcs-gw-progress-container';
const progressBar = document.createElement('div');
progressBar.className = 'gwhiz-progress-bar';
progressBar.className = 'gwhiz-progress-bar mcs-gw-stat-progress-bar';
progressBar.style.background = stat.color;
const progressText = document.createElement('div');
progressText.className = 'gwhiz-progress-text';
progressText.className = 'gwhiz-progress-text mcs-gw-stat-progress-text';
progressText.style.color = stat.color;
progressText.textContent = '0%';
progressContainer.appendChild(progressBar);
progressRow.appendChild(progressContainer);
progressRow.appendChild(progressText);
const rateLabel = document.createElement('div');
rateLabel.className = 'gwhiz-rate-label';
rateLabel.className = 'gwhiz-rate-label mcs-gw-rate-label';
rateLabel.textContent = 'Charms rule!';
itemDiv.appendChild(topRow);
itemDiv.appendChild(progressRow);
itemDiv.appendChild(rateLabel);
content.appendChild(itemDiv);
});
const timeToLevelSection = document.createElement('div');
timeToLevelSection.id = 'gwhiz-time-to-level-section';
timeToLevelSection.className = 'mcs-gw-section mcs-gw-section-mt';
const timeToLevelTitle = document.createElement('div');
timeToLevelTitle.className = 'mcs-gw-section-title mcs-gw-ttl-title';
const timeToLevelArrow = document.createElement('span');
timeToLevelArrow.id = 'gwhiz-ttl-arrow';
timeToLevelArrow.textContent = '▼';
timeToLevelArrow.className = 'mcs-gw-section-arrow';
const timeToLevelText = document.createElement('span');
timeToLevelText.textContent = 'Time to Level (given same charm level as current)';
timeToLevelTitle.appendChild(timeToLevelArrow);
timeToLevelTitle.appendChild(timeToLevelText);
const timeToLevelTable = document.createElement('div');
timeToLevelTable.id = 'gwhiz-time-to-level-table';
timeToLevelTable.className = 'mcs-gw-ttl-table';
const headerRow = document.createElement('div');
headerRow.className = 'mcs-gw-ttl-header';
const skillHeader = document.createElement('div');
skillHeader.style.textAlign = 'left';
skillHeader.textContent = 'Skill';
const currentHeader = document.createElement('div');
currentHeader.style.textAlign = 'center';
currentHeader.textContent = 'Current';
const expHrHeader = document.createElement('div');
expHrHeader.style.textAlign = 'center';
expHrHeader.textContent = 'Exp/Hr';
const targetHeader = document.createElement('div');
targetHeader.style.textAlign = 'center';
targetHeader.textContent = 'Target';
const timeHeader = document.createElement('div');
timeHeader.className = 'mcs-gw-ttl-time-header';
const timeText = document.createElement('span');
timeText.textContent = 'Time';
timeText.className = 'mcs-gw-ttl-time-text';
const bulwarkContainer = document.createElement('div');
bulwarkContainer.className = 'mcs-gw-bulwark-container';
const bulwarkCheckbox = document.createElement('input');
bulwarkCheckbox.type = 'checkbox';
bulwarkCheckbox.id = 'gwhiz-bulwark-checkbox';
bulwarkCheckbox.className = 'mcs-gw-bulwark-checkbox';
const bulwarkLabel = document.createElement('label');
bulwarkLabel.htmlFor = 'gwhiz-bulwark-checkbox';
bulwarkLabel.textContent = 'Bulwark';
bulwarkLabel.className = 'mcs-gw-bulwark-label';
bulwarkContainer.appendChild(bulwarkCheckbox);
bulwarkContainer.appendChild(bulwarkLabel);
timeHeader.appendChild(timeText);
timeHeader.appendChild(bulwarkContainer);
headerRow.appendChild(skillHeader);
headerRow.appendChild(currentHeader);
headerRow.appendChild(expHrHeader);
headerRow.appendChild(targetHeader);
headerRow.appendChild(timeHeader);
timeToLevelTable.appendChild(headerRow);
const ttlStats = [
{ name: 'Stamina', key: 'stamina', color: '#FF6B6B' },
{ name: 'Intelligence', key: 'intelligence', color: '#4ECDC4' },
{ name: 'Attack', key: 'attack', color: '#FFD93D' },
{ name: 'Defense', key: 'defense', color: '#95E1D3' },
{ name: 'Melee', key: 'melee', color: '#a62830' },
{ name: 'Ranged', key: 'ranged', color: '#259c85' },
{ name: 'Magic', key: 'magic', color: '#7445b8' }
];
ttlStats.forEach(stat => {
const row = document.createElement('div');
row.className = 'gwhiz-ttl-row mcs-gw-ttl-row';
row.setAttribute('data-stat-key', stat.key);
const nameCell = document.createElement('div');
nameCell.className = 'mcs-gw-ttl-name';
nameCell.style.color = stat.color;
nameCell.textContent = stat.name;
const currentLevelCell = document.createElement('div');
currentLevelCell.className = 'gwhiz-ttl-current mcs-gw-ttl-current';
currentLevelCell.textContent = '0';
const expHrCell = document.createElement('div');
expHrCell.className = 'gwhiz-ttl-exphr mcs-gw-ttl-exphr';
expHrCell.textContent = '--';
const targetLevelCell = document.createElement('div');
targetLevelCell.className = 'mcs-gw-ttl-target-cell';
const targetInput = document.createElement('input');
targetInput.type = 'number';
targetInput.className = 'gwhiz-ttl-target';
targetInput.min = '1';
targetInput.max = '200';
targetInput.value = '1';
targetInput.className = 'gwhiz-ttl-target mcs-gw-ttl-input';
targetLevelCell.appendChild(targetInput);
const timeCell = document.createElement('div');
timeCell.className = 'gwhiz-ttl-time mcs-gw-ttl-time';
timeCell.textContent = '--';
row.appendChild(nameCell);
row.appendChild(currentLevelCell);
row.appendChild(expHrCell);
row.appendChild(targetLevelCell);
row.appendChild(timeCell);
timeToLevelTable.appendChild(row);
targetInput.addEventListener('input', () => {
this.updateTimeToLevel(stat.key);
});
});
timeToLevelSection.appendChild(timeToLevelTitle);
timeToLevelSection.appendChild(timeToLevelTable);
content.appendChild(timeToLevelSection);
const bulwarkEnabled = this.gwStorage.get('bulwark_enabled');
bulwarkCheckbox.checked = bulwarkEnabled === true || bulwarkEnabled === 'true';
bulwarkCheckbox.addEventListener('change', () => {
this.gwStorage.set('bulwark_enabled', bulwarkCheckbox.checked);
this.updateTimeToLevelSection();
});
timeToLevelTitle.addEventListener('click', () => {
const isCollapsed = timeToLevelTable.style.display === 'none';
timeToLevelTable.style.display = isCollapsed ? 'flex' : 'none';
timeToLevelArrow.style.transform = isCollapsed ? 'rotate(0deg)' : 'rotate(-90deg)';
this.gwStorage.set('ttl_collapsed', !isCollapsed);
});
const ttlCollapsed = this.gwStorage.get('ttl_collapsed');
if (ttlCollapsed === true || ttlCollapsed === 'true') {
timeToLevelTable.style.display = 'none';
timeToLevelArrow.style.transform = 'rotate(-90deg)';
}
const charmsSection = document.createElement('div');
charmsSection.id = 'gwhiz-charms-section';
charmsSection.className = 'mcs-gw-section mcs-gw-section-mt';
const charmsTitle = document.createElement('div');
charmsTitle.className = 'mcs-gw-section-title mcs-gw-charms-title';
const charmsArrow = document.createElement('span');
charmsArrow.id = 'gwhiz-charms-arrow';
charmsArrow.textContent = '▼';
charmsArrow.className = 'mcs-gw-section-arrow';
const charmsText = document.createElement('span');
charmsText.textContent = 'Charms rule!';
charmsTitle.appendChild(charmsArrow);
charmsTitle.appendChild(charmsText);
const charmsGrid = document.createElement('div');
charmsGrid.id = 'gwhiz-charms-grid';
charmsGrid.className = 'mcs-gw-charms-grid';
charmsSection.appendChild(charmsTitle);
charmsSection.appendChild(charmsGrid);
content.appendChild(charmsSection);
charmsTitle.addEventListener('click', () => {
const isCollapsed = charmsGrid.style.display === 'none';
charmsGrid.style.display = isCollapsed ? 'grid' : 'none';
charmsArrow.style.transform = isCollapsed ? 'rotate(0deg)' : 'rotate(-90deg)';
this.gwStorage.set('charms_collapsed', !isCollapsed);
});
const charmsCollapsed = this.gwStorage.get('charms_collapsed');
if (charmsCollapsed === true || charmsCollapsed === 'true') {
charmsGrid.style.display = 'none';
charmsArrow.style.transform = 'rotate(-90deg)';
}
this.updateGWhizExperience();
}
updateGWhizExperience() {
if (this.gwhizIsMinimized) {
const currentTime = Date.now();
let totalExpPerHour = 0;
if (Object.keys(this.gwhizCurrentStatExp).length === 0) {
let characterSkills = null;
try {
const charData = CharacterDataStorage.get();
if (charData) {
characterSkills = charData.characterSkills;
}
} catch (e) {
console.error('[GWhiz] Failed to load character data:', e);
}
if (characterSkills) {
for (const skill of characterSkills) {
this.gwhizCurrentStatExp[skill.skillHrid] = {
level: skill.level,
experience: skill.experience,
skillHrid: skill.skillHrid
};
}
}
}
const statMapping = {
'stamina': 'stamina',
'intelligence': 'intelligence',
'attack': 'attack',
'defense': 'defense',
'melee': 'melee',
'ranged': 'ranged',
'magic': 'magic'
};
for (const skillHrid in this.gwhizCurrentStatExp) {
const statData = this.gwhizCurrentStatExp[skillHrid];
let statKey = null;
for (const key in statMapping) {
if (skillHrid.includes(key)) {
statKey = key;
break;
}
}
if (statKey && !this.gwhizExpTracking[statKey]) {
this.gwhizExpTracking[statKey] = {
startExp: statData.experience,
startTime: currentTime,
lastExp: statData.experience,
lastUpdateTime: currentTime,
savedPausedMs: window.MCS_TOTAL_PAUSED_MS ?? 0,
savedTabHiddenMs: window.MCS_TOTAL_TAB_HIDDEN_MS ?? 0
};
}
}
for (const statKey in this.gwhizExpTracking) {
const tracking = this.gwhizExpTracking[statKey];
if (!tracking) continue;
let currentExp = null;
for (const skillHrid in this.gwhizCurrentStatExp) {
if (skillHrid.includes(statKey)) {
currentExp = this.gwhizCurrentStatExp[skillHrid].experience;
break;
}
}
if (currentExp !== null) {
const expGained = currentExp - tracking.startExp;
const timeElapsed = mcsGetElapsedSeconds(tracking.startTime, currentTime, tracking.savedPausedMs, 0, true, tracking.savedTabHiddenMs);
tracking.lastExp = currentExp;
tracking.lastUpdateTime = currentTime;
if (expGained > 0 && timeElapsed > 0) {
totalExpPerHour += (expGained / timeElapsed) * 3600;
}
}
}
this.gwhizTotalExpPerHour = totalExpPerHour;
const totalExpSpan = document.getElementById('gwhiz-total-exp-hr');
if (totalExpSpan) {
if (totalExpPerHour > 0) {
totalExpSpan.textContent = Math.floor(totalExpPerHour).toLocaleString() + ' exp/hr';
} else {
totalExpSpan.textContent = '0 exp/hr';
}
}
return;
}
let characterSkills = null;
const levelExperienceTable = InitClientDataCache.getLevelExperienceTable();
try {
const charData = CharacterDataStorage.get();
if (charData) {
characterSkills = charData.characterSkills;
}
} catch (e) {
console.error('[GWhiz] Failed to load character data:', e);
}
if (!characterSkills || !levelExperienceTable) {
console.error('[GWhiz] Missing required data - characterSkills:', !!characterSkills, 'levelExperienceTable:', !!levelExperienceTable);
return;
}
if (Object.keys(this.gwhizCurrentStatExp).length > 0) {
for (const skill of characterSkills) {
const cached = this.gwhizCurrentStatExp[skill.skillHrid];
if (cached) {
skill.level = cached.level;
skill.experience = cached.experience;
}
}
}
const characterCombatStats = {};
for (const skill of characterSkills) {
if (skill.skillHrid.includes('stamina')) {
characterCombatStats.stamina = skill;
} else if (skill.skillHrid.includes('intelligence')) {
characterCombatStats.intelligence = skill;
} else if (skill.skillHrid.includes('attack')) {
characterCombatStats.attack = skill;
} else if (skill.skillHrid.includes('defense')) {
characterCombatStats.defense = skill;
} else if (skill.skillHrid.includes('melee')) {
characterCombatStats.melee = skill;
} else if (skill.skillHrid.includes('ranged')) {
characterCombatStats.ranged = skill;
} else if (skill.skillHrid.includes('magic')) {
characterCombatStats.magic = skill;
}
}
const statItems = document.querySelectorAll('.gwhiz-stat-item');
const currentTime = Date.now();
const levels = {
stamina: 0,
intelligence: 0,
attack: 0,
defense: 0,
melee: 0,
ranged: 0,
magic: 0
};
const levelDecimals = {
stamina: 0,
intelligence: 0,
attack: 0,
defense: 0,
melee: 0,
ranged: 0,
magic: 0
};
const progressPercents = {
stamina: 0,
intelligence: 0,
attack: 0,
defense: 0,
melee: 0,
ranged: 0,
magic: 0
};
const statColors = {
stamina: '#FF6B6B',
intelligence: '#4ECDC4',
attack: '#FFD93D',
defense: '#95E1D3',
melee: '#a62830',
ranged: '#259c85',
magic: '#7445b8'
};
const activelylevelingStats = new Set();
let totalExpPerHour = 0;
const statExpPerHour = {};
const statExpGained = {};
const statTimeElapsed = {};
statItems.forEach(item => {
const statKey = item.getAttribute('data-stat-key');
const levelLabel = item.querySelector('.gwhiz-level-label');
const expLabel = item.querySelector('.gwhiz-exp-label');
const timeLabel = item.querySelector('.gwhiz-time-label');
const rateLabel = item.querySelector('.gwhiz-rate-label');
const progressBar = item.querySelector('.gwhiz-progress-bar');
const progressText = item.querySelector('.gwhiz-progress-text');
if (!levelLabel || !expLabel || !timeLabel || !rateLabel || !progressBar || !progressText) {
return;
}
const statData = characterCombatStats[statKey];
if (!statData) {
levelLabel.textContent = '0';
expLabel.textContent = '0.0000000000';
rateLabel.textContent = 'Charms rule!';
timeLabel.textContent = '';
progressBar.style.width = '0%';
progressText.textContent = '0%';
return;
}
const currentLevel = statData.level;
const currentExp = statData.experience;
levels[statKey] = currentLevel;
try {
const minExpAtLevel = levelExperienceTable[currentLevel];
const maxExpAtLevel = levelExperienceTable[currentLevel + 1];
const expSpanInLevel = maxExpAtLevel - minExpAtLevel;
const expIntoLevel = currentExp - minExpAtLevel;
const expNeeded = maxExpAtLevel - currentExp;
const progressPercent = (expIntoLevel / expSpanInLevel) * 100;
const levelWithDecimal = currentLevel + (progressPercent / 100);
levelDecimals[statKey] = levelWithDecimal;
progressPercents[statKey] = progressPercent;
levelLabel.textContent = currentLevel.toString();
progressBar.style.width = progressPercent.toFixed(2) + '%';
progressText.textContent = (Math.floor(progressPercent * 10) / 10).toFixed(1) + '%';
expLabel.textContent = 'Remaining: ' + Math.ceil(expNeeded);
const trackingKey = statKey;
if (!this.gwhizExpTracking[trackingKey]) {
this.gwhizExpTracking[trackingKey] = {
startExp: currentExp,
startTime: currentTime,
lastExp: currentExp,
lastUpdateTime: currentTime,
savedPausedMs: window.MCS_TOTAL_PAUSED_MS ?? 0,
savedTabHiddenMs: window.MCS_TOTAL_TAB_HIDDEN_MS ?? 0
};
rateLabel.textContent = 'Charms rule!';
timeLabel.textContent = '';
item.style.display = 'flex';
} else {
const tracking = this.gwhizExpTracking[trackingKey];
const expGained = currentExp - tracking.startExp;
const timeElapsed = mcsGetElapsedSeconds(tracking.startTime, currentTime, tracking.savedPausedMs, 0, true, tracking.savedTabHiddenMs);
tracking.lastExp = currentExp;
tracking.lastUpdateTime = currentTime;
if (expGained > 0 && timeElapsed > 0) {
const expPerHour = (expGained / timeElapsed) * 3600;
totalExpPerHour += expPerHour;
statExpPerHour[statKey] = expPerHour;
statExpGained[statKey] = expGained;
statTimeElapsed[statKey] = timeElapsed;
rateLabel.textContent = Math.floor(expPerHour) + '/hr';
activelylevelingStats.add(statKey);
item.style.display = 'flex';
const hoursNeeded = expNeeded / expPerHour;
if (hoursNeeded < 1) {
const minutes = Math.ceil(hoursNeeded * 60);
timeLabel.textContent = minutes + 'm';
} else if (hoursNeeded < 24) {
const hours = Math.floor(hoursNeeded);
const minutes = Math.ceil((hoursNeeded - hours) * 60);
timeLabel.textContent = hours + 'h ' + minutes + 'm';
} else {
const days = Math.floor(hoursNeeded / 24);
const hours = Math.floor(hoursNeeded % 24);
timeLabel.textContent = days + 'd ' + hours + 'h';
}
} else {
rateLabel.textContent = 'Charms rule!';
timeLabel.textContent = '';
item.style.display = 'none';
}
}
} catch (error) {
console.error('[GWhiz]', statKey, 'error in calculation:', error);
levelLabel.textContent = currentLevel.toString();
expLabel.textContent = 'Error';
rateLabel.textContent = 'Charms rule!';
timeLabel.textContent = '';
item.style.display = 'none';
}
});
if (totalExpPerHour > 0) {
statItems.forEach(item => {
const statKey = item.getAttribute('data-stat-key');
const rateLabel = item.querySelector('.gwhiz-rate-label');
if (!rateLabel) return;
if (statExpPerHour[statKey]) {
const expPerHour = statExpPerHour[statKey];
const percentage = (expPerHour / totalExpPerHour * 100).toFixed(1);
const gained = statExpGained[statKey] || 0;
const elapsed = statTimeElapsed[statKey] || 0;
const mins = Math.floor(elapsed / 60);
const secs = Math.floor(elapsed % 60);
const timeStr = mins > 0 ? mins + 'm ' + secs + 's' : secs + 's';
rateLabel.textContent = Math.floor(expPerHour) + '/hr (' + percentage + '%) Exp: ' + gained.toLocaleString() + ' Time: ' + timeStr;
}
});
}
this.gwhizTotalExpPerHour = totalExpPerHour;
const totalExpSpan = document.getElementById('gwhiz-total-exp-hr');
if (totalExpSpan) {
if (totalExpPerHour > 0) {
totalExpSpan.textContent = Math.floor(totalExpPerHour).toLocaleString() + ' exp/hr';
} else {
totalExpSpan.textContent = '0 exp/hr';
}
}
const sessionStartEl = document.getElementById('gwhiz-session-start');
const sessionDurEl = document.getElementById('gwhiz-session-duration');
const sessionExpEl = document.getElementById('gwhiz-session-exp');
const sessionRateEl = document.getElementById('gwhiz-session-rate');
if (sessionStartEl && sessionDurEl && sessionExpEl && sessionRateEl) {
const combatStart = this.gwhizCombatStartTime;
if (combatStart) {
const startDate = new Date(combatStart);
sessionStartEl.textContent = startDate.toLocaleDateString([], { month: 'short', day: 'numeric' }) + ' ' + startDate.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
const serverDurMs = Date.now() - combatStart;
const serverDurSec = serverDurMs / 1000;
const durDays = Math.floor(serverDurSec / 86400);
const durHours = Math.floor((serverDurSec % 86400) / 3600);
const durMins = Math.floor((serverDurSec % 3600) / 60);
if (durDays > 0) {
sessionDurEl.textContent = durDays + 'd ' + durHours + 'h ' + durMins + 'm';
} else if (durHours > 0) {
sessionDurEl.textContent = durHours + 'h ' + durMins + 'm';
} else {
sessionDurEl.textContent = durMins + 'm';
}
let totalSessionExp = 0;
const sessionMap = this.gwhizSessionExpMap;
if (sessionMap) {
for (const skillHrid in sessionMap) {
totalSessionExp += sessionMap[skillHrid] || 0;
}
const snapshot = this.gwhizSessionExpSnapshot;
for (const skillHrid in snapshot) {
const currentExp = this.gwhizCurrentStatExp[skillHrid]?.experience;
if (currentExp != null) {
const delta = currentExp - snapshot[skillHrid];
if (delta > 0) totalSessionExp += delta;
}
}
}
sessionExpEl.textContent = Math.floor(totalSessionExp).toLocaleString();
if (serverDurSec > 0 && totalSessionExp > 0) {
const sessionExpPerHour = (totalSessionExp / serverDurSec) * 3600;
sessionRateEl.textContent = Math.floor(sessionExpPerHour).toLocaleString() + '/hr';
} else {
sessionRateEl.textContent = '--';
}
} else {
sessionStartEl.textContent = '--';
sessionDurEl.textContent = '--';
sessionExpEl.textContent = '--';
sessionRateEl.textContent = '--';
}
}
const charmsGrid = document.getElementById('gwhiz-charms-grid');
if (charmsGrid) {
charmsGrid.innerHTML = '';
const allStats = ['stamina', 'intelligence', 'attack', 'defense', 'melee', 'ranged', 'magic'];
const nonlevelingStats = allStats.filter(stat => !activelylevelingStats.has(stat));
nonlevelingStats.forEach(statKey => {
const statData = characterCombatStats[statKey];
if (!statData) return;
const statName = statKey.charAt(0).toUpperCase() + statKey.slice(1);
const statColor = statColors[statKey];
const statItem = document.createElement('div');
statItem.className = 'mcs-gw-charm-item';
const levelSpan = document.createElement('span');
levelSpan.className = 'mcs-gw-charm-level';
levelSpan.style.color = statColor;
levelSpan.textContent = levels[statKey].toString();
const nameSpan = document.createElement('span');
nameSpan.className = 'mcs-gw-charm-name';
nameSpan.textContent = statName;
const percentSpan = document.createElement('span');
percentSpan.className = 'mcs-gw-charm-percent';
percentSpan.textContent = Math.floor(progressPercents[statKey]) + '%';
statItem.appendChild(levelSpan);
statItem.appendChild(nameSpan);
statItem.appendChild(percentSpan);
charmsGrid.appendChild(statItem);
});
}
const maxCombatStyle = Math.max(levels.melee, levels.ranged, levels.magic);
const maxOffensiveDefensive = Math.max(levels.attack, levels.defense, levels.melee, levels.ranged, levels.magic);
const combatLevel = 0.1 * (levels.stamina + levels.intelligence + levels.attack + levels.defense + maxCombatStyle) +
0.5 * maxOffensiveDefensive;
const combatLevelValueDiv = document.getElementById('gwhiz-combat-level-value');
if (combatLevelValueDiv) {
combatLevelValueDiv.textContent = Math.floor(combatLevel).toString();
}
const flooredEquationDiv = document.getElementById('gwhiz-floored-equation');
if (flooredEquationDiv) {
const colorSpan = (value, color) => `<span style="color: ${color};">${value}</span>`;
const maxStyleName = levels.melee >= levels.ranged && levels.melee >= levels.magic ? 'melee' :
levels.ranged >= levels.magic ? 'ranged' : 'magic';
const maxOffDefName = levels.attack >= levels.defense && levels.attack >= levels.melee &&
levels.attack >= levels.ranged && levels.attack >= levels.magic ? 'attack' :
levels.defense >= levels.melee && levels.defense >= levels.ranged &&
levels.defense >= levels.magic ? 'defense' : maxStyleName;
const flooredEq = `0.1×(${colorSpan(levels.stamina, statColors.stamina)}+${colorSpan(levels.intelligence, statColors.intelligence)}+${colorSpan(levels.attack, statColors.attack)}+${colorSpan(levels.defense, statColors.defense)}+${colorSpan(levels[maxStyleName], statColors[maxStyleName])})+0.5×${colorSpan(levels[maxOffDefName], statColors[maxOffDefName])}=${colorSpan(combatLevel.toFixed(3), '#ffa500')}`;
flooredEquationDiv.innerHTML = flooredEq;
}
const nextCombatLevel = Math.floor(combatLevel) + 1;
const combatProgress = ((combatLevel - Math.floor(combatLevel)) * 100);
const combatProgressBar = document.getElementById('gwhiz-combat-progress-bar');
const combatProgressText = document.getElementById('gwhiz-combat-progress-text');
const oldProjected = document.querySelectorAll('.gwhiz-combat-progress-bar-projected');
oldProjected.forEach(bar => bar.remove());
let combatMinTimeHours = Infinity;
const targetCombatLevel = Math.floor(combatLevel) + 1;
const combatLevelGap = targetCombatLevel - combatLevel;
const combatLevelFloorChanged = Math.floor(combatLevel) !== Math.floor(this.gwhizLastCombatLevel);
this.gwhizLastCombatLevel = combatLevel;
const calcExpNeededToLevelNTimes = (statKey, numLevels) => {
const statData = characterCombatStats[statKey];
if (!statData) return Infinity;
const currentLevel = statData.level;
const currentExp = statData.experience;
let totalExpNeeded = 0;
const maxExpAtLevel = levelExperienceTable[currentLevel + 1];
totalExpNeeded += maxExpAtLevel - currentExp;
for (let i = 1; i < numLevels && currentLevel + i < 199; i++) {
const levelExp = levelExperienceTable[currentLevel + i + 1] - levelExperienceTable[currentLevel + i];
totalExpNeeded += levelExp;
}
return totalExpNeeded;
};
if (combatLevelGap > 0.001 && activelylevelingStats.size > 0 && (combatLevelFloorChanged || !this.gwhizCachedCombatStrategy)) {
const mainStatKeys = ['melee', 'ranged', 'magic'].filter(k => activelylevelingStats.has(k));
const secondaryStatKeys = ['stamina', 'intelligence', 'attack', 'defense'].filter(k => activelylevelingStats.has(k));
const mainStatLevelsNeeded = Math.ceil(combatLevelGap / 0.6);
const secondaryLevelsAfterMain = Math.ceil(Math.max(0, combatLevelGap - 0.6) / 0.1);
const secondaryLevelsOnly = Math.ceil(combatLevelGap / 0.1);
let bestStrategy = null;
let minExpNeeded = Infinity;
if (mainStatKeys.length > 0) {
for (const mainStat of mainStatKeys) {
const expNeeded = calcExpNeededToLevelNTimes(mainStat, mainStatLevelsNeeded);
if (expNeeded < minExpNeeded) {
minExpNeeded = expNeeded;
bestStrategy = { type: 1, stat: mainStat, levels: mainStatLevelsNeeded };
}
}
}
if (mainStatKeys.length > 0 && secondaryStatKeys.length > 0 && secondaryLevelsAfterMain > 0) {
for (const mainStat of mainStatKeys) {
const expNeeded = calcExpNeededToLevelNTimes(mainStat, mainStatLevelsNeeded);
if (expNeeded < minExpNeeded) {
minExpNeeded = expNeeded;
bestStrategy = { type: 2, stat: mainStat, levels: mainStatLevelsNeeded };
}
}
}
if (secondaryStatKeys.length > 0) {
for (const secStat of secondaryStatKeys) {
const expNeeded = calcExpNeededToLevelNTimes(secStat, secondaryLevelsOnly);
if (expNeeded < minExpNeeded) {
minExpNeeded = expNeeded;
bestStrategy = { type: 3, stat: secStat, levels: secondaryLevelsOnly };
}
}
}
this.gwhizCachedCombatStrategy = bestStrategy;
}
if (combatLevelGap > 0.001 && this.gwhizCachedCombatStrategy) {
const strategy = this.gwhizCachedCombatStrategy;
const tracking = this.gwhizExpTracking[strategy.stat];
if (tracking) {
const expGained = tracking.lastExp - tracking.startExp;
const timeElapsed = mcsGetElapsedSeconds(tracking.startTime, currentTime, tracking.savedPausedMs, 0, true, tracking.savedTabHiddenMs);
if (expGained > 0 && timeElapsed > 0) {
const expPerHour = (expGained / timeElapsed) * 3600;
const expNeeded = calcExpNeededToLevelNTimes(strategy.stat, strategy.levels);
combatMinTimeHours = expNeeded / expPerHour;
}
}
}
const contributingStats = [];
if (combatLevelGap > 0.001 && combatMinTimeHours < Infinity) {
for (const statKey of activelylevelingStats) {
const tracking = this.gwhizExpTracking[statKey];
if (!tracking) continue;
const expGained = tracking.lastExp - tracking.startExp;
const timeElapsed = mcsGetElapsedSeconds(tracking.startTime, currentTime, tracking.savedPausedMs, 0, true, tracking.savedTabHiddenMs);
if (expGained <= 0 || timeElapsed <= 0) continue;
const statData = characterCombatStats[statKey];
if (!statData) continue;
let contribution = 0;
if (statKey === 'melee' || statKey === 'ranged' || statKey === 'magic') {
if (levels[statKey] === maxCombatStyle) {
contribution += 0.1;
}
} else {
contribution += 0.1;
}
if ((statKey === 'attack' || statKey === 'defense' || statKey === 'melee' ||
statKey === 'ranged' || statKey === 'magic') &&
levels[statKey] === maxOffensiveDefensive) {
contribution += 0.5;
}
if (contribution === 0) continue;
const currentLevel = statData.level;
const currentExp = statData.experience;
const minExpAtLevel = levelExperienceTable[currentLevel];
const maxExpAtLevel = levelExperienceTable[currentLevel + 1];
const expNeeded = maxExpAtLevel - currentExp;
const expPerHour = (expGained / timeElapsed) * 3600;
const hoursToLevel = expNeeded / expPerHour;
if (hoursToLevel > combatMinTimeHours * 1.01) continue;
const expSpanInLevel = maxExpAtLevel - minExpAtLevel;
const expIntoLevel = currentExp - minExpAtLevel;
const statProgress = (expIntoLevel / expSpanInLevel) * 100;
contributingStats.push({
key: statKey,
contribution: contribution,
progress: statProgress,
hoursToLevel: hoursToLevel
});
}
}
if (combatProgressBar && combatProgressText) {
combatProgressBar.style.width = combatProgress.toFixed(2) + '%';
if (contributingStats.length > 0) {
const container = combatProgressBar.parentElement;
contributingStats.sort((a, b) => a.hoursToLevel - b.hoursToLevel);
const totalContribution = contributingStats.reduce((sum, s) => sum + s.contribution, 0);
let currentLeft = combatProgress;
let remainingSpace = 100 - combatProgress;
for (const stat of contributingStats) {
const statFullContribution = stat.contribution * 100;
const statSegment = (stat.progress / 100) * statFullContribution;
const segmentWidth = Math.min(statSegment, (stat.progress / 100) * remainingSpace);
remainingSpace -= segmentWidth;
const projectedBar = document.createElement('div');
projectedBar.className = 'gwhiz-combat-progress-bar-projected';
projectedBar.className = 'gwhiz-combat-progress-bar-projected';
projectedBar.style.left = `${currentLeft.toFixed(2)}%`;
projectedBar.style.background = statColors[stat.key] || '#259c85';
projectedBar.style.width = `${segmentWidth.toFixed(2)}%`;
container.appendChild(projectedBar);
currentLeft += segmentWidth;
}
combatProgressText.textContent = (Math.floor(currentLeft * 10) / 10).toFixed(1) + '%';
} else {
combatProgressText.textContent = (Math.floor(combatProgress * 10) / 10).toFixed(1) + '%';
}
}
const combatTimeLabel = document.getElementById('gwhiz-combat-time-label');
if (combatTimeLabel) {
if (combatMinTimeHours < Infinity) {
if (combatMinTimeHours < 1 / 60) {
const seconds = Math.ceil(combatMinTimeHours * 3600);
combatTimeLabel.textContent = seconds + 's';
} else if (combatMinTimeHours < 1) {
const minutes = Math.ceil(combatMinTimeHours * 60);
combatTimeLabel.textContent = minutes + 'm';
} else if (combatMinTimeHours < 24) {
const hours = Math.floor(combatMinTimeHours);
const minutes = Math.ceil((combatMinTimeHours - hours) * 60);
combatTimeLabel.textContent = hours + 'h ' + minutes + 'm';
} else {
const days = Math.floor(combatMinTimeHours / 24);
const hours = Math.floor(combatMinTimeHours % 24);
combatTimeLabel.textContent = days + 'd ' + hours + 'h';
}
} else {
combatTimeLabel.textContent = '';
}
}
this.updateTimeToLevelSection();
}
updateTimeToLevelSection() {
const timeToLevelTable = document.getElementById('gwhiz-time-to-level-table');
if (!timeToLevelTable || timeToLevelTable.style.display === 'none') {
return;
}
const rows = document.querySelectorAll('.gwhiz-ttl-row');
if (!rows || rows.length === 0) return;
let characterSkills = null;
let levelExperienceTable = null;
levelExperienceTable = InitClientDataCache.getLevelExperienceTable();
try {
const charData = CharacterDataStorage.get();
if (charData) {
characterSkills = charData.characterSkills;
}
} catch (e) {
console.error('[GWhiz] Failed to load character data:', e);
}
if (!characterSkills || !levelExperienceTable) {
return;
}
if (Object.keys(this.gwhizCurrentStatExp).length > 0) {
for (const skill of characterSkills) {
const cached = this.gwhizCurrentStatExp[skill.skillHrid];
if (cached) {
skill.level = cached.level;
skill.experience = cached.experience;
}
}
}
const characterCombatStats = {};
for (const skill of characterSkills) {
if (skill.skillHrid.includes('stamina')) {
characterCombatStats.stamina = skill;
} else if (skill.skillHrid.includes('intelligence')) {
characterCombatStats.intelligence = skill;
} else if (skill.skillHrid.includes('attack')) {
characterCombatStats.attack = skill;
} else if (skill.skillHrid.includes('defense')) {
characterCombatStats.defense = skill;
} else if (skill.skillHrid.includes('melee')) {
characterCombatStats.melee = skill;
} else if (skill.skillHrid.includes('ranged')) {
characterCombatStats.ranged = skill;
} else if (skill.skillHrid.includes('magic')) {
characterCombatStats.magic = skill;
}
}
const currentTime = Date.now();
const supportStats = ['stamina', 'intelligence', 'attack', 'defense'];
const combatStyles = ['melee', 'ranged', 'magic'];
let supportExpPerHour = 0;
let supportCount = 0;
let combatStyleExpPerHour = 0;
let combatStyleCount = 0;
for (const statKey of supportStats) {
const tracking = this.gwhizExpTracking[statKey];
if (tracking) {
const expGained = tracking.lastExp - tracking.startExp;
const timeElapsed = mcsGetElapsedSeconds(tracking.startTime, currentTime, tracking.savedPausedMs, 0, true, tracking.savedTabHiddenMs);
if (expGained > 0 && timeElapsed > 0) {
supportExpPerHour += (expGained / timeElapsed) * 3600;
supportCount++;
}
}
}
for (const statKey of combatStyles) {
const tracking = this.gwhizExpTracking[statKey];
if (tracking) {
const expGained = tracking.lastExp - tracking.startExp;
const timeElapsed = mcsGetElapsedSeconds(tracking.startTime, currentTime, tracking.savedPausedMs, 0, true, tracking.savedTabHiddenMs);
if (expGained > 0 && timeElapsed > 0) {
combatStyleExpPerHour += (expGained / timeElapsed) * 3600;
combatStyleCount++;
}
}
}
const avgSupportExpPerHour = supportCount > 0 ? supportExpPerHour / supportCount : 0;
const avgCombatStyleExpPerHour = combatStyleCount > 0 ? combatStyleExpPerHour / combatStyleCount : 0;
const bulwarkCheckbox = document.getElementById('gwhiz-bulwark-checkbox');
const isBulwarkEnabled = bulwarkCheckbox ? bulwarkCheckbox.checked : false;
const defenseTracking = this.gwhizExpTracking['defense'];
const staminaTracking = this.gwhizExpTracking['stamina'];
const defenseGaining = defenseTracking && (defenseTracking.lastExp - defenseTracking.startExp) > 0;
const staminaGaining = staminaTracking && (staminaTracking.lastExp - staminaTracking.startExp) > 0;
const hasDefenseCharm = defenseGaining && !staminaGaining;
rows.forEach(row => {
const statKey = row.getAttribute('data-stat-key');
const currentLevelCell = row.querySelector('.gwhiz-ttl-current');
const expHrCell = row.querySelector('.gwhiz-ttl-exphr');
const targetInput = row.querySelector('.gwhiz-ttl-target');
const timeCell = row.querySelector('.gwhiz-ttl-time');
const statData = characterCombatStats[statKey];
if (!statData || !currentLevelCell || !expHrCell || !targetInput || !timeCell) return;
const currentLevel = statData.level;
const currentExp = statData.experience;
currentLevelCell.textContent = currentLevel.toString();
const minLevel = currentLevel + 1;
targetInput.min = minLevel.toString();
if (!targetInput.dataset.initialized) {
targetInput.value = minLevel.toString();
targetInput.dataset.initialized = 'true';
}
let expPerHour = 0;
if (statKey === 'defense' && isBulwarkEnabled && hasDefenseCharm) {
expPerHour = supportExpPerHour + combatStyleExpPerHour;
} else if (statKey === 'defense' && isBulwarkEnabled) {
expPerHour = avgCombatStyleExpPerHour;
} else if (supportStats.includes(statKey)) {
expPerHour = avgSupportExpPerHour;
} else if (combatStyles.includes(statKey)) {
expPerHour = avgCombatStyleExpPerHour;
}
if (expPerHour > 0) {
expHrCell.textContent = Math.floor(expPerHour).toLocaleString();
} else {
expHrCell.textContent = '--';
}
const targetLevel = parseInt(targetInput.value);
if (expPerHour <= 0) {
timeCell.textContent = '--';
return;
}
let targetLevelClamped;
if (isNaN(targetLevel) || targetLevel === 0) {
targetLevelClamped = minLevel;
} else {
targetLevelClamped = Math.max(minLevel, Math.min(targetLevel, 200));
}
let totalExpNeeded = 0;
const maxExpAtCurrentLevel = levelExperienceTable[currentLevel + 1];
const remainingInCurrentLevel = maxExpAtCurrentLevel - currentExp;
totalExpNeeded += remainingInCurrentLevel;
for (let lvl = currentLevel + 1; lvl < targetLevelClamped; lvl++) {
const expForLevel = levelExperienceTable[lvl + 1] - levelExperienceTable[lvl];
totalExpNeeded += expForLevel;
}
const hoursNeeded = totalExpNeeded / expPerHour;
if (hoursNeeded < 1 / 60) {
const seconds = Math.ceil(hoursNeeded * 3600);
timeCell.textContent = seconds + 's';
} else if (hoursNeeded < 1) {
const minutes = Math.ceil(hoursNeeded * 60);
timeCell.textContent = minutes + 'm';
} else if (hoursNeeded < 24) {
const hours = Math.floor(hoursNeeded);
const minutes = Math.ceil((hoursNeeded - hours) * 60);
timeCell.textContent = hours + 'h ' + minutes + 'm';
} else {
const days = Math.floor(hoursNeeded / 24);
const hours = Math.floor(hoursNeeded % 24);
timeCell.textContent = days + 'd ' + hours + 'h';
}
});
}
updateTimeToLevel(statKey) {
this.updateTimeToLevelSection();
}
makeGWhizDraggable(pane, header) {
DragHandler.makeDraggable(pane, header, 'mcs_GW');
}
destroyGWhiz() {
VisibilityManager.clear('gwhiz-update');
if (this._gwhizCombatEndedListener) { window.removeEventListener('LootTrackerCombatEnded', this._gwhizCombatEndedListener); this._gwhizCombatEndedListener = null; }
if (this._gwhizWsListener) { window.removeEventListener('EquipSpyWebSocketMessage', this._gwhizWsListener); this._gwhizWsListener = null; }
const pane = document.getElementById('gwhiz-pane');
if (pane) pane.remove();
}
// GWhiz End
// AMazing Start
get amStorage() {
if (!this._amStorage) {
this._amStorage = createModuleStorage('AM');
}
return this._amStorage;
}
createAMazingPane() {
if (document.getElementById('amazing-pane')) return;
const pane = document.createElement('div');
pane.id = 'amazing-pane';
registerPanel('amazing-pane');
this.applyClasses(pane, 'mcs-pane', 'mcs-amazing-pane');
const header = document.createElement('div');
this.applyClasses(header, 'mcs-pane-header', 'mcs-gap-15');
const titleSpan = document.createElement('span');
this.applyClass(titleSpan, 'mcs-pane-title');
titleSpan.textContent = 'AMazing';
const minimizeBtn = document.createElement('button');
minimizeBtn.id = 'amazing-minimize-btn';
minimizeBtn.textContent = '−';
this.applyClasses(minimizeBtn, 'mcs-btn', 'mcs-btn-minimize');
header.appendChild(titleSpan);
header.appendChild(minimizeBtn);
const content = document.createElement('div');
content.id = 'amazing-content';
this.applyClasses(content, 'mcs-pane-content', 'mcs-gap-8');
pane.appendChild(header);
pane.appendChild(content);
document.body.appendChild(pane);
this.makeAMazingDraggable(pane, header);
this.amazingTracking = {};
this.mwiHideEnabled = false;
this.updateAMazingContent();
const savedAmazingMinimized = window.lootDropsTrackerInstance.amStorage.get('minimized') === 'true';
this.amazingIsMinimized = savedAmazingMinimized;
if (savedAmazingMinimized) {
content.classList.add('mcs-hidden');
minimizeBtn.textContent = '+';
header.classList.add('minimized');
}
minimizeBtn.onclick = () => {
this.amazingIsMinimized = !this.amazingIsMinimized;
if (this.amazingIsMinimized) {
content.classList.add('mcs-hidden');
minimizeBtn.textContent = '+';
header.classList.add('minimized');
window.lootDropsTrackerInstance.amStorage.set('minimized', 'true');
} else {
content.classList.remove('mcs-hidden');
minimizeBtn.textContent = '−';
header.classList.remove('minimized');
window.lootDropsTrackerInstance.amStorage.set('minimized', 'false');
this.constrainPanelToBoundaries('amazing-pane', 'mcs_AM', true);
}
};
}
updateAMazingContent() {
const content = document.getElementById('amazing-content');
if (!content) return;
content.innerHTML = '';
const hideMWIContainer = document.createElement('div');
this.applyClass(hideMWIContainer, 'mcs-container');
const buttonRow = document.createElement('div');
this.applyClasses(buttonRow, 'mcs-row', 'mcs-gap-10');
const hideMWIBtn = document.createElement('button');
hideMWIBtn.id = 'amazing-hide-mwi-btn';
hideMWIBtn.textContent = 'Hide MWI';
this.applyClass(hideMWIBtn, 'mcs-btn');
const descriptionText = document.createElement('span');
this.applyClass(descriptionText, 'mcs-text-description');
descriptionText.innerHTML = 'Hide everything besides <span class="mcs-text-highlight">MWI Combat Suite</span>';
buttonRow.appendChild(hideMWIBtn);
buttonRow.appendChild(descriptionText);
hideMWIContainer.appendChild(buttonRow);
content.appendChild(hideMWIContainer);
hideMWIBtn.onclick = () => {
this.toggleMWIHide();
};
const farOutContainer = document.createElement('div');
this.applyClass(farOutContainer, 'mcs-container');
const farOutButtonRow = document.createElement('div');
this.applyClasses(farOutButtonRow, 'mcs-row', 'mcs-gap-10');
const farOutBtn = document.createElement('button');
farOutBtn.id = 'amazing-far-out-btn';
farOutBtn.textContent = 'Far Out';
this.applyClass(farOutBtn, 'mcs-btn');
const farOutDescriptionText = document.createElement('span');
this.applyClass(farOutDescriptionText, 'mcs-text-description');
farOutDescriptionText.textContent = 'This will be useful one day';
farOutButtonRow.appendChild(farOutBtn);
farOutButtonRow.appendChild(farOutDescriptionText);
farOutContainer.appendChild(farOutButtonRow);
content.appendChild(farOutContainer);
farOutBtn.onclick = () => {
this.farOutRandomizeColors();
};
}
toggleMWIHide() {
this.mwiHideEnabled = !this.mwiHideEnabled;
const btn = document.getElementById('amazing-hide-mwi-btn');
if (this.mwiHideEnabled) {
this.applyMWIHide();
btn.textContent = 'Show MWI';
btn.classList.add('mcs-btn-active');
} else {
this.removeMWIHide();
btn.textContent = 'Hide MWI';
btn.classList.remove('mcs-btn-active');
}
}
applyMWIHide() {
const suitePanel = document.getElementById('mwi-combat-suite-panel');
if (suitePanel && suitePanel.classList.contains('visible')) {
suitePanel.classList.remove('visible');
}
if (!document.getElementById('mwi-hide-overlay')) {
const overlay = document.createElement('div');
overlay.id = 'mwi-hide-overlay';
this.applyClass(overlay, 'mcs-overlay');
document.body.appendChild(overlay);
}
const elementsToKeep = [
'amazing-pane',
'bread-pane',
'dps-pane',
'consumables-pane',
'equipment-spy-pane',
'hwhat-pane',
'milt-loot-drops-display',
'milt-loot-drops-tooltip',
'mwi-hide-overlay',
'gwhiz-pane',
'truedps-pane',
'ihurt-pane',
'meaters-pane',
'mwi-combat-suite-btn',
'mwi-combat-suite-panel',
'jhouse-pane',
'jhouse-status-pane',
'opanel-pane',
'kollection-pane',
'kollection-side-panel',
'lucky-panel',
'lucky-options-panel',
'lucky-stats-panel',
'lucky-revenue-panel',
'lucky-big-expected-panel',
'lucky-big-luck-panel',
'lucky-totals-panel',
'mana-pane',
'mcs_nt_pane',
'pformance-pane',
'qcharm-pane',
'treasure-pane',
'treasure-chest-side-panel'
];
if (document.getElementById('mwi-profitwatch-panel')) {
elementsToKeep.push('mwi-profitwatch-panel');
}
Array.from(document.body.children).forEach(child => {
if (!elementsToKeep.includes(child.id)) {
if (!child.getAttribute('data-mwi-original-display')) {
child.setAttribute('data-mwi-original-display', child.style.display || 'block');
}
child.style.display = 'none';
}
});
elementsToKeep.forEach(id => {
const panel = document.getElementById(id);
if (panel && id !== 'mwi-hide-overlay') {
if (!panel.getAttribute('data-mwi-original-zindex')) {
panel.setAttribute('data-mwi-original-zindex', panel.style.zIndex || '');
}
panel.style.zIndex = '100001';
}
});
}
removeMWIHide() {
const overlay = document.getElementById('mwi-hide-overlay');
if (overlay) {
overlay.remove();
}
Array.from(document.body.children).forEach(child => {
const originalDisplay = child.getAttribute('data-mwi-original-display');
if (originalDisplay) {
child.style.display = originalDisplay;
child.removeAttribute('data-mwi-original-display');
}
const originalZIndex = child.getAttribute('data-mwi-original-zindex');
if (originalZIndex !== null) {
child.style.zIndex = originalZIndex;
child.removeAttribute('data-mwi-original-zindex');
}
});
}
makeAMazingDraggable(pane, header) {
DragHandler.makeDraggable(pane, header, 'mcs_AM');
}
farOutRandomizeColors() {
const farOutBtn = document.getElementById('amazing-far-out-btn');
if (this.farOutInterval) {
clearInterval(this.farOutInterval);
this.farOutInterval = null;
farOutBtn.textContent = 'FIX IT';
farOutBtn.classList.remove('mcs-btn-warning');
farOutBtn.classList.add('mcs-btn-alert');
return;
}
if (this.farOutSpans && this.farOutSpans.length > 0) {
const spans = document.querySelectorAll('.far-out-span');
spans.forEach(span => {
const textNode = document.createTextNode(span.textContent);
span.parentNode.replaceChild(textNode, span);
});
document.body.normalize();
this.farOutSpans = [];
farOutBtn.textContent = 'Far Out';
farOutBtn.classList.remove('mcs-btn-alert', 'mcs-btn-warning');
return;
}
this.farOutSpans = [];
const getRandomColor = () => {
const r = Math.floor(Math.random() * 256);
const g = Math.floor(Math.random() * 256);
const b = Math.floor(Math.random() * 256);
return `rgb(${r}, ${g}, ${b})`;
};
const isASCII = (char) => {
const code = char.charCodeAt(0);
return code >= 0 && code <= 127;
};
const colorizeText = (node) => {
if (node.nodeType === Node.ELEMENT_NODE && node.id === 'amazing-far-out-btn') {
return;
}
if (node.nodeType === Node.ELEMENT_NODE && node.querySelector('#amazing-far-out-btn')) {
const children = Array.from(node.childNodes);
children.forEach(child => {
if (child.nodeType === Node.ELEMENT_NODE) {
colorizeText(child);
}
});
return;
}
if (node.nodeType === Node.TEXT_NODE) {
const text = node.textContent;
if (!text || text.trim().length === 0) return;
let hasASCII = false;
for (let char of text) {
if (isASCII(char)) {
hasASCII = true;
break;
}
}
if (!hasASCII) return;
const fragment = document.createDocumentFragment();
for (let char of text) {
if (isASCII(char)) {
const span = document.createElement('span');
span.textContent = char;
span.style.color = getRandomColor();
span.classList.add('far-out-span');
this.farOutSpans.push(span);
fragment.appendChild(span);
} else {
fragment.appendChild(document.createTextNode(char));
}
}
node.parentNode.replaceChild(fragment, node);
} else if (node.nodeType === Node.ELEMENT_NODE) {
if (node.tagName === 'SCRIPT' || node.tagName === 'STYLE' ||
node.tagName === 'INPUT' || node.tagName === 'TEXTAREA') {
return;
}
const children = Array.from(node.childNodes);
children.forEach(child => colorizeText(child));
}
};
try {
colorizeText(document.body);
farOutBtn.textContent = 'MAKE IT STOP';
farOutBtn.classList.add('mcs-btn-warning');
VisibilityManager.register('amazing-farout', () => {
this.farOutSpans.forEach(span => {
span.style.color = getRandomColor();
});
}, 100);
} catch (e) {
console.error('[AMazing] Far Out - Error occurred:', e);
}
}
// AMazing end
// SCroll start
get scStorage() {
if (!this._scStorage) {
this._scStorage = createModuleStorage('SC');
}
return this._scStorage;
}
meatersHandleWebSocketMessage(event) {
if (window.MCS_MODULES_DISABLED) return;
const data = event.detail;
if (data?.type === 'battle_updated') {
this.handleMEatersBattleUpdate(data);
}
if (data?.type === 'new_battle') {
this.handleMEatersNewBattle(data);
}
}
createMEatersPane() {
if (document.getElementById('meaters-pane')) {
return;
}
this.meatersLog = [];
this.meatersEnemyLog = [];
this.meatersMaxLogEntries = 100;
this.meatersFilters = {
player1: true,
player2: true,
player3: true,
enemy: true,
showAbilities: false
};
const pane = document.createElement('div');
pane.id = 'meaters-pane';
this.applyClasses(pane, 'mcs-pane', 'mcs-pane-dark', 'mcs-meaters-pane');
const header = document.createElement('div');
this.applyClasses(header, 'mcs-pane-header', 'mcs-gap-10');
const titleSpan = document.createElement('span');
this.applyClass(titleSpan, 'mcs-pane-title');
titleSpan.textContent = 'MEaters - Combat Log (Compact)';
const filterContainer = document.createElement('div');
this.applyClasses(filterContainer, 'mcs-filter-container', 'mcs-gap-4');
const createToggleBtn = (text, filterKey) => {
const btn = document.createElement('button');
btn.textContent = text;
btn.dataset.filterKey = filterKey;
this.applyClass(btn, 'mcs-filter-btn');
const updateButtonStyle = () => {
if (this.meatersFilters[filterKey]) {
btn.classList.remove('inactive');
btn.classList.add('active');
} else {
btn.classList.remove('active');
btn.classList.add('inactive');
}
};
updateButtonStyle();
btn.onclick = (e) => {
e.stopPropagation();
this.meatersFilters[filterKey] = !this.meatersFilters[filterKey];
updateButtonStyle();
this.updateMEatersContent();
};
return btn;
};
const player1Btn = createToggleBtn('P1', 'player1');
const player2Btn = createToggleBtn('P2', 'player2');
const player3Btn = createToggleBtn('P3', 'player3');
const enemyBtn = createToggleBtn('Enemy', 'enemy');
const clownBtn = document.createElement('button');
clownBtn.textContent = '🤡';
this.applyClasses(clownBtn, 'mcs-filter-btn', 'inactive', 'mcs-font-14');
clownBtn.onclick = (e) => {
e.stopPropagation();
this.meatersFilters.showAbilities = !this.meatersFilters.showAbilities;
if (this.meatersFilters.showAbilities) {
clownBtn.classList.remove('inactive');
clownBtn.classList.add('active');
} else {
clownBtn.classList.remove('active');
clownBtn.classList.add('inactive');
}
this.updateMEatersContent();
};
const infoBtn = document.createElement('button');
infoBtn.textContent = 'ℹ️';
this.applyClasses(infoBtn, 'mcs-filter-btn', 'mcs-font-14');
infoBtn.onclick = (e) => {
e.stopPropagation();
this.showMEatersInfo();
};
filterContainer.appendChild(player1Btn);
filterContainer.appendChild(player2Btn);
filterContainer.appendChild(player3Btn);
filterContainer.appendChild(enemyBtn);
filterContainer.appendChild(clownBtn);
filterContainer.appendChild(infoBtn);
const buttonContainer = document.createElement('div');
this.applyClasses(buttonContainer, 'mcs-row', 'mcs-gap-6');
const clearBtn = document.createElement('button');
clearBtn.id = 'meaters-clear-btn';
clearBtn.textContent = 'Clear';
this.applyClasses(clearBtn, 'mcs-btn', 'mcs-btn-small');
const minimizeBtn = document.createElement('button');
minimizeBtn.id = 'meaters-minimize-btn';
minimizeBtn.textContent = '−';
this.applyClasses(minimizeBtn, 'mcs-btn', 'mcs-btn-minimize');
buttonContainer.appendChild(clearBtn);
buttonContainer.appendChild(minimizeBtn);
header.appendChild(titleSpan);
header.appendChild(filterContainer);
header.appendChild(buttonContainer);
const content = document.createElement('div');
content.id = 'meaters-content';
this.applyClasses(content, 'mcs-pane-content', 'mcs-pane-content-dark', 'mcs-meaters-content');
content.style.maxHeight = '400px';
content.style.minHeight = '200px';
pane.appendChild(header);
pane.appendChild(content);
document.body.appendChild(pane);
registerPanel('meaters-pane');
this.makeMEatersDraggable(pane, header);
this.meatersLog = [];
this.meatersMaxLogEntries = 100;
const savedMeatersMinimized = this.scStorage.get('minimized');
this.meatersIsMinimized = savedMeatersMinimized === true || savedMeatersMinimized === 'true';
if (this.meatersIsMinimized) {
minimizeBtn.textContent = '+';
content.classList.add('mcs-hidden');
header.classList.add('minimized');
}
minimizeBtn.onclick = () => {
this.meatersIsMinimized = !this.meatersIsMinimized;
if (this.meatersIsMinimized) {
content.classList.add('mcs-hidden');
minimizeBtn.textContent = '+';
header.classList.add('minimized');
this.scStorage.set('minimized', true);
} else {
content.classList.remove('mcs-hidden');
minimizeBtn.textContent = '−';
header.classList.remove('minimized');
this.scStorage.set('minimized', false);
this.constrainPanelToBoundaries('meaters-pane', 'mcs_SC', true);
}
};
clearBtn.onclick = () => {
this.meatersLog = [];
this.updateMEatersContent();
};
this._meatersWsListener = this.meatersHandleWebSocketMessage.bind(this);
window.addEventListener('EquipSpyWebSocketMessage', this._meatersWsListener);
this.updateMEatersContent();
}
showMEatersInfo() {
let infoDialog = document.getElementById('meaters-info-dialog');
if (infoDialog) {
infoDialog.remove();
return;
}
infoDialog = document.createElement('div');
infoDialog.id = 'meaters-info-dialog';
this.applyClass(infoDialog, 'mcs-meaters-info-dialog');
const content = document.createElement('div');
content.innerHTML = `
<div class="mcs-meaters-info-title mcs-text-success">MEaters Combat Log</div>
<div class="mcs-meaters-info-text">
<strong>Filter Buttons:</strong><br>
• <strong>P1, P2, P3</strong> - Toggle player attacks<br>
• <strong>Enemy</strong> - Toggle enemy attacks & heals<br>
• <strong>🤡</strong> - Toggle ability names (off by default)<br>
• <strong>ℹ️</strong> - Show this info<br><br>
<strong>Display:</strong><br>
• Left border = Player attacks (green)<br>
• Right border = Enemy attacks (red) or heals (green)<br>
• Format: timestamp:attacker:ability:damage:target
</div>
<button id="meaters-info-close" class="mcs-btn mcs-btn-small mcs-meaters-info-close">Close</button>
`;
infoDialog.appendChild(content);
document.body.appendChild(infoDialog);
document.getElementById('meaters-info-close').onclick = () => {
infoDialog.remove();
};
setTimeout(() => {
document.addEventListener('click', function closeInfo(e) {
if (!infoDialog.contains(e.target)) {
infoDialog.remove();
document.removeEventListener('click', closeInfo);
}
});
}, 100);
}
handleMEatersNewBattle(data) {
if (!this.meatersTracking) {
this.meatersTracking = {
monstersHP: [],
monstersDmgCounter: [],
monstersCritCounter: [],
playersMP: [],
players: [],
monsters: [],
monstersMP: [],
monstersPreparing: {},
playersHP: [],
playersDmgCounter: []
};
}
this.meatersTracking.monsters = data.monsters || [];
this.meatersTracking.players = data.players || [];
this.meatersTracking.monstersHP = data.monsters.map((monster) => monster.currentHitpoints);
this.meatersTracking.monstersDmgCounter = data.monsters.map((monster) => monster.damageSplatCounter);
this.meatersTracking.monstersCritCounter = data.monsters.map((monster) => monster.criticalHitCounter ?? 0);
this.meatersTracking.playersMP = data.players.map((player) => player.currentManapoints);
this.meatersTracking.monstersMP = data.monsters.map((monster) => monster.currentManapoints);
this.meatersTracking.playersHP = data.players.map((player) => player.currentHitpoints);
this.meatersTracking.playersDmgCounter = data.players.map((player) => player.damageSplatCounter);
this.meatersTracking.monstersPreparing = {};
}
getPlayerNumber(playerName) {
if (!this.meatersTracking || !this.meatersTracking.players) return '1';
for (let i = 0; i < this.meatersTracking.players.length; i++) {
const player = this.meatersTracking.players[i];
if (player && player.username === playerName) {
return (parseInt(i) + 1).toString();
}
}
return '1';
}
handleMEatersBattleUpdate(data) {
if (!this.meatersTracking || !this.meatersTracking.monstersHP || this.meatersTracking.monstersHP.length === 0) {
return;
}
const mMap = data.mMap;
const pMap = data.pMap;
const playerIndices = Object.keys(pMap);
if (!this.meatersTracking.monstersPreparing) {
this.meatersTracking.monstersPreparing = {};
}
Object.keys(mMap).forEach((mIndex) => {
const monster = mMap[mIndex];
if (!monster) return;
if (monster.preparingAbilityHrid) {
this.meatersTracking.monstersPreparing[mIndex] = monster.preparingAbilityHrid;
} else if (monster.isPreparingAutoAttack) {
this.meatersTracking.monstersPreparing[mIndex] = 'auto_attack';
}
});
let castPlayer = -1;
playerIndices.forEach((userIndex) => {
if (pMap[userIndex].cMP < this.meatersTracking.playersMP[userIndex]) {
castPlayer = userIndex;
}
this.meatersTracking.playersMP[userIndex] = pMap[userIndex].cMP;
});
let castMonster = -1;
Object.keys(mMap).forEach((mIndex) => {
const monster = mMap[mIndex];
if (!monster) return;
const prevMP = this.meatersTracking.monstersMP?.[mIndex];
if (prevMP !== undefined && monster.cMP < prevMP) {
castMonster = mIndex;
}
if (!this.meatersTracking.monstersMP) {
this.meatersTracking.monstersMP = [];
}
this.meatersTracking.monstersMP[mIndex] = monster.cMP;
});
this.meatersTracking.monstersHP.forEach((mHP, mIndex) => {
const monster = mMap[mIndex];
if (!monster) return;
const hpDiff = mHP - monster.cHP;
let dmgSplat = false;
if (this.meatersTracking.monstersDmgCounter[mIndex] < monster.dmgCounter) {
dmgSplat = true;
}
let isCrit = false;
if (this.meatersTracking.monstersCritCounter[mIndex] < monster.critCounter) {
isCrit = true;
}
this.meatersTracking.monstersHP[mIndex] = monster.cHP;
this.meatersTracking.monstersDmgCounter[mIndex] = monster.dmgCounter;
this.meatersTracking.monstersCritCounter[mIndex] = monster.critCounter;
const monsterName = this.meatersTracking.monsters[mIndex]?.name || `Monster ${mIndex + 1}`;
if (dmgSplat && playerIndices.length > 0) {
const isMiss = hpDiff === 0;
let playerIndex = castPlayer;
if (playerIndices.length === 1) {
playerIndex = playerIndices[0];
}
if (playerIndex !== -1 && this.meatersTracking.players[playerIndex]) {
const player = this.meatersTracking.players[playerIndex];
const playerName = (player.character && player.character.name) || player.username || `Player ${parseInt(playerIndex) + 1}`;
let abilityName = 'Unknown';
if (player.preparingAbilityHrid) {
abilityName = player.preparingAbilityHrid.split('/').pop().replace(/_/g, ' ');
} else if (player.isPreparingAutoAttack) {
abilityName = 'Auto Attack';
} else {
abilityName = 'Idle';
}
this.addMEatersLogEntry(playerName, monsterName, abilityName, hpDiff, isCrit, isMiss);
}
}
if (hpDiff < 0) {
const healAmount = Math.abs(hpDiff);
let healerName = 'Unknown Healer';
let healerIndex = -1;
let targetName = monsterName;
let abilityName = 'Unknown Heal';
if (castMonster !== -1 && this.meatersTracking.monsters[castMonster]) {
healerIndex = castMonster;
healerName = this.meatersTracking.monsters[castMonster].name;
}
else if (this.meatersTracking.monsters.length === 1) {
healerIndex = 0;
healerName = this.meatersTracking.monsters[0].name;
}
else {
healerIndex = mIndex;
healerName = monsterName;
}
if (healerIndex !== -1) {
const healerData = mMap[healerIndex];
if (healerData && healerData.preparingAbilityHrid) {
abilityName = healerData.preparingAbilityHrid.split('/').pop().replace(/_/g, ' ');
}
else if (this.meatersTracking.monstersPreparing && this.meatersTracking.monstersPreparing[healerIndex]) {
const storedAbility = this.meatersTracking.monstersPreparing[healerIndex];
abilityName = storedAbility.split('/').pop().replace(/_/g, ' ');
}
else {
abilityName = 'Regeneration';
}
}
this.addMEatersEnemyHealEntry(healerName, targetName, abilityName, healAmount);
}
});
if (!this.meatersTracking.playersDmgCounter) {
this.meatersTracking.playersDmgCounter = [];
Object.keys(pMap).forEach(pIndex => {
this.meatersTracking.playersDmgCounter[pIndex] = pMap[pIndex].dmgCounter ?? 0;
});
}
if (!this.meatersTracking.playersHP) {
this.meatersTracking.playersHP = [];
Object.keys(pMap).forEach(pIndex => {
this.meatersTracking.playersHP[pIndex] = pMap[pIndex].cHP;
});
}
Object.keys(pMap).forEach(pIndex => {
const player = pMap[pIndex];
if (!player) return;
const prevHP = this.meatersTracking.playersHP[pIndex];
if (prevHP === undefined) {
this.meatersTracking.playersHP[pIndex] = player.cHP;
return;
}
const hpDiff = prevHP - player.cHP;
let dmgSplat = false;
const prevDmgCounter = this.meatersTracking.playersDmgCounter[pIndex] ?? 0;
const currentDmgCounter = player.dmgCounter ?? 0;
if (currentDmgCounter > prevDmgCounter) {
dmgSplat = true;
}
this.meatersTracking.playersDmgCounter[pIndex] = currentDmgCounter;
if (dmgSplat && hpDiff > 0) {
let actualAttacker = -1;
Object.keys(mMap).forEach((mIndex) => {
});
}
if (dmgSplat && hpDiff > 0) {
let monsterName = null;
let attackingMonsterIndex = -1;
let abilityName = 'Unknown';
if (castMonster !== -1 && this.meatersTracking.monsters[castMonster]) {
attackingMonsterIndex = castMonster;
monsterName = this.meatersTracking.monsters[castMonster].name;
}
if (!monsterName && this.meatersTracking.monsters.length === 1) {
attackingMonsterIndex = 0;
monsterName = this.meatersTracking.monsters[0].name;
}
if (!monsterName) {
Object.keys(mMap).forEach((mIndex) => {
const monster = mMap[mIndex];
if (!monster) return;
if (this.meatersTracking.monstersDmgCounter[mIndex] === undefined) {
this.meatersTracking.monstersDmgCounter[mIndex] = monster.dmgCounter ?? 0;
}
const prevDmg = this.meatersTracking.monstersDmgCounter[mIndex];
const currentDmg = monster.dmgCounter ?? 0;
if (currentDmg > prevDmg) {
attackingMonsterIndex = mIndex;
if (this.meatersTracking.monsters[mIndex]) {
monsterName = this.meatersTracking.monsters[mIndex].name;
}
}
});
}
if (!monsterName) {
for (let i = 0; i < this.meatersTracking.monsters.length; i++) {
const monster = mMap[i];
if (monster && monster.cHP > 0) {
attackingMonsterIndex = i;
monsterName = this.meatersTracking.monsters[i].name;
break;
}
}
}
if (!monsterName) {
monsterName = 'Unknown Enemy';
}
if (attackingMonsterIndex !== -1) {
const monsterData = mMap[attackingMonsterIndex];
if (monsterData && monsterData.preparingAbilityHrid) {
abilityName = monsterData.preparingAbilityHrid.split('/').pop().replace(/_/g, ' ');
}
else if (this.meatersTracking.monstersPreparing && this.meatersTracking.monstersPreparing[attackingMonsterIndex]) {
const storedAbility = this.meatersTracking.monstersPreparing[attackingMonsterIndex];
if (storedAbility === 'auto_attack') {
abilityName = 'Auto Attack';
} else {
abilityName = storedAbility.split('/').pop().replace(/_/g, ' ');
}
}
else if (monsterData && monsterData.isPreparingAutoAttack) {
abilityName = 'Auto Attack';
}
else if (this.meatersTracking.monsters[attackingMonsterIndex]) {
const monsterInfo = this.meatersTracking.monsters[attackingMonsterIndex];
if (monsterInfo.combatAbilities && monsterInfo.combatAbilities.length > 0) {
abilityName = monsterInfo.combatAbilities[0].abilityHrid.split('/').pop().replace(/_/g, ' ');
} else {
abilityName = 'Basic Attack';
}
} else {
abilityName = 'Basic Attack';
}
}
const playerName = this.meatersTracking.players[pIndex]?.character?.name ||
this.meatersTracking.players[pIndex]?.username ||
`Player ${parseInt(pIndex) + 1}`;
const isMiss = false;
this.addMEatersEnemyLogEntry(monsterName, playerName, abilityName, hpDiff, false, isMiss);
}
if (dmgSplat && hpDiff === 0) {
let monsterName = null;
let attackingMonsterIndex = -1;
let abilityName = 'Unknown';
if (castMonster !== -1 && this.meatersTracking.monsters[castMonster]) {
attackingMonsterIndex = castMonster;
monsterName = this.meatersTracking.monsters[castMonster].name;
}
if (!monsterName && this.meatersTracking.monsters.length === 1) {
attackingMonsterIndex = 0;
monsterName = this.meatersTracking.monsters[0].name;
}
if (!monsterName) {
Object.keys(mMap).forEach((mIndex) => {
const monster = mMap[mIndex];
if (!monster) return;
if (this.meatersTracking.monstersDmgCounter[mIndex] === undefined) {
this.meatersTracking.monstersDmgCounter[mIndex] = monster.dmgCounter ?? 0;
}
const prevDmg = this.meatersTracking.monstersDmgCounter[mIndex];
const currentDmg = monster.dmgCounter ?? 0;
if (currentDmg > prevDmg) {
attackingMonsterIndex = mIndex;
if (this.meatersTracking.monsters[mIndex]) {
monsterName = this.meatersTracking.monsters[mIndex].name;
}
}
});
}
if (!monsterName) {
for (let i = 0; i < this.meatersTracking.monsters.length; i++) {
const monster = mMap[i];
if (monster && monster.cHP > 0) {
attackingMonsterIndex = i;
monsterName = this.meatersTracking.monsters[i].name;
break;
}
}
}
if (!monsterName) {
monsterName = 'Unknown Enemy';
}
if (attackingMonsterIndex !== -1) {
const monsterData = mMap[attackingMonsterIndex];
if (monsterData && monsterData.preparingAbilityHrid) {
abilityName = monsterData.preparingAbilityHrid.split('/').pop().replace(/_/g, ' ');
}
else if (this.meatersTracking.monstersPreparing && this.meatersTracking.monstersPreparing[attackingMonsterIndex]) {
const storedAbility = this.meatersTracking.monstersPreparing[attackingMonsterIndex];
if (storedAbility === 'auto_attack') {
abilityName = 'Auto Attack';
} else {
abilityName = storedAbility.split('/').pop().replace(/_/g, ' ');
}
}
else if (monsterData && monsterData.isPreparingAutoAttack) {
abilityName = 'Auto Attack';
}
else if (this.meatersTracking.monsters[attackingMonsterIndex]) {
const monsterInfo = this.meatersTracking.monsters[attackingMonsterIndex];
if (monsterInfo.combatAbilities && monsterInfo.combatAbilities.length > 0) {
abilityName = monsterInfo.combatAbilities[0].abilityHrid.split('/').pop().replace(/_/g, ' ');
} else {
abilityName = 'Basic Attack';
}
} else {
abilityName = 'Basic Attack';
}
}
const playerName = this.meatersTracking.players[pIndex]?.character?.name ||
this.meatersTracking.players[pIndex]?.username ||
`Player ${parseInt(pIndex) + 1}`;
this.addMEatersEnemyLogEntry(monsterName, playerName, abilityName, 0, false, true);
}
this.meatersTracking.playersHP[pIndex] = player.cHP;
});
}
addMEatersLogEntry(playerName, monsterName, abilityName, damage, isCrit, isMiss) {
const timestamp = new Date().toLocaleTimeString();
this.meatersLog.push({
timestamp,
playerName,
monsterName,
abilityName,
damage,
isCrit,
isMiss
});
if (this.meatersLog.length > this.meatersMaxLogEntries) {
this.meatersLog.shift();
}
this.updateMEatersContent();
}
addMEatersEnemyLogEntry(enemyName, playerName, abilityName, damage, isCrit, isMiss) {
const timestamp = new Date().toLocaleTimeString();
this.meatersEnemyLog.push({
timestamp,
enemyName,
playerName,
abilityName,
damage,
isCrit,
isMiss
});
if (this.meatersEnemyLog.length > this.meatersMaxLogEntries) {
this.meatersEnemyLog.shift();
}
this.updateMEatersContent();
}
addMEatersEnemyHealEntry(healerName, targetName, abilityName, healAmount) {
const timestamp = new Date().toLocaleTimeString();
this.meatersEnemyLog.push({
timestamp,
enemyName: healerName,
playerName: targetName,
abilityName,
damage: healAmount,
isCrit: false,
isMiss: false,
isHeal: true
});
if (this.meatersEnemyLog.length > this.meatersMaxLogEntries) {
this.meatersEnemyLog.shift();
}
this.updateMEatersContent();
}
updateMEatersContent() {
const content = document.getElementById('meaters-content');
if (!content) return;
if (this.meatersLog.length === 0 && this.meatersEnemyLog.length === 0) {
content.innerHTML = '<div class="mcs-meaters-empty">No combat activity yet...</div>';
return;
}
const allLogs = [
...this.meatersLog.map(entry => ({ ...entry, type: 'player' })),
...this.meatersEnemyLog.map(entry => ({ ...entry, type: 'enemy' }))
].sort((a, b) => {
return a.timestamp.localeCompare(b.timestamp);
});
const displayCount = 20;
const startIdx = Math.max(0, allLogs.length - displayCount);
let html = '';
for (let i = allLogs.length - 1; i >= startIdx; i--) {
const entry = allLogs[i];
if (entry.type === 'player') {
const playerIndex = this.meatersTracking.players?.findIndex(p => (p.character && p.character.name === entry.playerName) ||
p.username === entry.playerName
);
const playerSlot = playerIndex !== -1 ? playerIndex + 1 : 1;
if (playerSlot === 1 && !this.meatersFilters.player1) continue;
if (playerSlot === 2 && !this.meatersFilters.player2) continue;
if (playerSlot === 3 && !this.meatersFilters.player3) continue;
if (playerSlot > 3 && !this.meatersFilters.player1) continue;
} else if (entry.type === 'enemy') {
if (!this.meatersFilters.enemy) continue;
}
let borderColor = entry.type === 'player' ? '#90EE90' : '#FF6B6B';
let damageText = entry.damage.toString();
let damageColor = entry.type === 'player' ? '#90EE90' : '#FF6B6B';
if (entry.isMiss) {
borderColor = entry.type === 'player' ? '#FF6B6B' : '#90EE90';
damageColor = entry.type === 'player' ? '#FF6B6B' : '#90EE90';
damageText = 'MISS';
} else if (entry.isCrit) {
borderColor = '#FFD700';
damageColor = '#FFD700';
damageText = '⚡' + entry.damage;
}
if (entry.type === 'player') {
html += `<div class="mcs-meaters-log-entry mcs-meaters-log-player" style="border-left-color: ${borderColor};">`;
html += `<span class="mcs-meaters-timestamp">${entry.timestamp}</span>`;
html += `<span class="mcs-meaters-separator">:</span>`;
html += `<span class="mcs-meaters-player-name">${entry.playerName}</span>`;
if (this.meatersFilters.showAbilities) {
html += `<span class="mcs-meaters-separator">:</span>`;
html += `<span class="mcs-meaters-ability">${entry.abilityName}</span>`;
}
html += `<span class="mcs-meaters-separator">:</span>`;
html += `<span class="mcs-meaters-damage" style="color: ${damageColor};">${damageText}</span>`;
html += `<span class="mcs-meaters-separator">:</span>`;
html += `<span class="mcs-meaters-monster-name">${entry.monsterName}</span>`;
html += `</div>`;
} else {
const isHeal = entry.isHeal || false;
if (isHeal) {
borderColor = '#4CAF50';
damageColor = '#4CAF50';
damageText = '+' + entry.damage;
}
html += `<div class="mcs-meaters-log-entry mcs-meaters-log-enemy" style="border-right-color: ${borderColor};">`;
html += `<span class="mcs-meaters-timestamp">${entry.timestamp}</span>`;
html += `<span class="mcs-meaters-separator">:</span>`;
if (isHeal) {
html += `<span class="mcs-meaters-monster-name">${entry.enemyName}</span>`;
html += `<span class="mcs-meaters-separator">→</span>`;
html += `<span class="mcs-meaters-monster-name">${entry.playerName}</span>`;
} else {
html += `<span class="mcs-meaters-monster-name">${entry.enemyName}</span>`;
}
if (this.meatersFilters.showAbilities) {
html += `<span class="mcs-meaters-separator">:</span>`;
html += `<span class="mcs-meaters-ability">${entry.abilityName}</span>`;
}
html += `<span class="mcs-meaters-separator">:</span>`;
html += `<span class="mcs-meaters-damage" style="color: ${damageColor};">${damageText}</span>`;
if (!isHeal) {
html += `<span class="mcs-meaters-separator">:</span>`;
html += `<span class="mcs-meaters-player-name">${entry.playerName}</span>`;
}
html += `</div>`;
}
}
content.innerHTML = html;
}
makeMEatersDraggable(pane, header) {
DragHandler.makeDraggable(pane, header, 'mcs_SC');
}
destroySCroll() {
if (this._meatersWsListener) { window.removeEventListener('EquipSpyWebSocketMessage', this._meatersWsListener); this._meatersWsListener = null; }
const pane = document.getElementById('meaters-pane');
if (pane) pane.remove();
}
// SCroll end
// DPs start
get dpStorage() {
if (!this._dpStorage) {
this._dpStorage = createModuleStorage('DP');
}
return this._dpStorage;
}
dpsGetContent() {
if (!this._dpsCachedContent || !this._dpsCachedContent.isConnected) {
this._dpsCachedContent = document.getElementById('dps-content');
}
return this._dpsCachedContent;
}
dpsGetPlayerItems() {
const content = this.dpsGetContent();
if (!content) return [];
if (this._dpsCachedPlayerItems && this._dpsLastCacheVersion === this._dpsCacheVersion) {
return this._dpsCachedPlayerItems;
}
this._dpsCachedPlayerItems = content.querySelectorAll('.mcs-dps-player-item');
this._dpsLastCacheVersion = this._dpsCacheVersion;
return this._dpsCachedPlayerItems;
}
dpsInvalidatePlayerCache() {
if (!this._dpsCacheVersion) this._dpsCacheVersion = 0;
this._dpsCacheVersion++;
this._dpsCachedPlayerItems = null;
}
dpsHandleWebSocketMessage(event) {
if (window.MCS_MODULES_DISABLED) return;
const data = event.detail;
if (data?.type === 'new_battle') {
this.handleDPsNewBattle(data);
this.handleTrueDPSNewBattle(data);
}
if (data?.type === 'battle_updated') {
this.handleDPsBattleUpdate(data);
this.handleTrueDPSBattleUpdate(data);
}
if (data?.type === 'battle_ended') {
this.handleDPsBattleEnded(data);
}
}
createDPsPane() {
if (document.getElementById('dps-pane')) return;
const pane = document.createElement('div');
pane.id = 'dps-pane';
registerPanel('dps-pane');
pane.className = 'mcs-pane mcs-dps-pane';
const header = document.createElement('div');
header.className = 'mcs-pane-header mcs-dps-header';
const titleSpan = document.createElement('span');
titleSpan.id = 'dps-title-span';
titleSpan.className = 'mcs-pane-title mcs-dps-title';
titleSpan.innerHTML = 'DPs';
const inactivityTimer = document.createElement('span');
inactivityTimer.id = 'dps-inactivity-timer';
inactivityTimer.className = 'mcs-dps-inactivity-timer';
inactivityTimer.textContent = '0.000s';
const buttonContainer = document.createElement('div');
buttonContainer.className = 'mcs-dps-button-container';
if (this.dpsFilterNondamage === undefined) {
this.dpsFilterNondamage = true;
}
const minimizeBtn = document.createElement('button');
minimizeBtn.id = 'dps-minimize-btn';
minimizeBtn.textContent = '−';
minimizeBtn.className = 'mcs-btn mcs-btn-minimize';
minimizeBtn.onclick = () => {
this.dpsIsMinimized = !this.dpsIsMinimized;
const filterBtn = document.getElementById('dps-filter-btn');
const resetBtn = document.getElementById('dps-reset-btn');
if (this.dpsIsMinimized) {
minimizeBtn.textContent = '+';
if (filterBtn) filterBtn.classList.add('mcs-hidden');
if (resetBtn) resetBtn.classList.add('mcs-hidden');
const headerDiv = this.dpsGetContent()?.querySelector(':scope > div:first-child');
if (headerDiv) {
headerDiv.classList.add('mcs-hidden');
}
this.dpsGetPlayerItems().forEach(item => {
const abilityContainer = item.querySelector('.mcs-dps-ability-container');
if (abilityContainer) {
abilityContainer.classList.add('mcs-hidden');
}
const playerRow = item.querySelector('.mcs-dps-player-row');
if (playerRow) {
const children = Array.from(playerRow.children);
children.forEach((child, index) => {
if (index === 0 || index >= 3) {
child.classList.add('mcs-hidden');
}
});
playerRow.classList.add('minimized');
playerRow.onclick = null;
}
});
const truedpsSection = document.getElementById('dps-truedps-section');
if (truedpsSection) truedpsSection.classList.add('mcs-hidden');
header.classList.add('minimized');
this.dpStorage.set('minimized', true);
} else {
minimizeBtn.textContent = '-';
if (filterBtn) filterBtn.classList.remove('mcs-hidden');
if (resetBtn) resetBtn.classList.remove('mcs-hidden');
header.classList.remove('minimized');
this.dpStorage.set('minimized', false);
this.updateDPsDisplay();
const headerDiv = this.dpsGetContent()?.querySelector(':scope > div:first-child');
if (headerDiv) {
headerDiv.classList.remove('mcs-hidden');
}
this.dpsGetPlayerItems().forEach(item => {
const abilityContainer = item.querySelector('.mcs-dps-ability-container');
const playerRow = item.querySelector('.mcs-dps-player-row');
const expandArrow = item.querySelector('.mcs-dps-expand-arrow');
if (abilityContainer) {
const playerIndex = parseInt(item.getAttribute('data-player-index'));
const isExpanded = this.dpsTracking.expandedPlayers.has(playerIndex);
if (isExpanded) {
abilityContainer.classList.remove('mcs-hidden');
} else {
abilityContainer.classList.add('mcs-hidden');
}
}
if (playerRow) {
const children = Array.from(playerRow.children);
children.forEach(child => {
child.classList.remove('mcs-hidden');
});
playerRow.classList.remove('minimized');
const playerIndex = parseInt(item.getAttribute('data-player-index'));
playerRow.onclick = () => {
const isExpanded = !abilityContainer.classList.contains('mcs-hidden');
if (isExpanded) {
abilityContainer.classList.add('mcs-hidden');
if (expandArrow) expandArrow.textContent = '▶';
this.dpsTracking.expandedPlayers.delete(playerIndex);
} else {
abilityContainer.classList.remove('mcs-hidden');
if (expandArrow) expandArrow.textContent = '▼';
this.dpsTracking.expandedPlayers.add(playerIndex);
}
};
}
});
const truedpsSection = document.getElementById('dps-truedps-section');
if (truedpsSection) truedpsSection.classList.remove('mcs-hidden');
this.constrainPanelToBoundaries('dps-pane', 'mcs_DP', true);
}
};
const filterBtn = document.createElement('button');
filterBtn.id = 'dps-filter-btn';
filterBtn.textContent = this.dpsFilterNondamage ? 'Filter Nondamage: Enabled' : 'Filter Nondamage: Disabled';
filterBtn.className = 'mcs-dps-filter-btn';
if (this.dpsFilterNondamage) {
filterBtn.classList.add('enabled');
}
filterBtn.onclick = () => {
this.dpsFilterNondamage = !this.dpsFilterNondamage;
if (this.dpsFilterNondamage) {
filterBtn.classList.add('enabled');
} else {
filterBtn.classList.remove('enabled');
}
filterBtn.textContent = this.dpsFilterNondamage ? 'Filter Nondamage: Enabled' : 'Filter Nondamage: Disabled';
};
const resetBtn = document.createElement('button');
resetBtn.id = 'dps-reset-btn';
resetBtn.textContent = 'Reset';
resetBtn.className = 'mcs-dps-reset-btn';
buttonContainer.appendChild(filterBtn);
buttonContainer.appendChild(resetBtn);
buttonContainer.appendChild(minimizeBtn);
header.appendChild(titleSpan);
header.appendChild(inactivityTimer);
header.appendChild(buttonContainer);
const content = document.createElement('div');
content.id = 'dps-content';
content.className = 'mcs-dps-content';
pane.appendChild(header);
pane.appendChild(content);
document.body.appendChild(pane);
this.makeDPsDraggable(pane, header);
this.dpsTracking = {
players: [],
monsters: [],
totalDamage: [],
monstersHP: [],
monstersDmgCounter: [],
monstersCritCounter: [],
playersMP: [],
startTime: null,
endTime: null,
totalDuration: 0,
lastCombatUpdate: null,
expandedPlayers: new Set(),
expandedMonsters: new Map(),
filteredDamage: [],
unfilteredDamage: [],
savedPausedMs: window.MCS_TOTAL_PAUSED_MS ?? 0
};
this.trueDPSTracking = {
startTime: Date.now(),
enemyKills: {},
monsterMaxHP: {},
monstersHP: [],
monstersDmgCounter: [],
monsters: [],
savedPausedMs: window.MCS_TOTAL_PAUSED_MS ?? 0
};
this.updateDPsContent();
resetBtn.onclick = () => {
this.dpsTracking = {
players: [],
monsters: [],
totalDamage: [],
monstersHP: [],
monstersDmgCounter: [],
monstersCritCounter: [],
playersMP: [],
startTime: null,
endTime: null,
totalDuration: 0,
lastCombatUpdate: null,
expandedPlayers: new Set(),
expandedMonsters: new Map(),
filteredDamage: [],
unfilteredDamage: [],
savedPausedMs: window.MCS_TOTAL_PAUSED_MS ?? 0
};
this.trueDPSTracking = {
startTime: Date.now(),
enemyKills: {},
monsterMaxHP: {},
monstersHP: [],
monstersDmgCounter: [],
monsters: [],
savedPausedMs: window.MCS_TOTAL_PAUSED_MS ?? 0
};
if (this.dpsStateCache) this.dpsStateCache.clear();
this.updateDPsContent();
};
if (!this._dpsWsListener) {
this._dpsWsListener = this.dpsHandleWebSocketMessage.bind(this);
window.addEventListener('EquipSpyWebSocketMessage', this._dpsWsListener);
}
VisibilityManager.register('dps-update', () => {
this.updateDPsDisplay();
}, 2000);
VisibilityManager.register('dps-inactivity-timer', () => {
this.updateInactivityTimer();
}, 100);
const savedDpsMinimized = this.dpStorage.get('minimized');
this.dpsIsMinimized = savedDpsMinimized === true || savedDpsMinimized === 'true';
if (this.dpsIsMinimized) {
minimizeBtn.textContent = '+';
header.classList.add('minimized');
const filterBtn = document.getElementById('dps-filter-btn');
const infoBtn = document.getElementById('dps-info-btn');
const resetBtn = document.getElementById('dps-reset-btn');
if (filterBtn) filterBtn.classList.add('mcs-hidden');
if (infoBtn) infoBtn.classList.add('mcs-hidden');
if (resetBtn) resetBtn.classList.add('mcs-hidden');
setTimeout(() => {
const headerDiv = this.dpsGetContent()?.querySelector(':scope > div:first-child');
if (headerDiv && headerDiv.classList.contains('mcs-dps-header-row')) {
headerDiv.classList.add('mcs-hidden');
}
this.dpsGetPlayerItems().forEach(item => {
const abilityContainer = item.querySelector('.mcs-dps-ability-container');
if (abilityContainer) abilityContainer.classList.add('mcs-hidden');
const playerRow = item.querySelector('.mcs-dps-player-row');
if (playerRow) {
const children = Array.from(playerRow.children);
children.forEach((child, index) => {
if (index === 0 || index >= 3) {
child.classList.add('mcs-hidden');
}
});
playerRow.classList.add('minimized');
playerRow.onclick = null;
}
});
const truedpsSection = document.getElementById('dps-truedps-section');
if (truedpsSection) truedpsSection.classList.add('mcs-hidden');
}, 150);
}
}
isNonDamagingAbility(abilityHrid) {
const nonDamagingAbilities = [
'/abilities/minor_heal',
'/abilities/heal',
'/abilities/quick_aid',
'/abilities/rejuvenate',
'/abilities/taunt',
'/abilities/provoke',
'/abilities/toughness',
'/abilities/elusiveness',
'/abilities/precision',
'/abilities/berserk',
'/abilities/elemental_affinity',
'/abilities/frenzy',
'/abilities/vampirism',
'/abilities/revive',
'/abilities/insanity',
'/abilities/invincible',
'/abilities/speed_aura',
'/abilities/critical_aura',
'/abilities/guardian_aura',
'/abilities/fierce_aura',
'/abilities/mystic_aura',
'/abilities/spike_shell'
];
return nonDamagingAbilities.includes(abilityHrid);
}
handleDPsNewBattle(data) {
let isNewSession = false;
if (this.dpsTracking.lastCombatUpdate) {
const timeSinceLastUpdate = (Date.now() - this.dpsTracking.lastCombatUpdate) / 1000;
if (timeSinceLastUpdate > 5) {
isNewSession = true;
}
}
this.dpsTracking.lastCombatUpdate = Date.now();
if (isNewSession) {
this.dpsTracking = {
players: data.players,
monsters: [],
totalDamage: new Array(data.players.length).fill(0),
filteredDamage: new Array(data.players.length).fill(0),
unfilteredDamage: new Array(data.players.length).fill(0),
monstersHP: [],
monstersDmgCounter: [],
monstersCritCounter: [],
playersMP: [],
startTime: Date.now(),
endTime: null,
totalDuration: 0,
lastCombatUpdate: Date.now(),
expandedPlayers: this.dpsTracking.expandedPlayers || new Set(),
savedPausedMs: window.MCS_TOTAL_PAUSED_MS ?? 0
};
} else {
if (this.dpsTracking.startTime) {
this.dpsTracking.totalDuration = mcsGetElapsedSeconds(
this.dpsTracking.startTime,
this.dpsTracking.endTime,
this.dpsTracking.savedPausedMs,
this.dpsTracking.totalDuration
);
}
this.dpsTracking.savedPausedMs = window.MCS_TOTAL_PAUSED_MS ?? 0;
this.dpsTracking.startTime = Date.now();
}
this.dpsTracking.endTime = null;
this.dpsTracking.monstersHP = data.monsters.map((monster) => monster.currentHitpoints);
this.dpsTracking.monstersDmgCounter = data.monsters.map((monster) => monster.damageSplatCounter);
this.dpsTracking.monstersCritCounter = data.monsters.map((monster) => monster.criticalHitCounter ?? 0);
this.dpsTracking.playersMP = data.players.map((player) => player.currentManapoints);
this.dpsTracking.monsters = data.monsters;
if (!this.dpsTracking.players || this.dpsTracking.players.length === 0) {
this.dpsTracking.players = data.players;
}
if (!this.dpsTracking.totalDamage.length) {
this.dpsTracking.totalDamage = new Array(this.dpsTracking.players.length).fill(0);
}
if (!this.dpsTracking.filteredDamage.length) {
this.dpsTracking.filteredDamage = new Array(this.dpsTracking.players.length).fill(0);
}
if (!this.dpsTracking.unfilteredDamage.length) {
this.dpsTracking.unfilteredDamage = new Array(this.dpsTracking.players.length).fill(0);
}
const playerIndices = Object.keys(this.dpsTracking.players);
playerIndices.forEach((userIndex) => {
this.dpsTracking.players[userIndex].currentAction = this.dpsTracking.players[userIndex].preparingAbilityHrid
? this.dpsTracking.players[userIndex].preparingAbilityHrid
: this.dpsTracking.players[userIndex].isPreparingAutoAttack
? "auto"
: "idle";
});
setTimeout(() => this.updateDPsContent(), 100);
}
handleDPsBattleUpdate(data) {
if (!this.dpsTracking.monstersHP || this.dpsTracking.monstersHP.length === 0) {
return;
}
this.dpsTracking.lastCombatUpdate = Date.now();
const mMap = data.mMap;
const pMap = data.pMap;
const playerIndices = Object.keys(pMap);
let castPlayer = -1;
playerIndices.forEach((userIndex) => {
if (pMap[userIndex].cMP < this.dpsTracking.playersMP[userIndex]) {
castPlayer = userIndex;
}
this.dpsTracking.playersMP[userIndex] = pMap[userIndex].cMP;
});
this.dpsTracking.monstersHP.forEach((mHP, mIndex) => {
const monster = mMap[mIndex];
if (monster) {
const hpDiff = mHP - monster.cHP;
let dmgSplat = false;
if (this.dpsTracking.monstersDmgCounter[mIndex] < monster.dmgCounter) {
dmgSplat = true;
}
let isCrit = false;
if (this.dpsTracking.monstersCritCounter[mIndex] < monster.critCounter) {
isCrit = true;
}
this.dpsTracking.monstersHP[mIndex] = monster.cHP;
this.dpsTracking.monstersDmgCounter[mIndex] = monster.dmgCounter;
this.dpsTracking.monstersCritCounter[mIndex] = monster.critCounter;
const monsterName = this.dpsTracking.monsters[mIndex]?.name || `Monster ${mIndex + 1}`;
if (dmgSplat && playerIndices.length > 0) {
const isMiss = hpDiff === 0;
if (playerIndices.length > 1) {
playerIndices.forEach((userIndex) => {
if (userIndex === castPlayer) {
const currentAction = this.dpsTracking.players[userIndex].currentAction;
this.dpsTracking.unfilteredDamage[userIndex] += hpDiff;
const isNonDamaging = this.isNonDamagingAbility(currentAction);
if (this.dpsFilterNondamage && isNonDamaging) {
return;
}
if (!isNonDamaging) {
this.dpsTracking.filteredDamage[userIndex] += hpDiff;
}
if (!this.dpsTracking.players[userIndex].damageMap) {
this.dpsTracking.players[userIndex].damageMap = new Map();
}
if (!this.dpsTracking.players[userIndex].hitStatsMap) {
this.dpsTracking.players[userIndex].hitStatsMap = new Map();
}
if (!this.dpsTracking.players[userIndex].monsterDamageMap) {
this.dpsTracking.players[userIndex].monsterDamageMap = new Map();
}
if (!this.dpsTracking.players[userIndex].monsterHitStatsMap) {
this.dpsTracking.players[userIndex].monsterHitStatsMap = new Map();
}
const currentDamage = this.dpsTracking.players[userIndex].damageMap.get(currentAction) ?? 0;
this.dpsTracking.players[userIndex].damageMap.set(currentAction, currentDamage + hpDiff);
const stats = this.dpsTracking.players[userIndex].hitStatsMap.get(currentAction) ?? {
attacks: 0,
hits: 0,
crits: 0,
misses: 0
};
stats.attacks++;
if (isMiss) {
stats.misses++;
} else if (isCrit) {
stats.crits++;
} else {
stats.hits++;
}
this.dpsTracking.players[userIndex].hitStatsMap.set(currentAction, stats);
if (!this.dpsTracking.players[userIndex].monsterDamageMap.has(monsterName)) {
this.dpsTracking.players[userIndex].monsterDamageMap.set(monsterName, new Map());
}
if (!this.dpsTracking.players[userIndex].monsterHitStatsMap.has(monsterName)) {
this.dpsTracking.players[userIndex].monsterHitStatsMap.set(monsterName, new Map());
}
const monsterDmgMap = this.dpsTracking.players[userIndex].monsterDamageMap.get(monsterName);
const currentMonsterDamage = monsterDmgMap.get(currentAction) ?? 0;
monsterDmgMap.set(currentAction, currentMonsterDamage + hpDiff);
const monsterStatsMap = this.dpsTracking.players[userIndex].monsterHitStatsMap.get(monsterName);
const monsterStats = monsterStatsMap.get(currentAction) ?? {
attacks: 0,
hits: 0,
crits: 0,
misses: 0
};
monsterStats.attacks++;
if (isMiss) {
monsterStats.misses++;
} else if (isCrit) {
monsterStats.crits++;
} else {
monsterStats.hits++;
}
monsterStatsMap.set(currentAction, monsterStats);
this.dpsTracking.totalDamage[userIndex] += hpDiff;
}
});
} else {
const userIndex = playerIndices[0];
const currentAction = this.dpsTracking.players[userIndex].currentAction;
this.dpsTracking.unfilteredDamage[userIndex] += hpDiff;
const isNonDamaging = this.isNonDamagingAbility(currentAction);
if (this.dpsFilterNondamage && isNonDamaging) {
return;
}
if (!isNonDamaging) {
this.dpsTracking.filteredDamage[userIndex] += hpDiff;
}
if (!this.dpsTracking.players[userIndex].damageMap) {
this.dpsTracking.players[userIndex].damageMap = new Map();
}
if (!this.dpsTracking.players[userIndex].hitStatsMap) {
this.dpsTracking.players[userIndex].hitStatsMap = new Map();
}
if (!this.dpsTracking.players[userIndex].monsterDamageMap) {
this.dpsTracking.players[userIndex].monsterDamageMap = new Map();
}
if (!this.dpsTracking.players[userIndex].monsterHitStatsMap) {
this.dpsTracking.players[userIndex].monsterHitStatsMap = new Map();
}
const currentDamage = this.dpsTracking.players[userIndex].damageMap.get(currentAction) ?? 0;
this.dpsTracking.players[userIndex].damageMap.set(currentAction, currentDamage + hpDiff);
const stats = this.dpsTracking.players[userIndex].hitStatsMap.get(currentAction) ?? {
attacks: 0,
hits: 0,
crits: 0,
misses: 0
};
stats.attacks++;
if (isMiss) {
stats.misses++;
} else if (isCrit) {
stats.crits++;
} else {
stats.hits++;
}
this.dpsTracking.players[userIndex].hitStatsMap.set(currentAction, stats);
if (!this.dpsTracking.players[userIndex].monsterDamageMap.has(monsterName)) {
this.dpsTracking.players[userIndex].monsterDamageMap.set(monsterName, new Map());
}
if (!this.dpsTracking.players[userIndex].monsterHitStatsMap.has(monsterName)) {
this.dpsTracking.players[userIndex].monsterHitStatsMap.set(monsterName, new Map());
}
const monsterDmgMap = this.dpsTracking.players[userIndex].monsterDamageMap.get(monsterName);
const currentMonsterDamage = monsterDmgMap.get(currentAction) ?? 0;
monsterDmgMap.set(currentAction, currentMonsterDamage + hpDiff);
const monsterStatsMap = this.dpsTracking.players[userIndex].monsterHitStatsMap.get(monsterName);
const monsterStats = monsterStatsMap.get(currentAction) ?? {
attacks: 0,
hits: 0,
crits: 0,
misses: 0
};
monsterStats.attacks++;
if (isMiss) {
monsterStats.misses++;
} else if (isCrit) {
monsterStats.crits++;
} else {
monsterStats.hits++;
}
monsterStatsMap.set(currentAction, monsterStats);
this.dpsTracking.totalDamage[userIndex] += hpDiff;
}
}
}
});
playerIndices.forEach((userIndex) => {
this.dpsTracking.players[userIndex].currentAction = pMap[userIndex].abilityHrid
? pMap[userIndex].abilityHrid
: pMap[userIndex].isAutoAtk
? "auto"
: "idle";
});
this.dpsTracking.endTime = Date.now();
setTimeout(() => this.updateDPsDisplay(), 50);
}
handleDPsBattleEnded(data) {
this.dpsTracking.endTime = Date.now();
setTimeout(() => this.updateDPsDisplay(), 100);
}
updateDPsContent() {
const content = this.dpsGetContent();
if (!content) return;
this.dpsInvalidatePlayerCache();
let players = this.dpsTracking.players;
const savedMinimized = this.dpStorage.get('minimized');
const isMinimized = savedMinimized === true || savedMinimized === 'true';
if (this.dpsIsMinimized !== isMinimized) {
this.dpsIsMinimized = isMinimized;
}
if (!players || players.length === 0) {
const padding = this.dpsIsMinimized ? '10px 20px' : '40px 20px';
const iconSize = this.dpsIsMinimized ? '16px' : '32px';
const textSize = this.dpsIsMinimized ? '11px' : '13px';
let waitingDiv = content.querySelector('.mcs-dps-waiting');
if (!waitingDiv) {
waitingDiv = document.createElement('div');
waitingDiv.className = `mcs-dps-waiting ${this.dpsIsMinimized ? 'minimized' : ''}`;
const iconDiv = document.createElement('div');
iconDiv.className = `mcs-dps-waiting-icon ${this.dpsIsMinimized ? 'minimized' : ''}`;
iconDiv.textContent = '⏳';
const textDiv = document.createElement('div');
textDiv.className = `mcs-dps-waiting-text ${this.dpsIsMinimized ? 'minimized' : ''}`;
textDiv.textContent = 'Waiting for first battle to start';
waitingDiv.appendChild(iconDiv);
waitingDiv.appendChild(textDiv);
content.innerHTML = '';
content.appendChild(waitingDiv);
}
return;
}
const existingWaiting = content.querySelector('.mcs-dps-waiting');
if (existingWaiting) {
existingWaiting.remove();
}
let headerDiv = content.querySelector('.mcs-dps-header-row');
if (!headerDiv) {
headerDiv = document.createElement('div');
headerDiv.className = 'mcs-dps-header-row';
const charHeader = document.createElement('div');
charHeader.className = 'mcs-dps-header-col mcs-dps-header-char';
charHeader.textContent = 'Character / Ability';
const dpsHeader = document.createElement('div');
dpsHeader.className = 'mcs-dps-header-col mcs-dps-header-dps';
dpsHeader.textContent = 'DPS';
const damageHeader = document.createElement('div');
damageHeader.className = 'mcs-dps-header-col mcs-dps-header-damage';
damageHeader.textContent = 'Damage';
const attacksHeader = document.createElement('div');
attacksHeader.className = 'mcs-dps-header-col mcs-dps-header-atks';
attacksHeader.textContent = 'Atks';
const hitsHeader = document.createElement('div');
hitsHeader.className = 'mcs-dps-header-col mcs-dps-header-hits';
hitsHeader.textContent = 'Hit';
const critsHeader = document.createElement('div');
critsHeader.className = 'mcs-dps-header-col mcs-dps-header-crits';
critsHeader.textContent = 'Crit';
const missesHeader = document.createElement('div');
missesHeader.className = 'mcs-dps-header-col mcs-dps-header-misses';
missesHeader.textContent = 'Miss';
headerDiv.appendChild(charHeader);
headerDiv.appendChild(dpsHeader);
headerDiv.appendChild(damageHeader);
headerDiv.appendChild(attacksHeader);
headerDiv.appendChild(hitsHeader);
headerDiv.appendChild(critsHeader);
headerDiv.appendChild(missesHeader);
content.appendChild(headerDiv);
}
if (this.dpsIsMinimized) {
headerDiv.classList.add('mcs-hidden');
} else {
headerDiv.classList.remove('mcs-hidden');
}
const currentUserName = this.dpsTracking.currentUserName || window.playerName || '';
players = [...players].sort((a, b) => {
if (!a || !b) return 0;
const aIsCurrentUser = a.name === currentUserName;
const bIsCurrentUser = b.name === currentUserName;
if (aIsCurrentUser && !bIsCurrentUser) return -1;
if (!aIsCurrentUser && bIsCurrentUser) return 1;
return 0;
});
const validPlayers = players.filter(p => p !== null && p !== undefined);
validPlayers.forEach((player, index) => {
const playerIndex = players.indexOf(player);
let itemDiv = content.querySelector(`[data-player-index="${playerIndex}"]`);
if (!itemDiv) {
itemDiv = document.createElement('div');
itemDiv.className = 'mcs-dps-player-item';
itemDiv.setAttribute('data-player-index', playerIndex);
const playerRow = document.createElement('div');
playerRow.className = 'mcs-dps-player-row';
const expandArrow = document.createElement('div');
expandArrow.className = 'mcs-dps-expand-arrow';
const nameLabel = document.createElement('div');
nameLabel.className = 'mcs-dps-name-label';
const dpsLabel = document.createElement('div');
dpsLabel.className = 'mcs-dps-dps-label';
const damageLabel = document.createElement('div');
damageLabel.className = 'mcs-dps-damage-label';
const totalAttacksLabel = document.createElement('div');
totalAttacksLabel.className = 'mcs-dps-total-attacks-label';
const totalHitsLabel = document.createElement('div');
totalHitsLabel.className = 'mcs-dps-total-hits-label';
const totalCritsLabel = document.createElement('div');
totalCritsLabel.className = 'mcs-dps-total-crits-label';
const totalMissesLabel = document.createElement('div');
totalMissesLabel.className = 'mcs-dps-total-misses-label';
playerRow.appendChild(expandArrow);
playerRow.appendChild(nameLabel);
playerRow.appendChild(dpsLabel);
playerRow.appendChild(damageLabel);
playerRow.appendChild(totalAttacksLabel);
playerRow.appendChild(totalHitsLabel);
playerRow.appendChild(totalCritsLabel);
playerRow.appendChild(totalMissesLabel);
itemDiv.appendChild(playerRow);
const abilityContainer = document.createElement('div');
abilityContainer.className = 'mcs-dps-ability-container';
itemDiv.appendChild(abilityContainer);
content.appendChild(itemDiv);
}
const playerRow = itemDiv.querySelector('.mcs-dps-player-row');
const expandArrow = itemDiv.querySelector('.mcs-dps-expand-arrow');
const nameLabel = itemDiv.querySelector('.mcs-dps-name-label');
const abilityContainer = itemDiv.querySelector('.mcs-dps-ability-container');
if (this.dpsIsMinimized) {
playerRow.classList.add('minimized');
} else {
playerRow.classList.remove('minimized');
}
const isLastPlayer = index === validPlayers.length - 1;
if (!isLastPlayer) {
itemDiv.classList.add('bordered');
} else {
itemDiv.classList.remove('bordered');
}
if (this.dpsIsMinimized) {
expandArrow.classList.add('mcs-hidden');
} else {
expandArrow.classList.remove('mcs-hidden');
}
const isPlayerExpanded = this.dpsTracking.expandedPlayers.has(playerIndex);
expandArrow.textContent = isPlayerExpanded ? '▼' : '▶';
if (!isPlayerExpanded) {
abilityContainer.classList.add('mcs-hidden');
} else {
abilityContainer.classList.remove('mcs-hidden');
}
if (!this.dpsIsMinimized) {
playerRow.onclick = () => {
const isExpanded = !abilityContainer.classList.contains('mcs-hidden');
if (isExpanded) {
abilityContainer.classList.add('mcs-hidden');
expandArrow.textContent = '▶';
this.dpsTracking.expandedPlayers.delete(playerIndex);
} else {
abilityContainer.classList.remove('mcs-hidden');
expandArrow.textContent = '▼';
this.dpsTracking.expandedPlayers.add(playerIndex);
}
};
} else {
playerRow.onclick = null;
}
nameLabel.textContent = player.name || 'Unknown';
const damageLabel = itemDiv.querySelector('.mcs-dps-damage-label');
const totalAttacksLabel = itemDiv.querySelector('.mcs-dps-total-attacks-label');
const totalHitsLabel = itemDiv.querySelector('.mcs-dps-total-hits-label');
const totalCritsLabel = itemDiv.querySelector('.mcs-dps-total-crits-label');
const totalMissesLabel = itemDiv.querySelector('.mcs-dps-total-misses-label');
if (this.dpsIsMinimized) {
damageLabel.classList.add('mcs-hidden');
totalAttacksLabel.classList.add('mcs-hidden');
totalHitsLabel.classList.add('mcs-hidden');
totalCritsLabel.classList.add('mcs-hidden');
totalMissesLabel.classList.add('mcs-hidden');
} else {
damageLabel.classList.remove('mcs-hidden');
totalAttacksLabel.classList.remove('mcs-hidden');
totalHitsLabel.classList.remove('mcs-hidden');
totalCritsLabel.classList.remove('mcs-hidden');
totalMissesLabel.classList.remove('mcs-hidden');
}
});
if (this.dpsIsMinimized) {
this.dpsGetPlayerItems().forEach(item => {
const abilityContainer = item.querySelector('.mcs-dps-ability-container');
if (abilityContainer) {
abilityContainer.classList.add('mcs-hidden');
}
});
}
this.updateDPsDisplay();
}
updateCellIfChanged(element, newValue, cacheKey, useHTML = false) {
if (!this.dpsStateCache) {
this.dpsStateCache = new Map();
}
const cached = this.dpsStateCache.get(cacheKey);
if (cached !== newValue) {
if (useHTML) {
element.innerHTML = newValue;
} else {
element.textContent = newValue;
}
this.dpsStateCache.set(cacheKey, newValue);
return true;
}
return false;
}
updateInactivityTimer() {
const pane = document.getElementById('dps-pane');
if (!pane || pane.classList.contains('mcs-hidden')) return;
const timerElement = document.getElementById('dps-inactivity-timer');
if (!timerElement) return;
if (!this.dpsTracking.lastCombatUpdate) {
timerElement.textContent = '0.000s';
return;
}
const timeSinceLastUpdate = (Date.now() - this.dpsTracking.lastCombatUpdate) / 1000;
timerElement.textContent = timeSinceLastUpdate.toFixed(3) + 's';
}
updateDPsDisplay() {
const players = this.dpsTracking.players;
const totalDamage = this.dpsTracking.totalDamage;
if (!players || players.length === 0) {
const content = this.dpsGetContent();
if (content && content.children.length === 0) {
const padding = this.dpsIsMinimized ? '10px 20px' : '40px 20px';
const iconSize = this.dpsIsMinimized ? '16px' : '32px';
const textSize = this.dpsIsMinimized ? '11px' : '13px';
const waitingDiv = document.createElement('div');
waitingDiv.className = `mcs-dps-waiting ${this.dpsIsMinimized ? 'minimized' : ''}`;
const iconDiv = document.createElement('div');
iconDiv.className = `mcs-dps-waiting-icon ${this.dpsIsMinimized ? 'minimized' : ''}`;
iconDiv.textContent = '⏳';
const textDiv = document.createElement('div');
textDiv.className = `mcs-dps-waiting-text ${this.dpsIsMinimized ? 'minimized' : ''}`;
textDiv.textContent = 'Waiting for first battle to start';
waitingDiv.appendChild(iconDiv);
waitingDiv.appendChild(textDiv);
content.innerHTML = '';
content.appendChild(waitingDiv);
}
return;
}
let totalTime = this.dpsTracking.totalDuration;
if (this.dpsTracking.startTime) {
totalTime = mcsGetElapsedSeconds(this.dpsTracking.startTime, this.dpsTracking.endTime, this.dpsTracking.savedPausedMs, totalTime);
}
const totalGroupDamage = totalDamage.reduce((acc, dmg) => acc + dmg, 0);
const overallGroupDPS = totalTime > 0 ? (totalGroupDamage / totalTime).toFixed(1) : '0.0';
const titleSpan = document.getElementById('dps-title-span');
if (titleSpan) {
const titleHTML = `DPs <span class="mcs-dps-title-highlight">${overallGroupDPS}</span> <span class="mcs-dps-title-subtitle">Total Damage: ${this.formatDPsNumber(totalGroupDamage)}</span>`;
this.updateCellIfChanged(titleSpan, titleHTML, 'title', true);
}
if (this.dpsIsMinimized) {
const playerItems = this.dpsGetPlayerItems();
playerItems.forEach((item, index) => {
const dpsLabel = item.querySelector('.mcs-dps-dps-label');
if (!dpsLabel) return;
const damage = totalDamage[index] ?? 0;
const dps = totalTime > 0 ? (damage / totalTime).toFixed(1) : '0.0';
let accuracyStr = '--';
if (players[index] && players[index].hitStatsMap) {
let totalAttacks = 0;
let totalMisses = 0;
players[index].hitStatsMap.forEach((stats) => {
totalAttacks += stats.attacks ?? 0;
totalMisses += stats.misses ?? 0;
});
if (totalAttacks > 0) {
const missPercent = (totalMisses / totalAttacks) * 100;
accuracyStr = (100 - missPercent).toFixed(1) + '%';
}
}
const dpsHTML = `${dps} <span class="mcs-dps-accuracy-span">${accuracyStr}</span>`;
this.updateCellIfChanged(dpsLabel, dpsHTML, `min_dps_${index}`, true);
});
return;
}
const playerItems = this.dpsGetPlayerItems();
playerItems.forEach((item, index) => {
const dpsLabel = item.querySelector('.mcs-dps-dps-label');
const damageLabel = item.querySelector('.mcs-dps-damage-label');
const totalAttacksLabel = item.querySelector('.mcs-dps-total-attacks-label');
const totalHitsLabel = item.querySelector('.mcs-dps-total-hits-label');
const totalCritsLabel = item.querySelector('.mcs-dps-total-crits-label');
const totalMissesLabel = item.querySelector('.mcs-dps-total-misses-label');
const abilityContainer = item.querySelector('.mcs-dps-ability-container');
if (!dpsLabel || !damageLabel || !abilityContainer) return;
const damage = totalDamage[index] ?? 0;
const dps = totalTime > 0 ? (damage / totalTime).toFixed(1) : '0.0';
let playerTotalAttacks = 0;
let playerTotalHits = 0;
let playerTotalCrits = 0;
let playerTotalMisses = 0;
if (players[index].hitStatsMap) {
players[index].hitStatsMap.forEach((stats) => {
playerTotalAttacks += stats.attacks;
playerTotalHits += stats.hits;
playerTotalCrits += stats.crits;
playerTotalMisses += stats.misses;
});
}
let accuracyStr = '--';
if (playerTotalAttacks > 0) {
accuracyStr = (100 - ((playerTotalMisses / playerTotalAttacks) * 100)).toFixed(1) + '%';
}
const dpsHTML = `${dps} <span class="mcs-dps-accuracy-span">${accuracyStr}</span>`;
this.updateCellIfChanged(dpsLabel, dpsHTML, `p${index}_dps`, true);
this.updateCellIfChanged(damageLabel, this.formatDPsNumber(damage), `p${index}_dmg`);
this.updateCellIfChanged(totalAttacksLabel, playerTotalAttacks.toString(), `p${index}_atks`);
const hitPercent = playerTotalAttacks > 0 ? ((playerTotalHits / playerTotalAttacks) * 100).toFixed(1) : 0;
this.updateCellIfChanged(totalHitsLabel, `${playerTotalHits} (${hitPercent}%)`, `p${index}_hits`);
const critPercent = playerTotalAttacks > 0 ? ((playerTotalCrits / playerTotalAttacks) * 100).toFixed(1) : 0;
this.updateCellIfChanged(totalCritsLabel, `${playerTotalCrits} (${critPercent}%)`, `p${index}_crits`);
const missPercent = playerTotalAttacks > 0 ? ((playerTotalMisses / playerTotalAttacks) * 100).toFixed(1) : 0;
this.updateCellIfChanged(totalMissesLabel, `${playerTotalMisses} (${missPercent}%)`, `p${index}_miss`);
if (players[index].damageMap && players[index].damageMap.size > 0) {
const sortedAbilities = Array.from(players[index].damageMap.entries())
.filter(([abilityHrid, damage]) => damage > 0)
.sort((a, b) => b[1] - a[1]);
sortedAbilities.forEach(([abilityHrid, abilityDamage]) => {
let abilityRow = abilityContainer.querySelector(`[data-ability="${abilityHrid}"]`);
if (!abilityRow) {
abilityRow = document.createElement('div');
abilityRow.className = 'mcs-dps-ability-row';
abilityRow.setAttribute('data-ability', abilityHrid);
const abilityName = document.createElement('div');
abilityName.className = 'mcs-dps-ability-name';
const displayName = abilityHrid === 'auto' ? 'Auto Attack' :
abilityHrid === 'idle' ? 'Idle' :
abilityHrid.split('/').pop().replace(/_/g, ' ');
abilityName.textContent = ' • ' + displayName;
const abilityDps = document.createElement('div');
abilityDps.className = 'mcs-dps-ability-dps';
const abilityDamageLabel = document.createElement('div');
abilityDamageLabel.className = 'mcs-dps-ability-damage';
const attacksLabel = document.createElement('div');
attacksLabel.className = 'mcs-dps-ability-attacks';
const hitsLabel = document.createElement('div');
hitsLabel.className = 'mcs-dps-ability-hits';
const critsLabel = document.createElement('div');
critsLabel.className = 'mcs-dps-ability-crits';
const missesLabel = document.createElement('div');
missesLabel.className = 'mcs-dps-ability-misses';
abilityRow.appendChild(abilityName);
abilityRow.appendChild(abilityDps);
abilityRow.appendChild(abilityDamageLabel);
abilityRow.appendChild(attacksLabel);
abilityRow.appendChild(hitsLabel);
abilityRow.appendChild(critsLabel);
abilityRow.appendChild(missesLabel);
const firstMonster = abilityContainer.querySelector('[data-monster-header]');
if (firstMonster) {
abilityContainer.insertBefore(abilityRow, firstMonster);
} else {
abilityContainer.appendChild(abilityRow);
}
}
const abilityDps = abilityRow.querySelector('.mcs-dps-ability-dps');
const abilityDamageLabel = abilityRow.querySelector('.mcs-dps-ability-damage');
const attacksLabel = abilityRow.querySelector('.mcs-dps-ability-attacks');
const hitsLabel = abilityRow.querySelector('.mcs-dps-ability-hits');
const critsLabel = abilityRow.querySelector('.mcs-dps-ability-crits');
const missesLabel = abilityRow.querySelector('.mcs-dps-ability-misses');
const abilityDpsValue = totalTime > 0 ? (abilityDamage / totalTime).toFixed(1) : '0.0';
this.updateCellIfChanged(abilityDps, abilityDpsValue, `p${index}_ability_${abilityHrid}_dps`);
const hitStats = players[index].hitStatsMap?.get(abilityHrid) ?? {
attacks: 0,
hits: 0,
crits: 0,
misses: 0
};
const damagePercent = damage > 0 ? ((abilityDamage / damage) * 100).toFixed(1) : 0;
const damageText = this.formatDPsNumber(abilityDamage) + ` (${damagePercent}%)`;
this.updateCellIfChanged(abilityDamageLabel, damageText, `p${index}_ability_${abilityHrid}_dmg`);
this.updateCellIfChanged(attacksLabel, hitStats.attacks.toString(), `p${index}_ability_${abilityHrid}_atks`);
const hitPercent = hitStats.attacks > 0 ? ((hitStats.hits / hitStats.attacks) * 100).toFixed(1) : 0;
const hitsText = `${hitStats.hits} (${hitPercent}%)`;
this.updateCellIfChanged(hitsLabel, hitsText, `p${index}_ability_${abilityHrid}_hits`);
const critPercent = hitStats.attacks > 0 ? ((hitStats.crits / hitStats.attacks) * 100).toFixed(1) : 0;
const critsText = `${hitStats.crits} (${critPercent}%)`;
this.updateCellIfChanged(critsLabel, critsText, `p${index}_ability_${abilityHrid}_crits`);
const missPercent = hitStats.attacks > 0 ? ((hitStats.misses / hitStats.attacks) * 100).toFixed(1) : 0;
const missesText = `${hitStats.misses} (${missPercent}%)`;
this.updateCellIfChanged(missesLabel, missesText, `p${index}_ability_${abilityHrid}_miss`);
});
}
if (players[index].monsterDamageMap && players[index].monsterDamageMap.size > 0) {
players[index].monsterDamageMap.forEach((abilityMap, monsterName) => {
let monsterHeaderRow = abilityContainer.querySelector(`[data-monster-header="${monsterName}"]`);
let monsterAbilityContainer = abilityContainer.querySelector(`[data-monster-container="${monsterName}"]`);
let monsterTotalDamage = 0;
let monsterTotalAttacks = 0;
let monsterTotalHits = 0;
let monsterTotalCrits = 0;
let monsterTotalMisses = 0;
abilityMap.forEach((dmg) => {
monsterTotalDamage += dmg;
});
const monsterHitStatsMap = players[index].monsterHitStatsMap?.get(monsterName);
if (monsterHitStatsMap) {
monsterHitStatsMap.forEach((stats) => {
monsterTotalAttacks += stats.attacks;
monsterTotalHits += stats.hits;
monsterTotalCrits += stats.crits;
monsterTotalMisses += stats.misses;
});
}
if (!monsterHeaderRow) {
monsterHeaderRow = document.createElement('div');
monsterHeaderRow.className = 'mcs-dps-monster-header';
monsterHeaderRow.setAttribute('data-monster-header', monsterName);
const monsterExpandArrow = document.createElement('div');
monsterExpandArrow.className = 'mcs-dps-monster-arrow';
const monsterNameLabel = document.createElement('div');
monsterNameLabel.className = 'mcs-dps-monster-name';
monsterNameLabel.textContent = monsterName;
const monsterDpsLabel = document.createElement('div');
monsterDpsLabel.className = 'mcs-dps-monster-dps';
const monsterDamageLabel = document.createElement('div');
monsterDamageLabel.className = 'mcs-dps-monster-damage';
const monsterAttacksLabel = document.createElement('div');
monsterAttacksLabel.className = 'mcs-dps-monster-attacks';
const monsterHitsLabel = document.createElement('div');
monsterHitsLabel.className = 'mcs-dps-monster-hits';
const monsterCritsLabel = document.createElement('div');
monsterCritsLabel.className = 'mcs-dps-monster-crits';
const monsterMissesLabel = document.createElement('div');
monsterMissesLabel.className = 'mcs-dps-monster-misses';
monsterHeaderRow.appendChild(monsterExpandArrow);
monsterHeaderRow.appendChild(monsterNameLabel);
monsterHeaderRow.appendChild(monsterDpsLabel);
monsterHeaderRow.appendChild(monsterDamageLabel);
monsterHeaderRow.appendChild(monsterAttacksLabel);
monsterHeaderRow.appendChild(monsterHitsLabel);
monsterHeaderRow.appendChild(monsterCritsLabel);
monsterHeaderRow.appendChild(monsterMissesLabel);
abilityContainer.appendChild(monsterHeaderRow);
monsterAbilityContainer = document.createElement('div');
monsterAbilityContainer.className = 'mcs-dps-monster-ability-container';
monsterAbilityContainer.setAttribute('data-monster-container', monsterName);
if (!this.dpsTracking.expandedMonsters.has(index)) {
this.dpsTracking.expandedMonsters.set(index, new Set());
}
const isMonsterExpanded = this.dpsTracking.expandedMonsters.get(index).has(monsterName);
if (!isMonsterExpanded) {
monsterAbilityContainer.classList.add('mcs-hidden');
}
monsterExpandArrow.textContent = isMonsterExpanded ? '▼' : '▶';
monsterHeaderRow.onclick = (e) => {
e.stopPropagation();
const isExpanded = !monsterAbilityContainer.classList.contains('mcs-hidden');
const arrow = monsterHeaderRow.querySelector('.mcs-dps-monster-arrow');
if (isExpanded) {
monsterAbilityContainer.classList.add('mcs-hidden');
arrow.textContent = '▶';
} else {
monsterAbilityContainer.classList.remove('mcs-hidden');
arrow.textContent = '▼';
}
const playerMonsters = this.dpsTracking.expandedMonsters.get(index);
if (isExpanded) {
playerMonsters.delete(monsterName);
} else {
playerMonsters.add(monsterName);
}
};
abilityContainer.appendChild(monsterAbilityContainer);
}
const monsterDpsLabel = monsterHeaderRow.querySelector('.mcs-dps-monster-dps');
const monsterDamageLabel = monsterHeaderRow.querySelector('.mcs-dps-monster-damage');
const monsterAttacksLabel = monsterHeaderRow.querySelector('.mcs-dps-monster-attacks');
const monsterHitsLabel = monsterHeaderRow.querySelector('.mcs-dps-monster-hits');
const monsterCritsLabel = monsterHeaderRow.querySelector('.mcs-dps-monster-crits');
const monsterMissesLabel = monsterHeaderRow.querySelector('.mcs-dps-monster-misses');
const monsterDpsValue = totalTime > 0 ? (monsterTotalDamage / totalTime).toFixed(1) : '0.0';
this.updateCellIfChanged(monsterDpsLabel, monsterDpsValue, `p${index}_monster_${monsterName}_dps`);
const monsterDamagePercent = damage > 0 ? ((monsterTotalDamage / damage) * 100).toFixed(1) : 0;
const monsterDamageText = this.formatDPsNumber(monsterTotalDamage) + ` (${monsterDamagePercent}%)`;
this.updateCellIfChanged(monsterDamageLabel, monsterDamageText, `p${index}_monster_${monsterName}_dmg`);
this.updateCellIfChanged(monsterAttacksLabel, monsterTotalAttacks.toString(), `p${index}_monster_${monsterName}_atks`);
const monsterHitPercent = monsterTotalAttacks > 0 ? ((monsterTotalHits / monsterTotalAttacks) * 100).toFixed(1) : 0;
const monsterHitsText = `${monsterTotalHits} (${monsterHitPercent}%)`;
this.updateCellIfChanged(monsterHitsLabel, monsterHitsText, `p${index}_monster_${monsterName}_hits`);
const monsterCritPercent = monsterTotalAttacks > 0 ? ((monsterTotalCrits / monsterTotalAttacks) * 100).toFixed(1) : 0;
const monsterCritsText = `${monsterTotalCrits} (${monsterCritPercent}%)`;
this.updateCellIfChanged(monsterCritsLabel, monsterCritsText, `p${index}_monster_${monsterName}_crits`);
const monsterMissPercent = monsterTotalAttacks > 0 ? ((monsterTotalMisses / monsterTotalAttacks) * 100).toFixed(1) : 0;
const monsterMissesText = `${monsterTotalMisses} (${monsterMissPercent}%)`;
this.updateCellIfChanged(monsterMissesLabel, monsterMissesText, `p${index}_monster_${monsterName}_miss`);
const sortedMonsterAbilities = Array.from(abilityMap.entries())
.filter(([abilityHrid, damage]) => damage > 0)
.sort((a, b) => b[1] - a[1]);
const activeAbilityHrids = new Set(sortedMonsterAbilities.map(([h]) => h));
const existingRows = monsterAbilityContainer.querySelectorAll('[data-monster-ability]');
existingRows.forEach(row => {
if (!activeAbilityHrids.has(row.getAttribute('data-monster-ability'))) {
row.remove();
}
});
sortedMonsterAbilities.forEach(([abilityHrid, abilityDamage]) => {
let monsterAbilityRow = monsterAbilityContainer.querySelector(`[data-monster-ability="${abilityHrid}"]`);
if (!monsterAbilityRow) {
monsterAbilityRow = document.createElement('div');
monsterAbilityRow.className = 'mcs-dps-monster-ability-row';
monsterAbilityRow.setAttribute('data-monster-ability', abilityHrid);
const abilityName = document.createElement('div');
abilityName.className = 'mcs-dps-monster-ability-name';
const displayName = abilityHrid === 'auto' ? 'Auto Attack' :
abilityHrid === 'idle' ? 'Idle' :
abilityHrid.split('/').pop().replace(/_/g, ' ');
abilityName.textContent = ' • ' + displayName;
const abilityDpsEl = document.createElement('div');
abilityDpsEl.className = 'mcs-dps-monster-ability-dps';
const abilityDamageLabel = document.createElement('div');
abilityDamageLabel.className = 'mcs-dps-monster-ability-damage';
const attacksLabel = document.createElement('div');
attacksLabel.className = 'mcs-dps-monster-ability-attacks';
const hitsLabel = document.createElement('div');
hitsLabel.className = 'mcs-dps-monster-ability-hits';
const critsLabel = document.createElement('div');
critsLabel.className = 'mcs-dps-monster-ability-crits';
const missesLabel = document.createElement('div');
missesLabel.className = 'mcs-dps-monster-ability-misses';
monsterAbilityRow.appendChild(abilityName);
monsterAbilityRow.appendChild(abilityDpsEl);
monsterAbilityRow.appendChild(abilityDamageLabel);
monsterAbilityRow.appendChild(attacksLabel);
monsterAbilityRow.appendChild(hitsLabel);
monsterAbilityRow.appendChild(critsLabel);
monsterAbilityRow.appendChild(missesLabel);
monsterAbilityContainer.appendChild(monsterAbilityRow);
}
const cachePrefix = `p${index}_ma_${monsterName}_${abilityHrid}`;
const abilityDpsEl = monsterAbilityRow.querySelector('.mcs-dps-monster-ability-dps');
const abilityDamageLabel = monsterAbilityRow.querySelector('.mcs-dps-monster-ability-damage');
const attacksLabel = monsterAbilityRow.querySelector('.mcs-dps-monster-ability-attacks');
const hitsLabel = monsterAbilityRow.querySelector('.mcs-dps-monster-ability-hits');
const critsLabel = monsterAbilityRow.querySelector('.mcs-dps-monster-ability-crits');
const missesLabel = monsterAbilityRow.querySelector('.mcs-dps-monster-ability-misses');
const abilityDpsValue = totalTime > 0 ? (abilityDamage / totalTime).toFixed(1) : '0.0';
this.updateCellIfChanged(abilityDpsEl, abilityDpsValue, `${cachePrefix}_dps`);
const damagePercent = monsterTotalDamage > 0 ? ((abilityDamage / monsterTotalDamage) * 100).toFixed(1) : 0;
this.updateCellIfChanged(abilityDamageLabel, this.formatDPsNumber(abilityDamage) + ` (${damagePercent}%)`, `${cachePrefix}_dmg`);
const monsterHitStatsMap = players[index].monsterHitStatsMap?.get(monsterName);
const hitStats = monsterHitStatsMap?.get(abilityHrid) ?? {
attacks: 0, hits: 0, crits: 0, misses: 0
};
this.updateCellIfChanged(attacksLabel, hitStats.attacks.toString(), `${cachePrefix}_atks`);
const hitPercent = hitStats.attacks > 0 ? ((hitStats.hits / hitStats.attacks) * 100).toFixed(1) : 0;
this.updateCellIfChanged(hitsLabel, `${hitStats.hits} (${hitPercent}%)`, `${cachePrefix}_hits`);
const critPercent = hitStats.attacks > 0 ? ((hitStats.crits / hitStats.attacks) * 100).toFixed(1) : 0;
this.updateCellIfChanged(critsLabel, `${hitStats.crits} (${critPercent}%)`, `${cachePrefix}_crits`);
const missPercent = hitStats.attacks > 0 ? ((hitStats.misses / hitStats.attacks) * 100).toFixed(1) : 0;
this.updateCellIfChanged(missesLabel, `${hitStats.misses} (${missPercent}%)`, `${cachePrefix}_miss`);
});
});
}
});
if (this.dpsIsMinimized) {
const filterBtn = document.getElementById('dps-filter-btn');
const infoBtn = document.getElementById('dps-info-btn');
const resetBtn = document.getElementById('dps-reset-btn');
if (filterBtn) filterBtn.classList.add('mcs-hidden');
if (infoBtn) infoBtn.classList.add('mcs-hidden');
if (resetBtn) resetBtn.classList.add('mcs-hidden');
const headerDiv = this.dpsGetContent()?.querySelector(':scope > div:first-child');
if (headerDiv) {
headerDiv.classList.add('mcs-hidden');
}
this.dpsGetPlayerItems().forEach(item => {
const abilityContainer = item.querySelector('.mcs-dps-ability-container');
if (abilityContainer) {
abilityContainer.classList.add('mcs-hidden');
}
const playerRow = item.querySelector('.mcs-dps-player-row');
if (playerRow) {
const children = Array.from(playerRow.children);
children.forEach((child, index) => {
if (index === 0 || index >= 3) {
child.classList.add('mcs-hidden');
}
});
playerRow.classList.add('minimized');
playerRow.onclick = null;
}
});
}
const content = this.dpsGetContent();
if (content) {
if (!this.dpsIsMinimized) {
this.renderTrueDPSSection(content);
} else {
const truedpsSection = document.getElementById('dps-truedps-section');
if (truedpsSection) truedpsSection.classList.add('mcs-hidden');
}
}
}
formatDPsNumber(num) {
return mcsFormatCurrency(num, 'dps');
}
handleTrueDPSNewBattle(data) {
if (!this.trueDPSTracking.startTime) {
this.trueDPSTracking.startTime = Date.now();
}
this.trueDPSTracking.monsters = data.monsters ?? [];
this.trueDPSTracking.monstersHP = data.monsters.map((monster) => monster.currentHitpoints);
this.trueDPSTracking.monstersDmgCounter = data.monsters.map((monster) => monster.damageSplatCounter);
data.monsters.forEach((monster, index) => {
const monsterName = monster.name || `Monster ${index + 1}`;
this.trueDPSTracking.monsterMaxHP[index] = monster.maxHitpoints;
if (!this.trueDPSTracking.enemyKills[monsterName]) {
this.trueDPSTracking.enemyKills[monsterName] = {
kills: 0,
maxHP: monster.maxHitpoints,
totalDamage: 0
};
}
});
}
handleTrueDPSBattleUpdate(data) {
if (!this.trueDPSTracking.monstersHP || this.trueDPSTracking.monstersHP.length === 0) {
return;
}
const mMap = data.mMap;
this.trueDPSTracking.monstersHP.forEach((prevHP, mIndex) => {
const monster = mMap[mIndex];
if (!monster) return;
const currentHP = monster.cHP;
const monsterName = this.trueDPSTracking.monsters[mIndex]?.name || `Monster ${mIndex + 1}`;
if (prevHP > 0 && currentHP === 0) {
if (!this.trueDPSTracking.enemyKills[monsterName]) {
this.trueDPSTracking.enemyKills[monsterName] = {
kills: 0,
maxHP: this.trueDPSTracking.monsterMaxHP[mIndex] ?? prevHP,
totalDamage: 0
};
}
const maxHP = this.trueDPSTracking.monsterMaxHP[mIndex] ?? prevHP;
this.trueDPSTracking.enemyKills[monsterName].kills++;
this.trueDPSTracking.enemyKills[monsterName].totalDamage += maxHP;
this.trueDPSTracking.enemyKills[monsterName].maxHP = maxHP;
}
this.trueDPSTracking.monstersHP[mIndex] = currentHP;
this.trueDPSTracking.monstersDmgCounter[mIndex] = monster.dmgCounter;
});
}
renderTrueDPSSection(content) {
let existingSection = document.getElementById('dps-truedps-section');
if (existingSection) {
existingSection.remove();
}
const tracking = this.trueDPSTracking;
let elapsedSeconds = mcsGetElapsedSeconds(tracking.startTime, null, tracking.savedPausedMs, 0);
let totalKills = 0;
let totalDamage = 0;
Object.values(tracking.enemyKills).forEach(enemy => {
totalKills += enemy.kills;
totalDamage += enemy.totalDamage;
});
const trueDPS = elapsedSeconds > 0 ? (totalDamage / elapsedSeconds) : 0;
const section = document.createElement('div');
section.id = 'dps-truedps-section';
section.className = 'mcs-dps-truedps-section';
const headerRow = document.createElement('div');
headerRow.className = 'mcs-dps-truedps-header';
const title = document.createElement('span');
title.textContent = 'DPS based off enemy HPs';
title.className = 'mcs-dps-truedps-title';
let battleTime = this.dpsTracking.totalDuration;
if (this.dpsTracking.startTime) {
battleTime = mcsGetElapsedSeconds(this.dpsTracking.startTime, this.dpsTracking.endTime, this.dpsTracking.savedPausedMs, battleTime);
}
const battleDPS = battleTime > 0 ? (totalDamage / battleTime) : 0;
const dpsContainer = document.createElement('div');
dpsContainer.className = 'mcs-dps-truedps-container';
const totalTimeDPSContainer = document.createElement('div');
totalTimeDPSContainer.className = 'mcs-dps-truedps-value-container';
const totalTimeDPSValue = document.createElement('div');
totalTimeDPSValue.textContent = trueDPS.toFixed(1);
totalTimeDPSValue.className = 'mcs-dps-truedps-value';
const totalTimeLabel = document.createElement('div');
totalTimeLabel.textContent = 'total time DPS';
totalTimeLabel.className = 'mcs-dps-truedps-label';
totalTimeDPSContainer.appendChild(totalTimeDPSValue);
totalTimeDPSContainer.appendChild(totalTimeLabel);
const battleTimeDPSContainer = document.createElement('div');
battleTimeDPSContainer.className = 'mcs-dps-truedps-value-container';
const battleTimeDPSValue = document.createElement('div');
battleTimeDPSValue.textContent = battleDPS.toFixed(1);
battleTimeDPSValue.className = 'mcs-dps-truedps-value';
const battleTimeLabel = document.createElement('div');
battleTimeLabel.textContent = 'battle time DPS';
battleTimeLabel.className = 'mcs-dps-truedps-label';
battleTimeDPSContainer.appendChild(battleTimeDPSValue);
battleTimeDPSContainer.appendChild(battleTimeLabel);
const dpsLoss = battleDPS - trueDPS;
const dpsLossContainer = document.createElement('div');
dpsLossContainer.className = 'mcs-dps-truedps-value-container';
const dpsLossValue = document.createElement('div');
dpsLossValue.textContent = dpsLoss.toFixed(1);
dpsLossValue.className = 'mcs-dps-loss-value';
const dpsLossLabel = document.createElement('div');
dpsLossLabel.textContent = 'DPS loss between battles';
dpsLossLabel.className = 'mcs-dps-truedps-label';
dpsLossContainer.appendChild(dpsLossValue);
dpsLossContainer.appendChild(dpsLossLabel);
dpsContainer.appendChild(totalTimeDPSContainer);
dpsContainer.appendChild(battleTimeDPSContainer);
dpsContainer.appendChild(dpsLossContainer);
headerRow.appendChild(title);
headerRow.appendChild(dpsContainer);
section.appendChild(headerRow);
const subtitle = document.createElement('div');
subtitle.textContent = 'Based on Enemy\'s max HP only';
subtitle.className = 'mcs-dps-truedps-subtitle';
section.appendChild(subtitle);
const statsRow = document.createElement('div');
statsRow.className = 'mcs-dps-truedps-stats';
const timeDiv = document.createElement('div');
timeDiv.innerHTML = `<span class="mcs-dps-stat-label">Time Logging:</span> <span class="mcs-dps-stat-time">${elapsedSeconds.toFixed(1)}s</span>`;
const battleTimeDiv = document.createElement('div');
battleTimeDiv.innerHTML = `<span class="mcs-dps-stat-label">Time in Battle:</span> <span class="mcs-dps-stat-time">${battleTime.toFixed(1)}s</span>`;
const damageDiv = document.createElement('div');
damageDiv.innerHTML = `<span class="mcs-dps-stat-label">Total Damage:</span> <span class="mcs-dps-stat-damage">${this.formatDPsNumber(totalDamage)}</span>`;
const killsDiv = document.createElement('div');
killsDiv.innerHTML = `<span class="mcs-dps-stat-label">Total Enemies Killed:</span> <span class="mcs-dps-stat-kills">${totalKills}</span>`;
statsRow.appendChild(timeDiv);
statsRow.appendChild(battleTimeDiv);
statsRow.appendChild(damageDiv);
statsRow.appendChild(killsDiv);
section.appendChild(statsRow);
if (Object.keys(tracking.enemyKills).length > 0) {
const enemyGrid = document.createElement('div');
enemyGrid.className = 'mcs-dps-enemy-grid';
const sortedEnemies = Object.entries(tracking.enemyKills).sort((a, b) => b[1].totalDamage - a[1].totalDamage);
sortedEnemies.forEach(([enemyName, data]) => {
const enemyBox = document.createElement('div');
enemyBox.className = 'mcs-dps-enemy-box';
const nameDiv = document.createElement('div');
nameDiv.textContent = enemyName;
nameDiv.className = 'mcs-dps-enemy-name';
const statsDiv = document.createElement('div');
statsDiv.className = 'mcs-dps-enemy-stats';
statsDiv.innerHTML = `${data.kills} kills × ${this.formatDPsNumber(data.maxHP)} HP = <span class="mcs-dps-enemy-damage">${this.formatDPsNumber(data.totalDamage)}</span>`;
enemyBox.appendChild(nameDiv);
enemyBox.appendChild(statsDiv);
enemyGrid.appendChild(enemyBox);
});
section.appendChild(enemyGrid);
}
content.appendChild(section);
}
makeDPsDraggable(pane, header) {
DragHandler.makeDraggable(pane, header, 'mcs_DP');
}
destroyDPs() {
VisibilityManager.clear('dps-update');
VisibilityManager.clear('dps-inactivity-timer');
if (this._dpsWsListener) { window.removeEventListener('EquipSpyWebSocketMessage', this._dpsWsListener); this._dpsWsListener = null; }
const pane = document.getElementById('dps-pane');
if (pane) pane.remove();
}
// DPs end
// BRead start
get brStorage() {
if (!this._brStorage) {
this._brStorage = createModuleStorage('BR');
}
return this._brStorage;
}
breadGetAbilityItems() {
const content = document.getElementById('bread-content');
if (!content) return [];
if (this._breadCachedAbilityItems && this._breadLastCacheVersion === this._breadAbilityCacheVersion) {
return this._breadCachedAbilityItems;
}
this._breadCachedAbilityItems = content.querySelectorAll('.bread-ability-item');
this._breadLastCacheVersion = this._breadAbilityCacheVersion;
return this._breadCachedAbilityItems;
}
breadInvalidateAbilityCache() {
if (!this._breadAbilityCacheVersion) this._breadAbilityCacheVersion = 0;
this._breadAbilityCacheVersion++;
this._breadCachedAbilityItems = null;
}
breadHandleWebSocketMessage(event) {
if (window.MCS_MODULES_DISABLED) return;
const data = event.detail;
if (data?.type === 'action_completed' && data.endCharacterAbilities) {
for (const ability of data.endCharacterAbilities) {
this.breadCurrentAbilityExp[ability.abilityHrid] = {
level: ability.level,
experience: ability.experience
};
}
setTimeout(() => this.updateBReadExperience(), 100);
}
if (data?.type === 'init_character_data' && data.characterAbilities) {
for (const ability of Object.values(data.characterAbilities)) {
this.breadCurrentAbilityExp[ability.abilityHrid] = {
level: ability.level,
experience: ability.experience
};
}
setTimeout(() => this.updateBReadExperience(), 100);
}
if (data?.characterAbilities && data.type !== 'init_character_data') {
for (const ability of Object.values(data.characterAbilities)) {
this.breadCurrentAbilityExp[ability.abilityHrid] = {
level: ability.level,
experience: ability.experience
};
}
setTimeout(() => this.updateBReadExperience(), 100);
}
if (data?.type === 'abilities_updated' && data.endCharacterAbilities) {
const currentAbilityHrids = new Set(
Array.from(document.querySelectorAll('#bread-content .bread-ability-item[data-ability-hrid]'))
.map(el => el.getAttribute('data-ability-hrid'))
);
for (const ability of data.endCharacterAbilities) {
this.breadCurrentAbilityExp[ability.abilityHrid] = {
level: ability.level,
experience: ability.experience
};
}
const cachedData = CharacterDataStorage.get();
if (cachedData?.combatUnit) {
const abilityMap = new Map();
for (const a of (cachedData.combatUnit.combatAbilities || [])) {
abilityMap.set(a.abilityHrid, a);
}
for (const ability of data.endCharacterAbilities) {
if (ability.slotNumber > 0) {
abilityMap.set(ability.abilityHrid, ability);
} else {
abilityMap.delete(ability.abilityHrid);
}
}
cachedData.combatUnit.combatAbilities = Array.from(abilityMap.values());
}
const updatedHrids = new Set(
(cachedData?.combatUnit?.combatAbilities || []).map(a => a.abilityHrid)
);
const abilitiesActuallyChanged = currentAbilityHrids.size !== updatedHrids.size ||
[...currentAbilityHrids].some(h => !updatedHrids.has(h));
if (abilitiesActuallyChanged) {
this.breadExpTracking = {};
setTimeout(() => {
this.updateBReadContent();
this.updateBReadExperience();
}, 100);
} else {
for (const ability of data.endCharacterAbilities) {
if (this.breadExpTracking[ability.abilityHrid]) {
this.breadExpTracking[ability.abilityHrid] = {
startExp: ability.experience,
startTime: Date.now(),
lastExp: ability.experience,
lastUpdateTime: Date.now(),
savedPausedMs: window.MCS_TOTAL_PAUSED_MS ?? 0,
savedTabHiddenMs: window.MCS_TOTAL_TAB_HIDDEN_MS ?? 0
};
}
}
setTimeout(() => this.updateBReadExperience(), 100);
}
}
if (data?.type === 'new_battle' && data.players) {
const cachedData = CharacterDataStorage.get();
const myName = cachedData?.character?.name;
if (myName) {
const me = data.players.find(p => p.character?.name === myName);
if (me?.combatDetails?.combatAbilities) {
const newAbilities = me.combatDetails.combatAbilities;
const currentAbilityHrids = new Set(
Array.from(document.querySelectorAll('#bread-content .bread-ability-item[data-ability-hrid]'))
.map(el => el.getAttribute('data-ability-hrid'))
);
const newAbilityHrids = new Set(newAbilities.map(a => a.abilityHrid));
const abilitiesChanged = currentAbilityHrids.size !== newAbilityHrids.size ||
[...currentAbilityHrids].some(h => !newAbilityHrids.has(h));
if (abilitiesChanged) {
if (cachedData?.combatUnit) {
cachedData.combatUnit.combatAbilities = newAbilities;
}
this.breadExpTracking = {};
setTimeout(() => {
this.updateBReadContent();
this.updateBReadExperience();
}, 100);
}
}
}
}
}
formatBReadCoins(value) {
return mcsFormatCurrency(value, 'precise');
}
createBReadPane() {
if (document.getElementById('bread-pane')) return;
const pane = document.createElement('div');
pane.id = 'bread-pane';
registerPanel('bread-pane');
pane.className = 'mcs-pane mcs-bread-pane';
const header = document.createElement('div');
header.className = 'mcs-pane-header mcs-bread-header';
const titleSection = document.createElement('div');
this.applyClass(titleSection, 'mcs-bread-title-section');
const titleSpan = document.createElement('span');
titleSpan.className = 'mcs-pane-title';
titleSpan.textContent = 'BRead';
const headerInfo = document.createElement('span');
headerInfo.id = 'bread-header-info';
this.applyClass(headerInfo, 'mcs-bread-header-info');
titleSection.appendChild(titleSpan);
titleSection.appendChild(headerInfo);
const buttonSection = document.createElement('div');
buttonSection.className = 'mcs-button-section';
const minimizeBtn = document.createElement('button');
minimizeBtn.id = 'bread-minimize-btn';
minimizeBtn.className = 'mcs-btn mcs-btn-minimize';
minimizeBtn.textContent = '−';
const resetBtn = document.createElement('button');
resetBtn.id = 'bread-reset-btn';
resetBtn.className = 'mcs-btn mcs-btn-small';
resetBtn.textContent = 'Reset';
buttonSection.appendChild(resetBtn);
buttonSection.appendChild(minimizeBtn);
header.appendChild(titleSection);
header.appendChild(buttonSection);
const rangeCalc = document.createElement('div');
rangeCalc.id = 'bread-range-calc';
rangeCalc.className = 'mcs-bread-range-calc';
const minLevelInput = document.createElement('input');
minLevelInput.id = 'bread-min-level';
minLevelInput.type = 'number';
minLevelInput.min = '1';
minLevelInput.max = '199';
minLevelInput.value = '1';
minLevelInput.className = 'mcs-bread-range-input';
minLevelInput.placeholder = 'Min';
const maxLevelInput = document.createElement('input');
maxLevelInput.id = 'bread-max-level';
maxLevelInput.type = 'number';
maxLevelInput.min = '2';
maxLevelInput.max = '200';
maxLevelInput.value = '100';
maxLevelInput.className = 'mcs-bread-range-input';
maxLevelInput.placeholder = 'Max';
const calcBtn = document.createElement('button');
calcBtn.id = 'bread-calc-btn';
calcBtn.className = 'mcs-btn mcs-btn-small';
calcBtn.textContent = 'How many Books?';
const rangeResult = document.createElement('div');
rangeResult.id = 'bread-range-result';
rangeResult.className = 'mcs-bread-range-result';
rangeCalc.appendChild(minLevelInput);
rangeCalc.appendChild(maxLevelInput);
rangeCalc.appendChild(calcBtn);
rangeCalc.appendChild(rangeResult);
const content = document.createElement('div');
content.id = 'bread-content';
content.className = 'mcs-pane-content mcs-bread-content';
pane.appendChild(header);
pane.appendChild(rangeCalc);
pane.appendChild(content);
document.body.appendChild(pane);
this.makeBReadDraggable(pane, header);
this.breadExpTracking = {};
this.breadCurrentAbilityExp = {};
this.breadCachedLevelExpTable = null;
this.breadCachedItemDetailMap = null;
const savedBreadMinimized = this.brStorage.get('minimized');
this.breadIsMinimized = savedBreadMinimized === true || savedBreadMinimized === 'true';
if (this.breadIsMinimized) {
content.classList.add('mcs-hidden');
pane.classList.add('mcs-width-fit', 'mcs-height-auto');
minimizeBtn.textContent = '+';
header.classList.add('minimized');
rangeCalc.classList.add('mcs-hidden');
}
this.updateBReadContent();
minimizeBtn.onclick = () => {
this.breadIsMinimized = !this.breadIsMinimized;
if (this.breadIsMinimized) {
content.classList.add('mcs-hidden');
content.classList.remove('mcs-display-flex');
pane.classList.add('mcs-width-fit', 'mcs-height-auto');
minimizeBtn.textContent = '+';
header.classList.add('minimized');
rangeCalc.classList.add('mcs-hidden');
this.brStorage.set('minimized', true);
} else {
content.classList.remove('mcs-hidden');
content.classList.add('mcs-display-flex');
pane.classList.add('mcs-width-fit', 'mcs-height-auto');
minimizeBtn.textContent = '−';
header.classList.remove('minimized');
rangeCalc.classList.remove('mcs-hidden');
this.brStorage.set('minimized', false);
this.constrainPanelToBoundaries('bread-pane', 'mcs_BR', true);
}
};
calcBtn.onclick = () => {
const minLevel = parseInt(document.getElementById('bread-min-level')?.value || 1);
const maxLevel = parseInt(document.getElementById('bread-max-level')?.value || 100);
const rangeResult = document.getElementById('bread-range-result');
if (rangeResult && minLevel < maxLevel && minLevel >= 1 && maxLevel <= 200) {
const levelExperienceTable = InitClientDataCache.getLevelExperienceTable();
if (levelExperienceTable) {
const startExp = levelExperienceTable[minLevel];
const endExp = levelExperienceTable[maxLevel];
const totalExpNeeded = endExp - startExp;
const booksFor50xp = Math.ceil(totalExpNeeded / 50);
const booksFor500xp = Math.ceil(totalExpNeeded / 500);
rangeResult.innerHTML = `
<div>Level ${minLevel} → ${maxLevel}</div>
<div>50xp/book: <strong>${booksFor50xp.toLocaleString()}</strong> books</div>
<div>500xp/book: <strong>${booksFor500xp.toLocaleString()}</strong> books</div>
`;
}
}
};
resetBtn.onclick = () => {
const inputs = document.querySelectorAll('.bread-target-input');
inputs.forEach(input => {
const min = parseInt(input.min);
input.value = min.toString();
input.dispatchEvent(new Event('change'));
});
};
if (!this._breadWsListener) {
this._breadWsListener = this.breadHandleWebSocketMessage.bind(this);
window.addEventListener('EquipSpyWebSocketMessage', this._breadWsListener);
}
VisibilityManager.register('bread-update', () => {
this.updateBReadExperience();
}, 2000);
}
updateBReadContent() {
const content = document.getElementById('bread-content');
if (!content) return;
this.breadInvalidateAbilityCache();
let equippedAbilities = [];
try {
const charData = CharacterDataStorage.get();
if (charData) {
if (charData.combatUnit?.combatAbilities) {
equippedAbilities = charData.combatUnit.combatAbilities;
}
}
} catch (e) {
console.error('[BRead] Failed to load equipped abilities:', e);
}
if (equippedAbilities.length === 0) {
content.innerHTML = '<div class="mcs-bread-no-abilities">No abilities equipped</div>';
return;
}
content.innerHTML = '';
equippedAbilities.forEach(ability => {
if (!ability || !ability.abilityHrid) return;
const itemDiv = document.createElement('div');
itemDiv.className = 'bread-ability-item mcs-bread-item';
itemDiv.setAttribute('data-ability-hrid', ability.abilityHrid);
const levelLabel = document.createElement('div');
levelLabel.className = 'bread-level-label mcs-bread-level mcs-font-16';
levelLabel.textContent = '...';
const iconId = ability.abilityHrid.split('/').pop();
const bookItemHrid = '/items/' + iconId;
const svgIcon = createItemIcon(iconId, { width: 30, height: 30, className: 'mcs-bread-icon', clickable: true, itemHrid: bookItemHrid });
svgIcon.addEventListener('click', (e) => {
e.stopPropagation();
mcsGoToMarketplace(bookItemHrid);
});
const expContainer = document.createElement('div');
expContainer.className = 'mcs-bread-exp-container';
const expLabel = document.createElement('div');
expLabel.className = 'bread-exp-label mcs-bread-exp mcs-text';
expLabel.textContent = '...';
const rateLabel = document.createElement('div');
rateLabel.className = 'bread-rate-label mcs-bread-rate mcs-font-9';
rateLabel.textContent = '0/hr';
expContainer.appendChild(expLabel);
expContainer.appendChild(rateLabel);
const timeLabel = document.createElement('div');
timeLabel.className = 'bread-time-label mcs-bread-time mcs-text';
timeLabel.textContent = '--';
const booksLabel = document.createElement('div');
booksLabel.className = 'bread-books-label mcs-bread-books mcs-font-22';
booksLabel.textContent = '...';
const marketValueLabel = document.createElement('div');
marketValueLabel.className = 'bread-market-value-label mcs-bread-market mcs-text';
marketValueLabel.textContent = '...';
const targetInput = document.createElement('input');
targetInput.className = 'bread-target-input mcs-bread-input mcs-text';
targetInput.type = 'number';
targetInput.min = '1';
targetInput.max = '200';
targetInput.value = '1';
itemDiv.appendChild(levelLabel);
itemDiv.appendChild(svgIcon);
itemDiv.appendChild(expContainer);
itemDiv.appendChild(timeLabel);
itemDiv.appendChild(booksLabel);
itemDiv.appendChild(marketValueLabel);
itemDiv.appendChild(targetInput);
content.appendChild(itemDiv);
});
this.updateBReadExperience();
}
breadShouldUpdateDOM() {
const pane = document.getElementById('bread-pane');
if (!pane) return false;
return true;
}
breadIsHidden() {
const pane = document.getElementById('bread-pane');
return pane && pane.classList.contains('mcs-hidden');
}
updateBReadExperience() {
if (!this.breadShouldUpdateDOM()) return;
const isMinimized = this.breadIsMinimized;
const isHidden = this.breadIsHidden();
let characterAbilities = null;
let levelExperienceTable = null;
let itemDetailMap = null;
if (Object.keys(this.breadCurrentAbilityExp).length > 0) {
characterAbilities = this.breadCurrentAbilityExp;
}
levelExperienceTable = InitClientDataCache.getLevelExperienceTable();
itemDetailMap = InitClientDataCache.getItemDetailMap();
if (!characterAbilities) {
try {
const charData = CharacterDataStorage.get();
if (charData) {
if (charData.characterAbilities) {
characterAbilities = {};
for (const ability of Object.values(charData.characterAbilities)) {
characterAbilities[ability.abilityHrid] = {
level: ability.level,
experience: ability.experience
};
}
}
}
} catch (e) {
console.error('[BRead] Failed to load character data:', e);
}
}
if (!characterAbilities || !levelExperienceTable || !itemDetailMap) {
return;
}
const marketData = mcsGetMarketData();
let cheapestCost = Infinity;
let cheapestBooks = 0;
let cheapestIconId = '';
const abilityItems = this.breadGetAbilityItems();
const currentTime = Date.now();
const calculateBooksToLevel = (currentLevel, currentExp, targetLevel, abilityPerBookExp) => {
const needExp = levelExperienceTable[targetLevel] - currentExp;
let needBooks = needExp / abilityPerBookExp;
if (currentLevel === 0) {
needBooks += 1;
}
return Math.ceil(needBooks);
};
abilityItems.forEach(item => {
const abilityHrid = item.getAttribute('data-ability-hrid');
const abilityData = characterAbilities[abilityHrid];
const currentLevel = abilityData?.level ?? 0;
const currentExp = abilityData?.experience ?? 0;
const itemHrid = abilityHrid.replace('/abilities/', '/items/');
const abilityPerBookExp = itemDetailMap[itemHrid]?.abilityBookDetail?.experienceGain;
const iconId = abilityHrid.split('/').pop();
const existingTracking = this.breadExpTracking[abilityHrid];
const hasGainedExp = existingTracking && (currentExp - existingTracking.startExp) > 0;
if (abilityPerBookExp && currentLevel < 200 && hasGainedExp) {
const booksToNextLevel = calculateBooksToLevel(currentLevel, currentExp, currentLevel + 1, abilityPerBookExp);
if (booksToNextLevel > 0) {
let askPrice = 0;
if (marketData?.marketData?.[itemHrid]?.['0']?.a > 0) {
askPrice = marketData.marketData[itemHrid]['0'].a;
}
const costToNextLevel = askPrice * booksToNextLevel;
if (askPrice > 0 && costToNextLevel < cheapestCost) {
cheapestCost = costToNextLevel;
cheapestBooks = booksToNextLevel;
cheapestIconId = iconId;
}
}
}
if (!this.breadExpTracking[abilityHrid]) {
this.breadExpTracking[abilityHrid] = {
startExp: currentExp,
startTime: currentTime,
lastExp: currentExp,
lastUpdateTime: currentTime,
savedPausedMs: window.MCS_TOTAL_PAUSED_MS ?? 0,
savedTabHiddenMs: window.MCS_TOTAL_TAB_HIDDEN_MS ?? 0
};
} else {
const tracking = this.breadExpTracking[abilityHrid];
tracking.lastExp = currentExp;
tracking.lastUpdateTime = currentTime;
}
if (isMinimized || isHidden) return;
const levelLabel = item.querySelector('.bread-level-label');
const expLabel = item.querySelector('.bread-exp-label');
const rateLabel = item.querySelector('.bread-rate-label');
const timeLabel = item.querySelector('.bread-time-label');
const booksLabel = item.querySelector('.bread-books-label');
const marketValueLabel = item.querySelector('.bread-market-value-label');
const targetInput = item.querySelector('.bread-target-input');
if (!levelLabel || !expLabel || !rateLabel || !timeLabel || !booksLabel || !targetInput) return;
if (!abilityData) {
levelLabel.textContent = '0';
expLabel.textContent = '0';
rateLabel.textContent = '0/hr';
timeLabel.textContent = '--';
booksLabel.textContent = '0';
marketValueLabel.textContent = '0 coin';
return;
}
levelLabel.textContent = currentLevel.toString();
const nextLevelExp = levelExperienceTable[currentLevel + 1];
const expNeeded = nextLevelExp - currentExp;
expLabel.textContent = Math.ceil(expNeeded).toString();
if (abilityPerBookExp) {
if (targetInput.value === '1' || parseInt(targetInput.value) <= currentLevel) {
targetInput.value = (currentLevel + 1).toString();
targetInput.min = (currentLevel + 1).toString();
}
const targetLevel = parseInt(targetInput.value);
if (targetLevel > currentLevel && targetLevel <= 200) {
const booksToTarget = calculateBooksToLevel(currentLevel, currentExp, targetLevel, abilityPerBookExp);
booksLabel.textContent = booksToTarget.toString();
if (marketData?.marketData?.[itemHrid]?.['0']?.a > 0) {
const askPrice = marketData.marketData[itemHrid]['0'].a;
const totalValue = askPrice * booksToTarget;
marketValueLabel.textContent = this.formatBReadCoins(totalValue);
} else {
marketValueLabel.textContent = '- coin';
}
} else {
booksLabel.textContent = '0';
marketValueLabel.textContent = '0 coin';
}
const updateTargetBooks = () => {
const targetLevel = parseInt(targetInput.value);
if (targetLevel > currentLevel && targetLevel <= 200) {
const booksToTarget = calculateBooksToLevel(currentLevel, currentExp, targetLevel, abilityPerBookExp);
booksLabel.textContent = booksToTarget.toString();
if (marketData?.marketData?.[itemHrid]?.['0']?.a > 0) {
const askPrice = marketData.marketData[itemHrid]['0'].a;
const totalValue = askPrice * booksToTarget;
marketValueLabel.textContent = this.formatBReadCoins(totalValue);
} else {
marketValueLabel.textContent = '-';
}
const tracking = this.breadExpTracking[abilityHrid];
if (tracking) {
const expGained = currentExp - tracking.startExp;
const timeElapsed = mcsGetElapsedSeconds(tracking.startTime, currentTime, tracking.savedPausedMs, 0, true, tracking.savedTabHiddenMs);
if (timeElapsed > 0 && expGained > 0) {
const expPerHour = (expGained / timeElapsed) * 3600;
const expNeededForTarget = levelExperienceTable[targetLevel] - currentExp;
const hoursNeeded = expNeededForTarget / expPerHour;
if (hoursNeeded < 1) {
const minutes = Math.ceil(hoursNeeded * 60);
timeLabel.textContent = minutes + 'm';
} else if (hoursNeeded < 24) {
const hours = Math.floor(hoursNeeded);
const minutes = Math.ceil((hoursNeeded - hours) * 60);
timeLabel.textContent = hours + 'h ' + minutes + 'm';
} else {
const days = Math.floor(hoursNeeded / 24);
const hours = Math.floor(hoursNeeded % 24);
timeLabel.textContent = days + 'd ' + hours + 'h';
}
}
}
} else {
booksLabel.textContent = '0';
marketValueLabel.textContent = '0';
timeLabel.textContent = '--';
}
};
targetInput.onchange = updateTargetBooks;
targetInput.onkeyup = updateTargetBooks;
} else {
booksLabel.textContent = 'N/A';
}
const tracking = this.breadExpTracking[abilityHrid];
const expGained = currentExp - tracking.startExp;
const timeElapsed = mcsGetElapsedSeconds(tracking.startTime, currentTime, tracking.savedPausedMs, 0, true, tracking.savedTabHiddenMs);
if (timeElapsed > 0 && expGained > 0) {
const expPerHour = (expGained / timeElapsed) * 3600;
rateLabel.textContent = Math.round(expPerHour).toString() + '/hr';
const targetLevel = parseInt(targetInput.value);
if (targetLevel > currentLevel && targetLevel <= 200 && expPerHour > 0) {
const expNeededForTarget = levelExperienceTable[targetLevel] - currentExp;
const hoursNeeded = expNeededForTarget / expPerHour;
if (hoursNeeded < 1) {
const minutes = Math.ceil(hoursNeeded * 60);
timeLabel.textContent = minutes + 'm';
} else if (hoursNeeded < 24) {
const hours = Math.floor(hoursNeeded);
const minutes = Math.ceil((hoursNeeded - hours) * 60);
timeLabel.textContent = hours + 'h ' + minutes + 'm';
} else {
const days = Math.floor(hoursNeeded / 24);
const hours = Math.floor(hoursNeeded % 24);
timeLabel.textContent = days + 'd ' + hours + 'h';
}
} else {
timeLabel.textContent = '--';
}
} else if (timeElapsed > 0) {
rateLabel.textContent = '0/hr';
timeLabel.textContent = '--';
} else {
rateLabel.textContent = '0/hr';
timeLabel.textContent = '--';
}
});
this.updateBReadHeaderInfo(cheapestIconId, cheapestBooks, cheapestCost);
}
updateBReadHeaderInfo(iconId, books, cost) {
const headerInfo = document.getElementById('bread-header-info');
if (!iconId || !books || books <= 0 || cost === Infinity || cost === 0) {
window.mcs_breadCheapestSkill = null;
} else {
window.mcs_breadCheapestSkill = { iconId, books, cost };
}
if (!headerInfo) return;
if (!iconId || !books || books <= 0 || cost === Infinity || cost === 0) {
headerInfo.innerHTML = '';
return;
}
headerInfo.innerHTML = `
${createItemIconHtml(iconId, { width: 18, height: 18, style: 'flex-shrink: 0' })}
<span class="mcs-bread-header-books">${books}</span>
<span class="mcs-bread-header-label">books</span>
<span class="mcs-bread-header-cost">${this.formatBReadCoins(cost)}</span>
`;
}
makeBReadDraggable(pane, header) {
DragHandler.makeDraggable(pane, header, 'mcs_BR');
}
destroyBRead() {
VisibilityManager.clear('bread-update');
if (this._breadWsListener) { window.removeEventListener('EquipSpyWebSocketMessage', this._breadWsListener); this._breadWsListener = null; }
const pane = document.getElementById('bread-pane');
if (pane) pane.remove();
}
// BRead end
// JHouse start
get jhStorage() {
if (!this._jhStorage) {
this._jhStorage = createModuleStorage('JH');
}
return this._jhStorage;
}
getHouseRecipes() {
if (this._cachedHouseRecipes) {
return this._cachedHouseRecipes;
}
try {
const clientData = InitClientDataCache.get();
if (clientData?.houseRoomDetailMap) {
const recipes = {};
for (const [hrid, roomDetail] of Object.entries(clientData.houseRoomDetailMap)) {
const roomKey = hrid.split('/').pop() || '';
const roomName = roomKey.split('_').map(word =>
word.charAt(0).toUpperCase() + word.slice(1)
).join(' ');
recipes[roomName] = {};
if (roomDetail.upgradeCostsMap) {
for (const [level, costs] of Object.entries(roomDetail.upgradeCostsMap)) {
const recipe = {
gold: 0,
materials: {}
};
for (const cost of costs) {
if (cost.itemHrid === '/items/coin') {
recipe.gold = cost.count;
} else {
recipe.materials[cost.itemHrid] = cost.count;
}
}
recipes[roomName][level] = recipe;
}
}
}
if (Object.keys(recipes).length > 0) {
this._cachedHouseRecipes = recipes;
return this._cachedHouseRecipes;
}
}
} catch (e) {
console.error('[JHouse] Error building recipes from client data:', e);
}
if (window.HOUSE_RECIPES) {
this._cachedHouseRecipes = window.HOUSE_RECIPES;
return this._cachedHouseRecipes;
}
if (this.HOUSE_RECIPES) {
this._cachedHouseRecipes = this.HOUSE_RECIPES;
return this._cachedHouseRecipes;
}
return {};
}
numberToEnglish(num) {
const words = ['zero', 'one', 'two', 'three', 'four', 'five', 'six', 'seven',
'eight', 'nine', 'ten', 'eleven', 'twelve', 'thirteen', 'fourteen',
'fifteen', 'sixteen', 'seventeen'];
return words[num] || num.toString();
}
formatGoldDetailed(amount) {
return mcsFormatCurrency(amount, 'detailed');
}
formatGold(amount) {
return mcsFormatCurrency(amount, 'gold');
}
formatItemName(itemHrid) {
return mcsFormatHrid(itemHrid);
}
getItemCount(itemHrid, cachedInventory = null) {
if (!window.lootDropsTrackerInstance?.spyCharacterItems && window._pendingInventoryData) {
const items = window._pendingInventoryData.filter(i =>
i.itemHrid === itemHrid &&
(i.itemLocationHrid === '/item_locations/inventory' || !i.itemLocationHrid) &&
(!i.enhancementLevel || i.enhancementLevel === 0)
);
return items.reduce((total, item) => total + (item.count ?? 0), 0);
}try {
if (cachedInventory) {
const items = cachedInventory.filter(i =>
i.itemHrid === itemHrid &&
(i.itemLocationHrid === '/item_locations/inventory' || !i.itemLocationHrid) &&
(!i.enhancementLevel || i.enhancementLevel === 0)
);
return items.reduce((total, item) => total + (item.count ?? 0), 0);
}
if (window.lootDropsTrackerInstance?.spyCharacterItems) {
const items = window.lootDropsTrackerInstance.spyCharacterItems.filter(i =>
i.itemHrid === itemHrid &&
(i.itemLocationHrid === '/item_locations/inventory' || !i.itemLocationHrid) &&
(!i.enhancementLevel || i.enhancementLevel === 0)
);
return items.reduce((total, item) => total + (item.count ?? 0), 0);
}
const cachedData = CharacterDataStorage.get();
if (cachedData) {
if (cachedData.characterItems && Array.isArray(cachedData.characterItems)) {
const items = cachedData.characterItems.filter(i =>
i.itemHrid === itemHrid &&
(i.itemLocationHrid === '/item_locations/inventory' || !i.itemLocationHrid) &&
(!i.enhancementLevel || i.enhancementLevel === 0)
);
return items.reduce((total, item) => total + (item.count ?? 0), 0);
}
}
} catch (e) {
}
return 0;
}
getCurrentGold(cachedInventory = null) {
if (!window.lootDropsTrackerInstance?.spyCharacterItems && window._pendingInventoryData) {
const coinItem = window._pendingInventoryData.find(i => i.itemHrid === '/items/coin');
if (coinItem) return coinItem.count ?? 0;
}
try {
if (cachedInventory) {
const coinItem = cachedInventory.find(i => i.itemHrid === '/items/coin');
return coinItem ? (coinItem.count ?? 0) : 0;
}
if (window.lootDropsTrackerInstance?.spyCharacterItems) {
const coinItem = window.lootDropsTrackerInstance.spyCharacterItems.find(i => i.itemHrid === '/items/coin');
if (coinItem) return coinItem.count ?? 0;
}
const cachedData = CharacterDataStorage.get();
if (cachedData) {
if (cachedData.characterItems && Array.isArray(cachedData.characterItems)) {
const coinItem = cachedData.characterItems.find(i => i.itemHrid === '/items/coin');
return coinItem ? (coinItem.count ?? 0) : 0;
}
}
} catch (e) {
}
return 0;
}
getCachedInventory() {
try {
if (!window.lootDropsTrackerInstance?.spyCharacterItems && window._pendingInventoryData) {
return window._pendingInventoryData;
}
if (window.lootDropsTrackerInstance?.spyCharacterItems) {
return window.lootDropsTrackerInstance.spyCharacterItems;
}
const cachedData = CharacterDataStorage.get();
if (cachedData) {
if (cachedData.characterItems && Array.isArray(cachedData.characterItems)) {
return cachedData.characterItems;
}
}
} catch (e) {
}
return [];
}
getCurrentHouseData() {
try {
const cachedData = CharacterDataStorage.get();
if (cachedData) {
if (cachedData.characterHouseRoomMap) {
return cachedData.characterHouseRoomMap;
}
}
if (window.lootDropsTrackerInstance?.spyHouseData) {
return window.lootDropsTrackerInstance.spyHouseData;
}
} catch (e) {
}
return null;
}
getItemPrice(itemHrid, priceType) {
try {
if (window.lootDropsTrackerInstance && window.lootDropsTrackerInstance.spyMarketData) {
const marketData = window.lootDropsTrackerInstance.spyMarketData[itemHrid];
if (marketData && marketData[0]) {
const price = priceType === 'ask' ? (marketData[0].a ?? 0) : (marketData[0].b ?? 0);
if (price > 0) return price;
}
}
} catch (e) {
}
return null;
}
calculateUpgradeCost(recipe, priceType, cachedInventory = null) {
const currentGold = this.getCurrentGold(cachedInventory);
const goldNeeded = recipe.gold;
let totalCost = goldNeeded;
let hasMarketDataIssue = false;
Object.entries(recipe.materials).forEach(([itemHrid, amountNeeded]) => {
const amountHave = this.getItemCount(itemHrid, cachedInventory);
const amountToBuy = Math.max(0, amountNeeded - amountHave);
if (amountToBuy > 0) {
const itemPrice = this.getItemPrice(itemHrid, priceType);
if (itemPrice === null) {
hasMarketDataIssue = true;
} else {
totalCost += itemPrice * amountToBuy;
}
}
});
return { cost: totalCost, hasMarketDataIssue: hasMarketDataIssue };
}
isUpgradeAffordable(recipe, cachedInventory = null) {
const currentGold = this.getCurrentGold(cachedInventory);
const costData = this.calculateUpgradeCost(recipe, 'ask', cachedInventory);
if (costData.hasMarketDataIssue) {
return false;
}
return currentGold >= costData.cost;
}
calculateAllAffordability() {
const HOUSE_RECIPES = this.getHouseRecipes();
const cachedInventory = this.getCachedInventory();
let houseData;
if (this.jhouseLastHouseData) {
try {
houseData = JSON.parse(this.jhouseLastHouseData);
} catch (e) {
houseData = this.getCurrentHouseData();
}
} else {
houseData = this.getCurrentHouseData();
}
if (!houseData || Object.keys(houseData).length === 0) {
return { affordable: [], allAffordable: [], count: 0, allMaxed: false, cheapestAsk: null, cheapestBid: null, cheapestRoom: null };
}
const affordableRooms = [];
const allAffordableRooms = [];
let totalRooms = 0;
let maxedRooms = 0;
let cheapestAsk = null;
let cheapestBid = null;
let cheapestRoom = null;
const currentGold = this.getCurrentGold(cachedInventory);
Object.entries(houseData).forEach(([houseRoomHrid, roomData]) => {
totalRooms++;
const level = typeof roomData === 'object' ? roomData.level : roomData;
const nextLevel = level + 1;
if (level >= 8) {
maxedRooms++;
return;
}
const roomKey = houseRoomHrid.split('/').pop() || '';
const isChecked = this.jhouseFilters && this.jhouseFilters[roomKey] === false ? false : true;
const roomName = roomKey.replace(/_/g, ' ').split(' ').map(word =>
word.charAt(0).toUpperCase() + word.slice(1)
).join(' ');
const recipe = HOUSE_RECIPES[roomName]?.[nextLevel.toString()];
if (recipe) {
const costDataAsk = this.calculateUpgradeCost(recipe, 'ask', cachedInventory);
const costDataBid = this.calculateUpgradeCost(recipe, 'bid', cachedInventory);
const totalCostNeeded = costDataAsk.cost;
const canAfford = !costDataAsk.hasMarketDataIssue && currentGold >= totalCostNeeded;
if (canAfford) {
allAffordableRooms.push(roomKey);
}
if (isChecked) {
if (canAfford) {
affordableRooms.push(roomKey);
}
const totalAsk = costDataAsk.cost;
if (!costDataAsk.hasMarketDataIssue && (cheapestAsk === null || totalAsk < cheapestAsk)) {
cheapestAsk = totalAsk;
cheapestBid = costDataBid.cost;
cheapestRoom = roomKey;
}
}
}
});
const allMaxed = maxedRooms === totalRooms;
return {
affordable: affordableRooms,
allAffordable: allAffordableRooms,
count: affordableRooms.length,
allMaxed: allMaxed,
cheapestAsk: cheapestAsk,
cheapestBid: cheapestBid,
cheapestRoom: cheapestRoom
};
}
getProgressColor(have, need) {
if (have === 0) return '#ff4444';
if (have >= need) return '#4CAF50';
return '#FFA500';
}
createJHousePane() {
if (document.getElementById('jhouse-pane')) return;
const pane = document.createElement('div');
pane.id = 'jhouse-pane';
registerPanel('jhouse-pane');
pane.className = 'mcs-pane mcs-jh-pane';
const header = document.createElement('div');
header.className = 'mcs-pane-header mcs-jh-header';
const titleSection = document.createElement('div');
titleSection.className = 'mcs-jh-title-section';
const titleSpan = document.createElement('span');
titleSpan.className = 'mcs-pane-title';
titleSpan.textContent = 'JHouse';
const affordableSpan = document.createElement('span');
affordableSpan.id = 'jhouse-affordable-status';
affordableSpan.className = 'mcs-jh-affordable-status';
const cheapestSpan = document.createElement('span');
cheapestSpan.id = 'jhouse-cheapest-status';
cheapestSpan.className = 'mcs-jh-cheapest-status';
titleSection.appendChild(titleSpan);
titleSection.appendChild(affordableSpan);
titleSection.appendChild(cheapestSpan);
const buttonSection = document.createElement('div');
buttonSection.className = 'mcs-button-section';
const minimizeBtn = document.createElement('button');
minimizeBtn.id = 'jhouse-minimize-btn';
minimizeBtn.textContent = '−';
minimizeBtn.className = 'mcs-btn';
buttonSection.appendChild(minimizeBtn);
header.appendChild(titleSection);
header.appendChild(buttonSection);
const statusPane = document.createElement('div');
statusPane.id = 'jhouse-status-pane';
statusPane.className = 'mcs-jh-status-pane';
const statusLines = [
{ id: 'status-house-levels', text: 'Waiting for current house levels...' },
{ id: 'status-inventory', text: 'Waiting for current inventory...' },
{ id: 'status-recipes', text: 'Waiting for house recipes...' },
{ id: 'status-calculations', text: 'Waiting for initial calculations...' }
];
statusLines.forEach(line => {
const lineDiv = document.createElement('div');
lineDiv.id = line.id;
lineDiv.className = 'mcs-jh-status-line';
lineDiv.textContent = line.text;
statusPane.appendChild(lineDiv);
});
const content = document.createElement('div');
content.id = 'jhouse-content';
content.className = 'mcs-jh-content';
pane.appendChild(header);
pane.appendChild(statusPane);
pane.appendChild(content);
document.body.appendChild(pane);
this.makeJHouseDraggable(pane, header);
this.jhouseSelectedRoom = 'dairy_barn';
this.jhouseAffordableRooms = [];
this.jhouseLastHouseData = null;
this.jhouseInitialized = false;
this._cachedHouseRecipes = null;
const savedFilters = this.jhStorage.get('filters');
if (savedFilters) {
try {
this.jhouseFilters = typeof savedFilters === 'string' ? JSON.parse(savedFilters) : savedFilters;
} catch (e) {
this.jhouseFilters = {};
}
} else {
this.jhouseFilters = {};
}
const savedJhouseMinimized = this.jhStorage.get('minimized') === true || this.jhStorage.get('minimized') === 'true';
this.jhouseIsMinimized = savedJhouseMinimized;
if (savedJhouseMinimized) {
minimizeBtn.textContent = '+';
statusPane.style.display = 'none';
content.style.display = 'none';
header.style.borderRadius = '6px';
}
this.initializeJHouse();
minimizeBtn.onclick = () => {
this.jhouseIsMinimized = !this.jhouseIsMinimized;
if (this.jhouseIsMinimized) {
content.style.display = 'none';
statusPane.style.display = 'none';
minimizeBtn.textContent = '+';
header.style.borderRadius = '6px';
this.jhStorage.set('minimized', true);
} else {
if (this.jhouseInitialized) {
content.style.display = 'flex';
this.jhouseLastFullRender = 0;
this.updateJHouseContent(true);
} else {
statusPane.style.display = 'flex';
}
minimizeBtn.textContent = '−';
header.style.borderRadius = '6px 6px 0 0';
this.jhStorage.set('minimized', false);
this.constrainPanelToBoundaries('jhouse-pane', 'mcs_JH', true);
}
};
const self = this;
this.manualRefreshJHouse = function() {
self.updateJHouseContent();
};
window.refreshJHouse = this.manualRefreshJHouse;
this.jhouseInventoryListener = (event) => {
if (self.jhouseInitialized && event.detail?.items) {
setTimeout(() => self.updateJHouseContent(true), 100);
}
};
this.jhouseInventoryDataListener = (event) => {
if (self.jhouseInitialized && event.detail?.items) {
setTimeout(() => self.updateJHouseContent(true), 150);
}
};
this.jhouseWebSocketListener = (event) => {
const data = event.detail;
if (data?.type === 'item_update' || data?.type === 'inventory_update') {
if (self.jhouseInitialized) {
setTimeout(() => self.updateJHouseContent(true), 100);
}
}
if (data?.characterHouseRoomMap) {
const newHouseDataStr = JSON.stringify(data.characterHouseRoomMap);
let hasValidUpgrade = false;
let hasInvalidDowngrade = false;
if (self.jhouseLastHouseData) {
try {
const oldData = JSON.parse(self.jhouseLastHouseData);
Object.entries(data.characterHouseRoomMap).forEach(([roomHrid, roomData]) => {
const oldLevel = oldData[roomHrid]?.level ?? 0;
const newLevel = roomData.level ?? 0;
if (newLevel > oldLevel) {
hasValidUpgrade = true;
const roomName = roomHrid.split('/').pop();
} else if (newLevel < oldLevel) {
hasInvalidDowngrade = true;
}
});
} catch (e) { /* house level data may be malformed */ }
} else {
hasValidUpgrade = true;
}
if (hasValidUpgrade && !hasInvalidDowngrade) {
self.jhouseLastHouseData = newHouseDataStr;
if (self.jhouseInitialized) {
setTimeout(() => self.updateJHouseContent(true), 50);
}
} else if (hasInvalidDowngrade) {
const now = Date.now();
if (!self.jhouseLastStaleWSLog || (now - self.jhouseLastStaleWSLog) > 1000) {
self.jhouseLastStaleWSLog = now;
}
}
}
if (data?.characterItems && self.jhouseInitialized) {
if (window.lootDropsTrackerInstance?.spyCharacterItems) {
window.lootDropsTrackerInstance.spyCharacterItems = data.characterItems;
}
setTimeout(() => self.updateJHouseContent(true), 50);
}
if (data?.marketListings && self.jhouseInitialized) {
setTimeout(() => self.updateJHouseContent(true), 50);
}
};
window.addEventListener('EquipSpyInventoryUpdated', this.jhouseInventoryListener);
window.addEventListener('InventoryDataUpdated', this.jhouseInventoryDataListener);
window.addEventListener('EquipSpyWebSocketMessage', this.jhouseWebSocketListener);
VisibilityManager.register('jhouse-update', () => {
if (!self.jhouseInitialized) return;
const pane = document.getElementById('jhouse-pane');
const isPaneClosed = !pane || pane.style.display === 'none';
try {
const cachedData = CharacterDataStorage.get();
if (cachedData && window.lootDropsTrackerInstance) {
if (cachedData.characterItems) {
const oldItems = window.lootDropsTrackerInstance.spyCharacterItems || [];
const newItems = cachedData.characterItems;
if (oldItems.length !== newItems.length) {
window.lootDropsTrackerInstance.spyCharacterItems = newItems;
} else {
let hasChanges = false;
for (const newItem of newItems) {
const oldItem = oldItems.find(i => i.itemHrid === newItem.itemHrid);
if (!oldItem || oldItem.count !== newItem.count) {
hasChanges = true;
break;
}
}
if (hasChanges) {
window.lootDropsTrackerInstance.spyCharacterItems = newItems;
}
}
}
}
} catch (e) {
}
if (!isPaneClosed && !self.jhouseIsMinimized) {
self.updateJHouseContent(false);
} else {
const affordabilityData = self.calculateAllAffordability();
self.jhouseAffordableRooms = affordabilityData.affordable;
self.jhouseAllAffordableRooms = affordabilityData.allAffordable;
self.jhouseCheapestRoom = affordabilityData.cheapestRoom;
self.jhouseAffordabilityResult = affordabilityData;
if (!isPaneClosed) {
const affordableStatus = document.getElementById('jhouse-affordable-status');
const cheapestStatus = document.getElementById('jhouse-cheapest-status');
if (affordableStatus) {
if (affordabilityData.allMaxed) {
affordableStatus.style.color = '#FFD700';
affordableStatus.textContent = 'You are a true land baron!';
affordableStatus.style.display = 'block';
} else if (affordabilityData.count > 0) {
affordableStatus.style.color = '#4CAF50';
affordableStatus.textContent = `You can purchase ${affordabilityData.count}`;
affordableStatus.style.display = 'block';
} else {
affordableStatus.style.color = '#ff4444';
affordableStatus.textContent = 'You can purchase 0';
affordableStatus.style.display = 'block';
}
}
if (cheapestStatus) {
if (!affordabilityData.allMaxed && affordabilityData.cheapestAsk !== null && affordabilityData.cheapestBid !== null) {
const roomDisplayName = affordabilityData.cheapestRoom
? affordabilityData.cheapestRoom.replace(/_/g, ' ').split(' ').map(word => word.charAt(0).toUpperCase() + word.slice(1)).join(' ')
: '';
cheapestStatus.innerHTML = roomDisplayName
? `<div class="mcs-jh-cheapest-room-name">${roomDisplayName}</div><div>Cheapest ${self.formatGoldDetailed(affordabilityData.cheapestAsk)}a ${self.formatGoldDetailed(affordabilityData.cheapestBid)}b</div>`
: `Cheapest ${self.formatGoldDetailed(affordabilityData.cheapestAsk)}a ${self.formatGoldDetailed(affordabilityData.cheapestBid)}b`;
cheapestStatus.style.display = 'block';
} else {
cheapestStatus.style.display = 'none';
}
}
}
}
}, 5000);
}
initializeJHouse() {
const updateStatus = (id, complete) => {
const elem = document.getElementById(id);
if (elem) {
if (complete) {
elem.style.color = '#4CAF50';
elem.textContent = elem.textContent.replace('...', '...Complete');
}
}
};
const checkAndComplete = async () => {
let allComplete = true;
const houseData = this.getCurrentHouseData();
if (houseData && Object.keys(houseData).length > 0) {
updateStatus('status-house-levels', true);
this.jhouseLastHouseData = JSON.stringify(houseData);
} else {
allComplete = false;
}
const gold = this.getCurrentGold();
if (gold !== 0 || this.getItemCount('/items/coin') >= 0) {
updateStatus('status-inventory', true);
} else {
allComplete = false;
}
const recipes = this.getHouseRecipes();
if (recipes && Object.keys(recipes).length > 0) {
updateStatus('status-recipes', true);
} else {
allComplete = false;
}
if (allComplete) {
updateStatus('status-calculations', true);
setTimeout(() => {
const statusPane = document.getElementById('jhouse-status-pane');
const content = document.getElementById('jhouse-content');
if (statusPane && content) {
statusPane.style.display = 'none';
if (!this.jhouseIsMinimized) {
content.style.display = 'flex';
}
this.jhouseInitialized = true;
this.updateJHouseContent();
}
}, 500);
return true;
}
return false;
};
const startTime = Date.now();
const MAX_INIT_TIME = 30000;
const initInterval = setInterval(() => {
if (checkAndComplete()) {
clearInterval(initInterval);
} else if (Date.now() - startTime > MAX_INIT_TIME) {
clearInterval(initInterval);
}
}, 200);
checkAndComplete();
}
destroyJHouse() {
VisibilityManager.clear('jhouse-update');
if (this.jhouseInventoryListener) {
window.removeEventListener('EquipSpyInventoryUpdated', this.jhouseInventoryListener);
this.jhouseInventoryListener = null;
}
if (this.jhouseInventoryDataListener) {
window.removeEventListener('InventoryDataUpdated', this.jhouseInventoryDataListener);
this.jhouseInventoryDataListener = null;
}
if (this.jhouseWebSocketListener) {
window.removeEventListener('EquipSpyWebSocketMessage', this.jhouseWebSocketListener);
this.jhouseWebSocketListener = null;
}
const pane = document.getElementById('jhouse-pane');
if (pane) {
pane.remove();
}
this.jhouseInitialized = false;
}
updateJHouseContent(forceFullRender = false) {
let houseData;
if (this.jhouseLastHouseData) {
try {
houseData = JSON.parse(this.jhouseLastHouseData);
} catch (e) {
houseData = this.getCurrentHouseData();
}
} else {
houseData = this.getCurrentHouseData();
}
const affordabilityData = this.calculateAllAffordability();
this.jhouseAffordableRooms = affordabilityData.affordable;
this.jhouseAllAffordableRooms = affordabilityData.allAffordable;
this.jhouseCheapestRoom = affordabilityData.cheapestRoom;
this.jhouseAffordabilityResult = affordabilityData;
const affordableStatus = document.getElementById('jhouse-affordable-status');
const cheapestStatus = document.getElementById('jhouse-cheapest-status');
if (affordableStatus) {
if (affordabilityData.allMaxed) {
affordableStatus.style.color = '#FFD700';
affordableStatus.textContent = 'You are a true land baron!';
affordableStatus.style.display = 'block';
} else if (affordabilityData.count > 0) {
affordableStatus.style.color = '#4CAF50';
affordableStatus.textContent = `You can purchase ${affordabilityData.count}`;
affordableStatus.style.display = 'block';
} else {
affordableStatus.style.color = '#ff4444';
affordableStatus.textContent = 'You can purchase 0';
affordableStatus.style.display = 'block';
}
}
if (cheapestStatus) {
if (!affordabilityData.allMaxed && affordabilityData.cheapestAsk !== null && affordabilityData.cheapestBid !== null) {
const roomDisplayName = affordabilityData.cheapestRoom
? affordabilityData.cheapestRoom.replace(/_/g, ' ').split(' ').map(word => word.charAt(0).toUpperCase() + word.slice(1)).join(' ')
: '';
cheapestStatus.innerHTML = roomDisplayName
? `<div class="mcs-jh-cheapest-room-name">${roomDisplayName}</div><div>Cheapest ${this.formatGoldDetailed(affordabilityData.cheapestAsk)}a ${this.formatGoldDetailed(affordabilityData.cheapestBid)}b</div>`
: `Cheapest ${this.formatGoldDetailed(affordabilityData.cheapestAsk)}a ${this.formatGoldDetailed(affordabilityData.cheapestBid)}b`;
cheapestStatus.style.display = 'block';
} else {
cheapestStatus.style.display = 'none';
}
}
const content = document.getElementById('jhouse-content');
if (!content) return;
if (this.jhouseIsMinimized) {
return;
}
const now = Date.now();
const timeSinceLastRender = now - (this.jhouseLastFullRender ?? 0);
const needsFullRender = forceFullRender || timeSinceLastRender > 10000;
if (!needsFullRender) {
this.updateDetailPanelOnly(houseData);
return;
}
this.jhouseLastFullRender = now;
content.innerHTML = '';
if (!houseData || Object.keys(houseData).length === 0) {
const noDataDiv = document.createElement('div');
noDataDiv.className = 'mcs-jh-no-data';
noDataDiv.textContent = 'No house data available';
content.appendChild(noDataDiv);
return;
}
const mainContainer = document.createElement('div');
mainContainer.className = 'mcs-jh-main-grid';
const gridContainer = document.createElement('div');
gridContainer.className = 'mcs-jh-room-grid';
const houseRoomOrder = [
'dairy_barn',
'garden',
'log_shed',
'forge',
'workshop',
'sewing_parlor',
'kitchen',
'brewery',
'laboratory',
'observatory',
'dining_room',
'library',
'dojo',
'armory',
'gym',
'archery_range',
'mystical_study'
];
const houseRooms = Object.entries(houseData).sort((a, b) => {
const keyA = a[0].split('/').pop() || '';
const keyB = b[0].split('/').pop() || '';
const indexA = houseRoomOrder.indexOf(keyA);
const indexB = houseRoomOrder.indexOf(keyB);
return indexA - indexB;
});
const houseRoomToIconMap = {
'dairy_barn': 'milking',
'archery_range': 'ranged',
'garden': 'foraging',
'log_shed': 'woodcutting',
'forge': 'cheesesmithing',
'workshop': 'crafting',
'sewing_parlor': 'tailoring',
'kitchen': 'cooking',
'brewery': 'brewing',
'laboratory': 'alchemy',
'observatory': 'enhancing',
'dining_room': 'stamina',
'library': 'intelligence',
'dojo': 'attack',
'armory': 'defense',
'gym': 'melee',
'mystical_study': 'magic'
};
houseRooms.forEach(([houseRoomHrid, roomData]) => {
const level = typeof roomData === 'object' ? roomData.level : roomData;
const roomName = (houseRoomHrid.split('/').pop() || '').replace(/_/g, ' ');
const displayName = roomName.split(' ').map(word =>
word.charAt(0).toUpperCase() + word.slice(1)
).join(' ');
const roomKey = houseRoomHrid.split('/').pop() || '';
const iconId = houseRoomToIconMap[roomKey] || roomKey;
const isAffordable = this.jhouseAllAffordableRooms && this.jhouseAllAffordableRooms.includes(roomKey);
const isCheapest = this.jhouseCheapestRoom === roomKey;
const isChecked = this.jhouseFilters[roomKey] !== false;
let backgroundColor = '#333';
if (isCheapest && isChecked) {
backgroundColor = 'rgba(255, 215, 0, 0.2)';
} else if (isAffordable) {
backgroundColor = isChecked ? 'rgba(76, 175, 80, 0.2)' : 'rgba(100, 150, 255, 0.05)';
}
let borderColor = 'transparent';
if (roomKey === this.jhouseSelectedRoom) {
borderColor = '#FFA500';
} else if (isCheapest && isChecked) {
borderColor = 'rgba(255, 215, 0, 0.5)';
} else if (isAffordable) {
borderColor = isChecked ? 'rgba(76, 175, 80, 0.5)' : 'rgba(100, 150, 255, 0.2)';
}
const roomDiv = document.createElement('div');
roomDiv.className = 'mcs-jh-room';
roomDiv.style.background = backgroundColor;
roomDiv.style.borderColor = borderColor;
roomDiv.onclick = (e) => {
if (e.target.type === 'checkbox') return;
this.jhouseSelectedRoom = roomKey;
this.jhouseLastFullRender = 0;
this.updateJHouseContent();
};
roomDiv.onmouseover = () => {
if (roomKey !== this.jhouseSelectedRoom) {
if (isCheapest && isChecked) {
roomDiv.style.background = 'rgba(255, 215, 0, 0.3)';
} else if (isAffordable) {
roomDiv.style.background = isChecked ? 'rgba(76, 175, 80, 0.3)' : 'rgba(100, 150, 255, 0.15)';
} else {
roomDiv.style.background = '#444';
}
}
};
roomDiv.onmouseout = () => {
if (roomKey !== this.jhouseSelectedRoom) {
if (isCheapest && isChecked) {
roomDiv.style.background = 'rgba(255, 215, 0, 0.2)';
} else if (isAffordable) {
roomDiv.style.background = isChecked ? 'rgba(76, 175, 80, 0.2)' : 'rgba(100, 150, 255, 0.05)';
} else {
roomDiv.style.background = '#333';
}
}
};
const svgIcon = createItemIcon(iconId, { width: 16, height: 16, sprite: 'skills' });
svgIcon.style.flexShrink = '0';
roomDiv.appendChild(svgIcon);
const levelSpan = document.createElement('span');
levelSpan.className = 'mcs-jh-room-level';
levelSpan.textContent = `Lv ${level}`;
roomDiv.appendChild(levelSpan);
const checkbox = document.createElement('input');
checkbox.type = 'checkbox';
if (!this.jhouseFilters) {
this.jhouseFilters = {};
}
checkbox.checked = this.jhouseFilters[roomKey] !== false;
checkbox.className = 'mcs-jh-room-checkbox';
checkbox.onclick = (e) => {
e.stopPropagation();
if (!this.jhouseFilters) {
this.jhouseFilters = {};
}
this.jhouseFilters[roomKey] = checkbox.checked;
this.jhStorage.set('filters', this.jhouseFilters);
this.jhouseLastFullRender = 0;
this.updateJHouseContent();
};
roomDiv.appendChild(checkbox);
gridContainer.appendChild(roomDiv);
});
const detailPanel = document.createElement('div');
detailPanel.className = 'mcs-jh-detail-panel';
const selectedRoomName = this.jhouseSelectedRoom.replace(/_/g, ' ').split(' ').map(word =>
word.charAt(0).toUpperCase() + word.slice(1)
).join(' ');
let currentLevel = 0;
for (const [houseRoomHrid, roomData] of Object.entries(houseData)) {
const roomKey = houseRoomHrid.split('/').pop() || '';
if (roomKey === this.jhouseSelectedRoom) {
currentLevel = typeof roomData === 'object' ? roomData.level : roomData;
break;
}
}
const detailTitle = document.createElement('div');
detailTitle.className = 'mcs-jh-detail-title';
detailTitle.textContent = `${selectedRoomName} ${currentLevel}`;
detailPanel.appendChild(detailTitle);
const nextLevel = currentLevel + 1;
const HOUSE_RECIPES = this.getHouseRecipes();
const recipe = HOUSE_RECIPES[selectedRoomName]?.[nextLevel.toString()];
const cachedInventory = this.getCachedInventory();
if (recipe && nextLevel <= 8) {
const costDataAsk = this.calculateUpgradeCost(recipe, 'ask', cachedInventory);
const costDataBid = this.calculateUpgradeCost(recipe, 'bid', cachedInventory);
const totalAsk = costDataAsk.cost;
const totalBid = costDataBid.cost;
const recipeTitle = document.createElement('div');
recipeTitle.className = 'mcs-jh-recipe-title';
let costText = `${nextLevel} `;
if (costDataBid.hasMarketDataIssue && costDataAsk.hasMarketDataIssue) {
costText += 'has no market data';
} else {
costText += 'costs ';
if (!costDataAsk.hasMarketDataIssue) {
costText += `${this.formatGold(totalAsk)}a `;
}
if (!costDataBid.hasMarketDataIssue) {
costText += `${this.formatGold(totalBid)}b`;
}
}
recipeTitle.textContent = costText.trim();
detailPanel.appendChild(recipeTitle);
const materialsCostLabel = document.createElement('div');
materialsCostLabel.className = 'mcs-jh-section-label mcs-jh-label-materials-cost';
materialsCostLabel.textContent = 'Materials Cost';
detailPanel.appendChild(materialsCostLabel);
const totalCostValues = document.createElement('div');
totalCostValues.className = 'mcs-jh-cost-values';
const askSpan = document.createElement('span');
askSpan.className = 'mcs-jh-ask-value';
askSpan.textContent = costDataAsk.hasMarketDataIssue ? '⚠️ No market data' : `${this.formatGold(costDataAsk.cost - recipe.gold)} ask`;
const bidSpan = document.createElement('span');
bidSpan.className = 'mcs-jh-bid-value';
bidSpan.textContent = costDataBid.hasMarketDataIssue ? '⚠️ No market data' : `${this.formatGold(costDataBid.cost - recipe.gold)} bid`;
totalCostValues.appendChild(askSpan);
totalCostValues.appendChild(bidSpan);
detailPanel.appendChild(totalCostValues);
const currentGold = this.getCurrentGold(cachedInventory);
const goldNeeded = recipe.gold;
const goldColor = this.getProgressColor(currentGold, goldNeeded);
const goldLabel = document.createElement('div');
goldLabel.className = 'mcs-jh-section-label mcs-jh-label-gold';
goldLabel.textContent = 'Gold';
detailPanel.appendChild(goldLabel);
const goldValue = document.createElement('div');
goldValue.className = 'mcs-jh-gold-value';
goldValue.style.color = goldColor;
goldValue.textContent = `${this.formatGold(currentGold)} / ${this.formatGold(goldNeeded)}`;
detailPanel.appendChild(goldValue);
const materialsTitle = document.createElement('div');
materialsTitle.className = 'mcs-jh-materials-title';
materialsTitle.textContent = 'Materials Required';
detailPanel.appendChild(materialsTitle);
const materialsList = document.createElement('div');
materialsList.className = 'mcs-jh-materials-list';
Object.entries(recipe.materials).forEach(([itemHrid, amountNeeded]) => {
const amountHave = this.getItemCount(itemHrid, cachedInventory);
const progressColor = this.getProgressColor(amountHave, amountNeeded);
const amountToBuy = Math.max(0, amountNeeded - amountHave);
const hasAskData = amountToBuy > 0 ? this.getItemPrice(itemHrid, 'ask') !== null : true;
const hasBidData = amountToBuy > 0 ? this.getItemPrice(itemHrid, 'bid') !== null : true;
const hasMarketData = hasAskData && hasBidData;
const materialDiv = document.createElement('div');
materialDiv.className = 'mcs-jh-material-row';
materialDiv.addEventListener('click', () => {
mcsGoToMarketplace(itemHrid);
});
const itemName = document.createElement('span');
itemName.className = 'mcs-jh-material-name';
itemName.textContent = this.formatItemName(itemHrid) + (!hasMarketData ? ' ⚠️' : '');
const itemAmount = document.createElement('span');
itemAmount.className = 'mcs-jh-material-amount';
itemAmount.style.color = progressColor;
itemAmount.textContent = `${amountHave.toLocaleString()} / ${amountNeeded.toLocaleString()}`;
materialDiv.appendChild(itemName);
materialDiv.appendChild(itemAmount);
materialsList.appendChild(materialDiv);
});
detailPanel.appendChild(materialsList);
} else if (currentLevel >= 8) {
const maxLevelDiv = document.createElement('div');
maxLevelDiv.className = 'mcs-jh-max-level';
maxLevelDiv.textContent = 'MAX LEVEL REACHED!';
detailPanel.appendChild(maxLevelDiv);
} else {
const noRecipeDiv = document.createElement('div');
noRecipeDiv.className = 'mcs-jh-no-recipe';
noRecipeDiv.textContent = 'Recipe data not available';
detailPanel.appendChild(noRecipeDiv);
}
mainContainer.appendChild(gridContainer);
mainContainer.appendChild(detailPanel);
content.appendChild(mainContainer);
}
updateDetailPanelOnly(houseData) {
if (!houseData) return;
const detailPanel = document.querySelector('#jhouse-content > div > div:nth-child(2)');
if (!detailPanel) return;
const cachedInventory = this.getCachedInventory();
const selectedRoomName = this.jhouseSelectedRoom.replace(/_/g, ' ').split(' ').map(word =>
word.charAt(0).toUpperCase() + word.slice(1)
).join(' ');
let currentLevel = 0;
for (const [houseRoomHrid, roomData] of Object.entries(houseData)) {
const roomKey = houseRoomHrid.split('/').pop() || '';
if (roomKey === this.jhouseSelectedRoom) {
currentLevel = typeof roomData === 'object' ? roomData.level : roomData;
break;
}
}
const nextLevel = currentLevel + 1;
const HOUSE_RECIPES = this.getHouseRecipes();
const recipe = HOUSE_RECIPES[selectedRoomName]?.[nextLevel.toString()];
if (!recipe || nextLevel > 8) return;
const costDataAsk = this.calculateUpgradeCost(recipe, 'ask', cachedInventory);
const costDataBid = this.calculateUpgradeCost(recipe, 'bid', cachedInventory);
const askSpan = detailPanel.querySelector('.mcs-jh-ask-value');
const bidSpan = detailPanel.querySelector('.mcs-jh-bid-value');
if (askSpan) {
askSpan.textContent = costDataAsk.hasMarketDataIssue ? '⚠️ No market data' : `${this.formatGold(costDataAsk.cost - recipe.gold)} ask`;
}
if (bidSpan) {
bidSpan.textContent = costDataBid.hasMarketDataIssue ? '⚠️ No market data' : `${this.formatGold(costDataBid.cost - recipe.gold)} bid`;
}
const currentGold = this.getCurrentGold(cachedInventory);
const goldNeeded = recipe.gold;
const goldColor = this.getProgressColor(currentGold, goldNeeded);
const goldValueDiv = detailPanel.querySelector('.mcs-jh-gold-value');
if (goldValueDiv) {
goldValueDiv.style.color = goldColor;
goldValueDiv.textContent = `${this.formatGold(currentGold)} / ${this.formatGold(goldNeeded)}`;
}
const materialDivs = detailPanel.querySelectorAll('.mcs-jh-material-row');
let materialIndex = 0;
Object.entries(recipe.materials).forEach(([itemHrid, amountNeeded]) => {
if (materialIndex >= materialDivs.length) return;
const amountHave = this.getItemCount(itemHrid, cachedInventory);
const progressColor = this.getProgressColor(amountHave, amountNeeded);
const materialDiv = materialDivs[materialIndex];
const amountSpan = materialDiv.querySelector('span:last-child');
if (amountSpan) {
amountSpan.style.color = progressColor;
amountSpan.textContent = `${amountHave.toLocaleString()} / ${amountNeeded.toLocaleString()}`;
}
materialIndex++;
});
}
makeJHouseDraggable(pane, header) {
DragHandler.makeDraggable(pane, header, 'mcs_JH');
}
// JHouse end
// KOllection start
get _koMatrix() {
if (!this.__koMatrixCached) {
const km = {};
km.zeros = (rows, cols) => {
const data = [];
for (let i = 0; i < rows; i++) data[i] = new Float64Array(cols);
return { rows, cols, data, get(r, c) { return this.data[r][c]; }, set(r, c, v) { this.data[r][c] = v; } };
};
km.identity = (n) => {
const m = km.zeros(n, n);
for (let i = 0; i < n; i++) m.data[i][i] = 1;
return m;
};
km.subtract = (a, b) => {
const m = km.zeros(a.rows, a.cols);
for (let i = 0; i < a.rows; i++)
for (let j = 0; j < a.cols; j++)
m.data[i][j] = a.data[i][j] - b.data[i][j];
return m;
};
km.subset = (mat, r0, r1, c0, c1) => {
const m = km.zeros(r1 - r0, c1 - c0);
for (let i = r0; i < r1; i++)
for (let j = c0; j < c1; j++)
m.data[i - r0][j - c0] = mat.data[i][j];
return m;
};
km.inv = (mat) => {
const n = mat.rows;
const aug = km.zeros(n, 2 * n);
for (let i = 0; i < n; i++) {
for (let j = 0; j < n; j++) aug.data[i][j] = mat.data[i][j];
aug.data[i][n + i] = 1;
}
for (let col = 0; col < n; col++) {
let maxRow = col;
for (let row = col + 1; row < n; row++)
if (Math.abs(aug.data[row][col]) > Math.abs(aug.data[maxRow][col])) maxRow = row;
[aug.data[col], aug.data[maxRow]] = [aug.data[maxRow], aug.data[col]];
const pivot = aug.data[col][col];
if (Math.abs(pivot) < 1e-12) continue;
for (let j = 0; j < 2 * n; j++) aug.data[col][j] /= pivot;
for (let row = 0; row < n; row++) {
if (row === col) continue;
const factor = aug.data[row][col];
for (let j = 0; j < 2 * n; j++) aug.data[row][j] -= factor * aug.data[col][j];
}
}
return km.subset(aug, 0, n, n, 2 * n);
};
km.rowSum = (mat, row) => {
let sum = 0;
for (let j = 0; j < mat.cols; j++) sum += mat.data[row][j];
return sum;
};
km.rowValues = (mat, row, c0, c1) => {
const arr = [];
for (let j = c0; j < c1; j++) arr.push(mat.data[row][j]);
return arr;
};
this.__koMatrixCached = km;
}
return this.__koMatrixCached;
}
get koStorage() {
if (!this._koStorage) {
this._koStorage = createModuleStorage('KO');
}
return this._koStorage;
}
mcs_ko_handleWebSocketMessage(event) {
if (window.MCS_MODULES_DISABLED) return;
const data = event.detail;
if (data && data.type === "init_character_data") {
this.mcs_ko_characterItems = data.characterItems;
this.mcs_ko_characterHouseRoomMap = data.characterHouseRoomMap;
this.mcs_ko_combatAbilities = data.combatUnit?.combatAbilities;
this.mcs_ko_character = data.character;
this.mcs_ko_characterSkills = data.characterSkills;
if (data.myMarketListings && window.lootDropsTrackerInstance) {
window.lootDropsTrackerInstance.myMarketListings = data.myMarketListings;
}
setTimeout(() => this.mcs_ko_updateScores(), 100);
}
if (data && data.type === "init_client_data") {
this.mcs_ko_levelExperienceTable = data.levelExperienceTable;
this.mcs_ko_houseRoomDetailMap = data.houseRoomDetailMap;
this.mcs_ko_itemDetailMap = data.itemDetailMap;
this.mcs_ko_actionDetailMap = data.actionDetailMap;
setTimeout(() => this.mcs_ko_updateScores(), 100);
}
if (data?.characterItems || data?.characterHouseRoomMap || data?.combatAbilities) {
if (data.characterItems) this.mcs_ko_characterItems = data.characterItems;
if (data.characterHouseRoomMap) this.mcs_ko_characterHouseRoomMap = data.characterHouseRoomMap;
if (data.combatAbilities) this.mcs_ko_combatAbilities = data.combatAbilities;
setTimeout(() => this.mcs_ko_updateScores(), 100);
}
if (data && data.type === "profile_shared") {
setTimeout(() => this.mcs_ko_handleProfileShared(data), 100);
}
if (data && data.type === "market_listings_updated") {
if (data.myMarketListings && window.lootDropsTrackerInstance) {
window.lootDropsTrackerInstance.myMarketListings = data.myMarketListings;
}
setTimeout(() => this.mcs_ko_updateScores(), 100);
}
}
mcs_ko_handleEquipmentChanged() {
if (window.mcs__global_equipment_tracker?.allCharacterItems) {
this.mcs_ko_characterItems = window.mcs__global_equipment_tracker.allCharacterItems;
}
this.mcs_ko_lastDataHash = null;
this.mcs_ko_updateScores();
}
mcs_ko_addSpecialItems(marketData) {
marketData['/items/coin'] = [{ a: 1, b: 1 }];
marketData['/items/task_token'] = [{ a: 0, b: 0 }];
marketData['/items/cowbell'] = [{ a: 0, b: 0 }];
marketData['/items/small_treasure_chest'] = [{ a: 0, b: 0 }];
marketData['/items/medium_treasure_chest'] = [{ a: 0, b: 0 }];
marketData['/items/large_treasure_chest'] = [{ a: 0, b: 0 }];
marketData['/items/basic_task_badge'] = [{ a: 0, b: 0 }];
marketData['/items/advanced_task_badge'] = [{ a: 0, b: 0 }];
marketData['/items/expert_task_badge'] = [{ a: 0, b: 0 }];
return marketData;
}
async mcs_ko_fetchMarketData() {
if (window.lootDropsTrackerInstance?.spyMarketData) {
let marketData = {...window.lootDropsTrackerInstance.spyMarketData};
marketData = this.mcs_ko_addSpecialItems(marketData);
const itemCount = Object.keys(marketData).length;
if (itemCount > 0) {
return marketData;
}
}
return null;
}
mcs_ko_getWeightedPrice(marketPrices, ratio = 0.5) {
if (!marketPrices || !marketPrices[0]) return 0;
let ask = marketPrices[0].a;
let bid = marketPrices[0].b;
if (ask > 0 && bid < 0) {
bid = ask;
}
if (bid > 0 && ask < 0) {
ask = bid;
}
const weightedPrice = ask * ratio + bid * (1 - ratio);
return weightedPrice;
}
async mcs_ko_calculateHouseScore() {
const [score] = await this.mcs_ko_calculateHouseScoreWithDetails();
return score;
}
async mcs_ko_calculateHouseScoreWithDetails() {
const marketData = await this.mcs_ko_fetchMarketData();
if (!marketData) {
return [0, []];
}
try {
if (!this.mcs_ko_characterHouseRoomMap || !this.mcs_ko_houseRoomDetailMap) {
return [0, []];
}
const battleHouses = ["dining_room", "library", "dojo", "gym", "armory", "archery_range", "mystical_study"];
let battleHouseScore = 0;
const details = [];
for (const key in this.mcs_ko_characterHouseRoomMap) {
const house = this.mcs_ko_characterHouseRoomMap[key];
if (battleHouses.some((battleHouse) => house.houseRoomHrid.includes(battleHouse))) {
const houseCost = await this.mcs_ko_getHouseFullBuildPrice(house, marketData);
const scoreValue = houseCost / 1000000;
battleHouseScore += scoreValue;
const houseDetail = this.mcs_ko_houseRoomDetailMap[house.houseRoomHrid];
const houseName = houseDetail ? houseDetail.name : house.houseRoomHrid.replace('/house_rooms/', '');
details.push({
name: `${houseName} ${house.level}`,
value: scoreValue.toFixed(1)
});
}
}
details.sort((a, b) => parseFloat(b.value) - parseFloat(a.value));
return [battleHouseScore, details];
} catch (e) {
console.error('[KOllection] Error calculating house score:', e);
return [0, []];
}
}
async mcs_ko_getHouseFullBuildPrice(house, marketData) {
try {
if (!this.mcs_ko_houseRoomDetailMap[house.houseRoomHrid]) {
return 0;
}
const upgradeCostsMap = this.mcs_ko_houseRoomDetailMap[house.houseRoomHrid].upgradeCostsMap;
if (!upgradeCostsMap) {
return 0;
}
const level = house.level;
let cost = 0;
for (let i = 1; i <= level; i++) {
const levelUpgrades = upgradeCostsMap[i];
if (!levelUpgrades) {
continue;
}
for (const item of levelUpgrades) {
const marketPrices = marketData[item.itemHrid];
if (marketPrices && marketPrices[0]) {
const itemCost = item.count * this.mcs_ko_getWeightedPrice(marketPrices);
cost += itemCost;
}
}
}
return cost;
} catch (e) {
console.error(`[KOllection] Error in getHouseFullBuildPrice for ${house.houseRoomHrid}:`, e);
return 0;
}
}
async mcs_ko_calculateAbilityScore() {
const [score] = await this.mcs_ko_calculateAbilityScoreWithDetails();
return score;
}
async mcs_ko_calculateAbilityScoreWithDetails() {
const marketData = await this.mcs_ko_fetchMarketData();
if (!marketData) return [0, []];
const exp_50_skill = ["poke", "scratch", "smack", "quick_shot", "water_strike", "fireball", "entangle", "minor_heal"];
try {
if (!this.mcs_ko_combatAbilities || !this.mcs_ko_levelExperienceTable) {
return [0, []];
}
let totalPrice = 0;
const details = [];
this.mcs_ko_combatAbilities.forEach((ability) => {
const needExp = this.mcs_ko_levelExperienceTable[ability.level] ?? 0;
const expPerBook = exp_50_skill.some(skill => ability.abilityHrid.includes(skill)) ? 50 : 500;
let needBooks = needExp / expPerBook;
needBooks += 1;
const itemHrid = ability.abilityHrid.replace("/abilities/", "/items/");
const marketPrices = marketData[itemHrid];
if (marketPrices && marketPrices[0]) {
const weightedPrice = this.mcs_ko_getWeightedPrice(marketPrices);
const cost = needBooks * weightedPrice;
totalPrice += cost;
const abilityName = ability.abilityHrid.replace('/abilities/', '').split('_').map(w => w.charAt(0).toUpperCase() + w.slice(1)).join(' ');
details.push({
name: `${abilityName} ${ability.level}`,
value: (cost / 1000000).toFixed(1)
});
}
});
details.sort((a, b) => parseFloat(b.value) - parseFloat(a.value));
return [totalPrice / 1000000, details];
} catch (e) {
console.error('[KOllection] Error calculating ability score:', e);
return [0, []];
}
}
async mcs_ko_calculateEquippedAbilityScore() {
const marketData = await this.mcs_ko_fetchMarketData();
if (!marketData) return 0;
const exp_50_skill = ["poke", "scratch", "smack", "quick_shot", "water_strike", "fireball", "entangle", "minor_heal"];
try {
if (!this.mcs_ko_combatAbilities) {
return 0;
}
if (!this.mcs_ko_levelExperienceTable) {
return 0;
}
let totalPrice = 0;
const equippedAbilities = this.mcs_ko_combatAbilities.filter(ability => ability.isEquipped);
equippedAbilities.forEach((ability) => {
const needExp = this.mcs_ko_levelExperienceTable[ability.level] ?? 0;
const expPerBook = exp_50_skill.some(skill => ability.abilityHrid.includes(skill)) ? 50 : 500;
let needBooks = needExp / expPerBook;
needBooks += 1;
needBooks = parseFloat(needBooks.toFixed(1));
const itemHrid = ability.abilityHrid.replace("/abilities/", "/items/");
const marketPrices = marketData[itemHrid];
if (marketPrices && marketPrices[0]) {
const weightedPrice = this.mcs_ko_getWeightedPrice(marketPrices);
const cost = needBooks * weightedPrice;
totalPrice += cost;
}
});
return totalPrice / 1000000;
} catch (e) {
console.error('[KOllection] Error calculating equipped ability score:', e);
return 0;
}
}
mcs_ko_buildWearableItemMap() {
if (window.mcs__global_equipment_tracker?.allCharacterItems) {
const wearableItemMap = {};
for (const item of window.mcs__global_equipment_tracker.allCharacterItems) {
if (item.itemLocationHrid &&
!item.itemLocationHrid.includes('/item_locations/inventory') &&
item.count > 0) {
wearableItemMap[item.itemLocationHrid] = {
itemLocationHrid: item.itemLocationHrid,
itemHrid: item.itemHrid,
enhancementLevel: item.enhancementLevel ?? 0,
count: item.count
};
}
}
return wearableItemMap;
}
if (!this.mcs_ko_characterItems) return null;
const wearableItemMap = {};
for (const item of this.mcs_ko_characterItems) {
if (item.itemLocationHrid &&
!item.itemLocationHrid.includes('/item_locations/inventory') &&
item.count > 0) {
wearableItemMap[item.itemLocationHrid] = {
itemLocationHrid: item.itemLocationHrid,
itemHrid: item.itemHrid,
enhancementLevel: item.enhancementLevel ?? 0,
count: item.count
};
}
}
return wearableItemMap;
}
async mcs_ko_calculateEquipmentScore() {
const [score] = await this.mcs_ko_calculateEquipmentScoreWithDetails();
return score;
}
async mcs_ko_calculateEquipmentScoreWithDetails() {
const marketData = await this.mcs_ko_fetchMarketData();
if (!marketData) return [0, []];
try {
const wearableItemMap = this.mcs_ko_buildWearableItemMap();
if (!wearableItemMap) {
return [0, []];
}
let networthAsk = 0;
let networthBid = 0;
const details = [];
for (const key in wearableItemMap) {
const item = wearableItemMap[key];
const enhanceLevel = item.enhancementLevel ?? 0;
const itemHrid = item.itemHrid;
const marketPrices = marketData[itemHrid];
let itemCost = 0;
if (enhanceLevel && enhanceLevel > 1) {
let totalCost = await this.mcs_ko_estimateEnhancementCost(itemHrid, enhanceLevel, marketData);
totalCost = totalCost ? Math.round(totalCost) : 0;
itemCost = totalCost;
networthAsk += item.count * (totalCost > 0 ? totalCost : 0);
networthBid += item.count * (totalCost > 0 ? totalCost : 0);
} else if (marketPrices && marketPrices[0]) {
const askPrice = marketPrices[0].a > 0 ? marketPrices[0].a : 0;
const bidPrice = marketPrices[0].b > 0 ? marketPrices[0].b : 0;
itemCost = askPrice * 0.5 + bidPrice * 0.5;
networthAsk += item.count * askPrice;
networthBid += item.count * bidPrice;
}
const itemDetail = this.mcs_ko_itemDetailMap?.[itemHrid];
let itemName = itemDetail ? itemDetail.name : itemHrid.replace('/items/', '');
let displayName = itemName;
if (displayName.startsWith('Refined ')) {
displayName = displayName.replace('Refined ', '') + ' (R)';
}
if (enhanceLevel && enhanceLevel > 0) {
displayName += ` +${enhanceLevel}`;
}
details.push({
name: displayName,
value: (itemCost / 1000000).toFixed(1)
});
}
details.sort((a, b) => parseFloat(b.value) - parseFloat(a.value));
const equippedNetworth = networthAsk * 0.5 + networthBid * 0.5;
return [equippedNetworth / 1000000, details];
} catch (e) {
console.error('[KOllection] Error calculating equipment score:', e);
return [0, []];
}
}
async mcs_ko_calculateNetWorth() {
const marketData = await this.mcs_ko_fetchMarketData();
if (!marketData) return { total: 0, equipment: 0, inventory: 0, market: 0, houses: 0, abilities: 0 };
try {
let equipmentNetworthAsk = 0;
let equipmentNetworthBid = 0;
let inventoryNetworthAsk = 0;
let inventoryNetworthBid = 0;
let marketValue = 0;
let housesValue = 0;
let abilitiesValue = 0;
const characterItems = this.mcs_ko_characterItems ?? [];
for (const item of characterItems) {
if (!item.itemHrid) continue;
const enhanceLevel = item.enhancementLevel ?? 0;
const marketPrices = marketData[item.itemHrid];
if (enhanceLevel > 1) {
const totalCost = await this.mcs_ko_estimateEnhancementCost(item.itemHrid, enhanceLevel, marketData);
const cost = totalCost ? Math.round(totalCost) : 0;
if (item.itemLocationHrid === '/item_locations/inventory') {
inventoryNetworthAsk += item.count * (cost > 0 ? cost : 0);
inventoryNetworthBid += item.count * (cost > 0 ? cost : 0);
} else {
equipmentNetworthAsk += item.count * (cost > 0 ? cost : 0);
equipmentNetworthBid += item.count * (cost > 0 ? cost : 0);
}
} else if (marketPrices && marketPrices[0]) {
const askPrice = marketPrices[0].a > 0 ? marketPrices[0].a : 0;
const bidPrice = marketPrices[0].b > 0 ? marketPrices[0].b : 0;
if (item.itemLocationHrid === '/item_locations/inventory') {
inventoryNetworthAsk += item.count * askPrice;
inventoryNetworthBid += item.count * bidPrice;
} else {
equipmentNetworthAsk += item.count * askPrice;
equipmentNetworthBid += item.count * bidPrice;
}
}
}
const equipmentValue = equipmentNetworthAsk * 0.5 + equipmentNetworthBid * 0.5;
const inventoryValue = inventoryNetworthAsk * 0.5 + inventoryNetworthBid * 0.5;
let myMarketListings = window.lootDropsTrackerInstance?.myMarketListings;
if (!myMarketListings || myMarketListings.length === 0) {
try {
const cachedData = CharacterDataStorage.get();
if (cachedData) {
myMarketListings = cachedData.myMarketListings ?? [];
if (window.lootDropsTrackerInstance && myMarketListings.length > 0) {
window.lootDropsTrackerInstance.myMarketListings = myMarketListings;
}
}
} catch (e) {
console.error('[KOllection] Error loading market listings from localStorage:', e);
myMarketListings = [];
}
}
let marketListingsNetworthAsk = 0;
let marketListingsNetworthBid = 0;
if (myMarketListings && myMarketListings.length > 0) {
for (const listing of myMarketListings) {
if (!listing.itemHrid) continue;
const quantity = listing.orderQuantity - (listing.filledQuantity ?? 0);
const enhancementLevel = listing.enhancementLevel ?? 0;
const marketPrices = marketData[listing.itemHrid];
if (!marketPrices || !marketPrices[0]) continue;
let askPrice = marketPrices[0].a ?? 0;
let bidPrice = marketPrices[0].b ?? 0;
if (listing.isSell) {
const taxRate = listing.itemHrid === '/items/bag_of_10_cowbells' ? 0.18 : 0.02;
askPrice *= (1 - taxRate);
bidPrice *= (1 - taxRate);
if (enhancementLevel > 1) {
const totalCost = await this.mcs_ko_estimateEnhancementCost(listing.itemHrid, enhancementLevel, marketData);
const cost = totalCost ? Math.round(totalCost) : 0;
marketListingsNetworthAsk += quantity * (cost > 0 ? cost : 0);
marketListingsNetworthBid += quantity * (cost > 0 ? cost : 0);
} else {
marketListingsNetworthAsk += quantity * (askPrice > 0 ? askPrice : 0);
marketListingsNetworthBid += quantity * (bidPrice > 0 ? bidPrice : 0);
}
marketListingsNetworthAsk += listing.unclaimedCoinCount ?? 0;
marketListingsNetworthBid += listing.unclaimedCoinCount ?? 0;
} else {
marketListingsNetworthAsk += quantity * listing.price;
marketListingsNetworthBid += quantity * listing.price;
marketListingsNetworthAsk += (listing.unclaimedItemCount ?? 0) * (askPrice > 0 ? askPrice : 0);
marketListingsNetworthBid += (listing.unclaimedItemCount ?? 0) * (bidPrice > 0 ? bidPrice : 0);
}
}
}
marketValue = marketListingsNetworthAsk * 0.5 + marketListingsNetworthBid * 0.5;
const marketData2 = await this.mcs_ko_fetchMarketData();
if (marketData2 && this.mcs_ko_characterHouseRoomMap) {
for (const key in this.mcs_ko_characterHouseRoomMap) {
const house = this.mcs_ko_characterHouseRoomMap[key];
const houseCost = await this.mcs_ko_getHouseFullBuildPrice(house, marketData2);
housesValue += houseCost;
}
}
const [abilityScore] = await this.mcs_ko_calculateAbilityScoreWithDetails();
abilitiesValue = abilityScore * 1000000;
const total = equipmentValue + inventoryValue + marketValue + housesValue + abilitiesValue;
return {
total,
equipment: equipmentValue,
inventory: inventoryValue,
market: marketValue,
houses: housesValue,
abilities: abilitiesValue
};
} catch (e) {
console.error('[KOllection] Error calculating net worth:', e);
return { total: 0, equipment: 0, inventory: 0, market: 0, houses: 0, abilities: 0 };
}
}
mcs_ko_formatNetWorth(value) {
return mcsFormatCurrency(value, 'cost');
}
mcs_ko_getActionHridFromItemName(itemHrid) {
if (!this.mcs_ko_itemDetailMap || !this.mcs_ko_actionDetailMap) return null;
const itemDetail = this.mcs_ko_itemDetailMap[itemHrid];
if (!itemDetail) return null;
if (itemDetail.actionHrid) return itemDetail.actionHrid;
let itemName = itemDetail.name;
itemName = itemName.replace("Milk", "Cow");
itemName = itemName.replace("Log", "Tree");
itemName = itemName.replace("Cowing", "Milking");
itemName = itemName.replace("Rainbow Cow", "Unicow");
itemName = itemName.replace("Collector's Boots", "Collectors Boots");
itemName = itemName.replace("Knight's Aegis", "Knights Aegis");
for (const action of Object.values(this.mcs_ko_actionDetailMap)) {
if (action.name === itemName) {
return action.hrid;
}
}
return null;
}
mcs_ko_getBaseItemProductionCost(itemHrid, marketData) {
const actionHrid = this.mcs_ko_getActionHridFromItemName(itemHrid);
if (!actionHrid || !this.mcs_ko_actionDetailMap?.[actionHrid]) return -1;
const actionDetail = this.mcs_ko_actionDetailMap[actionHrid];
let totalPrice = 0;
if (actionDetail.inputItems) {
for (const item of actionDetail.inputItems) {
const itemPrice = this.mcs_ko_getItemMarketPrice(item.itemHrid, marketData);
totalPrice += itemPrice * item.count;
}
}
totalPrice *= 0.9;
if (actionDetail.upgradeItemHrid) {
const upgradePrice = this.mcs_ko_getItemMarketPrice(actionDetail.upgradeItemHrid, marketData);
totalPrice += upgradePrice * 1;
}
return totalPrice;
}
mcs_ko_getRealisticBaseItemPrice(itemHrid, marketData) {
const capeItemTokenData = {
'/items/chimerical_quiver': {
tokenCost: 35000,
tokenShopItems: [
{ hrid: '/items/griffin_leather', cost: 600 },
{ hrid: '/items/manticore_sting', cost: 1000 },
{ hrid: '/items/jackalope_antler', cost: 1200 },
{ hrid: '/items/dodocamel_plume', cost: 3000 },
{ hrid: '/items/griffin_talon', cost: 3000 }
]
},
'/items/sinister_cape': {
tokenCost: 27000,
tokenShopItems: [
{ hrid: '/items/acrobats_ribbon', cost: 2000 },
{ hrid: '/items/magicians_cloth', cost: 2000 },
{ hrid: '/items/chaotic_chain', cost: 3000 },
{ hrid: '/items/cursed_ball', cost: 3000 }
]
},
'/items/enchanted_cloak': {
tokenCost: 27000,
tokenShopItems: [
{ hrid: '/items/royal_cloth', cost: 2000 },
{ hrid: '/items/knights_ingot', cost: 2000 },
{ hrid: '/items/bishops_scroll', cost: 2000 },
{ hrid: '/items/regal_jewel', cost: 3000 },
{ hrid: '/items/sundering_jewel', cost: 3000 }
]
}
};
if (capeItemTokenData[itemHrid]) {
const capeData = capeItemTokenData[itemHrid];
let bestValuePerToken = 0;
for (const shopItem of capeData.tokenShopItems) {
const shopItemPrice = this.mcs_ko_getItemMarketPrice(shopItem.hrid, marketData);
if (shopItemPrice > 0) {
const valuePerToken = shopItemPrice / shopItem.cost;
if (valuePerToken > bestValuePerToken) {
bestValuePerToken = valuePerToken;
}
}
}
return bestValuePerToken * capeData.tokenCost;
}
const itemDetail = this.mcs_ko_itemDetailMap[itemHrid];
const productionCost = itemDetail ? this.mcs_ko_getBaseItemProductionCost(itemHrid, marketData) : -1;
const marketPrices = marketData[itemHrid];
if (!marketPrices || !marketPrices[0]) {
return productionCost;
}
const ask = marketPrices[0].a;
const bid = marketPrices[0].b;
let result = 0;
if (ask > 0) {
if (bid > 0) {
if (ask / bid > 1.3) {
result = Math.max(bid, productionCost);
} else {
result = ask;
}
} else {
if (productionCost > 0 && ask / productionCost > 1.3) {
result = productionCost;
} else {
result = Math.max(ask, productionCost);
}
}
} else {
if (bid > 0) {
result = Math.max(bid, productionCost);
} else {
result = productionCost;
}
}
return result;
}
mcs_ko_getItemMarketPrice(itemHrid, marketData) {
const marketPrices = marketData[itemHrid];
if (!marketPrices || !marketPrices[0]) return 0;
let ask = marketPrices[0].a;
let bid = marketPrices[0].b;
if (ask < 0 && bid < 0) return 0;
if (ask > 0 && bid < 0) return ask;
if (bid > 0 && ask < 0) return bid;
return ask;
}
mcs_ko_getCosts(itemHrid, marketData) {
const itemDetail = this.mcs_ko_itemDetailMap[itemHrid];
if (!itemDetail) return { baseCost: 0, minProtectionCost: 0, perActionCost: 0 };
const baseCost = this.mcs_ko_getRealisticBaseItemPrice(itemHrid, marketData);
const protectionHrids = itemDetail.protectionItemHrids ?? [];
const allProtectionHrids = [itemHrid, '/items/mirror_of_protection', ...protectionHrids];
let minProtectionCost = baseCost;
for (let i = 0; i < allProtectionHrids.length; i++) {
const protHrid = allProtectionHrids[i];
const cost = this.mcs_ko_getRealisticBaseItemPrice(protHrid, marketData);
if (i === 0) {
minProtectionCost = cost;
} else {
if (cost > 0 && (minProtectionCost < 0 || cost < minProtectionCost)) {
minProtectionCost = cost;
}
}
}
let perActionCost = 0;
if (itemDetail.enhancementCosts) {
for (const need of itemDetail.enhancementCosts) {
const price = need.itemHrid.startsWith('/items/trainee_')
? 250000
: this.mcs_ko_getItemMarketPrice(need.itemHrid, marketData);
perActionCost += price * need.count;
}
}
return { baseCost, minProtectionCost, perActionCost };
}
mcs_ko_enhancelate(itemHrid, stopAt, protectAt) {
const successRate = [50, 45, 45, 40, 40, 40, 35, 35, 35, 35, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30];
const itemDetail = this.mcs_ko_itemDetailMap[itemHrid];
if (!itemDetail) return { actions: 0, protect_count: 0 };
const itemLevel = itemDetail.itemLevel || 1;
const enhancingLevel = 125;
const laboratoryLevel = 6;
const enhancerBonus = 5.42;
const teaEnhancing = false;
const teaSuperEnhancing = false;
const teaUltraEnhancing = true;
const teaBlessed = true;
const effectiveLevel = enhancingLevel + (teaEnhancing ? 3 : 0) + (teaSuperEnhancing ? 6 : 0) + (teaUltraEnhancing ? 8 : 0);
let totalBonus;
if (effectiveLevel >= itemLevel) {
totalBonus = 1 + (0.05 * (effectiveLevel + laboratoryLevel - itemLevel) + enhancerBonus) / 100;
} else {
totalBonus = 1 - 0.5 * (1 - effectiveLevel / itemLevel) + (0.05 * laboratoryLevel + enhancerBonus) / 100;
}
const km = this._koMatrix;
let markov = km.zeros(20, 20);
for (let i = 0; i < stopAt; i++) {
const successChance = (successRate[i] / 100.0) * totalBonus;
const destination = i >= protectAt ? i - 1 : 0;
if (teaBlessed) {
markov.set(i, i + 2, successChance * 0.01);
markov.set(i, i + 1, successChance * 0.99);
markov.set(i, destination, 1 - successChance);
} else {
markov.set(i, i + 1, successChance);
markov.set(i, destination, 1.0 - successChance);
}
}
markov.set(stopAt, stopAt, 1.0);
const Q = km.subset(markov, 0, stopAt, 0, stopAt);
const M = km.inv(km.subtract(km.identity(stopAt), Q));
const attempts = km.rowSum(M, 0);
const protectValues = km.rowValues(M, 0, protectAt, stopAt);
const protects = protectValues.map((a, i) => a * markov.get(i + protectAt, i + protectAt - 1)).reduce((a, b) => a + b, 0);
return { actions: attempts, protect_count: protects };
}
async mcs_ko_findBestEnhanceStrat(itemHrid, stopAt, marketData) {
const costs = this.mcs_ko_getCosts(itemHrid, marketData);
const allResults = [];
for (let protectAt = 2; protectAt <= stopAt; protectAt++) {
const simResult = this.mcs_ko_enhancelate(itemHrid, stopAt, protectAt);
const totalCost = costs.baseCost + costs.minProtectionCost * simResult.protect_count + costs.perActionCost * simResult.actions;
allResults.push({
protect_at: protectAt,
protect_count: simResult.protect_count,
simResult: simResult,
costs: costs,
totalCost: totalCost
});
}
let best = null;
for (const r of allResults) {
if (best === null || r.totalCost < best.totalCost) {
best = r;
}
}
return best;
}
async mcs_ko_estimateEnhancementCost(itemHrid, targetLevel, marketData) {
if (!this.mcs_ko_itemDetailMap) {
const marketPrices = marketData[itemHrid];
if (marketPrices && marketPrices[0]) {
const basePrice = this.mcs_ko_getWeightedPrice(marketPrices);
return basePrice * targetLevel * 2;
}
return 0;
}
const best = await this.mcs_ko_findBestEnhanceStrat(itemHrid, targetLevel, marketData);
return best ? best.totalCost : 0;
}
mcs_ko_getPlayerName() {
try {
return this.mcs_ko_character?.name || 'Unknown';
} catch (e) {
console.error('[KOllection] Error getting player name:', e);
return 'Unknown';
}
}
mcs_ko_loadFromLocalStorage() {
try {
const cachedData = CharacterDataStorage.get();
if (cachedData) {
this.mcs_ko_characterItems = cachedData.characterItems;
this.mcs_ko_characterHouseRoomMap = cachedData.characterHouseRoomMap;
this.mcs_ko_combatAbilities = cachedData.combatUnit?.combatAbilities;
this.mcs_ko_character = cachedData.character;
this.mcs_ko_characterSkills = cachedData.characterSkills;
}
this.mcs_ko_levelExperienceTable = InitClientDataCache.getLevelExperienceTable();
this.mcs_ko_houseRoomDetailMap = InitClientDataCache.getHouseRoomDetailMap();
this.mcs_ko_itemDetailMap = InitClientDataCache.getItemDetailMap();
this.mcs_ko_actionDetailMap = InitClientDataCache.getActionDetailMap();
} catch (e) {
console.error('[KOllection] Error loading from localStorage:', e);
}
}
createKOllectionPane() {
if (document.getElementById('kollection-pane')) return;
this.mcs_ko_characterItems = null;
this.mcs_ko_combatAbilities = null;
this.mcs_ko_characterHouseRoomMap = null;
this.mcs_ko_levelExperienceTable = null;
this.mcs_ko_houseRoomDetailMap = null;
this.mcs_ko_itemDetailMap = null;
this.mcs_ko_actionDetailMap = null;
this.mcs_ko_character = null;
this.mcs_ko_characterSkills = null;
this.mcs_ko_lastDataHash = null;
this.mcs_ko_cachedScores = null;
this.mcs_ko_loadFromLocalStorage();
const pane = document.createElement('div');
pane.id = 'kollection-pane';
registerPanel('kollection-pane');
pane.className = 'mcs-pane mcs-ko-pane';
const header = document.createElement('div');
header.className = 'mcs-pane-header';
const titleSection = document.createElement('div');
titleSection.className = 'mcs-ko-title-section';
const titleSpan = document.createElement('span');
titleSpan.id = 'kollection-title';
titleSpan.className = 'mcs-pane-title';
titleSpan.textContent = 'KOllection Score: 0.0';
titleSection.appendChild(titleSpan);
const buttonSection = document.createElement('div');
buttonSection.className = 'mcs-button-section';
const minimizeBtn = document.createElement('button');
minimizeBtn.id = 'kollection-minimize-btn';
minimizeBtn.textContent = '−';
minimizeBtn.className = 'mcs-btn';
buttonSection.appendChild(minimizeBtn);
header.appendChild(titleSection);
header.appendChild(buttonSection);
const content = document.createElement('div');
content.id = 'kollection-content';
content.className = 'mcs-ko-content';
const playerNameDiv = document.createElement('div');
playerNameDiv.className = 'mcs-ko-player-name';
playerNameDiv.id = 'kollection-player-name';
playerNameDiv.textContent = 'Loading...';
content.appendChild(playerNameDiv);
const scoreItems = [
{ id: 'house', label: 'Battle House score', detailsId: 'kollection-house-details' },
{ id: 'ability', label: 'Ability score', detailsId: 'kollection-ability-details' },
{ id: 'equipment', label: 'Equipment score', detailsId: 'kollection-equipment-details' }
];
scoreItems.forEach(item => {
const containerDiv = document.createElement('div');
containerDiv.className = 'mcs-ko-score-container';
const scoreDiv = document.createElement('div');
scoreDiv.className = 'mcs-ko-score-row';
const toggleSpan = document.createElement('span');
toggleSpan.id = `kollection-${item.id}-toggle`;
toggleSpan.textContent = '+ ';
toggleSpan.className = 'mcs-ko-toggle-indicator';
const labelValueDiv = document.createElement('div');
labelValueDiv.className = 'mcs-ko-label-value';
const labelSpan = document.createElement('span');
labelSpan.textContent = item.label + ':';
labelSpan.className = 'mcs-ko-score-label';
const valueSpan = document.createElement('span');
valueSpan.id = `kollection-${item.id}-score`;
valueSpan.textContent = '0.0';
valueSpan.className = 'mcs-ko-score-value';
labelValueDiv.appendChild(labelSpan);
labelValueDiv.appendChild(valueSpan);
scoreDiv.appendChild(toggleSpan);
scoreDiv.appendChild(labelValueDiv);
const detailsDiv = document.createElement('div');
detailsDiv.id = item.detailsId;
detailsDiv.className = 'mcs-ko-details';
scoreDiv.onclick = () => {
const isCollapsed = detailsDiv.style.display === 'none';
detailsDiv.style.display = isCollapsed ? 'block' : 'none';
toggleSpan.textContent = isCollapsed ? '↓ ' : '+ ';
};
containerDiv.appendChild(scoreDiv);
containerDiv.appendChild(detailsDiv);
content.appendChild(containerDiv);
});
const totalDiv = document.createElement('div');
totalDiv.className = 'mcs-ko-total-row';
const totalLabel = document.createElement('span');
totalLabel.textContent = 'Character Build Score:';
totalLabel.className = 'mcs-ko-total-label';
const totalValue = document.createElement('span');
totalValue.id = 'kollection-total-score';
totalValue.textContent = '0.0';
totalValue.className = 'mcs-ko-total-value';
totalDiv.appendChild(totalLabel);
totalDiv.appendChild(totalValue);
content.appendChild(totalDiv);
const netWorthContainer = document.createElement('div');
netWorthContainer.className = 'mcs-ko-networth-container';
const netWorthHeader = document.createElement('div');
netWorthHeader.className = 'mcs-ko-networth-header';
const netWorthToggle = document.createElement('span');
netWorthToggle.id = 'kollection-networth-toggle';
netWorthToggle.textContent = '+ Net Worth:';
const netWorthTotal = document.createElement('span');
netWorthTotal.id = 'kollection-networth-total';
netWorthTotal.textContent = '0';
netWorthTotal.className = 'mcs-ko-score-value';
netWorthHeader.appendChild(netWorthToggle);
netWorthHeader.appendChild(netWorthTotal);
const netWorthDetails = document.createElement('div');
netWorthDetails.id = 'kollection-networth-details';
netWorthDetails.className = 'mcs-ko-networth-details';
netWorthDetails.innerHTML = `
<div class="mcs-ko-networth-row"><span>Equipment:</span><span id="kollection-networth-equipment">0</span></div>
<div class="mcs-ko-networth-row"><span>Inventory:</span><span id="kollection-networth-inventory">0</span></div>
<div class="mcs-ko-networth-row"><span>Market Listings:</span><span id="kollection-networth-market">0</span></div>
<div class="mcs-ko-networth-row"><span>Houses:</span><span id="kollection-networth-houses">0</span></div>
<div class="mcs-ko-networth-row"><span>Abilities:</span><span id="kollection-networth-abilities">0</span></div>
`;
netWorthHeader.onclick = () => {
const isCollapsed = netWorthDetails.style.display === 'none';
netWorthDetails.style.display = isCollapsed ? 'block' : 'none';
netWorthToggle.textContent = (isCollapsed ? '- ' : '+ ') + 'Net Worth:';
};
netWorthContainer.appendChild(netWorthHeader);
netWorthContainer.appendChild(netWorthDetails);
content.appendChild(netWorthContainer);
const inspectedPlayersDiv = document.createElement('div');
inspectedPlayersDiv.className = 'mcs-ko-inspected-section';
const inspectedHeader = document.createElement('div');
inspectedHeader.className = 'mcs-ko-inspected-header';
inspectedHeader.id = 'kollection-inspected-header';
inspectedHeader.textContent = '+ Inspected Players (0)';
const inspectedList = document.createElement('div');
inspectedList.id = 'kollection-inspected-list';
inspectedList.className = 'mcs-ko-inspected-list';
inspectedPlayersDiv.appendChild(inspectedHeader);
inspectedPlayersDiv.appendChild(inspectedList);
content.appendChild(inspectedPlayersDiv);
inspectedHeader.onclick = () => {
const isCollapsed = inspectedList.style.display === 'none';
inspectedList.style.display = isCollapsed ? 'block' : 'none';
const count = inspectedList.children.length;
inspectedHeader.textContent = (isCollapsed ? '↓ ' : '+ ') + `Inspected Players (${count})`;
};
pane.appendChild(header);
pane.appendChild(content);
document.body.appendChild(pane);
this.mcs_ko_makeDraggable(pane, header);
const savedMinimized = this.koStorage.get('minimized') === true || this.koStorage.get('minimized') === 'true';
this.mcs_ko_isMinimized = savedMinimized;
if (savedMinimized) {
minimizeBtn.textContent = '+';
content.style.display = 'none';
header.style.borderRadius = '6px';
}
minimizeBtn.onclick = () => {
this.mcs_ko_isMinimized = !this.mcs_ko_isMinimized;
if (this.mcs_ko_isMinimized) {
content.style.display = 'none';
minimizeBtn.textContent = '+';
header.style.borderRadius = '6px';
this.koStorage.set('minimized', true);
} else {
content.style.display = 'flex';
minimizeBtn.textContent = '−';
header.style.borderRadius = '6px 6px 0 0';
this.koStorage.set('minimized', false);
this.constrainPanelToBoundaries('kollection-pane', 'mcs_KO', true);
}
};
setTimeout(() => {
this.mcs_ko_updateScores();
this.mcs_ko_updateInspectedPlayersList();
}, 2000);
VisibilityManager.register('kollection-update', () => {
this.mcs_ko_loadFromLocalStorage();
this.mcs_ko_updateScores();
this.mcs_ko_updateInspectedPlayersList();
}, 30000);
this._koWsListener = this.mcs_ko_handleWebSocketMessage.bind(this);
this._koEquipChangedListener = this.mcs_ko_handleEquipmentChanged.bind(this);
window.addEventListener('EquipSpyWebSocketMessage', this._koWsListener);
window.addEventListener('MCS_EquipmentChanged', this._koEquipChangedListener);
}
async mcs_ko_updateScores() {
try {
const playerName = this.mcs_ko_getPlayerName();
const equippedItems = window.mcs__global_equipment_tracker?.getEquippedItems() ?? [];
const equippedHash = JSON.stringify(equippedItems.map(i => ({
location: i.itemLocationHrid,
item: i.itemHrid,
enhancement: i.enhancementLevel,
count: i.count
})));
const marketListings = window.lootDropsTrackerInstance?.myMarketListings ?? [];
const marketListingsHash = JSON.stringify(marketListings.map(l => ({
id: l.id,
filled: l.filledQuantity,
quantity: l.orderQuantity,
unclaimed: (l.unclaimedCoinCount ?? 0) + (l.unclaimedItemCount ?? 0)
})));
const dataHash = JSON.stringify({
items: this.mcs_ko_characterItems?.length ?? 0,
abilities: this.mcs_ko_combatAbilities?.length ?? 0,
houses: Object.keys(this.mcs_ko_characterHouseRoomMap ?? {}).length,
skills: JSON.stringify(this.mcs_ko_characterSkills),
equipped: equippedHash,
marketListings: marketListingsHash
});
const isDataLoading = !this.mcs_ko_characterItems &&
!this.mcs_ko_combatAbilities &&
!this.mcs_ko_characterHouseRoomMap;
if (!isDataLoading && this.mcs_ko_lastDataHash === dataHash && this.mcs_ko_cachedScores) {
const cached = this.mcs_ko_cachedScores;
const playerNameElem = document.getElementById('kollection-player-name');
const houseScoreElem = document.getElementById('kollection-house-score');
const abilityScoreElem = document.getElementById('kollection-ability-score');
const equipmentScoreElem = document.getElementById('kollection-equipment-score');
const totalScoreElem = document.getElementById('kollection-total-score');
const titleElem = document.getElementById('kollection-title');
if (playerNameElem) playerNameElem.textContent = playerName;
if (houseScoreElem) houseScoreElem.textContent = cached.houseScore.toFixed(1);
if (abilityScoreElem) abilityScoreElem.textContent = cached.abilityScore.toFixed(1);
if (equipmentScoreElem) equipmentScoreElem.textContent = cached.equipmentScore.toFixed(1);
if (totalScoreElem) totalScoreElem.textContent = cached.totalScore.toFixed(1);
if (titleElem) titleElem.textContent = `KOllection Score: ${cached.totalScore.toFixed(1)}`;
this.mcs_ko_updateDetailsSection('kollection-house-details', cached.houseDetails);
this.mcs_ko_updateDetailsSection('kollection-ability-details', cached.abilityDetails);
this.mcs_ko_updateDetailsSection('kollection-equipment-details', cached.equipmentDetails);
if (cached.netWorth) {
this.mcs_ko_updateNetWorthDisplay(cached.netWorth);
}
return;
}
const [houseScore, houseDetails] = await this.mcs_ko_calculateHouseScoreWithDetails();
const [abilityScore, abilityDetails] = await this.mcs_ko_calculateAbilityScoreWithDetails();
const netWorth = await this.mcs_ko_calculateNetWorth();
const equipmentScore = netWorth.equipment / 1000000;
const [, equipmentDetails] = await this.mcs_ko_calculateEquipmentScoreWithDetails();
const totalScore = houseScore + abilityScore + equipmentScore;
this.mcs_ko_lastDataHash = dataHash;
this.mcs_ko_cachedScores = {
houseScore,
abilityScore,
equipmentScore,
totalScore,
houseDetails,
abilityDetails,
equipmentDetails,
netWorth
};
const playerNameElem = document.getElementById('kollection-player-name');
const houseScoreElem = document.getElementById('kollection-house-score');
const abilityScoreElem = document.getElementById('kollection-ability-score');
const equipmentScoreElem = document.getElementById('kollection-equipment-score');
const totalScoreElem = document.getElementById('kollection-total-score');
const titleElem = document.getElementById('kollection-title');
if (playerNameElem) playerNameElem.textContent = playerName;
if (houseScoreElem) houseScoreElem.textContent = houseScore.toFixed(1);
if (abilityScoreElem) abilityScoreElem.textContent = abilityScore.toFixed(1);
if (equipmentScoreElem) equipmentScoreElem.textContent = equipmentScore.toFixed(1);
if (totalScoreElem) totalScoreElem.textContent = totalScore.toFixed(1);
if (titleElem) titleElem.textContent = `KOllection Score: ${totalScore.toFixed(1)}`;
this.mcs_ko_updateDetailsSection('kollection-house-details', houseDetails);
this.mcs_ko_updateDetailsSection('kollection-ability-details', abilityDetails);
this.mcs_ko_updateDetailsSection('kollection-equipment-details', equipmentDetails);
this.mcs_ko_updateNetWorthDisplay(netWorth);
} catch (e) {
console.error('[KOllection] Error updating scores:', e);
}
}
mcs_ko_updateNetWorthDisplay(netWorth) {
const totalElem = document.getElementById('kollection-networth-total');
const equipmentElem = document.getElementById('kollection-networth-equipment');
const inventoryElem = document.getElementById('kollection-networth-inventory');
const marketElem = document.getElementById('kollection-networth-market');
const housesElem = document.getElementById('kollection-networth-houses');
const abilitiesElem = document.getElementById('kollection-networth-abilities');
if (totalElem) totalElem.textContent = this.mcs_ko_formatNetWorth(netWorth.total);
if (equipmentElem) equipmentElem.textContent = this.mcs_ko_formatNetWorth(netWorth.equipment);
if (inventoryElem) inventoryElem.textContent = this.mcs_ko_formatNetWorth(netWorth.inventory);
if (marketElem) marketElem.textContent = this.mcs_ko_formatNetWorth(netWorth.market);
if (housesElem) housesElem.textContent = this.mcs_ko_formatNetWorth(netWorth.houses);
if (abilitiesElem) abilitiesElem.textContent = this.mcs_ko_formatNetWorth(netWorth.abilities);
}
mcs_ko_updateDetailsSection(elementId, details) {
const elem = document.getElementById(elementId);
if (!elem || !details || details.length === 0) return;
elem.innerHTML = details.map(detail => `
<div class="mcs-ko-detail-row">
<span>${detail.name}</span>
<span class="mcs-ko-score-value">${detail.value}</span>
</div>
`).join('');
}
mcs_ko_makeDraggable(pane, header) {
DragHandler.makeDraggable(pane, header, 'mcs_KO');
}
mcs_ko_isProfileInKollection(characterID) {
try {
const stored = localStorage.getItem('mcs__global_KO_inspected_players');
if (!stored) return false;
const inspectedPlayers = JSON.parse(stored);
return !!inspectedPlayers[characterID];
} catch (e) {
console.error('[KOllection] Error checking if profile is in KOllection:', e);
return false;
}
}
async mcs_ko_addProfileToKollection(profileData) {
try {
const playerName = profileData.profile?.sharableCharacter?.name || 'Unknown';
const characterID = profileData.profile?.characterSkills?.[0]?.characterID;
let inspectedPlayers = {};
try {
const stored = localStorage.getItem('mcs__global_KO_inspected_players');
if (stored) {
inspectedPlayers = JSON.parse(stored);
}
} catch (e) {
console.error('[KOllection] Error loading inspected players:', e);
}
const [houseScore, abilityScore, equipmentScore, houseDetails, abilityDetails, equipmentDetails] = await this.mcs_ko_calculateProfileScoresWithDetails(profileData);
const totalScore = houseScore + abilityScore + equipmentScore;
if (!inspectedPlayers[characterID]) {
inspectedPlayers[characterID] = {
name: playerName,
count: Object.keys(inspectedPlayers).length + 1,
firstSeen: Date.now(),
lastSeen: Date.now(),
profileData: profileData
};
} else {
inspectedPlayers[characterID].lastSeen = Date.now();
inspectedPlayers[characterID].profileData = profileData;
}
inspectedPlayers[characterID].houseScore = houseScore;
inspectedPlayers[characterID].abilityScore = abilityScore;
inspectedPlayers[characterID].equipmentScore = equipmentScore;
inspectedPlayers[characterID].totalScore = totalScore;
inspectedPlayers[characterID].equipmentHidden = profileData.profile?.hideWearableItems;
inspectedPlayers[characterID].houseDetails = houseDetails;
inspectedPlayers[characterID].abilityDetails = abilityDetails;
inspectedPlayers[characterID].equipmentDetails = equipmentDetails;
localStorage.setItem('mcs__global_KO_inspected_players', JSON.stringify(inspectedPlayers));
this.mcs_ko_updateInspectedPlayersList();
return true;
} catch (e) {
console.error('[KOllection] Error adding profile to KOllection:', e);
return false;
}
}
mcs_ko_removeProfileFromKollection(characterID) {
try {
const stored = localStorage.getItem('mcs__global_KO_inspected_players');
if (!stored) return false;
const inspectedPlayers = JSON.parse(stored);
delete inspectedPlayers[characterID];
localStorage.setItem('mcs__global_KO_inspected_players', JSON.stringify(inspectedPlayers));
this.mcs_ko_updateInspectedPlayersList();
return true;
} catch (e) {
console.error('[KOllection] Error removing profile from KOllection:', e);
return false;
}
}
async mcs_ko_handleProfileShared(profileData) {
try {
window.mcs_last_profile_shared = profileData;
const playerName = profileData.profile?.sharableCharacter?.name || 'Unknown';
const characterID = profileData.profile?.characterSkills?.[0]?.characterID;
const [houseScore, abilityScore, equipmentScore, houseDetails, abilityDetails, equipmentDetails] = await this.mcs_ko_calculateProfileScoresWithDetails(profileData);
const totalScore = houseScore + abilityScore + equipmentScore;
const isInKollection = this.mcs_ko_isProfileInKollection(characterID);
await this.mcs_ko_showProfileScores(playerName, houseScore, abilityScore, equipmentScore, totalScore, profileData.profile?.hideWearableItems, characterID, isInKollection);
} catch (e) {
console.error('[KOllection] Error handling profile shared:', e);
}
}
async mcs_ko_calculateProfileScores(profileData) {
const [houseScore, abilityScore, equipmentScore] = await this.mcs_ko_calculateProfileScoresWithDetails(profileData);
return [houseScore, abilityScore, equipmentScore];
}
async mcs_ko_calculateProfileScoresWithDetails(profileData) {
const marketData = await this.mcs_ko_fetchMarketData();
if (!marketData) return [0, 0, 0, [], [], []];
const profile = profileData.profile;
const playerName = profile?.sharableCharacter?.name || 'Unknown';
const battleHouses = ["dining_room", "library", "dojo", "gym", "armory", "archery_range", "mystical_study"];
let houseScore = 0;
const houseDetails = [];
if (profile.characterHouseRoomMap) {
for (const key in profile.characterHouseRoomMap) {
const house = profile.characterHouseRoomMap[key];
if (battleHouses.some((battleHouse) => house.houseRoomHrid.includes(battleHouse))) {
const houseCost = await this.mcs_ko_getHouseFullBuildPrice(house, marketData);
const scoreValue = houseCost / 1000000;
houseScore += scoreValue;
const houseDetail = this.mcs_ko_houseRoomDetailMap?.[house.houseRoomHrid];
const houseName = houseDetail ? houseDetail.name : house.houseRoomHrid.replace('/house_rooms/', '');
houseDetails.push({
name: `${houseName} ${house.level}`,
value: scoreValue.toFixed(1)
});
}
}
}
houseDetails.sort((a, b) => parseFloat(b.value) - parseFloat(a.value));
if (profile.hideWearableItems) {
return [houseScore, 0, 0, houseDetails, [], []];
}
let abilityScore = 0;
const abilityDetails = [];
const exp_50_skill = ["poke", "scratch", "smack", "quick_shot", "water_strike", "fireball", "entangle", "minor_heal"];
if (profile.equippedAbilities && profile.equippedAbilities.length > 0) {
const abilityDetailsForLog = [];
for (const ability of profile.equippedAbilities) {
const needExp = this.mcs_ko_levelExperienceTable?.[ability.level] ?? 0;
const expPerBook = exp_50_skill.some(skill => ability.abilityHrid.includes(skill)) ? 50 : 500;
let needBooks = needExp / expPerBook;
needBooks += 1;
const itemHrid = ability.abilityHrid.replace("/abilities/", "/items/");
const marketPrices = marketData[itemHrid];
let cost = 0;
let pricePerBook = 0;
if (marketPrices && marketPrices[0]) {
pricePerBook = this.mcs_ko_getWeightedPrice(marketPrices);
cost = needBooks * pricePerBook;
abilityScore += cost;
}
const abilityName = ability.abilityHrid.replace('/abilities/', '').split('_').map(w => w.charAt(0).toUpperCase() + w.slice(1)).join(' ');
abilityDetails.push({
name: `${abilityName} ${ability.level}`,
value: (cost / 1000000).toFixed(1)
});
abilityDetailsForLog.push({
hrid: ability.abilityHrid,
level: ability.level,
books: needBooks.toFixed(1),
pricePerBook: (pricePerBook / 1000000).toFixed(2),
totalCost: (cost / 1000000).toFixed(2)
});
}
abilityDetails.sort((a, b) => parseFloat(b.value) - parseFloat(a.value));
abilityDetailsForLog.sort((a, b) => parseFloat(b.totalCost) - parseFloat(a.totalCost));
abilityDetailsForLog.forEach(detail => {
const hridShort = detail.hrid.replace('/abilities/', '');
});
abilityScore = abilityScore / 1000000;
} else {
}
let equipmentScore = 0;
let networthAsk = 0;
let networthBid = 0;
const equipmentDetails = [];
if (profile.wearableItemMap) {
for (const key in profile.wearableItemMap) {
const item = profile.wearableItemMap[key];
const enhanceLevel = item.enhancementLevel ?? 0;
const itemHrid = item.itemHrid;
const marketPrices = marketData[itemHrid];
let itemCost = 0;
if (enhanceLevel && enhanceLevel > 1) {
let totalCost = await this.mcs_ko_estimateEnhancementCost(itemHrid, enhanceLevel, marketData);
totalCost = totalCost ? Math.round(totalCost) : 0;
itemCost = totalCost;
networthAsk += item.count * (totalCost > 0 ? totalCost : 0);
networthBid += item.count * (totalCost > 0 ? totalCost : 0);
} else if (marketPrices && marketPrices[0]) {
const askPrice = marketPrices[0].a > 0 ? marketPrices[0].a : 0;
const bidPrice = marketPrices[0].b > 0 ? marketPrices[0].b : 0;
itemCost = askPrice * 0.5 + bidPrice * 0.5;
networthAsk += item.count * askPrice;
networthBid += item.count * bidPrice;
}
const itemDetail = this.mcs_ko_itemDetailMap?.[itemHrid];
let itemName = itemDetail ? itemDetail.name : itemHrid.replace('/items/', '');
let displayName = itemName;
if (displayName.startsWith('Refined ')) {
displayName = displayName.replace('Refined ', '') + ' (R)';
}
if (enhanceLevel && enhanceLevel > 0) {
displayName += ` +${enhanceLevel}`;
}
equipmentDetails.push({
name: displayName,
value: (itemCost / 1000000).toFixed(1)
});
}
equipmentDetails.sort((a, b) => parseFloat(b.value) - parseFloat(a.value));
equipmentScore = (networthAsk * 0.5 + networthBid * 0.5) / 1000000;
}
return [houseScore, abilityScore, equipmentScore, houseDetails, abilityDetails, equipmentDetails];
}
async mcs_ko_getProfilePanel() {
for (let i = 0; i < 20; i++) {
const selectedElement = document.querySelector('div.SharableProfile_overviewTab__W4dCV');
if (selectedElement) {
return selectedElement;
}
await new Promise(resolve => setTimeout(resolve, 100));
}
return null;
}
async mcs_ko_showProfileScores(playerName, houseScore, abilityScore, equipmentScore, totalScore, equipmentHidden, characterID, isInKollection) {
const profilePanel = await this.mcs_ko_getProfilePanel();
if (!profilePanel) {
console.error('[KOllection] Could not find profile panel');
return;
}
const existing = document.getElementById('kollection-side-panel');
if (existing) {
existing.remove();
}
const equipmentHiddenText = equipmentHidden ? " (Equipment hidden)" : "";
const sidePanel = document.createElement('div');
sidePanel.id = 'kollection-side-panel';
sidePanel.className = 'mcs-ko-side-panel';
const scoreSection = document.createElement('div');
scoreSection.id = 'kollection-profile-scores';
scoreSection.className = 'mcs-ko-score-section';
scoreSection.innerHTML = `
<div class="mcs-ko-side-name-row">
<div class="mcs-ko-side-player-name">${playerName}</div>
<span id="kollection-side-panel-close-btn" class="mcs-ko-close-btn" title="Close">×</span>
</div>
<div class="mcs-ko-build-score-toggle" id="kollection-toggle-scores">
+ Build Score: ${totalScore.toFixed(1)}${equipmentHiddenText}
</div>
<div id="kollection-score-details" class="mcs-ko-side-score-details">
<div>House: ${houseScore.toFixed(1)}</div>
<div>Ability: ${abilityScore.toFixed(1)}</div>
<div>Equipment: ${equipmentScore.toFixed(1)}</div>
</div>
`;
sidePanel.appendChild(scoreSection);
const buttonsContainer = document.createElement('div');
buttonsContainer.className = 'mcs-ko-buttons-container';
sidePanel.appendChild(buttonsContainer);
const modal = profilePanel.closest('.Modal_modalContent__Iw0Yv') || profilePanel.closest('[class*="Modal"]') || profilePanel.parentElement;
document.body.appendChild(sidePanel);
const positionPanel = () => {
if (modal) {
const modalRect = modal.getBoundingClientRect();
const panelWidth = 220;
const gap = 8;
if (modalRect.right + gap + panelWidth < window.innerWidth) {
sidePanel.style.left = (modalRect.right + gap) + 'px';
} else {
sidePanel.style.left = Math.max(10, modalRect.left - panelWidth - gap) + 'px';
}
sidePanel.style.top = modalRect.top + 'px';
}
};
positionPanel();
const observer = new MutationObserver(() => {
if (!document.body.contains(modal) || !document.querySelector('.SharableProfile_overviewTab__W4dCV')) {
sidePanel.remove();
observer.disconnect();
}
});
observer.observe(document.body, { childList: true, subtree: true });
const closeBtn = document.getElementById('kollection-side-panel-close-btn');
if (closeBtn) {
closeBtn.addEventListener('click', () => {
sidePanel.remove();
observer.disconnect();
});
}
const toggleScores = document.getElementById('kollection-toggle-scores');
const scoreDetails = document.getElementById('kollection-score-details');
if (toggleScores && scoreDetails) {
toggleScores.addEventListener('click', () => {
const isCollapsed = scoreDetails.style.display === 'none';
scoreDetails.style.display = isCollapsed ? 'block' : 'none';
toggleScores.textContent =
(isCollapsed ? '- ' : '+ ') +
`Build Score: ${totalScore.toFixed(1)}${equipmentHiddenText}`;
});
}
this.mcs_ko_addKollectionButton(buttonsContainer, characterID, isInKollection);
this.mcs_ko_addShykaiExportButton(buttonsContainer);
}
mcs_ko_addKollectionButton(panel, characterID, isInKollection) {
const existingBtn = document.getElementById('kollection-add-remove-btn');
if (existingBtn) {
existingBtn.remove();
}
const button = document.createElement('button');
button.id = 'kollection-add-remove-btn';
button.textContent = isInKollection ? 'Remove from KOllection' : 'Add to KOllection';
button.className = 'mcs-ko-action-btn';
button.style.backgroundColor = isInKollection ? '#ff5555' : '#4CAF50';
button.style.color = isInKollection ? 'white' : 'black';
button.onclick = async () => {
try {
const profileData = window.mcs_last_profile_shared;
if (!profileData) {
console.error('[KOllection] No profile data found');
button.textContent = 'Error: No Profile Data';
setTimeout(() => {
button.textContent = isInKollection ? 'Remove from KOllection' : 'Add to KOllection';
}, 2000);
return;
}
if (isInKollection) {
const success = this.mcs_ko_removeProfileFromKollection(characterID);
if (success) {
button.textContent = 'Removed!';
button.style.backgroundColor = '#4CAF50';
button.style.color = 'black';
isInKollection = false;
setTimeout(() => {
button.textContent = 'Add to KOllection';
}, 1500);
} else {
button.textContent = 'Error!';
setTimeout(() => {
button.textContent = 'Remove from KOllection';
}, 2000);
}
} else {
const success = await this.mcs_ko_addProfileToKollection(profileData);
if (success) {
button.textContent = 'Added!';
button.style.backgroundColor = '#ff5555';
button.style.color = 'white';
isInKollection = true;
setTimeout(() => {
button.textContent = 'Remove from KOllection';
}, 1500);
} else {
button.textContent = 'Error!';
setTimeout(() => {
button.textContent = 'Add to KOllection';
}, 2000);
}
}
} catch (e) {
console.error('[KOllection] Error toggling KOllection:', e);
button.textContent = 'Error!';
setTimeout(() => {
button.textContent = isInKollection ? 'Remove from KOllection' : 'Add to KOllection';
}, 2000);
}
};
panel.appendChild(button);
}
mcs_ko_constructShykaiExport(profileData) {
const playerObj = {};
playerObj.player = {};
const profile = profileData.profile;
playerObj.name = profile.sharableCharacter?.name || 'Unknown';
if (profile.characterSkills) {
for (const skill of profile.characterSkills) {
if (skill.skillHrid.includes('stamina')) {
playerObj.player.staminaLevel = skill.level;
} else if (skill.skillHrid.includes('intelligence')) {
playerObj.player.intelligenceLevel = skill.level;
} else if (skill.skillHrid.includes('attack')) {
playerObj.player.attackLevel = skill.level;
} else if (skill.skillHrid.includes('melee')) {
playerObj.player.meleeLevel = skill.level;
} else if (skill.skillHrid.includes('defense')) {
playerObj.player.defenseLevel = skill.level;
} else if (skill.skillHrid.includes('ranged')) {
playerObj.player.rangedLevel = skill.level;
} else if (skill.skillHrid.includes('magic')) {
playerObj.player.magicLevel = skill.level;
}
}
}
playerObj.player.equipment = [];
if (profile.wearableItemMap) {
for (const key in profile.wearableItemMap) {
const item = profile.wearableItemMap[key];
playerObj.player.equipment.push({
itemLocationHrid: item.itemLocationHrid,
itemHrid: item.itemHrid,
enhancementLevel: item.enhancementLevel ?? 0
});
}
}
playerObj.food = {};
playerObj.food['/action_types/combat'] = [];
playerObj.drinks = {};
playerObj.drinks['/action_types/combat'] = [];
for (let i = 0; i < 3; i++) {
playerObj.food['/action_types/combat'][i] = { itemHrid: "" };
playerObj.drinks['/action_types/combat'][i] = { itemHrid: "" };
}
if (profile.combatConsumables && Array.isArray(profile.combatConsumables)) {
let foodIndex = 0;
let drinkIndex = 0;
const itemDetailMap = this.mcs_ko_itemDetailMap || InitClientDataCache.getItemDetailMap();
profile.combatConsumables.forEach(consumable => {
const itemHrid = consumable.itemHrid;
const isDrink = itemHrid.includes('/drinks/') ||
itemHrid.includes('coffee') ||
itemDetailMap?.[itemHrid]?.type === 'drink';
if (isDrink && drinkIndex < 3) {
playerObj.drinks['/action_types/combat'][drinkIndex++] = { itemHrid: itemHrid };
} else if (!isDrink && foodIndex < 3) {
playerObj.food['/action_types/combat'][foodIndex++] = { itemHrid: itemHrid };
}
});
}
playerObj.abilities = [
{ abilityHrid: '', level: 1 },
{ abilityHrid: '', level: 1 },
{ abilityHrid: '', level: 1 },
{ abilityHrid: '', level: 1 },
{ abilityHrid: '', level: 1 }
];
if (profile.equippedAbilities) {
const abilityDetailMap = InitClientDataCache.getAbilityDetailMap();
let normalAbilityIndex = 1;
for (const ability of profile.equippedAbilities) {
if (ability && abilityDetailMap[ability.abilityHrid]?.isSpecialAbility) {
playerObj.abilities[0] = {
abilityHrid: ability.abilityHrid,
level: ability.level || 1
};
} else if (ability) {
playerObj.abilities[normalAbilityIndex++] = {
abilityHrid: ability.abilityHrid,
level: ability.level || 1
};
}
}
}
playerObj.triggerMap = {
...(profile.abilityCombatTriggersMap ?? {}),
...(profile.consumableCombatTriggersMap ?? {})
};
playerObj.houseRooms = {};
if (profile.characterHouseRoomMap) {
for (const house of Object.values(profile.characterHouseRoomMap)) {
playerObj.houseRooms[house.houseRoomHrid] = house.level;
}
}
playerObj.achievements = {};
if (profile.characterAchievements) {
for (const achievement of profile.characterAchievements) {
if (achievement.achievementHrid && achievement.isCompleted) {
playerObj.achievements[achievement.achievementHrid] = true;
}
}
}
return playerObj;
}
mcs_ko_addShykaiExportButton(panel) {
const existingBtn = document.getElementById('kollection-shykai-export-btn');
if (existingBtn) {
existingBtn.remove();
}
const button = document.createElement('button');
button.id = 'kollection-shykai-export-btn';
button.textContent = 'Export (Shykai)';
button.className = 'mcs-ko-action-btn';
button.style.backgroundColor = '#4CAF50';
button.style.color = 'black';
button.onclick = () => {
try {
const profileData = window.mcs_last_profile_shared;
if (!profileData || !profileData.profile) {
console.error('[KOllection] No profile data found');
button.textContent = 'Error: No Profile Data';
setTimeout(() => {
button.textContent = 'Export (Shykai)';
}, 2000);
return;
}
const exportObj = this.mcs_ko_constructShykaiExport(profileData);
const exportString = JSON.stringify(exportObj);
navigator.clipboard.writeText(exportString);
button.textContent = 'Copied!';
setTimeout(() => {
button.textContent = 'Export (Shykai)';
}, 2000);
} catch (e) {
console.error('[KOllection] Error exporting to clipboard:', e);
button.textContent = 'Error!';
setTimeout(() => {
button.textContent = 'Export (Shykai)';
}, 2000);
}
};
panel.appendChild(button);
}
mcs_ko_updateInspectedPlayersList() {
const listContainer = document.getElementById('kollection-inspected-list');
const headerElement = document.getElementById('kollection-inspected-header');
if (!listContainer || !headerElement) return;
try {
const stored = localStorage.getItem('mcs__global_KO_inspected_players');
if (!stored) {
listContainer.innerHTML = '<div class="mcs-ko-empty-message">No players inspected yet</div>';
headerElement.textContent = '+ Inspected Players (0)';
return;
}
const inspectedPlayers = JSON.parse(stored);
const playerArray = Object.entries(inspectedPlayers).map(([id, data]) => ({
id,
...data
}));
playerArray.sort((a, b) => a.count - b.count);
if (playerArray.length === 0) {
listContainer.innerHTML = '<div class="mcs-ko-empty-message">No players inspected yet</div>';
headerElement.textContent = '+ Inspected Players (0)';
return;
}
listContainer.innerHTML = '';
playerArray.forEach(player => {
const playerContainer = document.createElement('div');
playerContainer.className = 'mcs-ko-player-container';
const playerDiv = document.createElement('div');
playerDiv.className = 'mcs-ko-player-row';
const totalScore = player.totalScore ?? 0;
const equipmentHiddenText = player.equipmentHidden ? ' (Hidden)' : '';
const deleteBtn = document.createElement('button');
deleteBtn.textContent = '×';
deleteBtn.className = 'mcs-ko-delete-btn';
deleteBtn.onclick = (e) => {
e.stopPropagation();
this.mcs_ko_deleteInspectedPlayer(player.id);
};
const toggleBtn = document.createElement('span');
toggleBtn.textContent = '+ ';
toggleBtn.className = 'mcs-ko-toggle-indicator';
const infoDiv = document.createElement('div');
infoDiv.className = 'mcs-ko-player-info';
infoDiv.innerHTML = `
<span class="mcs-ko-score-value">${player.name}</span>
<span class="mcs-ko-total-value">${totalScore.toFixed(1)}${equipmentHiddenText}</span>
`;
const detailsDiv = document.createElement('div');
detailsDiv.className = 'mcs-ko-player-details';
const houseScore = player.houseScore ?? 0;
const abilityScore = player.abilityScore ?? 0;
const equipmentScore = player.equipmentScore ?? 0;
const houseDetails = player.houseDetails ?? [];
const abilityDetails = player.abilityDetails ?? [];
const equipmentDetails = player.equipmentDetails ?? [];
let detailsHTML = '';
detailsHTML += `
<div class="mcs-ko-detail-subsection">
<div class="mcs-ko-detail-sub-header"
onclick="this.nextElementSibling.style.display = this.nextElementSibling.style.display === 'none' ? 'block' : 'none'; this.querySelector('span').textContent = this.nextElementSibling.style.display === 'none' ? '+ ' : '↓ ';">
<span>+ </span><span>House score:</span>
<span class="mcs-ko-score-value">${houseScore.toFixed(1)}</span>
</div>
<div class="mcs-ko-detail-sub-content">
${houseDetails.map(d => `
<div class="mcs-ko-detail-item">
<span>${d.name}</span>
<span class="mcs-ko-detail-item-value">${d.value}</span>
</div>
`).join('')}
${houseDetails.length === 0 ? '<div class="mcs-ko-empty-message">No data</div>' : ''}
</div>
</div>
`;
detailsHTML += `
<div class="mcs-ko-detail-subsection">
<div class="mcs-ko-detail-sub-header"
onclick="this.nextElementSibling.style.display = this.nextElementSibling.style.display === 'none' ? 'block' : 'none'; this.querySelector('span').textContent = this.nextElementSibling.style.display === 'none' ? '+ ' : '↓ ';">
<span>+ </span><span>Ability score:</span>
<span class="mcs-ko-score-value">${abilityScore.toFixed(1)}</span>
</div>
<div class="mcs-ko-detail-sub-content-scroll">
${abilityDetails.map(d => `
<div class="mcs-ko-detail-item">
<span>${d.name}</span>
<span class="mcs-ko-detail-item-value">${d.value}</span>
</div>
`).join('')}
${abilityDetails.length === 0 ? '<div class="mcs-ko-empty-message">No data</div>' : ''}
</div>
</div>
`;
detailsHTML += `
<div class="mcs-ko-detail-subsection">
<div class="mcs-ko-detail-sub-header"
onclick="this.nextElementSibling.style.display = this.nextElementSibling.style.display === 'none' ? 'block' : 'none'; this.querySelector('span').textContent = this.nextElementSibling.style.display === 'none' ? '+ ' : '↓ ';">
<span>+ </span><span>Equipment score:</span>
<span class="mcs-ko-score-value">${equipmentScore.toFixed(1)}</span>
</div>
<div class="mcs-ko-detail-sub-content-scroll">
${equipmentDetails.map(d => `
<div class="mcs-ko-detail-item">
<span>${d.name}</span>
<span class="mcs-ko-detail-item-value">${d.value}</span>
</div>
`).join('')}
${equipmentDetails.length === 0 ? '<div class="mcs-ko-empty-message">No data</div>' : ''}
</div>
</div>
`;
detailsDiv.innerHTML = detailsHTML;
const toggleDetails = () => {
const isCollapsed = detailsDiv.style.display === 'none';
detailsDiv.style.display = isCollapsed ? 'block' : 'none';
toggleBtn.textContent = isCollapsed ? '↓ ' : '+ ';
};
toggleBtn.onclick = toggleDetails;
infoDiv.onclick = toggleDetails;
playerDiv.appendChild(deleteBtn);
playerDiv.appendChild(toggleBtn);
playerDiv.appendChild(infoDiv);
playerContainer.appendChild(playerDiv);
playerContainer.appendChild(detailsDiv);
listContainer.appendChild(playerContainer);
});
const isExpanded = listContainer.style.display !== 'none';
headerElement.textContent = (isExpanded ? '↓ ' : '+ ') + `Inspected Players (${playerArray.length})`;
} catch (e) {
console.error('[KOllection] Error updating inspected players list:', e);
listContainer.innerHTML = '<div class="mcs-ko-error-message">Error loading list</div>';
}
}
mcs_ko_deleteInspectedPlayer(characterID) {
try {
const stored = localStorage.getItem('mcs__global_KO_inspected_players');
if (!stored) return;
const inspectedPlayers = JSON.parse(stored);
delete inspectedPlayers[characterID];
localStorage.setItem('mcs__global_KO_inspected_players', JSON.stringify(inspectedPlayers));
this.mcs_ko_updateInspectedPlayersList();
} catch (e) {
console.error('[KOllection] Error deleting inspected player:', e);
}
}
destroyKOllection() {
VisibilityManager.clear('kollection-update');
if (this._koWsListener) { window.removeEventListener('EquipSpyWebSocketMessage', this._koWsListener); this._koWsListener = null; }
if (this._koEquipChangedListener) { window.removeEventListener('MCS_EquipmentChanged', this._koEquipChangedListener); this._koEquipChangedListener = null; }
const pane = document.getElementById('kollection-pane');
if (pane) {
pane.remove();
}
}
// KOllection end
// NTally start
get ntStorage() {
if (!this._ntStorage) {
this._ntStorage = createModuleStorage('NT');
}
return this._ntStorage;
}
mcs_nt_createPane() {
if (document.getElementById('mcs_nt_pane')) return;
if (!this.mcs_nt_data) this.mcs_nt_data = [];
if (!this.mcs_nt_observer) this.mcs_nt_observer = null;
if (!this.mcs_nt_marketData) this.mcs_nt_marketData = {};
if (!this.mcs_nt_sortBy) this.mcs_nt_sortBy = 'name';
if (!this.mcs_nt_sortDirection) this.mcs_nt_sortDirection = 'asc';
if (this.mcs_nt_marketTallyExpanded === undefined) this.mcs_nt_marketTallyExpanded = false;
if (this.mcs_nt_planetSetsExpanded === undefined) this.mcs_nt_planetSetsExpanded = false;
if (this.mcs_nt_treasureSetsExpanded === undefined) this.mcs_nt_treasureSetsExpanded = false;
this.mcs_nt_planets = [
{ id: 'smelly_planet', actionHrid: '/actions/combat/smelly_planet', name: 'Smelly Planet' },
{ id: 'swamp_planet', actionHrid: '/actions/combat/swamp_planet', name: 'Swamp Planet' },
{ id: 'aqua_planet', actionHrid: '/actions/combat/aqua_planet', name: 'Aqua Planet' },
{ id: 'jungle_planet', actionHrid: '/actions/combat/jungle_planet', name: 'Jungle Planet' },
{ id: 'gobo_planet', actionHrid: '/actions/combat/gobo_planet', name: 'Gobo Planet' },
{ id: 'planet_of_the_eyes', actionHrid: '/actions/combat/planet_of_the_eyes', name: 'Planet of the Eyes' },
{ id: 'sorcerers_tower', actionHrid: '/actions/combat/sorcerers_tower', name: "Sorcerer's Tower" },
{ id: 'bear_with_it', actionHrid: '/actions/combat/bear_with_it', name: 'Bear With It' },
{ id: 'golem_cave', actionHrid: '/actions/combat/golem_cave', name: 'Golem Cave' },
{ id: 'twilight_zone', actionHrid: '/actions/combat/twilight_zone', name: 'Twilight Zone' },
{ id: 'infernal_abyss', actionHrid: '/actions/combat/infernal_abyss', name: 'Infernal Abyss' },
{ id: 'chimerical_den', actionHrid: '/actions/combat/chimerical_den', name: 'Chimerical Den' },
{ id: 'sinister_circus', actionHrid: '/actions/combat/sinister_circus', name: 'Sinister Circus' },
{ id: 'enchanted_fortress', actionHrid: '/actions/combat/enchanted_fortress', name: 'Enchanted Fortress' },
{ id: 'pirate_cove', actionHrid: '/actions/combat/pirate_cove', name: 'Pirate Cove' }
];
if (!this.mcs_nt_planetEnabled) this.mcs_nt_planetEnabled = {};
this.mcs_nt_planets.forEach(planet => {
if (this.mcs_nt_planetEnabled[planet.id] === undefined) {
const enabled = this.ntStorage.get(`planet_${planet.id}`);
this.mcs_nt_planetEnabled[planet.id] = enabled === true || enabled === 'true';
}
});
if (!this.mcs_nt_treasureEnabled) this.mcs_nt_treasureEnabled = {};
const self = this;
window.isItemTrackedByNtally = (itemHrid) => {
if (!self.mcs_nt_data || !Array.isArray(self.mcs_nt_data)) {
return false;
}
return self.mcs_nt_data.some(item => item.hrid === itemHrid);
};
const pane = document.createElement('div');
pane.id = 'mcs_nt_pane';
registerPanel('mcs_nt_pane');
const savedSize = this.ntStorage.get('size');
let width = 420;
let height = 550;
if (savedSize) {
width = savedSize.width || 420;
height = savedSize.height || 550;
}
pane.className = 'mcs-pane mcs-nt-pane';
pane.style.width = width + 'px';
pane.style.height = height + 'px';
pane.dataset.savedHeight = height;
const header = document.createElement('div');
header.className = 'mcs-pane-header';
const titleSection = document.createElement('div');
titleSection.className = 'mcs-nt-title-section';
const titleSpan = document.createElement('span');
titleSpan.id = 'mcs_nt_title';
titleSpan.className = 'mcs-pane-title';
titleSpan.textContent = 'NTally 0 / 0';
titleSection.appendChild(titleSpan);
const buttonSection = document.createElement('div');
buttonSection.className = 'mcs-button-section';
const minimizeBtn = document.createElement('button');
minimizeBtn.id = 'mcs_nt_minimize_btn';
minimizeBtn.textContent = '−';
minimizeBtn.className = 'mcs-btn';
buttonSection.appendChild(minimizeBtn);
header.appendChild(titleSection);
header.appendChild(buttonSection);
const columnHeader = document.createElement('div');
columnHeader.id = 'mcs_nt_column_header';
columnHeader.className = 'mcs-nt-column-header';
columnHeader.innerHTML = `<div class="mcs-nt-col-icon-spacer"></div>` +
`<div id="mcs_nt_sort_name" class="mcs-nt-col-sort-name" title="Click to sort by name">Name ▼</div>` +
`<div id="mcs_nt_sort_value" class="mcs-nt-col-sort-value" title="Click to sort by value">Value</div>` +
`<div class="mcs-nt-col-x-spacer"></div>`;
const content = document.createElement('div');
content.id = 'mcs_nt_content';
content.className = 'mcs-nt-content';
content.innerHTML = '<div class="mcs-nt-empty-state">Click items in your inventory to add them to the tally</div>';
pane.appendChild(header);
pane.appendChild(columnHeader);
pane.appendChild(content);
document.body.appendChild(pane);
content.addEventListener('click', (e) => {
const deleteBtn = e.target.closest('.mcs_nt_delete_btn');
if (deleteBtn) {
const itemName = deleteBtn.getAttribute('data-item');
if (itemName) self.mcs_nt_removeItem(itemName);
return;
}
const itemIcon = e.target.closest('.mcs_nt_item_icon');
if (itemIcon) {
e.stopPropagation();
const itemHrid = itemIcon.getAttribute('data-hrid');
if (itemHrid) self.mcs_nt_openMarketplace(itemHrid);
return;
}
});
let dragStartX, dragStartY, initialLeft, initialTop;
const onDragMove = (e) => {
const dx = e.clientX - dragStartX;
const dy = e.clientY - dragStartY;
let newLeft = initialLeft + dx;
let newTop = initialTop + dy;
const paneRect = pane.getBoundingClientRect();
const headerHeight = header.getBoundingClientRect().height;
const minLeft = 0;
const maxLeft = window.innerWidth - paneRect.width;
const minTop = 0;
const maxTop = window.innerHeight - headerHeight;
newLeft = Math.max(minLeft, Math.min(maxLeft, newLeft));
newTop = Math.max(minTop, Math.min(maxTop, newTop));
pane.style.left = newLeft + 'px';
pane.style.top = newTop + 'px';
};
const onDragUp = () => {
document.removeEventListener('mousemove', onDragMove);
document.removeEventListener('mouseup', onDragUp);
header.style.cursor = 'move';
const rect = pane.getBoundingClientRect();
this.ntStorage.set('position', { top: rect.top, left: rect.left });
};
header.addEventListener('mousedown', (e) => {
if (e.target.closest('button')) return;
dragStartX = e.clientX;
dragStartY = e.clientY;
const rect = pane.getBoundingClientRect();
initialLeft = rect.left;
initialTop = rect.top;
header.style.cursor = 'grabbing';
pane.style.left = initialLeft + 'px';
pane.style.right = 'auto';
this._ntDragMove = onDragMove;
this._ntDragUp = onDragUp;
document.addEventListener('mousemove', onDragMove);
document.addEventListener('mouseup', onDragUp);
});
minimizeBtn.addEventListener('click', () => {
const isMinimized = content.style.display === 'none';
if (isMinimized) {
content.style.display = 'block';
columnHeader.style.display = '';
minimizeBtn.textContent = '−';
header.style.borderRadius = '6px 6px 0 0';
const savedHeight = pane.dataset.savedHeight || height;
pane.style.height = savedHeight + 'px';
pane.style.minHeight = '300px';
pane.style.resize = 'both';
this.ntStorage.set('minimized', false);
this.mcs_nt_renderContent();
} else {
const currentRect = pane.getBoundingClientRect();
pane.dataset.savedHeight = currentRect.height;
content.style.display = 'none';
columnHeader.style.display = 'none';
minimizeBtn.textContent = '+';
header.style.borderRadius = '6px';
const headerHeight = header.getBoundingClientRect().height;
pane.style.height = headerHeight + 'px';
pane.style.minHeight = '0';
pane.style.resize = 'none';
this.ntStorage.set('minimized', true);
}
});
const savedPosition = this.ntStorage.get('position');
if (savedPosition) {
pane.style.top = savedPosition.top + 'px';
if (savedPosition.left !== undefined) {
pane.style.left = savedPosition.left + 'px';
pane.style.right = 'auto';
} else if (savedPosition.right !== undefined) {
pane.style.right = savedPosition.right + 'px';
}
}
const savedMinimized = this.ntStorage.get('minimized');
if (savedMinimized === true || savedMinimized === 'true') {
content.style.display = 'none';
columnHeader.style.display = 'none';
minimizeBtn.textContent = '+';
header.style.borderRadius = '6px';
const headerHeight = header.getBoundingClientRect().height;
pane.style.height = headerHeight + 'px';
pane.style.minHeight = '0';
pane.style.resize = 'none';
}
this._ntResizeObserver = new ResizeObserver(() => {
const rect = pane.getBoundingClientRect();
const isMinimized = content.style.display === 'none';
if (!isMinimized) {
pane.dataset.savedHeight = rect.height;
}
this.ntStorage.set('size', {
width: rect.width,
height: isMinimized ? pane.dataset.savedHeight : rect.height
});
});
this._ntResizeObserver.observe(pane);
const sortNameHeader = document.getElementById('mcs_nt_sort_name');
const sortValueHeader = document.getElementById('mcs_nt_sort_value');
sortNameHeader.addEventListener('click', () => {
if (this.mcs_nt_sortBy === 'name') {
this.mcs_nt_sortDirection = this.mcs_nt_sortDirection === 'asc' ? 'desc' : 'asc';
} else {
this.mcs_nt_sortBy = 'name';
this.mcs_nt_sortDirection = 'asc';
}
this.mcs_nt_updateSortHeaders();
this.mcs_nt_renderContent();
});
sortValueHeader.addEventListener('click', () => {
if (this.mcs_nt_sortBy === 'value') {
this.mcs_nt_sortDirection = this.mcs_nt_sortDirection === 'asc' ? 'desc' : 'asc';
} else {
this.mcs_nt_sortBy = 'value';
this.mcs_nt_sortDirection = 'desc';
}
this.mcs_nt_updateSortHeaders();
this.mcs_nt_renderContent();
});
this.mcs_nt_loadData();
this.mcs_nt_loadMarketData();
this.mcs_nt_startObserver();
this.mcs_nt_startMarketplacePanelMonitor();
this.mcs_nt_setupEventListeners();
this.mcs_nt_waitForMarketData();
}
mcs_nt_waitForMarketData() {
const checkMarketData = () => {
if (window.lootDropsTrackerInstance?.spyMarketData &&
Object.keys(window.lootDropsTrackerInstance.spyMarketData).length > 0) {
this.mcs_nt_calculateChestValues().then(() => {
this.mcs_nt_renderContent();
});
} else {
setTimeout(checkMarketData, 500);
}
};
checkMarketData();
}
async mcs_nt_calculateChestValues() {
if (!this.mcs_nt_chestCache) {
this.mcs_nt_chestCache = {};
}
try {
const initData = InitClientDataCache.get();
if (!initData || !initData.openableLootDropMap) {
console.error('[NTally] No openableLootDropMap in init data');
return;
}
const marketData = window.lootDropsTrackerInstance?.spyMarketData || {};
const itemHridToName = {};
if (initData.itemDetailMap) {
for (const key in initData.itemDetailMap) {
const item = initData.itemDetailMap[key];
if (item && item.name) {
itemHridToName[key] = item.name;
}
}
}
const formatItemName = (hrid) => mcsFormatHrid(hrid);
const useCowbell0 = typeof window.getTreasureUseCowbell0 === 'function' ? window.getTreasureUseCowbell0() : false;
const specialItemPrices = {
'Coin': {
ask: 1,
bid: 1
},
'Cowbell': {
ask: useCowbell0 ? 0 : ((marketData['/items/bag_of_10_cowbells']?.['0']?.a || 360000 * 0.82)) / 10,
bid: useCowbell0 ? 0 : ((marketData['/items/bag_of_10_cowbells']?.['0']?.b || 350000 * 0.82)) / 10
}
};
const formattedChestDropData = {};
for (let iteration = 0; iteration < 4; iteration++) {
for (let [chestHrid, items] of Object.entries(initData.openableLootDropMap)) {
const chestName = itemHridToName[chestHrid] || formatItemName(chestHrid);
if (!formattedChestDropData[chestName]) {
formattedChestDropData[chestName] = { items: {} };
}
let totalAsk = 0;
let totalBid = 0;
items.forEach(item => {
const { itemHrid, dropRate, minCount, maxCount } = item;
if (dropRate < 0.01) return;
const itemName = itemHridToName[itemHrid] || formatItemName(itemHrid);
const expectedYield = ((minCount + maxCount) / 2) * dropRate;
let askPrice = 0;
let bidPrice = 0;
if (specialItemPrices[itemName]) {
askPrice = specialItemPrices[itemName].ask || 0;
bidPrice = specialItemPrices[itemName].bid || 0;
}
else if (marketData[itemHrid] && marketData[itemHrid]['0']) {
askPrice = marketData[itemHrid]['0'].a || 0;
bidPrice = marketData[itemHrid]['0'].b || 0;
}
const taxFactor = (itemName in specialItemPrices) ? 1 : 0.98;
totalAsk += (askPrice * expectedYield) * taxFactor;
totalBid += (bidPrice * expectedYield) * taxFactor;
});
formattedChestDropData[chestName] = {
...formattedChestDropData[chestName],
expectedAsk: totalAsk,
expectedBid: totalBid
};
specialItemPrices[chestName] = {
ask: totalAsk,
bid: totalBid
};
}
}
for (let chestHrid of Object.keys(initData.openableLootDropMap)) {
const chestName = itemHridToName[chestHrid] || formatItemName(chestHrid);
if (formattedChestDropData[chestName]) {
this.mcs_nt_chestCache[chestHrid] = formattedChestDropData[chestName];
}
}
} catch (error) {
console.error('[NTally] Error calculating chest values:', error);
}
}
mcs_nt_loadData() {
try {
const saved = this.ntStorage.get('data');
if (saved) {
this.mcs_nt_data = typeof saved === 'string' ? JSON.parse(saved) : saved;
}
} catch (e) {
console.error('[NTally] Error loading data:', e);
this.mcs_nt_data = [];
}
}
mcs_nt_saveData() {
try {
this.ntStorage.set('data', this.mcs_nt_data);
} catch (e) {
console.error('[NTally] Error saving data:', e);
}
}
mcs_nt_loadMarketData() {
}
mcs_nt_getVendorValue(itemHrid) {
try {
const itemDetailMap = InitClientDataCache.getItemDetailMap();
if (itemDetailMap && itemDetailMap[itemHrid]) {
return itemDetailMap[itemHrid].sellPrice || 0;
}
} catch (e) {
console.error('[NTally] Error getting vendor value:', e);
}
return 0;
}
mcs_nt_getItemPrice(itemHrid) {
if (!itemHrid) {
return { ask: 0, bid: 0 };
}
let askPrice = 0;
let bidPrice = 0;
if (this.mcs_nt_chestCache && this.mcs_nt_chestCache[itemHrid]) {
const chestData = this.mcs_nt_chestCache[itemHrid];
askPrice = chestData.expectedAsk || 0;
bidPrice = chestData.expectedBid || 0;
}
else {
const marketData = window.lootDropsTrackerInstance?.spyMarketData || {};
if (marketData[itemHrid] && marketData[itemHrid]['0']) {
const priceData = marketData[itemHrid]['0'];
const rawAsk = priceData.a || 0;
const rawBid = priceData.b || 0;
if (rawAsk > 0) {
askPrice = rawAsk < 900 ? Math.floor(rawAsk * 0.98) : Math.ceil(rawAsk * 0.98);
}
if (rawBid > 0) {
bidPrice = rawBid < 900 ? Math.floor(rawBid * 0.98) : Math.ceil(rawBid * 0.98);
}
}
}
return {
ask: askPrice,
bid: bidPrice
};
}
mcs_nt_renderMarketTally() {
let marketListings = window.lootDropsTrackerInstance?.myMarketListings;
if (!marketListings || marketListings.length === 0) {
try {
const cachedData = CharacterDataStorage.get();
if (cachedData) {
marketListings = cachedData.myMarketListings || [];
if (window.lootDropsTrackerInstance && marketListings.length > 0) {
window.lootDropsTrackerInstance.myMarketListings = marketListings;
}
}
} catch (e) {
console.error('[NTally] Error loading market listings from localStorage:', e);
marketListings = [];
}
}
if (!marketListings || marketListings.length === 0) {
return '';
}
const itemDetailMap = InitClientDataCache.getItemDetailMap() || {};
const isExpanded = this.mcs_nt_marketTallyExpanded;
let totalSellListingValue = 0;
let totalSellAskValue = 0;
let totalSellBidValue = 0;
let sellOrderCount = 0;
let totalBuyCommitted = 0;
let totalBuyUnclaimedValue = 0;
let buyOrderCount = 0;
const sellOrders = marketListings.filter(listing => {
if (!listing.isSell) return false;
const remaining = listing.orderQuantity - (listing.filledQuantity || 0);
return remaining > 0;
});
const buyOrders = marketListings.filter(listing => {
if (listing.isSell) return false;
const remaining = listing.orderQuantity - (listing.filledQuantity || 0);
const hasUnclaimed = (listing.unclaimedItemCount || 0) > 0;
return remaining > 0 || hasUnclaimed;
});
sellOrders.forEach(listing => {
const remaining = listing.orderQuantity - (listing.filledQuantity || 0);
const prices = this.mcs_nt_getItemPrice(listing.itemHrid);
totalSellListingValue += remaining * listing.price;
totalSellAskValue += remaining * prices.ask;
totalSellBidValue += remaining * prices.bid;
sellOrderCount++;
});
buyOrders.forEach(listing => {
const remaining = listing.orderQuantity - (listing.filledQuantity || 0);
const prices = this.mcs_nt_getItemPrice(listing.itemHrid);
totalBuyCommitted += remaining * listing.price;
totalBuyUnclaimedValue += (listing.unclaimedItemCount || 0) * prices.ask;
buyOrderCount++;
});
const totalOrderCount = sellOrderCount + buyOrderCount;
const useAskPrice = window.getFlootUseAskPrice ? window.getFlootUseAskPrice() : false;
const realSellValue = useAskPrice ? totalSellAskValue : totalSellBidValue;
const realValueColor = useAskPrice ? '#6495ED' : '#4CAF50';
const totalValue = realSellValue + totalBuyCommitted;
let html = `
<div class="mcs-nt-section">
<div class="mcs_nt_market_tally_header mcs-nt-section-header">
<span class="mcs-nt-section-title-gold">
${isExpanded ? '▼' : '▶'} Market Tally (${totalOrderCount})
</span>
<span class="mcs-nt-section-summary">
<span class="mcs-nt-sell-value">Sell: ${this.mcs_nt_formatNumber(totalSellListingValue)}</span>
<span style="color: ${realValueColor};">(${this.mcs_nt_formatNumber(realSellValue)})</span>
<span class="mcs-nt-separator">|</span>
<span class="mcs-nt-buy-value">Buy: ${this.mcs_nt_formatNumber(totalBuyCommitted)}</span>
<span class="mcs-nt-separator">|</span>
<span class="mcs-nt-total-value">Total: ${this.mcs_nt_formatNumber(totalValue)}</span>
</span>
</div>
`;
if (isExpanded) {
html += `<div class="mcs-nt-expanded-content">`;
if (sellOrders.length > 0) {
html += `<div class="mcs-nt-sell-orders-header">SELL ORDERS (${sellOrderCount})</div>`;
const sortedSellOrders = [...sellOrders].sort((a, b) => {
const remainingA = a.orderQuantity - (a.filledQuantity || 0);
const remainingB = b.orderQuantity - (b.filledQuantity || 0);
return (b.price * remainingB) - (a.price * remainingA);
});
sortedSellOrders.forEach(listing => {
const remaining = listing.orderQuantity - (listing.filledQuantity || 0);
const prices = this.mcs_nt_getItemPrice(listing.itemHrid);
const itemDetail = itemDetailMap[listing.itemHrid];
const itemName = itemDetail ? itemDetail.name : listing.itemHrid.replace('/items/', '').replace(/_/g, ' ');
const spriteId = listing.itemHrid.replace('/items/', '');
const listingTotal = remaining * listing.price;
const askTotal = remaining * prices.ask;
const bidTotal = remaining * prices.bid;
let listingColor = '#FFA500';
if (listing.price > prices.ask) {
listingColor = '#ff5555';
} else if (listing.price < prices.bid) {
listingColor = '#4CAF50';
}
html += `
<div class="mcs-nt-order-row">
<div class="mcs-nt-order-icon">
${createItemIconHtml(spriteId, { width: '100%', height: '100%', sprite: 'items_sprite', className: 'Icon_icon__2LtL_', style: 'pointer-events: none', title: itemName })}
</div>
<div class="mcs-nt-order-details">
<div class="mcs-nt-order-name">
${itemName} x${remaining.toLocaleString()}
</div>
<div class="mcs-nt-order-unit-price">
@ ${listing.price.toLocaleString()} each
</div>
</div>
<div class="mcs-nt-order-totals">
<div class="mcs-nt-order-listing-total">
<span style="color: ${listingColor};">List: ${this.mcs_nt_formatNumber(listingTotal)}</span>
</div>
<div class="mcs-nt-order-market-totals">
<span class="mcs-nt-color-green">${this.mcs_nt_formatNumber(askTotal)}</span>
<span class="mcs-nt-color-muted"> / </span>
<span class="mcs-nt-color-blue">${this.mcs_nt_formatNumber(bidTotal)}</span>
</div>
</div>
</div>
`;
});
}
if (buyOrders.length > 0) {
html += `<div class="mcs-nt-buy-orders-header">BUY ORDERS (${buyOrderCount})</div>`;
const sortedBuyOrders = [...buyOrders].sort((a, b) => {
const remainingA = a.orderQuantity - (a.filledQuantity || 0);
const remainingB = b.orderQuantity - (b.filledQuantity || 0);
return (b.price * remainingB) - (a.price * remainingA);
});
sortedBuyOrders.forEach(listing => {
const remaining = listing.orderQuantity - (listing.filledQuantity || 0);
const unclaimedItems = listing.unclaimedItemCount || 0;
const prices = this.mcs_nt_getItemPrice(listing.itemHrid);
const itemDetail = itemDetailMap[listing.itemHrid];
const itemName = itemDetail ? itemDetail.name : listing.itemHrid.replace('/items/', '').replace(/_/g, ' ');
const spriteId = listing.itemHrid.replace('/items/', '');
const committedCoins = remaining * listing.price;
const unclaimedValue = unclaimedItems * prices.ask;
let bidColor = '#FF6B6B';
if (listing.price < prices.bid) {
bidColor = '#4CAF50';
} else if (listing.price > prices.ask) {
bidColor = '#ff5555';
}
html += `
<div class="mcs-nt-order-row">
<div class="mcs-nt-order-icon">
${createItemIconHtml(spriteId, { width: '100%', height: '100%', sprite: 'items_sprite', className: 'Icon_icon__2LtL_', style: 'pointer-events: none', title: itemName })}
</div>
<div class="mcs-nt-order-details">
<div class="mcs-nt-order-name">
${itemName} x${remaining.toLocaleString()}${unclaimedItems > 0 ? ` <span class="mcs-nt-color-gold">(+${unclaimedItems} unclaimed)</span>` : ''}
</div>
<div class="mcs-nt-order-unit-price">
@ ${listing.price.toLocaleString()} each
</div>
</div>
<div class="mcs-nt-order-totals">
<div class="mcs-nt-order-listing-total">
<span style="color: ${bidColor};">Committed: ${this.mcs_nt_formatNumber(committedCoins)}</span>
</div>
${unclaimedItems > 0 ? `
<div class="mcs-nt-order-market-totals">
<span class="mcs-nt-color-gold">Unclaimed: ${this.mcs_nt_formatNumber(unclaimedValue)}</span>
</div>
` : `
<div class="mcs-nt-order-market-totals">
<span class="mcs-nt-color-green">${this.mcs_nt_formatNumber(prices.ask)} ask</span>
<span class="mcs-nt-color-muted"> / </span>
<span class="mcs-nt-color-blue">${this.mcs_nt_formatNumber(prices.bid)} bid</span>
</div>
`}
</div>
</div>
`;
});
}
html += `</div>`;
}
html += `</div>`;
return html;
}
mcs_nt_attachMarketTallyListener(content) {
const header = content.querySelector('.mcs_nt_market_tally_header');
if (header) {
header.addEventListener('click', () => {
this.mcs_nt_marketTallyExpanded = !this.mcs_nt_marketTallyExpanded;
this.mcs_nt_renderContent();
});
}
}
mcs_nt_getPlanetDrops(actionHrid) {
const drops = new Map();
try {
const actionDetailMap = InitClientDataCache.getActionDetailMap();
const combatMonsterDetailMap = InitClientDataCache.getCombatMonsterDetailMap();
const itemDetailMap = InitClientDataCache.getItemDetailMap();
if (!actionDetailMap) {
return [];
}
const action = actionDetailMap[actionHrid];
if (!action || !action.combatZoneInfo) {
return [];
}
const addDrop = (drop) => {
if (drop.itemHrid === '/items/coin') return;
if (drop.itemHrid && !drops.has(drop.itemHrid)) {
const itemDetail = itemDetailMap?.[drop.itemHrid];
const itemName = itemDetail?.name || drop.itemHrid.replace('/items/', '').replace(/_/g, ' ');
drops.set(drop.itemHrid, { name: itemName, hrid: drop.itemHrid });
}
};
if (action.combatZoneInfo.isDungeon && action.combatZoneInfo.dungeonInfo) {
const dungeonInfo = action.combatZoneInfo.dungeonInfo;
if (dungeonInfo.rewardDropTable) {
dungeonInfo.rewardDropTable.forEach(drop => addDrop(drop));
}
return Array.from(drops.values());
}
const fightInfo = action.combatZoneInfo.fightInfo;
if (!fightInfo || !combatMonsterDetailMap) {
return [];
}
const addDropsFromMonster = (monsterHrid) => {
const monster = combatMonsterDetailMap[monsterHrid];
if (!monster) return;
if (monster.dropTable) {
monster.dropTable.forEach(drop => addDrop(drop));
}
if (monster.rareDropTable) {
monster.rareDropTable.forEach(drop => addDrop(drop));
}
};
if (fightInfo.randomSpawnInfo?.spawns) {
fightInfo.randomSpawnInfo.spawns.forEach(spawn => {
addDropsFromMonster(spawn.combatMonsterHrid);
});
}
if (fightInfo.bossSpawns) {
fightInfo.bossSpawns.forEach(bossSpawn => {
addDropsFromMonster(bossSpawn.combatMonsterHrid);
});
}
return Array.from(drops.values());
} catch (e) {
console.error('[NTally] Error getting planet drops:', e);
return [];
}
}
mcs_nt_togglePlanet(planetId, enabled) {
const planet = this.mcs_nt_planets.find(p => p.id === planetId);
if (!planet) return;
this.mcs_nt_planetEnabled[planetId] = enabled;
this.ntStorage.set(`planet_${planetId}`, enabled);
const drops = this.mcs_nt_getPlanetDrops(planet.actionHrid);
if (enabled) {
drops.forEach(drop => {
const existingIndex = this.mcs_nt_data.findIndex(item =>
item.hrid === drop.hrid || item.name === drop.name
);
if (existingIndex < 0) {
this.mcs_nt_data.push({
name: drop.name,
quantity: 0,
hrid: drop.hrid,
source: planetId
});
}
});
} else {
const itemsToKeep = new Set();
this.mcs_nt_planets.forEach(p => {
if (p.id !== planetId && this.mcs_nt_planetEnabled[p.id]) {
const otherDrops = this.mcs_nt_getPlanetDrops(p.actionHrid);
otherDrops.forEach(drop => itemsToKeep.add(drop.hrid));
}
});
const openableItems = this.mcs_nt_getAllOpenableItems();
for (const chestHrid of Object.keys(openableItems)) {
if (this.mcs_nt_treasureEnabled[chestHrid]) {
itemsToKeep.add(chestHrid);
const treasureDrops = this.mcs_nt_getTreasureDrops(chestHrid);
treasureDrops.forEach(drop => itemsToKeep.add(drop.hrid));
}
}
this.mcs_nt_data = this.mcs_nt_data.filter(item => {
if (!item.source) return true;
if (item.source !== planetId) return true;
if (itemsToKeep.has(item.hrid)) {
for (const chestHrid of Object.keys(openableItems)) {
if (this.mcs_nt_treasureEnabled[chestHrid]) {
if (item.hrid === chestHrid) {
item.source = `treasure_${chestHrid}`;
return true;
}
const treasureDrops = this.mcs_nt_getTreasureDrops(chestHrid);
if (treasureDrops.some(d => d.hrid === item.hrid)) {
item.source = `treasure_${chestHrid}`;
return true;
}
}
}
for (const p of this.mcs_nt_planets) {
if (p.id !== planetId && this.mcs_nt_planetEnabled[p.id]) {
const otherDrops = this.mcs_nt_getPlanetDrops(p.actionHrid);
if (otherDrops.some(d => d.hrid === item.hrid)) {
item.source = p.id;
return true;
}
}
}
}
return false;
});
}
this.mcs_nt_saveData();
this.mcs_nt_updateQuantities();
this.mcs_nt_renderContent();
if (window.updateAllInventoryPrices) {
setTimeout(() => window.updateAllInventoryPrices(true), 100);
}
}
mcs_nt_renderPlanetSets() {
const isExpanded = this.mcs_nt_planetSetsExpanded;
const enabledCount = this.mcs_nt_planets.filter(p => this.mcs_nt_planetEnabled[p.id]).length;
let html = `
<div class="mcs-nt-section">
<div class="mcs_nt_planet_sets_header mcs-nt-section-header">
<span class="mcs-nt-section-title-purple">
${isExpanded ? '▼' : '▶'} Planet Sets ${enabledCount > 0 ? `(${enabledCount})` : ''}
</span>
</div>
`;
if (isExpanded) {
html += `<div class="mcs-nt-section-content">`;
html += `<div class="mcs-nt-toggle-grid">`;
this.mcs_nt_planets.forEach(planet => {
const isEnabled = this.mcs_nt_planetEnabled[planet.id];
html += `
<div class="mcs-nt-toggle-item">
<label class="mcs-nt-toggle-switch">
<input type="checkbox" class="mcs-nt-toggle-checkbox" id="mcs_nt_planet_${planet.id}" data-planet-id="${planet.id}"
${isEnabled ? 'checked' : ''}>
<span class="mcs-nt-toggle-slider" style="background-color: ${isEnabled ? '#4CAF50' : '#555'};"></span>
<span class="mcs-nt-toggle-knob" style="left: ${isEnabled ? '16px' : '2px'};"></span>
</label>
<label for="mcs_nt_planet_${planet.id}" class="mcs-nt-toggle-label" style="color: ${isEnabled ? '#4CAF50' : '#e0e0e0'};">
${planet.name}
</label>
</div>
`;
});
html += `</div>`;
html += `</div>`;
}
html += `</div>`;
return html;
}
mcs_nt_attachPlanetSetsListener(content) {
const header = content.querySelector('.mcs_nt_planet_sets_header');
if (header) {
header.addEventListener('click', (e) => {
if (e.target.type === 'checkbox' || e.target.closest('.mcs_nt_toggle_switch')) {
return;
}
this.mcs_nt_planetSetsExpanded = !this.mcs_nt_planetSetsExpanded;
this.mcs_nt_renderContent();
});
}
this.mcs_nt_planets.forEach(planet => {
const toggle = content.querySelector(`#mcs_nt_planet_${planet.id}`);
if (toggle) {
toggle.addEventListener('change', (e) => {
e.stopPropagation();
this.mcs_nt_togglePlanet(planet.id, e.target.checked);
});
}
});
}
mcs_nt_getAllOpenableItems() {
try {
const initData = InitClientDataCache.get();
return initData?.openableLootDropMap || {};
} catch (e) {
console.error('[NTally] Error getting openable items:', e);
return {};
}
}
mcs_nt_formatTreasureName(hrid) {
let name = hrid.replace('/items/', '').replace(/_/g, ' ');
name = name.replace(/\b\w/g, c => c.toUpperCase());
name = name.replace(/\bSmall\b/g, 'Sm.');
name = name.replace(/\bMedium\b/g, 'Md.');
name = name.replace(/\bLarge\b/g, 'Lg.');
name = name.replace(/\bChest\b/g, 'Ch.');
name = name.replace(/\bCache\b/g, 'Ca.');
name = name.replace(/\bCrate\b/g, 'Cr.');
name = name.replace(/\bRefinement\b/g, 'Ref.');
name = name.replace(/\bBag Of 10 Cowbells\b/g, '10 Cowbells');
return name;
}
mcs_nt_getTreasureDrops(chestHrid) {
const drops = [];
try {
const openableItems = this.mcs_nt_getAllOpenableItems();
const lootTable = openableItems[chestHrid];
if (!lootTable) return drops;
const itemDetailMap = InitClientDataCache.getItemDetailMap();
const seen = new Set();
for (const drop of lootTable) {
if (drop.itemHrid === '/items/coin') continue;
if (drop.itemHrid === '/items/cowbell') continue;
if (seen.has(drop.itemHrid)) continue;
seen.add(drop.itemHrid);
const itemDetail = itemDetailMap?.[drop.itemHrid];
const itemName = itemDetail?.name || drop.itemHrid.replace('/items/', '').replace(/_/g, ' ');
drops.push({ name: itemName, hrid: drop.itemHrid });
}
} catch (e) {
console.error('[NTally] Error getting treasure drops:', e);
}
return drops;
}
mcs_nt_toggleTreasure(chestHrid, enabled) {
this.mcs_nt_treasureEnabled[chestHrid] = enabled;
this.ntStorage.set(`treasure_${chestHrid.replace('/items/', '')}`, enabled);
const drops = this.mcs_nt_getTreasureDrops(chestHrid);
const itemDetailMap = InitClientDataCache.getItemDetailMap();
const sourceId = `treasure_${chestHrid}`;
if (enabled) {
const chestDetail = itemDetailMap?.[chestHrid];
const chestName = chestDetail?.name || chestHrid.replace('/items/', '').replace(/_/g, ' ');
const existingChest = this.mcs_nt_data.findIndex(item => item.hrid === chestHrid);
if (existingChest < 0) {
this.mcs_nt_data.push({
name: chestName,
quantity: 0,
hrid: chestHrid,
source: sourceId
});
}
drops.forEach(drop => {
const existingIndex = this.mcs_nt_data.findIndex(item =>
item.hrid === drop.hrid || item.name === drop.name
);
if (existingIndex < 0) {
this.mcs_nt_data.push({
name: drop.name,
quantity: 0,
hrid: drop.hrid,
source: sourceId
});
}
});
} else {
const itemsToKeep = new Set();
const openableItems = this.mcs_nt_getAllOpenableItems();
for (const otherChestHrid of Object.keys(openableItems)) {
if (otherChestHrid !== chestHrid && this.mcs_nt_treasureEnabled[otherChestHrid]) {
const otherDrops = this.mcs_nt_getTreasureDrops(otherChestHrid);
otherDrops.forEach(drop => itemsToKeep.add(drop.hrid));
itemsToKeep.add(otherChestHrid);
}
}
this.mcs_nt_planets.forEach(p => {
if (this.mcs_nt_planetEnabled[p.id]) {
const planetDrops = this.mcs_nt_getPlanetDrops(p.actionHrid);
planetDrops.forEach(drop => itemsToKeep.add(drop.hrid));
}
});
this.mcs_nt_data = this.mcs_nt_data.filter(item => {
if (!item.source) return true;
if (item.source !== sourceId) return true;
if (itemsToKeep.has(item.hrid)) {
for (const p of this.mcs_nt_planets) {
if (this.mcs_nt_planetEnabled[p.id]) {
const planetDrops = this.mcs_nt_getPlanetDrops(p.actionHrid);
if (planetDrops.some(d => d.hrid === item.hrid)) {
item.source = p.id;
return true;
}
}
}
for (const otherChestHrid of Object.keys(openableItems)) {
if (otherChestHrid !== chestHrid && this.mcs_nt_treasureEnabled[otherChestHrid]) {
if (item.hrid === otherChestHrid) {
item.source = `treasure_${otherChestHrid}`;
return true;
}
const otherDrops = this.mcs_nt_getTreasureDrops(otherChestHrid);
if (otherDrops.some(d => d.hrid === item.hrid)) {
item.source = `treasure_${otherChestHrid}`;
return true;
}
}
}
}
return false;
});
}
this.mcs_nt_saveData();
this.mcs_nt_updateQuantities();
this.mcs_nt_renderContent();
if (window.updateAllInventoryPrices) {
setTimeout(() => window.updateAllInventoryPrices(true), 100);
}
}
mcs_nt_renderTreasureSets() {
const isExpanded = this.mcs_nt_treasureSetsExpanded;
const openableItems = this.mcs_nt_getAllOpenableItems();
const chestHrids = Object.keys(openableItems).sort((a, b) => {
const nameA = this.mcs_nt_formatTreasureName(a);
const nameB = this.mcs_nt_formatTreasureName(b);
return nameA.localeCompare(nameB);
});
chestHrids.forEach(chestHrid => {
if (this.mcs_nt_treasureEnabled[chestHrid] === undefined) {
const enabled = this.ntStorage.get(`treasure_${chestHrid.replace('/items/', '')}`);
this.mcs_nt_treasureEnabled[chestHrid] = enabled === true || enabled === 'true';
}
});
const enabledCount = chestHrids.filter(hrid => this.mcs_nt_treasureEnabled[hrid]).length;
let html = `
<div class="mcs-nt-section">
<div class="mcs_nt_treasure_sets_header mcs-nt-section-header">
<span class="mcs-nt-section-title-gold">
${isExpanded ? '▼' : '▶'} Treasure Sets ${enabledCount > 0 ? `(${enabledCount})` : ''}
</span>
</div>
`;
if (isExpanded) {
html += `<div class="mcs-nt-section-content">`;
html += `<div class="mcs-nt-toggle-grid">`;
chestHrids.forEach(chestHrid => {
const isEnabled = this.mcs_nt_treasureEnabled[chestHrid];
const displayName = this.mcs_nt_formatTreasureName(chestHrid);
const safeId = chestHrid.replace('/items/', '').replace(/[^a-z0-9]/gi, '_');
html += `
<div class="mcs-nt-toggle-item">
<label class="mcs-nt-toggle-switch">
<input type="checkbox" class="mcs-nt-toggle-checkbox" id="mcs_nt_treasure_${safeId}" data-treasure-hrid="${chestHrid}"
${isEnabled ? 'checked' : ''}>
<span class="mcs-nt-toggle-slider" style="background-color: ${isEnabled ? '#FFD700' : '#555'};"></span>
<span class="mcs-nt-toggle-knob" style="left: ${isEnabled ? '16px' : '2px'};"></span>
</label>
<label for="mcs_nt_treasure_${safeId}" class="mcs-nt-toggle-label" style="color: ${isEnabled ? '#FFD700' : '#e0e0e0'};" title="${chestHrid.replace('/items/', '').replace(/_/g, ' ')}">
${displayName}
</label>
</div>
`;
});
html += `</div>`;
html += `</div>`;
}
html += `</div>`;
return html;
}
mcs_nt_attachTreasureSetsListener(content) {
const header = content.querySelector('.mcs_nt_treasure_sets_header');
if (header) {
header.addEventListener('click', (e) => {
if (e.target.type === 'checkbox' || e.target.closest('.mcs_nt_toggle_switch')) {
return;
}
this.mcs_nt_treasureSetsExpanded = !this.mcs_nt_treasureSetsExpanded;
this.mcs_nt_renderContent();
});
}
const openableItems = this.mcs_nt_getAllOpenableItems();
for (const chestHrid of Object.keys(openableItems)) {
const safeId = chestHrid.replace('/items/', '').replace(/[^a-z0-9]/gi, '_');
const toggle = content.querySelector(`#mcs_nt_treasure_${safeId}`);
if (toggle) {
toggle.addEventListener('change', (e) => {
e.stopPropagation();
this.mcs_nt_toggleTreasure(chestHrid, e.target.checked);
});
}
}
}
mcs_nt_addItem(itemName, quantity, itemHrid) {
const existingIndex = this.mcs_nt_data.findIndex(item =>
itemHrid ? item.hrid === itemHrid : item.name === itemName
);
if (existingIndex >= 0) {
this.mcs_nt_data[existingIndex].quantity = quantity;
if (itemHrid && !this.mcs_nt_data[existingIndex].hrid) {
this.mcs_nt_data[existingIndex].hrid = itemHrid;
}
} else {
this.mcs_nt_data.push({
name: itemName,
quantity: quantity,
hrid: itemHrid || null
});
}
this.mcs_nt_saveData();
this.mcs_nt_renderContent();
if (window.updateAllInventoryPrices) {
setTimeout(() => window.updateAllInventoryPrices(true), 100);
}
}
mcs_nt_removeItem(itemName) {
this.mcs_nt_data = this.mcs_nt_data.filter(item => item.name !== itemName);
this.mcs_nt_saveData();
this.mcs_nt_renderContent();
if (window.updateAllInventoryPrices) {
setTimeout(() => window.updateAllInventoryPrices(true), 100);
}
}
mcs_nt_calculateTotals() {
let totalAsk = 0;
let totalBid = 0;
this.mcs_nt_data.forEach(item => {
const itemHrid = item.hrid || ('/items/' + item.name.toLowerCase().replace(/'/g, '').replace(/ /g, '_'));
const prices = this.mcs_nt_getItemPrice(itemHrid);
const vendorValue = this.mcs_nt_getVendorValue(itemHrid);
const isScamBid = vendorValue > 0 && prices.bid > 0 && prices.bid < vendorValue;
const isEqualToVendor = vendorValue > 0 && prices.bid > 0 && prices.bid === vendorValue;
const displayBid = (isScamBid || isEqualToVendor) ? vendorValue : prices.bid;
totalAsk += prices.ask * item.quantity;
totalBid += displayBid * item.quantity;
});
return { totalAsk, totalBid };
}
mcs_nt_calculateMarketTotal() {
let marketListings = window.lootDropsTrackerInstance?.myMarketListings;
if (!marketListings || marketListings.length === 0) {
this.mcs_nt_lastMarketTotal = 0;
return 0;
}
let totalSellAskValue = 0;
let totalSellBidValue = 0;
let totalBuyCommitted = 0;
marketListings.forEach(listing => {
const remaining = listing.orderQuantity - (listing.filledQuantity || 0);
if (remaining <= 0) return;
if (listing.isSell) {
const prices = this.mcs_nt_getItemPrice(listing.itemHrid);
totalSellAskValue += remaining * prices.ask;
totalSellBidValue += remaining * prices.bid;
} else {
totalBuyCommitted += remaining * listing.price;
}
});
const useAskPrice = window.getFlootUseAskPrice ? window.getFlootUseAskPrice() : false;
const realSellValue = useAskPrice ? totalSellAskValue : totalSellBidValue;
const total = realSellValue + totalBuyCommitted;
this.mcs_nt_lastMarketTotal = total;
return total;
}
mcs_nt_formatNumber(num) {
return mcsFormatCurrency(num, 'ntally');
}
mcs_nt_updateSortHeaders() {
const sortNameHeader = document.getElementById('mcs_nt_sort_name');
const sortValueHeader = document.getElementById('mcs_nt_sort_value');
if (!sortNameHeader || !sortValueHeader) return;
if (this.mcs_nt_sortBy === 'name') {
sortNameHeader.style.color = '#e0e0e0';
sortNameHeader.textContent = `Name ${this.mcs_nt_sortDirection === 'asc' ? '▼' : '▲'}`;
} else {
sortNameHeader.style.color = '#999';
sortNameHeader.textContent = 'Name';
}
if (this.mcs_nt_sortBy === 'value') {
sortValueHeader.style.color = '#e0e0e0';
sortValueHeader.textContent = `Value ${this.mcs_nt_sortDirection === 'asc' ? '▼' : '▲'}`;
} else {
sortValueHeader.style.color = '#999';
sortValueHeader.textContent = 'Value';
}
}
mcs_nt_shouldUpdateDOM() {
const pane = document.getElementById('mcs_nt_pane');
if (!pane || pane.style.display === 'none') return false;
const content = document.getElementById('mcs_nt_content');
if (!content || content.style.display === 'none') return false;
return true;
}
mcs_nt_renderContent() {
const content = document.getElementById('mcs_nt_content');
const titleSpan = document.getElementById('mcs_nt_title');
if (!titleSpan) return;
const { totalAsk, totalBid } = this.mcs_nt_calculateTotals();
this.mcs_nt_lastTotalAsk = totalAsk;
this.mcs_nt_lastTotalBid = totalBid;
titleSpan.textContent = `NTally ${this.mcs_nt_formatNumber(totalAsk)} ask ${this.mcs_nt_formatNumber(totalBid)} bid`;
if (!this.mcs_nt_shouldUpdateDOM()) return;
if (!content) return;
this.mcs_nt_updateSortHeaders();
const marketTallyHtml = this.mcs_nt_renderMarketTally();
const planetSetsHtml = this.mcs_nt_renderPlanetSets();
const treasureSetsHtml = this.mcs_nt_renderTreasureSets();
if (this.mcs_nt_data.length === 0) {
content.innerHTML = marketTallyHtml + planetSetsHtml + treasureSetsHtml + '<div class="mcs-nt-empty-state">Click items in your inventory to add them to the tally</div>';
this.mcs_nt_attachMarketTallyListener(content);
this.mcs_nt_attachPlanetSetsListener(content);
this.mcs_nt_attachTreasureSetsListener(content);
return;
}
const sortedData = [...this.mcs_nt_data].sort((a, b) => {
if (this.mcs_nt_sortBy === 'name') {
const nameA = a.name.toLowerCase();
const nameB = b.name.toLowerCase();
const comparison = nameA.localeCompare(nameB);
return this.mcs_nt_sortDirection === 'asc' ? comparison : -comparison;
} else {
const hridA = a.hrid || ('/items/' + a.name.toLowerCase().replace(/'/g, '').replace(/ /g, '_'));
const hridB = b.hrid || ('/items/' + b.name.toLowerCase().replace(/'/g, '').replace(/ /g, '_'));
const pricesA = this.mcs_nt_getItemPrice(hridA);
const pricesB = this.mcs_nt_getItemPrice(hridB);
const totalA = (pricesA.ask + pricesA.bid) / 2 * a.quantity;
const totalB = (pricesB.ask + pricesB.bid) / 2 * b.quantity;
const comparison = totalA - totalB;
return this.mcs_nt_sortDirection === 'asc' ? comparison : -comparison;
}
});
let html = '';
sortedData.forEach(item => {
const itemHrid = item.hrid || ('/items/' + item.name.toLowerCase().replace(/'/g, '').replace(/ /g, '_'));
const prices = this.mcs_nt_getItemPrice(itemHrid);
const vendorValue = this.mcs_nt_getVendorValue(itemHrid);
const isScamBid = vendorValue > 0 && prices.bid > 0 && prices.bid < vendorValue;
const isEqualToVendor = vendorValue > 0 && prices.bid > 0 && prices.bid === vendorValue;
const displayBid = (isScamBid || isEqualToVendor) ? vendorValue : prices.bid;
const itemTotalAsk = prices.ask * item.quantity;
const itemTotalBid = displayBid * item.quantity;
const spriteId = itemHrid.replace('/items/', '');
const isZeroQty = item.quantity === 0;
const iconCursor = isZeroQty ? 'not-allowed' : 'pointer';
const iconOpacity = isZeroQty ? '0.3' : '1';
const iconFilter = isZeroQty ? 'grayscale(100%)' : 'none';
const iconTitle = isZeroQty ? 'Item not in inventory' : 'Click to open in marketplace';
html += `<div data-item-name="${item.name}" class="mcs-nt-item-row">`;
html += `<div class="mcs_nt_item_icon mcs-nt-item-icon" data-hrid="${itemHrid}" data-quantity="${item.quantity}" style="cursor: ${iconCursor}; opacity: ${iconOpacity}; filter: ${iconFilter};" title="${iconTitle}">`;
html += createItemIconHtml(spriteId, { width: '100%', height: '100%', sprite: 'items_sprite', className: 'Icon_icon__2LtL_', style: 'pointer-events: none', title: item.name });
html += `</div>`;
html += `<div class="mcs-nt-item-info">`;
html += `<div class="mcs-nt-item-name">${item.name}</div>`;
html += `<div class="mcs_nt_qty mcs-nt-item-qty">Qty: ${item.quantity.toLocaleString()}</div>`;
html += `</div>`;
html += `<div class="mcs-nt-item-prices">`;
html += `<div class="mcs_nt_unit_price mcs-nt-item-unit-price">`;
if (isScamBid) {
html += `${prices.ask.toLocaleString()} ask <span class="mcs-nt-scam-bid">${displayBid.toLocaleString()} vendor</span>`;
} else if (isEqualToVendor) {
html += `${prices.ask.toLocaleString()} ask <span class="mcs-nt-equal-vendor">${displayBid.toLocaleString()} vend</span>`;
} else {
html += `${prices.ask.toLocaleString()} ask ${displayBid.toLocaleString()} bid`;
}
html += `</div>`;
html += `<div class="mcs_nt_total_price mcs-nt-item-total-price">`;
if (isScamBid) {
html += `${this.mcs_nt_formatNumber(itemTotalAsk)} ask <span class="mcs-nt-scam-total">⚠️ ${this.mcs_nt_formatNumber(itemTotalBid)} vend</span>`;
} else if (isEqualToVendor) {
html += `${this.mcs_nt_formatNumber(itemTotalAsk)} ask <span class="mcs-nt-equal-vendor-total">${this.mcs_nt_formatNumber(itemTotalBid)} vend</span>`;
} else {
html += `${this.mcs_nt_formatNumber(itemTotalAsk)} ask ${this.mcs_nt_formatNumber(itemTotalBid)} bid`;
}
html += `</div>`;
html += `</div>`;
html += `<button class="mcs_nt_delete_btn mcs-nt-delete-btn" data-item="${item.name}">X</button>`;
html += `</div>`;
});
content.innerHTML = marketTallyHtml + planetSetsHtml + treasureSetsHtml + html;
this.mcs_nt_attachMarketTallyListener(content);
this.mcs_nt_attachPlanetSetsListener(content);
this.mcs_nt_attachTreasureSetsListener(content);
}
mcs_nt_openMarketplace(itemHrid) {
mcsGoToMarketplace(itemHrid);
}
mcs_nt_startObserver() {
if (this.mcs_nt_observer) return;
let tooltipDebounceTimer = null;
let pendingMutations = [];
this.mcs_nt_observer = new MutationObserver((mutations) => {
pendingMutations.push(...mutations);
if (tooltipDebounceTimer) return;
tooltipDebounceTimer = setTimeout(() => {
tooltipDebounceTimer = null;
const batch = pendingMutations;
pendingMutations = [];
for (const mutation of batch) {
for (const node of mutation.addedNodes) {
if (node.nodeType === 1) {
if (node.classList && node.classList.contains('MuiTooltip-tooltip')) {
this.mcs_nt_injectButton(node);
}
const tooltips = node.querySelectorAll && node.querySelectorAll('.MuiTooltip-tooltip');
if (tooltips) {
tooltips.forEach(tooltip => this.mcs_nt_injectButton(tooltip));
}
}
}
}
}, 50);
});
this.mcs_nt_observer.observe(document.body, {
childList: true,
subtree: true
});
const ntTooltipObserver = this.mcs_nt_observer;
document.addEventListener('visibilitychange', () => {
if (document.hidden) {
ntTooltipObserver.disconnect();
} else {
ntTooltipObserver.observe(document.body, { childList: true, subtree: true });
}
});
}
mcs_nt_injectButton(tooltipElement) {
if (tooltipElement.querySelector('.mcs_nt_add_btn') || tooltipElement.querySelector('.mcs_nt_scam_warning')) return;
const actionMenu = tooltipElement.querySelector('[class*="Item_actionMenu"]');
if (!actionMenu) return;
const itemInfo = tooltipElement.querySelector('[class*="Item_itemInfo"]');
if (!itemInfo) return;
const nameElement = itemInfo.querySelector('[class*="Item_name"]');
const countElement = itemInfo.querySelector('[class*="Item_count"]');
if (!nameElement) return;
const itemName = nameElement.textContent.trim();
const itemCount = countElement ? parseInt(countElement.textContent.replace(/[^0-9]/g, '')) || 1 : 1;
const itemHrid = '/items/' + itemName.toLowerCase().replace(/'/g, '').replace(/ /g, '_');
const checkIsInTally = () => {
return this.mcs_nt_data.some(item =>
itemHrid ? item.hrid === itemHrid : item.name === itemName
);
};
let vendorSellPrice = 0;
const sellButton = tooltipElement.querySelector('[class*="Button_sell"]');
if (sellButton) {
const sellText = sellButton.textContent;
const match = sellText.match(/Sell For ([\d,]+) Coins?/i);
if (match) {
vendorSellPrice = parseInt(match[1].replace(/,/g, ''));
}
}
const marketData = window.lootDropsTrackerInstance?.spyMarketData || {};
let bidPrice = 0;
let rawBid = 0;
let rawAsk = 0;
if (marketData[itemHrid] && marketData[itemHrid]['0']) {
rawBid = marketData[itemHrid]['0'].b || 0;
rawAsk = marketData[itemHrid]['0'].a || 0;
if (rawBid > 0) {
bidPrice = rawBid < 900 ? Math.floor(rawBid * 0.98) : Math.ceil(rawBid * 0.98);
}
}
const isScamBid = vendorSellPrice > 0 && bidPrice > 0 && bidPrice < vendorSellPrice;
if (rawBid > 0 || rawAsk > 0) {
const marketInfo = document.createElement('div');
marketInfo.className = 'mcs_nt_market_info mcs-nt-market-info';
marketInfo.innerHTML = `Market: ${rawAsk.toLocaleString()} ask / ${rawBid.toLocaleString()} bid`;
actionMenu.appendChild(marketInfo);
}
if (isScamBid) {
const scamWarning = document.createElement('div');
scamWarning.className = 'mcs_nt_scam_warning mcs-nt-scam-warning';
scamWarning.innerHTML = '⚠️ SCAM BID!';
actionMenu.appendChild(scamWarning);
}
const updateButtonAppearance = (isInTally) => {
if (isInTally) {
addButton.textContent = 'Remove from Tally';
addButton.style.background = '#d32f2f';
} else {
addButton.textContent = 'Add to Tally';
addButton.style.background = '#4CAF50';
}
};
const addButton = document.createElement('button');
const gameButtonClasses = sellButton ? sellButton.className.replace(/Button_sell[^\s]*/g, '').trim() : '';
addButton.className = (gameButtonClasses ? gameButtonClasses + ' ' : '') + 'mcs_nt_add_btn mcs-nt-tally-btn';
updateButtonAppearance(checkIsInTally());
addButton.addEventListener('click', (e) => {
e.stopPropagation();
const isCurrentlyInTally = checkIsInTally();
if (isCurrentlyInTally) {
this.mcs_nt_removeItem(itemName);
addButton.textContent = 'Removed!';
addButton.style.background = '#2196F3';
setTimeout(() => {
updateButtonAppearance(checkIsInTally());
}, 1000);
} else {
this.mcs_nt_addItem(itemName, itemCount, itemHrid);
addButton.textContent = 'Added!';
addButton.style.background = '#2196F3';
setTimeout(() => {
updateButtonAppearance(checkIsInTally());
}, 1000);
}
});
actionMenu.appendChild(addButton);
}
mcs_nt_updateItemRow(itemName) {
if (!this.mcs_nt_shouldUpdateDOM()) return;
const content = document.getElementById('mcs_nt_content');
if (!content) return;
const item = this.mcs_nt_data.find(i => i.name === itemName);
if (!item) return;
const itemHrid = item.hrid || ('/items/' + item.name.toLowerCase().replace(/'/g, '').replace(/ /g, '_'));
const prices = this.mcs_nt_getItemPrice(itemHrid);
const vendorValue = this.mcs_nt_getVendorValue(itemHrid);
const isScamBid = vendorValue > 0 && prices.bid > 0 && prices.bid < vendorValue;
const isEqualToVendor = vendorValue > 0 && prices.bid > 0 && prices.bid === vendorValue;
const displayBid = (isScamBid || isEqualToVendor) ? vendorValue : prices.bid;
const itemTotalAsk = prices.ask * item.quantity;
const itemTotalBid = displayBid * item.quantity;
const rows = content.querySelectorAll('[data-item-name]');
for (const row of rows) {
if (row.getAttribute('data-item-name') === itemName) {
const iconElement = row.querySelector('.mcs_nt_item_icon');
if (iconElement) {
const isZeroQty = item.quantity === 0;
iconElement.style.cursor = isZeroQty ? 'not-allowed' : 'pointer';
iconElement.style.opacity = isZeroQty ? '0.3' : '1';
iconElement.style.filter = isZeroQty ? 'grayscale(100%)' : 'none';
iconElement.title = isZeroQty ? 'Item not in inventory' : 'Click to open in marketplace';
iconElement.setAttribute('data-quantity', item.quantity);
}
const qtyElement = row.querySelector('.mcs_nt_qty');
if (qtyElement) {
qtyElement.textContent = `Qty: ${item.quantity.toLocaleString()}`;
}
const unitPriceElement = row.querySelector('.mcs_nt_unit_price');
if (unitPriceElement) {
if (isScamBid) {
unitPriceElement.innerHTML = `${prices.ask.toLocaleString()} ask <span class="mcs-nt-scam-bid">${displayBid.toLocaleString()} vendor</span>`;
} else if (isEqualToVendor) {
unitPriceElement.innerHTML = `${prices.ask.toLocaleString()} ask <span class="mcs-nt-equal-vendor">${displayBid.toLocaleString()} vend</span>`;
} else {
unitPriceElement.textContent = `${prices.ask.toLocaleString()} ask ${displayBid.toLocaleString()} bid`;
}
}
const totalPriceElement = row.querySelector('.mcs_nt_total_price');
if (totalPriceElement) {
if (isScamBid) {
totalPriceElement.innerHTML = `${this.mcs_nt_formatNumber(itemTotalAsk)} ask <span class="mcs-nt-scam-total">⚠️ ${this.mcs_nt_formatNumber(itemTotalBid)} vend</span>`;
} else if (isEqualToVendor) {
totalPriceElement.innerHTML = `${this.mcs_nt_formatNumber(itemTotalAsk)} ask <span class="mcs-nt-equal-vendor-total">${this.mcs_nt_formatNumber(itemTotalBid)} vend</span>`;
} else {
totalPriceElement.textContent = `${this.mcs_nt_formatNumber(itemTotalAsk)} ask ${this.mcs_nt_formatNumber(itemTotalBid)} bid`;
}
}
break;
}
}
}
mcs_nt_updateTotals() {
const titleSpan = document.getElementById('mcs_nt_title');
if (!titleSpan) return;
const { totalAsk, totalBid } = this.mcs_nt_calculateTotals();
this.mcs_nt_lastTotalAsk = totalAsk;
this.mcs_nt_lastTotalBid = totalBid;
titleSpan.textContent = `NTally ${this.mcs_nt_formatNumber(totalAsk)} ask ${this.mcs_nt_formatNumber(totalBid)} bid`;
}
mcs_nt_refreshPrices() {
this.mcs_nt_calculateChestValues().then(() => {
this.mcs_nt_data.forEach(item => {
this.mcs_nt_updateItemRow(item.name);
});
this.mcs_nt_updateTotals();
});
}
mcs_nt_updateQuantities() {
const changedItems = [];
const removedItems = [];
const characterItems = window.lootDropsTrackerInstance?.spyCharacterItems || [];
this.mcs_nt_data.forEach(item => {
const itemHrid = item.hrid || ('/items/' + item.name.toLowerCase().replace(/'/g, '').replace(/ /g, '_'));
const inventoryItem = characterItems.find(charItem =>
charItem.itemHrid === itemHrid &&
(charItem.itemLocationHrid === '/item_locations/inventory' || !charItem.itemLocationHrid)
);
const currentQuantity = inventoryItem ? (inventoryItem.count || 0) : 0;
if (currentQuantity > 0) {
if (currentQuantity !== item.quantity) {
item.quantity = currentQuantity;
changedItems.push(item.name);
}
} else {
if (item.quantity !== 0) {
item.quantity = 0;
removedItems.push(item.name);
}
}
});
const allChangedItems = [...changedItems, ...removedItems];
if (allChangedItems.length > 0) {
this.mcs_nt_saveData();
allChangedItems.forEach(itemName => {
this.mcs_nt_updateItemRow(itemName);
});
this.mcs_nt_updateTotals();
}
}
mcs_nt_handleMarketDataUpdated() {
this.mcs_nt_refreshPrices();
}
mcs_nt_handleBattleEnded() {
this.mcs_nt_updateQuantities();
}
mcs_nt_handleFlootPricesUpdated() {
this.mcs_nt_calculateChestValues().then(() => {
this.mcs_nt_renderContent();
});
}
mcs_nt_handleWebSocketMessage(event) {
const data = event.detail;
if (data?.type === 'init_character_data' && data.myMarketListings) {
if (window.lootDropsTrackerInstance) {
window.lootDropsTrackerInstance.myMarketListings = data.myMarketListings;
}
setTimeout(() => this.mcs_nt_renderContent(), 100);
}
if (data?.type === 'market_item_order_updated' || data?.type === 'market_item_orders' || data?.type === 'market_listings_updated') {
if (data.myMarketListings && window.lootDropsTrackerInstance) {
window.lootDropsTrackerInstance.myMarketListings = data.myMarketListings;
}
setTimeout(() => {
this.mcs_nt_updateQuantities();
this.mcs_nt_renderContent();
}, 100);
}
}
mcs_nt_setupEventListeners() {
if (this._ntWsListener) return;
this._ntMarketDataListener = this.mcs_nt_handleMarketDataUpdated.bind(this);
this._ntBattleEndedListener = this.mcs_nt_handleBattleEnded.bind(this);
this._ntFlootPricesListener = this.mcs_nt_handleFlootPricesUpdated.bind(this);
this._ntWsListener = this.mcs_nt_handleWebSocketMessage.bind(this);
window.addEventListener('mcs-market-data-updated', this._ntMarketDataListener);
window.addEventListener('mcs-battle-ended', this._ntBattleEndedListener);
window.addEventListener('FlootPricesUpdated', this._ntFlootPricesListener);
window.addEventListener('EquipSpyWebSocketMessage', this._ntWsListener);
VisibilityManager.register('ntally-quantity-update', () => {
this.mcs_nt_updateQuantities();
}, 2000);
VisibilityManager.register('ntally-price-update', () => {
this.mcs_nt_refreshPrices();
}, 10000);
}
mcs_nt_destroy() {
if (this._ntDragMove) {
document.removeEventListener('mousemove', this._ntDragMove);
document.removeEventListener('mouseup', this._ntDragUp);
this._ntDragMove = null;
this._ntDragUp = null;
}
if (this.mcs_nt_observer) {
this.mcs_nt_observer.disconnect();
this.mcs_nt_observer = null;
}
if (this._ntResizeObserver) {
this._ntResizeObserver.disconnect();
this._ntResizeObserver = null;
}
VisibilityManager.clear('ntally-quantity-update');
VisibilityManager.clear('ntally-price-update');
if (this._ntMarketDataListener) { window.removeEventListener('mcs-market-data-updated', this._ntMarketDataListener); this._ntMarketDataListener = null; }
if (this._ntBattleEndedListener) { window.removeEventListener('mcs-battle-ended', this._ntBattleEndedListener); this._ntBattleEndedListener = null; }
if (this._ntFlootPricesListener) { window.removeEventListener('FlootPricesUpdated', this._ntFlootPricesListener); this._ntFlootPricesListener = null; }
if (this._ntWsListener) { window.removeEventListener('EquipSpyWebSocketMessage', this._ntWsListener); this._ntWsListener = null; }
const pane = document.getElementById('mcs_nt_pane');
if (pane) {
pane.remove();
}
this.mcs_nt_stopMarketplacePanelMonitor();
}
mcs_nt_startMarketplacePanelMonitor() {
this.mcs_nt_stopMarketplacePanelMonitor();
let marketplaceDebounceTimer = null;
this.mcs_nt_marketplaceObserver = new MutationObserver(() => {
if (marketplaceDebounceTimer) return;
marketplaceDebounceTimer = setTimeout(() => {
marketplaceDebounceTimer = null;
this.mcs_nt_checkMarketplacePanel();
}, 200);
});
this.mcs_nt_marketplaceObserver.observe(document.body, {
childList: true,
subtree: true
});
const ntMarketObserver = this.mcs_nt_marketplaceObserver;
const ntSelf = this;
document.addEventListener('visibilitychange', () => {
if (document.hidden) {
ntMarketObserver.disconnect();
} else {
ntMarketObserver.observe(document.body, { childList: true, subtree: true });
ntSelf.mcs_nt_checkMarketplacePanel();
}
});
this.mcs_nt_checkMarketplacePanel();
}
mcs_nt_stopMarketplacePanelMonitor() {
if (this.mcs_nt_marketplaceObserver) {
this.mcs_nt_marketplaceObserver.disconnect();
this.mcs_nt_marketplaceObserver = null;
}
}
mcs_nt_checkMarketplacePanel() {
const savedStates = ToolVisibilityStorage.get();
if (savedStates['floot-market-warnings'] === false) {
const existing = document.querySelector('.mcs-nt-marketplace-scam-warning');
if (existing) existing.remove();
this.mcs_nt_lastMarketBidState = null;
return;
}
let infoContainer = document.querySelector('[class*="MarketplacePanel"][class*="infoContainer"]');
if (!infoContainer) {
infoContainer = document.querySelector('[class*="MarketplacePanel_infoContainer"]');
}
if (!infoContainer) {
this.mcs_nt_lastMarketBidState = null;
return;
}
let itemName = null;
const iconSvg = infoContainer.querySelector('svg[aria-label]');
if (iconSvg) {
itemName = iconSvg.getAttribute('aria-label');
}
if (!itemName) {
const svgIcon = infoContainer.querySelector('svg[role="img"]');
if (svgIcon) {
itemName = svgIcon.getAttribute('aria-label');
}
}
if (!itemName) {
const nameElements = infoContainer.querySelectorAll('div, span, h1, h2, h3, h4, h5, h6');
for (const el of nameElements) {
const text = el.textContent.trim();
if (text.length > 2 && text.length < 50 && !text.match(/^[\d,]+/)) {
if (!text.includes('Bid') && !text.includes('Ask') && !text.includes('Price') && !text.includes('Quantity')) {
itemName = text;
break;
}
}
}
}
if (!itemName) {
return;
}
const itemHrid = '/items/' + itemName.toLowerCase().replace(/'/g, '').replace(/ /g, '_');
const vendorValue = this.mcs_nt_getVendorValue(itemHrid);
if (vendorValue === 0) {
return;
}
const orderBooksContainer = document.querySelector('[class*="orderBooksContainer"]');
if (!orderBooksContainer) {
return;
}
const orderBookContainers = orderBooksContainer.querySelectorAll('[class*="orderBookTableContainer"]');
if (orderBookContainers.length < 2) {
return;
}
const bidTableContainer = orderBookContainers[1];
const bidPriceElement = bidTableContainer.querySelector('[class*="price"] span');
if (!bidPriceElement) return;
const rawBidText = bidPriceElement.textContent.trim();
let rawBid = 0;
const cleanText = rawBidText.replace(/,/g, '').trim().toUpperCase();
if (cleanText.endsWith('M')) {
rawBid = parseFloat(cleanText) * 1000000;
} else if (cleanText.endsWith('K')) {
rawBid = parseFloat(cleanText) * 1000;
} else {
rawBid = parseInt(cleanText);
}
if (isNaN(rawBid) || rawBid === 0) return;
const bidAfterTax = rawBid < 900 ? Math.floor(rawBid * 0.98) : Math.ceil(rawBid * 0.98);
const isScamBid = bidAfterTax < vendorValue;
const isEqualToVendor = bidAfterTax === vendorValue;
const currentState = `${itemName}:${rawBid}:${isScamBid}`;
if (this.mcs_nt_lastMarketBidState === currentState) {
return;
}
this.mcs_nt_lastMarketBidState = currentState;
const bidPriceHeader = bidTableContainer.querySelector('th:nth-child(2)');
if (!bidPriceHeader) return;
const existingWarning = bidPriceHeader.querySelector('.mcs-nt-marketplace-scam-warning');
if (existingWarning) {
existingWarning.remove();
}
if (isScamBid) {
const warningDiv = document.createElement('div');
warningDiv.className = 'mcs-nt-marketplace-scam-warning mcs-nt-marketplace-warning';
warningDiv.style.color = '#ff1744';
warningDiv.innerHTML = '⚠️ SCAM! VENDOR';
bidPriceHeader.appendChild(warningDiv);
} else if (isEqualToVendor) {
const warningDiv = document.createElement('div');
warningDiv.className = 'mcs-nt-marketplace-scam-warning mcs-nt-marketplace-warning';
warningDiv.style.color = '#FFD700';
warningDiv.innerHTML = 'SAME AS VENDOR';
bidPriceHeader.appendChild(warningDiv);
}
}
// NTally end
// PF start
get pfStorage() {
if (!this._pfStorage) {
this._pfStorage = createModuleStorage('PF');
}
return this._pfStorage;
}
createPFormancePane() {
if (document.getElementById('pformance-pane')) return;
const pane = document.createElement('div');
pane.id = 'pformance-pane';
registerPanel('pformance-pane');
this.applyClass(pane, 'mcs-pf-pane');
const savedSize = this.pfStorage.get('size');
let width = 420;
let height = 550;
if (savedSize) {
try {
const size = typeof savedSize === 'string' ? JSON.parse(savedSize) : savedSize;
width = size.width || 420;
height = size.height || 550;
} catch (e) {
console.error('[PFormance] Error restoring size:', e);
}
}
pane.style.width = `${width}px`;
pane.style.height = `${height}px`;
pane.dataset.savedHeight = height;
const header = document.createElement('div');
this.applyClass(header, 'mcs-pf-header');
const titleSection = document.createElement('div');
this.applyClass(titleSection, 'mcs-pf-title-section');
const titleSpan = document.createElement('span');
titleSpan.id = 'pformance-title';
this.applyClass(titleSpan, 'mcs-pf-title');
titleSpan.textContent = 'PFormance: 0 KB';
titleSection.appendChild(titleSpan);
const buttonSection = document.createElement('div');
this.applyClass(buttonSection, 'mcs-pf-button-section');
const refreshBtn = document.createElement('button');
refreshBtn.id = 'pformance-refresh-btn';
refreshBtn.textContent = '↻';
refreshBtn.title = 'Refresh storage data';
this.applyClass(refreshBtn, 'mcs-pf-btn');
refreshBtn.onclick = () => this.pf_updateStorageData();
const minimizeBtn = document.createElement('button');
minimizeBtn.id = 'pformance-minimize-btn';
minimizeBtn.textContent = '−';
this.applyClasses(minimizeBtn, 'mcs-pf-btn', 'mcs-pf-minimize-btn');
buttonSection.appendChild(refreshBtn);
buttonSection.appendChild(minimizeBtn);
header.appendChild(titleSection);
header.appendChild(buttonSection);
const content = document.createElement('div');
content.id = 'pformance-content';
this.applyClass(content, 'mcs-pf-content');
content.innerHTML = '<div class="mcs-pf-loading">Loading storage data...</div>';
pane.appendChild(header);
pane.appendChild(content);
document.body.appendChild(pane);
let dragStartX, dragStartY, initialLeft, initialTop;
const onDragMove = (e) => {
const dx = e.clientX - dragStartX;
const dy = e.clientY - dragStartY;
let newLeft = initialLeft + dx;
let newTop = initialTop + dy;
const paneRect = pane.getBoundingClientRect();
const headerHeight = header.getBoundingClientRect().height;
const minLeft = -paneRect.width + 100;
const maxLeft = window.innerWidth - 100;
const minTop = 0;
const maxTop = window.innerHeight - headerHeight;
newLeft = Math.max(minLeft, Math.min(maxLeft, newLeft));
newTop = Math.max(minTop, Math.min(maxTop, newTop));
pane.style.left = newLeft + 'px';
pane.style.top = newTop + 'px';
};
const onDragUp = () => {
document.removeEventListener('mousemove', onDragMove);
document.removeEventListener('mouseup', onDragUp);
header.style.cursor = 'move';
const rect = pane.getBoundingClientRect();
this.pfStorage.set('position', { top: rect.top, left: rect.left });
this.constrainPanelToBoundaries('pformance-pane', 'mcs_PF', true);
};
header.addEventListener('mousedown', (e) => {
if (e.target.closest('button')) return;
dragStartX = e.clientX;
dragStartY = e.clientY;
const rect = pane.getBoundingClientRect();
initialLeft = rect.left;
initialTop = rect.top;
header.style.cursor = 'grabbing';
pane.style.left = initialLeft + 'px';
pane.style.right = 'auto';
this._pfDragMove = onDragMove;
this._pfDragUp = onDragUp;
document.addEventListener('mousemove', onDragMove);
document.addEventListener('mouseup', onDragUp);
});
minimizeBtn.addEventListener('click', () => {
const isMinimized = content.style.display === 'none';
if (isMinimized) {
content.style.display = 'block';
minimizeBtn.textContent = '−';
header.classList.remove('mcs-pf-header-minimized');
const savedHeight = pane.dataset.savedHeight || height;
pane.style.height = savedHeight + 'px';
pane.style.minHeight = '300px';
pane.style.resize = 'both';
this.pfStorage.set('minimized', false);
} else {
const currentRect = pane.getBoundingClientRect();
pane.dataset.savedHeight = currentRect.height;
content.style.display = 'none';
minimizeBtn.textContent = '+';
header.classList.add('mcs-pf-header-minimized');
const headerHeight = header.getBoundingClientRect().height;
pane.style.height = headerHeight + 'px';
pane.style.minHeight = '0';
pane.style.resize = 'none';
this.pfStorage.set('minimized', true);
}
});
const savedPosition = this.pfStorage.get('position');
if (savedPosition) {
try {
const position = typeof savedPosition === 'string' ? JSON.parse(savedPosition) : savedPosition;
pane.style.top = position.top + 'px';
if (position.left !== undefined) {
pane.style.left = position.left + 'px';
pane.style.right = 'auto';
} else if (position.right !== undefined) {
pane.style.right = position.right + 'px';
}
this.constrainPanelToBoundaries('pformance-pane', 'mcs_PF', true);
} catch (e) {
console.error('[PFormance] Error restoring position:', e);
}
}
const savedMinimized = this.pfStorage.get('minimized') === true || this.pfStorage.get('minimized') === 'true';
if (savedMinimized) {
content.style.display = 'none';
minimizeBtn.textContent = '+';
header.classList.add('mcs-pf-header-minimized');
const headerHeight = header.getBoundingClientRect().height;
pane.style.height = headerHeight + 'px';
pane.style.minHeight = '0';
pane.style.resize = 'none';
}
let pfResizeTimer = null;
this._pfResizeObserver = new ResizeObserver(() => {
const rect = pane.getBoundingClientRect();
const isMinimized = content.style.display === 'none';
if (!isMinimized) {
pane.dataset.savedHeight = rect.height;
}
clearTimeout(pfResizeTimer);
pfResizeTimer = setTimeout(() => {
this.pfStorage.set('size', {
width: rect.width,
height: isMinimized ? pane.dataset.savedHeight : rect.height
});
}, 300);
});
this._pfResizeObserver.observe(pane);
this.pf_updateStorageData();
const savedStates = ToolVisibilityStorage.get();
const shouldRun = savedStates['pformance'] !== false;
if (shouldRun) {
PerformanceMonitor.enabled = true;
StorageMonitor.enabled = true;
VisibilityManager.register('pformance-cpu-update', () => {
this.pf_updateCpuDisplay();
this.pf_updateStorageIODisplay();
}, 1000);
this.pformanceRunning = true;
} else {
PerformanceMonitor.enabled = false;
StorageMonitor.enabled = false;
this.pformanceRunning = false;
}
}
pf_getStorageSize(key) {
try {
const value = localStorage.getItem(key);
if (!value) return 0;
return new Blob([value]).size;
} catch (e) {
return 0;
}
}
pf_formatBytes(bytes) {
if (bytes === 0) return '0 B';
if (bytes < 1024) return bytes.toFixed(0) + ' B';
if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + ' KB';
return (bytes / (1024 * 1024)).toFixed(2) + ' MB';
}
pf_isKeyActive(key) {
const activeKeyPatterns = [
'mcs__global_marketAPI_json',
'initClientData', 'init_character_data', 'mcs__global_init_character_data',
'mcs__global_FC_enabled', 'mcs__global_FC_settings_map',
'mcs__global_KO_inspected_players',
'mcs__global_market_timestamp',
'mwi-suite-tool-visibility', 'mwi-combat-suite-position',
'mcs_AM_', 'mcs_BR_', 'mcs_CR_', 'mcs_DP_', 'mcs_EW_', 'mcs_FL_', 'mcs_GW_',
'mcs_HW_', 'mcs_IH_', 'mcs_JH_', 'mcs_KO_', 'mcs_LY_', 'mcs_MA_', 'mcs_NT_',
'mcs_OP_', 'mcs_PF_', 'mcs_QC_', 'mcs_TR_',
'amazingMinimized', 'amazingPanePosition', 'meatersMinimized',
'breadMinimized', 'breadPanePosition',
'crack_consumable_inventory',
'dpsMinimized', 'dpsFilterAbilities',
'equipSpyMinimized', 'equipSpy_position', 'equipSpy_locked', 'equipSpy_simpleMode',
'equipSpy_noSellMode', 'equipSpy_comparisonOrder', 'equipSpy_selectedHeaderSlot',
'fcb_enabled', 'fcb_settingsMap',
'floot_contentMinimized',
'lootDropsPosition', 'lootDropsHidden', 'lootDropsSortPref', 'lootDropsStartHidden', 'lootDropsSessionHistory',
'gwhizMinimized', 'gwhizBulwarkEnabled', 'gwhizTTLCollapsed', 'gwhizCharmsCollapsed', 'gwhizPanePosition',
'hwhatPanePosition', 'hwhatIsMinimized', 'hwhatMode', 'hwhatCostsEnabled', 'hwhatCowbellTaxEnabled',
'ihurtMinimized', 'ihurtPanePosition',
'jhouseMinimized', 'jhousePanePosition', 'jhouseFilters',
'kollection_market_timestamp', 'kollection_market_data', 'kollection_inspected_players', 'mcs_ko_minimized',
'lucky_settings', 'lucky_position', 'lucky_panel_state', 'lucky_panel_settings', 'lucky_panel_',
'manaPaneSize', 'manaPanePosition', 'manaMinimized',
'mcs_nt_data', 'mcs_nt_panePosition', 'mcs_nt_paneSize', 'mcs_nt_minimized', 'mcs_nt_planet_', 'mcs_nt_treasure_',
'oPanelPosition', 'oPanelSize', 'oPanelIsLocked', 'oPanelZoomLevels', 'oPanelConfig',
'pformancePanePosition', 'pformancePaneSize', 'pformanceMinimized',
'mcs_qc_panePosition', 'mcs_qc_paneSize', 'mcs_qc_minimized', 'mcs_qc_guide_collapsed', 'mcs_qc_sections_collapsed', 'mcs_qc_charms_',
'treasurePaneSize', 'treasureMinimized', 'treasurePanePosition', 'mcs_TR_'
];
const lowerKey = key.toLowerCase();
for (const pattern of activeKeyPatterns) {
if (lowerKey === pattern.toLowerCase() || lowerKey.includes(pattern.toLowerCase())) {
return true;
}
}
if (lowerKey.match(/^character_\d+_/)) return true;
if (lowerKey.match(/^mcs_ko_\d+_/)) return true;
return false;
}
pf_categorizeKeys() {
const categories = {
'Game Data': [],
'MCS Global': [],
'MCS Modules': [],
'Other': []
};
const gameDataKeys = new Set([
'initClientData',
'i18nextLng',
'lastExternalReferrer',
'lastExternalReferrerTime',
'topicsLastReferenceTime',
'_gcl_ls',
'localHash'
]);
for (let i = 0; i < localStorage.length; i++) {
const key = localStorage.key(i);
if (!key) continue;
const size = this.pf_getStorageSize(key);
const lowerKey = key.toLowerCase();
const knownModulePrefixes = ['AM', 'BR', 'CR', 'DP', 'EW', 'FC', 'FL', 'GW', 'HW', 'IH', 'JH', 'KO', 'LY', 'MA', 'NT', 'OP', 'PF', 'QC', 'SC', 'TR'];
if (key.startsWith('mcs__global_')) {
categories['MCS Global'].push({ key, size, isActive: true });
}
else if (key.match(/^mcs_[A-Z]{2}_/) && knownModulePrefixes.some(prefix => key.startsWith(`mcs_${prefix}_`))) {
categories['MCS Modules'].push({ key, size, isActive: true });
}
else if (gameDataKeys.has(key)) {
categories['Game Data'].push({ key, size, isActive: true });
}
else if (this.pf_isLegacyMcsKey(lowerKey)) {
categories['Other'].push({ key, size, isActive: true });
}
else {
categories['Other'].push({ key, size, isActive: false });
}
}
return categories;
}
pf_isLegacyMcsKey(lowerKey) {
const legacyPatterns = [
'mwi-suite-tool-visibility', 'mwi-combat-suite-position',
'lootdrops', 'equipspy', 'floot_', 'kollection', 'lucky',
'ntally', 'opanel', 'ewatch', 'jhouse', 'dps-pane', 'bread',
'hwhat', 'gwhiz', 'amazing', 'meaters', 'fcb', 'ihurt',
'crack', 'qcharm', 'treasure', 'mana', 'pformance'
];
return legacyPatterns.some(pattern => lowerKey.includes(pattern));
}
pf_updateStorageData(isAutoRefresh = false) {
const content = document.getElementById('pformance-content');
const titleSpan = document.getElementById('pformance-title');
if (!content || !titleSpan) return;
const categories = this.pf_categorizeKeys();
const categoryTotals = {};
let grandTotal = 0;
for (const [category, items] of Object.entries(categories)) {
const total = items.reduce((sum, item) => sum + item.size, 0);
categoryTotals[category] = total;
grandTotal += total;
}
titleSpan.textContent = `PFormance: ${this.pf_formatBytes(grandTotal)}`;
const collapsedStates = this.pfStorage.get('collapsedSections') ?? {};
if (isAutoRefresh && content.children.length > 0) {
const summaryDiv = content.querySelector('.mcs-pf-summary');
if (summaryDiv) {
const totalDiv = summaryDiv.querySelector('.mcs-pf-total');
const itemsDiv = summaryDiv.querySelector('.mcs-pf-items-count');
if (totalDiv) totalDiv.textContent = `Total Storage: ${this.pf_formatBytes(grandTotal)}`;
if (itemsDiv) itemsDiv.textContent = `Items: ${localStorage.length}`;
}
const categoryOrder = ['Game Data', 'MCS Global', 'MCS Modules', 'Other'];
categoryOrder.forEach(category => {
const containerId = `pf-cat-${category.replace(/\s+/g, '-').toLowerCase()}`;
const detailsId = `${containerId}-details`;
const details = document.getElementById(detailsId);
if (details && details.style.display !== 'none') {
this.pf_updateCategoryContent(containerId, category);
} else if (details) {
const items = categories[category] ?? [];
const total = categoryTotals[category] || 0;
const percentage = grandTotal > 0 ? ((total / grandTotal) * 100).toFixed(1) : '0.0';
const header = details.previousElementSibling;
if (header) {
const totalDiv = header.querySelector('.mcs-pf-category-total');
const percentDiv = header.querySelector('.mcs-pf-category-percent');
if (totalDiv) totalDiv.textContent = this.pf_formatBytes(total);
if (percentDiv) percentDiv.textContent = `${percentage}%`;
const countSpan = header.querySelector('.mcs-pf-item-count');
if (countSpan) {
countSpan.textContent = `(${items.length})`;
}
}
}
});
return;
}
const categoryOrder = ['Game Data', 'MCS Global', 'MCS Modules', 'Other'];
let html = '';
html += '<div class="mcs-pf-summary">';
html += `<div class="mcs-pf-total">Total Storage: ${this.pf_formatBytes(grandTotal)}</div>`;
html += `<div class="mcs-pf-items-count">Items: ${localStorage.length}</div>`;
html += '</div>';
const cpuCollapsed = collapsedStates['pf-cpu'] === true;
html += '<div class="mcs-pf-section">';
html += '<div class="pf-toggle-header mcs-pf-toggle-header" data-details-id="pf-cpu-details" data-container-id="pf-cpu">';
html += `<div><span id="pf-cpu-toggle" class="mcs-pf-toggle-indicator">${cpuCollapsed ? '+' : '↓'}</span><span class="mcs-pf-cpu-label">CPU Usage</span><span class="mcs-pf-section-subtitle">(real-time, 5s window)</span></div>`;
html += '</div>';
html += `<div id="pf-cpu-details" class="mcs-pf-section-details" style="${cpuCollapsed ? 'display: none;' : ''}"><div id="pf-cpu-section"></div></div>`;
html += '</div>';
const ioCollapsed = collapsedStates['pf-storage-io'] === true;
html += '<div class="mcs-pf-section mcs-pf-section-bordered">';
html += '<div class="pf-toggle-header mcs-pf-toggle-header" data-details-id="pf-storage-io-details" data-container-id="pf-storage-io">';
html += `<div><span id="pf-storage-io-toggle" class="mcs-pf-toggle-indicator">${ioCollapsed ? '+' : '↓'}</span><span class="mcs-pf-io-label">LocalStorage I/O</span><span class="mcs-pf-section-subtitle">(real-time, 5s window)</span></div>`;
html += '</div>';
html += `<div id="pf-storage-io-details" class="mcs-pf-section-details" style="${ioCollapsed ? 'display: none;' : ''}"><div id="pf-storage-io-section"></div></div>`;
html += '</div>';
categoryOrder.forEach(category => {
const items = categories[category];
const total = categoryTotals[category];
if (!items || items.length === 0) return;
const percentage = grandTotal > 0 ? ((total / grandTotal) * 100).toFixed(1) : '0.0';
const itemCount = items.length;
const containerId = `pf-cat-${category.replace(/\s+/g, '-').toLowerCase()}`;
const detailsId = `${containerId}-details`;
const isCollapsed = collapsedStates[containerId] !== false;
html += '<div class="mcs-pf-section">';
html += `<div class="pf-toggle-header mcs-pf-toggle-header" data-details-id="${detailsId}" data-container-id="${containerId}">`;
html += `<div style="flex: 1;">`;
html += `<span id="${containerId}-toggle" class="mcs-pf-toggle-indicator">${isCollapsed ? '+' : '↓'}</span>`;
html += `<span class="mcs-pf-category-label">${category}</span>`;
html += `<span class="mcs-pf-item-count">(${itemCount})</span>`;
html += `</div>`;
html += `<div style="text-align: right;">`;
html += `<div class="mcs-pf-category-total">${this.pf_formatBytes(total)}</div>`;
html += `<div class="mcs-pf-category-percent">${percentage}%</div>`;
html += `</div>`;
html += `</div>`;
html += `<div id="${detailsId}" class="mcs-pf-category-items" style="display: ${isCollapsed ? 'none' : 'block'};">`;
const sortedItems = category === 'MCS Modules'
? items.sort((a, b) => a.key.localeCompare(b.key))
: items.sort((a, b) => b.size - a.size);
sortedItems.forEach(item => {
const itemPercentage = total > 0 ? ((item.size / total) * 100).toFixed(1) : '0.0';
const keyClass = item.isActive ? 'mcs-pf-item-key-active' : 'mcs-pf-item-key-inactive';
const keyTitle = item.isActive ? `${item.key} (used by MCS)` : item.key;
html += '<div class="mcs-pf-item-row">';
html += `<span class="${keyClass}" title="${keyTitle}">${item.key}</span>`;
html += `<span class="mcs-pf-item-size">${this.pf_formatBytes(item.size)} (${itemPercentage}%)</span>`;
html += `<button class="pf-delete-item mcs-pf-delete-btn" data-key="${item.key}" title="Delete ${item.key}">×</button>`;
html += '</div>';
});
html += '</div>';
html += '</div>';
});
if (navigator.storage && navigator.storage.estimate) {
navigator.storage.estimate().then(estimate => {
const used = estimate.usage || 0;
const quota = estimate.quota || 0;
if (quota > 0) {
const percentUsed = ((used / quota) * 100).toFixed(2);
const quotaInfo = document.getElementById('pf-quota-info');
if (quotaInfo) {
const percentColor = percentUsed > 80 ? '#FF5252' : '#4CAF50';
quotaInfo.innerHTML = `
<div class="mcs-pf-quota-container">
<div class="mcs-pf-quota-title">Browser Storage Quota</div>
<div class="mcs-pf-quota-details">
<div>Used: ${this.pf_formatBytes(used)}</div>
<div>Quota: ${this.pf_formatBytes(quota)}</div>
<div style="color: ${percentColor};">${percentUsed}% used</div>
</div>
</div>
`;
}
}
});
}
html += '<div id="pf-quota-info"></div>';
content.innerHTML = html;
const toggleHeaders = content.querySelectorAll('.pf-toggle-header');
toggleHeaders.forEach(header => {
header.addEventListener('click', () => {
const detailsId = header.getAttribute('data-details-id');
const containerId = header.getAttribute('data-container-id');
this.pf_toggleDetails(detailsId, containerId);
});
});
const deleteButtons = content.querySelectorAll('.pf-delete-item');
deleteButtons.forEach(button => {
button.addEventListener('click', (e) => {
e.stopPropagation();
const key = button.getAttribute('data-key');
this.pf_deleteLocalStorageItem(key);
});
});
this.pf_updateCpuDisplay();
this.pf_updateStorageIODisplay();
}
pf_deleteLocalStorageItem(key) {
const isActive = this.pf_isKeyActive(key);
const warningMsg = isActive
? `⚠️ WARNING: "${key}" is ACTIVELY USED by MCS.\n\nDeleting this will likely break functionality!\n\nAre you SURE you want to delete it?`
: `Delete localStorage item "${key}"?`;
if (confirm(warningMsg)) {
try {
localStorage.removeItem(key);
this.pf_updateStorageData();
} catch (e) {
console.error(`[PFormance] Error deleting localStorage item:`, e);
alert(`Error deleting item: ${e.message}`);
}
}
}
pf_toggleDetails(detailsId, containerId) {
const details = document.getElementById(detailsId);
const toggle = document.getElementById(`${containerId}-toggle`);
if (details && toggle) {
const isCollapsed = details.style.display === 'none';
details.style.display = isCollapsed ? 'block' : 'none';
toggle.textContent = isCollapsed ? '↓' : '+';
const collapsedStates = this.pfStorage.get('collapsedSections') ?? {};
collapsedStates[containerId] = !isCollapsed;
this.pfStorage.set('collapsedSections', collapsedStates);
if (isCollapsed) {
this.pf_refreshSectionContent(containerId);
}
}
}
pf_refreshSectionContent(containerId) {
if (containerId === 'pf-cpu') {
this.pf_updateCpuDisplay();
} else if (containerId === 'pf-storage-io') {
this.pf_updateStorageIODisplay();
} else if (containerId.startsWith('pf-cat-')) {
const categoryName = containerId.replace('pf-cat-', '').replace(/-/g, ' ');
const categoryMap = {
'game data': 'Game Data',
'mcs global': 'MCS Global',
'mcs modules': 'MCS Modules',
'other': 'Other'
};
const category = categoryMap[categoryName];
if (category) {
this.pf_updateCategoryContent(containerId, category);
}
}
}
pf_updateCategoryContent(containerId, categoryName) {
const detailsId = `${containerId}-details`;
const details = document.getElementById(detailsId);
if (!details) return;
const categories = this.pf_categorizeKeys();
const items = categories[categoryName] ?? [];
const total = items.reduce((sum, item) => sum + item.size, 0);
const sortedItems = categoryName === 'MCS Modules'
? items.sort((a, b) => a.key.localeCompare(b.key))
: items.sort((a, b) => b.size - a.size);
let html = '';
sortedItems.forEach(item => {
const itemPercentage = total > 0 ? ((item.size / total) * 100).toFixed(1) : '0.0';
const keyClass = item.isActive ? 'mcs-pf-item-key-active' : 'mcs-pf-item-key-inactive';
const keyTitle = item.isActive ? `${item.key} (used by MCS)` : item.key;
html += '<div class="mcs-pf-item-row">';
html += `<span class="${keyClass}" title="${keyTitle}">${item.key}</span>`;
html += `<span class="mcs-pf-item-size">${this.pf_formatBytes(item.size)} (${itemPercentage}%)</span>`;
html += `<button class="pf-delete-item mcs-pf-delete-btn" data-key="${item.key}" title="Delete ${item.key}">×</button>`;
html += '</div>';
});
details.innerHTML = html;
const deleteButtons = details.querySelectorAll('.pf-delete-item');
deleteButtons.forEach(button => {
button.addEventListener('click', (e) => {
e.stopPropagation();
const key = button.getAttribute('data-key');
this.pf_deleteLocalStorageItem(key);
});
});
const header = details.previousElementSibling;
if (header) {
const categories2 = this.pf_categorizeKeys();
let grandTotal = 0;
for (const [_, catItems] of Object.entries(categories2)) {
grandTotal += catItems.reduce((sum, item) => sum + item.size, 0);
}
const percentage = grandTotal > 0 ? ((total / grandTotal) * 100).toFixed(1) : '0.0';
const totalDiv = header.querySelector('.mcs-pf-category-total');
const percentDiv = header.querySelector('.mcs-pf-category-percent');
if (totalDiv) totalDiv.textContent = this.pf_formatBytes(total);
if (percentDiv) percentDiv.textContent = `${percentage}%`;
const countSpan = header.querySelector('.mcs-pf-item-count');
if (countSpan) {
countSpan.textContent = `(${items.length})`;
}
}
}
pf_updateCpuDisplay() {
const cpuSection = document.getElementById('pf-cpu-section');
if (!cpuSection) return;
let html = '';
if (typeof PerformanceMonitor !== 'undefined') {
const perfStats = PerformanceMonitor.getAllStats();
const sortedPerf = Object.entries(perfStats)
.filter(([_, stats]) => stats.cpuPercent > 0 || stats.callCount > 0)
.sort((a, b) => b[1].cpuPercent - a[1].cpuPercent);
if (sortedPerf.length > 0) {
sortedPerf.forEach(([moduleName, stats]) => {
const cpuColor = stats.cpuPercent > 10 ? '#FF5252' :
stats.cpuPercent > 5 ? '#FFA500' :
stats.cpuPercent > 1 ? '#FFEB3B' : '#4CAF50';
html += '<div class="mcs-pf-stat-row">';
html += `<span class="mcs-pf-stat-label">${moduleName}</span>`;
html += `<div style="text-align: right;">`;
html += `<span class="mcs-pf-cpu-percent" style="color: ${cpuColor};">${stats.cpuPercent.toFixed(2)}%</span>`;
html += `<span class="mcs-pf-stat-meta">${stats.callCount} calls</span>`;
html += `<span class="mcs-pf-stat-meta">${stats.avgTime.toFixed(2)}ms avg</span>`;
html += `</div>`;
html += '</div>';
});
} else {
html += '<div class="mcs-pf-no-activity">No CPU activity</div>';
}
}
cpuSection.innerHTML = html;
}
pf_updateStorageIODisplay() {
const storageIOSection = document.getElementById('pf-storage-io-section');
if (!storageIOSection) return;
let html = '';
if (typeof StorageMonitor !== 'undefined') {
const ioStats = StorageMonitor.getAllStats();
const sortedIO = Object.entries(ioStats)
.filter(([_, stats]) => stats.reads > 0 || stats.writes > 0)
.sort((a, b) => (b[1].reads + b[1].writes) - (a[1].reads + a[1].writes));
if (sortedIO.length > 0) {
sortedIO.forEach(([key, stats]) => {
const totalOps = stats.reads + stats.writes;
const opsColor = totalOps > 50 ? '#FF5252' :
totalOps > 20 ? '#FFA500' :
totalOps > 5 ? '#FFEB3B' : '#4CAF50';
const displayKey = key.length > 35 ? key.substring(0, 32) + '...' : key;
html += '<div class="mcs-pf-stat-row">';
html += `<span class="mcs-pf-stat-label" title="${key}">${displayKey}</span>`;
html += `<div style="text-align: right;">`;
html += `<span class="mcs-pf-ops-count" style="color: ${opsColor};">${totalOps} ops</span>`;
html += `<span class="mcs-pf-stat-meta">${stats.reads}R</span>`;
html += `<span class="mcs-pf-stat-meta" style="margin-left: 4px;">${stats.writes}W</span>`;
html += `</div>`;
html += '</div>';
});
} else {
html += '<div class="mcs-pf-no-activity">No storage activity</div>';
}
}
storageIOSection.innerHTML = html;
}
destroyPFormance() {
if (this._pfDragMove) {
document.removeEventListener('mousemove', this._pfDragMove);
document.removeEventListener('mouseup', this._pfDragUp);
this._pfDragMove = null;
this._pfDragUp = null;
}
PerformanceMonitor.enabled = false;
StorageMonitor.enabled = false;
VisibilityManager.clear('pformance-cpu-update');
if (this._pfResizeObserver) {
this._pfResizeObserver.disconnect();
this._pfResizeObserver = null;
}
const pane = document.getElementById('pformance-pane');
if (pane) {
pane.remove();
}
}
// PF end
// QCharm start
get qcStorage() {
if (!this._qcStorage) {
this._qcStorage = createModuleStorage('QC');
}
return this._qcStorage;
}
qc_handleWebSocketMessage(event) {
if (window.MCS_MODULES_DISABLED) return;
const pane = document.getElementById('qcharm-pane');
if (pane && pane.classList.contains('mcs-hidden')) return;
const data = event.detail;
if (data?.type === 'init_character_data' ||
data?.type === 'item_update' ||
data?.type === 'inventory_update' ||
data?.type === 'character_update') {
this.qc_onEquipmentChanged();
}
}
qc_handleMarketDataUpdated() {
const pane = document.getElementById('qcharm-pane');
if (pane && pane.classList.contains('mcs-hidden')) return;
this.qc_updateCharmData();
}
qc_getBaseExp(tier) {
const baseExpValues = {
'trainee': 1,
'basic': 2,
'advanced': 3.5,
'expert': 5,
'master': 6.5,
'grandmaster': 8
};
return baseExpValues[tier] || 0;
}
qc_getEnhancementMultiplier(level) {
const multipliers = {
'0': 1, '1': 1.1, '2': 1.21, '3': 1.33, '4': 1.46,
'5': 1.6, '6': 1.75, '7': 1.91, '8': 2.08, '9': 2.26,
'10': 2.45, '11': 2.67, '12': 2.92, '13': 3.2, '14': 3.51,
'15': 3.85, '16': 4.22, '17': 4.62, '18': 5.05, '19': 5.51,
'20': 6
};
return multipliers[level.toString()] || 1;
}
createQCharmPane() {
if (document.getElementById('qcharm-pane')) return;
if (!this.qc_sortColumn) {
this.qc_sortColumn = 'expPerAsk';
this.qc_sortDirection = 'desc';
}
const pane = document.createElement('div');
pane.id = 'qcharm-pane';
registerPanel('qcharm-pane');
pane.className = 'mcs-pane mcs-qcharm-pane';
const savedSize = this.qcStorage.get('size');
let width = 500;
let height = 400;
if (savedSize) {
width = savedSize.width || 500;
height = savedSize.height || 400;
}
pane.style.width = `${width}px`;
pane.style.height = `${height}px`;
pane.dataset.savedHeight = height;
const header = document.createElement('div');
header.className = 'mcs-pane-header';
const titleSection = document.createElement('div');
titleSection.className = 'mcs-qcharm-title-section';
const titleSpan = document.createElement('span');
titleSpan.id = 'qcharm-title';
titleSpan.className = 'mcs-qcharm-title';
titleSpan.textContent = 'QCharm';
titleSection.appendChild(titleSpan);
const buttonSection = document.createElement('div');
buttonSection.className = 'mcs-qcharm-button-section';
const refreshBtn = document.createElement('button');
refreshBtn.id = 'qcharm-refresh-btn';
refreshBtn.className = 'mcs-qcharm-btn';
refreshBtn.textContent = '↻';
refreshBtn.title = 'Refresh charm data';
const minimizeBtn = document.createElement('button');
minimizeBtn.id = 'qcharm-minimize-btn';
minimizeBtn.className = 'mcs-qcharm-btn';
minimizeBtn.textContent = '−';
buttonSection.appendChild(refreshBtn);
buttonSection.appendChild(minimizeBtn);
header.appendChild(titleSection);
header.appendChild(buttonSection);
const content = document.createElement('div');
content.id = 'qcharm-content';
content.className = 'mcs-qcharm-content';
const guideSection = document.createElement('div');
guideSection.className = 'mcs-qcharm-guide-section';
const guideHeader = document.createElement('div');
guideHeader.className = 'mcs-qcharm-guide-header';
guideHeader.innerHTML = '<span>Charm EXP Guide</span><span class="mcs-qcharm-guide-toggle">▼</span>';
const guideContent = document.createElement('div');
guideContent.className = 'mcs-qcharm-guide-content';
const baseExpRow = document.createElement('div');
baseExpRow.className = 'mcs-qcharm-guide-row';
baseExpRow.innerHTML = `
<div class="mcs-qcharm-guide-item">Trainee 1%</div>
<div class="mcs-qcharm-guide-item">Basic 2%</div>
<div class="mcs-qcharm-guide-item">Advanced 3.5%</div>
<div class="mcs-qcharm-guide-item">Expert 5%</div>
<div class="mcs-qcharm-guide-item">Master 6.5%</div>
<div class="mcs-qcharm-guide-item">Grandmaster 8%</div>
`;
const enhTable = document.createElement('div');
enhTable.className = 'mcs-qcharm-guide-enh-table';
const enhMultipliers = [
['+0: 1x', '+1: 1.1x', '+2: 1.21x', '+3: 1.33x'],
['+4: 1.46x', '+5: 1.6x', '+6: 1.75x', '+7: 1.91x'],
['+8: 2.08x', '+9: 2.26x', '+10: 2.45x', '+11: 2.67x'],
['+12: 2.92x', '+13: 3.2x', '+14: 3.51x', '+15: 3.85x'],
['+16: 4.22x', '+17: 4.62x', '+18: 5.05x', '+19: 5.51x'],
['+20: 6x', '', '', '']
];
enhMultipliers.forEach(row => {
const rowDiv = document.createElement('div');
rowDiv.className = 'mcs-qcharm-guide-enh-row';
row.forEach(item => {
const cell = document.createElement('div');
cell.className = 'mcs-qcharm-guide-enh-cell';
cell.textContent = item;
rowDiv.appendChild(cell);
});
enhTable.appendChild(rowDiv);
});
guideContent.appendChild(baseExpRow);
guideContent.appendChild(enhTable);
guideSection.appendChild(guideHeader);
guideSection.appendChild(guideContent);
const savedGuideState = this.qcStorage.get('guide_collapsed');
if (savedGuideState === true || savedGuideState === 'true') {
guideContent.classList.add('mcs-qcharm-guide-collapsed');
guideHeader.querySelector('.mcs-qcharm-guide-toggle').textContent = '▶';
}
guideHeader.addEventListener('click', () => {
const isCollapsed = guideContent.classList.toggle('mcs-qcharm-guide-collapsed');
guideHeader.querySelector('.mcs-qcharm-guide-toggle').textContent = isCollapsed ? '▶' : '▼';
window.lootDropsTrackerInstance.qcStorage.set('guide_collapsed', isCollapsed);
});
content.appendChild(guideSection);
pane.appendChild(header);
pane.appendChild(content);
document.body.appendChild(pane);
let dragStartX, dragStartY, initialLeft, initialTop;
const onDragMove = (e) => {
const dx = e.clientX - dragStartX;
const dy = e.clientY - dragStartY;
let newLeft = initialLeft + dx;
let newTop = initialTop + dy;
const paneRect = pane.getBoundingClientRect();
const headerHeight = header.getBoundingClientRect().height;
const minLeft = -paneRect.width + 100;
const maxLeft = window.innerWidth - 100;
const minTop = 0;
const maxTop = window.innerHeight - headerHeight;
newLeft = Math.max(minLeft, Math.min(maxLeft, newLeft));
newTop = Math.max(minTop, Math.min(maxTop, newTop));
pane.style.left = newLeft + 'px';
pane.style.top = newTop + 'px';
};
const onDragUp = () => {
document.removeEventListener('mousemove', onDragMove);
document.removeEventListener('mouseup', onDragUp);
header.style.cursor = 'move';
const rect = pane.getBoundingClientRect();
window.lootDropsTrackerInstance.qcStorage.set('position', { top: rect.top, left: rect.left });
this.constrainPanelToBoundaries('qcharm-pane', 'mcs_QC', true);
};
header.addEventListener('mousedown', (e) => {
if (e.target.closest('button')) return;
dragStartX = e.clientX;
dragStartY = e.clientY;
const rect = pane.getBoundingClientRect();
initialLeft = rect.left;
initialTop = rect.top;
header.style.cursor = 'grabbing';
pane.style.left = initialLeft + 'px';
pane.style.right = 'auto';
window.lootDropsTrackerInstance._qcDragMove = onDragMove;
window.lootDropsTrackerInstance._qcDragUp = onDragUp;
document.addEventListener('mousemove', onDragMove);
document.addEventListener('mouseup', onDragUp);
});
minimizeBtn.addEventListener('click', () => {
const isMinimized = content.classList.contains('mcs-hidden');
if (isMinimized) {
content.classList.remove('mcs-hidden');
minimizeBtn.textContent = '−';
header.classList.remove('mcs-pane-header-minimized');
pane.classList.remove('mcs-qcharm-pane-minimized');
const savedHeight = pane.dataset.savedHeight || height;
pane.style.height = savedHeight + 'px';
window.lootDropsTrackerInstance.qcStorage.set('minimized', false);
} else {
const currentRect = pane.getBoundingClientRect();
pane.dataset.savedHeight = currentRect.height;
content.classList.add('mcs-hidden');
minimizeBtn.textContent = '+';
header.classList.add('mcs-pane-header-minimized');
pane.classList.add('mcs-qcharm-pane-minimized');
const headerHeight = header.getBoundingClientRect().height;
pane.style.height = headerHeight + 'px';
window.lootDropsTrackerInstance.qcStorage.set('minimized', true);
}
});
refreshBtn.addEventListener('click', () => {
this.qc_updateCharmData();
});
pane._qcPositionInitialized = false;
const restoreSavedPosition = () => {
const savedPosition = window.lootDropsTrackerInstance.qcStorage.get('position');
if (savedPosition && (savedPosition.top !== undefined || savedPosition.left !== undefined || savedPosition.right !== undefined)) {
const hasValidTop = savedPosition.top !== undefined && savedPosition.top > 0;
const hasValidLeft = savedPosition.left !== undefined && savedPosition.left > 0;
const hasValidRight = savedPosition.right !== undefined && savedPosition.right > 0;
if (hasValidTop || hasValidLeft || hasValidRight) {
if (hasValidTop) {
pane.style.top = savedPosition.top + 'px';
}
if (hasValidLeft) {
pane.style.left = savedPosition.left + 'px';
pane.style.right = 'auto';
} else if (hasValidRight) {
pane.style.right = savedPosition.right + 'px';
}
window.lootDropsTrackerInstance.constrainPanelToBoundaries('qcharm-pane', 'mcs_QC', true);
}
}
pane._qcPositionInitialized = true;
};
const savedMinimized = this.qcStorage.get('minimized');
if (savedMinimized === true || savedMinimized === 'true') {
content.classList.add('mcs-hidden');
minimizeBtn.textContent = '+';
header.classList.add('mcs-pane-header-minimized');
pane.classList.add('mcs-qcharm-pane-minimized');
const headerHeight = header.getBoundingClientRect().height;
pane.style.height = headerHeight + 'px';
}
let qcResizeTimer = null;
this._qcResizeObserver = new ResizeObserver(() => {
const rect = pane.getBoundingClientRect();
const isMinimized = content.classList.contains('mcs-hidden');
if (!isMinimized) {
pane.dataset.savedHeight = rect.height;
}
clearTimeout(qcResizeTimer);
qcResizeTimer = setTimeout(() => {
window.lootDropsTrackerInstance.qcStorage.set('size', {
width: rect.width,
height: isMinimized ? parseFloat(pane.dataset.savedHeight) : rect.height
});
}, 300);
});
this._qcResizeObserver.observe(pane);
this._qcVisibilityObserver = new MutationObserver((mutations) => {
for (const mutation of mutations) {
if (mutation.type === 'attributes' && mutation.attributeName === 'class') {
const isHidden = pane.classList.contains('mcs-hidden');
if (!isHidden) {
if (!pane._qcPositionInitialized) {
restoreSavedPosition();
window.lootDropsTrackerInstance.qc_updateCharmData();
}
} else {
if (pane._qcPositionInitialized) {
const rect = pane.getBoundingClientRect();
if (rect.top > 0 || rect.left > 0) {
const position = {
top: rect.top,
left: rect.left
};
window.lootDropsTrackerInstance.qcStorage.set('position', position);
}
}
}
}
}
});
this._qcVisibilityObserver.observe(pane, { attributes: true, attributeFilter: ['class'] });
this._qcWsListener = this.qc_handleWebSocketMessage.bind(this);
this._qcMarketDataListener = this.qc_handleMarketDataUpdated.bind(this);
window.addEventListener('EquipSpyWebSocketMessage', this._qcWsListener);
window.addEventListener('MarketDataUpdated', this._qcMarketDataListener);
const savedStates = ToolVisibilityStorage.get();
const shouldRun = savedStates['qcharm'] !== false;
if (shouldRun) {
restoreSavedPosition();
this.qc_updateCharmData();
} else {
pane.classList.add('mcs-hidden');
}
}
qc_getEquippedCharm() {
try {
const charData = CharacterDataStorage.get() || {};
if (!charData.characterItems) {
return null;
}
const charmItem = charData.characterItems.find(item =>
item.itemLocationHrid === '/item_locations/charm'
);
if (charmItem) {
return charmItem;
}
return null;
} catch (e) {
console.error('[QCharm] Error getting equipped charm:', e);
return null;
}
}
qc_getCharmFamily(charmHrid) {
if (!charmHrid) return [];
const match = charmHrid.match(/\/items\/(trainee|basic|advanced|expert|master|grandmaster)_(.+?)_charm/i);
if (!match) {
return [];
}
const baseType = match[2];
const tiers = ['trainee', 'basic', 'advanced', 'expert', 'master', 'grandmaster'];
const family = tiers.map(tier => `/items/${tier}_${baseType}_charm`);
return family;
}
qc_getStoredCharmData() {
return this.qcStorage.get('charms', {});
}
qc_saveCharmData(charmData) {
this.qcStorage.set('charms', charmData);
}
qc_updateCharmData() {
const equippedCharm = this.qc_getEquippedCharm();
if (!equippedCharm) {
this.qc_renderCharmTable([]);
return;
}
const charmFamily = this.qc_getCharmFamily(equippedCharm.itemHrid);
const storedData = this.qc_getStoredCharmData();
const marketData = window.lootDropsTrackerInstance?.spyMarketData ?? {};
const now = Date.now();
charmFamily.forEach(charmHrid => {
if (!storedData[charmHrid]) {
storedData[charmHrid] = {};
}
const isTrainee = charmHrid.includes('trainee');
if (isTrainee) {
storedData[charmHrid].askPrice = 250000;
storedData[charmHrid].lastSeen = now;
} else {
if (marketData[charmHrid]) {
if (!storedData[charmHrid].enhancements) {
storedData[charmHrid].enhancements = {};
}
let foundCount = 0;
for (let enhLevel = 0; enhLevel <= 20; enhLevel++) {
const enhKey = enhLevel.toString();
if (marketData[charmHrid][enhKey]) {
const askPrice = marketData[charmHrid][enhKey].a ?? 0;
if (askPrice > 0) {
storedData[charmHrid].enhancements[enhKey] = {
askPrice: askPrice,
lastSeen: now
};
foundCount++;
}
}
}
} else {
}
}
});
this.qc_saveCharmData(storedData);
this.qc_renderCharmTable(charmFamily, equippedCharm.itemHrid, storedData);
}
qc_onEquipmentChanged() {
this.qc_updateCharmData();
}
qc_getCharmDisplayName(charmHrid) {
const match = charmHrid.match(/\/items\/(trainee|basic|advanced|expert|master|grandmaster)_(.+?)_charm/i);
if (!match) return charmHrid;
const tier = match[1].charAt(0).toUpperCase() + match[1].slice(1).toLowerCase();
const baseName = match[2].split('_').map(word =>
word.charAt(0).toUpperCase() + word.slice(1)
).join(' ');
return `${baseName} (${tier})`;
}
qc_calculateCharmExp(charmHrid, enhancementLevel = 0) {
const match = charmHrid.match(/\/items\/(trainee|basic|advanced|expert|master|grandmaster)_/i);
if (!match) return null;
const tier = match[1].toLowerCase();
const baseExp = this.qc_getBaseExp(tier);
if (!baseExp) return null;
const multiplier = this.qc_getEnhancementMultiplier(enhancementLevel);
return baseExp * multiplier;
}
qc_getCharmExpPercent(charmHrid, enhancementLevel = 0) {
const exp = this.qc_calculateCharmExp(charmHrid, enhancementLevel);
return exp !== null ? exp.toFixed(2) + '%' : 'N/A';
}
qc_getQualityTier(charmHrid) {
const tiers = ['trainee', 'basic', 'advanced', 'expert', 'master', 'grandmaster'];
const match = charmHrid.match(/\/items\/(trainee|basic|advanced|expert|master|grandmaster)_/i);
if (!match) return 0;
return tiers.indexOf(match[1].toLowerCase());
}
qc_sortCharmRows(rows) {
const column = this.qc_sortColumn;
const direction = this.qc_sortDirection === 'asc' ? 1 : -1;
return rows.sort((a, b) => {
let valA, valB;
switch (column) {
case 'name':
valA = a.tier * 100 + (a.enhLevel !== null ? a.enhLevel : -1);
valB = b.tier * 100 + (b.enhLevel !== null ? b.enhLevel : -1);
break;
case 'expPercent':
valA = a.expPercent;
valB = b.expPercent;
break;
case 'askPrice':
valA = a.askPrice;
valB = b.askPrice;
break;
case 'expPerAsk':
valA = a.expPerAsk;
valB = b.expPerAsk;
break;
}
if (valA === valB) {
const tierDiff = a.tier - b.tier;
if (tierDiff !== 0) return tierDiff;
return (a.enhLevel ?? 0) - (b.enhLevel ?? 0);
}
return direction * (valA < valB ? -1 : 1);
});
}
qc_setSortColumn(column) {
if (this.qc_sortColumn === column) {
this.qc_sortDirection = this.qc_sortDirection === 'asc' ? 'desc' : 'asc';
} else {
this.qc_sortColumn = column;
this.qc_sortDirection = column === 'expPerAsk' ? 'desc' : 'asc';
}
this.qc_updateCharmData();
}
qc_renderCharmTable(charmFamily, equippedCharmHrid, storedData) {
const content = document.getElementById('qcharm-content');
if (!content) return;
let tableContainer = content.querySelector('.mcs-qcharm-table-container');
if (!tableContainer) {
tableContainer = document.createElement('div');
tableContainer.className = 'mcs-qcharm-table-container';
content.appendChild(tableContainer);
}
if (charmFamily.length === 0) {
tableContainer.innerHTML = '<div class="mcs-qcharm-empty">No charm equipped</div>';
return;
}
const equippedCharm = this.qc_getEquippedCharm();
const equippedEnhLevel = equippedCharm?.enhancementLevel ?? 0;
const equippedExp = this.qc_calculateCharmExp(equippedCharmHrid, equippedEnhLevel);
const allRows = [];
charmFamily.forEach(charmHrid => {
const name = this.qc_getCharmDisplayName(charmHrid);
const isTrainee = charmHrid.includes('trainee');
const tier = this.qc_getQualityTier(charmHrid);
if (isTrainee) {
const askPrice = storedData[charmHrid]?.askPrice || 250000;
const lastSeen = storedData[charmHrid]?.lastSeen ?? 0;
const exp = this.qc_calculateCharmExp(charmHrid, 0);
const expPercent = exp !== null ? exp : 0;
const expPerAsk = askPrice > 0 && exp !== null ? (exp / askPrice * 1000000) : 0;
const isEquipped = charmHrid === equippedCharmHrid && equippedEnhLevel === 0;
allRows.push({
charmHrid,
name,
enhLevel: null,
isEquipped,
tier,
expPercent,
expPercentStr: this.qc_getCharmExpPercent(charmHrid, 0),
askPrice,
askPriceStr: askPrice.toLocaleString(),
expPerAsk,
expPerAskStr: expPerAsk > 0 ? expPerAsk.toFixed(2) : 'N/A',
lastSeen
});
} else {
const enhancements = storedData[charmHrid]?.enhancements || {};
const enhLevels = Object.keys(enhancements).map(k => parseInt(k)).sort((a, b) => a - b);
if (enhLevels.length === 0) {
const exp = this.qc_calculateCharmExp(charmHrid, 0);
const expPercent = exp !== null ? exp : 0;
const isEquipped = charmHrid === equippedCharmHrid && equippedEnhLevel === 0;
allRows.push({
charmHrid,
name,
enhLevel: null,
isEquipped,
tier,
expPercent,
expPercentStr: this.qc_getCharmExpPercent(charmHrid, 0),
askPrice: 0,
askPriceStr: 'No data',
expPerAsk: 0,
expPerAskStr: 'N/A',
lastSeen: 0
});
} else {
enhLevels.forEach(enhLevel => {
const enhData = enhancements[enhLevel.toString()];
const askPrice = enhData?.askPrice ?? 0;
const lastSeen = enhData?.lastSeen ?? 0;
const exp = this.qc_calculateCharmExp(charmHrid, enhLevel);
const expPercent = exp !== null ? exp : 0;
const expPerAsk = askPrice > 0 && exp !== null ? (exp / askPrice * 1000000) : 0;
const isEquipped = charmHrid === equippedCharmHrid && enhLevel === equippedEnhLevel;
allRows.push({
charmHrid,
name,
enhLevel,
isEquipped,
tier,
expPercent,
expPercentStr: this.qc_getCharmExpPercent(charmHrid, enhLevel),
askPrice,
askPriceStr: askPrice > 0 ? askPrice.toLocaleString() : 'No data',
expPerAsk,
expPerAskStr: expPerAsk > 0 ? expPerAsk.toFixed(2) : 'N/A',
lastSeen
});
});
}
}
});
const sortedRows = this.qc_sortCharmRows(allRows);
const higherOrEqualExpRows = sortedRows.filter(row => row.expPercent >= equippedExp);
const lowerExpRows = sortedRows.filter(row => row.expPercent < equippedExp);
const renderTableSection = (rows, sectionTitle, sectionId) => {
if (rows.length === 0) return '';
let html = '';
html += `<div class="mcs-qcharm-section">`;
html += `<div class="mcs-qcharm-section-header" data-section="${sectionId}">`;
html += `<span>${sectionTitle}</span>`;
html += `<span class="mcs-qcharm-section-toggle">▼</span>`;
html += `</div>`;
html += `<div class="mcs-qcharm-section-content" data-section="${sectionId}">`;
html += '<table class="mcs-qcharm-table">';
html += '<thead><tr>';
html += `<th class="mcs-qcharm-th mcs-qcharm-sortable ${this.qc_sortColumn === 'name' ? 'mcs-qcharm-sorted-' + this.qc_sortDirection : ''}" data-column="name">Name</th>`;
html += `<th class="mcs-qcharm-th" title="Enhancement Level">Enh</th>`;
html += `<th class="mcs-qcharm-th mcs-qcharm-sortable ${this.qc_sortColumn === 'expPercent' ? 'mcs-qcharm-sorted-' + this.qc_sortDirection : ''}" data-column="expPercent">Exp%</th>`;
html += `<th class="mcs-qcharm-th mcs-qcharm-sortable ${this.qc_sortColumn === 'askPrice' ? 'mcs-qcharm-sorted-' + this.qc_sortDirection : ''}" data-column="askPrice">Ask Price</th>`;
html += `<th class="mcs-qcharm-th mcs-qcharm-sortable ${this.qc_sortColumn === 'expPerAsk' ? 'mcs-qcharm-sorted-' + this.qc_sortDirection : ''}" data-column="expPerAsk">Exp/Ask</th>`;
html += `<th class="mcs-qcharm-th" title="Last seen in market data">Last Seen</th>`;
html += '</tr></thead>';
html += '<tbody>';
rows.forEach(row => {
const rowClass = row.isEquipped ? 'mcs-qcharm-row mcs-qcharm-equipped' : 'mcs-qcharm-row';
const enhDisplay = row.enhLevel !== null ? row.enhLevel : '-';
let lastSeenDisplay = 'Never';
if (row.lastSeen > 0) {
const date = new Date(row.lastSeen);
const month = (date.getMonth() + 1).toString().padStart(2, '0');
const day = date.getDate().toString().padStart(2, '0');
const hours = date.getHours().toString().padStart(2, '0');
const minutes = date.getMinutes().toString().padStart(2, '0');
const seconds = date.getSeconds().toString().padStart(2, '0');
lastSeenDisplay = `${month}/${day} ${hours}:${minutes}:${seconds}`;
}
html += `<tr class="${rowClass}">`;
html += `<td class="mcs-qcharm-td">${row.name}</td>`;
html += `<td class="mcs-qcharm-td">${enhDisplay}</td>`;
html += `<td class="mcs-qcharm-td">${row.expPercentStr}</td>`;
html += `<td class="mcs-qcharm-td">${row.askPriceStr}</td>`;
html += `<td class="mcs-qcharm-td">${row.expPerAskStr}</td>`;
html += `<td class="mcs-qcharm-td mcs-qcharm-last-seen">${lastSeenDisplay}</td>`;
html += '</tr>';
});
html += '</tbody>';
html += '</table>';
html += '</div>';
html += '</div>';
return html;
};
let html = '';
html += renderTableSection(higherOrEqualExpRows, `Charm Upgrades (${equippedExp?.toFixed(2)}%)`, 'upgrades');
html += renderTableSection(lowerExpRows, `Charm Downgrades (${equippedExp?.toFixed(2)}%)`, 'downgrades');
tableContainer.innerHTML = html;
tableContainer.querySelectorAll('.mcs-qcharm-sortable').forEach(th => {
th.addEventListener('click', (e) => {
const column = e.target.dataset.column;
this.qc_setSortColumn(column);
});
});
const savedSectionStates = this.qcStorage.get('sections_collapsed', {});
tableContainer.querySelectorAll('.mcs-qcharm-section-header').forEach(header => {
const sectionId = header.dataset.section;
const content = tableContainer.querySelector(`.mcs-qcharm-section-content[data-section="${sectionId}"]`);
const toggle = header.querySelector('.mcs-qcharm-section-toggle');
if (savedSectionStates[sectionId] === true && content && toggle) {
content.classList.add('mcs-qcharm-section-collapsed');
toggle.textContent = '▶';
}
header.addEventListener('click', () => {
if (content && toggle) {
const isCollapsed = content.classList.toggle('mcs-qcharm-section-collapsed');
toggle.textContent = isCollapsed ? '▶' : '▼';
const states = window.lootDropsTrackerInstance.qcStorage.get('sections_collapsed', {});
states[sectionId] = isCollapsed;
window.lootDropsTrackerInstance.qcStorage.set('sections_collapsed', states);
}
});
});
}
destroyQCharmPane() {
if (this._qcDragMove) {
document.removeEventListener('mousemove', this._qcDragMove);
document.removeEventListener('mouseup', this._qcDragUp);
this._qcDragMove = null;
this._qcDragUp = null;
}
if (this._qcResizeObserver) { this._qcResizeObserver.disconnect(); this._qcResizeObserver = null; }
if (this._qcVisibilityObserver) { this._qcVisibilityObserver.disconnect(); this._qcVisibilityObserver = null; }
if (this._qcWsListener) { window.removeEventListener('EquipSpyWebSocketMessage', this._qcWsListener); this._qcWsListener = null; }
if (this._qcMarketDataListener) { window.removeEventListener('MarketDataUpdated', this._qcMarketDataListener); this._qcMarketDataListener = null; }
const pane = document.getElementById('qcharm-pane');
if (pane) pane.remove();
}
// QCharm end
// OPanel start
get opStorage() {
if (!this._opStorage) {
this._opStorage = createModuleStorage('OP');
}
return this._opStorage;
}
createOPanel() {
if (document.getElementById('opanel-pane')) return;
this._opanelActiveDocListeners = [];
if (!this.oPanelConfig) {
const saved = this.opStorage.get('config');
if (saved) {
try {
this.oPanelConfig = saved;
if (this.oPanelConfig.experiencePerHour === undefined) {
this.oPanelConfig.experiencePerHour = false;
}
if (this.oPanelConfig.totalProfit === undefined) {
this.oPanelConfig.totalProfit = false;
}
if (this.oPanelConfig.dps === undefined) {
this.oPanelConfig.dps = false;
}
if (this.oPanelConfig.overExpected === undefined) {
this.oPanelConfig.overExpected = false;
}
if (this.oPanelConfig.overExpectedOnlyPlayer === undefined) {
this.oPanelConfig.overExpectedOnlyPlayer = false;
}
if (this.oPanelConfig.overExpectedOnlyNumbers === undefined) {
this.oPanelConfig.overExpectedOnlyNumbers = false;
}
if (this.oPanelConfig.luck === undefined) {
this.oPanelConfig.luck = false;
}
if (this.oPanelConfig.luckOnlyPlayer === undefined) {
this.oPanelConfig.luckOnlyPlayer = false;
}
if (this.oPanelConfig.luckOnlyNumbers === undefined) {
this.oPanelConfig.luckOnlyNumbers = false;
}
if (this.oPanelConfig.deathsPerHour === undefined) {
this.oPanelConfig.deathsPerHour = false;
}
if (this.oPanelConfig.houses === undefined) {
this.oPanelConfig.houses = false;
}
if (this.oPanelConfig.equipmentWatch === undefined) {
this.oPanelConfig.equipmentWatch = false;
}
if (this.oPanelConfig.combatStatus === undefined) {
this.oPanelConfig.combatStatus = true;
}
if (this.oPanelConfig.usePlayerNameRecolor === undefined) {
this.oPanelConfig.usePlayerNameRecolor = false;
}
if (this.oPanelConfig.ntallyInventory === undefined) {
this.oPanelConfig.ntallyInventory = false;
}
if (this.oPanelConfig.kollectionBuildScore === undefined) {
this.oPanelConfig.kollectionBuildScore = false;
}
if (this.oPanelConfig.kollectionNetWorth === undefined) {
this.oPanelConfig.kollectionNetWorth = false;
}
if (this.oPanelConfig.ewatchCoins === undefined) {
this.oPanelConfig.ewatchCoins = false;
}
if (this.oPanelConfig.ewatchMarket === undefined) {
this.oPanelConfig.ewatchMarket = false;
}
if (this.oPanelConfig.skillBooks === undefined) {
this.oPanelConfig.skillBooks = false;
}
if (this.oPanelConfig.treasure === undefined) {
this.oPanelConfig.treasure = false;
}
if (!this.oPanelConfig.order) {
this.oPanelConfig.order = ['battleTimer', 'combatRevenue', 'consumables', 'experiencePerHour', 'totalProfit', 'dps', 'overExpected', 'luck', 'deathsPerHour', 'houses', 'equipmentWatch', 'combatStatus', 'ntallyInventory', 'kollectionBuildScore', 'kollectionNetWorth', 'ewatchCoins', 'ewatchMarket', 'skillBooks'];
} else {
if (!this.oPanelConfig.order.includes('experiencePerHour')) {
this.oPanelConfig.order.push('experiencePerHour');
}
if (!this.oPanelConfig.order.includes('totalProfit')) {
this.oPanelConfig.order.push('totalProfit');
}
if (!this.oPanelConfig.order.includes('dps')) {
this.oPanelConfig.order.push('dps');
}
if (!this.oPanelConfig.order.includes('overExpected')) {
this.oPanelConfig.order.push('overExpected');
}
if (!this.oPanelConfig.order.includes('luck')) {
this.oPanelConfig.order.push('luck');
}
if (!this.oPanelConfig.order.includes('deathsPerHour')) {
this.oPanelConfig.order.push('deathsPerHour');
}
if (!this.oPanelConfig.order.includes('houses')) {
this.oPanelConfig.order.push('houses');
}
if (!this.oPanelConfig.order.includes('equipmentWatch')) {
this.oPanelConfig.order.push('equipmentWatch');
}
if (!this.oPanelConfig.order.includes('combatStatus')) {
this.oPanelConfig.order.push('combatStatus');
}
if (!this.oPanelConfig.order.includes('ntallyInventory')) {
this.oPanelConfig.order.push('ntallyInventory');
}
if (!this.oPanelConfig.order.includes('kollectionBuildScore')) {
this.oPanelConfig.order.push('kollectionBuildScore');
}
if (!this.oPanelConfig.order.includes('kollectionNetWorth')) {
this.oPanelConfig.order.push('kollectionNetWorth');
}
if (!this.oPanelConfig.order.includes('ewatchCoins')) {
this.oPanelConfig.order.push('ewatchCoins');
}
if (!this.oPanelConfig.order.includes('ewatchMarket')) {
this.oPanelConfig.order.push('ewatchMarket');
}
if (!this.oPanelConfig.order.includes('skillBooks')) {
this.oPanelConfig.order.push('skillBooks');
}
if (!this.oPanelConfig.order.includes('treasure')) {
this.oPanelConfig.order.push('treasure');
}
}
if (!this.oPanelConfig.sizes) {
this.oPanelConfig.sizes = {
battleTimer: { width: null, height: 40 },
combatRevenue: { width: null, height: 150 },
consumables: { width: null, height: 80 },
experiencePerHour: { width: null, height: 40 },
totalProfit: { width: null, height: 40 },
dps: { width: null, height: 150 },
overExpected: { width: null, height: 70 },
luck: { width: null, height: 70 },
deathsPerHour: { width: null, height: 40 },
houses: { width: null, height: 40 },
equipmentWatch: { width: null, height: 80 },
combatStatus: { width: null, height: 40 },
ntallyInventory: { width: null, height: 40 },
kollectionBuildScore: { width: null, height: 40 },
kollectionNetWorth: { width: null, height: 40 },
ewatchCoins: { width: null, height: 40 },
ewatchMarket: { width: null, height: 40 },
skillBooks: { width: null, height: 40 },
treasure: { width: null, height: 40 }
};
} else {
if (!this.oPanelConfig.sizes.experiencePerHour) {
this.oPanelConfig.sizes.experiencePerHour = { width: null, height: 40 };
}
if (!this.oPanelConfig.sizes.totalProfit) {
this.oPanelConfig.sizes.totalProfit = { width: null, height: 40 };
}
if (!this.oPanelConfig.sizes.dps) {
this.oPanelConfig.sizes.dps = { width: null, height: 150 };
}
if (!this.oPanelConfig.sizes.overExpected) {
this.oPanelConfig.sizes.overExpected = { width: null, height: 70 };
}
if (!this.oPanelConfig.sizes.luck) {
this.oPanelConfig.sizes.luck = { width: null, height: 70 };
}
if (!this.oPanelConfig.sizes.deathsPerHour) {
this.oPanelConfig.sizes.deathsPerHour = { width: null, height: 40 };
}
if (!this.oPanelConfig.sizes.houses) {
this.oPanelConfig.sizes.houses = { width: null, height: 40 };
}
if (!this.oPanelConfig.sizes.equipmentWatch) {
this.oPanelConfig.sizes.equipmentWatch = { width: null, height: 80 };
}
if (!this.oPanelConfig.sizes.combatStatus) {
this.oPanelConfig.sizes.combatStatus = { width: null, height: 40 };
}
if (!this.oPanelConfig.sizes.ntallyInventory) {
this.oPanelConfig.sizes.ntallyInventory = { width: null, height: 40 };
}
if (!this.oPanelConfig.sizes.kollectionBuildScore) {
this.oPanelConfig.sizes.kollectionBuildScore = { width: null, height: 40 };
}
if (!this.oPanelConfig.sizes.kollectionNetWorth) {
this.oPanelConfig.sizes.kollectionNetWorth = { width: null, height: 40 };
}
if (!this.oPanelConfig.sizes.ewatchCoins) {
this.oPanelConfig.sizes.ewatchCoins = { width: null, height: 40 };
}
if (!this.oPanelConfig.sizes.ewatchMarket) {
this.oPanelConfig.sizes.ewatchMarket = { width: null, height: 40 };
}
if (!this.oPanelConfig.sizes.skillBooks) {
this.oPanelConfig.sizes.skillBooks = { width: null, height: 40 };
}
if (!this.oPanelConfig.sizes.treasure) {
this.oPanelConfig.sizes.treasure = { width: null, height: 40 };
}
}
if (!this.oPanelConfig.positions) {
this.oPanelConfig.positions = {
battleTimer: { x: 0, y: 0 },
combatRevenue: { x: 0, y: 0 },
consumables: { x: 0, y: 0 },
experiencePerHour: { x: 0, y: 0 },
totalProfit: { x: 0, y: 0},
dps: { x: 0, y: 0 },
overExpected: { x: 0, y: 0 },
luck: { x: 0, y: 0 },
deathsPerHour: { x: 0, y: 0 },
houses: { x: 0, y: 0 },
equipmentWatch: { x: 0, y: 0 },
combatStatus: { x: 0, y: 0 },
ntallyInventory: { x: 0, y: 0 },
kollectionBuildScore: { x: 0, y: 0 },
kollectionNetWorth: { x: 0, y: 0 },
ewatchCoins: { x: 0, y: 0 },
ewatchMarket: { x: 0, y: 0 },
skillBooks: { x: 0, y: 0 },
treasure: { x: 0, y: 0 }
};
} else {
if (!this.oPanelConfig.positions.experiencePerHour) {
this.oPanelConfig.positions.experiencePerHour = { x: 0, y:0 };
}
if (!this.oPanelConfig.positions.totalProfit) {
this.oPanelConfig.positions.totalProfit = { x: 0, y:0 };
}
if (!this.oPanelConfig.positions.dps) {
this.oPanelConfig.positions.dps = { x: 0, y: 0 };
}
if (!this.oPanelConfig.positions.overExpected) {
this.oPanelConfig.positions.overExpected = { x: 0, y: 0 };
}
if (!this.oPanelConfig.positions.luck) {
this.oPanelConfig.positions.luck = { x: 0, y: 0 };
}
if (!this.oPanelConfig.positions.deathsPerHour) {
this.oPanelConfig.positions.deathsPerHour = { x: 0, y: 0 };
}
if (!this.oPanelConfig.positions.houses) {
this.oPanelConfig.positions.houses = { x: 0, y: 0 };
}
if (!this.oPanelConfig.positions.equipmentWatch) {
this.oPanelConfig.positions.equipmentWatch = { x: 0, y: 0 };
}
if (!this.oPanelConfig.positions.combatStatus) {
this.oPanelConfig.positions.combatStatus = { x: 0, y: 0 };
}
if (!this.oPanelConfig.positions.ntallyInventory) {
this.oPanelConfig.positions.ntallyInventory = { x: 0, y: 0 };
}
if (!this.oPanelConfig.positions.kollectionBuildScore) {
this.oPanelConfig.positions.kollectionBuildScore = { x: 0, y:0 };
}
if (!this.oPanelConfig.positions.kollectionNetWorth) {
this.oPanelConfig.positions.kollectionNetWorth = { x: 0, y: 0 };
}
if (!this.oPanelConfig.positions.ewatchCoins) {
this.oPanelConfig.positions.ewatchCoins = { x: 0, y: 0 };
}
if (!this.oPanelConfig.positions.ewatchMarket) {
this.oPanelConfig.positions.ewatchMarket = { x: 0, y: 0 };
}
if (!this.oPanelConfig.positions.skillBooks) {
this.oPanelConfig.positions.skillBooks = { x: 0, y: 0 };
}
if (!this.oPanelConfig.positions.treasure) {
this.oPanelConfig.positions.treasure = { x: 0, y: 0 };
}
}
} catch (e) {
this.oPanelConfig = {
battleTimer: true,
combatRevenue: true,
consumables: true,
experiencePerHour: true,
totalProfit: true,
dps: true,
overExpected: true,
luck: true,
deathsPerHour: true,
houses: true,
equipmentWatch: true,
combatStatus: true,
ntallyInventory: true,
kollectionBuildScore: true,
kollectionNetWorth: true,
ewatchCoins: true,
ewatchMarket: true,
skillBooks: true,
treasure: true,
order: ['battleTimer', 'combatRevenue', 'consumables', 'experiencePerHour', 'totalProfit', 'dps', 'overExpected', 'luck', 'deathsPerHour', 'houses', 'equipmentWatch', 'combatStatus', 'ntallyInventory', 'kollectionBuildScore', 'kollectionNetWorth', 'ewatchCoins', 'ewatchMarket', 'skillBooks', 'treasure'],
sizes: {
battleTimer: { width: 400, height: 30 },
combatRevenue: { width: 400, height: 70 },
consumables: { width: 400, height: 70 },
experiencePerHour: { width: 400, height: 30 },
totalProfit: { width: 400, height: 30 },
dps: { width: 400, height: 70 },
overExpected: { width: null, height: 70 },
luck: { width: null, height: 70 },
deathsPerHour: { width: 400, height: 30 },
houses: { width: 400, height: 30 },
equipmentWatch: { width: 400, height: 40 },
combatStatus: { width: 400, height: 30 },
ntallyInventory: { width: 400, height: 30 },
kollectionBuildScore: { width: 400, height: 30 },
kollectionNetWorth: { width: 400, height: 30 },
ewatchCoins: { width: 400, height: 30 },
ewatchMarket: { width: 400, height: 30 },
skillBooks: { width: 400, height: 30 },
treasure: { width: 400, height: 30 }
},
positions: {}
};
let currentY = 20;
this.oPanelConfig.order.forEach(optionKey => {
this.oPanelConfig.positions[optionKey] = {
x: 40,
y: currentY
};
currentY += this.oPanelConfig.sizes[optionKey].height + 10;
});
}
} else {
this.oPanelConfig = {
battleTimer: true,
combatRevenue: true,
consumables: true,
experiencePerHour: true,
totalProfit: true,
dps: true,
overExpected: true,
luck: true,
deathsPerHour: true,
houses: true,
equipmentWatch: true,
combatStatus: true,
ntallyInventory: true,
kollectionBuildScore: true,
kollectionNetWorth: true,
ewatchCoins: true,
ewatchMarket: true,
skillBooks: true,
treasure: true,
overExpectedOnlyNumbers: true,
overExpectedOnlyPlayer: false,
luckOnlyNumbers: true,
luckOnlyPlayer: false,
usePlayerNameRecolor: true,
snapToGrid: true,
ewatchShowBar: true,
order: ['battleTimer', 'combatRevenue', 'consumables', 'experiencePerHour', 'totalProfit', 'dps', 'overExpected', 'luck', 'deathsPerHour', 'houses', 'equipmentWatch', 'combatStatus', 'ntallyInventory', 'kollectionBuildScore', 'kollectionNetWorth', 'ewatchCoins', 'ewatchMarket', 'skillBooks', 'treasure'],
firstLoad: false,
sizes: {
battleTimer: { width: 400, height: 30 },
combatRevenue: { width: 280, height: 70 },
consumables: { width: 160, height: 80 },
experiencePerHour: { width: 90, height: 30 },
totalProfit: { width: 280, height: 40 },
dps: { width: 140, height: 80 },
overExpected: { width: 50, height: 60 },
luck: { width: 80, height: 70 },
deathsPerHour: { width: 90, height: 30 },
houses: { width: 160, height: 50 },
equipmentWatch: { width: 280, height: 50 },
combatStatus: { width: 160, height: 30 },
ntallyInventory: { width: 160, height: 30 },
kollectionBuildScore: { width: 160, height: 30 },
kollectionNetWorth: { width: 160, height: 30 },
ewatchCoins: { width: 160, height: 30 },
ewatchMarket: { width: 160, height: 30 },
skillBooks: { width: 160, height: 30 },
treasure: { width: 160, height: 30 }
},
positions: {
battleTimer: { x: 10, y: 10 },
combatRevenue: { x: 10, y: 50 },
consumables: { x: 310, y: 90 },
experiencePerHour: { x: 150, y: 10 },
totalProfit: { x: 10, y: 100 },
dps: { x: 10, y: 180 },
overExpected: { x: 160, y: 180 },
luck: { x: 210, y: 180 },
deathsPerHour: { x: 240, y: 10 },
houses: { x: 310, y: 40 },
equipmentWatch: { x: 10, y: 130 },
combatStatus: { x: 310, y: 10 },
ntallyInventory: { x: 310, y: 170 },
kollectionBuildScore: { x: 10, y: 260 },
kollectionNetWorth: { x: 130, y: 260 },
ewatchCoins: { x: 310, y: 190 },
ewatchMarket: { x: 310, y: 210 },
skillBooks: { x: 310, y: 230 },
treasure: { x: 310, y: 260 }
}
};
this.opStorage.set('is_locked', true);
this.opStorage.set('position', { top: 0, left: 0 });
this.opStorage.set('size', { width: 527, height: 296 });
this.opStorage.set('zoom_levels', {
dps: 110, overExpected: 110, luck: 110,
combatStatus: 100, equipmentWatch: 130, ewatchCoins: 110,
houses: 100, consumables: 100, deathsPerHour: 90,
skillBooks: 100
});
}
} else if (!this.oPanelConfig.order) {
this.oPanelConfig.order = ['battleTimer', 'combatRevenue', 'consumables', 'experiencePerHour', 'totalProfit', 'dps', 'deathsPerHour', 'houses', 'equipmentWatch', 'ntallyInventory', 'kollectionBuildScore', 'kollectionNetWorth', 'ewatchCoins', 'ewatchMarket'];
}
if (this.oPanelConfig.experiencePerHour === undefined) {
this.oPanelConfig.experiencePerHour = false;
}
if (this.oPanelConfig.totalProfit === undefined) {
this.oPanelConfig.totalProfit = false;
}
if (this.oPanelConfig.dps === undefined) {
this.oPanelConfig.dps = false;
}
if (this.oPanelConfig.deathsPerHour === undefined) {
this.oPanelConfig.deathsPerHour = false;
}
if (this.oPanelConfig.houses === undefined) {
this.oPanelConfig.houses = false;
}
if (this.oPanelConfig.equipmentWatch === undefined) {
this.oPanelConfig.equipmentWatch = false;
}
if (this.oPanelConfig.ntallyInventory === undefined) {
this.oPanelConfig.ntallyInventory = false;
}
if (this.oPanelConfig.kollectionBuildScore === undefined) {
this.oPanelConfig.kollectionBuildScore = false;
}
if (this.oPanelConfig.kollectionNetWorth === undefined) {
this.oPanelConfig.kollectionNetWorth = false;
}
if (this.oPanelConfig.ewatchCoins === undefined) {
this.oPanelConfig.ewatchCoins = false;
}
if (this.oPanelConfig.ewatchMarket === undefined) {
this.oPanelConfig.ewatchMarket = false;
}
if (this.oPanelConfig.skillBooks === undefined) {
this.oPanelConfig.skillBooks = false;
}
if (this.oPanelConfig.treasure === undefined) {
this.oPanelConfig.treasure = false;
}
if (this.oPanelConfig.snapToGrid === undefined) {
this.oPanelConfig.snapToGrid = true;
}
if (this.oPanelConfig.ewatchShowBar === undefined) {
this.oPanelConfig.ewatchShowBar = true;
}
if (!this.oPanelConfig.sizes) {
this.oPanelConfig.sizes = {
battleTimer: { width: null, height: 40 },
combatRevenue: { width: null, height: 150 },
consumables: { width: null, height: 80 },
experiencePerHour: { width: null, height: 40 },
totalProfit: { width: null, height: 40 },
dps: { width: null, height: 150 },
deathsPerHour: { width: null, height: 40 },
houses: { width: null, height: 40 },
equipmentWatch: { width: null, height: 80 },
ntallyInventory: { width: null, height: 40 },
kollectionBuildScore: { width: null, height: 40 },
kollectionNetWorth: { width: null, height: 40 },
ewatchCoins: { width: null, height: 40 },
ewatchMarket: { width: null, height: 40 },
skillBooks: { width: null, height: 40 },
treasure: { width: null, height: 40 }
};
} else {
if (!this.oPanelConfig.sizes.experiencePerHour) {
this.oPanelConfig.sizes.experiencePerHour = { width: null, height: 40 };
}
if (!this.oPanelConfig.sizes.totalProfit) {
this.oPanelConfig.sizes.totalProfit = { width: null, height: 40 };
}
if (!this.oPanelConfig.sizes.dps) {
this.oPanelConfig.sizes.dps = { width: null, height: 150 };
}
if (!this.oPanelConfig.sizes.deathsPerHour) {
this.oPanelConfig.sizes.deathsPerHour = { width: null, height: 40 };
}
if (!this.oPanelConfig.sizes.houses) {
this.oPanelConfig.sizes.houses = { width: null, height: 40 };
}
if (!this.oPanelConfig.sizes.equipmentWatch) {
this.oPanelConfig.sizes.equipmentWatch = { width: null, height: 80 };
}
if (!this.oPanelConfig.sizes.ntallyInventory) {
this.oPanelConfig.sizes.ntallyInventory = { width: null, height: 40 };
}
if (!this.oPanelConfig.sizes.kollectionBuildScore) {
this.oPanelConfig.sizes.kollectionBuildScore = { width: null, height: 40 };
}
if (!this.oPanelConfig.sizes.kollectionNetWorth) {
this.oPanelConfig.sizes.kollectionNetWorth = { width: null, height: 40 };
}
if (!this.oPanelConfig.sizes.ewatchCoins) {
this.oPanelConfig.sizes.ewatchCoins = { width: null, height: 40 };
}
if (!this.oPanelConfig.sizes.ewatchMarket) {
this.oPanelConfig.sizes.ewatchMarket = { width: null, height: 40 };
}
if (!this.oPanelConfig.sizes.skillBooks) {
this.oPanelConfig.sizes.skillBooks = { width: null, height: 40 };
}
if (!this.oPanelConfig.sizes.treasure) {
this.oPanelConfig.sizes.treasure = { width: null, height: 40 };
}
}
if (!this.oPanelConfig.positions) {
this.oPanelConfig.positions = {
battleTimer: { x: 0, y: 0 },
combatRevenue: { x: 0, y: 50 },
consumables: { x: 0, y: 210 },
experiencePerHour: { x: 0, y: 300 },
totalProfit: { x: 0, y: 350 },
dps: { x: 0, y: 400 },
deathsPerHour: { x: 0, y: 560 },
houses: { x: 0, y: 610 },
equipmentWatch: { x: 0, y: 660 },
ntallyInventory: { x: 0, y: 710 },
kollectionBuildScore: { x: 0, y: 760 },
kollectionNetWorth: { x: 0, y: 810 },
ewatchCoins: { x: 0, y: 860 },
ewatchMarket: { x: 0, y: 910 },
skillBooks: { x: 0, y: 960 },
treasure: { x: 0, y: 0 }
};
} else {
if (!this.oPanelConfig.positions.experiencePerHour) {
this.oPanelConfig.positions.experiencePerHour = { x: 0, y: 300 };
}
if (!this.oPanelConfig.positions.totalProfit) {
this.oPanelConfig.positions.totalProfit = { x: 0, y: 350 };
}
if (!this.oPanelConfig.positions.dps) {
this.oPanelConfig.positions.dps = { x: 0, y: 400 };
}
if (!this.oPanelConfig.positions.deathsPerHour) {
this.oPanelConfig.positions.deathsPerHour = { x: 0, y: 560 };
}
if (!this.oPanelConfig.positions.houses) {
this.oPanelConfig.positions.houses = { x: 0, y: 610 };
}
if (!this.oPanelConfig.positions.equipmentWatch) {
this.oPanelConfig.positions.equipmentWatch = { x: 0, y: 660 };
}
if (!this.oPanelConfig.positions.ntallyInventory) {
this.oPanelConfig.positions.ntallyInventory = { x: 0, y: 710 };
}
if (!this.oPanelConfig.positions.kollectionBuildScore) {
this.oPanelConfig.positions.kollectionBuildScore = { x: 0, y: 760 };
}
if (!this.oPanelConfig.positions.kollectionNetWorth) {
this.oPanelConfig.positions.kollectionNetWorth = { x: 0, y: 810 };
}
if (!this.oPanelConfig.positions.skillBooks) {
this.oPanelConfig.positions.skillBooks = { x: 0, y: 960 };
}
if (!this.oPanelConfig.positions.treasure) {
this.oPanelConfig.positions.treasure = { x: 0, y: 0 };
}
}
if (this.oPanelConfig.hasOwnProperty('option2')) {
this.oPanelConfig.combatRevenue = this.oPanelConfig.option2;
delete this.oPanelConfig.option2;
const idx = this.oPanelConfig.order.indexOf('option2');
if (idx !== -1) {
this.oPanelConfig.order[idx] = 'combatRevenue';
}
window.lootDropsTrackerInstance.opStorage.set('config', this.oPanelConfig);
}
if (this.oPanelConfig.hasOwnProperty('option3')) {
this.oPanelConfig.consumables = this.oPanelConfig.option3;
delete this.oPanelConfig.option3;
const idx = this.oPanelConfig.order.indexOf('option3');
if (idx !== -1) {
this.oPanelConfig.order[idx] = 'consumables';
}
window.lootDropsTrackerInstance.opStorage.set('config', this.oPanelConfig);
}
const pane = document.createElement('div');
pane.id = 'opanel-pane';
registerPanel('opanel-pane');
this.applyClass(pane, 'mcs-opanel-pane');
const iconsBackground = document.createElement('div');
iconsBackground.id = 'opanel-icons-background';
this.applyClass(iconsBackground, 'mcs-opanel-icons-bg');
const computerIcon = document.createElement('button');
computerIcon.id = 'opanel-computer-icon';
computerIcon.innerHTML = '💻';
this.applyClasses(computerIcon, 'mcs-opanel-icon', 'mcs-opanel-computer-icon');
computerIcon.onclick = (e) => {
e.stopPropagation();
this.toggleOPanelImportExport();
};
const lockIcon = document.createElement('button');
lockIcon.id = 'opanel-lock-icon';
lockIcon.innerHTML = '🔓';
this.applyClasses(lockIcon, 'mcs-opanel-icon', 'mcs-opanel-lock-icon');
lockIcon.onclick = (e) => {
e.stopPropagation();
this.toggleOPanelLock();
};
const gearIcon = document.createElement('button');
gearIcon.id = 'opanel-gear-icon';
gearIcon.innerHTML = '⚙';
this.applyClasses(gearIcon, 'mcs-opanel-icon', 'mcs-opanel-gear-icon');
gearIcon.onclick = (e) => {
e.stopPropagation();
this.toggleOPanelConfigMenu();
};
const dragHandle = document.createElement('div');
dragHandle.id = 'opanel-drag-handle';
dragHandle.innerHTML = '✥';
this.applyClasses(dragHandle, 'mcs-opanel-icon', 'mcs-opanel-drag-handle');
const configMenu = document.createElement('div');
configMenu.id = 'opanel-config-menu';
this.applyClass(configMenu, 'mcs-opanel-config-menu');
const closeButton = document.createElement('div');
closeButton.textContent = '×';
this.applyClass(closeButton, 'mcs-opanel-close-btn');
closeButton.onclick = () => {
configMenu.style.display = 'none';
};
configMenu.appendChild(closeButton);
const optionsContainer = document.createElement('div');
optionsContainer.id = 'opanel-options-container';
this.applyClass(optionsContainer, 'mcs-opanel-options-container');
configMenu.appendChild(optionsContainer);
this.oPanelConfig.order.forEach((optionKey) => {
const checkboxContainer = document.createElement('div');
this.applyClass(checkboxContainer, 'mcs-opanel-checkbox-container');
const checkbox = document.createElement('input');
checkbox.type = 'checkbox';
checkbox.id = `opanel-checkbox-${optionKey}`;
checkbox.checked = this.oPanelConfig[optionKey];
this.applyClass(checkbox, 'mcs-opanel-checkbox');
checkbox.onchange = () => {
this.oPanelConfig[optionKey] = checkbox.checked;
window.lootDropsTrackerInstance.opStorage.set('config', this.oPanelConfig);
this.toggleOPanelOption(optionKey, checkbox.checked);
};
const label = document.createElement('label');
label.htmlFor = `opanel-checkbox-${optionKey}`;
const optionNames = {
battleTimer: 'Session Timer / EPH',
combatRevenue: 'Combat Revenue',
consumables: 'Consumables',
experiencePerHour: 'Experience/hr',
totalProfit: 'Total Profit',
dps: 'DPS',
overExpected: 'Over Expected %',
luck: 'Luck',
deathsPerHour: 'Deaths/hr',
houses: 'Houses',
equipmentWatch: 'Equipment Watch',
combatStatus: 'Combat Status',
usePlayerNameRecolor: 'Use Player Name Recolor',
ntallyInventory: 'NTally Inventory',
kollectionBuildScore: 'Build Score',
kollectionNetWorth: 'Net Worth',
ewatchCoins: 'EWatch Coins',
ewatchMarket: 'EWatch Market',
skillBooks: 'Skill Books',
treasure: 'Treasure'
};
label.textContent = optionNames[optionKey];
this.applyClass(label, 'mcs-opanel-label');
checkboxContainer.appendChild(checkbox);
checkboxContainer.appendChild(label);
optionsContainer.appendChild(checkboxContainer);
});
const snapToggleContainer = document.createElement('div');
this.applyClass(snapToggleContainer, 'mcs-opanel-checkbox-container');
const snapCheckbox = document.createElement('input');
snapCheckbox.type = 'checkbox';
snapCheckbox.id = 'opanel-snap-to-grid-checkbox';
snapCheckbox.checked = this.oPanelConfig.snapToGrid !== false;
this.applyClass(snapCheckbox, 'mcs-opanel-checkbox');
snapCheckbox.onchange = () => {
this.oPanelConfig.snapToGrid = snapCheckbox.checked;
window.lootDropsTrackerInstance.opStorage.set('config', this.oPanelConfig);
};
const snapLabel = document.createElement('label');
snapLabel.htmlFor = 'opanel-snap-to-grid-checkbox';
snapLabel.textContent = 'Snap to Grid';
this.applyClass(snapLabel, 'mcs-opanel-label');
snapToggleContainer.appendChild(snapCheckbox);
snapToggleContainer.appendChild(snapLabel);
optionsContainer.appendChild(snapToggleContainer);
const recolorToggleContainer = document.createElement('div');
this.applyClass(recolorToggleContainer, 'mcs-opanel-checkbox-container');
const recolorCheckbox = document.createElement('input');
recolorCheckbox.type = 'checkbox';
recolorCheckbox.id = 'opanel-player-name-recolor-checkbox';
recolorCheckbox.checked = this.oPanelConfig.usePlayerNameRecolor || false;
this.applyClass(recolorCheckbox, 'mcs-opanel-checkbox');
recolorCheckbox.onchange = () => {
this.oPanelConfig.usePlayerNameRecolor = recolorCheckbox.checked;
window.lootDropsTrackerInstance.opStorage.set('config', this.oPanelConfig);
this.updateOPanelDPS();
this.updateOPanelCombatRevenue();
this.updateOPanelOverExpected();
this.updateOPanelLuck();
};
const recolorLabel = document.createElement('label');
recolorLabel.htmlFor = 'opanel-player-name-recolor-checkbox';
recolorLabel.textContent = 'Use Player Name Recolor';
this.applyClass(recolorLabel, 'mcs-opanel-label');
recolorToggleContainer.appendChild(recolorCheckbox);
recolorToggleContainer.appendChild(recolorLabel);
optionsContainer.appendChild(recolorToggleContainer);
const onlyPlayerContainer = document.createElement('div');
this.applyClass(onlyPlayerContainer, 'mcs-opanel-checkbox-container');
const onlyPlayerCheckbox = document.createElement('input');
onlyPlayerCheckbox.type = 'checkbox';
onlyPlayerCheckbox.id = 'opanel-expected-only-player-checkbox';
onlyPlayerCheckbox.checked = this.oPanelConfig.overExpectedOnlyPlayer || false;
this.applyClass(onlyPlayerCheckbox, 'mcs-opanel-checkbox');
onlyPlayerCheckbox.onchange = () => {
this.oPanelConfig.overExpectedOnlyPlayer = onlyPlayerCheckbox.checked;
window.lootDropsTrackerInstance.opStorage.set('config', this.oPanelConfig);
this.updateOPanelOverExpected();
};
const onlyPlayerLabel = document.createElement('label');
onlyPlayerLabel.htmlFor = 'opanel-expected-only-player-checkbox';
onlyPlayerLabel.textContent = 'Expected: Only Player';
this.applyClass(onlyPlayerLabel, 'mcs-opanel-label');
onlyPlayerContainer.appendChild(onlyPlayerCheckbox);
onlyPlayerContainer.appendChild(onlyPlayerLabel);
optionsContainer.appendChild(onlyPlayerContainer);
const onlyNumbersContainer = document.createElement('div');
this.applyClass(onlyNumbersContainer, 'mcs-opanel-checkbox-container');
const onlyNumbersCheckbox = document.createElement('input');
onlyNumbersCheckbox.type = 'checkbox';
onlyNumbersCheckbox.id = 'opanel-expected-only-numbers-checkbox';
onlyNumbersCheckbox.checked = this.oPanelConfig.overExpectedOnlyNumbers || false;
this.applyClass(onlyNumbersCheckbox, 'mcs-opanel-checkbox');
onlyNumbersCheckbox.onchange = () => {
this.oPanelConfig.overExpectedOnlyNumbers = onlyNumbersCheckbox.checked;
window.lootDropsTrackerInstance.opStorage.set('config', this.oPanelConfig);
this.updateOPanelOverExpected();
};
const onlyNumbersLabel = document.createElement('label');
onlyNumbersLabel.htmlFor = 'opanel-expected-only-numbers-checkbox';
onlyNumbersLabel.textContent = 'Expected: Only Numbers';
this.applyClass(onlyNumbersLabel, 'mcs-opanel-label');
onlyNumbersContainer.appendChild(onlyNumbersCheckbox);
onlyNumbersContainer.appendChild(onlyNumbersLabel);
optionsContainer.appendChild(onlyNumbersContainer);
const ewatchBarContainer = document.createElement('div');
this.applyClass(ewatchBarContainer, 'mcs-opanel-checkbox-container');
const ewatchBarCheckbox = document.createElement('input');
ewatchBarCheckbox.type = 'checkbox';
ewatchBarCheckbox.id = 'opanel-ewatch-bar-checkbox';
ewatchBarCheckbox.checked = this.oPanelConfig.ewatchShowBar !== false;
this.applyClass(ewatchBarCheckbox, 'mcs-opanel-checkbox');
ewatchBarCheckbox.onchange = () => {
this.oPanelConfig.ewatchShowBar = ewatchBarCheckbox.checked;
window.lootDropsTrackerInstance.opStorage.set('config', this.oPanelConfig);
this.updateOPanelEWatchLayout();
};
const ewatchBarLabel = document.createElement('label');
ewatchBarLabel.htmlFor = 'opanel-ewatch-bar-checkbox';
ewatchBarLabel.textContent = 'Equipment Watch: Bar';
this.applyClass(ewatchBarLabel, 'mcs-opanel-label');
ewatchBarContainer.appendChild(ewatchBarCheckbox);
ewatchBarContainer.appendChild(ewatchBarLabel);
optionsContainer.appendChild(ewatchBarContainer);
const luckOnlyPlayerContainer = document.createElement('div');
this.applyClass(luckOnlyPlayerContainer, 'mcs-opanel-checkbox-container');
const luckOnlyPlayerCheckbox = document.createElement('input');
luckOnlyPlayerCheckbox.type = 'checkbox';
luckOnlyPlayerCheckbox.id = 'opanel-luck-only-player-checkbox';
luckOnlyPlayerCheckbox.checked = this.oPanelConfig.luckOnlyPlayer || false;
this.applyClass(luckOnlyPlayerCheckbox, 'mcs-opanel-checkbox');
luckOnlyPlayerCheckbox.onchange = () => {
this.oPanelConfig.luckOnlyPlayer = luckOnlyPlayerCheckbox.checked;
window.lootDropsTrackerInstance.opStorage.set('config', this.oPanelConfig);
this.updateOPanelLuck();
};
const luckOnlyPlayerLabel = document.createElement('label');
luckOnlyPlayerLabel.htmlFor = 'opanel-luck-only-player-checkbox';
luckOnlyPlayerLabel.textContent = 'Luck: Only Player';
this.applyClass(luckOnlyPlayerLabel, 'mcs-opanel-label');
luckOnlyPlayerContainer.appendChild(luckOnlyPlayerCheckbox);
luckOnlyPlayerContainer.appendChild(luckOnlyPlayerLabel);
optionsContainer.appendChild(luckOnlyPlayerContainer);
const luckOnlyNumbersContainer = document.createElement('div');
this.applyClass(luckOnlyNumbersContainer, 'mcs-opanel-checkbox-container');
const luckOnlyNumbersCheckbox = document.createElement('input');
luckOnlyNumbersCheckbox.type = 'checkbox';
luckOnlyNumbersCheckbox.id = 'opanel-luck-only-numbers-checkbox';
luckOnlyNumbersCheckbox.checked = this.oPanelConfig.luckOnlyNumbers || false;
this.applyClass(luckOnlyNumbersCheckbox, 'mcs-opanel-checkbox');
luckOnlyNumbersCheckbox.onchange = () => {
this.oPanelConfig.luckOnlyNumbers = luckOnlyNumbersCheckbox.checked;
window.lootDropsTrackerInstance.opStorage.set('config', this.oPanelConfig);
this.updateOPanelLuck();
};
const luckOnlyNumbersLabel = document.createElement('label');
luckOnlyNumbersLabel.htmlFor = 'opanel-luck-only-numbers-checkbox';
luckOnlyNumbersLabel.textContent = 'Luck: Only Numbers';
this.applyClass(luckOnlyNumbersLabel, 'mcs-opanel-label');
luckOnlyNumbersContainer.appendChild(luckOnlyNumbersCheckbox);
luckOnlyNumbersContainer.appendChild(luckOnlyNumbersLabel);
optionsContainer.appendChild(luckOnlyNumbersContainer);
const autogridContainer = document.createElement('div');
this.applyClass(autogridContainer, 'mcs-opanel-action-btn-container');
const autogridButton = document.createElement('button');
autogridButton.textContent = 'Autogrid';
this.applyClass(autogridButton, 'mcs-opanel-action-btn');
autogridButton.onclick = () => {
this.autogridOPanelPanels();
};
autogridContainer.appendChild(autogridButton);
optionsContainer.appendChild(autogridContainer);
const resetContainer = document.createElement('div');
this.applyClass(resetContainer, 'mcs-opanel-reset-btn-container');
const resetButton = document.createElement('button');
resetButton.textContent = 'Reset';
this.applyClass(resetButton, 'mcs-opanel-reset-btn');
resetButton.onclick = () => {
this.resetOPanelToDefaults();
};
resetContainer.appendChild(resetButton);
optionsContainer.appendChild(resetContainer);
pane.appendChild(configMenu);
const content = document.createElement('div');
content.id = 'opanel-content';
this.applyClass(content, 'mcs-opanel-content');
const contentGrid = document.createElement('div');
contentGrid.id = 'opanel-content-grid';
this.applyClass(contentGrid, 'mcs-opanel-content-grid');
content.appendChild(contentGrid);
const gridSizeShim = document.createElement('div');
gridSizeShim.id = 'opanel-grid-size-shim';
this.applyClass(gridSizeShim, 'mcs-opanel-grid-shim');
contentGrid.appendChild(gridSizeShim);
const gridOverlay = document.createElement('div');
gridOverlay.id = 'opanel-grid-overlay';
this.applyClass(gridOverlay, 'mcs-opanel-grid-overlay');
contentGrid.appendChild(gridOverlay);
const spacerWrapper = document.createElement('div');
spacerWrapper.id = 'opanel-spacer-wrapper';
this.applyClass(spacerWrapper, 'mcs-opanel-spacer-wrapper');
contentGrid.appendChild(spacerWrapper);
for (let i = 0; i < 10; i++) {
const spacerBox = document.createElement('div');
spacerBox.className = 'opanel-spacer-box';
this.applyClass(spacerBox, 'mcs-opanel-spacer-box');
spacerBox.style.top = `${i * 100}px`;
const spacerContent = document.createElement('div');
this.applyClass(spacerContent, 'mcs-opanel-spacer-content');
spacerContent.textContent = '1\n2\n3\n4\n5\n6\n7\n8\n9\n10';
spacerBox.appendChild(spacerContent);
spacerWrapper.appendChild(spacerBox);
}
const heightForcer = document.createElement('div');
heightForcer.id = 'opanel-height-forcer';
this.applyClass(heightForcer, 'mcs-opanel-height-forcer');
contentGrid.appendChild(heightForcer);
pane.appendChild(content);
pane.appendChild(iconsBackground);
pane.appendChild(computerIcon);
pane.appendChild(lockIcon);
pane.appendChild(gearIcon);
pane.appendChild(dragHandle);
document.body.appendChild(pane);
const corners = [
{ name: 'nw', cursor: 'nw-resize', position: 'top: 0; left: 0;' },
{ name: 'ne', cursor: 'ne-resize', position: 'top: 0; right: 0;' },
{ name: 'sw', cursor: 'sw-resize', position: 'bottom: 0; left: 0;' },
{ name: 'se', cursor: 'se-resize', position: 'bottom: 0; right: 0;' }
];
corners.forEach(corner => {
const handle = document.createElement('div');
handle.className = `opanel-resize-handle opanel-resize-${corner.name}`;
handle.setAttribute('data-corner', corner.name);
this.applyClass(handle, 'mcs-opanel-corner-handle');
handle.style.cursor = corner.cursor;
const positions = corner.position.split(';').map(s => s.trim()).filter(s => s);
positions.forEach(pos => {
const [prop, val] = pos.split(':').map(s => s.trim());
handle.style[prop] = val;
});
pane.appendChild(handle);
this.makeOPanelCornerResizable(pane, handle, corner.name);
});
this.makeOPanelDraggable(pane, dragHandle, 'mcs_OP');
const savedPos = this.opStorage.get('position');
if (savedPos) {
const pos = typeof savedPos === 'string' ? JSON.parse(savedPos) : savedPos;
pane.style.top = pos.top + 'px';
pane.style.left = pos.left + 'px';
}
const savedSize = this.opStorage.get('size');
if (savedSize) {
const size = typeof savedSize === 'string' ? JSON.parse(savedSize) : savedSize;
pane.style.width = size.width + 'px';
pane.style.height = size.height + 'px';
}
const lockedVal = this.opStorage.get('is_locked');
this.oPanelIsLocked = lockedVal === true || lockedVal === 'true';
if (this.oPanelIsLocked) {
lockIcon.innerHTML = '🔒';
}
if (this.oPanelMainResizeObserver) {
this.oPanelMainResizeObserver.disconnect();
}
this.oPanelMainResizeObserver = new ResizeObserver(() => {
const rect = pane.getBoundingClientRect();
const sizeData = {
width: rect.width,
height: rect.height
};
window.lootDropsTrackerInstance.opStorage.set('size', sizeData);
this.updateOPanelLayout();
this.updateOPanelConfigMenuPosition();
});
this.oPanelMainResizeObserver.observe(pane);
this.updateOPanelContent();
this.updateOPanelConfigMenuPosition();
pane.addEventListener('mouseenter', () => {
iconsBackground.style.opacity = '1';
dragHandle.style.opacity = '1';
gearIcon.style.opacity = '1';
lockIcon.style.opacity = '1';
computerIcon.style.opacity = '1';
});
pane.addEventListener('mouseleave', () => {
iconsBackground.style.opacity = '0';
dragHandle.style.opacity = '0';
gearIcon.style.opacity = '0';
lockIcon.style.opacity = '0';
computerIcon.style.opacity = '0';
});
}
toggleOPanelConfigMenu() {
const menu = document.getElementById('opanel-config-menu');
const pane = document.getElementById('opanel-pane');
if (menu && pane) {
if (menu.style.display !== 'block') {
menu.style.display = 'block';
requestAnimationFrame(() => {
this.updateOPanelConfigMenuPosition();
});
} else {
menu.style.display = 'none';
}
}
}
toggleOPanelImportExport() {
let panel = document.getElementById('opanel-import-export-panel');
if (!panel) {
panel = document.createElement('div');
panel.id = 'opanel-import-export-panel';
this.applyClass(panel, 'mcs-opanel-import-export-panel');
const title = document.createElement('span');
title.textContent = 'Config:';
this.applyClass(title, 'mcs-opanel-ie-title');
panel.appendChild(title);
const exportBtn = document.createElement('button');
exportBtn.textContent = 'Export';
this.applyClass(exportBtn, 'mcs-opanel-ie-btn');
exportBtn.onclick = () => this.exportOPanelConfig();
panel.appendChild(exportBtn);
const importBtn = document.createElement('button');
importBtn.textContent = 'Import';
this.applyClass(importBtn, 'mcs-opanel-ie-btn');
importBtn.onclick = () => this.importOPanelConfig();
panel.appendChild(importBtn);
document.body.appendChild(panel);
}
if (panel.style.display !== 'flex') {
panel.style.display = 'flex';
const pane = document.getElementById('opanel-pane');
if (pane) {
const paneRect = pane.getBoundingClientRect();
panel.style.left = paneRect.left + 'px';
panel.style.width = paneRect.width + 'px';
requestAnimationFrame(() => {
panel.style.top = (paneRect.top - panel.offsetHeight - 4) + 'px';
});
}
} else {
panel.style.display = 'none';
}
}
exportOPanelConfig() {
const exportData = {
config: this.oPanelConfig,
is_locked: this.oPanelIsLocked || false,
position: this.opStorage.get('position') || { top: 0, left: 0 },
size: this.opStorage.get('size') || { width: 527, height: 296 },
zoom_levels: this.oPanelZoomLevels || this.opStorage.get('zoom_levels') || {}
};
const json = JSON.stringify(exportData, null, 2);
const blob = new Blob([json], { type: 'application/json' });
const url = URL.createObjectURL(blob);
const playerName = (CharacterDataStorage.getCurrentCharacterName() || 'unknown').replace(/\s+/g, '-');
const a = document.createElement('a');
a.href = url;
a.download = `${playerName}-opanel-config.json`;
a.click();
URL.revokeObjectURL(url);
}
importOPanelConfig() {
const input = document.createElement('input');
input.type = 'file';
input.accept = '.json';
input.onchange = (e) => {
const file = e.target.files[0];
if (!file) return;
const reader = new FileReader();
reader.onload = (evt) => {
try {
const importData = JSON.parse(evt.target.result);
if (!importData.config || !importData.config.order) {
alert('Invalid OPanel configuration file.');
return;
}
if (!confirm('This will overwrite your current OPanel configuration. Continue?')) {
return;
}
this.oPanelConfig = importData.config;
this.opStorage.set('config', this.oPanelConfig);
if (importData.is_locked !== undefined) {
this.oPanelIsLocked = importData.is_locked;
this.opStorage.set('is_locked', this.oPanelIsLocked);
const lockIcon = document.getElementById('opanel-lock-icon');
if (lockIcon) {
lockIcon.innerHTML = this.oPanelIsLocked ? '🔒' : '🔓';
}
}
if (importData.position) {
this.opStorage.set('position', importData.position);
const pane = document.getElementById('opanel-pane');
if (pane) {
pane.style.top = importData.position.top + 'px';
pane.style.left = importData.position.left + 'px';
}
}
if (importData.size) {
this.opStorage.set('size', importData.size);
const pane = document.getElementById('opanel-pane');
if (pane) {
pane.style.width = importData.size.width + 'px';
pane.style.height = importData.size.height + 'px';
}
}
if (importData.zoom_levels) {
this.oPanelZoomLevels = importData.zoom_levels;
this.opStorage.set('zoom_levels', this.oPanelZoomLevels);
}
this.updateOPanelContent(true);
const panel = document.getElementById('opanel-import-export-panel');
if (panel) panel.style.display = 'none';
} catch (err) {
alert('Failed to parse configuration file: ' + err.message);
}
};
reader.readAsText(file);
};
input.click();
}
updateOPanelConfigMenuPosition() {
const menu = document.getElementById('opanel-config-menu');
const pane = document.getElementById('opanel-pane');
if (menu && pane) {
void pane.offsetHeight;
const paneRect = pane.getBoundingClientRect();
const originalDisplay = menu.style.display;
menu.style.display = 'block';
menu.style.visibility = 'hidden';
const menuHeight = menu.offsetHeight;
menu.style.visibility = 'visible';
menu.style.display = originalDisplay;
menu.style.left = paneRect.left + 'px';
menu.style.top = (paneRect.top - menuHeight - 4) + 'px';
menu.style.width = paneRect.width + 'px';
menu.style.boxSizing = 'border-box';
}
}
toggleOPanelLock() {
if (this.oPanelIsLocked === undefined) {
const lockedVal = this.opStorage.get('is_locked');
this.oPanelIsLocked = lockedVal === true || lockedVal === 'true';
}
this.oPanelIsLocked = !this.oPanelIsLocked;
this.opStorage.set('is_locked', this.oPanelIsLocked);
const lockIcon = document.getElementById('opanel-lock-icon');
if (lockIcon) {
lockIcon.innerHTML = this.oPanelIsLocked ? '🔒' : '🔓';
}
const contentGrid = document.getElementById('opanel-content-grid');
if (contentGrid) {
const boxes = contentGrid.querySelectorAll('.opanel-option-box');
boxes.forEach(box => {
box.style.cursor = this.oPanelIsLocked ? 'default' : 'move';
const resizeHandle = box.querySelector('.opanel-box-resize-handle');
if (resizeHandle) {
resizeHandle.style.display = this.oPanelIsLocked ? 'none' : 'block';
}
const zoomControls = box.querySelector('.opanel-zoom-controls');
if (zoomControls) {
zoomControls.style.pointerEvents = this.oPanelIsLocked ? 'none' : 'auto';
}
});
}
}
adjustOPanelZoom(optionKey, delta) {
if (!this.oPanelZoomLevels) {
const saved = this.opStorage.get('zoom_levels');
this.oPanelZoomLevels = saved ?? {};
}
const currentZoom = this.oPanelZoomLevels[optionKey] || 100;
const newZoom = Math.max(50, Math.min(200, currentZoom + (delta * 10)));
this.oPanelZoomLevels[optionKey] = newZoom;
this.opStorage.set('zoom_levels', this.oPanelZoomLevels);
const displayIdMap = {
'battleTimer': 'opanel-battle-timer',
'combatRevenue': 'opanel-combat-revenue',
'consumables': 'opanel-consumables',
'experiencePerHour': 'opanel-experience-hr',
'totalProfit': 'opanel-total-profit',
'dps': 'opanel-dps',
'overExpected': 'opanel-over-expected',
'luck': 'opanel-luck',
'deathsPerHour': 'opanel-deaths-hr',
'houses': 'opanel-houses',
'equipmentWatch': 'opanel-ewatch',
'combatStatus': 'opanel-combat-status',
'ntallyInventory': 'opanel-ntally-inventory',
'kollectionBuildScore': 'opanel-kollection-build-score',
'kollectionNetWorth': 'opanel-kollection-net-worth',
'ewatchCoins': 'opanel-ewatch-coins',
'ewatchMarket': 'opanel-ewatch-market',
'skillBooks': 'opanel-skill-books',
'treasure': 'opanel-treasure'
};
const displayId = displayIdMap[optionKey];
if (displayId) {
const displayElement = document.getElementById(displayId);
if (displayElement) {
const zoomPercent = newZoom / 100;
displayElement.style.zoom = zoomPercent;
}
}
}
applyOPanelZoom(optionKey, displayElement) {
if (!this.oPanelZoomLevels) {
const saved = this.opStorage.get('zoom_levels');
this.oPanelZoomLevels = saved ?? {};
}
const zoomLevel = this.oPanelZoomLevels[optionKey];
if (zoomLevel && displayElement) {
const zoomPercent = zoomLevel / 100;
displayElement.style.zoom = zoomPercent;
}
}
toggleOPanelOption(optionKey, isEnabled) {
const contentGrid = document.getElementById('opanel-content-grid');
if (!contentGrid) {
return;
}
if (isEnabled) {
this.updateOPanelContent();
} else {
const existingBox = contentGrid.querySelector(`[data-option-key="${optionKey}"]`);
if (existingBox) {
const computedStyle = window.getComputedStyle(existingBox);
const width = parseFloat(computedStyle.width);
const height = parseFloat(computedStyle.height);
const leftValue = parseInt(existingBox.style.left);
const topValue = parseInt(existingBox.style.top);
const x = isNaN(leftValue) ? 0 : leftValue;
const y = isNaN(topValue) ? 0 : topValue;
if (!this.oPanelConfig.sizes) {
this.oPanelConfig.sizes = {};
}
if (!this.oPanelConfig.sizes[optionKey]) {
this.oPanelConfig.sizes[optionKey] = {};
}
this.oPanelConfig.sizes[optionKey].width = width;
this.oPanelConfig.sizes[optionKey].height = height;
if (!this.oPanelConfig.positions) {
this.oPanelConfig.positions = {};
}
if (!this.oPanelConfig.positions[optionKey]) {
this.oPanelConfig.positions[optionKey] = {};
}
this.oPanelConfig.positions[optionKey].x = x;
this.oPanelConfig.positions[optionKey].y = y;
window.lootDropsTrackerInstance.opStorage.set('config', this.oPanelConfig);
existingBox.remove();
}
}
}
updateOPanelContent(skipSaveCurrentState = false) {
const contentGrid = document.getElementById('opanel-content-grid');
if (!contentGrid) {
return;
}
if (this._opanelActiveDocListeners) {
this._opanelActiveDocListeners.forEach(l => {
document.removeEventListener('mousemove', l.move);
document.removeEventListener('mouseup', l.up);
});
this._opanelActiveDocListeners = [];
}
const existingBoxes = contentGrid.querySelectorAll('.opanel-option-box');
existingBoxes.forEach(box => {
const optionKey = box.dataset.optionKey;
if (optionKey && !skipSaveCurrentState) {
const computedStyle = window.getComputedStyle(box);
const width = parseFloat(computedStyle.width);
const height = parseFloat(computedStyle.height);
if (!this.oPanelConfig.sizes) {
this.oPanelConfig.sizes = {};
}
if (!this.oPanelConfig.sizes[optionKey]) {
this.oPanelConfig.sizes[optionKey] = {};
}
this.oPanelConfig.sizes[optionKey].width = width;
this.oPanelConfig.sizes[optionKey].height = height;
const leftValue = parseInt(box.style.left);
const topValue = parseInt(box.style.top);
const x = isNaN(leftValue) ? 0 : leftValue;
const y = isNaN(topValue) ? 0 : topValue;
if (!this.oPanelConfig.positions) {
this.oPanelConfig.positions = {};
}
if (!this.oPanelConfig.positions[optionKey]) {
this.oPanelConfig.positions[optionKey] = {};
}
this.oPanelConfig.positions[optionKey].x = x;
this.oPanelConfig.positions[optionKey].y = y;
}
box.remove();
});
window.lootDropsTrackerInstance.opStorage.set('config', this.oPanelConfig);
this.oPanelConfig.order.forEach((optionKey) => {
if (this.oPanelConfig[optionKey] === true) {
const optionBox = document.createElement('div');
optionBox.className = 'opanel-option-box';
optionBox.dataset.optionKey = optionKey;
const savedSize = this.oPanelConfig.sizes?.[optionKey] ?? {};
const savedPos = this.oPanelConfig.positions?.[optionKey] ?? {};
const defaultSizes = {
battleTimer: { width: 400, height: 30 },
combatRevenue: { width: 400, height: 70 },
consumables: { width: 400, height: 70 },
experiencePerHour: { width: 400, height: 30 },
totalProfit: { width: 400, height: 30 },
dps: { width: 400, height: 70 },
overExpected: { width: 400, height: 70 },
deathsPerHour: { width: 400, height: 30 },
houses: { width: 400, height: 30 },
equipmentWatch: { width: 400, height: 40 }
};
const defaultPositions = {
battleTimer: { x: 40, y: 20 },
combatRevenue: { x: 40, y: 60 },
consumables: { x: 40, y: 140 },
experiencePerHour: { x: 40, y: 220 },
totalProfit: { x: 40, y: 260 },
dps: { x: 40, y: 300 },
overExpected: { x: 40, y: 380 },
deathsPerHour: { x: 40, y: 460 },
houses: { x: 40, y: 500 },
equipmentWatch: { x: 40, y: 540 }
};
const defaultSize = defaultSizes[optionKey] || { width: 400, height: 30 };
const width = (savedSize.width !== null && savedSize.width !== undefined) ? savedSize.width : defaultSize.width;
const height = (savedSize.height !== null && savedSize.height !== undefined) ? savedSize.height : defaultSize.height;
const defaultPos = defaultPositions[optionKey] || { x: 0, y: 0 };
const x = savedPos.x !== undefined ? savedPos.x : defaultPos.x;
const y = savedPos.y !== undefined ? savedPos.y : defaultPos.y;
const baseStyles = `
position: absolute;
left: ${x}px;
top: ${y}px;
width: ${width}px;
height: ${height}px;
min-width: 50px;
min-height: 30px;
background: transparent;
box-sizing: border-box;
resize: none;
overflow: hidden;
cursor: move;
border: 1px solid transparent;
border-top: 1px solid #4a4a4a;
z-index: 1;
display: block;
visibility: visible;
transition: background 0.2s, border-color 0.2s;
`;
if (optionKey === 'battleTimer') {
optionBox.style.cssText = baseStyles + `
padding: 4px 0 0 0;
`;
const spacer = document.createElement('div');
spacer.className = 'opanel-spacer';
this.applyClass(spacer, 'mcs-opanel-spacer');
optionBox.appendChild(spacer);
const timerWrapper = document.createElement('div');
this.applyClass(timerWrapper, 'mcs-opanel-widget-wrapper');
const timerDisplay = document.createElement('div');
timerDisplay.id = 'opanel-battle-timer';
this.applyClass(timerDisplay, 'mcs-opanel-timer-display');
timerDisplay.textContent = '--:--:--';
timerWrapper.appendChild(timerDisplay);
optionBox.appendChild(timerWrapper);
this.applyOPanelZoom('battleTimer', timerDisplay);
} else if (optionKey === 'combatRevenue') {
optionBox.style.cssText = baseStyles + `
padding: 4px 0 0 0;
`;
const spacer = document.createElement('div');
spacer.className = 'opanel-spacer';
this.applyClass(spacer, 'mcs-opanel-spacer');
optionBox.appendChild(spacer);
const revenueWrapper = document.createElement('div');
this.applyClass(revenueWrapper, 'mcs-opanel-widget-wrapper');
const revenueDisplay = document.createElement('div');
revenueDisplay.id = 'opanel-combat-revenue';
this.applyClass(revenueDisplay, 'mcs-opanel-revenue-display');
revenueDisplay.innerHTML = '<div style="text-align: center; color: #888;">--</div>';
revenueWrapper.appendChild(revenueDisplay);
optionBox.appendChild(revenueWrapper);
this.applyOPanelZoom('combatRevenue', revenueDisplay);
} else if (optionKey === 'consumables') {
optionBox.style.cssText = baseStyles + `
padding: 4px 0 0 0;
`;
const spacer = document.createElement('div');
spacer.className = 'opanel-spacer';
this.applyClass(spacer, 'mcs-opanel-spacer');
optionBox.appendChild(spacer);
const consumablesWrapper = document.createElement('div');
this.applyClass(consumablesWrapper, 'mcs-opanel-widget-wrapper');
const consumablesDisplay = document.createElement('div');
consumablesDisplay.id = 'opanel-consumables';
this.applyClass(consumablesDisplay, 'mcs-opanel-consumables-display');
consumablesDisplay.innerHTML = '<div style="text-align: center; color: #888;">Loading...</div>';
consumablesWrapper.appendChild(consumablesDisplay);
optionBox.appendChild(consumablesWrapper);
this.applyOPanelZoom('consumables', consumablesDisplay);
} else if (optionKey === 'experiencePerHour') {
optionBox.style.cssText = baseStyles + `
padding: 4px 0 0 0;
`;
const spacer = document.createElement('div');
spacer.className = 'opanel-spacer';
this.applyClass(spacer, 'mcs-opanel-spacer');
optionBox.appendChild(spacer);
const expWrapper = document.createElement('div');
this.applyClass(expWrapper, 'mcs-opanel-widget-wrapper');
const expDisplay = document.createElement('div');
expDisplay.id = 'opanel-experience-hr';
this.applyClass(expDisplay, 'mcs-opanel-exp-display');
expDisplay.textContent = '0 exp/hr';
expWrapper.appendChild(expDisplay);
optionBox.appendChild(expWrapper);
this.applyOPanelZoom('experiencePerHour', expDisplay);
} else if (optionKey === 'totalProfit') {
optionBox.style.cssText = baseStyles + `
padding: 4px 0 0 0;
`;
const spacer = document.createElement('div');
spacer.className = 'opanel-spacer';
this.applyClass(spacer, 'mcs-opanel-spacer');
optionBox.appendChild(spacer);
const profitWrapper = document.createElement('div');
this.applyClass(profitWrapper, 'mcs-opanel-widget-wrapper');
const profitDisplay = document.createElement('div');
profitDisplay.id = 'opanel-total-profit';
this.applyClass(profitDisplay, 'mcs-opanel-profit-display');
profitDisplay.innerHTML = '<span style="color: #4CAF50;">0</span><span style="color: white;">-</span><span style="color: #f44336;">0</span><span style="color: white;">=</span><span style="color: #FFD700;">0/day</span>';
profitWrapper.appendChild(profitDisplay);
optionBox.appendChild(profitWrapper);
this.applyOPanelZoom('totalProfit', profitDisplay);
} else if (optionKey === 'dps') {
optionBox.style.cssText = baseStyles + `
padding: 4px 0 0 0;
`;
const spacer = document.createElement('div');
spacer.className = 'opanel-spacer';
this.applyClass(spacer, 'mcs-opanel-spacer');
optionBox.appendChild(spacer);
const dpsWrapper = document.createElement('div');
this.applyClass(dpsWrapper, 'mcs-opanel-widget-wrapper');
const dpsDisplay = document.createElement('div');
dpsDisplay.id = 'opanel-dps';
this.applyClass(dpsDisplay, 'mcs-opanel-dps-display');
dpsDisplay.innerHTML = '<div style="color: #888;">No DPS data</div>';
dpsWrapper.appendChild(dpsDisplay);
optionBox.appendChild(dpsWrapper);
this.applyOPanelZoom('dps', dpsDisplay);
} else if (optionKey === 'overExpected') {
optionBox.style.cssText = baseStyles + `
padding: 4px 0 0 0;
`;
const spacer = document.createElement('div');
spacer.className = 'opanel-spacer';
this.applyClass(spacer, 'mcs-opanel-spacer');
optionBox.appendChild(spacer);
const overExpectedWrapper = document.createElement('div');
this.applyClass(overExpectedWrapper, 'mcs-opanel-widget-wrapper');
const overExpectedDisplay = document.createElement('div');
overExpectedDisplay.id = 'opanel-over-expected';
this.applyClass(overExpectedDisplay, 'mcs-opanel-over-expected-display');
overExpectedDisplay.innerHTML = '<div style="color: #888;">No Lucky data</div>';
overExpectedWrapper.appendChild(overExpectedDisplay);
optionBox.appendChild(overExpectedWrapper);
this.applyOPanelZoom('overExpected', overExpectedDisplay);
} else if (optionKey === 'luck') {
optionBox.style.cssText = baseStyles + `
padding: 4px 0 0 0;
`;
const spacer = document.createElement('div');
spacer.className = 'opanel-spacer';
this.applyClass(spacer, 'mcs-opanel-spacer');
optionBox.appendChild(spacer);
const luckWrapper = document.createElement('div');
this.applyClass(luckWrapper, 'mcs-opanel-widget-wrapper');
const luckDisplay = document.createElement('div');
luckDisplay.id = 'opanel-luck';
this.applyClass(luckDisplay, 'mcs-opanel-luck-display');
luckDisplay.innerHTML = '<div style="color: #888;">No Lucky data</div>';
luckWrapper.appendChild(luckDisplay);
optionBox.appendChild(luckWrapper);
this.applyOPanelZoom('luck', luckDisplay);
} else if (optionKey === 'deathsPerHour') {
optionBox.style.cssText = baseStyles + `
padding: 4px 0 0 0;
`;
const spacer = document.createElement('div');
spacer.className = 'opanel-spacer';
this.applyClass(spacer, 'mcs-opanel-spacer');
optionBox.appendChild(spacer);
const deathsWrapper = document.createElement('div');
this.applyClass(deathsWrapper, 'mcs-opanel-widget-wrapper');
const deathsDisplay = document.createElement('div');
deathsDisplay.id = 'opanel-deaths-hr';
this.applyClass(deathsDisplay, 'mcs-opanel-deaths-display');
deathsDisplay.textContent = '0.0 deaths/hr';
deathsWrapper.appendChild(deathsDisplay);
optionBox.appendChild(deathsWrapper);
this.applyOPanelZoom('deathsPerHour', deathsDisplay);
} else if (optionKey === 'houses') {
optionBox.style.cssText = baseStyles + `
padding: 4px 0 0 0;
`;
const spacer = document.createElement('div');
spacer.className = 'opanel-spacer';
this.applyClass(spacer, 'mcs-opanel-spacer');
optionBox.appendChild(spacer);
const housesWrapper = document.createElement('div');
this.applyClass(housesWrapper, 'mcs-opanel-widget-wrapper');
const housesDisplay = document.createElement('div');
housesDisplay.id = 'opanel-houses';
this.applyClass(housesDisplay, 'mcs-opanel-houses-display');
housesDisplay.innerHTML = '<span style="white-space: nowrap; color: #4CAF50;">0 houses affordable</span><span style="padding-top:3px;white-space: nowrap; color: #888;">Cheapest: --</span>';
housesWrapper.appendChild(housesDisplay);
optionBox.appendChild(housesWrapper);
this.applyOPanelZoom('houses', housesDisplay);
} else if (optionKey === 'equipmentWatch') {
optionBox.style.cssText = baseStyles + `
padding: 4px 0 0 0;
`;
const spacer = document.createElement('div');
spacer.className = 'opanel-spacer';
this.applyClass(spacer, 'mcs-opanel-spacer');
optionBox.appendChild(spacer);
const ewatchWrapper = document.createElement('div');
this.applyClass(ewatchWrapper, 'mcs-opanel-widget-wrapper');
const ewatchDisplay = document.createElement('div');
ewatchDisplay.id = 'opanel-ewatch';
this.applyClass(ewatchDisplay, 'mcs-opanel-ewatch-display');
ewatchDisplay.innerHTML = `
<div style="display: flex; flex-wrap: wrap; align-items: line-height:3px; center; gap: 4px; margin-bottom: 3px;">
<span id="opanel-ewatch-item" style="margin-top:2px; white-space: nowrap; color: #FFD700; font-weight: bold;">--</span>
<span id="opanel-ewatch-remaining" style="margin-top:2px;white-space: nowrap; color: #888;">--</span>
<span id="opanel-ewatch-time" style="margin-top:2px;white-space: nowrap; color: #f5a623;">--</span>
<span id="opanel-ewatch-percent-inline" style="margin-top:2px; font-size: 10px; color: #888; display: none;">0.0%</span>
</div>
<div id="opanel-ewatch-bar-row" style="display: flex; align-items: center; gap: 6px;">
<div id="opanel-ewatch-progress" style="width: 100px; background: rgba(0,0,0,0.3); border-radius: 2px; height: 4px; overflow: hidden;">
<div id="opanel-ewatch-progress-bar" style="width: 0%; height: 100%; background: #6495ED; transition: width 0.3s ease;"></div>
</div>
<span id="opanel-ewatch-percent" style="font-size: 10px; color: #888; min-width: 35px;">0.0%</span>
</div>
`;
ewatchWrapper.appendChild(ewatchDisplay);
optionBox.appendChild(ewatchWrapper);
this.applyOPanelZoom('equipmentWatch', ewatchDisplay);
} else if (optionKey === 'combatStatus') {
optionBox.style.cssText = baseStyles + `
padding: 4px 0 0 0;
`;
const spacer = document.createElement('div');
spacer.className = 'opanel-spacer';
this.applyClass(spacer, 'mcs-opanel-spacer');
optionBox.appendChild(spacer);
const combatStatusWrapper = document.createElement('div');
this.applyClass(combatStatusWrapper, 'mcs-opanel-widget-wrapper');
const combatStatusDisplay = document.createElement('div');
combatStatusDisplay.id = 'opanel-combat-status';
this.applyClass(combatStatusDisplay, 'mcs-opanel-combat-status-display');
let inCombat = window.MCS_IN_COMBAT === true;
if (window.lootDropsTrackerInstance?.isLiveSessionActive) {
inCombat = true;
}
combatStatusDisplay.innerHTML = inCombat
? '<span style="color: #4CAF50;">In Combat</span>'
: '<span style="color: #f44336;">No Combat</span>';
combatStatusWrapper.appendChild(combatStatusDisplay);
optionBox.appendChild(combatStatusWrapper);
this.applyOPanelZoom('combatStatus', combatStatusDisplay);
} else if (optionKey === 'ntallyInventory') {
optionBox.style.cssText = baseStyles + `
padding: 4px 0 0 0;
`;
const spacer = document.createElement('div');
spacer.className = 'opanel-spacer';
this.applyClass(spacer, 'mcs-opanel-spacer');
optionBox.appendChild(spacer);
const ntallyWrapper = document.createElement('div');
this.applyClass(ntallyWrapper, 'mcs-opanel-widget-wrapper');
const ntallyDisplay = document.createElement('div');
ntallyDisplay.id = 'opanel-ntally-inventory';
this.applyClass(ntallyDisplay, 'mcs-opanel-ntally-display');
ntallyDisplay.innerHTML = '<span>📦</span><span style="color: #999;">0 bid</span>';
ntallyWrapper.appendChild(ntallyDisplay);
optionBox.appendChild(ntallyWrapper);
this.applyOPanelZoom('ntallyInventory', ntallyDisplay);
} else if (optionKey === 'kollectionBuildScore') {
optionBox.style.cssText = baseStyles + `
padding: 4px 0 0 0;
`;
const spacer = document.createElement('div');
spacer.className = 'opanel-spacer';
this.applyClass(spacer, 'mcs-opanel-spacer');
optionBox.appendChild(spacer);
const buildScoreWrapper = document.createElement('div');
this.applyClass(buildScoreWrapper, 'mcs-opanel-widget-wrapper');
const buildScoreDisplay = document.createElement('div');
buildScoreDisplay.id = 'opanel-kollection-build-score';
this.applyClass(buildScoreDisplay, 'mcs-opanel-build-score-display');
buildScoreDisplay.textContent = 'Build Score: 0.0';
buildScoreWrapper.appendChild(buildScoreDisplay);
optionBox.appendChild(buildScoreWrapper);
this.applyOPanelZoom('kollectionBuildScore', buildScoreDisplay);
} else if (optionKey === 'kollectionNetWorth') {
optionBox.style.cssText = baseStyles + `
padding: 4px 0 0 0;
`;
const spacer = document.createElement('div');
spacer.className = 'opanel-spacer';
this.applyClass(spacer, 'mcs-opanel-spacer');
optionBox.appendChild(spacer);
const netWorthWrapper = document.createElement('div');
this.applyClass(netWorthWrapper, 'mcs-opanel-widget-wrapper');
const netWorthDisplay = document.createElement('div');
netWorthDisplay.id = 'opanel-kollection-net-worth';
this.applyClass(netWorthDisplay, 'mcs-opanel-net-worth-display');
netWorthDisplay.textContent = 'Net Worth: 0';
netWorthWrapper.appendChild(netWorthDisplay);
optionBox.appendChild(netWorthWrapper);
this.applyOPanelZoom('kollectionNetWorth', netWorthDisplay);
} else if (optionKey === 'ewatchCoins') {
optionBox.style.cssText = baseStyles + `
padding: 4px 0 0 0;
`;
const spacer = document.createElement('div');
spacer.className = 'opanel-spacer';
this.applyClass(spacer, 'mcs-opanel-spacer');
optionBox.appendChild(spacer);
const coinsWrapper = document.createElement('div');
this.applyClass(coinsWrapper, 'mcs-opanel-widget-wrapper');
const coinsDisplay = document.createElement('div');
coinsDisplay.id = 'opanel-ewatch-coins';
this.applyClass(coinsDisplay, 'mcs-opanel-coins-display');
coinsDisplay.innerHTML = '<span style="width: 11px; height: 11px; display: inline-flex;">' + createItemIconHtml('coin', { width: '100%', height: '100%', sprite: 'items_sprite' }) + '</span><span style="color: #FFD700;">0</span>';
coinsWrapper.appendChild(coinsDisplay);
optionBox.appendChild(coinsWrapper);
this.applyOPanelZoom('ewatchCoins', coinsDisplay);
} else if (optionKey === 'ewatchMarket') {
optionBox.style.cssText = baseStyles + `
padding: 4px 0 0 0;
`;
const spacer = document.createElement('div');
spacer.className = 'opanel-spacer';
this.applyClass(spacer, 'mcs-opanel-spacer');
optionBox.appendChild(spacer);
const marketWrapper = document.createElement('div');
this.applyClass(marketWrapper, 'mcs-opanel-widget-wrapper');
const marketDisplay = document.createElement('div');
marketDisplay.id = 'opanel-ewatch-market';
this.applyClass(marketDisplay, 'mcs-opanel-market-display');
marketDisplay.innerHTML = '<span>📈</span><span style="color: #4CAF50;">0</span>';
marketWrapper.appendChild(marketDisplay);
optionBox.appendChild(marketWrapper);
this.applyOPanelZoom('ewatchMarket', marketDisplay);
} else if (optionKey === 'skillBooks') {
optionBox.style.cssText = baseStyles + `
padding: 4px 0 0 0;
`;
const spacer = document.createElement('div');
spacer.className = 'opanel-spacer';
this.applyClass(spacer, 'mcs-opanel-spacer');
optionBox.appendChild(spacer);
const skillBooksWrapper = document.createElement('div');
this.applyClass(skillBooksWrapper, 'mcs-opanel-widget-wrapper');
const skillBooksDisplay = document.createElement('div');
skillBooksDisplay.id = 'opanel-skill-books';
this.applyClass(skillBooksDisplay, 'mcs-opanel-skill-books-display');
skillBooksDisplay.innerHTML = '<span style="color: #999;">No skill data</span>';
skillBooksWrapper.appendChild(skillBooksDisplay);
optionBox.appendChild(skillBooksWrapper);
this.applyOPanelZoom('skillBooks', skillBooksDisplay);
} else if (optionKey === 'treasure') {
optionBox.style.cssText = baseStyles + `
padding: 4px 0 0 0;
`;
const spacer = document.createElement('div');
spacer.className = 'opanel-spacer';
this.applyClass(spacer, 'mcs-opanel-spacer');
optionBox.appendChild(spacer);
const treasureWrapper = document.createElement('div');
this.applyClass(treasureWrapper, 'mcs-opanel-widget-wrapper');
const treasureDisplay = document.createElement('div');
treasureDisplay.id = 'opanel-treasure';
this.applyClass(treasureDisplay, 'mcs-opanel-treasure-display');
treasureDisplay.innerHTML = `${createItemIconHtml('large_treasure_chest', { width: 18, height: 18, sprite: 'items_sprite', style: 'vertical-align: middle' })}<span>Treasure</span>`;
treasureWrapper.appendChild(treasureDisplay);
optionBox.appendChild(treasureWrapper);
this.applyOPanelZoom('treasure', treasureDisplay);
}
const resizeHandle = document.createElement('div');
resizeHandle.className = 'opanel-box-resize-handle';
this.applyClass(resizeHandle, 'mcs-opanel-box-resize-handle');
optionBox.appendChild(resizeHandle);
const zoomControls = document.createElement('div');
zoomControls.className = 'opanel-zoom-controls';
this.applyClass(zoomControls, 'mcs-opanel-zoom-controls');
const zoomOutBtn = document.createElement('button');
zoomOutBtn.textContent = '−';
this.applyClass(zoomOutBtn, 'mcs-opanel-zoom-btn');
zoomOutBtn.onclick = (e) => {
e.stopPropagation();
this.adjustOPanelZoom(optionKey, -1);
};
const zoomInBtn = document.createElement('button');
zoomInBtn.textContent = '+';
this.applyClass(zoomInBtn, 'mcs-opanel-zoom-btn');
zoomInBtn.onclick = (e) => {
e.stopPropagation();
this.adjustOPanelZoom(optionKey, 1);
};
zoomControls.appendChild(zoomOutBtn);
zoomControls.appendChild(zoomInBtn);
optionBox.appendChild(zoomControls);
optionBox.addEventListener('mouseenter', () => {
optionBox.style.background = '#2a2a2a';
optionBox.style.borderLeft = '1px solid #4a4a4a';
optionBox.style.borderRight = '1px solid #4a4a4a';
optionBox.style.borderBottom = '1px solid #4a4a4a';
resizeHandle.style.opacity = '1';
if (!this.oPanelIsLocked) {
zoomControls.style.opacity = '1';
}
});
optionBox.addEventListener('mouseleave', () => {
optionBox.style.background = 'transparent';
optionBox.style.borderLeft = '1px solid transparent';
optionBox.style.borderRight = '1px solid transparent';
optionBox.style.borderBottom = '1px solid transparent';
resizeHandle.style.opacity = '0';
zoomControls.style.opacity = '0';
});
if (this.oPanelIsLocked) {
optionBox.style.cursor = 'default';
resizeHandle.style.display = 'none';
zoomControls.style.pointerEvents = 'none';
} else {
optionBox.style.cursor = 'move';
zoomControls.style.pointerEvents = 'auto';
}
this.makeOPanelBoxResizable(optionBox, resizeHandle, optionKey);
this.makeOPanelBoxDraggable(optionBox, optionKey);
optionBox.addEventListener('dblclick', (e) => {
e.stopPropagation();
const panelMapping = {
'battleTimer': 'milt-loot-drops-display',
'combatRevenue': 'milt-loot-drops-display',
'consumables': 'consumables-pane',
'experiencePerHour': 'gwhiz-pane',
'totalProfit': 'hwhat-pane',
'dps': 'dps-pane',
'overExpected': 'lucky-panel',
'luck': 'lucky-panel',
'deathsPerHour': 'ihurt-pane',
'houses': 'jhouse-pane',
'equipmentWatch': 'equipment-spy-pane',
'ntallyInventory': 'mcs_nt_pane',
'kollectionBuildScore': 'kollection-pane',
'kollectionNetWorth': 'kollection-pane',
'ewatchCoins': 'equipment-spy-pane',
'ewatchMarket': 'mcs_nt_pane',
'skillBooks': 'bread-pane',
'treasure': 'treasure-pane'
};
const targetPanelId = panelMapping[optionKey];
if (targetPanelId) {
const checkbox = document.querySelector(`input[type="checkbox"][data-tool-panel="${targetPanelId}"]`);
if (checkbox) {
checkbox.checked = !checkbox.checked;
checkbox.dispatchEvent(new Event('change', { bubbles: true }));
}
}
});
let resizeTimeout;
let ignoreInitialResize = true;
setTimeout(() => {
ignoreInitialResize = false;
}, 2000);
if (!this.oPanelOptionResizeObservers) {
this.oPanelOptionResizeObservers = new Map();
}
if (this.oPanelOptionResizeObservers.has(optionKey)) {
this.oPanelOptionResizeObservers.get(optionKey).disconnect();
}
const resizeObserver = new ResizeObserver(() => {
if (ignoreInitialResize) {
return;
}
clearTimeout(resizeTimeout);
resizeTimeout = setTimeout(() => {
const computedStyle = window.getComputedStyle(optionBox);
const width = parseFloat(computedStyle.width);
const height = parseFloat(computedStyle.height);
if (!this.oPanelConfig.sizes) {
this.oPanelConfig.sizes = {};
}
if (!this.oPanelConfig.sizes[optionKey]) {
this.oPanelConfig.sizes[optionKey] = {};
}
this.oPanelConfig.sizes[optionKey].width = width;
this.oPanelConfig.sizes[optionKey].height = height;
window.lootDropsTrackerInstance.opStorage.set('config', this.oPanelConfig);
}, 100);
});
this.oPanelOptionResizeObservers.set(optionKey, resizeObserver);
resizeObserver.observe(optionBox);
contentGrid.appendChild(optionBox);
}
});
this.updateOPanelLayout();
this.startOPanelMasterUpdate();
if (this.oPanelConfig.firstLoad) {
this.oPanelConfig.firstLoad = false;
window.lootDropsTrackerInstance.opStorage.set('config', this.oPanelConfig);
setTimeout(() => {
this.autogridOPanelPanels();
}, 100);
}
}
updateOPanelLayout() {
}
makeOPanelBoxDraggable(box, optionKey) {
const GRID_SIZE = 10;
let startX, startY, startLeft, startTop;
const gridOverlay = document.getElementById('opanel-grid-overlay');
const snapToGrid = (value) => Math.round(value / GRID_SIZE) * GRID_SIZE;
const onMouseMove = (e) => {
const deltaX = e.clientX - startX;
const deltaY = e.clientY - startY;
let newLeft = startLeft + deltaX;
let newTop = startTop + deltaY;
if (this.oPanelConfig.snapToGrid !== false) {
newLeft = snapToGrid(newLeft);
newTop = snapToGrid(newTop);
}
box.style.left = newLeft + 'px';
box.style.top = newTop + 'px';
};
const onMouseUp = () => {
document.removeEventListener('mousemove', onMouseMove);
document.removeEventListener('mouseup', onMouseUp);
this._opanelActiveDocListeners = this._opanelActiveDocListeners.filter(l => l.move !== onMouseMove);
box.style.zIndex = '1';
box.style.cursor = 'move';
if (gridOverlay) gridOverlay.style.display = 'none';
const newLeft = parseInt(box.style.left);
const newTop = parseInt(box.style.top);
if (!this.oPanelConfig.positions) this.oPanelConfig.positions = {};
if (!this.oPanelConfig.positions[optionKey]) this.oPanelConfig.positions[optionKey] = {};
this.oPanelConfig.positions[optionKey].x = newLeft;
this.oPanelConfig.positions[optionKey].y = newTop;
window.lootDropsTrackerInstance.opStorage.set('config', this.oPanelConfig);
};
box.addEventListener('mousedown', (e) => {
if (this.oPanelIsLocked) return;
if (e.target.classList.contains('opanel-box-resize-handle')) return;
const rect = box.getBoundingClientRect();
if (e.clientX > rect.right - 20 && e.clientY > rect.bottom - 20) return;
startX = e.clientX;
startY = e.clientY;
startLeft = parseInt(box.style.left) || 0;
startTop = parseInt(box.style.top) || 0;
box.style.zIndex = '100';
box.style.cursor = 'grabbing';
if (gridOverlay) gridOverlay.style.display = 'block';
e.preventDefault();
document.addEventListener('mousemove', onMouseMove);
document.addEventListener('mouseup', onMouseUp);
this._opanelActiveDocListeners.push({ move: onMouseMove, up: onMouseUp });
});
}
makeOPanelBoxResizable(box, handle, optionKey) {
let startX, startY, startWidth, startHeight;
const onMouseMove = (e) => {
const deltaX = e.clientX - startX;
const deltaY = e.clientY - startY;
const minWidth = parseInt(box.style.minWidth) || 10;
const minHeight = parseInt(box.style.minHeight) || 10;
let newWidth = Math.max(minWidth, startWidth + deltaX);
let newHeight = Math.max(minHeight, startHeight + deltaY);
if (this.oPanelConfig.snapToGrid !== false) {
const GRID_SIZE = 10;
newWidth = Math.round(newWidth / GRID_SIZE) * GRID_SIZE;
newHeight = Math.round(newHeight / GRID_SIZE) * GRID_SIZE;
}
box.style.width = newWidth + 'px';
box.style.height = newHeight + 'px';
};
const onMouseUp = () => {
document.removeEventListener('mousemove', onMouseMove);
document.removeEventListener('mouseup', onMouseUp);
this._opanelActiveDocListeners = this._opanelActiveDocListeners.filter(l => l.move !== onMouseMove);
const rect = box.getBoundingClientRect();
if (!this.oPanelConfig.sizes) this.oPanelConfig.sizes = {};
if (!this.oPanelConfig.sizes[optionKey]) this.oPanelConfig.sizes[optionKey] = {};
this.oPanelConfig.sizes[optionKey].width = rect.width;
this.oPanelConfig.sizes[optionKey].height = rect.height;
window.lootDropsTrackerInstance.opStorage.set('config', this.oPanelConfig);
};
handle.addEventListener('mousedown', (e) => {
if (this.oPanelIsLocked) { e.preventDefault(); e.stopPropagation(); return; }
startX = e.clientX;
startY = e.clientY;
const rect = box.getBoundingClientRect();
startWidth = rect.width;
startHeight = rect.height;
e.preventDefault();
e.stopPropagation();
document.addEventListener('mousemove', onMouseMove);
document.addEventListener('mouseup', onMouseUp);
this._opanelActiveDocListeners.push({ move: onMouseMove, up: onMouseUp });
});
}
makeOPanelDraggable(pane, dragHandle, storageKey) {
let dragOffset = { x: 0, y: 0 };
const onMouseMove = (e) => {
let newLeft = e.clientX - dragOffset.x;
let newTop = e.clientY - dragOffset.y;
const rect = pane.getBoundingClientRect();
const winWidth = window.innerWidth;
const winHeight = window.innerHeight;
const minVisiblePx = 30;
const handleRect = dragHandle.getBoundingClientRect();
const handleOffsetX = handleRect.left - rect.left;
const handleOffsetY = handleRect.top - rect.top;
const handleLeft = newLeft + handleOffsetX;
const handleRight = handleLeft + handleRect.width;
const handleTop = newTop + handleOffsetY;
const handleBottom = handleTop + handleRect.height;
if (handleRight < minVisiblePx) newLeft = minVisiblePx - handleOffsetX - handleRect.width;
if (handleLeft > winWidth - minVisiblePx) newLeft = winWidth - minVisiblePx - handleOffsetX;
if (handleBottom < minVisiblePx) newTop = minVisiblePx - handleOffsetY - handleRect.height;
if (handleTop > winHeight - minVisiblePx) newTop = winHeight - minVisiblePx - handleOffsetY;
pane.style.left = newLeft + 'px';
pane.style.top = newTop + 'px';
pane.style.right = 'auto';
pane.style.bottom = 'auto';
};
const onMouseUp = () => {
document.removeEventListener('mousemove', onMouseMove);
document.removeEventListener('mouseup', onMouseUp);
this._opanelActiveDocListeners = this._opanelActiveDocListeners.filter(l => l.move !== onMouseMove);
const rect = pane.getBoundingClientRect();
this.opStorage.set('position', { top: rect.top, left: rect.left });
};
dragHandle.addEventListener('mousedown', (e) => {
const rect = pane.getBoundingClientRect();
dragOffset.x = e.clientX - rect.left;
dragOffset.y = e.clientY - rect.top;
e.preventDefault();
e.stopPropagation();
document.addEventListener('mousemove', onMouseMove);
document.addEventListener('mouseup', onMouseUp);
this._opanelActiveDocListeners.push({ move: onMouseMove, up: onMouseUp });
});
}
makeOPanelCornerResizable(pane, handle, cornerName) {
let startX, startY, startWidth, startHeight, startLeft, startTop;
const onMouseMove = (e) => {
const deltaX = e.clientX - startX;
const deltaY = e.clientY - startY;
let newWidth = startWidth;
let newHeight = startHeight;
let newLeft = startLeft;
let newTop = startTop;
if (cornerName.includes('e')) newWidth = startWidth + deltaX;
if (cornerName.includes('w')) { newWidth = startWidth - deltaX; newLeft = startLeft + deltaX; }
if (cornerName.includes('s')) newHeight = startHeight + deltaY;
if (cornerName.includes('n')) { newHeight = startHeight - deltaY; newTop = startTop + deltaY; }
const minWidth = parseInt(pane.style.minWidth) || 10;
const minHeight = parseInt(pane.style.minHeight) || 10;
if (newWidth < minWidth) { newWidth = minWidth; if (cornerName.includes('w')) newLeft = startLeft + startWidth - minWidth; }
if (newHeight < minHeight) { newHeight = minHeight; if (cornerName.includes('n')) newTop = startTop + startHeight - minHeight; }
pane.style.width = newWidth + 'px';
pane.style.height = newHeight + 'px';
pane.style.left = newLeft + 'px';
pane.style.top = newTop + 'px';
};
const onMouseUp = () => {
document.removeEventListener('mousemove', onMouseMove);
document.removeEventListener('mouseup', onMouseUp);
this._opanelActiveDocListeners = this._opanelActiveDocListeners.filter(l => l.move !== onMouseMove);
const rect = pane.getBoundingClientRect();
window.lootDropsTrackerInstance.opStorage.set('size', { width: rect.width, height: rect.height });
window.lootDropsTrackerInstance.opStorage.set('position', { top: rect.top, left: rect.left });
};
handle.addEventListener('mousedown', (e) => {
if (this.oPanelIsLocked) { e.preventDefault(); e.stopPropagation(); return; }
startX = e.clientX;
startY = e.clientY;
const rect = pane.getBoundingClientRect();
startWidth = rect.width;
startHeight = rect.height;
startLeft = rect.left;
startTop = rect.top;
e.preventDefault();
e.stopPropagation();
document.addEventListener('mousemove', onMouseMove);
document.addEventListener('mouseup', onMouseUp);
this._opanelActiveDocListeners.push({ move: onMouseMove, up: onMouseUp });
});
}
resetOPanelPositions() {
this.oPanelConfig.positions = {
battleTimer: { x: 0, y: 0 },
combatRevenue: { x: 0, y: 50 },
consumables: { x: 0, y: 210 },
experiencePerHour: { x: 0, y: 300 },
totalProfit: { x: 0, y: 350 },
dps: { x: 0, y: 400 },
deathsPerHour: { x: 0, y: 560 },
houses: { x: 0, y: 610 },
equipmentWatch: { x: 0, y: 660 }
};
window.lootDropsTrackerInstance.opStorage.set('config', this.oPanelConfig);
this.updateOPanelContent();
}
autogridOPanelPanels() {
const contentGrid = document.getElementById('opanel-content-grid');
const content = document.getElementById('opanel-content');
if (!contentGrid || !content) return;
const GRID_SIZE = 10;
const viewableHeight = content.clientHeight;
const visiblePanels = [];
this.oPanelConfig.order.forEach((optionKey) => {
if (this.oPanelConfig[optionKey] === true) {
const box = contentGrid.querySelector(`[data-option-key="${optionKey}"]`);
if (box) {
const currentX = parseInt(box.style.left) || 0;
const currentY = parseInt(box.style.top) || 0;
visiblePanels.push({
key: optionKey,
box: box,
width: box.offsetWidth,
height: box.offsetHeight,
beforeX: currentX,
beforeY: currentY
});
}
}
});
if (visiblePanels.length === 0) return;
const COLUMN_SPACER = 10;
const ROW_SPACER = 10;
const START_X = 10;
const START_Y = 10;
const columnWidths = [];
let currentColumn = 0;
let currentY = START_Y;
visiblePanels.forEach((panel) => {
const snappedY = Math.round(currentY / GRID_SIZE) * GRID_SIZE;
if (snappedY + panel.height > viewableHeight && currentY > START_Y) {
currentColumn++;
currentY = START_Y;
const newSnappedY = START_Y;
let x = START_X;
for (let i = 0; i < currentColumn; i++) {
x += (columnWidths[i] ?? 0) + COLUMN_SPACER;
}
panel.box.style.left = x + 'px';
panel.box.style.top = newSnappedY + 'px';
if (!columnWidths[currentColumn] || panel.width > columnWidths[currentColumn]) {
columnWidths[currentColumn] = panel.width;
}
if (!this.oPanelConfig.positions) {
this.oPanelConfig.positions = {};
}
if (!this.oPanelConfig.positions[panel.key]) {
this.oPanelConfig.positions[panel.key] = {};
}
this.oPanelConfig.positions[panel.key].x = x;
this.oPanelConfig.positions[panel.key].y = newSnappedY;
currentY = newSnappedY + panel.height + ROW_SPACER;
} else {
let x = START_X;
for (let i = 0; i < currentColumn; i++) {
x += (columnWidths[i] ?? 0) + COLUMN_SPACER;
}
panel.box.style.left = x + 'px';
panel.box.style.top = snappedY + 'px';
if (!columnWidths[currentColumn] || panel.width > columnWidths[currentColumn]) {
columnWidths[currentColumn] = panel.width;
}
if (!this.oPanelConfig.positions) {
this.oPanelConfig.positions = {};
}
if (!this.oPanelConfig.positions[panel.key]) {
this.oPanelConfig.positions[panel.key] = {};
}
this.oPanelConfig.positions[panel.key].x = x;
this.oPanelConfig.positions[panel.key].y = snappedY;
currentY = snappedY + panel.height + ROW_SPACER;
}
});
window.lootDropsTrackerInstance.opStorage.set('config', this.oPanelConfig);
}
resetOPanelToDefaults() {
const defaultConfig = {
battleTimer: true,
combatRevenue: true,
consumables: true,
experiencePerHour: true,
totalProfit: true,
dps: true,
overExpected: true,
luck: true,
deathsPerHour: true,
houses: true,
equipmentWatch: true,
combatStatus: true,
order: ['battleTimer', 'combatRevenue', 'consumables', 'experiencePerHour', 'totalProfit', 'dps', 'overExpected', 'luck', 'deathsPerHour', 'houses', 'equipmentWatch', 'combatStatus'],
sizes: {
battleTimer: { width: 400, height: 30 },
combatRevenue: { width: 400, height: 70 },
consumables: { width: 400, height: 70 },
experiencePerHour: { width: 400, height: 30 },
totalProfit: { width: 400, height: 30 },
dps: { width: 400, height: 70 },
overExpected: { width: null, height: 70 },
luck: { width: null, height: 70 },
deathsPerHour: { width: 400, height: 30 },
houses: { width: 400, height: 30 },
equipmentWatch: { width: 400, height: 40 },
combatStatus: { width: 400, height: 30 }
},
positions: {}
};
let currentY = 20;
defaultConfig.order.forEach(optionKey => {
defaultConfig.positions[optionKey] = {
x: 40,
y: currentY
};
currentY += defaultConfig.sizes[optionKey].height + 10;
});
this.oPanelConfig = defaultConfig;
window.lootDropsTrackerInstance.opStorage.set('config', this.oPanelConfig);
const pane = document.getElementById('opanel-pane');
if (pane) {
pane.style.width = '500px';
pane.style.height = '550px';
window.lootDropsTrackerInstance.opStorage.set('size', { width: 500, height: 550 });
}
defaultConfig.order.forEach(optionKey => {
const checkbox = document.getElementById(`opanel-checkbox-${optionKey}`);
if (checkbox) {
checkbox.checked = defaultConfig[optionKey];
}
});
this.updateOPanelContent(true);
}
toggleOPanel() {
const pane = document.getElementById('opanel-pane');
if (pane) {
this.stopAllOPanelIntervals();
pane.remove();
} else {
this.createOPanel();
}
}
startOPanelMasterUpdate() {
this.stopAllOPanelIntervals();
this.updateOPanelTimer();
this.updateOPanelCombatRevenue();
this.updateOPanelConsumables();
this.updateOPanelExperience();
this.updateOPanelTotalProfit();
this.updateOPanelDPS();
this.updateOPanelOverExpected();
this.updateOPanelLuck();
this.updateOPanelDeaths();
this.updateOPanelHouses();
this.updateOPanelEWatch();
this.updateOPanelEWatchLayout();
this.updateOPanelCombatStatus();
this.updateOPanelNtallyInventory();
this.updateOPanelKollectionBuildScore();
this.updateOPanelKollectionNetWorth();
this.updateOPanelEwatchCoins();
this.updateOPanelEwatchMarket();
this.updateOPanelSkillBooks();
let frameCount = 0;
VisibilityManager.register('opanel-master', () => {
frameCount++;
this.updateOPanelTimer();
this.updateOPanelCombatStatus();
if (frameCount % 2 === 0) {
this.updateOPanelCombatRevenue();
this.updateOPanelConsumables();
this.updateOPanelExperience();
this.updateOPanelTotalProfit();
this.updateOPanelDPS();
this.updateOPanelOverExpected();
this.updateOPanelLuck();
this.updateOPanelDeaths();
this.updateOPanelNtallyInventory();
this.updateOPanelEwatchCoins();
this.updateOPanelEwatchMarket();
this.updateOPanelSkillBooks();
}
if (frameCount % 5 === 0) {
this.updateOPanelHouses();
this.updateOPanelEWatch();
this.updateOPanelKollectionBuildScore();
this.updateOPanelKollectionNetWorth();
}
if (frameCount >= 100) frameCount = 0;
}, 1000);
}
stopAllOPanelIntervals() {
VisibilityManager.clear('opanel-master');
if (this.oPanelMainResizeObserver) {
this.oPanelMainResizeObserver.disconnect();
this.oPanelMainResizeObserver = null;
}
if (this.oPanelOptionResizeObservers) {
this.oPanelOptionResizeObservers.forEach(observer => observer.disconnect());
this.oPanelOptionResizeObservers.clear();
}
}
updateOPanelTimer() {
const timerDisplay = document.getElementById('opanel-battle-timer');
if (!timerDisplay) return;
let timerText = '--:--:--';
if (this.startTime && this.isLiveSessionActive) {
const now = new Date();
const elapsed = now - this.startTime;
timerText = this.formatElapsedTime(elapsed);
} else if (this.startTime && this.sessionEndTime) {
const elapsed = this.sessionEndTime - this.startTime;
timerText = this.formatElapsedTime(elapsed);
}
if (this.isLiveSessionActive && this.sessionStartTime) {
const eph = this.calculateEPH();
timerDisplay.textContent = `${timerText} | ${eph} EPH`;
} else {
timerDisplay.textContent = timerText;
}
}
updateOPanelCombatRevenue() {
const revenueDisplay = document.getElementById('opanel-combat-revenue');
if (!revenueDisplay) return;
const tracker = window.lootDropsTrackerInstance;
if (!tracker) {
revenueDisplay.innerHTML = '<div style="text-align: center; color: #888;">--</div>';
return;
}
const userName = tracker.userName;
const playerRevenue = tracker.flootPlayerRevenue;
if (!playerRevenue || Object.keys(playerRevenue).length === 0) {
revenueDisplay.innerHTML = '<div style="text-align: center; color: #888;">No loot tracked</div>';
return;
}
const playerNames = Object.keys(playerRevenue).sort((a, b) => {
if (a === userName) return -1;
if (b === userName) return 1;
return 0;
});
let html = '';
for (const playerName of playerNames) {
const { total, perDay } = playerRevenue[playerName];
const totalText = total > 0 ? this.formatOPanelCoins(total) : '--';
const goldPerDayText = perDay > 0 ? this.formatOPanelCoins(Math.round(perDay)) : '';
const isCurrentUser = playerName === userName;
const nameColor = this.getPlayerNameColorByName(playerName);
const perDayColor = isCurrentUser ? '#90EE90' : '#aaa';
html += `
<div style="display: flex; flex-wrap: wrap; align-items: center; gap: 3px 6px; margin: 0 0 3px 0; font-size: 10px;">
<span style="color: ${nameColor}; font-weight: bold;">${playerName}</span>
<span style="display: flex; align-items: center; gap: 4px; color: #ffffff;">
<span style="font-size: 10px;">💰</span>
<span>${totalText}</span>
</span>
${goldPerDayText ? `<span style="color: ${perDayColor};">${goldPerDayText}/day</span>` : ''}
</div>
`;
}
revenueDisplay.innerHTML = html || '<div style="text-align: center; color: #888;">--</div>';
}
formatOPanelCoins(value) {
if (window.formatFlootCoins) {
return window.formatFlootCoins(value);
}
return `${Math.round(value).toLocaleString()} coin`;
}
updateOPanelConsumables() {
const consumablesDisplay = document.getElementById('opanel-consumables');
if (!consumablesDisplay) {
return;
}
const tracker = window.lootDropsTrackerInstance;
if (!tracker) {
consumablesDisplay.innerHTML = '<div style="text-align: center; color: #888;">--</div>';
return;
}
let yourTime = '--';
let yourColor = '#e0e0e0';
if (tracker.yourMinTime && tracker.yourMinTime !== Infinity) {
yourTime = mcsFormatDuration(tracker.yourMinTime, 'eta');
const days = tracker.yourMinTime / 86400;
if (days < 1) {
yourColor = '#c42323';
} else if (days < 2) {
yourColor = '#e8a738';
} else {
yourColor = '#65b83e';
}
}
let partyTime = '--';
let partyColor = '#e0e0e0';
if (tracker.partyMinTime !== undefined && tracker.partyMinTime !== Infinity) {
partyTime = mcsFormatDuration(tracker.partyMinTime, 'eta');
const days = tracker.partyMinTime / 86400;
if (days < 1) {
partyColor = '#FF6B6B';
} else if (days < 2) {
partyColor = '#FFA500';
} else {
partyColor = '#90EE90';
}
}
let row2Html = '';
if (tracker.minConsumables && tracker.minConsumables.length > 0 && tracker.minConsumablesCount) {
let iconsHtml = '';
tracker.minConsumables.forEach(item => {
const iconId = item.itemHrid.split('/').pop();
iconsHtml += createItemIconHtml(iconId, { className: 'opanel-consumable-icon-clickable', clickable: true, itemHrid: item.itemHrid, style: 'flex-shrink: 0' });
});
const askText = tracker.formatPriceShort ? tracker.formatPriceShort(tracker.totalConsumableAsk || 0) : '--';
const bidText = tracker.formatPriceShort ? tracker.formatPriceShort(tracker.totalConsumableBid || 0) : '--';
row2Html = `
<div style="display: flex; flex-wrap: wrap; align-items: center; gap: 3px 6px; font-size: 10px;">
<span style="color: #ff6666; padding-top:3px;font-weight: bold; gap: 4px;">${tracker.minConsumablesCount} remaining</span>
<span style="display: flex; padding-top:3px;align-items: center; gap: 4px;">${iconsHtml}</span>
<span style="color: #90EE90; padding-top:3px;font-weight: bold; gap: 4px;">Total Cost/Day:</span>
<span style="color: #ffffff; padding-top:3px;gap: 4px;">Ask: ${askText} / Bid: ${bidText}</span>
</div>
`;
}
const html = `
<div style="display: flex; flex-wrap: wrap; align-items: center; gap: 3px 6px; margin-bottom: 0px; font-size: 10px;">
<span style="color: #e0e0e0; font-weight: bold; gap: 4px;">You:</span>
<span style="color: ${yourColor}; font-weight: bold; gap: 4px;">${yourTime}</span>
<span style="color: #e0e0e0; font-weight: bold; gap: 4px;">Party:</span>
<span style="color: ${partyColor}; font-weight: bold; gap: 4px;">${partyTime}</span>
</div>
${row2Html}
`;
consumablesDisplay.innerHTML = html;
if (!consumablesDisplay.dataset.clickListenerAttached) {
consumablesDisplay.addEventListener('click', (e) => {
let icon = e.target;
const tagNameUpper = icon.tagName ? icon.tagName.toUpperCase() : '';
if (tagNameUpper === 'USE') {
icon = icon.parentElement;
}
if (!icon.classList || !icon.classList.contains('opanel-consumable-icon-clickable')) {
icon = e.target.closest('.opanel-consumable-icon-clickable');
}
if (icon && icon.classList && icon.classList.contains('opanel-consumable-icon-clickable')) {
const itemHrid = icon.getAttribute('data-item-hrid');
if (itemHrid) {
mcsGoToMarketplace(itemHrid);
}
}
});
consumablesDisplay.dataset.clickListenerAttached = 'true';
}
}
updateOPanelExperience() {
const expDisplay = document.getElementById('opanel-experience-hr');
if (!expDisplay) return;
const tracker = window.lootDropsTrackerInstance;
if (tracker && tracker.gwhizTotalExpPerHour !== undefined) {
const totalExpPerHour = tracker.gwhizTotalExpPerHour || 0;
if (totalExpPerHour > 0) {
expDisplay.textContent = Math.floor(totalExpPerHour).toLocaleString() + ' exp/hr';
} else {
expDisplay.textContent = '0 exp/hr';
}
} else {
expDisplay.textContent = '0 exp/hr';
}
}
updateOPanelTotalProfit() {
const profitDisplay = document.getElementById('opanel-total-profit');
if (!profitDisplay) {
return;
}
const tracker = window.lootDropsTrackerInstance;
if (tracker && tracker.hwhatCurrentRevenue !== undefined) {
const revenue = tracker.hwhatCurrentRevenue || 0;
const cost = tracker.hwhatCurrentCost || 0;
const profit = tracker.hwhatCurrentProfit || 0;
const tax = tracker.hwhatCowbellTaxPerDay || 0;
const showTax = tracker.hwhatTaxEnabled || false;
const revenueText = tracker.formatGoldNumber ? tracker.formatGoldNumber(revenue) : revenue.toString();
const costText = tracker.formatGoldNumber ? tracker.formatGoldNumber(cost) : cost.toString();
const profitText = tracker.formatGoldNumber ? tracker.formatGoldNumber(profit) : profit.toString();
const taxText = tracker.formatGoldNumber ? tracker.formatGoldNumber(tax) : tax.toString();
let html = '';
html += `<span style="white-space: nowrap;"><span style="color: #4CAF50;">${revenueText}</span><span style="color: white;"> - </span></span>`;
if (showTax) {
const iconHtml = createItemIconHtml('bag_of_10_cowbells', { width: 16, height: 16, style: 'vertical-align: middle' });
html += `<span style="white-space: nowrap;">${iconHtml}<span style="color: #dc3545;">${taxText}</span><span style="color: white;"> - </span></span>`;
}
html += `<span style="white-space: nowrap;"><span style="color: #f44336;">${costText}</span><span style="color: white;"> = </span></span>`;
html += `<span style="white-space: nowrap; color: #FFD700;">${profitText}/day</span>`;
profitDisplay.innerHTML = html;
} else {
profitDisplay.innerHTML = '<span style="white-space: nowrap;"><span style="color: #4CAF50;">0</span><span style="color: white;"> - </span></span><span style="white-space: nowrap;"><span style="color: #f44336;">0</span><span style="color: white;"> = </span></span><span style="white-space: nowrap; color: #FFD700;">0/day</span>';
}
}
getPlayerIndexByName(playerName) {
if (!this.dpsTracking || !this.dpsTracking.players) {
return -1;
}
for (let i = 0; i < this.dpsTracking.players.length; i++) {
if (this.dpsTracking.players[i].name === playerName) {
return i;
}
}
return -1;
}
getPlayerNameColor(playerIndex) {
if (!this.oPanelConfig.usePlayerNameRecolor) {
return '#FFD700';
}
const tracker = window.lootDropsTrackerInstance;
if (!tracker || !tracker.fcbPlayerNameRecolorEnabled || !tracker.fcbSettingsMap) {
return '#FFD700';
}
const trackerKey = `tracker${playerIndex}`;
if (tracker.fcbSettingsMap[trackerKey]) {
const settings = tracker.fcbSettingsMap[trackerKey];
return `rgb(${settings.r},${settings.g},${settings.b})`;
}
return '#FFD700';
}
getPlayerNameColorByName(playerName) {
const playerIndex = this.getPlayerIndexByName(playerName);
if (playerIndex === -1) {
return '#FFD700';
}
return this.getPlayerNameColor(playerIndex);
}
updateOPanelDPS() {
const dpsDisplay = document.getElementById('opanel-dps');
if (!dpsDisplay) {
return;
}
const titleSpan = document.getElementById('dps-title-span');
const dpsContent = document.getElementById('dps-content');
if (!dpsContent) {
dpsDisplay.innerHTML = '<div style="color: #888;">No DPS data</div>';
return;
}
const playerItems = dpsContent.querySelectorAll('.mcs-dps-player-item');
if (playerItems.length === 0) {
dpsDisplay.innerHTML = '<div style="color: #888;">No DPS data</div>';
return;
}
let totalDPSText = '0.0';
if (titleSpan) {
const highlight = titleSpan.querySelector('.mcs-dps-title-highlight');
if (highlight) {
totalDPSText = highlight.textContent.trim();
}
}
let html = '';
playerItems.forEach(item => {
const nameLabel = item.querySelector('.mcs-dps-name-label');
const dpsLabel = item.querySelector('.mcs-dps-dps-label');
if (!nameLabel || !dpsLabel) return;
const playerName = nameLabel.textContent.trim();
const nameColor = this.getPlayerNameColorByName(playerName);
const dpsText = dpsLabel.childNodes[0]?.textContent?.trim() || '0.0';
const accuracySpan = dpsLabel.querySelector('.mcs-dps-accuracy-span');
const accuracyText = accuracySpan ? accuracySpan.textContent.trim() : '--';
html += `<div style="margin-bottom:3px;display: flex; justify-content: space-between; gap: 4px;">`;
html += `<span style="color: ${nameColor};gap: 4px;">${playerName}</span>`;
html += `<span style="color: #4CAF50;gap: 4px;">${dpsText} <span style="color: #FF9800;">${accuracyText}</span></span>`;
html += `</div>`;
});
html += `<div style="display: flex; justify-content: space-between; gap: 4px;">`;
html += `<span style="color: #fff; font-weight: bold;gap: 4px;">Total DPS</span>`;
html += `<span style="color: #90EE90; font-weight: bold;gap: 4px;">${totalDPSText}</span>`;
html += `</div>`;
dpsDisplay.innerHTML = html;
}
updateOPanelOverExpected() {
const overExpectedDisplay = document.getElementById('opanel-over-expected');
if (!overExpectedDisplay) {
return;
}
const bigExpectedContent = document.getElementById('lucky-big-expected-content');
if (!bigExpectedContent) {
overExpectedDisplay.innerHTML = '<div style="color: #888;">Lucky not available</div>';
return;
}
const playerItems = bigExpectedContent.querySelectorAll('.lucky-big-expected-item:not(.lucky-big-expected-total)');
const totalItem = bigExpectedContent.querySelector('.lucky-big-expected-total');
if (playerItems.length === 0 && !totalItem) {
overExpectedDisplay.innerHTML = '<div style="color: #888;">No Lucky data</div>';
return;
}
const onlyPlayer = this.oPanelConfig.overExpectedOnlyPlayer || false;
const onlyNumbers = this.oPanelConfig.overExpectedOnlyNumbers || false;
const tracker = window.lootDropsTrackerInstance;
const currentUserName = tracker ? tracker.userName : '';
const playersData = [];
playerItems.forEach(item => {
const nameSpan = item.querySelector('.lucky-big-expected-name');
const percentSpan = item.querySelector('.lucky-big-expected-percent');
if (nameSpan && percentSpan) {
const playerName = nameSpan.textContent;
const percent = percentSpan.textContent;
const color = percentSpan.style.color || '#a8aed4';
const isCurrentUser = playerName === currentUserName;
playersData.push({
name: playerName,
percent: percent,
color: color,
isCurrentUser: isCurrentUser
});
}
});
playersData.sort((a, b) => {
if (a.isCurrentUser && !b.isCurrentUser) return -1;
if (!a.isCurrentUser && b.isCurrentUser) return 1;
return 0;
});
let html = '';
const filteredPlayers = onlyPlayer ? playersData.filter(p => p.isCurrentUser) : playersData;
filteredPlayers.forEach(player => {
const nameColor = this.getPlayerNameColorByName(player.name);
if (onlyPlayer && onlyNumbers) {
html += `<div style="display: flex; justify-content: space-between; gap: 4px;">`;
html += `<span style="padding-bottom:3px;gap: 4px;color: ${player.color};">${player.percent}</span>`;
html += `</div>`;
} else if (onlyNumbers) {
html += `<div style="display: flex; justify-content: space-between; gap: 4px;">`;
html += `<span style="padding-bottom:3px;gap: 4px;color: ${player.color};">${player.percent}</span>`;
html += `</div>`;
} else if (onlyPlayer) {
html += `<div style="display: flex; justify-content: space-between; gap: 4px;">`;
html += `<span style="padding-bottom:3px;gap: 4px;color: ${nameColor};">${player.name}</span>`;
html += `<span style="padding-bottom:3px;gap: 4px;color: ${player.color};">${player.percent}</span>`;
html += `</div>`;
} else {
html += `<div style="display: flex; justify-content: space-between; gap: 4px;">`;
html += `<span style="padding-bottom:3px; gap: 4px;color: ${nameColor};">${player.name}</span>`;
html += `<span style="padding-bottom:3px; gap: 4px;color: ${player.color};">${player.percent}</span>`;
html += `</div>`;
}
});
if (totalItem && !onlyPlayer) {
const nameSpan = totalItem.querySelector('.lucky-big-expected-name');
const percentSpan = totalItem.querySelector('.lucky-big-expected-percent');
if (nameSpan && percentSpan) {
const totalName = nameSpan.textContent;
const totalPercent = percentSpan.textContent;
const totalColor = percentSpan.style.color || '#a8aed4';
if (onlyNumbers) {
html += `<div style="display: flex; justify-content: space-between; gap: 4px;">`;
html += `<span style="padding bottom:3px;color: ${totalColor}; font-weight: bold;">${totalPercent}</span>`;
html += `</div>`;
} else {
html += `<div style="display: flex; justify-content: space-between; gap: 4px;">`;
html += `<span style="padding bottom:3px;gap: 4px;color: #fff; font-weight: bold;">${totalName}</span>`;
html += `<span style="padding bottom:3px;gap: 4px;color: ${totalColor}; font-weight: bold;">${totalPercent}</span>`;
html += `</div>`;
}
}
}
overExpectedDisplay.innerHTML = html || '<div style="color: #888;">No data</div>';
}
updateOPanelLuck() {
const luckDisplay = document.getElementById('opanel-luck');
if (!luckDisplay) {
return;
}
const bigLuckContent = document.getElementById('lucky-big-luck-content');
if (!bigLuckContent) {
luckDisplay.innerHTML = '<div style="color: #888;">Lucky not available</div>';
return;
}
const playerItems = bigLuckContent.querySelectorAll('.lucky-big-luck-item');
if (playerItems.length === 0) {
luckDisplay.innerHTML = '<div style="color: #888;">No Lucky data</div>';
return;
}
const onlyPlayer = this.oPanelConfig.luckOnlyPlayer || false;
const onlyNumbers = this.oPanelConfig.luckOnlyNumbers || false;
const tracker = window.lootDropsTrackerInstance;
const currentUserName = tracker ? tracker.userName : '';
const playersData = [];
playerItems.forEach(item => {
const nameSpan = item.querySelector('.lucky-big-luck-name');
const percentSpan = item.querySelector('.lucky-big-luck-percent');
if (nameSpan && percentSpan) {
const playerName = nameSpan.textContent;
const percent = percentSpan.textContent;
const color = percentSpan.style.color || '#a8aed4';
const isCurrentUser = playerName === currentUserName;
playersData.push({
name: playerName,
percent: percent,
color: color,
isCurrentUser: isCurrentUser
});
}
});
playersData.sort((a, b) => {
if (a.isCurrentUser && !b.isCurrentUser) return -1;
if (!a.isCurrentUser && b.isCurrentUser) return 1;
return 0;
});
let html = '';
const filteredPlayers = onlyPlayer ? playersData.filter(p => p.isCurrentUser) : playersData;
filteredPlayers.forEach(player => {
const nameColor = this.getPlayerNameColorByName(player.name);
if (onlyPlayer && onlyNumbers) {
html += `<div style="display: flex; justify-content: space-between; gap: 4px;">`;
html += `<span style="padding-bottom:3px;gap: 4px;color: ${player.color};">${player.percent}</span>`;
html += `</div>`;
} else if (onlyNumbers) {
html += `<div style="display: flex; justify-content: space-between; gap: 4px;">`;
html += `<span style="padding-bottom:3px;gap: 4px;color: ${player.color};">${player.percent}</span>`;
html += `</div>`;
} else if (onlyPlayer) {
html += `<div style="display: flex; justify-content: space-between; gap: 4px;">`;
html += `<span style="padding-bottom:3px;gap: 4px;color: ${nameColor};">${player.name}</span>`;
html += `<span style="padding-bottom:3px;gap: 4px;color: ${player.color};">${player.percent}</span>`;
html += `</div>`;
} else {
html += `<div style="display: flex; justify-content: space-between; gap: 4px;">`;
html += `<span style="padding-bottom:3px;gap: 4px;color: ${nameColor};">${player.name}</span>`;
html += `<span style="padding-bottom:3px;gap: 4px;color: ${player.color};">${player.percent}</span>`;
html += `</div>`;
}
});
luckDisplay.innerHTML = html || '<div style="color: #888;">No data</div>';
}
updateOPanelDeaths() {
const deathsDisplay = document.getElementById('opanel-deaths-hr');
if (!deathsDisplay) {
return;
}
const ihurtDeathsSpan = document.getElementById('ihurt-deaths-per-hour');
if (ihurtDeathsSpan) {
deathsDisplay.textContent = ihurtDeathsSpan.textContent;
} else {
deathsDisplay.textContent = '0.0 deaths/hr';
}
}
updateOPanelHouses() {
const housesDisplay = document.getElementById('opanel-houses');
if (!housesDisplay) return;
const affordability = this.jhouseAffordabilityResult;
if (!affordability) {
housesDisplay.innerHTML = '<span style="white-space: nowrap; color: #888;">No house data</span>';
return;
}
let html = '';
const count = affordability.count ?? 0;
const countColor = count > 0 ? '#4CAF50' : '#ff4444';
html += `<span style="white-space: nowrap; color: ${countColor};">${count} houses affordable</span>`;
if (!affordability.allMaxed && affordability.cheapestAsk !== null) {
const roomDisplayName = affordability.cheapestRoom
? affordability.cheapestRoom.replace(/_/g, ' ').split(' ').map(word => word.charAt(0).toUpperCase() + word.slice(1)).join(' ')
: '';
if (roomDisplayName) {
html += `<span style="white-space: nowrap; color: #FFD700;">${roomDisplayName}</span>`;
}
const cheapestText = this.formatGold ? this.formatGold(affordability.cheapestAsk) : affordability.cheapestAsk.toLocaleString();
html += `<span style="white-space: nowrap; color: #888;">Cheapest: ${cheapestText}</span>`;
} else if (affordability.allMaxed) {
html += `<span style="white-space: nowrap; color: #FFD700;">All maxed!</span>`;
}
housesDisplay.innerHTML = html;
}
updateOPanelEWatch() {
const ewatchDisplay = document.getElementById('opanel-ewatch');
if (!ewatchDisplay) return;
const itemSpan = document.getElementById('opanel-ewatch-item');
const remainingSpan = document.getElementById('opanel-ewatch-remaining');
const timeSpan = document.getElementById('opanel-ewatch-time');
const progressBar = document.getElementById('opanel-ewatch-progress-bar');
const percentSpan = document.getElementById('opanel-ewatch-percent');
const percentInline = document.getElementById('opanel-ewatch-percent-inline');
const displayItem = this.spyHeaderDisplayItem;
if (!displayItem) {
if (itemSpan) itemSpan.textContent = '--';
if (remainingSpan) remainingSpan.textContent = '--';
if (timeSpan) timeSpan.textContent = '--';
if (progressBar) { progressBar.style.width = '0%'; progressBar.style.background = '#6495ED'; }
if (percentSpan) percentSpan.textContent = '0.0%';
if (percentInline) percentInline.textContent = '0.0%';
return;
}
const itemDisplay = displayItem.enhLevel !== ''
? `${displayItem.name} +${displayItem.enhLevel}`
: displayItem.name;
if (itemSpan) {
itemSpan.textContent = itemDisplay;
itemSpan.style.color = displayItem.name === 'Everything' ? '#FFA500' : '#FFD700';
}
if (remainingSpan) {
if (displayItem.stillNeeded > 0) {
const formattedGold = this.formatGoldCompact ? this.formatGoldCompact(displayItem.stillNeeded) : displayItem.stillNeeded.toLocaleString();
remainingSpan.textContent = formattedGold;
remainingSpan.style.display = '';
remainingSpan.style.color = 'white';
} else {
remainingSpan.style.display = 'none';
}
}
if (timeSpan) {
timeSpan.textContent = displayItem.timeStr;
timeSpan.style.color = !displayItem.inCombat ? '#f44336' : (displayItem.percent >= 100 ? '#4CAF50' : '#f5a623');
}
if (progressBar) {
progressBar.style.width = `${displayItem.percent}%`;
progressBar.style.background = displayItem.percent >= 100 ? '#4CAF50' : (displayItem.inCombat ? '#6495ED' : '#f44336');
}
if (percentSpan) {
percentSpan.textContent = `${displayItem.percent.toFixed(1)}%`;
percentSpan.style.color = displayItem.percent >= 100 ? '#4CAF50' : '#888';
}
if (percentInline) {
percentInline.textContent = `${displayItem.percent.toFixed(1)}%`;
percentInline.style.color = displayItem.percent >= 100 ? '#4CAF50' : '#888';
}
}
updateOPanelEWatchLayout() {
const barRow = document.getElementById('opanel-ewatch-bar-row');
const percentSpan = document.getElementById('opanel-ewatch-percent');
const percentInline = document.getElementById('opanel-ewatch-percent-inline');
if (!barRow || !percentSpan || !percentInline) return;
const showBar = this.oPanelConfig.ewatchShowBar !== false;
if (showBar) {
barRow.style.display = 'flex';
percentInline.style.display = 'none';
} else {
barRow.style.display = 'none';
percentInline.style.display = '';
}
}
updateOPanelCombatStatus() {
const combatStatusDisplay = document.getElementById('opanel-combat-status');
if (!combatStatusDisplay) return;
let inCombat = window.MCS_IN_COMBAT === true;
if (window.lootDropsTrackerInstance?.isLiveSessionActive) {
inCombat = true;
}
combatStatusDisplay.innerHTML = inCombat
? '<span style="color: #4CAF50;">In Combat</span>'
: '<span style="color: #f44336;">No Combat</span>';
}
updateOPanelNtallyInventory() {
const ntallyDisplay = document.getElementById('opanel-ntally-inventory');
if (!ntallyDisplay) return;
const totalAsk = this.mcs_nt_lastTotalAsk ?? 0;
const totalBid = this.mcs_nt_lastTotalBid ?? 0;
const useAskPrice = window.getFlootUseAskPrice ? window.getFlootUseAskPrice() : false;
const ntallyPrice = useAskPrice ? totalAsk : totalBid;
const ntallyLabel = useAskPrice ? 'ask' : 'bid';
const ntallyColor = useAskPrice ? '#6495ED' : '#4CAF50';
const formattedValue = this.mcs_nt_formatNumber ? this.mcs_nt_formatNumber(ntallyPrice) : ntallyPrice.toLocaleString();
ntallyDisplay.innerHTML = `<span>📦</span><span style="color: ${ntallyPrice > 0 ? ntallyColor : '#999'}; font-weight: ${ntallyPrice > 0 ? 'bold' : 'normal'};">${formattedValue} ${ntallyLabel}</span>`;
}
updateOPanelKollectionBuildScore() {
const buildScoreDisplay = document.getElementById('opanel-kollection-build-score');
if (!buildScoreDisplay) return;
const totalScoreElem = document.getElementById('kollection-total-score');
if (totalScoreElem) {
buildScoreDisplay.textContent = `Build Score: ${totalScoreElem.textContent}`;
} else {
buildScoreDisplay.textContent = 'Build Score: --';
}
}
updateOPanelKollectionNetWorth() {
const netWorthDisplay = document.getElementById('opanel-kollection-net-worth');
if (!netWorthDisplay) return;
const netWorthElem = document.getElementById('kollection-networth-total');
if (netWorthElem) {
netWorthDisplay.textContent = `Net Worth: ${netWorthElem.textContent}`;
} else {
netWorthDisplay.textContent = 'Net Worth: --';
}
}
updateOPanelEwatchCoins() {
const coinsDisplay = document.getElementById('opanel-ewatch-coins');
if (!coinsDisplay) return;
let coinValue = 0;
if (this.spyCharacterItems) {
const coinItem = this.spyCharacterItems.find(item => item.itemHrid === '/items/coin');
if (coinItem && coinItem.count) {
coinValue = coinItem.count;
}
}
const formattedValue = this.mcs_nt_formatNumber ? this.mcs_nt_formatNumber(coinValue) : coinValue.toLocaleString();
coinsDisplay.innerHTML = `<span style="width: 11px; height: 11px; display: inline-flex;">${createItemIconHtml('coin', { width: '100%', height: '100%', sprite: 'items_sprite' })}</span><span style="color: ${coinValue > 0 ? '#FFD700' : '#999'}; font-weight: ${coinValue > 0 ? 'bold' : 'normal'};">${formattedValue}</span>`;
}
updateOPanelEwatchMarket() {
const marketDisplay = document.getElementById('opanel-ewatch-market');
if (!marketDisplay) return;
const marketTotal = this.mcs_nt_lastMarketTotal ?? 0;
const formattedValue = this.mcs_nt_formatNumber ? this.mcs_nt_formatNumber(marketTotal) : marketTotal.toLocaleString();
marketDisplay.innerHTML = `<span>📈</span><span style="color: ${marketTotal > 0 ? '#FFD700' : '#999'}; font-weight: ${marketTotal > 0 ? 'bold' : 'normal'};">${formattedValue}</span>`;
}
updateOPanelSkillBooks() {
const skillBooksDisplay = document.getElementById('opanel-skill-books');
if (!skillBooksDisplay) return;
const skillBooksInfo = window.mcs_breadCheapestSkill;
if (!skillBooksInfo || !skillBooksInfo.iconId || !skillBooksInfo.books || skillBooksInfo.books <= 0) {
if (this._lastSkillBooksKey !== null) {
skillBooksDisplay.innerHTML = '<span style="color: #999;">No skill data</span>';
this._lastSkillBooksKey = null;
}
return;
}
const key = skillBooksInfo.iconId + '|' + skillBooksInfo.books + '|' + skillBooksInfo.cost;
if (key === this._lastSkillBooksKey) return;
this._lastSkillBooksKey = key;
const itemHrid = '/items/' + skillBooksInfo.iconId;
const newContent = `
${createItemIconHtml(skillBooksInfo.iconId, { width: 18, height: 18, className: 'opanel-bread-icon-clickable', clickable: true, itemHrid, style: 'flex-shrink: 0; pointer-events: auto' })}
<span style="color: #4CAF50; font-weight: bold;">${skillBooksInfo.books}</span>
<span style="color: #aaa;">books</span>
<span style="color: #FFD700;">${mcsFormatCurrency(skillBooksInfo.cost, 'cost')}</span>
`;
skillBooksDisplay.innerHTML = newContent;
if (!skillBooksDisplay.dataset.clickListenerAttached) {
skillBooksDisplay.addEventListener('click', (e) => {
let icon = e.target;
if (icon.tagName && icon.tagName.toUpperCase() === 'USE') {
icon = icon.parentElement;
}
if (!icon.classList || !icon.classList.contains('opanel-bread-icon-clickable')) {
icon = e.target.closest('.opanel-bread-icon-clickable');
}
if (icon && icon.classList && icon.classList.contains('opanel-bread-icon-clickable')) {
const itemHrid = icon.getAttribute('data-item-hrid');
if (itemHrid) {
e.stopPropagation();
mcsGoToMarketplace(itemHrid);
}
}
});
skillBooksDisplay.dataset.clickListenerAttached = 'true';
}
}
// OPanel end
// CRackData start
get crStorage() {
if (!this._crStorage) {
this._crStorage = createModuleStorage('CR');
}
return this._crStorage;
}
loadAbilityDetailMap() {
return InitClientDataCache.getAbilityDetailMap();
}
getDefaultConsumed(itemHrid) {
const name = itemHrid.toLowerCase();
if (name.includes('coffee')) return 2;
if (name.includes('donut') || name.includes('cupcake') || name.includes('cake') || name.includes('gummy') || name.includes('yogurt')) return 10;
return 0;
}
calcElapsedSeconds(tracker) {
const now = Date.now();
const start = tracker.actualStartTime || tracker.startTime || now;
const saved = tracker.savedPausedMs ?? 0;
const paused = (window.MCS_TOTAL_PAUSED_MS ?? 0) - saved;
const current = window.MCS_MODULES_DISABLED && window.MCS_PAUSE_START_TIME
? (Date.now() - window.MCS_PAUSE_START_TIME) : 0;
return Math.max(0, ((now - start) - paused - current) / 1000);
}
calcElapsedMs(tracker) {
return this.calcElapsedSeconds(tracker) * 1000;
}
createPartyTracker() {
return {
actualConsumed: {},
defaultConsumed: {},
inventoryAmount: {},
startTime: Date.now(),
actualStartTime: Date.now(),
lastUpdate: null,
savedPausedMs: window.MCS_TOTAL_PAUSED_MS ?? 0
};
}
resetConsumableTracking() {
const cachedData = CharacterDataStorage.get();
this.consumableTracker = {
actualConsumed: {},
defaultConsumed: {},
inventoryAmount: {},
startTime: Date.now(),
actualStartTime: Date.now(),
lastUpdate: null,
savedPausedMs: window.MCS_TOTAL_PAUSED_MS ?? 0
};
this.partyConsumableTracker = {};
this.partyConsumableSnapshots = {};
this.crStorage.set('consumable_tracker', null);
this.crStorage.set('party_tracker', null);
this.crStorage.set('party_snapshots', null);
this.crStorage.set('last_battle', null);
if (cachedData) {
const foodSlots = cachedData.actionTypeFoodSlotsMap?.['/action_types/combat'] ?? [];
const drinkSlots = cachedData.actionTypeDrinkSlotsMap?.['/action_types/combat'] ?? [];
[...foodSlots, ...drinkSlots].forEach(slot => {
if (slot && slot.itemHrid) {
const inventoryItem = cachedData.characterItems.find(item =>
item.itemHrid === slot.itemHrid &&
item.itemLocationHrid === '/item_locations/inventory'
);
if (inventoryItem) {
this.consumableTracker.inventoryAmount[slot.itemHrid] = inventoryItem.count;
this.consumableTracker.actualConsumed[slot.itemHrid] = 0;
this.consumableTracker.defaultConsumed[slot.itemHrid] = this.getDefaultConsumed(slot.itemHrid);
}
}
});
if (this.lastBattleData && this.lastBattleData.players) {
const currentPlayerName = cachedData.character?.name || this.spyCharacterName || '';
this.lastBattleData.players.forEach(player => {
if (player.name === currentPlayerName) return;
if (player.combatConsumables && player.combatConsumables.length > 0) {
this.partyConsumableTracker[player.name] = this.createPartyTracker();
player.combatConsumables.forEach(consumable => {
if (consumable && consumable.itemHrid && consumable.count) {
this.partyConsumableTracker[player.name].inventoryAmount[consumable.itemHrid] = consumable.count;
this.partyConsumableTracker[player.name].actualConsumed[consumable.itemHrid] = 0;
this.partyConsumableTracker[player.name].defaultConsumed[consumable.itemHrid] = this.getDefaultConsumed(consumable.itemHrid);
}
});
}
});
}
this.saveConsumableTracking();
}
this.consumableResetPending = false;
this.consumableResetBattleCount = 0;
this.consumableResetItems = [];
this.consumableResetPartyItems = {};
this.updateConsumablesDisplay();
}
estimateTimeToZeroForPartyMemberTracked(playerName, itemHrid, currentCount) {
if (!this.partyConsumableTracker[playerName]) {
return null;
}
const tracker = this.partyConsumableTracker[playerName];
const actualConsumed = tracker.actualConsumed[itemHrid] ?? 0;
let defaultConsumed = tracker.defaultConsumed[itemHrid] ?? 0;
if (defaultConsumed === 0) {
defaultConsumed = this.getDefaultConsumed(itemHrid);
if (defaultConsumed === 0) return null;
tracker.defaultConsumed[itemHrid] = defaultConsumed;
}
const actualElapsedSeconds = this.calcElapsedSeconds(tracker);
const DEFAULT_TIME = 10 * 60;
const actualRate = actualElapsedSeconds > 0 ? (actualConsumed / actualElapsedSeconds) : 0;
const combinedRate = (defaultConsumed + actualConsumed) / (DEFAULT_TIME + actualElapsedSeconds);
const consumptionRate = (actualRate * 0.9) + (combinedRate * 0.1);
if (consumptionRate === 0) {
return null;
}
const actualCurrentAmount = tracker.inventoryAmount[itemHrid] !== undefined
? tracker.inventoryAmount[itemHrid]
: currentCount;
const remainingSeconds = actualCurrentAmount / consumptionRate;
const consumedPerDay = Math.ceil((consumptionRate * 86400));
return { remainingSeconds, consumedPerDay };
}
initConsumableTracking() {
const saved = this.crStorage.get('consumable_tracker');
if (saved) {
try {
if (saved.consumedCount && !saved.actualConsumed) {
const migratedDefault = {};
const migratedActual = {};
Object.keys(saved.consumedCount ?? {}).forEach(itemHrid => {
migratedDefault[itemHrid] = this.getDefaultConsumed(itemHrid);
migratedActual[itemHrid] = 0;
});
this.consumableTracker = {
actualConsumed: migratedActual,
defaultConsumed: migratedDefault,
inventoryAmount: saved.inventoryAmount ?? {},
startTime: Date.now(),
actualStartTime: Date.now(),
lastUpdate: null,
savedPausedMs: window.MCS_TOTAL_PAUSED_MS ?? 0
};
this.saveSoloConsumableTracking();
} else if (saved.usageCounts || saved.currentCounts) {
const migratedDefault = {};
const migratedActual = {};
Object.keys(saved.usageCounts ?? {}).forEach(itemHrid => {
migratedDefault[itemHrid] = this.getDefaultConsumed(itemHrid);
migratedActual[itemHrid] = 0;
});
this.consumableTracker = {
actualConsumed: migratedActual,
defaultConsumed: migratedDefault,
inventoryAmount: saved.currentCounts ?? {},
startTime: Date.now(),
actualStartTime: Date.now(),
lastUpdate: null,
savedPausedMs: window.MCS_TOTAL_PAUSED_MS ?? 0
};
this.saveSoloConsumableTracking();
} else if (saved.startCounts && !saved.actualConsumed) {
this.consumableTracker = {
actualConsumed: {},
defaultConsumed: {},
inventoryAmount: {},
startTime: Date.now(),
actualStartTime: Date.now(),
lastUpdate: null,
savedPausedMs: window.MCS_TOTAL_PAUSED_MS ?? 0
};
this.saveSoloConsumableTracking();
} else {
const now = Date.now();
const elapsedMs = saved.elapsedTrackingMs ?? 0;
this.consumableTracker = {
actualConsumed: saved.actualConsumed ?? {},
defaultConsumed: saved.defaultConsumed ?? {},
inventoryAmount: saved.inventoryAmount ?? {},
startTime: now - elapsedMs,
actualStartTime: now - elapsedMs,
lastUpdate: saved.lastUpdate || null,
savedPausedMs: window.MCS_TOTAL_PAUSED_MS ?? 0
};
}
} catch (e) {
console.error('[Consumables] Failed to load tracker:', e);
}
}
if (!this.consumableTracker) {
this.consumableTracker = {
actualConsumed: {},
defaultConsumed: {},
inventoryAmount: {},
startTime: null,
actualStartTime: null,
lastUpdate: null,
savedPausedMs: window.MCS_TOTAL_PAUSED_MS ?? 0
};
}
const savedPartyTracker = this.crStorage.get('party_tracker');
if (savedPartyTracker) {
try {
const now = Date.now();
this.partyConsumableTracker = {};
Object.keys(savedPartyTracker).forEach(playerName => {
const playerTracker = savedPartyTracker[playerName];
if (playerTracker.actualConsumed && playerTracker.defaultConsumed &&
playerTracker.inventoryAmount && typeof playerTracker.actualConsumed === 'object') {
const elapsedMs = playerTracker.elapsedTrackingMs ?? 0;
this.partyConsumableTracker[playerName] = {
actualConsumed: playerTracker.actualConsumed ?? {},
defaultConsumed: playerTracker.defaultConsumed ?? {},
inventoryAmount: playerTracker.inventoryAmount ?? {},
startTime: now - elapsedMs,
actualStartTime: now - elapsedMs,
lastUpdate: playerTracker.lastUpdate || null,
savedPausedMs: window.MCS_TOTAL_PAUSED_MS ?? 0
};
} else {
}
});
} catch (e) {
console.error('[Consumables] Failed to load party tracker:', e);
this.partyConsumableTracker = {};
}
} else {
this.partyConsumableTracker = {};
}
const savedPartySnapshots = this.crStorage.get('party_snapshots');
if (savedPartySnapshots) {
try {
this.partyConsumableSnapshots = savedPartySnapshots;
} catch (e) {
console.error('[Consumables] Failed to load party snapshots:', e);
this.partyConsumableSnapshots = {};
}
} else {
this.partyConsumableSnapshots = {};
}
const savedBattleData = this.crStorage.get('last_battle');
if (savedBattleData) {
try {
this.lastBattleData = savedBattleData;
} catch (e) {
console.error('[Consumables] Failed to load last battle data:', e);
}
}
const savedLastKnown = this.crStorage.get('party_last_known');
if (savedLastKnown) {
try {
this.partyLastKnownConsumables = savedLastKnown;
} catch (e) {
console.error('[Consumables] Failed to load last known consumables:', e);
this.partyLastKnownConsumables = {};
}
} else {
this.partyLastKnownConsumables = {};
}
}
saveSoloConsumableTracking() {
if (this.consumableTracker) {
try {
const elapsedTrackingMs = this.calcElapsedMs(this.consumableTracker);
const trackerToSave = {
actualConsumed: this.consumableTracker.actualConsumed ?? {},
defaultConsumed: this.consumableTracker.defaultConsumed ?? {},
inventoryAmount: this.consumableTracker.inventoryAmount ?? {},
elapsedTrackingMs: Math.max(0, elapsedTrackingMs),
lastUpdate: this.consumableTracker.lastUpdate || null,
savedPausedMs: window.MCS_TOTAL_PAUSED_MS ?? 0,
saveTimestamp: Date.now()
};
this.crStorage.set('consumable_tracker', trackerToSave);
} catch (e) {
console.error('[Consumables] Failed to save solo tracker:', e);
console.error('[Consumables] Tracker data:', this.consumableTracker);
}
}
}
savePartyConsumableTracking() {
if (this.partyConsumableTracker) {
try {
const partyTrackerToSave = {};
Object.keys(this.partyConsumableTracker).forEach(playerName => {
const tracker = this.partyConsumableTracker[playerName];
if (tracker && tracker.actualConsumed && tracker.defaultConsumed && tracker.inventoryAmount) {
partyTrackerToSave[playerName] = {
actualConsumed: tracker.actualConsumed ?? {},
defaultConsumed: tracker.defaultConsumed ?? {},
inventoryAmount: tracker.inventoryAmount ?? {},
elapsedTrackingMs: Math.max(0, this.calcElapsedMs(tracker)),
lastUpdate: tracker.lastUpdate || null,
savedPausedMs: window.MCS_TOTAL_PAUSED_MS ?? 0,
saveTimestamp: Date.now()
};
}
});
this.crStorage.set('party_tracker', partyTrackerToSave);
} catch (e) {
console.error('[Consumables] Failed to save party tracker:', e);
console.error('[Consumables] Party tracker data:', this.partyConsumableTracker);
}
}
if (this.partyConsumableSnapshots) {
try {
this.crStorage.set('party_snapshots', this.partyConsumableSnapshots);
} catch (e) {
console.error('[Consumables] Failed to save party snapshots:', e);
}
}
if (this.lastBattleData) {
try {
this.crStorage.set('last_battle', this.lastBattleData);
} catch (e) {
console.error('[Consumables] Failed to save last battle data:', e);
}
}
if (this.partyLastKnownConsumables) {
try {
this.crStorage.set('party_last_known', this.partyLastKnownConsumables);
} catch (e) {
console.error('[Consumables] Failed to save last known consumables:', e);
}
}
}
saveConsumableTracking() {
this.saveSoloConsumableTracking();
this.savePartyConsumableTracking();
}
saveCrackInventory() {
if (this.consumableTracker && this.consumableTracker.inventoryAmount) {
this.crStorage.set('consumable_inventory', this.consumableTracker.inventoryAmount);
}
}
estimateTimeToZero(itemHrid, currentCount) {
if (!this.consumableTracker || !this.consumableTracker.startTime) {
return null;
}
const actualConsumed = this.consumableTracker.actualConsumed[itemHrid] ?? 0;
const defaultConsumed = this.consumableTracker.defaultConsumed[itemHrid] ?? 0;
const actualElapsedSeconds = this.calcElapsedSeconds(this.consumableTracker);
const DEFAULT_TIME = 10 * 60;
const actualRate = actualElapsedSeconds > 0 ? (actualConsumed / actualElapsedSeconds) : 0;
const combinedRate = (defaultConsumed + actualConsumed) / (DEFAULT_TIME + actualElapsedSeconds);
const consumptionRate = (actualRate * 0.9) + (combinedRate * 0.1);
if (consumptionRate === 0) {
return null;
}
const actualCurrentAmount = this.consumableTracker.inventoryAmount[itemHrid] !== undefined
? this.consumableTracker.inventoryAmount[itemHrid]
: currentCount;
const remainingSeconds = actualCurrentAmount / consumptionRate;
const consumedPerDay = Math.ceil((consumptionRate * 86400));
return { remainingSeconds, consumedPerDay };
}
formatTimeRemaining(seconds) {
return mcsFormatDuration(seconds, 'eta');
}
formatTimeShort(seconds) {
return mcsFormatDuration(seconds, 'short');
}
async fetchMarketJSON() {
return mcsGetMarketData();
}
getItemPrices(itemHrid, price_data) {
if (!price_data || !price_data.marketData) return null;
const item_price_data = price_data.marketData[itemHrid];
if (!item_price_data || !item_price_data[0]) return null;
const ask = item_price_data[0].a;
const bid = item_price_data[0].b;
if ((ask <= 0 || ask === -1) && (bid <= 0 || bid === -1)) return null;
return {
ask: ask > 0 ? ask : 0,
bid: bid > 0 ? bid : 0
};
}
formatPriceShort(price) {
return mcsFormatCurrency(price, 'short');
}
// CRackData end
// CRackUI start
renderConsumableRow({ itemHrid, count, tracker, iconId, itemName, countColorClass, nameColorClass, timeColorClass, extraClasses, extraAttrs, priceData }) {
let html = '';
html += '<div class="mcs-crack-consumable-container">';
html += '<div class="consumable-row ' + (extraClasses || '') + ' mcs-crack-consumable-row" ' + (extraAttrs || '') + ' data-icon-id="' + iconId + '">';
html += '<span class="mcs-crack-stat-count ' + (countColorClass || '') + '">' + count + '</span>';
html += createItemIconHtml(iconId, { className: 'consumable-icon-clickable mcs-crack-consumable-icon', clickable: true, itemHrid: itemHrid });
html += '<span class="mcs-crack-consumable-name ' + (nameColorClass || '') + '">' + itemName + '</span>';
const etaData = tracker._etaData;
if (etaData) {
const actualElapsedSeconds = this.calcElapsedSeconds(tracker);
const actualConsumed = tracker.actualConsumed[itemHrid] ?? 0;
const defaultConsumed = tracker.defaultConsumed[itemHrid] ?? 0;
const combinedConsumed = defaultConsumed + actualConsumed;
const DEFAULT_TIME = 10 * 60;
const combinedTime = Math.floor(DEFAULT_TIME + actualElapsedSeconds);
html += '<div class="mcs-crack-stack-container">';
html += '<div class="mcs-crack-stack-row">';
html += '<span class="mcs-crack-stat-actual" title="90% weight">\u2B06 ' + Math.floor(actualElapsedSeconds) + 's</span>';
html += '<span class="mcs-crack-stat-actual" title="90% weight">\u2B06 ' + actualConsumed + '</span>';
html += '</div>';
html += '<div class="mcs-crack-stack-row">';
html += '<span class="mcs-crack-stat-combined" title="10% weight">\u2B07 ' + combinedTime + 's</span>';
html += '<span class="mcs-crack-stat-combined" title="10% weight">\u2B07 ' + combinedConsumed + '</span>';
html += '</div>';
html += '</div>';
html += '<span class="mcs-crack-per-day">' + etaData.consumedPerDay + '/day</span>';
html += this.generatePriceHTML(itemHrid, priceData, etaData.consumedPerDay);
html += '<span class="mcs-crack-time-remaining ' + (timeColorClass || '') + '">' + this.formatTimeRemaining(etaData.remainingSeconds) + '</span>';
} else {
html += '<span class="mcs-crack-waiting">Waiting...</span>';
}
html += '</div>';
html += '</div>';
return html;
}
createConsumablesPane() {
if (document.getElementById('consumables-pane')) return;
const pane = document.createElement('div');
pane.id = 'consumables-pane';
registerPanel('consumables-pane');
pane.className = 'mcs-crack-pane';
const header = document.createElement('div');
header.className = 'mcs-crack-header';
const leftSection = document.createElement('div');
leftSection.className = 'mcs-crack-header-left';
const titleSpan = document.createElement('span');
titleSpan.className = 'mcs-crack-title';
titleSpan.textContent = 'CRack';
const yourEtaContainer = document.createElement('span');
yourEtaContainer.className = 'mcs-crack-eta-container';
yourEtaContainer.innerHTML = '<span id="consumables-days-left">--</span>';
const partyEtaContainer = document.createElement('span');
partyEtaContainer.id = 'party-eta-container';
partyEtaContainer.className = 'mcs-crack-party-eta-container';
partyEtaContainer.innerHTML = 'Party: <span id="party-min-eta"></span>';
leftSection.appendChild(titleSpan);
leftSection.appendChild(yourEtaContainer);
leftSection.appendChild(partyEtaContainer);
const buttonContainer = document.createElement('div');
buttonContainer.className = 'mcs-crack-button-container';
const resetBtn = document.createElement('button');
resetBtn.className = 'mcs-crack-reset-btn';
resetBtn.textContent = '↻';
resetBtn.title = 'Reset tracking to defaults immediately';
resetBtn.onclick = (e) => {
e.stopPropagation();
this.resetConsumableTracking();
};
const minimizeBtn = document.createElement('button');
minimizeBtn.id = 'consumables-minimize-btn';
minimizeBtn.className = 'mcs-crack-minimize-btn';
minimizeBtn.textContent = '−';
buttonContainer.appendChild(resetBtn);
buttonContainer.appendChild(minimizeBtn);
header.appendChild(leftSection);
header.appendChild(buttonContainer);
const content = document.createElement('div');
content.id = 'consumables-content';
content.className = 'mcs-crack-content';
pane.appendChild(header);
pane.appendChild(content);
document.body.appendChild(pane);
const self = this;
content.addEventListener('click', (e) => {
let target = e.target;
if (target.tagName && target.tagName.toUpperCase() === 'USE') {
target = target.parentElement;
}
if (!target.classList || !target.classList.contains('consumable-icon-clickable')) {
target = e.target.closest('.consumable-icon-clickable');
}
if (target && target.classList && target.classList.contains('consumable-icon-clickable')) {
const itemHrid = target.getAttribute('data-item-hrid');
if (itemHrid) {
self.openConsumableMarketplace(itemHrid);
}
}
});
if (!this.partyConsumableSnapshots) {
this.partyConsumableSnapshots = {};
}
const handleWebSocketMessage = PerformanceMonitor.wrap('CRack', (event) => {
if (window.MCS_MODULES_DISABLED) return;
const data = event.detail;
if (data?.type === 'init_character_data' || data?.type === 'action_type_consumable_slots_updated') {
if (data?.type === 'action_type_consumable_slots_updated') {
const cachedData = CharacterDataStorage.get();
if (cachedData) {
if (data.actionTypeFoodSlotsMap) {
cachedData.actionTypeFoodSlotsMap = data.actionTypeFoodSlotsMap;
}
if (data.actionTypeDrinkSlotsMap) {
cachedData.actionTypeDrinkSlotsMap = data.actionTypeDrinkSlotsMap;
}
}
}
const foodSlots = data.actionTypeFoodSlotsMap?.['/action_types/combat'] ?? [];
const drinkSlots = data.actionTypeDrinkSlotsMap?.['/action_types/combat'] ?? [];
const currentEquippedHrids = new Set();
[...foodSlots, ...drinkSlots].forEach(slot => {
if (slot?.itemHrid) {
currentEquippedHrids.add(slot.itemHrid);
}
});
const previousHrids = self.lastSeenConsumableHrids
|| new Set(Object.keys(self.consumableTracker?.actualConsumed ?? {}));
self.lastSeenConsumableHrids = currentEquippedHrids;
const hasChanges = currentEquippedHrids.size !== previousHrids.size ||
[...currentEquippedHrids].some(hrid => !previousHrids.has(hrid));
if (hasChanges && self.consumableTracker) {
Object.keys(self.consumableTracker.actualConsumed ?? {}).forEach(itemHrid => {
if (!currentEquippedHrids.has(itemHrid)) {
delete self.consumableTracker.actualConsumed[itemHrid];
delete self.consumableTracker.defaultConsumed[itemHrid];
delete self.consumableTracker.inventoryAmount[itemHrid];
}
});
self.updateConsumablesDisplay();
}
}
if (data?.type === 'new_battle') {
if (data.players) {
const cachedData = CharacterDataStorage.get();
if (cachedData) {
const currentPlayerName = cachedData?.character?.name || self.spyCharacterName || '';
const currentPartyMembers = new Set();
data.players.forEach(player => {
if (player?.character?.name && player.character.name !== currentPlayerName) {
currentPartyMembers.add(player.character.name);
}
});
if (self.partyConsumableTracker) {
Object.keys(self.partyConsumableTracker).forEach(playerName => {
if (!currentPartyMembers.has(playerName)) {
delete self.partyConsumableTracker[playerName];
}
});
}
if (self.partyConsumableSnapshots) {
Object.keys(self.partyConsumableSnapshots).forEach(playerName => {
if (!currentPartyMembers.has(playerName)) {
delete self.partyConsumableSnapshots[playerName];
}
});
}
if (self.lastMinTimes) {
Object.keys(self.lastMinTimes).forEach(playerName => {
if (!currentPartyMembers.has(playerName)) {
delete self.lastMinTimes[playerName];
}
});
}
self.savePartyConsumableTracking();
data.players.forEach(player => {
if (!player || !player.character) return;
const playerName = player.character.name;
if (playerName === currentPlayerName) {
return;
}
if (!self.partyConsumableSnapshots[playerName]) {
self.partyConsumableSnapshots[playerName] = {};
}
if (!self.partyLastKnownConsumables) {
self.partyLastKnownConsumables = {};
}
if (!self.partyLastKnownConsumables[playerName]) {
self.partyLastKnownConsumables[playerName] = {};
}
if (!self.partyConsumableTracker) {
self.partyConsumableTracker = {};
}
if (!self.partyConsumableTracker[playerName]) {
self.partyConsumableTracker[playerName] = self.createPartyTracker();
player.combatConsumables.forEach(consumable => {
if (consumable && consumable.itemHrid) {
self.partyConsumableTracker[playerName].actualConsumed[consumable.itemHrid] = 0;
self.partyConsumableTracker[playerName].defaultConsumed[consumable.itemHrid] =
self.getDefaultConsumed(consumable.itemHrid);
}
});
}
const tracker = self.partyConsumableTracker[playerName];
if (player.combatConsumables && player.combatConsumables.length > 0 && tracker) {
const currentConsumableHrids = new Set(
player.combatConsumables
.filter(c => c && c.itemHrid)
.map(c => c.itemHrid)
);
Object.keys(tracker.actualConsumed).forEach(itemHrid => {
if (!currentConsumableHrids.has(itemHrid)) {
delete tracker.actualConsumed[itemHrid];
delete tracker.defaultConsumed[itemHrid];
delete tracker.inventoryAmount[itemHrid];
}
});
}
const currentlySeenHrids = new Set();
if (player.combatConsumables && player.combatConsumables.length > 0) {
player.combatConsumables.forEach(consumable => {
if (!consumable || !consumable.itemHrid) return;
const itemHrid = consumable.itemHrid;
const currentCount = consumable.count;
const previousCount = self.partyConsumableSnapshots[playerName][itemHrid];
currentlySeenHrids.add(itemHrid);
self.partyLastKnownConsumables[playerName][itemHrid] = {
itemHrid: itemHrid,
lastSeenCount: currentCount
};
if (previousCount !== undefined) {
const diff = previousCount - currentCount;
if (diff === 1) {
tracker.actualConsumed[itemHrid] = (tracker.actualConsumed[itemHrid] ?? 0) + 1;
tracker.lastUpdate = Date.now();
}
}
self.partyConsumableSnapshots[playerName][itemHrid] = currentCount;
tracker.inventoryAmount[itemHrid] = currentCount;
});
}
Object.keys(self.partyLastKnownConsumables[playerName] ?? {}).forEach(itemHrid => {
if (!currentlySeenHrids.has(itemHrid)) {
const previousCount = self.partyConsumableSnapshots[playerName][itemHrid];
if (previousCount !== undefined && previousCount > 0) {
tracker.inventoryAmount[itemHrid] = 0;
self.partyConsumableSnapshots[playerName][itemHrid] = 0;
}
}
});
});
self.lastBattleData = data;
self.savePartyConsumableTracking();
self.updateConsumablesDisplay();
}
}
if (self.consumableWaitingForFreshData) {
self.consumableWaitingForFreshData = false;
}
}
if (data?.type === 'battle_consumable_ability_updated') {
if (data.consumable) {
const itemHrid = data.consumable.itemHrid;
if (!self.consumableTracker) {
self.consumableTracker = {
consumedCount: {},
inventoryAmount: {},
startTime: Date.now(),
lastUpdate: null,
savedPausedMs: window.MCS_TOTAL_PAUSED_MS ?? 0
};
}
if (!self.consumableTracker.startTime) {
self.consumableTracker.startTime = Date.now();
}
if (!self.consumableTracker.actualStartTime) {
self.consumableTracker.actualStartTime = Date.now();
}
if (self.consumableTracker.savedPausedMs === undefined) {
self.consumableTracker.savedPausedMs = window.MCS_TOTAL_PAUSED_MS ?? 0;
}
const isFirstTimeSeen = self.consumableTracker.actualConsumed[itemHrid] === undefined;
if (isFirstTimeSeen) {
self.consumableTracker.actualConsumed[itemHrid] = 0;
self.consumableTracker.defaultConsumed[itemHrid] = self.getDefaultConsumed(itemHrid);
}
self.consumableTracker.actualConsumed[itemHrid]++;
self.consumableTracker.lastUpdate = Date.now();
self.saveSoloConsumableTracking();
self.updateConsumablesDisplay();
}
}
});
this._crWsListener = handleWebSocketMessage;
window.addEventListener('EquipSpyWebSocketMessage', handleWebSocketMessage);
const savedConsumablesMinimized = this.crStorage.get('minimized');
this.consumablesIsMinimized = savedConsumablesMinimized === true || savedConsumablesMinimized === 'true';
if (this.consumablesIsMinimized) {
content.classList.add('mcs-hidden');
minimizeBtn.textContent = '+';
}
let wasDraggedAfterMaximize = false;
minimizeBtn.onclick = () => {
this.consumablesIsMinimized = !this.consumablesIsMinimized;
if (this.consumablesIsMinimized) {
content.classList.add('mcs-hidden');
minimizeBtn.textContent = '+';
this.crStorage.set('minimized', true);
this.updateMinimizedSummary();
if (this.consumablesPreMaximizePosition && !wasDraggedAfterMaximize) {
pane.style.top = this.consumablesPreMaximizePosition.top + 'px';
pane.style.left = this.consumablesPreMaximizePosition.left + 'px';
pane.style.right = 'auto';
this.crStorage.set('position', this.consumablesPreMaximizePosition);
this.consumablesPreMaximizePosition = null;
}
} else {
const rect = pane.getBoundingClientRect();
this.consumablesPreMaximizePosition = {
top: rect.top,
left: rect.left
};
wasDraggedAfterMaximize = false;
content.classList.remove('mcs-hidden');
minimizeBtn.textContent = '−';
this.crStorage.set('minimized', false);
const summary = document.getElementById('consumables-minimized-summary');
if (summary) summary.remove();
this.constrainPanelToBoundaries('consumables-pane', 'mcs_CR', true);
}
};
const savedPos = this.crStorage.get('position');
if (savedPos) {
pane.style.top = savedPos.top + 'px';
pane.style.left = savedPos.left + 'px';
pane.style.right = 'auto';
}
DragHandler.makeDraggable(pane, header, 'mcs_CR');
header.addEventListener('dblclick', (e) => {
if (e.target.tagName === 'BUTTON') return;
minimizeBtn.click();
});
this.updateConsumablesDisplay();
}
async updateMinimizedSummary() {
if (this.isUpdatingMinimizedSummary) return;
this.isUpdatingMinimizedSummary = true;
try {
const content = document.getElementById('consumables-content');
if (!content || !content.classList.contains('mcs-hidden')) {
this.isUpdatingMinimizedSummary = false;
return;
}
const pane = document.getElementById('consumables-pane');
if (!pane) {
this.isUpdatingMinimizedSummary = false;
return;
}
const cachedData = CharacterDataStorage.get();
if (!cachedData) {
this.isUpdatingMinimizedSummary = false;
return;
}
const foodSlots = cachedData.actionTypeFoodSlotsMap?.['/action_types/combat'] ?? [];
const drinkSlots = cachedData.actionTypeDrinkSlotsMap?.['/action_types/combat'] ?? [];
const inventoryByHrid = new Map();
if (cachedData.characterItems) {
for (const item of cachedData.characterItems) {
if (item.itemLocationHrid === '/item_locations/inventory') {
inventoryByHrid.set(item.itemHrid, item);
}
}
}
const allConsumables = [];
[...foodSlots, ...drinkSlots].forEach(slot => {
if (slot && slot.itemHrid) {
const inventoryItem = inventoryByHrid.get(slot.itemHrid);
if (inventoryItem) {
allConsumables.push({
itemHrid: slot.itemHrid,
count: inventoryItem.count
});
}
}
});
if (allConsumables.length === 0) {
this.isUpdatingMinimizedSummary = false;
return;
}
let minTime = Infinity;
const itemsWithTime = [];
allConsumables.forEach(item => {
const etaData = this.estimateTimeToZero(item.itemHrid, item.count);
if (etaData) {
const roundedSeconds = Math.round(etaData.remainingSeconds / 10) * 10;
if (roundedSeconds < minTime) {
minTime = roundedSeconds;
}
}
itemsWithTime.push({ item, etaData });
});
this.updateConsumablesDaysLeft(minTime);
const minItems = [];
if (minTime === Infinity) {
itemsWithTime.forEach(({ item }) => {
minItems.push({ itemHrid: item.itemHrid, count: item.count });
});
} else {
itemsWithTime.forEach(({ item, etaData }) => {
if (etaData && Math.abs(etaData.remainingSeconds - minTime) <= 60) {
minItems.push({ itemHrid: item.itemHrid, count: item.count });
}
});
}
if (minItems.length === 0) {
this.isUpdatingMinimizedSummary = false;
return;
}
const minCount = minItems[0].count;
this.yourMinTime = minTime;
this.minConsumables = minItems;
this.minConsumablesCount = minCount;
let totalAsk = 0;
let totalBid = 0;
const price_data = await this.fetchMarketJSON();
if (price_data) {
allConsumables.forEach(item => {
const etaData = this.estimateTimeToZero(item.itemHrid, item.count);
if (etaData) {
const prices = this.getItemPrices(item.itemHrid, price_data);
if (prices && etaData.consumedPerDay) {
totalAsk += prices.ask * etaData.consumedPerDay;
totalBid += prices.bid * etaData.consumedPerDay;
}
}
});
}
this.totalConsumableAsk = totalAsk;
this.totalConsumableBid = totalBid;
const summaryKey = minItems.map(i => i.itemHrid).sort().join('|');
const existing = document.getElementById('consumables-minimized-summary');
if (existing && this._lastMinSummaryKey === summaryKey) {
const countEl = existing.querySelector('.mcs-crack-summary-count');
if (countEl) countEl.textContent = minCount;
const nameEl = existing.querySelector('.mcs-crack-summary-name');
if (nameEl && minItems.length === 1) {
nameEl.textContent = this.getSpyItemName(minItems[0].itemHrid);
}
const costEl = existing.querySelector('.mcs-crack-summary-cost-value');
if (costEl) {
costEl.textContent = 'Ask: ' + this.formatPriceShort(totalAsk) + ' / Bid: ' + this.formatPriceShort(totalBid);
}
} else {
if (existing) existing.remove();
this._lastMinSummaryKey = summaryKey;
const summary = document.createElement('div');
summary.id = 'consumables-minimized-summary';
summary.className = 'mcs-crack-minimized-summary';
const leftSide = document.createElement('div');
leftSide.className = 'mcs-crack-summary-left';
const countSpan = document.createElement('span');
countSpan.className = 'mcs-crack-summary-count';
countSpan.textContent = minCount;
leftSide.appendChild(countSpan);
minItems.forEach(item => {
const iconId = item.itemHrid.split('/').pop();
const svgIcon = createItemIcon(iconId, { className: 'mcs-crack-consumable-icon consumable-icon-clickable', clickable: true, itemHrid: item.itemHrid });
svgIcon.addEventListener('click', (e) => {
e.stopPropagation();
mcsGoToMarketplace(item.itemHrid);
});
leftSide.appendChild(svgIcon);
});
if (minItems.length === 1) {
const nameSpan = document.createElement('span');
nameSpan.className = 'mcs-crack-summary-name';
nameSpan.textContent = this.getSpyItemName(minItems[0].itemHrid);
leftSide.appendChild(nameSpan);
}
summary.appendChild(leftSide);
const rightSide = document.createElement('div');
rightSide.className = 'mcs-crack-summary-right';
rightSide.innerHTML = `
<div class="mcs-crack-summary-cost-label">Total Cost/Day:</div>
<div class="mcs-crack-summary-cost-value">Ask: ${this.formatPriceShort(totalAsk)} / Bid: ${this.formatPriceShort(totalBid)}</div>
`;
summary.appendChild(rightSide);
const header = pane.querySelector('div:first-child');
if (header && header.nextSibling) {
pane.insertBefore(summary, header.nextSibling);
} else {
pane.appendChild(summary);
}
}
this.isUpdatingMinimizedSummary = false;
} catch (error) {
console.error('[Consumables] Error in updateMinimizedSummary:', error);
this.isUpdatingMinimizedSummary = false;
}
}
updateConsumablesDaysLeft(minTimeSeconds) {
const daysLeftSpan = document.getElementById('consumables-days-left');
if (!daysLeftSpan) return;
if (minTimeSeconds === Infinity || minTimeSeconds === null || minTimeSeconds === undefined) return;
daysLeftSpan.textContent = mcsFormatDuration(minTimeSeconds, 'eta');
const days = minTimeSeconds / 86400;
daysLeftSpan.classList.remove('mcs-crack-eta-critical', 'mcs-crack-eta-warning', 'mcs-crack-eta-good');
if (days < 1) {
daysLeftSpan.classList.add('mcs-crack-eta-critical');
} else if (days < 2) {
daysLeftSpan.classList.add('mcs-crack-eta-warning');
} else {
daysLeftSpan.classList.add('mcs-crack-eta-good');
}
}
generatePriceHTML(itemHrid, price_data, consumedPerDay) {
const prices = this.getItemPrices(itemHrid, price_data);
if (!prices) return '';
const dailyAskCost = prices.ask * consumedPerDay;
const dailyBidCost = prices.bid * consumedPerDay;
const askStr = this.formatPriceShort(dailyAskCost);
const bidStr = this.formatPriceShort(dailyBidCost);
return `<span class="mcs-crack-price-text">
<span>Ask: ${askStr}</span>
<span>Bid: ${bidStr}</span>
</span>`;
}
async updateConsumablesDisplay() {
const price_data = await this.fetchMarketJSON();
if (this.consumableWaitingForFreshData) {
const content = document.getElementById('consumables-content');
if (!content) return;
let html = '';
html += '<div class="mcs-crack-reset-complete">';
html += '<div class="mcs-crack-reset-title">✅ Reset Complete!</div>';
html += '<div class="mcs-crack-reset-waiting">⏳ Waiting for fresh data...</div>';
html += '<div class="mcs-crack-reset-subtitle">Next battle will initialize tracking</div>';
html += '</div>';
if (this.consumableResetItems && this.consumableResetItems.length > 0) {
html += '<div class="mcs-crack-party-name">Your Consumables:</div>';
this.consumableResetItems.forEach(item => {
const itemName = this.getSpyItemName(item.itemHrid);
const iconId = item.itemHrid.split('/').pop();
html += '<div class="consumable-row mcs-crack-consumable-row mcs-crack-consumable-container" data-icon-id="' + iconId + '">';
html += '<span class="mcs-crack-consumable-count mcs-crack-count-placeholder">--</span>';
html += '<span class="icon-placeholder consumable-icon-clickable mcs-crack-consumable-icon" data-item-hrid="' + item.itemHrid + '" title="Click to open in marketplace"></span>';
html += '<span class="mcs-crack-consumable-name mcs-crack-name-placeholder">' + itemName + '</span>';
html += '</div>';
});
}
content.innerHTML = html;
const self = this;
setTimeout(() => {
if (self.loadConsumableIcons) {
self.loadConsumableIcons();
}
}, 100);
return;
}
const content = document.getElementById('consumables-content');
if (!content) return;
let html = '';
try {
const cachedData = CharacterDataStorage.get();
if (!cachedData) {
content.innerHTML = '<div class="mcs-crack-no-data">No data</div>';
return;
}
if (!cachedData.actionTypeFoodSlotsMap || !cachedData.actionTypeDrinkSlotsMap) {
content.innerHTML = '<div class="mcs-crack-no-data">No consumables</div>';
return;
}
const foodSlots = cachedData.actionTypeFoodSlotsMap['/action_types/combat'] ?? [];
const drinkSlots = cachedData.actionTypeDrinkSlotsMap['/action_types/combat'] ?? [];
const allConsumables = [];
foodSlots.forEach(food => {
if (food && food.itemHrid) {
const inventoryItem = cachedData.characterItems.find(item => item.itemHrid === food.itemHrid &&
item.itemLocationHrid === '/item_locations/inventory'
);
if (inventoryItem) {
allConsumables.push({
itemHrid: food.itemHrid,
count: inventoryItem.count,
type: 'food'
});
}
}
});
drinkSlots.forEach(drink => {
if (drink && drink.itemHrid) {
const inventoryItem = cachedData.characterItems.find(item => item.itemHrid === drink.itemHrid &&
item.itemLocationHrid === '/item_locations/inventory'
);
if (inventoryItem) {
allConsumables.push({
itemHrid: drink.itemHrid,
count: inventoryItem.count,
type: 'drink'
});
}
}
});
if (allConsumables.length === 0) {
content.innerHTML = '<div class="mcs-crack-no-data">No consumables equipped</div>';
return;
}
if (!this.consumableTracker || !this.consumableTracker.startTime) {
this.initConsumableTracking();
if (!this.consumableTracker.startTime) {
this.consumableTracker.startTime = Date.now();
allConsumables.forEach(item => {
this.consumableTracker.inventoryAmount[item.itemHrid] = item.count;
if (!this.consumableTracker.actualConsumed[item.itemHrid]) {
this.consumableTracker.actualConsumed[item.itemHrid] = 0;
}
if (!this.consumableTracker.defaultConsumed[item.itemHrid]) {
this.consumableTracker.defaultConsumed[item.itemHrid] = this.getDefaultConsumed(item.itemHrid);
}
});
this.saveConsumableTracking();
}
} else {
const crackData = this.crStorage.get('consumable_inventory');
allConsumables.forEach(item => {
const newAmount = (crackData && crackData[item.itemHrid] !== undefined)
? crackData[item.itemHrid]
: item.count;
this.consumableTracker.inventoryAmount[item.itemHrid] = newAmount;
if (this.consumableTracker.actualConsumed[item.itemHrid] === undefined) {
this.consumableTracker.actualConsumed[item.itemHrid] = 0;
this.consumableTracker.defaultConsumed[item.itemHrid] = this.getDefaultConsumed(item.itemHrid);
}
});
}
let minTime = Infinity;
const itemsWithTime = [];
allConsumables.forEach(item => {
const itemName = this.getSpyItemName(item.itemHrid);
const iconId = item.itemHrid.split('/').pop();
const etaData = this.estimateTimeToZero(item.itemHrid, item.count);
if (etaData) {
const roundedSeconds = Math.round(etaData.remainingSeconds / 30) * 30;
if (roundedSeconds < minTime) {
minTime = roundedSeconds;
}
}
itemsWithTime.push({ item, itemName, iconId, etaData });
});
this.updateConsumablesDaysLeft(minTime);
this.yourMinTime = minTime;
let entirePartyMinTime = minTime;
const battleObj = this.lastBattleData;
if (battleObj) {
try {
if (battleObj.players && battleObj.players.length > 1) {
const currentPlayerName = cachedData?.character?.name || this.spyCharacterName || '';
battleObj.players.forEach(player => {
if (player.name === currentPlayerName) return;
if (player.combatConsumables && player.combatConsumables.length > 0) {
player.combatConsumables.forEach(consumable => {
if (consumable && consumable.itemHrid && consumable.count) {
const etaData = this.estimateTimeToZeroForPartyMemberTracked(player.name, consumable.itemHrid, consumable.count);
if (etaData) {
const roundedSeconds = Math.round(etaData.remainingSeconds / 10) * 10;
if (roundedSeconds < entirePartyMinTime) {
entirePartyMinTime = roundedSeconds;
}
}
}
});
}
});
}
} catch (e) {
console.error('[Consumables] Error calculating overall min:', e);
}
}
const minItems = [];
itemsWithTime.forEach(({ item, etaData }) => {
if (etaData && Math.abs(etaData.remainingSeconds - minTime) <= 60) {
minItems.push({
itemHrid: item.itemHrid,
count: item.count
});
}
});
this.minConsumables = minItems;
this.minConsumablesCount = minItems.length > 0 ? minItems[0].count : 0;
let playerTotalAsk = 0;
let playerTotalBid = 0;
itemsWithTime.forEach(({ item, itemName, iconId, etaData }) => {
let countColorClass = '';
let nameColorClass = '';
let timeColorClass = '';
if (etaData) {
const isOverallMin = Math.abs(etaData.remainingSeconds - entirePartyMinTime) <= 60;
const isPlayerMin = Math.abs(etaData.remainingSeconds - minTime) <= 60;
if (isOverallMin) {
countColorClass = 'mcs-crack-count-critical';
nameColorClass = 'mcs-crack-consumable-name-warning';
timeColorClass = 'mcs-crack-time-critical';
} else if (isPlayerMin) {
countColorClass = 'mcs-crack-count-warning';
nameColorClass = 'mcs-crack-count-warning';
timeColorClass = 'mcs-crack-time-warning';
}
}
const tracker = Object.assign({}, this.consumableTracker, { _etaData: etaData });
html += this.renderConsumableRow({
itemHrid: item.itemHrid, count: item.count, tracker, iconId, itemName,
countColorClass, nameColorClass, timeColorClass,
extraAttrs: 'data-item-hrid="' + item.itemHrid + '"',
priceData: price_data
});
if (etaData) {
const prices = this.getItemPrices(item.itemHrid, price_data);
if (prices && etaData.consumedPerDay) {
playerTotalAsk += prices.ask * etaData.consumedPerDay;
playerTotalBid += prices.bid * etaData.consumedPerDay;
}
}
});
html += '<div class="mcs-crack-total-cost">';
html += '<span class="mcs-crack-party-name">Total Cost/Day:</span> ';
html += '<span>Ask: ' + this.formatPriceShort(playerTotalAsk) + ' / Bid: ' + this.formatPriceShort(playerTotalBid) + '</span>';
html += '</div>';
window.lootDropsTrackerInstance.consumablesCostPerDayAsk = playerTotalAsk;
window.lootDropsTrackerInstance.consumablesCostPerDayBid = playerTotalBid;
this.totalConsumableAsk = playerTotalAsk;
this.totalConsumableBid = playerTotalBid;
if (battleObj) {
try {
if (battleObj.players && battleObj.players.length > 1) {
const currentPlayerName = cachedData?.character?.name || this.spyCharacterName || '';
if (!this.partyConsumableTracker) {
this.partyConsumableTracker = {};
}
if (!this.lastMinTimes) {
this.lastMinTimes = {};
}
battleObj.players.forEach(player => {
if (player.name === currentPlayerName) return;
if (!this.partyConsumableTracker[player.name] && player.combatConsumables && player.combatConsumables.length > 0) {
this.partyConsumableTracker[player.name] = this.createPartyTracker();
player.combatConsumables.forEach(consumable => {
if (consumable && consumable.itemHrid && consumable.count) {
this.partyConsumableTracker[player.name].inventoryAmount[consumable.itemHrid] = consumable.count;
this.partyConsumableTracker[player.name].actualConsumed[consumable.itemHrid] = 0;
this.partyConsumableTracker[player.name].defaultConsumed[consumable.itemHrid] = this.getDefaultConsumed(consumable.itemHrid);
}
});
this.savePartyConsumableTracking();
}
html += '<div class="mcs-crack-party-section">';
html += `<div class="mcs-crack-party-name">${player.name}</div>`;
const allPlayerConsumables = new Map();
if (player.combatConsumables && player.combatConsumables.length > 0) {
player.combatConsumables.forEach(consumable => {
if (consumable && consumable.itemHrid) {
allPlayerConsumables.set(consumable.itemHrid, consumable.count);
}
});
}
if (this.partyConsumableTracker[player.name]) {
Object.keys(this.partyConsumableTracker[player.name].inventoryAmount ?? {}).forEach(itemHrid => {
if (!allPlayerConsumables.has(itemHrid)) {
allPlayerConsumables.set(itemHrid, this.partyConsumableTracker[player.name].inventoryAmount[itemHrid] ?? 0);
}
});
}
if (allPlayerConsumables.size > 0) {
let playerMinTime = Infinity;
let partyMemberTotalAsk = 0;
let partyMemberTotalBid = 0;
allPlayerConsumables.forEach((count, itemHrid) => {
const etaData = this.estimateTimeToZeroForPartyMemberTracked(player.name, itemHrid, count);
if (etaData) {
const roundedSeconds = Math.round(etaData.remainingSeconds / 30) * 30;
if (roundedSeconds < playerMinTime) {
playerMinTime = roundedSeconds;
}
}
});
const cachedMinTime = this.lastMinTimes[player.name];
if (cachedMinTime !== undefined && Math.abs(playerMinTime - cachedMinTime) < 30) {
playerMinTime = cachedMinTime;
} else {
this.lastMinTimes[player.name] = playerMinTime;
}
allPlayerConsumables.forEach((count, itemHrid) => {
const itemName = this.getSpyItemName(itemHrid);
const iconId = itemHrid.split('/').pop();
const partyTracker = this.partyConsumableTracker[player.name];
const etaData = this.estimateTimeToZeroForPartyMemberTracked(player.name, itemHrid, count);
const isMinItem = (etaData && Math.abs(etaData.remainingSeconds - playerMinTime) <= 60);
if (partyTracker && partyTracker.actualConsumed && partyTracker.defaultConsumed) {
const tracker = Object.assign({}, partyTracker, { _etaData: etaData });
const rowAttrs = `data-player-name="${player.name}" data-item-hrid="${itemHrid}" data-is-min="${isMinItem}"`;
html += this.renderConsumableRow({
itemHrid, count, tracker, iconId, itemName,
countColorClass: isMinItem ? 'mcs-crack-count-critical' : '',
nameColorClass: isMinItem ? 'mcs-crack-consumable-name-warning' : '',
timeColorClass: isMinItem ? 'mcs-crack-time-critical' : 'mcs-crack-time-normal',
extraClasses: 'party-consumable-row',
extraAttrs: rowAttrs,
priceData: price_data
});
} else {
html += '<div class="mcs-crack-consumable-container"><span class="mcs-crack-tracker-error">Tracker Error</span></div>';
}
if (etaData) {
const prices = this.getItemPrices(itemHrid, price_data);
if (prices && etaData.consumedPerDay) {
partyMemberTotalAsk += prices.ask * etaData.consumedPerDay;
partyMemberTotalBid += prices.bid * etaData.consumedPerDay;
}
}
});
html += '<div class="mcs-crack-total-cost">';
html += '<span class="mcs-crack-party-name">Total Cost/Day:</span> ';
html += '<span>Ask: ' + this.formatPriceShort(partyMemberTotalAsk) + ' / Bid: ' + this.formatPriceShort(partyMemberTotalBid) + '</span>';
html += '</div>';
if (player.name === currentPlayerName) {
window.lootDropsTrackerInstance.consumablesCostPerDayAsk = partyMemberTotalAsk;
window.lootDropsTrackerInstance.consumablesCostPerDayBid = partyMemberTotalBid;
}
} else {
html += '<div class="mcs-crack-no-data">No consumables</div>';
}
html += '</div>';
});
}
} catch (e) {
console.error('[Consumables] Error parsing party data:', e);
}
}
content.innerHTML = html;
if (battleObj) {
try {
const partyEtaContainer = document.getElementById('party-eta-container');
if (battleObj.players && battleObj.players.length > 1) {
if (partyEtaContainer) {
partyEtaContainer.classList.remove('mcs-hidden');
}
let partyMinEta = Infinity;
const currentPlayerName = cachedData?.character?.name || this.spyCharacterName || '';
battleObj.players.forEach(player => {
if (player.name === currentPlayerName) return;
if (player.combatConsumables && player.combatConsumables.length > 0) {
player.combatConsumables.forEach(consumable => {
if (consumable && consumable.itemHrid && consumable.count !== undefined) {
if (consumable.count === 0) {
partyMinEta = 0;
} else {
const etaData = this.estimateTimeToZeroForPartyMemberTracked(player.name, consumable.itemHrid, consumable.count);
if (etaData && etaData.remainingSeconds < partyMinEta) {
partyMinEta = etaData.remainingSeconds;
}
}
}
});
}
});
this.partyMinTime = partyMinEta;
const partyEtaSpan = document.getElementById('party-min-eta');
if (partyEtaSpan) {
partyEtaSpan.classList.remove('mcs-crack-party-eta-disabled', 'mcs-crack-party-eta-critical', 'mcs-crack-party-eta-warning', 'mcs-crack-party-eta-good');
if (partyMinEta === Infinity) {
partyEtaSpan.textContent = 'Waiting...';
partyEtaSpan.classList.add('mcs-crack-party-eta-disabled');
} else {
partyEtaSpan.textContent = mcsFormatDuration(partyMinEta, 'eta');
const days = partyMinEta / 86400;
if (days < 1) {
partyEtaSpan.classList.add('mcs-crack-party-eta-critical');
} else if (days < 2) {
partyEtaSpan.classList.add('mcs-crack-party-eta-warning');
} else {
partyEtaSpan.classList.add('mcs-crack-party-eta-good');
}
}
}
} else {
if (partyEtaContainer) {
partyEtaContainer.classList.add('mcs-hidden');
}
}
} catch (e) {
console.error('[Consumables] Error updating party ETA:', e);
}
}
} catch (error) {
console.error('[Consumables] Error in updateConsumablesDisplay:', error);
console.error('[Consumables] Error stack:', error.stack);
content.innerHTML = '<div class="mcs-crack-tracker-error">Error loading consumables</div>';
}
if (this.consumablesIsMinimized) {
this.updateMinimizedSummary();
}
}
openConsumableMarketplace(itemHrid) {
mcsGoToMarketplace(itemHrid);
}
loadConsumableIcons() {
const content = document.getElementById('consumables-content');
if (!content) {
return;
}
const rows = content.querySelectorAll('.consumable-row');
rows.forEach(row => {
const iconId = row.getAttribute('data-icon-id');
if (!iconId) return;
const placeholder = row.querySelector('.icon-placeholder');
if (!placeholder) return;
if (placeholder.querySelector('svg')) return;
placeholder.appendChild(createItemIcon(iconId, { className: 'mcs-crack-consumable-icon' }));
});
}
destroyCRack() {
if (this._crWsListener) {
window.removeEventListener('EquipSpyWebSocketMessage', this._crWsListener);
this._crWsListener = null;
}
const pane = document.getElementById('consumables-pane');
if (pane) pane.remove();
}
// CRackUI end
// HWhat start
get hwStorage() {
if (!this._hwStorage) {
this._hwStorage = createModuleStorage('HW');
}
return this._hwStorage;
}
_updateCowbellCache(characterItems) {
if (!this._cowbellCache) {
this._cowbellCache = {
cowbellCount: 0,
bagCount: 0,
lastUpdate: 0,
cacheDuration: 5000
};
}
if (!characterItems || !Array.isArray(characterItems)) return;
const cowbellItem = characterItems.find(item => item.itemHrid === '/items/cowbell');
const bagItem = characterItems.find(item => item.itemHrid === '/items/bag_of_10_cowbells');
this._cowbellCache.cowbellCount = cowbellItem ? (cowbellItem.count || 0) : 0;
this._cowbellCache.bagCount = bagItem ? (bagItem.count || 0) : 0;
this._cowbellCache.lastUpdate = Date.now();
}
_getCowbellCounts() {
if (!this._cowbellCache) {
this._cowbellCache = {
cowbellCount: 0,
bagCount: 0,
lastUpdate: 0,
cacheDuration: 5000
};
}
const now = Date.now();
if (now - this._cowbellCache.lastUpdate > this._cowbellCache.cacheDuration) {
try {
const cachedData = CharacterDataStorage.get();
if (cachedData) {
this._updateCowbellCache(cachedData.characterItems);
}
} catch (e) {
console.error('[HWhat] Error refreshing cowbell cache:', e);
}
}
return {
cowbellCount: this._cowbellCache.cowbellCount,
bagCount: this._cowbellCache.bagCount
};
}
_updateCharacterItemsInStorage(endCharacterItems) {
if (!endCharacterItems) return false;
try {
const characterData = CharacterDataStorage.get();
if (!characterData) return false;
let hasChanges = false;
endCharacterItems.forEach(updatedItem => {
const existingItem = characterData.characterItems.find(
item => item.itemHrid === updatedItem.itemHrid
);
if (existingItem) {
if (existingItem.count !== updatedItem.count) {
existingItem.count = updatedItem.count;
hasChanges = true;
}
} else {
characterData.characterItems.push(updatedItem);
hasChanges = true;
}
});
if (hasChanges) {
CharacterDataStorage.set(characterData);
this._updateCowbellCache(characterData.characterItems);
}
return hasChanges;
} catch (e) {
console.error('[HWhat] Error updating character items:', e);
return false;
}
}
hwhatHandleBattle() {
setTimeout(() => this.updateHWhatDisplay(), 100);
}
hwhatHandleWebSocketMessage(event) {
if (window.MCS_MODULES_DISABLED) return;
const data = event.detail;
if (data?.type === 'init_character_data' && data.characterItems) {
this._updateCowbellCache(data.characterItems);
setTimeout(() => this.updateHWhatDisplay(), 100);
}
if ((data?.type === 'action_completed' || data?.type === 'items_updated') && data.endCharacterItems) {
if (this._updateCharacterItemsInStorage(data.endCharacterItems)) {
setTimeout(() => this.updateHWhatDisplay(), 100);
}
}
}
hwhatHandleStorage(event) {
if (event.key && event.key.startsWith('mcs__global_init_character_data')) {
try {
const cachedData = CharacterDataStorage.get();
if (cachedData) {
if (cachedData.characterItems) {
this._updateCowbellCache(cachedData.characterItems);
if (window.lootDropsTrackerInstance) {
window.lootDropsTrackerInstance.spyCharacterItems = cachedData.characterItems;
}
}
}
} catch (e) {
console.error('[HWhat] Error syncing inventory from storage event:', e);
}
setTimeout(() => this.updateHWhatDisplay(), 100);
}
}
createHWhatPanel() {
if (document.getElementById('hwhat-pane')) {
return;
}
const hwhatPane = document.createElement('div');
hwhatPane.id = 'hwhat-pane';
registerPanel('hwhat-pane');
hwhatPane.className = 'mcs-pane mcs-hw-pane';
hwhatPane.innerHTML = `
<div id="hwhat-header" class="mcs-pane-header">
<div class="mcs-hw-header-left">
<span class="mcs-pane-title mcs-hw-title">HWhat</span>
<span id="hwhat-combat-status" class="mcs-hw-combat-status">Not in Combat</span>
<div id="hwhat-header-calc" class="mcs-hw-header-calc">
<span class="mcs-hw-color-green"><span id="hwhat-header-revenue">0</span></span>
<span class="mcs-hw-color-white"> - </span>
<span id="hwhat-header-tax-section" class="mcs-hw-header-tax-section">
<div id="hwhat-header-tax-icon-placeholder" class="mcs-hw-header-tax-icon"></div>
<span class="mcs-hw-color-tax"><span id="hwhat-header-tax">0</span></span>
<span class="mcs-hw-color-white"> - </span>
</span>
<span class="mcs-hw-color-red"><span id="hwhat-header-cost">0</span></span>
<span class="mcs-hw-color-white"> = </span>
<span class="mcs-hw-color-gold"><span id="hwhat-header-profit">0</span>/day</span>
</div>
</div>
<div class="mcs-button-section">
<button id="hwhat-costs-toggle" class="mcs-hw-toggle-btn">Costs On</button>
<button id="hwhat-mode-toggle" class="mcs-hw-toggle-btn">Lazy</button>
<button id="hwhat-minimize-btn" class="mcs-hw-toggle-btn">-</button>
</div>
</div>
<div id="hwhat-content" class="mcs-hw-content">
<div id="hwhat-maximized">
<div id="hwhat-tax-section" class="mcs-hw-tax-section">
<div id="hwhat-tax-icon-placeholder" class="mcs-hw-tax-icon"></div>
<div class="mcs-hw-tax-details">
<div id="hwhat-tax-header" class="mcs-hw-tax-header-text">Pay the Tax!</div>
<div class="mcs-hw-profit-value mcs-hw-color-tax"><span id="hwhat-tax-cost">0</span> coin/day</div>
<div class="mcs-hw-info-text">
25 Bag of 10 Cowbells per week <span id="hwhat-bags-needed" class="mcs-hw-bags-needed">(You need 0 bags)</span>
</div>
</div>
<button id="hwhat-tax-toggle" class="mcs-hw-toggle-btn mcs-hw-tax-toggle">No thanks ima swipe</button>
</div>
<div class="mcs-hw-profit-box mcs-hw-lazy-box">
<div class="mcs-hw-profit-title mcs-hw-color-green">Lazy Profit</div>
<div class="mcs-hw-profit-value mcs-hw-color-green"><span id="hwhat-lazy-profit">0</span> coin/day</div>
<div class="mcs-hw-info-text">Revenue (Bid) - Cost (Ask)</div>
<div class="mcs-hw-equation mcs-hw-lazy-equation">
<span id="hwhat-lazy-equation">0 - 0 = 0</span>
</div>
</div>
<div class="mcs-hw-profit-box mcs-hw-mid-box">
<div class="mcs-hw-profit-title mcs-hw-color-blue">Mid Profit</div>
<div class="mcs-hw-profit-value mcs-hw-color-blue"><span id="hwhat-mid-profit">0</span> coin/day</div>
<div class="mcs-hw-info-text">Revenue (Ask) - Cost (Bid)</div>
<div class="mcs-hw-equation mcs-hw-mid-equation">
<span id="hwhat-mid-equation">0 - 0 = 0</span>
</div>
</div>
<div class="mcs-hw-diff-box">
<div class="mcs-hw-profit-title mcs-hw-color-gold">Difference</div>
<div class="mcs-hw-profit-value mcs-hw-color-gold"><span id="hwhat-difference">0</span> coin/day</div>
<div class="mcs-hw-diff-message">Don't be lazy!</div>
</div>
</div>
</div>
`;
document.body.appendChild(hwhatPane);
setTimeout(() => {
const placeholder = document.getElementById('hwhat-tax-icon-placeholder');
if (placeholder) {
const svgIcon = createItemIcon('bag_of_10_cowbells', { width: 80, height: 80 });
svgIcon.style.flexShrink = '0';
placeholder.replaceWith(svgIcon);
}
const headerTaxPlaceholder = document.getElementById('hwhat-header-tax-icon-placeholder');
if (headerTaxPlaceholder) {
const svgIcon = createItemIcon('bag_of_10_cowbells', { width: 16, height: 16 });
svgIcon.style.verticalAlign = 'middle';
svgIcon.style.marginRight = '2px';
headerTaxPlaceholder.replaceWith(svgIcon);
}
}, 100);
const savedPos = this.hwStorage.get('position');
if (savedPos) {
hwhatPane.style.top = savedPos.top + 'px';
hwhatPane.style.left = savedPos.left + 'px';
}
const savedMinimized = this.hwStorage.get('minimized');
this.hwhatIsMinimized = savedMinimized === true || savedMinimized === 'true';
this.updateHWhatMinimizeState();
const header = document.getElementById('hwhat-header');
DragHandler.makeDraggable(hwhatPane, header, 'mcs_HW');
if (this.hwStorage.get('mode') === null) {
this.hwStorage.set('mode', 'lazy');
}
this.hwhatMode = this.hwStorage.get('mode') || 'lazy';
const modeToggle = document.getElementById('hwhat-mode-toggle');
if (modeToggle) {
modeToggle.textContent = this.hwhatMode === 'lazy' ? 'Lazy' : 'Mid';
modeToggle.style.background = this.hwhatMode === 'lazy' ? 'rgba(76, 175, 80, 0.3)' : 'rgba(33, 150, 243, 0.3)';
modeToggle.style.borderColor = this.hwhatMode === 'lazy' ? 'rgba(76, 175, 80, 0.5)' : 'rgba(33, 150, 243, 0.5)';
modeToggle.style.color = this.hwhatMode === 'lazy' ? '#4CAF50' : '#2196F3';
}
document.getElementById('hwhat-minimize-btn').onclick = () => this.toggleHWhatMinimize();
document.getElementById('hwhat-mode-toggle').onclick = () => this.toggleHWhatMode();
if (this.hwStorage.get('costs_enabled') === null) {
this.hwStorage.set('costs_enabled', true);
}
const savedCostsEnabled = this.hwStorage.get('costs_enabled');
this.hwhatCostsEnabled = savedCostsEnabled === true || savedCostsEnabled === 'true';
const costsToggle = document.getElementById('hwhat-costs-toggle');
if (costsToggle) {
costsToggle.textContent = this.hwhatCostsEnabled ? 'Costs On' : 'Costs Off';
}
document.getElementById('hwhat-costs-toggle').onclick = () => this.toggleHWhatCosts();
if (this.hwStorage.get('cowbell_tax_enabled') === null) {
this.hwStorage.set('cowbell_tax_enabled', false);
}
const savedTaxEnabled = this.hwStorage.get('cowbell_tax_enabled');
this.hwhatCowbellTaxEnabled = savedTaxEnabled === true || savedTaxEnabled === 'true';
const taxToggle = document.getElementById('hwhat-tax-toggle');
const taxHeader = document.getElementById('hwhat-tax-header');
if (taxToggle) {
taxToggle.textContent = this.hwhatCowbellTaxEnabled ? 'No thanks ima swipe' : 'Moooooooooooo';
taxToggle.style.background = this.hwhatCowbellTaxEnabled ? 'rgba(220, 53, 69, 0.3)' : 'rgba(255, 255, 255, 0.1)';
taxToggle.style.borderColor = this.hwhatCowbellTaxEnabled ? 'rgba(220, 53, 69, 0.5)' : 'rgba(255, 255, 255, 0.3)';
taxToggle.style.color = this.hwhatCowbellTaxEnabled ? '#dc3545' : 'white';
}
if (taxHeader) {
taxHeader.textContent = this.hwhatCowbellTaxEnabled ? 'Paying the Tax!' : 'Pay the Tax!';
}
document.getElementById('hwhat-tax-toggle').onclick = () => this.toggleHWhatCowbellTax();
const savedStates = ToolVisibilityStorage.get();
this.hwhatShowFullNumbers = savedStates['hwhat-full-numbers'] !== false;
const waitForData = setInterval(() => {
if (this.spyMarketData && Object.keys(this.spyMarketData).length > 0) {
clearInterval(waitForData);
this.updateHWhatDisplay();
}
}, 500);
setTimeout(() => {
clearInterval(waitForData);
this.updateHWhatDisplay();
}, 5000);
this._hwhatBattleListener = this.hwhatHandleBattle.bind(this);
this._hwhatCombatEndedListener = this.hwhatHandleBattle.bind(this);
this._hwhatWsListener = this.hwhatHandleWebSocketMessage.bind(this);
this._hwhatStorageListener = this.hwhatHandleStorage.bind(this);
window.addEventListener('LootTrackerBattle', this._hwhatBattleListener);
window.addEventListener('LootTrackerCombatEnded', this._hwhatCombatEndedListener);
window.addEventListener('EquipSpyWebSocketMessage', this._hwhatWsListener);
window.addEventListener('storage', this._hwhatStorageListener);
VisibilityManager.register('hwhat-update', () => {
const hwhatPane = document.getElementById('hwhat-pane');
if (!hwhatPane) {
VisibilityManager.clear('hwhat-update');
return;
}
try {
const cachedData = CharacterDataStorage.get();
if (cachedData && window.lootDropsTrackerInstance) {
if (cachedData.characterItems) {
const oldItems = window.lootDropsTrackerInstance.spyCharacterItems ?? [];
const newItems = cachedData.characterItems;
if (oldItems.length !== newItems.length) {
window.lootDropsTrackerInstance.spyCharacterItems = newItems;
} else {
let hasChanges = false;
for (const newItem of newItems) {
const oldItem = oldItems.find(i => i.itemHrid === newItem.itemHrid);
if (!oldItem || oldItem.count !== newItem.count) {
hasChanges = true;
break;
}
}
if (hasChanges) {
window.lootDropsTrackerInstance.spyCharacterItems = newItems;
}
}
}
}
} catch (e) {
console.error('[HWhat] Error syncing inventory in fallback:', e);
}
this.updateHWhatDisplay();
}, 5000);
}
toggleHWhatMinimize() {
this.hwhatIsMinimized = !this.hwhatIsMinimized;
this.hwStorage.set('minimized', this.hwhatIsMinimized);
this.updateHWhatMinimizeState();
}
toggleHWhatMode() {
this.hwhatMode = this.hwhatMode === 'lazy' ? 'mid' : 'lazy';
this.hwStorage.set('mode', this.hwhatMode);
const modeToggle = document.getElementById('hwhat-mode-toggle');
if (modeToggle) {
modeToggle.textContent = this.hwhatMode === 'lazy' ? 'Lazy' : 'Mid';
modeToggle.style.background = this.hwhatMode === 'lazy' ? 'rgba(76, 175, 80, 0.3)' : 'rgba(33, 150, 243, 0.3)';
modeToggle.style.borderColor = this.hwhatMode === 'lazy' ? 'rgba(76, 175, 80, 0.5)' : 'rgba(33, 150, 243, 0.5)';
modeToggle.style.color = this.hwhatMode === 'lazy' ? '#4CAF50' : '#2196F3';
}
this.updateHWhatDisplay();
if (this.updateProfitCostDisplay) {
this.updateProfitCostDisplay();
}
if (this.purchaseTimerIntervals) {
Object.values(this.purchaseTimerIntervals).forEach(interval => clearInterval(interval));
this.purchaseTimerIntervals = {};
}
if (this.purchaseTimerInterval) {
clearInterval(this.purchaseTimerInterval);
this.purchaseTimerInterval = null;
}
if (this.updateLockedTimers) {
this.updateLockedTimers();
}
if (this.updateSpyDisplay) {
this.updateSpyDisplay();
}
if (this.updateHeaderStatus) {
this.updateHeaderStatus();
}
}
toggleHWhatCosts() {
this.hwhatCostsEnabled = !this.hwhatCostsEnabled;
this.hwStorage.set('costs_enabled', this.hwhatCostsEnabled);
const costsToggle = document.getElementById('hwhat-costs-toggle');
if (costsToggle) {
costsToggle.textContent = this.hwhatCostsEnabled ? 'Costs On' : 'Costs Off';
}
this.updateHWhatDisplay();
if (this.updateProfitCostDisplay) {
this.updateProfitCostDisplay();
}
if (this.purchaseTimerIntervals) {
Object.values(this.purchaseTimerIntervals).forEach(interval => clearInterval(interval));
this.purchaseTimerIntervals = {};
}
if (this.purchaseTimerInterval) {
clearInterval(this.purchaseTimerInterval);
this.purchaseTimerInterval = null;
}
if (this.updateLockedTimers) {
this.updateLockedTimers();
}
if (this.updateSpyDisplay) {
this.updateSpyDisplay();
}
if (this.updateHeaderStatus) {
this.updateHeaderStatus();
}
}
toggleHWhatCowbellTax() {
this.hwhatCowbellTaxEnabled = !this.hwhatCowbellTaxEnabled;
this.hwStorage.set('cowbell_tax_enabled', this.hwhatCowbellTaxEnabled);
const taxToggle = document.getElementById('hwhat-tax-toggle');
const taxHeader = document.getElementById('hwhat-tax-header');
if (taxToggle) {
taxToggle.textContent = this.hwhatCowbellTaxEnabled ? 'No thanks ima swipe' : 'Moooooooooooo';
taxToggle.style.background = this.hwhatCowbellTaxEnabled ? 'rgba(220, 53, 69, 0.3)' : 'rgba(255, 255, 255, 0.1)';
taxToggle.style.borderColor = this.hwhatCowbellTaxEnabled ? 'rgba(220, 53, 69, 0.5)' : 'rgba(255, 255, 255, 0.3)';
taxToggle.style.color = this.hwhatCowbellTaxEnabled ? '#dc3545' : 'white';
}
if (taxHeader) {
taxHeader.textContent = this.hwhatCowbellTaxEnabled ? 'Paying the Tax!' : 'Pay the Tax!';
}
this.updateHWhatDisplay();
if (this.updateProfitCostDisplay) {
this.updateProfitCostDisplay();
}
if (this.purchaseTimerIntervals) {
Object.values(this.purchaseTimerIntervals).forEach(interval => clearInterval(interval));
this.purchaseTimerIntervals = {};
}
if (this.purchaseTimerInterval) {
clearInterval(this.purchaseTimerInterval);
this.purchaseTimerInterval = null;
}
if (this.updateLockedTimers) {
this.updateLockedTimers();
}
if (this.updateSpyDisplay) {
this.updateSpyDisplay();
}
if (this.updateHeaderStatus) {
this.updateHeaderStatus();
}
}
updateHWhatMinimizeState() {
const maximizedDiv = document.getElementById('hwhat-maximized');
const contentDiv = document.getElementById('hwhat-content');
const headerDiv = document.getElementById('hwhat-header');
const btn = document.getElementById('hwhat-minimize-btn');
if (!maximizedDiv || !btn) return;
if (this.hwhatIsMinimized) {
maximizedDiv.style.display = 'none';
if (contentDiv) contentDiv.style.display = 'none';
if (headerDiv) headerDiv.style.borderRadius = '6px';
btn.textContent = '+';
} else {
maximizedDiv.style.display = 'block';
if (contentDiv) contentDiv.style.display = 'block';
if (headerDiv) headerDiv.style.borderRadius = '6px 6px 0 0';
btn.textContent = '-';
this.constrainPanelToBoundaries('hwhat-pane', 'mcs_HW', true);
}
}
updateHWhatDisplay() {
let inCombat = window.MCS_IN_COMBAT === true;
if (window.lootDropsTrackerInstance?.isLiveSessionActive) {
inCombat = true;
}
const combatStatusEl = document.getElementById('hwhat-combat-status');
if (combatStatusEl) {
combatStatusEl.style.display = inCombat ? 'none' : 'inline';
}
if (!inCombat && this.hwhatFrozenValues) {
const frozen = this.hwhatFrozenValues;
this.updateHWhatDisplayElements(frozen);
return;
}
const tracker = window.lootDropsTrackerInstance;
const userName = tracker?.userName;
const revenue = (tracker?.flootPlayerRevenue?.[userName]?.perDay) ?? 0;
const costBid = this.hwhatCostsEnabled ? this.consumableCost('bid') : 0;
const costAsk = this.hwhatCostsEnabled ? this.consumableCost('ask') : 0;
let cowbellTaxPerDay = 0;
let bagsNeeded = 0;
if (this.hwhatCowbellTaxEnabled) {
bagsNeeded = this.calculateNeededCowbellBags();
const cowbellItem = this.spyMarketData['/items/bag_of_10_cowbells'];
if (cowbellItem && cowbellItem['0'] && cowbellItem['0'].a) {
const bagsPerWeek = Math.max(0, 25 - (this.calculateOwnedCowbellBags ? this.calculateOwnedCowbellBags() : 0));
cowbellTaxPerDay = (cowbellItem['0'].a * bagsPerWeek) / 7;
}
}
const lazyProfit = revenue - costAsk - (this.hwhatCowbellTaxEnabled ? cowbellTaxPerDay : 0);
const midProfit = revenue - costBid - (this.hwhatCowbellTaxEnabled ? cowbellTaxPerDay : 0);
const difference = midProfit - lazyProfit;
const currentMode = this.hwhatMode || 'lazy';
const currentCost = currentMode === 'lazy' ? costAsk : costBid;
const currentProfit = currentMode === 'lazy' ? lazyProfit : midProfit;
this.hwhatCurrentRevenue = revenue;
this.hwhatCurrentCost = currentCost;
this.hwhatCurrentProfit = currentProfit;
this.hwhatCostBid = costBid;
this.hwhatCostAsk = costAsk;
this.hwhatCowbellTaxPerDay = cowbellTaxPerDay;
this.hwhatTaxEnabled = this.hwhatCowbellTaxEnabled && cowbellTaxPerDay > 0;
if (inCombat) {
this.hwhatFrozenValues = {
revenue, costBid, costAsk,
cowbellTaxPerDay, bagsNeeded,
lazyProfit, midProfit, difference,
currentCost, currentProfit
};
}
this.updateHWhatDisplayElements({
revenue, costBid, costAsk,
cowbellTaxPerDay, bagsNeeded,
lazyProfit, midProfit, difference,
currentCost, currentProfit
});
}
updateHWhatDisplayElements(values) {
const {
revenue, costBid, costAsk,
cowbellTaxPerDay, bagsNeeded,
lazyProfit, midProfit, difference,
currentCost, currentProfit
} = values;
const headerRevenueEl = document.getElementById('hwhat-header-revenue');
const headerCostEl = document.getElementById('hwhat-header-cost');
const headerProfitEl = document.getElementById('hwhat-header-profit');
const headerTaxSection = document.getElementById('hwhat-header-tax-section');
const headerTaxEl = document.getElementById('hwhat-header-tax');
if (headerRevenueEl) headerRevenueEl.textContent = this.formatGoldNumber(revenue);
if (headerCostEl) headerCostEl.textContent = this.formatGoldNumber(currentCost);
if (headerProfitEl) headerProfitEl.textContent = this.formatGoldNumber(currentProfit);
if (headerTaxSection && headerTaxEl) {
if (this.hwhatCowbellTaxEnabled && cowbellTaxPerDay > 0) {
headerTaxSection.style.display = 'inline';
headerTaxEl.textContent = this.formatGoldNumber(cowbellTaxPerDay);
} else {
headerTaxSection.style.display = 'none';
}
}
const lazyProfitEl = document.getElementById('hwhat-lazy-profit');
const midProfitEl = document.getElementById('hwhat-mid-profit');
const differenceEl = document.getElementById('hwhat-difference');
const lazyEquationEl = document.getElementById('hwhat-lazy-equation');
const midEquationEl = document.getElementById('hwhat-mid-equation');
if (lazyProfitEl) lazyProfitEl.textContent = this.formatGoldNumber(lazyProfit);
if (midProfitEl) midProfitEl.textContent = this.formatGoldNumber(midProfit);
if (differenceEl) differenceEl.textContent = this.formatGoldNumber(difference);
const taxCostEl = document.getElementById('hwhat-tax-cost');
if (taxCostEl) {
taxCostEl.textContent = this.formatGoldNumber(cowbellTaxPerDay);
}
if (lazyEquationEl) {
if (this.hwhatCowbellTaxEnabled && cowbellTaxPerDay > 0) {
lazyEquationEl.textContent = `${this.formatGoldNumber(revenue)} - ${this.formatGoldNumber(costAsk)} - ${this.formatGoldNumber(cowbellTaxPerDay)} = ${this.formatGoldNumber(lazyProfit)}`;
} else {
lazyEquationEl.textContent = `${this.formatGoldNumber(revenue)} - ${this.formatGoldNumber(costAsk)} = ${this.formatGoldNumber(lazyProfit)}`;
}
}
if (midEquationEl) {
if (this.hwhatCowbellTaxEnabled && cowbellTaxPerDay > 0) {
midEquationEl.textContent = `${this.formatGoldNumber(revenue)} - ${this.formatGoldNumber(costBid)} - ${this.formatGoldNumber(cowbellTaxPerDay)} = ${this.formatGoldNumber(midProfit)}`;
} else {
midEquationEl.textContent = `${this.formatGoldNumber(revenue)} - ${this.formatGoldNumber(costBid)} = ${this.formatGoldNumber(midProfit)}`;
}
}
const bagsNeededEl = document.getElementById('hwhat-bags-needed');
if (bagsNeededEl) {
bagsNeededEl.textContent = `(You need ${bagsNeeded} bag${bagsNeeded !== 1 ? 's' : ''})`;
if (bagsNeeded === 0) {
bagsNeededEl.style.color = '#4CAF50';
} else if (bagsNeeded <= 10) {
bagsNeededEl.style.color = '#ffa500';
} else {
bagsNeededEl.style.color = '#dc3545';
}
}
}
calculateNeededCowbellBags() {
try {
const { cowbellCount, bagCount } = this._getCowbellCounts();
const totalCowbellsOwned = cowbellCount + (10 * bagCount);
const cowbellsNeeded = Math.max(0, 250 - totalCowbellsOwned);
const bagsNeeded = Math.ceil(cowbellsNeeded / 10);
return bagsNeeded;
} catch (e) {
console.error('[HWhat] Error calculating needed cowbell bags:', e);
return 0;
}
}
calculateOwnedCowbellBags() {
try {
const { cowbellCount, bagCount } = this._getCowbellCounts();
const totalCowbellsOwned = cowbellCount + (10 * bagCount);
const equivalentBags = Math.floor(totalCowbellsOwned / 10);
return equivalentBags;
} catch (e) {
console.error('[HWhat] Error calculating owned cowbell bags:', e);
return 0;
}
}
startHWhatCowbellPolling() {
}
consumableCost(priceType) {
if (!window.lootDropsTrackerInstance) {
return 0;
}
const cost = priceType === 'ask'
? (window.lootDropsTrackerInstance.consumablesCostPerDayAsk || 0)
: (window.lootDropsTrackerInstance.consumablesCostPerDayBid || 0);
return cost;
}
getItemPriceForCostCustom(itemHrid, priceType) {
if (!this.spyMarketData || !this.spyMarketData[itemHrid]) {
return 0;
}
const itemData = this.spyMarketData[itemHrid];
const level0Data = itemData[0];
if (!level0Data) {
return 0;
}
const price = priceType === 'ask' ?
(level0Data.a || 0) :
(level0Data.b || 0);
return price;
}
destroyHWhat() {
VisibilityManager.clear('hwhat-update');
if (this._hwhatBattleListener) { window.removeEventListener('LootTrackerBattle', this._hwhatBattleListener); this._hwhatBattleListener = null; }
if (this._hwhatCombatEndedListener) { window.removeEventListener('LootTrackerCombatEnded', this._hwhatCombatEndedListener); this._hwhatCombatEndedListener = null; }
if (this._hwhatWsListener) { window.removeEventListener('EquipSpyWebSocketMessage', this._hwhatWsListener); this._hwhatWsListener = null; }
if (this._hwhatStorageListener) { window.removeEventListener('storage', this._hwhatStorageListener); this._hwhatStorageListener = null; }
const pane = document.getElementById('hwhat-pane');
if (pane) pane.remove();
}
// HWhat end
// EWatch start
get ewStorage() {
if (!this._ewStorage) {
this._ewStorage = createModuleStorage('EW');
}
return this._ewStorage;
}
ewatchHandleWebSocketMessage(event) {
if (window.MCS_MODULES_DISABLED) return;
const data = event.detail;
if ((data?.type === 'init_character_data' ||
data?.type === 'item_update' ||
data?.type === 'inventory_update' ||
data?.type === 'character_update') && data.characterItems) {
const equippedItems = [];
for (const item of data.characterItems) {
if (!item.itemLocationHrid.includes("/item_locations/inventory")) {
equippedItems.push(item);
}
}
this.spyCharacterItems = equippedItems;
setTimeout(() => this.updateSpyDisplay(), 100);
}
if (data?.type === 'init_client_data' && data.itemDetailMap) {
this.spyItemDetailMap = data.itemDetailMap;
}
}
ewatchHandleForceLoad(event) {
this.spyCharacterItems = event.detail.items;
this.updateSpyDisplay();
}
ewatchHandleCharacterData(event) {
const data = event.detail;
if (data.characterItems) {
if (window.lootDropsTrackerInstance) {
window.lootDropsTrackerInstance.spyCharacterItems = data.characterItems;
setTimeout(() => window.lootDropsTrackerInstance.updateSpyDisplay(), 100);
}
}
}
ewatchHandleCoinUpdate(event) {
if (window.lootDropsTrackerInstance && window.lootDropsTrackerInstance.spyCharacterItems) {
const coinItem = window.lootDropsTrackerInstance.spyCharacterItems.find(
item => item.itemHrid === '/items/coin'
);
if (coinItem) {
coinItem.count = event.detail.count;
} else {
window.lootDropsTrackerInstance.spyCharacterItems.push({
itemHrid: '/items/coin',
itemLocationHrid: '/item_locations/inventory',
count: event.detail.count
});
}
window.lootDropsTrackerInstance.updateCoinHeader();
}
}
ewatchHandleBattleEvent() {
setTimeout(() => {
this.updateSpyDisplay();
this.updateHeaderStatus();
this.updateLockedTimers();
}, 100);
}
ewatchHandleEquipmentChanged() {
if (this.spyIsInteracting) return;
try {
const newData = window.mcs__global_equipment_tracker?.allCharacterItems;
if (!newData) return;
this.spyCharacterItems = newData;
this.updateSpyDisplay();
} catch (e) {
console.error('[EWatch] Error handling equipment change:', e);
}
}
isInCombat() {
if (window.MCS_IN_COMBAT === true) return true;
if (window.lootDropsTrackerInstance?.isLiveSessionActive) return true;
return false;
}
_buildItemLocationMap() {
const map = new Map();
if (this.spyCharacterItems) {
for (const item of this.spyCharacterItems) {
if (item.itemLocationHrid) {
map.set(item.itemLocationHrid, item);
}
}
}
return map;
}
updateHeaderStatus() {
const statusEl = document.getElementById('spy-header-status');
if (!statusEl) return;
const inCombat = this.isInCombat();
const lockedSlots = Object.keys(this.spyLockedComparisons);
if (lockedSlots.length === 0) {
statusEl.innerHTML = 'Not Watching Anything';
statusEl.style.color = '#aaa';
return;
}
const itemByLocation = this._buildItemLocationMap();
if (!inCombat && this.spyFrozenHeaderValues) {
const frozen = this.spyFrozenHeaderValues;
const itemDisplay = frozen.enhLevel !== ''
? `${frozen.name} +${frozen.enhLevel}`
: frozen.name;
let tickMarksHtml = '';
if (frozen.segments && frozen.segments.length > 1) {
let cumulativePercent = 0;
frozen.segments.forEach((seg, idx) => {
cumulativePercent += seg.percent;
if (idx < frozen.segments.length - 1) {
tickMarksHtml += `<div class="mcs-ew-tick-mark" style="left: ${cumulativePercent}%;"></div>`;
}
});
}
const goldNeededText = frozen.stillNeeded > 0 ? ` <span class="mcs-ew-gold-needed">${this.formatGoldCompact(frozen.stillNeeded)}</span>` : '';
const barColor = '#f44336';
statusEl.innerHTML = `
<div class="mcs-ew-status-col">
<div class="mcs-ew-status-row">
<span style="color: ${frozen.name === 'Everything' ? '#FFA500' : '#FFD700'};">${itemDisplay}</span>
${goldNeededText}
<span class="mcs-ew-nc-label">NC</span>
<span class="mcs-ew-nc-time">(${frozen.timeStr})</span>
</div>
<div class="mcs-ew-header-bar-track">
<div style="width: ${frozen.percent}%; height: 100%; background: ${barColor}; transition: width 0.3s ease; position: absolute; top: 0; left: 0;"></div>
${tickMarksHtml}
</div>
</div>
`;
return;
}
let shortestTime = Infinity;
let shortestItem = null;
let selectedItem = null;
let everythingItem = null;
const currentGold = this.getSpyCurrentGold();
let totalCost = 0;
let segmentData = [];
let orderedLockedSlots = lockedSlots;
if (this.comparisonOrder && Array.isArray(this.comparisonOrder)) {
const orderedSlots = this.comparisonOrder.filter(slot => lockedSlots.includes(slot));
const newSlots = lockedSlots.filter(slot => !this.comparisonOrder.includes(slot));
orderedLockedSlots = [...orderedSlots, ...newSlots];
}
orderedLockedSlots.forEach(slot => {
const locked = this.spyLockedComparisons[slot];
const item = itemByLocation.get(`/item_locations/${slot}`);
const currentAskPrice = this.getSpyAskPrice(locked.itemHrid, locked.enhLevel);
let priceToUse = currentAskPrice;
if (currentAskPrice === 0 && locked.lastKnownAskPrice && locked.lastKnownAskPrice > 0) {
priceToUse = locked.lastKnownAskPrice;
}
const currentEquippedBid = item ? this.getSpyEquippedValue(item.itemHrid, item.enhancementLevel ?? 0) : 0;
const currentDifference = this.spyNoSellMode ? priceToUse : (priceToUse - currentEquippedBid);
if (currentDifference > 0) {
totalCost += currentDifference;
segmentData.push({
slot: slot,
cost: currentDifference
});
}
});
if (totalCost > 0) {
const progressPercent = Math.min((currentGold / totalCost) * 100, 100);
const stillNeeded = totalCost - currentGold;
let totalSeconds = stillNeeded <= 0 ? 0 : -1;
if (stillNeeded > 0 && this.totalPerDay > 0) {
const daysToAfford = stillNeeded / this.totalPerDay;
totalSeconds = Math.floor(daysToAfford * 24 * 60 * 60);
}
segmentData.forEach(seg => {
seg.percent = (seg.cost / totalCost) * 100;
});
everythingItem = {
name: 'Everything',
enhLevel: '',
seconds: totalSeconds,
percent: progressPercent,
segments: segmentData,
stillNeeded: stillNeeded
};
}
lockedSlots.forEach(slot => {
const locked = this.spyLockedComparisons[slot];
const item = itemByLocation.get(`/item_locations/${slot}`);
const currentAskPrice = this.getSpyAskPrice(locked.itemHrid, locked.enhLevel);
let priceToUse = currentAskPrice;
if (currentAskPrice === 0 && (!locked.lastKnownAskPrice || locked.lastKnownAskPrice === 0)) {
return;
}
if (currentAskPrice === 0 && locked.lastKnownAskPrice && locked.lastKnownAskPrice > 0) {
priceToUse = locked.lastKnownAskPrice;
}
const currentEquippedBid = item
? this.getSpyEquippedValue(item.itemHrid, item.enhancementLevel ?? 0)
: 0;
const currentDifference = this.spyNoSellMode ? priceToUse : (priceToUse - currentEquippedBid);
const stillNeeded = currentDifference - currentGold;
const itemData = {
name: this.getSpyItemName(locked.itemHrid),
enhLevel: locked.enhLevel,
seconds: stillNeeded <= 0 ? 0 : -1,
percent: stillNeeded <= 0 ? 100 : Math.min((currentGold / currentDifference) * 100, 100),
stillNeeded: stillNeeded
};
if (stillNeeded > 0 && this.totalPerDay > 0) {
const daysToAfford = stillNeeded / this.totalPerDay
const totalSeconds = daysToAfford * 24 * 60 * 60;
itemData.seconds = totalSeconds;
if (totalSeconds < shortestTime) {
shortestTime = totalSeconds;
shortestItem = itemData;
}
} else if (stillNeeded <= 0) {
if (0 < shortestTime) {
shortestTime = 0;
shortestItem = itemData;
}
}
if (this.spySelectedHeaderSlot && slot === this.spySelectedHeaderSlot) {
selectedItem = itemData;
}
});
if (this.spySelectedHeaderSlot === 'everything' && everythingItem) {
selectedItem = everythingItem;
}
const displayItem = selectedItem || shortestItem;
if (!displayItem) {
statusEl.innerHTML = 'Calculating...';
statusEl.style.color = '#aaa';
return;
}
const formatTime = (totalSeconds) => mcsFormatDuration(totalSeconds, 'compact');
const timeStr = !inCombat ? '<span class="mcs-ew-no-combat">No Combat</span>'
: displayItem.seconds < 0 ? '--' : formatTime(displayItem.seconds);
const itemDisplay = displayItem.enhLevel !== ''
? `${displayItem.name} +${displayItem.enhLevel}`
: displayItem.name;
let tickMarksHtml = '';
if (displayItem.segments && displayItem.segments.length > 1) {
let cumulativePercent = 0;
displayItem.segments.forEach((seg, idx) => {
cumulativePercent += seg.percent;
if (idx < displayItem.segments.length - 1) {
tickMarksHtml += `<div class="mcs-ew-tick-mark" style="left: ${cumulativePercent}%;"></div>`;
}
});
}
const goldNeededText = displayItem.stillNeeded > 0 ? ` <span class="mcs-ew-gold-needed">${this.formatGoldCompact(displayItem.stillNeeded)}</span>` : '';
this.spyHeaderDisplayItem = {
name: displayItem.name,
enhLevel: displayItem.enhLevel,
seconds: displayItem.seconds,
percent: displayItem.percent,
stillNeeded: displayItem.stillNeeded,
inCombat: inCombat,
timeStr: !inCombat ? 'No Combat' : displayItem.seconds < 0 ? '--' : formatTime(displayItem.seconds)
};
if (inCombat) {
this.spyFrozenHeaderValues = {
name: displayItem.name,
enhLevel: displayItem.enhLevel,
seconds: displayItem.seconds,
percent: displayItem.percent,
segments: displayItem.segments,
stillNeeded: displayItem.stillNeeded,
timeStr: timeStr
};
}
const barColor = inCombat ?
(displayItem.percent >= 100 ? '#4CAF50' : '#6495ED') :
(displayItem.percent >= 100 ? '#4CAF50' : '#f44336');
statusEl.innerHTML = `
<div class="mcs-ew-status-col">
<div class="mcs-ew-status-row">
<span style="color: ${displayItem.name === 'Everything' ? '#FFA500' : '#FFD700'};">${itemDisplay}</span>
${goldNeededText}
<span style="color: ${inCombat ? (displayItem.percent >= 100 ? '#4CAF50' : '#f5a623') : '#f44336'}; font-weight: bold;">${timeStr}</span>
</div>
<div class="mcs-ew-header-bar-track">
<div style="width: ${displayItem.percent}%; height: 100%; background: ${barColor}; transition: width 0.3s ease; position: absolute; top: 0; left: 0;"></div>
${tickMarksHtml}
</div>
</div>
`;
}
setupPageContextBridge() {
const self = this;
if (this.bridgePollInterval) {
return;
}
const bridge = document.getElementById('equipspy-data-bridge');
if (!bridge) {
return;
}
let hasCharacterData = false;
let hasClientData = false;
let attemptCount = 0;
const maxAttempts = 60;
const pollInterval = setInterval(() => {
attemptCount++;
if (attemptCount % 10 === 0) {
}
if (!hasCharacterData) {
const charData = bridge.getAttribute('data-character-items');
if (charData) {
try {
const data = JSON.parse(charData);
if (self.spyCharacterItems.length === 0 || data.length > self.spyCharacterItems.length) {
self.spyCharacterItems = data;
self.updateSpyDisplay();
hasCharacterData = true;
}
} catch (e) {
}
}
}
if (!hasClientData) {
const clientData = bridge.getAttribute('data-item-detail-map');
if (clientData) {
try {
const data = JSON.parse(clientData);
self.spyItemDetailMap = data;
hasClientData = true;
} catch (e) {
}
}
}
if (hasCharacterData && hasClientData) {
clearInterval(pollInterval);
self.bridgePollInterval = null;
}
}, 500);
setTimeout(() => {
clearInterval(pollInterval);
if (!hasCharacterData) {
self.tryLoadFromPageContext();
setTimeout(() => {
if (self.spyCharacterItems.length === 0) {
const content = document.getElementById('spy-content');
if (content) {
content.innerHTML = '<div class="mcs-ew-error-msg">Equipment data failed to load.<br><br>Try:<br>1. Refresh the page (F5)<br>2. Change combat zones<br>3. Open your inventory</div>';
}
} else {
self.updateSpyDisplay();
}
}, 2000);
}
}, maxAttempts * 500);
self._ewatchBridgeForceLoadListener = (event) => {
self.spyCharacterItems = event.detail.items;
self.updateSpyDisplay();
};
window.addEventListener('EquipSpyForceLoadSuccess', self._ewatchBridgeForceLoadListener);
}
startDataPolling() {
const self = this;
let attempts = 0;
const maxAttempts = 30;
const pollInterval = setInterval(() => {
attempts++;
const equipped = self.extractEquipmentFromUI();
if (equipped.length > 0) {
self.spyCharacterItems = equipped;
self.updateSpyDisplay();
clearInterval(pollInterval);
} else if (attempts >= maxAttempts) {
clearInterval(pollInterval);
}
}, 1000);
}
extractEquipmentFromUI() {
const equipped = [];
try {
const rootElement = document.querySelector('#root') || document.body;
if (rootElement) {
const keys = Object.keys(rootElement);
for (const key of keys) {
if (key.startsWith('__reactContainer') ||
key.startsWith('__reactFiber') ||
key.startsWith('__reactInternalInstance')) {
try {
const reactData = rootElement[key];
const items = this.searchReactTree(reactData, 'characterItems');
if (items && Array.isArray(items) && items.length > 0) {
const equippedItems = items.filter(item => {
if (!item.itemLocationHrid) return false;
if (item.itemLocationHrid === '/item_locations/inventory') return false;
const slot = item.itemLocationHrid.replace('/item_locations/', '');
return this.spyConfig.ALLOWED_SLOTS.includes(slot);
});
if (equippedItems.length > 0) {
equipped.push(...equippedItems);
break;
}
}
} catch (e) {
}
}
}
}
} catch (e) {
}
return equipped;
}
searchReactTree(node, targetKey, depth = 0, maxDepth = 10, visited = new WeakSet()) {
if (depth > maxDepth || !node || typeof node !== 'object') {
return null;
}
if (visited.has(node)) {
return null;
}
visited.add(node);
if (node[targetKey]) {
return node[targetKey];
}
const propsToSearch = [
'child', 'sibling',
'memoizedState', 'memoizedProps', 'pendingProps',
'stateNode'
];
for (const prop of propsToSearch) {
if (node[prop]) {
try {
const result = this.searchReactTree(node[prop], targetKey, depth + 1, maxDepth, visited);
if (result) return result;
} catch (e) {
}
}
}
return null;
}
interceptNetworkRequests() {
if (this._networkIntercepted) return;
this._networkIntercepted = true;
this._ewatchWsListener = this.ewatchHandleWebSocketMessage.bind(this);
this._ewatchForceLoadListener = this.ewatchHandleForceLoad.bind(this);
this._ewatchCharDataListener = this.ewatchHandleCharacterData.bind(this);
this._ewatchCoinListener = this.ewatchHandleCoinUpdate.bind(this);
window.addEventListener('EquipSpyWebSocketMessage', this._ewatchWsListener);
window.addEventListener('EquipSpyForceLoadSuccess', this._ewatchForceLoadListener);
window.addEventListener('LootTrackerCharacterData', this._ewatchCharDataListener);
window.addEventListener('EquipSpyCoinUpdate', this._ewatchCoinListener);
}
tryLoadFromPageContext() {
try {
if (window.characterItems && Array.isArray(window.characterItems)) {
this.spyCharacterItems = window.characterItems;
}
const keys = Object.keys(localStorage);
for (const key of keys) {
if (key.toLowerCase().includes('character') || key.toLowerCase().includes('init')) {
try {
const data = localStorage.getItem(key);
if (data) {
const parsed = JSON.parse(data);
if (parsed.characterItems && Array.isArray(parsed.characterItems)) {
this.spyCharacterItems = parsed.characterItems;
break;
}
}
} catch (e) {
}
}
}
} catch (e) {
}
}
async loadSpyMarketData() {
const callTime = Date.now();
if (this.isLoadingMarketData) {
return;
}
if (this.spyMarketDataTimestamp && (callTime - this.spyMarketDataTimestamp) < 60000) {
const secAgo = ((callTime - this.spyMarketDataTimestamp) / 1000).toFixed(1);
return;
}
const fetchStart = performance.now();
try {
this.isLoadingMarketData = true;
const res = await fetch('https://www.milkywayidle.com/game_data/marketplace.json?t=' + callTime);
const json = await res.json();
this.spyMarketData = json.marketData ?? {};
this.spyMarketDataTimestamp = Date.now();
localStorage.setItem('mcs__global_market_timestamp', this.spyMarketDataTimestamp);
const fetchElapsed = performance.now() - fetchStart;
window.dispatchEvent(new CustomEvent('MarketDataUpdated', {
detail: { timestamp: this.spyMarketDataTimestamp }
}));
} catch (e) {
console.error('[EWatch] loadSpyMarketData FAILED:', e);
} finally {
this.isLoadingMarketData = false;
}
}
loadSpySettings() {
try {
const savedLocked = this.ewStorage.get('locked');
if (savedLocked) {
this.spyLockedComparisons = typeof savedLocked === 'string' ? JSON.parse(savedLocked) : savedLocked;
}
const savedHeaderSlot = this.ewStorage.get('selected_header_slot');
if (savedHeaderSlot) {
this.spySelectedHeaderSlot = savedHeaderSlot;
}
this.loadComparisonOrder();
const savedMarketValue = this.ewStorage.get('market_value_mode');
this.spyMarketValueMode = savedMarketValue !== false && savedMarketValue !== 'false';
} catch (e) {
}
}
createSpyPane() {
const savedEquipSpyMinimizedRaw = this.ewStorage.get('minimized');
this.spyIsMinimized = savedEquipSpyMinimizedRaw === true || savedEquipSpyMinimizedRaw === 'true';
const pane = document.createElement('div');
pane.id = this.spyConfig.PANE_ID;
pane.className = 'mcs-ew-pane';
try {
const pos = this.ewStorage.get('position');
if (pos) {
pane.style.top = (typeof pos.top === 'number' ? pos.top + 'px' : pos.top);
pane.style.right = (typeof pos.right === 'number' ? pos.right + 'px' : pos.right);
pane.style.left = 'auto';
}
} catch (e) {
}
pane.style.background = this.spyConfig.BG_COLOR;
const header = document.createElement('div');
header.className = 'mcs-ew-header';
header.innerHTML = `
<div class="mcs-ew-header-left">
<span class="mcs-ew-header-title">EWatch</span>
<div id="spy-header-status" class="mcs-ew-header-status">
Not Watching Anything
</div>
</div>
<div class="mcs-ew-header-buttons">
<button id="spy-nosell-toggle" class="mcs-ew-btn" style="
background: ${this.spyNoSellMode ? 'rgba(255, 100, 100, 0.3)' : 'rgba(100, 149, 237, 0.3)'};
border-color: ${this.spyNoSellMode ? '#ff6666' : '#6495ED'};
color: ${this.spyNoSellMode ? '#ff6666' : '#6495ED'};
">${this.spyNoSellMode ? 'No Sell' : 'Sell'}</button>
<button id="spy-simple-mode-btn" class="mcs-ew-btn" style="
background: rgba(100,149,237,0.3);
border-color: #6495ED;
color: #6495ED;
">Simple</button>
<button id="spy-minimize-btn" class="mcs-ew-btn-minimize">${this.spyIsMinimized ? '+' : '-'}</button>
</div>
`;
const content = document.createElement('div');
content.id = 'spy-content';
content.className = 'mcs-ew-content';
content.style.display = this.spyIsMinimized ? 'none' : 'block';
content.innerHTML = '<div class="mcs-ew-status-msg">Ready</div>';
const profitCostSection = document.createElement('div');
profitCostSection.id = 'spy-profit-cost-section';
profitCostSection.className = 'mcs-ew-profit-section';
profitCostSection.style.borderBottom = `1px solid ${this.spyConfig.BORDER_COLOR}`;
profitCostSection.innerHTML = `
<div class="mcs-ew-profit-label" style="color: #4CAF50;">Profit:</div>
<div id="spy-profit-type" class="mcs-ew-profit-type">Bid</div>
<div id="spy-profit-value" class="mcs-ew-profit-value" style="color: #4CAF50;">0 g/day</div>
<div class="mcs-ew-profit-label" style="color: #FF6B6B;">Cost:</div>
<div id="spy-cost-type" class="mcs-ew-profit-type">Bid</div>
<div id="spy-cost-value" class="mcs-ew-profit-value" style="color: #FF6B6B;">0 g/day</div>
<div class="mcs-ew-profit-label" style="color: #FFD700;">Total:</div>
<div class="mcs-ew-profit-empty"></div>
<div id="spy-total-value" class="mcs-ew-profit-value" style="color: #FFD700;">0 g/day</div>
`;
pane.appendChild(header);
pane.appendChild(profitCostSection);
pane.appendChild(content);
document.body.appendChild(pane);
registerPanel('equipment-spy-pane');
const scrollbarStyle = document.createElement('style');
scrollbarStyle.textContent = `
#spy-content::-webkit-scrollbar {
width: 24px;
}
#spy-content::-webkit-scrollbar-track {
background: rgba(0, 0, 0, 0.3);
border-radius: 4px;
}
#spy-content::-webkit-scrollbar-thumb {
background: rgba(76, 175, 80, 0.6);
border-radius: 4px;
border: 0px solid rgba(0, 0, 0, 0.3);
}
#spy-content::-webkit-scrollbar-thumb:hover {
background: rgba(76, 175, 80, 0.8);
}
/* Firefox scrollbar styling for content */
#spy-content {
scrollbar-width: auto;
scrollbar-color: rgba(76, 175, 80, 0.6) rgba(0, 0, 0, 0.3);
}
/* 4x LARGER scrollbars for dropdown selects (48px = 4x default 12px) */
select[id^="spy-item-select"]::-webkit-scrollbar {
width: 10px !important;
}
select[id^="spy-item-select"]::-webkit-scrollbar-track {
background: rgba(0, 0, 0, 0.5);
border-radius: 6px;
}
select[id^="spy-item-select"]::-webkit-scrollbar-thumb {
background: rgba(76, 175, 80, 0.7);
border-radius: 6px;
border: 0px solid rgba(0, 0, 0, 0.5);
}
select[id^="spy-item-select"]::-webkit-scrollbar-thumb:hover {
background: rgba(76, 175, 80, 0.9);
}
/* Firefox scrollbar styling for dropdowns */
select[id^="spy-item-select"] {
scrollbar-width: thick !important;
scrollbar-color: rgba(76, 175, 80, 0.7) rgba(0, 0, 0, 0.5);
}
/* Make dropdown list taller to show scrollbar better */
select[id^="spy-item-select"] {
height: auto !important;
max-height: 300px !important;
}
/* Drag cursors */
.spy-slot-draggable {
cursor: grab !important;
}
.spy-slot-draggable:active {
cursor: grabbing !important;
}
/* Force grabbing cursor during drag everywhere - prevent copy/not-allowed */
body.spy-dragging,
body.spy-dragging *,
body.spy-dragging .spy-slot-draggable,
body.spy-dragging #spy-content,
body.spy-dragging .spy-everything-section,
body.spy-dragging .spy-drop-zone {
cursor: grabbing !important;
}
`;
document.head.appendChild(scrollbarStyle);
let dragOffset = { x: 0, y: 0 };
this._spyOnDragMove = (e) => {
let newLeft = e.clientX - dragOffset.x;
let newTop = e.clientY - dragOffset.y;
const winWidth = window.innerWidth;
const winHeight = window.innerHeight;
const headerHeight = header.offsetHeight;
const paneWidth = pane.getBoundingClientRect().width;
newLeft = Math.max(-paneWidth + 100, Math.min(newLeft, winWidth - 100));
newTop = Math.max(0, Math.min(newTop, winHeight - headerHeight));
pane.style.left = newLeft + 'px';
pane.style.top = newTop + 'px';
pane.style.right = 'auto';
};
this._spyOnDragUp = () => {
document.removeEventListener('mousemove', this._spyOnDragMove);
document.removeEventListener('mouseup', this._spyOnDragUp);
const rect = pane.getBoundingClientRect();
const right = window.innerWidth - rect.right;
this.ewStorage.set('position', { top: rect.top, right: right });
};
this._spyOnDragStart = (e) => {
if (e.target.tagName === 'BUTTON') return;
const rect = pane.getBoundingClientRect();
dragOffset.x = e.clientX - rect.left;
dragOffset.y = e.clientY - rect.top;
document.addEventListener('mousemove', this._spyOnDragMove);
document.addEventListener('mouseup', this._spyOnDragUp);
};
header.addEventListener('mousedown', this._spyOnDragStart);
document.getElementById('spy-minimize-btn').addEventListener('click', () => this.toggleSpyMinimize());
document.getElementById('spy-nosell-toggle').addEventListener('click', () => this.toggleNoSellMode());
document.getElementById('spy-simple-mode-btn').addEventListener('click', () => {
if (this.spyIsInteracting || this.spyOpenComparisons.size > 0) {
return;
}
this.spySimpleMode = !this.spySimpleMode;
this.ewStorage.set('simple_mode', this.spySimpleMode);
const btn = document.getElementById('spy-simple-mode-btn');
if (this.spySimpleMode) {
btn.textContent = 'Edit';
btn.style.background = 'rgba(76, 175, 80, 0.3)';
btn.style.borderColor = '#4CAF50';
btn.style.color = '#4CAF50';
} else {
btn.textContent = 'Lock';
btn.style.background = 'rgba(100,149,237,0.3)';
btn.style.borderColor = '#6495ED';
btn.style.color = '#6495ED';
}
this.updateSpyDisplay();
});
this._ewatchBattleListener = this.ewatchHandleBattleEvent.bind(this);
this._ewatchCombatEndedListener = this.ewatchHandleBattleEvent.bind(this);
window.addEventListener('LootTrackerBattle', this._ewatchBattleListener);
window.addEventListener('LootTrackerCombatEnded', this._ewatchCombatEndedListener);
this.updateSpyDisplay();
const simpleModeBtn = document.getElementById('spy-simple-mode-btn');
if (this.spySimpleMode && simpleModeBtn) {
simpleModeBtn.textContent = 'Edit';
simpleModeBtn.style.background = 'rgba(76, 175, 80, 0.3)';
simpleModeBtn.style.borderColor = '#4CAF50';
simpleModeBtn.style.color = '#4CAF50';
} else if (simpleModeBtn) {
simpleModeBtn.textContent = 'Lock';
simpleModeBtn.style.background = 'rgba(100,149,237,0.3)';
simpleModeBtn.style.borderColor = '#6495ED';
simpleModeBtn.style.color = '#6495ED';
}
if (this.spyIsMinimized && simpleModeBtn) {
simpleModeBtn.style.display = 'none';
}
if (this.spyIsMinimized) {
const header = pane.querySelector('div:first-child');
if (header) {
header.style.borderRadius = '8px';
}
}
VisibilityManager.register('ewatch-market-refresh', async () => {
if (this.spyIsInteracting) return;
await this.loadSpyMarketData();
this.updateLastKnownPrices();
}, 10 * 60 * 1000);
VisibilityManager.register('ewatch-profit-cost', () => {
if (this.spyIsInteracting) {
return;
}
this.updateProfitCostDisplay();
this.updateHeaderStatus();
}, 1000);
this._ewatchEquipChangedListener = this.ewatchHandleEquipmentChanged.bind(this);
window.addEventListener('MCS_EquipmentChanged', this._ewatchEquipChangedListener);
setTimeout(() => {
const testSlot = document.querySelector('.spy-slot-draggable');
if (testSlot) {
const cursor = window.getComputedStyle(testSlot).cursor;
}
}, 1000);
if (!document._spyDragoverAdded) {
document._spyDragoverAdded = true;
this._spyDragoverListener = (e) => {
if (document.body.classList.contains('spy-dragging')) {
e.preventDefault();
e.dataTransfer.dropEffect = 'move';
}
};
document.addEventListener('dragover', this._spyDragoverListener);
}
}
calculateRevenuePerDay(priceType) {
if (!window.lootDropsTrackerInstance) {
return 0;
}
const playerStats = window.lootDropsTrackerInstance.playerDropStats ?? {};
const userName = window.lootDropsTrackerInstance.userName;
if (!playerStats[userName]) return 0;
const items = playerStats[userName].items ?? {};
let total = 0;
for (const hrid in items) {
const count = items[hrid];
const unitValue = window.getUnitValue(hrid, 'live', 0, priceType);
if (unitValue !== null) {
total += unitValue * count;
}
}
let hoursElapsed = 0;
if (window.lootDropsTrackerInstance.startTime && window.lootDropsTrackerInstance.isLiveSessionActive) {
const elapsedMs = Date.now() - window.lootDropsTrackerInstance.startTime.getTime();
hoursElapsed = elapsedMs / (1000 * 60 * 60);
}
if (hoursElapsed < 0.01) return 0;
return (total / hoursElapsed) * 24;
}
getItemPriceForCost(itemHrid, priceType) {
if (!this.spyMarketData || !this.spyMarketData[itemHrid]) return 0;
const itemData = this.spyMarketData[itemHrid];
const level0Data = itemData[0];
if (!level0Data) return 0;
return priceType === 'ask' ? (level0Data.a ?? 0) : (level0Data.b ?? 0);
}
updateProfitCostDisplay() {
const profitTypeEl = document.getElementById('spy-profit-type');
const profitValueEl = document.getElementById('spy-profit-value');
const costTypeEl = document.getElementById('spy-cost-type');
const costValueEl = document.getElementById('spy-cost-value');
const totalValueEl = document.getElementById('spy-total-value');
if (!profitValueEl || !costValueEl || !totalValueEl) {
return;
}
if (!window.lootDropsTrackerInstance) {
return;
}
const hwhatMode = window.lootDropsTrackerInstance.hwhatMode || 'lazy';
let profitPerDay = 0;
let costPerDay = 0;
let taxPerDay = 0;
profitPerDay = window.lootDropsTrackerInstance.hwhatCurrentRevenue ?? 0;
if (hwhatMode === 'lazy') {
costPerDay = window.lootDropsTrackerInstance.hwhatCostAsk ?? 0;
} else {
costPerDay = window.lootDropsTrackerInstance.hwhatCostBid ?? 0;
}
if (window.lootDropsTrackerInstance.hwhatTaxEnabled) {
taxPerDay = window.lootDropsTrackerInstance.hwhatCowbellTaxPerDay ?? 0;
}
const totalPerDay = profitPerDay - costPerDay - taxPerDay;
this.totalPerDay = totalPerDay;
this.totalPerDayFormatted = this.formatGoldNumber(totalPerDay);
const priceTypeDisplay = hwhatMode === 'lazy' ? 'Lazy' : 'Mid';
if (profitTypeEl) profitTypeEl.textContent = priceTypeDisplay;
if (costTypeEl) costTypeEl.textContent = priceTypeDisplay;
profitValueEl.textContent = this.formatGoldNumber(profitPerDay) + ' coin/day';
costValueEl.textContent = this.formatGoldNumber(costPerDay) + ' coin/day';
totalValueEl.textContent = this.formatGoldNumber(totalPerDay) + ' coin/day';
totalValueEl.style.color = 'gold';
}
formatGoldNumber(num) {
const showFullNumbers = this.hwhatShowFullNumbers !== false;
if (showFullNumbers) {
return Math.floor(num).toLocaleString('en-US');
}
const sign = num < 0 ? '-' : '';
return sign + mcsFormatCurrency(Math.abs(num), 'abbreviated');
}
manualRefresh() {
this.tryLoadFromPageContext();
this.updateSpyDisplay();
}
toggleSpyMinimize() {
this.spyIsMinimized = !this.spyIsMinimized;
this.ewStorage.set('minimized', this.spyIsMinimized);
const pane = document.getElementById('equipment-spy-pane');
const content = document.getElementById('spy-content');
const btn = document.getElementById('spy-minimize-btn');
const simpleModeBtn = document.getElementById('spy-simple-mode-btn');
const headerStatus = document.getElementById('spy-header-status');
const header = pane?.querySelector('div:first-child');
if (!pane || !content || !btn) {
return;
}
if (this.spyIsMinimized) {
content.style.display = 'none';
btn.textContent = '+';
if (simpleModeBtn) simpleModeBtn.style.display = 'none';
if (headerStatus) headerStatus.style.display = 'flex';
if (header) header.style.borderRadius = '8px';
} else {
content.style.display = 'block';
btn.textContent = '-';
if (simpleModeBtn) simpleModeBtn.style.display = 'block';
if (headerStatus) headerStatus.style.display = 'flex';
if (header) header.style.borderRadius = '6px 6px 0 0';
this.constrainPanelToBoundaries('equipment-spy-pane', this.spyConfig.STORAGE_KEY, false);
}
this.updateSpyDisplay();
}
formatSpyCoins(value) {
if (value === null || typeof value === 'undefined') return '0';
return Math.round(value).toLocaleString();
}
formatGoldCompact(value) {
return mcsFormatCurrency(value, 'abbreviated');
}
getSpyCurrentGold() {
try {
let totalGold = 0;
const coinItem = this.spyCharacterItems.find(item => item.itemHrid === '/items/coin');
if (coinItem && coinItem.count) {
totalGold = coinItem.count;
}
const { totalAsk, totalBid } = this.mcs_nt_calculateTotals ? this.mcs_nt_calculateTotals() : { totalAsk: 0, totalBid: 0 };
const useAskPrice = window.getFlootUseAskPrice ? window.getFlootUseAskPrice() : false;
const ntallyPrice = useAskPrice ? totalAsk : totalBid;
totalGold += ntallyPrice;
if (this.spyMarketValueMode !== false) {
const marketTotal = this.mcs_nt_calculateMarketTotal ? this.mcs_nt_calculateMarketTotal() : 0;
totalGold += marketTotal;
}
return totalGold;
} catch (e) {
return 0;
}
}
getSpyItemName(hrid) {
if (!hrid) return 'Unknown';
if (hrid === '/items/empty') return 'Empty';
if (this.spyItemDetailMap[hrid] && this.spyItemDetailMap[hrid].name) {
return this.spyItemDetailMap[hrid].name;
}
return mcsFormatHrid(hrid);
}
getSpyBidPrice(itemHrid, enhancementLevel = 0) {
if (itemHrid === '/items/empty') return 0;
if (!this.spyMarketData[itemHrid]) return 0;
const priceData = this.spyMarketData[itemHrid][enhancementLevel] || this.spyMarketData[itemHrid][0];
return priceData?.b > 0 ? priceData.b : 0;
}
getSpyEquippedValue(itemHrid, enhancementLevel = 0) {
if (itemHrid === '/items/empty') return 0;
const useAskPrice = window.getFlootUseAskPrice ? window.getFlootUseAskPrice() : false;
const basePrice = useAskPrice
? this.getSpyAskPrice(itemHrid, enhancementLevel)
: this.getSpyBidPrice(itemHrid, enhancementLevel);
if (basePrice < 900) {
return Math.floor(basePrice * 0.98);
} else {
return Math.ceil(basePrice * 0.98);
}
}
getSpyAskPrice(itemHrid, enhancementLevel = 0) {
if (itemHrid === '/items/empty') return 0;
if (!this.spyMarketData[itemHrid]) return 0;
const priceData = this.spyMarketData[itemHrid][enhancementLevel];
return priceData?.a > 0 ? priceData.a : 0;
}
getSpyAvailableEnhancements(itemHrid) {
return Array.from({ length: 21 }, (_, i) => i);
}
getSpyItemsInCategory(slot) {
const items = [];
for (const [hrid, details] of Object.entries(this.spyItemDetailMap)) {
if (details.equipmentDetail &&
details.equipmentDetail.type === `/equipment_types/${slot}`) {
items.push({ hrid, name: details.name });
}
}
return items.sort((a, b) => a.name.localeCompare(b.name));
}
lockSpyComparison(slot, itemHrid, enhLevel, askPrice, difference) {
const savedItems = [...this.spyCharacterItems];
const currentGold = this.getSpyCurrentGold();
const goldNeeded = difference - currentGold;
const hasMarketData = this.spyMarketData[itemHrid] &&
this.spyMarketData[itemHrid][enhLevel] &&
this.spyMarketData[itemHrid][enhLevel].a > 0;
this.spyLockedComparisons[slot] = {
itemHrid: itemHrid,
enhLevel: enhLevel,
askPrice: hasMarketData ? askPrice : 0,
difference: hasMarketData ? difference : 0,
goldNeeded: hasMarketData && goldNeeded > 0 ? goldNeeded : 0,
timestamp: Date.now(),
lastKnownAskPrice: hasMarketData ? askPrice : 0,
lastKnownPriceTimestamp: hasMarketData ? Date.now() : null,
hasMarketData: hasMarketData
};
this.ignoreNextStorageEvent = true;
this.ewStorage.set('locked', this.spyLockedComparisons);
setTimeout(() => {
this.ignoreNextStorageEvent = false;
}, 100);
const comparisonElement = document.getElementById(`spy-comparison-${slot}`);
if (comparisonElement) {
comparisonElement.remove();
}
this.spyCharacterItems = savedItems;
this.updateSpyDisplay();
this.updateHeaderStatus();
}
saveLockedComparisons() {
if (this.ignoreNextStorageEvent) return;
this.ewStorage.set('locked', this.spyLockedComparisons);
}
unlockSpyComparison(slot) {
const savedItems = [...this.spyCharacterItems];
delete this.spyLockedComparisons[slot];
if (this.spySelectedHeaderSlot === slot) {
this.spySelectedHeaderSlot = null;
this.ewStorage.set('selected_header_slot', null);
}
this.ignoreNextStorageEvent = true;
this.ewStorage.set('locked', this.spyLockedComparisons);
setTimeout(() => {
this.ignoreNextStorageEvent = false;
}, 100);
if (this.purchaseTimerIntervals && this.purchaseTimerIntervals[slot]) {
clearInterval(this.purchaseTimerIntervals[slot]);
delete this.purchaseTimerIntervals[slot];
}
this.spyCharacterItems = savedItems;
this.updateSpyDisplay();
this.updateHeaderStatus();
}
toggleHeaderSelection(slot) {
if (this.spySelectedHeaderSlot === slot) {
this.spySelectedHeaderSlot = null;
} else {
this.spySelectedHeaderSlot = slot;
}
this.ewStorage.set('selected_header_slot', this.spySelectedHeaderSlot);
this.updateSpyDisplay();
this.updateHeaderStatus();
}
updateLockedTimers() {
if (!this.purchaseTimerIntervals) {
this.purchaseTimerIntervals = {};
}
const itemByLocation = this._buildItemLocationMap();
Object.keys(this.spyLockedComparisons).forEach(slot => {
const locked = this.spyLockedComparisons[slot];
if (this.purchaseTimerIntervals[slot]) {
return;
}
const updateDisplay = () => {
if (document.hidden) return;
const etaDisplay = document.getElementById(`spy-locked-eta-${slot}`);
if (!etaDisplay) return;
const inCombat = this.isInCombat();
if (!inCombat) {
if (!this.spyFrozenLockedTimers) {
this.spyFrozenLockedTimers = {};
}
const frozenTimer = this.spyFrozenLockedTimers[slot];
if (frozenTimer) {
etaDisplay.innerHTML = `<div class="mcs-ew-eta-text">
<span class="mcs-ew-nc-label">NC</span>
<span class="mcs-ew-nc-time"> (${frozenTimer})</span>
</div>`;
} else {
etaDisplay.innerHTML = `<div class="mcs-ew-eta-nc">
NC
</div>`;
}
return;
}
const item = itemByLocation.get(`/item_locations/${slot}`);
const currentPrice = this.getSpyAskPrice(locked.itemHrid, locked.enhLevel);
if (currentPrice === 0 && (!locked.lastKnownAskPrice || locked.lastKnownAskPrice === 0)) {
etaDisplay.innerHTML = `<div class="mcs-ew-eta-waiting">
Never Seen
</div>`;
return;
}
if (!this.totalPerDay || this.totalPerDay <= 0) {
etaDisplay.innerHTML = `<div class="mcs-ew-eta-waiting">
Waiting for data...
</div>`;
return;
}
let priceToUse = currentPrice;
if (currentPrice === 0 && locked.lastKnownAskPrice && locked.lastKnownAskPrice > 0) {
priceToUse = locked.lastKnownAskPrice;
}
const currentEquippedBid = item
? this.getSpyEquippedValue(item.itemHrid, item.enhancementLevel ?? 0)
: 0;
const currentDifference = this.spyNoSellMode
? priceToUse
: (priceToUse - currentEquippedBid);
const currentGold = this.getSpyCurrentGold();
const stillNeeded = currentDifference - currentGold;
if (stillNeeded <= 0) {
if (!this.spyFrozenLockedTimers) {
this.spyFrozenLockedTimers = {};
}
this.spyFrozenLockedTimers[slot] = `Affordable!`;
etaDisplay.innerHTML = `<div class="mcs-ew-eta-affordable">
Affordable!
</div>`;
return;
}
const daysNeeded = stillNeeded / this.totalPerDay;
const hoursNeeded = daysNeeded * 24;
const minutesNeeded = hoursNeeded * 60;
const secondsNeeded = minutesNeeded * 60;
let timeStr;
if (daysNeeded >= 1) {
const days = Math.floor(daysNeeded);
const hours = Math.floor((daysNeeded - days) * 24);
const minutes = Math.floor(((daysNeeded - days) * 24 - hours) * 60);
const seconds = Math.floor((((daysNeeded - days) * 24 - hours) * 60 - minutes) * 60);
timeStr = `${days}d ${hours}h ${minutes}m ${seconds}s`;
} else if (hoursNeeded >= 1) {
const hours = Math.floor(hoursNeeded);
const minutes = Math.floor((hoursNeeded - hours) * 60);
const seconds = Math.floor(((hoursNeeded - hours) * 60 - minutes) * 60);
timeStr = `${hours}h ${minutes}m ${seconds}s`;
} else if (minutesNeeded >= 1) {
const minutes = Math.floor(minutesNeeded);
const seconds = Math.floor((minutesNeeded - minutes) * 60);
timeStr = `${minutes}m ${seconds}s`;
} else {
const seconds = Math.ceil(secondsNeeded);
timeStr = `${seconds}s`;
}
if (!this.spyFrozenLockedTimers) {
this.spyFrozenLockedTimers = {};
}
this.spyFrozenLockedTimers[slot] = `ETA: ${timeStr}`;
etaDisplay.innerHTML = `<div class="mcs-ew-eta-timer">
ETA: ${timeStr}
</div>`;
};
updateDisplay();
this.purchaseTimerIntervals[slot] = setInterval(updateDisplay, 1000);
});
}
updateCoinHeader() {
if (this.spyIsInteracting) {
return;
}
const coinAmountElement = document.getElementById('spy-coin-amount');
const coinPerDayElement = document.getElementById('spy-coin-per-day');
if (!coinAmountElement || !coinPerDayElement) {
return;
}
const itemByLocation = this._buildItemLocationMap();
let coinOnlyGold = 0;
let hasGoldData = false;
if (this.spyCharacterItems && Array.isArray(this.spyCharacterItems)) {
const coinItem = this.spyCharacterItems.find(item => item.itemHrid === '/items/coin');
if (coinItem && typeof coinItem.count === 'number') {
coinOnlyGold = coinItem.count;
hasGoldData = true;
}
}
const currentGold = this.getSpyCurrentGold();
coinAmountElement.textContent = this.formatSpyCoins(coinOnlyGold);
const hwhatMode = window.lootDropsTrackerInstance?.hwhatMode || 'lazy';
const inCombat = this.isInCombat();
if (!inCombat && this.spyFrozenProfit) {
const frozen = this.spyFrozenProfit;
coinPerDayElement.style.color = 'gold';
coinPerDayElement.style.fontWeight = 'bold';
coinPerDayElement.innerHTML = `<span class="mcs-ew-nc-label">NC</span> <span class="mcs-ew-nc-time">(${frozen.strategyLabel}: ${frozen.profitFormatted}/day)</span>`;
} else {
const currentRevenue = this.hwhatCurrentRevenue ?? 0;
const currentCostForMode = hwhatMode === 'lazy' ? (this.hwhatCostAsk ?? 0) : (this.hwhatCostBid ?? 0);
let currentProfit = currentRevenue - currentCostForMode;
if (currentProfit > 0) {
const profitFormatted = this.formatGoldNumber(currentProfit);
const strategyLabel = hwhatMode === 'lazy' ? 'Lazy' : 'Mid';
coinPerDayElement.style.color = 'gold';
coinPerDayElement.style.fontWeight = 'bold';
coinPerDayElement.textContent = `${strategyLabel}: ${profitFormatted}/day`;
if (inCombat) {
this.spyFrozenProfit = { profitFormatted, strategyLabel };
}
} else {
coinPerDayElement.style.color = '#999';
coinPerDayElement.style.fontWeight = 'normal';
coinPerDayElement.textContent = 'No income data';
}
}
Object.keys(this.spyLockedComparisons).forEach(slot => {
const locked = this.spyLockedComparisons[slot];
const item = itemByLocation.get(`/item_locations/${slot}`);
const currentPrice = this.getSpyAskPrice(locked.itemHrid, locked.enhLevel);
let priceToUse = currentPrice;
if (currentPrice === 0 && (!locked.lastKnownAskPrice || locked.lastKnownAskPrice === 0)) {
return;
}
if (currentPrice === 0 && locked.lastKnownAskPrice && locked.lastKnownAskPrice > 0) {
priceToUse = locked.lastKnownAskPrice;
}
const currentEquippedBid = item ?
this.getSpyEquippedValue(item.itemHrid, item.enhancementLevel ?? 0) : 0;
const currentDifference = this.spyNoSellMode ? priceToUse : (priceToUse - currentEquippedBid);
const needGold = currentDifference;
let progressPercent = 0;
let isAffordable = false;
if (!hasGoldData) {
progressPercent = 0;
isAffordable = false;
} else if (needGold > 0) {
progressPercent = Math.min((currentGold / needGold) * 100, 100);
isAffordable = progressPercent >= 100;
} else {
progressPercent = 100;
isAffordable = true;
}
const progressBarContainer = document.querySelector(`#spy-slot-${slot} .mcs-ew-progress-track`);
if (progressBarContainer) {
const progressBar = progressBarContainer.querySelector('div');
const progressText = progressBarContainer.closest('.mcs-ew-progress-row').querySelector('.mcs-ew-progress-text');
if (progressBar) {
progressBar.style.width = `${progressPercent}%`;
const inCombat = this.isInCombat();
progressBar.style.background = inCombat ? (isAffordable ? '#4CAF50' : '#6495ED') : '#f44336';
}
if (progressText) {
const inCombat = this.isInCombat();
progressText.style.color = inCombat ? (isAffordable ? '#fff' : '#6495ED') : '#f44336';
progressText.style.fontWeight = isAffordable ? 'bold' : 'normal';
progressText.textContent = !hasGoldData ? 'Waiting for data...' :
(isAffordable ? 'Affordable' : `${progressPercent.toFixed(5)}%`);
}
}
const remainingGoldEl = document.getElementById(`spy-remaining-${slot}`);
if (remainingGoldEl) {
const stillNeeded = currentDifference - currentGold;
if (stillNeeded > 0) {
remainingGoldEl.textContent = `${this.formatGoldCompact(stillNeeded)} needed`;
remainingGoldEl.style.display = '';
} else {
remainingGoldEl.style.display = 'none';
}
}
});
const lockedSlots = Object.keys(this.spyLockedComparisons);
if (lockedSlots.length > 0) {
let totalCost = 0;
lockedSlots.forEach(slot => {
const locked = this.spyLockedComparisons[slot];
const item = itemByLocation.get(`/item_locations/${slot}`);
const currentAskPrice = this.getSpyAskPrice(locked.itemHrid, locked.enhLevel);
let priceToUse = currentAskPrice;
if (currentAskPrice === 0 && locked.lastKnownAskPrice && locked.lastKnownAskPrice > 0) {
priceToUse = locked.lastKnownAskPrice;
}
const currentEquippedBid = item ?
this.getSpyEquippedValue(item.itemHrid, item.enhancementLevel ?? 0) : 0;
const currentDifference = this.spyNoSellMode ? priceToUse : (priceToUse - currentEquippedBid);
if (currentDifference > 0) {
totalCost += currentDifference;
}
});
if (totalCost > 0) {
const progressPercent = Math.min((currentGold / totalCost) * 100, 100);
const isAffordable = progressPercent >= 100;
const everythingTimer = document.getElementById('spy-everything-timer');
if (everythingTimer && this.goldPerDay > 0 && !isAffordable) {
const stillNeeded = totalCost - currentGold;
const daysToAfford = stillNeeded / this.totalPerDay;
const totalSeconds = Math.floor(daysToAfford * 24 * 60 * 60);
const months = Math.floor(totalSeconds / (30 * 24 * 60 * 60));
const days = Math.floor((totalSeconds % (30 * 24 * 60 * 60)) / (24 * 60 * 60));
const hours = Math.floor((totalSeconds % (24 * 60 * 60)) / (60 * 60));
const minutes = Math.floor((totalSeconds % (60 * 60)) / 60);
const seconds = totalSeconds % 60;
const parts = [];
if (months > 0) parts.push(`${months}mo`);
if (days > 0) parts.push(`${days}d`);
if (hours > 0) parts.push(`${hours}h`);
if (minutes > 0) parts.push(`${minutes}m`);
if (seconds > 0 || parts.length === 0) parts.push(`${seconds}s`);
everythingTimer.textContent = parts.join(' ');
everythingTimer.style.color = '#f5a623';
} else if (everythingTimer && isAffordable) {
everythingTimer.textContent = 'Affordable!';
everythingTimer.style.color = '#66ff66';
everythingTimer.style.fontWeight = 'bold';
}
const everythingProgressBar = document.querySelector('.mcs-ew-everything-track > div:first-child');
const everythingProgressText = document.querySelector('.mcs-ew-everything .mcs-ew-progress-text');
if (everythingProgressBar) {
everythingProgressBar.style.width = `${progressPercent}%`;
const inCombat = this.isInCombat();
everythingProgressBar.style.background = inCombat ? (isAffordable ? '#4CAF50' : '#6495ED') : '#f44336';
}
if (everythingProgressText) {
const inCombat = this.isInCombat();
everythingProgressText.style.color = inCombat ? (isAffordable ? '#fff' : '#6495ED') : '#f44336';
everythingProgressText.style.fontWeight = isAffordable ? 'bold' : 'normal';
everythingProgressText.textContent = !hasGoldData ? 'Waiting for data...' :
(isAffordable ? 'Affordable' : `${progressPercent.toFixed(5)}%`);
}
}
}
}
toggleNoSellMode() {
this.spyNoSellMode = !this.spyNoSellMode;
this.ewStorage.set('no_sell_mode', this.spyNoSellMode);
const toggle = document.getElementById('spy-nosell-toggle');
if (toggle) {
if (this.spyNoSellMode) {
toggle.textContent = 'No Sell';
toggle.style.background = 'rgba(255, 100, 100, 0.3)';
toggle.style.borderColor = '#ff6666';
toggle.style.color = '#ff6666';
} else {
toggle.textContent = 'Sell';
toggle.style.background = 'rgba(100, 149, 237, 0.3)';
toggle.style.borderColor = '#6495ED';
toggle.style.color = '#6495ED';
}
}
this.updateSpyDisplay();
}
toggleMarketValueMode() {
this.spyMarketValueMode = !this.spyMarketValueMode;
this.ewStorage.set('market_value_mode', this.spyMarketValueMode);
const toggle = document.getElementById('spy-market-value-toggle');
if (toggle) {
if (this.spyMarketValueMode) {
toggle.textContent = 'Market Value On';
toggle.style.background = 'rgba(76, 175, 80, 0.3)';
toggle.style.borderColor = '#4CAF50';
toggle.style.color = '#4CAF50';
} else {
toggle.textContent = 'Market Value Off';
toggle.style.background = 'rgba(255, 100, 100, 0.3)';
toggle.style.borderColor = '#ff6666';
toggle.style.color = '#ff6666';
}
}
this.updateSpyDisplay();
}
showSpyComparison(slot, currentItemHrid) {
const slotContainer = document.getElementById(`spy-slot-${slot}`);
if (!slotContainer) {
return;
}
if (this.spyLockedComparisons[slot]) {
return;
}
const existingDropdown = document.getElementById(`spy-comparison-${slot}`);
if (existingDropdown) {
this.spyIsInteracting = false;
this.spyOpenComparisons.delete(slot);
existingDropdown.remove();
this.updateSpyDisplay();
return;
}
this.spyIsInteracting = true;
this.spyOpenComparisons.add(slot);
const items = this.getSpyItemsInCategory(slot);
if (items.length === 0) return;
const container = document.getElementById(`spy-slot-${slot}`);
if (!container) return;
const dropdown = document.createElement('div');
dropdown.id = `spy-comparison-${slot}`;
dropdown.className = 'mcs-ew-comparison';
dropdown.style.borderLeft = `2px solid ${this.spyConfig.MAIN_COLOR}`;
let html = `
<div class="mcs-ew-compare-header">
<div class="mcs-ew-compare-label">Compare with:</div>
<button id="spy-watch-comparison-${slot}" class="mcs-ew-watch-btn">👁️ Watch</button>
</div>
<select id="spy-item-select-${slot}" class="mcs-ew-item-select" size=10>
<option value="">-- Select Item --</option>
`;
items.forEach(item => {
const selected = item.hrid === currentItemHrid ? 'selected' : '';
let lastSeenText = '';
if (this.spyMarketData[item.hrid]) {
let mostRecentTimestamp = 0;
Object.keys(this.spyMarketData[item.hrid]).forEach(level => {
const data = this.spyMarketData[item.hrid][level];
if (data && data.t && data.t > mostRecentTimestamp) {
mostRecentTimestamp = data.t;
}
});
if (mostRecentTimestamp > 0) {
const timeSince = Date.now() - mostRecentTimestamp;
const minutesAgo = Math.floor(timeSince / (1000 * 60));
const hoursAgo = Math.floor(minutesAgo / 60);
const daysAgo = Math.floor(hoursAgo / 24);
if (daysAgo > 0) {
lastSeenText = ` (${daysAgo}d ago)`;
} else if (hoursAgo > 0) {
lastSeenText = ` (${hoursAgo}h ago)`;
} else if (minutesAgo > 0) {
lastSeenText = ` (${minutesAgo}m ago)`;
} else {
lastSeenText = ` (just now)`;
}
}
}
html += `<option value="${item.hrid}"${selected}>${item.name}${lastSeenText}</option>`;
});
html += `</select>`;
html += `<div id="spy-enh-buttons-${slot}" class="mcs-ew-enh-container"></div>`;
html += `<div id="spy-price-display-${slot}" class="mcs-ew-price-display"></div>`;
html += `<div id="spy-eta-display-${slot}" class="mcs-ew-eta-display"></div>`;
dropdown.innerHTML = html;
container.appendChild(dropdown);
const select = document.getElementById(`spy-item-select-${slot}`);
if (!this.ewatchDropdownListeners) {
this.ewatchDropdownListeners = new Map();
}
const dropdownKey = `dropdown-${slot}`;
if (this.ewatchDropdownListeners.has(dropdownKey)) {
const oldListeners = this.ewatchDropdownListeners.get(dropdownKey);
if (oldListeners.change) {
select.removeEventListener('change', oldListeners.change);
}
if (oldListeners.focus) {
select.removeEventListener('focus', oldListeners.focus);
}
if (oldListeners.blur) {
select.removeEventListener('blur', oldListeners.blur);
}
}
const changeHandler = (e) => {
const selectedHrid = e.target.value;
if (!selectedHrid) {
document.getElementById(`spy-enh-buttons-${slot}`).innerHTML = '';
document.getElementById(`spy-price-display-${slot}`).innerHTML = '';
return;
}
this.showSpyEnhancementButtons(slot, selectedHrid);
};
const focusHandler = () => {
this.spyIsInteracting = true;
};
const blurHandler = () => {
this.spyIsInteracting = false;
};
this.ewatchDropdownListeners.set(dropdownKey, {
change: changeHandler,
focus: focusHandler,
blur: blurHandler
});
select.addEventListener('change', changeHandler);
select.addEventListener('focus', focusHandler);
select.addEventListener('blur', blurHandler);
const watchBtn = document.getElementById(`spy-watch-comparison-${slot}`);
const watchBtnKey = `watchbtn-${slot}`;
if (this.ewatchDropdownListeners.has(watchBtnKey)) {
const oldWatchClick = this.ewatchDropdownListeners.get(watchBtnKey).click;
if (oldWatchClick) {
watchBtn.removeEventListener('click', oldWatchClick);
}
}
const watchBtnClickHandler = () => {
const selectedHrid = select.value;
if (!selectedHrid) {
return;
}
const selectedBtn = dropdown.querySelector('.spy-enh-btn.spy-selected');
if (!selectedBtn) {
return;
}
const enhLevel = parseInt(selectedBtn.dataset.level);
const askPrice = this.getSpyAskPrice(selectedHrid, enhLevel);
const currentItem = this.spyCharacterItems.find(item => item.itemLocationHrid === `/item_locations/${slot}`);
const currentBid = currentItem ? this.getSpyEquippedValue(currentItem.itemHrid, currentItem.enhancementLevel ?? 0) : 0;
const difference = this.spyNoSellMode ? askPrice : (askPrice - currentBid);
this.spyIsInteracting = false;
this.spyOpenComparisons.delete(slot);
this.lockSpyComparison(slot, selectedHrid, enhLevel, askPrice, difference);
};
this.ewatchDropdownListeners.set(watchBtnKey, { click: watchBtnClickHandler });
watchBtn.addEventListener('click', watchBtnClickHandler);
if (currentItemHrid) {
this.showSpyEnhancementButtons(slot, currentItemHrid);
}
}
showSpyEnhancementButtons(slot, itemHrid) {
const buttonContainer = document.getElementById(`spy-enh-buttons-${slot}`);
const priceDisplay = document.getElementById(`spy-price-display-${slot}`);
if (!buttonContainer || !priceDisplay) return;
const levelsWithData = new Set();
if (this.spyMarketData[itemHrid]) {
Object.keys(this.spyMarketData[itemHrid]).forEach(level => {
const data = this.spyMarketData[itemHrid][level];
if (data && data.a > 0) {
levelsWithData.add(parseInt(level));
}
});
}
let html = '<div class="mcs-ew-enh-col">';
html += '<div class="mcs-ew-enh-row">';
for (let level = 0; level <= 10; level++) {
const hasData = levelsWithData.has(level);
const bgColor = hasData ? 'rgba(76, 175, 80, 0.5)' : 'rgba(100, 100, 100, 0.3)';
const borderColor = hasData ? '#4CAF50' : '#666';
html += `<button class="spy-enh-btn mcs-ew-enh-btn" data-slot="${slot}" data-hrid="${itemHrid}" data-level="${level}" style="
background: ${bgColor};
border: 1px solid ${borderColor};
">+${level}</button>`;
}
html += '</div>';
html += '<div class="mcs-ew-enh-row">';
for (let level = 11; level <= 20; level++) {
const hasData = levelsWithData.has(level);
const bgColor = hasData ? 'rgba(76, 175, 80, 0.5)' : 'rgba(100, 100, 100, 0.3)';
const borderColor = hasData ? '#4CAF50' : '#666';
html += `<button class="spy-enh-btn mcs-ew-enh-btn" data-slot="${slot}" data-hrid="${itemHrid}" data-level="${level}" style="
background: ${bgColor};
border: 1px solid ${borderColor};
">+${level}</button>`;
}
html += '</div>';
html += '</div>';
buttonContainer.innerHTML = html;
if (!this.ewatchButtonListeners) {
this.ewatchButtonListeners = new Map();
}
const containerKey = `container-${slot}`;
if (this.ewatchButtonListeners.has(containerKey)) {
const oldListeners = this.ewatchButtonListeners.get(containerKey);
if (oldListeners.mouseenter) {
buttonContainer.removeEventListener('mouseenter', oldListeners.mouseenter);
}
if (oldListeners.mouseleave) {
buttonContainer.removeEventListener('mouseleave', oldListeners.mouseleave);
}
}
const mouseenterHandler = () => {
this.spyIsInteracting = true;
};
const mouseleaveHandler = () => {
setTimeout(() => {
this.spyIsInteracting = false;
}, 100);
};
this.ewatchButtonListeners.set(containerKey, {
mouseenter: mouseenterHandler,
mouseleave: mouseleaveHandler
});
buttonContainer.addEventListener('mouseenter', mouseenterHandler);
buttonContainer.addEventListener('mouseleave', mouseleaveHandler);
const equippedItem = this.spyCharacterItems.find(item => item.itemLocationHrid === `/item_locations/${slot}`
);
const equippedBidPrice = equippedItem ? this.getSpyEquippedValue(equippedItem.itemHrid, equippedItem.enhancementLevel ?? 0) : 0;
const buttonKey = `buttons-${slot}`;
if (this.ewatchButtonListeners.has(buttonKey)) {
const oldClickListener = this.ewatchButtonListeners.get(buttonKey).click;
if (oldClickListener) {
buttonContainer.removeEventListener('click', oldClickListener);
}
}
const buttonClickHandler = (e) => {
const btn = e.target.closest('.spy-enh-btn');
if (!btn) return;
e.stopPropagation();
this.spyIsInteracting = true;
buttonContainer.querySelectorAll('.spy-enh-btn').forEach(b => {
const btnLevel = parseInt(b.dataset.level);
const btnHasData = levelsWithData.has(btnLevel);
b.style.background = btnHasData ? 'rgba(76, 175, 80, 0.5)' : 'rgba(100, 100, 100, 0.3)';
b.style.borderColor = btnHasData ? '#4CAF50' : '#666';
b.classList.remove('spy-selected');
});
btn.style.background = this.spyConfig.MAIN_COLOR;
btn.style.borderColor = this.spyConfig.MAIN_COLOR;
btn.classList.add('spy-selected');
const hrid = btn.dataset.hrid;
const level = parseInt(btn.dataset.level);
const hasMarketData = this.spyMarketData[hrid] &&
this.spyMarketData[hrid][level] &&
this.spyMarketData[hrid][level].a > 0;
const askPrice = hasMarketData ? this.getSpyAskPrice(hrid, level) : 0;
const itemName = this.getSpyItemName(hrid);
const etaDisplay = document.getElementById(`spy-eta-display-${slot}`);
const difference = hasMarketData ?
(this.spyNoSellMode ? Number(askPrice) : (Number(askPrice) - Number(equippedBidPrice)))
: 0;
if (this.purchaseTimerInterval) {
clearInterval(this.purchaseTimerInterval);
this.purchaseTimerInterval = null;
}
if (etaDisplay) {
etaDisplay.innerHTML = '';
}
if (hasMarketData) {
if (difference > 0) {
if (this.totalPerDay > 0) {
const currentGold = this.getSpyCurrentGold();
const goldNeeded = difference - currentGold;
if (goldNeeded > 0) {
const targetGold = difference;
const updateTimerDisplay = () => {
if (document.hidden) return;
const inCombat = this.isInCombat();
if (!inCombat) {
if (etaDisplay) {
if (this.spyFrozenEditModeTimer) {
etaDisplay.innerHTML = `<div class="mcs-ew-eta-text">
<span class="mcs-ew-nc-label">NC</span>
<span class="mcs-ew-nc-time"> (${this.spyFrozenEditModeTimer})</span>
</div>`;
} else {
etaDisplay.innerHTML = `<div class="mcs-ew-eta-nc">
NC
</div>`;
}
}
return;
}
const currentGold = this.getSpyCurrentGold();
const stillNeeded = targetGold - currentGold;
if (stillNeeded <= 0) {
this.spyFrozenEditModeTimer = 'Affordable!';
if (etaDisplay) {
etaDisplay.innerHTML = `<div class="mcs-ew-eta-affordable-inline">
Affordable!
</div>`;
}
if (this.purchaseTimerInterval) {
clearInterval(this.purchaseTimerInterval);
this.purchaseTimerInterval = null;
}
return;
}
const daysToAfford = stillNeeded / this.totalPerDay;
const totalSeconds = Math.floor(daysToAfford * 24 * 60 * 60);
const months = Math.floor(totalSeconds / (30 * 24 * 60 * 60));
const days = Math.floor((totalSeconds % (30 * 24 * 60 * 60)) / (24 * 60 * 60));
const hours = Math.floor((totalSeconds % (24 * 60 * 60)) / (60 * 60));
const minutes = Math.floor((totalSeconds % (60 * 60)) / 60);
const seconds = totalSeconds % 60;
const parts = [];
if (months > 0) parts.push(`${months}mo`);
if (days > 0) parts.push(`${days}d`);
parts.push(`${hours}h`);
parts.push(`${minutes}m`);
parts.push(`${seconds}s`);
const timerDisplay = parts.join(' ');
this.spyFrozenEditModeTimer = `Time: ${timerDisplay}`;
if (etaDisplay) {
etaDisplay.innerHTML = `<div class="mcs-ew-eta-countdown">
Time: ${timerDisplay}
</div>`;
}
};
updateTimerDisplay();
this.purchaseTimerInterval = setInterval(updateTimerDisplay, 1000);
} else {
if (etaDisplay) {
etaDisplay.innerHTML = `<div class="mcs-ew-eta-affordable-inline">
Affordable!
</div>`;
}
}
} else {
if (etaDisplay) {
etaDisplay.innerHTML = `<div class="mcs-ew-eta-nodata">
No income data from loot tracker.
</div>`;
}
}
}
} else {
if (etaDisplay) {
etaDisplay.innerHTML = `<div class="mcs-ew-eta-nodata">
Never Seen
</div>`;
}
}
const diffColor = difference > 0 ? '#ff6666' : difference < 0 ? '#66ff66' : '#FFD700';
const diffSign = difference > 0 ? '+' : '';
let timestampText = '';
if (this.spyMarketDataTimestamp) {
const timeSince = Date.now() - this.spyMarketDataTimestamp;
const minutesAgo = Math.floor(timeSince / (1000 * 60));
const hoursAgo = Math.floor(minutesAgo / 60);
const daysAgo = Math.floor(hoursAgo / 24);
if (daysAgo > 0) {
timestampText = `${daysAgo}d ago`;
} else if (hoursAgo > 0) {
timestampText = `${hoursAgo}h ago`;
} else if (minutesAgo > 0) {
timestampText = `${minutesAgo}m ago`;
} else {
timestampText = 'just now';
}
}
const currentGold = this.getSpyCurrentGold();
const stillNeeded = difference - currentGold;
const needsGoldText = stillNeeded > 0 ? ` <span class="mcs-ew-gold-needed">${this.formatGoldCompact(stillNeeded)} needed</span>` : '';
priceDisplay.innerHTML = `
${itemName} +${level}${needsGoldText}<br>
${hasMarketData && timestampText ?
`<span class="mcs-ew-price-lastseen">Last Seen: ${timestampText}</span><br>` :
`<span class="mcs-ew-price-nodata">⚠ No Market Data</span><br>`}
Lowest Ask: ${hasMarketData ? this.formatSpyCoins(askPrice) : 'N/A'}<br>
<span style="color: ${diffColor};">Difference: ${hasMarketData ? diffSign + this.formatSpyCoins(difference) : 'N/A'}</span>
`;
const lockBtn = document.getElementById(`spy-lock-comparison-${slot}`);
if (lockBtn) {
lockBtn.onclick = () => this.lockSpyComparison(slot, hrid, level, askPrice, difference);
}
setTimeout(() => {
this.spyIsInteracting = false;
}, 2000);
};
this.ewatchButtonListeners.set(buttonKey, { click: buttonClickHandler });
buttonContainer.addEventListener('click', buttonClickHandler);
}
spyEyeButtonHtml(slot, isSelected) {
const icon = isSelected
? `👁️`
: `<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="#999" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M17.94 17.94A10.07 10.07 0 0 1 12 20c-7 0-11-8-11-8a18.45 18.45 0 0 1 5.06-5.94"/><path d="M9.9 4.24A9.12 9.12 0 0 1 12 4c7 0 11 8 11 8a18.5 18.5 0 0 1-2.16 3.19"/><line x1="1" y1="1" x2="23" y2="23"/><path d="M14.12 14.12a3 3 0 1 1-4.24-4.24"/></svg>`;
return `<button class="spy-header-select-btn mcs-ew-eye-btn" data-slot="${slot}" style="
background: ${isSelected ? 'rgba(100, 149, 237, 0.3)' : 'rgba(100, 100, 100, 0.2)'};
border: 1px solid ${isSelected ? '#6495ED' : '#666'};
color: ${isSelected ? '#6495ED' : '#999'};
" title="Show this item in header">${icon}</button>`;
}
spyProgressBarHtml(progressPercent, isAffordable, hasGoldData) {
const barColor = this.isInCombat() ? (isAffordable ? '#4CAF50' : '#6495ED') : '#f44336';
const textColor = this.isInCombat() ? (isAffordable ? '#fff' : '#6495ED') : '#f44336';
const statusText = !hasGoldData ? 'Waiting for data...' : (isAffordable ? 'Affordable' : `${progressPercent.toFixed(5)}%`);
return `<div class="mcs-ew-progress-row">` +
`<div class="mcs-ew-progress-track">` +
`<div style="width: ${progressPercent}%; height: 100%; background: ${barColor}; transition: width 0.3s ease;"></div>` +
`</div>` +
`<span class="mcs-ew-progress-text" style="color: ${textColor}; font-weight: ${isAffordable ? 'bold' : 'normal'};">` +
statusText +
`</span>` +
`</div>`;
}
spySlotWrapperOpen(isSelected) {
return `<div style="
margin-top: 4px; padding: 6px 6px 2px 6px; border-radius: 3px;
background: ${isSelected ? 'rgba(100, 149, 237, 0.1)' : 'rgba(255,215,0,0.1)'};
border-left: 2px solid ${isSelected ? '#6495ED' : '#FFD700'};
">`;
}
updateSpyDisplay() {
if (this.spyIsInteracting || this.spyOpenComparisons.size > 0) {
return;
}
if (this.purchaseTimerIntervals) {
Object.keys(this.purchaseTimerIntervals).forEach(slot => {
clearInterval(this.purchaseTimerIntervals[slot]);
});
this.purchaseTimerIntervals = {};
}
const content = document.getElementById('spy-content');
if (!content) return;
const itemByLocation = this._buildItemLocationMap();
let equipped = this.spyCharacterItems.filter(item => {
if (!item.itemLocationHrid || item.itemLocationHrid === '/item_locations/inventory') {
return false;
}
const slot = item.itemLocationHrid.replace('/item_locations/', '');
return this.spyConfig.ALLOWED_SLOTS.includes(slot);
});
if (equipped.length === 0) {
content.innerHTML = `
<div class="mcs-ew-no-equip">
<div class="mcs-ew-no-equip-mb">No equipment equipped</div>
${createForceLoadEquipmentButton()}
</div>
`;
setTimeout(() => {
const btn = document.getElementById('spy-force-load-btn');
if (btn) {
btn.addEventListener('click', forceExtractEquipmentData);
}
}, 100);
return;
}
const openComparisons = {};
this.spyConfig.ALLOWED_SLOTS.forEach(slot => {
const comparisonElement = document.getElementById(`spy-comparison-${slot}`);
if (comparisonElement && !comparisonElement.classList.contains('spy-default-watch-panel')) {
openComparisons[slot] = {
isOpen: true,
selectedItem: document.getElementById(`spy-item-select-${slot}`)?.value || '',
selectedEnhancement: null
};
const selectedBtn = comparisonElement.querySelector('.spy-enh-btn[style*="' + this.spyConfig.MAIN_COLOR + '"]');
if (selectedBtn) {
openComparisons[slot].selectedEnhancement = parseInt(selectedBtn.dataset.level);
}
}
});
let totalBidValue = 0;
let currentGold = 0;
let hasGoldData = false;
const coinItem = this.spyCharacterItems.find(item => item.itemHrid === '/items/coin');
if (coinItem && coinItem.count) {
currentGold = coinItem.count;
hasGoldData = true;
}
const goldPerDayFormatted = this.goldPerDayFormatted || '';
let html = '<div class="mcs-ew-content-wrap">';
html += `<div class="mcs-ew-gold-bar">`;
html += `<div class="mcs-ew-gold-coin">`;
html += `<span class="mcs-ew-coin-icon">${createItemIconHtml('coin', { width: '100%', height: '100%', sprite: 'items_sprite' })}</span>`;
html += `<span id="spy-coin-amount">${this.formatSpyCoins(currentGold)}</span>`;
html += `</div>`;
const { totalAsk, totalBid } = this.mcs_nt_calculateTotals ? this.mcs_nt_calculateTotals() : { totalAsk: 0, totalBid: 0 };
const useAskPrice = window.getFlootUseAskPrice ? window.getFlootUseAskPrice() : false;
const ntallyPrice = useAskPrice ? totalAsk : totalBid;
const ntallyLabel = useAskPrice ? 'ask' : 'bid';
const ntallyColor = useAskPrice ? '#6495ED' : '#4CAF50';
const marketTotal = this.mcs_nt_calculateMarketTotal ? this.mcs_nt_calculateMarketTotal() : 0;
html += `<div class="mcs-ew-gold-stat" style="color: ${ntallyPrice > 0 ? ntallyColor : '#999'}; font-weight: ${ntallyPrice > 0 ? 'bold' : 'normal'};">`;
html += `<span>📦</span>`;
html += `<span>${this.mcs_nt_formatNumber ? this.mcs_nt_formatNumber(ntallyPrice) : ntallyPrice.toLocaleString()} ${ntallyLabel}</span>`;
html += `</div>`;
html += `<div class="mcs-ew-gold-stat" style="color: ${marketTotal > 0 ? '#FFD700' : '#999'}; font-weight: ${marketTotal > 0 ? 'bold' : 'normal'};">`;
html += `<span>📈</span>`;
html += `<span>${this.mcs_nt_formatNumber ? this.mcs_nt_formatNumber(marketTotal) : marketTotal.toLocaleString()}</span>`;
html += `</div>`;
html += `<div id="spy-coin-per-day" class="mcs-ew-gold-stat" style="color: ${goldPerDayFormatted ? '#90EE90' : '#999'}; font-weight: ${goldPerDayFormatted ? 'bold' : 'normal'};">`;
if (goldPerDayFormatted) {
const inCombat = this.isInCombat();
if (!inCombat) {
html += `<span class="mcs-ew-nc-label">NC</span> <span class="mcs-ew-nc-time">(${goldPerDayFormatted}/day)</span>`;
} else {
html += `${goldPerDayFormatted}/day`;
}
} else {
html += `No income data`;
}
html += `</div>`;
html += `</div>`;
const lastRefreshed = this.spyMarketDataTimestamp
? new Date(this.spyMarketDataTimestamp).toLocaleString([], {
month: 'short',
day: 'numeric',
hour: 'numeric',
minute: '2-digit'
})
: 'Never';
html += `<div class="mcs-ew-market-bar">`;
html += `<span class="mcs-ew-market-label">Market Data: ${lastRefreshed}</span>`;
html += `<button id="spy-refresh-market" class="mcs-ew-refresh-btn">🔄 Refresh</button>`;
html += `</div>`;
const mvOn = this.spyMarketValueMode !== false;
html += `<div class="mcs-ew-market-value-bar">`;
html += `<span class="mcs-ew-market-label">Include market orders in calculations</span>`;
html += `<button id="spy-market-value-toggle" class="mcs-ew-toggle-btn" style="
background: ${mvOn ? 'rgba(76, 175, 80, 0.3)' : 'rgba(255, 100, 100, 0.3)'};
border-color: ${mvOn ? '#4CAF50' : '#ff6666'};
color: ${mvOn ? '#4CAF50' : '#ff6666'};
">${mvOn ? 'Market Value On' : 'Market Value Off'}</button>`;
html += `</div>`;
const slots = {};
this.spyConfig.ALLOWED_SLOTS.forEach(slot => {
slots[slot] = [];
});
equipped.forEach(item => {
const slot = item.itemLocationHrid.replace('/item_locations/', '');
if (this.spyConfig.ALLOWED_SLOTS.includes(slot) && item.count > 0) {
slots[slot].push(item);
}
});
this.spyConfig.ALLOWED_SLOTS.forEach(slot => {
if (slots[slot].length === 0) {
slots[slot].push({
itemLocationHrid: `/item_locations/${slot}`,
itemHrid: '/items/empty',
enhancementLevel: 0,
count: 0
});
}
});
const sortedSlots = this.getOrderedSlots(slots);
sortedSlots.forEach(slot => {
if (!this.spyConfig.ALLOWED_SLOTS.includes(slot)) {
return;
}
const slotName = slot.split('_').map(w => w.charAt(0).toUpperCase() + w.slice(1)).join(' ');
slots[slot].forEach(item => {
if (this.spySimpleMode && !this.spyLockedComparisons[slot]) {
return;
}
const itemName = this.getSpyItemName(item.itemHrid);
const enhLevel = item.enhancementLevel ?? 0;
const equippedValue = this.getSpyEquippedValue(item.itemHrid, enhLevel);
totalBidValue += equippedValue * item.count;
const enhText = enhLevel > 0 ? ` +${enhLevel}` : '';
const countText = item.count > 1 ? ` x${item.count}` : '';
const bidText = equippedValue > 0 ? this.formatSpyCoins(equippedValue) : 'No bid';
html += `<div id="spy-slot-${slot}"
draggable="true"
data-slot="${slot}"
class="spy-slot-draggable mcs-ew-slot">`;
if (!this.spySimpleMode) {
html += `<div class="mcs-ew-equip-row spy-equip-slot-row" data-slot="${slot}" data-item-hrid="${item.itemHrid}">`;
html += `<span class="mcs-ew-slot-name">${slotName}:</span>`;
html += `<span class="mcs-ew-slot-item">${itemName}${enhText}${countText}</span>`;
html += `<span class="mcs-ew-slot-bid">${bidText}</span>`;
html += `</div>`;
}
if (this.spyLockedComparisons[slot]) {
const locked = this.spyLockedComparisons[slot];
const lockedItemName = this.getSpyItemName(locked.itemHrid);
let currentAskPrice = this.getSpyAskPrice(locked.itemHrid, locked.enhLevel);
const hasCurrentMarketData = currentAskPrice > 0;
if (!hasCurrentMarketData && (!locked.lastKnownAskPrice || locked.lastKnownAskPrice === 0)) {
if (this.spySimpleMode) {
html += `<div class="mcs-ew-locked-nodata">`;
html += `<div class="mcs-ew-flex-between">`;
html += `<div class="mcs-ew-flex-col">`;
html += `<div class="mcs-ew-locked-name-nodata">${lockedItemName} +${locked.enhLevel}</div>`;
html += `<div class="mcs-ew-waiting-market">Waiting for market data...</div>`;
html += `</div>`;
html += `<div class="mcs-ew-never-seen">Never Seen</div>`;
html += `</div>`;
html += `</div>`;
} else {
html += `<div class="mcs-ew-locked-nodata">`;
html += `<div class="mcs-ew-flex-between">`;
html += `<div class="mcs-ew-watching-label">👁️ Watching:</div>`;
html += `<button class="spy-unlock-btn mcs-ew-unwatch-btn" data-slot="${slot}">✖ Unwatch</button>`;
html += `</div>`;
html += `<div class="mcs-ew-locked-name">${lockedItemName} +${locked.enhLevel}</div>`;
html += `<div class="mcs-ew-waiting-centered">Waiting for market data...</div>`;
html += `<div id="spy-locked-eta-${slot}" class="mcs-ew-eta-display"></div>`;
html += `</div>`;
}
} else {
let usingLastKnownPrice = false;
if (currentAskPrice === 0 && locked.lastKnownAskPrice && locked.lastKnownAskPrice > 0) {
currentAskPrice = locked.lastKnownAskPrice;
usingLastKnownPrice = true;
}
const currentEquippedBid = this.getSpyEquippedValue(item.itemHrid, enhLevel);
const currentDifference = this.spyNoSellMode ? currentAskPrice : (currentAskPrice - currentEquippedBid);
const diffColor = currentDifference > 0 ? '#ff6666' : currentDifference < 0 ? '#66ff66' : '#FFD700';
const diffSign = currentDifference > 0 ? '+' : '';
const totalGold = currentGold;
const needGold = currentDifference;
let progressPercent = 0;
let isAffordable = false;
if (needGold > 0) {
progressPercent = Math.min((totalGold / needGold) * 100, 100);
isAffordable = progressPercent >= 100;
} else {
progressPercent = 100;
isAffordable = true;
}
const isSelected = this.spySelectedHeaderSlot === slot;
const stillNeeded = currentDifference - currentGold;
const needsGoldText = stillNeeded > 0 ? ` <span id="spy-remaining-${slot}" class="mcs-ew-gold-needed">${this.formatGoldCompact(stillNeeded)} needed</span>` : '';
html += this.spySlotWrapperOpen(isSelected);
if (this.spySimpleMode) {
html += `<div class="mcs-ew-flex-between">`;
html += `<div class="mcs-ew-flex-row">`;
html += this.spyEyeButtonHtml(slot, isSelected);
html += `<div class="mcs-ew-flex-col">`;
html += `<div class="mcs-ew-locked-gold-name">${lockedItemName} +${locked.enhLevel}${needsGoldText}</div>`;
if (usingLastKnownPrice) {
html += `<div class="mcs-ew-last-price-note">Using last seen price</div>`;
}
html += `</div>`;
html += `</div>`;
html += `<div id="spy-locked-eta-${slot}" class="mcs-ew-locked-eta"></div>`;
html += `</div>`;
} else {
html += `<div class="mcs-ew-flex-between">`;
html += `<div class="mcs-ew-flex-row">`;
html += this.spyEyeButtonHtml(slot, isSelected);
html += `<div class="mcs-ew-watching-title">Watching</div>`;
html += `</div>`;
html += `<button class="spy-unlock-btn mcs-ew-unwatch-btn" data-slot="${slot}">✖ Unwatch</button>`;
html += `</div>`;
html += `<div class="mcs-ew-locked-gold-name-mb">${lockedItemName} +${locked.enhLevel}${needsGoldText}</div>`;
if (locked.lastKnownPriceTimestamp) {
const timeSinceLastSeen = Date.now() - locked.lastKnownPriceTimestamp;
const minutesAgo = Math.floor(timeSinceLastSeen / (1000 * 60));
const hoursAgo = Math.floor(minutesAgo / 60);
const daysAgo = Math.floor(hoursAgo / 24);
let timeAgoText = '';
if (daysAgo > 0) {
timeAgoText = `${daysAgo}d ago`;
} else if (hoursAgo > 0) {
timeAgoText = `${hoursAgo}h ago`;
} else if (minutesAgo > 0) {
timeAgoText = `${minutesAgo}m ago`;
} else {
timeAgoText = 'just now';
}
html += `<div class="mcs-ew-price-change">Last Price Change: ${timeAgoText}</div>`;
}
html += `<div class="mcs-ew-price-row">`;
html += `<span class="mcs-ew-price-label">Ask Price:</span>`;
if (usingLastKnownPrice) {
html += `<div class="mcs-ew-ask-price-group">`;
html += `<span class="mcs-ew-price-gold">${this.formatSpyCoins(currentAskPrice)}</span>`;
html += `<span class="mcs-ew-last-known-note">(last known)</span>`;
html += `</div>`;
} else {
html += `<span class="mcs-ew-price-gold">${this.formatSpyCoins(currentAskPrice)}</span>`;
}
html += `</div>`;
html += `<div class="mcs-ew-price-row-mb6">`;
html += `<span class="mcs-ew-price-label">Difference:</span>`;
html += `<span style="color: ${diffColor}; font-weight: bold;">${diffSign}${this.formatSpyCoins(currentDifference)}</span>`;
html += `</div>`;
}
html += this.spyProgressBarHtml(progressPercent, isAffordable, hasGoldData);
if (!this.spySimpleMode) {
html += `<div id="spy-locked-eta-${slot}" class="mcs-ew-eta-display"></div>`;
}
html += `</div>`;
}
} else {
html += `<div id="spy-default-${slot}" class="mcs-ew-default-panel spy-default-watch-panel" style="border-left: 2px solid ${this.spyConfig.MAIN_COLOR};" data-slot="${slot}" data-item-hrid="${item.itemHrid}">`;
html += `<div class="mcs-ew-default-inner">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="#666" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M17.94 17.94A10.07 10.07 0 0 1 12 20c-7 0-11-8-11-8a18.45 18.45 0 0 1 5.06-5.94"/>
<path d="M9.9 4.24A9.12 9.12 0 0 1 12 4c7 0 11 8 11 8a18.5 18.5 0 0 1-2.16 3.19"/>
<line x1="1" y1="1" x2="23" y2="23"/>
<path d="M14.12 14.12a3 3 0 1 1-4.24-4.24"/>
</svg>
<span>Click to watch</span>
</div>`;
html += `</div>`;
}
html += `</div>`;
});
});
if (!this.spySimpleMode) {
html += `<div class="mcs-ew-total-row" style="border-top: 1px solid ${this.spyConfig.BORDER_COLOR};">`;
html += `<span style="color: ${this.spyConfig.MAIN_COLOR};">Total:</span>`;
html += `<span style="color: #FFD700;">${this.formatSpyCoins(totalBidValue)}</span>`;
html += `</div>`;
}
let lockedSlots = Object.keys(this.spyLockedComparisons);
if (this.comparisonOrder && Array.isArray(this.comparisonOrder)) {
const orderedSlots = this.comparisonOrder.filter(slot => lockedSlots.includes(slot));
const newSlots = lockedSlots.filter(slot => !this.comparisonOrder.includes(slot));
lockedSlots = [...orderedSlots, ...newSlots];
}
if (lockedSlots.length > 0) {
let totalCost = 0;
let segmentData = [];
lockedSlots.forEach(slot => {
const locked = this.spyLockedComparisons[slot];
const item = itemByLocation.get(`/item_locations/${slot}`);
const currentAskPrice = this.getSpyAskPrice(locked.itemHrid, locked.enhLevel);
let priceToUse = currentAskPrice;
if (currentAskPrice === 0 && locked.lastKnownAskPrice && locked.lastKnownAskPrice > 0) {
priceToUse = locked.lastKnownAskPrice;
}
const currentEquippedBid = item ?
this.getSpyEquippedValue(item.itemHrid, item.enhancementLevel ?? 0) : 0;
const currentDifference = this.spyNoSellMode ? priceToUse : (priceToUse - currentEquippedBid);
if (currentDifference > 0) {
totalCost += currentDifference;
segmentData.push({
slot: slot,
cost: currentDifference
});
}
});
if (totalCost > 0) {
const progressPercent = Math.min((currentGold / totalCost) * 100, 100);
const isAffordable = progressPercent >= 100;
segmentData.forEach(seg => {
seg.percent = (seg.cost / totalCost) * 100;
});
const stillNeeded = totalCost - currentGold;
const everythingGoldText = stillNeeded > 0 ? ` <span class="mcs-ew-gold-needed">${this.formatGoldCompact(stillNeeded)} needed</span>` : '';
let everythingTimerHtml = '';
if (this.goldPerDay > 0 && !isAffordable) {
const daysToAfford = stillNeeded / this.totalPerDay;
const totalSeconds = Math.floor(daysToAfford * 24 * 60 * 60);
const months = Math.floor(totalSeconds / (30 * 24 * 60 * 60));
const days = Math.floor((totalSeconds % (30 * 24 * 60 * 60)) / (24 * 60 * 60));
const hours = Math.floor((totalSeconds % (24 * 60 * 60)) / (60 * 60));
const minutes = Math.floor((totalSeconds % (60 * 60)) / 60);
const seconds = totalSeconds % 60;
const parts = [];
if (months > 0) parts.push(`${months}mo`);
if (days > 0) parts.push(`${days}d`);
if (hours > 0) parts.push(`${hours}h`);
if (minutes > 0) parts.push(`${minutes}m`);
if (seconds > 0 || parts.length === 0) parts.push(`${seconds}s`);
everythingTimerHtml = `<div id="spy-everything-timer" class="mcs-ew-everything-timer">${parts.join(' ')}</div>`;
} else if (isAffordable) {
everythingTimerHtml = `<div class="mcs-ew-everything-affordable">Affordable!</div>`;
}
const isEverythingSelected = this.spySelectedHeaderSlot === 'everything';
html += `<div class="spy-everything-section mcs-ew-everything" style="
background: ${isEverythingSelected ? 'rgba(100, 149, 237, 0.1)' : 'rgba(255,165,0,0.1)'};
border-left: 2px solid ${isEverythingSelected ? '#6495ED' : '#FFA500'};
border-top: 1px solid ${this.spyConfig.BORDER_COLOR};
">`;
html += `<div class="mcs-ew-flex-between">`;
html += `<div class="mcs-ew-flex-row">`;
const eyeIconEverything = isEverythingSelected
? `👁️`
: `<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="#999" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M17.94 17.94A10.07 10.07 0 0 1 12 20c-7 0-11-8-11-8a18.45 18.45 0 0 1 5.06-5.94"/><path d="M9.9 4.24A9.12 9.12 0 0 1 12 4c7 0 11 8 11 8a18.5 18.5 0 0 1-2.16 3.19"/><line x1="1" y1="1" x2="23" y2="23"/><path d="M14.12 14.12a3 3 0 1 1-4.24-4.24"/></svg>`;
html += `<button class="spy-header-select-btn mcs-ew-eye-btn" data-slot="everything" style="
background: ${isEverythingSelected ? 'rgba(100, 149, 237, 0.3)' : 'rgba(100, 100, 100, 0.2)'};
border: 1px solid ${isEverythingSelected ? '#6495ED' : '#666'};
color: ${isEverythingSelected ? '#6495ED' : '#999'};
" title="Show everything in header">${eyeIconEverything}</button>`;
html += `<div class="mcs-ew-everything-title">Everything${everythingGoldText}</div>`;
html += `</div>`;
html += everythingTimerHtml;
html += `</div>`;
html += `<div class="mcs-ew-everything-progress">`;
html += `<div class="mcs-ew-everything-track">`;
const everythingBarColor = this.isInCombat() ? (isAffordable ? '#4CAF50' : '#6495ED') : '#f44336';
html += `<div style="width: ${progressPercent}%; height: 100%; background: ${everythingBarColor}; transition: width 0.3s ease; position: absolute; top: 0; left: 0;"></div>`;
let cumulativePercent = 0;
segmentData.forEach((seg, idx) => {
cumulativePercent += seg.percent;
if (idx < segmentData.length - 1) {
html += `<div class="mcs-ew-tick-mark" style="left: ${cumulativePercent}%;"></div>`;
}
});
html += `</div>`;
const everythingTextColor = this.isInCombat() ? (isAffordable ? '#fff' : '#6495ED') : '#f44336';
html += `<span class="mcs-ew-progress-text" style="color: ${everythingTextColor}; font-weight: ${isAffordable ? 'bold' : 'normal'};">`;
html += !hasGoldData ? 'Waiting for data...' : (isAffordable ? 'Affordable' : `${progressPercent.toFixed(5)}%`);
html += `</span>`;
html += `</div>`;
html += `</div>`;
}
}
if (this.spySimpleMode && Object.keys(this.spyLockedComparisons).length === 0) {
html += `<div class="mcs-ew-simple-empty">
Simple mode: Lock comparisons to track items
</div>`;
}
html += '</div>';
const preservedTimers = {};
Object.keys(this.spyLockedComparisons).forEach(slot => {
const existingEta = document.getElementById(`spy-locked-eta-${slot}`);
if (existingEta && existingEta.innerHTML.trim()) {
preservedTimers[slot] = existingEta.innerHTML;
}
});
content.innerHTML = html;
Object.keys(preservedTimers).forEach(slot => {
const etaElement = document.getElementById(`spy-locked-eta-${slot}`);
if (etaElement) {
etaElement.innerHTML = preservedTimers[slot];
}
});
const slotDivs = document.querySelectorAll('[id^="spy-slot-"]');
slotDivs.forEach(slotDiv => {
slotDiv.addEventListener('dragstart', (e) => {
this.spyIsInteracting = true;
this.draggedSlot = slotDiv.dataset.slot;
slotDiv.classList.add('dragging');
slotDiv.style.backgroundColor = 'rgba(200, 200, 200, 0.5)';
slotDiv.style.border = '2px solid #999';
document.body.classList.add('spy-dragging');
e.dataTransfer.effectAllowed = 'move';
e.dataTransfer.setData('text/plain', slotDiv.dataset.slot);
const canvas = document.createElement('canvas');
canvas.width = 1;
canvas.height = 1;
const ctx = canvas.getContext('2d');
ctx.clearRect(0, 0, 1, 1);
e.dataTransfer.setDragImage(canvas, 0, 0);
setTimeout(() => {
const spyContentEl = document.getElementById('spy-content');
const allSlots = [...spyContentEl.querySelectorAll('.spy-slot-draggable')];
const draggingIndex = allSlots.indexOf(slotDiv);
const isFirstItem = draggingIndex === 0;
const isLastItem = draggingIndex === allSlots.length - 1;
allSlots.forEach((slot, index) => {
if (index === draggingIndex) return;
if (isLastItem && index === draggingIndex + 1) return;
if (isFirstItem && index === 0) return;
if (isFirstItem && index === 1) return;
const dropZone = document.createElement('div');
dropZone.className = 'spy-drop-zone mcs-ew-drop-zone';
slot.parentNode.insertBefore(dropZone, slot);
});
if (!isLastItem) {
const everythingSection = spyContentEl.querySelector('.spy-everything-section');
const lastDropZone = document.createElement('div');
lastDropZone.className = 'spy-drop-zone mcs-ew-drop-zone';
if (everythingSection && everythingSection.parentNode) {
everythingSection.parentNode.insertBefore(lastDropZone, everythingSection);
} else {
spyContentEl.appendChild(lastDropZone);
}
}
}, 10);
});
slotDiv.addEventListener('dragend', (e) => {
this.spyIsInteracting = false;
slotDiv.classList.remove('dragging');
slotDiv.style.backgroundColor = '';
slotDiv.style.border = '';
document.body.classList.remove('spy-dragging');
this.draggedSlot = null;
this.lastHighlightedZone = null;
document.querySelectorAll('.spy-drop-zone').forEach(dz => dz.remove());
});
});
const spyContent = document.getElementById('spy-content');
if (spyContent && !spyContent.dataset.dragListenersAdded) {
spyContent.dataset.dragListenersAdded = 'true';
spyContent.addEventListener('dragover', (e) => {
e.preventDefault();
e.stopPropagation();
e.dataTransfer.dropEffect = 'move';
e.dataTransfer.effectAllowed = 'move';
const dropZone = document.elementFromPoint(e.clientX, e.clientY);
if (dropZone !== this.lastHighlightedZone) {
if (this.lastHighlightedZone && this.lastHighlightedZone.classList.contains('spy-drop-zone')) {
this.lastHighlightedZone.style.background = 'rgba(200, 200, 200, 0.3)';
this.lastHighlightedZone.style.borderColor = '#999';
}
if (dropZone && dropZone.classList.contains('spy-drop-zone')) {
dropZone.style.background = 'rgba(76, 175, 80, 0.6)';
dropZone.style.borderColor = '#4CAF50';
this.targetDropZone = dropZone;
} else {
this.targetDropZone = null;
}
this.lastHighlightedZone = dropZone;
}
});
spyContent.addEventListener('dragenter', (e) => {
e.preventDefault();
e.dataTransfer.dropEffect = 'move';
});
spyContent.addEventListener('drop', (e) => {
e.preventDefault();
if (!this.draggedSlot) return;
const spyContentEl = document.getElementById('spy-content');
const draggingElement = document.querySelector('.spy-slot-draggable.dragging');
if (this.targetDropZone && draggingElement) {
this.targetDropZone.parentNode.insertBefore(draggingElement, this.targetDropZone);
}
const allSlots = [...spyContentEl.querySelectorAll('[id^="spy-slot-"]')];
this.comparisonOrder = allSlots.map(el => el.dataset.slot);
this.saveComparisonOrder();
this.updateEverythingSegments();
this.targetDropZone = null;
});
}
document.querySelectorAll('.spy-default-watch-panel').forEach(panel => {
panel.addEventListener('click', () => {
const slot = panel.dataset.slot;
const itemHrid = panel.dataset.itemHrid;
this.showSpyComparison(slot, itemHrid);
});
});
document.querySelectorAll('.spy-unlock-btn').forEach(btn => {
btn.addEventListener('click', (e) => {
e.stopPropagation();
const slot = btn.dataset.slot;
this.unlockSpyComparison(slot);
});
});
document.querySelectorAll('.spy-header-select-btn').forEach(btn => {
btn.addEventListener('click', (e) => {
e.stopPropagation();
const slot = btn.dataset.slot;
this.toggleHeaderSelection(slot);
});
});
const refreshMarketBtn = document.getElementById('spy-refresh-market');
if (refreshMarketBtn) {
refreshMarketBtn.addEventListener('click', async () => {
refreshMarketBtn.disabled = true;
refreshMarketBtn.textContent = '⏳ Refreshing...';
refreshMarketBtn.style.opacity = '0.6';
await this.loadSpyMarketData();
this.updateLastKnownPrices();
this.updateSpyDisplay();
refreshMarketBtn.textContent = '✓ Updated!';
refreshMarketBtn.style.background = 'rgba(76, 175, 80, 0.3)';
refreshMarketBtn.style.borderColor = '#4CAF50';
refreshMarketBtn.style.color = '#4CAF50';
setTimeout(() => {
refreshMarketBtn.disabled = false;
refreshMarketBtn.textContent = '🔄 Refresh';
refreshMarketBtn.style.background = 'rgba(100, 149, 237, 0.3)';
refreshMarketBtn.style.borderColor = '#6495ED';
refreshMarketBtn.style.color = '#6495ED';
refreshMarketBtn.style.opacity = '1';
}, 2000);
});
}
const marketValueToggle = document.getElementById('spy-market-value-toggle');
if (marketValueToggle) {
marketValueToggle.addEventListener('click', () => this.toggleMarketValueMode());
}
this.updateCoinHeader();
setTimeout(() => this.updateLockedTimers(), 100);
VisibilityManager.clear('ewatch-coin-header');
VisibilityManager.register('ewatch-coin-header', () => this.updateCoinHeader(), 5000);
this.updateProfitCostDisplay();
this.updateHeaderStatus();
}
updateLastKnownPrices() {
let needsSave = false;
Object.keys(this.spyLockedComparisons).forEach(slot => {
const locked = this.spyLockedComparisons[slot];
const currentAskPrice = this.getSpyAskPrice(locked.itemHrid, locked.enhLevel);
if (currentAskPrice > 0 && currentAskPrice !== locked.lastKnownAskPrice) {
locked.lastKnownAskPrice = currentAskPrice;
locked.lastKnownPriceTimestamp = Date.now();
needsSave = true;
}
});
if (needsSave) {
this.saveLockedComparisons();
}
}
updateEverythingSegments() {
const everythingSection = document.querySelector('.spy-everything-section');
if (!everythingSection) return;
const lockedSlots = Object.keys(this.spyLockedComparisons);
if (lockedSlots.length === 0) return;
const itemByLocation = this._buildItemLocationMap();
let orderedLockedSlots = lockedSlots;
if (this.comparisonOrder && Array.isArray(this.comparisonOrder)) {
const orderedSlots = this.comparisonOrder.filter(slot => lockedSlots.includes(slot));
const newSlots = lockedSlots.filter(slot => !this.comparisonOrder.includes(slot));
orderedLockedSlots = [...orderedSlots, ...newSlots];
}
let totalCost = 0;
const segmentData = [];
orderedLockedSlots.forEach(slot => {
const locked = this.spyLockedComparisons[slot];
const item = itemByLocation.get(`/item_locations/${slot}`);
const currentAskPrice = this.getSpyAskPrice(locked.itemHrid, locked.enhLevel);
let priceToUse = currentAskPrice;
if (currentAskPrice === 0 && locked.lastKnownAskPrice && locked.lastKnownAskPrice > 0) {
priceToUse = locked.lastKnownAskPrice;
}
const currentEquippedBid = item ?
this.getSpyEquippedValue(item.itemHrid, item.enhancementLevel ?? 0) : 0;
const currentDifference = this.spyNoSellMode ? priceToUse : (priceToUse - currentEquippedBid);
if (currentDifference > 0) {
totalCost += currentDifference;
segmentData.push({
slot: slot,
cost: currentDifference
});
}
});
if (totalCost === 0) return;
segmentData.forEach(seg => {
seg.percent = (seg.cost / totalCost) * 100;
});
const progressBarContainer = everythingSection.querySelector('.mcs-ew-everything-track');
if (!progressBarContainer) return;
progressBarContainer.querySelectorAll('.mcs-ew-tick-mark').forEach(marker => marker.remove());
let cumulativePercent = 0;
segmentData.forEach((seg, idx) => {
cumulativePercent += seg.percent;
if (idx < segmentData.length - 1) {
const marker = document.createElement('div');
marker.className = 'mcs-ew-tick-mark';
marker.style.left = `${cumulativePercent}%`;
progressBarContainer.appendChild(marker);
}
});
}
getDragAfterElement(container, y) {
const draggableElements = [...container.querySelectorAll('.spy-slot-draggable:not([style*="opacity: 0.5"])')]
.filter(el => el.dataset.slot !== this.draggedSlot);
return draggableElements.reduce((closest, child) => {
const box = child.getBoundingClientRect();
const offset = y - box.top - box.height / 2;
if (offset < 0 && offset > closest.offset) {
return { offset: offset, element: child };
} else {
return closest;
}
}, { offset: Number.NEGATIVE_INFINITY }).element;
}
handleStartHiddenChange() {
this.startHiddenEnabled = this.domRefs.startHiddenCheckbox.checked;
localStorage.setItem(CONFIG.STORAGE.startHidden, String(this.startHiddenEnabled));
}
handleHistorySelectionChange() {
const selectedValue = this.domRefs.historySelect.value;
this.viewingLive = (selectedValue === 'live');
if (selectedValue === 'combined') {
this.aggregateSessionHistory();
}
this.renderCurrentView();
this.updateTimerDisplay();
}
bindUiEvents() {
this.domRefs.headerTopRow.addEventListener('mousedown', (e) => {
if (!this.isHidden && e.button === 0 && !e.target.closest('button, .ldt-settings-menu, .ldt-history-select, input')) {
this.startMove(e);
}
});
this.domRefs.panel.addEventListener('mousedown', (e) => {
if (this.isHidden && e.button === 0 && !e.target.closest('button')) {
this.startMove(e);
}
});
this.domRefs.settingsButton.onclick = (e) => { e.stopPropagation(); this.toggleSettingsMenu(); };
this.domRefs.clearButton.onclick = () => this.clearHistory();
this.domRefs.exportButton.onclick = () => this.exportLootCsv();
if (this.domRefs.spyButton) this.domRefs.spyButton.onclick = () => this.spy();
this.domRefs.showButton.onclick = () => this.toggleVisibility();
if (this.domRefs.minimizeContentButton) {
this.domRefs.minimizeContentButton.onclick = () => this.toggleVisibility();
}
this.domRefs.startHiddenCheckbox.onchange = () => this.handleStartHiddenChange();
this.domRefs.historySelect.onchange = () => this.handleHistorySelectionChange();
const tooltipElements = this.domRefs.panel.querySelectorAll('[data-tooltip]');
tooltipElements.forEach(el => {
el.addEventListener('mouseenter', (e) => this.showTooltip(e));
el.addEventListener('mouseleave', () => this.hideTooltip());
});
if (!this._boundHandleClickOutsideMenu) {
this._boundHandleClickOutsideMenu = this.handleClickOutsideMenu.bind(this);
}
document.addEventListener('click', this._boundHandleClickOutsideMenu);
window.addEventListener("LootTrackerCombatEnded", (e) => this.handleSessionEnd(e.detail));
window.addEventListener("LootTrackerBattle", (e) => this.processBattleUpdate(e.detail));
window.addEventListener('storage', (e) => this.handleStorageChange(e));
const waitForData = () => {
const hasCharacterData = CharacterDataStorage.getCurrentCharacterName();
const hasClientData = InitClientDataCache.get();
if (hasCharacterData && hasClientData) {
console.log('[MCS] Panels generated');
this.createEquipmentSpy();
} else {
setTimeout(waitForData, 500);
}
};
setTimeout(waitForData, 1000);
if (this.domRefs.minimizeContentButton) {
this.domRefs.minimizeContentButton.textContent = this.isHidden ? '+' : '−';
}
}
destroyEWatch() {
if (this.purchaseTimerIntervals) {
Object.keys(this.purchaseTimerIntervals).forEach(slot => {
clearInterval(this.purchaseTimerIntervals[slot]);
});
this.purchaseTimerIntervals = {};
}
VisibilityManager.clear('ewatch-market-refresh');
VisibilityManager.clear('ewatch-profit-cost');
VisibilityManager.clear('ewatch-coin-header');
if (this._ewatchWsListener) { window.removeEventListener('EquipSpyWebSocketMessage', this._ewatchWsListener); this._ewatchWsListener = null; }
if (this._ewatchForceLoadListener) { window.removeEventListener('EquipSpyForceLoadSuccess', this._ewatchForceLoadListener); this._ewatchForceLoadListener = null; }
if (this._ewatchBridgeForceLoadListener) { window.removeEventListener('EquipSpyForceLoadSuccess', this._ewatchBridgeForceLoadListener); this._ewatchBridgeForceLoadListener = null; }
if (this._ewatchCharDataListener) { window.removeEventListener('LootTrackerCharacterData', this._ewatchCharDataListener); this._ewatchCharDataListener = null; }
if (this._ewatchCoinListener) { window.removeEventListener('EquipSpyCoinUpdate', this._ewatchCoinListener); this._ewatchCoinListener = null; }
if (this._ewatchBattleListener) { window.removeEventListener('LootTrackerBattle', this._ewatchBattleListener); this._ewatchBattleListener = null; }
if (this._ewatchCombatEndedListener) { window.removeEventListener('LootTrackerCombatEnded', this._ewatchCombatEndedListener); this._ewatchCombatEndedListener = null; }
if (this._ewatchEquipChangedListener) { window.removeEventListener('MCS_EquipmentChanged', this._ewatchEquipChangedListener); this._ewatchEquipChangedListener = null; }
if (this._equipChangedListener) { window.removeEventListener('MCS_EquipmentChanged', this._equipChangedListener); this._equipChangedListener = null; }
if (this._spyDragoverListener) {
document.removeEventListener('dragover', this._spyDragoverListener);
this._spyDragoverListener = null;
document._spyDragoverAdded = false;
}
if (this._spyOnDragMove) {
document.removeEventListener('mousemove', this._spyOnDragMove);
document.removeEventListener('mouseup', this._spyOnDragUp);
this._spyOnDragMove = null;
this._spyOnDragUp = null;
}
if (this._spyOnDragStart) {
const header = document.querySelector('#equipment-spy-pane .mcs-ew-spy-header');
if (header) header.removeEventListener('mousedown', this._spyOnDragStart);
this._spyOnDragStart = null;
}
if (this._boundMovePanel) {
document.removeEventListener('mousemove', this._boundMovePanel);
this._boundMovePanel = null;
}
if (this._boundEndMove) {
document.removeEventListener('mouseup', this._boundEndMove);
this._boundEndMove = null;
}
if (this._boundHandleClickOutsideMenu) {
document.removeEventListener('click', this._boundHandleClickOutsideMenu);
this._boundHandleClickOutsideMenu = null;
}
const pane = document.getElementById('equipment-spy-pane');
if (pane) pane.remove();
}
// EWatch end
// FCB start
get fcStorage() {
if (!this._fcStorage) {
this._fcStorage = createModuleStorage('FC');
}
return this._fcStorage;
}
initFCB() {
this.fcbEnabled = this.fcStorage.get('enabled', false) === true;
const self = this;
const recheckSettings = () => {
const charName = CharacterDataStorage.getCurrentCharacterName();
if (charName && charName !== 'default') {
self.fcStorage.clearCache();
self.fcbEnabled = self.fcStorage.get('enabled', false) === true;
self.fcbEnemyDPSEnabled = self.fcStorage.get('enemyd', false) === true;
self.fcbPlayerDPSEnabled = self.fcStorage.get('playerd', false) === true;
self.fcbPlayerNameRecolorEnabled = self.fcStorage.get('playerc', false) === true;
} else {
setTimeout(recheckSettings, 1000);
}
};
setTimeout(recheckSettings, 1000);
this.fcbEnemyDPSEnabled = this.fcStorage.get('enemyd', false) === true;
this.fcbPlayerDPSEnabled = this.fcStorage.get('playerd', false) === true;
this.fcbPlayerNameRecolorEnabled = this.fcStorage.get('playerc', false) === true;
this.fcbSettingsMap = {
tracker0 : {
id: "tracker0",
desc: "Enable player #1 damage text",
isTrue: true,
descH: "Enable player #1 healing text",
isTrueH: true,
r: 255,
g: 99,
b: 132,
},
tracker1 : {
id: "tracker1",
desc: "Enable player #2 damage text",
isTrue: true,
descH: "Enable player #2 healing text",
isTrueH: true,
r: 54,
g: 162,
b: 235,
},
tracker2 : {
id: "tracker2",
desc: "Enable player #3 damage text",
isTrue: true,
descH: "Enable player #3 healing text",
isTrueH: true,
r: 255,
g: 206,
b: 86,
},
tracker3 : {
id: "tracker3",
desc: "Enable player #4 damage text",
isTrue: true,
descH: "Enable player #4 healing text",
isTrueH: true,
r: 75,
g: 192,
b: 192,
},
tracker4 : {
id: "tracker4",
desc: "Enable player #5 damage text",
isTrue: true,
descH: "Enable player #5 healing text",
isTrueH: true,
r: 153,
g: 102,
b: 255,
},
tracker6 : {
id: "tracker6",
desc: "Enable enemies damage text",
isTrue: true,
descH: "Enable enemies healing text",
isTrueH: true,
r: 255,
g: 0,
b: 0,
},
missedText : {
id: "missedText",
desc: "Enable missed attack text",
isTrue: true,
}
};
this.fcbReadSettings();
this.fcbMonstersHP = [];
this.fcbMonstersMP = [];
this.fcbMonstersDmgCounter = [];
this.fcbPlayersHP = [];
this.fcbPlayersMP = [];
this.fcbPlayersDmgCounter = [];
this.fcbBattleStartTime = null;
this.fcbPlayerDamageByMonster = {};
this.fcbCumulativePlayerDamage = {};
this.fcbCurrentBattlePlayerDamage = {};
this.fcbCumulativeTimeElapsed = 0;
this.fcbCurrentBattleStartTime = null;
this.fcbIsPaused = true;
this.fcbLastCombatUpdate = null;
this.fcbEndTime = null;
this.fcbPlayerFloatingQueues = {};
this.fcbEnemyFloatingQueues = {};
this.fcbActiveFloatingTexts = new Set();
this.fcbPlayerDPSInterval = null;
this.fcbEnemyDPSInterval = null;
this.fcbCachedPlayersArea = null;
this.fcbCachedMonstersArea = null;
this.fcbCacheInvalidationTime = 0;
this.fcbHookWebSocket();
this.fcbWatchPlayerDamageSplats();
this.fcbWatchMonsterElements();
this.fcbStartIntervals();
}
fcbWatchMonsterElements() {
if (this.fcbMonsterObserver) {
this.fcbMonsterObserver.disconnect();
}
if (this.fcbMonsterWatchTimeout) {
clearTimeout(this.fcbMonsterWatchTimeout);
this.fcbMonsterWatchTimeout = null;
}
const checkAndWatch = () => {
const monstersArea = document.querySelector('[class*="monstersArea"]');
if (!monstersArea) {
this.fcbMonsterWatchTimeout = setTimeout(checkAndWatch, 1000);
return;
}
this.fcbMonsterObserver = new MutationObserver((mutations) => {
if (document.hidden) return;
if (!this.fcbEnemyDPSEnabled) return;
mutations.forEach((mutation) => {
if (mutation.type === 'childList' && mutation.addedNodes.length > 0) {
mutation.addedNodes.forEach(node => {
if (node.nodeType === Node.ELEMENT_NODE) {
if (typeof node.className === 'string' && node.className.includes('CombatUnit_combatUnit__')) {
this.fcbPreCreateDPSContainer(node);
}
const monsters = node.querySelectorAll('[class*="CombatUnit_combatUnit__"]');
monsters.forEach(monster => {
this.fcbPreCreateDPSContainer(monster);
});
}
});
}
});
});
this.fcbMonsterObserver.observe(monstersArea, {
childList: true,
subtree: true
});
};
checkAndWatch();
}
fcbPreCreateDPSContainer(monsterElement) {
if (!monsterElement || monsterElement.querySelector('.floating-text-dps-container')) return;
const dpsContainer = document.createElement('div');
dpsContainer.className = 'floating-text-dps-container';
let totalPlayers = this.fcbCurrentPlayerCount || 1;
if (!totalPlayers || totalPlayers < 1) {
const playersArea = this.fcbGetCachedPlayersArea();
totalPlayers = playersArea && playersArea.children[0] ? playersArea.children[0].children.length : 1;
}
let columnsHtml = '';
for (let i = 0; i < totalPlayers; i++) {
let color = '#808080';
if (this.fcbSettingsMap[`tracker${i}`]) {
color = `rgb(${this.fcbSettingsMap[`tracker${i}`].r},${this.fcbSettingsMap[`tracker${i}`].g},${this.fcbSettingsMap[`tracker${i}`].b})`;
}
columnsHtml += `<div class="mcs-fcb-dps-column mcs-fcb-text-shadow" style="color: ${color};">0/s</div>`;
}
dpsContainer.innerHTML = columnsHtml;
monsterElement.appendChild(dpsContainer);
}
fcbStartIntervals() {
this.fcbStopIntervals();
if (this.fcbPlayerDPSEnabled) {
VisibilityManager.register('fcb-player-dps', () => {
if (!this.fcbIsPaused) {
this.fcbUpdateAllPlayerDPS();
}
}, 2000);
}
if (this.fcbEnemyDPSEnabled) {
VisibilityManager.register('fcb-enemy-dps', () => {
if (!this.fcbIsPaused && this.fcbBattleStartTime) {
Object.keys(this.fcbPlayerDamageByMonster).forEach(monsterIndex => {
const monsterHP = this.fcbMonstersHP[parseInt(monsterIndex)];
if (monsterHP > 0) {
this.fcbUpdateDPSDisplay(parseInt(monsterIndex));
}
});
}
}, 250);
}
VisibilityManager.register('fcb-floating-cleanup', () => {
this.fcbCleanupOrphanedFloatingTexts();
}, 30000);
}
fcbStopIntervals() {
VisibilityManager.clear('fcb-player-dps');
VisibilityManager.clear('fcb-enemy-dps');
VisibilityManager.clear('fcb-floating-cleanup');
}
fcbCleanup() {
this.fcbStopIntervals();
if (this.fcbPlayerSplatRecheckTimeout) {
clearTimeout(this.fcbPlayerSplatRecheckTimeout);
this.fcbPlayerSplatRecheckTimeout = null;
}
if (this.fcbMonsterWatchTimeout) {
clearTimeout(this.fcbMonsterWatchTimeout);
this.fcbMonsterWatchTimeout = null;
}
if (this.fcbPlayerSplatObserver) {
this.fcbPlayerSplatObserver.disconnect();
this.fcbPlayerSplatObserver = null;
}
if (this.fcbMonsterObserver) {
this.fcbMonsterObserver.disconnect();
this.fcbMonsterObserver = null;
}
this.fcbActiveFloatingTexts.forEach(div => {
if (div.parentNode) {
div.parentNode.removeChild(div);
}
});
this.fcbActiveFloatingTexts.clear();
this.fcbPlayerFloatingQueues = {};
this.fcbEnemyFloatingQueues = {};
this.fcbInvalidateCache();
}
fcbCleanupOrphanedFloatingTexts() {
const toRemove = [];
this.fcbActiveFloatingTexts.forEach(div => {
if (!div || !div.parentNode || !document.body.contains(div)) {
toRemove.push(div);
}
});
toRemove.forEach(div => {
this.fcbActiveFloatingTexts.delete(div);
if (div && div.parentNode) {
div.parentNode.removeChild(div);
}
});
if (this.fcbActiveFloatingTexts.size > 100) {
console.warn('FCB: Floating text Set exceeded 100 entries, clearing old entries');
const arr = Array.from(this.fcbActiveFloatingTexts);
const toKeep = arr.slice(-50);
arr.slice(0, -50).forEach(div => {
if (div && div.parentNode) {
div.parentNode.removeChild(div);
}
});
this.fcbActiveFloatingTexts = new Set(toKeep);
}
}
fcbWatchPlayerDamageSplats() {
const self = this;
if (this.fcbPlayerSplatObserver) {
this.fcbPlayerSplatObserver.disconnect();
}
if (this.fcbPlayerSplatRecheckTimeout) {
clearTimeout(this.fcbPlayerSplatRecheckTimeout);
this.fcbPlayerSplatRecheckTimeout = null;
}
const checkAndWatch = () => {
const playersArea = document.querySelector('[class*="playersArea"]');
if (!playersArea || !playersArea.children[0]) {
this.fcbPlayerSplatRecheckTimeout = setTimeout(checkAndWatch, 1000);
return;
}
const playersContainer = playersArea.children[0];
self.fcbPlayerSplatObserver = new MutationObserver((mutations) => {
if (document.hidden) return;
mutations.forEach((mutation) => {
try {
const nodesToProcess = [];
if (mutation.type === 'attributes' && mutation.attributeName === 'class') {
nodesToProcess.push(mutation.target);
}
else if (mutation.type === 'childList' && mutation.addedNodes.length > 0) {
mutation.addedNodes.forEach(addedNode => {
if (addedNode.nodeType === Node.ELEMENT_NODE) {
const isDamage = typeof addedNode.className === 'string' && addedNode.className.includes('CombatUnit_damage__');
const isMiss = typeof addedNode.className === 'string' && addedNode.className.includes('CombatUnit_miss__');
const isMana = typeof addedNode.className === 'string' && addedNode.className.includes('CombatUnit_mana__');
const isHeal = typeof addedNode.className === 'string' && addedNode.className.includes('CombatUnit_heal__');
if (isDamage || isMiss || isMana || isHeal) {
nodesToProcess.push(addedNode);
}
}
});
}
nodesToProcess.forEach(node => {
if (!node) return;
const isDamage = typeof node.className === 'string' && node.className.includes('CombatUnit_damage__');
const isMiss = typeof node.className === 'string' && node.className.includes('CombatUnit_miss__');
const isMana = typeof node.className === 'string' && node.className.includes('CombatUnit_mana__');
const isHeal = typeof node.className === 'string' && node.className.includes('CombatUnit_heal__');
if (!isDamage && !isMiss && !isMana && !isHeal) return;
const value = node.textContent.trim();
if (!value) return;
if (node.dataset.fcbLastValue === value) return;
node.dataset.fcbLastValue = value;
let playerElement = node;
while (playerElement && !(typeof playerElement.className === 'string' && playerElement.className.includes('CombatUnit_combatUnit__'))) {
playerElement = playerElement.parentElement;
}
if (!playerElement) return;
const playerIndex = Array.from(playersContainer.children).indexOf(playerElement);
if (playerIndex === -1) return;
let color, columnOffset;
if (isMana) {
color = '#2196F3';
columnOffset = -1;
} else if (isDamage) {
color = self.fcbSettingsMap.tracker6 ?
`rgb(${self.fcbSettingsMap.tracker6.r},${self.fcbSettingsMap.tracker6.g},${self.fcbSettingsMap.tracker6.b})` :
'#ff0000';
columnOffset = 0;
} else if (isMiss) {
color = '#808080';
columnOffset = 0;
} else if (isHeal) {
color = '#4CAF50';
columnOffset = 1;
}
self.fcbCreateFloatingTextForPlayerSplat(playerIndex, value, color, columnOffset);
});
} catch (error) {
console.error('FCB: Error processing player splat mutation:', error);
}
});
});
Array.from(playersContainer.children).forEach((player) => {
const splatsContainer = player.querySelector('[class*="CombatUnit_splatsContainer__"]');
if (splatsContainer) {
self.fcbPlayerSplatObserver.observe(splatsContainer, {
attributes: true,
attributeFilter: ['class'],
childList: true,
subtree: true
});
}
});
this.fcbPlayerSplatRecheckTimeout = setTimeout(checkAndWatch, 30000);
};
checkAndWatch();
}
fcbCreateFloatingTextForPlayerSplat(playerIndex, value, color, columnOffset) {
if (!this.fcbEnabled) return;
if (document.hidden) return;
if (!this.fcbPlayerFloatingQueues[playerIndex]) {
this.fcbPlayerFloatingQueues[playerIndex] = {
'-1': { queue: [], processing: false },
'0': { queue: [], processing: false },
'1': { queue: [], processing: false }
};
}
const queueKey = columnOffset.toString();
const queue = this.fcbPlayerFloatingQueues[playerIndex][queueKey];
queue.queue.push({ value, color, columnOffset });
if (queue.queue.length > 5) {
queue.queue.shift();
}
if (!queue.processing) {
this.fcbProcessPlayerFloatingQueue(playerIndex, columnOffset);
}
}
fcbProcessPlayerFloatingQueue(playerIndex, columnOffset) {
const queueKey = columnOffset.toString();
const queue = this.fcbPlayerFloatingQueues[playerIndex]?.[queueKey];
if (!queue) {
console.warn('FCB: Queue not found for player', playerIndex, 'column', columnOffset);
return;
}
if (queue.queue.length === 0) {
queue.processing = false;
return;
}
queue.processing = true;
const item = queue.queue.shift();
try {
this.fcbDisplayPlayerFloatingText(playerIndex, item.value, item.color, item.columnOffset);
} catch (error) {
console.error('FCB: Error displaying player floating text:', error);
}
if (queue.queue.length > 0) {
setTimeout(() => {
this.fcbProcessPlayerFloatingQueue(playerIndex, columnOffset);
}, 500);
} else {
queue.processing = false;
}
}
fcbDisplayPlayerFloatingText(playerIndex, value, color, columnOffset) {
const playerElement = this.fcbGetPlayerElement(playerIndex);
if (!playerElement) return;
const rect = playerElement.getBoundingClientRect();
const centerX = rect.left + rect.width / 2;
const columnWidth = rect.width / 3;
const columnSpacing = 12;
const startX = centerX + (columnOffset * (columnWidth * 0.5 + columnSpacing));
const startY = rect.top + rect.height / 2 - 50;
const endY = startY - 100;
const floatingDiv = document.createElement('div');
floatingDiv.className = 'fcb-floating-text';
floatingDiv.style.left = `${startX}px`;
floatingDiv.style.top = `${startY}px`;
floatingDiv.style.color = color;
floatingDiv.textContent = value;
floatingDiv.dataset.columnOffset = columnOffset;
floatingDiv.dataset.playerIndex = playerIndex;
floatingDiv.dataset.targetY = endY;
this.fcbActiveFloatingTexts.add(floatingDiv);
document.body.appendChild(floatingDiv);
this.fcbPushUpCollidingTexts(floatingDiv, endY, columnOffset, playerIndex);
setTimeout(() => {
floatingDiv.style.transition = 'top 0.5s ease-out';
floatingDiv.style.top = `${endY}px`;
}, 10);
setTimeout(() => {
floatingDiv.style.transition = 'opacity 0.5s ease-out';
floatingDiv.style.opacity = '0';
}, 1000);
setTimeout(() => {
this.fcbActiveFloatingTexts.delete(floatingDiv);
if (floatingDiv.parentNode) {
floatingDiv.parentNode.removeChild(floatingDiv);
}
}, 2000);
}
fcbPushUpCollidingTexts(newDiv, targetY, columnOffset, playerIndex) {
const pushDistance = 30;
const columnDivs = [];
this.fcbActiveFloatingTexts.forEach(existingDiv => {
if (existingDiv === newDiv) return;
if (existingDiv.dataset.columnOffset !== columnOffset.toString()) return;
if (existingDiv.dataset.playerIndex !== playerIndex.toString()) return;
const existingRect = existingDiv.getBoundingClientRect();
columnDivs.push({
div: existingDiv,
y: existingRect.top,
targetY: parseFloat(existingDiv.dataset.targetY)
});
});
columnDivs.sort((a, b) => b.y - a.y);
for (let i = 0; i < columnDivs.length; i++) {
const current = columnDivs[i];
const checkAgainstY = (i === 0) ? targetY : columnDivs[i - 1].targetY;
if (Math.abs(current.y - checkAgainstY) < pushDistance || current.targetY >= checkAgainstY) {
const newTargetY = checkAgainstY - pushDistance;
current.div.dataset.targetY = newTargetY;
current.targetY = newTargetY;
current.div.style.transition = 'top 0.3s ease-out';
current.div.style.top = `${newTargetY}px`;
}
}
}
fcbToggleEnabled(enabled) {
this.fcbEnabled = enabled;
this.fcStorage.set('enabled', enabled);
if (!enabled) {
document.querySelectorAll('.fcb-floating-text').forEach(el => {
if (el.parentNode) el.parentNode.removeChild(el);
});
this.fcbActiveFloatingTexts.clear();
}
}
fcbToggleEnemyDPS(enabled) {
this.fcbEnemyDPSEnabled = enabled;
this.fcStorage.set('enemyd', enabled);
this.fcbStartIntervals();
if (!enabled) {
document.querySelectorAll('.floating-text-dps-container').forEach(el => {
if (el.parentNode) el.parentNode.removeChild(el);
});
} else {
const monstersArea = this.fcbGetCachedMonstersArea();
if (monstersArea && monstersArea.children[0]) {
const monstersContainer = monstersArea.children[0];
const monsters = monstersContainer.children;
for (let i = 0; i < monsters.length; i++) {
this.fcbUpdateDPSDisplay(i);
}
}
}
}
fcbTogglePlayerDPS(enabled) {
this.fcbPlayerDPSEnabled = enabled;
this.fcStorage.set('playerd', enabled);
this.fcbStartIntervals();
if (!enabled) {
document.querySelectorAll('.floating-text-player-dps-current').forEach(el => {
if (el.parentNode) el.parentNode.removeChild(el);
});
document.querySelectorAll('.floating-text-player-dps-total').forEach(el => {
if (el.parentNode) el.parentNode.removeChild(el);
});
} else {
this.fcbUpdateAllPlayerDPS();
}
}
fcbTogglePlayerNameRecolor(enabled) {
this.fcbPlayerNameRecolorEnabled = enabled;
this.fcStorage.set('playerc', enabled);
if (!enabled) {
const playersArea = this.fcbGetCachedPlayersArea();
if (!playersArea || !playersArea.children[0]) return;
const playersContainer = playersArea.children[0];
const players = playersContainer.children;
for (let i = 0; i < players.length; i++) {
const playerElement = players[i];
const nameElement = playerElement.querySelector('[class^="CombatUnit_name"]');
if (nameElement) {
nameElement.style.backgroundColor = '';
nameElement.style.color = '';
nameElement.style.padding = '';
nameElement.style.borderRadius = '';
}
}
} else {
this.fcbUpdatePlayerNameColors();
}
}
fcbReadSettings() {
const savedSettings = localStorage.getItem("mcs__global_FC_settings_map");
if (savedSettings) {
const parsedSettings = JSON.parse(savedSettings);
for (const key in parsedSettings) {
if (this.fcbSettingsMap[key]) {
Object.assign(this.fcbSettingsMap[key], parsedSettings[key]);
}
}
}
}
fcbResetDPSTracking(isNewSession = false) {
if (isNewSession) {
this.fcbBattleStartTime = Date.now();
this.fcbCurrentBattleStartTime = Date.now();
this.fcbCumulativeTimeElapsed = 0;
this.fcbCumulativePlayerDamage = {};
this.fcbEndTime = null;
} else {
if (this.fcbBattleStartTime && this.fcbEndTime) {
this.fcbCumulativeTimeElapsed += (this.fcbEndTime - this.fcbBattleStartTime) / 1000;
} else if (this.fcbBattleStartTime && !this.fcbEndTime) {
this.fcbCumulativeTimeElapsed += (Date.now() - this.fcbBattleStartTime) / 1000;
}
this.fcbBattleStartTime = Date.now();
this.fcbCurrentBattleStartTime = Date.now();
this.fcbEndTime = null;
}
this.fcbIsPaused = false;
this.fcbPlayerDamageByMonster = {};
this.fcbCurrentBattlePlayerDamage = {};
this.fcbLastCombatUpdate = Date.now();
this.fcbInvalidateCache();
const monstersArea = this.fcbGetCachedMonstersArea();
if (monstersArea && monstersArea.children[0]) {
const monstersContainer = monstersArea.children[0];
const monsters = monstersContainer.children;
for (let i = 0; i < monsters.length; i++) {
const dpsContainer = monsters[i].querySelector('.floating-text-dps-container');
if (dpsContainer) {
const totalPlayers = this.fcbCurrentPlayerCount || 1;
let columnsHtml = '';
for (let j = 0; j < totalPlayers; j++) {
let color = '#808080';
if (this.fcbSettingsMap[`tracker${j}`]) {
color = `rgb(${this.fcbSettingsMap[`tracker${j}`].r},${this.fcbSettingsMap[`tracker${j}`].g},${this.fcbSettingsMap[`tracker${j}`].b})`;
}
columnsHtml += `<div class="mcs-fcb-dps-column mcs-fcb-text-shadow" style="color: ${color};">0/s</div>`;
}
dpsContainer.innerHTML = columnsHtml;
}
}
}
}
fcbPauseDPSTracking() {
if (!this.fcbIsPaused) {
this.fcbEndTime = Date.now();
this.fcbIsPaused = true;
}
this.fcbPlayerFloatingQueues = {};
this.fcbEnemyFloatingQueues = {};
}
fcbRecordDamage(playerIndex, monsterIndex, damage) {
if (!this.fcbPlayerDamageByMonster[monsterIndex]) {
this.fcbPlayerDamageByMonster[monsterIndex] = {};
}
if (!this.fcbPlayerDamageByMonster[monsterIndex][playerIndex]) {
this.fcbPlayerDamageByMonster[monsterIndex][playerIndex] = 0;
}
this.fcbPlayerDamageByMonster[monsterIndex][playerIndex] += damage;
if (!this.fcbCumulativePlayerDamage[playerIndex]) {
this.fcbCumulativePlayerDamage[playerIndex] = 0;
}
this.fcbCumulativePlayerDamage[playerIndex] += damage;
if (!this.fcbCurrentBattlePlayerDamage[playerIndex]) {
this.fcbCurrentBattlePlayerDamage[playerIndex] = 0;
}
this.fcbCurrentBattlePlayerDamage[playerIndex] += damage;
this.fcbUpdateDPSDisplay(monsterIndex);
this.fcbUpdatePlayerDPSDisplay(playerIndex);
}
fcbCalculateDPS(totalDamage) {
if (!this.fcbBattleStartTime) return 0;
const elapsedSeconds = (Date.now() - this.fcbBattleStartTime) / 1000;
if (elapsedSeconds < 0.1) return 0;
return Math.round(totalDamage / elapsedSeconds);
}
fcbCalculateCumulativeDPS(playerIndex) {
const totalDamage = this.fcbCumulativePlayerDamage[playerIndex] || 0;
let totalTime = this.fcbCumulativeTimeElapsed;
if (this.fcbBattleStartTime) {
const now = this.fcbEndTime || Date.now();
totalTime += (now - this.fcbBattleStartTime) / 1000;
}
if (totalTime < 0.1) return 0;
return Math.round(totalDamage / totalTime);
}
fcbCalculateCurrentBattleDPS(playerIndex) {
const currentDamage = this.fcbCurrentBattlePlayerDamage[playerIndex] || 0;
if (!this.fcbBattleStartTime || this.fcbIsPaused) return 0;
const elapsedSeconds = (Date.now() - this.fcbBattleStartTime) / 1000;
if (elapsedSeconds < 0.1) return 0;
return Math.round(currentDamage / elapsedSeconds);
}
fcbFormatNumber(num) {
return mcsFormatCurrency(num, 'compact');
}
fcbUpdatePlayerDPSDisplay(playerIndex) {
const playerElement = this.fcbGetPlayerElement(playerIndex);
if (!playerElement) return;
let currentContainer = playerElement.querySelector('.floating-text-player-dps-current');
let totalContainer = playerElement.querySelector('.floating-text-player-dps-total');
if (!this.fcbPlayerDPSEnabled) {
if (currentContainer && currentContainer.parentNode) {
currentContainer.parentNode.removeChild(currentContainer);
}
if (totalContainer && totalContainer.parentNode) {
totalContainer.parentNode.removeChild(totalContainer);
}
return;
}
if (!currentContainer) {
currentContainer = document.createElement('div');
currentContainer.className = 'floating-text-player-dps-current';
playerElement.insertBefore(currentContainer, playerElement.firstChild);
}
if (!totalContainer) {
totalContainer = document.createElement('div');
totalContainer.className = 'floating-text-player-dps-total';
if (currentContainer.nextSibling) {
playerElement.insertBefore(totalContainer, currentContainer.nextSibling);
} else {
playerElement.insertBefore(totalContainer, playerElement.firstChild);
playerElement.insertBefore(currentContainer, totalContainer);
}
}
const currentDamage = this.fcbCurrentBattlePlayerDamage[playerIndex] || 0;
const currentDPS = this.fcbCalculateCurrentBattleDPS(playerIndex);
const totalDamage = this.fcbCumulativePlayerDamage[playerIndex] || 0;
const totalDPS = this.fcbCalculateCumulativeDPS(playerIndex);
if (this.fcbSettingsMap[`tracker${playerIndex}`]) {
const color = `rgb(${this.fcbSettingsMap[`tracker${playerIndex}`].r},${this.fcbSettingsMap[`tracker${playerIndex}`].g},${this.fcbSettingsMap[`tracker${playerIndex}`].b})`;
currentContainer.style.color = color;
currentContainer.classList.add('mcs-fcb-text-shadow');
totalContainer.style.color = color;
totalContainer.classList.add('mcs-fcb-text-shadow');
}
currentContainer.textContent = `${currentDPS} DPS ${this.fcbFormatNumber(currentDamage)} cur`;
totalContainer.textContent = `${totalDPS} DPS ${this.fcbFormatNumber(totalDamage)} total`;
}
fcbUpdateAllPlayerDPS() {
const playersArea = this.fcbGetCachedPlayersArea();
if (!playersArea || !playersArea.children[0]) return;
const totalPlayers = playersArea.children[0].children.length;
for (let i = 0; i < totalPlayers; i++) {
this.fcbUpdatePlayerDPSDisplay(i);
}
}
fcbUpdateDPSDisplay(monsterIndex) {
const monsterElement = this.fcbGetMonsterElement(monsterIndex);
if (!monsterElement) return;
let dpsContainer = monsterElement.querySelector('.floating-text-dps-container');
if (!this.fcbEnemyDPSEnabled) {
if (dpsContainer) {
dpsContainer.style.visibility = 'hidden';
}
return;
}
if (!dpsContainer) {
dpsContainer = document.createElement('div');
dpsContainer.className = 'floating-text-dps-container';
monsterElement.appendChild(dpsContainer);
} else {
dpsContainer.style.visibility = 'visible';
}
dpsContainer.innerHTML = '';
const playersArea = this.fcbGetCachedPlayersArea();
const totalPlayers = playersArea && playersArea.children[0] ? playersArea.children[0].children.length : 0;
for (let i = 0; i < totalPlayers; i++) {
const totalDamage = this.fcbPlayerDamageByMonster[monsterIndex]?.[i] || 0;
const dps = this.fcbCalculateDPS(totalDamage);
const column = document.createElement('div');
column.className = 'mcs-fcb-dps-column';
if (this.fcbSettingsMap[`tracker${i}`]) {
const color = `rgb(${this.fcbSettingsMap[`tracker${i}`].r},${this.fcbSettingsMap[`tracker${i}`].g},${this.fcbSettingsMap[`tracker${i}`].b})`;
column.style.color = color;
column.classList.add('mcs-fcb-text-shadow');
}
column.textContent = `${dps}/s`;
dpsContainer.appendChild(column);
}
}
fcbCreateFloatingText(targetElement, text, color, isCrit, isMiss, attackerName, targetName, playerIndex = null) {
if (!this.fcbEnabled) return;
const monsterIndex = this.fcbGetMonsterIndexFromElement(targetElement);
if (monsterIndex === -1) return;
this.fcbEnqueueEnemyFloatingText(monsterIndex, playerIndex, text, color, isMiss);
}
fcbGetMonsterIndexFromElement(targetElement) {
const monstersArea = this.fcbGetCachedMonstersArea();
if (!monstersArea || !monstersArea.children[0]) return -1;
const monstersContainer = monstersArea.children[0];
const monsters = Array.from(monstersContainer.children);
return monsters.indexOf(targetElement);
}
fcbEnqueueEnemyFloatingText(monsterIndex, playerIndex, text, color, isMiss) {
if (document.hidden) return;
if (!this.fcbEnemyFloatingQueues[monsterIndex]) {
this.fcbEnemyFloatingQueues[monsterIndex] = {};
}
if (!this.fcbEnemyFloatingQueues[monsterIndex][playerIndex]) {
this.fcbEnemyFloatingQueues[monsterIndex][playerIndex] = {
queue: [],
processing: false
};
}
const queue = this.fcbEnemyFloatingQueues[monsterIndex][playerIndex];
queue.queue.push({ text, color, isMiss });
if (queue.queue.length > 5) {
queue.queue.shift();
}
if (!queue.processing) {
this.fcbProcessEnemyFloatingQueue(monsterIndex, playerIndex);
}
}
fcbProcessEnemyFloatingQueue(monsterIndex, playerIndex) {
const queue = this.fcbEnemyFloatingQueues[monsterIndex]?.[playerIndex];
if (!queue) return;
if (queue.queue.length === 0) {
queue.processing = false;
return;
}
queue.processing = true;
const item = queue.queue.shift();
try {
this.fcbDisplayEnemyFloatingText(monsterIndex, playerIndex, item.text, item.color, item.isMiss);
} catch (error) {
console.error('FCB: Error displaying enemy floating text:', error);
}
if (queue.queue.length > 0) {
setTimeout(() => {
this.fcbProcessEnemyFloatingQueue(monsterIndex, playerIndex);
}, 500);
} else {
queue.processing = false;
}
}
fcbDisplayEnemyFloatingText(monsterIndex, playerIndex, text, color, isMiss) {
const targetElement = this.fcbGetMonsterElement(monsterIndex);
if (!targetElement) return;
const rect = targetElement.getBoundingClientRect();
const startY = rect.top + rect.height / 2 - 50;
const endY = startY - 100;
const playersArea = this.fcbGetCachedPlayersArea();
const totalPlayers = playersArea && playersArea.children[0] ? playersArea.children[0].children.length : 1;
const monsterLeft = rect.left;
const monsterWidth = rect.width;
const columnWidth = monsterWidth / totalPlayers;
const columnCenterX = monsterLeft + (playerIndex * columnWidth) + (columnWidth / 2);
const startX = columnCenterX;
const floatingDiv = document.createElement('div');
floatingDiv.className = 'fcb-floating-text';
floatingDiv.style.left = `${startX}px`;
floatingDiv.style.top = `${startY}px`;
floatingDiv.style.color = color;
let combatText = '';
if (isMiss) {
combatText = '0';
} else {
combatText = text;
}
floatingDiv.textContent = combatText;
floatingDiv.dataset.monsterIndex = monsterIndex;
floatingDiv.dataset.playerIndex = playerIndex;
floatingDiv.dataset.targetY = endY;
this.fcbActiveFloatingTexts.add(floatingDiv);
document.body.appendChild(floatingDiv);
this.fcbPushUpCollidingEnemyTexts(floatingDiv, endY, monsterIndex, playerIndex);
setTimeout(() => {
floatingDiv.style.transition = 'top 0.5s ease-out';
floatingDiv.style.top = `${endY}px`;
}, 10);
setTimeout(() => {
floatingDiv.style.transition = 'opacity 0.5s ease-out';
floatingDiv.style.opacity = '0';
}, 1000);
setTimeout(() => {
this.fcbActiveFloatingTexts.delete(floatingDiv);
if (floatingDiv.parentNode) {
floatingDiv.parentNode.removeChild(floatingDiv);
}
}, 2000);
}
fcbPushUpCollidingEnemyTexts(newDiv, targetY, monsterIndex, playerIndex) {
const pushDistance = 30;
const columnDivs = [];
this.fcbActiveFloatingTexts.forEach(existingDiv => {
if (existingDiv === newDiv) return;
if (existingDiv.dataset.monsterIndex !== monsterIndex.toString()) return;
if (existingDiv.dataset.playerIndex !== playerIndex.toString()) return;
const existingRect = existingDiv.getBoundingClientRect();
columnDivs.push({
div: existingDiv,
y: existingRect.top,
targetY: parseFloat(existingDiv.dataset.targetY)
});
});
columnDivs.sort((a, b) => b.y - a.y);
for (let i = 0; i < columnDivs.length; i++) {
const current = columnDivs[i];
const checkAgainstY = (i === 0) ? targetY : columnDivs[i - 1].targetY;
if (Math.abs(current.y - checkAgainstY) < pushDistance || current.targetY >= checkAgainstY) {
const newTargetY = checkAgainstY - pushDistance;
current.div.dataset.targetY = newTargetY;
current.targetY = newTargetY;
current.div.style.transition = 'top 0.3s ease-out';
current.div.style.top = `${newTargetY}px`;
}
}
}
fcbGetCachedPlayersArea() {
const now = Date.now();
if (!this.fcbCachedPlayersArea || now - this.fcbCacheInvalidationTime > 5000) {
this.fcbCachedPlayersArea = document.querySelector('[class*="playersArea"]');
this.fcbCachedMonstersArea = document.querySelector('[class*="monstersArea"]');
this.fcbCacheInvalidationTime = now;
}
return this.fcbCachedPlayersArea;
}
fcbGetCachedMonstersArea() {
this.fcbGetCachedPlayersArea();
return this.fcbCachedMonstersArea;
}
fcbInvalidateCache() {
this.fcbCachedPlayersArea = null;
this.fcbCachedMonstersArea = null;
this.fcbCacheInvalidationTime = 0;
}
fcbGetPlayerElement(playerIndex) {
const playersArea = this.fcbGetCachedPlayersArea();
if (!playersArea || !playersArea.children[0]) {
return null;
}
const playersContainer = playersArea.children[0];
const players = playersContainer.children;
return players[playerIndex] || null;
}
fcbGetMonsterElement(monsterIndex) {
const monstersArea = this.fcbGetCachedMonstersArea();
if (!monstersArea || !monstersArea.children[0]) {
return null;
}
const monstersContainer = monstersArea.children[0];
const monsters = monstersContainer.children;
return monsters[monsterIndex] || null;
}
fcbGetPlayerName(playerElement) {
if (!playerElement) return 'Player';
const nameElement = playerElement.querySelector('[class*="name"]');
return nameElement ? nameElement.textContent.trim() : 'Player';
}
fcbGetMonsterName(monsterElement) {
if (!monsterElement) return 'Monster';
const nameElement = monsterElement.querySelector('[class*="name"]');
return nameElement ? nameElement.textContent.trim() : 'Monster';
}
fcbUpdatePlayerNameColors() {
const playersArea = this.fcbGetCachedPlayersArea();
if (!playersArea || !playersArea.children[0]) return;
const playersContainer = playersArea.children[0];
const players = playersContainer.children;
for (let i = 0; i < players.length; i++) {
const playerElement = players[i];
const nameElement = playerElement.querySelector('[class^="CombatUnit_name"]');
if (nameElement && this.fcbSettingsMap[`tracker${i}`]) {
if (this.fcbPlayerNameRecolorEnabled) {
const color = `rgb(${this.fcbSettingsMap[`tracker${i}`].r},${this.fcbSettingsMap[`tracker${i}`].g},${this.fcbSettingsMap[`tracker${i}`].b})`;
nameElement.style.backgroundColor = color;
nameElement.style.color = '#000000';
nameElement.style.padding = '2px 6px';
nameElement.style.borderRadius = '3px';
} else {
nameElement.style.backgroundColor = '';
nameElement.style.color = '';
nameElement.style.padding = '';
nameElement.style.borderRadius = '';
}
}
}
}
fcbCreateFloatingCombatText(fromIndex, toIndex, hpDiff, reversed = false) {
if (!reversed && hpDiff > 0) {
this.fcbRecordDamage(fromIndex, toIndex, hpDiff);
}
if (!this.fcbEnabled) return;
if (reversed) {
return;
}
const attackerElement = this.fcbGetPlayerElement(fromIndex);
const targetElement = this.fcbGetMonsterElement(toIndex);
const attackerName = this.fcbGetPlayerName(attackerElement);
const targetName = this.fcbGetMonsterName(targetElement);
const settingId = `tracker${fromIndex}`;
let text, color, isCrit = false, isMiss = false;
if (hpDiff === 0) {
text = '';
color = '#808080';
isMiss = true;
if (!this.fcbSettingsMap.missedText?.isTrue) {
return;
}
} else if (hpDiff > 0) {
text = hpDiff.toString();
if (!this.fcbSettingsMap[settingId]?.isTrue) {
return;
}
color = `rgb(${this.fcbSettingsMap[settingId].r},${this.fcbSettingsMap[settingId].g},${this.fcbSettingsMap[settingId].b})`;
} else {
text = '+' + Math.abs(hpDiff);
if (!this.fcbSettingsMap[settingId]?.isTrueH) {
return;
}
color = '#4CAF50';
}
if (hpDiff > 0 && Math.random() < 0.2) {
isCrit = true;
}
const playersArea = this.fcbGetCachedPlayersArea();
const totalPlayers = playersArea && playersArea.children[0] ? playersArea.children[0].children.length : 1;
this.fcbCreateFloatingText(targetElement, text, color, isCrit, isMiss, attackerName, targetName, fromIndex, totalPlayers, true);
}
fcbHandleMessage(message) {
let obj = JSON.parse(message);
if (obj && obj.type === "new_battle") {
let isNewSession = false;
if (this.fcbLastCombatUpdate) {
const timeSinceLastUpdate = (Date.now() - this.fcbLastCombatUpdate) / 1000;
if (timeSinceLastUpdate > 5) {
isNewSession = true;
}
} else {
isNewSession = true;
}
this.fcbMonstersHP = obj.monsters.map((monster) => monster.currentHitpoints);
this.fcbMonstersMP = obj.monsters.map((monster) => monster.currentManapoints);
this.fcbMonstersDmgCounter = obj.monsters.map((monster) => monster.damageSplatCounter);
this.fcbPlayersHP = obj.players.map((player) => player.currentHitpoints);
this.fcbPlayersMP = obj.players.map((player) => player.currentManapoints);
this.fcbPlayersDmgCounter = obj.players.map((player) => player.damageSplatCounter);
this.fcbCurrentPlayerCount = obj.players.length;
this.fcbResetDPSTracking(isNewSession);
setTimeout(() => {
this.fcbUpdatePlayerNameColors();
const playersArea = this.fcbGetCachedPlayersArea();
if (playersArea && playersArea.children[0]) {
const totalPlayers = playersArea.children[0].children.length;
for (let i = 0; i < totalPlayers; i++) {
this.fcbUpdatePlayerDPSDisplay(i);
}
}
if (this.fcbEnemyDPSEnabled) {
const monstersArea = this.fcbGetCachedMonstersArea();
if (monstersArea && monstersArea.children[0]) {
const monstersContainer = monstersArea.children[0];
for (let i = 0; i < monstersContainer.children.length; i++) {
this.fcbUpdateDPSDisplay(i);
}
}
}
}, 100);
} else if (obj && obj.type === "battle_updated" && this.fcbMonstersHP.length) {
this.fcbLastCombatUpdate = Date.now();
this.fcbEndTime = Date.now();
const mMap = obj.mMap;
const pMap = obj.pMap;
const monsterIndices = Object.keys(obj.mMap);
const playerIndices = Object.keys(obj.pMap);
let castMonster = -1;
monsterIndices.forEach((monsterIndex) => {
if(mMap[monsterIndex].cMP < this.fcbMonstersMP[monsterIndex]){castMonster = monsterIndex;}
this.fcbMonstersMP[monsterIndex] = mMap[monsterIndex].cMP;
});
let castPlayer = -1;
playerIndices.forEach((userIndex) => {
if(pMap[userIndex].cMP < this.fcbPlayersMP[userIndex]){castPlayer = userIndex;}
this.fcbPlayersMP[userIndex] = pMap[userIndex].cMP;
});
let hurtMonster = false;
let hurtPlayer = false;
let monsterLifeSteal = {from:null, to:null, hpDiff:null};
let playerLifeSteal = {from:null, to:null, hpDiff:null};
this.fcbMonstersHP.forEach((mHP, mIndex) => {
const monster = mMap[mIndex];
if (monster) {
const hpDiff = mHP - monster.cHP;
if (hpDiff > 0) {hurtMonster = true;}
let dmgSplat = false;
if (this.fcbMonstersDmgCounter[mIndex] < monster.dmgCounter) {dmgSplat = true;}
this.fcbMonstersHP[mIndex] = monster.cHP;
this.fcbMonstersDmgCounter[mIndex] = monster.dmgCounter;
if (dmgSplat && hpDiff >= 0 && playerIndices.length > 0) {
if (playerIndices.length > 1) {
playerIndices.forEach((userIndex) => {
if(userIndex === castPlayer) {
this.fcbCreateFloatingCombatText(parseInt(userIndex), parseInt(mIndex), hpDiff);
}
});
} else {
this.fcbCreateFloatingCombatText(parseInt(playerIndices[0]), parseInt(mIndex), hpDiff);
}
}
if (hpDiff < 0 ) {
if (castMonster > -1){
this.fcbCreateFloatingCombatText(parseInt(mIndex), parseInt(castMonster), hpDiff, true);
}else{
monsterLifeSteal.from=parseInt(mIndex);
monsterLifeSteal.to=parseInt(mIndex);
monsterLifeSteal.hpDiff=hpDiff;
}
}
}
});
this.fcbPlayersHP.forEach((pHP, pIndex) => {
const player = pMap[pIndex];
if (player) {
const hpDiff = pHP - player.cHP;
if (hpDiff > 0) {hurtPlayer = true;}
let dmgSplat = false;
if (this.fcbPlayersDmgCounter[pIndex] < player.dmgCounter) {dmgSplat = true;}
this.fcbPlayersHP[pIndex] = player.cHP;
this.fcbPlayersDmgCounter[pIndex] = player.dmgCounter;
if (dmgSplat && hpDiff >= 0 && monsterIndices.length > 0) {
if (monsterIndices.length > 1) {
monsterIndices.forEach((monsterIndex) => {
if(monsterIndex === castMonster) {
this.fcbCreateFloatingCombatText(parseInt(pIndex), parseInt(monsterIndex), hpDiff, true);
}
});
} else {
this.fcbCreateFloatingCombatText(parseInt(pIndex), parseInt(monsterIndices[0]), hpDiff, true);
}
}
if (hpDiff < 0 ) {
if (castPlayer > -1){
this.fcbCreateFloatingCombatText(parseInt(castPlayer), parseInt(pIndex), hpDiff);
}else{
playerLifeSteal.from=parseInt(pIndex);
playerLifeSteal.to=parseInt(pIndex);
playerLifeSteal.hpDiff=hpDiff;
}
}
}
});
if (hurtMonster && playerLifeSteal.from !== null) {
this.fcbCreateFloatingCombatText(playerLifeSteal.from, playerLifeSteal.to, playerLifeSteal.hpDiff);
}
if (hurtPlayer && monsterLifeSteal.from !== null) {
this.fcbCreateFloatingCombatText(monsterLifeSteal.from, monsterLifeSteal.to, monsterLifeSteal.hpDiff, true);
}
const allMonstersDead = this.fcbMonstersHP.every(hp => hp <= 0);
if (allMonstersDead) {
this.fcbPauseDPSTracking();
}
}
return message;
}
fcbHookWebSocket() {
if (this.fcbWebSocketHooked) return;
this.fcbWebSocketHooked = true;
const dataProperty = Object.getOwnPropertyDescriptor(MessageEvent.prototype, "data");
const oriGet = dataProperty.get;
const self = this;
dataProperty.get = function hookedGet() {
const socket = this.currentTarget;
if (!(socket instanceof WebSocket)) {
return oriGet.call(this);
}
if (socket.url.indexOf("api.milkywayidle.com/ws") <= -1 && socket.url.indexOf("api-test.milkywayidle.com/ws") <= -1) {
return oriGet.call(this);
}
const message = oriGet.call(this);
Object.defineProperty(this, "data", { value: message });
return self.fcbHandleMessage(message);
};
Object.defineProperty(MessageEvent.prototype, "data", dataProperty);
}
// FCB end
// MAna start
get maStorage() {
if (!this._maStorage) {
this._maStorage = createModuleStorage('MA');
}
return this._maStorage;
}
loadAbilityDetailMap() {
return InitClientDataCache.getAbilityDetailMap();
}
createMAnaPane() {
if (document.getElementById('mana-pane')) return;
const pane = document.createElement('div');
pane.id = 'mana-pane';
registerPanel('mana-pane');
const savedSize = this.maStorage.get('size');
let width = 600;
let height = 550;
if (savedSize) {
try {
const size = typeof savedSize === 'string' ? JSON.parse(savedSize) : savedSize;
width = size.width || 600;
height = size.height || 550;
} catch (e) {
console.error('[MAna] Error restoring size:', e);
}
}
pane.className = 'mcs-pane mcs-ma-pane';
pane.style.width = width + 'px';
pane.style.height = height + 'px';
pane.dataset.savedHeight = height;
const header = document.createElement('div');
header.className = 'mcs-pane-header';
const titleSection = document.createElement('div');
titleSection.className = 'mcs-ma-title-section';
const title = document.createElement('span');
title.className = 'mcs-pane-title';
title.textContent = 'MAna';
const timeDisplay = document.createElement('div');
timeDisplay.id = 'mana-time-display';
timeDisplay.className = 'mcs-ma-time-display';
timeDisplay.textContent = '00:00:00';
const manaDisplay = document.createElement('div');
manaDisplay.id = 'mana-header-display';
manaDisplay.className = 'mcs-ma-mana-display';
manaDisplay.textContent = '0/0';
titleSection.appendChild(title);
titleSection.appendChild(timeDisplay);
titleSection.appendChild(manaDisplay);
const buttonSection = document.createElement('div');
buttonSection.className = 'mcs-button-section';
const resetBtn = document.createElement('button');
resetBtn.textContent = '↻';
resetBtn.title = 'Reset tracking';
resetBtn.className = 'mcs-btn';
resetBtn.onclick = () => this.resetMAnaTracking();
const minimizeBtn = document.createElement('button');
minimizeBtn.id = 'mana-minimize-btn';
minimizeBtn.textContent = '−';
minimizeBtn.className = 'mcs-btn';
buttonSection.appendChild(resetBtn);
buttonSection.appendChild(minimizeBtn);
header.appendChild(titleSection);
header.appendChild(buttonSection);
const content = document.createElement('div');
content.id = 'mana-content';
content.className = 'mcs-ma-content';
content.innerHTML = `
<div class="mcs-ma-waiting">
<div class="mcs-ma-waiting-icon">⏳</div>
<div>Waiting for new fight to begin</div>
<div class="mcs-ma-waiting-sub">MAna will begin tracking mana usage shortly</div>
</div>
`;
pane.appendChild(header);
pane.appendChild(content);
document.body.appendChild(pane);
let dragStartX, dragStartY, initialLeft, initialTop;
const onDragMove = (e) => {
const dx = e.clientX - dragStartX;
const dy = e.clientY - dragStartY;
let newLeft = initialLeft + dx;
let newTop = initialTop + dy;
const paneRect = pane.getBoundingClientRect();
const headerHeight = header.getBoundingClientRect().height;
const minLeft = -paneRect.width + 100;
const maxLeft = window.innerWidth - 100;
const minTop = 0;
const maxTop = window.innerHeight - headerHeight;
newLeft = Math.max(minLeft, Math.min(maxLeft, newLeft));
newTop = Math.max(minTop, Math.min(maxTop, newTop));
pane.style.left = newLeft + 'px';
pane.style.top = newTop + 'px';
};
const onDragUp = () => {
document.removeEventListener('mousemove', onDragMove);
document.removeEventListener('mouseup', onDragUp);
header.style.cursor = 'move';
const rect = pane.getBoundingClientRect();
this.maStorage.set('position', { top: rect.top, left: rect.left });
};
header.addEventListener('mousedown', (e) => {
if (e.target.closest('button')) return;
dragStartX = e.clientX;
dragStartY = e.clientY;
const rect = pane.getBoundingClientRect();
initialLeft = rect.left;
initialTop = rect.top;
header.style.cursor = 'grabbing';
pane.style.left = initialLeft + 'px';
pane.style.right = 'auto';
this._maDragMove = onDragMove;
this._maDragUp = onDragUp;
document.addEventListener('mousemove', onDragMove);
document.addEventListener('mouseup', onDragUp);
});
minimizeBtn.addEventListener('click', () => {
const isMinimized = content.style.display === 'none';
if (isMinimized) {
content.style.display = 'block';
minimizeBtn.textContent = '−';
header.style.borderRadius = '6px 6px 0 0';
const savedHeight = pane.dataset.savedHeight || height;
pane.style.height = savedHeight + 'px';
pane.style.minHeight = '300px';
pane.style.resize = 'both';
this.maStorage.set('minimized', false);
} else {
const currentRect = pane.getBoundingClientRect();
pane.dataset.savedHeight = currentRect.height;
content.style.display = 'none';
minimizeBtn.textContent = '+';
header.style.borderRadius = '6px';
const headerHeight = header.getBoundingClientRect().height;
pane.style.height = headerHeight + 'px';
pane.style.minHeight = '0';
pane.style.resize = 'none';
this.maStorage.set('minimized', true);
}
});
const savedPosition = this.maStorage.get('position');
if (savedPosition) {
try {
const position = typeof savedPosition === 'string' ? JSON.parse(savedPosition) : savedPosition;
pane.style.top = position.top + 'px';
if (position.left !== undefined) {
pane.style.left = position.left + 'px';
pane.style.right = 'auto';
} else if (position.right !== undefined) {
pane.style.right = position.right + 'px';
}
} catch (e) {
console.error('[MAna] Error restoring position:', e);
}
}
const savedMinimized = this.maStorage.get('minimized') === true || this.maStorage.get('minimized') === 'true';
if (savedMinimized) {
content.style.display = 'none';
minimizeBtn.textContent = '+';
header.style.borderRadius = '6px';
const headerHeight = header.getBoundingClientRect().height;
pane.style.height = headerHeight + 'px';
pane.style.minHeight = '0';
pane.style.resize = 'none';
}
this._maResizeObserver = new ResizeObserver(() => {
const rect = pane.getBoundingClientRect();
const isMinimized = content.style.display === 'none';
if (!isMinimized) {
pane.dataset.savedHeight = rect.height;
}
this.maStorage.set('size', {
width: rect.width,
height: isMinimized ? pane.dataset.savedHeight : rect.height
});
});
this._maResizeObserver.observe(pane);
this.manaTracking = {
players: {},
startTime: null,
lastUpdateTime: null,
totalDuration: 0,
savedPausedMs: window.MCS_TOTAL_PAUSED_MS ?? 0,
savedTabHiddenMs: window.MCS_TOTAL_TAB_HIDDEN_MS ?? 0
};
const self = this;
const handleWebSocketMessage = PerformanceMonitor.wrap('MAna', (event) => {
if (window.MCS_MODULES_DISABLED) return;
const data = event.detail;
const messageType = data?.type;
if (!messageType ||
(messageType !== 'battle_updated' &&
messageType !== 'new_battle' &&
messageType !== 'battle_consumable_ability_updated')) {
return;
}
if (messageType === 'battle_updated') {
self.handleMAnaBattleUpdate(data);
return;
}
if (messageType === 'new_battle') {
self.initializeMAnaTracking(data.players);
return;
}
if (messageType === 'battle_consumable_ability_updated') {
if (data.ability) {
const abilityHrid = data.ability.abilityHrid || data.ability;
const abilityDetailMap = self.loadAbilityDetailMap();
const abilityDetail = abilityDetailMap[abilityHrid];
if (abilityDetail && abilityDetail.manaCost) {
const mainPlayerName = CharacterStorageUtils.getPlayerKey();
const playerData = Object.values(self.manaTracking.players || {}).find(p => p.name === mainPlayerName);
if (playerData) {
if (!playerData.knownAbilityCosts) {
playerData.knownAbilityCosts = {};
}
playerData.knownAbilityCosts[abilityHrid] = abilityDetail.manaCost;
if (!playerData.equippedAbilities) {
playerData.equippedAbilities = {};
}
const abilityName = abilityHrid.split('/').pop().replace(/_/g, ' ');
if (!playerData.equippedAbilities[abilityHrid]) {
playerData.equippedAbilities[abilityHrid] = {
name: abilityName,
manaCost: abilityDetail.manaCost,
casts: 1,
totalMana: abilityDetail.manaCost
};
} else {
playerData.equippedAbilities[abilityHrid].casts++;
playerData.equippedAbilities[abilityHrid].totalMana += abilityDetail.manaCost;
}
self.updateMAnaContent();
}
}
}
}
});
window.addEventListener('EquipSpyWebSocketMessage', handleWebSocketMessage);
if (window.lootDropsTrackerInstance && window.lootDropsTrackerInstance.lastBattleData) {
const battleData = window.lootDropsTrackerInstance.lastBattleData;
if (battleData.players) {
self.initializeMAnaTracking(battleData.players);
}
}
}
initializeMAnaTracking(players) {
const currentTime = Date.now();
if (this.manaTracking.startTime) {
this.manaTracking.totalDuration = mcsGetElapsedSeconds(
this.manaTracking.startTime,
this.manaTracking.lastUpdateTime,
this.manaTracking.savedPausedMs,
this.manaTracking.totalDuration,
true,
this.manaTracking.savedTabHiddenMs
);
}
this.manaTracking.savedPausedMs = window.MCS_TOTAL_PAUSED_MS ?? 0;
this.manaTracking.savedTabHiddenMs = window.MCS_TOTAL_TAB_HIDDEN_MS ?? 0;
if (!this.manaTracking.players) {
this.manaTracking.players = {};
}
players.forEach((player, index) => {
if (!this.manaTracking.players[index]) {
this.manaTracking.players[index] = {
name: player.name,
abilities: {},
specificAbilities: {},
equippedAbilities: {},
knownAbilityCosts: {},
lastMP: null,
lastAbilityHrid: null,
lastIsAutoAttack: false,
lastInt: null,
seenInts: new Set(),
totalManaGained: 0,
manaGainsBySize: {},
manaChanges: {},
fullManaCount: 0,
maxMP: player.mMP ?? 0,
abilityQueue: [],
derivedCurrentMP: player.cMP ?? 0,
derivedMaxMP: player.mMP ?? 0,
actualCurrentMP: player.cMP ?? 0,
actualMaxMP: player.mMP ?? 0
};
} else {
if (this.manaTracking.players[index].lastMP === undefined) {
this.manaTracking.players[index].lastMP = null;
}
if (this.manaTracking.players[index].totalManaGained === undefined) {
this.manaTracking.players[index].totalManaGained = 0;
}
if (!this.manaTracking.players[index].manaGainsBySize) {
this.manaTracking.players[index].manaGainsBySize = {};
}
if (this.manaTracking.players[index].fullManaCount === undefined) {
this.manaTracking.players[index].fullManaCount = 0;
}
if (!this.manaTracking.players[index].manaChanges) {
this.manaTracking.players[index].manaChanges = {};
}
this.manaTracking.players[index].maxMP = (player.mMP ?? this.manaTracking.players[index].maxMP) ?? 0;
if (this.manaTracking.players[index].derivedCurrentMP === undefined) {
this.manaTracking.players[index].derivedCurrentMP = player.cMP ?? 0;
}
if (this.manaTracking.players[index].derivedMaxMP === undefined) {
this.manaTracking.players[index].derivedMaxMP = player.mMP ?? 0;
}
if (player.cMP !== undefined && player.cMP !== null) {
this.manaTracking.players[index].actualCurrentMP = player.cMP;
}
if (player.mMP !== undefined && player.mMP !== null && player.mMP > 0) {
this.manaTracking.players[index].actualMaxMP = player.mMP;
}
if (!this.manaTracking.players[index].abilityQueue) {
this.manaTracking.players[index].abilityQueue = [];
}
if (!this.manaTracking.players[index].specificAbilities) {
this.manaTracking.players[index].specificAbilities = {};
}
if (!this.manaTracking.players[index].knownAbilityCosts) {
this.manaTracking.players[index].knownAbilityCosts = {};
}
if (this.manaTracking.players[index].lastInt === undefined) {
this.manaTracking.players[index].lastInt = null;
}
if (!this.manaTracking.players[index].seenInts) {
this.manaTracking.players[index].seenInts = new Set();
}
if (!this.manaTracking.players[index].equippedAbilities) {
this.manaTracking.players[index].equippedAbilities = {};
}
}
});
this.manaTracking.startTime = currentTime;
this.manaTracking.lastUpdateTime = currentTime;
this.updateMAnaContent();
}
handleMAnaBattleUpdate(data) {
if (!this.manaTracking || !this.manaTracking.players) return;
const pMap = data.pMap;
if (!pMap) return;
const currentTime = Date.now();
Object.keys(pMap).forEach(pIndex => {
const player = pMap[pIndex];
if (!player || !this.manaTracking.players[pIndex]) return;
const playerTracking = this.manaTracking.players[pIndex];
const currentInt = player.int;
const currentMP = player.cMP ?? 0;
const lastMP = playerTracking.lastMP;
const isNewEntity = currentInt !== undefined && currentInt !== null &&
currentInt !== playerTracking.lastInt;
playerTracking.actualCurrentMP = currentMP;
playerTracking.actualMaxMP = (player.mMP ?? playerTracking.actualMaxMP) ?? 0;
const currentAbilityHrid = player.abilityHrid;
const currentIsAutoAttack = player.isAutoAtk;
if (!playerTracking.abilityQueue) {
playerTracking.abilityQueue = [];
}
const lastInQueue = playerTracking.abilityQueue[playerTracking.abilityQueue.length - 1];
const shouldQueue = (() => {
if (!currentAbilityHrid) {
return false;
}
if (isNewEntity) {
return false;
}
if (!lastInQueue || lastInQueue.hrid !== currentAbilityHrid) {
return true;
}
if (lastInQueue && lastInQueue.hrid === currentAbilityHrid) {
if (lastInQueue.isAutoAttack !== currentIsAutoAttack && !currentIsAutoAttack) {
return true;
}
}
return false;
})();
if (shouldQueue) {
playerTracking.abilityQueue.push({
hrid: currentAbilityHrid,
isAutoAttack: currentIsAutoAttack,
int: currentInt,
timestamp: currentTime
});
if (playerTracking.abilityQueue.length > 10) {
playerTracking.abilityQueue.shift();
}
}
if (lastMP === null) {
playerTracking.lastMP = currentMP;
playerTracking.derivedCurrentMP = currentMP;
playerTracking.derivedMaxMP = player.mMP ?? 0;
return;
}
if (currentMP !== lastMP) {
const manaChange = currentMP - lastMP;
const changeKey = `Mana change (${manaChange > 0 ? '+' : ''}${manaChange})`;
if (!playerTracking.manaChanges[changeKey]) {
playerTracking.manaChanges[changeKey] = {
amount: manaChange,
count: 0
};
}
playerTracking.manaChanges[changeKey].count += 1;
playerTracking.derivedCurrentMP += manaChange;
playerTracking.derivedCurrentMP = Math.max(0, Math.min(playerTracking.derivedMaxMP, playerTracking.derivedCurrentMP));
}
if (lastMP > currentMP) {
let remainingManaCost = lastMP - currentMP;
const manaUseKey = `Mana use (${remainingManaCost})`;
if (!playerTracking.abilities[manaUseKey]) {
playerTracking.abilities[manaUseKey] = {
name: manaUseKey,
manaCost: remainingManaCost,
casts: 0,
totalMana: 0
};
}
const abilityData = playerTracking.abilities[manaUseKey];
abilityData.casts += 1;
abilityData.totalMana += remainingManaCost;
abilityData.manaCost = remainingManaCost;
if (playerTracking.abilityQueue.length > 0) {
if (playerTracking.abilityQueue.length === 1) {
const ability = playerTracking.abilityQueue.shift();
if (ability.hrid) {
const abilityHrid = ability.hrid;
const abilityName = abilityHrid.split('/').pop().replace(/_/g, ' ');
playerTracking.knownAbilityCosts[abilityHrid] = remainingManaCost;
if (!playerTracking.specificAbilities[abilityHrid]) {
playerTracking.specificAbilities[abilityHrid] = {
name: abilityName,
manaCost: remainingManaCost,
casts: 0,
totalMana: 0
};
}
const specificAbilityData = playerTracking.specificAbilities[abilityHrid];
specificAbilityData.casts++;
specificAbilityData.totalMana += remainingManaCost;
specificAbilityData.manaCost = remainingManaCost;
}
} else {
let attributedMana = 0;
const abilitiesToProcess = [...playerTracking.abilityQueue];
playerTracking.abilityQueue = [];
for (const ability of abilitiesToProcess) {
if (ability.hrid) {
const abilityHrid = ability.hrid;
const abilityName = abilityHrid.split('/').pop().replace(/_/g, ' ');
let manaCost = playerTracking.knownAbilityCosts[abilityHrid] ||
(remainingManaCost - attributedMana) / (abilitiesToProcess.length - abilitiesToProcess.indexOf(ability));
if (!playerTracking.specificAbilities[abilityHrid]) {
playerTracking.specificAbilities[abilityHrid] = {
name: abilityName,
manaCost: manaCost,
casts: 0,
totalMana: 0
};
}
const specificAbilityData = playerTracking.specificAbilities[abilityHrid];
specificAbilityData.casts++;
specificAbilityData.totalMana += manaCost;
if (playerTracking.knownAbilityCosts[abilityHrid]) {
specificAbilityData.manaCost = playerTracking.knownAbilityCosts[abilityHrid];
} else {
specificAbilityData.manaCost = specificAbilityData.totalMana / specificAbilityData.casts;
}
attributedMana += manaCost;
}
}
}
} else if (playerTracking.lastAbilityHrid) {
const abilityHrid = playerTracking.lastAbilityHrid;
const abilityName = abilityHrid.split('/').pop().replace(/_/g, ' ');
if (!playerTracking.specificAbilities[abilityHrid]) {
playerTracking.specificAbilities[abilityHrid] = {
name: abilityName,
manaCost: remainingManaCost,
casts: 0,
totalMana: 0
};
}
const specificAbilityData = playerTracking.specificAbilities[abilityHrid];
specificAbilityData.casts++;
specificAbilityData.totalMana += remainingManaCost;
specificAbilityData.manaCost = specificAbilityData.totalMana / specificAbilityData.casts;
}
} else if (currentMP > lastMP) {
const manaGain = currentMP - lastMP;
if (!playerTracking.totalManaGained) {
playerTracking.totalManaGained = 0;
}
if (!playerTracking.manaGainsBySize) {
playerTracking.manaGainsBySize = {};
}
playerTracking.totalManaGained += manaGain;
const gainKey = `${manaGain}`;
if (!playerTracking.manaGainsBySize[gainKey]) {
playerTracking.manaGainsBySize[gainKey] = {
amount: manaGain,
count: 0,
totalMana: 0
};
}
playerTracking.manaGainsBySize[gainKey].count++;
playerTracking.manaGainsBySize[gainKey].totalMana += manaGain;
if (currentMP >= playerTracking.maxMP && lastMP < playerTracking.maxMP) {
if (!playerTracking.fullManaCount) {
playerTracking.fullManaCount = 0;
}
playerTracking.fullManaCount++;
}
}
playerTracking.lastMP = currentMP;
playerTracking.lastAbilityHrid = currentAbilityHrid;
playerTracking.lastIsAutoAttack = currentIsAutoAttack;
if (isNewEntity) {
playerTracking.lastInt = currentInt;
playerTracking.seenInts.add(currentInt);
}
});
this.manaTracking.lastUpdateTime = currentTime;
const now = Date.now();
const content = document.getElementById('mana-content');
const isMinimized = content && content.style.display === 'none';
const throttleInterval = isMinimized ? 2000 : 500;
if (!this.manaTracking.lastUIUpdate || now - this.manaTracking.lastUIUpdate >= throttleInterval) {
this.manaTracking.lastUIUpdate = now;
this.updateMAnaContent();
}
}
resetMAnaTracking() {
this.manaTracking = {
players: {},
startTime: null,
lastUpdateTime: null,
totalDuration: 0,
lastUIUpdate: 0,
savedPausedMs: window.MCS_TOTAL_PAUSED_MS ?? 0,
savedTabHiddenMs: window.MCS_TOTAL_TAB_HIDDEN_MS ?? 0
};
this.updateMAnaContent();
}
updateMAnaContent() {
const content = document.getElementById('mana-content');
if (!content) return;
let elapsedSeconds = this.manaTracking?.startTime
? mcsGetElapsedSeconds(this.manaTracking.startTime, this.manaTracking.lastUpdateTime, this.manaTracking.savedPausedMs, this.manaTracking?.totalDuration ?? 0, true, this.manaTracking?.savedTabHiddenMs)
: (this.manaTracking?.totalDuration ?? 0);
if (elapsedSeconds === 0) elapsedSeconds = 1;
const mainPlayerName = CharacterStorageUtils.getPlayerKey();
let playerData = null;
let mainPlayerIndex = null;
if (this.manaTracking?.players) {
for (const [index, player] of Object.entries(this.manaTracking.players)) {
if (player.name === mainPlayerName) {
playerData = player;
mainPlayerIndex = index;
break;
}
}
}
const timeDisplay = document.getElementById('mana-time-display');
if (timeDisplay) {
const timeStr = mcsFormatDuration(elapsedSeconds, 'clock');
if (timeDisplay.textContent !== timeStr) {
timeDisplay.textContent = timeStr;
}
}
const manaDisplay = document.getElementById('mana-header-display');
if (manaDisplay && playerData) {
const currentMana = Math.round(playerData.actualCurrentMP ?? 0);
const maxMana = playerData.actualMaxMP ?? 0;
const manaStr = `${currentMana}/${maxMana}`;
if (manaDisplay.textContent !== manaStr) {
manaDisplay.textContent = manaStr;
}
}
if (content.style.display === 'none') return;
if (!this.manaTracking?.players || Object.keys(this.manaTracking.players).length === 0) {
if (!content.querySelector('.mana-waiting')) {
content.innerHTML = `
<div class="mana-waiting mcs-ma-waiting">
<div class="mcs-ma-waiting-icon">⏳</div>
<div>Waiting for new fight to begin</div>
<div class="mcs-ma-waiting-sub">MAna will begin tracking mana usage shortly</div>
</div>
`;
}
return;
}
if (!playerData) {
if (!content.querySelector('.mana-no-usage')) {
content.innerHTML = `
<div class="mana-no-usage mcs-ma-waiting">
<div class="mcs-ma-waiting-icon">⏳</div>
<div>No mana usage detected yet</div>
<div class="mcs-ma-waiting-sub">Cast abilities to see tracking data</div>
</div>
`;
}
return;
}
const hasAbilities = playerData.equippedAbilities && Object.keys(playerData.equippedAbilities).length > 0;
const hasManaChanges = playerData.manaChanges && Object.keys(playerData.manaChanges).length > 0;
if (!hasAbilities && !hasManaChanges) {
if (!content.querySelector('.mana-no-usage')) {
content.innerHTML = `
<div class="mana-no-usage mcs-ma-waiting">
<div class="mcs-ma-waiting-icon">⏳</div>
<div>No mana usage detected yet</div>
<div class="mcs-ma-waiting-sub">Cast abilities to see tracking data</div>
</div>
`;
}
return;
}
this.initializeManaDOMStructure(content, playerData);
this.updateAbilityRows(playerData, elapsedSeconds);
this.updateManaChangesSection(playerData, elapsedSeconds);
}
initializeManaDOMStructure(content, playerData) {
if (content.querySelector('#mana-abilities-table')) return;
content.innerHTML = `
<div class="mcs-ma-table-wrapper">
<table id="mana-abilities-table" class="mcs-ma-table">
<thead>
<tr class="mcs-ma-thead-row">
<th class="mcs-ma-th mcs-ma-th-left">Ability Name</th>
<th class="mcs-ma-th mcs-ma-th-right">Casts</th>
<th class="mcs-ma-th mcs-ma-th-right">Mana Cost</th>
<th class="mcs-ma-th mcs-ma-th-right">Total Mana</th>
<th class="mcs-ma-th mcs-ma-th-right">m/s</th>
</tr>
</thead>
<tbody id="mana-abilities-tbody"></tbody>
</table>
<div class="mcs-ma-changes-section">
<table id="mana-changes-table" class="mcs-ma-table">
<thead>
<tr class="mcs-ma-thead-row">
<th class="mcs-ma-th mcs-ma-th-left">Change</th>
<th class="mcs-ma-th mcs-ma-th-right">Total</th>
<th class="mcs-ma-th mcs-ma-th-right">Count</th>
<th class="mcs-ma-th mcs-ma-th-right">Mana/s</th>
</tr>
</thead>
<tbody id="mana-changes-tbody"></tbody>
<tfoot id="mana-changes-tfoot"></tfoot>
</table>
</div>
</div>
`;
}
updateAbilityRows(playerData, elapsedSeconds) {
const tbody = document.getElementById('mana-abilities-tbody');
if (!tbody) return;
if (!playerData.equippedAbilities || Object.keys(playerData.equippedAbilities).length === 0) {
if (tbody.children.length > 0) {
tbody.innerHTML = '';
}
return;
}
const abilities = Object.entries(playerData.equippedAbilities)
.map(([hrid, ability]) => ({ ...ability, hrid }))
.sort((a, b) => b.totalMana - a.totalMana);
const currentIds = new Set();
abilities.forEach((ability, index) => {
const sanitizedId = ability.hrid.replace(/[^a-zA-Z0-9_-]/g, '_');
const rowId = `ability-row-${sanitizedId}`;
currentIds.add(rowId);
let row = document.getElementById(rowId);
if (!row) {
row = document.createElement('tr');
row.id = rowId;
row.className = 'mcs-ma-ability-row';
row.innerHTML = `
<td class="ability-name mcs-ma-ability-name"></td>
<td class="ability-casts mcs-ma-td-right mcs-ma-color-green"></td>
<td class="ability-cost mcs-ma-td-right mcs-ma-color-orange"></td>
<td class="ability-total mcs-ma-td-right mcs-ma-color-red"></td>
<td class="ability-rate mcs-ma-td-right mcs-ma-color-red"></td>
`;
if (index < tbody.children.length) {
tbody.insertBefore(row, tbody.children[index]);
} else {
tbody.appendChild(row);
}
} else {
const currentIndex = Array.from(tbody.children).indexOf(row);
if (currentIndex !== index) {
if (index < tbody.children.length) {
tbody.insertBefore(row, tbody.children[index]);
} else {
tbody.appendChild(row);
}
}
}
const manaPerSecond = (ability.totalMana / elapsedSeconds).toFixed(2);
const nameCell = row.querySelector('.ability-name');
if (nameCell.textContent !== ability.name) nameCell.textContent = ability.name;
const castsCell = row.querySelector('.ability-casts');
const castsText = `${ability.casts}×`;
if (castsCell.textContent !== castsText) castsCell.textContent = castsText;
const costCell = row.querySelector('.ability-cost');
const costText = ability.manaCost.toString();
if (costCell.textContent !== costText) costCell.textContent = costText;
const totalCell = row.querySelector('.ability-total');
const totalText = `-${ability.totalMana.toFixed(1)}`;
if (totalCell.textContent !== totalText) totalCell.textContent = totalText;
const rateCell = row.querySelector('.ability-rate');
const rateText = `-${manaPerSecond}`;
if (rateCell.textContent !== rateText) rateCell.textContent = rateText;
});
const existingRows = Array.from(tbody.querySelectorAll('[id^="ability-row-"]'));
existingRows.forEach(row => {
if (!currentIds.has(row.id)) {
row.remove();
}
});
}
updateManaChangesSection(playerData, elapsedSeconds) {
const tbody = document.getElementById('mana-changes-tbody');
const tfoot = document.getElementById('mana-changes-tfoot');
if (!tbody || !tfoot) return;
const manaChanges = Object.values(playerData.manaChanges || {});
if (manaChanges.length === 0) {
if (tbody.children.length > 0) {
tbody.innerHTML = '';
}
let fullManaRow = document.getElementById('mana-full-mana-row-empty');
if (!fullManaRow) {
tfoot.innerHTML = `
<tr id="mana-full-mana-row-empty" class="mcs-ma-full-mana-row">
<td class="mcs-ma-td mcs-ma-color-purple">Full Mana</td>
<td class="mcs-ma-td-right mcs-ma-color-purple">—</td>
<td class="full-mana-count mcs-ma-td-right mcs-ma-color-green"></td>
<td class="mcs-ma-td-right mcs-ma-color-purple">—</td>
</tr>
`;
fullManaRow = document.getElementById('mana-full-mana-row-empty');
}
const fullManaCell = fullManaRow.querySelector('.full-mana-count');
const fullManaText = `${playerData.fullManaCount ?? 0}×`;
if (fullManaCell.textContent !== fullManaText) {
fullManaCell.textContent = fullManaText;
}
return;
}
manaChanges.sort((a, b) => Math.abs(b.amount) - Math.abs(a.amount));
let netChange = 0;
manaChanges.forEach(change => {
const totalChange = change.amount * change.count;
const changePerSecond = (totalChange / elapsedSeconds).toFixed(2);
netChange += totalChange;
const color = change.amount < 0 ? '#ef5350' : '#64b5f6';
const sign = change.amount > 0 ? '+' : '';
const rowId = `change-row-${change.amount}`;
let row = document.getElementById(rowId);
if (!row) {
row = document.createElement('tr');
row.id = rowId;
row.className = 'mcs-ma-change-row';
row.innerHTML = `
<td class="change-amount mcs-ma-td"></td>
<td class="change-total mcs-ma-td-right"></td>
<td class="change-count mcs-ma-td-right mcs-ma-color-green"></td>
<td class="change-rate mcs-ma-td-right"></td>
`;
tbody.appendChild(row);
}
const amountCell = row.querySelector('.change-amount');
const amountText = `${sign}${change.amount}`;
if (amountCell.textContent !== amountText || amountCell.style.color !== color) {
amountCell.textContent = amountText;
amountCell.style.color = color;
}
const totalCell = row.querySelector('.change-total');
const totalText = `${sign}${totalChange.toFixed(1)}`;
if (totalCell.textContent !== totalText || totalCell.style.color !== color) {
totalCell.textContent = totalText;
totalCell.style.color = color;
}
const countCell = row.querySelector('.change-count');
const countText = `${change.count}×`;
if (countCell.textContent !== countText) countCell.textContent = countText;
const rateCell = row.querySelector('.change-rate');
const rateText = `${sign}${changePerSecond}`;
if (rateCell.textContent !== rateText || rateCell.style.color !== color) {
rateCell.textContent = rateText;
rateCell.style.color = color;
}
});
const netChangePerSecond = (netChange / elapsedSeconds).toFixed(2);
const netColor = netChange < 0 ? '#ef5350' : '#64b5f6';
const netSign = netChange > 0 ? '+' : '';
let netChangeRow = document.getElementById('mana-net-change-row');
let fullManaRow = document.getElementById('mana-full-mana-row');
if (!netChangeRow || !fullManaRow) {
tfoot.innerHTML = `
<tr id="mana-net-change-row" class="mcs-ma-net-row">
<td class="net-label mcs-ma-td">Net Change</td>
<td class="net-total mcs-ma-td-right"></td>
<td class="mcs-ma-td-right mcs-ma-color-green">—</td>
<td class="net-rate mcs-ma-td-right"></td>
</tr>
<tr id="mana-full-mana-row" class="mcs-ma-full-mana-row">
<td class="mcs-ma-td mcs-ma-color-purple">Full Mana</td>
<td class="mcs-ma-td-right mcs-ma-color-purple">—</td>
<td class="full-mana-count mcs-ma-td-right mcs-ma-color-green"></td>
<td class="mcs-ma-td-right mcs-ma-color-purple">—</td>
</tr>
`;
netChangeRow = document.getElementById('mana-net-change-row');
fullManaRow = document.getElementById('mana-full-mana-row');
}
const netLabelCell = netChangeRow.querySelector('.net-label');
if (netLabelCell.style.color !== netColor) netLabelCell.style.color = netColor;
const netTotalCell = netChangeRow.querySelector('.net-total');
const netTotalText = `${netSign}${netChange.toFixed(1)}`;
if (netTotalCell.textContent !== netTotalText || netTotalCell.style.color !== netColor) {
netTotalCell.textContent = netTotalText;
netTotalCell.style.color = netColor;
}
const netRateCell = netChangeRow.querySelector('.net-rate');
const netRateText = `${netSign}${netChangePerSecond}`;
if (netRateCell.textContent !== netRateText || netRateCell.style.color !== netColor) {
netRateCell.textContent = netRateText;
netRateCell.style.color = netColor;
}
const fullManaCell = fullManaRow.querySelector('.full-mana-count');
const fullManaText = `${playerData.fullManaCount ?? 0}×`;
if (fullManaCell.textContent !== fullManaText) {
fullManaCell.textContent = fullManaText;
}
}
destroyMAna() {
if (this._maDragMove) {
document.removeEventListener('mousemove', this._maDragMove);
document.removeEventListener('mouseup', this._maDragUp);
this._maDragMove = null;
this._maDragUp = null;
}
if (this._maResizeObserver) {
this._maResizeObserver.disconnect();
this._maResizeObserver = null;
}
const pane = document.getElementById('mana-pane');
if (pane) pane.remove();
}
// MAna end
// TReasure start
mcs_tr_handleWebSocketMessage(event) {
if (window.MCS_MODULES_DISABLED) return;
const data = event.detail;
if (data?.type === 'loot_opened') {
this.mcs_tr_handleLootOpened(data);
}
}
mcs_tr_handleStorage(event) {
if (event.key && event.key.includes('mcs_FL_ask_bid_price_mode')) {
this.mcs_tr_renderContent();
}
}
mcs_tr_handlePriceToggleClick(event) {
const target = event.target;
if (target.classList.contains('ldt-price-toggle-btn') ||
target.classList.contains('ldt-price-toggle-btn-hidden')) {
setTimeout(() => this.mcs_tr_renderContent(), 200);
}
}
mcs_tr_handleFlootPricesUpdated() {
this.mcs_tr_renderContent();
}
mcs_tr_getStorageKey() {
const playerName = this.mcs_tr_getPlayerName();
return `mcs_TR_${playerName}`;
}
mcs_tr_getPlayerName() {
try {
const cachedData = CharacterDataStorage.get();
if (cachedData) {
if (cachedData.character?.name) {
return cachedData.character.name;
}
}
} catch (e) {
console.error('[TReasure] Error getting player name:', e);
}
return 'Unknown';
}
mcs_tr_initData() {
return {
chests: {},
hidden: [],
minimized: false,
position: null,
size: null,
useMirrorValue: false,
useCowbell0: false,
expandedTokenShops: []
};
}
mcs_tr_getTokenShopData() {
return {
'/items/chimerical_token': {
name: 'Chimerical Token',
items: [
{ hrid: '/items/griffin_leather', name: 'Griffin Leather', cost: 600 },
{ hrid: '/items/manticore_sting', name: 'Manticore Sting', cost: 1000 },
{ hrid: '/items/jackalope_antler', name: 'Jackalope Antler', cost: 1200 },
{ hrid: '/items/dodocamel_plume', name: 'Dodocamel Plume', cost: 3000 },
{ hrid: '/items/griffin_talon', name: 'Griffin Talon', cost: 3000 },
{ hrid: '/items/chimerical_quiver', name: 'Chimerical Quiver', cost: 35000, isCapeItem: true }
]
},
'/items/sinister_token': {
name: 'Sinister Token',
items: [
{ hrid: '/items/acrobats_ribbon', name: "Acrobat's Ribbon", cost: 2000 },
{ hrid: '/items/magicians_cloth', name: "Magician's Cloth", cost: 2000 },
{ hrid: '/items/chaotic_chain', name: 'Chaotic Chain', cost: 3000 },
{ hrid: '/items/cursed_ball', name: 'Cursed Ball', cost: 3000 },
{ hrid: '/items/sinister_cape', name: 'Sinister Cape', cost: 27000, isCapeItem: true }
]
},
'/items/enchanted_token': {
name: 'Enchanted Token',
items: [
{ hrid: '/items/royal_cloth', name: 'Royal Cloth', cost: 2000 },
{ hrid: '/items/knights_ingot', name: "Knight's Ingot", cost: 2000 },
{ hrid: '/items/bishops_scroll', name: "Bishop's Scroll", cost: 2000 },
{ hrid: '/items/regal_jewel', name: 'Regal Jewel', cost: 3000 },
{ hrid: '/items/sundering_jewel', name: 'Sundering Jewel', cost: 3000 },
{ hrid: '/items/enchanted_cloak', name: 'Enchanted Cloak', cost: 27000, isCapeItem: true }
]
},
'/items/pirate_token': {
name: 'Pirate Token',
items: [
{ hrid: '/items/marksman_brooch', name: 'Marksman Brooch', cost: 2000 },
{ hrid: '/items/corsair_crest', name: 'Corsair Crest', cost: 2000 },
{ hrid: '/items/damaged_anchor', name: 'Damaged Anchor', cost: 2000 },
{ hrid: '/items/maelstrom_plating', name: 'Maelstrom Plating', cost: 2000 },
{ hrid: '/items/kraken_leather', name: 'Kraken Leather', cost: 2000 },
{ hrid: '/items/kraken_fang', name: 'Kraken Fang', cost: 3000 }
]
}
};
}
mcs_tr_getMirrorPrice() {
const price = typeof window.getUnitValue === 'function' ? window.getUnitValue('/items/mirror_of_protection', 'live') : 0;
return price ?? 0;
}
mcs_tr_calculateTokenValue(tokenHrid) {
const data = this.mcs_tr_loadData();
const useMirrorValue = data.useMirrorValue;
const shopData = this.mcs_tr_getTokenShopData()[tokenHrid];
if (!shopData) return 0;
let bestValuePerToken = 0;
for (const item of shopData.items) {
let itemPrice;
if (item.isCapeItem && useMirrorValue) {
itemPrice = this.mcs_tr_getMirrorPrice();
} else if (item.isCapeItem) {
continue;
} else {
itemPrice = this.mcs_tr_getItemPrice(item.hrid);
}
if (itemPrice > 0) {
const valuePerToken = itemPrice / item.cost;
if (valuePerToken > bestValuePerToken) {
bestValuePerToken = valuePerToken;
}
}
}
return bestValuePerToken;
}
mcs_tr_getCapeItemPrice(tokenHrid, capeItem) {
const data = this.mcs_tr_loadData();
if (data.useMirrorValue) {
return this.mcs_tr_getMirrorPrice();
}
const shopData = this.mcs_tr_getTokenShopData()[tokenHrid];
if (!shopData) return 0;
let bestValuePerToken = 0;
for (const item of shopData.items) {
if (item.isCapeItem) continue;
const itemPrice = this.mcs_tr_getItemPrice(item.hrid);
if (itemPrice > 0) {
const valuePerToken = itemPrice / item.cost;
if (valuePerToken > bestValuePerToken) {
bestValuePerToken = valuePerToken;
}
}
}
return bestValuePerToken * capeItem.cost;
}
mcs_tr_loadData() {
try {
const key = this.mcs_tr_getStorageKey();
const saved = localStorage.getItem(key);
if (saved) {
const data = JSON.parse(saved);
if (!data.chests) data.chests = {};
if (!data.hidden) data.hidden = [];
if (data.useMirrorValue === undefined) data.useMirrorValue = false;
if (data.useCowbell0 === undefined) data.useCowbell0 = false;
if (!data.expandedTokenShops) data.expandedTokenShops = [];
return data;
}
} catch (e) {
console.error('[TReasure] Error loading data:', e);
}
return this.mcs_tr_initData();
}
mcs_tr_saveData(data) {
try {
const key = this.mcs_tr_getStorageKey();
localStorage.setItem(key, JSON.stringify(data));
} catch (e) {
console.error('[TReasure] Error saving data:', e);
}
}
mcs_tr_resetChest(chestHrid = null) {
const data = this.mcs_tr_loadData();
if (chestHrid) {
delete data.chests[chestHrid];
} else {
data.chests = {};
}
this.mcs_tr_saveData(data);
this.mcs_tr_renderContent();
}
mcs_tr_loadHiddenChests() {
const data = this.mcs_tr_loadData();
return new Set(data.hidden || []);
}
mcs_tr_saveHiddenChests(hiddenSet) {
const data = this.mcs_tr_loadData();
data.hidden = [...hiddenSet];
this.mcs_tr_saveData(data);
}
mcs_tr_toggleChestVisibility(chestHrid) {
const data = this.mcs_tr_loadData();
const hidden = new Set(data.hidden || []);
if (hidden.has(chestHrid)) {
hidden.delete(chestHrid);
} else {
hidden.add(chestHrid);
}
data.hidden = [...hidden];
this.mcs_tr_saveData(data);
this.mcs_tr_renderContent();
}
mcs_tr_getChestLootTable(chestHrid) {
try {
const initData = InitClientDataCache.get();
if (!initData?.openableLootDropMap) return null;
return initData.openableLootDropMap[chestHrid] || null;
} catch (e) {
console.error('[TReasure] Error getting chest loot table:', e);
return null;
}
}
mcs_tr_getAllOpenableItems() {
try {
const initData = InitClientDataCache.get();
return initData?.openableLootDropMap || {};
} catch (e) {
console.error('[TReasure] Error getting openable items:', e);
return {};
}
}
mcs_tr_calculateExpectedForOne(chestHrid) {
const lootTable = this.mcs_tr_getChestLootTable(chestHrid);
if (!lootTable) return {};
const expected = {};
for (const drop of lootTable) {
const itemHrid = drop.itemHrid;
const dropRate = drop.dropRate ?? 0;
const minCount = drop.minCount ?? 0;
const maxCount = drop.maxCount ?? 0;
const avgCount = (minCount + maxCount) / 2;
const expectedCount = dropRate * avgCount;
if (expectedCount > 0) {
expected[itemHrid] = (expected[itemHrid] ?? 0) + expectedCount;
}
}
return expected;
}
mcs_tr_calculateExpectedForN(chestHrid, count) {
const expectedForOne = this.mcs_tr_calculateExpectedForOne(chestHrid);
const expected = {};
for (const [itemHrid, value] of Object.entries(expectedForOne)) {
expected[itemHrid] = value * count;
}
return expected;
}
mcs_tr_calculateChestExpectedValue(chestHrid) {
const expectedForOne = this.mcs_tr_calculateExpectedForOne(chestHrid);
let totalValue = 0;
for (const [itemHrid, expectedCount] of Object.entries(expectedForOne)) {
const itemPrice = this.mcs_tr_getItemPrice(itemHrid);
totalValue += expectedCount * itemPrice;
}
return totalValue;
}
mcs_tr_getItemPrice(itemHrid) {
if (itemHrid === '/items/coin') {
return 1;
}
if (itemHrid === '/items/cowbell') {
const data = this.mcs_tr_loadData();
if (data.useCowbell0) {
return 0;
}
const useAskPrice = typeof window.getFlootUseAskPrice === 'function' ? window.getFlootUseAskPrice() : false;
const bagPrice = typeof window.getUnitValue === 'function' ? window.getUnitValue('/items/bag_of_10_cowbells', 'live') : 0;
if (bagPrice) {
return bagPrice / 10;
}
return useAskPrice ? 36000 : 35000;
}
if (itemHrid === '/items/task_token') {
const cachePrice = typeof window.getUnitValue === 'function' ? window.getUnitValue('/items/large_meteorite_cache', 'live') : 0;
if (cachePrice) {
return cachePrice / 30;
}
return 0;
}
const capeItemMap = {
'/items/chimerical_quiver': { tokenHrid: '/items/chimerical_token', cost: 35000 },
'/items/sinister_cape': { tokenHrid: '/items/sinister_token', cost: 27000 },
'/items/enchanted_cloak': { tokenHrid: '/items/enchanted_token', cost: 27000 }
};
if (capeItemMap[itemHrid]) {
const data = this.mcs_tr_loadData();
if (data.useMirrorValue) {
return this.mcs_tr_getMirrorPrice();
} else {
const capeInfo = capeItemMap[itemHrid];
return this.mcs_tr_getCapeItemPrice(capeInfo.tokenHrid, { cost: capeInfo.cost });
}
}
const tokenHrids = [
'/items/chimerical_token',
'/items/sinister_token',
'/items/enchanted_token',
'/items/pirate_token'
];
if (tokenHrids.includes(itemHrid)) {
return this.mcs_tr_calculateTokenValue(itemHrid);
}
const price = typeof window.getUnitValue === 'function' ? window.getUnitValue(itemHrid, 'live') : 0;
return price ?? 0;
}
mcs_tr_formatItemName(hrid) {
return mcsFormatHrid(hrid);
}
mcs_tr_formatNumber(value) {
return mcsFormatCurrency(value, 'cost');
}
mcs_tr_formatExpectedCount(value) {
if (value >= 1000000000) {
return (value / 1000000000).toFixed(2) + 'B';
} else if (value >= 1000000) {
return (value / 1000000).toFixed(2) + 'M';
} else if (value >= 1000) {
return (value / 1000).toFixed(1) + 'K';
} else if (value >= 100) {
return value.toFixed(1);
} else if (value >= 1) {
return value.toFixed(2);
} else if (value >= 0.01) {
return value.toFixed(3);
} else if (value >= 0.001) {
return value.toFixed(4);
} else if (value > 0) {
return value.toExponential(2);
}
return '0';
}
mcs_tr_formatPercentDiff(actual, expected) {
if (expected === 0) {
return actual > 0 ? '+∞%' : '0%';
}
const diff = ((actual - expected) / expected) * 100;
const sign = diff >= 0 ? '+' : '';
return `${sign}${diff.toFixed(1)}%`;
}
mcs_tr_exportData() {
try {
const data = this.mcs_tr_loadData();
const playerName = this.mcs_tr_getPlayerName();
const exportObj = {
player: playerName,
exportedAt: new Date().toISOString(),
settings: {
useMirrorValue: data.useMirrorValue,
useCowbell0: data.useCowbell0
},
chests: {}
};
for (const [chestHrid, chest] of Object.entries(data.chests)) {
const chestName = this.mcs_tr_formatItemName(chestHrid);
const expectedPerOpen = this.mcs_tr_calculateExpectedForOne(chestHrid);
const chestEV = this.mcs_tr_calculateChestExpectedValue(chestHrid);
const totalLoot = {};
for (const [itemHrid, count] of Object.entries(chest.total.loot || {})) {
totalLoot[itemHrid] = {
name: this.mcs_tr_formatItemName(itemHrid),
count,
unitPrice: this.mcs_tr_getItemPrice(itemHrid),
totalValue: count * this.mcs_tr_getItemPrice(itemHrid)
};
}
const lastLoot = {};
for (const [itemHrid, count] of Object.entries(chest.last.loot || {})) {
lastLoot[itemHrid] = {
name: this.mcs_tr_formatItemName(itemHrid),
count,
unitPrice: this.mcs_tr_getItemPrice(itemHrid),
totalValue: count * this.mcs_tr_getItemPrice(itemHrid)
};
}
let totalActualValue = 0;
for (const item of Object.values(totalLoot)) {
totalActualValue += item.totalValue;
}
const totalExpectedValue = chestEV * chest.total.opened;
exportObj.chests[chestHrid] = {
name: chestName,
expectedValuePerOpen: chestEV,
total: {
opened: chest.total.opened,
actualValue: totalActualValue,
expectedValue: totalExpectedValue,
luck: totalExpectedValue > 0 ? ((totalActualValue / totalExpectedValue - 1) * 100).toFixed(1) + '%' : 'N/A',
loot: totalLoot
},
last: {
opened: chest.last.opened,
loot: lastLoot
},
expectedPerOpen
};
}
const blob = new Blob([JSON.stringify(exportObj, null, 2)], { type: 'application/json' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `treasure_${playerName}_${new Date().toISOString().slice(0, 10)}.json`;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
} catch (e) {
console.error('[TReasure] Error exporting data:', e);
}
}
mcs_tr_importData() {
const input = document.createElement('input');
input.type = 'file';
input.accept = '.json';
input.onchange = (e) => {
const file = e.target.files[0];
if (!file) return;
const reader = new FileReader();
reader.onload = (ev) => {
try {
const imported = JSON.parse(ev.target.result);
if (!imported.chests || typeof imported.chests !== 'object') {
alert('Invalid treasure export file: no chests data found.');
return;
}
const rawChests = {};
for (const [chestHrid, chest] of Object.entries(imported.chests)) {
const totalLoot = {};
for (const [itemHrid, item] of Object.entries(chest.total?.loot || {})) {
totalLoot[itemHrid] = typeof item === 'object' ? item.count : item;
}
const lastLoot = {};
for (const [itemHrid, item] of Object.entries(chest.last?.loot || {})) {
lastLoot[itemHrid] = typeof item === 'object' ? item.count : item;
}
rawChests[chestHrid] = {
total: { opened: chest.total?.opened || 0, loot: totalLoot },
last: { opened: chest.last?.opened || 0, loot: lastLoot }
};
}
const data = this.mcs_tr_loadData();
data.chests = rawChests;
if (imported.settings) {
if (imported.settings.useMirrorValue !== undefined) data.useMirrorValue = imported.settings.useMirrorValue;
if (imported.settings.useCowbell0 !== undefined) data.useCowbell0 = imported.settings.useCowbell0;
}
this.mcs_tr_saveData(data);
this.mcs_tr_renderContent();
} catch (err) {
console.error('[TReasure] Error importing data:', err);
alert('Error importing treasure data: ' + err.message);
}
};
reader.readAsText(file);
};
input.click();
}
mcs_tr_getEdibleToolsChestData() {
try {
const edibleTools = JSON.parse(localStorage.getItem('Edible_Tools'));
if (!edibleTools?.Chest_Open_Data) return null;
const openData = edibleTools.Chest_Open_Data;
const cachedData = CharacterDataStorage.get();
const playerId = cachedData?.character?.id;
const playerName = cachedData?.character?.name;
let playerEntry = null;
if (playerId != null) {
playerEntry = openData[playerId] || openData[String(playerId)];
}
if (!playerEntry && playerName) {
for (const data of Object.values(openData)) {
if (data?.玩家昵称 === playerName) {
playerEntry = data;
break;
}
}
}
if (playerEntry?.开箱数据 && Object.keys(playerEntry.开箱数据).length > 0) {
return playerEntry.开箱数据;
}
for (const data of Object.values(openData)) {
if (data?.开箱数据 && Object.keys(data.开箱数据).length > 0) {
return data.开箱数据;
}
}
return null;
} catch (e) {
console.error('[TReasure] Error reading Edible Tools data:', e);
return null;
}
}
mcs_tr_showEdibleImportDialog() {
const overlay = document.createElement('div');
overlay.style.cssText = 'position:fixed;top:0;left:0;width:100%;height:100%;background:rgba(0,0,0,0.6);z-index:100000;display:flex;align-items:center;justify-content:center';
const dialog = document.createElement('div');
dialog.style.cssText = 'background:#2a2a2a;border:1px solid #555;border-radius:8px;padding:20px;min-width:320px;color:#ddd;font-family:sans-serif;text-align:center';
dialog.innerHTML = '<div style="font-size:15px;font-weight:bold;margin-bottom:12px">Import from Edible Tools</div>' +
'<div style="font-size:13px;color:#aaa;margin-bottom:18px;line-height:1.5">' +
'<b>Append</b> — add Edible Tools stats to existing data<br>' +
'<b>Overwrite</b> — replace all data with Edible Tools data</div>';
const btnStyle = 'padding:8px 20px;margin:0 6px;border:none;border-radius:4px;cursor:pointer;font-size:13px;font-weight:bold';
const appendBtn = document.createElement('button');
appendBtn.textContent = 'Append';
appendBtn.style.cssText = btnStyle + ';background:#4a7c4a;color:#fff';
appendBtn.onclick = () => { overlay.remove(); this.mcs_tr_importEdibleTools('append'); };
const overwriteBtn = document.createElement('button');
overwriteBtn.textContent = 'Overwrite';
overwriteBtn.style.cssText = btnStyle + ';background:#7c4a4a;color:#fff';
overwriteBtn.onclick = () => { overlay.remove(); this.mcs_tr_importEdibleTools('overwrite'); };
const cancelBtn = document.createElement('button');
cancelBtn.textContent = 'Cancel';
cancelBtn.style.cssText = btnStyle + ';background:#555;color:#ccc';
cancelBtn.onclick = () => overlay.remove();
dialog.append(appendBtn, overwriteBtn, cancelBtn);
overlay.appendChild(dialog);
overlay.onclick = (e) => { if (e.target === overlay) overlay.remove(); };
document.body.appendChild(overlay);
}
mcs_tr_importEdibleTools(mode = 'overwrite') {
try {
const ediChests = this.mcs_tr_getEdibleToolsChestData();
if (!ediChests) {
alert('No Edible Tools chest data found.');
return;
}
const itemDetailMap = InitClientDataCache.getItemDetailMap();
const nameToHrid = {};
for (const [hrid, detail] of Object.entries(itemDetailMap)) {
if (detail?.name) {
nameToHrid[detail.name] = hrid;
}
}
const convertedChests = {};
let convertedCount = 0;
for (const [chestDisplayName, chestData] of Object.entries(ediChests)) {
const chestHrid = nameToHrid[chestDisplayName];
if (!chestHrid) {
console.warn('[TReasure] Could not find hrid for chest:', chestDisplayName);
continue;
}
const loot = {};
if (chestData.获得物品) {
for (const [itemDisplayName, itemData] of Object.entries(chestData.获得物品)) {
const itemHrid = nameToHrid[itemDisplayName];
if (!itemHrid) {
console.warn('[TReasure] Could not find hrid for item:', itemDisplayName);
continue;
}
loot[itemHrid] = itemData.数量 || 0;
}
}
convertedChests[chestHrid] = {
total: { opened: chestData.总计开箱数量 || 0, loot },
last: { opened: 0, loot: {} }
};
convertedCount++;
}
if (convertedCount === 0) {
alert('No chest data could be converted. Item names may not match.');
return;
}
const data = this.mcs_tr_loadData();
if (mode === 'append') {
for (const [chestHrid, imported] of Object.entries(convertedChests)) {
if (!data.chests[chestHrid]) {
data.chests[chestHrid] = imported;
} else {
const existing = data.chests[chestHrid];
existing.total.opened += imported.total.opened;
for (const [itemHrid, count] of Object.entries(imported.total.loot)) {
existing.total.loot[itemHrid] = (existing.total.loot[itemHrid] ?? 0) + count;
}
}
}
} else {
data.chests = convertedChests;
}
this.mcs_tr_saveData(data);
this.mcs_tr_renderContent();
} catch (e) {
console.error('[TReasure] Error importing Edible Tools data:', e);
alert('Error importing Edible Tools data: ' + e.message);
}
}
mcs_tr_getItemIconHtml(itemHrid, size = 24) {
const itemName = itemHrid.replace('/items/', '');
return createItemIconHtml(itemName, { width: size, height: size, sprite: 'items_sprite', style: 'vertical-align: middle' });
}
mcs_tr_handleLootOpened(data) {
if (!data || !data.openedItem || !data.gainedItems) return;
const chestHrid = data.openedItem.itemHrid;
const chestCount = data.openedItem.count || 1;
const lootMap = {};
for (const item of data.gainedItems) {
const itemHrid = item.itemHrid;
const count = item.count ?? 0;
lootMap[itemHrid] = (lootMap[itemHrid] ?? 0) + count;
}
const treasureData = this.mcs_tr_loadData();
if (!treasureData.chests[chestHrid]) {
treasureData.chests[chestHrid] = {
total: { opened: 0, loot: {} },
last: { opened: 0, loot: {} }
};
}
const chest = treasureData.chests[chestHrid];
chest.last = {
opened: chestCount,
loot: { ...lootMap }
};
chest.total.opened += chestCount;
for (const [itemHrid, count] of Object.entries(lootMap)) {
chest.total.loot[itemHrid] = (chest.total.loot[itemHrid] ?? 0) + count;
}
this.mcs_tr_saveData(treasureData);
this.mcs_tr_renderContent();
}
createTReasurePane() {
if (document.getElementById('treasure-pane')) return;
this.mcs_tr_expandedChests = new Set();
const pane = document.createElement('div');
pane.id = 'treasure-pane';
registerPanel('treasure-pane');
const trData = this.mcs_tr_loadData();
let initialWidth = 520;
let initialHeight = 600;
if (trData.size) {
initialWidth = trData.size.width || 520;
initialHeight = trData.size.height || 600;
}
pane.className = 'mcs-pane mcs-tr-pane';
pane.style.width = initialWidth + 'px';
pane.style.height = initialHeight + 'px';
const header = document.createElement('div');
header.className = 'mcs-pane-header';
const titleSection = document.createElement('div');
titleSection.className = 'mcs-tr-title-section';
const title = document.createElement('span');
title.textContent = 'TReasure';
title.className = 'mcs-tr-title';
titleSection.appendChild(title);
const resetAllBtn = document.createElement('span');
resetAllBtn.id = 'treasure-reset-all-btn';
resetAllBtn.textContent = 'Reset All';
resetAllBtn.className = 'mcs-tr-header-btn mcs-tr-reset-all-btn';
resetAllBtn.onclick = (e) => {
e.stopPropagation();
if (confirm('Reset ALL treasure tracking data?')) {
this.mcs_tr_resetChest(null);
}
};
titleSection.appendChild(resetAllBtn);
const mirrorToggleBtn = document.createElement('span');
mirrorToggleBtn.id = 'treasure-mirror-toggle-btn';
const updateMirrorToggleAppearance = () => {
const currentData = this.mcs_tr_loadData();
const useMirror = currentData.useMirrorValue;
mirrorToggleBtn.textContent = useMirror ? 'Mirror Value' : 'Token Value';
mirrorToggleBtn.style.color = useMirror ? '#9370DB' : '#FFD700';
mirrorToggleBtn.style.background = useMirror ? '#4a3a5a' : '#444';
};
mirrorToggleBtn.title = 'Toggle cape/quiver/cloak valuation method';
mirrorToggleBtn.className = 'mcs-tr-header-btn';
updateMirrorToggleAppearance();
mirrorToggleBtn.onmouseover = () => mirrorToggleBtn.style.opacity = '0.8';
mirrorToggleBtn.onmouseout = () => mirrorToggleBtn.style.opacity = '1';
mirrorToggleBtn.onclick = (e) => {
e.stopPropagation();
const data = this.mcs_tr_loadData();
data.useMirrorValue = !data.useMirrorValue;
this.mcs_tr_saveData(data);
updateMirrorToggleAppearance();
this.mcs_tr_renderContent();
};
titleSection.appendChild(mirrorToggleBtn);
const cowbellToggleBtn = document.createElement('span');
cowbellToggleBtn.id = 'treasure-cowbell-toggle-btn';
const updateCowbellToggleAppearance = () => {
const currentData = this.mcs_tr_loadData();
const useCowbell0 = currentData.useCowbell0;
cowbellToggleBtn.textContent = useCowbell0 ? 'Cowbell 0' : 'Cowbell Market';
cowbellToggleBtn.style.color = useCowbell0 ? '#888' : '#FFD700';
cowbellToggleBtn.style.background = useCowbell0 ? '#3a3a3a' : '#444';
};
cowbellToggleBtn.title = 'Toggle cowbell valuation: 0 or market price';
cowbellToggleBtn.className = 'mcs-tr-header-btn';
updateCowbellToggleAppearance();
cowbellToggleBtn.onmouseover = () => cowbellToggleBtn.style.opacity = '0.8';
cowbellToggleBtn.onmouseout = () => cowbellToggleBtn.style.opacity = '1';
cowbellToggleBtn.onclick = (e) => {
e.stopPropagation();
const data = this.mcs_tr_loadData();
data.useCowbell0 = !data.useCowbell0;
this.mcs_tr_saveData(data);
updateCowbellToggleAppearance();
this.mcs_tr_renderContent();
window.dispatchEvent(new CustomEvent('FlootPricesUpdated'));
};
titleSection.appendChild(cowbellToggleBtn);
const configureBtn = document.createElement('span');
configureBtn.id = 'treasure-configure-btn';
configureBtn.innerHTML = '⚙';
configureBtn.title = 'Configure visible chests';
configureBtn.className = 'mcs-tr-header-btn mcs-tr-configure-btn';
configureBtn.onclick = (e) => {
e.stopPropagation();
this.mcs_tr_configureMode = !this.mcs_tr_configureMode;
configureBtn.classList.toggle('active', this.mcs_tr_configureMode);
this.mcs_tr_renderContent();
};
titleSection.appendChild(configureBtn);
const exportBtn = document.createElement('span');
exportBtn.id = 'treasure-export-btn';
exportBtn.textContent = 'Export';
exportBtn.title = 'Export treasure data';
exportBtn.className = 'mcs-tr-header-btn mcs-tr-configure-btn';
exportBtn.onclick = (e) => {
e.stopPropagation();
this.mcs_tr_exportData();
};
titleSection.appendChild(exportBtn);
const importBtn = document.createElement('span');
importBtn.id = 'treasure-import-btn';
importBtn.textContent = 'Import';
importBtn.title = 'Import treasure data';
importBtn.className = 'mcs-tr-header-btn mcs-tr-configure-btn';
importBtn.onclick = (e) => {
e.stopPropagation();
this.mcs_tr_importData();
};
titleSection.appendChild(importBtn);
const ediBtn = document.createElement('span');
ediBtn.id = 'treasure-edi-import-btn';
ediBtn.innerHTML = '🍴';
ediBtn.title = 'Import from Edible Tools';
ediBtn.className = 'mcs-tr-header-btn mcs-tr-configure-btn';
ediBtn.onclick = (e) => {
e.stopPropagation();
if (!this.mcs_tr_getEdibleToolsChestData()) {
alert('No Edible Tools chest data found.');
return;
}
this.mcs_tr_showEdibleImportDialog();
};
titleSection.appendChild(ediBtn);
this.mcs_tr_configureMode = false;
const buttonsSection = document.createElement('div');
buttonsSection.className = 'mcs-button-section';
const minimizeBtn = document.createElement('span');
minimizeBtn.id = 'treasure-minimize-btn';
minimizeBtn.textContent = '−';
minimizeBtn.className = 'mcs-tr-minimize-btn';
buttonsSection.appendChild(minimizeBtn);
header.appendChild(titleSection);
header.appendChild(buttonsSection);
const content = document.createElement('div');
content.id = 'treasure-content';
content.className = 'mcs-tr-content';
const minimizedContent = document.createElement('div');
minimizedContent.id = 'treasure-content-minimized';
minimizedContent.className = 'mcs-tr-content-minimized mcs-hidden';
pane.appendChild(header);
pane.appendChild(content);
pane.appendChild(minimizedContent);
document.body.appendChild(pane);
this.makeTReasureDraggable(pane, header);
this.treasureIsMinimized = trData.minimized === true;
if (this.treasureIsMinimized) {
content.classList.add('mcs-hidden');
minimizedContent.classList.remove('mcs-hidden');
pane.classList.add('mcs-width-fit', 'mcs-height-auto');
pane.style.minWidth = '50px';
pane.style.minHeight = '50px';
minimizeBtn.textContent = '+';
}
minimizeBtn.onclick = () => {
this.treasureIsMinimized = !this.treasureIsMinimized;
if (this.treasureIsMinimized) {
content.classList.add('mcs-hidden');
minimizedContent.classList.remove('mcs-hidden');
pane.classList.add('mcs-width-fit', 'mcs-height-auto');
pane.style.minWidth = '50px';
pane.style.minHeight = '50px';
minimizeBtn.textContent = '+';
} else {
content.classList.remove('mcs-hidden');
minimizedContent.classList.add('mcs-hidden');
pane.classList.remove('mcs-width-fit', 'mcs-height-auto');
pane.style.minWidth = '400px';
pane.style.minHeight = '200px';
minimizeBtn.textContent = '−';
}
const data = this.mcs_tr_loadData();
data.minimized = this.treasureIsMinimized;
this.mcs_tr_saveData(data);
this.mcs_tr_renderContent();
};
if (trData.position) {
pane.style.top = trData.position.top + 'px';
pane.style.left = trData.position.left + 'px';
pane.style.right = 'auto';
}
let resizeTimeout;
this._trResizeObserver = new ResizeObserver(() => {
clearTimeout(resizeTimeout);
resizeTimeout = setTimeout(() => {
const rect = pane.getBoundingClientRect();
const data = window.lootDropsTrackerInstance.mcs_tr_loadData();
data.size = {
width: Math.round(rect.width),
height: Math.round(rect.height)
};
window.lootDropsTrackerInstance.mcs_tr_saveData(data);
}, 300);
});
this._trResizeObserver.observe(pane);
this.mcs_tr_renderContent();
this._trWsListener = this.mcs_tr_handleWebSocketMessage.bind(this);
this._trStorageListener = this.mcs_tr_handleStorage.bind(this);
this._trClickListener = this.mcs_tr_handlePriceToggleClick.bind(this);
this._trFlootPricesListener = this.mcs_tr_handleFlootPricesUpdated.bind(this);
window.addEventListener('EquipSpyWebSocketMessage', this._trWsListener);
window.addEventListener('storage', this._trStorageListener);
document.addEventListener('click', this._trClickListener, true);
window.addEventListener('FlootPricesUpdated', this._trFlootPricesListener);
this.mcs_tr_setupChestModalObserver();
const treasureInstance = this;
window.getTreasureUseCowbell0 = () => {
try {
const data = treasureInstance.mcs_tr_loadData();
return data.useCowbell0 === true;
} catch (e) {
return false;
}
};
}
mcs_tr_setupChestModalObserver() {
const self = this;
const isModalContainer = (node) => {
if (!node.classList) return false;
return node.classList.contains('Modal_modalContainer__3B80m') ||
node.classList.contains('lll_plainPopup_root');
};
this._trModalObserver = new MutationObserver((mutations) => {
for (const mutation of mutations) {
if (mutation.type === 'childList') {
mutation.addedNodes.forEach(addedNode => {
if (isModalContainer(addedNode)) {
setTimeout(() => self.mcs_tr_checkAndShowChestPanel(addedNode), 100);
}
});
mutation.removedNodes.forEach(removedNode => {
if (isModalContainer(removedNode)) {
self.mcs_tr_removeChestSidePanel();
}
});
}
}
});
const rootElement = document.getElementById('root');
if (rootElement) {
this._trModalObserver.observe(rootElement, { childList: true, subtree: true });
const trObserver = this._trModalObserver;
const trRoot = rootElement;
document.addEventListener('visibilitychange', () => {
if (document.hidden) {
trObserver.disconnect();
} else {
trObserver.observe(trRoot, { childList: true, subtree: true });
}
});
}
}
mcs_tr_checkAndShowChestPanel(modalContainer) {
let chestIconElement = null;
const standardSelector = 'div.Modal_modal__1Jiep div.Item_iconContainer__5z7j4 svg use, div.Modal_modal__1Jiep div.Item_itemContainer__x7kH1 svg use';
chestIconElement = modalContainer.querySelector(standardSelector);
if (!chestIconElement) {
const lllChestPopup = modalContainer.querySelector('#lll_chestOpenPopup');
if (lllChestPopup) {
chestIconElement = lllChestPopup.querySelector('svg use');
}
}
if (!chestIconElement) return;
const href = chestIconElement.getAttribute('href') || '';
const match = href.match(/#(.+)$/);
if (!match) return;
const itemName = match[1];
const chestHrid = `/items/${itemName}`;
const openableItems = this.mcs_tr_getAllOpenableItems();
if (!openableItems[chestHrid]) return;
this.mcs_tr_showChestSidePanel(modalContainer, chestHrid);
}
mcs_tr_removeChestSidePanel() {
const existing = document.getElementById('treasure-chest-side-panel');
if (existing) {
existing.remove();
}
}
mcs_tr_showChestSidePanel(modalContainer, chestHrid) {
this.mcs_tr_removeChestSidePanel();
const treasureData = this.mcs_tr_loadData();
const chestData = treasureData.chests[chestHrid];
const chestName = this.mcs_tr_formatItemName(chestHrid);
const sidePanel = document.createElement('div');
sidePanel.id = 'treasure-chest-side-panel';
sidePanel.className = 'mcs-tr-side-panel';
let html = `
<div class="mcs-tr-side-header">
<div class="mcs-tr-side-title">TReasure - ${chestName}</div>
<span id="treasure-side-panel-close-btn" class="mcs-tr-close-btn" title="Close">×</span>
</div>
`;
if (!chestData || chestData.last.opened === 0) {
html += `<div class="mcs-tr-no-data">No previous opening data</div>`;
} else {
const last = chestData.last;
const expectedForLast = this.mcs_tr_calculateExpectedForN(chestHrid, last.opened);
let lastActualValue = 0;
let lastExpectedValue = 0;
for (const [itemHrid, count] of Object.entries(last.loot)) {
lastActualValue += count * this.mcs_tr_getItemPrice(itemHrid);
}
for (const [itemHrid, count] of Object.entries(expectedForLast)) {
lastExpectedValue += count * this.mcs_tr_getItemPrice(itemHrid);
}
const lastPercentDiff = this.mcs_tr_formatPercentDiff(lastActualValue, lastExpectedValue);
const lastDiffColor = lastActualValue >= lastExpectedValue ? '#4CAF50' : '#F44336';
const useAskPrice = typeof window.getFlootUseAskPrice === 'function' ? window.getFlootUseAskPrice() : false;
const priceColor = useAskPrice ? '#6495ED' : '#4CAF50';
const priceLabel = useAskPrice ? 'ask' : 'bid';
html += `
<div class="mcs-tr-side-section">
<div class="mcs-tr-side-section-title">Last Opening (x${last.opened})</div>
<div class="mcs-tr-side-value-row">
<span style="color: ${priceColor};">${this.mcs_tr_formatNumber(lastActualValue)} ${priceLabel}</span>
<span style="color: ${lastDiffColor}; font-weight: bold;">${lastPercentDiff}</span>
</div>
</div>
`;
html += `<div class="mcs-tr-font-9">`;
const lootTable = this.mcs_tr_getChestLootTable(chestHrid);
const allItems = lootTable ? [...new Set(lootTable.map(drop => drop.itemHrid))] : Object.keys(last.loot);
for (const itemHrid of allItems) {
const actual = last.loot[itemHrid] ?? 0;
const expected = expectedForLast[itemHrid] ?? 0;
if (actual === 0 && expected < 0.01) continue;
const itemIconHtml = this.mcs_tr_getItemIconHtml(itemHrid, 16);
const itemPrice = this.mcs_tr_getItemPrice(itemHrid);
const actualValue = actual * itemPrice;
const expectedValue = expected * itemPrice;
const percentDiff = this.mcs_tr_formatPercentDiff(actual, expected);
const diffColor = actual >= expected ? '#4CAF50' : '#F44336';
html += `
<div class="mcs-tr-side-item-row">
${itemIconHtml}
<div class="mcs-tr-side-item-details">
<div class="mcs-tr-side-item-values">
<span class="mcs-tr-min-30">${this.mcs_tr_formatNumber(actual)}</span>
<span class="mcs-tr-min-45" style="color: ${priceColor};">${actualValue > 0 ? this.mcs_tr_formatNumber(actualValue) : ''}</span>
<span class="mcs-tr-font-9" style="color: ${diffColor};">(${percentDiff})</span>
</div>
<div class="mcs-tr-side-expected-row">
<span class="mcs-tr-min-30">${this.mcs_tr_formatExpectedCount(expected)}</span>
<span class="mcs-tr-min-45">${expectedValue > 0 ? this.mcs_tr_formatNumber(expectedValue) : ''}</span>
<span>expected</span>
</div>
</div>
</div>
`;
}
html += `</div>`;
if (chestData.total.opened > last.opened) {
const totalExpected = this.mcs_tr_calculateExpectedForN(chestHrid, chestData.total.opened);
let totalActualValue = 0;
let totalExpectedValue = 0;
for (const [itemHrid, count] of Object.entries(chestData.total.loot)) {
totalActualValue += count * this.mcs_tr_getItemPrice(itemHrid);
}
for (const [itemHrid, count] of Object.entries(totalExpected)) {
totalExpectedValue += count * this.mcs_tr_getItemPrice(itemHrid);
}
const totalPercentDiff = this.mcs_tr_formatPercentDiff(totalActualValue, totalExpectedValue);
const totalDiffColor = totalActualValue >= totalExpectedValue ? '#4CAF50' : '#F44336';
html += `
<div class="mcs-tr-side-total">
<div class="mcs-tr-side-total-text">
Total (x${chestData.total.opened}):
<span style="color: ${priceColor};">${this.mcs_tr_formatNumber(totalActualValue)}</span>
<span style="color: ${totalDiffColor};"> (${totalPercentDiff})</span>
</div>
</div>
`;
}
html += `
<div class="mcs-tr-side-action-row">
<button id="treasure-view-full-stats-btn" class="mcs-tr-view-stats-btn" data-chest="${chestHrid}">View Full Stats</button>
</div>
`;
}
sidePanel.innerHTML = html;
const isLLLPopup = modalContainer.classList.contains('lll_plainPopup_root');
let appendTarget = document.body;
let lllContainer = null;
if (isLLLPopup) {
lllContainer = modalContainer.querySelector('.lll_plainPopup_container');
} else {
const lllRoot = document.querySelector('.lll_plainPopup_root');
if (lllRoot) {
lllContainer = lllRoot.querySelector('.lll_plainPopup_container');
}
}
if (lllContainer) {
const containerRect = lllContainer.getBoundingClientRect();
sidePanel.style.left = (containerRect.right + 8) + 'px';
sidePanel.style.top = containerRect.top + 'px';
const lllCloseBtn = lllContainer.querySelector('button.Button_button__1Fe9z');
if (lllCloseBtn) {
lllCloseBtn.addEventListener('click', () => {
this.mcs_tr_removeChestSidePanel();
});
}
const lllRoot = modalContainer.classList.contains('lll_plainPopup_root') ? modalContainer : document.querySelector('.lll_plainPopup_root');
if (lllRoot) {
const lllBackground = lllRoot.querySelector('.lll_plainPopup_background');
if (lllBackground) {
lllBackground.addEventListener('click', () => {
this.mcs_tr_removeChestSidePanel();
});
}
}
}
appendTarget.appendChild(sidePanel);
const closeBtn = sidePanel.querySelector('#treasure-side-panel-close-btn');
if (closeBtn) {
closeBtn.addEventListener('click', () => {
this.mcs_tr_removeChestSidePanel();
});
}
const viewFullStatsBtn = sidePanel.querySelector('#treasure-view-full-stats-btn');
if (viewFullStatsBtn) {
viewFullStatsBtn.addEventListener('click', () => {
const clickedChestHrid = viewFullStatsBtn.dataset.chest;
this.mcs_tr_showFullStats(clickedChestHrid);
});
}
if (!lllContainer) {
const modal = modalContainer.querySelector('.Modal_modal__1Jiep');
if (modal) {
const positionPanel = () => {
const modalRect = modal.getBoundingClientRect();
const panelWidth = 220;
const gap = 8;
if (modalRect.right + gap + panelWidth < window.innerWidth) {
sidePanel.style.left = (modalRect.right + gap) + 'px';
} else {
sidePanel.style.left = Math.max(10, modalRect.left - panelWidth - gap) + 'px';
}
sidePanel.style.top = modalRect.top + 'px';
};
positionPanel();
}
}
}
mcs_tr_showFullStats(chestHrid) {
const pane = document.getElementById('treasure-pane');
if (!pane) return;
if (pane.classList.contains('mcs-hidden')) {
pane.classList.remove('mcs-hidden');
const checkbox = document.querySelector('input[type="checkbox"][data-tool-panel="treasure-pane"]');
if (checkbox && !checkbox.checked) {
checkbox.checked = true;
}
try {
const savedStates = ToolVisibilityStorage.get();
savedStates['treasure'] = true;
ToolVisibilityStorage.set(savedStates);
} catch (e) {
}
}
if (this.treasureIsMinimized) {
const content = document.getElementById('treasure-content');
const minimizedContent = document.getElementById('treasure-content-minimized');
const minimizeBtn = document.getElementById('treasure-minimize-btn');
if (content && minimizedContent) {
this.treasureIsMinimized = false;
content.classList.remove('mcs-hidden');
minimizedContent.classList.add('mcs-hidden');
pane.classList.remove('mcs-width-fit', 'mcs-height-auto');
pane.style.minWidth = '400px';
pane.style.minHeight = '200px';
if (minimizeBtn) {
minimizeBtn.textContent = '−';
}
const data = this.mcs_tr_loadData();
data.minimized = false;
this.mcs_tr_saveData(data);
}
}
if (chestHrid) {
this.mcs_tr_expandedChests.add(chestHrid);
this.mcs_tr_renderContent();
setTimeout(() => {
const chestRow = document.querySelector(`.treasure-chest-row[data-chest="${chestHrid}"]`);
if (chestRow) {
chestRow.scrollIntoView({ behavior: 'smooth', block: 'center' });
}
}, 100);
}
}
makeTReasureDraggable(pane, header) {
let startX, startY, startLeft, startTop;
const onDragMove = (e) => {
const dx = e.clientX - startX;
const dy = e.clientY - startY;
pane.style.left = (startLeft + dx) + 'px';
pane.style.top = (startTop + dy) + 'px';
pane.style.right = 'auto';
};
const onDragUp = () => {
document.removeEventListener('mousemove', onDragMove);
document.removeEventListener('mouseup', onDragUp);
const rect = pane.getBoundingClientRect();
const data = this.mcs_tr_loadData();
data.position = { left: rect.left, top: rect.top };
this.mcs_tr_saveData(data);
};
header.addEventListener('mousedown', (e) => {
if (e.target.id === 'treasure-minimize-btn') return;
startX = e.clientX;
startY = e.clientY;
const rect = pane.getBoundingClientRect();
startLeft = rect.left;
startTop = rect.top;
e.preventDefault();
this._trDragMove = onDragMove;
this._trDragUp = onDragUp;
document.addEventListener('mousemove', onDragMove);
document.addEventListener('mouseup', onDragUp);
});
}
mcs_tr_renderContent() {
const content = document.getElementById('treasure-content');
if (!content) return;
const treasureData = this.mcs_tr_loadData();
const openableItems = this.mcs_tr_getAllOpenableItems();
const allChestHrids = Object.keys(openableItems);
if (allChestHrids.length === 0) {
content.innerHTML = `
<div class="mcs-tr-loading">
Loading chest data...<br>
Please wait for game data to load.
</div>
`;
return;
}
allChestHrids.sort((a, b) => {
const nameA = this.mcs_tr_formatItemName(a);
const nameB = this.mcs_tr_formatItemName(b);
return nameA.localeCompare(nameB);
});
const hiddenChests = this.mcs_tr_loadHiddenChests();
const isConfigureMode = this.mcs_tr_configureMode;
let html = '';
if (isConfigureMode) {
html += `
<div class="mcs-tr-configure-header">
<strong>Configure Mode</strong> - Click 👁 to show/hide chests
</div>
`;
}
if (!isConfigureMode) {
html += this.mcs_tr_renderTokenShopSection(treasureData);
}
if (!isConfigureMode) {
html += this.mcs_tr_renderKeysSection();
}
for (const chestHrid of allChestHrids) {
const isHidden = hiddenChests.has(chestHrid);
if (!isConfigureMode && isHidden) {
continue;
}
const chestData = treasureData.chests[chestHrid] ?? { total: { opened: 0, loot: {} }, last: { opened: 0, loot: {} } };
const chestName = this.mcs_tr_formatItemName(chestHrid);
const chestIconHtml = this.mcs_tr_getItemIconHtml(chestHrid, 24);
const isExpanded = this.mcs_tr_expandedChests.has(chestHrid);
const hasBeenOpened = chestData.total.opened > 0;
const chestExpectedValue = this.mcs_tr_calculateChestExpectedValue(chestHrid);
const chestExpectedValueFormatted = this.mcs_tr_formatNumber(chestExpectedValue);
const totalOpened = chestData.total.opened;
const expectedForTotal = this.mcs_tr_calculateExpectedForN(chestHrid, totalOpened);
let totalActualValue = 0;
let totalExpectedValue = 0;
for (const [itemHrid, actualCount] of Object.entries(chestData.total.loot)) {
const itemPrice = this.mcs_tr_getItemPrice(itemHrid);
totalActualValue += actualCount * itemPrice;
}
for (const [itemHrid, expectedCount] of Object.entries(expectedForTotal)) {
const itemPrice = this.mcs_tr_getItemPrice(itemHrid);
totalExpectedValue += expectedCount * itemPrice;
}
const percentDiff = hasBeenOpened ? this.mcs_tr_formatPercentDiff(totalActualValue, totalExpectedValue) : '';
const diffColor = totalActualValue >= totalExpectedValue ? '#4CAF50' : '#F44336';
const resetBtnHtml = (hasBeenOpened && !isConfigureMode) ? `
<button class="treasure-reset-btn mcs-tr-reset-btn" data-chest="${chestHrid}">Reset</button>
` : '';
const visibilityBtnHtml = isConfigureMode ? `
<button class="treasure-visibility-btn mcs-tr-visibility-btn ${isHidden ? 'mcs-tr-visibility-btn-hidden' : 'mcs-tr-visibility-btn-visible'}" data-chest="${chestHrid}" title="${isHidden ? 'Show this chest' : 'Hide this chest'}">${isHidden ? '👁🗨' : '👁'}</button>
` : '';
if (isConfigureMode) {
html += `
<div class="treasure-chest-row mcs-tr-chest-row" data-chest="${chestHrid}" style="${isHidden ? 'background: #2a2a2a; opacity: 0.6;' : ''}">
<div class="treasure-chest-header mcs-tr-chest-header">
${chestIconHtml}
<span class="mcs-tr-chest-name">${chestName} <span class="mcs-tr-chest-ev">(${chestExpectedValueFormatted})</span></span>
${visibilityBtnHtml}
</div>
</div>
`;
continue;
}
html += `
<div class="treasure-chest-row mcs-tr-chest-row" data-chest="${chestHrid}">
<div class="treasure-chest-header mcs-tr-chest-header mcs-tr-chest-header-clickable">
<span class="treasure-expand-icon mcs-tr-expand-icon mcs-tr-expand-icon-green">
${isExpanded ? '−' : '+'}
</span>
${chestIconHtml}
<span class="mcs-tr-chest-name">${chestName} <span class="mcs-tr-chest-ev">(${chestExpectedValueFormatted})</span></span>
<span class="mcs-tr-chest-count">${hasBeenOpened ? 'x' + totalOpened : ''}</span>
<span style="color: ${diffColor};">${percentDiff}</span>
${resetBtnHtml}
</div>
${isExpanded ? this.mcs_tr_renderExpandedChest(chestHrid, chestData) : ''}
</div>
`;
}
content.innerHTML = html;
content.querySelectorAll('.treasure-chest-header').forEach(header => {
header.addEventListener('click', (e) => {
if (e.target.classList.contains('treasure-reset-btn')) return;
if (e.target.classList.contains('treasure-visibility-btn')) return;
if (this.mcs_tr_configureMode) return;
const chestHrid = header.parentElement.dataset.chest;
if (this.mcs_tr_expandedChests.has(chestHrid)) {
this.mcs_tr_expandedChests.delete(chestHrid);
} else {
this.mcs_tr_expandedChests.add(chestHrid);
}
this.mcs_tr_renderContent();
});
});
content.querySelectorAll('.treasure-reset-btn').forEach(btn => {
btn.addEventListener('click', (e) => {
e.stopPropagation();
const chestHrid = btn.dataset.chest;
if (confirm(`Reset tracking data for ${this.mcs_tr_formatItemName(chestHrid)}?`)) {
this.mcs_tr_resetChest(chestHrid);
}
});
});
content.querySelectorAll('.treasure-visibility-btn').forEach(btn => {
btn.addEventListener('click', (e) => {
e.stopPropagation();
const chestHrid = btn.dataset.chest;
this.mcs_tr_toggleChestVisibility(chestHrid);
});
});
content.querySelectorAll('.treasure-token-shop-header').forEach(header => {
header.addEventListener('click', () => {
const tokenHrid = header.dataset.token;
const data = this.mcs_tr_loadData();
const expandedSet = new Set(data.expandedTokenShops || []);
if (expandedSet.has(tokenHrid)) {
expandedSet.delete(tokenHrid);
} else {
expandedSet.add(tokenHrid);
}
data.expandedTokenShops = [...expandedSet];
this.mcs_tr_saveData(data);
this.mcs_tr_renderContent();
});
});
this.mcs_tr_renderMinimizedContent(treasureData, allChestHrids, hiddenChests);
}
mcs_tr_renderKeysSection() {
const keys = [
{ hrid: '/items/chimerical_entry_key', name: 'Chimerical Entry' },
{ hrid: '/items/sinister_entry_key', name: 'Sinister Entry' },
{ hrid: '/items/enchanted_entry_key', name: 'Enchanted Entry' },
{ hrid: '/items/pirate_entry_key', name: 'Pirate Entry' },
{ hrid: '/items/chimerical_chest_key', name: 'Chimerical Chest' },
{ hrid: '/items/sinister_chest_key', name: 'Sinister Chest' },
{ hrid: '/items/enchanted_chest_key', name: 'Enchanted Chest' },
{ hrid: '/items/pirate_chest_key', name: 'Pirate Chest' }
];
const priceColor = '#6495ED';
let html = `<div class="mcs-tr-section-container">
<div class="mcs-tr-keys-grid">`;
for (const key of keys) {
const iconHtml = this.mcs_tr_getItemIconHtml(key.hrid, 18);
const askPrice = typeof window.getUnitValue === 'function' ? window.getUnitValue(key.hrid, 'live', 0, 'ask') : 0;
const priceFormatted = this.mcs_tr_formatNumber(askPrice ?? 0);
html += `
<div class="mcs-tr-key-item">
${iconHtml}
<div class="mcs-tr-key-details">
<div class="mcs-tr-key-name">${key.name}</div>
<div class="mcs-tr-key-price" style="color: ${priceColor};">${priceFormatted}</div>
</div>
</div>
`;
}
html += `</div></div>`;
return html;
}
mcs_tr_renderTokenShopSection(treasureData) {
const tokenShopData = this.mcs_tr_getTokenShopData();
const expandedSet = new Set(treasureData.expandedTokenShops || []);
const useMirrorValue = treasureData.useMirrorValue;
const useAskPrice = typeof window.getFlootUseAskPrice === 'function' ? window.getFlootUseAskPrice() : false;
const priceColor = useAskPrice ? '#6495ED' : '#4CAF50';
let html = `<div class="mcs-tr-section-container">`;
const tokenOrder = [
'/items/chimerical_token',
'/items/sinister_token',
'/items/enchanted_token',
'/items/pirate_token'
];
for (const tokenHrid of tokenOrder) {
const shop = tokenShopData[tokenHrid];
if (!shop) continue;
const isExpanded = expandedSet.has(tokenHrid);
const tokenIconHtml = this.mcs_tr_getItemIconHtml(tokenHrid, 20);
const valuePerToken = this.mcs_tr_calculateTokenValue(tokenHrid);
const valuePerTokenFormatted = valuePerToken.toFixed(1);
let bestValuePerToken = 0;
let bestItemHrid = null;
for (const item of shop.items) {
if (item.isCapeItem) continue;
const itemPrice = this.mcs_tr_getItemPrice(item.hrid);
if (itemPrice > 0) {
const vpt = itemPrice / item.cost;
if (vpt > bestValuePerToken) {
bestValuePerToken = vpt;
bestItemHrid = item.hrid;
}
}
}
html += `
<div class="treasure-token-shop-row mcs-tr-token-row" data-token="${tokenHrid}">
<div class="treasure-token-shop-header mcs-tr-token-header" data-token="${tokenHrid}">
<span class="treasure-expand-icon mcs-tr-expand-icon mcs-tr-expand-icon-purple">
${isExpanded ? '−' : '+'}
</span>
${tokenIconHtml}
<span class="mcs-tr-token-name">${shop.name}</span>
<span class="mcs-tr-token-value" style="color: ${priceColor};" title="Value per token">${valuePerTokenFormatted}/token</span>
</div>
${isExpanded ? this.mcs_tr_renderTokenShopItems(tokenHrid, shop, bestItemHrid, useMirrorValue, priceColor) : ''}
</div>
`;
}
html += `</div>`;
return html;
}
mcs_tr_renderTokenShopItems(tokenHrid, shop, bestItemHrid, useMirrorValue, priceColor) {
let html = `<div class="mcs-tr-token-items">`;
let bestNonCapeValuePerToken = 0;
for (const item of shop.items) {
if (item.isCapeItem) continue;
const itemPrice = this.mcs_tr_getItemPrice(item.hrid);
if (itemPrice > 0) {
const vpt = itemPrice / item.cost;
if (vpt > bestNonCapeValuePerToken) {
bestNonCapeValuePerToken = vpt;
}
}
}
for (const item of shop.items) {
const itemIconHtml = this.mcs_tr_getItemIconHtml(item.hrid, 18);
let itemPrice;
let priceSource = '';
let valuePerToken;
if (item.isCapeItem) {
valuePerToken = bestNonCapeValuePerToken;
if (useMirrorValue) {
itemPrice = this.mcs_tr_getMirrorPrice();
priceSource = ' (mirror)';
} else {
itemPrice = this.mcs_tr_getCapeItemPrice(tokenHrid, item);
priceSource = ' (token)';
}
} else {
itemPrice = this.mcs_tr_getItemPrice(item.hrid);
valuePerToken = itemPrice > 0 ? itemPrice / item.cost : 0;
}
const valuePerTokenFormatted = valuePerToken.toFixed(1);
const itemPriceFormatted = this.mcs_tr_formatNumber(itemPrice);
const isBestValue = item.hrid === bestItemHrid;
const bestBadge = isBestValue ? '<span class="mcs-tr-best-badge">★ BEST</span>' : '';
html += `
<div class="mcs-tr-token-item-row ${isBestValue ? 'mcs-tr-best-value' : ''}">
${itemIconHtml}
<span class="mcs-tr-token-item-name">${item.name}${priceSource}${bestBadge}</span>
<span class="mcs-tr-token-item-cost">${item.cost.toLocaleString()}</span>
<span class="mcs-tr-token-item-price" style="color: ${priceColor};">${itemPriceFormatted}</span>
<span class="mcs-tr-token-item-vpt">${valuePerTokenFormatted}/t</span>
</div>
`;
}
html += `</div>`;
return html;
}
mcs_tr_renderMinimizedContent(treasureData, allChestHrids, hiddenChests) {
const minimizedContent = document.getElementById('treasure-content-minimized');
if (!minimizedContent) return;
let html = '';
for (const chestHrid of allChestHrids) {
if (hiddenChests.has(chestHrid)) continue;
const chestData = treasureData.chests[chestHrid] ?? { total: { opened: 0, loot: {} }, last: { opened: 0, loot: {} } };
const hasBeenOpened = chestData.total.opened > 0;
if (!hasBeenOpened) continue;
const chestIconHtml = this.mcs_tr_getItemIconHtml(chestHrid, 18);
const totalOpened = chestData.total.opened;
const expectedForTotal = this.mcs_tr_calculateExpectedForN(chestHrid, totalOpened);
let totalActualValue = 0;
let totalExpectedValue = 0;
for (const [itemHrid, actualCount] of Object.entries(chestData.total.loot)) {
const itemPrice = this.mcs_tr_getItemPrice(itemHrid);
totalActualValue += actualCount * itemPrice;
}
for (const [itemHrid, expectedCount] of Object.entries(expectedForTotal)) {
const itemPrice = this.mcs_tr_getItemPrice(itemHrid);
totalExpectedValue += expectedCount * itemPrice;
}
const percentDiff = this.mcs_tr_formatPercentDiff(totalActualValue, totalExpectedValue);
const diffColor = totalActualValue >= totalExpectedValue ? '#4CAF50' : '#F44336';
html += `
<div class="mcs-tr-mini-item">
${chestIconHtml}
<span style="color: ${diffColor}; font-weight: bold;">${percentDiff}</span>
</div>
`;
}
minimizedContent.innerHTML = html || '<span class="mcs-tr-no-chests">No chests opened yet</span>';
}
mcs_tr_renderExpandedChest(chestHrid, chestData) {
const lootTable = this.mcs_tr_getChestLootTable(chestHrid);
if (!lootTable) {
return `<div class="mcs-tr-no-loot">Loot table not available</div>`;
}
const expectedForOne = this.mcs_tr_calculateExpectedForOne(chestHrid);
const hasBeenOpened = chestData.total.opened > 0;
const allItems = [...new Set(lootTable.map(drop => drop.itemHrid))];
if (!hasBeenOpened) {
let html = `
<div class="mcs-tr-expanded">
<div style="font-size: 11px;">
<div class="mcs-tr-col-header" style="margin-bottom: 6px;">
EXPECTED LOOT (x1)
</div>
${this.mcs_tr_renderExpectedColumn(expectedForOne, allItems)}
</div>
</div>
`;
return html;
}
const expectedForTotal = this.mcs_tr_calculateExpectedForN(chestHrid, chestData.total.opened);
const expectedForLast = this.mcs_tr_calculateExpectedForN(chestHrid, chestData.last.opened);
let lastActualValue = 0;
let lastExpectedValue = 0;
for (const [itemHrid, count] of Object.entries(chestData.last.loot)) {
lastActualValue += count * this.mcs_tr_getItemPrice(itemHrid);
}
for (const [itemHrid, count] of Object.entries(expectedForLast)) {
lastExpectedValue += count * this.mcs_tr_getItemPrice(itemHrid);
}
const lastPercentDiff = this.mcs_tr_formatPercentDiff(lastActualValue, lastExpectedValue);
const lastDiffColor = lastActualValue >= lastExpectedValue ? '#4CAF50' : '#F44336';
let totalActualValue = 0;
let totalExpectedValue = 0;
for (const [itemHrid, count] of Object.entries(chestData.total.loot)) {
totalActualValue += count * this.mcs_tr_getItemPrice(itemHrid);
}
for (const [itemHrid, count] of Object.entries(expectedForTotal)) {
totalExpectedValue += count * this.mcs_tr_getItemPrice(itemHrid);
}
const totalPercentDiff = this.mcs_tr_formatPercentDiff(totalActualValue, totalExpectedValue);
const totalDiffColor = totalActualValue >= totalExpectedValue ? '#4CAF50' : '#F44336';
let expectedOneValue = 0;
for (const [itemHrid, count] of Object.entries(expectedForOne)) {
expectedOneValue += count * this.mcs_tr_getItemPrice(itemHrid);
}
const useAskPrice = typeof window.getFlootUseAskPrice === 'function' ? window.getFlootUseAskPrice() : false;
const priceColor = useAskPrice ? '#6495ED' : '#4CAF50';
let html = `
<div class="mcs-tr-expanded">
<div class="mcs-tr-grid-3col">
<div>
<div class="mcs-tr-col-header">
LAST (x${chestData.last.opened})
</div>
<div class="mcs-tr-col-value">
<span style="color: ${priceColor};">${this.mcs_tr_formatNumber(lastActualValue)}</span>
<span style="color: ${lastDiffColor};"> (${lastPercentDiff})</span>
</div>
${this.mcs_tr_renderLootColumn(chestData.last.loot, expectedForLast, allItems)}
</div>
<div>
<div class="mcs-tr-col-header">
TOTAL (x${chestData.total.opened})
</div>
<div class="mcs-tr-col-value">
<span style="color: ${priceColor};">${this.mcs_tr_formatNumber(totalActualValue)}</span>
<span style="color: ${totalDiffColor};"> (${totalPercentDiff})</span>
</div>
${this.mcs_tr_renderLootColumn(chestData.total.loot, expectedForTotal, allItems)}
</div>
<div>
<div class="mcs-tr-col-header">
EXPECTED (x1/x${chestData.total.opened})
</div>
<div class="mcs-tr-col-value">
<span style="color: ${priceColor};">${this.mcs_tr_formatNumber(expectedOneValue)}</span>
<span class="mcs-tr-expected-sep"> | </span>
<span style="color: ${priceColor};">${this.mcs_tr_formatNumber(totalExpectedValue)}</span>
</div>
${this.mcs_tr_renderExpectedColumn(expectedForOne, allItems, chestData.total.opened)}
</div>
</div>
</div>
`;
return html;
}
mcs_tr_renderLootColumn(actualLoot, expectedLoot, allItems) {
let html = '';
const useAskPrice = typeof window.getFlootUseAskPrice === 'function' ? window.getFlootUseAskPrice() : false;
const priceColor = useAskPrice ? '#6495ED' : '#4CAF50';
const priceLabel = useAskPrice ? 'ask' : 'bid';
for (const itemHrid of allItems) {
const actual = actualLoot[itemHrid] ?? 0;
const expected = expectedLoot[itemHrid] ?? 0;
const itemIconHtml = this.mcs_tr_getItemIconHtml(itemHrid, 16);
const itemName = this.mcs_tr_formatItemName(itemHrid);
const percentDiff = this.mcs_tr_formatPercentDiff(actual, expected);
const diffColor = actual >= expected ? '#4CAF50' : '#F44336';
const itemPrice = this.mcs_tr_getItemPrice(itemHrid);
const actualValue = actual * itemPrice;
const valueFormatted = actualValue > 0 ? this.mcs_tr_formatNumber(actualValue) : '';
html += `
<div class="mcs-tr-loot-row" title="${itemName}: ${actual} actual @ ${this.mcs_tr_formatNumber(itemPrice)} ${priceLabel} = ${valueFormatted}">
${itemIconHtml}
<span class="mcs-tr-loot-count">
${this.mcs_tr_formatNumber(actual)}
</span>
<span class="mcs-tr-loot-value" style="color: ${priceColor};">
${valueFormatted}
</span>
<span class="mcs-tr-loot-diff" style="color: ${diffColor};">(${percentDiff})</span>
</div>
`;
}
return html;
}
mcs_tr_renderExpectedColumn(expectedForOne, allItems, totalOpened = 0) {
let html = '';
const useAskPrice = typeof window.getFlootUseAskPrice === 'function' ? window.getFlootUseAskPrice() : false;
const priceColor = useAskPrice ? '#6495ED' : '#4CAF50';
const priceLabel = useAskPrice ? 'ask' : 'bid';
for (const itemHrid of allItems) {
const expectedOne = expectedForOne[itemHrid] ?? 0;
const expectedTotal = expectedOne * totalOpened;
const itemIconHtml = this.mcs_tr_getItemIconHtml(itemHrid, 16);
const itemName = this.mcs_tr_formatItemName(itemHrid);
const itemPrice = this.mcs_tr_getItemPrice(itemHrid);
const expectedValueOne = expectedOne * itemPrice;
const expectedValueTotal = expectedTotal * itemPrice;
const valueOneFormatted = expectedValueOne > 0 ? this.mcs_tr_formatNumber(expectedValueOne) : '';
const valueTotalFormatted = expectedValueTotal > 0 ? this.mcs_tr_formatNumber(expectedValueTotal) : '';
if (totalOpened > 0) {
html += `
<div class="mcs-tr-expected-row" title="${itemName}: x1=${this.mcs_tr_formatExpectedCount(expectedOne)}, x${totalOpened}=${this.mcs_tr_formatExpectedCount(expectedTotal)} @ ${this.mcs_tr_formatNumber(itemPrice)} ${priceLabel}">
${itemIconHtml}
<span class="mcs-tr-expected-count">
${this.mcs_tr_formatExpectedCount(expectedOne)}
</span>
<span class="mcs-tr-expected-value" style="color: ${priceColor};">
${valueOneFormatted}
</span>
<span class="mcs-tr-expected-sep">|</span>
<span class="mcs-tr-expected-count">
${this.mcs_tr_formatExpectedCount(expectedTotal)}
</span>
<span class="mcs-tr-expected-value" style="color: ${priceColor};">
${valueTotalFormatted}
</span>
</div>
`;
} else {
html += `
<div class="mcs-tr-expected-row-single" title="${itemName}: ${this.mcs_tr_formatExpectedCount(expectedOne)} expected @ ${this.mcs_tr_formatNumber(itemPrice)} ${priceLabel}">
${itemIconHtml}
<span class="mcs-tr-expected-count-single">
${this.mcs_tr_formatExpectedCount(expectedOne)}
</span>
<span class="mcs-tr-expected-value-single" style="color: ${priceColor};">
${valueOneFormatted}
</span>
</div>
`;
}
}
return html;
}
destroyTReasure() {
if (this._trDragMove) {
document.removeEventListener('mousemove', this._trDragMove);
document.removeEventListener('mouseup', this._trDragUp);
this._trDragMove = null;
this._trDragUp = null;
}
if (this._trResizeObserver) { this._trResizeObserver.disconnect(); this._trResizeObserver = null; }
if (this._trModalObserver) { this._trModalObserver.disconnect(); this._trModalObserver = null; }
if (this._trWsListener) { window.removeEventListener('EquipSpyWebSocketMessage', this._trWsListener); this._trWsListener = null; }
if (this._trStorageListener) { window.removeEventListener('storage', this._trStorageListener); this._trStorageListener = null; }
if (this._trClickListener) { document.removeEventListener('click', this._trClickListener, true); this._trClickListener = null; }
if (this._trFlootPricesListener) { window.removeEventListener('FlootPricesUpdated', this._trFlootPricesListener); this._trFlootPricesListener = null; }
const pane = document.getElementById('treasure-pane');
if (pane) pane.remove();
}
// TReasure end
// FLoot start
spy() {
const spyPane = document.getElementById('equipment-spy-pane');
if (spyPane) {
const isHiding = spyPane.style.display !== 'none';
spyPane.style.display = isHiding ? 'none' : 'flex';
if (isHiding) {
if (this.purchaseTimerIntervals) {
Object.keys(this.purchaseTimerIntervals).forEach(slot => {
clearInterval(this.purchaseTimerIntervals[slot]);
});
this.purchaseTimerIntervals = {};
}
VisibilityManager.clear('floot-coin-header');
VisibilityManager.clear('floot-market-refresh');
VisibilityManager.clear('floot-profit-cost');
this.coinHeaderInterval = null;
this.spyMarketRefreshInterval = null;
this.profitCostUpdateInterval = null;
} else {
if (!this.spyMarketRefreshInterval) {
VisibilityManager.register('floot-market-refresh', async () => {
if (this.spyIsInteracting) {
return;
}
await this.loadSpyMarketData();
this.updateLastKnownPrices();
this.updateSpyDisplay();
}, 10 * 60 * 1000);
this.spyMarketRefreshInterval = true;
}
if (!this.coinHeaderInterval) {
VisibilityManager.register('floot-coin-header', () => this.updateCoinHeader(), 2000);
this.coinHeaderInterval = true;
}
if (!this.profitCostUpdateInterval) {
VisibilityManager.register('floot-profit-cost', () => {
if (this.spyIsInteracting) {
return;
}
this.updateProfitCostDisplay();
}, 5000);
this.profitCostUpdateInterval = true;
}
this.updateLockedTimers();
}
} else {
const hasCharacterData = CharacterDataStorage.getCurrentCharacterName();
const hasClientData = InitClientDataCache.get();
if (hasCharacterData && hasClientData) {
this.createEquipmentSpy();
} else {
console.warn('[Floot] Cannot create equipment spy - waiting for character/client data');
setTimeout(() => this.spy(), 1000);
}
}
}
clearHistory() {
if (!this.userName) {
console.warn("LDT: Cannot clear history - no username");
return;
}
if (window.confirm(`Are you sure you want to clear your session history for '${this.userName}'? Sessions involving only other players will remain. This cannot be undone.`)) {
let currentHistory = readSessionHistory();
const originalLength = currentHistory.length;
const filteredHistory = currentHistory.filter(s => !s.key.split('@')[0].split(',').includes(this.userName));
const removedCount = originalLength - filteredHistory.length;
if (removedCount > 0) {
writeSessionHistory(filteredHistory);
this.sessionHistory = filteredHistory;
currentHistory.forEach(session => {
if (session.key.split('@')[0].split(',').includes(this.userName)) {
clearSessionPriceCache(session.key);
}
});
this.aggregatedHistoryData = null;
this.updateHistoryDropdown();
if (!this.viewingLive) {
const selectedValue = this.domRefs.historySelect?.value;
if (selectedValue !== 'live' && selectedValue !== 'combined') {
if (!this.sessionHistory.some(s => s.key === selectedValue)) {
if (this.domRefs.historySelect) this.domRefs.historySelect.value = 'live';
this.handleHistorySelectionChange();
}
} else if (selectedValue === 'combined') {
this.aggregatedHistoryData = null;
if (!this.isHidden) this.renderCurrentView();
if (this.sessionHistory.length === 0) {
const combinedOption = this.domRefs.historySelect?.querySelector('option[value="combined"]');
if (combinedOption) combinedOption.remove();
if (this.domRefs.historySelect) this.domRefs.historySelect.value = 'live';
this.handleHistorySelectionChange();
}
}
} else {
if (!this.isHidden) this.renderCurrentView();
}
}
}
}
handleHistorySelectionChange() {
if (!this.domRefs.historySelect) return;
const selectedValue = this.domRefs.historySelect.value;
const wasViewingLive = this.viewingLive;
const newViewingLive = (selectedValue === 'live');
let viewCategoryChanged = false;
if (newViewingLive !== wasViewingLive) {
viewCategoryChanged = true;
}
this.viewingLive = newViewingLive;
this.aggregatedHistoryData = null;
if (viewCategoryChanged) {
if (this.viewingLive) {
if (this.isLiveSessionActive) this.startTimer();
} else {
this.stopTimer();
}
}
this.updateTimerDisplay();
if (!this.isHidden) {
this.renderCurrentView();
}
}
isViewingCombined() {
return !this.viewingLive && this.domRefs.historySelect?.value === 'combined';
}
getCurrentHistoryViewItem() {
if (this.viewingLive || this.isViewingCombined() || !this.userName) return null;
const selectedValue = this.domRefs.historySelect?.value;
if (!selectedValue || selectedValue === 'live' || selectedValue === 'combined') return null;
const session = this.sessionHistory.find(s => s.key === selectedValue);
if (session && session.key.split('@')[0].split(',').includes(this.userName)) {
return session;
}
return null;
}
handleClickOutsideMenu(event) {
if (!this.isSettingsMenuVisible) return;
const isClickInsideMenu = this.domRefs.settingsMenu?.contains(event.target);
const isClickOnButton = this.domRefs.settingsButton?.contains(event.target);
if (!isClickInsideMenu && !isClickOnButton) {
this.hideSettingsMenu();
}
}
toggleSettingsMenu() {
if (!this.domRefs.settingsMenu) return;
this.isSettingsMenuVisible = !this.isSettingsMenuVisible;
this.domRefs.settingsMenu.classList.toggle('visible', this.isSettingsMenuVisible);
}
hideSettingsMenu() {
if (this.domRefs.settingsMenu) {
this.domRefs.settingsMenu.classList.remove('visible');
this.isSettingsMenuVisible = false;
}
}
handleStartHiddenChange() {
if (!this.domRefs.startHiddenCheckbox) return;
this.startHiddenEnabled = this.domRefs.startHiddenCheckbox.checked;
flStorage.set('start_hidden', this.startHiddenEnabled);
}
showTooltip(event) {
const targetButton = event.currentTarget;
let tooltipText = targetButton.dataset.tooltip;
if (targetButton === this.domRefs.clearButton) {
tooltipText = `Clear history for '${this.userName || '?'}'`;
targetButton.dataset.tooltip = tooltipText;
} else if (targetButton === this.domRefs.settingsButton && !tooltipText) {
tooltipText = "Settings";
targetButton.dataset.tooltip = tooltipText;
}
const tooltipElement = this.domRefs.tooltip;
if (!tooltipText || !tooltipElement) return;
tooltipElement.textContent = tooltipText;
const btnRect = targetButton.getBoundingClientRect();
tooltipElement.style.visibility = 'hidden';
tooltipElement.style.display = 'block';
const tooltipRect = tooltipElement.getBoundingClientRect();
tooltipElement.style.display = '';
tooltipElement.style.visibility = '';
let top = btnRect.top - tooltipRect.height - 6;
let left = btnRect.left + (btnRect.width / 2) - (tooltipRect.width / 2);
if (top < 0) top = btnRect.bottom + 6;
if (left < 5) left = 5;
if (left + tooltipRect.width > window.innerWidth - 5) {
left = window.innerWidth - tooltipRect.width - 5;
}
tooltipElement.style.top = `${top + window.scrollY}px`;
tooltipElement.style.left = `${left + window.scrollX}px`;
tooltipElement.classList.add('visible');
}
hideTooltip() {
const tooltipElement = this.domRefs.tooltip;
if (tooltipElement) tooltipElement.classList.remove('visible');
}
cycleSortPreference() {
this.sortPreference = (this.sortPreference === 'count') ? 'name' : 'count';
flStorage.set('sort', this.sortPreference);
this.renderCurrentView();
this.hideTooltip();
}
processBattleUpdate(data) {
if (!data || typeof data !== 'object' || !Array.isArray(data.players) || !data.combatStartTime) {
return;
}
if (data.battleId && typeof data.battleId === 'number') {
this.encounterCount = data.battleId;
}
if (!this.userName) this.findUserName();
const playerNames = data.players.map(p => p?.name).filter(Boolean);
const combatStartTimeString = data.combatStartTime;
const newSessionKey = generateSessionKey(playerNames, combatStartTimeString);
if (!newSessionKey) {
return;
}
let needsRender = false;
let statsUpdated = false;
if (newSessionKey !== this.currentSessionKey) {
if (this.isLiveSessionActive) {
this.saveCurrentSessionToHistory(this.lastKnownActionHrid);
}
let currentHistory = readSessionHistory();
const existingSession = currentHistory.find(s => s.key === newSessionKey);
this.playerDropStats = {};
this.currentSessionKey = newSessionKey;
this.isLiveSessionActive = true;
this.sessionEndTime = null;
this.lastKnownActionHrid = null;
window.MCS_IN_COMBAT = true;
this.aggregatedHistoryData = null;
this.sessionStartTime = Date.now();
this.firstBattleSeenTime = Date.now();
this.lastBattleTimestamp = Date.now();
try {
this.startTime = new Date(combatStartTimeString);
} catch (e) {
this.startTime = new Date();
}
statsUpdated = this.updatePlayerStatsFromEvent(data);
this.updateTimerDisplay();
this.startTimer();
if (!this.viewingLive) {
this.viewingLive = true;
if (this.domRefs.historySelect) this.domRefs.historySelect.value = 'live';
this.handleHistorySelectionChange();
} else {
needsRender = true;
}
} else if (this.isLiveSessionActive) {
statsUpdated = this.updatePlayerStatsFromEvent(data);
if (statsUpdated) {
this.aggregatedHistoryData = null;
}
if (!this.timerInterval) this.startTimer();
if (this.viewingLive && statsUpdated && !this.isHidden) {
needsRender = true;
}
}
if (needsRender && !this.isHidden) {
if (this.viewingLive || this.isViewingCombined()) {
this.renderCurrentView();
}
}
}
updatePlayerStatsFromEvent(data) {
let updated = false;
data.players.forEach(playerInfo => {
if (playerInfo?.name && typeof playerInfo.totalLootMap === 'object') {
this.updatePlayerStats(playerInfo);
updated = true;
}
});
return updated;
}
handleSessionEnd(detail) {
const actionHrid = detail?.actionHrid;
if (!this.isLiveSessionActive) {
return;
}
this.sessionEndTime = Date.now();
this.isLiveSessionActive = false;
this.lastKnownActionHrid = actionHrid;
window.MCS_IN_COMBAT = false;
this.stopTimer();
this.aggregatedHistoryData = null;
this.saveCurrentSessionToHistory(actionHrid);
if (this.viewingLive) {
if (!this.isHidden) {
this.renderCurrentView();
this.updateTimerDisplay();
}
} else if (this.isViewingCombined()) {
if (!this.isHidden) this.renderCurrentView();
this.updateTimerDisplay();
}
}
saveCurrentSessionToHistory(actionHrid) {
if (!this.currentSessionKey || !this.startTime || !this.userName || !this.playerDropStats[this.userName]) {
return;
}
const userItems = this.playerDropStats[this.userName].items || {};
if (Object.keys(userItems).length === 0) {
return;
}
const endTime = this.sessionEndTime || Date.now();
const duration = Math.max(0, endTime - this.startTime.getTime());
if (duration < 5000 && !actionHrid?.includes('tutorial')) {
return;
}
let currentHistory = readSessionHistory();
const existingSessionIndex = currentHistory.findIndex(s => s.key === this.currentSessionKey);
if (existingSessionIndex === -1) {
try {
const sessionLocation = formatLocationName(actionHrid);
const statsToSave = JSON.parse(JSON.stringify(this.playerDropStats));
const completedSession = {
key: this.currentSessionKey,
start: this.startTime.getTime(),
end: endTime,
duration: duration,
location: sessionLocation,
stats: statsToSave,
encounterCount: this.encounterCount || 0
};
currentHistory.push(completedSession);
currentHistory.sort((a, b) => b.start - a.start);
while (currentHistory.length > CONFIG.HISTORY_LIMIT) {
currentHistory.pop();
}
writeSessionHistory(currentHistory);
this.sessionHistory = currentHistory;
this.updateHistoryDropdown();
} catch (e) {
console.error("LDT: Error creating or saving session JSON", e);
}
} else {
try {
const statsToSave = JSON.parse(JSON.stringify(this.playerDropStats));
currentHistory[existingSessionIndex].end = endTime;
currentHistory[existingSessionIndex].duration = duration;
currentHistory[existingSessionIndex].stats = statsToSave;
currentHistory[existingSessionIndex].encounterCount = this.encounterCount || 0;
writeSessionHistory(currentHistory);
this.sessionHistory = currentHistory;
} catch (e) {
console.error("LDT: Error updating session JSON", e);
}
}
}
handleStorageChange(event) {
if (this.ignoreNextStorageEvent) {
this.ignoreNextStorageEvent = false;
return;
}
if (event.key === this.spyConfig?.STORAGE_LOCKED_KEY) {
return;
}
if (event.key === CONFIG.STORAGE.sessionHistory && event.newValue !== null) {
const oldSelectedKey = this.domRefs.historySelect?.value;
this.sessionHistory = readSessionHistory();
this.aggregatedHistoryData = null;
this.updateHistoryDropdown();
if (!this.viewingLive && !this.isViewingCombined()) {
const currentViewStillExists = this.sessionHistory.some(s => s.key === oldSelectedKey);
if (!currentViewStillExists) {
if (this.domRefs.historySelect) this.domRefs.historySelect.value = 'live';
this.handleHistorySelectionChange();
}
} else if (this.isViewingCombined()) {
if (!this.isHidden) this.renderCurrentView();
this.updateTimerDisplay();
}
}
}
startTimer() {
this.stopTimer();
if (this.startTime && !this.isLiveSessionActive && !this.sessionEndTime) {
console.warn('[Timer] startTimer called in zombie state - cleaning up');
this.startTime = null;
}
if (!this.startTime || !this.viewingLive || !this.isLiveSessionActive) {
this.updateTimerDisplay();
return;
}
this.updateTimerDisplay();
VisibilityManager.register('floot-timer', () => {
if (this.startTime && this.viewingLive && this.isLiveSessionActive) {
this.updateTimerDisplay();
} else {
this.stopTimer();
this.updateTimerDisplay();
}
}, 1000);
}
stopTimer() {
VisibilityManager.clear('floot-timer');
this.timerInterval = null;
}
updateTimerDisplay() {
if (this.isHidden && !this.domRefs.timerDisplayHidden) return;
if (!this.isHidden && !this.domRefs.timerDisplay) return;
const selectedValue = this.domRefs.historySelect?.value;
let timerText = '--:--:--';
if (this.viewingLive || selectedValue === 'live') {
if (!this.startTime) {
timerText = '--:--:--';
} else if (!this.isLiveSessionActive && this.sessionEndTime) {
const duration = this.sessionEndTime - this.startTime.getTime();
timerText = `(${this.formatElapsedTime(duration)}) Ended`;
} else if (this.isLiveSessionActive) {
const elapsedMs = Date.now() - this.startTime.getTime();
timerText = this.formatElapsedTime(elapsedMs);
} else {
console.warn('[Timer] Detected zombie state - resetting startTime');
this.startTime = null;
this.sessionEndTime = null;
timerText = '--:--:--';
}
} else if (selectedValue === 'combined') {
if (this.aggregatedHistoryData === null) this.aggregateSessionHistory();
timerText = `(Σ ${this.formatElapsedTime(this.aggregatedHistoryDuration)})`;
} else {
const selectedSession = this.getCurrentHistoryViewItem();
if (selectedSession) {
timerText = `(${this.formatElapsedTime(selectedSession.duration)})`;
} else {
timerText = '??:??:??';
}
}
let displayText = timerText;
if (this.isLiveSessionActive && this.sessionStartTime && (this.viewingLive || selectedValue === 'live' || !selectedValue)) {
const eph = this.calculateEPH();
displayText = `${timerText} | ${eph} EPH`;
} else if (selectedValue && selectedValue !== 'live' && selectedValue !== 'combined') {
const selectedSession = this.getCurrentHistoryViewItem();
if (selectedSession && selectedSession.encounterCount && selectedSession.duration) {
const elapsedSeconds = selectedSession.duration / 1000;
if (elapsedSeconds > 0) {
const eph = (selectedSession.encounterCount / elapsedSeconds) * 3600;
displayText = `${timerText} | ${eph.toFixed(2)} EPH`;
}
}
}
if (this.domRefs.timerDisplay) {
this.domRefs.timerDisplay.textContent = displayText;
}
if (this.domRefs.timerDisplayHidden) {
this.domRefs.timerDisplayHidden.textContent = displayText;
}
}
formatElapsedTime(ms) {
return mcsFormatDuration((ms || 0) / 1000, 'elapsed');
}
calculateEPH() {
if (!this.startTime || this.encounterCount === 0) {
return '0.00';
}
const now = Date.now();
const elapsedMs = now - this.startTime.getTime();
const elapsedSeconds = elapsedMs / 1000;
if (elapsedSeconds === 0) {
return '0.00';
}
const eph = (this.encounterCount / elapsedSeconds) * 3600;
return eph.toFixed(2);
}
startMove(event) {
if (this.isMoving) return;
this.isMoving = true;
const panelRect = this.domRefs.panel.getBoundingClientRect();
this.moveOffset.y = event.clientY - panelRect.top;
const winWidth = window.innerWidth;
const winHeight = window.innerHeight;
const snapThreshold = 5;
let wasSnapped = false;
let unsnapRight = this.initialRight;
if (panelRect.right >= winWidth - snapThreshold) {
wasSnapped = true;
unsnapRight = 20;
}
else if (panelRect.left <= snapThreshold) {
wasSnapped = true;
const newLeft = 20;
unsnapRight = winWidth - newLeft - panelRect.width;
}
if (panelRect.top <= snapThreshold) {
wasSnapped = true;
this.domRefs.panel.style.top = '20px';
this.moveOffset.y = event.clientY - 20;
}
else if (panelRect.bottom >= winHeight - snapThreshold) {
wasSnapped = true;
const newTop = winHeight - panelRect.height - 20;
this.domRefs.panel.style.top = newTop + 'px';
this.moveOffset.y = event.clientY - newTop;
}
if (wasSnapped) {
this.domRefs.panel.style.right = unsnapRight + 'px';
this.domRefs.panel.style.left = 'auto';
}
this.initialRight = parseFloat(this.domRefs.panel.style.right || CONFIG.DEFAULT_POS.right);
this.initialClientX = event.clientX;
this.domRefs.panel.style.cursor = 'grabbing';
this.domRefs.panel.style.transition = 'none';
this.domRefs.panel.style.opacity = '0.88';
this.domRefs.panel.style.willChange = 'top, right';
event.preventDefault();
event.stopPropagation();
this.hideSettingsMenu();
if (!this._boundMovePanel) {
this._boundMovePanel = this.movePanel.bind(this);
this._boundEndMove = this.endMove.bind(this);
}
document.addEventListener('mousemove', this._boundMovePanel, { passive: true });
document.addEventListener('mouseup', this._boundEndMove);
}
movePanel(event) {
if (!this.isMoving) return;
const container = this.domRefs.panel;
let newTop = event.clientY - this.moveOffset.y;
const dx = event.clientX - this.initialClientX;
let newRight = this.initialRight - dx;
const winWidth = window.innerWidth;
const winHeight = window.innerHeight;
const rect = container.getBoundingClientRect();
const overlayWidth = rect.width;
const overlayHeight = rect.height;
newTop = Math.max(0, Math.min(newTop, Math.max(0, winHeight - overlayHeight)));
newRight = Math.max(0, Math.min(newRight, Math.max(0, winWidth - overlayWidth)));
container.style.top = `${newTop}px`;
container.style.right = `${newRight}px`;
container.style.left = 'auto';
}
endMove() {
if (!this.isMoving) return;
this.isMoving = false;
document.removeEventListener('mousemove', this._boundMovePanel);
document.removeEventListener('mouseup', this._boundEndMove);
this.domRefs.panel.style.cursor = '';
this.domRefs.panel.style.transition = '';
this.domRefs.panel.style.opacity = '';
this.domRefs.panel.style.willChange = '';
const finalTop = this.domRefs.panel.style.top;
const finalRight = this.domRefs.panel.style.right;
if (finalTop && finalTop.endsWith('px') && finalRight && finalRight.endsWith('px')) {
const position = { top: finalTop, right: finalRight };
flStorage.set('position', position);
} else {
console.warn("LDT: Invalid final position values not saved.", { top: finalTop, right: finalRight });
}
}
findUserName() {
if (this.userName) return;
const attemptFind = () => {
const nameDiv = document.querySelector(CONFIG.USERNAME_SELECTOR);
if (nameDiv && nameDiv.dataset.name) {
this.userName = nameDiv.dataset.name;
if (this.spyConfig) {
this.loadSpySettings();
}
this.updateHistoryDropdown();
if (!this.isHidden) this.renderCurrentView();
this.aggregateHistoryData = null;
} else {
setTimeout(attemptFind, 2000);
}
};
attemptFind();
}
extractItemDetailMapFromPage() {
try {
const data = InitClientDataCache.get();
if (data && data.itemDetailMap) {
const bridge = document.getElementById('equipspy-data-bridge');
if (bridge) {
bridge.setAttribute('data-item-detail-map', JSON.stringify(data.itemDetailMap));
} else {
console.error('[EquipSpy] Bridge element not found');
}
}
} catch (e) {
console.error('[EquipSpy] Error extracting itemDetailMap:', e);
}
}
setupFeedListener() {
if (window._lootDropsMessageHooked) {
return;
}
try {
const script = document.createElement('script');
script.textContent = `
(function() {
if (window._lootDropsMessageHooked) return;
window._lootDropsMessageHooked = true;
console.log('[MCS] WebSocket connected');
if (!window.CharacterDataStorage) {
window.CharacterDataStorage = {
getCurrentCharacterName() {
if (window.MCS_CHARACTER_DATA_CACHE?.character?.name) {
return window.MCS_CHARACTER_DATA_CACHE.character.name;
}
return null;
},
get(characterName) {
return window.MCS_CHARACTER_DATA_CACHE || null;
},
set(value, characterName) {
if (typeof value === 'string') {
try {
window.MCS_CHARACTER_DATA_CACHE = JSON.parse(value);
} catch (e) {
console.error('[CharacterDataStorage] Error parsing data:', e);
}
} else if (value && typeof value === 'object') {
window.MCS_CHARACTER_DATA_CACHE = value;
}
},
getParsed(characterName) {
return window.MCS_CHARACTER_DATA_CACHE || null;
},
setParsed(obj, characterName) {
window.MCS_CHARACTER_DATA_CACHE = obj;
}
};
}
const CharacterDataStorage = window.CharacterDataStorage;
function updateCacheSynchronously(parsed) {
try {
if (parsed?.type === 'init_character_data') {
if (parsed.characterItems) {
CharacterDataStorage.set(parsed);
const bridge = document.getElementById('equipspy-data-bridge');
if (bridge) {
try {
bridge.setAttribute('data-character-items', JSON.stringify(parsed.characterItems));
bridge.setAttribute('data-character-full', JSON.stringify(parsed));
bridge.setAttribute('data-character-name', parsed.character?.name || '');
} catch (e) {
console.error('[EquipSpy] Error setting bridge attribute:', e);
}
}
if (window.lootDropsTrackerInstance) {
window.lootDropsTrackerInstance.spyCharacterItems = parsed.characterItems;
}
}
if (parsed.myMarketListings) {
if (window.lootDropsTrackerInstance) {
window.lootDropsTrackerInstance.myMarketListings = parsed.myMarketListings;
}
}
}
} catch (e) {
console.error('[LootDrops] Error in synchronous cache update:', e);
}
}
function processMessage(parsed) {
try {
const tabHidden = document.hidden;
let marketListingsUpdated = false;
let updatedListings = null;
if (parsed?.type === 'market_listings_updated' && parsed.endMarketListings) {
let listings = window.lootDropsTrackerInstance?.myMarketListings;
if (!listings) {
const cachedData = CharacterDataStorage.get();
listings = cachedData?.myMarketListings ?? [];
}
listings = [...listings];
for (const updatedListing of parsed.endMarketListings) {
const existingIndex = listings.findIndex(l => l.id === updatedListing.id);
const isCancelled = updatedListing.status === '/market_listing_status/cancelled';
const isFullyFilled = updatedListing.filledQuantity >= updatedListing.orderQuantity;
if (isCancelled || isFullyFilled) {
if (existingIndex >= 0) {
listings.splice(existingIndex, 1);
}
} else if (existingIndex >= 0) {
listings[existingIndex] = updatedListing;
} else {
listings.push(updatedListing);
}
}
if (window.lootDropsTrackerInstance) {
window.lootDropsTrackerInstance.myMarketListings = listings;
}
const cachedData = CharacterDataStorage.get();
if (cachedData) {
cachedData.myMarketListings = listings;
}
if (parsed.endCharacterItems && Array.isArray(parsed.endCharacterItems)) {
itemsToUpdate = parsed.endCharacterItems;
if (window.lootDropsTrackerInstance) {
window.lootDropsTrackerInstance.spyCharacterItems = itemsToUpdate;
}
const cachedData2 = CharacterDataStorage.get();
if (cachedData2) {
cachedData2.characterItems = itemsToUpdate;
}
}
marketListingsUpdated = true;
updatedListings = listings;
}
let coinCount = null;
if (parsed?.type === 'action_completed') {
if (parsed.endCharacterItems && Array.isArray(parsed.endCharacterItems)) {
const coinItem = parsed.endCharacterItems.find(item => item.itemHrid === '/items/coin');
if (coinItem) {
coinCount = coinItem.count;
const cachedData = CharacterDataStorage.get();
if (cachedData && cachedData.characterItems) {
const storedCoin = cachedData.characterItems.find(
item => item.itemHrid === '/items/coin'
);
if (storedCoin) {
storedCoin.count = coinItem.count;
}
}
}
}
}
let itemsToUpdate = null;
if (parsed && typeof parsed === 'object') {
if (parsed.characterItems && Array.isArray(parsed.characterItems)) {
itemsToUpdate = parsed.characterItems;
} else if (parsed.items && Array.isArray(parsed.items)) {
itemsToUpdate = parsed.items;
} else if (parsed.characterItemsMap && typeof parsed.characterItemsMap === 'object') {
itemsToUpdate = Object.values(parsed.characterItemsMap);
} else if (parsed.character?.items && Array.isArray(parsed.character.items)) {
itemsToUpdate = parsed.character.items;
}
if (itemsToUpdate && itemsToUpdate.length > 0) {
if (window.lootDropsTrackerInstance) {
window.lootDropsTrackerInstance.spyCharacterItems = itemsToUpdate;
}
}
if (parsed.type && (
parsed.type === 'item_update' ||
parsed.type === 'inventory_update' ||
parsed.type === 'character_update' ||
parsed.type === 'coins_update' ||
parsed.type === 'actions_updated'
)) {
const searchForCoins = (obj, depth = 0) => {
if (depth > 5) return null;
for (const key in obj) {
const val = obj[key];
if (typeof val === 'object' && val !== null) {
if (val.itemHrid === '/items/coin' && typeof val.count === 'number') {
return val.count;
}
const result = searchForCoins(val, depth + 1);
if (result !== null) return result;
}
}
return null;
};
const coins = searchForCoins(parsed);
if (coins !== null) {
if (window.lootDropsTrackerInstance && window.lootDropsTrackerInstance.spyCharacterItems) {
let ci = window.lootDropsTrackerInstance.spyCharacterItems.find(
item => item.itemHrid === '/items/coin'
);
if (ci) {
ci.count = coins;
} else {
window.lootDropsTrackerInstance.spyCharacterItems.push({
itemHrid: '/items/coin',
count: coins
});
}
}
}
const needsCacheUpdate = coins !== null || (itemsToUpdate && itemsToUpdate.length > 0);
if (needsCacheUpdate) {
const cachedData = CharacterDataStorage.get();
if (cachedData && cachedData.characterItems) {
if (itemsToUpdate && itemsToUpdate.length > 0) {
cachedData.characterItems = itemsToUpdate;
const crackInventory = {};
itemsToUpdate.forEach(item => {
if (item.itemLocationHrid === '/item_locations/inventory' && item.itemHrid) {
const itemName = item.itemHrid.toLowerCase();
const isConsumable = itemName.includes('coffee') || itemName.includes('donut') ||
itemName.includes('cupcake') || itemName.includes('cake') ||
itemName.includes('gummy') || itemName.includes('yogurt');
if (isConsumable) {
crackInventory[item.itemHrid] = item.count;
}
}
});
if (Object.keys(crackInventory).length > 0) {
const crStorage = createModuleStorage('CR');
crStorage.set('consumable_inventory', crackInventory);
}
if (window.lootDropsTrackerInstance) {
window.lootDropsTrackerInstance.spyCharacterItems = itemsToUpdate;
}
} else if (coins !== null) {
let storedCoin = cachedData.characterItems.find(
item => item.itemHrid === '/items/coin'
);
if (storedCoin) {
storedCoin.count = coins;
} else {
cachedData.characterItems.push({
itemHrid: '/items/coin',
itemLocationHrid: '/item_locations/inventory',
count: coins
});
}
}
}
}
}
}
if (parsed?.type === 'init_character_data' && parsed.characterItems) {
window.dispatchEvent(new CustomEvent('LootTrackerCharacterData', { detail: parsed }));
}
if (parsed?.type === 'init_client_data' && parsed.itemDetailMap) {
window.dispatchEvent(new CustomEvent('LootTrackerClientData', { detail: parsed }));
}
if (tabHidden) {
if (parsed?.type === 'init_character_data' || parsed?.type === 'init_client_data') {
window._mcsPendingInitMessages = window._mcsPendingInitMessages || [];
window._mcsPendingInitMessages.push(parsed);
}
return;
}
if (marketListingsUpdated) {
window.dispatchEvent(new CustomEvent('EquipSpyWebSocketMessage', {
detail: { type: 'market_listings_updated', myMarketListings: updatedListings }
}));
}
if (coinCount !== null) {
window.dispatchEvent(new CustomEvent('EquipSpyCoinUpdate', {
detail: { count: coinCount }
}));
}
if (parsed?.type === 'new_battle' &&
Array.isArray(parsed.players) &&
parsed.combatStartTime) {
window.dispatchEvent(
new CustomEvent('LootTrackerBattle', {
detail: parsed
})
);
}
if (itemsToUpdate && itemsToUpdate.length > 0) {
const bridge = document.getElementById('equipspy-data-bridge');
if (bridge) {
try {
bridge.setAttribute('data-character-items', JSON.stringify(itemsToUpdate));
} catch (e) { /* stringify may fail on circular refs */ }
}
}
if (parsed && typeof parsed === 'object' && parsed.type && (
parsed.type === 'item_update' ||
parsed.type === 'inventory_update' ||
parsed.type === 'character_update' ||
parsed.type === 'coins_update' ||
parsed.type === 'actions_updated'
)) {
const cachedData = CharacterDataStorage.get();
const coins = cachedData?.characterItems?.find(i => i.itemHrid === '/items/coin')?.count;
if (coins !== null || (itemsToUpdate && itemsToUpdate.length > 0)) {
window.dispatchEvent(new CustomEvent('InventoryDataUpdated', {
detail: {
items: itemsToUpdate || window.lootDropsTrackerInstance?.spyCharacterItems || [],
type: parsed.type
}
}));
}
}
if (parsed?.type) {
window.dispatchEvent(new CustomEvent('EquipSpyWebSocketMessage', {
detail: parsed
}));
}
let combatEnded = false;
let completedActionHrid = null;
if (parsed?.type === 'actions_updated' &&
Array.isArray(parsed.endCharacterActions)) {
const completedCombatAction = parsed.endCharacterActions.find(
action => action?.actionHrid?.startsWith('/actions/combat/') &&
action.isDone === true &&
action.currentCount > 0
);
if (completedCombatAction) {
combatEnded = true;
completedActionHrid = completedCombatAction.actionHrid;
}
} else if (parsed?.type === 'cancel_character_action' &&
parsed?.cancelCharacterActionData?.characterActionId) {
combatEnded = true;
completedActionHrid = parsed?.cancelCharacterActionData?.actionHrid || null;
}
if (combatEnded) {
window.dispatchEvent(
new CustomEvent('LootTrackerCombatEnded', {
detail: { actionHrid: completedActionHrid }
})
);
}
} catch (e) {
if (!(e instanceof SyntaxError)) {
console.error('[LootDrops] Error processing message:', e);
}
}
}
const originalDescriptor = Object.getOwnPropertyDescriptor(
MessageEvent.prototype,
'data'
);
const originalGetter = originalDescriptor.get;
Object.defineProperty(MessageEvent.prototype, 'data', {
get: function() {
const data = originalGetter.call(this);
const socket = this.currentTarget;
if (socket instanceof WebSocket &&
socket.url &&
socket.url.includes('milkywayidle.com')) {
if (window._MCS_DISABLED) return data;
if (typeof data === 'string' && data.startsWith('{')) {
try {
const parsed = JSON.parse(data);
if (parsed && parsed.type) {
updateCacheSynchronously(parsed);
queueMicrotask(() => processMessage(parsed));
}
} catch (e) {
}
}
}
return data;
},
configurable: true
});
let recoveryCheckCount = 0;
const recoveryInterval = setInterval(() => {
recoveryCheckCount++;
if (recoveryCheckCount > 16) {
clearInterval(recoveryInterval);
return;
}
if (window.MCS_CHARACTER_DATA_CACHE) {
clearInterval(recoveryInterval);
return;
}
const nameEl = document.querySelector('[data-name]');
if (nameEl && nameEl.getAttribute('data-name')) {
clearInterval(recoveryInterval);
const reloadKey = 'mcs_init_recovery_reload';
if (!sessionStorage.getItem(reloadKey)) {
sessionStorage.setItem(reloadKey, '1');
console.log('[MCS] Missed init_character_data (late injection after update) — reloading once to recover.');
window.location.reload();
}
}
}, 500);
document.addEventListener('visibilitychange', () => {
if (!document.hidden && window._mcsPendingInitMessages?.length) {
const pending = window._mcsPendingInitMessages;
window._mcsPendingInitMessages = [];
const lastByType = new Map();
pending.forEach(msg => lastByType.set(msg.type, msg));
const deduped = Array.from(lastByType.values());
deduped.forEach((msg, i) => {
setTimeout(() => {
window.dispatchEvent(new CustomEvent('EquipSpyWebSocketMessage', { detail: msg }));
}, i * 50);
});
}
});
})();
`;
(document.head || document.documentElement).appendChild(script);
setTimeout(() => {
if (script.parentNode) {
script.parentNode.removeChild(script);
}
}, 100);
} catch (error) {
console.error('[LootDrops] Failed to setup message interceptor:', error);
}
}
// FLoot end
}
// Class end
// Lucky start
const LuckyGameData = {
itemNames: {},
itemPrices: {},
monsterNames: {},
mapNames: {},
mapData: {},
playerStats: {},
currentMapHrid: null,
currentDifficultyTier: 0,
hasReceivedFirstBattle: false
};
const FlootData = {
getBattleCount() {
if (!window.lootDropsTrackerInstance) return 0;
const instance = window.lootDropsTrackerInstance;
const storedCount = instance.encounterCount || 0;
return storedCount > 0 ? storedCount : 1;
},
getEPH() {
if (!window.lootDropsTrackerInstance) return 0;
const eph = window.lootDropsTrackerInstance.calculateEPH();
return parseFloat(eph) || 0;
},
getSessionTime() {
if (!window.lootDropsTrackerInstance) return 0;
if (!window.lootDropsTrackerInstance.startTime) return 0;
const startTime = window.lootDropsTrackerInstance.startTime;
const elapsedMs = Date.now() - startTime.getTime();
return elapsedMs / 1000;
},
getPlayerDrops(playerName) {
if (!window.lootDropsTrackerInstance) return {};
const playerStats = window.lootDropsTrackerInstance.playerDropStats;
if (!playerStats || !playerStats[playerName]) return {};
return playerStats[playerName].items || {};
},
getCurrentPlayerName() {
if (!window.lootDropsTrackerInstance) return null;
return window.lootDropsTrackerInstance.userName;
},
getAllPlayerNames() {
if (!window.lootDropsTrackerInstance) return [];
const playerStats = window.lootDropsTrackerInstance.playerDropStats;
if (!playerStats) return [];
return Object.keys(playerStats);
},
getPartySize() {
return this.getAllPlayerNames().length;
}
};
const Complex = new class {
add = (a, b) => [a[0] + b[0], a[1] + b[1]]
sub = (a, b) => [a[0] - b[0], a[1] - b[1]]
mul = (a, b) => [a[0] * b[0] - a[1] * b[1], a[0] * b[1] + a[1] * b[0]]
mulRe = (a, x) => [a[0] * x, a[1] * x]
div = (a, b) => {
const mag = b[0] * b[0] + b[1] * b[1];
return [(a[0] * b[0] + a[1] * b[1]) / mag, (a[1] * b[0] - a[0] * b[1]) / mag];
}
abs = (c) => Math.sqrt(c[0] * c[0] + c[1] * c[1])
pow = (c, x) => {
const arg = Math.atan2(c[1], c[0]) * x;
const mag = Math.pow(c[0] * c[0] + c[1] * c[1], x / 2);
return [mag * Math.cos(arg), mag * Math.sin(arg)];
}
};
const ComplexVector = new class {
constantRe(n, a) {
const v = Array(n);
for (let i = 0; i < n; i += 4) {
v[i] = [a, 0]; v[i + 1] = [a, 0]; v[i + 2] = [a, 0]; v[i + 3] = [a, 0];
}
return v;
}
mul(a, b) {
const n = a.length, z = Array(n);
for (let i = 0; i < n;) {
z[i] = [a[i][0] * b[i][0] - a[i][1] * b[i][1], a[i][0] * b[i][1] + a[i][1] * b[i][0]]; ++i;
z[i] = [a[i][0] * b[i][0] - a[i][1] * b[i][1], a[i][0] * b[i][1] + a[i][1] * b[i][0]]; ++i;
z[i] = [a[i][0] * b[i][0] - a[i][1] * b[i][1], a[i][0] * b[i][1] + a[i][1] * b[i][0]]; ++i;
z[i] = [a[i][0] * b[i][0] - a[i][1] * b[i][1], a[i][0] * b[i][1] + a[i][1] * b[i][0]]; ++i;
}
return z;
}
mulEq(a, b) {
const n = a.length;
for (let i = 0; i < n;) {
a[i] = [a[i][0] * b[i][0] - a[i][1] * b[i][1], a[i][0] * b[i][1] + a[i][1] * b[i][0]]; ++i;
a[i] = [a[i][0] * b[i][0] - a[i][1] * b[i][1], a[i][0] * b[i][1] + a[i][1] * b[i][0]]; ++i;
a[i] = [a[i][0] * b[i][0] - a[i][1] * b[i][1], a[i][0] * b[i][1] + a[i][1] * b[i][0]]; ++i;
a[i] = [a[i][0] * b[i][0] - a[i][1] * b[i][1], a[i][0] * b[i][1] + a[i][1] * b[i][0]]; ++i;
}
return a;
}
mulReEq(a, x) {
const n = a.length;
for (let i = 0; i < n;) {
a[i][0] *= x; a[i][1] *= x; ++i;
a[i][0] *= x; a[i][1] *= x; ++i;
a[i][0] *= x; a[i][1] *= x; ++i;
a[i][0] *= x; a[i][1] *= x; ++i;
}
return a;
}
addEq(a, b) {
const n = a.length;
for (let i = 0; i < n;) {
a[i][0] += b[i][0]; a[i][1] += b[i][1]; ++i;
a[i][0] += b[i][0]; a[i][1] += b[i][1]; ++i;
a[i][0] += b[i][0]; a[i][1] += b[i][1]; ++i;
a[i][0] += b[i][0]; a[i][1] += b[i][1]; ++i;
}
return a;
}
addMulEq(dest, a, b) {
const n = dest.length;
for (let i = 0; i < n;) {
dest[i][0] += a[i][0] * b[i][0] - a[i][1] * b[i][1]; dest[i][1] += a[i][0] * b[i][1] + a[i][1] * b[i][0]; ++i;
dest[i][0] += a[i][0] * b[i][0] - a[i][1] * b[i][1]; dest[i][1] += a[i][0] * b[i][1] + a[i][1] * b[i][0]; ++i;
dest[i][0] += a[i][0] * b[i][0] - a[i][1] * b[i][1]; dest[i][1] += a[i][0] * b[i][1] + a[i][1] * b[i][0]; ++i;
dest[i][0] += a[i][0] * b[i][0] - a[i][1] * b[i][1]; dest[i][1] += a[i][0] * b[i][1] + a[i][1] * b[i][0]; ++i;
}
return a;
}
};
const CDFUtils = new class {
#inf = 0x3FFFFFFE;
floor(n) { return n > this.#inf || n < -this.#inf ? Math.floor(n) : ((n + this.#inf) | 0) - this.#inf; }
round(n) { return this.floor(n + 0.5); }
binarySearch(f, l, r, dest, maxIter = 60) {
for (let i = 0; i < maxIter; ++i) {
let mid = (l + r) / 2;
if (f(mid) < dest) l = mid;
else r = mid;
}
return (l + r) / 2;
}
};
const SimpleFFT = new class {
#cache = {};
init(n) {
if (this.#cache[n]) return;
this.#cache[n] = {
cos: new Array(n),
sin: new Array(n)
};
for (let i = 0; i < n; i++) {
const angle = -2 * Math.PI * i / n;
this.#cache[n].cos[i] = Math.cos(angle);
this.#cache[n].sin[i] = Math.sin(angle);
}
}
fft(re, im) {
const n = re.length;
if (n <= 1) return;
for (let i = 0, j = 0; i < n; i++) {
if (i < j) {
[re[i], re[j]] = [re[j], re[i]];
[im[i], im[j]] = [im[j], im[i]];
}
let k = n >> 1;
while (k > 0 && k <= j) {
j -= k;
k >>= 1;
}
j += k;
}
for (let len = 2; len <= n; len <<= 1) {
const halfLen = len >> 1;
const angle = -2 * Math.PI / len;
const wlenRe = Math.cos(angle);
const wlenIm = Math.sin(angle);
for (let i = 0; i < n; i += len) {
let wRe = 1;
let wIm = 0;
for (let j = 0; j < halfLen; j++) {
const tRe = wRe * re[i + j + halfLen] - wIm * im[i + j + halfLen];
const tIm = wRe * im[i + j + halfLen] + wIm * re[i + j + halfLen];
re[i + j + halfLen] = re[i + j] - tRe;
im[i + j + halfLen] = im[i + j] - tIm;
re[i + j] += tRe;
im[i + j] += tIm;
const nextWRe = wRe * wlenRe - wIm * wlenIm;
wIm = wRe * wlenIm + wIm * wlenRe;
wRe = nextWRe;
}
}
}
}
};
const CDFConfig = {
charaFunc: {
verbose: false,
cdfIterSpeed: 0.9,
cdfLimitEps: 1e-4,
cdfMaxIter: 30,
cdfEps: 1e-4,
cdfWrapping: 0.4,
rescaleSamples: 64,
samples: (typeof window !== 'undefined' && window.innerWidth < 768) ? 512 : 4096
}
};
const CharaFunc = new class {
getRoots(a, samples) {
let sin = Array(samples), cos = Array(samples);
sin[0] = 0; cos[0] = 1;
sin[1] = Math.sin(a); cos[1] = Math.cos(a);
sin[2] = sin[1] * cos[1] + cos[1] * sin[1]; cos[2] = cos[1] * cos[1] - sin[1] * sin[1];
sin[3] = sin[1] * cos[2] + cos[1] * sin[2]; cos[3] = cos[1] * cos[2] - sin[1] * sin[2];
for (let i = 4; i < samples; i += 4) {
const j = CDFUtils.floor(i / 2), k = i - j;
sin[i] = sin[j] * cos[k] + cos[j] * sin[k]; cos[i] = cos[j] * cos[k] - sin[j] * sin[k];
sin[i + 1] = sin[j] * cos[k + 1] + cos[j] * sin[k + 1]; cos[i + 1] = cos[j] * cos[k + 1] - sin[j] * sin[k + 1];
sin[i + 2] = sin[j + 1] * cos[k + 1] + cos[j + 1] * sin[k + 1]; cos[i + 2] = cos[j + 1] * cos[k + 1] - sin[j + 1] * sin[k + 1];
sin[i + 3] = sin[j + 1] * cos[k + 2] + cos[j + 1] * sin[k + 2]; cos[i + 3] = cos[j + 1] * cos[k + 2] - sin[j + 1] * sin[k + 2];
}
return [cos, sin];
}
constant(x) {
return (samples, _) => ComplexVector.constantRe(samples, x);
}
mul(cf1, cf2) {
return (samples, scale) => {
const z = cf1(samples, scale);
const y = cf2(samples, scale);
ComplexVector.mulEq(z, y);
return z;
};
}
mulList(cfs) {
if (cfs.length === 0) return this.constant(1);
return (samples, scale) => {
let z = cfs[0](samples, scale);
for (let i = 1; i < cfs.length; ++i) {
const y = cfs[i](samples, scale);
ComplexVector.mulEq(z, y);
}
return z;
};
}
pow(cf, n) {
return (samples, scale) => {
let z = cf(samples, scale);
for (let T = 0; T < samples; ++T) z[T] = Complex.pow(z[T], n);
return z;
};
}
getScaledCDF(cf, samples, scale) {
const padding = 2;
const offset = CDFConfig.charaFunc.cdfWrapping;
const N = samples * padding;
const val = cf(samples, scale * (1 - offset))
.concat(Array(N - samples).fill([0, 0]));
let re = val.map(a => a[0]);
let im = val.map(a => a[1]);
const hasNaN = re.some(x => !isFinite(x)) || im.some(x => !isFinite(x));
if (hasNaN) {
throw new Error("CF produced invalid values");
}
SimpleFFT.init(N);
SimpleFFT.fft(re, im);
re = re.map(a => a - 0.5);
const sum = re.reduce((acc, x) => acc + x, 0);
if (Math.abs(sum) < 1e-10) {
throw new Error("FFT output sum is zero");
}
re = re.map(a => a / sum);
let cdf = Array(N);
cdf[0] = (re[0] + re[N - 1]) / 2;
for (let i = 1; i < N; ++i) {
cdf[i] = cdf[i - 1] + (re[i] + re[i - 1]) / 2;
}
const movingMedian = (a, siz) => {
const n = a.length;
let b = Array(n);
for (let i = 0; i < n; ++i) {
let w = [];
for (let j = i - siz + 1; j <= i + siz; ++j) {
const p = a[(j + n) % n];
const x = j < 0 ? p - 1 : j >= n ? p + 1 : p;
w.push(x);
}
for (let i = 0; i <= siz; ++i) {
for (let j = i + 1; j < w.length; ++j) {
if (w[i] > w[j]) { const t = w[i]; w[i] = w[j]; w[j] = t; }
}
}
b[i] = (w[siz - 1] + w[siz]) / 2;
}
return b;
}
cdf = movingMedian(cdf, padding);
let base = cdf[CDFUtils.floor(N * (1 - offset))] - 1;
for (let i = 0; i < N; ++i) cdf[i] -= base;
for (let i = 1; i < N; ++i) if (cdf[i] < cdf[i - 1]) cdf[i] = cdf[i - 1];
const interpolate = (acc, x) => {
if (x < 0) return 0;
if (x >= 1) return 1;
const t = x * (1 - offset) * N - 0.5;
const i = CDFUtils.round(t), r = t - i;
const L = i - 1 < 0 ? acc[i + N - 1] - 1 : acc[i - 1];
const R = i + 1 >= N ? acc[i - N + 1] + 1 : acc[i + 1];
const A = (acc[i] + L) / 2, B = (acc[i] + R) / 2;
const kA = acc[i] - L, kB = R - acc[i];
const ret = 2 * (r + 1) * (r - 0.5) * (r - 0.5) * A
+ 2 * (1 - r) * (r + 0.5) * (r + 0.5) * B
+ (r * r - 0.25) * ((r - 0.5) * kA + (r + 0.5) * kB);
return ret < 0 ? 0 : ret > 1 ? 1 : ret;
};
return (x) => interpolate(cdf, x);
}
getCDF(cf, samples, limit = 1e8, rescaleSamples = null) {
const eps = CDFConfig.charaFunc.cdfEps;
const speed = CDFConfig.charaFunc.cdfIterSpeed;
const maxIter = CDFConfig.charaFunc.cdfMaxIter;
rescaleSamples = rescaleSamples || CDFConfig.charaFunc.rescaleSamples;
for (let i = 0; i < maxIter; ++i) {
let cdf = this.getScaledCDF(cf, rescaleSamples, 1 / limit);
if (cdf(speed) < 1 - eps) {
break;
}
const x = CDFUtils.binarySearch(cdf, 0, 1, 1 - eps);
if (x / speed > 1 - CDFConfig.charaFunc.cdfLimitEps) {
break;
}
limit *= x / speed;
}
let cdf = this.getScaledCDF(cf, samples, 1 / limit);
return {
limit: limit,
cdf: (x) => cdf(x / limit),
};
}
};
const CDFDropAnalyzer = new class {
charaFunc(data) {
const { minCount: l, maxCount: r, dropRate, price } = data;
const eps = 1e-8;
const L = Math.ceil(l);
const R = CDFUtils.floor(r);
if (L > R || r - l < eps) {
const p = (l + r) / 2 - R;
const pr = p * dropRate;
const mpr = (1 - p) * dropRate;
const mr = 1 - dropRate;
return (samples, scale) => {
let val = Array(samples);
const base = 2 * Math.PI * scale * price;
const [cosR1, sinR1] = CharaFunc.getRoots(base * (R + 1), samples);
const [cosR, sinR] = CharaFunc.getRoots(base * R, samples);
for (let T = 0; T < samples; ++T) {
val[T] = [
cosR1[T] * pr + cosR[T] * mpr + mr,
sinR1[T] * pr + sinR[T] * mpr
]
}
return val;
};
}
if (L == R) {
const pL = dropRate * (L - l) * (L - l) / ((r - l) * 2);
const pR = dropRate * (r - R) * (r - R) / ((r - l) * 2);
const mr = 1 - dropRate;
return (samples, scale) => {
let val = Array(samples);
const base = 2 * Math.PI * scale * price;
const [cos, sin] = CharaFunc.getRoots(base, samples);
const [cosR, sinR] = CharaFunc.getRoots(base * R, samples);
for (let T = 0; T < samples; ++T) {
const a = [dropRate + (pL + pR) * (cos[T] - 1), (-pL + pR) * sin[T]];
val[T] = Complex.mul([cosR[T], sinR[T]], a);
val[T][0] += mr;
}
return val;
};
}
const dL = L - l, dR = r - R;
const dL2 = dL * dL, dR2 = dR * dR;
const mr = 1 - dropRate;
const invLen = dropRate / (r - l);
return (samples, scale) => {
let val = Array(samples);
const base = 2 * Math.PI * scale * price;
const [cos, sin] = CharaFunc.getRoots(base, samples);
const [cosR, sinR] = CharaFunc.getRoots(base * R, samples);
const [cosL, sinL] = CharaFunc.getRoots(base * L, samples);
for (let T = 0; T < samples; ++T) {
const ctm1d2 = (cos[T] - 1) / 2, std2 = sin[T] / 2;
const elt = [cosL[T], sinL[T]];
const ert = [cosR[T], sinR[T]];
const fL = Complex.mul([dL + dL2 * ctm1d2, -dL2 * std2], elt);
const fR = Complex.mul([dR + dR2 * ctm1d2, dR2 * std2], ert)
const irwin = ctm1d2 > -eps && std2 < eps && std2 > -eps ?
[(R - L) * elt[0], (R - L) * (elt[1] + std2 * (R - L - 1))] :
Complex.div([ert[0] - elt[0], ert[1] - elt[1]], [ctm1d2 * 2, std2 * 2]);
const fMid = Complex.mul(irwin, [1 + ctm1d2, std2]);
val[T] = [mr + invLen * (fL[0] + fR[0] + fMid[0]), invLen * (fL[1] + fR[1] + fMid[1])];
}
return val;
};
}
};
const RuckBattleDropAnalyzer = new class {
#monsterCF(monsterDrops) {
const cfs = [];
for (const drop of monsterDrops) {
cfs.push(CDFDropAnalyzer.charaFunc(drop));
}
return CharaFunc.mulList(cfs);
}
#getSpawnTransGraph(spawnInfo) {
const { spawns, maxSpawnCount: K, maxTotalStrength: N } = spawnInfo;
const idMap = {};
const nodes = [];
const hasId = (i, j) => { return idMap.hasOwnProperty(i * (K + 1) + j); };
const getId = (i, j) => {
const h = i * (K + 1) + j;
if (!hasId(i, j)) {
idMap[h] = nodes.length;
nodes.push({ init: 0, edges: [] });
}
return idMap[h];
};
getId(0, 0);
for (let i = 0; i <= N; ++i) {
for (let j = 0; j <= K; ++j) {
if (!hasId(i, j)) continue;
const id = getId(i, j);
for (const monster of spawns) {
const ni = i + monster.strength, nj = j + 1;
if (ni > N || nj > K) {
nodes[id].init += monster.rate;
continue;
}
const monsterHrid = monster.combatMonsterHrid || monster.hrid;
nodes[id].edges.push({
to: getId(ni, nj),
hrid: monsterHrid,
});
}
}
}
return nodes;
}
#normalWaveCF(spawnInfo, monsterDrops) {
const spawns = spawnInfo.spawns;
const cfs = {};
for (const monster of spawns) {
const monsterHrid = monster.combatMonsterHrid || monster.hrid;
const drops = monsterDrops[monsterHrid] || [];
cfs[monsterHrid] = this.#monsterCF(drops);
}
const transGraph = this.#getSpawnTransGraph(spawnInfo);
return (samples, scale) => {
const cfTab = {};
for (const monster of spawns) {
const monsterHrid = monster.combatMonsterHrid || monster.hrid;
const z = cfs[monsterHrid](samples, scale);
ComplexVector.mulReEq(z, monster.rate);
cfTab[monsterHrid] = z;
}
const val = Array(transGraph.length);
for (let u = transGraph.length - 1; u >= 0; --u) {
val[u] = ComplexVector.constantRe(samples, transGraph[u].init);
for (const e of transGraph[u].edges) {
ComplexVector.addMulEq(val[u], val[e.to], cfTab[e.hrid]);
}
}
return val[0];
};
}
battleCF(dropData) {
const normalCF = this.#normalWaveCF(dropData.spawnInfo, dropData.monsterDrops);
const bossCF = CharaFunc.mulList(
Object.values(dropData.bossDrops).map(m => this.#monsterCF(m)));
return CharaFunc.mul(
CharaFunc.pow(normalCF, dropData.normalCount),
CharaFunc.pow(bossCF, dropData.bossCount)
);
}
battleCDF(dropData) {
const samples = CDFConfig.charaFunc.samples;
const cf = RuckBattleDropAnalyzer.battleCF(dropData);
let cdf;
const minLimit = 1e8;
const dungeonDrop = dropData.bossDrops?.['_dungeon']?.[0];
if (!dungeonDrop) {
const perWaveLimit = 2e5;
const limit = Math.max(minLimit, perWaveLimit * (dropData.bossCount + dropData.normalCount));
cdf = CharaFunc.getCDF(cf, samples, limit);
} else {
const chestPrice = LuckyUtils.getItemValue(dungeonDrop.hrid, 1);
const epoch = dropData.bossCount;
const count = (dungeonDrop.minCount + dungeonDrop.maxCount) / 2;
const baseCount = Math.floor(count);
const basePrice = chestPrice * baseCount * epoch;
const limit = Math.max(samples, epoch);
const decCDF = CharaFunc.getCDF(CharaFunc.pow(CDFDropAnalyzer.charaFunc({
hrid: dungeonDrop.hrid,
minCount: count - baseCount,
maxCount: count - baseCount,
dropRate: 1,
price: 1,
}), epoch), samples, limit);
cdf = {
limit: decCDF.limit * chestPrice + basePrice,
cdf: (x) => {
const chestCount = (x - basePrice) / chestPrice;
return decCDF.cdf(chestCount + 16 / samples);
},
};
}
return cdf;
}
};
const RuckBattleData = new class {
mapData = {};
playerStat = {};
playerList = [];
playerLoot = {};
currentMapHrid = null;
difficultyTier = 0;
runCount = 0;
getDropData(mapHrid, runCount = 11, playerName = null) {
const mapData = this.mapData[mapHrid];
if (!mapData) return null;
const bossWave = mapData.spawnInfo.bossWave;
const bossCount = bossWave ? Math.floor((runCount - 1) / bossWave) : 0;
const normalCount = bossWave ? bossCount * (bossWave - 1) + (runCount - 1) % bossWave : runCount - 1;
const dropData = {
spawnInfo: mapData.spawnInfo,
bossCount: bossCount,
normalCount: normalCount,
bossDrops: {},
monsterDrops: {},
};
const processDrop = (item) => {
const itemPrice = LuckyUtils.getItemValue(item.itemHrid, 1);
let { minCount, maxCount, dropRate } = item;
const dropRatePerTier = item.dropRatePerDifficultyTier || 0;
if (playerName) {
const playerStat = this.playerStat[playerName];
if (playerStat) {
const commonRateMultiplier = 1 + (playerStat.combatDropRate || 0);
const rareRateMultiplier = 1 + (playerStat.combatRareFind || 0);
const quantityMultiplier = (1 + (playerStat.combatDropQuantity || 0)) / this.playerList.length * (mapData.type === 'dungeon' ? 5 : 1);
const rateMultiplier = item.isRare ? rareRateMultiplier : commonRateMultiplier;
minCount *= quantityMultiplier;
maxCount *= quantityMultiplier;
const len = mapData.type === 'dungeon'? 3 : (mapData.type === 'group'? 6 : 1);
dropRate = Array.from({length: len}, (_, n) => {
let rate = dropRate + n * dropRatePerTier;
rate = rate * (1 + n * 0.1) * rateMultiplier;
return Math.min(Math.max(rate, 0), 1);
});
}
}
return {
hrid: item.itemHrid,
price: itemPrice,
minCount: minCount,
maxCount: maxCount,
dropRate: dropRate,
};
};
for (let [hrid, drops] of Object.entries(mapData.bossDrops || {})){
dropData.bossDrops[hrid] = drops.map(drop => processDrop(drop));}
for (let [hrid, drops] of Object.entries(mapData.monsterDrops || {})){
dropData.monsterDrops[hrid] = drops.map(drop => processDrop(drop));}
return dropData;
}
getDropDataDifficulty(mapHrid, runCount = 11, playerName = null) {
let dropData = this.getDropData(mapHrid, runCount, playerName);
if (!dropData) return null;
for (let [hrid, drops] of Object.entries(dropData.bossDrops)) {
dropData.bossDrops[hrid] = drops.map(drop => {
const newDropRate = drop.dropRate?.[this.difficultyTier];
return { ...drop, dropRate: newDropRate };
});
}
for (let [hrid, drops] of Object.entries(dropData.monsterDrops)) {
dropData.monsterDrops[hrid] = drops.map(drop => {
const newDropRate = drop.dropRate?.[this.difficultyTier];
return { ...drop, dropRate: newDropRate };
});
}
return dropData
}
getCurrentDropData(playerName = null) {
if (!this.currentMapHrid) return null;
return this.getDropDataDifficulty(this.currentMapHrid, this.runCount, playerName);
}
syncFromLucky() {
this.mapData = LuckyGameData.mapData;
this.currentMapHrid = LuckyGameData.currentMapHrid;
this.difficultyTier = LuckyGameData.currentDifficultyTier;
this.runCount = FlootData.getBattleCount();
this.playerList = Object.keys(LuckyGameData.playerStats);
for (const playerName of this.playerList) {
const stats = LuckyGameData.playerStats[playerName];
this.playerStat[playerName] = {
combatDropQuantity: stats.combatDropQuantity || 0,
combatDropRate: stats.combatDropRate || 0,
combatRareFind: stats.combatRareFind || 0,
};
}
for (const playerName of this.playerList) {
const drops = FlootData.getPlayerDrops(playerName);
const items = [];
for (const [hrid, count] of Object.entries(drops)) {
items.push({ hrid, count });
}
this.playerLoot[playerName] = {
items,
price: () => items.reduce((total, item) => {
const price = LuckyUtils.getItemValue(item.hrid, 1);
return total + item.count * price;
}, 0)
};
}
}
};
const RuckAnalyzeCurrent = (playerName) => {
RuckBattleData.syncFromLucky();
const dropData = RuckBattleData.getCurrentDropData(playerName);
if (!dropData) return null;
const income = RuckBattleData.playerLoot[playerName]?.price() || 0;
const luck = RuckBattleDropAnalyzer.battleCDF(dropData).cdf(income);
return { luck: luck, income: income, dropData: dropData };
};
const LuckyDropAnalyzer = {
spawnCache: {},
itemCountExpt(drop, tier) {
let baseDropRate;
if (typeof drop.dropRate === 'number') {
baseDropRate = drop.dropRate;
} else if (Array.isArray(drop.dropRate)) {
baseDropRate = drop.dropRate[tier] || drop.dropRate[0];
return baseDropRate * (drop.minCount + drop.maxCount) / 2;
} else {
baseDropRate = 0;
}
const dropRatePerTier = drop.dropRatePerDifficultyTier || 0;
let adjustedDropRate = baseDropRate + tier * dropRatePerTier;
adjustedDropRate = adjustedDropRate * (1 + tier * 0.1);
adjustedDropRate = Math.min(Math.max(adjustedDropRate, 0), 1);
const expected = adjustedDropRate * (drop.minCount + drop.maxCount) / 2;
return expected;
},
getTierDropRate(drop, tier) {
let baseDropRate;
if (typeof drop.dropRate === 'number') {
baseDropRate = drop.dropRate;
} else if (Array.isArray(drop.dropRate)) {
return drop.dropRate[tier] || drop.dropRate[0];
} else {
return 0;
}
const dropRatePerTier = drop.dropRatePerDifficultyTier || 0;
let adjustedDropRate = baseDropRate + tier * dropRatePerTier;
adjustedDropRate = adjustedDropRate * (1 + tier * 0.1);
adjustedDropRate = Math.min(Math.max(adjustedDropRate, 0), 1);
return adjustedDropRate;
},
computeExpectedSpawns(spawnInfo) {
const cacheKey = LuckyGameData.currentMapHrid;
if (this.spawnCache[cacheKey]) {
return this.spawnCache[cacheKey];
}
const spawns = spawnInfo.spawns;
const maxSpawnCount = spawnInfo.maxSpawnCount;
const maxTotalStrength = spawnInfo.maxTotalStrength;
const res = {};
spawns.forEach(m => { res[m.combatMonsterHrid] = 0; });
const dp = [];
for (let i = 0; i <= maxTotalStrength; i++) {
dp[i] = new Array(maxSpawnCount + 1).fill(0);
}
dp[0][0] = 1;
for (let i = 0; i <= maxTotalStrength; i++) {
for (let j = 0; j <= maxSpawnCount; j++) {
if (dp[i][j] === 0) continue;
for (const monster of spawns) {
const ni = i + monster.strength;
const nj = j + 1;
if (ni > maxTotalStrength || nj > maxSpawnCount) continue;
const val = dp[i][j] * monster.rate;
dp[ni][nj] += val;
res[monster.combatMonsterHrid] += val;
}
}
}
this.spawnCache[cacheKey] = res;
return res;
},
calculateDropsCommon(mapHrid, runCount, difficultyTier, calcFunc) {
const mapData = LuckyGameData.mapData[mapHrid];
if (!mapData) {
console.warn('[Lucky] No map data found for:', mapHrid, 'Available maps:', Object.keys(LuckyGameData.mapData));
return {};
}
const spawnInfo = mapData.spawnInfo;
let bossWave = spawnInfo.bossWave || 0;
if (!bossWave && mapData.type === 'dungeon') {
bossWave = 1;
} else if (!bossWave && mapData.type === 'group' && mapData.bossDrops && Object.keys(mapData.bossDrops).length > 0) {
bossWave = 10;
}
const bossCount = bossWave ? Math.floor((runCount - 1) / bossWave) : 0;
const normalCount = bossWave ?
bossCount * (bossWave - 1) + (runCount - 1) % bossWave :
runCount - 1;
const expectedSpawns = this.computeExpectedSpawns(spawnInfo);
const drops = {};
if (mapData.bossDrops) {
for (const [monsterHrid, monsterDrops] of Object.entries(mapData.bossDrops)) {
for (const drop of monsterDrops) {
const value = calcFunc(drop, difficultyTier, bossCount);
if (!drops[drop.itemHrid]) {
drops[drop.itemHrid] = { count: 0, tierDropRate: this.getTierDropRate(drop, difficultyTier), isRare: drop.isRare || false };
}
drops[drop.itemHrid].count += value;
}
}
}
if (mapData.monsterDrops) {
for (const [monsterHrid, monsterDrops] of Object.entries(mapData.monsterDrops)) {
const spawnCount = expectedSpawns[monsterHrid] || 0;
for (const drop of monsterDrops) {
const value = calcFunc(drop, difficultyTier, spawnCount * normalCount);
if (!drops[drop.itemHrid]) {
drops[drop.itemHrid] = { count: 0, tierDropRate: this.getTierDropRate(drop, difficultyTier), isRare: drop.isRare || false };
}
drops[drop.itemHrid].count += value;
}
}
}
return drops;
},
calculateExpectedDrops(mapHrid, runCount, difficultyTier, partySize) {
return this.calculateDropsCommon(mapHrid, runCount, difficultyTier, (drop, tier, count) => {
const itemExpt = this.itemCountExpt(drop, tier);
return count * itemExpt;
});
},
calculateMaximumDrops(mapHrid, runCount, difficultyTier, partySize) {
return this.calculateDropsCommon(mapHrid, runCount, difficultyTier, (drop, tier, count) => {
const adjustedDropRate = this.getTierDropRate(drop, tier);
return count * adjustedDropRate * drop.maxCount;
});
}
};
const LuckyUtils = {
formatNumber(num) {
if (num >= 1000000) {
return (num / 1000000).toFixed(2) + 'M';
}
if (num >= 1000) {
return (num / 1000).toFixed(1) + 'K';
}
if (num >= 10) {
return num.toFixed(1);
}
if (num >= 1) {
return num.toFixed(2);
}
if (num > 0) {
return num.toFixed(3);
}
return '0';
},
getPercentClass(percent, expected100Mode = false) {
if (expected100Mode) {
if (percent > 105) return 'value-positive';
if (percent < 95) return 'value-negative';
return 'value-neutral';
} else {
if (percent > 5) return 'value-positive';
if (percent < -5) return 'value-negative';
return 'value-neutral';
}
},
getPercentColor(percent, expected100Mode = false) {
const colorMap = {
'value-positive': '#4ade80',
'value-negative': '#f87171',
'value-neutral': '#a8aed4'
};
return colorMap[this.getPercentClass(percent, expected100Mode)];
},
formatTime(seconds) {
return mcsFormatDuration(seconds, 'clock');
},
getItemName(hrid) {
return mcsFormatHrid(hrid);
},
getItemValue(itemHrid, count = 1) {
if (typeof window.getUnitValue === 'function') {
const unitValue = window.getUnitValue(itemHrid, 'live');
if (unitValue !== null) {
return unitValue * count;
}
}
return 0;
},
HSVtoRGB(h, s, v) {
var r, g, b, i, f, p, q, t;
i = Math.floor(h * 6);
f = h * 6 - i;
p = v * (1 - s);
q = v * (1 - f * s);
t = v * (1 - (1 - f) * s);
switch (i % 6) {
case 0: r = v; g = t; b = p; break;
case 1: r = q; g = v; b = p; break;
case 2: r = p; g = v; b = t; break;
case 3: r = p; g = q; b = v; break;
case 4: r = t; g = p; b = v; break;
case 5: r = v; g = p; b = q; break;
}
r = Math.round(r * 255);
g = Math.round(g * 255);
b = Math.round(b * 255);
return `rgb(${r}, ${g}, ${b})`;
},
getLuckColor(luckPercent) {
const luck = Math.min(Math.max(luckPercent / 100, 0), 1);
const h = luck * 0.34;
const s = 0.9 - luck * 0.25;
const v = 1 - luck * 0.25;
return this.HSVtoRGB(h, s, v);
}
};
let _luckyWsListener = null;
function setupLuckyMessageHandlers() {
try {
const clientData = InitClientDataCache.get();
if (clientData) {
processInitClientData(clientData);
}
} catch (e) {
console.error('[Lucky] Error loading cached data:', e);
}
try {
if (typeof GM_getValue !== 'undefined') {
const storedCharData = GM_getValue("init_character_data", null);
if (storedCharData) {
const charData = JSON.parse(storedCharData);
if (charData.characterActions && charData.characterActions[0]) {
const action = charData.characterActions[0];
if (action.actionHrid) {
if (LuckyGameData.currentMapHrid !== action.actionHrid) {
LuckyDropAnalyzer.spawnCache = {};
}
LuckyGameData.currentMapHrid = action.actionHrid;
LuckyGameData.currentDifficultyTier = action.difficultyTier || 0;
}
}
}
}
} catch (e) {
}
_luckyWsListener = (event) => {
if (window.MCS_MODULES_DISABLED) return;
const msg = event.detail;
if (!msg) return;
if (msg.type === 'init_client_data') {
processInitClientData(msg);
}
if (msg.type === 'init_character_data') {
if (msg.characterActions && msg.characterActions[0]) {
const action = msg.characterActions[0];
if (action.actionHrid) {
if (LuckyGameData.currentMapHrid !== action.actionHrid) {
LuckyDropAnalyzer.spawnCache = {};
}
LuckyGameData.currentMapHrid = action.actionHrid;
LuckyGameData.currentDifficultyTier = action.difficultyTier || 0;
}
}
}
if (msg.type === 'new_battle') {
if (msg.players && msg.players.length > 0) {
LuckyGameData.hasReceivedFirstBattle = true;
for (const player of msg.players) {
if (!player.character) continue;
if (!player.combatDetails || !player.combatDetails.combatStats) continue;
const playerName = player.character.name;
const stats = player.combatDetails.combatStats;
LuckyGameData.playerStats[playerName] = {
combatDropQuantity: stats.combatDropQuantity || 0,
combatDropRate: stats.combatDropRate || 0,
combatRareFind: stats.combatRareFind || 0
};
}
}
}
if (msg.type === 'action_completed') {
if (msg.endCharacterAction && msg.endCharacterAction.actionHrid) {
if (LuckyGameData.currentMapHrid !== msg.endCharacterAction.actionHrid) {
LuckyDropAnalyzer.spawnCache = {};
}
LuckyGameData.currentMapHrid = msg.endCharacterAction.actionHrid;
LuckyGameData.currentDifficultyTier = msg.endCharacterAction.difficultyTier || 0;
}
}
};
window.addEventListener('EquipSpyWebSocketMessage', _luckyWsListener);
}
function processInitClientData(msg) {
if (!msg) return;
if (msg.itemDetailMap) {
for (const [hrid, item] of Object.entries(msg.itemDetailMap)) {
LuckyGameData.itemNames[hrid] = item.name;
LuckyGameData.itemPrices[hrid] = hrid === '/items/coin' ? 1 : (item.sellPrice || 0);
}
}
if (msg.actionDetailMap && msg.combatMonsterDetailMap) {
const monsterMap = msg.combatMonsterDetailMap;
for (const [hrid, monster] of Object.entries(monsterMap)) {
if (monster.name) LuckyGameData.monsterNames[hrid] = monster.name;
}
const actionDetailMap = msg.actionDetailMap;
for (const [actionHrid, actionDetail] of Object.entries(actionDetailMap)) {
if (!actionHrid.startsWith("/actions/combat/")) continue;
if (!actionDetail.combatZoneInfo) continue;
LuckyGameData.mapNames[actionHrid] = actionDetail.name;
if (actionDetail.combatZoneInfo.isDungeon) {
const dungeonInfo = actionDetail.combatZoneInfo.dungeonInfo;
LuckyGameData.mapData[actionHrid] = {
name: actionDetail.name,
type: 'dungeon',
spawnInfo: { bossWave: 1, maxSpawnCount: 0, maxTotalStrength: 0, spawns: [] },
monsterDrops: {},
bossDrops: {
'_dungeon': dungeonInfo.rewardDropTable.map(item => ({ isRare: false, ...item }))
}
};
continue;
}
const fightInfo = actionDetail.combatZoneInfo.fightInfo;
if (!fightInfo) continue;
const spawnInfo = fightInfo.randomSpawnInfo;
if (!spawnInfo || !spawnInfo.spawns || spawnInfo.spawns.length === 0) continue;
const mapType = spawnInfo.spawns.length > 1 || spawnInfo.bossWave > 0 ? "group" : "solo";
const totalRate = spawnInfo.spawns.reduce((s, x) => s + x.rate, 0);
const spawns = spawnInfo.spawns.map(s => ({
combatMonsterHrid: s.combatMonsterHrid,
strength: s.strength,
rate: s.rate / totalRate
}));
const monsterDrops = {};
for (const spawn of spawns) {
const monster = monsterMap[spawn.combatMonsterHrid];
if (!monster) continue;
const drops = [];
if (monster.dropTable) {
monster.dropTable.forEach(drop => {
drops.push({
itemHrid: drop.itemHrid,
dropRate: drop.dropRate,
minCount: drop.minCount,
maxCount: drop.maxCount,
isRare: false,
dropRatePerDifficultyTier: drop.dropRatePerDifficultyTier || 0
});
});
}
if (monster.rareDropTable) {
monster.rareDropTable.forEach(drop => {
drops.push({
itemHrid: drop.itemHrid,
dropRate: drop.dropRate,
minCount: drop.minCount,
maxCount: drop.maxCount,
isRare: true,
dropRatePerDifficultyTier: drop.dropRatePerDifficultyTier || 0
});
});
}
monsterDrops[spawn.combatMonsterHrid] = drops;
}
const bossDrops = {};
if (fightInfo.bossSpawns) {
for (const bossSpawn of fightInfo.bossSpawns) {
const boss = monsterMap[bossSpawn.combatMonsterHrid];
if (!boss) continue;
const drops = [];
if (boss.dropTable) {
boss.dropTable.forEach(drop => {
drops.push({
itemHrid: drop.itemHrid,
dropRate: drop.dropRate,
minCount: drop.minCount,
maxCount: drop.maxCount,
isRare: false,
dropRatePerDifficultyTier: drop.dropRatePerDifficultyTier || 0
});
});
}
if (boss.rareDropTable) {
boss.rareDropTable.forEach(drop => {
drops.push({
itemHrid: drop.itemHrid,
dropRate: drop.dropRate,
minCount: drop.minCount,
maxCount: drop.maxCount,
isRare: true,
dropRatePerDifficultyTier: drop.dropRatePerDifficultyTier || 0
});
});
}
bossDrops[bossSpawn.combatMonsterHrid] = drops;
}
}
LuckyGameData.mapData[actionHrid] = {
name: actionDetail.name,
type: mapType,
spawnInfo: {
maxSpawnCount: spawnInfo.maxSpawnCount,
maxTotalStrength: spawnInfo.maxTotalStrength,
bossWave: fightInfo.battlesPerBoss || 0,
spawns: spawns
},
monsterDrops: monsterDrops,
bossDrops: bossDrops
};
}
}
}
class LuckyPanel {
get lyStorage() {
if (!this._lyStorage) {
this._lyStorage = createModuleStorage('LY');
}
return this._lyStorage;
}
constructor() {
this.panel = null;
this.isDragging = false;
this.dragOffset = { x: 0, y: 0 };
this.updateInterval = null;
this.isLocked = false;
this.expected100Mode = false;
this.snapToGrid = true;
this.panelVisibility = {
stats: true,
revenue: true,
totals: true,
bigExpected: true,
bigLuck: true,
mobExpected: true
};
this.playerPanels = new Map();
this.lastPlayerNames = [];
this.cachedExpectedDrops = null;
this.cachedMaximumDrops = null;
this.lastDropsCacheKey = null;
this.cachedLuckResults = new Map();
this.lastLuckBattleCount = 0;
this.statsElements = null;
}
saveState() {
if (!this.panel) return;
const hasExplicitPosition = this.panel.style.left || this.panel.style.top;
if (!hasExplicitPosition) {
return;
}
const rect = this.panel.getBoundingClientRect();
const left = parseFloat(this.panel.style.left) || rect.left;
const top = parseFloat(this.panel.style.top) || rect.top;
if (left < -500 || top < -500) {
return;
}
const state = {
x: left,
y: top,
width: this.panel.style.width || rect.width + 'px',
height: this.panel.style.height || rect.height + 'px'
};
this.lyStorage.set('panel_state', state);
}
loadState() {
if (!this.panel) {
return null;
}
try {
const state = this.lyStorage.get('panel_state');
if (state) {
if (typeof state.x === 'number' && typeof state.y === 'number') {
this.panel.style.left = state.x + 'px';
this.panel.style.top = state.y + 'px';
this.panel.style.right = 'auto';
}
if (state.width) this.panel.style.width = state.width;
if (state.height) this.panel.style.height = state.height;
return state;
}
} catch (e) {
console.error('[Lucky] Error in loadState:', e);
}
return null;
}
init() {
this.loadPanelSettings();
this.createPanel();
this.applyLockState();
this.loadState();
this.startAutoUpdate();
this.applyDefaultGridIfNeeded();
}
createPanel() {
this.panel = document.createElement('div');
this.panel.className = 'lucky-panel';
this.panel.id = 'lucky-panel';
registerPanel('lucky-panel');
const content = document.createElement('div');
content.className = 'lucky-content';
content.id = 'lucky-content';
for (let i = 0; i < 15; i++) {
const spacer = document.createElement('div');
spacer.className = 'lucky-content-spacer';
spacer.style.top = (i * 100) + 'px';
content.appendChild(spacer);
}
const gridOverlay = document.createElement('div');
gridOverlay.id = 'lucky-grid-overlay';
gridOverlay.className = 'lucky-grid-overlay';
content.appendChild(gridOverlay);
this.createControlIcons(content);
this.panel.appendChild(content);
document.body.appendChild(this.panel);
this.panel.dataset.originalDisplay = 'block';
this.setupResizeHandles(this.panel);
this.createOptionsPanel();
this.setupResizeObserver();
}
setupResizeHandles(element, storageId = null) {
const isMainPanel = (element === this.panel);
const rightHandle = document.createElement('div');
rightHandle.className = 'lucky-resize-handle lucky-resize-handle-right';
element.appendChild(rightHandle);
const bottomHandle = document.createElement('div');
bottomHandle.className = 'lucky-resize-handle lucky-resize-handle-bottom';
element.appendChild(bottomHandle);
const cornerHandle = document.createElement('div');
cornerHandle.className = 'lucky-resize-handle lucky-resize-handle-corner';
element.appendChild(cornerHandle);
this.setupEdgeResize(element, rightHandle, 'right', storageId);
this.setupEdgeResize(element, bottomHandle, 'bottom', storageId);
this.setupEdgeResize(element, cornerHandle, 'corner', storageId);
if (isMainPanel) {
const leftHandle = document.createElement('div');
leftHandle.className = 'lucky-resize-handle lucky-resize-handle-left';
element.appendChild(leftHandle);
const topHandle = document.createElement('div');
topHandle.className = 'lucky-resize-handle lucky-resize-handle-top';
element.appendChild(topHandle);
const cornerTopLeftHandle = document.createElement('div');
cornerTopLeftHandle.className = 'lucky-resize-handle lucky-resize-handle-corner-topleft';
element.appendChild(cornerTopLeftHandle);
const cornerTopRightHandle = document.createElement('div');
cornerTopRightHandle.className = 'lucky-resize-handle lucky-resize-handle-corner-topright';
element.appendChild(cornerTopRightHandle);
const cornerBottomLeftHandle = document.createElement('div');
cornerBottomLeftHandle.className = 'lucky-resize-handle lucky-resize-handle-corner-bottomleft';
element.appendChild(cornerBottomLeftHandle);
this.setupEdgeResize(element, leftHandle, 'left', storageId);
this.setupEdgeResize(element, topHandle, 'top', storageId);
this.setupEdgeResize(element, cornerTopLeftHandle, 'corner-topleft', storageId);
this.setupEdgeResize(element, cornerTopRightHandle, 'corner-topright', storageId);
this.setupEdgeResize(element, cornerBottomLeftHandle, 'corner-bottomleft', storageId);
}
}
_trackDocListener(event, fn) {
if (!this._luckyDocListeners) this._luckyDocListeners = [];
this._luckyDocListeners.push({ event, fn });
}
_cleanupDocListeners() {
if (this._luckyDocListeners) {
for (const { event, fn } of this._luckyDocListeners) {
document.removeEventListener(event, fn);
}
this._luckyDocListeners = null;
}
}
setupEdgeResize(element, handle, type, storageId = null) {
let startX, startY, startWidth, startHeight, startLeft, startTop;
const onResizeMove = (e) => {
const deltaX = e.clientX - startX;
const deltaY = e.clientY - startY;
const minWidth = 50;
const minHeight = 50;
const GRID_SIZE = 10;
if (type === 'right' || type === 'corner' || type === 'corner-topright') {
let newWidth = startWidth + deltaX;
if (this.snapToGrid) newWidth = Math.round(newWidth / GRID_SIZE) * GRID_SIZE;
element.style.width = Math.max(minWidth, newWidth) + 'px';
}
if (type === 'left' || type === 'corner-topleft' || type === 'corner-bottomleft') {
let newWidth = startWidth - deltaX;
if (this.snapToGrid) newWidth = Math.round(newWidth / GRID_SIZE) * GRID_SIZE;
if (newWidth >= minWidth) {
element.style.width = newWidth + 'px';
element.style.left = (startLeft + deltaX) + 'px';
}
}
if (type === 'bottom' || type === 'corner' || type === 'corner-bottomleft') {
let newHeight = startHeight + deltaY;
if (this.snapToGrid) newHeight = Math.round(newHeight / GRID_SIZE) * GRID_SIZE;
element.style.height = Math.max(minHeight, newHeight) + 'px';
}
if (type === 'top' || type === 'corner-topleft' || type === 'corner-topright') {
let newHeight = startHeight - deltaY;
if (this.snapToGrid) newHeight = Math.round(newHeight / GRID_SIZE) * GRID_SIZE;
if (newHeight >= minHeight) {
element.style.height = newHeight + 'px';
element.style.top = (startTop + deltaY) + 'px';
}
}
};
const onResizeUp = () => {
document.removeEventListener('mousemove', onResizeMove);
document.removeEventListener('mouseup', onResizeUp);
if (element === this.panel) {
this.saveState();
} else if (storageId) {
this.savePanelState(element, storageId);
}
};
handle.addEventListener('mousedown', (e) => {
if (this.isLocked) return;
startX = e.clientX;
startY = e.clientY;
startWidth = element.offsetWidth;
startHeight = element.offsetHeight;
startLeft = element.offsetLeft;
startTop = element.offsetTop;
e.preventDefault();
e.stopPropagation();
this._trackDocListener('mousemove', onResizeMove);
this._trackDocListener('mouseup', onResizeUp);
document.addEventListener('mousemove', onResizeMove);
document.addEventListener('mouseup', onResizeUp);
});
}
createControlIcons(content) {
const controls = document.createElement('div');
controls.className = 'lucky-content-controls';
controls.id = 'lucky-content-controls';
const lockIcon = document.createElement('div');
lockIcon.className = 'lucky-control-icon';
lockIcon.id = 'lucky-lock-icon';
lockIcon.innerHTML = '🔓';
lockIcon.title = 'Lock panels';
lockIcon.onclick = () => this.toggleLock();
const optionsIcon = document.createElement('div');
optionsIcon.className = 'lucky-control-icon';
optionsIcon.id = 'lucky-options-icon';
optionsIcon.innerHTML = '⚙';
optionsIcon.title = 'Panel options';
optionsIcon.onclick = () => this.toggleOptions();
const dragIcon = document.createElement('div');
dragIcon.className = 'lucky-control-icon';
dragIcon.id = 'lucky-drag-icon';
dragIcon.innerHTML = '✥';
dragIcon.title = 'Drag to reposition panels';
dragIcon.style.cursor = 'move';
controls.appendChild(lockIcon);
controls.appendChild(optionsIcon);
controls.appendChild(dragIcon);
content.appendChild(controls);
this.setupContentDragging(dragIcon);
}
createOptionsPanel() {
const optionsPanel = document.createElement('div');
optionsPanel.className = 'lucky-options-panel';
optionsPanel.id = 'lucky-options-panel';
const statsChecked = this.panelVisibility.stats !== false ? 'checked' : '';
const revenueChecked = this.panelVisibility.revenue !== false ? 'checked' : '';
const totalsChecked = this.panelVisibility.totals !== false ? 'checked' : '';
const bigExpectedChecked = this.panelVisibility.bigExpected !== false ? 'checked' : '';
const bigLuckChecked = this.panelVisibility.bigLuck !== false ? 'checked' : '';
const mobExpectedChecked = this.panelVisibility.mobExpected !== false ? 'checked' : '';
const expected100Checked = this.expected100Mode ? 'checked' : '';
optionsPanel.innerHTML = `
<div class="lucky-options-title">Panel Visibility</div>
<div id="lucky-options-container" class="lucky-options-container">
<div class="lucky-option-row">
<input type="checkbox" class="lucky-option-checkbox" id="lucky-checkbox-stats" data-panel="stats" ${statsChecked}>
<label for="lucky-checkbox-stats" class="lucky-option-label">Session Statistics</label>
</div>
<div class="lucky-option-row">
<input type="checkbox" class="lucky-option-checkbox" id="lucky-checkbox-revenue" data-panel="revenue" ${revenueChecked}>
<label for="lucky-checkbox-revenue" class="lucky-option-label">Revenue</label>
</div>
<div class="lucky-option-row">
<input type="checkbox" class="lucky-option-checkbox" id="lucky-checkbox-totals" data-panel="totals" ${totalsChecked}>
<label for="lucky-checkbox-totals" class="lucky-option-label">Totals & Expected</label>
</div>
<div class="lucky-option-row">
<input type="checkbox" class="lucky-option-checkbox" id="lucky-checkbox-bigExpected" data-panel="bigExpected" ${bigExpectedChecked}>
<label for="lucky-checkbox-bigExpected" class="lucky-option-label">Expected</label>
</div>
<div class="lucky-option-row">
<input type="checkbox" class="lucky-option-checkbox" id="lucky-checkbox-bigLuck" data-panel="bigLuck" ${bigLuckChecked}>
<label for="lucky-checkbox-bigLuck" class="lucky-option-label">Luck</label>
</div>
<div class="lucky-option-row">
<input type="checkbox" class="lucky-option-checkbox" id="lucky-checkbox-mobExpected" data-panel="mobExpected" ${mobExpectedChecked}>
<label for="lucky-checkbox-mobExpected" class="lucky-option-label">Mob Expected</label>
</div>
<div id="lucky-player-options" style="display: contents;"></div>
</div>
<div class="lucky-options-title lucky-options-title-mt">Display Options</div>
<div class="lucky-option-row">
<input type="checkbox" id="lucky-checkbox-expected100" ${expected100Checked}>
<label for="lucky-checkbox-expected100" class="lucky-option-label">100% for Expected (centers on 100% instead of 0%)</label>
</div>
<div class="lucky-option-row">
<input type="checkbox" id="lucky-checkbox-snap-to-grid" ${this.snapToGrid ? 'checked' : ''}>
<label for="lucky-checkbox-snap-to-grid" class="lucky-option-label">Snap to Grid</label>
</div>
<button id="lucky-reset-panels-btn" class="lucky-reset-btn">Reset Panel Positions</button>
<button id="lucky-auto-grid-btn" class="lucky-auto-grid-btn">Auto Grid (2x4)</button>
`;
document.body.appendChild(optionsPanel);
optionsPanel.querySelectorAll('.lucky-option-checkbox').forEach(checkbox => {
checkbox.addEventListener('change', (e) => {
const panelId = e.target.getAttribute('data-panel');
this.togglePanelVisibility(panelId, e.target.checked);
});
});
const expected100Checkbox = document.getElementById('lucky-checkbox-expected100');
if (expected100Checkbox) {
expected100Checkbox.addEventListener('change', (e) => {
this.expected100Mode = e.target.checked;
this.savePanelSettings();
this.updateContent();
});
}
const snapToGridCheckbox = document.getElementById('lucky-checkbox-snap-to-grid');
if (snapToGridCheckbox) {
snapToGridCheckbox.addEventListener('change', (e) => {
this.snapToGrid = e.target.checked;
this.savePanelSettings();
});
}
const resetBtn = document.getElementById('lucky-reset-panels-btn');
if (resetBtn) {
resetBtn.addEventListener('click', () => {
this.resetPanelPositions();
});
}
const autoGridBtn = document.getElementById('lucky-auto-grid-btn');
if (autoGridBtn) {
autoGridBtn.addEventListener('click', () => {
this.autoGridPanels();
});
}
}
setupContentDragging(dragIcon) {
let startX, startY;
const onDragMove = (e) => {
let newX = e.clientX - startX;
let newY = e.clientY - startY;
const panelRect = this.panel.getBoundingClientRect();
const iconEl = document.getElementById('lucky-drag-icon');
const dragIconRect = iconEl?.getBoundingClientRect();
if (dragIconRect) {
const minX = -dragIconRect.left + panelRect.left;
const minY = -dragIconRect.top + panelRect.top;
const maxX = window.innerWidth - (dragIconRect.left - panelRect.left) - dragIconRect.width;
const maxY = window.innerHeight - (dragIconRect.top - panelRect.top) - dragIconRect.height;
newX = Math.max(minX, Math.min(newX, maxX));
newY = Math.max(minY, Math.min(newY, maxY));
}
this.panel.style.left = newX + 'px';
this.panel.style.top = newY + 'px';
this.panel.style.right = 'auto';
this.updateOptionsPanelPosition();
};
const onDragUp = () => {
document.removeEventListener('mousemove', onDragMove);
document.removeEventListener('mouseup', onDragUp);
dragIcon.style.cursor = 'move';
this.saveState();
};
dragIcon.addEventListener('mousedown', (e) => {
const rect = this.panel.getBoundingClientRect();
startX = e.clientX - rect.left;
startY = e.clientY - rect.top;
dragIcon.style.cursor = 'grabbing';
e.preventDefault();
e.stopPropagation();
this._trackDocListener('mousemove', onDragMove);
this._trackDocListener('mouseup', onDragUp);
document.addEventListener('mousemove', onDragMove);
document.addEventListener('mouseup', onDragUp);
});
}
updateOptionsPanelPosition() {
const optionsPanel = document.getElementById('lucky-options-panel');
if (!optionsPanel || !this.panel) return;
const rect = this.panel.getBoundingClientRect();
const optionsRect = optionsPanel.getBoundingClientRect();
optionsPanel.style.left = rect.left + 'px';
optionsPanel.style.top = (rect.top - optionsRect.height - 4) + 'px';
optionsPanel.style.width = rect.width + 'px';
}
toggleLock() {
this.isLocked = !this.isLocked;
const lockIcon = document.getElementById('lucky-lock-icon');
if (lockIcon) {
lockIcon.innerHTML = this.isLocked ? '🔒' : '🔓';
lockIcon.title = this.isLocked ? 'Unlock panels' : 'Lock panels';
if (this.isLocked) {
lockIcon.classList.add('active');
} else {
lockIcon.classList.remove('active');
}
}
const allPanels = document.querySelectorAll('.lucky-floating-panel');
allPanels.forEach(panel => {
if (this.isLocked) {
panel.style.pointerEvents = 'none';
} else {
panel.style.pointerEvents = 'auto';
}
});
this.savePanelSettings();
}
applyLockState() {
const lockIcon = document.getElementById('lucky-lock-icon');
if (lockIcon) {
lockIcon.innerHTML = this.isLocked ? '🔒' : '🔓';
lockIcon.title = this.isLocked ? 'Unlock panels' : 'Lock panels';
if (this.isLocked) {
lockIcon.classList.add('active');
} else {
lockIcon.classList.remove('active');
}
}
const allPanels = document.querySelectorAll('.lucky-floating-panel');
allPanels.forEach(panel => {
if (this.isLocked) {
panel.style.pointerEvents = 'none';
} else {
panel.style.pointerEvents = 'auto';
}
});
}
toggleOptions() {
const optionsPanel = document.getElementById('lucky-options-panel');
const optionsIcon = document.getElementById('lucky-options-icon');
if (optionsPanel) {
optionsPanel.classList.toggle('visible');
if (optionsIcon) {
if (optionsPanel.classList.contains('visible')) {
optionsIcon.classList.add('active');
this.updateOptionsPanelPosition();
} else {
optionsIcon.classList.remove('active');
}
}
}
}
togglePanelVisibility(panelId, visible) {
this.panelVisibility[panelId] = visible;
let panel;
if (panelId === 'stats') {
panel = document.getElementById('lucky-stats-panel');
} else if (panelId === 'revenue') {
panel = document.getElementById('lucky-revenue-panel');
} else if (panelId === 'totals') {
panel = document.getElementById('lucky-totals-panel');
} else if (panelId === 'bigExpected') {
panel = document.getElementById('lucky-big-expected-panel');
} else if (panelId === 'bigLuck') {
panel = document.getElementById('lucky-big-luck-panel');
} else if (panelId === 'mobExpected') {
panel = document.getElementById('lucky-mob-expected-panel');
} else if (panelId.startsWith('player-')) {
const playerName = panelId.replace('player-', '');
panel = document.getElementById(`lucky-player-panel-${playerName}`);
}
if (panel) {
panel.style.display = visible ? 'block' : 'none';
}
this.savePanelSettings();
}
savePanelSettings() {
const settings = {
isLocked: this.isLocked,
panelVisibility: this.panelVisibility,
expected100Mode: this.expected100Mode,
snapToGrid: this.snapToGrid
};
this.lyStorage.set('settings', settings);
}
loadPanelSettings() {
try {
const settings = this.lyStorage.get('settings');
if (settings) {
if (settings.isLocked !== undefined) {
this.isLocked = settings.isLocked;
}
if (settings.panelVisibility) {
this.panelVisibility = { ...this.panelVisibility, ...settings.panelVisibility };
}
if (settings.expected100Mode !== undefined) {
this.expected100Mode = settings.expected100Mode;
}
if (settings.snapToGrid !== undefined) {
this.snapToGrid = settings.snapToGrid;
}
}
} catch (e) {
}
}
setupResizeObserver() {
let resizeTimeout;
const resizeObserver = new ResizeObserver(() => {
this.updateOptionsPanelPosition();
clearTimeout(resizeTimeout);
resizeTimeout = setTimeout(() => {
this.saveState();
}, 500);
});
resizeObserver.observe(this.panel);
if (!this._luckyResizeObservers) this._luckyResizeObservers = [];
this._luckyResizeObservers.push(resizeObserver);
}
show() {
this.panel.style.display = 'block';
this.updateContent();
this.updateOptionsPanelPosition();
this.saveState();
}
hide() {
this.panel.style.display = 'none';
const optionsPanel = document.getElementById('lucky-options-panel');
if (optionsPanel) {
optionsPanel.classList.remove('visible');
const optionsIcon = document.getElementById('lucky-options-icon');
if (optionsIcon) {
optionsIcon.classList.remove('active');
}
}
this.saveState();
}
startAutoUpdate() {
this.updateContent();
VisibilityManager.register('lucky-update', () => {
this.updateContent();
}, 5000);
}
updateContent() {
const content = document.getElementById('lucky-content');
if (!content) return;
const sessionTime = FlootData.getSessionTime();
const playerNames = FlootData.getAllPlayerNames();
const hasData = playerNames.length > 0 && sessionTime > 0;
const effectivePlayerNames = hasData ? playerNames : [];
const playerNamesChanged = JSON.stringify(effectivePlayerNames) !== JSON.stringify(this.lastPlayerNames);
if (playerNamesChanged || !document.getElementById('lucky-stats-panel')) {
this.ensurePanelsExist(effectivePlayerNames);
this.lastPlayerNames = [...effectivePlayerNames];
}
const oldNoDataMsg = content.querySelector('.lucky-no-data');
if (oldNoDataMsg) {
oldNoDataMsg.remove();
}
if (!hasData) {
this.updateStatsPanelEmpty();
this.updateRevenuePanelEmpty();
this.updateBigExpectedPanelEmpty();
this.updateBigLuckPanelEmpty();
this.updateTotalsPanelEmpty();
this.updateMobExpectedPanelEmpty();
return;
}
const dropsData = this.calculateDropsData(playerNames);
this.updateStatsPanel();
this.updateMobExpectedPanel();
this.updateRevenuePanel(playerNames, dropsData);
this.updateBigExpectedPanel(playerNames, dropsData);
this.updateBigLuckPanel(playerNames, dropsData);
this.updatePlayerPanels(playerNames, dropsData);
this.updateTotalsPanel(playerNames, dropsData);
}
updateStatsPanelEmpty() {
const statsContent = document.getElementById('lucky-stats-content');
if (statsContent) {
statsContent.innerHTML = '<div class="lucky-empty-state">Waiting for combat...</div>';
}
}
updateRevenuePanelEmpty() {
const revenueContent = document.getElementById('lucky-revenue-content');
if (revenueContent) {
revenueContent.innerHTML = '<div class="lucky-empty-state">--</div>';
}
}
updateBigExpectedPanelEmpty() {
const expectedContent = document.getElementById('lucky-big-expected-content');
if (expectedContent) {
expectedContent.innerHTML = '<div class="lucky-empty-state">--</div>';
}
}
updateBigLuckPanelEmpty() {
const luckContent = document.getElementById('lucky-big-luck-content');
if (luckContent) {
luckContent.innerHTML = '<div class="lucky-empty-state">--</div>';
}
}
updateTotalsPanelEmpty() {
const totalsContent = document.getElementById('lucky-totals-content');
if (totalsContent) {
totalsContent.innerHTML = '<tr><td colspan="6" class="lucky-empty-state">--</td></tr>';
}
}
ensurePanelsExist(playerNames) {
const content = document.getElementById('lucky-content');
if (!content) return;
if (!document.getElementById('lucky-stats-panel')) {
const statsPanel = document.createElement('div');
statsPanel.id = 'lucky-stats-panel';
statsPanel.className = 'lucky-floating-panel lucky-stats-section';
statsPanel.innerHTML = `
<div class="lucky-stats-header" data-panel-id="stats">
<div class="lucky-stats-title">Session Statistics</div>
</div>
<div id="lucky-stats-content"></div>
`;
statsPanel.style.display = this.panelVisibility.stats !== false ? 'block' : 'none';
content.appendChild(statsPanel);
this.setupPanelDragging('lucky-stats-panel', 'stats');
}
for (const [playerName, panelRef] of this.playerPanels) {
if (!playerNames.includes(playerName)) {
if (panelRef && panelRef.parentNode) {
panelRef.remove();
}
this.playerPanels.delete(playerName);
}
}
playerNames.forEach((playerName, index) => {
if (!this.playerPanels.has(playerName)) {
const playerPanel = document.createElement('div');
playerPanel.id = `lucky-player-panel-${playerName}`;
playerPanel.className = 'lucky-floating-panel lucky-data-panel';
const defaultLeft = 60 + (index * 50);
const defaultTop = 100 + (index * 50);
playerPanel.style.left = defaultLeft + 'px';
playerPanel.style.top = defaultTop + 'px';
playerPanel.style.height = '500px';
playerPanel.innerHTML = `
<div class="lucky-panel-header">
<div class="lucky-stats-header" data-panel-id="player-${playerName}">
<div class="lucky-stats-title">${playerName}</div>
</div>
<div id="lucky-player-stats-${playerName}" class="lucky-player-stats-info"></div>
<table class="lucky-drop-table">
<thead>
<tr>
<th>Item</th>
<th>Qty</th>
<th>Value</th>
<th colspan="2">Expected</th>
</tr>
</thead>
</table>
</div>
<div class="lucky-panel-content-scrollable">
<table class="lucky-drop-table">
<tbody id="lucky-player-content-${playerName}"></tbody>
</table>
</div>
`;
const panelId = `player-${playerName}`;
if (this.panelVisibility[panelId] === undefined) {
this.panelVisibility[panelId] = true;
}
playerPanel.style.display = this.panelVisibility[panelId] !== false ? 'block' : 'none';
content.appendChild(playerPanel);
this.setupPanelDragging(`lucky-player-panel-${playerName}`, `player-${playerName}`);
this.playerPanels.set(playerName, playerPanel);
}
});
if (!document.getElementById('lucky-revenue-panel')) {
const revenuePanel = document.createElement('div');
revenuePanel.id = 'lucky-revenue-panel';
revenuePanel.className = 'lucky-floating-panel lucky-revenue-panel';
revenuePanel.style.left = '300px';
revenuePanel.style.top = '60px';
revenuePanel.innerHTML = `
<div class="lucky-stats-header" data-panel-id="revenue">
<div class="lucky-stats-title">Revenue</div>
</div>
<div id="lucky-revenue-content"></div>
`;
revenuePanel.style.display = this.panelVisibility.revenue !== false ? 'block' : 'none';
content.appendChild(revenuePanel);
this.setupPanelDragging('lucky-revenue-panel', 'revenue');
}
if (!document.getElementById('lucky-big-expected-panel')) {
const bigExpectedPanel = document.createElement('div');
bigExpectedPanel.id = 'lucky-big-expected-panel';
bigExpectedPanel.className = 'lucky-floating-panel lucky-big-expected-panel';
bigExpectedPanel.style.left = '600px';
bigExpectedPanel.style.top = '60px';
bigExpectedPanel.innerHTML = `
<div class="lucky-big-expected-header">
<div class="lucky-stats-header" data-panel-id="bigExpected">
<div class="lucky-stats-title">Expected</div>
</div>
</div>
<div id="lucky-big-expected-content" class="lucky-big-expected-content"></div>
`;
bigExpectedPanel.style.display = this.panelVisibility.bigExpected !== false ? 'block' : 'none';
content.appendChild(bigExpectedPanel);
this.setupPanelDragging('lucky-big-expected-panel', 'bigExpected');
}
if (!document.getElementById('lucky-big-luck-panel')) {
const bigLuckPanel = document.createElement('div');
bigLuckPanel.id = 'lucky-big-luck-panel';
bigLuckPanel.className = 'lucky-floating-panel lucky-big-luck-panel';
bigLuckPanel.style.left = '1020px';
bigLuckPanel.style.top = '60px';
bigLuckPanel.innerHTML = `
<div class="lucky-big-luck-header">
<div class="lucky-stats-header" data-panel-id="bigLuck">
<div class="lucky-stats-title">Luck</div>
</div>
</div>
<div id="lucky-big-luck-content" class="lucky-big-luck-content"></div>
`;
bigLuckPanel.style.display = this.panelVisibility.bigLuck !== false ? 'block' : 'none';
content.appendChild(bigLuckPanel);
this.setupPanelDragging('lucky-big-luck-panel', 'bigLuck');
}
if (!document.getElementById('lucky-mob-expected-panel')) {
const mobExpectedPanel = document.createElement('div');
mobExpectedPanel.id = 'lucky-mob-expected-panel';
mobExpectedPanel.className = 'lucky-floating-panel lucky-stats-section';
mobExpectedPanel.innerHTML = `
<div class="lucky-stats-header" data-panel-id="mobExpected">
<div class="lucky-stats-title">Mob Expected</div>
</div>
<div id="lucky-mob-expected-content"></div>
`;
mobExpectedPanel.style.display = this.panelVisibility.mobExpected !== false ? 'block' : 'none';
content.appendChild(mobExpectedPanel);
this.setupPanelDragging('lucky-mob-expected-panel', 'mobExpected');
}
if (!document.getElementById('lucky-totals-panel')) {
const totalsPanel = document.createElement('div');
totalsPanel.id = 'lucky-totals-panel';
totalsPanel.className = 'lucky-floating-panel lucky-data-panel';
const defaultLeft = 60 + (playerNames.length * 50);
const defaultTop = 100 + (playerNames.length * 50);
totalsPanel.style.left = defaultLeft + 'px';
totalsPanel.style.top = defaultTop + 'px';
totalsPanel.style.height = '500px';
totalsPanel.innerHTML = `
<div class="lucky-panel-header">
<div class="lucky-stats-header" data-panel-id="totals">
<div class="lucky-stats-title">Totals & Expected</div>
</div>
<table class="lucky-drop-table">
<thead>
<tr>
<th>Item</th>
<th>Total Qty</th>
<th>Total Value</th>
<th>%</th>
<th colspan="2">Expected</th>
</tr>
</thead>
</table>
</div>
<div class="lucky-panel-content-scrollable">
<table class="lucky-drop-table">
<tbody id="lucky-totals-content"></tbody>
</table>
</div>
`;
totalsPanel.style.display = this.panelVisibility.totals !== false ? 'block' : 'none';
content.appendChild(totalsPanel);
this.setupPanelDragging('lucky-totals-panel', 'totals');
}
this.updatePlayerOptions(playerNames);
this.applyVisibilitySettings();
}
updatePlayerOptions(playerNames) {
const playerOptionsContainer = document.getElementById('lucky-player-options');
if (!playerOptionsContainer) return;
playerOptionsContainer.innerHTML = '';
playerNames.forEach(playerName => {
const panelId = `player-${playerName}`;
if (this.panelVisibility[panelId] === undefined) {
this.panelVisibility[panelId] = true;
}
const optionRow = document.createElement('div');
optionRow.className = 'lucky-option-row';
optionRow.innerHTML = `
<input type="checkbox" class="lucky-option-checkbox" id="lucky-checkbox-${panelId}" data-panel="${panelId}" ${this.panelVisibility[panelId] ? 'checked' : ''}>
<label for="lucky-checkbox-${panelId}" class="lucky-option-label">${playerName}</label>
`;
playerOptionsContainer.appendChild(optionRow);
const checkbox = optionRow.querySelector('.lucky-option-checkbox');
checkbox.addEventListener('change', (e) => {
this.togglePanelVisibility(panelId, e.target.checked);
});
});
}
applyVisibilitySettings() {
for (const [panelId, visible] of Object.entries(this.panelVisibility)) {
let panel;
if (panelId === 'stats') {
panel = document.getElementById('lucky-stats-panel');
} else if (panelId === 'revenue') {
panel = document.getElementById('lucky-revenue-panel');
} else if (panelId === 'totals') {
panel = document.getElementById('lucky-totals-panel');
} else if (panelId === 'bigExpected') {
panel = document.getElementById('lucky-big-expected-panel');
} else if (panelId === 'bigLuck') {
panel = document.getElementById('lucky-big-luck-panel');
} else if (panelId === 'mobExpected') {
panel = document.getElementById('lucky-mob-expected-panel');
} else if (panelId.startsWith('player-')) {
const playerName = panelId.replace('player-', '');
panel = document.getElementById(`lucky-player-panel-${playerName}`);
}
if (panel) {
panel.style.display = visible ? 'block' : 'none';
}
}
}
calculatePercentOfExpected(actual, expected) {
if (expected <= 0) return 0;
const basePercent = ((actual / expected) - 1) * 100;
return this.expected100Mode ? basePercent + 100 : basePercent;
}
calculateDropsData(playerNames) {
return this._calculateDropsDataImpl(playerNames);
}
_calculateDropsDataImpl(playerNames) {
const battleCount = FlootData.getBattleCount();
const currentMapHrid = LuckyGameData.currentMapHrid;
const currentTier = LuckyGameData.currentDifficultyTier;
const partySize = FlootData.getPartySize();
const runCount = battleCount;
const cacheKey = `${currentMapHrid}|${battleCount}|${currentTier}|${partySize}`;
let expectedDropsTotal, maximumDropsTotal;
if (this.lastDropsCacheKey === cacheKey && this.cachedExpectedDrops && this.cachedMaximumDrops) {
expectedDropsTotal = this.cachedExpectedDrops;
maximumDropsTotal = this.cachedMaximumDrops;
} else {
expectedDropsTotal = LuckyDropAnalyzer.calculateExpectedDrops(
currentMapHrid,
runCount,
currentTier,
partySize
);
maximumDropsTotal = LuckyDropAnalyzer.calculateMaximumDrops(
currentMapHrid,
runCount,
currentTier,
partySize
);
this.cachedExpectedDrops = expectedDropsTotal;
this.cachedMaximumDrops = maximumDropsTotal;
this.lastDropsCacheKey = cacheKey;
}
const allItems = new Set();
const playerDropsData = {};
playerNames.forEach(playerName => {
const drops = FlootData.getPlayerDrops(playerName);
playerDropsData[playerName] = drops;
for (const itemHrid of Object.keys(drops)) {
allItems.add(itemHrid);
}
});
for (const itemHrid of Object.keys(expectedDropsTotal)) {
const dropData = expectedDropsTotal[itemHrid];
const expectedCount = typeof dropData === 'number' ? dropData : dropData.count;
if (expectedCount > 0) {
allItems.add(itemHrid);
}
}
const mapType = LuckyGameData.mapData[LuckyGameData.currentMapHrid]?.type || 'solo';
const dropsList = [];
for (const itemHrid of allItems) {
const itemName = LuckyUtils.getItemName(itemHrid);
const itemPrice = LuckyUtils.getItemValue(itemHrid, 1);
const dropData = expectedDropsTotal[itemHrid] || { count: 0, tierDropRate: 1.0, isRare: false };
const baseExpectedTotal = typeof dropData === 'number' ? dropData : dropData.count;
const tierDropRate = typeof dropData === 'object' ? dropData.tierDropRate : 1.0;
const isRareItem = typeof dropData === 'object' ? dropData.isRare : false;
const maxDropData = maximumDropsTotal[itemHrid] || { count: 0, tierDropRate: 1.0 };
const baseMaximumTotal = typeof maxDropData === 'number' ? maxDropData : maxDropData.count;
const playerStatsData = {};
let totalActualValue = 0;
let totalActualQty = 0;
let totalExpectedQty = 0;
let totalExpectedValue = 0;
for (let i = 0; i < playerNames.length; i++) {
const playerName = playerNames[i];
const actualCount = playerDropsData[playerName][itemHrid] || 0;
const actualValue = actualCount * itemPrice;
const stats = LuckyGameData.playerStats[playerName];
let playerExpected = baseExpectedTotal;
let playerMaximum = baseMaximumTotal;
if (stats) {
const commonRateMultiplier = 1 + (stats.combatDropRate || 0);
const rareRateMultiplier = 1 + (stats.combatRareFind || 0);
const rateMultiplier = isRareItem ? rareRateMultiplier : commonRateMultiplier;
const dungeonMultiplier = mapType === 'dungeon' ? 5 : 1;
const quantityMultiplier = (1 + stats.combatDropQuantity) / playerNames.length * dungeonMultiplier;
playerExpected = playerExpected * rateMultiplier * quantityMultiplier;
playerMaximum = playerMaximum * rateMultiplier * quantityMultiplier;
} else {
const dungeonMultiplier = mapType === 'dungeon' ? 5 : 1;
playerExpected = playerExpected / playerNames.length * dungeonMultiplier;
playerMaximum = playerMaximum / playerNames.length * dungeonMultiplier;
}
const playerExpectedValue = playerExpected * itemPrice;
const playerMaximumValue = playerMaximum * itemPrice;
const percentOfExpected = this.calculatePercentOfExpected(actualCount, playerExpected);
if (i === 0) {
const itemName = itemHrid.split('/').pop();
}
playerStatsData[playerName] = {
actualCount,
actualValue,
expectedCount: playerExpected,
expectedValue: playerExpectedValue,
percentOfExpected,
maximumCount: playerMaximum,
maximumValue: playerMaximumValue
};
totalActualValue += actualValue;
totalActualQty += actualCount;
totalExpectedQty += playerExpected;
totalExpectedValue += playerExpectedValue;
}
const totalPercentOfExpected = this.calculatePercentOfExpected(totalActualQty, totalExpectedQty);
dropsList.push({
itemHrid,
itemName,
expectedCountTotal: totalExpectedQty,
expectedValueTotal: totalExpectedValue,
totalPercentOfExpected,
playerStats: playerStatsData,
totalActualValue,
totalActualQty
});
}
dropsList.sort((a, b) => b.totalActualValue - a.totalActualValue);
return { dropsList, playerDropsData, expectedDropsTotal };
}
updateStatsPanel() {
return this._updateStatsPanelImpl();
}
_updateStatsPanelImpl() {
if (this.panelVisibility.stats === false) return;
const statsContent = document.getElementById('lucky-stats-content');
if (!statsContent) return;
if (!this.statsElements) {
statsContent.innerHTML = `
<div class="lucky-stat-row">
<span class="lucky-stat-label">Encounters:</span>
<span class="lucky-stat-value" id="lucky-stat-encounters"></span>
</div>
<div class="lucky-stat-row">
<span class="lucky-stat-label">Session Time:</span>
<span class="lucky-stat-value" id="lucky-stat-time"></span>
</div>
<div class="lucky-stat-row">
<span class="lucky-stat-label">EPH:</span>
<span class="lucky-stat-value" id="lucky-stat-eph"></span>
</div>
`;
this.statsElements = {
encounters: document.getElementById('lucky-stat-encounters'),
time: document.getElementById('lucky-stat-time'),
eph: document.getElementById('lucky-stat-eph')
};
}
const battleCount = FlootData.getBattleCount();
const sessionTime = FlootData.getSessionTime();
const eph = FlootData.getEPH();
this.statsElements.encounters.textContent = battleCount;
this.statsElements.time.textContent = LuckyUtils.formatTime(sessionTime);
this.statsElements.eph.textContent = eph.toFixed(2);
}
updateMobExpectedPanelEmpty() {
const content = document.getElementById('lucky-mob-expected-content');
if (content) {
content.innerHTML = '<div class="lucky-empty-state">Waiting for combat...</div>';
}
}
updateMobExpectedPanel() {
if (this.panelVisibility.mobExpected === false) return;
const content = document.getElementById('lucky-mob-expected-content');
if (!content) return;
const currentMapHrid = LuckyGameData.currentMapHrid;
const mapData = LuckyGameData.mapData[currentMapHrid];
if (!mapData || !mapData.spawnInfo) {
content.innerHTML = '<div class="lucky-empty-state">No map data</div>';
return;
}
const spawnInfo = mapData.spawnInfo;
const battleCount = FlootData.getBattleCount();
const expectedSpawns = LuckyDropAnalyzer.computeExpectedSpawns(spawnInfo);
let bossWave = spawnInfo.bossWave || 0;
if (!bossWave && mapData.type === 'dungeon') {
bossWave = 1;
} else if (!bossWave && mapData.type === 'group' && mapData.bossDrops && Object.keys(mapData.bossDrops).length > 0) {
bossWave = 10;
}
const bossCount = bossWave ? Math.floor((battleCount - 1) / bossWave) : 0;
const normalCount = bossWave ?
bossCount * (bossWave - 1) + (battleCount - 1) % bossWave :
battleCount - 1;
let html = '';
for (const [monsterHrid, spawnRate] of Object.entries(expectedSpawns)) {
const expectedCount = spawnRate * normalCount;
const mobName = LuckyGameData.monsterNames[monsterHrid] || monsterHrid.split('/').pop();
html += `
<div class="lucky-stat-row">
<span class="lucky-stat-label">${mobName}:</span>
<span class="lucky-stat-value">${LuckyUtils.formatNumber(expectedCount)}</span>
</div>
`;
}
if (mapData.bossDrops) {
for (const bossHrid of Object.keys(mapData.bossDrops)) {
const bossName = bossHrid === '_dungeon'
? 'Dungeon Boss'
: (LuckyGameData.monsterNames[bossHrid] || bossHrid.split('/').pop());
html += `
<div class="lucky-stat-row">
<span class="lucky-stat-label">${bossName}:</span>
<span class="lucky-stat-value">${LuckyUtils.formatNumber(bossCount)}</span>
</div>
`;
}
}
if (!html) {
html = '<div class="lucky-empty-state">No mob data</div>';
}
content.innerHTML = html;
}
updateRevenuePanel(playerNames, dropsData) {
return this._updateRevenuePanelImpl(playerNames, dropsData);
}
_updateRevenuePanelImpl(playerNames, dropsData) {
if (this.panelVisibility.revenue === false) return;
const revenueContent = document.getElementById('lucky-revenue-content');
if (!revenueContent) return;
if (!LuckyGameData.hasReceivedFirstBattle) {
let html = '';
playerNames.forEach(playerName => {
html += `
<div class="lucky-revenue-row-container">
<div class="lucky-revenue-row">
<div class="lucky-revenue-name">${playerName}</div>
<div class="lucky-revenue-stats-group">
<div class="lucky-revenue-stat">
<span class="lucky-revenue-stat-label">Expected</span>
<span class="lucky-revenue-stat-value">--</span>
</div>
<div class="lucky-revenue-stat">
<span class="lucky-revenue-stat-label">Actual</span>
<span class="lucky-revenue-stat-value">--</span>
</div>
<div class="lucky-revenue-stat">
<span class="lucky-revenue-stat-label">Max</span>
<span class="lucky-revenue-stat-value">--</span>
</div>
</div>
<span class="lucky-revenue-stat-value">--</span>
</div>
</div>
`;
});
html += `
<div class="lucky-revenue-row-container">
<div class="lucky-revenue-row">
<div class="lucky-revenue-name">TOTAL</div>
<div class="lucky-revenue-stats-group">
<div class="lucky-revenue-stat">
<span class="lucky-revenue-stat-label">Expected</span>
<span class="lucky-revenue-stat-value">--</span>
</div>
<div class="lucky-revenue-stat">
<span class="lucky-revenue-stat-label">Actual</span>
<span class="lucky-revenue-stat-value">--</span>
</div>
<div class="lucky-revenue-stat">
<span class="lucky-revenue-stat-label">Max</span>
<span class="lucky-revenue-stat-value">--</span>
</div>
</div>
<span class="lucky-revenue-stat-value">--</span>
</div>
</div>
`;
revenueContent.innerHTML = html;
return;
}
const { dropsList } = dropsData;
let html = '';
playerNames.forEach((playerName) => {
let playerActual = 0;
let playerExpected = 0;
let playerActualQty = 0;
let playerExpectedQty = 0;
let playerMaximumQty = 0;
let playerMaximumValue = 0;
for (const drop of dropsList) {
const playerStats = drop.playerStats[playerName];
if (playerStats) {
playerActual += playerStats.actualValue;
playerExpected += playerStats.expectedValue;
playerActualQty += playerStats.actualCount;
playerExpectedQty += playerStats.expectedCount;
playerMaximumQty += playerStats.maximumCount;
playerMaximumValue += playerStats.maximumValue;
}
}
const percentDiff = this.calculatePercentOfExpected(playerActual, playerExpected);
const sign = this.expected100Mode ? '' : (percentDiff > 0 ? '+' : '');
const color = LuckyUtils.getPercentColor(percentDiff, this.expected100Mode);
html += `
<div class="lucky-revenue-row-container">
<div class="lucky-revenue-row">
<div class="lucky-revenue-name">${playerName}</div>
<div class="lucky-revenue-stats-group">
<div class="lucky-revenue-stat">
<span class="lucky-revenue-stat-label">Expected</span>
<span class="lucky-revenue-stat-value">${LuckyUtils.formatNumber(playerExpected)}</span>
</div>
<div class="lucky-revenue-stat">
<span class="lucky-revenue-stat-label">Actual</span>
<span class="lucky-revenue-stat-value">${LuckyUtils.formatNumber(playerActual)}</span>
</div>
<div class="lucky-revenue-stat">
<span class="lucky-revenue-stat-label">Max</span>
<span class="lucky-revenue-stat-value">${LuckyUtils.formatNumber(playerMaximumValue)}</span>
</div>
</div>
<span class="lucky-revenue-stat-value colored" style="color: ${color};">${sign}${percentDiff.toFixed(1)}%</span>
</div>
</div>
`;
});
let totalActual = 0;
let totalExpected = 0;
let totalActualQty = 0;
let totalMaximumQty = 0;
let totalMaximumValue = 0;
for (const drop of dropsList) {
totalActual += drop.totalActualValue;
totalExpected += drop.expectedValueTotal;
totalActualQty += drop.totalActualQty;
for (const playerName of playerNames) {
const playerStats = drop.playerStats[playerName];
if (playerStats) {
totalMaximumQty += playerStats.maximumCount;
totalMaximumValue += playerStats.maximumValue;
}
}
}
const totalPercentDiff = this.calculatePercentOfExpected(totalActual, totalExpected);
const totalSign = this.expected100Mode ? '' : (totalPercentDiff > 0 ? '+' : '');
const totalColor = LuckyUtils.getPercentColor(totalPercentDiff, this.expected100Mode);
html += `
<div class="lucky-revenue-row-container">
<div class="lucky-revenue-row">
<div class="lucky-revenue-name">TOTAL</div>
<div class="lucky-revenue-stats-group">
<div class="lucky-revenue-stat">
<span class="lucky-revenue-stat-label">Expected</span>
<span class="lucky-revenue-stat-value">${LuckyUtils.formatNumber(totalExpected)}</span>
</div>
<div class="lucky-revenue-stat">
<span class="lucky-revenue-stat-label">Actual</span>
<span class="lucky-revenue-stat-value">${LuckyUtils.formatNumber(totalActual)}</span>
</div>
<div class="lucky-revenue-stat">
<span class="lucky-revenue-stat-label">Max</span>
<span class="lucky-revenue-stat-value">${LuckyUtils.formatNumber(totalMaximumValue)}</span>
</div>
</div>
<span class="lucky-revenue-stat-value colored" style="color: ${totalColor};">${totalSign}${totalPercentDiff.toFixed(1)}%</span>
</div>
</div>
`;
revenueContent.innerHTML = html;
}
updateBigExpectedPanel(playerNames, dropsData) {
return this._updateBigExpectedPanelImpl(playerNames, dropsData);
}
_updateBigExpectedPanelImpl(playerNames, dropsData) {
const bigExpectedContent = document.getElementById('lucky-big-expected-content');
if (!bigExpectedContent) return;
if (!LuckyGameData.hasReceivedFirstBattle) {
let html = '';
playerNames.forEach(playerName => {
html += `
<div class="lucky-big-expected-item">
<span class="lucky-big-expected-name">${playerName}</span>
<span class="lucky-big-expected-percent">--</span>
</div>
`;
});
html += `
<div class="lucky-big-expected-item lucky-big-expected-total">
<span class="lucky-big-expected-name">TOTAL</span>
<span class="lucky-big-expected-percent">--</span>
</div>
`;
bigExpectedContent.innerHTML = html;
return;
}
const { dropsList } = dropsData;
let html = '';
let totalActual = 0;
let totalExpected = 0;
playerNames.forEach(playerName => {
let playerActual = 0;
let playerExpected = 0;
for (const drop of dropsList) {
const playerStats = drop.playerStats[playerName];
if (playerStats) {
playerActual += playerStats.actualValue;
playerExpected += playerStats.expectedValue;
}
}
totalActual += playerActual;
totalExpected += playerExpected;
const percentDiff = this.calculatePercentOfExpected(playerActual, playerExpected);
const sign = this.expected100Mode ? '' : (percentDiff > 0 ? '+' : '');
const color = LuckyUtils.getPercentColor(percentDiff, this.expected100Mode);
html += `
<div class="lucky-big-expected-item">
<span class="lucky-big-expected-name">${playerName}</span>
<span class="lucky-big-expected-percent" style="color: ${color};">${sign}${percentDiff.toFixed(1)}%</span>
</div>
`;
});
const totalPercentDiff = this.calculatePercentOfExpected(totalActual, totalExpected);
const totalSign = this.expected100Mode ? '' : (totalPercentDiff > 0 ? '+' : '');
const totalColor = LuckyUtils.getPercentColor(totalPercentDiff, this.expected100Mode);
html += `
<div class="lucky-big-expected-item lucky-big-expected-total">
<span class="lucky-big-expected-name">TOTAL</span>
<span class="lucky-big-expected-percent" style="color: ${totalColor};">${totalSign}${totalPercentDiff.toFixed(1)}%</span>
</div>
`;
bigExpectedContent.innerHTML = html;
}
getDropData(playerName = null, battleCount = 1) {
const mapData = LuckyGameData.mapData[LuckyGameData.currentMapHrid];
if (!mapData) return null;
const spawnInfo = mapData.spawnInfo;
const difficultyTier = LuckyGameData.currentDifficultyTier;
const partySize = Object.keys(LuckyGameData.playerStats).length || 1;
const playerStats = playerName ? LuckyGameData.playerStats[playerName] : null;
const bossWave = spawnInfo.bossWave || 0;
const bossCount = bossWave ? Math.floor((battleCount - 1) / bossWave) + 1 : (mapData.type === 'dungeon' ? battleCount : 0);
const normalCount = bossWave ? bossCount * (bossWave - 1) + (battleCount - 1) % bossWave : battleCount - 1;
const dropData = {
spawnInfo: spawnInfo,
bossCount: bossCount,
normalCount: normalCount,
bossDrops: {},
monsterDrops: {}
};
const processDrop = (item, monsterName = 'unknown') => {
const itemPrice = LuckyUtils.getItemValue(item.itemHrid, 1);
const itemName = item.itemHrid.split('/').pop();
let { minCount, maxCount, dropRate } = item;
const dropRatePerTier = item.dropRatePerDifficultyTier || 0;
if (playerStats) {
const commonRateMultiplier = 1 + (playerStats.combatDropRate || 0);
const rareRateMultiplier = 1 + (playerStats.combatRareFind || 0);
const quantityMultiplier = (1 + (playerStats.combatDropQuantity || 0)) / partySize * (mapData.type === 'dungeon' ? 5 : 1);
const rateMultiplier = item.isRare ? rareRateMultiplier : commonRateMultiplier;
const baseMin = minCount;
const baseMax = maxCount;
const baseRate = dropRate;
minCount *= quantityMultiplier;
maxCount *= quantityMultiplier;
const len = mapData.type === 'dungeon' ? 3 : (mapData.type === 'group' ? 6 : 1);
dropRate = Array.from({length: len}, (_, n) => {
let rate = baseRate + n * dropRatePerTier;
rate = rate * (1 + n * 0.1) * rateMultiplier;
return Math.min(Math.max(rate, 0), 1);
});
}
return {
hrid: item.itemHrid,
price: itemPrice,
minCount: minCount,
maxCount: maxCount,
dropRate: dropRate
};
};
for (const [hrid, drops] of Object.entries(mapData.bossDrops || {})) {
const monsterName = hrid.split('/').pop();
dropData.bossDrops[hrid] = drops.map(drop => processDrop(drop, monsterName));
}
for (const [hrid, drops] of Object.entries(mapData.monsterDrops || {})) {
const monsterName = hrid.split('/').pop();
dropData.monsterDrops[hrid] = drops.map(drop => processDrop(drop, monsterName));
}
for (const [hrid, drops] of Object.entries(dropData.bossDrops)) {
dropData.bossDrops[hrid] = drops.map(drop => ({
...drop,
dropRate: Array.isArray(drop.dropRate) ? (drop.dropRate[difficultyTier] || drop.dropRate[0]) : drop.dropRate
}));
}
for (const [hrid, drops] of Object.entries(dropData.monsterDrops)) {
dropData.monsterDrops[hrid] = drops.map(drop => ({
...drop,
dropRate: Array.isArray(drop.dropRate) ? (drop.dropRate[difficultyTier] || drop.dropRate[0]) : drop.dropRate
}));
}
return dropData;
}
updateBigLuckPanel(playerNames, dropsData) {
return this._updateBigLuckPanelImpl(playerNames, dropsData);
}
_updateBigLuckPanelImpl(playerNames, dropsData) {
const bigLuckContent = document.getElementById('lucky-big-luck-content');
if (!bigLuckContent) return;
if (!LuckyGameData.hasReceivedFirstBattle) {
let html = '';
playerNames.forEach(playerName => {
html += `
<div class="lucky-big-luck-item">
<span class="lucky-big-luck-name">${playerName}</span>
<span class="lucky-big-luck-percent">--</span>
</div>
`;
});
bigLuckContent.innerHTML = html;
return;
}
const { dropsList } = dropsData;
const battleCount = FlootData.getBattleCount();
const partySize = playerNames.length;
const battleCountChanged = battleCount !== this.lastLuckBattleCount;
if (battleCountChanged) {
this.cachedLuckResults.clear();
this.lastLuckBattleCount = battleCount;
playerNames.forEach(playerName => {
const ruckResult = RuckAnalyzeCurrent(playerName);
this.cachedLuckResults.set(playerName, ruckResult);
});
}
let html = '';
let totalActualValue = 0;
playerNames.forEach(playerName => {
let playerActualValue = 0;
for (const drop of dropsList) {
const playerStats = drop.playerStats[playerName];
if (playerStats) {
playerActualValue += playerStats.actualValue;
}
}
totalActualValue += playerActualValue;
const ruckResult = this.cachedLuckResults.get(playerName);
const luckDecimal = ruckResult?.luck || 0;
const luckPercent = luckDecimal * 100;
const displayPercent = ruckResult
? luckPercent.toFixed(2)
: '--';
const color = ruckResult
? LuckyUtils.getLuckColor(luckPercent)
: '#ffffff';
html += `
<div class="lucky-big-luck-item">
<span class="lucky-big-luck-name">${playerName}</span>
<span class="lucky-big-luck-percent" style="color: ${color};">${displayPercent}%</span>
</div>
`;
});
bigLuckContent.innerHTML = html;
}
updatePlayerPanels(playerNames, dropsData) {
return this._updatePlayerPanelsImpl(playerNames, dropsData);
}
_updatePlayerPanelsImpl(playerNames, dropsData) {
playerNames.forEach(playerName => {
const panelId = `player-${playerName}`;
if (this.panelVisibility[panelId] === false) return;
const playerContent = document.getElementById(`lucky-player-content-${playerName}`);
const playerStatsDiv = document.getElementById(`lucky-player-stats-${playerName}`);
if (!playerContent) return;
if (playerStatsDiv) {
const stats = LuckyGameData.playerStats[playerName];
if (stats) {
const dropQty = (stats.combatDropQuantity * 100).toFixed(3);
const dropRate = (stats.combatDropRate * 100).toFixed(3);
const rareFind = (stats.combatRareFind * 100).toFixed(3);
const newText = `DQ: ${dropQty}% | DR: ${dropRate}% | RF: ${rareFind}%`;
if (playerStatsDiv.textContent !== newText) {
playerStatsDiv.textContent = newText;
}
}
}
if (!LuckyGameData.hasReceivedFirstBattle) {
playerContent.innerHTML = '<tr><td colspan="5" class="lucky-empty-state">Waiting for first battle...</td></tr>';
return;
}
const { dropsList } = dropsData;
let playerTotalActual = 0;
let playerTotalExpected = 0;
const rowsData = [];
for (const drop of dropsList) {
const playerStats = drop.playerStats[playerName];
if (!playerStats) {
continue;
}
const percentClass = LuckyUtils.getPercentClass(playerStats.percentOfExpected, this.expected100Mode);
const percentSign = this.expected100Mode ? '' : (playerStats.percentOfExpected > 0 ? '+' : '');
playerTotalActual += playerStats.actualValue ?? 0;
playerTotalExpected += playerStats.expectedValue ?? 0;
rowsData.push({
itemName: drop.itemName || 'Unknown',
actualCount: LuckyUtils.formatNumber(playerStats.actualCount ?? 0),
actualValue: LuckyUtils.formatNumber(playerStats.actualValue ?? 0),
expectedCount: LuckyUtils.formatNumber(playerStats.expectedCount ?? 0),
percentClass: percentClass,
percentText: `${percentSign}${(playerStats.percentOfExpected ?? 0).toFixed(1)}%`
});
}
const totalPercentDiff = this.calculatePercentOfExpected(playerTotalActual, playerTotalExpected);
const totalPercentClass = LuckyUtils.getPercentClass(totalPercentDiff, this.expected100Mode);
const totalPercentSign = this.expected100Mode ? '' : (totalPercentDiff > 0 ? '+' : '');
const totalRowData = {
actualValue: LuckyUtils.formatNumber(playerTotalActual),
expectedValue: LuckyUtils.formatNumber(playerTotalExpected),
percentClass: totalPercentClass,
percentText: `${totalPercentSign}${totalPercentDiff.toFixed(1)}%`
};
const rows = playerContent.rows;
const neededRows = rowsData.length + 1;
while (rows.length > neededRows) {
playerContent.deleteRow(-1);
}
for (let i = 0; i < rowsData.length; i++) {
const data = rowsData[i];
let row = rows[i];
if (!row) {
row = playerContent.insertRow();
row.innerHTML = '<td class="lucky-item-name"></td><td></td><td></td><td></td><td></td>';
} else if (row.cells.length !== 5) {
row.innerHTML = '<td class="lucky-item-name"></td><td></td><td></td><td></td><td></td>';
}
const cells = row.cells;
if (cells[0]) cells[0].textContent = data.itemName;
if (cells[1]) cells[1].textContent = data.actualCount;
if (cells[2]) cells[2].textContent = data.actualValue;
if (cells[3]) cells[3].textContent = data.expectedCount;
if (cells[4]) cells[4].textContent = data.percentText;
const newClass = `lucky-${data.percentClass}`;
if (cells[4] && cells[4].className !== newClass) cells[4].className = newClass;
}
let totalRow = rows[rowsData.length];
if (!totalRow) {
totalRow = playerContent.insertRow();
totalRow.className = 'lucky-total-row';
totalRow.innerHTML = '<td class="lucky-item-name">TOTAL</td><td></td><td></td><td></td><td></td>';
}
const totalCells = totalRow.cells;
if (totalCells[2] && totalCells[2].textContent !== totalRowData.actualValue) totalCells[2].textContent = totalRowData.actualValue;
if (totalCells[3] && totalCells[3].textContent !== totalRowData.expectedValue) totalCells[3].textContent = totalRowData.expectedValue;
if (totalCells[4] && totalCells[4].textContent !== totalRowData.percentText) totalCells[4].textContent = totalRowData.percentText;
const newTotalClass = `lucky-${totalRowData.percentClass}`;
if (totalCells[4] && totalCells[4].className !== newTotalClass) totalCells[4].className = newTotalClass;
});
}
updateTotalsPanel(playerNames, dropsData) {
return this._updateTotalsPanelImpl(playerNames, dropsData);
}
_updateTotalsPanelImpl(playerNames, dropsData) {
if (this.panelVisibility.totals === false) return;
const totalsContent = document.getElementById('lucky-totals-content');
if (!totalsContent) return;
if (!LuckyGameData.hasReceivedFirstBattle) {
totalsContent.innerHTML = '<tr><td colspan="6" class="lucky-empty-state">Waiting for first battle...</td></tr>';
return;
}
const { dropsList } = dropsData;
let summaryTotalActualValue = 0;
let summaryTotalExpectedValue = 0;
let summaryTotalActualQty = 0;
let summaryTotalExpectedQty = 0;
const rowsData = [];
for (const drop of dropsList) {
const totalPercentClass = LuckyUtils.getPercentClass(drop.totalPercentOfExpected, this.expected100Mode);
const totalPercentSign = this.expected100Mode ? '' : (drop.totalPercentOfExpected > 0 ? '+' : '');
summaryTotalActualValue += drop.totalActualValue;
summaryTotalExpectedValue += drop.expectedValueTotal;
summaryTotalActualQty += drop.totalActualQty;
summaryTotalExpectedQty += drop.expectedCountTotal;
rowsData.push({
itemName: drop.itemName || 'Unknown',
actualQty: LuckyUtils.formatNumber(drop.totalActualQty ?? 0),
actualValue: LuckyUtils.formatNumber(drop.totalActualValue ?? 0),
percentClass: totalPercentClass,
percentText: `${totalPercentSign}${(drop.totalPercentOfExpected ?? 0).toFixed(1)}%`,
expectedQty: LuckyUtils.formatNumber(drop.expectedCountTotal ?? 0),
expectedValue: LuckyUtils.formatNumber(drop.expectedValueTotal ?? 0)
});
}
const totalPercentDiff = this.calculatePercentOfExpected(summaryTotalActualValue, summaryTotalExpectedValue);
const totalPercentClass = LuckyUtils.getPercentClass(totalPercentDiff, this.expected100Mode);
const totalPercentSign = this.expected100Mode ? '' : (totalPercentDiff > 0 ? '+' : '');
const totalRowData = {
actualQty: LuckyUtils.formatNumber(summaryTotalActualQty),
actualValue: LuckyUtils.formatNumber(summaryTotalActualValue),
percentClass: totalPercentClass,
percentText: `${totalPercentSign}${totalPercentDiff.toFixed(1)}%`,
expectedQty: LuckyUtils.formatNumber(summaryTotalExpectedQty),
expectedValue: LuckyUtils.formatNumber(summaryTotalExpectedValue)
};
const rows = totalsContent.rows;
const neededRows = rowsData.length + 1;
while (rows.length > neededRows) {
totalsContent.deleteRow(-1);
}
for (let i = 0; i < rowsData.length; i++) {
const data = rowsData[i];
let row = rows[i];
if (!row) {
row = totalsContent.insertRow();
row.innerHTML = '<td class="lucky-item-name"></td><td></td><td></td><td></td><td></td><td></td>';
} else if (row.cells.length !== 6) {
row.innerHTML = '<td class="lucky-item-name"></td><td></td><td></td><td></td><td></td><td></td>';
}
const cells = row.cells;
if (cells[0]) cells[0].textContent = data.itemName;
if (cells[1]) cells[1].textContent = data.actualQty;
if (cells[2]) cells[2].textContent = data.actualValue;
if (cells[3]) cells[3].textContent = data.percentText;
if (cells[4]) cells[4].textContent = data.expectedQty;
if (cells[5]) cells[5].textContent = data.expectedValue;
const newClass = `lucky-${data.percentClass}`;
if (cells[3] && cells[3].className !== newClass) cells[3].className = newClass;
}
let totalRow = rows[rowsData.length];
if (!totalRow) {
totalRow = totalsContent.insertRow();
totalRow.className = 'lucky-total-row';
totalRow.innerHTML = '<td class="lucky-item-name">TOTAL</td><td></td><td></td><td></td><td></td><td></td>';
}
const totalCells = totalRow.cells;
if (totalCells[1] && totalCells[1].textContent !== totalRowData.actualQty) totalCells[1].textContent = totalRowData.actualQty;
if (totalCells[2] && totalCells[2].textContent !== totalRowData.actualValue) totalCells[2].textContent = totalRowData.actualValue;
if (totalCells[3] && totalCells[3].textContent !== totalRowData.percentText) totalCells[3].textContent = totalRowData.percentText;
if (totalCells[4] && totalCells[4].textContent !== totalRowData.expectedQty) totalCells[4].textContent = totalRowData.expectedQty;
if (totalCells[5] && totalCells[5].textContent !== totalRowData.expectedValue) totalCells[5].textContent = totalRowData.expectedValue;
const newTotalClass = `lucky-${totalRowData.percentClass}`;
if (totalCells[3] && totalCells[3].className !== newTotalClass) totalCells[3].className = newTotalClass;
const summaryTotals = {
players: {},
totalActualValue: summaryTotalActualValue,
totalExpectedValue: summaryTotalExpectedValue
};
for (const playerName of playerNames) {
let playerActual = 0;
let playerExpected = 0;
for (const drop of dropsList) {
const playerStats = drop.playerStats[playerName];
if (playerStats) {
playerActual += playerStats.actualValue;
playerExpected += playerStats.expectedValue;
}
}
summaryTotals.players[playerName] = {
actualValue: playerActual,
expectedValue: playerExpected
};
}
}
setupPanelDragging(panelId, storageId) {
const panel = document.getElementById(panelId);
const header = panel?.querySelector(`[data-panel-id="${storageId}"]`);
if (!panel || !header) return;
this.setupResizeHandles(panel, storageId);
this.loadPanelState(panel, storageId);
panel.addEventListener('mousedown', () => {
this.bringPanelToFront(panel);
});
const GRID_SIZE = 10;
let startX, startY;
let initialLeft, initialTop;
const gridOverlay = document.getElementById('lucky-grid-overlay');
const snapToGrid = (value) => Math.round(value / GRID_SIZE) * GRID_SIZE;
const onMouseMove = (e) => {
const deltaX = e.clientX - startX;
const deltaY = e.clientY - startY;
let newLeft = initialLeft + deltaX;
let newTop = initialTop + deltaY;
if (this.snapToGrid) {
newLeft = snapToGrid(newLeft);
newTop = snapToGrid(newTop);
}
const content = document.getElementById('lucky-content');
if (content && header) {
const contentRect = content.getBoundingClientRect();
const panelRect = panel.getBoundingClientRect();
if (storageId === 'bigExpected' || storageId === 'bigLuck') {
newLeft = Math.max(0, Math.min(newLeft, contentRect.width - panelRect.width));
newTop = Math.max(0, Math.min(newTop, contentRect.height - panelRect.height));
} else {
const headerRect = header.getBoundingClientRect();
const headerOffsetX = headerRect.left - panelRect.left;
const headerOffsetY = headerRect.top - panelRect.top;
newLeft = Math.max(-headerOffsetX, Math.min(newLeft, contentRect.width - headerRect.width - headerOffsetX));
newTop = Math.max(-headerOffsetY, Math.min(newTop, contentRect.height - headerRect.height - headerOffsetY));
}
}
panel.style.left = newLeft + 'px';
panel.style.top = newTop + 'px';
panel.style.right = 'auto';
};
const onMouseUp = () => {
document.removeEventListener('mousemove', onMouseMove);
document.removeEventListener('mouseup', onMouseUp);
header.style.cursor = 'move';
if (gridOverlay) gridOverlay.style.display = 'none';
this.savePanelState(panel, storageId);
};
header.addEventListener('mousedown', (e) => {
this.bringPanelToFront(panel);
startX = e.clientX;
startY = e.clientY;
const rect = panel.getBoundingClientRect();
const content = document.getElementById('lucky-content');
const contentRect = content.getBoundingClientRect();
initialLeft = rect.left - contentRect.left;
initialTop = rect.top - contentRect.top;
header.style.cursor = 'grabbing';
if (gridOverlay) gridOverlay.style.display = 'block';
e.preventDefault();
e.stopPropagation();
this._trackDocListener('mousemove', onMouseMove);
this._trackDocListener('mouseup', onMouseUp);
document.addEventListener('mousemove', onMouseMove);
document.addEventListener('mouseup', onMouseUp);
});
const resizeObserver = new ResizeObserver(() => {
if (!this.isDragging) {
this.savePanelState(panel, storageId);
this.updateScrollableHeight(panel);
}
});
resizeObserver.observe(panel);
if (!this._luckyResizeObservers) this._luckyResizeObservers = [];
this._luckyResizeObservers.push(resizeObserver);
setTimeout(() => this.updateScrollableHeight(panel), 0);
}
updateScrollableHeight(panel) {
const headerEl = panel.querySelector('.lucky-panel-header');
const scrollableEl = panel.querySelector('.lucky-panel-content-scrollable');
if (!headerEl || !scrollableEl) return;
const panelHeight = panel.clientHeight;
const headerHeight = headerEl.offsetHeight;
const padding = 28;
const scrollableHeight = panelHeight - headerHeight - padding;
scrollableEl.style.maxHeight = scrollableHeight + 'px';
}
bringPanelToFront(panel) {
if (!panel) return;
const allPanels = document.querySelectorAll('.lucky-floating-panel');
let maxZIndex = 10001;
allPanels.forEach(p => {
const zIndex = parseInt(p.style.zIndex || window.getComputedStyle(p).zIndex) || 10001;
if (zIndex > maxZIndex) {
maxZIndex = zIndex;
}
});
const currentZIndex = parseInt(panel.style.zIndex || window.getComputedStyle(panel).zIndex) || 10001;
if (currentZIndex <= maxZIndex) {
panel.style.zIndex = maxZIndex + 1;
}
}
resetPanelPositions() {
const content = document.getElementById('lucky-content');
if (!content) return;
const startLeft = 0;
const startTop = 0;
const verticalSpacing = 50;
let offsetIndex = 0;
const panelConfigs = [
{ id: 'lucky-stats-panel', storageId: 'stats' },
{ id: 'lucky-revenue-panel', storageId: 'revenue' },
{ id: 'lucky-big-expected-panel', storageId: 'bigExpected' },
{ id: 'lucky-big-luck-panel', storageId: 'bigLuck' },
{ id: 'lucky-mob-expected-panel', storageId: 'mobExpected' },
{ id: 'lucky-totals-panel', storageId: 'totals' }
];
panelConfigs.forEach(config => {
const panel = document.getElementById(config.id);
if (panel && panel.style.display !== 'none') {
const left = startLeft;
const top = startTop + (offsetIndex * verticalSpacing);
const zIndex = 10001 + offsetIndex;
panel.style.left = left + 'px';
panel.style.top = top + 'px';
panel.style.right = 'auto';
panel.style.zIndex = zIndex;
this.savePanelState(panel, config.storageId);
offsetIndex++;
}
});
const playerNames = FlootData.getAllPlayerNames();
playerNames.forEach(playerName => {
const panel = document.getElementById(`lucky-player-panel-${playerName}`);
if (panel && panel.style.display !== 'none') {
const left = startLeft;
const top = startTop + (offsetIndex * verticalSpacing);
const zIndex = 10001 + offsetIndex;
panel.style.left = left + 'px';
panel.style.top = top + 'px';
panel.style.right = 'auto';
panel.style.zIndex = zIndex;
this.savePanelState(panel, `player-${playerName}`);
offsetIndex++;
}
});
}
autoGridPanels() {
const content = document.getElementById('lucky-content');
if (!content) return;
const panelWidth = 160;
const panelHeight = 160;
const startX = 0;
const startY = 0;
const cols = 4;
let panelIndex = 0;
const panelConfigs = [
{ id: 'lucky-stats-panel', storageId: 'stats' },
{ id: 'lucky-revenue-panel', storageId: 'revenue' },
{ id: 'lucky-big-expected-panel', storageId: 'bigExpected' },
{ id: 'lucky-big-luck-panel', storageId: 'bigLuck' },
{ id: 'lucky-mob-expected-panel', storageId: 'mobExpected' },
{ id: 'lucky-totals-panel', storageId: 'totals' }
];
panelConfigs.forEach(config => {
const panel = document.getElementById(config.id);
if (panel && panel.style.display !== 'none') {
const col = panelIndex % cols;
const row = Math.floor(panelIndex / cols);
const x = startX + (col * panelWidth);
const y = startY + (row * panelHeight);
panel.style.width = panelWidth + 'px';
panel.style.height = panelHeight + 'px';
panel.style.left = x + 'px';
panel.style.top = y + 'px';
panel.style.right = 'auto';
panel.style.zIndex = 10001 + panelIndex;
this.savePanelState(panel, config.storageId);
panelIndex++;
}
});
const playerNames = FlootData.getAllPlayerNames();
playerNames.forEach(playerName => {
const panel = document.getElementById(`lucky-player-panel-${playerName}`);
if (panel && panel.style.display !== 'none') {
const col = panelIndex % cols;
const row = Math.floor(panelIndex / cols);
const x = startX + (col * panelWidth);
const y = startY + (row * panelHeight);
panel.style.width = panelWidth + 'px';
panel.style.height = panelHeight + 'px';
panel.style.left = x + 'px';
panel.style.top = y + 'px';
panel.style.right = 'auto';
panel.style.zIndex = 10001 + panelIndex;
this.savePanelState(panel, `player-${playerName}`);
panelIndex++;
}
});
}
applyDefaultGridIfNeeded() {
const panelConfigs = [
{ storageId: 'stats' },
{ storageId: 'revenue' },
{ storageId: 'bigExpected' },
{ storageId: 'bigLuck' },
{ storageId: 'mobExpected' },
{ storageId: 'totals' }
];
let hasSavedState = false;
for (const config of panelConfigs) {
if (this.lyStorage.get(`panel_${config.storageId}`)) {
hasSavedState = true;
break;
}
}
if (!hasSavedState) {
setTimeout(() => {
this.autoGridPanels();
}, 500);
}
}
savePanelState(panel, storageId) {
if (!panel) return;
const state = {
left: panel.style.left,
top: panel.style.top,
width: panel.style.width || panel.offsetWidth + 'px',
height: panel.style.height || panel.offsetHeight + 'px'
};
this.lyStorage.set(`panel_${storageId}`, state);
}
loadPanelState(panel, storageId) {
if (!panel) return;
try {
const state = this.lyStorage.get(`panel_${storageId}`);
if (state) {
if (state.left) panel.style.left = state.left;
if (state.top) panel.style.top = state.top;
if (state.width) panel.style.width = state.width;
if (state.height) panel.style.height = state.height;
panel.style.right = 'auto';
}
} catch (e) {
}
}
destroy() {
if (_luckyWsListener) {
window.removeEventListener('EquipSpyWebSocketMessage', _luckyWsListener);
_luckyWsListener = null;
}
VisibilityManager.clear('lucky-update');
this._cleanupDocListeners();
if (this._luckyResizeObservers) {
this._luckyResizeObservers.forEach(obs => obs.disconnect());
this._luckyResizeObservers = null;
}
if (this.panel) {
this.panel.remove();
}
}
}
let luckyPanelInstance = null;
function initLucky() {
setupLuckyMessageHandlers();
let attempts = 0;
const checkFloot = setInterval(() => {
attempts++;
if (window.lootDropsTrackerInstance) {
clearInterval(checkFloot);
luckyPanelInstance = new LuckyPanel();
luckyPanelInstance.init();
} else if (attempts > 30) {
console.error('[Lucky] Timeout waiting for lootDropsTrackerInstance');
clearInterval(checkFloot);
}
}, 1000);
}
window.luckyPanelInstance = luckyPanelInstance;
if (typeof LootDropsTracker !== 'undefined') {
LootDropsTracker.prototype.createLuckyPanel = function() {
if (!window.luckyPanelInstance) {
setupLuckyMessageHandlers();
window.luckyPanelInstance = new LuckyPanel();
window.luckyPanelInstance.init();
}
};
} else {
console.error('[Lucky] LootDropsTracker not defined!');
}
// Lucky end
// Main start
async function main() {
if (!window.lootDropsTrackerInstance) {
window.lootDropsTrackerInstance = new LootDropsTracker();
}
await loadMarketData();
window.MCS_MODULES_INITIALIZED = true;
VisibilityManager.register('skeleton-loot-overlay', () => {
if (window.lootDropsTrackerInstance) {
const coinItem = window.lootDropsTrackerInstance.spyCharacterItems?.find(
item => item.itemHrid === '/items/coin'
);
if (!window._lastGoldAmount) window._lastGoldAmount = 0;
if (coinItem && coinItem.count !== window._lastGoldAmount) {
window._lastGoldAmount = coinItem.count;
}
}
const overlay = document.getElementById('milt-loot-drops-display');
if (!overlay) return;
const isHidden = overlay.classList.contains('is-hidden');
injectValuesAndSort();
if (!isHidden) {
}
}, 1000);
VisibilityManager.register('skeleton-gold-per-day', () => {
if (typeof updateGoldPerDay === 'function') {
updateGoldPerDay();
}
}, 1000);
VisibilityManager.register('skeleton-hwhat-display', () => {
if (window.lootDropsTrackerInstance && typeof window.lootDropsTrackerInstance.updateHWhatDisplay === 'function') {
window.lootDropsTrackerInstance.updateHWhatDisplay();
}
}, 1000);
}
function injectSuitePanelCSS() {
if (document.getElementById('mcs-suite-panel-styles')) return;
const style = document.createElement('style');
style.id = 'mcs-suite-panel-styles';
style.textContent = `
.mcs-hidden { display: none !important; }
.mcs-suite-header { grid-column: 1 / -1; display: flex; justify-content: center; align-items: center; gap: 8px; padding: 6px 8px 2px 8px; background: rgba(0,0,0,0.3); border-radius: 4px 4px 0 0; font-size: 11px; text-align: center; }
.mcs-suite-char-mode { color: #87CEEB; font-weight: 600; }
.mcs-suite-char-name { grid-column: 1 / -1; margin-bottom: 8px; padding: 2px 8px 6px 8px; background: rgba(0,0,0,0.3); border-radius: 0 0 4px 4px; font-size: 11px; color: #FFD700; text-align: center; }
.mcs-suite-toggle-row { grid-column: 1 / -1; display: flex; justify-content: center; align-items: center; padding: 6px 0; margin-bottom: 6px; border-bottom: 1px solid #444; }
.mcs-suite-toggle-label { display: flex; align-items: center; gap: 6px; cursor: pointer; font-size: 12px; }
.mcs-suite-toggle-text { color: #ccc; }
.mcs-suite-refresh-msg { color: #f44336; font-size: 11px; margin-left: 10px; }
.mcs-suite-gm-available { color: #4CAF50; }
.mcs-suite-gm-unavailable { color: #f44336; }
.mcs-suite-columns { display: flex; gap: 16px; }
.mcs-suite-col { flex: 1; }
.mcs-suite-separator { margin: 10px 0; border: none; border-top: 1px solid #444; }
.mcs-suite-tool-label { display: flex; align-items: center; margin-bottom: 2px; cursor: pointer; user-select: none; }
.mcs-suite-sub-label { display: flex; align-items: center; margin-bottom: 3px; cursor: pointer; user-select: none; margin-left: 16px; font-size: 11px; }
.mcs-suite-checkbox { margin-right: 8px; cursor: pointer; }
.mcs-suite-tool-name { font-size: 13px; }
`;
document.head.appendChild(style);
}
LootDropsTracker.prototype.setupFeedListener();
function startMCS() {
injectSuitePanelCSS();
if (!document.getElementById('equipspy-data-bridge')) {
const bridge = document.createElement('div');
bridge.id = 'equipspy-data-bridge';
bridge.className = 'mcs-data-bridge';
document.body.appendChild(bridge);
}
function waitForCharacter() {
const el = document.querySelector('[data-name]');
const name = el && el.getAttribute('data-name');
if (name) {
const enabled = MCSEnabledStorage.get(name);
if (!enabled) {
window._MCS_DISABLED = true;
console.log('[MCS] Disabled for character:', name);
return;
}
main();
} else {
setTimeout(waitForCharacter, 500);
}
}
waitForCharacter();
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', startMCS);
} else {
startMCS();
}
(function initCombatSuiteButton() {
'use strict';
function waitForDOM() {
if (document.body && document.head) {
createButton();
} else {
setTimeout(waitForDOM, 100);
}
}
function createButton() {
const styles = `
#mwi-combat-suite-btn {
position: fixed;
top: 0px;
background: #808080;
color: white;
border: 1px solid #666666;
border-top: none;
border-radius: 0 0 6px 6px;
padding: 3px 10px;
font-weight: bold;
font-size: 10px;
cursor: move;
z-index: 200000;
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.4);
user-select: none;
transition: background 0.2s ease;
white-space: nowrap;
}
#mwi-combat-suite-btn:hover {
background: #909090;
}
#mwi-combat-suite-btn.dragging {
cursor: grabbing;
opacity: 0.8;
}
#mwi-combat-suite-panel {
position: fixed;
background: rgba(30, 30, 40, 0.95);
border: 2px solid rgba(255, 255, 255, 0.2);
border-radius: 8px;
padding: 16px;
color: white;
z-index: 200001;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.5);
min-width: 200px;
display: none;
}
#mwi-combat-suite-panel.visible {
display: block;
}
.mwi-panel-content {
font-size: 14px;
}
.mcs-emergency-section {
margin-bottom: 8px;
border: 1px solid rgba(255, 80, 80, 0.4);
border-radius: 4px;
overflow: hidden;
}
.mcs-emergency-header {
background: rgba(255, 50, 50, 0.15);
color: #ff6b6b;
padding: 4px 8px;
cursor: pointer;
font-size: 11px;
font-weight: bold;
user-select: none;
}
.mcs-emergency-header:hover {
background: rgba(255, 50, 50, 0.25);
}
.mcs-emergency-body {
padding: 6px 8px;
background: rgba(255, 50, 50, 0.05);
display: flex;
gap: 6px;
}
.mcs-emergency-btn {
background: rgba(255, 60, 60, 0.3);
color: #ff9999;
border: 1px solid rgba(255, 80, 80, 0.5);
border-radius: 3px;
padding: 3px 8px;
font-size: 11px;
cursor: pointer;
}
.mcs-emergency-btn:hover {
background: rgba(255, 60, 60, 0.5);
color: white;
}
`;
const styleSheet = document.createElement('style');
styleSheet.textContent = styles;
document.head.appendChild(styleSheet);
const defaultLeft = Math.floor(window.innerWidth * 0.75);
const savedPosition = JSON.parse(localStorage.getItem('mcs__global_combat_suite_btn_position') || `{"left": ${defaultLeft}}`);
const button = document.createElement('div');
button.id = 'mwi-combat-suite-btn';
button.textContent = 'MWI Combat Suite v0.9.36073 Beta';
const leftVal = typeof savedPosition.left === 'number' ? savedPosition.left : parseInt(savedPosition.left) || defaultLeft;
button.style.left = leftVal + 'px';
document.body.appendChild(button);
registerPanel('mwi-combat-suite-btn');
window.addEventListener('resize', () => {
const suiteBtn = document.getElementById('mwi-combat-suite-btn');
if (!suiteBtn) return;
const rect = suiteBtn.getBoundingClientRect();
const windowWidth = window.innerWidth;
if (rect.left > windowWidth - 100 || rect.right < 50) {
const newLeft = Math.max(20, Math.min(rect.left, windowWidth - 150));
suiteBtn.style.left = newLeft + 'px';
suiteBtn.style.right = 'auto';
localStorage.setItem('mcs__global_combat_suite_btn_position', JSON.stringify({
left: newLeft
}));
}
});
const panel = document.createElement('div');
panel.id = 'mwi-combat-suite-panel';
const tools = [
{ name: 'Shykai Export', panelId: 'shykai-simulator', isSimulator: true },
{ name: 'AMazing', panelId: 'amazing-pane' },
{ name: 'BRead', panelId: 'bread-pane' },
{ name: 'CRack', panelId: 'consumables-pane' },
{ name: 'DPs', panelId: 'dps-pane' },
{ name: 'EWatch', panelId: 'equipment-spy-pane' },
{ name: 'FLoot', panelId: 'milt-loot-drops-display' },
{ name: 'GWhiz', panelId: 'gwhiz-pane' },
{ name: 'HWhat', panelId: 'hwhat-pane' },
{ name: 'IHurt', panelId: 'ihurt-pane' },
{ name: 'JHouse', panelId: 'jhouse-pane' },
{ name: 'KOllection', panelId: 'kollection-pane' },
{ name: 'LYuck', panelId: 'lucky-panel' },
{ name: 'MAna', panelId: 'mana-pane' },
{ name: 'NTally', panelId: 'mcs_nt_pane' },
{ name: 'OPanel', panelId: 'opanel-pane' },
{ name: 'PFormance', panelId: 'pformance-pane' },
{ name: 'QCharm', panelId: 'qcharm-pane' },
{ name: 'SCrolling Combat Text', panelId: 'meaters-pane' },
{ name: 'TReasure', panelId: 'treasure-pane' },
{ name: 'Floating Combat Text', panelId: 'fcb-feature', isFCB: true, isHR: true }
];
let savedStates = {};
let savedStatesLoaded = false;
const loadSavedStates = () => {
const characterName = CharacterDataStorage.getCurrentCharacterName();
if (characterName) {
savedStates = ToolVisibilityStorage.get(characterName);
savedStatesLoaded = true;
return true;
}
return false;
};
function togglePanelVisibility(panelId, visible) {
if (panelId === 'qcharm-pane' && typeof window.lootDropsTrackerInstance !== 'undefined') {
if (visible) {
window.lootDropsTrackerInstance.createQCharmPane();
} else {
window.lootDropsTrackerInstance.destroyQCharmPane();
}
return;
}
const panelElement = document.getElementById(panelId);
if (panelElement) {
if (visible) {
panelElement.classList.remove('mcs-hidden');
if (panelId === 'pformance-pane' && typeof window.lootDropsTrackerInstance !== 'undefined') {
PerformanceMonitor.enabled = true;
StorageMonitor.enabled = true;
if (!window.lootDropsTrackerInstance.pformanceRunning) {
VisibilityManager.register('pformance-cpu-update', () => {
window.lootDropsTrackerInstance.pf_updateCpuDisplay();
window.lootDropsTrackerInstance.pf_updateStorageIODisplay();
}, 1000);
window.lootDropsTrackerInstance.pformanceRunning = true;
}
}
if (panelId === 'mcs_nt_pane' && typeof window.lootDropsTrackerInstance !== 'undefined') {
window.lootDropsTrackerInstance.mcs_nt_renderContent();
}
} else {
panelElement.classList.add('mcs-hidden');
if (panelId === 'pformance-pane') {
PerformanceMonitor.enabled = false;
StorageMonitor.enabled = false;
VisibilityManager.clear('pformance-cpu-update');
if (typeof window.lootDropsTrackerInstance !== 'undefined') {
window.lootDropsTrackerInstance.pformanceRunning = false;
}
}
}
}
}
let visibilityApplied = false;
let _cachedAddonDisabled = false;
function _refreshCachedEnabledState() {
const currentChar = CharacterDataStorage.getCurrentCharacterName();
_cachedAddonDisabled = currentChar ? !MCSEnabledStorage.get(currentChar) : false;
}
function applyVisibilityStates() {
if (visibilityApplied) return;
loadSavedStates();
if (!savedStatesLoaded) return;
_refreshCachedEnabledState();
if (_cachedAddonDisabled) return;
visibilityApplied = true;
tools.forEach(tool => {
const toolKey = tool.name.replace(/\s+/g, '-').toLowerCase();
const isVisible = tool.isSimulator ? (savedStatesLoaded && savedStates[toolKey] !== false) : savedStates[toolKey] === true;
const checkbox = document.querySelector(`input[data-tool-key="${toolKey}"]`);
if (checkbox && checkbox.checked !== isVisible) {
checkbox.checked = isVisible;
}
if (tool.isSimulator) return;
togglePanelVisibility(tool.panelId, isVisible);
});
const flootInventoryCheckbox = document.querySelector('input[data-floot-sub="inventory-value"]');
if (flootInventoryCheckbox) {
const inventoryValueVisible = savedStates['floot-inventory-value'] === true;
if (flootInventoryCheckbox.checked !== inventoryValueVisible) {
flootInventoryCheckbox.checked = inventoryValueVisible;
}
}
const flootFullNumbersCheckbox = document.querySelector('input[data-floot-sub="full-numbers"]');
if (flootFullNumbersCheckbox) {
const fullNumbersVisible = savedStates['floot-full-numbers'] === true;
if (flootFullNumbersCheckbox.checked !== fullNumbersVisible) {
flootFullNumbersCheckbox.checked = fullNumbersVisible;
}
}
const hwhatFullNumbersCheckbox = document.querySelector('input[data-hwhat-sub="full-numbers"]');
if (hwhatFullNumbersCheckbox) {
const hwhatFullNumbersVisible = savedStates['hwhat-full-numbers'] === true;
if (hwhatFullNumbersCheckbox.checked !== hwhatFullNumbersVisible) {
hwhatFullNumbersCheckbox.checked = hwhatFullNumbersVisible;
}
}
}
applyVisibilityStates();
setTimeout(applyVisibilityStates, 500);
setTimeout(applyVisibilityStates, 1500);
setTimeout(applyVisibilityStates, 3000);
setTimeout(applyVisibilityStates, 5000);
window.addEventListener('LootTrackerCharacterData', () => applyVisibilityStates(), { once: true });
const observer = new MutationObserver((mutations) => {
if (!savedStatesLoaded) return;
const disabled = _cachedAddonDisabled;
mutations.forEach((mutation) => {
mutation.addedNodes.forEach((node) => {
if (node.nodeType === 1 && node.id) {
tools.forEach(tool => {
if (tool.isSimulator) return;
if (node.id === tool.panelId) {
if (disabled) {
setTimeout(() => togglePanelVisibility(tool.panelId, false), 100);
} else {
const toolKey = tool.name.replace(/\s+/g, '-').toLowerCase();
const isVisible = savedStates[toolKey] === true;
setTimeout(() => togglePanelVisibility(tool.panelId, isVisible), 100);
}
}
});
}
});
});
});
observer.observe(document.body, {
childList: true,
subtree: true
});
const getCharacterInfo = () => {
try {
const cachedData = CharacterDataStorage.get();
if (cachedData && cachedData.character) {
let mode = null;
if (cachedData.character.gameMode === 'ironcow') {
mode = 'Iron Cow enabled';
} else if (cachedData.character.gameMode === 'standard') {
mode = 'Market Cow enabled';
}
const name = cachedData.character.name || null;
return { mode, name };
}
} catch (e) {
}
return { mode: null, name: null };
};
let characterInfo = getCharacterInfo();
let characterMode = characterInfo.mode || 'Loading...';
let characterName = characterInfo.name || '';
const updateCharacterModeDisplay = () => {
const info = getCharacterInfo();
if (info.mode) {
const modeEl = document.getElementById('mcs-character-mode-display');
if (modeEl) {
modeEl.textContent = info.mode;
}
}
if (info.name) {
const nameEl = document.getElementById('mcs-character-name-display');
if (nameEl) {
nameEl.textContent = info.name;
nameEl.classList.remove('mcs-hidden');
}
const toggle = document.getElementById('mcs-enabled-toggle');
const toolsSection = document.getElementById('mcs-suite-tools-section');
if (toggle && toolsSection) {
const isEnabled = MCSEnabledStorage.get(info.name);
_cachedAddonDisabled = !isEnabled;
toggle.checked = isEnabled;
if (isEnabled) {
toolsSection.classList.remove('mcs-hidden');
} else {
toolsSection.classList.add('mcs-hidden');
}
}
}
};
if (characterMode === 'Loading...') {
setTimeout(updateCharacterModeDisplay, 1000);
setTimeout(updateCharacterModeDisplay, 2000);
setTimeout(updateCharacterModeDisplay, 4000);
}
loadSavedStates();
const addonEnabled = window._MCS_DISABLED ? false : (!characterName || MCSEnabledStorage.get(characterName));
const mainTools = tools.filter(t => !t.isFCB);
const fcbTool = tools.find(t => t.isFCB);
function buildToolHTML(tool) {
const toolKey = tool.name.replace(/\s+/g, '-').toLowerCase();
const isVisible = tool.isSimulator ? (savedStatesLoaded && savedStates[toolKey] !== false) : savedStates[toolKey] === true;
let html = `
<label class="mcs-suite-tool-label">
<input type="checkbox"
data-tool-panel="${tool.panelId}"
data-tool-key="${toolKey}"
${tool.isSimulator ? 'data-simulator="true"' : ''}
${isVisible ? 'checked' : ''}
class="mcs-suite-checkbox">
<span class="mcs-suite-tool-name">${tool.name}</span>
</label>`;
if (tool.name === 'FLoot') {
const inventoryValueVisible = savedStates['floot-inventory-value'] === true;
const fullNumbersVisible = savedStates['floot-full-numbers'] === true;
html += `
<label class="mcs-suite-sub-label">
<input type="checkbox" data-floot-sub="inventory-value" ${inventoryValueVisible ? 'checked' : ''} class="mcs-suite-checkbox">
<span class="mcs-suite-tool-name">Show Inventory Value</span>
</label>
<label class="mcs-suite-sub-label">
<input type="checkbox" data-floot-sub="full-numbers" ${fullNumbersVisible ? 'checked' : ''} class="mcs-suite-checkbox">
<span class="mcs-suite-tool-name">Show Full Numbers</span>
</label>`;
const marketWarningsVisible = savedStates['floot-market-warnings'] !== false;
const inventoryWarningsVisible = savedStates['floot-inventory-warnings'] !== false;
html += `
<label class="mcs-suite-sub-label">
<input type="checkbox" data-floot-sub="market-warnings" ${marketWarningsVisible ? 'checked' : ''} class="mcs-suite-checkbox">
<span class="mcs-suite-tool-name">Market Warnings</span>
</label>
<label class="mcs-suite-sub-label">
<input type="checkbox" data-floot-sub="inventory-warnings" ${inventoryWarningsVisible ? 'checked' : ''} class="mcs-suite-checkbox">
<span class="mcs-suite-tool-name">Inventory Warnings</span>
</label>`;
}
if (tool.name === 'HWhat') {
const hwhatFullNumbersVisible = savedStates['hwhat-full-numbers'] === true;
html += `
<label class="mcs-suite-sub-label">
<input type="checkbox" data-hwhat-sub="full-numbers" ${hwhatFullNumbersVisible ? 'checked' : ''} class="mcs-suite-checkbox">
<span class="mcs-suite-tool-name">Show Full Numbers</span>
</label>`;
}
return html;
}
const half = Math.ceil(mainTools.length / 2);
const leftTools = mainTools.slice(0, half);
const rightTools = mainTools.slice(half);
let fcbHTML = '';
if (fcbTool) {
const fcStorage = createModuleStorage('FC');
const fcbKey = fcbTool.name.replace(/\s+/g, '-').toLowerCase();
const fcbVisible = fcStorage.get('enabled', false) === true;
const enemyDpsVisible = fcStorage.get('enemyd', false) === true;
const playerDpsVisible = fcStorage.get('playerd', false) === true;
const playerNameRecolor = fcStorage.get('playerc', false) === true;
const fcbItems = [
{ label: fcbTool.name, attrs: `data-tool-panel="${fcbTool.panelId}" data-tool-key="${fcbKey}" data-fcb="true"`, checked: fcbVisible },
{ label: 'Enemy DPS Numbers', attrs: 'data-fcb-sub="enemy-dps"', checked: enemyDpsVisible },
{ label: 'Player DPS Numbers', attrs: 'data-fcb-sub="player-dps"', checked: playerDpsVisible },
{ label: 'Player Name Recolor', attrs: 'data-fcb-sub="player-name-recolor"', checked: playerNameRecolor }
];
const fcbLeft = fcbItems.filter((_, i) => i % 2 === 0);
const fcbRight = fcbItems.filter((_, i) => i % 2 === 1);
const fcbItemHTML = (item) => `
<label class="mcs-suite-tool-label">
<input type="checkbox" ${item.attrs} ${item.checked ? 'checked' : ''} class="mcs-suite-checkbox">
<span class="mcs-suite-tool-name">${item.label}</span>
</label>`;
fcbHTML = `
<hr class="mcs-suite-separator">
<div class="mcs-suite-columns">
<div class="mcs-suite-col">${fcbLeft.map(fcbItemHTML).join('')}</div>
<div class="mcs-suite-col">${fcbRight.map(fcbItemHTML).join('')}</div>
</div>`;
}
const contentHTML = `
<div class="mwi-panel-content">
<div class="mcs-emergency-section">
<div class="mcs-emergency-header" id="mcs-emergency-toggle">Emergency ▸</div>
<div class="mcs-emergency-body mcs-hidden" id="mcs-emergency-body">
<button class="mcs-emergency-btn" id="mcs-shrink-lucky-btn">Set LYuck panel to 500x500</button>
<button class="mcs-emergency-btn" id="mcs-center-lucky-btn">Center LYuck in window</button>
</div>
</div>
<div class="mcs-suite-header">
<span id="mcs-character-mode-display" class="mcs-suite-char-mode">${characterMode}</span>
<span id="mcs-gm-storage-display" class="${typeof GM_info !== 'undefined' ? 'mcs-suite-gm-available' : 'mcs-suite-gm-unavailable'}">GM Storage ${typeof GM_info !== 'undefined' ? 'Available' : 'Unavailable'}</span>
</div>
<div id="mcs-character-name-display" class="mcs-suite-char-name ${characterName ? '' : 'mcs-hidden'}">
${characterName}
</div>
<div class="mcs-suite-toggle-row">
<label class="mcs-suite-toggle-label">
<input type="checkbox" id="mcs-enabled-toggle" class="mcs-suite-checkbox" ${addonEnabled ? 'checked' : ''}>
<span class="mcs-suite-toggle-text">Enabled</span>
</label>
<div id="mcs-refresh-msg" class="mcs-suite-refresh-msg mcs-hidden">Refresh to apply</div>
</div>
<div id="mcs-suite-tools-section" ${addonEnabled ? '' : 'class="mcs-hidden"'}>
<div class="mcs-suite-columns">
<div class="mcs-suite-col">${leftTools.map(buildToolHTML).join('')}</div>
<div class="mcs-suite-col">${rightTools.map(buildToolHTML).join('')}</div>
</div>
${fcbHTML}
</div>
</div>
`;
panel.innerHTML = contentHTML;
document.body.appendChild(panel);
registerPanel('mwi-combat-suite-panel');
const emergencyToggle = panel.querySelector('#mcs-emergency-toggle');
const emergencyBody = panel.querySelector('#mcs-emergency-body');
if (emergencyToggle && emergencyBody) {
emergencyToggle.addEventListener('click', () => {
const isHidden = emergencyBody.classList.toggle('mcs-hidden');
emergencyToggle.textContent = isHidden ? 'Emergency ▸' : 'Emergency ▾';
});
}
const shrinkLuckyBtn = panel.querySelector('#mcs-shrink-lucky-btn');
if (shrinkLuckyBtn) {
shrinkLuckyBtn.addEventListener('click', () => {
const luckyPanel = document.getElementById('lucky-panel');
if (luckyPanel) {
luckyPanel.style.width = '500px';
luckyPanel.style.height = '500px';
}
});
}
const centerLuckyBtn = panel.querySelector('#mcs-center-lucky-btn');
if (centerLuckyBtn) {
centerLuckyBtn.addEventListener('click', () => {
const luckyPanel = document.getElementById('lucky-panel');
if (luckyPanel) {
const rect = luckyPanel.getBoundingClientRect();
luckyPanel.style.left = ((window.innerWidth - rect.width) / 2) + 'px';
luckyPanel.style.top = ((window.innerHeight - rect.height) / 2) + 'px';
luckyPanel.style.right = 'auto';
}
});
}
const enabledToggle = panel.querySelector('#mcs-enabled-toggle');
if (enabledToggle) {
enabledToggle.addEventListener('change', (e) => {
const enabled = e.target.checked;
const name = CharacterDataStorage.getCurrentCharacterName();
if (name) {
MCSEnabledStorage.set(name, enabled);
_cachedAddonDisabled = !enabled;
}
const toolsSection = document.getElementById('mcs-suite-tools-section');
const refreshMsg = document.getElementById('mcs-refresh-msg');
if (refreshMsg) refreshMsg.classList.remove('mcs-hidden');
if (enabled) {
if (toolsSection) toolsSection.classList.remove('mcs-hidden');
visibilityApplied = false;
applyVisibilityStates();
} else {
if (toolsSection) toolsSection.classList.add('mcs-hidden');
tools.forEach(tool => {
if (!tool.isSimulator) {
togglePanelVisibility(tool.panelId, false);
}
});
}
});
}
panel.querySelectorAll('input[type="checkbox"]').forEach(checkbox => {
checkbox.addEventListener('change', (e) => {
if (e.target.id === 'mcs-enabled-toggle') return;
const flootSub = e.target.dataset.flootSub;
const fcbSub = e.target.dataset.fcbSub;
if (flootSub) {
const isVisible = e.target.checked;
savedStates[`floot-${flootSub}`] = isVisible;
ToolVisibilityStorage.set(savedStates);
if (flootSub === 'inventory-value') {
if (typeof window.toggleInventoryPrices === 'function') {
window.toggleInventoryPrices(isVisible);
}
} else if (flootSub === 'full-numbers') {
if (typeof window.toggleFlootFullNumbers === 'function') {
window.toggleFlootFullNumbers(isVisible);
}
} else if (flootSub === 'market-warnings') {
if (typeof window.toggleMarketWarnings === 'function') {
window.toggleMarketWarnings(isVisible);
}
} else if (flootSub === 'inventory-warnings') {
if (typeof window.toggleInventoryWarnings === 'function') {
window.toggleInventoryWarnings(isVisible);
}
}
return;
}
const hwhatSub = e.target.dataset.hwhatSub;
if (hwhatSub) {
const isVisible = e.target.checked;
savedStates[`hwhat-${hwhatSub}`] = isVisible;
ToolVisibilityStorage.set(savedStates);
if (hwhatSub === 'full-numbers') {
if (window.lootDropsTrackerInstance) {
window.lootDropsTrackerInstance.hwhatShowFullNumbers = isVisible;
if (typeof window.lootDropsTrackerInstance.updateHWhatDisplay === 'function') {
window.lootDropsTrackerInstance.updateHWhatDisplay();
}
if (window.skeletonInstance && typeof window.skeletonInstance.updateOPanelTotalProfit === 'function') {
window.skeletonInstance.updateOPanelTotalProfit();
}
}
}
return;
}
if (fcbSub) {
const isVisible = e.target.checked;
if (window.lootDropsTrackerInstance) {
if (fcbSub === 'enemy-dps') {
if (typeof window.lootDropsTrackerInstance.fcbToggleEnemyDPS === 'function') {
window.lootDropsTrackerInstance.fcbToggleEnemyDPS(isVisible);
}
} else if (fcbSub === 'player-dps') {
if (typeof window.lootDropsTrackerInstance.fcbTogglePlayerDPS === 'function') {
window.lootDropsTrackerInstance.fcbTogglePlayerDPS(isVisible);
}
} else if (fcbSub === 'player-name-recolor') {
if (typeof window.lootDropsTrackerInstance.fcbTogglePlayerNameRecolor === 'function') {
window.lootDropsTrackerInstance.fcbTogglePlayerNameRecolor(isVisible);
}
}
}
return;
}
const panelId = e.target.dataset.toolPanel;
const toolKey = e.target.dataset.toolKey;
const isSimulator = e.target.dataset.simulator === 'true';
const isFCB = e.target.dataset.fcb === 'true';
const isVisible = e.target.checked;
savedStates[toolKey] = isVisible;
ToolVisibilityStorage.set(savedStates);
if (isSimulator) {
if (isVisible) {
GM_setValue('shykai_simulator_enabled', 'true');
} else {
GM_setValue('shykai_simulator_enabled', 'false');
}
} else if (isFCB) {
if (window.lootDropsTrackerInstance && typeof window.lootDropsTrackerInstance.fcbToggleEnabled === 'function') {
window.lootDropsTrackerInstance.fcbToggleEnabled(isVisible);
}
} else {
togglePanelVisibility(panelId, isVisible);
}
});
});
let panelVisible = false;
let hasMoved = false;
let startX = 0;
let startLeft = 0;
const onBtnDragMove = (e) => {
const deltaX = e.clientX - startX;
if (Math.abs(deltaX) > 5) {
hasMoved = true;
}
const newLeft = startLeft + deltaX;
button.style.left = newLeft + 'px';
if (panelVisible) {
panel.style.left = newLeft + 'px';
}
};
const onBtnDragUp = () => {
document.removeEventListener('mousemove', onBtnDragMove);
document.removeEventListener('mouseup', onBtnDragUp);
const position = {
left: parseInt(button.style.left) || 0
};
localStorage.setItem('mcs__global_combat_suite_btn_position', JSON.stringify(position));
setTimeout(() => {
button.classList.remove('dragging');
if (!hasMoved) {
panelVisible = !panelVisible;
panel.classList.toggle('visible', panelVisible);
if (panelVisible) {
const btnRect = button.getBoundingClientRect();
panel.style.left = btnRect.left + 'px';
panel.style.top = (btnRect.bottom + 5) + 'px';
}
} else if (panelVisible) {
const btnRect = button.getBoundingClientRect();
panel.style.left = btnRect.left + 'px';
panel.style.top = (btnRect.bottom + 5) + 'px';
}
hasMoved = false;
}, 50);
};
button.addEventListener('mousedown', (e) => {
hasMoved = false;
startX = e.clientX;
const rect = button.getBoundingClientRect();
startLeft = rect.left;
button.classList.add('dragging');
document.addEventListener('mousemove', onBtnDragMove);
document.addEventListener('mouseup', onBtnDragUp);
});
document.addEventListener('click', (e) => {
if (panelVisible &&
!button.contains(e.target) &&
!panel.contains(e.target)) {
panelVisible = false;
panel.classList.remove('visible');
}
});
}
waitForDOM();
})();
}
})();