Caelus Value Display

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

Você precisará instalar uma extensão como Tampermonkey, Greasemonkey ou Violentmonkey para instalar este script.

Você precisará instalar uma extensão como Tampermonkey para instalar este script.

Você precisará instalar uma extensão como Tampermonkey ou Violentmonkey para instalar este script.

Você precisará instalar uma extensão como Tampermonkey ou Userscripts para instalar este script.

Você precisará instalar uma extensão como o Tampermonkey para instalar este script.

Você precisará instalar um gerenciador de scripts de usuário para instalar este script.

(Eu já tenho um gerenciador de scripts de usuário, me deixe instalá-lo!)

Você precisará instalar uma extensão como o Stylus para instalar este estilo.

Você precisará instalar uma extensão como o Stylus para instalar este estilo.

Você precisará instalar uma extensão como o Stylus para instalar este estilo.

Você precisará instalar um gerenciador de estilos de usuário para instalar este estilo.

Você precisará instalar um gerenciador de estilos de usuário para instalar este estilo.

Você precisará instalar um gerenciador de estilos de usuário para instalar este estilo.

(Eu já possuo um gerenciador de estilos de usuário, me deixar fazer a instalação!)

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

})();