Torn Addiction Tracker

Tracks drug use, overdoses, addiction points, and company addiction between rehabs

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         Torn Addiction Tracker
// @namespace    torn.addiction.tracker
// @version      2.0.6
// @description  Tracks drug use, overdoses, addiction points, and company addiction between rehabs
// @author       xaeksx
// @match        https://www.torn.com/*
// @grant        GM.addStyle
// @run-at       document-start
// @license      MIT
// @supportURL   https://www.torn.com/forums.php#/p=forums&f=67
// @homepageURL  https://greasyfork.org
// ==/UserScript==

/*
  ┌─────────────────────────────────────────────────────────────────────────┐
  │  API TERMS OF SERVICE DISCLOSURE  (required by Torn API ToS guidelines) │
  ├──────────────────┬──────────────────────────────────────────────────────┤
  │ Data Storage     │ Only locally (browser localStorage on your device).  │
  │                  │ Nothing is stored on any external server.             │
  ├──────────────────┼──────────────────────────────────────────────────────┤
  │ Data Sharing     │ Nobody. Data never leaves your browser.               │
  ├──────────────────┼──────────────────────────────────────────────────────┤
  │ Purpose of Use   │ Personal gain — track own drug usage and estimate     │
  │                  │ Company Addiction impact between rehabs.              │
  ├──────────────────┼──────────────────────────────────────────────────────┤
  │ Key Storage      │ Stored only in your browser's localStorage.           │
  │ & Sharing        │ Never transmitted anywhere except api.torn.com.       │
  ├──────────────────┼──────────────────────────────────────────────────────┤
  │ Key Access Level │ Limited Access (minimum required).                    │
  │                  │ Selections used: user → personalstats, events         │
  │                  │ Perk setting: Addiction XXV faction perk (0–50%)      │
  └──────────────────┴──────────────────────────────────────────────────────┘

  API KEY SETUP:
  - Go to: Torn → Settings → API  (torn.com/preferences.php#tab=api)
  - Click "Add new key"
  - Set access level to "Limited Access" (minimum needed for personalstats + events)
  - Give it a recognisable name, e.g. "Addiction Tracker"
  - Paste the key into this script's Settings panel (⚙ button on the floating widget)
  - IMPORTANT: Use a dedicated key for this script, NOT your Full Access key.
    That way you can revoke it independently if needed.

  NOTES:
  - On first install, your current drug totals become the baseline (drugs before
    install won't count). Use "Manual Rehab Reset" in settings if needed.
  - Overdose detection is event-based. OD events are scanned incrementally each
    poll, so they won't be missed even if they happen between polls.
  - Only Xanax (+100 AP) and Ecstasy (+20 AP) overdoses contribute extra AP.
  - API field names are based on Torn API v1 personalstats. If a drug always
    shows 0, verify field names at:
    https://api.torn.com/user/?selections=personalstats&key=YOUR_KEY
    Known uncertain field: Love Juice → 'lovtaken' (may differ — check if 0).
*/

'use strict';

// ─────────────────────────────────────────────────────────────────────────────
// GM SHIM  (TornPDA / Greasemonkey compatibility)
// ─────────────────────────────────────────────────────────────────────────────
try {
    if (typeof GM === 'undefined') window.GM = {};
    if (typeof GM.addStyle === 'undefined') {
        GM.addStyle = (css) => {
            const s = document.createElement('style');
            s.textContent = css;
            document.head.appendChild(s);
        };
    }
} catch {}

// ─────────────────────────────────────────────────────────────────────────────
// DRUG DEFINITIONS
// Each drug has: a display label, AP value, and the personalstats API field name.
// ─────────────────────────────────────────────────────────────────────────────
const DRUGS = [
    { id: 'xanax',     label: 'Xanax',      ap: 35, field: 'xantaken'  },
    { id: 'lovejuice', label: 'Love Juice',  ap: 50, field: 'lovtaken'  }, // ⚠ verify field
    { id: 'pcp',       label: 'PCP',         ap: 26, field: 'pcptaken'  },
    { id: 'ecstasy',   label: 'Ecstasy',     ap: 20, field: 'exttaken'  },
    { id: 'lsd',       label: 'LSD',         ap: 20, field: 'lsdtaken'  },
    { id: 'speed',     label: 'Speed',       ap: 14, field: 'spetaken'  },
    { id: 'vicodin',   label: 'Vicodin',     ap: 13, field: 'victaken'  },
    { id: 'opium',     label: 'Opium',       ap: 10, field: 'opitaken'  },
    { id: 'ketamine',  label: 'Ketamine',    ap: 8,  field: 'kettaken'  },
    { id: 'shrooms',   label: 'Shrooms',     ap: 6,  field: 'shrtaken'  },
    { id: 'cannabis',  label: 'Cannabis',    ap: 1,  field: 'cantaken'  },
];

// ─────────────────────────────────────────────────────────────────────────────
// CONSTANTS
// ─────────────────────────────────────────────────────────────────────────────
const DECAY_AP    = 20;    // AP subtracted at 5 TCT every day
const DECAY_HOUR  = 3;     // 03:00 UTC = 3 TCT
const CA_HOUR     = 18;    // 18:00 UTC = 18 TCT  (CA is calculated here)
const AP_PER_CA   = 8;     // 8 AP = −1 Company Addiction point
const POLL_MS        = 30000;  // personalstats every 30 seconds (uses Torn's cache naturally)
const POLL_EVENTS_MS = 300000; // events every 5 minutes (rehab/OD detection)

