[MWI] Dungeon Profit

Shows most profitable items in Dungeon Shop

// ==UserScript==
// @name         [MWI] Dungeon Profit
// @namespace    http://tampermonkey.net/
// @version      1.0
// @description  Shows most profitable items in Dungeon Shop
// @author       WataFX
// @license MIT
// @icon         https://www.google.com/s2/favicons?sz=64&domain=milkywayidle.com
// @match        https://www.milkywayidle.com/game?characterId=*
// @grant        GM_xmlhttpRequest
// @connect      www.milkywayidle.com
// @run-at       document-end
// ==/UserScript==


(async function() {
    'use strict';

    const tokenTypes = ['Chimerical Token','Sinister Token','Enchanted Token','Pirate Token'];
    const colors = {
        'Chimerical Token': { border: '2px solid white', boxShadow: '0 0 8px white' },
        'Sinister Token' : { border: '2px solid #C71585', boxShadow: '0 0 8px #C71585' },
        'Enchanted Token': { border: '2px solid #00BFFF', boxShadow: '0 0 8px #00BFFF' },
        'Pirate Token'   : { border: '2px solid #32CD32', boxShadow: '0 0 8px #32CD32' }
    };
    const apiUrl = 'https://www.milkywayidle.com/game_data/marketplace.json';
    const containerSelector = '.ShopPanel_shopItems__3QZSJ';
    const refreshIntervalMs = 60000;

    const log = (...args) => console.log('[MarketProfit]', ...args);
    const warn = (...args) => console.warn('[MarketProfit]', ...args);
    const error = (...args) => console.error('[MarketProfit]', ...args);

    const nameToSlug = name => {
        const cleaned = name.toLowerCase()
            .replace(/[‘’']/g, '')
            .replace(/[^a-z0-9]+/g, '_')
            .replace(/^_+|_+$/g, ''); 
        return '/items/' + cleaned;
    };
    function getMaxPrice(entries, key) {
        let max = null;
        for (const obj of Object.values(entries)) {
            const p = obj[key];
            if (p > 0 && (max === null || p > max)) max = p;
        }
        return max;
    }
    const debounce = (fn, delay) => {
        let t;
        return (...args) => { clearTimeout(t); t = setTimeout(() => fn(...args), delay); };
    };
    const isVisible = el => el && el.offsetParent !== null;

    let marketData = {};
    async function fetchMarketPrices() {
        return new Promise((resolve, reject) => {
            GM_xmlhttpRequest({
                method: 'GET', url: apiUrl,
                onload(res) {
                    if (res.status >= 200 && res.status < 300) {
                        try {
                            const data = JSON.parse(res.responseText);
                            marketData = data.marketData || data.marketplace || {};
                            log('Market data loaded:', Object.keys(marketData).length, 'entries');
                            resolve();
                        } catch (e) {
                            error('JSON parse error', e);
                            reject(e);
                        }
                    } else {
                        error('HTTP error', res.status);
                        reject(res.status);
                    }
                },
                onerror(err) { error('Request error', err); reject(err); }
            });
        });
    }

    function annotateContainer(container) {
        const cards = container.querySelectorAll('.ShopPanel_shopItem__10Noo');
        if (!cards.length) return;
        if (!Object.keys(marketData).length) return;

        const grouped = {};
        cards.forEach(div => {
            div.querySelectorAll('.gm-profit-annotation').forEach(e => e.remove());
            div.style.border = '';
            div.style.boxShadow = '';
            div.style.padding = '';

            const nameEl = div.querySelector('.ShopPanel_name__3vA-H');
            const costEl = div.querySelector('.ShopPanel_costs__XffBM > div > div');
            if (!nameEl || !costEl) return;
            const name = nameEl.textContent.trim();
            const [amtText, ...tokParts] = costEl.textContent.trim().split(' ');
            const amount = parseInt(amtText.replace(/\D/g, ''), 10);
            const token = tokParts.join(' ');
            if (!tokenTypes.includes(token) || isNaN(amount)) return;

            const slug = nameToSlug(name);
            const entries = marketData[slug];
            if (!entries) return;
            const maxAsk = getMaxPrice(entries, 'a');
            const maxBid = getMaxPrice(entries, 'b');
            if (!maxAsk && !maxBid) return;
            const askValue = maxAsk ? maxAsk / amount : null;
            const bidValue = maxBid ? maxBid / amount : null;

            if (!grouped[token]) grouped[token] = [];
            grouped[token].push({ div, askValue, bidValue });
        });

        for (const token of Object.keys(grouped)) {
            const items = grouped[token];
            const best = items.reduce((m, c) => (c.bidValue || 0) > (m.bidValue || 0) ? c : m, items[0]);
            items.forEach(({ div, askValue, bidValue }) => {
                const parent = div.querySelector('.ShopPanel_costs__XffBM > div');
                if (askValue != null) {
                    const spanA = document.createElement('span');
                    spanA.className = 'gm-profit-annotation';
                    spanA.textContent = `ask: ${askValue.toFixed(2)}`;
                    spanA.style.display = 'block';
                    spanA.style.marginTop = '4px';
                    parent.appendChild(spanA);
                }
                if (bidValue != null) {
                    const spanB = document.createElement('span');
                    spanB.className = 'gm-profit-annotation';
                    spanB.textContent = `bid: ${bidValue.toFixed(2)}`;
                    spanB.style.display = 'block';
                    spanB.style.marginTop = '2px';
                    if (div === best.div) {
                        const style = colors[token];
                        div.style.border = style.border;
                        div.style.boxShadow = style.boxShadow;
                        div.style.padding = '4px';
                        spanB.style.fontWeight = 'bold';
                    }
                    parent.appendChild(spanB);
                }
            });
        }
    }

    function annotateVisible() {
        document.querySelectorAll(containerSelector).forEach(cont => {
            if (isVisible(cont)) annotateContainer(cont);
        });
    }

    try {
        await fetchMarketPrices();
        annotateVisible();
        setInterval(async () => {
            try { await fetchMarketPrices(); annotateVisible(); } catch {};
        }, refreshIntervalMs);
        document.body.addEventListener('click', annotateVisible);
        new MutationObserver(debounce(annotateVisible, 200)).observe(document.body, { childList: true, subtree: true });
    } catch (e) {
        error('Initialization failed', e);
    }
})();