Caelus Value Display

Shows item value, demand, and trend on Caelus item and trade pages

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Userscripts ,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name         Caelus Value Display
// @namespace    http://tampermonkey.net/
// @version      1.2
// @description  Shows item value, demand, and trend on Caelus item and trade pages
// @author       dax / czy
// @match        https://www.caelus.lol/catalog/*
// @match        https://www.caelus.lol/trade/*
// @match        https://www.caelus.lol/internal/limiteds*
// @match        https://www.caelus.lol/trades*
// @match        https://www.caelus.lol/users/*/profile*
// @grant        GM_xmlhttpRequest
// @connect      raw.githubusercontent.com
// ==/UserScript==

(function () {
    'use strict';

    const VALUE_LIST_URL = "https://raw.githubusercontent.com/temptationless/Caelus-Extensions/refs/heads/main/valuelist";

    function parseValues(csv) {
        const map = {};
        for (const line of csv.split("\n")) {
            const parts = line.split(",").map(s => s.trim());
            if (parts.length >= 2) {
                const rawName = parts[0];
                const cleanedName = rawName.replace(/^[):>\s]+/, "").trim();
                const entry = {
                    value:  parts[1] || "N/A",
                    demand: parts[2] || "N/A",
                    trend:  parts[3] || "N/A",
                };
                map[rawName.toLowerCase()] = entry;
                map[cleanedName.toLowerCase()] = entry;
            }
        }
        return map;
    }

    function demandColor(demand) {
        switch ((demand || "").toLowerCase()) {
            case "high":   return "#4caf50";
            case "medium": case "med": case "mid": return "#ff9800";
            case "low":    return "#f44336";
            default:       return "#888";
        }
    }

    function trendColor(trend) {
        switch ((trend || "").toLowerCase()) {
            case "rising":      return "#4caf50";
            case "stable":      return "#2196f3";
            case "dropping": case "falling": case "fluctuating": return "#f44336";
            default:            return "#888";
        }
    }

    function waitForElement(selector, callback) {
        if (document.querySelector(selector)) { callback(); return; }
        const observer = new MutationObserver(() => {
            if (document.querySelector(selector)) {
                observer.disconnect();
                callback();
            }
        });
        observer.observe(document.body, { childList: true, subtree: true });
    }

    function injectValueBox(valueData) {
        if (document.getElementById("caelus-value-box")) return;
        const itemName = (document.querySelector("h1, h2") || {}).innerText?.trim();
        if (!itemName) return;
        const priceEl = document.querySelector("[class*='amount-']");
        if (!priceEl) return;
        const container = priceEl.closest("p, div");
        if (!container || !container.parentElement) return;
        const entry = valueData[itemName.toLowerCase()];
        const box = document.createElement("div");
        box.id = "caelus-value-box";
        box.style.cssText = `background:#e1e1e1;border-radius:8px;padding:10px 14px;margin-bottom:10px;font-family:sans-serif;font-size:13px;color:#1a1a1a;`;
        if (entry) {
            box.innerHTML = `
                <div style="font-weight:700;font-size:14px;margin-bottom:6px;color:#1a1a1a;">Value Info</div>
                <div style="display:flex;gap:16px;flex-wrap:wrap;">
                    <div><span style="color:#444;">Value</span><br><span style="font-weight:600;color:${demandColor(entry.demand)};">R$ ${Number(entry.value).toLocaleString()}</span></div>
                    <div><span style="color:#444;">Demand</span><br><span style="font-weight:600;color:${demandColor(entry.demand)};">${entry.demand}</span></div>
                    <div><span style="color:#444;">Trend</span><br><span style="font-weight:600;color:${trendColor(entry.trend)};">${entry.trend}</span></div>
                </div>`;
        } else {
            box.innerHTML = `<div style="font-weight:700;font-size:14px;margin-bottom:4px;color:#1a1a1a;">Value Info</div><div style="color:#444;">No value data for <em>${itemName}</em></div>`;
        }
        container.parentElement.insertBefore(box, container);
    }

    function getSlotTotals(slotRow, valueData) {
        let totalValue = 0;
        slotRow.querySelectorAll("img[alt]").forEach(img => {
            const name = img.getAttribute("alt").trim();
            if (!name || img.src.includes("empty.png")) return;
            const entry = valueData[name.toLowerCase()];
            if (entry) totalValue += parseInt(entry.value) || 0;
        });
        return { totalValue };
    }

    function createCalcBox() {
        const box = document.createElement("div");
        box.id = "caelus-calc";
        box.style.cssText = `
            background: #1a1a1a;
            border-radius: 8px;
            padding: 8px 14px;
            font-family: sans-serif;
            font-size: 12px;
            color: #fff;
            margin: 4px 0;
            text-align: center;
        `;
        box.innerHTML = `
            <div style="font-weight:700;font-size:10px;color:#aaa;margin-bottom:4px;text-transform:uppercase;letter-spacing:1px;">Value</div>
            <div id="calc-value-row" style="font-size:14px;font-weight:700;">
                <span id="calc-offer-value" style="color:#4caf50;">0</span>
                <span style="color:#555;margin:0 6px;">vs</span>
                <span id="calc-request-value" style="color:#f44336;">0</span>
            </div>
        `;

        const divider = document.querySelector("[class*='divider-top']");
        if (divider) {
            const dividerRow = divider.closest(".row");
            dividerRow.replaceWith(box);
        } else {
            document.body.appendChild(box);
        }
        return box;
    }

    function updateCalculators(valueData) {
        const card = document.querySelector("[class*='offerRequestCard-']");
        if (!card) return;

        const slotRows = Array.from(card.querySelectorAll("[class*='row-0-2-']"));
        if (slotRows.length < 2) return;

        let box = document.getElementById("caelus-calc");
        if (!box) box = createCalcBox();

        const offer   = getSlotTotals(slotRows[0], valueData);
        const request = getSlotTotals(slotRows[1], valueData);

        document.getElementById("calc-offer-value").innerText   = offer.totalValue.toLocaleString();
        document.getElementById("calc-request-value").innerText = request.totalValue.toLocaleString();
    }

    function labelInventoryItems(valueData) {
        document.querySelectorAll("[class*='itemCard-']").forEach(card => {
            if (card.dataset.valueLabeled) return;
            const a = card.querySelector("a[href*='/catalog/']");
            if (!a) return;
            const name = a.innerText.trim();
            const entry = valueData[name.toLowerCase()];
            card.dataset.valueLabeled = "1";
            if (!entry) return;
            const tag = document.createElement("div");
            tag.style.cssText = `font-size:11px;font-weight:600;color:${demandColor(entry.demand)};text-align:center;margin-top:2px;`;
            tag.innerText = `R$ ${Number(entry.value).toLocaleString()}`;
            const p = a.closest("p");
            if (p) p.after(tag);
        });
    }

    function renameValueToRap() {
        document.querySelectorAll("[class*='valueText-'] .pe-2").forEach(span => {
            if (span.innerText.trim() === "Value:") span.innerText = "RAP:";
        });
    }

    function initTrade(valueData) {
        labelInventoryItems(valueData);
        updateCalculators(valueData);
        renameValueToRap();

        let debounceTimer = null;
        const observer = new MutationObserver(() => {
            clearTimeout(debounceTimer);
            debounceTimer = setTimeout(() => {
                labelInventoryItems(valueData);
                updateCalculators(valueData);
                renameValueToRap();
            }, 300);
        });
        observer.observe(document.body, { childList: true, subtree: true });
    }

    function cleanName(raw) {
        return raw.replace(/^[):>\s]+/, "").trim();
    }

    function initLimiteds(valueData) {
        let totalValue = 0;

        document.querySelectorAll("p.mb-0.fw-bolder").forEach(p => {
            if (p.dataset.valueLabeled) return;
            p.dataset.valueLabeled = "1";

            const raw = p.innerText.trim();
            const name = cleanName(raw);
            const entry = valueData[name.toLowerCase()];
            if (!entry) return;

            const val = parseInt(entry.value) || 0;
            totalValue += val;

            const tag = document.createElement("p");
            tag.style.cssText = `margin:0;font-size:11px;font-weight:600;color:${demandColor(entry.demand)};`;
            tag.innerText = `Value: R$ ${val.toLocaleString()}`;
            p.after(tag);
        });

        if (document.getElementById("caelus-total-value")) return;
        const rapEl = document.querySelector("p.rap");
        if (!rapEl) return;

        const totalEl = document.createElement("p");
        totalEl.id = "caelus-total-value";
        totalEl.style.cssText = `margin:0;font-weight:600;color:#4caf50;`;
        totalEl.innerHTML = `Total Value: <span class="fw-bold">R$ ${totalValue.toLocaleString()}</span>`;
        rapEl.after(totalEl);
    }

    function initTrades(valueData) {
        let debounce = null;

        function processDetails() {
            document.querySelectorAll("[class*='innerSection-']").forEach(panel => {
                if (panel.dataset.valueProcessed) return;
                panel.dataset.valueProcessed = "1";

                const sections = panel.querySelectorAll(".row");
                let giveItems = [];
                let receiveItems = [];
                let currentSection = null;

                const allItemCols = Array.from(panel.querySelectorAll("[class*='col-0-2-']"));
                const half = Math.ceil(allItemCols.length / 2);
                allItemCols.forEach((col, idx) => {
                    const a = col.querySelector("a[href*='/catalog/']");
                    const img = col.querySelector("img[class*='image-']");
                    if (!a && !img) return;
                    const name = cleanName((img?.getAttribute("alt") || a?.innerText || "").trim());
                    if (!name) return;
                    if (idx < half) giveItems.push(name);
                    else receiveItems.push(name);
                });

                let giveValue = 0, receiveValue = 0;
                giveItems.forEach(name => {
                    const entry = valueData[name.toLowerCase()];
                    if (entry) giveValue += parseInt(entry.value) || 0;
                });
                receiveItems.forEach(name => {
                    const entry = valueData[name.toLowerCase()];
                    if (entry) receiveValue += parseInt(entry.value) || 0;
                });

                panel.querySelectorAll("[class*='itemName-']").forEach(p => {
                    if (p.dataset.valueLabeled) return;
                    p.dataset.valueLabeled = "1";
                    const a = p.querySelector("a[href*='/catalog/']");
                    if (!a) return;
                    const name = cleanName(a.innerText.trim());
                    const entry = valueData[name.toLowerCase()];
                    if (!entry) return;
                    const tag = document.createElement("p");
                    tag.style.cssText = `margin:0;font-size:11px;font-weight:600;color:${demandColor(entry.demand)};padding:0 4px;`;
                    tag.innerText = `R$ ${Number(entry.value).toLocaleString()}`;
                    p.after(tag);
                });

                const divider = panel.querySelector(".divider-top");

                const buttonRow = panel.querySelector("button[class*='acceptButton-']")?.closest(".row.mt-4");
                if (!buttonRow) return;

                const calc = document.createElement("div");
                calc.style.cssText = `
                    background:#1a1a1a;
                    border-radius:8px;
                    padding:6px 12px;
                    font-family:sans-serif;
                    font-size:12px;
                    color:#fff;
                    display:inline-block;
                    margin-right: 10px;
                    vertical-align:middle;
                `;
                calc.innerHTML = `
                    <div style="font-weight:700;font-size:10px;color:#aaa;margin-bottom:2px;text-transform:uppercase;letter-spacing:1px;">Value</div>
                    <div style="font-size:14px;font-weight:700;">
                        <span style="color:#4caf50;">${giveValue.toLocaleString()}</span>
                        <span style="color:#555;margin:0 5px;">vs</span>
                        <span style="color:#f44336;">${receiveValue.toLocaleString()}</span>
                    </div>
                `;

                const col = document.createElement("div");
                col.className = "col-3";
                col.style.cssText = "display:flex;align-items:center;";
                col.appendChild(calc);

                const offsetCol = buttonRow.querySelector(".offset-2");
                if (offsetCol) {
                    offsetCol.classList.remove("offset-2");
                    offsetCol.classList.add("col-7");
                }
                buttonRow.querySelector(".row.mx-auto")?.closest(".col-8")?.before(col);
            });
        }

        processDetails();
        const observer = new MutationObserver(() => {
            clearTimeout(debounce);
            debounce = setTimeout(processDetails, 300);
        });
        observer.observe(document.body, { childList: true, subtree: true });
    }

    function initProfile(valueData) {
        const match = window.location.href.match(/\/users\/(\d+)\//);
        if (!match) return;
        const userId = match[1];

        GM_xmlhttpRequest({
            method: "GET",
            url: `https://www.caelus.lol/internal/limiteds?userId=${userId}`,
            onload: function(res) {
                const parser = new DOMParser();
                const doc = parser.parseFromString(res.responseText, "text/html");

                const rapEl = doc.querySelector("p.rap span.fw-bold");
                const rap = rapEl ? rapEl.innerText.trim() : "0";

                let totalValue = 0;
                doc.querySelectorAll("p.mb-0.fw-bolder").forEach(p => {
                    const name = cleanName(p.innerText.trim());
                    const entry = valueData[name.toLowerCase()];
                    if (entry) totalValue += parseInt(entry.value) || 0;
                });

                if (document.getElementById("caelus-profile-stats")) return;

                const usernameEl = document.querySelector("h2[class*='username-']");
                if (!usernameEl) return;

                let targetEl = null;
                let sibling = usernameEl.nextElementSibling;
                while (sibling) {
                    if (sibling.tagName === "P" && sibling.innerHTML.includes("&emsp;") || sibling.innerText.trim() === "") {
                        targetEl = sibling;
                        break;
                    }
                    sibling = sibling.nextElementSibling;
                }

                const statsBox = document.createElement("div");
                statsBox.id = "caelus-profile-stats";
                statsBox.style.cssText = `display:flex;gap:16px;font-family:sans-serif;`;
                statsBox.innerHTML = `
                    <div style="text-align:center;">
                        <p style="margin:0;font-size:16px;font-weight:700;color:#2196f3;">${Number(rap.replace(/,/g,'')).toLocaleString()}</p>
                        <p style="margin:0;font-size:12px;color:#888;">RAP</p>
                    </div>
                    <div style="text-align:center;">
                        <p style="margin:0;font-size:16px;font-weight:700;color:#4caf50;">${totalValue.toLocaleString()}</p>
                        <p style="margin:0;font-size:12px;color:#888;">Value</p>
                    </div>
                `;

                if (targetEl) {
                    targetEl.replaceWith(statsBox);
                } else {
                    usernameEl.after(statsBox);
                }
            },
            onerror: () => console.error("[Caelus Values] Failed to fetch limiteds for user " + userId)
        });
    }

    function start() {
        GM_xmlhttpRequest({
            method: "GET",
            url: VALUE_LIST_URL,
            onload: function (res) {
                const valueData = parseValues(res.responseText);
                if (window.location.href.includes("/catalog/")) {
                    waitForElement("[class*='amount-']", () => injectValueBox(valueData));
                } else if (window.location.href.includes("/trade/")) {
                    waitForElement("[class*='offerRequestCard-']", () => initTrade(valueData));
                } else if (window.location.href.includes("/users/") && window.location.href.includes("/profile")) {
                    waitForElement("[class*='username-']", () => initProfile(valueData));
                } else if (window.location.href.includes("/trades")) {
                    waitForElement("table", () => initTrades(valueData));
                } else if (window.location.href.includes("/internal/limiteds")) {
                    waitForElement("p.mb-0.fw-bolder", () => {
                        initLimiteds(valueData);
                        let t = null;
                        const obs = new MutationObserver(() => {
                            clearTimeout(t);
                            t = setTimeout(() => initLimiteds(valueData), 300);
                        });
                        obs.observe(document.body, { childList: true, subtree: true });
                    });
                }
            },
            onerror: () => console.error("[Caelus Values] Failed to fetch value list.")
        });
    }

    if (document.readyState === "complete") {
        start();
    } else {
        window.addEventListener("load", start);
    }

})();