// Overdose bonus AP per OD event (only Xanax and Ecstasy generate extra AP)
const OD_AP = {
    xanax:   100,
    ecstasy:  20,
};

// Torn item image URLs (item ID confirmed via Torn API torn/?selections=items)
const XANAX_IMG_URL = 'https://www.torn.com/images/items/206/small.png';

// localStorage keys
const LS = {
    API_KEY    : 'tat-api-key',
    FAC_MIT    : 'tat-fac-mit',    // Addiction XXV perk  0–50  (%)
    BASELINE   : 'tat-baseline',   // JSON: drug totals at last rehab reset
    REHAB_TS   : 'tat-rehab-ts',   // ms timestamp of last rehab reset
    OD_XANAX   : 'tat-od-xanax',   // cumulative Xanax OD count since rehab
    OD_ECSTASY : 'tat-od-ecstasy', // cumulative Ecstasy OD count since rehab
    OD_LAST_TS : 'tat-od-last-ts', // ms timestamp of the most recent event we scanned
    POS        : 'tat-pos',        // 0=BL 1=TL 2=BR 3=TR
    SIZE       : 'tat-size',       // small | medium | large
};

// ─────────────────────────────────────────────────────────────────────────────
// REACTIVE STATE
// ─────────────────────────────────────────────────────────────────────────────
let S = {
    drugCounts  : {},   // { drugId: countSinceRehab }
    odXanax     : 0,    // Xanax overdoses since last rehab
    odEcstasy   : 0,    // Ecstasy overdoses since last rehab
    odAP        : 0,    // total extra AP from overdoses
    rawAP       : 0,    // sum of (count × AP) + odAP, before any reductions
    mitigatedAP : 0,    // rawAP × (1 − Addiction XXV%)
    decayCount  : 0,    // number of 5TCT crossings since rehab
    currentAP   : 0,    // mitigatedAP − (decayCount × 20), floored at 0
    apAt18      : 0,    // projected AP when CA is calculated at 18 TCT
    caImpact    : 0,    // round(apAt18 / 8)
    lastPoll    : null, // Date.now() of last successful API call
    apiError    : null, // error string or null
    expanded    : false,
    inSettings  : false,
};

// ─────────────────────────────────────────────────────────────────────────────
// STORAGE HELPERS
// ─────────────────────────────────────────────────────────────────────────────
const lsGet  = (k, d) => { const v = localStorage.getItem(k); return v === null ? d : v; };
const lsNum  = (k, d) => { const v = parseFloat(lsGet(k, d)); return isNaN(v) ? d : v; };
const lsSet  = (k, v) => localStorage.setItem(k, String(v));

function getBaseline() {
    try { return JSON.parse(localStorage.getItem(LS.BASELINE)) || {}; }
    catch { return {}; }
}

// Save current personalstats as the new rehab baseline.
// Also resets all OD counters — a new session starts clean.
function saveBaseline(stats) {
    const b = {};
    DRUGS.forEach(d => { b[d.id] = stats[d.field] || 0; });
    lsSet(LS.BASELINE,   JSON.stringify(b));
    lsSet(LS.REHAB_TS,   Date.now());
    // Reset OD tracking — new cycle starts from now
    lsSet(LS.OD_XANAX,   0);
    lsSet(LS.OD_ECSTASY, 0);
    lsSet(LS.OD_LAST_TS, Date.now());
    console.log('[TornAddictionTracker] Baseline saved, OD counters reset.');
}

// ─────────────────────────────────────────────────────────────────────────────
// TIME UTILITIES  (all UTC because TCT = UTC)
// ─────────────────────────────────────────────────────────────────────────────

// Returns how many times HH:00 UTC has ticked by since `sinceMs` up to now
function countHourCrossings(hourUTC, sinceMs) {
    if (!sinceMs) return 0;
    const now = Date.now();
    if (sinceMs >= now) return 0;
    const DAY = 86400000;
    let t = new Date(sinceMs);
    t.setUTCHours(hourUTC, 0, 0, 0);
    if (t.getTime() <= sinceMs) t = new Date(t.getTime() + DAY);
    let count = 0;
    while (t.getTime() <= now) { count++; t = new Date(t.getTime() + DAY); }
    return count;
}

// ms until the NEXT occurrence of HH:00 UTC from right now
function msUntil(hourUTC) {
    const now = new Date();
    let t = new Date();
    t.setUTCHours(hourUTC, 0, 0, 0);
    if (t <= now) t = new Date(t.getTime() + 86400000);
    return t.getTime() - now.getTime();
}

// Format ms duration as "Xh Ym", "Xm Ys", or "Xs"
function fmtMs(ms) {
    if (ms <= 0) return '0s';
    const h = Math.floor(ms / 3600000);
    const m = Math.floor((ms % 3600000) / 60000);
    const s = Math.floor((ms % 60000) / 1000);
    if (h > 0) return `${h}h ${m}m`;
    if (m > 0) return `${m}m ${s}s`;
    return `${s}s`;
}

