✨ Points Museum - Fixed

Points Museum with item images in suggestions - Fixed memory leaks and refresh issues

スクリプトをインストールするには、Tampermonkey, GreasemonkeyViolentmonkey のような拡張機能のインストールが必要です。

You will need to install an extension such as Tampermonkey to install this script.

スクリプトをインストールするには、TampermonkeyViolentmonkey のような拡張機能のインストールが必要です。

スクリプトをインストールするには、TampermonkeyUserscripts のような拡張機能のインストールが必要です。

このスクリプトをインストールするには、Tampermonkeyなどの拡張機能をインストールする必要があります。

このスクリプトをインストールするには、ユーザースクリプト管理ツールの拡張機能をインストールする必要があります。

(ユーザースクリプト管理ツールは設定済みなのでインストール!)

このスタイルをインストールするには、Stylusなどの拡張機能をインストールする必要があります。

このスタイルをインストールするには、Stylus などの拡張機能をインストールする必要があります。

このスタイルをインストールするには、Stylus tなどの拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

(ユーザースタイル管理ツールは設定済みなのでインストール!)

このスクリプトの質問や評価の投稿はこちら通報はこちらへお寄せください
// ==UserScript==
// @name         ✨ Points Museum - Fixed
// @namespace    http://tampermonkey.net/
// @version      14.5.0
// @description  Points Museum with item images in suggestions - Fixed memory leaks and refresh issues
// @match        https://www.torn.com/*
// @grant        GM_addStyle
// @grant        GM_getValue
// @grant        GM_setValue
// @grant        GM_xmlhttpRequest
// @run-at       document-end
// ==/UserScript==

