Drink Gains

Shows energy per can and nerve per alcohol inline on the items page (perk-adjusted; forked from TornTools)

Bu betiği kurabilmeniz için Tampermonkey, Greasemonkey ya da Violentmonkey gibi bir kullanıcı betiği eklentisini kurmanız gerekmektedir.

Bu betiği yüklemek için Tampermonkey gibi bir uzantı yüklemeniz gerekir.

Bu betiği kurabilmeniz için Tampermonkey ya da Violentmonkey gibi bir kullanıcı betiği eklentisini kurmanız gerekmektedir.

Bu betiği kurabilmeniz için Tampermonkey ya da Userscripts gibi bir kullanıcı betiği eklentisini kurmanız gerekmektedir.

Bu betiği indirebilmeniz için ayrıca Tampermonkey gibi bir eklenti kurmanız gerekmektedir.

Bu komut dosyasını yüklemek için bir kullanıcı komut dosyası yöneticisi uzantısı yüklemeniz gerekecek.

(Zaten bir kullanıcı komut dosyası yöneticim var, kurmama izin verin!)

Advertisement:

Bu stili yüklemek için Stylus gibi bir uzantı yüklemeniz gerekir.

Bu stili yüklemek için Stylus gibi bir uzantı kurmanız gerekir.

Bu stili yükleyebilmek için Stylus gibi bir uzantı yüklemeniz gerekir.

Bu stili yüklemek için bir kullanıcı stili yöneticisi uzantısı yüklemeniz gerekir.

Bu stili yüklemek için bir kullanıcı stili yöneticisi uzantısı kurmanız gerekir.

Bu stili yükleyebilmek için bir kullanıcı stili yöneticisi uzantısı yüklemeniz gerekir.

(Zateb bir user-style yöneticim var, yükleyeyim!)

Advertisement:

// ==UserScript==
// @name         Drink Gains
// @namespace    RussianRob
// @author       RussianRob
// @version      1.2.2
// @description  Shows energy per can and nerve per alcohol inline on the items page (perk-adjusted; forked from TornTools)
// @license      GPL-3.0-or-later
// @match        https://www.torn.com/item.php*
// @grant        GM_xmlhttpRequest
// @grant        GM_setValue
// @grant        GM_getValue
// @grant        GM_addStyle
// @grant        GM_registerMenuCommand
// @connect      api.torn.com
// ==/UserScript==
(function () {
    "use strict";

    const SCRIPT_VERSION = "1.2.2";
    const KEY_STORE = "ce_apikey";
    const MULT_STORE = "ce_mult";
    const MULT_TTL = 24 * 60 * 60 * 1000;
    const CAL_STORE = "ce_cal";
    const IS_PDA = typeof window !== "undefined" && typeof window.flutter_inappwebview !== "undefined";
    const PDA_API_KEY = "###PDA-APIKEY###";

    const CANS = [
        ["goose juice", 5], ["damp valley", 10], ["crocozade", 15], ["munster", 20],
        ["santa shooters", 20], ["red cow", 25], ["rockstar rudolph", 25],
        ["taurine elite", 30], ["x-mass", 30],
    ];
    const CAN_BASE = { 985: 5, 986: 10, 987: 15, 530: 20, 553: 20, 532: 25, 554: 25, 533: 30, 555: 30 };
    const NERVE_BASE = { 180: 1, 181: 1, 294: 1, 426: 1, 531: 2, 541: 4, 542: 3, 550: 2, 551: 3, 552: 4, 638: 3, 816: 2, 873: 5, 924: 5, 984: 5 };
    const PROVIDERS = [
        { key: "energy", cls: "ce-energy", base: CAN_BASE, tip: "Effective energy (your perks)",
          value: (base, p, ctx) => effectiveEnergy(base, p ? p.energyMult : 1, !!(ctx && ctx.events && ctx.events.caffeineCon)) + "E" },
        { key: "nerve", cls: "ce-nerve", base: NERVE_BASE, tip: "Effective nerve (your perks)",
          value: (base, p, ctx) => {
              const ev = (ctx && ctx.events) || {};
              const id = ctx ? ctx.id : 0;
              const mult = (ev.stPatricks ? 2 : 1) * (ev.beerDay && (id === 180 || id === 816) ? 5 : 1);
              return nerveRange(base, p ? p.alcFaction : 0, p ? p.alcCompany : 0, mult);
          } },
    ];

    let lastError = null;
    let renderTimer = null;

    function digitsPct(s) {
        const n = parseInt(String(s).replace(/\D+/g, ""), 10);
        return Number.isNaN(n) ? 0 : n;
    }

    function alcoholPerks(perks) {
        const faction = (perks.faction_perks || []).find((s) => /alcohol/i.test(s));
        const company = (perks.job_perks || []).find((s) => /alcohol boost|consumable boost/i.test(s));
        return { faction: faction ? digitsPct(faction) : 0, company: company ? digitsPct(company) : 0 };
    }

    function computePerks(payload) {
        const alc = alcoholPerks(payload);
        return { energyMult: perkMultiplier(payload), alcFaction: alc.faction, alcCompany: alc.company };
    }

    function perkMultiplier(perks) {
        const arrs = [perks.faction_perks, perks.job_perks, perks.book_perks];
        let mult = 1;
        for (const arr of arrs) {
            for (const s of arr || []) {
                if (!/energy drinks/i.test(s) && !/consumable gain/i.test(s)) continue;
                const n = parseInt(String(s).replace(/\D+/g, ""), 10);
                if (!Number.isNaN(n)) mult *= 1 + n / 100;
            }
        }
        return mult;
    }

    function effectiveEnergy(base, mult, eventActive) {
        return Math.round(base * mult) * (eventActive ? 2 : 1);
    }

    function nerveRange(base, faction, company, eventMult) {
        const total = base * (1 + faction / 100) * (1 + company / 100) * (eventMult || 1);
        const min = Math.floor(total), max = Math.ceil(total);
        return min === max ? min + " N" : min + " - " + max + " N";
    }

    function eventActive(events, matcher, now) {
        const ev = (events || []).find(matcher);
        if (!ev) return false;
        const start = ev.start * 1000 - 86400000;
        const end = ev.end * 1000 + 86400000;
        return now > start && now < end;
    }

    function computeEvents(events, now) {
        return {
            caffeineCon: eventActive(events, (e) => /^caffeinecon/i.test((e.title || "").trim()), now),
            stPatricks: eventActive(events, (e) => /\bst\.?\s*patrick/i.test(e.title || ""), now),
            beerDay: eventActive(events, (e) => /^international beer day(\s+\d{4})?$/i.test((e.title || "").trim()), now),
        };
    }

    function canBase(text) {
        const t = (text || "").toLowerCase();
        for (const [n, b] of CANS) if (t.indexOf(n) !== -1) return b;
        return null;
    }

    function nameElForRow(row) {
        return row.querySelector(
            ".name-wrap .name, .item-name, .name-wrap, .title-wrap .name, [class*='name___'], [class*='itemName']"
        );
    }

    function rowFullName(row) {
        const al = row.querySelector("[aria-label]");
        if (al) {
            const v = al.getAttribute("aria-label") || "";
            if (/(can|bottle|glass) of /i.test(v)) return v;
        }
        const ds = row.getAttribute("data-sort") || "";
        const m = ds.match(/(can|bottle|glass) of .+$/i);
        return m ? m[0] : "";
    }

    function findNameTextEl(row, fullName) {
        if (!fullName) return null;
        const cand = row.querySelectorAll("a, span, b, p, div");
        let contains = null;
        for (let i = 0; i < cand.length; i++) {
            const el = cand[i];
            if (el.children.length !== 0) continue;
            const t = (el.textContent || "").trim();
            if (t === fullName) return el;
            if (!contains && t.indexOf(fullName) !== -1 && t.length < fullName.length + 14) contains = el;
        }
        return contains;
    }

    function findRows() {
        const rows = document.querySelectorAll(
            "ul.items-cont > li, ul.items-list > li, li.show-item-info, [data-category='Energy Drink'], [data-category='Alcohol']"
        );
        const out = [];
        const seen = new Set();
        rows.forEach((row) => {
            if (seen.has(row)) return;
            seen.add(row);
            const id = parseInt(row.getAttribute("data-item"), 10);
            let provider = null, base = null;
            for (const p of PROVIDERS) {
                if (p.base[id] != null) { provider = p; base = p.base[id]; break; }
            }
            if (provider == null) {
                const b = canBase((nameElForRow(row) || row).textContent);
                if (b != null) { provider = PROVIDERS[0]; base = b; }
            }
            if (provider == null) return;
            const nameLeaf = findNameTextEl(row, rowFullName(row)) || nameElForRow(row) || row;
            const nameWrap = row.querySelector(".name-wrap");
            out.push({ row: row, id: id, nameLeaf: nameLeaf, nameWrap: nameWrap, provider: provider, base: base });
        });
        return out;
    }

    const getKey = () => {
        const k = (GM_getValue(KEY_STORE, "") || "").trim();
        if (k) return k;
        if (IS_PDA && PDA_API_KEY.indexOf("#") === -1) return PDA_API_KEY;
        return "";
    };

    function cachedPerks() {
        try {
            const m = JSON.parse(GM_getValue(MULT_STORE, ""));
            if (m && typeof m.energyMult === "number" && typeof m.alcFaction === "number" && typeof m.alcCompany === "number") return m;
            return null;
        } catch (e) {
            return null;
        }
    }

    function fetchPerks() {
        const key = getKey();
        if (!key) return;
        GM_xmlhttpRequest({
            method: "GET",
            url: "https://api.torn.com/user/?selections=perks&key=" + encodeURIComponent(key),
            onload: (r) => {
                try {
                    const d = JSON.parse(r.responseText);
                    if (d.error) { lastError = d.error.error; return; }
                    GM_setValue(MULT_STORE, JSON.stringify(Object.assign(computePerks(d), { fetchedAt: Date.now() })));
                    lastError = null;
                    render();
                } catch (e) {
                    lastError = String(e);
                }
            },
            onerror: () => { lastError = "network error"; },
        });
    }

    function cachedCalendar() {
        try {
            const c = JSON.parse(GM_getValue(CAL_STORE, ""));
            if (c && Array.isArray(c.events)) return c;
            return null;
        } catch (e) {
            return null;
        }
    }

    function activeEvents() {
        const c = cachedCalendar();
        return computeEvents(c ? c.events : [], Date.now());
    }

    function fetchCalendar() {
        const key = getKey();
        if (!key) return;
        GM_xmlhttpRequest({
            method: "GET",
            url: "https://api.torn.com/v2/torn?selections=calendar&key=" + encodeURIComponent(key),
            onload: (r) => {
                try {
                    const d = JSON.parse(r.responseText);
                    if (d && d.error) return;
                    const cal = d && d.calendar ? d.calendar : null;
                    const events = cal && Array.isArray(cal.events) ? cal.events : [];
                    GM_setValue(CAL_STORE, JSON.stringify({ events: events, fetchedAt: Date.now() }));
                    render();
                } catch (e) {}
            },
            onerror: () => {},
        });
    }

    function render() {
        const perks = cachedPerks();
        const events = activeEvents();
        const hasKey = !!getKey();
        const rows = findRows();
        rows.forEach((entry) => {
            const row = entry.row;
            let span = row.querySelector(".ce-badge");
            if (!span) {
                span = document.createElement("span");
                span.className = "ce-badge " + entry.provider.cls;
            }
            const priced = !!row.querySelector(".rwp-base-price-tag");
            let ref;
            if (priced && entry.nameLeaf && entry.nameWrap &&
                entry.nameLeaf !== entry.nameWrap && entry.nameWrap.contains(entry.nameLeaf)) {
                ref = entry.nameLeaf;
            } else if (entry.nameWrap) {
                ref = entry.nameWrap;
            } else {
                ref = entry.nameLeaf || row;
            }
            if (ref === row) {
                if (span.parentElement !== ref) ref.appendChild(span);
            } else if (span.previousElementSibling !== ref) {
                ref.insertAdjacentElement("afterend", span);
            }
            const txt = " " + entry.provider.value(entry.base, perks, { id: entry.id, events: events }) + (hasKey ? "" : "*");
            if (span.textContent !== txt) span.textContent = txt;
            const tip = hasKey ? entry.provider.tip
                : (lastError ? "API error: " + lastError : "Base value — tap the cog to add your API key for perk-adjusted values");
            if (span.title !== tip) span.title = tip;
        });
        injectCog();
    }

    function scheduleRender() {
        if (renderTimer) return;
        renderTimer = setTimeout(() => { renderTimer = null; render(); }, 200);
    }

    function injectCog() {
        let cog = document.getElementById("ce-cog");
        if (!cog) {
            const firstBadge = document.querySelector(".ce-badge");
            const list = document.querySelector("ul.items-cont, ul.items-list") || (firstBadge && firstBadge.closest("ul"));
            if (!list || !list.parentElement) return;
            const wrap = document.createElement("div");
            wrap.id = "ce-cog-wrap";
            cog = document.createElement("span");
            cog.id = "ce-cog";
            cog.title = "Drink Gains — set your Torn API key for perk-adjusted energy & nerve";
            cog.addEventListener("click", (e) => { e.preventDefault(); e.stopPropagation(); toggleKeyPanel(cog); });
            wrap.appendChild(cog);
            list.parentElement.insertBefore(wrap, list);
        }
        const label = getKey() ? "⚙ Drink Gains" : "⚙ Drink Gains — tap to add API key";
        if (cog.textContent !== label) cog.textContent = label;
    }

    function toggleKeyPanel(cog) {
        let panel = document.getElementById("ce-keypanel");
        if (panel) { panel.remove(); return; }
        panel = document.createElement("span");
        panel.id = "ce-keypanel";
        const input = document.createElement("input");
        input.type = "text";
        input.placeholder = "Torn API key";
        input.value = getKey();
        input.autocomplete = "off";
        input.autocapitalize = "off";
        input.spellcheck = false;
        const save = document.createElement("button");
        save.textContent = "Save";
        save.addEventListener("click", (e) => {
            e.preventDefault();
            e.stopPropagation();
            const k = input.value.trim();
            GM_setValue(KEY_STORE, k);
            panel.remove();
            if (k) { fetchPerks(); fetchCalendar(); } else render();
        });
        panel.appendChild(input);
        panel.appendChild(save);
        cog.parentElement.appendChild(panel);
        try { input.focus(); } catch (e) {}
    }

    function boot() {
        const c = cachedPerks();
        if (getKey() && (!c || Date.now() - c.fetchedAt > MULT_TTL)) fetchPerks();
        const cal = cachedCalendar();
        if (getKey() && (!cal || Date.now() - cal.fetchedAt > MULT_TTL)) fetchCalendar();
        render();
    }

    if (typeof document !== "undefined") {
        try { GM_addStyle(".ce-badge{font-weight:600;} .ce-energy{color:#19b34a;} .ce-nerve{color:#e0556b;} #ce-cog-wrap{padding:6px 10px;} #ce-cog{cursor:pointer;display:inline-block;padding:3px 12px;border:1px solid #19b34a;border-radius:14px;background:#1c2030;color:#19b34a;font-size:.85em;font-weight:600;opacity:.9;} #ce-cog:hover{opacity:1;} #ce-keypanel{margin-top:6px;display:flex;gap:4px;align-items:center;flex-wrap:wrap;} #ce-keypanel input{width:170px;padding:3px 6px;font-size:.85em;border:1px solid #2a3447;border-radius:6px;background:#1c2030;color:#e6e8ee;} #ce-keypanel button{padding:3px 10px;font-size:.85em;border:1px solid #19b34a;border-radius:6px;background:#19b34a;color:#fff;cursor:pointer;}"); } catch (e) {}
        try { GM_registerMenuCommand("Drink Gains: refresh", function () { fetchPerks(); fetchCalendar(); }); } catch (e) {}
        try { new MutationObserver(scheduleRender).observe(document.body, { childList: true, subtree: true }); } catch (e) {}
        boot();
    }
    if (typeof module !== "undefined" && typeof module.exports !== "undefined") {
        module.exports = { perkMultiplier, effectiveEnergy, alcoholPerks, nerveRange, eventActive, computeEvents, computePerks, PROVIDERS, CAN_BASE, NERVE_BASE };
    }
})();