// ─────────────────────────────────────────────────────────────────────────────
// CALCULATION ENGINE
// Called after every successful API poll with fresh personalstats.
// ─────────────────────────────────────────────────────────────────────────────
function recalculate(personalstats) {
    const facMit   = lsNum(LS.FAC_MIT, 0) / 100;
    const baseline = getBaseline();
    const rehabTs  = parseInt(lsGet(LS.REHAB_TS, 0)) || 0;

    // ── 1. Drug counts & raw AP ───────────────────────────────────────────────
    // personalstats is a lifetime odometer; baseline is the reading at last rehab.
    // delta = drugs taken since rehab.
    const counts = {};
    let raw = 0;
    DRUGS.forEach(d => {
        const lifetime = personalstats[d.field] || 0;
        const atRehab  = baseline[d.id]         || 0;
        const delta    = Math.max(0, lifetime - atRehab);
        counts[d.id]   = delta;
        raw           += delta * d.ap;
    });

    // ── 2. Overdose AP ───────────────────────────────────────────────────────
    // OD counts are maintained by poll() via event scanning.
    // Each Xanax OD = +100 AP, each Ecstasy OD = +20 AP. Added to raw before
    // mitigation and decay, since the game treats OD AP the same as drug AP.
    const odXanax   = lsNum(LS.OD_XANAX,   0);
    const odEcstasy = lsNum(LS.OD_ECSTASY, 0);
    const odAP      = odXanax * OD_AP.xanax + odEcstasy * OD_AP.ecstasy;
    raw += odAP;

    // ── 3. Apply mitigation ───────────────────────────────────────────────────
    // Addiction XXV perk reduces raw AP by up to 50%.
    const mitigated = raw * (1 - facMit);

    // ── 4. Daily 5TCT decays ──────────────────────────────────────────────────
    // Flat −20 AP once per day at 05:00 UTC. Only counts crossings AFTER rehab.
    const decays  = countHourCrossings(DECAY_HOUR, rehabTs);
    const current = Math.max(0, mitigated - decays * DECAY_AP);

    // ── 5. Project AP at 18 TCT ───────────────────────────────────────────────
    // AP at 18 TCT = current AP. The -20 decay only fires at exactly 5 TCT.
    // countHourCrossings already only counts crossings that ACTUALLY happened
    // after the rehab, so current AP is always correct in real time.
    const apAt18 = current;

    // ── 6. Company Addiction impact ───────────────────────────────────────────
    // CA is always round(currentAP / 8) — it reflects your AP right now.
    // apAt18 is kept as a separate projection for planning purposes only.
    const caImpact = Math.round(current / AP_PER_CA);

    // ── Commit to state ───────────────────────────────────────────────────────
    S.drugCounts  = counts;
    S.odXanax     = odXanax;
    S.odEcstasy   = odEcstasy;
    S.odAP        = odAP;
    S.rawAP       = raw;
    S.mitigatedAP = mitigated;
    S.decayCount  = decays;
    S.currentAP   = current;
    S.apAt18      = apAt18;
    S.caImpact    = caImpact;
}

// ─────────────────────────────────────────────────────────────────────────────
// OVERDOSE EVENT SCANNER
// Called each poll with the fresh events object from the API.
// Scans for new OD events AFTER the last scanned timestamp to avoid
// double-counting between polls. Updates localStorage OD counts incrementally.
// ─────────────────────────────────────────────────────────────────────────────
function scanODEvents(events, rehabTs) {
    if (!events) return;

    // Scan from whichever is later: last rehab or last scan checkpoint.
    // This prevents counting ODs that happened before the current rehab cycle.
    const lastScan  = parseInt(lsGet(LS.OD_LAST_TS, 0)) || 0;
    const scanFrom  = Math.max(rehabTs, lastScan);

    let newOdXanax   = 0;
    let newOdEcstasy = 0;
    let maxEventTs   = scanFrom; // advance the checkpoint even on non-OD events

    Object.values(events).forEach(e => {
        const eTs = e.timestamp * 1000;
        if (eTs <= scanFrom) return; // already seen this event
        maxEventTs = Math.max(maxEventTs, eTs);
        if (typeof e.event !== 'string' || !/overdos/i.test(e.event)) return;
        if (/xanax/i.test(e.event))   newOdXanax++;
        else if (/ecstasy/i.test(e.event)) newOdEcstasy++;
    });

    // Persist checkpoint and new counts
    lsSet(LS.OD_LAST_TS, maxEventTs);
    if (newOdXanax)   lsSet(LS.OD_XANAX,   lsNum(LS.OD_XANAX,   0) + newOdXanax);
    if (newOdEcstasy) lsSet(LS.OD_ECSTASY, lsNum(LS.OD_ECSTASY, 0) + newOdEcstasy);

    if (newOdXanax || newOdEcstasy) {
        console.log(`[TornAddictionTracker] New ODs: Xanax×${newOdXanax} Ecstasy×${newOdEcstasy}`);
    }
}

// ─────────────────────────────────────────────────────────────────────────────
// API POLLING
// Split into two rates:
//   pollStats()  — personalstats only, every 30s. Uses Torn's 30s cache
//                  naturally (no timestamp bypass) so back-to-back calls
//                  within the cache window are free (don't count against quota).
//   pollEvents() — events only, every 5 minutes. Events are the expensive
//                  cloud selection. Only needed for rehab/OD detection which
//                  are rare events, not second-by-second.
// ─────────────────────────────────────────────────────────────────────────────
async function pollStats() {
    const key = lsGet(LS.API_KEY, '');
    if (!key) {
        S.apiError = '⚠ No API key — open Settings';
        renderAll();
        return;
    }

    try {
        // No &timestamp here — we let the 30s cache work.
        // Two polls within 30s return the same cached data for free.
        const resp = await fetch(
            `https://api.torn.com/user/?selections=personalstats&key=${key}&comment=TornAddictionTracker`
        );
        const data = await resp.json();

        if (data.error) {
            S.apiError = `API error: ${data.error.error}`;
            renderAll();
            return;
        }

        S.apiError = null;
        S.lastPoll = Date.now();

        recalculate(data.personalstats);
        renderAll();

    } catch (err) {
        S.apiError = 'Network error';
        renderAll();
    }
}

