Caelus Value Display

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

Tendrás que instalar una extensión para tu navegador como Tampermonkey, Greasemonkey o Violentmonkey si quieres utilizar este script.

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

Tendrás que instalar una extensión como Tampermonkey o Violentmonkey para instalar este script.

Necesitarás instalar una extensión como Tampermonkey o Userscripts para instalar este script.

Tendrás que instalar una extensión como Tampermonkey antes de poder instalar este script.

Necesitarás instalar una extensión para administrar scripts de usuario si quieres instalar este script.

(Ya tengo un administrador de scripts de usuario, déjame instalarlo)

Tendrás que instalar una extensión como Stylus antes de poder instalar este script.

Tendrás que instalar una extensión como Stylus antes de poder instalar este script.

Tendrás que instalar una extensión como Stylus antes de poder instalar este script.

Para poder instalar esto tendrás que instalar primero una extensión de estilos de usuario.

Para poder instalar esto tendrás que instalar primero una extensión de estilos de usuario.

Para poder instalar esto tendrás que instalar primero una extensión de estilos de usuario.

(Ya tengo un administrador de estilos de usuario, déjame instalarlo)

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

})();