(function() {
    'use strict';

    // Check if already initialized
    if (window.__pointsMuseumFixedInitialized) {
        console.log('Points Museum already running, skipping duplicate');
        return;
    }
    window.__pointsMuseumFixedInitialized = true;

    const UID = 'pm_' + Math.random().toString(36).substr(2, 8);
    const PREFIX = `pts_museum_${UID}`;

    function id(name) {
        return `${PREFIX}_${name}`;
    }

    // ================= CONFIG =================
    const THEME_KEY = "pts_museum_theme_preference";
    const POLL = 120000;
    const PRE_PTS = 25,
        FLO_PTS = 10,
        PLU_PTS = 10;
    const CATEGORY_ORDER_KEY = 'pts_museum_category_order';
    const COLLAPSED_CATEGORIES_KEY = 'pts_museum_collapsed_categories';
    const MUSEUM_DAY_MONTH = 5;
    const MUSEUM_DAY_DATE = 18;
    const POINTS_PRICE_CACHE_DURATION = 600000;

    const DEFAULT_CATEGORY_ORDER = ['Prehistoric', 'Flowers', 'Plushies', 'Special'];

    let currentPointsPrice = 0;
    let pointsPriceCache = {
        time: 0,
        price: 0
    };
    let isMuseumDay = false;
    let daysToMuseumDay = 0;
    let isPanelOpen = false;
    let refreshTimer = null;
    let isLoading = false;
    let isDestroyed = false;

    // Store event listeners for cleanup
    const eventListeners = new Map();
    let urlObserver = null;

    // ================= DATA =================
    const GROUPS = {
        Prehistoric: {
            name: "Prehistoric",
            emoji: "🦕",
            pts: PRE_PTS,
            bonusPts: PRE_PTS * 0.10,
            items: {
                "Quartz Point": {
                    s: "Q",
                    flag: "🇨🇦",
                    id: 1504
                },
                "Chalcedony Point": {
                    s: "CH",
                    flag: "🇦🇷",
                    id: 1503
                },
                "Basalt Point": {
                    s: "B",
                    flag: "🏝️",
                    id: 1502
                },
                "Quartzite Point": {
                    s: "QZ",
                    flag: "🇿🇦",
                    id: 1500
                },
                "Chert Point": {
                    s: "CT",
                    flag: "🇬🇧",
                    id: 1501
                },
                "Obsidian Point": {
                    s: "O",
                    flag: "🇲🇽",
                    id: 1499
                }
            }
        },
        Flowers: {
            name: "Flowers",
            emoji: "🌸",
            pts: FLO_PTS,
            bonusPts: FLO_PTS * 0.10,
            items: {
                "Dahlia": {
                    s: "DH",
                    flag: "🇲🇽",
                    id: 260
                },
                "Orchid": {
                    s: "OR",
                    flag: "🏝️",
                    id: 264
                },
                "African Violet": {
                    s: "V",
                    flag: "🇿🇦",
                    id: 282
                },
                "Cherry Blossom": {
                    s: "CB",
                    flag: "🇯🇵",
                    id: 277
                },
                "Peony": {
                    s: "P",
                    flag: "🇨🇳",
                    id: 276
                },
                "Ceibo Flower": {
                    s: "CE",
                    flag: "🇦🇷",
                    id: 271
                },
                "Edelweiss": {
                    s: "E",
                    flag: "🇨🇭",
                    id: 272
                },
                "Crocus": {
                    s: "CR",
                    flag: "🇨🇦",
                    id: 263
                },
                "Heather": {
                    s: "H",
                    flag: "🇬🇧",
                    id: 267
                },
                "Tribulus Omanense": {
                    s: "T",
                    flag: "🇦🇪",
                    id: 385
                },
                "Banana Orchid": {
                    s: "BO",
                    flag: "🇰🇾",
                    id: 617
                }
            }
        },
        Plushies: {
            name: "Plushies",
            emoji: "🧸",
            pts: PLU_PTS,
            bonusPts: PLU_PTS * 0.10,
            items: {
                "Sheep Plushie": {
                    s: "SH",
                    flag: "🏝️",
                    id: 186
                },
                "Teddy Bear Plushie": {
                    s: "TB",
                    flag: "🏝️",
                    id: 187
                },
                "Kitten Plushie": {
                    s: "KT",
                    flag: "🏝️",
                    id: 215
                },
                "Jaguar Plushie": {
                    s: "J",
                    flag: "🇲🇽",
                    id: 258
                },
                "Wolverine Plushie": {
                    s: "W",
                    flag: "🇨🇦",
                    id: 261
                },
                "Nessie Plushie": {
                    s: "N",
                    flag: "🇬🇧",
                    id: 266
                },
                "Red Fox Plushie": {
                    s: "F",
                    flag: "🇬🇧",
                    id: 268
                },
                "Monkey Plushie": {
                    s: "M",
                    flag: "🇦🇷",
                    id: 269
                },
                "Chamois Plushie": {
                    s: "CM",
                    flag: "🇨🇭",
                    id: 273
                },
                "Panda Plushie": {
                    s: "PD",
                    flag: "🇨🇳",
                    id: 274
                },
                "Lion Plushie": {
                    s: "L",
                    flag: "🇿🇦",
                    id: 281
                },
                "Camel Plushie": {
                    s: "CA",
                    flag: "🇦🇪",
                    id: 384
                },
                "Stingray Plushie": {
                    s: "SR",
                    flag: "🇰🇾",
                    id: 618
                }
            }
        }
    };

    // Safe event listener addition
    function addSafeEventListener(element, event, handler) {
        if (!element) return;
        const key = `${event}_${Date.now()}_${Math.random()}`;
        element.addEventListener(event, handler);
        if (!eventListeners.has(element)) {
            eventListeners.set(element, []);
        }
        eventListeners.get(element).push({
            event,
            handler,
            key
        });
    }

    // Cleanup function
    function destroy() {
        if (isDestroyed) return;
        isDestroyed = true;

        // Clear timers
        if (refreshTimer) {
            clearTimeout(refreshTimer);
            refreshTimer = null;
        }

        // Remove all event listeners
        for (const [element, listeners] of eventListeners) {
            if (element && element.removeEventListener) {
                listeners.forEach(({
                    event,
                    handler
                }) => {
                    element.removeEventListener(event, handler);
                });
            }
        }
        eventListeners.clear();

        // Disconnect observers
        if (urlObserver) {
            urlObserver.disconnect();
            urlObserver = null;
        }

        // Remove DOM elements
        const container = document.getElementById(id('edge_container'));
        if (container) container.remove();

        // Remove toasts
        document.querySelectorAll(`.${PREFIX}_toast`).forEach(toast => toast.remove());
    }

    // Cleanup on page unload
    window.addEventListener('beforeunload', () => {
        destroy();
    });

    // Watch for navigation changes (SPA)
    let lastUrl = location.href;
    urlObserver = new MutationObserver(() => {
        if (location.href !== lastUrl) {
            lastUrl = location.href;
            // Don't destroy, just reset state
            isLoading = false;
            if (refreshTimer) {
                clearTimeout(refreshTimer);
                refreshTimer = null;
            }
            // Re-initialize for new page
            setTimeout(() => {
                if (!isDestroyed) {
                    mainLoop();
                }
            }, 1000);
        }
    });
    urlObserver.observe(document, {
        subtree: true,
        childList: true
    });

    function getItemImageUrl(itemId) {
        return `https://www.torn.com/images/items/${itemId}/small.png`;
    }

    function formatMoney(value) {
        if (value === undefined || value === null) return '$0';
        return '$' + Math.round(value).toLocaleString();
    }

    function calculateDaysToMuseumDay() {
        const today = new Date();
        const currentYear = today.getFullYear();
        let museumDay = new Date(currentYear, MUSEUM_DAY_MONTH - 1, MUSEUM_DAY_DATE);

        if (today > museumDay) {
            museumDay = new Date(currentYear + 1, MUSEUM_DAY_MONTH - 1, MUSEUM_DAY_DATE);
        }

        const diffTime = museumDay - today;
        const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24));
        return diffDays;
    }

    function checkMuseumDay() {
        const now = new Date();
        isMuseumDay = (now.getMonth() + 1 === MUSEUM_DAY_MONTH && now.getDate() === MUSEUM_DAY_DATE);
        daysToMuseumDay = calculateDaysToMuseumDay();
        return isMuseumDay;
    }

    // ================= Get limiting item for each category =================
    function getLimitingItem(inventory, group) {
        const quantities = [];
        for (const [name, data] of Object.entries(group.items)) {
            quantities.push({
                name: name,
                id: data.id,
                quantity: inventory[name] || 0
            });
        }
        const minQty = Math.min(...quantities.map(q => q.quantity));
        const limitingItems = quantities.filter(q => q.quantity === minQty);
        return limitingItems[0];
    }

    // ================= UI =================
    function getCategoryOrder() {
        const saved = GM_getValue(CATEGORY_ORDER_KEY, DEFAULT_CATEGORY_ORDER);
        const validOrder = saved.filter(cat => cat === 'Prehistoric' || cat === 'Flowers' || cat === 'Plushies' || cat === 'Special');
        DEFAULT_CATEGORY_ORDER.forEach(cat => {
            if (!validOrder.includes(cat)) validOrder.push(cat);
        });
        return validOrder;
    }

    function saveCategoryOrder(order) {
        GM_setValue(CATEGORY_ORDER_KEY, order);
    }

    function getCollapsedCategories() {
        return GM_getValue(COLLAPSED_CATEGORIES_KEY, []);
    }

    function saveCollapsedCategory(category, isCollapsed) {
        let collapsed = getCollapsedCategories();
        if (isCollapsed && !collapsed.includes(category)) collapsed.push(category);
        else if (!isCollapsed && collapsed.includes(category)) collapsed = collapsed.filter(c => c !== category);
        GM_setValue(COLLAPSED_CATEGORIES_KEY, collapsed);
    }

    function toggleCategory(category) {
        saveCollapsedCategory(category, !getCollapsedCategories().includes(category));
        render();
    }

    function moveCategoryUp(category) {
        const order = getCategoryOrder();
        const index = order.indexOf(category);
        if (index > 0) {
            [order[index - 1], order[index]] = [order[index], order[index - 1]];
            saveCategoryOrder(order);
            render();
        }
    }

    function moveCategoryDown(category) {
        const order = getCategoryOrder();
        const index = order.indexOf(category);
        if (index < order.length - 1) {
            [order[index], order[index + 1]] = [order[index + 1], order[index]];
            saveCategoryOrder(order);
            render();
        }
    }

    function applyTheme(theme) {
        const container = document.getElementById(id('edge_container'));
        if (!container) return;

        container.classList.remove(`${PREFIX}_dark_edge`);
        container.classList.remove(`${PREFIX}_light_edge`);

        if (theme === 'light') {
            container.classList.add(`${PREFIX}_light_edge`);
        } else {
            container.classList.add(`${PREFIX}_dark_edge`);
        }

        GM_setValue(THEME_KEY, theme);
    }

    function toggleTheme() {
        const container = document.getElementById(id('edge_container'));
        if (!container) return;

        const isDark = container.classList.contains(`${PREFIX}_dark_edge`);
        const newTheme = isDark ? 'light' : 'dark';
        applyTheme(newTheme);
        showToast(isDark ? "🎨 Light theme" : "🎨 Dark theme");
    }

    function loadSavedTheme() {
        const savedTheme = GM_getValue(THEME_KEY, 'dark');
        applyTheme(savedTheme);
    }

    function showToast(message, duration = 2000) {
        const existing = document.querySelector(`.${PREFIX}_toast`);
        if (existing) existing.remove();
        const toast = document.createElement('div');
        toast.className = `${PREFIX}_toast`;
        toast.textContent = message;
        document.body.appendChild(toast);
        setTimeout(() => toast.remove(), duration);
    }

    function updateLastUpdateTime() {
        const el = document.getElementById(id('last_update'));
        if (el) {
            const now = new Date();
            el.textContent = now.toLocaleTimeString();
        }
    }

    function initEvents() {
        const panel = document.getElementById(id('edge_panel'));
        const pullTab = document.getElementById(id('pull_tab'));
        const modalOverlay = document.getElementById(id('modal_overlay'));

        const closePanelBtn = document.getElementById(id('close_panel'));
        if (closePanelBtn) {
            addSafeEventListener(closePanelBtn, 'click', () => {
                isPanelOpen = false;
                if (panel) panel.classList.remove(`${PREFIX}_open`);
                if (pullTab) {
                    const icon = pullTab.querySelector(`.${PREFIX}_tab_icon`);
                    if (icon) icon.textContent = '▶';
                }
            });
        }

        if (pullTab) {
            addSafeEventListener(pullTab, 'click', () => {
                isPanelOpen = !isPanelOpen;
                if (panel) {
                    if (isPanelOpen) {
                        panel.classList.add(`${PREFIX}_open`);
                        const icon = pullTab.querySelector(`.${PREFIX}_tab_icon`);
                        if (icon) icon.textContent = '◀';
                    } else {
                        panel.classList.remove(`${PREFIX}_open`);
                        const icon = pullTab.querySelector(`.${PREFIX}_tab_icon`);
                        if (icon) icon.textContent = '▶';
                    }
                }
            });
        }

        const refreshBtn = document.getElementById(id('refresh_panel_btn'));
        if (refreshBtn) {
            addSafeEventListener(refreshBtn, 'click', () => {
                showToast("⟳ Refreshing...");
                forceRefresh();
            });
        }

        const settingsBtn = document.getElementById(id('settings_btn'));
        if (settingsBtn && modalOverlay) {
            addSafeEventListener(settingsBtn, 'click', () => {
                modalOverlay.style.display = 'flex';
                document.body.style.overflow = 'hidden';
                const input = document.getElementById(id('api_input'));
                if (input) input.value = GM_getValue('tornAPIKey', '');
            });
        }

        const closeModal = () => {
            if (modalOverlay) modalOverlay.style.display = 'none';
            document.body.style.overflow = '';
        };

        const modalCloseBtn = document.getElementById(id('modal_close_btn'));
        if (modalCloseBtn) addSafeEventListener(modalCloseBtn, 'click', closeModal);

        if (modalOverlay) {
            addSafeEventListener(modalOverlay, 'click', (e) => {
                if (e.target === modalOverlay) closeModal();
            });
        }

        addSafeEventListener(document, 'keydown', (e) => {
            if (e.key === 'Escape' && modalOverlay && modalOverlay.style.display === 'flex') closeModal();
        });

        const themeToggle = document.getElementById(id('theme_toggle'));
        if (themeToggle) {
            addSafeEventListener(themeToggle, 'click', () => {
                toggleTheme();
                closeModal();
            });
        }

        const resetBtn = document.getElementById(id('reset_btn'));
        if (resetBtn) {
            addSafeEventListener(resetBtn, 'click', () => {
                if (confirm('Delete ALL data?')) {
                    GM_setValue('tornAPIKey', '');
                    GM_setValue(THEME_KEY, '');
                    GM_setValue(CATEGORY_ORDER_KEY, '');
                    GM_setValue(COLLAPSED_CATEGORIES_KEY, '');
                    showToast('All data reset. Refresh page.');
                    setTimeout(() => location.reload(), 1500);
                }
                closeModal();
            });
        }

        const apiSaveBtn = document.getElementById(id('api_save'));
        if (apiSaveBtn) {
            addSafeEventListener(apiSaveBtn, 'click', () => {
                const input = document.getElementById(id('api_input'));
                const key = input ? input.value.trim() : '';
                if (key && key.length === 16) {
                    GM_setValue('tornAPIKey', key);
                    showToast("🔑 API Key saved!");
                    closeModal();
                    forceRefresh();
                } else {
                    showToast("❌ API key must be 16 characters");
                }
            });
        }

        const inventoryToggle = document.getElementById(id('inventory_toggle'));
        const inventoryContent = document.getElementById(id('inventory_content'));
        if (inventoryToggle && inventoryContent) {
            addSafeEventListener(inventoryToggle, 'click', () => {
                const isVisible = inventoryContent.style.display !== 'none';
                inventoryContent.style.display = isVisible ? 'none' : 'block';
                const toggleIcon = inventoryToggle.querySelector(`.${PREFIX}_toggle_icon`);
                if (toggleIcon) toggleIcon.textContent = isVisible ? '▶' : '▼';
            });
        }
    }

    function createHaloUI() {
        if (document.getElementById(id('edge_container'))) return;

        const container = document.createElement("div");
        container.id = id('edge_container');
        container.className = `${PREFIX}_edge_container ${PREFIX}_dark_edge`;
        container.innerHTML = `
        <div class="${PREFIX}_pull_tab ${PREFIX}_pull_tab_right" id="${id('pull_tab')}">
            <div class="${PREFIX}_tab_icon">▶</div>
            <div class="${PREFIX}_tab_text">PTS</div>
            <div class="${PREFIX}_tab_stats" id="${id('tab_sets')}">0</div>
        </div>
        <div class="${PREFIX}_edge_panel ${PREFIX}_edge_panel_right" id="${id('edge_panel')}">
            <div class="${PREFIX}_panel_header">
                <div class="${PREFIX}_brand"><span class="${PREFIX}_star">✨</span><span>POINTS MUSEUM</span></div>
                <div class="${PREFIX}_header_buttons">
                    <button class="${PREFIX}_refresh_btn_panel" id="${id('refresh_panel_btn')}" title="Refresh">⟳</button>
                    <button class="${PREFIX}_settings_btn" id="${id('settings_btn')}" title="Settings">⚙</button>
                    <button class="${PREFIX}_close_panel" id="${id('close_panel')}" title="Close">✕</button>
                </div>
            </div>
            <div class="${PREFIX}_panel_inner">
                <div class="${PREFIX}_quick_stats">
                    <div class="${PREFIX}_stat_item"><div class="${PREFIX}_stat_number" id="${id('stat_sets')}">0</div><div class="${PREFIX}_stat_label">Sets</div></div>
                    <div class="${PREFIX}_stat_item"><div class="${PREFIX}_stat_number" id="${id('stat_points')}">0</div><div class="${PREFIX}_stat_label">Points</div></div>
                    <div class="${PREFIX}_stat_item"><div class="${PREFIX}_stat_number" id="${id('stat_value')}">$0</div><div class="${PREFIX}_stat_label">Value</div></div>
                </div>
                
                <div class="${PREFIX}_highlight_bar" id="${id('highlight_bar')}">
                    <div class="${PREFIX}_highlight_text">-- days 🏛️ 10% -- points @ --</div>
                </div>
                
                <div class="${PREFIX}_suggestions_section">
                    <div class="${PREFIX}_suggestions_content" id="${id('suggestions_content')}">
                        <div class="${PREFIX}_loading">Loading...</div>
                    </div>
                </div>
                
                <div class="${PREFIX}_inventory_section">
                    <div class="${PREFIX}_section_header clickable" id="${id('inventory_toggle')}">
                        <span>📦 INVENTORY</span>
                        <span class="${PREFIX}_toggle_icon">▼</span>
                    </div>
                    <div class="${PREFIX}_inventory_content" id="${id('inventory_content')}">
                        <div id="${id('items_container')}"><div class="${PREFIX}_loading">Loading...</div></div>
                    </div>
                </div>
                
                <div class="${PREFIX}_footer">
                    <span class="${PREFIX}_update_time" id="${id('last_update')}">--:--:--</span>
                    <a href="https://www.torn.com/profiles.php?XID=2637223" target="_blank">❤️</a>
                </div>
            </div>
        </div>
        <div class="${PREFIX}_modal_overlay" id="${id('modal_overlay')}" style="display: none;">
            <div class="${PREFIX}_modal">
                <div class="${PREFIX}_modal_header"><span>⚙ Settings</span><button class="${PREFIX}_modal_close" id="${id('modal_close_btn')}">✕</button></div>
                <div class="${PREFIX}_modal_body">
                    <div class="${PREFIX}_settings_field"><label>API Key (16 chars)</label><input type="password" id="${id('api_input')}" placeholder="Enter Torn API key" class="${PREFIX}_input"><button id="${id('api_save')}" class="${PREFIX}_btn_primary">Save</button></div>
                    <div class="${PREFIX}_settings_field"><label>Appearance</label><button id="${id('theme_toggle')}" class="${PREFIX}_btn_secondary">🌓 Toggle Theme</button></div>
                    <div class="${PREFIX}_settings_field"><label style="color: var(--pts-danger);">Danger</label><button id="${id('reset_btn')}" class="${PREFIX}_btn_danger">⚠ Reset All</button></div>
                </div>
            </div>
        </div>
    `;
        document.body.appendChild(container);
    }

    async function localItems() {
        const key = GM_getValue('tornAPIKey');
        if (!key) throw new Error('No API key');
        const response = await fetch(`https://api.torn.com/user/?selections=display&key=${key}`).then(r => r.json());
        if (response.error) throw new Error(response.error.error || 'API Error');
        const items = {},
            prices = {};
        if (response.display && Array.isArray(response.display)) {
            response.display.forEach(item => {
                items[item.name] = (items[item.name] || 0) + item.quantity;
                prices[item.name] = item.market_price || 0;
            });
        }
        return {
            items,
            prices
        };
    }

    function gmJSON(url) {
        return new Promise(resolve => {
            GM_xmlhttpRequest({
                method: 'GET',
                url,
                onload: r => {
                    try {
                        resolve(JSON.parse(r.responseText));
                    } catch {
                        resolve({});
                    }
                },
                onerror: () => resolve({})
            });
        });
    }

    async function abroadItems() {
        const [yataData] = await Promise.all([gmJSON('https://yata.yt/api/v1/travel/export/')]);
        const abroadMap = {};
        if (yataData && yataData.stocks) {
            Object.values(yataData.stocks).forEach(country => {
                if (country && country.stocks) {
                    country.stocks.forEach(item => {
                        abroadMap[item.name] = (abroadMap[item.name] || 0) + (item.quantity || 0);
                    });
                }
            });
        }
        return abroadMap;
    }

    async function fetchPointsPrice(apiKey) {
        const now = Date.now();
        if (pointsPriceCache.time && (now - pointsPriceCache.time) < POINTS_PRICE_CACHE_DURATION) return pointsPriceCache.price;
        try {
            const response = await fetch(`https://api.torn.com/v2/market/pointsmarket?key=${apiKey}`);
            const data = await response.json();
            if (data && data.pointsmarket) {
                const listings = Object.values(data.pointsmarket).filter(l => l.quantity > 0).map(l => l.cost).sort((a, b) => a - b);
                if (listings.length > 0) {
                    const avgPrice = Math.round(listings.slice(0, 5).reduce((s, p) => s + p, 0) / Math.min(5, listings.length));
                    pointsPriceCache = {
                        time: now,
                        price: avgPrice
                    };
                    return avgPrice;
                }
            }
        } catch (error) {}
        return currentPointsPrice || 0;
    }

    function calcSet(inventory, items) {
        const values = Object.keys(items).map(key => inventory[key] || 0);
        return {
            sets: values.length ? Math.min(...values) : 0
        };
    }

    function getStatusClass(abroadCount) {
        if (abroadCount === 0) return `${PREFIX}_status_red`;
        if (abroadCount > 1000) return `${PREFIX}_status_green`;
        return `${PREFIX}_status_orange`;
    }

    async function forceRefresh() {
        if (isLoading) {
            showToast("Already refreshing...");
            return;
        }
        pointsPriceCache = {
            time: 0,
            price: 0
        };
        await render(true);
    }

    async function render(force = false) {
        if (isLoading && !force) return;
        if (isDestroyed) return;

        isLoading = true;

        const container = document.getElementById(id('items_container'));
        if (!container) {
            isLoading = false;
            return;
        }

        const apiKey = GM_getValue('tornAPIKey');
        if (!apiKey) {
            if (container) container.innerHTML = `<div class="${PREFIX}_edge_empty">🔑 No API key - Click ⚙</div>`;
            const suggestionsContent = document.getElementById(id('suggestions_content'));
            if (suggestionsContent) suggestionsContent.innerHTML = `<div class="${PREFIX}_suggest_note">🔑 Set API key first</div>`;
            updateStats(0, 0, 0);
            updateLastUpdateTime();
            isLoading = false;
            return;
        }

        try {
            const {
                items: inventory,
                prices: itemPrices
            } = await localItems();
            const abroad = await abroadItems();
            let pointsPrice = await fetchPointsPrice(apiKey);
            checkMuseumDay();

            let totalSets = 0,
                totalPoints = 0,
                totalItemValue = 0,
                totalRemainingValue = 0;
            let categoryHtml = {},
                categoryRemainingValue = {};

            const processCat = (catName, group) => {
                const {
                    sets
                } = calcSet(inventory, group.items);
                let html = `<div class="${PREFIX}_category_t" data-category="${catName}">
                <div class="${PREFIX}_category_title"><span>${catName === 'Prehistoric' ? '🦕' : catName === 'Flowers' ? '🌸' : '🧸'}</span> ${catName} <span class="${PREFIX}_category_sets">${sets.toLocaleString()}</span></div>
                <div class="${PREFIX}_category_controls"><span class="${PREFIX}_category_btn up" data-category="${catName}">▲</span><span class="${PREFIX}_category_btn down" data-category="${catName}">▼</span></div>
            </div><div class="${PREFIX}_category_content" data-category="${catName}">`;

                let totalVal = 0,
                    remainVal = 0;
                const sorted = Object.entries(group.items).map(([name, data]) => ({
                    name,
                    data,
                    remaining: (inventory[name] || 0) - sets
                })).sort((a, b) => a.remaining - b.remaining);

                for (const {
                        name,
                        data,
                        remaining
                    } of sorted) {
                    const price = itemPrices[name] || 0;
                    const abroadCount = abroad[name] || 0;
                    totalVal += (inventory[name] || 0) * price;
                    remainVal += remaining * price;
                    html += `<div class="${PREFIX}_item_row">
                            <img src="${getItemImageUrl(data.id)}" class="${PREFIX}_item_img" alt="${name}">
                            <span class="${PREFIX}_item_local">${remaining.toLocaleString()}</span>
                            <span class="${PREFIX}_item_abroad ${getStatusClass(abroadCount)}">${abroadCount.toLocaleString()}</span>
                            <span class="${PREFIX}_item_flag">${data.flag}</span>
                         </div>`;
                }
                html += `</div>`;
                return {
                    sets,
                    points: sets * group.pts,
                    html,
                    totalValue: totalVal,
                    remainingValue: remainVal
                };
            };

            const prehistoric = processCat('Prehistoric', GROUPS.Prehistoric);
            const flowers = processCat('Flowers', GROUPS.Flowers);
            const plushies = processCat('Plushies', GROUPS.Plushies);

            categoryHtml['Prehistoric'] = prehistoric.html;
            categoryHtml['Flowers'] = flowers.html;
            categoryHtml['Plushies'] = plushies.html;
            categoryRemainingValue['Prehistoric'] = prehistoric.remainingValue;
            categoryRemainingValue['Flowers'] = flowers.remainingValue;
            categoryRemainingValue['Plushies'] = plushies.remainingValue;

            totalSets = prehistoric.sets + flowers.sets + plushies.sets;
            totalPoints = prehistoric.points + flowers.points + plushies.points;
            totalItemValue = prehistoric.totalValue + flowers.totalValue + plushies.totalValue;
            totalRemainingValue = prehistoric.remainingValue + flowers.remainingValue + plushies.remainingValue;

            const meteorite = inventory["Meteorite Fragment"] || 0;
            const fossil = inventory["Patagonian Fossil"] || 0;
            const meteoritePrice = itemPrices["Meteorite Fragment"] || 0;
            const fossilPrice = itemPrices["Patagonian Fossil"] || 0;
            totalItemValue += (meteorite * meteoritePrice) + (fossil * fossilPrice);
            totalPoints += (meteorite * 15) + (fossil * 20);

            categoryHtml['Special'] = `<div class="${PREFIX}_category_t" data-category="Special"><div class="${PREFIX}_category_title"><span>⭐</span> Special <span class="${PREFIX}_category_sets">${(meteorite + fossil).toLocaleString()}</span></div><div class="${PREFIX}_category_controls"><span class="${PREFIX}_category_btn up" data-category="Special">▲</span><span class="${PREFIX}_category_btn down" data-category="Special">▼</span></div></div><div class="${PREFIX}_category_content" data-category="Special"><div class="${PREFIX}_item_row"><img src="${getItemImageUrl(1488)}" class="${PREFIX}_item_img"><span class="${PREFIX}_item_local">${meteorite.toLocaleString()}</span><span class="${PREFIX}_item_abroad ${getStatusClass(abroad["Meteorite Fragment"] || 0)}">${(abroad["Meteorite Fragment"] || 0).toLocaleString()}</span><span class="${PREFIX}_item_flag">🇦🇷</span></div><div class="${PREFIX}_item_row"><img src="${getItemImageUrl(1487)}" class="${PREFIX}_item_img"><span class="${PREFIX}_item_local">${fossil.toLocaleString()}</span><span class="${PREFIX}_item_abroad ${getStatusClass(abroad["Patagonian Fossil"] || 0)}">${(abroad["Patagonian Fossil"] || 0).toLocaleString()}</span><span class="${PREFIX}_item_flag">🇦🇷</span></div></div>`;

            const pointsValue = totalPoints * pointsPrice;
            const bonusPoints = Math.round(totalPoints * 0.10);
            const bonusValue = Math.round(bonusPoints * pointsPrice);

            const statSets = document.getElementById(id('stat_sets'));
            const statPoints = document.getElementById(id('stat_points'));
            const statValue = document.getElementById(id('stat_value'));
            const tabSets = document.getElementById(id('tab_sets'));

            if (statSets) statSets.textContent = totalSets.toLocaleString();
            if (statPoints) statPoints.textContent = totalPoints.toLocaleString();
            if (statValue) statValue.textContent = formatMoney(pointsValue);
            if (tabSets) tabSets.textContent = totalSets.toLocaleString();

            const highlightBar = document.getElementById(id('highlight_bar'));
            if (highlightBar) {
                highlightBar.innerHTML = `<div class="${PREFIX}_highlight_text">${daysToMuseumDay} days 🏛️ 10%  ${bonusPoints.toLocaleString()} points @ ${formatMoney(bonusValue)}</div>`;
            }

            const suggestionsContainer = document.getElementById(id('suggestions_content'));
            if (suggestionsContainer) {
                const categories = ['Plushies', 'Flowers', 'Prehistoric'];
                let suggestionsHtml = '';

                for (const catName of categories) {
                    const group = GROUPS[catName];
                    const bonusPts = group.bonusPts;
                    const bonusValuePerSet = Math.round(bonusPts * pointsPrice);
                    const limitingItem = getLimitingItem(inventory, group);

                    const bonusPtsDisplay = Number.isInteger(bonusPts) ? bonusPts : bonusPts.toFixed(1);
                    const ptsLabel = bonusPts === 1 ? 'pt' : 'pts';

                    suggestionsHtml += `<div class="${PREFIX}_suggestion_row">
                    <img src="${getItemImageUrl(limitingItem.id)}" class="${PREFIX}_suggestion_img">
                    <span class="${PREFIX}_suggestion_emoji">${group.emoji}</span>
                    <span class="${PREFIX}_suggestion_text">+${bonusPtsDisplay} ${ptsLabel}  →  ${formatMoney(bonusValuePerSet)}</span>
                </div>`;
                }

                suggestionsContainer.innerHTML = suggestionsHtml;
            }

            let inventoryHtml = '';
            const categoryOrder = getCategoryOrder();
            const collapsedCategories = getCollapsedCategories();

            for (const cat of categoryOrder) {
                if (categoryHtml[cat]) {
                    inventoryHtml += categoryHtml[cat];
                }
            }

            if (!inventoryHtml) inventoryHtml = `<div class="${PREFIX}_edge_empty">📦 No items</div>`;
            if (container) container.innerHTML = inventoryHtml;

            // Clean up old listeners before adding new ones
            const upBtns = document.querySelectorAll(`.${PREFIX}_category_btn.up`);
            const downBtns = document.querySelectorAll(`.${PREFIX}_category_btn.down`);
            const titles = document.querySelectorAll(`.${PREFIX}_category_t`);

            upBtns.forEach(btn => {
                const newBtn = btn.cloneNode(true);
                btn.parentNode.replaceChild(newBtn, btn);
                addSafeEventListener(newBtn, 'click', (e) => {
                    e.stopPropagation();
                    moveCategoryUp(newBtn.dataset.category);
                });
            });

            downBtns.forEach(btn => {
                const newBtn = btn.cloneNode(true);
                btn.parentNode.replaceChild(newBtn, btn);
                addSafeEventListener(newBtn, 'click', (e) => {
                    e.stopPropagation();
                    moveCategoryDown(newBtn.dataset.category);
                });
            });

            titles.forEach(title => {
                const category = title.dataset.category;
                const content = document.querySelector(`.${PREFIX}_category_content[data-category="${category}"]`);
                if (collapsedCategories.includes(category) && content) {
                    content.style.maxHeight = '0';
                    content.style.overflow = 'hidden';
                }
                const newTitle = title.cloneNode(true);
                title.parentNode.replaceChild(newTitle, title);
                addSafeEventListener(newTitle, 'click', (e) => {
                    if (e.target.classList && e.target.classList.contains(`${PREFIX}_category_btn`)) return;
                    toggleCategory(category);
                });
            });

            updateLastUpdateTime();

        } catch (error) {
            console.error('Points Museum error:', error);
            if (container) container.innerHTML = `<div class="${PREFIX}_edge_empty">⚠️ Error: ${error.message}</div>`;
            updateStats(0, 0, 0);
        } finally {
            isLoading = false;
        }
    }

    function updateStats(totalSets, totalPoints, totalValue) {
        const setsEl = document.getElementById(id('stat_sets'));
        const pointsEl = document.getElementById(id('stat_points'));
        const valueEl = document.getElementById(id('stat_value'));
        const tabSetsEl = document.getElementById(id('tab_sets'));
        if (setsEl) setsEl.textContent = totalSets.toLocaleString();
        if (pointsEl) pointsEl.textContent = totalPoints.toLocaleString();
        if (valueEl) valueEl.textContent = formatMoney(totalValue);
        if (tabSetsEl) tabSetsEl.textContent = totalSets.toLocaleString();
    }

    async function mainLoop() {
        if (isDestroyed) return;
        const apiKey = GM_getValue('tornAPIKey');
        if (apiKey) {
            await fetchPointsPrice(apiKey).catch(() => {});
        }
        await render();
        if (refreshTimer) clearTimeout(refreshTimer);
        refreshTimer = setTimeout(() => mainLoop(), POLL);
    }

    function init() {
        // Prevent duplicate initialization
        if (window.__pointsMuseumFixedRunning) {
            console.log('Points Museum already running');
            return;
        }
        window.__pointsMuseumFixedRunning = true;

        console.log('✨ Points Museum v14.5 - Fixed memory leaks');
        createHaloUI();
        initEvents();
        loadSavedTheme();

        const savedKey = GM_getValue('tornAPIKey');
        if (!savedKey) {
            const key = prompt('Enter your Torn API key (16 chars):');
            if (key && key.length === 16) {
                GM_setValue('tornAPIKey', key);
                showToast('API key saved!');
                mainLoop();
            } else if (key) {
                showToast('Invalid API key');
            }
        } else {
            mainLoop();
        }
    }

    // Clean up on page unload
    window.addEventListener('pagehide', () => {
        if (refreshTimer) {
            clearTimeout(refreshTimer);
            refreshTimer = null;
        }
        window.__pointsMuseumFixedRunning = false;
        window.__pointsMuseumFixedInitialized = false;
    });

    if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init);
    else init();

    // ================= STYLES =================
    GM_addStyle(`
    .${PREFIX}_edge_container { position: fixed; top: 0; right: 0; height: 100vh; width: 0; z-index: 99999; pointer-events: none; }

    .${PREFIX}_dark_edge { --pts-bg: rgba(8,12,20,0.98); --pts-border: rgba(52,152,219,0.35); --pts-text: #e0e0e0; --pts-text-dim: #7f8c8d; --pts-accent: #3498db; --pts-accent-glow: rgba(52,152,219,0.12); --pts-success: #2ecc71; --pts-warning: #f39c12; --pts-danger: #e74c3c; }
    .${PREFIX}_light_edge { --pts-bg: rgba(255,255,255,0.98); --pts-border: rgba(52,152,219,0.4); --pts-text: #2c3e50; --pts-text-dim: #95a5a6; --pts-accent: #2980b9; --pts-accent-glow: rgba(52,152,219,0.08); --pts-success: #27ae60; --pts-warning: #f39c12; --pts-danger: #e74c3c; }

    .${PREFIX}_pull_tab_right { position: fixed; right: 0; top: 20%; width: 20px; background: var(--pts-bg); backdrop-filter: blur(12px); border: 1px solid var(--pts-border); border-right: none; border-radius: 6px 0 0 6px; padding: 8px 2px; display: flex; flex-direction: column; align-items: center; gap: 4px; cursor: pointer; pointer-events: auto; z-index: 100000; }
    .${PREFIX}_tab_icon { font-size: 7px; color: var(--pts-accent); }
    .${PREFIX}_tab_text { font-size: 6px; font-weight: 700; writing-mode: vertical-rl; color: var(--pts-accent); }
    .${PREFIX}_tab_stats { font-size: 8px; font-weight: 700; text-align: center; color: var(--pts-accent); background: var(--pts-accent-glow); padding: 2px 2px; border-radius: 3px; writing-mode: vertical-rl; }
    .${PREFIX}_edge_panel_right { position: fixed; right: 0; top: 50%; transform: translateY(-50%) translateX(100%); width: 200px; max-height: 90vh; background: var(--pts-bg); backdrop-filter: blur(16px); border-left: 1px solid var(--pts-border); border-radius: 8px 0 0 8px; transition: transform 0.25s ease; pointer-events: auto; display: flex; flex-direction: column; overflow: hidden; z-index: 100000; }
    .${PREFIX}_edge_panel_right.${PREFIX}_open { transform: translateY(-50%) translateX(0); }
    .${PREFIX}_panel_header { display: flex; justify-content: space-between; align-items: center; padding: 5px 7px; border-bottom: 1px solid var(--pts-border); background: var(--pts-accent-glow); }
    .${PREFIX}_brand { font-size: 9px; font-weight: 700; color: var(--pts-accent); }
    .${PREFIX}_star { font-size: 9px; animation: ptsSpinStar 3s linear infinite; }
    @keyframes ptsSpinStar { 100% { transform: rotate(360deg); } }
    .${PREFIX}_header_buttons { display: flex; gap: 4px; }
    .${PREFIX}_settings_btn, .${PREFIX}_refresh_btn_panel, .${PREFIX}_close_panel { background: rgba(0,0,0,0.3); border: none; border-radius: 3px; padding: 2px 5px; font-size: 8px; cursor: pointer; color: var(--pts-text-dim); }
    .${PREFIX}_refresh_btn_panel:hover { background: var(--pts-success); color: white; }
    .${PREFIX}_settings_btn:hover, .${PREFIX}_close_panel:hover { background: var(--pts-accent); color: white; }
    .${PREFIX}_close_panel:hover { background: var(--pts-danger); }
    .${PREFIX}_quick_stats { display: flex; padding: 5px; gap: 4px; border-bottom: 1px solid var(--pts-border); background: var(--pts-accent-glow); }
    .${PREFIX}_stat_item { flex: 1; text-align: center; background: rgba(0,0,0,0.2); border-radius: 4px; padding: 3px 1px; }
    .${PREFIX}_stat_number { font-size: 9px; font-weight: 700; color: var(--pts-accent); }
    .${PREFIX}_stat_label { font-size: 6px; color: var(--pts-text-dim); text-transform: uppercase; }
    .${PREFIX}_highlight_bar { padding: 5px 5px; margin: 5px 6px; background: rgba(241,196,15,0.15); border-radius: 5px; text-align: center; }
    .${PREFIX}_highlight_text { font-size: 8px; font-weight: 700; color: #f1c40f; }
    .${PREFIX}_suggestions_section { padding: 5px 6px; border-bottom: 1px solid var(--pts-border); }
    .${PREFIX}_suggestions_content { max-height: 110px; overflow-y: auto; }
    .${PREFIX}_suggestions_content::-webkit-scrollbar { width: 2px; }
    .${PREFIX}_suggestions_content::-webkit-scrollbar-thumb { background: var(--pts-accent); border-radius: 2px; }
    .${PREFIX}_suggestion_row { display: flex; align-items: center; gap: 6px; padding: 4px 6px; background: rgba(0,0,0,0.2); border-radius: 4px; margin-bottom: 4px; }
    .${PREFIX}_suggestion_img { width: 16px; height: 16px; object-fit: contain; }
    .${PREFIX}_suggestion_emoji { font-size: 12px; }
    .${PREFIX}_suggestion_text { font-size: 7px; font-weight: 700; color: var(--pts-success); }
    .${PREFIX}_inventory_section { flex: 1; display: flex; flex-direction: column; min-height: 0; padding: 0 5px; }
    .${PREFIX}_section_header.clickable { cursor: pointer; display: flex; justify-content: space-between; align-items: center; border-radius: 3px; padding: 4px 0; font-size: 7px; font-weight: 700; color: var(--pts-accent); }
    .${PREFIX}_section_header.clickable:hover { background: var(--pts-accent-glow); }
    .${PREFIX}_toggle_icon { font-size: 7px; color: var(--pts-text-dim); }
    .${PREFIX}_inventory_content { flex: 1; overflow-y: auto; max-height: 220px; }
    .${PREFIX}_inventory_content::-webkit-scrollbar { width: 2px; }
    .${PREFIX}_inventory_content::-webkit-scrollbar-thumb { background: var(--pts-accent); border-radius: 2px; }
    .${PREFIX}_category_t { padding: 3px 4px; background: var(--pts-accent-glow); color: var(--pts-accent); font-weight: 700; font-size: 6.5px; display: flex; justify-content: space-between; align-items: center; cursor: pointer; border-radius: 3px; margin-top: 3px; }
    .${PREFIX}_category_title { display: flex; align-items: center; gap: 3px; }
    .${PREFIX}_category_sets { font-size: 5.5px; color: var(--pts-text-dim); margin-left: 3px; }
    .${PREFIX}_category_controls { display: flex; gap: 2px; }
    .${PREFIX}_category_btn { width: 11px; height: 11px; background: rgba(0,0,0,0.3); border-radius: 2px; color: var(--pts-text-dim); font-size: 5px; display: flex; align-items: center; justify-content: center; cursor: pointer; }
    .${PREFIX}_category_btn:hover { background: var(--pts-accent); color: white; }
    .${PREFIX}_category_content { transition: max-height 0.2s; overflow: hidden; }
    .${PREFIX}_item_row { 
        display: grid; 
        grid-template-columns: 22px 42px 42px 22px; 
        gap: 6px; 
        align-items: center; 
        padding: 4px 3px; 
        font-size: 7px; 
        border-bottom: 1px solid var(--pts-border); 
    }
    .${PREFIX}_item_img { 
        width: 20px; 
        height: 20px; 
        object-fit: contain; 
        justify-self: center; 
    }
    .${PREFIX}_item_local { 
        color: var(--pts-success); 
        font-weight: 700; 
        text-align: center; 
    }
    .${PREFIX}_item_abroad { 
        text-align: center; 
        padding: 2px 4px; 
        border-radius: 3px; 
        font-size: 6px; 
        font-weight: 600; 
    }
    .${PREFIX}_item_flag { 
        font-size: 11px; 
        text-align: center; 
        justify-self: center; 
    }
    .${PREFIX}_status_green { color: var(--pts-success); background: rgba(46,204,113,0.15); }
    .${PREFIX}_status_orange { color: var(--pts-warning); background: rgba(243,156,18,0.15); }
    .${PREFIX}_status_red { color: var(--pts-danger); background: rgba(231,76,60,0.15); }
    .${PREFIX}_loading, .${PREFIX}_edge_empty { text-align: center; padding: 10px 4px; font-size: 6px; color: var(--pts-text-dim); }
    .${PREFIX}_footer { padding: 4px; text-align: center; border-top: 1px solid var(--pts-border); font-size: 5.5px; display: flex; justify-content: space-between; align-items: center; }
    .${PREFIX}_footer a { color: var(--pts-accent); text-decoration: none; }
    .${PREFIX}_update_time { color: var(--pts-text-dim); font-size: 5.5px; }
    .${PREFIX}_modal_overlay { position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.85); backdrop-filter: blur(4px); z-index: 200000; display: none; align-items: center; justify-content: center; }
    .${PREFIX}_modal { background: var(--pts-bg); border: 1px solid var(--pts-border); border-radius: 8px; width: 240px; max-width: 90%; }
    .${PREFIX}_modal_header { padding: 8px 10px; border-bottom: 1px solid var(--pts-border); display: flex; justify-content: space-between; color: var(--pts-accent); font-weight: 600; font-size: 10px; }
    .${PREFIX}_modal_close { background: rgba(0,0,0,0.3); border: none; border-radius: 3px; width: 20px; height: 20px; cursor: pointer; color: var(--pts-text-dim); font-size: 9px; }
    .${PREFIX}_modal_close:hover { background: var(--pts-danger); color: white; }
    .${PREFIX}_modal_body { padding: 10px; }
    .${PREFIX}_settings_field { margin-bottom: 10px; }
    .${PREFIX}_settings_field label { display: block; font-size: 8px; color: var(--pts-text-dim); margin-bottom: 3px; }
    .${PREFIX}_input { width: 100%; padding: 6px; background: rgba(0,0,0,0.3); border: 1px solid var(--pts-border); border-radius: 4px; color: var(--pts-text); font-size: 8px; box-sizing: border-box; }
    .${PREFIX}_input:focus { outline: none; border-color: var(--pts-accent); }
    .${PREFIX}_btn_primary, .${PREFIX}_btn_secondary, .${PREFIX}_btn_danger { padding: 5px 8px; border-radius: 4px; font-size: 8px; font-weight: 600; cursor: pointer; border: none; }
    .${PREFIX}_btn_primary { background: var(--pts-accent); color: white; }
    .${PREFIX}_btn_primary:hover { filter: brightness(1.1); }
    .${PREFIX}_btn_secondary { background: rgba(0,0,0,0.3); border: 1px solid var(--pts-border); color: var(--pts-text); }
    .${PREFIX}_btn_secondary:hover { background: var(--pts-accent-glow); }
    .${PREFIX}_btn_danger { background: var(--pts-danger); color: white; }
    .${PREFIX}_btn_danger:hover { filter: brightness(1.1); }
    .${PREFIX}_toast { position: fixed; bottom: 20px; left: 50%; transform: translateX(-50%); background: var(--pts-bg); border: 1px solid var(--pts-accent); padding: 5px 10px; border-radius: 14px; font-size: 8px; color: var(--pts-accent); z-index: 200001; white-space: nowrap; pointer-events: none; animation: ptsFadeOut 2s forwards; }
    @keyframes ptsFadeOut { 0% { opacity: 1; } 70% { opacity: 1; } 100% { opacity: 0; visibility: hidden; } }
    `);
})();