async function pollEvents() {
    const key = lsGet(LS.API_KEY, '');
    if (!key) return;

    try {
        const resp = await fetch(
            `https://api.torn.com/user/?selections=personalstats,events&key=${key}&comment=TornAddictionTracker`
        );
        const data = await resp.json();

        if (data.error) return;

        // ── Rehab detection ───────────────────────────────────────────────────
        const rehabTs     = parseInt(lsGet(LS.REHAB_TS, 0)) || 0;
        const hasBaseline = Object.keys(getBaseline()).length > 0;

        if (data.events) {
            const events     = Object.values(data.events);
            const rehabEvent = events.find(e =>
                e.timestamp * 1000 > rehabTs &&
                typeof e.event === 'string' &&
                /rehab/i.test(e.event)
            );
            if (rehabEvent || !hasBaseline) {
                saveBaseline(data.personalstats);
            }
        } else if (!hasBaseline) {
            saveBaseline(data.personalstats);
        }

        // ── OD event scanning ─────────────────────────────────────────────────
        const currentRehabTs = parseInt(lsGet(LS.REHAB_TS, 0)) || 0;
        scanODEvents(data.events, currentRehabTs);

        recalculate(data.personalstats);
        renderAll();

    } catch (err) { /* silent — pollStats will surface errors */ }
}

// Convenience: full poll (stats + events), used on focus/navigation events
async function poll() {
    await pollEvents(); // includes personalstats too
}

// ─────────────────────────────────────────────────────────────────────────────
// DOM HELPERS
// ─────────────────────────────────────────────────────────────────────────────
const $  = (id) => document.getElementById(id);
const tx = (id, v) => { const e = $(id); if (e) e.textContent = v; };

// ─────────────────────────────────────────────────────────────────────────────
// RENDER
// ─────────────────────────────────────────────────────────────────────────────
function renderAll() {
    renderButton();
    if (S.expanded && !S.inSettings) renderPanel();
}

function renderButton() {
    // Button shows: [xanax icon] [xanax count] | AP: [total current AP]
    tx('tat-btn-xanax', `${S.drugCounts?.xanax ?? 0}`);
    tx('tat-btn-ap',    `AP: ${Math.round(S.currentAP)}`);
    const errDot = $('tat-btn-err');
    if (errDot) errDot.style.display = S.apiError ? 'inline' : 'none';
}

function renderPanel() {
    // ── Drug table ────────────────────────────────────────────────────────────
    const tbody = $('tat-tbody');
    if (tbody) {
        let html = '';
        let totCount = 0, totAP = 0;
        DRUGS.forEach(d => {
            const n = S.drugCounts[d.id] || 0;
            if (n > 0) {
                const ap = n * d.ap;
                totCount += n;
                totAP    += ap;
                html += `<tr>
                    <td>${d.label}</td>
                    <td class="tat-num">${n}</td>
                    <td class="tat-num">${ap}</td>
                </tr>`;
            }

            // Add OD sub-row beneath Xanax and Ecstasy if any ODs occurred
            const odCount = d.id === 'xanax'   ? S.odXanax
                          : d.id === 'ecstasy'  ? S.odEcstasy
                          : 0;
            if (odCount > 0) {
                const odApVal = odCount * OD_AP[d.id];
                totAP += odApVal;
                html += `<tr class="tat-od-row">
                    <td class="tat-od-label">↳ OD ×${odCount}</td>
                    <td></td>
                    <td class="tat-num tat-od-ap">+${odApVal}</td>
                </tr>`;
            }
        });
        // Add decay rows — one row per 5 TCT that has fired since rehab
        if (S.decayCount > 0) {
            html += `<tr class="tat-decay-sep"><td colspan="3"></td></tr>`;
            for (let i = 0; i < S.decayCount; i++) {
                html += `<tr class="tat-decay-row">
                    <td class="tat-decay-label">3 TCT Decay</td>
                    <td></td>
                    <td class="tat-num tat-decay-ap">−${DECAY_AP}</td>
                </tr>`;
            }
            totAP = Math.max(0, totAP - S.decayCount * DECAY_AP);
        }

        tbody.innerHTML = html ||
            `<tr><td colspan="3" class="tat-none">No drugs recorded yet</td></tr>`;
        tx('tat-tot-count', totCount);
        tx('tat-tot-ap',    totAP);
    }

    // ── AP summary ────────────────────────────────────────────────────────────
    const facMit = lsNum(LS.FAC_MIT, 0);
    tx('tat-raw-ap',  Math.round(S.rawAP));
    tx('tat-mit-ap',  `${Math.round(S.mitigatedAP)} (−${facMit}%)`);
    tx('tat-cur-ap',  Math.round(S.currentAP));

    // ── Stale baseline warning ────────────────────────────────────────────────
    // If decays have fully zeroed out AP despite drugs being present,
    // the REHAB_TS is likely stale (e.g. set on first install, not after a real rehab).
    const warnEl = $('tat-stale-warn');
    if (warnEl) {
        const stale = S.rawAP > 0 && S.currentAP === 0 && S.decayCount > 0;
        warnEl.style.display = stale ? 'block' : 'none';
    }

    // ── CA ────────────────────────────────────────────────────────────────────
    tx('tat-ca-imp', `−${S.caImpact}`);

    // ── Countdown timers ──────────────────────────────────────────────────────
    tx('tat-18-val',   fmtMs(msUntil(CA_HOUR)));
    tx('tat-5-val',    fmtMs(msUntil(DECAY_HOUR)));

    // ── Status bar ────────────────────────────────────────────────────────────
    if (S.lastPoll) {
        tx('tat-poll-ago', `Updated ${Math.round((Date.now() - S.lastPoll) / 1000)}s ago`);
    } else {
        tx('tat-poll-ago', 'Polling...');
    }
    tx('tat-err-msg', S.apiError || '');
}

