Caelus Value Display

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

You will need to install an extension such as Tampermonkey, Greasemonkey or Violentmonkey to install this script.

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

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

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

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

You will need to install a user script manager extension to install this script.

(I already have a user script manager, let me install it!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

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

})();