Caelus Value Display

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

Vous devrez installer une extension telle que Tampermonkey, Greasemonkey ou Violentmonkey pour installer ce script.

Vous devrez installer une extension telle que Tampermonkey pour installer ce script.

Vous devrez installer une extension telle que Tampermonkey ou Violentmonkey pour installer ce script.

Vous devrez installer une extension telle que Tampermonkey ou Userscripts pour installer ce script.

Vous devrez installer une extension telle que Tampermonkey pour installer ce script.

Vous devrez installer une extension de gestionnaire de script utilisateur pour installer ce script.

(J'ai déjà un gestionnaire de scripts utilisateur, laissez-moi l'installer !)

Vous devrez installer une extension telle que Stylus pour installer ce style.

Vous devrez installer une extension telle que Stylus pour installer ce style.

Vous devrez installer une extension telle que Stylus pour installer ce style.

Vous devrez installer une extension du gestionnaire de style pour utilisateur pour installer ce style.

Vous devrez installer une extension du gestionnaire de style pour utilisateur pour installer ce style.

Vous devrez installer une extension du gestionnaire de style pour utilisateur pour installer ce style.

(J'ai déjà un gestionnaire de style utilisateur, laissez-moi l'installer!)

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

})();