// ─────────────────────────────────────────────────────────────────────────────
// HTML TEMPLATE
// ─────────────────────────────────────────────────────────────────────────────
function buildHTML() {
    return `
    <div id="tat-button">
        <img src="${XANAX_IMG_URL}" class="tat-xanax-icon" alt="Xanax" title="Xanax">
        <span id="tat-btn-xanax">0</span>
        <span class="tat-sep">|</span>
        <span id="tat-btn-ap">AP: 0</span>
        <span id="tat-btn-err" class="tat-err-dot" style="display:none" title="Error — open settings">⚠</span>
    </div>

    <div id="tat-panel" style="display:none">

        <!-- ── Header ─────────────────────────────────────────── -->
        <div class="tat-header">
            <span><img src="${XANAX_IMG_URL}" class="tat-xanax-icon-sm" alt=""> Drug Tracker</span>
            <span class="tat-hdr-btns">
                <button id="tat-btn-settings" title="Settings">⚙</button>
                <button id="tat-btn-close"    title="Close">✕</button>
            </span>
        </div>

        <!-- ── Main view ──────────────────────────────────────── -->
        <div id="tat-view-main">

            <div class="tat-block">
                <div class="tat-block-title">Drugs Since Last Rehab</div>
                <table class="tat-table">
                    <thead>
                        <tr><th>Drug</th><th class="tat-num">Count</th><th class="tat-num">AP</th></tr>
                    </thead>
                    <tbody id="tat-tbody"></tbody>
                    <tfoot>
                        <tr>
                            <td><strong>Total (raw)</strong></td>
                            <td class="tat-num" id="tat-tot-count">0</td>
                            <td class="tat-num" id="tat-tot-ap">0</td>
                        </tr>
                    </tfoot>
                </table>
            </div>

            <div class="tat-block">
                <div class="tat-block-title">Addiction Points (AP)</div>
                <div class="tat-row">
                    <span>Raw AP</span>
                    <span id="tat-raw-ap">0</span>
                </div>
                <div class="tat-row">
                    <span>After Mitigation</span>
                    <span id="tat-mit-ap">0</span>
                </div>
                <div class="tat-row tat-accent">
                    <span>Current AP</span>
                    <span id="tat-cur-ap">0</span>
                </div>
                <div id="tat-stale-warn" class="tat-warn" style="display:none">
                    ⚠ Daily decays have fully zeroed your AP.<br>
                    Your baseline may be stale. Use <strong>Manual Rehab Reset</strong> in Settings if you recently rehabbed and it wasn't detected.
                </div>
            </div>

            <div class="tat-block">
                <div class="tat-block-title">Company Addiction</div>
                <div class="tat-row tat-accent">
                    <span>CA Impact</span>
                    <span id="tat-ca-imp">−0</span>
                </div>
                <div class="tat-row tat-dim">
                    <span>Time to 18 TCT</span>
                    <span id="tat-18-val">—</span>
                </div>
                <div class="tat-row tat-dim">
                    <span>Time to 3 TCT (decay)</span>
                    <span id="tat-5-val">—</span>
                </div>
            </div>

            <div class="tat-status">
                <span id="tat-poll-ago">—</span>
                <span id="tat-err-msg" class="tat-err-txt"></span>
            </div>

        </div><!-- /tat-view-main -->

        <!-- ── Settings view ──────────────────────────────────── -->
        <div id="tat-view-settings" style="display:none">

            <div class="tat-block">
                <div class="tat-block-title">Settings</div>

                <div class="tat-field">
                    <label>Torn API Key</label>
                    <input id="tat-s-key" type="password" placeholder="Paste your API key here" autocomplete="off" spellcheck="false">
                    <small>
                        Torn → Settings → API → Add new key<br>
                        ⚠ Requires <strong>Limited Access</strong> (not Minimal or Public).<br>
                        Use a <strong>dedicated key</strong> for this script — not your Full Access key.<br>
                        Selections needed: <code>user → personalstats, events</code>
                    </small>
                </div>

                <div class="tat-field">
                    <label>Faction Perk — "Addiction XXV" <span class="tat-muted">(0–50%, steps of 2%)</span></label>
                    <div class="tat-range">
                        <input id="tat-s-fac"     type="range" min="0" max="50" step="2" value="0">
                        <span  id="tat-s-fac-val">0%</span>
                    </div>
                </div>

                <div class="tat-field">
                    <label>Float Position</label>
                    <select id="tat-s-pos">
                        <option value="0">Bottom Left</option>
                        <option value="1">Top Left</option>
                        <option value="2">Bottom Right</option>
                        <option value="3">Top Right</option>
                    </select>
                </div>

                <div class="tat-field">
                    <label>Button Size</label>
                    <select id="tat-s-size">
                        <option value="small">Small</option>
                        <option value="medium" selected>Medium</option>
                        <option value="large">Large</option>
                    </select>
                </div>

                <div class="tat-field">
                    <button id="tat-s-rehab" class="tat-btn-danger">⟳  Manual Rehab Reset</button>
                    <small>Use this if you rehabbed but the script did not auto-detect it.</small>
                </div>
            </div>

            <div class="tat-settings-footer">
                <button id="tat-s-save"   class="tat-btn-save">Save</button>
                <button id="tat-s-cancel" class="tat-btn-cancel">Cancel</button>
            </div>

        </div><!-- /tat-view-settings -->

    </div><!-- /tat-panel -->`;
}

