Milky Way Idle - Loot Drops Overlay

Displays combat loot drops in a draggable overlay with column layout

// ==UserScript==
// @name         Milky Way Idle - Loot Drops Overlay
// @namespace    http://tampermonkey.net/
// @version      1.3.1
// @description  Displays combat loot drops in a draggable overlay with column layout
// @author       Kjay
// @license      MIT License
// @match        https://www.milkywayidle.com/*
// @match        https://test.milkywayidle.com/*
// @grant        none
// ==/UserScript==

(function () {
    'use strict';

    const CONFIG = {
        CONTAINER_ID: 'milt-loot-drops-display',
        TOOLTIP_ID: 'milt-loot-drops-tooltip',
        DEFAULT_POS: { top: '120px', right: '30px' },
        STORAGE: {
            position: 'lootDropsPosition',
            hidden: 'lootDropsHidden',
            sort: 'lootDropsSortPref',
            startHidden: 'lootDropsStartHidden',
            sessionHistory: 'lootDropsSessionHistory'
        },
        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'
    };

    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 {
            let historyString = JSON.stringify(history);
            while (historyString.length > 4.0 * 1024 * 1024 && history.length > 0) {
                 history.shift();
                 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);
        };
    };

    class LootDropsTracker {
        constructor() {
            if (document.getElementById(CONFIG.CONTAINER_ID)) {
                return;
            }

            this.userName = null;
            this.playerDropStats = {};
            this.sessionHistory = readSessionHistory();
            this.viewingLive = true;
            this.startHiddenEnabled = localStorage.getItem(CONFIG.STORAGE.startHidden) === 'true';
            this.isHidden = this.startHiddenEnabled || localStorage.getItem(CONFIG.STORAGE.hidden) === '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, sortButton: 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.timerInterval = null;
            this.isLiveSessionActive = false;
            this.sortPreference = localStorage.getItem(CONFIG.STORAGE.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.init();
        }

        init() {
            this.injectCss();
            this.createPanel();
            this.setInitialCheckboxState();
            this.createTooltipElement();
            this.findUserName();
            this.bindUiEvents();
            this.setupFeedListener();
             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; background-color: var(--ldt-bg-primary); color: var(--ldt-text-primary); font-family: sans-serif; font-size: var(--ldt-font-size-base); border: 1px solid var(--ldt-border-primary); 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: inline-flex; align-items: center; gap: 5px; }
                #${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 { background-color: var(--ldt-bg-button-show); 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); background-color: var(--ldt-bg-button-show-hover); }
                .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-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 { color: var(--ldt-text-primary); flex-grow: 1; margin-right: 10px; word-break: break-word; }
                .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;
             this.domRefs.panel = panel;
             let savedPosition = {};
             try {
                savedPosition = JSON.parse(localStorage.getItem(CONFIG.STORAGE.position) || '{}');
             } catch (e) { console.error("LDT: Error parsing saved position", e); }
             panel.style.top = savedPosition.top || CONFIG.DEFAULT_POS.top;
             panel.style.right = savedPosition.right || CONFIG.DEFAULT_POS.right;
             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">
                     <span class="ldt-hide-label">Loot:</span>
                     <button class="ldt-show-btn" id="${CONFIG.CONTAINER_ID}-minbtn-minimized">Show</button>
                 </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">Loot Drops</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-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>
                            <button class="ldt-hide-btn" id="${CONFIG.CONTAINER_ID}-minbtn-expanded">Hide</button>
                        </div>
                    </div>
                    <div class="ldt-header-bottom-row" id="${CONFIG.CONTAINER_ID}-header-bottom">
                        <input type="text" class="ldt-search-input" id="${CONFIG.CONTAINER_ID}-search" placeholder="Highlight...">
                        <div class="ldt-header-controls" id="${CONFIG.CONTAINER_ID}-header-controls-bottom">
                            <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-sort-btn" id="${CONFIG.CONTAINER_ID}-sortbtn">Sort: Count</button>
                            <span class="ldt-timer" id="${CONFIG.CONTAINER_ID}-timer">--:--:--</span>
                        </div>
                    </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`),
                headerBottomRow: panel.querySelector(`#${CONFIG.CONTAINER_ID}-header-bottom`),
                content: panel.querySelector(`#${CONFIG.CONTAINER_ID}-content`),
                showButton: panel.querySelector(`#${CONFIG.CONTAINER_ID}-minbtn-minimized`),
                hideButton: panel.querySelector(`#${CONFIG.CONTAINER_ID}-minbtn-expanded`),
                settingsButton: panel.querySelector(`#${CONFIG.CONTAINER_ID}-settingsbtn`),
                settingsMenu: panel.querySelector(`#${CONFIG.CONTAINER_ID}-settings-menu`),
                startHiddenCheckbox: panel.querySelector(`#${CONFIG.CONTAINER_ID}-starthidden-check`),
                historySelect: panel.querySelector(`#${CONFIG.CONTAINER_ID}-history-select`),
                clearButton: panel.querySelector(`#${CONFIG.CONTAINER_ID}-clearbtn`),
                exportButton: panel.querySelector(`#${CONFIG.CONTAINER_ID}-exportbtn`),
                timerDisplay: panel.querySelector(`#${CONFIG.CONTAINER_ID}-timer`),
                sortButton: panel.querySelector(`#${CONFIG.CONTAINER_ID}-sortbtn`),
                searchInput: panel.querySelector(`#${CONFIG.CONTAINER_ID}-search`)
             });

             this.updateSortButtonText();
        }

        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);
            localStorage.setItem(CONFIG.STORAGE.hidden, String(this.isHidden));
            this.hideSettingsMenu();

            if (!this.isHidden) {
                 this.renderCurrentView();
                 this.updateTimerDisplay();
                 if (this.viewingLive && this.isLiveSessionActive) {
                     this.startTimer();
                 }
            } else {
                 this.stopTimer();
            }
        }

        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) {
            if (!this.domRefs.content || this.isHidden) return;

            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 style="color: var(--ldt-text-secondary);">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>`;
                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" style="font-size:var(--ldt-font-size-small); padding: 5px 0; color: var(--ldt-text-secondary);">No items collected.</li>`;
                } else {
                    sortedItems.forEach(([hrid, count]) => {
                        const name = hrid.replace("/items/", "").replace(/_/g, " ");
                        const itemLi = document.createElement('li');
                        itemLi.className = 'ldt-loot-item-entry';

                        const nameSpan = document.createElement('span');
                        nameSpan.className = 'ldt-item-name';
                        nameSpan.textContent = name;

                        const countSpan = document.createElement('span');
                        countSpan.className = 'ldt-item-count';
                        countSpan.textContent = `× ${count}`;

                        if (searchTerm && name.toLowerCase().includes(searchTerm)) {
                            itemLi.classList.add('highlight-match');
                        }

                        itemLi.appendChild(nameSpan);
                        itemLi.appendChild(countSpan);
                        listDiv.appendChild(itemLi);
                    });
                }
                sectionDiv.appendChild(headerDiv);
                sectionDiv.appendChild(listDiv);
                fragment.appendChild(sectionDiv);
            });

            this.domRefs.content.appendChild(fragment);
        }

        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.replace("/items/", "").replace(/_/g, " ");
                     const safePlayerName = `"${playerName.replace(/"/g, '""')}"`;
                     const safeItemName = `"${itemName.replace(/"/g, '""')}"`;
                     csvRows.push(`${safePlayerName},${safeItemName},${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);
        }

        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);
                 }
            });
            document.addEventListener('mousemove', this.movePanel.bind(this), { passive: true });
            document.addEventListener('mouseup', this.endMove.bind(this));

            this.domRefs.settingsButton.onclick = (e) => { e.stopPropagation(); this.toggleSettingsMenu(); };
            this.domRefs.clearButton.onclick = () => this.clearHistory();
            this.domRefs.exportButton.onclick = () => this.exportLootCsv();
            this.domRefs.showButton.onclick = () => this.toggleVisibility();
            this.domRefs.hideButton.onclick = () => this.toggleVisibility();
            this.domRefs.sortButton.onclick = () => this.cycleSortPreference();

            this.domRefs.startHiddenCheckbox.onchange = () => this.handleStartHiddenChange();
            this.domRefs.historySelect.onchange = () => this.handleHistorySelectionChange();
            this.domRefs.searchInput.addEventListener('input', (e) => {
                 this.currentSearchTerm = e.target.value.trim();
                 this.debouncedRender();
             });

             const tooltipElements = this.domRefs.panel.querySelectorAll('[data-tooltip]');
             tooltipElements.forEach(el => {
                 el.addEventListener('mouseenter', (e) => this.showTooltip(e));
                 el.addEventListener('mouseleave', () => this.hideTooltip());
             });

            document.addEventListener('click', this.handleClickOutsideMenu.bind(this));
            window.addEventListener("LootTrackerCombatEnded", (e) => this.handleSessionEnd(e.detail));
            window.addEventListener("LootTrackerBattle", (e) => this.processBattleUpdate(e.detail));
            window.addEventListener('storage', (e) => this.handleStorageChange(e));
        }

        clearHistory() {
             if (!this.userName) {
                 alert("Cannot clear history: User not identified yet.");
                 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;
                     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();
                             }
                         } else {
                            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;
             localStorage.setItem(CONFIG.STORAGE.startHidden, String(this.startHiddenEnabled));
         }

        showTooltip(event) {
            const targetButton = event.currentTarget;
            let tooltipText = targetButton.dataset.tooltip;

            if (targetButton === this.domRefs.sortButton) {
                tooltipText = `Click to sort by ${this.sortPreference === 'count' ? 'Name' : 'Count'}`;
            } else 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';
            localStorage.setItem(CONFIG.STORAGE.sort, this.sortPreference);
            this.updateSortButtonText();
            this.renderCurrentView();
            this.hideTooltip();
        }

        updateSortButtonText() {
            if (this.domRefs.sortButton) {
                const sortMode = this.sortPreference.charAt(0).toUpperCase() + this.sortPreference.slice(1);
                this.domRefs.sortButton.textContent = `Sort: ${sortMode}`;
            }
        }

        processBattleUpdate(data) {
             if (!data || typeof data !== 'object' || !Array.isArray(data.players) || !data.combatStartTime) {
                 return;
             }
             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);
                 }

                 this.playerDropStats = {};
                 this.currentSessionKey = newSessionKey;
                 this.isLiveSessionActive = true;
                 this.sessionEndTime = null;
                 this.lastKnownActionHrid = null;
                 this.aggregatedHistoryData = null;

                 try {
                     this.startTime = new Date(combatStartTimeString);
                 } catch (e) {
                     this.startTime = new Date();
                 }


                 statsUpdated = this.updatePlayerStatsFromEvent(data);
                 this.updateTimerDisplay();
                 if (!this.isHidden) 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.isHidden) 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;
             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
                    };

                    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);
                }
            }
        }

        handleStorageChange(event) {
            if (event.key === CONFIG.STORAGE.startHidden && event.newValue !== null) {
                const newPref = event.newValue === 'true';
                if (newPref !== this.startHiddenEnabled) {
                    this.startHiddenEnabled = newPref;
                    this.setInitialCheckboxState();
                }
            }
             if (event.key === CONFIG.STORAGE.sort && event.newValue !== null) {
                 if (event.newValue !== this.sortPreference) {
                     this.sortPreference = event.newValue === 'name' ? 'name' : 'count';
                     this.updateSortButtonText();
                     if (!this.isHidden) this.renderCurrentView();
                 }
             }
             if (event.key === CONFIG.STORAGE.position && event.newValue !== null) {
                 try {
                     const newPos = JSON.parse(event.newValue);
                     if (newPos && newPos.top && newPos.right && this.domRefs.panel) {
                        if (this.domRefs.panel.style.top !== newPos.top || this.domRefs.panel.style.right !== newPos.right) {
                            this.domRefs.panel.style.top = newPos.top;
                            this.domRefs.panel.style.right = newPos.right;
                        }
                     }
                 } catch (e) { console.error("LDT: Error syncing position from storage", e); }
             }

            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.viewingLive || !this.isLiveSessionActive || this.isHidden) {
                 this.updateTimerDisplay();
                 return;
            }

            this.updateTimerDisplay();
            this.timerInterval = setInterval(() => {
                 if (this.startTime && this.viewingLive && this.isLiveSessionActive && !this.isHidden) {
                     this.updateTimerDisplay();
                 } else {
                     this.stopTimer();
                     this.updateTimerDisplay();
                 }
             }, 1000);
        }

        stopTimer() {
            if (this.timerInterval) {
                 clearInterval(this.timerInterval);
                 this.timerInterval = null;
             }
        }

        updateTimerDisplay() {
            if (!this.domRefs.timerDisplay || this.isHidden) return;

            const selectedValue = this.domRefs.historySelect?.value;

            if (this.viewingLive || selectedValue === 'live') {
                if (!this.startTime) {
                    this.domRefs.timerDisplay.textContent = '--:--:--';
                } else if (!this.isLiveSessionActive && this.sessionEndTime) {
                    const duration = this.sessionEndTime - this.startTime.getTime();
                    this.domRefs.timerDisplay.textContent = `(${this.formatElapsedTime(duration)}) Ended`;
                } else if (this.isLiveSessionActive) {
                    const elapsedMs = Date.now() - this.startTime.getTime();
                    this.domRefs.timerDisplay.textContent = this.formatElapsedTime(elapsedMs);
                } else {
                     this.domRefs.timerDisplay.textContent = `(${this.formatElapsedTime(0)}) Waiting...`;
                }
            } else if (selectedValue === 'combined') {
                 if (this.aggregatedHistoryData === null) this.aggregateSessionHistory();
                 this.domRefs.timerDisplay.textContent = `(Σ ${this.formatElapsedTime(this.aggregatedHistoryDuration)})`;
            } else {
                 const selectedSession = this.getCurrentHistoryViewItem();
                 if (selectedSession) {
                     this.domRefs.timerDisplay.textContent = `(${this.formatElapsedTime(selectedSession.duration)})`;
                 } else {
                    this.domRefs.timerDisplay.textContent = '??:??:??';
                 }
            }
        }

        formatElapsedTime(ms) {
            if (ms === null || ms === undefined || ms < 0 || isNaN(ms)) ms = 0;
            let totalSeconds = Math.floor(ms / 1000);
            const days = Math.floor(totalSeconds / 86400);
            totalSeconds %= 86400;
            const hours = Math.floor(totalSeconds / 3600);
            totalSeconds %= 3600;
            const minutes = Math.floor(totalSeconds / 60);
            const seconds = totalSeconds % 60;

            const hoursStr = String(hours).padStart(2, '0');
            const minutesStr = String(minutes).padStart(2, '0');
            const secondsStr = String(seconds).padStart(2, '0');

            if (days > 0) {
                return `${days}d ${hoursStr}:${minutesStr}:${secondsStr}`;
            } else {
                return `${hoursStr}:${minutesStr}:${secondsStr}`;
            }
        }

        startMove(event) {
             if (this.isMoving) return;
             this.isMoving = true;
             const panelRect = this.domRefs.panel.getBoundingClientRect();
             this.moveOffset.y = event.clientY - panelRect.top;
             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();
        }

        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 = '';
        }

        endMove() {
             if (!this.isMoving) return;
             this.isMoving = false;
             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 };
                 try {
                     localStorage.setItem(CONFIG.STORAGE.position, JSON.stringify(position));
                 } catch (e) {
                     console.error("LDT: Failed to save panel position.", e);
                 }
             } 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;
                    this.updateHistoryDropdown();
                    if (!this.isHidden) this.renderCurrentView();
                    this.aggregateHistoryData = null;
                } else {
                    setTimeout(attemptFind, 2000);
                }
            };
            attemptFind();
        }

        setupFeedListener() {
            const flag = CONFIG.INJECTION_FLAG;
            if (window[flag]) {
                return;
            }
            window[flag] = true;

            const script = document.createElement('script');
            script.textContent = `
                (function() {
                    const flag = '${flag}';
                    const lootDropsWsKey = 'OriginalWebSocket_' + flag;

                    if (window[lootDropsWsKey]) {
                        return;
                    }
                    window[lootDropsWsKey] = window.WebSocket;

                    window.WebSocket = class LootDropsWebSocket extends window[lootDropsWsKey] {
                        constructor(...args) {
                            super(...args);
                            this._lootDropsListener = (event) => {
                                let data;
                                try {
                                    if (this.readyState !== 1 || typeof event.data !== 'string' || !event.data.startsWith('{')) return;
                                    data = JSON.parse(event.data);

                                    if (data?.type === "new_battle" && Array.isArray(data.players) && data.combatStartTime) {
                                        window.dispatchEvent(new CustomEvent("LootTrackerBattle", { detail: data }));
                                    }

                                    let combatEnded = false;
                                    let completedActionHrid = null;

                                    if (data?.type === "actions_updated" && Array.isArray(data.endCharacterActions)) {
                                        const completedCombatAction = data.endCharacterActions.find(action =>
                                            action?.actionHrid?.startsWith("/actions/combat/") && action.isDone === true
                                        );
                                        if (completedCombatAction) {
                                            combatEnded = true;
                                            completedActionHrid = completedCombatAction.actionHrid;
                                        }
                                    }
                                    else if (data?.type === "cancel_character_action" && data?.cancelCharacterActionData?.characterActionId) {
                                       combatEnded = true;
                                       completedActionHrid = data?.cancelCharacterActionData?.actionHrid || null;
                                    }

                                    if (combatEnded) {
                                        window.dispatchEvent(new CustomEvent("LootTrackerCombatEnded", {
                                            detail: { actionHrid: completedActionHrid }
                                        }));
                                    }

                                } catch (e) {
                                     if (!(e instanceof SyntaxError)) { console.error("LDT Inject: Error processing message:", e, event.data?.substring(0, 500)); }
                                }
                            };
                            this.addEventListener('message', this._lootDropsListener);
                        }

                        close(...args) {
                            try {
                                this.removeEventListener('message', this._lootDropsListener);
                            } catch (e) { console.error("LDT Inject: Error removing listener on close", e); }
                            super.close(...args);
                        }
                    };
                })();
            `;
            (document.documentElement || document.body).appendChild(script);
            setTimeout(() => { if (script.parentNode) script.parentNode.removeChild(script); }, 100);
        }

    }

    function main() {
        if (!window.lootDropsTrackerInstance) {
             window.lootDropsTrackerInstance = new LootDropsTracker();
        }
    }

    if (document.readyState === 'loading') {
        document.addEventListener('DOMContentLoaded', main);
    } else {
        main();
    }

})();