Torn Addiction Tracker

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

이 스크립트를 설치하려면 Tampermonkey, Greasemonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey와 같은 확장 프로그램을 설치해야 합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Userscripts와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 유저 스크립트 관리자 확장 프로그램이 필요합니다.

(이미 유저 스크립트 관리자가 설치되어 있습니다. 설치를 진행합니다!)

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

(이미 유저 스타일 관리자가 설치되어 있습니다. 설치를 진행합니다!)

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