// ─────────────────────────────────────────────────────────────────────────────
// VIEW SWITCHING
// ─────────────────────────────────────────────────────────────────────────────
function showMain() {
    S.inSettings = false;
    $('tat-view-main').style.display     = 'block';
    $('tat-view-settings').style.display = 'none';
}

function showSettings() {
    S.inSettings = true;
    $('tat-view-main').style.display     = 'none';
    $('tat-view-settings').style.display = 'block';
    $('tat-s-key').value  = lsGet(LS.API_KEY, '');
    const fac = lsNum(LS.FAC_MIT, 0);
    $('tat-s-fac').value  = fac; tx('tat-s-fac-val', `${fac}%`);
    $('tat-s-pos').value  = lsGet(LS.POS,  '0');
    $('tat-s-size').value = lsGet(LS.SIZE, 'medium');
}

// ─────────────────────────────────────────────────────────────────────────────
// POSITION & SIZE
// ─────────────────────────────────────────────────────────────────────────────
function applyPosition() {
    const wrap = $('tat-wrap');
    wrap.classList.remove('pos-bl', 'pos-tl', 'pos-br', 'pos-tr');
    wrap.classList.add(['pos-bl','pos-tl','pos-br','pos-tr'][parseInt(lsGet(LS.POS, 0))] || 'pos-bl');
}

function applySize() {
    const btn = $('tat-button');
    btn.classList.remove('sz-small', 'sz-medium', 'sz-large');
    btn.classList.add(`sz-${lsGet(LS.SIZE, 'medium')}`);
}

// ─────────────────────────────────────────────────────────────────────────────
// EVENT WIRING
// ─────────────────────────────────────────────────────────────────────────────
function attachEvents() {

    $('tat-button').addEventListener('click', () => {
        S.expanded = !S.expanded;
        $('tat-panel').style.display = S.expanded ? 'flex' : 'none';
        if (S.expanded) { showMain(); renderPanel(); }
    });

    $('tat-btn-close').addEventListener('click', e => {
        e.stopPropagation();
        S.expanded = false;
        $('tat-panel').style.display = 'none';
    });

    $('tat-btn-settings').addEventListener('click', e => {
        e.stopPropagation();
        showSettings();
    });

    const slider = $('tat-s-fac');
    const label  = $('tat-s-fac-val');
    slider.addEventListener('input', () => { label.textContent = `${slider.value}%`; });

    $('tat-s-save').addEventListener('click', () => {
        lsSet(LS.API_KEY, $('tat-s-key').value.trim());
        lsSet(LS.FAC_MIT, $('tat-s-fac').value);
        lsSet(LS.POS,     $('tat-s-pos').value);
        lsSet(LS.SIZE,    $('tat-s-size').value);
        applyPosition();
        applySize();
        showMain();
        poll();
    });

    $('tat-s-cancel').addEventListener('click', () => showMain());

    $('tat-s-rehab').addEventListener('click', () => {
        if (confirm(
            'Reset the drug tracker?\n\n' +
            'This sets your CURRENT drug totals as the new baseline, ' +
            'as if you just walked out of rehab. OD counters will also reset.\n\n' +
            'Only do this if the script missed your rehab.'
        )) {
            localStorage.removeItem(LS.BASELINE);
            localStorage.removeItem(LS.REHAB_TS);
            localStorage.removeItem(LS.OD_XANAX);
            localStorage.removeItem(LS.OD_ECSTASY);
            localStorage.removeItem(LS.OD_LAST_TS);
            showMain();
            poll();
        }
    });
}

// ─────────────────────────────────────────────────────────────────────────────
// INSERT UI
// ─────────────────────────────────────────────────────────────────────────────
function insertUI() {
    if ($('tat-wrap')) return;
    const wrap = document.createElement('div');
    wrap.id = 'tat-wrap';
    wrap.innerHTML = buildHTML();
    document.body.appendChild(wrap);
    applyPosition();
    applySize();
    attachEvents();
    insertCSS();
}

