Overseas sets companion — Plushies · Flowers · Prehistoric · Special · Xanax · Upgrade Planner — Torn
Verze ze dne
// ==UserScript==
// @name ✈️ Sets Tracker
// @namespace https://osdevscape.com
// @version 6.1.0
// @author Phillip_J_Fry (OSMays8338) — OSDevscape
// @license All Rights Reserved © 2026 OSDevscape
// @homepageURL https://greatest.deepsurf.us/users/OSMays8338
// @description Overseas sets companion — Plushies · Flowers · Prehistoric · Special · Xanax · Upgrade Planner — Torn
// @match https://www.torn.com/*
// @grant GM_getValue
// @grant GM_setValue
// @grant GM_xmlhttpRequest
// @grant unsafeWindow
// @connect yata.yt
// @connect api.torn.com
// @run-at document-end
// ==/UserScript==
// ╔══════════════════════════════════════════════════════════════╗
// ║ ✈️ SETS TRACKER v6.1.0.0 ║
// ║ ║
// ║ Author : Phillip_J_Fry (Torn) · OSMays8338 (Greasyfork) ║
// ║ Company : OSDevscape ║
// ║ License : All Rights Reserved © 2026 OSDevscape ║
// ║ ║
// ║ Based on "Points Museum" by SuperNovae [2637223] ║
// ║ Rebuilt under OSDevscape suite architecture v5 ║
// ╚══════════════════════════════════════════════════════════════╝
(function () {
'use strict';
/* ─────────────────────────────────────────
STORAGE — dual layer: localStorage + GM
Mirrors Target Tracker / Race Tracker
───────────────────────────────────────── */
const store = {
get(k, d) {
try { const v = localStorage.getItem('lt_' + k); if (v !== null) return v; } catch(e) {}
try { return GM_getValue(k, d); } catch(e) {}
return d;
},
set(k, v) {
try { localStorage.setItem('lt_' + k, v); } catch(e) {}
try { GM_setValue(k, v); } catch(e) {}
},
getJSON(k, d) {
try { const v = localStorage.getItem('lt_' + k); if (v !== null) return JSON.parse(v); } catch(e) {}
try { return JSON.parse(GM_getValue(k, JSON.stringify(d))); } catch(e) {}
return d;
},
setJSON(k, v) {
const s = JSON.stringify(v);
try { localStorage.setItem('lt_' + k, s); } catch(e) {}
try { GM_setValue(k, s); } catch(e) {}
}
};
/* ─────────────────────────────────────────
CONFIG
───────────────────────────────────────── */
const cfg = {
get apiKey() { return store.get('lt_api', ''); },
set apiKey(v) { store.set('lt_api', v); },
get userId() { return store.get('lt_uid', ''); },
set userId(v) { store.set('lt_uid', v); },
getSectionVis() { return store.getJSON('lt_sections', { prehistoric: true, plushies: true, flowers: true, special: true, xanax: true }); },
setSectionVis(v){ store.setJSON('lt_sections', v); },
getXanCount() { return store.getJSON('lt_xan_count', 0); },
setXanCount(v) { store.setJSON('lt_xan_count', v); },
getXanCarry() { return store.getJSON('lt_xan_carry', 0); },
setXanCarry(v) { store.setJSON('lt_xan_carry', v); },
getXanPriority() { return store.getJSON('lt_xan_priority', false); },
setXanPriority(v) { store.setJSON('lt_xan_priority', v); },
getXanThreshold() { return store.getJSON('lt_xan_threshold', 50); },
setXanThreshold(v) { store.setJSON('lt_xan_threshold', v); },
};
/* ─────────────────────────────────────────
TORN THEME DETECTION
─────────────────────────────────────────
Torn signals light mode via one or more of:
• document.body classList → check TORN_LIGHT_CLASSES
• document.documentElement → same list
• data-theme / data-color-scheme attribute
⚠️ CONFIRM YOUR CLASS:
Open DevTools → Elements → inspect <html> or <body>
Switch Torn between light/dark and watch what class
appears/disappears. Then add it to TORN_LIGHT_CLASSES.
───────────────────────────────────────── */
const TORN_LIGHT_CLASSES = [
// Add / remove entries once you've confirmed via DevTools:
'day-mode', // most common Torn convention
't-theme-day',
'theme-light',
'light-mode',
'light',
'daymode',
];
function isLightMode() {
const targets = [document.documentElement, document.body];
for (const el of targets) {
if (!el) continue;
for (const cls of TORN_LIGHT_CLASSES) {
if (el.classList.contains(cls)) return true;
}
// data-theme / data-color-scheme attribute fallback
const dt = el.getAttribute('data-theme') || el.getAttribute('data-color-scheme') || '';
if (dt === 'light' || dt === 'day') return true;
}
return false;
}
/* ─────────────────────────────────────────
COLOUR PALETTES (dark = default)
───────────────────────────────────────── */
const DARK_PALETTE = {
bg: '#001214',
bg2: '#001e24',
border: 'rgba(0,180,210,0.35)',
gold: '#00c8e0',
goldDim: 'rgba(0,200,224,0.65)',
goldGlow: 'rgba(0,200,224,0.10)',
green: '#8BC34A',
greenDim: 'rgba(139,195,74,0.7)',
text: '#d0f0f8',
textDim: 'rgba(140,220,235,0.55)',
abroad: '#7fe0f0',
stockHi: '#00ff00',
stockMid: '#ffa500',
stockLo: '#ff0000',
okay: '#66dd66',
mono: '"Share Tech Mono",Consolas,monospace',
sans: 'Rajdhani,"Segoe UI",Arial,sans-serif',
// settings popup extras
settBg: '#000e12',
settBorder:'rgba(0,180,210,0.55)',
settNote: 'rgba(0,80,100,0.3)',
settHdr: 'rgba(0,80,110,0.3)',
};
const LIGHT_PALETTE = {
bg: '#f0fdff',
bg2: '#d8f6fc',
border: 'rgba(0,140,170,0.3)',
gold: '#007a8f', // darkened so it reads on white
goldDim: 'rgba(0,140,170,0.75)',
goldGlow: 'rgba(0,170,200,0.10)',
green: '#4a7c10',
greenDim: 'rgba(60,110,15,0.75)',
text: '#001a22',
textDim: 'rgba(0,80,100,0.55)',
abroad: '#007a9a',
stockHi: '#1a7a00',
stockMid: '#8a5500',
stockLo: '#bb0000',
okay: '#1a7a00',
mono: '"Share Tech Mono",Consolas,monospace',
sans: 'Rajdhani,"Segoe UI",Arial,sans-serif',
// settings popup extras
settBg: '#f0fdff',
settBorder:'rgba(0,140,170,0.45)',
settNote: 'rgba(0,170,200,0.15)',
settHdr: 'rgba(0,160,190,0.15)',
};
// C is a live proxy — always reflects current Torn theme
let C = isLightMode() ? { ...LIGHT_PALETTE } : { ...DARK_PALETTE };
function syncTheme() {
const light = isLightMode();
const src = light ? LIGHT_PALETTE : DARK_PALETTE;
Object.assign(C, src);
}
// Watch <html> and <body> for class/attribute changes (user switches theme mid-session)
(function watchTheme() {
const observer = new MutationObserver(() => {
syncTheme();
if (panelEl) renderPanel(); // re-render immediately on theme switch
});
const opts = { attributes: true, attributeFilter: ['class','data-theme','data-color-scheme'] };
if (document.documentElement) observer.observe(document.documentElement, opts);
if (document.body) observer.observe(document.body, opts);
})();
/* ─────────────────────────────────────────
POINTS & THRESHOLDS
───────────────────────────────────────── */
const PRE_PTS = 25, FLO_PTS = 10, PLU_PTS = 10, MET_PTS = 15, FOS_PTS = 20;
const PLU_THRESH = 2000, FLO_THRESH = 5000;
const XANAX_ID = 206;
const POINTS_ENDPOINT = 'https://api.torn.com/v2/market/pointsmarket';
const POINTS_CACHE_DUR = 300000;
const POINTS_HIST_SIZE = 5;
let pointsPriceCache = { time: 0, price: 0, history: [] };
/* ─────────────────────────────────────────
LOCATION MAP
───────────────────────────────────────── */
const LOCATIONS = {
'Mexico': { flag: '🇲🇽', label: 'Mexico' },
'Hawaii': { flag: '🏝️', label: 'Hawaii' },
'South Africa': { flag: '🇿🇦', label: 'South Africa' },
'Japan': { flag: '🇯🇵', label: 'Japan' },
'China': { flag: '🇨🇳', label: 'China' },
'Argentina': { flag: '🇦🇷', label: 'Argentina' },
'Switzerland': { flag: '🇨🇭', label: 'Switzerland' },
'Canada': { flag: '🇨🇦', label: 'Canada' },
'UK': { flag: '🇬🇧', label: 'United Kingdom' },
'UAE': { flag: '🇦🇪', label: 'UAE' },
'Cayman Islands': { flag: '🇰🇾', label: 'Cayman Islands' },
'BoB': { flag: '🏪', label: "Bits n' Bobs" },
};
/* ─────────────────────────────────────────
ITEM GROUPS
───────────────────────────────────────── */
const GROUPS = {
Prehistoric: {
pts: PRE_PTS, icon: '🪨',
items: {
'Quartz Point': { id: 619, s: 'Quartz', loc: 'Canada' },
'Chalcedony Point': { id: 620, s: 'Chalced', loc: 'Argentina' },
'Basalt Point': { id: 621, s: 'Basalt', loc: 'Hawaii' },
'Quartzite Point': { id: 622, s: 'Quartzit', loc: 'South Africa' },
'Chert Point': { id: 623, s: 'Chert', loc: 'UK' },
'Obsidian Point': { id: 624, s: 'Obsidian', loc: 'Mexico' },
}
},
Plushies: {
pts: PLU_PTS, icon: '🧸',
items: {
'Sheep Plushie': { id: 186, s: 'Sheep', loc: 'BoB' },
'Teddy Bear Plushie': { id: 187, s: 'Teddy', loc: 'BoB' },
'Kitten Plushie': { id: 215, s: 'Kitten', loc: 'BoB' },
'Jaguar Plushie': { id: 258, s: 'Jaguar', loc: 'Mexico' },
'Wolverine Plushie': { id: 261, s: 'Wolverine', loc: 'Canada' },
'Nessie Plushie': { id: 266, s: 'Nessie', loc: 'UK' },
'Red Fox Plushie': { id: 268, s: 'Fox', loc: 'UK' },
'Monkey Plushie': { id: 269, s: 'Monkey', loc: 'Argentina' },
'Chamois Plushie': { id: 273, s: 'Chamois', loc: 'Switzerland' },
'Panda Plushie': { id: 274, s: 'Panda', loc: 'China' },
'Lion Plushie': { id: 281, s: 'Lion', loc: 'South Africa' },
'Camel Plushie': { id: 384, s: 'Camel', loc: 'UAE' },
'Stingray Plushie': { id: 618, s: 'Stingray', loc: 'Cayman Islands'},
}
},
Flowers: {
pts: FLO_PTS, icon: '🌸',
items: {
'Dahlia': { id: 260, s: 'Dahlia', loc: 'Mexico' },
'Orchid': { id: 264, s: 'Orchid', loc: 'Hawaii' },
'African Violet': { id: 282, s: 'Violet', loc: 'South Africa' },
'Cherry Blossom': { id: 277, s: 'Blossoms', loc: 'Japan' },
'Peony': { id: 276, s: 'Peony', loc: 'China' },
'Ceibo Flower': { id: 271, s: 'Ceibo', loc: 'Argentina' },
'Edelweiss': { id: 272, s: 'Edelweiss', loc: 'Switzerland' },
'Crocus': { id: 263, s: 'Crocus', loc: 'Canada' },
'Heather': { id: 267, s: 'Heather', loc: 'UK' },
'Tribulus Omanense': { id: 385, s: 'Tribulus', loc: 'UAE' },
'Banana Orchid': { id: 617, s: 'B.Orchid', loc: 'Cayman Islands'},
}
},
};
const SPECIAL_ITEMS = {
'Meteorite Fragment': { id: 512, s: 'Meteor', loc: 'Argentina', pts: MET_PTS },
'Patagonian Fossil': { id: 513, s: 'Fossil', loc: 'Argentina', pts: FOS_PTS },
};
const BOB_IDS = new Set([186, 187, 215]);
const itemImg = id => `https://www.torn.com/images/items/${id}/large.png`;
/* ─────────────────────────────────────────
BUILD ID→NAME LOOKUP for YATA parsing
───────────────────────────────────────── */
function buildIdMap() {
const m = {};
Object.values(GROUPS).forEach(g => Object.entries(g.items).forEach(([name, d]) => { m[d.id] = name; }));
Object.entries(SPECIAL_ITEMS).forEach(([name, d]) => { m[d.id] = name; });
m[XANAX_ID] = 'Xanax';
return m;
}
const ID_TO_NAME = buildIdMap();
/* ─────────────────────────────────────────
STATE
───────────────────────────────────────── */
let toggleEl = null;
let panelEl = null;
let panelOpen = false;
let activeTab = 'sets'; // sets | xanax | travel
let isLoading = false;
let pollTimer = null;
// cached data
let invCache = {}; // display items { name: qty }
let abroadCache = {}; // YATA abroad stock { name: qty }
let xanSACache = { qty: 0, price: 0 };
let xanFacCache = null;
let pointsPrice = 0;
/* ─────────────────────────────────────────
STYLESHEET — only keyframes & scrollbar
All layout uses inline styles (CSP-safe)
───────────────────────────────────────── */
function injectCSS() {
if (document.getElementById('lt-css')) return;
const s = document.createElement('style');
s.id = 'lt-css';
s.textContent = `
#lt-toggle { position:fixed; z-index:999990; user-select:none; touch-action:none; cursor:grab; }
#lt-toggle:active { cursor:grabbing; }
#lt-panel { position:fixed; z-index:999989; overflow:hidden; display:flex; flex-direction:column; }
#lt-panel .lt-body { overflow-y:auto; overflow-x:hidden; flex:1; scrollbar-width:thin; scrollbar-color:rgba(0,180,210,0.4) transparent; touch-action:pan-y; -webkit-overflow-scrolling:touch; }
#lt-panel .lt-body::-webkit-scrollbar { width:3px; }
#lt-panel .lt-body::-webkit-scrollbar-thumb { background:rgba(0,180,210,0.5); border-radius:2px; }
#lt-panel a { text-decoration:none !important; color:inherit; }
@keyframes lt-spin { to{transform:rotate(360deg)} }
@keyframes lt-pulse { 0%,100%{box-shadow:0 0 0 0 rgba(0,200,224,0.5)} 50%{box-shadow:0 0 0 5px rgba(0,200,224,0)} }
@keyframes lt-hi-pulse { 0%,100%{box-shadow:0 0 0 0 rgba(0,255,0,0.4)} 50%{box-shadow:0 0 0 3px rgba(0,255,0,0)} }
@keyframes lt-wa-pulse { 0%,100%{box-shadow:0 0 0 0 rgba(255,165,0,0.4)} 50%{box-shadow:0 0 0 3px rgba(255,165,0,0)} }
@keyframes lt-da-pulse { 0%,100%{box-shadow:0 0 0 0 rgba(255,0,0,0.4)} 50%{box-shadow:0 0 0 3px rgba(255,0,0,0)} }
@keyframes lt-float { 0%,100%{transform:translateY(0)} 50%{transform:translateY(-2px)} }
.lt-spin { animation:lt-spin 0.8s linear infinite; display:inline-block; }
.lt-float { animation:lt-float 3s ease-in-out infinite; }
.lt-hi { animation:lt-hi-pulse 2s ease-in-out infinite; }
.lt-wa { animation:lt-wa-pulse 2s ease-in-out infinite; }
.lt-da { animation:lt-da-pulse 2s ease-in-out infinite; }
`;
(document.head || document.documentElement).appendChild(s);
}
/* ─────────────────────────────────────────
TOAST
───────────────────────────────────────── */
function toast(msg, dur) {
dur = dur || 3000;
const old = document.getElementById('lt-toast'); if (old) old.remove();
const el = document.createElement('div');
el.id = 'lt-toast'; el.textContent = msg;
el.setAttribute('style',
'position:fixed !important;top:18px !important;left:50% !important;' +
'transform:translateX(-50%) !important;background:#00121a !important;' +
'border:1px solid rgba(0,200,224,0.7) !important;border-radius:8px !important;' +
'padding:10px 20px !important;color:#00c8e0 !important;font-size:13px !important;' +
'font-weight:700 !important;font-family:Arial,sans-serif !important;' +
'z-index:2147483647 !important;max-width:360px !important;text-align:center !important;' +
'box-shadow:0 4px 20px rgba(0,0,0,0.8) !important;pointer-events:none !important;'
);
document.body.appendChild(el);
setTimeout(() => {
el.style.setProperty('opacity','0','important');
el.style.setProperty('transition','opacity 0.3s','important');
setTimeout(() => el.remove(), 320);
}, dur);
}
/* ─────────────────────────────────────────
SETTINGS POPUP — fully inline, CSP-safe
───────────────────────────────────────── */
function openSettings() {
const existing = document.getElementById('lt-settings-wrap');
if (existing) { existing.remove(); return; }
function el(tag, css) { const e = document.createElement(tag); if (css) e.setAttribute('style', css); return e; }
function imp(css) { return css.split(';').filter(Boolean).map(r => r.trim() + ' !important').join(';') + ';'; }
const S = {
wrap: 'position:fixed;top:0;left:0;width:100%;height:100%;background:rgba(0,0,0,0.85);z-index:2147483647;display:flex;align-items:flex-start;justify-content:center;padding-top:5vh;box-sizing:border-box;overflow-y:auto;',
box: `background:${C.settBg};border:2px solid ${C.settBorder};border-radius:12px;width:370px;max-width:94vw;color:${C.text};font-family:Arial,sans-serif;overflow:hidden;box-shadow:0 16px 60px rgba(0,0,0,0.85);margin-bottom:20px;`,
hdr: `background:${C.settHdr};padding:13px 18px;font-size:12px;font-weight:700;letter-spacing:1.4px;color:${C.gold};border-bottom:1px solid ${C.border};`,
body: 'padding:16px;',
note: `background:${C.settNote};border-left:3px solid ${C.border};padding:9px 12px;border-radius:4px;font-size:11px;color:${C.goldDim};margin-bottom:16px;line-height:1.5;`,
secHdr: `font-size:9px;font-weight:700;letter-spacing:1.2px;text-transform:uppercase;color:${C.goldDim};padding-bottom:5px;margin:16px 0 8px;border-bottom:1px solid ${C.border};font-family:Consolas,monospace;`,
lbl: `display:block;margin-bottom:5px;font-size:10px;font-weight:700;letter-spacing:1px;text-transform:uppercase;color:${C.green};`,
inp: `display:block;width:100%;box-sizing:border-box;padding:9px 11px;background:${C.bg};border:1px solid ${C.border};border-radius:5px;color:${C.text};font-size:13px;font-family:Consolas,monospace;outline:none;`,
hint: `font-size:9px;color:${C.textDim};margin-top:4px;font-family:Consolas,monospace;`,
btnRow: 'display:flex;gap:8px;margin-top:18px;',
btn: `flex:1;padding:10px 0;border-radius:7px;font-size:12px;font-weight:700;cursor:pointer;border:1px solid ${C.border};font-family:Arial,sans-serif;letter-spacing:0.5px;`,
};
const wrap = el('div'); wrap.id = 'lt-settings-wrap'; wrap.setAttribute('style', imp(S.wrap));
const box = el('div'); box.setAttribute('style', imp(S.box));
const hdr = el('div'); hdr.textContent = '⚙ LOOT TRACKER — Settings'; hdr.setAttribute('style', imp(S.hdr));
const body = el('div'); body.setAttribute('style', imp(S.body));
const note = el('div');
note.innerHTML = '🔒 Requires a <b>Limited Access</b> API key (Display permission only). Your API key and all item data stay on your device — nothing is transmitted externally.';
note.setAttribute('style', imp(S.note));
function field(labelText, id, placeholder, value, hint, isSecret) {
const g = el('div'); g.setAttribute('style', imp('margin-bottom:12px;'));
const lbl = el('label'); lbl.textContent = labelText; lbl.setAttribute('style', imp(S.lbl));
const inp = document.createElement('input');
inp.setAttribute('type', isSecret ? 'password' : 'text');
inp.id = id; inp.placeholder = placeholder; inp.value = value || '';
inp.setAttribute('style', imp(S.inp));
inp.addEventListener('focus', () => inp.style.setProperty('border-color','rgba(0,190,215,0.8)','important'));
inp.addEventListener('blur', () => inp.style.setProperty('border-color','rgba(0,140,170,0.4)','important'));
if (isSecret) {
const row = el('div'); row.setAttribute('style', imp('display:flex;align-items:center;gap:6px;'));
const tog = el('div'); tog.textContent = '👁'; tog.setAttribute('style', imp('cursor:pointer;font-size:14px;flex-shrink:0;opacity:0.45;user-select:none;padding:2px;'));
tog.addEventListener('click', () => {
const shown = inp.getAttribute('type') === 'text';
inp.setAttribute('type', shown ? 'password' : 'text');
tog.style.setProperty('opacity', shown ? '0.45' : '1', 'important');
});
row.appendChild(inp); row.appendChild(tog);
const h = el('div'); h.textContent = hint; h.setAttribute('style', imp(S.hint));
g.appendChild(lbl); g.appendChild(row); g.appendChild(h);
} else {
const h = el('div'); h.textContent = hint; h.setAttribute('style', imp(S.hint));
g.appendChild(lbl); g.appendChild(inp); g.appendChild(h);
}
return g;
}
// ── Credentials ──
const credHdr = el('div'); credHdr.textContent = '🔑 Credentials'; credHdr.setAttribute('style', imp(S.secHdr));
const fAPI = field('API Key (Limited — Display only)', 'lt-si-api', 'Your 16-char Torn API key', cfg.apiKey, 'Torn → Settings → API → Limited (Display access)', true);
const fUID = field('Your User ID', 'lt-si-uid', 'Your Torn player ID', cfg.userId, 'Used to fetch your display items from the API');
const apiLink = el('div');
apiLink.setAttribute('style', imp('margin-top:-8px;margin-bottom:12px;font-size:10px;color:rgba(0,170,200,0.75);'));
apiLink.innerHTML = '🔑 No key? <a href="https://www.torn.com/preferences.php#tab=api?step=addNewKey&title=LootTracker&access_level=1" style="color:#00c8e0;font-weight:700;text-decoration:underline !important;">Create a Limited (Display) key on Torn</a>';
// ── Section Visibility ──
const secHdr = el('div'); secHdr.textContent = '👁 Section Visibility'; secHdr.setAttribute('style', imp(S.secHdr));
const vis = cfg.getSectionVis();
const sections = [
{ key: 'prehistoric', label: '🪨 Prehistoric Points' },
{ key: 'plushies', label: '🧸 Plushies' },
{ key: 'flowers', label: '🌸 Flowers' },
{ key: 'special', label: '☄️ Special Items' },
{ key: 'xanax', label: '🧪 Xanax' },
];
const visGrid = el('div'); visGrid.setAttribute('style', imp('display:flex;flex-direction:column;gap:4px;'));
sections.forEach(sec => {
const row = el('div'); row.setAttribute('style', imp('display:flex;align-items:center;gap:8px;padding:5px 8px;border-radius:5px;background:rgba(0,200,224,0.04);border:1px solid rgba(0,140,170,0.18);cursor:pointer;'));
const chk = document.createElement('input'); chk.type = 'checkbox'; chk.id = 'lt-vis-' + sec.key; chk.checked = vis[sec.key] !== false;
chk.setAttribute('style', imp('width:14px;height:14px;cursor:pointer;accent-color:#00c8e0;flex-shrink:0;'));
const lbl = el('label'); lbl.textContent = sec.label; lbl.setAttribute('for', 'lt-vis-' + sec.key);
lbl.setAttribute('style', imp('font-size:10px;font-weight:600;color:#e8f0d0;cursor:pointer;flex:1;'));
row.appendChild(chk); row.appendChild(lbl);
row.addEventListener('click', e => { if (e.target !== chk) chk.checked = !chk.checked; });
visGrid.appendChild(row);
});
// ── Preferences ──
const prefHdr = el('div'); prefHdr.textContent = '💡 Preferences'; prefHdr.setAttribute('style', imp(S.secHdr));
const tooltipRow = el('div');
tooltipRow.setAttribute('style', imp('display:flex;align-items:center;gap:8px;padding:5px 8px;border-radius:5px;background:rgba(0,200,224,0.04);border:1px solid rgba(0,140,170,0.18);cursor:pointer;'));
const tooltipChk = document.createElement('input'); tooltipChk.type = 'checkbox'; tooltipChk.id = 'lt-pref-tooltip';
let tooltipAlreadySeen = false;
try { tooltipAlreadySeen = !!localStorage.getItem('lt_tooltip_seen'); } catch(e) {}
tooltipChk.checked = !tooltipAlreadySeen;
tooltipChk.setAttribute('style', imp('width:14px;height:14px;cursor:pointer;accent-color:#00c8e0;flex-shrink:0;'));
const tooltipLbl = el('label'); tooltipLbl.textContent = '✈️ Show welcome tooltip on next load'; tooltipLbl.setAttribute('for', 'lt-pref-tooltip');
tooltipLbl.setAttribute('style', imp('font-size:10px;font-weight:600;color:#e8f0d0;cursor:pointer;flex:1;'));
tooltipRow.appendChild(tooltipChk); tooltipRow.appendChild(tooltipLbl);
tooltipRow.addEventListener('click', e => { if (e.target !== tooltipChk) tooltipChk.checked = !tooltipChk.checked; });
// ── 🧪 Xanax ──
const xanSettHdr = el('div'); xanSettHdr.textContent = '🧪 Xanax'; xanSettHdr.setAttribute('style', imp(S.secHdr));
// Adjust Count row
const xanCountGroup = el('div'); xanCountGroup.setAttribute('style', imp('margin-bottom:10px;'));
const xanCountLbl = el('label'); xanCountLbl.textContent = 'Personal Count'; xanCountLbl.setAttribute('for','lt-si-xan-count'); xanCountLbl.setAttribute('style', imp(S.lbl));
const xanCountRow = el('div'); xanCountRow.setAttribute('style', imp('display:flex;align-items:center;gap:6px;'));
const xanDecBtn = el('button'); xanDecBtn.textContent = '−'; xanDecBtn.setAttribute('type','button');
xanDecBtn.setAttribute('style', imp('padding:6px 12px;border-radius:5px;border:1px solid rgba(0,200,224,0.35);background:rgba(0,40,60,0.5);color:#00c8e0;font-weight:800;font-size:15px;cursor:pointer;line-height:1;'));
const xanCountInp = document.createElement('input'); xanCountInp.type = 'number'; xanCountInp.min = '0';
xanCountInp.id = 'lt-si-xan-count'; xanCountInp.value = cfg.getXanCount();
xanCountInp.setAttribute('style', imp('font-family:Consolas,monospace;font-size:14px;font-weight:700;text-align:center;width:72px;padding:6px 4px;background:#000e14;border:1px solid rgba(0,140,170,0.4);border-radius:5px;color:#e8f0d0;outline:none;-moz-appearance:textfield;'));
xanCountInp.addEventListener('focus', () => xanCountInp.style.setProperty('border-color','rgba(0,190,215,0.8)','important'));
xanCountInp.addEventListener('blur', () => xanCountInp.style.setProperty('border-color','rgba(0,140,170,0.4)','important'));
const xanIncBtn = el('button'); xanIncBtn.textContent = '+'; xanIncBtn.setAttribute('type','button');
xanIncBtn.setAttribute('style', imp('padding:6px 12px;border-radius:5px;border:1px solid rgba(0,200,224,0.35);background:rgba(0,40,60,0.5);color:#00c8e0;font-weight:800;font-size:15px;cursor:pointer;line-height:1;'));
xanDecBtn.addEventListener('click', () => { xanCountInp.value = Math.max(0,(parseInt(xanCountInp.value)||0)-1); });
xanIncBtn.addEventListener('click', () => { xanCountInp.value = (parseInt(xanCountInp.value)||0)+1; });
xanCountRow.appendChild(xanDecBtn); xanCountRow.appendChild(xanCountInp); xanCountRow.appendChild(xanIncBtn);
const xanCountHint = el('div'); xanCountHint.textContent = 'Your current personal Xanax stockpile'; xanCountHint.setAttribute('style', imp(S.hint));
xanCountGroup.appendChild(xanCountLbl); xanCountGroup.appendChild(xanCountRow); xanCountGroup.appendChild(xanCountHint);
// Carry Limit row
const xanCarryGroup = el('div'); xanCarryGroup.setAttribute('style', imp('margin-bottom:10px;'));
const xanCarryLbl = el('label'); xanCarryLbl.textContent = 'Carry Limit'; xanCarryLbl.setAttribute('for','lt-si-xan-carry'); xanCarryLbl.setAttribute('style', imp(S.lbl));
const xanCarryRow = el('div'); xanCarryRow.setAttribute('style', imp('display:flex;align-items:center;gap:6px;'));
const xanCarDecBtn = el('button'); xanCarDecBtn.textContent = '−'; xanCarDecBtn.setAttribute('type','button');
xanCarDecBtn.setAttribute('style', imp('padding:6px 12px;border-radius:5px;border:1px solid rgba(0,200,224,0.35);background:rgba(0,40,60,0.5);color:#00c8e0;font-weight:800;font-size:15px;cursor:pointer;line-height:1;'));
const xanCarryInp = document.createElement('input'); xanCarryInp.type = 'number'; xanCarryInp.min = '0';
xanCarryInp.id = 'lt-si-xan-carry'; xanCarryInp.value = cfg.getXanCarry();
xanCarryInp.setAttribute('style', imp('font-family:Consolas,monospace;font-size:14px;font-weight:700;text-align:center;width:72px;padding:6px 4px;background:#000e14;border:1px solid rgba(0,140,170,0.4);border-radius:5px;color:#e8f0d0;outline:none;-moz-appearance:textfield;'));
xanCarryInp.addEventListener('focus', () => xanCarryInp.style.setProperty('border-color','rgba(0,190,215,0.8)','important'));
xanCarryInp.addEventListener('blur', () => xanCarryInp.style.setProperty('border-color','rgba(0,140,170,0.4)','important'));
const xanCarIncBtn = el('button'); xanCarIncBtn.textContent = '+'; xanCarIncBtn.setAttribute('type','button');
xanCarIncBtn.setAttribute('style', imp('padding:6px 12px;border-radius:5px;border:1px solid rgba(0,200,224,0.35);background:rgba(0,40,60,0.5);color:#00c8e0;font-weight:800;font-size:15px;cursor:pointer;line-height:1;'));
xanCarDecBtn.addEventListener('click', () => { xanCarryInp.value = Math.max(0,(parseInt(xanCarryInp.value)||0)-1); });
xanCarIncBtn.addEventListener('click', () => { xanCarryInp.value = (parseInt(xanCarryInp.value)||0)+1; });
const addCarrySettBtn = el('button'); addCarrySettBtn.textContent = '+ Add Carry to Count'; addCarrySettBtn.setAttribute('type','button');
addCarrySettBtn.setAttribute('style', imp('padding:6px 10px;border-radius:5px;background:rgba(30,50,10,0.5);border:1px solid rgba(139,195,74,0.4);color:#8BC34A;font-weight:700;font-size:10px;cursor:pointer;font-family:Arial,sans-serif;white-space:nowrap;'));
addCarrySettBtn.addEventListener('click', () => {
const carry = parseInt(xanCarryInp.value)||0;
xanCountInp.value = (parseInt(xanCountInp.value)||0) + carry;
xanCountInp.style.setProperty('border-color','#8BC34A','important');
setTimeout(() => xanCountInp.style.setProperty('border-color','rgba(0,140,170,0.4)','important'), 600);
});
xanCarryRow.appendChild(xanCarDecBtn); xanCarryRow.appendChild(xanCarryInp); xanCarryRow.appendChild(xanCarIncBtn); xanCarryRow.appendChild(addCarrySettBtn);
const xanCarryHint = el('div'); xanCarryHint.textContent = 'Max Xanax you can carry per trip'; xanCarryHint.setAttribute('style', imp(S.hint));
xanCarryGroup.appendChild(xanCarryLbl); xanCarryGroup.appendChild(xanCarryRow); xanCarryGroup.appendChild(xanCarryHint);
// Xanax Priority section
const xanPriRowWrap = el('div'); xanPriRowWrap.setAttribute('style', imp('margin-bottom:8px;'));
const xanPriRow = el('div'); xanPriRow.setAttribute('style', imp('display:flex;align-items:center;gap:8px;padding:5px 8px;border-radius:5px;background:rgba(0,200,224,0.04);border:1px solid rgba(0,140,170,0.18);cursor:pointer;'));
const xanPriChk = document.createElement('input'); xanPriChk.type = 'checkbox'; xanPriChk.id = 'lt-si-xan-priority'; xanPriChk.checked = cfg.getXanPriority();
xanPriChk.setAttribute('style', imp('width:14px;height:14px;cursor:pointer;accent-color:#00c8e0;flex-shrink:0;'));
const xanPriLbl = el('label'); xanPriLbl.textContent = '🚨 Prioritise Xanax run in Travel planner'; xanPriLbl.setAttribute('for','lt-si-xan-priority');
xanPriLbl.setAttribute('style', imp('font-size:10px;font-weight:600;color:#e8f0d0;cursor:pointer;flex:1;'));
xanPriRow.appendChild(xanPriChk); xanPriRow.appendChild(xanPriLbl);
xanPriRow.addEventListener('click', e => { if (e.target !== xanPriChk) xanPriChk.checked = !xanPriChk.checked; updateThresholdVis(); });
xanPriChk.addEventListener('change', updateThresholdVis);
xanPriRowWrap.appendChild(xanPriRow);
// Threshold field — only visible when priority is on
const xanThreshGroup = el('div'); xanThreshGroup.id = 'lt-xan-thresh-group';
xanThreshGroup.setAttribute('style', imp('margin-top:6px;padding:8px 10px;background:rgba(0,200,224,0.03);border:1px solid rgba(0,140,170,0.2);border-radius:5px;' + (cfg.getXanPriority() ? '' : 'display:none;')));
const xanThreshLbl = el('label'); xanThreshLbl.textContent = 'Stop prioritising above'; xanThreshLbl.setAttribute('for','lt-si-xan-thresh'); xanThreshLbl.setAttribute('style', imp(S.lbl));
const xanThreshRow = el('div'); xanThreshRow.setAttribute('style', imp('display:flex;align-items:center;gap:6px;'));
const xanThreshInp = document.createElement('input'); xanThreshInp.type = 'number'; xanThreshInp.min = '0';
xanThreshInp.id = 'lt-si-xan-thresh'; xanThreshInp.value = cfg.getXanThreshold();
xanThreshInp.setAttribute('style', imp('font-family:Consolas,monospace;font-size:14px;font-weight:700;text-align:center;width:72px;padding:6px 4px;background:#000e14;border:1px solid rgba(0,140,170,0.4);border-radius:5px;color:#e8f0d0;outline:none;-moz-appearance:textfield;'));
xanThreshInp.addEventListener('focus', () => xanThreshInp.style.setProperty('border-color','rgba(0,190,215,0.8)','important'));
xanThreshInp.addEventListener('blur', () => xanThreshInp.style.setProperty('border-color','rgba(0,140,170,0.4)','important'));
const xanThreshUnit = el('div'); xanThreshUnit.textContent = 'Xanax'; xanThreshUnit.setAttribute('style', imp('font-size:10px;color:rgba(200,220,160,0.55);font-family:Consolas,monospace;'));
xanThreshRow.appendChild(xanThreshInp); xanThreshRow.appendChild(xanThreshUnit);
const xanThreshHint = el('div'); xanThreshHint.textContent = 'South Africa stays top-ranked until personal count exceeds this'; xanThreshHint.setAttribute('style', imp(S.hint));
xanThreshGroup.appendChild(xanThreshLbl); xanThreshGroup.appendChild(xanThreshRow); xanThreshGroup.appendChild(xanThreshHint);
function updateThresholdVis() {
const on = document.getElementById('lt-si-xan-priority') && document.getElementById('lt-si-xan-priority').checked;
xanThreshGroup.style.setProperty('display', on ? 'block' : 'none', 'important');
}
// ── Buttons ──
const btnRow = el('div'); btnRow.setAttribute('style', imp(S.btnRow));
const btnCancel = el('button'); btnCancel.textContent = 'Cancel'; btnCancel.setAttribute('type','button');
const btnSave = el('button'); btnSave.textContent = 'Save & Refresh'; btnSave.setAttribute('type','button');
btnCancel.setAttribute('style', imp(S.btn + `background:${C.bg};color:${C.green};`));
btnSave.setAttribute('style', imp(S.btn + `background:${C.settNote};color:${C.gold};`));
btnRow.appendChild(btnCancel); btnRow.appendChild(btnSave);
body.appendChild(note);
body.appendChild(credHdr);
body.appendChild(fAPI);
body.appendChild(apiLink);
body.appendChild(fUID);
body.appendChild(secHdr);
body.appendChild(visGrid);
body.appendChild(prefHdr);
body.appendChild(tooltipRow);
body.appendChild(xanSettHdr);
body.appendChild(xanCountGroup);
body.appendChild(xanCarryGroup);
body.appendChild(xanPriRowWrap);
body.appendChild(xanThreshGroup);
body.appendChild(btnRow);
box.appendChild(hdr); box.appendChild(body);
wrap.appendChild(box);
document.body.appendChild(wrap);
setTimeout(() => { const i = document.getElementById('lt-si-api'); if (i) i.focus(); }, 50);
function doSave() {
const key = (document.getElementById('lt-si-api').value || '').trim();
const uid = (document.getElementById('lt-si-uid').value || '').trim();
if (!key) { toast('⚠ API key is required'); return; }
cfg.apiKey = key; cfg.userId = uid;
const newVis = {};
sections.forEach(sec => { newVis[sec.key] = document.getElementById('lt-vis-' + sec.key).checked; });
cfg.setSectionVis(newVis);
// Xanax count, carry, priority, threshold
cfg.setXanCount(Math.max(0, parseInt(document.getElementById('lt-si-xan-count').value)||0));
cfg.setXanCarry(Math.max(0, parseInt(document.getElementById('lt-si-xan-carry').value)||0));
cfg.setXanPriority(document.getElementById('lt-si-xan-priority').checked);
cfg.setXanThreshold(Math.max(0, parseInt(document.getElementById('lt-si-xan-thresh').value)||0));
// Retrigger tooltip: clear seen key so carousel shows again on next load
const showTooltip = document.getElementById('lt-pref-tooltip').checked;
try {
if (showTooltip) localStorage.removeItem('lt_tooltip_seen');
else localStorage.setItem('lt_tooltip_seen', '1');
} catch(e) {}
wrap.remove();
toast('✓ Saved!');
invCache = {}; abroadCache = {}; xanSACache = { qty:0, price:0 }; xanFacCache = null;
if (panelEl) renderPanel();
refreshAll();
}
btnSave.addEventListener('click', doSave);
btnCancel.addEventListener('click', () => wrap.remove());
wrap.addEventListener('click', e => { if (e.target === wrap) wrap.remove(); });
wrap.addEventListener('keydown', e => { if (e.key === 'Escape') wrap.remove(); });
}
/* ─────────────────────────────────────────
API / FETCH
───────────────────────────────────────── */
function gmFetch(url, timeoutMs) {
timeoutMs = timeoutMs || 12000;
const req = new Promise(resolve => {
if (typeof GM_xmlhttpRequest !== 'undefined') {
GM_xmlhttpRequest({
method: 'GET', url, timeout: timeoutMs,
onload: r => { try { resolve(JSON.parse(r.responseText)); } catch { resolve({}); } },
onerror: () => resolve({}),
ontimeout: () => resolve({}),
});
} else {
fetch(url).then(r => r.json()).then(resolve).catch(() => resolve({}));
}
});
return Promise.race([req, new Promise(r => setTimeout(() => r({}), timeoutMs))]);
}
async function fetchInventory() {
if (!cfg.apiKey || !cfg.userId) throw new Error('No API key or User ID');
const uid = parseInt(String(cfg.userId).replace(/\D/g,''));
const d = await gmFetch(`https://api.torn.com/user/${uid}?selections=display&key=${cfg.apiKey}`);
if (d.error) throw new Error(d.error.error || 'API error');
const items = {};
(d.display || []).forEach(item => { items[item.name] = (items[item.name] || 0) + item.quantity; });
return items;
}
async function fetchAbroad() {
const data = await gmFetch('https://yata.yt/api/v1/travel/export/');
const map = {};
if (!data) return map;
let entries = [];
if (Array.isArray(data)) entries = data;
else if (data.stocks && typeof data.stocks === 'object') entries = Object.values(data.stocks);
else entries = Object.values(data).filter(v => v && typeof v === 'object' && v.items);
entries.forEach(country => {
const items = Array.isArray(country?.items) ? country.items
: Array.isArray(country?.stocks) ? country.stocks
: [];
items.forEach(item => {
const name = ID_TO_NAME[Number(item.id)];
if (name) map[name] = (map[name] || 0) + Number(item.quantity || 0);
});
});
return map;
}
async function fetchXanaxSA() {
try {
const data = await gmFetch('https://yata.yt/api/v1/travel/export/');
const SA_KEYS = ['sou','saf','zaf','za','south_africa','South Africa'];
const entries = Array.isArray(data) ? data
: Array.isArray(data?.exports) ? data.exports
: Object.entries(data?.stocks || {}).map(([k,v]) => ({ ...v, _key: k }));
for (const country of entries) {
const key = country._key || country.country || '';
if (!SA_KEYS.some(k => key.toLowerCase().includes(k.toLowerCase()))) continue;
const items = country?.items || country?.stocks || [];
const hit = items.find(i => Number(i.id) === XANAX_ID);
if (hit) return { qty: hit.quantity || 0, price: hit.cost || hit.price || 0 };
}
} catch(e) {}
return { qty: 0, price: 0 };
}
async function fetchXanaxFaction() {
if (!cfg.apiKey) return null;
try {
const d = await gmFetch(`https://api.torn.com/v2/faction/items?key=${cfg.apiKey}`);
if (!d.error) {
const items = d.items || {};
const direct = items[String(XANAX_ID)];
if (direct !== undefined) return direct.quantity || 0;
let total = 0;
Object.values(items).forEach(i => { if (i.name === 'Xanax' || Number(i.id) === XANAX_ID) total += (i.quantity || 0); });
return total;
}
} catch(e) {}
return null;
}
async function fetchPointsPrice() {
const now = Date.now();
if (pointsPriceCache.time && now - pointsPriceCache.time < POINTS_CACHE_DUR) return pointsPriceCache.price;
if (!cfg.apiKey) return 0;
try {
const data = await gmFetch(`${POINTS_ENDPOINT}?key=${cfg.apiKey}`);
if (data.pointsmarket) {
const listings = Object.values(data.pointsmarket).filter(l => l.quantity > 0).map(l => l.cost).sort((a,b) => a-b);
if (listings.length) {
const top = listings.slice(0, Math.min(5, listings.length));
const avg = Math.round(top.reduce((s,p) => s+p, 0) / top.length);
pointsPriceCache.history.push(avg);
if (pointsPriceCache.history.length > POINTS_HIST_SIZE) pointsPriceCache.history.shift();
const stable = Math.round(pointsPriceCache.history.reduce((s,p) => s+p, 0) / pointsPriceCache.history.length);
pointsPriceCache = { time: now, price: stable, history: pointsPriceCache.history };
pointsPrice = stable;
return stable;
}
}
} catch(e) {}
return pointsPrice || 0;
}
/* ─────────────────────────────────────────
CALCULATION HELPERS
───────────────────────────────────────── */
function calcSet(inv, items) {
const vals = Object.keys(items).map(k => inv[k] || 0);
return vals.length ? Math.min(...vals) : 0;
}
function getSortedItems(inv, items, sets) {
return Object.entries(items)
.map(([name, data]) => ({ name, data, remaining: (inv[name] || 0) - sets }))
.sort((a, b) => a.remaining - b.remaining);
}
function getBottleneck(inv, items, sets) {
const sorted = getSortedItems(inv, items, sets).filter(i => i.remaining < 5);
if (!sorted.length) return null;
const parts = sorted.map(i => {
const locLabel = LOCATIONS[i.data.loc]?.label || i.data.loc;
return `${i.data.s} → ${locLabel}`;
});
return 'Need ' + parts.join(' & ');
}
function stockClass(name, qty) {
if (qty === 0) return 'lt-da';
if (GROUPS.Plushies.items[name]) return qty >= PLU_THRESH ? 'lt-hi' : 'lt-wa';
if (GROUPS.Flowers.items[name]) return qty >= FLO_THRESH ? 'lt-hi' : 'lt-wa';
return qty > 0 ? 'lt-hi' : 'lt-da';
}
function stockColor(name, qty) {
if (qty === 0) return C.stockLo;
if (GROUPS.Plushies.items[name]) return qty >= PLU_THRESH ? C.stockHi : C.stockMid;
if (GROUPS.Flowers.items[name]) return qty >= FLO_THRESH ? C.stockHi : C.stockMid;
return qty > 0 ? C.stockHi : C.stockLo;
}
/* ─────────────────────────────────────────
RENDER HELPERS
───────────────────────────────────────── */
function makeEmpty(icon, msg) {
const el = document.createElement('div');
el.style.cssText = `padding:28px 16px;text-align:center;color:${C.textDim};font-size:11px;line-height:1.6;font-family:Arial,sans-serif;`;
el.innerHTML = `<div style="font-size:26px;margin-bottom:8px;">${icon}</div>${msg}`;
return el;
}
function makeSectionLabel(text, setCount, pts) {
const el = document.createElement('div');
el.style.cssText = `display:flex;justify-content:space-between;align-items:center;padding:4px 10px;font-size:9px;font-weight:700;letter-spacing:1px;text-transform:uppercase;color:${C.goldDim};background:rgba(0,60,80,0.15);border-top:1px solid rgba(0,140,180,0.2);border-bottom:1px solid rgba(0,140,180,0.15);font-family:Consolas,monospace;`;
const left = document.createElement('span'); left.textContent = text;
const right = document.createElement('span');
right.style.cssText = `color:${C.gold};font-weight:700;font-size:8.5px;`;
right.textContent = setCount !== undefined ? `${setCount} sets · ${setCount * pts} pts` : '';
el.appendChild(left); el.appendChild(right);
return el;
}
function makeAlertRow(msg) {
const el = document.createElement('div');
el.style.cssText = `margin:3px 8px;padding:5px 8px 5px 20px;position:relative;background:rgba(160,20,20,0.18);border-left:2px solid rgba(220,50,50,0.8);border-radius:3px;font-size:9.5px;font-weight:600;color:#ff8888;line-height:1.3;font-family:Arial,sans-serif;`;
el.innerHTML = `<span style="position:absolute;left:6px;top:50%;transform:translateY(-50%);font-size:9px;opacity:0.9;color:#ff6666;">!</span>${msg}`;
return el;
}
/* ─────────────────────────────────────────
ITEM ROW BUILDER
───────────────────────────────────────── */
function makeItemRow(name, data, remaining, abroadQty, isBob) {
const row = document.createElement('div');
row.style.cssText = `display:grid;grid-template-columns:34px 34px 36px 1fr;gap:6px;align-items:center;padding:4px 8px;border-bottom:1px solid rgba(0,80,100,0.2);min-height:38px;transition:background 0.15s;`;
row.addEventListener('mouseover', () => row.style.background = 'rgba(0,200,224,0.05)');
row.addEventListener('mouseout', () => row.style.background = 'transparent');
// col 1 — item image
const imgWrap = document.createElement('div');
imgWrap.style.cssText = 'position:relative;width:32px;height:32px;';
const img = document.createElement('img');
img.src = itemImg(data.id); img.alt = data.s; img.title = name;
img.style.cssText = 'width:30px;height:30px;object-fit:contain;border-radius:2px;background:rgba(255,255,255,0.04);border:1px solid rgba(255,255,255,0.08);display:block;transition:transform 0.2s;';
img.addEventListener('mouseover', () => img.style.transform = 'scale(1.15)');
img.addEventListener('mouseout', () => img.style.transform = 'scale(1)');
const imgFallback = document.createElement('span');
imgFallback.style.cssText = 'display:none;width:30px;height:30px;font-size:7px;font-weight:700;text-align:center;line-height:30px;border-radius:2px;border:1px solid rgba(255,255,255,0.1);background:rgba(255,255,255,0.05);color:#c0e0ff;font-family:Consolas,monospace;';
imgFallback.textContent = data.s;
img.addEventListener('error', () => { img.style.display = 'none'; imgFallback.style.display = 'block'; });
imgWrap.appendChild(img); imgWrap.appendChild(imgFallback);
// col 2 — own count (remaining after sets)
const ownEl = document.createElement('div');
ownEl.style.cssText = `color:${C.green};background:rgba(0,180,210,0.08);font-weight:700;text-align:center;border:1px solid rgba(0,180,210,0.15);font-family:Consolas,monospace;padding:2px 3px;border-radius:2px;font-size:10px;`;
ownEl.title = `You have ${remaining} extra after completing sets`;
ownEl.textContent = remaining;
// col 3 — abroad / BoB button
let abroadEl;
if (isBob) {
abroadEl = document.createElement('a');
abroadEl.href = 'https://www.torn.com/shops.php?step=bitsnbobs';
abroadEl.style.cssText = `display:flex;align-items:center;justify-content:center;font-size:8px;font-weight:700;color:${C.gold};background:rgba(0,200,224,0.1);border:1px solid rgba(0,200,224,0.35);border-radius:3px;padding:2px 3px;cursor:pointer;white-space:nowrap;text-decoration:none !important;transition:all 0.15s;`;
abroadEl.textContent = '🏪 BoB';
abroadEl.title = "Open Bits n' Bobs shop";
abroadEl.addEventListener('mouseover', () => { abroadEl.style.background = 'rgba(0,200,224,0.22)'; abroadEl.style.borderColor = 'rgba(0,200,224,0.7)'; });
abroadEl.addEventListener('mouseout', () => { abroadEl.style.background = 'rgba(0,200,224,0.1)'; abroadEl.style.borderColor = 'rgba(0,200,224,0.35)'; });
} else {
const sc = stockClass(name, abroadQty);
const col = stockColor(name, abroadQty);
abroadEl = document.createElement('div');
abroadEl.className = sc;
abroadEl.style.cssText = `color:${col};background:${col}1a;font-weight:700;text-align:center;border:1px solid ${col}44;font-family:Consolas,monospace;padding:2px 3px;border-radius:2px;font-size:10px;transition:all 0.3s;text-shadow:0 0 4px ${col}66;`;
abroadEl.title = `Overseas stock (YATA): ${abroadQty}`;
abroadEl.textContent = abroadQty;
}
// col 4 — flag / location
const loc = LOCATIONS[data.loc] || { flag: '❓', label: data.loc };
const flagEl = document.createElement('div');
flagEl.style.cssText = 'display:flex;align-items:center;justify-content:center;font-size:16px;line-height:1;';
flagEl.title = loc.label;
flagEl.textContent = loc.flag;
row.appendChild(imgWrap);
row.appendChild(ownEl);
row.appendChild(abroadEl);
row.appendChild(flagEl);
return row;
}
/* ─────────────────────────────────────────
COLUMN HEADER ROW
───────────────────────────────────────── */
function makeColHeader() {
const row = document.createElement('div');
row.style.cssText = `display:grid;grid-template-columns:34px 34px 36px 1fr;gap:6px;padding:3px 8px;background:rgba(0,200,224,0.04);border-bottom:1px solid rgba(0,150,180,0.15);`;
['', 'Own', 'Abroad', ''].forEach((txt, i) => {
const s = document.createElement('span');
s.textContent = txt;
s.style.cssText = `font:600 8px Consolas,monospace;color:rgba(0,200,224,0.45);text-align:center;text-transform:uppercase;letter-spacing:.4px;`;
if (txt === 'Own') s.title = 'Your display items minus completed sets. Lowest = bottleneck.';
if (txt === 'Abroad') s.title = 'Live overseas stock from YATA.\n🟢 Plushie ≥2000 / Flower ≥5000\n🟠 Below threshold 🔴 Zero\n🏪 BoB = Bits n\' Bobs shop shortcut';
row.appendChild(s);
});
return row;
}
/* ─────────────────────────────────────────
TAB: SETS
───────────────────────────────────────── */
function renderSetsBody() {
const body = panelEl.querySelector('.lt-body');
body.innerHTML = '';
if (!cfg.apiKey) {
body.appendChild(makeEmpty('🔑', 'Set your API key in Settings<br>to track your item sets'));
return;
}
if (!Object.keys(invCache).length) {
body.appendChild(makeEmpty('◌', 'Loading your items…'));
return;
}
const vis = cfg.getSectionVis();
body.appendChild(makeColHeader());
// ── GROUPS ──
Object.entries(GROUPS).forEach(([groupName, g]) => {
if (vis[groupName.toLowerCase()] === false) return;
const sets = calcSet(invCache, g.items);
const warn = getBottleneck(invCache, g.items, sets);
body.appendChild(makeSectionLabel(g.icon + ' ' + groupName.toUpperCase(), sets, g.pts));
if (warn) body.appendChild(makeAlertRow(warn));
getSortedItems(invCache, g.items, sets).forEach(({ name, data, remaining }) => {
const isBob = BOB_IDS.has(data.id);
const abroad = abroadCache[name] || 0;
body.appendChild(makeItemRow(name, data, remaining, abroad, isBob));
});
});
// ── SPECIAL ──
if (vis.special !== false) {
const specCount = Object.values(SPECIAL_ITEMS).reduce((s, d) => {
const name = Object.keys(SPECIAL_ITEMS).find(k => SPECIAL_ITEMS[k] === d);
return s + (invCache[name] || 0);
}, 0);
body.appendChild(makeSectionLabel('☄️ SPECIAL', specCount, 0));
Object.entries(SPECIAL_ITEMS).forEach(([name, data]) => {
const own = invCache[name] || 0;
const abroad = abroadCache[name] || 0;
body.appendChild(makeItemRow(name, data, own, abroad, false));
});
}
}
/* ─────────────────────────────────────────
TAB: XANAX
───────────────────────────────────────── */
function renderXanaxBody() {
const body = panelEl.querySelector('.lt-body');
body.innerHTML = '';
const xanCount = cfg.getXanCount();
const xanCarry = cfg.getXanCarry();
const vis = cfg.getSectionVis();
if (vis.xanax === false) {
body.appendChild(makeEmpty('🧪', 'Xanax section is hidden.<br>Enable it in Settings.'));
return;
}
const wrap = document.createElement('div');
wrap.style.cssText = 'padding:10px;display:flex;flex-direction:column;gap:10px;';
function secTitle(text) {
const t = document.createElement('div'); t.textContent = text;
t.style.cssText = `font-size:9px;font-weight:700;letter-spacing:1.2px;text-transform:uppercase;color:${C.goldDim};margin-bottom:6px;padding-bottom:4px;border-bottom:1px solid rgba(0,140,170,0.3);font-family:Consolas,monospace;`;
return t;
}
// ── Personal count card ──
const cardSec = document.createElement('div'); cardSec.appendChild(secTitle('🧪 Personal Count'));
const card = document.createElement('div');
card.style.cssText = `background:rgba(0,16,22,0.6);border:1px solid rgba(0,140,170,0.3);border-radius:8px;padding:12px;display:flex;align-items:center;gap:12px;`;
const xImg = document.createElement('img');
xImg.src = itemImg(XANAX_ID); xImg.alt = 'Xanax';
xImg.style.cssText = 'width:40px;height:40px;object-fit:contain;border-radius:3px;flex-shrink:0;';
const countRight = document.createElement('div'); countRight.style.cssText = 'flex:1;';
const countVal = document.createElement('div');
countVal.style.cssText = `font-size:36px;font-weight:700;color:${C.gold};font-family:Consolas,monospace;line-height:1;`;
countVal.textContent = xanCount;
const countSub = document.createElement('div');
countSub.style.cssText = `font-size:9px;color:${C.textDim};font-family:Consolas,monospace;margin-top:2px;`;
countSub.textContent = `Carry limit: ${xanCarry || '—'}`;
countRight.appendChild(countVal); countRight.appendChild(countSub);
// Edit shortcut button
const editBtn = document.createElement('button'); editBtn.type = 'button'; editBtn.textContent = '⚙ Edit';
editBtn.style.cssText = `padding:5px 10px;border-radius:5px;border:1px solid rgba(0,200,224,0.3);background:rgba(50,40,0,0.5);color:${C.goldDim};font-size:9px;font-weight:700;cursor:pointer;font-family:Arial,sans-serif;flex-shrink:0;`;
editBtn.title = 'Adjust count & carry in Settings';
editBtn.addEventListener('click', openSettings);
card.appendChild(xImg); card.appendChild(countRight); card.appendChild(editBtn);
cardSec.appendChild(card);
// ── Live Data ──
const infSec = document.createElement('div'); infSec.appendChild(secTitle('📡 Live Data'));
const infGrid = document.createElement('div'); infGrid.style.cssText = 'display:grid;grid-template-columns:1fr 1fr;gap:8px;';
[
{ label: 'SA Stock', value: xanSACache.qty, color: xanSACache.qty > 0 ? C.okay : C.textDim },
{ label: 'SA Price', value: xanSACache.price > 0 ? '$' + xanSACache.price.toLocaleString() : '—', color: C.gold },
{ label: 'Faction', value: xanFacCache !== null ? xanFacCache.toLocaleString() : '—', color: C.green },
{ label: 'Carry Lmt', value: xanCarry || '—', color: C.goldDim },
].forEach(item => {
const c = document.createElement('div');
c.style.cssText = `background:rgba(0,16,22,0.5);border:1px solid rgba(0,140,170,0.22);border-radius:6px;padding:8px 10px;text-align:center;`;
c.innerHTML = `<div style="font-size:8.5px;color:${C.textDim};font-family:Consolas,monospace;letter-spacing:.5px;margin-bottom:3px;">${item.label}</div>
<div style="font-size:16px;font-weight:700;color:${item.color};font-family:Consolas,monospace;">${item.value}</div>`;
infGrid.appendChild(c);
});
infSec.appendChild(infGrid);
wrap.appendChild(cardSec); wrap.appendChild(infSec);
body.appendChild(wrap);
}
/* ─────────────────────────────────────────
TAB: TRAVEL
───────────────────────────────────────── */
function renderTravelBody() {
const body = panelEl.querySelector('.lt-body');
body.innerHTML = '';
const vis = cfg.getSectionVis();
const wrap = document.createElement('div');
wrap.style.cssText = 'padding:10px;display:flex;flex-direction:column;gap:10px;';
function secTitle(text) {
const t = document.createElement('div'); t.textContent = text;
t.style.cssText = `font-size:9px;font-weight:700;letter-spacing:1.2px;text-transform:uppercase;color:${C.goldDim};margin-bottom:6px;padding-bottom:4px;border-bottom:1px solid rgba(0,140,170,0.3);font-family:Consolas,monospace;`;
return t;
}
// ── Loot Run Planner ──
const escSec = document.createElement('div'); escSec.appendChild(secTitle('💰 Loot Run Planner'));
const lootRuns = [
{
flag: '🇲🇽', name: 'Mexico', col: '#ffb830', time: '~1.5h',
loot: ['Dahlia', 'Jaguar Plushie', 'Obsidian Point'],
},
{
flag: '🇦🇷', name: 'Argentina', col: '#74c9ff', time: '~14h',
loot: ['Ceibo Flower', 'Monkey Plushie', 'Chalcedony Point', 'Meteorite Fragment', 'Patagonian Fossil'],
},
{
flag: '🇬🇧', name: 'UK', col: '#cc88ff', time: '~10h',
loot: ['Heather', 'Nessie Plushie', 'Red Fox Plushie', 'Chert Point'],
},
{
flag: '🇨🇦', name: 'Canada', col: '#ff7070', time: '~9h',
loot: ['Crocus', 'Wolverine Plushie', 'Quartz Point'],
},
{
flag: '🇿🇦', name: 'South Africa', col: '#60cc60', time: '~16h',
loot: ['African Violet', 'Lion Plushie', 'Quartzite Point'],
},
{
flag: '🇨🇭', name: 'Switzerland', col: '#ff9999', time: '~11h',
loot: ['Edelweiss', 'Chamois Plushie'],
},
{
flag: '🇯🇵', name: 'Japan', col: '#ffaacc', time: '~12h',
loot: ['Cherry Blossom'],
},
{
flag: '🇨🇳', name: 'China', col: '#ff6040', time: '~14h',
loot: ['Peony', 'Panda Plushie'],
},
{
flag: '🏝️', name: 'Hawaii', col: '#ffe066', time: '~6h',
loot: ['Orchid', 'Basalt Point'],
},
{
flag: '🇦🇪', name: 'UAE', col: '#88ddaa', time: '~14h',
loot: ['Tribulus Omanense', 'Camel Plushie'],
},
{
flag: '🇰🇾', name: 'Cayman Islands', col: '#66ccff', time: '~10h',
loot: ['Banana Orchid', 'Stingray Plushie'],
},
];
lootRuns.forEach(run => {
let needed = 0, total = 0;
run.loot.forEach(itemName => {
// Determine which group (and thus which section key) this item belongs to
let groupKey = null;
for (const [gName, g] of Object.entries(GROUPS)) {
if (g.items[itemName]) { groupKey = gName.toLowerCase(); break; }
}
if (itemName === 'Meteorite Fragment' || itemName === 'Patagonian Fossil') groupKey = 'special';
// Skip items whose section is hidden — don't count them at all
if (groupKey && vis[groupKey] === false) return;
total++;
let sets = 0;
for (const g of Object.values(GROUPS)) {
if (g.items[itemName]) { sets = calcSet(invCache, g.items); break; }
}
const have = (invCache[itemName] || 0) - sets;
if (have < 5) needed++;
});
run.needed = needed; run.total = total;
});
// Xanax priority: pin South Africa to top if personal count is below threshold
const xanPriority = cfg.getXanPriority();
const xanThreshold = cfg.getXanThreshold();
const xanBelowThresh = xanPriority && cfg.getXanCount() < xanThreshold;
lootRuns.sort((a, b) => {
// If SA priority active, force SA to top regardless of needed count
if (xanBelowThresh) {
const aIsSA = a.name === 'South Africa';
const bIsSA = b.name === 'South Africa';
if (aIsSA && !bIsSA) return -1;
if (bIsSA && !aIsSA) return 1;
}
return b.needed - a.needed;
});
const runGrid = document.createElement('div'); runGrid.style.cssText = 'display:flex;flex-direction:column;gap:5px;';
// Xanax priority banner
if (xanBelowThresh) {
const xanBanner = document.createElement('div');
xanBanner.style.cssText = `display:flex;align-items:center;gap:8px;padding:7px 10px;border-radius:6px;background:rgba(102,187,102,0.1);border:1px solid rgba(102,187,102,0.4);margin-bottom:2px;`;
xanBanner.innerHTML = `<span style="font-size:14px;">🧪</span><div style="flex:1;"><div style="font-size:9.5px;font-weight:700;color:${C.green};font-family:Arial,sans-serif;">Xanax Priority Active</div><div style="font-size:8.5px;color:${C.textDim};font-family:Consolas,monospace;margin-top:1px;">South Africa pinned — personal count ${cfg.getXanCount()} / ${cfg.getXanThreshold()} threshold</div></div>`;
runGrid.appendChild(xanBanner);
}
lootRuns.forEach(run => {
if (run.total === 0) return; // all items in this country are from hidden sections — skip
const urgency = run.needed / run.total;
const a = document.createElement('div');
a.style.cssText = `display:flex;align-items:center;gap:8px;padding:8px 10px;border-radius:7px;cursor:pointer;border:1px solid ${run.col}${urgency > 0.5 ? '66' : '28'};background:${run.col}${urgency > 0.5 ? '14' : '07'};transition:opacity 0.2s;opacity:${urgency === 0 ? '0.35' : '1'};`;
a.addEventListener('mouseover', () => { if (urgency > 0) a.style.opacity = '0.8'; });
a.addEventListener('mouseout', () => { if (urgency > 0) a.style.opacity = '1'; });
a.onclick = function() {
var FULL_NAMES = { 'UK': 'United Kingdom' };
var travelName = FULL_NAMES[run.name] || run.name;
var onTravelPage = (location.pathname + location.search).indexOf('sid=travel') !== -1;
if (onTravelPage) {
clickCountry(travelName);
} else {
window.location.href = 'https://www.torn.com/page.php?sid=travel#lt_travel=' + encodeURIComponent(travelName);
}
};
const flag = document.createElement('span'); flag.textContent = run.flag; flag.style.cssText = 'font-size:16px;flex-shrink:0;';
const mid = document.createElement('div'); mid.style.cssText = 'flex:1;min-width:0;';
const nameRow = document.createElement('div'); nameRow.style.cssText = 'display:flex;align-items:baseline;gap:5px;';
const nameEl = document.createElement('span'); nameEl.textContent = run.name; nameEl.style.cssText = `font-size:10.5px;font-weight:700;color:${run.col};font-family:Arial,sans-serif;`;
const timeEl = document.createElement('span'); timeEl.textContent = run.time; timeEl.style.cssText = `font-size:8.5px;color:${C.textDim};font-family:Consolas,monospace;`;
nameRow.appendChild(nameEl); nameRow.appendChild(timeEl);
const tagWrap = document.createElement('div'); tagWrap.style.cssText = 'display:flex;flex-wrap:wrap;gap:3px;margin-top:3px;';
run.loot.forEach(itemName => {
// Skip items whose section is hidden
let groupKey = null;
for (const [gName, g] of Object.entries(GROUPS)) { if (g.items[itemName]) { groupKey = gName.toLowerCase(); break; } }
if (itemName === 'Meteorite Fragment' || itemName === 'Patagonian Fossil') groupKey = 'special';
if (groupKey && vis[groupKey] === false) return;
let sets = 0;
for (const g of Object.values(GROUPS)) { if (g.items[itemName]) { sets = calcSet(invCache, g.items); break; } }
const have = (invCache[itemName] || 0) - sets;
const isNeeded = have < 5;
const tag = document.createElement('span');
tag.textContent = itemName;
tag.style.cssText = isNeeded
? `font-size:8px;font-family:Consolas,monospace;color:#ff9966;background:rgba(200,80,20,0.2);border:1px solid rgba(200,80,20,0.45);border-radius:3px;padding:1px 4px;`
: `font-size:8px;font-family:Consolas,monospace;color:${C.textDim};background:rgba(80,60,0,0.15);border:1px solid rgba(100,80,0,0.18);border-radius:3px;padding:1px 4px;opacity:0.5;`;
tagWrap.appendChild(tag);
});
mid.appendChild(nameRow); mid.appendChild(tagWrap);
const badge = document.createElement('div'); badge.style.cssText = 'flex-shrink:0;text-align:center;';
if (run.needed > 0) {
badge.innerHTML = `<div style="font-size:14px;font-weight:700;color:${run.col};font-family:Consolas,monospace;line-height:1;">${run.needed}</div><div style="font-size:7.5px;color:${C.textDim};font-family:Consolas,monospace;">needed</div>`;
} else {
badge.innerHTML = `<div style="font-size:11px;color:${C.green};font-family:Consolas,monospace;">✓</div>`;
}
a.appendChild(flag); a.appendChild(mid); a.appendChild(badge);
runGrid.appendChild(a);
});
escSec.appendChild(runGrid);
// ── Best items to carry per country ──
const carSec = document.createElement('div'); carSec.appendChild(secTitle('🎯 Best Items per Country'));
const countryGuide = [
{ flag: '🇲🇽', name: 'Mexico', items: ['Dahlia', 'Jaguar Plushie', 'Obsidian Point'] },
{ flag: '🏝️', name: 'Hawaii', items: ['Orchid', 'Basalt Point'] },
{ flag: '🇿🇦', name: 'South Africa', items: ['African Violet', 'Lion Plushie', 'Quartzite Point'] },
{ flag: '🇯🇵', name: 'Japan', items: ['Cherry Blossom'] },
{ flag: '🇨🇳', name: 'China', items: ['Peony', 'Panda Plushie'] },
{ flag: '🇦🇷', name: 'Argentina', items: ['Ceibo Flower', 'Monkey Plushie', 'Chalcedony Point', 'Meteorite Fragment', 'Patagonian Fossil'] },
{ flag: '🇨🇭', name: 'Switzerland', items: ['Edelweiss', 'Chamois Plushie'] },
{ flag: '🇨🇦', name: 'Canada', items: ['Crocus', 'Wolverine Plushie', 'Quartz Point'] },
{ flag: '🇬🇧', name: 'United Kingdom', items: ['Heather', 'Nessie Plushie', 'Red Fox Plushie', 'Chert Point'] },
{ flag: '🇦🇪', name: 'UAE', items: ['Tribulus Omanense', 'Camel Plushie'] },
{ flag: '🇰🇾', name: 'Cayman Islands', items: ['Banana Orchid', 'Stingray Plushie'] },
{ flag: '🏪', name: "Bits n' Bobs", items: ['Sheep Plushie', 'Teddy Bear Plushie', 'Kitten Plushie'] },
];
countryGuide.forEach(({ flag, name, items: itemNames }) => {
// Filter out items from hidden sections
const visibleItems = itemNames.filter(iName => {
let groupKey = null;
for (const [gName, g] of Object.entries(GROUPS)) { if (g.items[iName]) { groupKey = gName.toLowerCase(); break; } }
if (iName === 'Meteorite Fragment' || iName === 'Patagonian Fossil') groupKey = 'special';
return !(groupKey && vis[groupKey] === false);
});
if (!visibleItems.length) return; // entire country hidden — skip row
const row = document.createElement('div');
row.style.cssText = `display:flex;gap:8px;padding:5px 0;border-bottom:1px solid rgba(0,80,100,0.2);align-items:flex-start;`;
const flagEl = document.createElement('div');
flagEl.textContent = flag; flagEl.style.cssText = 'font-size:16px;flex-shrink:0;padding-top:1px;';
const nameEl = document.createElement('div');
nameEl.style.cssText = `font-size:10px;font-weight:700;color:${C.text};font-family:Arial,sans-serif;flex-shrink:0;min-width:80px;`;
nameEl.textContent = name;
const tagsWrap = document.createElement('div'); tagsWrap.style.cssText = 'display:flex;flex-wrap:wrap;gap:3px;flex:1;';
visibleItems.forEach(iName => {
const tag = document.createElement('span');
tag.style.cssText = `font-size:8.5px;font-family:Consolas,monospace;color:${C.goldDim};background:rgba(120,90,0,0.18);border:1px solid rgba(140,110,0,0.25);border-radius:3px;padding:1px 5px;`;
tag.textContent = iName;
tagsWrap.appendChild(tag);
});
row.appendChild(flagEl); row.appendChild(nameEl); row.appendChild(tagsWrap);
carSec.appendChild(row);
});
wrap.appendChild(escSec); wrap.appendChild(carSec);
body.appendChild(wrap);
}
/* ─────────────────────────────────────────
STATUS BAR
───────────────────────────────────────── */
function renderStatusBar() {
const bar = panelEl ? panelEl.querySelector('#lt-sbar') : null;
if (!bar) return;
bar.innerHTML = '';
if (activeTab === 'sets') {
const vis = cfg.getSectionVis();
let totalSets = 0, totalPts = 0;
Object.entries(GROUPS).forEach(([n, g]) => {
if (vis[n.toLowerCase()] === false) return;
const s = calcSet(invCache, g.items); totalSets += s; totalPts += s * g.pts;
});
// Special items: each individual item counts toward pts (no "set" mechanic)
if (vis.special !== false) {
Object.entries(SPECIAL_ITEMS).forEach(([name, data]) => {
const qty = invCache[name] || 0;
totalPts += qty * data.pts;
totalSets += qty; // each special item counts as its own unit
});
}
const sSpan = document.createElement('span'); sSpan.style.cssText = `color:${C.goldDim};font-weight:700;`; sSpan.textContent = totalSets + ' sets · ' + totalPts + ' pts';
bar.appendChild(sSpan);
if (pointsPrice > 0 && totalPts > 0) {
const pv = totalPts * pointsPrice;
const pvFmt = pv >= 1e6 ? `$${(pv/1e6).toFixed(1)}M` : pv >= 1000 ? `$${Math.round(pv/1000)}k` : `$${pv}`;
const vSpan = document.createElement('span');
vSpan.style.cssText = `margin-left:auto;color:${C.gold};font-weight:700;`;
vSpan.title = `${totalPts} pts × $${pointsPrice.toLocaleString()} per pt`;
vSpan.textContent = pvFmt;
bar.appendChild(vSpan);
}
} else if (activeTab === 'xanax') {
const span = document.createElement('span'); span.style.cssText = `color:${C.goldDim};font-weight:700;`;
span.textContent = 'Personal: ' + cfg.getXanCount() + ' · Faction: ' + (xanFacCache !== null ? xanFacCache : '—');
bar.appendChild(span);
} else if (activeTab === 'travel') {
const span = document.createElement('span'); span.style.cssText = `color:${C.goldDim};`;
span.textContent = 'Loot runs ranked by items needed';
bar.appendChild(span);
}
if (isLoading) {
const sp = document.createElement('span'); sp.className = 'lt-spin'; sp.textContent = '◌';
sp.style.cssText = `margin-left:auto;color:${C.goldDim};`;
bar.appendChild(sp);
}
}
/* ─────────────────────────────────────────
MAIN PANEL RENDER
───────────────────────────────────────── */
function renderPanel() {
if (!panelEl) return;
syncTheme();
renderStatusBar();
if (activeTab === 'sets') renderSetsBody();
if (activeTab === 'xanax') renderXanaxBody();
if (activeTab === 'travel') renderTravelBody();
}
/* ─────────────────────────────────────────
BUILD PANEL
───────────────────────────────────────── */
function buildPanel() {
const light = isLightMode();
const panelBg = light
? 'linear-gradient(158deg,rgba(240,253,255,0.99),rgba(225,248,252,0.98))'
: 'linear-gradient(158deg,rgba(0,14,18,0.98),rgba(0,8,12,0.97))';
const tabDimCol = light ? 'rgba(0,100,130,0.4)' : 'rgba(0,160,190,0.45)';
panelEl.setAttribute('style',
'position:fixed !important;width:0 !important;opacity:0 !important;' +
'max-height:84vh !important;' +
`background:${panelBg} !important;` +
`border:1px solid ${C.border} !important;` +
'border-radius:11px !important;z-index:999989 !important;' +
'display:flex !important;flex-direction:column !important;' +
`box-shadow:0 10px 44px ${light ? 'rgba(0,0,0,0.25)' : 'rgba(0,0,0,0.9)'} !important;` +
'backdrop-filter:blur(14px) !important;overflow:hidden !important;' +
'transition:width 0.26s ease,opacity 0.2s ease !important;' +
`font-family:Arial,sans-serif !important;color:${C.text} !important;`
);
// ── HEADER ──
const hdr = document.createElement('div');
hdr.setAttribute('style', `padding:8px 12px;background:${C.settHdr};border-bottom:1px solid ${C.border};display:flex;align-items:center;gap:7px;flex-shrink:0;`);
const titleEl = document.createElement('span');
titleEl.setAttribute('style', `font-size:10.5px;font-weight:700;letter-spacing:1.5px;text-transform:uppercase;color:${C.gold};flex:1;white-space:nowrap;font-family:Arial,sans-serif;`);
titleEl.innerHTML = '<span class="lt-float" style="display:inline-block;margin-right:4px;">✈</span> Sets Tracker';
function iconBtn(svg, title) {
const b = document.createElement('span'); b.title = title; b.innerHTML = svg;
b.setAttribute('style', `width:20px;height:20px;cursor:pointer;flex-shrink:0;display:flex;align-items:center;justify-content:center;color:${C.goldDim};transition:color 0.18s;`);
b.addEventListener('mouseover', () => b.style.color = C.gold);
b.addEventListener('mouseout', () => b.style.color = C.goldDim);
return b;
}
const SVG_REFRESH = '<svg width="14" height="14" viewBox="0 0 16 16" fill="none"><path d="M13.5 8A5.5 5.5 0 1 1 8 2.5c1.8 0 3.4.87 4.4 2.2" stroke="currentColor" stroke-width="1.4" stroke-linecap="round"/><polyline points="11,1 13.5,3.5 11,6" fill="none" stroke="currentColor" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round"/></svg>';
const SVG_GEAR = '<svg width="14" height="14" viewBox="0 0 16 16" fill="none"><circle cx="8" cy="8" r="2.5" stroke="currentColor" stroke-width="1.4"/><path d="M8 1v2M8 13v2M1 8h2M13 8h2M2.93 2.93l1.41 1.41M11.66 11.66l1.41 1.41M2.93 13.07l1.41-1.41M11.66 4.34l1.41-1.41" stroke="currentColor" stroke-width="1.2" stroke-linecap="round"/></svg>';
const btnRefresh = iconBtn(SVG_REFRESH, 'Refresh data');
const btnSettings = iconBtn(SVG_GEAR, 'Settings');
btnRefresh.addEventListener('click', () => {
invCache = {}; abroadCache = {}; xanSACache = { qty:0, price:0 }; xanFacCache = null;
renderPanel(); refreshAll(); toast('Refreshing…', 1500);
});
btnSettings.addEventListener('click', openSettings);
hdr.appendChild(titleEl); hdr.appendChild(btnRefresh); hdr.appendChild(btnSettings);
// ── TABS ──
const tabs = document.createElement('div');
tabs.setAttribute('style', `display:flex;flex-shrink:0;border-bottom:1px solid ${C.border};`);
function makeTab(label, key) {
const t = document.createElement('div');
t.textContent = label; t.dataset.tab = key;
t.setAttribute('style', `flex:1;padding:6px 3px;font-size:9.5px;font-weight:700;letter-spacing:.6px;text-transform:uppercase;color:${tabDimCol};cursor:pointer;text-align:center;border-bottom:2px solid transparent;transition:all 0.2s;user-select:none;font-family:Arial,sans-serif;`);
t.addEventListener('mouseover', () => { if (!t.classList.contains('lt-tab-active')) t.style.color = C.gold; });
t.addEventListener('mouseout', () => { if (!t.classList.contains('lt-tab-active')) t.style.color = tabDimCol; });
t.addEventListener('click', () => {
tabs.querySelectorAll('[data-tab]').forEach(x => {
x.classList.remove('lt-tab-active');
x.style.color = tabDimCol;
x.style.borderBottomColor = 'transparent';
x.style.background = 'transparent';
});
t.classList.add('lt-tab-active');
t.style.color = C.gold;
t.style.borderBottomColor = C.gold;
t.style.background = C.goldGlow;
activeTab = key;
renderPanel();
});
return t;
}
const tabSets = makeTab('🎒 Sets', 'sets');
const tabXanax = makeTab('🧪 Xanax', 'xanax');
const tabTravel = makeTab('✈ Travel', 'travel');
tabSets.classList.add('lt-tab-active');
tabSets.style.color = C.gold; tabSets.style.borderBottomColor = C.gold; tabSets.style.background = C.goldGlow;
tabs.appendChild(tabSets); tabs.appendChild(tabXanax); tabs.appendChild(tabTravel);
// ── STATUS BAR ──
const sbar = document.createElement('div');
sbar.id = 'lt-sbar';
sbar.setAttribute('style', `padding:3px 10px;font-size:9px;letter-spacing:.4px;color:${C.textDim};border-bottom:1px solid ${C.border};display:flex;gap:8px;align-items:center;flex-shrink:0;font-family:Consolas,monospace;`);
sbar.textContent = 'Loading…';
// ── BODY ──
const bdy = document.createElement('div'); bdy.className = 'lt-body';
panelEl.appendChild(hdr);
panelEl.appendChild(tabs);
panelEl.appendChild(sbar);
panelEl.appendChild(bdy);
}
/* ─────────────────────────────────────────
TOGGLE BUTTON
───────────────────────────────────────── */
function buildToggle() {
const SVG = `<svg width="28" height="28" viewBox="0 0 44 44" fill="none">
<path d="M6 22 L38 10 L32 22 L38 34 Z" fill="rgba(0,200,224,0.18)" stroke="rgba(0,200,224,0.8)" stroke-width="1.3" stroke-linejoin="round"/>
<path d="M20 22 L32 22 L38 34 L20 28 Z" fill="rgba(0,200,224,0.12)" stroke="rgba(0,200,224,0.55)" stroke-width="1" stroke-linejoin="round"/>
<path d="M18 16 L26 12 L28 18 L18 22 Z" fill="rgba(0,200,224,0.15)" stroke="rgba(0,190,220,0.6)" stroke-width="0.9" stroke-linejoin="round"/>
<circle cx="14" cy="22" r="2" fill="rgba(0,200,224,0.7)"/>
</svg>`;
toggleEl.innerHTML = SVG;
toggleEl.title = 'Loot Tracker · Right-click = Settings';
const toggleBg = isLightMode()
? 'radial-gradient(circle at 38% 34%,#d0f8ff,#a8eef8)'
: 'radial-gradient(circle at 38% 34%,#001a20,#000c10)';
toggleEl.setAttribute('style',
'position:fixed !important;width:46px !important;height:46px !important;' +
`background:${toggleBg} !important;` +
`border:2px solid ${C.border} !important;` +
'border-radius:50% !important;' +
'display:flex !important;align-items:center !important;justify-content:center !important;' +
'cursor:grab !important;z-index:999990 !important;' +
`box-shadow:0 0 16px ${C.goldGlow},inset 0 0 12px rgba(0,0,0,0.2) !important;` +
'user-select:none !important;touch-action:none !important;overflow:visible !important;'
);
toggleEl.addEventListener('mouseover', () => {
toggleEl.style.setProperty('box-shadow', `0 0 24px ${C.gold}aa,inset 0 0 12px rgba(0,0,0,0.2)`, 'important');
toggleEl.style.setProperty('border-color', C.gold, 'important');
});
toggleEl.addEventListener('mouseout', () => {
toggleEl.style.setProperty('box-shadow', `0 0 16px ${C.goldGlow},inset 0 0 12px rgba(0,0,0,0.2)`, 'important');
toggleEl.style.setProperty('border-color', C.border, 'important');
});
}
/* ─────────────────────────────────────────
PANEL OPEN/CLOSE + POSITION
───────────────────────────────────────── */
function positionPanel() {
const tr = toggleEl.getBoundingClientRect();
panelEl.style.setProperty('top', Math.min(tr.top, window.innerHeight - 80) + 'px', 'important');
panelEl.style.setProperty('bottom', 'auto', 'important');
if (toggleEl._side === 'left') {
panelEl.style.setProperty('left', (tr.right + 7) + 'px', 'important');
panelEl.style.setProperty('right', 'auto', 'important');
} else {
panelEl.style.setProperty('right', (window.innerWidth - tr.left + 7) + 'px', 'important');
panelEl.style.setProperty('left', 'auto', 'important');
}
}
function openPanel() { panelOpen = true; panelEl.style.setProperty('width','310px','important'); panelEl.style.setProperty('opacity','1','important'); positionPanel(); renderPanel(); }
function closePanel() { panelOpen = false; panelEl.style.setProperty('width','0','important'); panelEl.style.setProperty('opacity','0','important'); }
/* ─────────────────────────────────────────
DRAG TO SNAP
───────────────────────────────────────── */
function setupDrag() {
const SZ = 46, EDGE = 6;
function snap(side, top) {
toggleEl._side = side;
const t = Math.max(EDGE, Math.min(top, window.innerHeight - SZ - EDGE));
toggleEl.style.setProperty('top', t + 'px', 'important');
toggleEl.style.setProperty('bottom', 'auto', 'important');
toggleEl.style.setProperty('left', side === 'left' ? EDGE + 'px' : 'auto', 'important');
toggleEl.style.setProperty('right', side === 'right' ? EDGE + 'px' : 'auto', 'important');
if (panelOpen) positionPanel();
}
try {
const saved = JSON.parse(store.get('lt_pos', '{}'));
snap(saved.side || 'right', saved.top || 280);
} catch(e) { snap('right', 280); }
let dragging = false, moved = false, sX, sY, sL, sT;
function start(cx, cy) { moved = false; dragging = true; sX = cx; sY = cy; const r = toggleEl.getBoundingClientRect(); sL = r.left; sT = r.top; toggleEl.style.setProperty('opacity','0.7','important'); toggleEl.style.setProperty('transform','scale(1.1)','important'); }
function move(cx, cy) {
if (!dragging) return;
const dx = cx - sX, dy = cy - sY;
if (!moved && Math.hypot(dx, dy) < 5) return;
moved = true;
toggleEl.style.setProperty('left', Math.max(EDGE, Math.min(sL + dx, window.innerWidth - SZ - EDGE)) + 'px', 'important');
toggleEl.style.setProperty('right', 'auto', 'important');
toggleEl.style.setProperty('top', Math.max(EDGE, Math.min(sT + dy, window.innerHeight - SZ - EDGE)) + 'px', 'important');
toggleEl.style.setProperty('bottom', 'auto', 'important');
if (panelOpen) positionPanel();
}
function end() {
if (!dragging) return; dragging = false;
toggleEl.style.setProperty('opacity','1','important'); toggleEl.style.setProperty('transform','none','important');
if (!moved) return;
const r = toggleEl.getBoundingClientRect();
const side = (r.left + SZ / 2) < window.innerWidth / 2 ? 'left' : 'right';
snap(side, r.top);
store.set('lt_pos', JSON.stringify({ side, top: r.top }));
}
toggleEl.addEventListener('mousedown', e => { if (e.button !== 0) return; start(e.clientX, e.clientY); });
document.addEventListener('mousemove', e => move(e.clientX, e.clientY));
document.addEventListener('mouseup', end);
toggleEl.addEventListener('touchstart', e => { const t = e.touches[0]; start(t.clientX, t.clientY); }, { passive: true });
toggleEl.addEventListener('touchmove', e => { if (!dragging) return; e.preventDefault(); const t = e.touches[0]; move(t.clientX, t.clientY); }, { passive: false });
toggleEl.addEventListener('touchend', end);
toggleEl.addEventListener('click', () => { if (moved) return; if (panelOpen) closePanel(); else openPanel(); });
toggleEl.addEventListener('contextmenu', e => { e.preventDefault(); openSettings(); });
/* ── Carousel Tooltip ── */
(function buildCarousel() {
const slides = [
{ icon: '✈️', title: 'Sets Tracker', body: 'Your overseas sets companion. Track Plushie, Flower, Prehistoric, and Special item sets — plus Xanax and loot run planning.' },
{ icon: '🎒', title: 'Sets Tab', body: 'Shows every item sorted by bottleneck. "Own" = extras after completing sets. "Abroad" = live YATA overseas stock.' },
{ icon: '🟢', title: 'Stock Colours', body: 'Green = stock above threshold (Plushie ≥2000 / Flower ≥5000). Orange = below threshold. Red = zero stock abroad.' },
{ icon: '🧪', title: 'Xanax Tab', body: 'Track your personal Xanax count and carry limit. Use the stepper controls and "+ Add Carry Limit" to update after a run.' },
{ icon: '✈', title: 'Travel Tab', body: 'Loot Run Planner ranks destinations by how many items you still need. Highlighted tags = items below 5 surplus.' },
{ icon: '🔑', title: 'API Key', body: 'Requires a Limited Access (Display) key from Torn. Only display data is read — your key stays on your device.' },
{ icon: '🔄', title: 'Manual Refresh', body: 'Tap ↺ in the panel header to instantly re-fetch inventory, YATA abroad stock, faction Xanax, and points price.' },
{ icon: '📱', title: 'PDA Compatible', body: 'Built for Torn PDA on mobile. Drag the ✈ button anywhere on screen — it snaps to the nearest edge automatically.' },
{ icon: '⚙', title: 'Settings', body: 'Right-click the ✈ button (or tap ⚙ in the header) to open Settings. Control API key, User ID, and section visibility.' },
];
let cur = 0;
const tip = document.createElement('div');
tip.id = 'lt-carousel';
tip.setAttribute('style',
'position:fixed !important;bottom:72px !important;right:12px !important;' +
'width:234px !important;' +
'background:linear-gradient(145deg,rgba(0,12,18,0.97),rgba(0,8,14,0.97)) !important;' +
'border:1px solid rgba(0,180,210,0.45) !important;border-radius:10px !important;' +
'z-index:999995 !important;padding:12px 14px 10px !important;' +
'box-shadow:0 6px 28px rgba(0,0,0,0.85) !important;' +
'font-family:Arial,sans-serif !important;color:#e8f0d0 !important;' +
'opacity:0 !important;transition:opacity 0.35s ease !important;'
);
const iconEl = document.createElement('div');
const titleEl = document.createElement('div');
const bodyEl = document.createElement('div');
const dotsWrap = document.createElement('div');
const navWrap = document.createElement('div');
iconEl.setAttribute('style',
'font-size:24px !important;margin-bottom:5px !important;line-height:1 !important;');
titleEl.setAttribute('style',
'font-size:11px !important;font-weight:700 !important;color:#00c8e0 !important;' +
'margin-bottom:5px !important;letter-spacing:.5px !important;text-transform:uppercase !important;');
bodyEl.setAttribute('style',
'font-size:10.5px !important;line-height:1.55 !important;' +
'color:rgba(190,240,248,0.88) !important;min-height:48px !important;');
dotsWrap.setAttribute('style',
'display:flex !important;justify-content:center !important;gap:6px !important;' +
'margin-top:2px !important;align-items:center !important;');
navWrap.setAttribute('style',
'display:flex !important;justify-content:space-between !important;' +
'align-items:center !important;margin-top:9px !important;');
function makeDot(i) {
const d = document.createElement('div');
d.setAttribute('style',
'width:6px !important;height:6px !important;border-radius:50% !important;' +
'cursor:pointer !important;flex-shrink:0 !important;transition:background 0.2s !important;' +
'background:' + (i === cur ? 'rgba(0,200,224,0.9)' : 'rgba(120,120,120,0.3)') + ' !important;'
);
(function(idx) { d.addEventListener('click', function() { clearInterval(autoTimer); go(idx); }); })(i);
return d;
}
function renderDots() {
dotsWrap.innerHTML = '';
for (let i = 0; i < slides.length; i++) dotsWrap.appendChild(makeDot(i));
}
function go(n) {
cur = (n + slides.length) % slides.length;
iconEl.textContent = slides[cur].icon;
titleEl.textContent = slides[cur].title;
bodyEl.textContent = slides[cur].body;
renderDots();
}
const prevBtn = document.createElement('div');
prevBtn.textContent = '◀';
prevBtn.setAttribute('style',
'cursor:pointer !important;font-size:12px !important;color:rgba(0,190,215,0.8) !important;' +
'padding:3px 8px !important;user-select:none !important;border-radius:4px !important;' +
'border:1px solid rgba(0,170,205,0.3) !important;'
);
prevBtn.addEventListener('click', function() { clearInterval(autoTimer); go(cur - 1); });
const nextBtn = document.createElement('div');
nextBtn.textContent = '▶';
nextBtn.setAttribute('style',
'cursor:pointer !important;font-size:12px !important;color:rgba(0,190,215,0.8) !important;' +
'padding:3px 8px !important;user-select:none !important;border-radius:4px !important;' +
'border:1px solid rgba(0,170,205,0.3) !important;'
);
nextBtn.addEventListener('click', function() { clearInterval(autoTimer); go(cur + 1); });
const closeBtn = document.createElement('div');
closeBtn.textContent = '✕';
closeBtn.setAttribute('style',
'position:absolute !important;top:8px !important;right:10px !important;' +
'cursor:pointer !important;font-size:11px !important;' +
'color:rgba(0,190,215,0.65) !important;line-height:1 !important;user-select:none !important;'
);
closeBtn.addEventListener('click', function() {
clearInterval(autoTimer);
tip.style.setProperty('opacity', '0', 'important');
setTimeout(function() { if (tip.parentNode) tip.parentNode.removeChild(tip); }, 350);
});
navWrap.appendChild(prevBtn);
navWrap.appendChild(dotsWrap);
navWrap.appendChild(nextBtn);
tip.appendChild(closeBtn);
tip.appendChild(iconEl);
tip.appendChild(titleEl);
tip.appendChild(bodyEl);
tip.appendChild(navWrap);
go(0);
let autoTimer = setInterval(function() { go(cur + 1); }, 5000);
// Only show once — persisted in localStorage
const SEEN_KEY = 'lt_tooltip_seen';
let alreadySeen = false;
try { alreadySeen = !!localStorage.getItem(SEEN_KEY); } catch(e) {}
if (alreadySeen) return;
document.body.appendChild(tip);
setTimeout(function() { tip.style.setProperty('opacity', '1', 'important'); }, 1800);
function markSeen() {
try { localStorage.setItem(SEEN_KEY, '1'); } catch(e) {}
}
closeBtn.addEventListener('click', markSeen);
// Also mark seen after cycling through all slides once
let seenCount = 0;
const origGo = go;
go = function(n) {
origGo(n);
seenCount++;
if (seenCount >= slides.length) markSeen();
};
})();
}
/* ─────────────────────────────────────────
REFRESH
───────────────────────────────────────── */
async function refreshAll() {
isLoading = true;
renderStatusBar();
await Promise.allSettled([
fetchInventory()
.then(d => { invCache = d; })
.catch(() => {}),
fetchAbroad()
.then(d => { abroadCache = d; })
.catch(() => {}),
fetchXanaxSA()
.then(d => { xanSACache = d; })
.catch(() => {}),
fetchXanaxFaction()
.then(d => { if (d !== null) xanFacCache = d; })
.catch(() => {}),
fetchPointsPrice()
.then(p => { pointsPrice = p; })
.catch(() => {}),
]);
isLoading = false;
if (panelEl) renderPanel();
}
async function mainLoop() {
await refreshAll();
pollTimer = setTimeout(mainLoop, 45000);
}
/* ─────────────────────────────────────────
INIT
───────────────────────────────────────── */
// Click a country row on the travel page — works in both TornPDA and Tampermonkey
function clickCountry(name) {
var tLow = name.toLowerCase();
var doc = (typeof unsafeWindow !== 'undefined' && unsafeWindow.document) ? unsafeWindow.document : document;
var cells = doc.querySelectorAll('div[class*="flagAndName"]');
for (var i = 0; i < cells.length; i++) {
var txt = (cells[i].textContent || '').trim().toLowerCase();
if (txt.indexOf(tLow) !== -1) {
var parent = cells[i].parentElement;
if (parent) { parent.click(); return true; }
}
}
return false;
}
function init() {
// Auto-click via page-context script injection (bypasses GM sandbox)
(function() {
var target = null;
try {
var m = (location.hash || '').match(/lt_travel=([^&]+)/);
if (m) target = decodeURIComponent(m[1]);
} catch(e) {}
if (!target || (location.pathname + location.search).indexOf('sid=travel') === -1) return;
var tName = target;
// Try once after 3s with debug
// Poll every 300ms for up to 15s
var _attempts = 0;
var _poll = setInterval(function() {
_attempts++;
if (_attempts > 50) { clearInterval(_poll); return; }
if (clickCountry(tName)) { clearInterval(_poll); }
}, 300);
})();
if (document.getElementById('lt-toggle')) return;
try { injectCSS(); } catch(e) { console.warn('[LT] CSS inject failed:', e); }
toggleEl = document.createElement('div'); toggleEl.id = 'lt-toggle';
panelEl = document.createElement('div'); panelEl.id = 'lt-panel';
document.body.appendChild(panelEl);
document.body.appendChild(toggleEl);
buildToggle();
buildPanel();
setupDrag();
if (!cfg.apiKey) {
setTimeout(openSettings, 600);
} else {
mainLoop();
}
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', () => setTimeout(init, 400));
} else {
setTimeout(init, 400);
}
})();