Ultra-digital foreign target intelligence and organized-crime flight warnings for Torn
// ==UserScript==
// @name LostMaster TARGET//ZERO
// @namespace lostmaster.travel
// @version 4.0.3
// @description Ultra-digital foreign target intelligence and organized-crime flight warnings for Torn
// @author LostMaster
// @license MIT
// @match https://www.torn.com/*
// @grant GM_xmlhttpRequest
// @connect yata.yt
// ==/UserScript==
(function () {
'use strict';
const APP = 'LostMaster TARGET//ZERO';
const VERSION = '4.0.3';
const API_BASE = 'https://api.torn.com/v2';
const YATA_URL = 'https://yata.yt/api/v1/travel/export/';
const ROOT_ID = 'lmx-root';
const STYLE_ID = 'lmx-style';
const STORAGE = {
key: 'lmt_api_key',
profile: 'lmt_travel_profile',
minimized: 'lmx_minimized',
position: 'lmx_panel_position',
size: 'lmx_panel_size',
yata: 'lmx_yata_cache',
stockHistory: 'lmx_xanax_stock_history',
targetLogs: 'lmx_target_logs',
oc: 'lmx_oc_cache',
warningHours: 'lmx_oc_warning_hours'
};
const TRAVEL = {
'Mexico': { minutes: 24, city: 'Ciudad Juarez', yata: 'mex' },
'Cayman Islands': { minutes: 33, city: 'George Town', yata: 'cay' },
'Canada': { minutes: 39, city: 'Toronto', yata: 'can' },
'Hawaii': { minutes: 127, city: 'Honolulu', yata: 'haw' },
'United Kingdom': { minutes: 151, city: 'London', yata: 'uni' },
'Argentina': { minutes: 158, city: 'Buenos Aires', yata: 'arg' },
'Switzerland': { minutes: 166, city: 'Zurich', yata: 'swi' },
'Japan': { minutes: 213, city: 'Tokyo', yata: 'jap' },
'China': { minutes: 229, city: 'Beijing', yata: 'chi' },
'United Arab Emirates': { minutes: 257, city: 'Dubai', yata: 'uae' },
'South Africa': { minutes: 282, city: 'Johannesburg', yata: 'sou' }
};
const YATA_COUNTRY = Object.fromEntries(
Object.entries(TRAVEL).map(([country, data]) => [data.yata, country])
);
const METHOD_MULTIPLIER = {
standard: 1,
airstrip: 0.70,
wlt: 0.50,
business: 0.30
};
const DEFAULT_PROFILE = {
method: 'standard',
mailingBook: false,
capacity: 28,
businessTicketsFree: false
};
const KEY_STOP_ERROR_CODES = new Set([1, 2, 10, 13, 16, 18]);
const YATA_TTL = 60 * 1000;
const OC_TTL = 5 * 60 * 1000;
const WATCH_MS = 1000;
const HISTORY_MAX_AGE = 7 * 24 * 60 * 60 * 1000;
const TARGET_MAX_AGE = 120 * 24 * 60 * 60 * 1000;
let settingsOpen = false;
let dataBusy = false;
let dataError = '';
let yataWarning = '';
let apiKeyNotice = '';
let lastSignature = '';
let lastRenderedMinute = -1;
let dragState = null;
let resizeState = null;
let confirmBypassUntil = 0;
function esc(value) {
const node = document.createElement('div');
node.textContent = String(value ?? '');
return node.innerHTML;
}
function readJson(key, fallback = null) {
try {
return JSON.parse(localStorage.getItem(key) || 'null') ?? fallback;
} catch (_) {
return fallback;
}
}
function writeJson(key, value) {
localStorage.setItem(key, JSON.stringify(value));
}
function cacheGet(key, ttl = Number.MAX_SAFE_INTEGER) {
const cached = readJson(key);
if (!cached || !Number.isFinite(cached.updated)) return null;
if (Date.now() - cached.updated > ttl) return null;
return cached.data ?? null;
}
function cacheSet(key, data) {
writeJson(key, { updated: Date.now(), data });
}
function cacheIsFresh(key, ttl) {
const cached = readJson(key);
return Boolean(cached && Number.isFinite(cached.updated) && Date.now() - cached.updated <= ttl);
}
function getKey() {
return localStorage.getItem(STORAGE.key) || '';
}
function setKey(value) {
localStorage.setItem(STORAGE.key, String(value || '').trim());
apiKeyNotice = '';
}
function getProfile() {
const raw = readJson(STORAGE.profile, {});
return {
method: METHOD_MULTIPLIER[raw.method] !== undefined ? raw.method : DEFAULT_PROFILE.method,
mailingBook: Boolean(raw.mailingBook),
capacity: Math.max(1, Math.min(100, Number(raw.capacity) || DEFAULT_PROFILE.capacity)),
businessTicketsFree: Boolean(raw.businessTicketsFree)
};
}
function saveProfile(profile) {
writeJson(STORAGE.profile, profile);
}
function warningHours() {
return Math.max(1, Math.min(72, Number(localStorage.getItem(STORAGE.warningHours)) || 12));
}
function flightMinutes(country) {
const profile = getProfile();
const base = TRAVEL[country]?.minutes || 0;
const bookMultiplier = profile.mailingBook ? 0.75 : 1;
return Math.max(1, Math.round(base * METHOD_MULTIPLIER[profile.method] * bookMultiplier));
}
function durationFromSeconds(seconds) {
const total = Math.max(0, Math.floor(Number(seconds) || 0));
const days = Math.floor(total / 86400);
const hours = Math.floor((total % 86400) / 3600);
const minutes = Math.floor((total % 3600) / 60);
if (days) return `${days}d ${hours}h ${minutes}m`;
if (hours) return `${hours}h ${minutes}m`;
return `${minutes}m`;
}
function durationFromMinutes(minutes) {
return durationFromSeconds(Number(minutes) * 60);
}
function money(value) {
return Number.isFinite(Number(value)) ? '$' + Math.round(Number(value)).toLocaleString() : '—';
}
function ageText(timestampSeconds) {
if (!timestampSeconds) return 'unknown age';
const seconds = Math.max(0, Date.now() / 1000 - Number(timestampSeconds));
if (seconds < 60) return '<1m old';
return durationFromSeconds(seconds) + ' old';
}
async function rawApi(path, keyOverride = null) {
const key = keyOverride === null ? getKey() : keyOverride;
if (!key) throw new Error('No API key configured.');
const url = new URL(API_BASE + path);
url.searchParams.set('key', key);
const response = await fetch(url.toString());
if (!response.ok) throw new Error(`Torn API HTTP ${response.status}.`);
const data = await response.json();
if (data?.error) {
const code = Number(data.error.code);
const message = data.error.error || 'Torn API error.';
if (keyOverride === null && KEY_STOP_ERROR_CODES.has(code)) {
localStorage.removeItem(STORAGE.key);
apiKeyNotice = message;
}
const error = new Error(message);
error.tornCode = code;
throw error;
}
return data;
}
function gmJson(url) {
return new Promise((resolve, reject) => {
if (typeof GM_xmlhttpRequest !== 'function') {
fetch(url).then(r => {
if (!r.ok) throw new Error(`YATA HTTP ${r.status}`);
return r.json();
}).then(resolve).catch(reject);
return;
}
GM_xmlhttpRequest({
method: 'GET', url, headers: { Accept: 'application/json' }, timeout: 20000,
onload(response) {
if (response.status < 200 || response.status >= 300) {
reject(new Error(`YATA HTTP ${response.status}`));
return;
}
try { resolve(JSON.parse(response.responseText || '{}')); }
catch (error) { reject(error); }
},
onerror: () => reject(new Error('Could not reach YATA.')),
ontimeout: () => reject(new Error('YATA request timed out.'))
});
});
}
function normalizeYata(raw) {
const result = {};
for (const [code, countryData] of Object.entries(raw?.stocks || {})) {
const country = YATA_COUNTRY[code];
if (!country) continue;
const item = (countryData?.stocks || []).find(entry =>
String(entry?.name || '').trim().toLowerCase() === 'xanax'
);
if (!item) continue;
result[country] = {
update: Number(countryData?.update || 0),
quantity: Math.max(0, Number(item.quantity) || 0),
cost: Math.max(0, Number(item.cost) || 0),
nextRestock: item.nextRestock || null
};
}
return result;
}
function recordStockSnapshots(data) {
const history = readJson(STORAGE.stockHistory, {});
const now = Date.now();
for (const [country, stock] of Object.entries(data)) {
const rows = Array.isArray(history[country]) ? history[country] : [];
const stamp = Number(stock.update) > 0 ? Number(stock.update) * 1000 : now;
const last = rows[rows.length - 1];
if (!last || last.timestamp !== stamp || last.quantity !== stock.quantity) {
rows.push({ timestamp: stamp, quantity: stock.quantity });
}
history[country] = rows
.filter(row => now - Number(row.timestamp) <= HISTORY_MAX_AGE)
.slice(-250);
}
writeJson(STORAGE.stockHistory, history);
}
async function getYata(force = false) {
if (!force) {
const fresh = cacheGet(STORAGE.yata, YATA_TTL);
if (fresh) return fresh;
}
try {
const data = normalizeYata(await gmJson(YATA_URL));
recordStockSnapshots(data);
cacheSet(STORAGE.yata, data);
yataWarning = '';
return data;
} catch (error) {
const stale = cacheGet(STORAGE.yata);
if (!stale) throw error;
yataWarning = `YATA unavailable; using cached stock (${String(error?.message || error)}).`;
return stale;
}
}
function stockTrend(country) {
const rows = (readJson(STORAGE.stockHistory, {})[country] || [])
.filter(row => Date.now() - Number(row.timestamp) <= HISTORY_MAX_AGE);
let depleted = 0;
let observedHours = 0;
let events = 0;
for (let i = 1; i < rows.length; i += 1) {
const hours = (rows[i].timestamp - rows[i - 1].timestamp) / 3600000;
if (hours <= 0 || hours > 12) continue;
const delta = rows[i - 1].quantity - rows[i].quantity;
observedHours += hours;
events += 1;
if (delta > 0) {
depleted += delta;
}
}
if (!events || observedHours < 0.08) {
return { rate: null, label: 'NO SIGNAL', className: 'neutral', events, observedHours };
}
const rate = depleted / observedHours;
if (rate >= 20) return { rate, label: 'TARGET-RICH', className: 'hot', events, observedHours };
if (rate >= 8) return { rate, label: 'ACTIVE', className: 'hot', events, observedHours };
if (rate >= 2) return { rate, label: 'TRACE', className: 'warm', events, observedHours };
return { rate, label: 'LOW SIGNAL', className: 'cool', events, observedHours };
}
async function refreshOc(force = false) {
if (!getKey()) return null;
if (!force) {
const fresh = cacheGet(STORAGE.oc, OC_TTL);
if (fresh !== null) return fresh;
}
const data = await rawApi('/user/organizedcrime');
const crime = data?.organizedCrime ?? null;
cacheSet(STORAGE.oc, crime);
return crime;
}
function ocState() {
const crime = cacheGet(STORAGE.oc);
if (!crime) return { kind: 'none', text: 'No active OC found', crime: null, seconds: null };
const readyAt = Number(crime.ready_at);
if (!readyAt) return { kind: 'neutral', text: `${crime.name || 'OC'} has no ready time`, crime, seconds: null };
const seconds = readyAt - Math.floor(Date.now() / 1000);
if (seconds <= 0) return { kind: 'danger', text: `${crime.name || 'OC'} is ready now`, crime, seconds };
if (seconds <= warningHours() * 3600) {
return { kind: 'danger', text: `SYSTEM INTERRUPT // ${crime.name || 'OC'} IN ${durationFromSeconds(seconds)} // FLIGHT LOCK`, crime, seconds };
}
return { kind: 'safe', text: `MISSION CLEAR // ${crime.name || 'OC'} IN ${durationFromSeconds(seconds)}`, crime, seconds };
}
function pageText() {
if (!document.body) return '';
const clone = document.body.cloneNode(true);
clone.querySelector('#' + ROOT_ID)?.remove();
return String(clone.textContent || '').replace(/\s+/g, ' ').trim();
}
function currentCountry() {
const text = pageText().toLowerCase();
for (const country of Object.keys(TRAVEL).sort((a, b) => b.length - a.length)) {
if (text.includes(country.toLowerCase())) return country;
}
return null;
}
function isTravelAgency() {
return location.pathname.toLowerCase().endsWith('/travelagency.php');
}
function flightRouteText() {
const section = document.querySelector('section[class*="flightProgressSection"]');
return String(section?.innerText || section?.textContent || '').replace(/\s+/g, ' ').trim();
}
function isFlying() {
const route = flightRouteText();
return Boolean(route && /remaining flight time/i.test(route));
}
function isAbroad() {
if (!location.pathname.toLowerCase().endsWith('/shops.php')) return false;
const text = pageText().toLowerCase();
const store = text.includes('general store') || text.includes('black market');
return store && text.includes('stock') && text.includes('amount') && text.includes('buy');
}
function isTravelContext() {
return isTravelAgency() || isAbroad() || isFlying();
}
function tctParts(timestamp = Date.now()) {
const date = new Date(timestamp);
return {
day: date.getUTCDay(),
bucket: Math.floor(date.getUTCHours() / 4)
};
}
function saveTargetLog(country, count) {
const value = Math.max(0, Math.min(999, Math.round(Number(count))));
if (!country || !Number.isFinite(value)) return false;
const logs = readJson(STORAGE.targetLogs, []);
const now = Date.now();
const time = tctParts(now);
logs.push({ country, count: value, timestamp: now, day: time.day, bucket: time.bucket });
writeJson(STORAGE.targetLogs, logs
.filter(row => now - Number(row.timestamp) <= TARGET_MAX_AGE)
.slice(-1000));
return true;
}
function targetEstimate(country) {
const now = Date.now();
const time = tctParts(now + flightMinutes(country) * 60000);
const all = readJson(STORAGE.targetLogs, []).filter(row =>
row.country === country && now - Number(row.timestamp) <= TARGET_MAX_AGE
);
let matched = all.filter(row => row.day === time.day && row.bucket === time.bucket);
let scope = 'same day/time';
if (matched.length < 3) {
matched = all.filter(row => row.bucket === time.bucket);
scope = 'same time window';
}
if (matched.length < 3) {
matched = all;
scope = 'all visits';
}
if (!matched.length) return { samples: 0, scope, text: 'No personal estimate yet', average: null };
const counts = matched.map(row => Number(row.count)).filter(Number.isFinite);
const average = counts.reduce((sum, value) => sum + value, 0) / counts.length;
const variance = counts.reduce((sum, value) => sum + Math.pow(value - average, 2), 0) / counts.length;
const spread = Math.max(1, Math.round(Math.sqrt(variance)));
const low = Math.max(0, Math.round(average - spread));
const high = Math.max(low, Math.round(average + spread));
return {
samples: counts.length,
scope,
average,
text: counts.length === 1 ? `Observed: ${counts[0]}` : `Expected: ${low}–${high} (avg ${average.toFixed(1)})`
};
}
function destinationRows() {
const yata = cacheGet(STORAGE.yata) || {};
return Object.entries(yata).map(([country, stock]) => {
const trend = stockTrend(country);
const flight = flightMinutes(country);
const projected = trend.rate === null ? null : Math.max(0, Math.floor(stock.quantity - trend.rate * flight / 60));
const targets = targetEstimate(country);
return { country, stock, trend, flight, projected, targets };
}).sort((a, b) => {
const aTargets = a.targets.average ?? -1;
const bTargets = b.targets.average ?? -1;
if (aTargets !== bTargets) return bTargets - aTargets;
const aRate = a.trend.rate ?? -1;
const bRate = b.trend.rate ?? -1;
return bRate - aRate;
});
}
function injectStyle() {
if (document.getElementById(STYLE_ID)) return;
const style = document.createElement('style');
style.id = STYLE_ID;
style.textContent = `
#${ROOT_ID}{position:fixed;z-index:999999;top:18%;right:10px;width:390px;max-height:72vh;background:linear-gradient(145deg,#020717 0%,#050b22 55%,#090527 100%);color:#b9eaff;border:1px solid #00d9ff;border-radius:2px;clip-path:polygon(0 0,calc(100% - 15px) 0,100% 15px,100% 100%,15px 100%,0 calc(100% - 15px));box-shadow:0 0 0 1px #08113a,0 0 26px #00d9ff45,0 10px 35px #000f;font:12px Consolas,"Courier New",monospace;overflow:hidden;display:flex;flex-direction:column}
#${ROOT_ID}:before{content:"";position:absolute;inset:0;pointer-events:none;z-index:19;background:linear-gradient(90deg,transparent 0 48%,#00eaff08 50%,transparent 52%),repeating-linear-gradient(90deg,#00d9ff08 0,#00d9ff08 1px,transparent 1px,transparent 24px)}
#${ROOT_ID}:after{content:"";position:absolute;left:0;right:0;height:42px;top:-45px;pointer-events:none;z-index:20;background:linear-gradient(180deg,transparent,#00efff20,transparent);animation:lmxScan 4.2s linear infinite}
#${ROOT_ID}.hidden{display:none} #${ROOT_ID}.mini{width:auto!important;height:auto!important;max-height:none!important;background:transparent;border:0;box-shadow:none;top:50%!important;right:0!important;left:auto!important;bottom:auto!important;transform:translateY(-50%)}
#${ROOT_ID} *{box-sizing:border-box} #${ROOT_ID} button,#${ROOT_ID} input,#${ROOT_ID} select{font:inherit}
#${ROOT_ID} .head{display:flex;align-items:center;gap:7px;padding:9px 10px;background:linear-gradient(90deg,#061535,#150838);border-bottom:1px solid #00d9ff;box-shadow:0 4px 15px #00d9ff18;cursor:move;user-select:none;touch-action:none;letter-spacing:1.2px}
#${ROOT_ID} .title{font-weight:900;color:#73f6ff;flex:1;text-shadow:0 0 5px #00eaff,0 0 13px #008cff} #${ROOT_ID} .icon{border:1px solid #00d9ff;background:#071333;color:#75f7ff;border-radius:1px;padding:4px 7px;cursor:pointer;box-shadow:inset 0 0 8px #00d9ff20}
#${ROOT_ID} .body{overflow:auto;padding:10px;scrollbar-color:#00d9ff #030719} #${ROOT_ID} .oc{padding:9px;border-radius:1px;margin-bottom:9px;font-weight:800;letter-spacing:.5px;clip-path:polygon(0 0,calc(100% - 9px) 0,100% 9px,100% 100%,0 100%)}
#${ROOT_ID} .oc.safe{background:#022920;color:#63ffb2;border:1px solid #00ff9d;box-shadow:inset 0 0 14px #00ff9d20} #${ROOT_ID} .oc.danger{background:#31000b;color:#ff2452;border:1px solid #ff003c;box-shadow:inset 0 0 18px #ff003340;animation:lmxPulse 1.1s infinite}
#${ROOT_ID} .oc.neutral,#${ROOT_ID} .oc.none{background:#0b1431;color:#77dfff;border:1px solid #246da0}
@keyframes lmxPulse{50%{box-shadow:inset 0 0 22px #ff003344,0 0 0 2px #ff003377,0 0 18px #ff003355}}
@keyframes lmxScan{to{top:calc(100% + 45px)}}
#${ROOT_ID} .notice{padding:7px;margin-bottom:8px;background:#2d1c07;color:#ffe760;border:1px solid #ffb900;border-radius:1px}
#${ROOT_ID} .cards{display:grid;gap:8px} #${ROOT_ID} .card{position:relative;background:linear-gradient(135deg,#06142c,#0c0a2c);border:1px solid #176d9d;border-left:3px solid #00d9ff;border-radius:1px;padding:9px;clip-path:polygon(0 0,calc(100% - 9px) 0,100% 9px,100% 100%,0 100%);box-shadow:inset 0 0 17px #008cff16}
#${ROOT_ID} .card:before{content:"◆";position:absolute;right:8px;bottom:3px;color:#ea35ff;font-size:11px;text-shadow:0 0 7px #ea35ff}
#${ROOT_ID} .cardTop{display:flex;align-items:center;gap:8px;margin-bottom:5px} #${ROOT_ID} .country{font-weight:900;color:#72f5ff;flex:1;font-size:13px;letter-spacing:1px;text-shadow:0 0 8px #00cfff}
#${ROOT_ID} .heat{border-radius:1px;padding:2px 7px;font-weight:800;letter-spacing:.7px} #${ROOT_ID} .heat.hot{background:#350013;color:#ff2d63;border:1px solid #ff0051;text-shadow:0 0 7px #ff0051} #${ROOT_ID} .heat.warm{background:#291038;color:#f067ff;border:1px solid #d52bff;text-shadow:0 0 7px #d52bff} #${ROOT_ID} .heat.cool{background:#03273c;color:#52e8ff;border:1px solid #008fc2} #${ROOT_ID} .heat.neutral{background:#10172e;color:#7291b9;border:1px solid #34496d}
#${ROOT_ID} .grid{display:grid;grid-template-columns:1fr 1fr;gap:3px 10px;color:#688eaf} #${ROOT_ID} .value{color:#9dffbc;text-align:right;text-shadow:0 0 5px #00ff8840}
#${ROOT_ID} .targets{margin-top:7px;padding-top:6px;border-top:1px solid #174d78;color:#f06cff} #${ROOT_ID} .muted{color:#627f9f;font-size:11px}
#${ROOT_ID} .log{display:flex;gap:6px;margin-top:9px;padding-top:9px;border-top:1px solid #174d78} #${ROOT_ID} input,#${ROOT_ID} select{background:#020718;color:#a7f6ff;border:1px solid #1676a7;border-radius:1px;padding:6px;min-width:0}
#${ROOT_ID} .primary{background:linear-gradient(90deg,#003c58,#162766);color:#78f7ff;border:1px solid #00d9ff;border-radius:1px;padding:6px 9px;cursor:pointer;text-transform:uppercase;font-weight:800;box-shadow:inset 0 0 10px #00d9ff24}
#${ROOT_ID} .settings{margin-top:9px;padding:9px;background:#070d25;border:1px solid #254e82;border-left:3px solid #d52bff;border-radius:1px} #${ROOT_ID} label{display:block;margin:7px 0 3px;color:#7fa8ca}
#${ROOT_ID} .actions{display:flex;gap:6px;margin-top:9px} #${ROOT_ID} .error{color:#ff9c9c;margin-top:6px}
#${ROOT_ID} .miniButton{background:#071331;color:#6ff5ff;border:1px solid #00d9ff;border-right:0;border-radius:2px 0 0 2px;padding:10px 9px;cursor:pointer;font-weight:900;text-shadow:0 0 8px #00eaff;box-shadow:0 0 13px #00d9ff66}
#${ROOT_ID} .resize{position:absolute;z-index:30;right:0;bottom:0;width:30px;height:30px;cursor:nwse-resize;touch-action:none;background:none}
#${ROOT_ID} .resize:after{content:"";position:absolute;right:3px;bottom:3px;width:11px;height:11px;border-right:2px solid #ff0033;border-bottom:2px solid #ff0033;box-shadow:3px 3px 7px #ff003399,inset -2px -2px 5px #ff003355;pointer-events:none}
#lmx-page-warning{position:fixed;z-index:999998;top:8px;left:50%;transform:translateX(-50%);background:#210005;color:#ff1744;border:2px solid #ff0033;border-radius:2px;padding:10px 16px;font:bold 14px Consolas,"Courier New",monospace;box-shadow:0 0 26px #ff003388,0 5px 20px #0009;letter-spacing:.5px;text-shadow:0 0 8px #ff0033}
@media(max-width:700px){#${ROOT_ID}{width:min(94vw,390px);right:3vw;top:12%;max-height:78vh}#${ROOT_ID} .head{min-height:44px}#${ROOT_ID} .resize{width:32px;height:32px}}
`;
document.head.appendChild(style);
}
function root() {
let panel = document.getElementById(ROOT_ID);
if (!panel) {
panel = document.createElement('div');
panel.id = ROOT_ID;
document.body.appendChild(panel);
}
return panel;
}
function applyPosition(panel) {
const saved = readJson(STORAGE.position);
if (!saved) return;
panel.style.left = Math.max(0, Math.min(window.innerWidth - 80, Number(saved.left) || 0)) + 'px';
panel.style.top = Math.max(0, Math.min(window.innerHeight - 50, Number(saved.top) || 0)) + 'px';
panel.style.right = 'auto';
}
function applySize(panel) {
const saved = readJson(STORAGE.size);
if (!saved) return;
panel.style.width = Math.max(280, Math.min(window.innerWidth - 20, Number(saved.width) || 390)) + 'px';
panel.style.maxHeight = Math.max(240, Math.min(window.innerHeight - 20, Number(saved.height) || 600)) + 'px';
panel.style.height = Math.max(240, Math.min(window.innerHeight - 20, Number(saved.height) || 600)) + 'px';
}
function setupUI() {
return `<div class="body"><div class="oc neutral">NETWORK OFFLINE // API KEY REQUIRED</div>
<div class="muted">Connect a Torn Minimal Access key. It stays in this browser and is sent only to Torn.</div>
<label>API key</label><input id="lmx-key" type="password" style="width:100%" autocomplete="off">
<div class="actions"><button id="lmx-save-key" class="primary">LINK NETWORK</button></div>
${apiKeyNotice ? `<div class="error">${esc(apiKeyNotice)}</div>` : ''}</div>`;
}
function settingsUI() {
const p = getProfile();
return `<div class="body"><div class="oc neutral">SYSTEM CONFIG // TARGET ZERO</div><div class="settings">
<label>Travel method</label><select id="lmx-method" style="width:100%">
<option value="standard" ${p.method === 'standard' ? 'selected' : ''}>Standard</option>
<option value="airstrip" ${p.method === 'airstrip' ? 'selected' : ''}>Private airstrip</option>
<option value="wlt" ${p.method === 'wlt' ? 'selected' : ''}>WLT benefit</option>
<option value="business" ${p.method === 'business' ? 'selected' : ''}>Business Class</option>
</select>
<label><input id="lmx-book" type="checkbox" ${p.mailingBook ? 'checked' : ''}> Mailing Yourself Abroad (-25%)</label>
<label>Travel capacity</label><input id="lmx-capacity" type="number" min="1" max="100" value="${p.capacity}" style="width:100%">
<label>OC flight warning (hours)</label><input id="lmx-warning" type="number" min="1" max="72" value="${warningHours()}" style="width:100%">
<div class="actions"><button id="lmx-save-settings" class="primary">COMMIT // RETURN</button></div>
<div class="actions"><button id="lmx-change-key" class="icon">Change API key</button></div>
<div class="muted" style="margin-top:8px">Target history is stored only in this browser for 120 days. Stock movement history is retained for 7 days.</div>
</div></div>`;
}
function hubUI() {
if (settingsOpen) return settingsUI();
const oc = ocState();
const rows = destinationRows();
const country = currentCountry();
const showLog = isAbroad() && country && rows.some(row => row.country === country);
return `<div class="body">
<div class="oc ${oc.kind}">${esc(oc.text)}</div>
${yataWarning ? `<div class="notice">${esc(yataWarning)}</div>` : ''}
${dataError ? `<div class="notice">${esc(dataError)}</div>` : ''}
${showLog ? `<div class="card"><div class="country">LOCAL SCAN // ${esc(country)}</div><div class="muted">Count players matching your own mugging rules.</div><div class="log"><input id="lmx-target-count" type="number" min="0" max="999" placeholder="Targets observed" style="flex:1"><button id="lmx-log-targets" class="primary">UPLOAD TRACE</button></div></div>` : ''}
<div class="cards">${rows.length ? rows.map(row => {
const purchase = row.stock.cost * getProfile().capacity;
const projected = row.projected === null ? 'Need trend data' : row.projected.toLocaleString();
const rate = row.trend.rate === null ? 'BUILDING SIGNAL MAP' : `${row.trend.rate.toFixed(1)}/hr · ${durationFromSeconds(row.trend.observedHours * 3600)} observed`;
return `<div class="card"><div class="cardTop"><div class="country">${esc(row.country)}</div><div class="heat ${row.trend.className}">${esc(row.trend.label)}</div></div>
<div class="grid"><span>FLIGHT RANGE</span><span class="value">${durationFromMinutes(row.flight)}</span><span>XANAX STOCK</span><span class="value">${row.stock.quantity.toLocaleString()}</span><span>ARRIVAL PROJECTION</span><span class="value">${projected}</span><span>XANAX ACTIVITY</span><span class="value">${rate}</span><span>LOADOUT CASH · ${getProfile().capacity}</span><span class="value">${money(purchase)}</span><span>INTEL AGE</span><span class="value">${ageText(row.stock.update)}</span></div>
<div class="targets">${esc(row.targets.text)}<div class="muted">${row.targets.samples} sample${row.targets.samples === 1 ? '' : 's'} · ${esc(row.targets.scope)} · arrival TCT window</div></div></div>`;
}).join('') : '<div class="card">No Xanax destinations in the current YATA report.</div>'}</div>
</div>`;
}
function render() {
if (!document.body) return;
injectStyle();
const panel = root();
if (!isTravelContext()) {
panel.className = 'hidden';
panel.innerHTML = '';
document.getElementById('lmx-page-warning')?.remove();
return;
}
if (localStorage.getItem(STORAGE.minimized) === '1') {
panel.className = 'mini';
panel.innerHTML = '<button id="lmx-restore" class="miniButton" title="TARGET//ZERO">Z//</button>';
panel.querySelector('#lmx-restore').onclick = () => {
localStorage.setItem(STORAGE.minimized, '0');
render();
};
updatePageWarning();
return;
}
panel.className = '';
panel.innerHTML = `<div class="head"><div class="title">${settingsOpen ? '⚙ SYSTEM CONFIG' : 'TARGET//ZERO'} <span class="muted">v${VERSION}</span></div>${settingsOpen ? '<button id="lmx-close-settings" class="icon" title="Return to network">←</button>' : '<button id="lmx-refresh" class="icon" title="Rescan network">↻</button><button id="lmx-open-settings" class="icon" title="System configuration">⚙</button>'}<button id="lmx-minimize" class="icon">—</button></div>${getKey() ? hubUI() : setupUI()}<div class="resize"></div>`;
applyPosition(panel);
applySize(panel);
bind(panel);
updatePageWarning();
}
function bind(panel) {
panel.querySelector('#lmx-minimize')?.addEventListener('click', () => {
localStorage.setItem(STORAGE.minimized, '1');
settingsOpen = false;
render();
});
panel.querySelector('#lmx-open-settings')?.addEventListener('click', () => {
settingsOpen = true;
render();
});
panel.querySelector('#lmx-close-settings')?.addEventListener('click', () => {
settingsOpen = false;
render();
});
panel.querySelector('#lmx-refresh')?.addEventListener('click', () => ensureData(true));
panel.querySelector('#lmx-save-key')?.addEventListener('click', async () => {
const candidate = String(panel.querySelector('#lmx-key')?.value || '').trim();
if (!candidate) return;
dataError = 'Checking key...'; render();
try {
await rawApi('/user/organizedcrime', candidate);
setKey(candidate); dataError = ''; await ensureData(true);
} catch (error) { dataError = String(error?.message || error); render(); }
});
panel.querySelector('#lmx-change-key')?.addEventListener('click', () => {
localStorage.removeItem(STORAGE.key);
cacheSet(STORAGE.oc, null);
settingsOpen = false;
render();
});
panel.querySelector('#lmx-save-settings')?.addEventListener('click', async () => {
saveProfile({
method: panel.querySelector('#lmx-method')?.value || 'standard',
mailingBook: Boolean(panel.querySelector('#lmx-book')?.checked),
capacity: Math.max(1, Math.min(100, Number(panel.querySelector('#lmx-capacity')?.value) || 28)),
businessTicketsFree: false
});
localStorage.setItem(STORAGE.warningHours, String(Math.max(1, Math.min(72, Number(panel.querySelector('#lmx-warning')?.value) || 12))));
settingsOpen = false;
render();
});
panel.querySelector('#lmx-log-targets')?.addEventListener('click', () => {
const input = panel.querySelector('#lmx-target-count');
const country = currentCountry();
if (input && input.value !== '' && saveTargetLog(country, input.value)) {
render();
}
});
const head = panel.querySelector('.head');
head?.addEventListener('pointerdown', event => {
if (event.target.closest('button')) return;
event.preventDefault();
const rect = panel.getBoundingClientRect();
dragState = { pointerId: event.pointerId, x: event.clientX, y: event.clientY, left: rect.left, top: rect.top };
head.setPointerCapture?.(event.pointerId);
});
panel.querySelector('.resize')?.addEventListener('pointerdown', event => {
event.preventDefault(); event.stopPropagation();
const rect = panel.getBoundingClientRect();
resizeState = { pointerId: event.pointerId, x: event.clientX, y: event.clientY, width: rect.width, height: rect.height };
panel.style.height = rect.height + 'px';
event.target.setPointerCapture?.(event.pointerId);
});
}
function updatePageWarning() {
let banner = document.getElementById('lmx-page-warning');
const state = ocState();
const shouldShow = getKey() && isTravelAgency() && state.kind === 'danger';
if (!shouldShow) { banner?.remove(); return; }
if (!banner) {
banner = document.createElement('div');
banner.id = 'lmx-page-warning';
document.body.appendChild(banner);
}
banner.textContent = '⚠ ' + state.text;
}
function likelyTravelControl(target) {
if (!isTravelAgency()) return false;
const clickable = target.closest('button,a,[role="button"],input[type="submit"]');
if (!clickable || clickable.closest('#' + ROOT_ID)) return false;
const text = String(clickable.textContent || clickable.value || '').toLowerCase();
return /travel|fly|depart|purchase/.test(text) || Object.keys(TRAVEL).some(country => text.includes(country.toLowerCase()));
}
function handleTravelClick(event) {
if (Date.now() < confirmBypassUntil || !likelyTravelControl(event.target)) return;
const state = ocState();
if (state.kind !== 'danger') return;
const proceed = window.confirm(`⚠ ${state.text}\n\nTravel anyway?`);
if (!proceed) {
event.preventDefault(); event.stopImmediatePropagation();
} else {
confirmBypassUntil = Date.now() + 3000;
}
}
async function ensureData(force = false) {
if (dataBusy) return;
dataBusy = true;
dataError = '';
try {
const tasks = [getYata(force)];
if (getKey()) tasks.push(refreshOc(force));
await Promise.all(tasks);
} catch (error) {
dataError = String(error?.message || error);
console.error(APP, error);
} finally {
dataBusy = false;
lastSignature = '';
render();
}
}
function signature() {
return JSON.stringify({
href: location.href,
minute: Math.floor(Date.now() / 60000),
minimized: localStorage.getItem(STORAGE.minimized),
yata: readJson(STORAGE.yata)?.updated || 0,
oc: readJson(STORAGE.oc)?.updated || 0,
targets: (readJson(STORAGE.targetLogs, [])).length,
profile: getProfile()
});
}
function tick() {
const minute = Math.floor(Date.now() / 60000);
const current = signature();
if (current !== lastSignature && !root().contains(document.activeElement)) {
lastSignature = current;
lastRenderedMinute = minute;
render();
}
const yataStale = !cacheIsFresh(STORAGE.yata, YATA_TTL);
const ocStale = getKey() && !cacheIsFresh(STORAGE.oc, OC_TTL);
if (isTravelContext() && (yataStale || ocStale) && !dataBusy) ensureData(false);
if (minute !== lastRenderedMinute) updatePageWarning();
}
document.addEventListener('pointermove', event => {
const panel = document.getElementById(ROOT_ID);
if (!panel) return;
if (dragState && event.pointerId === dragState.pointerId) {
event.preventDefault();
const left = Math.max(0, Math.min(window.innerWidth - 80, dragState.left + event.clientX - dragState.x));
const top = Math.max(0, Math.min(window.innerHeight - 50, dragState.top + event.clientY - dragState.y));
panel.style.left = left + 'px'; panel.style.top = top + 'px'; panel.style.right = 'auto';
}
if (resizeState && event.pointerId === resizeState.pointerId) {
event.preventDefault();
panel.style.width = Math.max(280, Math.min(window.innerWidth - 20, resizeState.width + event.clientX - resizeState.x)) + 'px';
const height = Math.max(240, Math.min(window.innerHeight - 20, resizeState.height + event.clientY - resizeState.y));
panel.style.height = height + 'px';
panel.style.maxHeight = height + 'px';
}
}, { passive: false });
document.addEventListener('pointerup', event => {
const panel = document.getElementById(ROOT_ID);
if (panel && dragState && event.pointerId === dragState.pointerId) {
writeJson(STORAGE.position, { left: panel.offsetLeft, top: panel.offsetTop });
dragState = null;
}
if (panel && resizeState && event.pointerId === resizeState.pointerId) {
writeJson(STORAGE.size, { width: panel.offsetWidth, height: panel.getBoundingClientRect().height });
resizeState = null;
}
});
document.addEventListener('click', handleTravelClick, true);
function start() {
if (!document.body || !document.head) { setTimeout(start, 100); return; }
render();
if (isTravelContext()) ensureData(false);
lastSignature = signature();
setInterval(tick, WATCH_MS);
}
start();
})();