// ─────────────────────────────────────────────────────────────────────────────
// CSS
// ─────────────────────────────────────────────────────────────────────────────
function insertCSS() {
    GM.addStyle(`

/* ── Wrapper ────────────────────────────────────────────── */
#tat-wrap {
    position: fixed;
    z-index: 999999;
    font-family: Arial, sans-serif;
    font-size: 13px;
}
#tat-wrap.pos-bl { bottom: 80px; left:  10px; }
#tat-wrap.pos-tl { top:    80px; left:  10px; }
#tat-wrap.pos-br { bottom: 80px; right: 10px; }
#tat-wrap.pos-tr { top:    80px; right: 10px; }

/* ── Panel ──────────────────────────────────────────────── */
#tat-panel {
    position: absolute;
    width: 295px;
    flex-direction: column;
    background: var(--default-bg-panel-color, #1c1c1c);
    border: 1px solid var(--default-panel-divider-outer-side-color, #444);
    border-radius: 8px;
    box-shadow: 0 6px 24px rgba(0,0,0,.5);
    color: var(--default-color, #e0e0e0);
    max-height: 82vh;
    overflow-y: auto;
}
#tat-wrap.pos-bl #tat-panel,
#tat-wrap.pos-br #tat-panel { bottom: calc(100% + 6px); }
#tat-wrap.pos-tl #tat-panel,
#tat-wrap.pos-tr #tat-panel { top: calc(100% + 6px); }
#tat-wrap.pos-bl #tat-panel,
#tat-wrap.pos-tl #tat-panel { left: 0; }
#tat-wrap.pos-br #tat-panel,
#tat-wrap.pos-tr #tat-panel { right: 0; }

/* ── Button ─────────────────────────────────────────────── */
#tat-button {
    display: flex;
    align-items: center;
    gap: 6px;
    border-radius: 8px;
    padding: 5px 12px;
    font-weight: 700;
    white-space: nowrap;
    cursor: pointer;
    user-select: none;
    color: var(--default-color, #eee);
    background: var(--info-msg-bg-gradient, linear-gradient(180deg,#2e2e2e,#1a1a1a));
    border: 1px solid var(--default-panel-divider-outer-side-color, #444);
    box-shadow: 0 2px 8px rgba(0,0,0,.4);
    transition: filter 0.1s;
}
#tat-button:hover { filter: brightness(1.18); }
#tat-button .tat-sep { opacity: 0.35; }
.tat-err-dot { color: #e05555; font-size: 12px; }

/* Xanax icon sizing */
.tat-xanax-icon {
    width: 22px;
    height: 22px;
    object-fit: contain;
    image-rendering: pixelated;
    flex-shrink: 0;
}
.tat-xanax-icon-sm {
    width: 14px;
    height: 14px;
    object-fit: contain;
    image-rendering: pixelated;
    vertical-align: middle;
    margin-right: 2px;
}

#tat-button.sz-small  { font-size: 11px; padding: 3px 9px; }
#tat-button.sz-small  .tat-xanax-icon { width: 16px; height: 16px; }
#tat-button.sz-medium { font-size: 13px; padding: 5px 12px; }
#tat-button.sz-large  { font-size: 16px; padding: 8px 16px; }
#tat-button.sz-large  .tat-xanax-icon { width: 28px; height: 28px; }

/* ── Panel header ───────────────────────────────────────── */
.tat-header {
    display: flex;
    justify-content: space-between;
    align-items: center;
    padding: 8px 10px;
    font-weight: 700;
    font-size: 13px;
    background: var(--default-bg-panel-active-color, #242424);
    border-bottom: 1px solid var(--default-panel-divider-inner-color, #333);
    border-radius: 8px 8px 0 0;
    flex-shrink: 0;
}
.tat-hdr-btns { display: flex; gap: 2px; }
.tat-hdr-btns button {
    background: transparent;
    border: none;
    color: var(--default-color, #ccc);
    cursor: pointer;
    padding: 2px 7px;
    border-radius: 4px;
    font-size: 15px;
    line-height: 1;
}
.tat-hdr-btns button:hover { background: rgba(255,255,255,.08); }

/* ── Content blocks ─────────────────────────────────────── */
.tat-block {
    padding: 8px 10px;
    border-bottom: 1px solid var(--default-panel-divider-inner-color, #2a2a2a);
}
.tat-block:last-child { border-bottom: none; }

.tat-block-title {
    font-size: 9px;
    letter-spacing: 1.3px;
    text-transform: uppercase;
    color: var(--default-full-text-color, #666);
    margin-bottom: 6px;
    padding-bottom: 4px;
    border-bottom: 1px solid var(--default-panel-divider-inner-color, #2a2a2a);
}

/* ── Data rows ──────────────────────────────────────────── */
.tat-row {
    display: flex;
    justify-content: space-between;
    padding: 2px 0;
    font-size: 12px;
}
.tat-row.tat-accent { font-weight: 700; color: #afc372; margin-top: 3px; }
.tat-row.tat-dim    { font-size: 11px; color: var(--default-full-text-color, #777); }

/* ── Drug table ─────────────────────────────────────────── */
.tat-table {
    width: 100%;
    border-collapse: collapse;
    font-size: 12px;
}
.tat-table th {
    text-align: left;
    font-size: 9px;
    letter-spacing: 1px;
    text-transform: uppercase;
    color: var(--default-full-text-color, #555);
    padding: 0 4px 4px;
}
.tat-table th.tat-num,
.tat-table td.tat-num { text-align: right; }
.tat-table td  { padding: 2px 4px; }
.tat-table tfoot td {
    font-weight: 700;
    border-top: 1px solid var(--default-panel-divider-inner-color, #333);
    padding-top: 4px;
}
.tat-none {
    text-align: center;
    font-style: italic;
    color: var(--default-full-text-color, #555);
    padding: 6px 0 !important;
}

/* Stale baseline warning */
.tat-warn {
    margin-top: 6px;
    padding: 6px 8px;
    border-radius: 4px;
    background: rgba(224,85,85,.12);
    border: 1px solid #e05555;
    color: #e05555;
    font-size: 10px;
    line-height: 1.5;
}

/* Decay rows in drug table */
.tat-decay-sep td {
    border-top: 1px solid var(--default-panel-divider-inner-color, #333);
    padding: 2px 0 0 !important;
}
.tat-decay-label {
    font-size: 11px;
    color: #e05555;
    font-style: italic;
    padding-left: 4px !important;
}
.tat-decay-ap {
    color: #e05555;
    font-weight: 700;
    font-size: 11px;
}

/* OD sub-rows: indented, orange accent */
.tat-od-row { }
.tat-od-label {
    font-size: 11px;
    padding-left: 12px !important;
    color: #d4863a;
    font-style: italic;
}
.tat-od-ap {
    color: #d4863a;
    font-size: 11px;
    font-weight: 700;
}

/* ── Status bar ─────────────────────────────────────────── */
.tat-status {
    display: flex;
    justify-content: space-between;
    padding: 4px 10px 6px;
    font-size: 10px;
    color: var(--default-full-text-color, #555);
    background: var(--default-bg-panel-active-color, #1a1a1a);
    border-radius: 0 0 8px 8px;
}
.tat-err-txt { color: #e05555; }

/* ── Settings ───────────────────────────────────────────── */
#tat-view-settings { padding: 0; }

.tat-field { margin-bottom: 11px; }
.tat-field label {
    display: block;
    font-size: 11px;
    font-weight: 600;
    margin-bottom: 4px;
    color: var(--default-full-text-color, #aaa);
    letter-spacing: 0.2px;
}
.tat-muted {
    font-weight: 400;
    font-size: 10px;
    color: var(--default-full-text-color, #666);
}
.tat-field small {
    display: block;
    font-size: 10px;
    color: var(--default-full-text-color, #555);
    margin-top: 3px;
}
.tat-field input[type="password"],
.tat-field select {
    width: 100%;
    box-sizing: border-box;
    background: var(--default-bg-panel-color, #111);
    border: 1px solid var(--default-panel-divider-outer-side-color, #444);
    border-radius: 4px;
    color: var(--default-color, #eee);
    padding: 5px 8px;
    font-size: 12px;
    font-family: Arial, sans-serif;
}
.tat-range {
    display: flex;
    align-items: center;
    gap: 8px;
}
.tat-range input[type="range"] {
    flex: 1;
    accent-color: #afc372;
    cursor: pointer;
}
.tat-range span {
    font-size: 12px;
    font-weight: 700;
    color: #afc372;
    min-width: 34px;
    text-align: right;
}
.tat-btn-danger {
    width: 100%;
    background: transparent;
    color: var(--user-status-red-color, #e05555);
    border: 1px solid var(--user-status-red-color, #e05555);
    border-radius: 4px;
    padding: 6px 10px;
    cursor: pointer;
    font-size: 12px;
    font-family: Arial, sans-serif;
}
.tat-btn-danger:hover { background: rgba(224,85,85,.1); }
.tat-settings-footer {
    display: flex;
    gap: 8px;
    padding: 8px 10px;
    border-top: 1px solid var(--default-panel-divider-inner-color, #333);
    background: var(--default-bg-panel-active-color, #1a1a1a);
    border-radius: 0 0 8px 8px;
}
.tat-btn-save {
    background: #afc372;
    color: #1a1a1a;
    border: none;
    border-radius: 4px;
    padding: 6px 20px;
    font-weight: 700;
    cursor: pointer;
    font-size: 12px;
    font-family: Arial, sans-serif;
}
.tat-btn-save:hover { background: #c5da88; }
.tat-btn-cancel {
    background: transparent;
    color: var(--default-color, #eee);
    border: 1px solid var(--default-panel-divider-outer-side-color, #555);
    border-radius: 4px;
    padding: 6px 14px;
    cursor: pointer;
    font-size: 12px;
    font-family: Arial, sans-serif;
}
.tat-btn-cancel:hover { background: rgba(255,255,255,.06); }

    `);
}

