查看交易所冰淇淋融化情况

查看交易所此刻冰淇淋的融化情况

// ==UserScript==
// @name         查看交易所冰淇淋融化情况
// @namespace    https://github.com/gangbaRuby
// @version      1.0.0
// @license      AGPL-3.0
// @description  查看交易所此刻冰淇淋的融化情况
// @author       Rabbit House
// @match        *://www.simcompanies.com/*
// @icon         https://www.google.com/s2/favicons?sz=64&domain=simcompanies.com
// @grant        GM_info
// ==/UserScript==

(function () {
    'use strict';

    const targetIds = [153, 154];
    const targetRealms = [0, 1];
    let GLOBAL_REALM_ID = null;

    // 计算剩余量
    function calculateRemainingQuantity(entry, nowTime) {
        const decayTime = Date.parse(entry.datetimeDecayUpdated);
        const a = Math.abs(nowTime - decayTime);
        const o = Math.round(a / (1000 * 60) / 4) * 4 / 60;
        return Math.floor(entry.quantity * Math.pow(1 - 0.05, o));
    }

    // 创建浮动按钮,控制浮动框显示隐藏
    function createToggleButton() {
        if (document.getElementById("market-toggle-button")) return;

        const btn = document.createElement("div");
        btn.id = "market-toggle-button";
        btn.textContent = "查看交易所🍦融化情况";
        btn.style.cssText = `
        position: fixed;
        bottom: 20px;
        right: 20px;
        z-index: 10001;
        background: #222;
        color: white;
        border-radius: 4px;
        padding: 6px 10px;
        font-size: 13px;d
        cursor: pointer;
        user-select: none;
        opacity: 0.85;
    `;
        btn.title = "点击显示/隐藏市场订单预览";

        btn.addEventListener("mouseenter", () => btn.style.opacity = "1");
        btn.addEventListener("mouseleave", () => btn.style.opacity = "0.85");

        // 拖动逻辑
        btn.onmousedown = function (e) {
            e.preventDefault();
            let shiftX = e.clientX - btn.getBoundingClientRect().left;
            let shiftY = e.clientY - btn.getBoundingClientRect().top;

            function moveAt(pageX, pageY) {
                btn.style.left = pageX - shiftX + 'px';
                btn.style.top = pageY - shiftY + 'px';
                btn.style.bottom = 'auto';
                btn.style.right = 'auto';
            }

            function onMouseMove(e) {
                moveAt(e.pageX, e.pageY);
            }

            document.addEventListener('mousemove', onMouseMove);

            document.onmouseup = function () {
                document.removeEventListener('mousemove', onMouseMove);
                document.onmouseup = null;
            };
        };

        btn.ondragstart = () => false;

        // 点击显示/隐藏窗口
        btn.onclick = () => {
            const box = document.getElementById("market-floating-box");
            if (!box) return;
            box.style.display = box.style.display === "none" ? "block" : "none";
        };

        // 设置初始位置和附加样式
        btn.style.position = 'fixed';
        btn.style.left = 'unset';
        btn.style.top = 'unset';
        btn.style.bottom = '20px';
        btn.style.right = '20px';

        document.body.appendChild(btn);
    }

    // 创建浮动框,支持拖拽
    function createFloatingBox(hiddenInitially = false) {
        if (document.getElementById("market-floating-box")) return;

        const box = document.createElement("div");
        box.id = "market-floating-box";
        box.style.cssText = `
            position: fixed;
            left: 10px;
            top: 50px;
            z-index: 9999;
            background: #222;
            color: #eee;
            padding: 10px;
            border-radius: 6px;
            max-height: 60vh;
            width: 520px;
            box-shadow: 0 0 12px rgba(0,0,0,0.7);
            overflow: hidden;
            user-select: none;
            font-family: "Segoe UI", Tahoma, Geneva, Verdana, sans-serif;
            font-size: 13px;
            display: ${hiddenInitially ? "none" : "block"};
        `;

        // 头部拖拽条
        const header = document.createElement("div");
        // 创建关闭按钮
        const closeBtn = document.createElement("span");
        closeBtn.textContent = "✖";
        closeBtn.title = "关闭窗口";
        closeBtn.style.cssText = `
            float: right;
            margin-left: 8px;
            color: #aaa;
            cursor: pointer;
            font-size: 14px;
        `;
        closeBtn.onmouseenter = () => closeBtn.style.color = "#fff";
        closeBtn.onmouseleave = () => closeBtn.style.color = "#aaa";
        closeBtn.onclick = () => {
            const box = document.getElementById("market-floating-box");
            if (box) box.style.display = "none";
        };


        header.textContent = "🍦 我的🍦😭";
        header.style.cssText = `
            cursor: move;
            padding: 6px 8px;
            background: #111;
            border-radius: 4px 4px 0 0;
            font-weight: bold;
            user-select: none;
        `;

        // 内容区,含表格和统计
        const content = document.createElement("div");
        content.id = "market-floating-content";
        content.style.cssText = `
            margin-top: 8px;
            max-height: calc(60vh - 40px);
            overflow: hidden;
            display: flex;
            flex-direction: column;
        `;

        box.appendChild(header);
        header.appendChild(closeBtn); // 放入 header 最后
        box.appendChild(content);
        // 添加右下角缩放控件
        const resizer = document.createElement("div");
        resizer.style.cssText = `
            width: 12px;
            height: 12px;
            position: absolute;
            right: 2px;
            bottom: 2px;
            cursor: nwse-resize;
            background: #666;
            border-radius: 2px;
            z-index: 10000;
        `;
        box.appendChild(resizer);

        // 缩放逻辑
        resizer.addEventListener("mousedown", function (e) {
            e.preventDefault();
            document.body.style.userSelect = "none"; // 防止选中文字
            const startX = e.clientX;
            const startY = e.clientY;
            const startWidth = box.offsetWidth;
            const startHeight = box.offsetHeight;

            function doDrag(e) {
                const newWidth = Math.max(300, startWidth + e.clientX - startX);
                const newHeight = Math.max(200, startHeight + e.clientY - startY);
                box.style.width = newWidth + "px";
                box.style.maxHeight = "unset";
                box.style.height = newHeight + "px";
            }

            function stopDrag() {
                document.removeEventListener("mousemove", doDrag);
                document.removeEventListener("mouseup", stopDrag);
                document.body.style.userSelect = "";
            }

            document.addEventListener("mousemove", doDrag);
            document.addEventListener("mouseup", stopDrag);
        });
        document.body.appendChild(box);

        // 拖拽逻辑
        dragElement(box, header);
    }

    // 拖拽函数,传入浮动元素和拖拽头部元素
    function dragElement(elmnt, dragHandle) {
        let pos1 = 0, pos2 = 0, pos3 = 0, pos4 = 0;
        dragHandle.onmousedown = dragMouseDown;

        function dragMouseDown(e) {
            e.preventDefault();
            pos3 = e.clientX;
            pos4 = e.clientY;
            document.onmouseup = closeDragElement;
            document.onmousemove = elementDrag;
        }

        function elementDrag(e) {
            e.preventDefault();
            pos1 = pos3 - e.clientX;
            pos2 = pos4 - e.clientY;
            pos3 = e.clientX;
            pos4 = e.clientY;

            let newTop = elmnt.offsetTop - pos2;
            let newLeft = elmnt.offsetLeft - pos1;

            // 限制不出屏幕(简易)
            newTop = Math.max(0, Math.min(window.innerHeight - elmnt.offsetHeight, newTop));
            newLeft = Math.max(0, Math.min(window.innerWidth - elmnt.offsetWidth, newLeft));

            elmnt.style.top = newTop + "px";
            elmnt.style.left = newLeft + "px";
        }

        function closeDragElement() {
            document.onmouseup = null;
            document.onmousemove = null;
        }
    }

    // 渲染表格和统计,分两块显示,表格滚动独立
    function renderFloatingTable(data) {
        createFloatingBox();
        createToggleButton();

        const container = document.getElementById("market-floating-content");
        if (!data || data.length === 0) {
            container.innerHTML = "<b>暂无数据</b>";
            return;
        }

        // 表格和统计容器
        container.innerHTML = "";

        const tableContainer = document.createElement("div");
        tableContainer.style.cssText = `
            flex-grow: 1;
            overflow: auto;
            maxHeight: "calc(60vh - 80px)";
            background: #333;
            border-radius: 4px;
            padding: 6px;
        `;

        const summaryContainer = document.createElement("div");
        summaryContainer.style.cssText = `
            margin-top: 8px;
            maxHeight: 100px;
            overflow: auto;
            background: #111;
            border-radius: 4px;
            padding: 6px;
            font-size: 12px;
            line-height: 1.4;
            color: #ccc;
        `;

        // 处理数据,加入产品名和融化数
        const kindMap = { 153: "巧克力冰淇淋", 154: "苹果冰淇淋" };
        const processed = data.map(row => {
            const melt = row.quantity - row.estimatedRemaining;
            return {
                ...row,
                kindName: kindMap[row.kind] || `未知(${row.kind})`,
                melt,
            };
        });

        // 排序参数
        let sortKey = null;
        let sortAsc = true;

        // 创建表格
        const table = document.createElement("table");
        table.style.cssText = `
            border-collapse: collapse;
            width: 100%;
            table-layout: auto;
            color: #eee;
            user-select: none;
        `;

        // 表头
        const headers = [
            { label: "订单ID", key: "id" },
            { label: "产品名", key: "kindName" },
            { label: "品质", key: "quality" },
            { label: "单价", key: "price" },
            { label: "数量", key: "quantity" },
            { label: "剩余量", key: "estimatedRemaining" },
            { label: "融化数量", key: "melt" },
            { label: "公司名", key: "company" },
        ];

        const thead = document.createElement("thead");
        const headerRow = document.createElement("tr");

        for (const h of headers) {
            const th = document.createElement("th");
            th.textContent = h.label;
            th.style.cssText = `
                border: 1px solid #555;
                padding: 4px 8px;
                background: #111;
                position: sticky;
                top: 0;
                z-index: 10;
                cursor: pointer;
                user-select: none;
            `;

            th.onclick = () => {
                if (sortKey === h.key) {
                    sortAsc = !sortAsc;
                } else {
                    sortKey = h.key;
                    sortAsc = true;
                }
                renderBody();
            };
            headerRow.appendChild(th);
        }
        thead.appendChild(headerRow);
        table.appendChild(thead);

        const tbody = document.createElement("tbody");
        table.appendChild(tbody);

        tableContainer.appendChild(table);
        container.appendChild(tableContainer);
        container.appendChild(summaryContainer);

        function renderBody() {
            tbody.innerHTML = "";

            let sorted = [...processed];
            if (sortKey) {
                sorted.sort((a, b) => {
                    const v1 = a[sortKey], v2 = b[sortKey];
                    if (typeof v1 === "number" && typeof v2 === "number") {
                        return sortAsc ? v1 - v2 : v2 - v1;
                    }
                    return sortAsc ? String(v1).localeCompare(String(v2)) : String(v2).localeCompare(String(v1));
                });
            }

            for (const row of sorted) {
                const tr = document.createElement("tr");
                for (const h of headers) {
                    const td = document.createElement("td");
                    td.style.cssText = `
                        border: 1px solid #555;
                        padding: 4px 8px;
                        white-space: nowrap;
                        max-width: 80px;
                        overflow: hidden;
                        text-overflow: ellipsis;
                        color: #eee;
                    `;
                    if (h.key === "price") {
                        td.textContent = row[h.key].toFixed(3);
                    } else {
                        td.textContent = row[h.key];
                    }
                    tr.appendChild(td);
                }
                tbody.appendChild(tr);
            }

            renderSummary();
        }

        // 统计渲染
        function renderSummary() {
            summaryContainer.innerHTML = "";

            const summaryMap = new Map();
            const qualityTotalMap = new Map();
            let grandTotal = 0;

            for (const row of processed) {
                if (row.melt <= 0) continue;
                const company = row.company;
                const quality = row.quality;

                if (!summaryMap.has(company)) summaryMap.set(company, { total: 0, q: {} });
                const record = summaryMap.get(company);
                record.total += row.melt;
                record.q[quality] = (record.q[quality] || 0) + row.melt;

                qualityTotalMap.set(quality, (qualityTotalMap.get(quality) || 0) + row.melt);
                grandTotal += row.melt;
            }

            const lines = [];

            if (grandTotal > 0) {
                const sortedQualities = Array.from(qualityTotalMap.entries())
                    .sort(([a], [b]) => a - b)
                    .map(([q, amt]) => `Q${q}=${amt}`);
                lines.push(`<div>🏪 当前交易所融化:${sortedQualities.join(",")}(共 ${grandTotal})</div>`);
            }

            if (summaryMap.size > 0) {
                lines.push(`<div>🧾 公司融化统计:</div>`);
                const sortedCompanies = Array.from(summaryMap.entries())
                    .sort(([, a], [, b]) => b.total - a.total);
                for (const [company, info] of sortedCompanies) {
                    const parts = Object.entries(info.q)
                        .sort(([a], [b]) => a - b)
                        .map(([q, amt]) => `Q${q}=${amt}`)
                        .join(",");
                    lines.push(`<div>${company}:${parts}(共 ${info.total})</div>`);
                }
            }

            summaryContainer.innerHTML = lines.join("");
        }

        // 首次渲染
        renderBody();
    }

    // 拦截 fetch
    const originalFetch = window.fetch;
    window.fetch = async function (...args) {
        const url = args[0];
        const match = typeof url === 'string' && url.match(/\/api\/v3\/market\/(\d+)\/(\d+)\/$/);
        if (match) {
            const realm = parseInt(match[1]), id = parseInt(match[2]);
            if (targetRealms.includes(realm) && targetIds.includes(id)) {
                const response = await originalFetch(...args);
                response.clone().json().then(json => {
                    processMarketData(json, realm, id);
                });
                return response;
            }
        }
        return originalFetch(...args);
    };

    // 拦截 XMLHttpRequest
    const originalOpen = XMLHttpRequest.prototype.open;
    XMLHttpRequest.prototype.open = function (method, url, ...rest) {
        this._isTargetMarketRequest = false;

        const match = typeof url === 'string' && url.match(/\/api\/v3\/market\/(\d+)\/(\d+)\/$/);
        if (match) {
            const realm = parseInt(match[1], 10);
            const id = parseInt(match[2], 10);
            if (targetRealms.includes(realm) && targetIds.includes(id)) {
                this._isTargetMarketRequest = true;
                this._realm = realm;
                this._id = id;
            }
        }
        return originalOpen.call(this, method, url, ...rest);
    };

    const originalSend = XMLHttpRequest.prototype.send;
    XMLHttpRequest.prototype.send = function (...args) {
        if (this._isTargetMarketRequest) {
            this.addEventListener("load", () => {
                try {
                    const json = JSON.parse(this.responseText);
                    processMarketData(json, this._realm, this._id);
                } catch (e) {
                    console.error("JSON parse failed:", e);
                }
            });
        }
        return originalSend.call(this, ...args);
    };

    // 监听 DOM 变化,提取 realmId
    const domObserver = new MutationObserver(() => {
        const realmId = getRealmIdFromLink();
        if (realmId !== null && realmId !== GLOBAL_REALM_ID) {
            console.log('[RegionAutoUpdater] 获取到 realmId:', realmId);
            GLOBAL_REALM_ID = realmId;
            tryAutoRenderFromStorage();
        }
    });
    domObserver.observe(document.body, { childList: true, subtree: true });

    // 监听 URL 变化
    function addUrlChangeListener(callback) {
        let lastUrl = location.href;
        new MutationObserver(() => {
            const url = location.href;
            if (url !== lastUrl) {
                lastUrl = url;
                callback(url);
            }
        }).observe(document, { subtree: true, childList: true });
    }

    addUrlChangeListener(url => {
        tryAutoRenderFromStorage();
    });

    // 获取 realmId 函数
    function getRealmIdFromLink() {
        const link = document.querySelector('a[href*="/company/"]');
        if (link) {
            const match = link.href.match(/\/company\/(\d+)\//);
            return match ? parseInt(match[1], 10) : null;
        }
        return null;
    }

    // 从 localStorage 读取数据渲染
    function tryAutoRenderFromStorage() {
        const match = location.pathname.match(/^\/zh-cn\/market\/resource\/(\d+)\/?$/);
        if (!match) return;

        const id = parseInt(match[1]);
        if (!targetIds.includes(id)) return;

        const realm = GLOBAL_REALM_ID !== null ? GLOBAL_REALM_ID : 0;

        const dataStr = localStorage.getItem(`marketFloating_${realm}_${id}`);
        if (dataStr) {
            try {
                const data = JSON.parse(dataStr);
                renderFloatingTable(data);
            } catch (e) {
                console.error("localStorage 数据解析失败", e);
            }
        } else {
            const container = document.getElementById("market-floating-content");
            if (container) container.innerHTML = "<b>暂无数据</b>";
        }
    }

    // 处理数据存储并渲染
    function processMarketData(json, realm, id) {
        const now = Date.now();
        const processed = json.map(entry => ({
            id: entry.id,
            kind: entry.kind,
            quality: entry.quality,
            price: entry.price,
            quantity: entry.quantity,
            estimatedRemaining: calculateRemainingQuantity(entry, now),
            datetimeDecayUpdated: entry.datetimeDecayUpdated,
            company: entry.seller?.company || "未知公司"
        }));
        localStorage.setItem(`marketFloating_${realm}_${id}`, JSON.stringify(processed));
        renderFloatingTable(processed);
    }

    // 页面首次尝试渲染
    tryAutoRenderFromStorage();
    createToggleButton();
    createFloatingBox(true);

    const localVersion = GM_info.script.version;
    const scriptUrl = 'https://simcompanies-scripts.pages.dev/checkIceCreamMeltInMarket.user.js?t=' + Date.now();
    const downloadUrl = 'https://simcompanies-scripts.pages.dev/checkIceCreamMeltInMarket.user.js';

    function compareVersions(v1, v2) {
        const a = v1.split('.').map(Number);
        const b = v2.split('.').map(Number);
        const len = Math.max(a.length, b.length);
        for (let i = 0; i < len; i++) {
            const n1 = a[i] || 0;
            const n2 = b[i] || 0;
            if (n1 > n2) return 1;
            if (n1 < n2) return -1;
        }
        return 0;
    }

    function checkUpdate() {
        fetch(scriptUrl)
            .then(r => r.text())
            .then(text => {
                const match = text.match(/@version\s+([0-9.]+)/);
                if (!match) return;

                const remoteVersion = match[1];
                if (compareVersions(remoteVersion, localVersion) > 0) {
                    if (confirm(`查看交易所冰淇淋融化情况插件发现新版本 v${remoteVersion},是否前往更新?`)) {
                        window.open(downloadUrl, '_blank');
                    }
                }
            })
            .catch(err => {
                console.warn('检查更新失败:', err);
            });
    }

    setTimeout(checkUpdate, 3000);

})();