Drink Gains

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

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!)

Advertisement:

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!)

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 };
    }
})();