// ─────────────────────────────────────────────────────────────────────────────
// INIT
// ─────────────────────────────────────────────────────────────────────────────
function init() {
    if (!document.body) {
        const obs = new MutationObserver(() => {
            if (document.body) { obs.disconnect(); init(); }
        });
        obs.observe(document.documentElement, { childList: true });
        return;
    }

    insertUI();

    // Initial load: full poll (stats + events) to set baseline and OD state
    poll();

    // Frequent: personalstats only — drug counts, AP, CA
    setInterval(pollStats, POLL_MS);

    // Infrequent: events — rehab detection, OD detection
    setInterval(pollEvents, POLL_EVENTS_MS);

    // On focus/navigation: full poll so data is always fresh when returning
    document.addEventListener('visibilitychange', () => {
        if (document.visibilityState === 'visible') poll();
    });
    window.addEventListener('pageshow',   () => poll());
    window.addEventListener('hashchange', () => pollStats()); // navigation = stats only

    setInterval(() => {
        renderButton();
        if (S.expanded && !S.inSettings) {
            tx('tat-18-val',   fmtMs(msUntil(CA_HOUR)));
            tx('tat-5-val',    fmtMs(msUntil(DECAY_HOUR)));
            if (S.lastPoll) {
                tx('tat-poll-ago', `Updated ${Math.round((Date.now() - S.lastPoll) / 1000)}s ago`);
            }
        }
    }, 1000);
}

init();