War Roster · Chain Targets · Operator Dashboard — Torn
// ==UserScript==
// @name 🎯 Target Tracker
// @namespace https://osdevscape.com
// @version 8.8.2
// @author Phillip_J_Fry (OSMays8338) — OSDevscape
// @license All Rights Reserved © 2026 OSDevscape
// @homepageURL https://greasyfork.org/en/scripts/568658-war-target-tracker
// @supportURL https://greasyfork.org/en/scripts/568658-war-target-tracker/feedback
// @description War Roster · Chain Targets · Operator Dashboard — Torn
// @match https://www.torn.com/*
// @grant GM_getValue
// @grant GM_setValue
// @grant GM_xmlhttpRequest
// @grant unsafeWindow
// @run-at document-end
// ==/UserScript==
// ╔══════════════════════════════════════════════════════════════╗
// ║ 🎯 TARGET TRACKER v8.8.2 ║
// ║ ║
// ║ Author : Phillip_J_Fry (Torn) · OSMays8338 (Greasyfork) ║
// ║ Company : OSDevscape ║
// ║ License : All Rights Reserved © 2026 OSDevscape ║
// ║ Script : https://greasyfork.org/en/scripts/568658 ║
// ╚══════════════════════════════════════════════════════════════╝
(function () {
'use strict';
/* ─────────────────────────────────────────
STORAGE HELPERS
Dual-layer: localStorage (PDA + all browsers)
with GM_setValue/getValue fallback (Tampermonkey PC)
───────────────────────────────────────── */
const store = {
get: function(k, d) {
// Try localStorage first (works in PDA and browsers)
try {
var v = localStorage.getItem('wtt_' + k);
if (v !== null) return v;
} catch(e) {}
// Fallback to GM_getValue (Tampermonkey)
try { return GM_getValue(k, d); } catch(e) {}
return d;
},
set: function(k, v) {
// Write to both so either environment can read it
try { localStorage.setItem('wtt_' + k, v); } catch(e) {}
try { GM_setValue(k, v); } catch(e) {}
},
getJSON: function(k, d) {
try {
var v = localStorage.getItem('wtt_' + 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: function(k, v) {
var s = JSON.stringify(v);
try { localStorage.setItem('wtt_' + k, s); } catch(e) {}
try { GM_setValue(k, s); } catch(e) {}
}
};
/* ─────────────────────────────────────────
CONFIG
───────────────────────────────────────── */
const cfg = {
get apiKey() { return store.get('wtt_api', ''); },
set apiKey(v) { store.set('wtt_api', v); },
get factionId() { return store.get('wtt_fac', ''); },
set factionId(v){ store.set('wtt_fac', v); },
get userId() { return store.get('wtt_uid', ''); },
set userId(v) { store.set('wtt_uid', v); },
getChainIds() { return store.getJSON('wtt_chain', []); },
setChainIds(a) { store.setJSON('wtt_chain', a); },
get refillDest() { return store.get('wtt_refill_dest', 'inventory'); },
set refillDest(v) { store.set('wtt_refill_dest', v); },
// Chain Watcher
get cwEnabled() { return store.get('wtt_cw_on', '0') === '1'; },
set cwEnabled(v) { store.set('wtt_cw_on', v ? '1' : '0'); },
// threshold bitmask stored as JSON array of seconds: [240,180,120,60,30,10]
getCwThresholds() { return store.getJSON('wtt_cw_thresh', [60, 30, 10]); },
setCwThresholds(a){ store.setJSON('wtt_cw_thresh', a); }
};
/* ─────────────────────────────────────────
STATE
───────────────────────────────────────── */
let toggleEl = null;
let panelEl = null;
let panelOpen = false;
let activeTab = 'dash'; // dash | war | chain
let activeFilter = 'all';
let warRoster = [];
let enemyRoster = []; // enemy faction members fetched via war data
let chainCache = [];
let selfData = null;
let factionChain = null;
let isLoading = false;
let regenTimer = null;
let nerveTimer = null;
let pollTimer = null;
let selfTimer = null; // fast 30s self/bars refresh
let chainTimer = null; // 90s chain targets refresh
let warsTimer = null; // 120s war score refresh
let lastWarsFetch = 0; // unix ms of last v2/wars call
let notifGranted = false;
let prevHospStatus = {}; // id -> bool, for hosp-out notifications
let warScore = null; // { us, them, estRespect }
let enemyData = null; // { name, respect, id }
let ownFactionData = null; // { name, respect, rank, bestChain, memberCount, wins, losses }
let ownFactionId = null; // derived from selfData.faction.faction_id
/* ── Chain Watcher state ── */
let cwEnabled = false; // master toggle (from settings)
let cwData = null; // { current, timeout, cooldown } from last API fetch
let cwSecsLeft = 0; // local countdown (decrements every 1s)
let cwTickTimer = null; // 1s setInterval handle
let cwPollTimer = null; // 60s resync setInterval handle
let cwAlerted = {}; // { 240:true, 180:true, ... } — fired thresholds this chain
let cwCooldown = false; // true when in cooldown mode
let cwCount = 0; // current chain hit count
/* ─────────────────────────────────────────
COLOUR PALETTE (used inline everywhere)
───────────────────────────────────────── */
const C = {
bg: '#0e0303',
bg2: '#160505',
border: 'rgba(160,20,20,0.45)',
red: '#dd4444',
redDim: 'rgba(220,60,60,0.7)',
text: '#d4d4d4',
textDim: 'rgba(180,180,180,0.55)',
hosp: '#ff5555',
travel: '#f0b030',
jail: '#78a8ff',
okay: '#66bb66',
mono: '"Share Tech Mono",Consolas,monospace',
sans: 'Rajdhani,"Segoe UI",Arial,sans-serif'
};
/* ─────────────────────────────────────────
INJECT STYLESHEET (non-critical styles only)
Settings + toast use ONLY inline styles
───────────────────────────────────────── */
function injectCSS() {
if (document.getElementById('wtt-css')) return;
const s = document.createElement('style');
s.id = 'wtt-css';
// Only safe, non-import CSS. No @import, no external resources.
s.textContent = `
#wtt-toggle { position:fixed; z-index:999990; user-select:none; touch-action:none; cursor:grab; }
#wtt-toggle:active { cursor:grabbing; }
#wtt-panel { position:fixed; z-index:999989; overflow:hidden; display:flex; flex-direction:column; }
#wtt-panel .wtt-body { overflow-y:auto; overflow-x:hidden; flex:1; scrollbar-width:thin; scrollbar-color:rgba(160,20,20,0.4) transparent; touch-action:pan-y; -webkit-overflow-scrolling:touch; }
#wtt-panel .wtt-body::-webkit-scrollbar { width:3px; }
#wtt-panel .wtt-body::-webkit-scrollbar-thumb { background:rgba(160,20,20,0.5); border-radius:2px; }
#wtt-panel a { text-decoration:none !important; color:inherit; }
#wtt-panel a:hover { text-decoration:none !important; }
@keyframes wtt-pulseRed { 0%,100%{box-shadow:0 0 0 0 rgba(220,50,50,0.55)} 50%{box-shadow:0 0 0 5px rgba(220,50,50,0)} }
@keyframes wtt-pulseAmb { 0%,100%{box-shadow:0 0 0 0 rgba(230,160,30,0.45)} 50%{box-shadow:0 0 0 5px rgba(230,160,30,0)} }
@keyframes wtt-spin { to{transform:rotate(360deg)} }
@keyframes wtt-blink { 0%,100%{opacity:1} 50%{opacity:0.35} }
.wtt-pulse-red { animation:wtt-pulseRed 2s ease-in-out infinite; }
.wtt-pulse-amb { animation:wtt-pulseAmb 2s ease-in-out infinite; }
.wtt-spin-anim { animation:wtt-spin 0.8s linear infinite; display:inline-block; }
.wtt-blink { animation:wtt-blink 2.2s ease-in-out infinite; }
@keyframes wtt-urgentBlink { 0%,100%{opacity:1;background:rgba(200,30,30,0.18)} 50%{opacity:0.6;background:rgba(200,30,30,0.04)} }
.wtt-urgent { animation:wtt-urgentBlink 0.9s ease-in-out infinite; }
`;
(document.head || document.documentElement).appendChild(s);
}
/* ─────────────────────────────────────────
TOAST — 100% inline, cannot be hidden
───────────────────────────────────────── */
function toast(msg, dur) {
dur = dur || 3000;
const old = document.getElementById('wtt-toast');
if (old) old.remove();
const el = document.createElement('div');
el.id = 'wtt-toast';
el.textContent = msg;
el.setAttribute('style',
'position:fixed !important;' +
'top:18px !important;' +
'left:50% !important;' +
'transform:translateX(-50%) !important;' +
'background:#5a0808 !important;' +
'border:1px solid #cc3333 !important;' +
'border-radius:8px !important;' +
'padding:10px 20px !important;' +
'color:#ffaaaa !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(function() {
el.style.setProperty('opacity', '0', 'important');
el.style.setProperty('transition', 'opacity 0.3s', 'important');
setTimeout(function() { el.remove(); }, 320);
}, dur);
}
/* ─────────────────────────────────────────
SETTINGS POPUP — 100% inline, CANNOT be
hidden or overridden by page CSS
───────────────────────────────────────── */
function openSettings() {
var existing = document.getElementById('wtt-settings-wrap');
if (existing) { existing.remove(); return; }
/* ── helpers ── */
function el(tag, css, extra) {
var e = document.createElement(tag);
if (css) e.setAttribute('style', css);
if (extra) Object.assign(e, extra);
return e;
}
function IS(css) { return css + ' !important;'; } // shorthand: wrap value in !important
var ISTYLE = {
wrap: 'position:fixed;top:0;left:0;width:100%;height:100%;background:rgba(0,0,0,0.9);z-index:2147483647;display:flex;align-items:flex-start;justify-content:center;padding-top:5vh;box-sizing:border-box;overflow-y:auto;',
box: 'background:#150303;border:2px solid #aa2222;border-radius:12px;width:360px;max-width:94vw;color:#d4d4d4;font-family:Arial,sans-serif;overflow:hidden;box-shadow:0 16px 60px rgba(0,0,0,0.95);margin-bottom:20px;',
hdr: 'background:#2a0808;padding:13px 18px;font-size:12px;font-weight:700;letter-spacing:1.4px;color:#ee5555;border-bottom:1px solid #551111;',
body: 'padding:16px;',
note: 'background:#2a1500;border-left:3px solid #cc8800;padding:9px 12px;border-radius:4px;font-size:11px;color:#ddaa55;margin-bottom:16px;line-height:1.5;',
secHdr:'font-size:9px;font-weight:700;letter-spacing:1.2px;text-transform:uppercase;color:rgba(200,80,80,0.7);padding-bottom:5px;margin:16px 0 8px;border-bottom:1px solid rgba(130,16,16,0.3);font-family:Consolas,monospace;',
lbl: 'display:block;margin-bottom:5px;font-size:10px;font-weight:700;letter-spacing:1px;text-transform:uppercase;color:#cc8888;',
inp: 'display:block;width:100%;box-sizing:border-box;padding:9px 11px;background:#0e0202;border:1px solid #661111;border-radius:5px;color:#dddddd;font-size:13px;font-family:Consolas,monospace;outline:none;',
hint: 'font-size:9px;color:rgba(160,100,100,0.65);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 rgba(180,40,40,0.5);font-family:Arial,sans-serif;letter-spacing:0.5px;'
};
function addImportant(css) {
return css.split(';').filter(Boolean).map(function(r){ return r.trim() + ' !important'; }).join(';') + ';';
}
function applyStyle(element, key) {
element.setAttribute('style', addImportant(ISTYLE[key]));
}
/* ── structure ── */
var wrap = el('div'); wrap.id = 'wtt-settings-wrap'; applyStyle(wrap, 'wrap');
var box = el('div'); applyStyle(box, 'box');
var hdr = el('div'); hdr.textContent = '⚙ TARGET TRACKER — Settings'; applyStyle(hdr, 'hdr');
var body = el('div'); applyStyle(body, 'body');
var note = el('div'); note.innerHTML = '⚠ Requires a <b>Full Access</b> API key for all features.'; applyStyle(note, 'note');
/* ── field builder ── */
function field(labelText, id, placeholder, value, hint, maxLen, isSecret) {
var g = el('div'); g.setAttribute('style', 'margin-bottom:12px !important;');
var lbl = el('label'); lbl.textContent = labelText; applyStyle(lbl, 'lbl');
// Create input — set type via setAttribute so webviews honour it
var inp = document.createElement('input');
inp.setAttribute('type', isSecret ? 'password' : 'text');
inp.id = id; inp.placeholder = placeholder; inp.value = value || '';
applyStyle(inp, 'inp');
if (maxLen) inp.maxLength = maxLen;
inp.addEventListener('focus', function(){ inp.style.setProperty('border-color','#cc2222','important'); });
inp.addEventListener('blur', function(){ inp.style.setProperty('border-color','#661111','important'); });
if (isSecret) {
var row = document.createElement('div');
row.setAttribute('style','display:flex !important;align-items:center !important;gap:6px !important;');
var tog = document.createElement('div');
tog.textContent = '👁';
tog.setAttribute('style','cursor:pointer !important;font-size:14px !important;flex-shrink:0 !important;opacity:0.45 !important;user-select:none !important;padding:2px !important;');
tog.addEventListener('click', function(){
var 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);
var h = el('div'); h.textContent = hint; applyStyle(h, 'hint');
g.appendChild(lbl); g.appendChild(row); g.appendChild(h);
} else {
var h = el('div'); h.textContent = hint; applyStyle(h, 'hint');
g.appendChild(lbl); g.appendChild(inp); g.appendChild(h);
}
return g;
}
/* ── Section: Credentials ── */
var credHdr = el('div'); credHdr.textContent = '🔑 Credentials'; applyStyle(credHdr, 'secHdr');
var fAPI = field('API Key', 'wtt-si-api', 'Enter your Torn API key', cfg.apiKey, 'Torn → Settings → API → Full Access', null, true);
// "Create API key" helper link
var apiKeyHint = el('div');
apiKeyHint.setAttribute('style', addImportant(
'margin-top:-8px;margin-bottom:12px;font-size:10px;font-family:Arial,sans-serif;' +
'color:rgba(180,130,60,0.85);line-height:1.5;'
));
apiKeyHint.innerHTML = '🔑 Don\'t have a key? ';
var apiKeyLink = document.createElement('a');
apiKeyLink.href = 'https://www.torn.com/preferences.php#tab=api?step=addNewKey&title=WarTargetTracker&access_level=4';
apiKeyLink.textContent = 'Create Full Access key on Torn';
apiKeyLink.setAttribute('style', addImportant(
'color:rgba(220,160,60,0.9);font-weight:700;text-decoration:underline;cursor:pointer;'
));
apiKeyHint.appendChild(apiKeyLink);
var apiNameNote = el('div');
apiNameNote.setAttribute('style', addImportant(
'font-size:9px;font-family:Consolas,monospace;color:rgba(140,100,50,0.7);margin-top:2px;'
));
apiNameNote.textContent = 'The key will be named "WarTargetTracker" automatically.';
apiKeyHint.appendChild(document.createElement('br'));
apiKeyHint.appendChild(apiNameNote);
var fUID = field('Your User ID', 'wtt-si-uid', 'Enter your Torn player ID', cfg.userId, 'Used for dashboard health stats');
var fFac = field('Your Faction ID', 'wtt-si-fac', 'e.g. 37155', cfg.factionId, 'YOUR faction ID (not the enemy). Used to pull your roster, chain, and war data.');
/* ── Save / Cancel ── */
var btnRow = el('div'); applyStyle(btnRow, 'btnRow');
var btnCancel = el('button', null, {type:'button', textContent:'Cancel'});
var btnSave = el('button', null, {type:'button', textContent:'Save & Refresh'});
btnCancel.setAttribute('style', addImportant(ISTYLE.btn + 'background:#1a0505;color:#cc8888;'));
btnSave.setAttribute('style', addImportant(ISTYLE.btn + 'background:#660f0f;color:#ffaaaa;'));
btnRow.appendChild(btnCancel); btnRow.appendChild(btnSave);
/* ── Assemble ── */
/* ── Chain Targets section ── */
var chainActHdr = el('div'); chainActHdr.textContent = '🔗 Chain Targets'; applyStyle(chainActHdr, 'secHdr');
// Scrollable remove list
var chainListBox = el('div');
chainListBox.setAttribute('style', addImportant(
'background:#0a0101;border:1px solid #3a0808;border-radius:6px;' +
'max-height:140px;overflow-y:auto;margin-bottom:8px;'
));
function rebuildSettingsChainList() {
chainListBox.innerHTML = '';
var ids = cfg.getChainIds();
if (!ids.length) {
var emp = el('div');
emp.textContent = 'No targets tracked';
emp.setAttribute('style', addImportant(
'padding:10px;text-align:center;font-size:11px;' +
'color:rgba(150,90,90,0.55);font-family:Arial,sans-serif;'
));
chainListBox.appendChild(emp);
return;
}
ids.forEach(function(id) {
var cached = chainCache.find(function(p){ return p.id === id; });
var label = cached ? cached.name : 'ID ' + id;
var status = cached ? ' · ' + cached.status.sub : '';
var sRow = el('div');
sRow.setAttribute('style', addImportant(
'display:flex;align-items:center;justify-content:space-between;' +
'padding:5px 10px;border-bottom:1px solid rgba(60,8,8,0.3);'
));
var sName = el('div');
sName.textContent = label + status;
sName.setAttribute('style', addImportant(
'font-size:11px;font-family:Consolas,monospace;color:#cccccc;' +
'flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;'
));
var sRm = el('div');
sRm.textContent = '✕ Remove';
sRm.setAttribute('style', addImportant(
'font-size:10px;font-weight:700;font-family:Arial,sans-serif;' +
'color:rgba(220,80,80,0.7);cursor:pointer;flex-shrink:0;padding-left:10px;'
));
sRm.addEventListener('click', function() {
var newIds = cfg.getChainIds().filter(function(x){ return x !== id; });
cfg.setChainIds(newIds);
chainCache = chainCache.filter(function(p){ return p.id !== id; });
rebuildSettingsChainList();
if (activeTab === 'chain' && panelEl) renderChainBody();
toast('✓ Removed from chain list');
});
sRow.appendChild(sName); sRow.appendChild(sRm);
chainListBox.appendChild(sRow);
});
}
rebuildSettingsChainList();
// Export + Clear buttons
var chainActRow = el('div');
chainActRow.setAttribute('style', addImportant('display:flex;gap:8px;'));
function settingsActBtn(label, bg, col) {
var b = el('button', null, {type:'button', textContent:label});
b.setAttribute('style', addImportant('flex:1;padding:9px 0;border-radius:6px;font-size:11px;font-weight:700;cursor:pointer;font-family:Arial,sans-serif;background:' + bg + ';color:' + col + ';border:1px solid ' + col + ';'));
return b;
}
var sExpBtn = settingsActBtn('📋 Export', 'rgba(10,30,100,0.3)', 'rgba(96,144,223,0.9)');
var sClearBtn = settingsActBtn('✕ Clear All', 'rgba(80,8,8,0.3)', 'rgba(200,80,80,0.85)');
sExpBtn.addEventListener('click', function() { exportChain(); });
sClearBtn.addEventListener('click', function() {
var ids = cfg.getChainIds();
if (!ids.length) { toast('Chain list is already empty'); return; }
if (!confirm('Clear all ' + ids.length + ' chain targets?')) return;
cfg.setChainIds([]); chainCache = [];
rebuildSettingsChainList();
if (activeTab === 'chain' && panelEl) renderChainBody();
toast('✓ Chain list cleared');
});
chainActRow.appendChild(sExpBtn); chainActRow.appendChild(sClearBtn);
/* ── Section: Preferences ── */
var prefHdr = el('div'); prefHdr.textContent = '💡 Preferences'; applyStyle(prefHdr, 'secHdr');
var tooltipRow = document.createElement('div');
tooltipRow.setAttribute('style',
'display:flex !important;align-items:center !important;justify-content:space-between !important;' +
'padding:8px 10px !important;background:rgba(255,255,255,0.03) !important;' +
'border:1px solid rgba(130,16,16,0.25) !important;border-radius:6px !important;margin-bottom:4px !important;'
);
var tooltipLabel = document.createElement('div');
tooltipLabel.setAttribute('style','font-size:11px !important;color:rgba(210,210,210,0.85) !important;');
tooltipLabel.textContent = '🎠 Show tips carousel on next load';
// Read current state
var SEEN_KEY = 'wtt_tooltip_seen';
var isSeen = false;
try { isSeen = !!localStorage.getItem(SEEN_KEY); } catch(e) {}
// Toggle pill
var togPill = document.createElement('div');
var togKnob = document.createElement('div');
function setPillState(active) {
togPill.setAttribute('style',
'width:36px !important;height:20px !important;border-radius:10px !important;' +
'cursor:pointer !important;flex-shrink:0 !important;position:relative !important;' +
'transition:background 0.2s !important;user-select:none !important;' +
'background:' + (active ? 'rgba(60,180,60,0.7)' : 'rgba(100,30,30,0.5)') + ' !important;' +
'border:1px solid ' + (active ? 'rgba(80,200,80,0.5)' : 'rgba(180,40,40,0.35)') + ' !important;'
);
togKnob.setAttribute('style',
'position:absolute !important;top:2px !important;' +
(active ? 'left:17px' : 'left:2px') + ' !important;' +
'width:14px !important;height:14px !important;border-radius:50% !important;' +
'background:#ffffff !important;transition:left 0.2s !important;'
);
}
// active = tooltip will show (seen flag cleared)
var pillActive = false; // default: don't show again
setPillState(pillActive);
togPill.appendChild(togKnob);
togPill.addEventListener('click', function() {
pillActive = !pillActive;
setPillState(pillActive);
if (pillActive) {
try { localStorage.removeItem(SEEN_KEY); } catch(e) {}
} else {
try { localStorage.setItem(SEEN_KEY, '1'); } catch(e) {}
}
});
tooltipRow.appendChild(tooltipLabel);
tooltipRow.appendChild(togPill);
// ── Refill destination ──
var refillHdr = el('div'); refillHdr.textContent = '⚡ Energy Refill Destination'; applyStyle(refillHdr, 'secHdr');
var curRefillDest = cfg.refillDest || 'inventory';
var refillRow = document.createElement('div');
refillRow.setAttribute('style', 'display:flex !important;gap:6px !important;');
function makeRefillBtn(label, val) {
var b = el('button', null, { type: 'button', textContent: label });
var active = curRefillDest === val;
b.setAttribute('style', addImportant(
'flex:1;padding:9px 0;border-radius:6px;font-size:11px;font-weight:700;cursor:pointer;font-family:Arial,sans-serif;' +
(active ? 'background:rgba(140,16,16,0.55);color:#ffaaaa;border:1px solid rgba(200,40,40,0.6);'
: 'background:#1a0505;color:#cc8888;border:1px solid rgba(130,16,16,0.3);')
));
b.addEventListener('click', function() {
curRefillDest = val; cfg.refillDest = val;
refillRow.querySelectorAll('button').forEach(function(x) {
var isActive = x === b;
x.setAttribute('style', addImportant(
'flex:1;padding:9px 0;border-radius:6px;font-size:11px;font-weight:700;cursor:pointer;font-family:Arial,sans-serif;' +
(isActive ? 'background:rgba(140,16,16,0.55);color:#ffaaaa;border:1px solid rgba(200,40,40,0.6);'
: 'background:#1a0505;color:#cc8888;border:1px solid rgba(130,16,16,0.3);')
));
});
toast('✓ Refill: ' + label);
});
return b;
}
refillRow.appendChild(makeRefillBtn('📦 Inventory', 'inventory'));
refillRow.appendChild(makeRefillBtn('🏢 Points Building', 'points'));
body.appendChild(note);
body.appendChild(credHdr);
body.appendChild(fAPI);
body.appendChild(apiKeyHint);
body.appendChild(fUID);
body.appendChild(fFac);
body.appendChild(chainActHdr);
body.appendChild(chainListBox);
body.appendChild(chainActRow);
body.appendChild(prefHdr);
body.appendChild(tooltipRow);
body.appendChild(refillHdr);
body.appendChild(refillRow);
/* ── Section: Chain Watcher ── */
var cwHdr = el('div'); cwHdr.textContent = '⛓ Chain Watcher'; applyStyle(cwHdr, 'secHdr');
// Master on/off toggle row
var cwTogRow = document.createElement('div');
cwTogRow.setAttribute('style',
'display:flex !important;align-items:center !important;justify-content:space-between !important;' +
'padding:8px 10px !important;background:rgba(255,255,255,0.03) !important;' +
'border:1px solid rgba(130,16,16,0.25) !important;border-radius:6px !important;margin-bottom:8px !important;'
);
var cwTogLabel = document.createElement('div');
cwTogLabel.setAttribute('style','font-size:11px !important;color:rgba(210,210,210,0.85) !important;line-height:1.4 !important;');
cwTogLabel.innerHTML = '⛓ Live chain countdown & alerts<br><span style="font-size:9px;color:rgba(150,100,100,0.65);font-family:Consolas,monospace;">Faction API · auto-syncs every 60s</span>';
var cwPill = document.createElement('div');
var cwKnob = document.createElement('div');
var cwActive = cfg.cwEnabled;
function setCwPill(on) {
cwPill.setAttribute('style',
'width:36px !important;height:20px !important;border-radius:10px !important;' +
'cursor:pointer !important;flex-shrink:0 !important;position:relative !important;' +
'transition:background 0.2s !important;user-select:none !important;' +
'background:' + (on ? 'rgba(60,180,60,0.7)' : 'rgba(100,30,30,0.5)') + ' !important;' +
'border:1px solid ' + (on ? 'rgba(80,200,80,0.5)' : 'rgba(180,40,40,0.35)') + ' !important;'
);
cwKnob.setAttribute('style',
'position:absolute !important;top:2px !important;' +
(on ? 'left:17px' : 'left:2px') + ' !important;' +
'width:14px !important;height:14px !important;border-radius:50% !important;' +
'background:#ffffff !important;transition:left 0.2s !important;'
);
}
setCwPill(cwActive);
cwPill.appendChild(cwKnob);
cwPill.addEventListener('click', function() {
cwActive = !cwActive;
setCwPill(cwActive);
cfg.cwEnabled = cwActive;
cwEnabled = cwActive;
if (cwActive) { cwStart(); } else { cwStop(); }
toast(cwActive ? '⛓ Chain Watcher ON' : '⛓ Chain Watcher OFF');
});
cwTogRow.appendChild(cwTogLabel); cwTogRow.appendChild(cwPill);
// Audio alert thresholds — checkboxes
var cwThreshHdr = document.createElement('div');
cwThreshHdr.setAttribute('style', addImportant(
'font-size:9px;font-weight:700;letter-spacing:1px;text-transform:uppercase;' +
'color:rgba(180,100,100,0.65);margin:8px 0 6px;font-family:Consolas,monospace;'
));
cwThreshHdr.textContent = 'Audio alerts at:';
var cwThreshBox = document.createElement('div');
cwThreshBox.setAttribute('style', addImportant(
'display:flex;flex-wrap:wrap;gap:6px;padding:8px 10px;' +
'background:rgba(255,255,255,0.02);border:1px solid rgba(100,16,16,0.25);border-radius:6px;'
));
var savedThresh = cfg.getCwThresholds();
var CW_T_LABELS = [{secs:240,label:'4:00'},{secs:180,label:'3:00'},{secs:120,label:'2:00'},{secs:60,label:'1:00'},{secs:30,label:'0:30'},{secs:10,label:'0:10'}];
CW_T_LABELS.forEach(function(t) {
var checked = savedThresh.indexOf(t.secs) !== -1;
var item = document.createElement('div');
item.setAttribute('style', addImportant(
'display:flex;align-items:center;gap:5px;cursor:pointer;' +
'padding:4px 8px;border-radius:4px;' +
'border:1px solid ' + (checked ? 'rgba(200,80,80,0.5)' : 'rgba(80,30,30,0.3)') + ';' +
'background:' + (checked ? 'rgba(140,16,16,0.25)' : 'rgba(20,5,5,0.3)') + ';' +
'transition:all 0.15s;'
));
var cb = document.createElement('input');
cb.type = 'checkbox'; cb.checked = checked;
cb.setAttribute('style', addImportant('cursor:pointer;accent-color:#cc3333;width:13px;height:13px;margin:0;'));
var lbl = document.createElement('span');
lbl.textContent = t.secs <= 30 ? '⏱ ' + t.label : t.label;
lbl.setAttribute('style', addImportant(
'font-size:11px;font-family:Consolas,monospace;font-weight:700;' +
'color:' + (t.secs <= 30 ? '#ff7744' : t.secs <= 60 ? '#f0b030' : 'rgba(200,200,200,0.8)') + ';'
));
function updateItem(isOn) {
item.setAttribute('style', addImportant(
'display:flex;align-items:center;gap:5px;cursor:pointer;' +
'padding:4px 8px;border-radius:4px;' +
'border:1px solid ' + (isOn ? 'rgba(200,80,80,0.5)' : 'rgba(80,30,30,0.3)') + ';' +
'background:' + (isOn ? 'rgba(140,16,16,0.25)' : 'rgba(20,5,5,0.3)') + ';' +
'transition:all 0.15s;'
));
}
item.addEventListener('click', function() {
cb.checked = !cb.checked;
updateItem(cb.checked);
var cur = cfg.getCwThresholds();
if (cb.checked) { if (cur.indexOf(t.secs) === -1) cur.push(t.secs); }
else { cur = cur.filter(function(x){ return x !== t.secs; }); }
cfg.setCwThresholds(cur);
});
item.appendChild(cb); item.appendChild(lbl);
cwThreshBox.appendChild(item);
});
body.appendChild(cwHdr);
body.appendChild(cwTogRow);
body.appendChild(cwThreshHdr);
body.appendChild(cwThreshBox);
body.appendChild(btnRow);
box.appendChild(hdr); box.appendChild(body);
wrap.appendChild(box);
document.body.appendChild(wrap);
setTimeout(function(){ var i = document.getElementById('wtt-si-api'); if(i) i.focus(); }, 50);
function doSave() {
var key = (document.getElementById('wtt-si-api').value || '').trim();
var uid = (document.getElementById('wtt-si-uid').value || '').trim();
var fac = (document.getElementById('wtt-si-fac').value || '').trim();
if (!key) { toast('⚠ API key is required'); return; }
console.log('[TargetTracker] Saving: key=' + key.substring(0,4) + '*** uid=' + uid + ' fac=' + fac);
cfg.apiKey = key; cfg.userId = uid; cfg.factionId = fac;
// Verify it actually saved
var saved = cfg.apiKey;
console.log('[TargetTracker] Verified saved key starts with: ' + (saved ? saved.substring(0,4) : 'EMPTY'));
if (!saved) { toast('⚠ Save failed — storage unavailable'); return; }
wrap.remove();
toast('✓ Saved! Key: ' + key.substring(0,4) + '***');
selfData = null; warRoster = []; enemyRoster = []; factionChain = null; enemyData = null; ownFactionData = null; warScore = null;
if (panelEl) renderPanel();
stopPolling();
// Restart chain watcher if enabled
cwStop();
cwEnabled = cfg.cwEnabled;
refreshAll().then(function() { startPolling(); if (cwEnabled) cwStart(); });
}
btnSave.addEventListener('click', doSave);
btnCancel.addEventListener('click', function(){ wrap.remove(); });
wrap.addEventListener('click', function(e){ if (e.target === wrap) wrap.remove(); });
box.addEventListener('click', function(e){ e.stopPropagation(); });
wrap.addEventListener('keydown', function(e){ if (e.key === 'Escape') wrap.remove(); });
}
/* ─────────────────────────────────────────
API
───────────────────────────────────────── */
function gmFetch(url) {
// Try GM_xmlhttpRequest first (Tampermonkey — bypasses CORS)
if (typeof GM_xmlhttpRequest !== 'undefined') {
return new Promise(function(resolve) {
GM_xmlhttpRequest({
method: 'GET',
url: url,
onload: function(r) {
try { resolve(JSON.parse(r.responseText)); }
catch(e) { resolve({ _parseError: true }); }
},
onerror: function() { resolve({ _netError: true }); }
});
});
}
// Fallback: native fetch (PDA and modern browsers)
return fetch(url)
.then(function(r) { return r.json(); })
.catch(function() { return { _netError: true }; });
}
function cleanId(raw) {
var n = parseInt(String(raw || '').replace(/\D/g, ''));
return isNaN(n) ? 0 : n;
}
function fmtSecs(sec) {
if (sec <= 0) return 'soon';
var h = Math.floor(sec / 3600);
var m = Math.floor((sec % 3600) / 60);
var s = sec % 60;
if (h > 0) return h + 'h ' + m + 'm';
if (m > 0) return m + 'm ' + s + 's';
return s + 's';
}
function fmtUntil(unix) { return fmtSecs(unix - Math.floor(Date.now() / 1000)); }
var COUNTRY_ABBR = {
'cayman islands':'Cayman', 'south africa':'S.Africa', 'united kingdom':'UK',
'united states':'USA', 'argentina':'ARG', 'canada':'CAN', 'switzerland':'Swiss',
'japan':'Japan', 'china':'China', 'mexico':'Mexico', 'hawaii':'Hawaii',
'united arab emirates':'UAE', 'torn':'Torn'
};
function abbrCountry(desc, isAbroad) {
if (!desc) return '?';
var d = desc.toLowerCase();
var m;
if (isAbroad) {
// "In South Africa" -> "Abroad in S.Africa"
m = d.match(/^in (.+)/);
var country = m ? (COUNTRY_ABBR[m[1].trim()] || cap(m[1].trim())) : (COUNTRY_ABBR[d.trim()] || cap(desc.trim()));
return 'Abroad in ' + country;
}
// "Returning to Torn from South Africa" -> "From S.Africa"
m = d.match(/returning to torn from (.+)/);
if (m) return 'From ' + (COUNTRY_ABBR[m[1].trim()] || cap(m[1].trim()));
// "Traveling to South Africa" -> "To S.Africa"
m = d.match(/traveling to (.+)/);
if (m) return 'To ' + (COUNTRY_ABBR[m[1].trim()] || cap(m[1].trim()));
// fallback
for (var k in COUNTRY_ABBR) { if (d.indexOf(k) !== -1) return COUNTRY_ABBR[k]; }
return desc.split(' ').slice(0,2).join(' ');
}
function cap(s) { return s.charAt(0).toUpperCase() + s.slice(1); }
function parseStatus(player) {
var s = player.status || {};
var state = (s.state || '').toLowerCase();
if (state === 'hospital' || state.indexOf('hospital') !== -1) {
return { type:'hosp', sub: s.until ? 'Out ' + fmtUntil(s.until) : 'Hosp' };
}
if (state === 'traveling' || state.indexOf('traveling') !== -1) {
return { type:'travel', sub: abbrCountry(s.description, false) };
}
if (state === 'abroad' || state.indexOf('abroad') !== -1) {
return { type:'travel', sub: abbrCountry(s.description, true) };
}
if (state === 'jail' || state.indexOf('jail') !== -1) {
return { type:'jail', sub: s.until ? 'Jail ' + fmtUntil(s.until) : 'Jail' };
}
if (state === 'federal' || state.indexOf('federal') !== -1) {
return { type:'jail', sub: 'Fed jail' };
}
return { type:'okay', sub: 'Active' };
}
/* ─────────────────────────────────────────
API CALLS
───────────────────────────────────────── */
async function fetchSelf() {
var uid = cleanId(cfg.userId);
if (!uid || !cfg.apiKey) return null;
var d = await gmFetch('https://api.torn.com/user/' + uid + '?selections=profile,bars,perks&key=' + cfg.apiKey);
if (d.error) {
var msg = (d.error && d.error.error) ? d.error.error : JSON.stringify(d.error);
console.warn('[TargetTracker] fetchSelf error (userId=' + uid + '):', msg);
toast('⚠ User API: ' + msg);
return null;
}
return d;
}
async function fetchFaction() {
// YOUR faction — members, chain, basic info
if (!cfg.factionId || !cfg.apiKey) return null;
var d = await gmFetch('https://api.torn.com/faction/' + cfg.factionId + '?selections=basic,chain&key=' + cfg.apiKey);
if (d.error) {
var msg = (d.error && d.error.error) ? d.error.error : JSON.stringify(d.error);
console.warn('[TargetTracker] fetchFaction error:', msg);
toast('⚠ Faction API: ' + msg);
return null;
}
return d;
}
async function fetchWars() {
// Single call — returns wars data including both faction member lists
if (!cfg.factionId || !cfg.apiKey) return null;
var w = await gmFetch('https://api.torn.com/v2/faction/' + cfg.factionId + '/wars?key=' + cfg.apiKey);
if (w.error) { console.warn('[TargetTracker] fetchWars error:', JSON.stringify(w.error)); return null; }
if (!w.wars) { console.warn('[TargetTracker] fetchWars: no wars in response'); return null; }
lastWarsFetch = Date.now();
return w.wars;
}
function applyWarScore(wars) {
if (!wars) return;
var warIds = Object.keys(wars);
if (!warIds.length) { console.warn('[TargetTracker] applyWarScore: wars object is empty'); return; }
var w = wars[warIds[0]];
// v2/wars factions is an ARRAY: [ { id, name, score, ... }, { id, name, score, ... } ]
// NOT keyed by faction ID — must read .id from each entry
var facs = w.factions || [];
if (!Array.isArray(facs)) facs = Object.values(facs); // normalise just in case
console.log('[TargetTracker] applyWarScore factions count:', facs.length, 'ids:', facs.map(function(f){ return f.id; }), 'cfg.factionId:', cfg.factionId);
if (facs.length < 2) { console.warn('[TargetTracker] applyWarScore: less than 2 factions'); return; }
var ourIdNum = parseInt(cfg.factionId);
var usFac = facs.find(function(f){ return f.id === ourIdNum; });
var themFac = facs.find(function(f){ return f.id !== ourIdNum; });
// Fallback: use selfData faction name if ID doesn't match
if (!usFac && selfData && selfData.faction) {
var selfName = (selfData.faction.faction_name || '').toLowerCase();
usFac = facs.find(function(f){ return (f.name||'').toLowerCase() === selfName; });
themFac = facs.find(function(f){ return f !== usFac; });
}
// Final fallback: just pick first as us
if (!usFac) { usFac = facs[0]; themFac = facs[1]; }
warScore = {
us: usFac.score || 0,
them: themFac.score || 0,
usName: usFac.name || 'Us',
themName: themFac.name || 'Them',
enemyId: String(themFac.id), // actual faction ID from inside the object
startTime: w.start || 0
};
console.log('[TargetTracker] warScore set — us:', warScore.usName, warScore.us, '| them:', warScore.themName, warScore.them, '| enemyId:', warScore.enemyId);
// enemyId is now the real faction ID — fetchEnemyFactionDirect called from refreshFaction
}
async function fetchEnemyFactionDirect(enemyId) {
if (!enemyId || !cfg.apiKey) return;
var eid = String(enemyId).replace(/[^0-9]/g, '');
if (!eid) return;
console.log('[TargetTracker] fetchEnemyFactionDirect id=' + eid);
// Two parallel v2 calls: basic info + members list
var results = await Promise.allSettled([
gmFetch('https://api.torn.com/v2/faction/' + eid + '?selections=basic&key=' + cfg.apiKey),
gmFetch('https://api.torn.com/v2/faction/' + eid + '/members?key=' + cfg.apiKey)
]);
var info = (results[0].status === 'fulfilled') ? results[0].value : {};
var membRes = (results[1].status === 'fulfilled') ? results[1].value : {};
if (info.error) { console.warn('[TargetTracker] enemy basic error:', JSON.stringify(info.error)); }
if (membRes.error) { console.warn('[TargetTracker] enemy members error:', JSON.stringify(membRes.error)); }
// Build enemyData from basic info
var rank = info.rank || {};
enemyData = {
id: eid,
name: info.name || ('Faction ' + eid),
respect: info.respect || 0,
bestChain: info.best_chain || 0,
memberCount: 0,
capacity: info.capacity || 0,
rankName: rank.name || '',
rankDiv: rank.division || '',
wins: rank.wins || 0,
losses: rank.losses || 0,
};
// v2/faction/{id}/members returns { members: { "userId": { name, level, position, status } } }
// Keys are player IDs — must use Object.keys() not Object.values() to preserve them
// Log full top-level keys so we can see what the API returned
console.log('[TargetTracker] members response top-level keys:', JSON.stringify(Object.keys(membRes)));
console.log('[TargetTracker] members response snippet:', JSON.stringify(membRes).substring(0, 400));
// Handle every possible response shape from Torn v2 members endpoint:
// { members: { "id": {...} } } OR { members: [ {...} ] } OR { "id": {...} } (flat)
var scoreMap = {};
enemyRoster.forEach(function(m) { if (m.warScore) scoreMap[m.id] = m.warScore; });
var newRoster = [];
var raw = membRes.members !== undefined ? membRes.members : membRes;
if (Array.isArray(raw)) {
// Array of member objects with id field
newRoster = raw.map(function(m) {
return { id: parseInt(m.id || m.player_id || 0), name: m.name || '?', level: m.level || 0, warScore: scoreMap[parseInt(m.id||0)] || 0, position: m.position || '', status: parseStatus(m) };
});
} else if (raw && typeof raw === 'object') {
// Object keyed by player ID
newRoster = Object.keys(raw).filter(function(k){ return !isNaN(parseInt(k)); }).map(function(uid) {
var m = raw[uid];
var pid = parseInt(uid);
return { id: pid, name: m.name || '?', level: m.level || 0, warScore: scoreMap[pid] || 0, position: m.position || '', status: parseStatus(m) };
});
}
newRoster = newRoster.filter(function(m) { return m.id > 0; });
if (newRoster.length) {
enemyData.memberCount = newRoster.length;
enemyRoster = newRoster;
console.log('[TargetTracker] enemy roster loaded:', newRoster.length, 'members');
} else {
console.warn('[TargetTracker] enemy roster still empty after parsing. Full response:', JSON.stringify(membRes).substring(0, 500));
}
}
async function fetchPlayer(rawId) {
var id = cleanId(rawId);
if (!id) { console.warn('[TargetTracker] fetchPlayer: bad id', rawId); return null; }
if (!cfg.apiKey) { console.warn('[TargetTracker] fetchPlayer: no API key'); return null; }
console.log('[TargetTracker] fetchPlayer id=' + id);
var d = await gmFetch('https://api.torn.com/user/' + id + '?selections=profile&key=' + cfg.apiKey);
if (d.error) {
var msg = (d.error && d.error.error) ? d.error.error : JSON.stringify(d.error);
console.warn('[TargetTracker] fetchPlayer error:', msg);
toast('⚠ API: ' + msg);
return null;
}
return { id: d.player_id || id, name: d.name || ('Player ' + id), level: d.level || 0, status: parseStatus(d) };
}
/* ─────────────────────────────────────────
REFRESH — call budget:
• refreshSelf() — every 60s = 1 call/min
• refreshFaction() — every 45s = ~2 calls/min (own faction + wars)
• fetchEnemyFactionDirect() — triggered by applyWarScore when at war
• refreshChain() — every 90s = N/1.5 calls/min (1 per chain target)
───────────────────────────────────────── */
async function refreshSelf() {
var d = await fetchSelf();
if (d) selfData = d;
if (activeTab === 'dash') renderDashBody();
renderStatusBar();
}
async function refreshFaction() {
isLoading = true; renderStatusBar();
// Call 1: our faction (members + chain)
var d = await fetchFaction();
if (d) {
var rank = d.rank || {};
ownFactionData = {
name: d.name || '',
respect: d.respect || 0,
bestChain: d.best_chain || 0,
memberCount: Object.keys(d.members || {}).length,
capacity: d.capacity || 0,
rankName: rank.name || '',
rankDiv: rank.division || '',
wins: rank.wins || 0,
losses: rank.losses || 0,
};
var raw = d.members || {};
var newRoster = Object.keys(raw).map(function(id) {
var m = raw[id];
return { id: parseInt(id), name: m.name || '?', level: m.level || 0, position: m.position || '', status: parseStatus(m) };
});
newRoster.forEach(function(m) {
var wasHosp = prevHospStatus[m.id];
var isHosp = m.status.type === 'hosp';
if (wasHosp && !isHosp) sendHospOutNotif(m);
prevHospStatus[m.id] = isHosp;
});
warRoster = newRoster;
if (d.chain) factionChain = { current: d.chain.current || 0, timeout: d.chain.timeout || 0, cooldown: d.chain.cooldown || 0, modifier: d.chain.modifier || 0 };
}
// Call 2 (every 120s only): wars — derives enemyId
if (cfg.factionId && (Date.now() - lastWarsFetch) > 120000) {
applyWarScore(await fetchWars());
}
// Call 3: enemy faction — always fetch every cycle for live statuses
if (warScore && warScore.enemyId) {
await fetchEnemyFactionDirect(warScore.enemyId);
}
isLoading = false;
if (activeTab === 'war' || activeTab === 'chain') renderPanel();
else renderStatusBar();
updateChainBadge();
}
async function refreshChain() {
var ids = cfg.getChainIds();
if (!ids.length || !cfg.apiKey) return;
var res = await Promise.allSettled(ids.map(fetchPlayer));
var newCache = res.filter(function(r) { return r.status === 'fulfilled' && r.value; }).map(function(r) { return r.value; });
newCache.forEach(function(m) {
var wasHosp = prevHospStatus['c_' + m.id];
var isHosp = m.status.type === 'hosp';
if (wasHosp && !isHosp) sendHospOutNotif(m);
prevHospStatus['c_' + m.id] = isHosp;
});
chainCache = newCache;
if (activeTab === 'chain') renderChainBody();
renderStatusBar();
}
// Full refresh — self first so selfData is set before applyWarScore cross-references it
async function refreshAll() {
isLoading = true; renderStatusBar();
await refreshSelf();
await Promise.allSettled([refreshFaction(), refreshChain()]);
isLoading = false;
syncWarTabVisibility();
renderPanel();
updateChainBadge();
}
function sendHospOutNotif(m) {
if (!notifGranted) return;
try {
// ToS: only fire desktop notifications while the user is actively on this page
if (document.visibilityState !== 'visible') return;
var n = new Notification('🏥 ' + m.name + ' left hospital!', {
body: 'Lv' + m.level + ' — available to attack now',
icon: 'https://www.torn.com/favicon.ico',
tag: 'wtt-hosp-' + m.id
});
n.onclick = function() { window.open('https://www.torn.com/loader.php?sid=attack&user2ID=' + m.id); };
setTimeout(function() { n.close(); }, 8000);
} catch(e) { console.warn('[TargetTracker] Notification failed:', e); }
}
/* ─────────────────────────────────────────
RENDER HELPERS
───────────────────────────────────────── */
var STATUS_COLOR = { hosp: C.hosp, travel: C.travel, jail: C.jail, okay: C.okay };
var STATUS_BG = { hosp: 'rgba(140,10,10,0.12)', travel: 'rgba(95,65,4,0.08)', jail: 'rgba(8,26,78,0.1)', okay: 'transparent' };
var STATUS_BDR = { hosp: '#c03030', travel: '#c89820', jail: '#3a60c0', okay: 'transparent' };
function sortList(list) {
var o = { okay:0, hosp:1, travel:2, jail:3 };
return list.slice().sort(function(a,b) {
var d = (o[a.status.type]||3) - (o[b.status.type]||3);
return d !== 0 ? d : b.level - a.level;
});
}
function filterList(list, f) {
if (f === 'all') return list;
return list.filter(function(m) { return m.status.type === f; });
}
function makeRow(m, idx, showRemove) {
var st = m.status;
var col = STATUS_COLOR[st.type] || C.text;
var bg = STATUS_BG[st.type] || 'transparent';
var bdr = STATUS_BDR[st.type] || 'transparent';
var anim = st.type === 'hosp' ? 'wtt-pulse-red' : st.type === 'travel' ? 'wtt-pulse-amb' : '';
var canAtk = st.type === 'okay';
var atkUrl = 'https://www.torn.com/loader.php?sid=attack&user2ID=' + m.id;
var dotCol = { hosp:'#ff5555', travel:'#f0b030', jail:'#78a8ff', okay:'#66bb66' };
// ── row: 3 cols — rank | name block | attack button ──
var row = document.createElement('div');
row.setAttribute('style',
'display:grid !important;' +
'grid-template-columns:14px 1fr auto !important;' +
'align-items:center !important;gap:6px !important;' +
'padding:5px 8px !important;border-bottom:1px solid rgba(40,10,10,0.35) !important;' +
'border-left:3px solid ' + bdr + ' !important;background:' + bg + ' !important;' +
'box-sizing:border-box !important;'
);
row.dataset.id = m.id;
// col 1 — rank number
var rank = document.createElement('div');
rank.textContent = idx + 1;
rank.setAttribute('style',
'font-family:Consolas,monospace !important;font-size:8px !important;' +
'color:rgba(120,120,120,0.6) !important;text-align:center !important;'
);
// col 2 — name (fixed 110px) + dot + sub below
var nameBlock = document.createElement('div');
nameBlock.setAttribute('style', 'overflow:hidden !important;');
var nameRow = document.createElement('div');
nameRow.setAttribute('style',
'display:flex !important;align-items:center !important;gap:4px !important;'
);
var nameEl = document.createElement('div');
nameEl.textContent = m.name;
nameEl.setAttribute('style',
'font-size:12px !important;font-weight:600 !important;color:' + C.text + ' !important;' +
'white-space:nowrap !important;overflow:hidden !important;text-overflow:ellipsis !important;' +
'max-width:110px !important;'
);
var dot = document.createElement('div');
dot.className = anim;
dot.setAttribute('style',
'width:7px !important;height:7px !important;border-radius:50% !important;flex-shrink:0 !important;' +
'background:' + (dotCol[st.type] || '#888') + ' !important;'
);
nameRow.appendChild(nameEl);
nameRow.appendChild(dot);
var sub = document.createElement('div');
sub.textContent = st.sub;
sub.setAttribute('style',
'font-family:Consolas,monospace !important;font-size:9px !important;color:' + col + ' !important;' +
'white-space:nowrap !important;overflow:hidden !important;text-overflow:ellipsis !important;' +
'margin-top:1px !important;'
);
nameBlock.appendChild(nameRow);
nameBlock.appendChild(sub);
// col 3 — attack: plain <a> with official Torn attack URL, appended to body not panel
var atkWrap = document.createElement('div');
atkWrap.setAttribute('style', 'flex-shrink:0 !important;');
if (canAtk) {
var atkLink = document.createElement('a');
atkLink.href = atkUrl;
atkLink.textContent = '⚔ ATK';
atkLink.setAttribute('style',
'display:block !important;font-family:Consolas,monospace !important;' +
'font-size:9px !important;font-weight:700 !important;' +
'padding:4px 7px !important;border-radius:4px !important;' +
'border:1px solid rgba(60,180,60,0.8) !important;' +
'background:rgba(10,100,10,0.55) !important;' +
'color:#88ff88 !important;white-space:nowrap !important;' +
'text-decoration:none !important;-webkit-tap-highlight-color:rgba(60,200,60,0.4) !important;'
);
atkWrap.appendChild(atkLink);
} else {
var atkDim = document.createElement('div');
atkDim.textContent = st.type === 'hosp' ? '🏥' : st.type === 'travel' ? '✈' : st.type === 'jail' ? '🔒' : '—';
atkDim.setAttribute('style',
'font-size:16px !important;color:rgba(160,160,160,0.55) !important;' +
'text-align:center !important;width:36px !important;line-height:1 !important;'
);
atkWrap.appendChild(atkDim);
}
row.appendChild(rank);
row.appendChild(nameBlock);
row.appendChild(atkWrap);
return row;
}
function makeSectionLabel(text) {
var el = document.createElement('div');
el.textContent = text;
el.style.cssText = 'padding:3px 10px;font-size:9px;font-weight:700;letter-spacing:1px;text-transform:uppercase;color:rgba(180,72,72,0.65);background:rgba(72,8,8,0.15);border-bottom:1px solid rgba(72,8,8,0.22);font-family:Consolas,monospace;';
return el;
}
function makeEmpty(icon, msg) {
var el = document.createElement('div');
el.style.cssText = 'padding:28px 16px;text-align:center;color:rgba(150,90,90,0.65);font-size:11px;line-height:1.5;font-family:Arial,sans-serif;';
el.innerHTML = '<div style="font-size:22px;margin-bottom:8px;">' + icon + '</div>' + msg;
return el;
}
/* ─────────────────────────────────────────
RENDER: WAR
───────────────────────────────────────── */
function renderWarBody() {
var body = panelEl.querySelector('.wtt-body');
body.innerHTML = '';
if (!cfg.factionId || !cfg.apiKey) {
body.appendChild(makeEmpty('⚔', 'Set your API key & Your Faction ID in Settings')); return;
}
if (!warScore) {
body.appendChild(makeEmpty('🏳', 'No active ranked war')); return;
}
// Show the war card even while enemy roster is still loading
body.appendChild(makeWarCard(false));
if (!enemyRoster.length) {
body.appendChild(makeEmpty('◌', 'Loading enemy roster…')); return;
}
// ── Enemy roster (primary — these are your targets) ──
var enemyList = filterList(sortList(enemyRoster), activeFilter);
if (enemyList.length) {
var enemyName2 = (enemyData && enemyData.name) ? enemyData.name : (warScore ? warScore.themName : 'Enemy');
var groups = {};
enemyList.forEach(function(m) { if (!groups[m.status.type]) groups[m.status.type] = []; groups[m.status.type].push(m); });
var SECS = [['okay','🟢 Active'],['hosp','🏥 Hospitalized'],['travel','✈ Traveling'],['jail','🔒 Jailed']];
var idx = 0;
if (activeFilter !== 'all') {
var lbl = (SECS.find(function(s){ return s[0] === activeFilter; }) || ['',''])[1];
body.appendChild(makeSectionLabel('🎯 ' + enemyName2.toUpperCase() + ' · ' + lbl + ' (' + enemyList.length + ')'));
enemyList.forEach(function(m, i) { body.appendChild(makeRow(m, i, false)); });
} else {
SECS.forEach(function(sec) {
var g = groups[sec[0]];
if (!g || !g.length) return;
body.appendChild(makeSectionLabel('🎯 ' + sec[1] + ' (' + g.length + ')'));
g.forEach(function(m) { body.appendChild(makeRow(m, idx++, false)); });
});
}
} else if (warScore) {
body.appendChild(makeEmpty('◌', 'Loading enemy roster…'));
}
}
/* ─────────────────────────────────────────
RENDER: CHAIN
───────────────────────────────────────── */
function renderChainBody() {
var body = panelEl.querySelector('.wtt-body');
body.innerHTML = '';
// ── add-input row (always visible) ──
var addWrap = document.createElement('div');
addWrap.style.cssText = 'display:flex;gap:5px;padding:6px 8px;border-bottom:1px solid rgba(55,8,8,0.4);flex-shrink:0;';
var cInp = document.createElement('input');
cInp.type = 'text'; cInp.placeholder = 'Add player ID'; cInp.maxLength = 12;
cInp.style.cssText = 'flex:1;padding:5px 8px;background:rgba(18,4,4,0.9);border:1px solid rgba(95,16,16,0.45);border-radius:4px;color:#d4d4d4;font-size:11px;font-family:Consolas,monospace;outline:none;';
cInp.addEventListener('focus', function(){ cInp.style.borderColor = 'rgba(195,44,44,0.7)'; });
cInp.addEventListener('blur', function(){ cInp.style.borderColor = 'rgba(95,16,16,0.45)'; });
var cAdd = document.createElement('button');
cAdd.type = 'button'; cAdd.textContent = 'ADD';
cAdd.style.cssText = 'padding:5px 11px;background:rgba(140,16,16,0.5);border:1px solid rgba(175,36,36,0.55);border-radius:4px;color:#df5555;font-size:10px;font-weight:700;cursor:pointer;font-family:Arial,sans-serif;';
async function doAdd() {
var id = cleanId(cInp.value); cInp.value = '';
if (!id) { toast('⚠ Enter a numeric player ID'); return; }
if (!cfg.apiKey) { toast('⚠ Add your API key in Settings'); return; }
var ids = cfg.getChainIds();
if (ids.indexOf(id) !== -1) { toast('Already tracking ' + id); return; }
toast('Looking up ' + id + '…', 3000);
var player = await fetchPlayer(id);
if (!player) return;
ids.push(id); cfg.setChainIds(ids);
chainCache.push(player);
toast('✓ Added ' + player.name);
renderChainBody();
}
cAdd.addEventListener('click', doAdd);
cInp.addEventListener('keydown', function(e){ if (e.key === 'Enter') doAdd(); });
addWrap.appendChild(cInp); addWrap.appendChild(cAdd);
body.appendChild(addWrap);
if (!cfg.apiKey) { body.appendChild(makeEmpty('🔑', 'Add your API key in Settings')); return; }
// ── Chain Watcher card — always shown when API key is set ──
(function() {
var card = document.createElement('div');
card.id = 'wtt-cw-card';
card.style.cssText = 'margin:8px 8px 0;border-radius:8px;padding:10px 12px;border:1px solid rgba(80,20,20,0.4);background:rgba(10,3,3,0.65);flex-shrink:0;transition:border-color 0.3s;';
// Header row: label + state badge
var hRow = document.createElement('div');
hRow.style.cssText = 'display:flex;justify-content:space-between;align-items:center;margin-bottom:6px;';
var hLbl = document.createElement('span');
hLbl.textContent = '⛓ CHAIN WATCHER';
hLbl.style.cssText = 'font-size:9px;font-weight:700;font-family:Consolas,monospace;color:rgba(200,160,60,0.8);letter-spacing:.6px;';
var stateEl = document.createElement('span');
stateEl.id = 'wtt-cw-state';
stateEl.style.cssText = 'font-size:8px;font-weight:700;font-family:Consolas,monospace;color:rgba(140,120,60,0.65);letter-spacing:.5px;';
stateEl.textContent = cwEnabled ? (cwCount > 0 ? 'CHAINING' : 'Waiting…') : 'Off — enable in Settings';
hRow.appendChild(hLbl); hRow.appendChild(stateEl);
// Big timer + count row
var midRow = document.createElement('div');
midRow.style.cssText = 'display:flex;justify-content:space-between;align-items:flex-end;margin-bottom:7px;';
var timerEl = document.createElement('div');
timerEl.id = 'wtt-cw-timer';
timerEl.style.cssText = 'font-size:34px;font-weight:700;font-family:Consolas,monospace;line-height:1;color:rgba(150,100,100,0.5);letter-spacing:2px;';
timerEl.textContent = '--:--';
var countWrap = document.createElement('div');
countWrap.style.cssText = 'text-align:right;';
var countLbl = document.createElement('div');
countLbl.style.cssText = 'font-size:8px;font-family:Consolas,monospace;color:rgba(140,120,80,0.55);margin-bottom:1px;';
countLbl.textContent = 'HITS';
var countEl = document.createElement('div');
countEl.id = 'wtt-cw-count';
countEl.style.cssText = 'font-size:18px;font-weight:700;font-family:Consolas,monospace;color:' + C.travel + ';line-height:1;';
countEl.textContent = '—';
countWrap.appendChild(countLbl); countWrap.appendChild(countEl);
midRow.appendChild(timerEl); midRow.appendChild(countWrap);
// Progress bar (5:00 = 100%)
var barTrack = document.createElement('div');
barTrack.style.cssText = 'height:4px;background:rgba(30,10,10,0.6);border-radius:2px;overflow:hidden;';
var barFill = document.createElement('div');
barFill.id = 'wtt-cw-bar';
barFill.style.cssText = 'height:100%;width:0%;background:#66bb66;border-radius:2px;transition:width 0.9s linear,background 0.4s;';
barTrack.appendChild(barFill);
// "Enable in Settings" hint if off
if (!cwEnabled) {
var hint = document.createElement('div');
hint.style.cssText = 'font-size:9px;color:rgba(140,100,60,0.55);font-family:Consolas,monospace;margin-top:5px;';
hint.textContent = '⚙ Enable Chain Watcher in Settings → ⛓ Chain Watcher';
card.appendChild(hRow); card.appendChild(midRow); card.appendChild(barTrack); card.appendChild(hint);
} else {
card.appendChild(hRow); card.appendChild(midRow); card.appendChild(barTrack);
}
body.appendChild(card);
// Initial render
cwUpdateUI();
})();
// ── chain target guards ──
var ids = cfg.getChainIds();
if (!ids.length) { body.appendChild(makeEmpty('+', 'Enter a player ID above to start tracking')); return; }
if (!chainCache.length) {
body.appendChild(makeEmpty('◌', 'Loading ' + ids.length + ' target' + (ids.length !== 1 ? 's' : '') + '…'));
return;
}
// ── target rows ──
var CHAIN_SECS = [['okay','🟢 Active'],['hosp','🏥 Hospitalized'],['travel','✈ Traveling'],['jail','🔒 Jailed']];
var list = filterList(sortList(chainCache), activeFilter);
if (!list.length) { body.appendChild(makeEmpty('—', 'No targets match this filter')); return; }
if (activeFilter !== 'all') {
var secLbl = (CHAIN_SECS.find(function(s){ return s[0] === activeFilter; }) || ['',''])[1];
body.appendChild(makeSectionLabel(secLbl + ' (' + list.length + ')'));
list.forEach(function(m, i) { body.appendChild(makeRow(m, i, true)); });
} else {
var groups = {};
list.forEach(function(m) { if (!groups[m.status.type]) groups[m.status.type] = []; groups[m.status.type].push(m); });
var idx = 0;
CHAIN_SECS.forEach(function(sec) {
var g = groups[sec[0]];
if (!g || !g.length) return;
body.appendChild(makeSectionLabel(sec[1] + ' (' + g.length + ')'));
g.forEach(function(m) { body.appendChild(makeRow(m, idx++, true)); });
});
}
}
/* ─────────────────────────────────────────
WAR CARD — used on both Dash and War tabs
───────────────────────────────────────── */
// showStats=true on war tab, false on dash
function makeWarCard(showStats) {
var winning = warScore.us >= warScore.them;
var usD = ownFactionData || {};
var thD = enemyData || {};
var card = document.createElement('div');
card.style.cssText = 'border-radius:8px;border:1px solid rgba(120,16,16,0.45);background:rgba(14,2,2,0.7);overflow:hidden;flex-shrink:0;';
// Score bar top
var total = warScore.us + warScore.them;
var usPct = total > 0 ? Math.round(warScore.us / total * 100) : 50;
var barOuter = document.createElement('div');
barOuter.style.cssText = 'height:5px;background:rgba(140,20,20,0.3);';
var barInner = document.createElement('div');
barInner.style.cssText = 'height:100%;width:' + usPct + '%;background:' + (winning ? C.okay : C.hosp) + ';transition:width 0.6s ease;';
barOuter.appendChild(barInner);
card.appendChild(barOuter);
// Two faction panels
var panels = document.createElement('div');
panels.style.cssText = 'display:grid;grid-template-columns:1fr 1fr;';
function makeFacPanel(name, score, scoreColor, statusLabel, data, isLeft) {
var p = document.createElement('div');
p.style.cssText = 'padding:9px 10px;display:flex;flex-direction:column;gap:3px;' + (isLeft ? 'border-right:1px solid rgba(80,10,10,0.4);' : '');
var nameEl = document.createElement('div');
nameEl.textContent = name.toUpperCase();
nameEl.style.cssText = 'font-size:9px;font-weight:700;letter-spacing:.7px;color:rgba(200,200,200,0.75);font-family:Consolas,monospace;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;';
p.appendChild(nameEl);
var scoreEl = document.createElement('div');
scoreEl.textContent = score.toLocaleString();
scoreEl.style.cssText = 'font-size:24px;font-weight:700;font-family:Consolas,monospace;line-height:1;color:' + scoreColor + ';';
p.appendChild(scoreEl);
var statusEl = document.createElement('div');
statusEl.textContent = statusLabel;
statusEl.style.cssText = 'font-size:8px;font-weight:700;color:' + scoreColor + ';font-family:Consolas,monospace;opacity:0.85;';
p.appendChild(statusEl);
if (showStats) {
var divider = document.createElement('div');
divider.style.cssText = 'border-top:1px solid rgba(80,10,10,0.3);margin:4px 0;';
p.appendChild(divider);
function stat(label, val, title) {
if (val === null || val === undefined || val === '') return;
var row = document.createElement('div');
row.title = title || '';
row.style.cssText = 'display:flex;justify-content:space-between;gap:4px;';
var lbl = document.createElement('span');
lbl.textContent = label;
lbl.style.cssText = 'font-size:8px;color:rgba(140,120,120,0.7);font-family:Consolas,monospace;';
var v = document.createElement('span');
v.textContent = val;
v.style.cssText = 'font-size:8px;color:rgba(200,180,180,0.85);font-family:Consolas,monospace;font-weight:600;text-align:right;';
row.appendChild(lbl); row.appendChild(v);
p.appendChild(row);
}
if (data.respect) stat('Respect', data.respect.toLocaleString(), 'Total faction respect');
if (data.rankName) stat('Rank', data.rankName + (data.rankDiv ? ' ' + data.rankDiv : ''), 'War rank');
if (data.wins !== undefined) stat('W / L', data.wins + ' / ' + data.losses, 'Ranked war wins / losses');
if (data.memberCount) stat('Members', data.memberCount + ' / ' + (data.capacity || '?'), 'Members / capacity');
if (data.bestChain) stat('Best chain', data.bestChain, 'Best chain hit');
}
return p;
}
var usColor = winning ? C.okay : C.hosp;
var themColor = !winning ? C.okay : C.hosp;
panels.appendChild(makeFacPanel(warScore.usName || 'Us', warScore.us, usColor, winning ? '▲ WINNING' : '▼ LOSING', usD, true));
panels.appendChild(makeFacPanel(warScore.themName || 'Enemy', warScore.them, themColor, !winning ? '▲ WINNING' : '▼ LOSING', thD, false));
card.appendChild(panels);
return card;
}
/* ─────────────────────────────────────────
RENDER: DASHBOARD
───────────────────────────────────────── */
function renderDashBody() {
var body = panelEl.querySelector('.wtt-body');
body.innerHTML = '';
if (!cfg.apiKey || !cfg.userId) {
body.appendChild(makeEmpty('🔑', 'Set your API key & User ID<br>in Settings to use the dashboard')); return;
}
if (!selfData) {
body.appendChild(makeEmpty('◌', 'Loading operator data…')); return;
}
var wrap = document.createElement('div');
wrap.style.cssText = 'padding:10px;display:flex;flex-direction:column;gap:12px;';
/* ── health section ── */
var hp = selfData.life || {};
var cur = hp.current || 0;
var max = hp.maximum || 1;
var pct = Math.min(100, Math.round(cur / max * 100));
var intv = hp.interval || 300;
var inc = hp.increment || 0;
var barColor = '#1a6ea8';
var valColor = pct >= 100 ? '#5ab4f0' : pct >= 60 ? '#4a9cd4' : pct >= 30 ? '#3a7aaa' : '#2a5888';
var st = parseStatus(selfData);
var stCol = STATUS_COLOR[st.type] || C.okay;
function sec(titleText) {
var s = document.createElement('div');
var t = document.createElement('div');
t.textContent = titleText;
t.style.cssText = 'font-size:9px;font-weight:700;letter-spacing:1.2px;text-transform:uppercase;color:rgba(200,72,72,0.75);margin-bottom:7px;padding-bottom:4px;border-bottom:1px solid rgba(130,16,16,0.3);font-family:Consolas,monospace;';
s.appendChild(t);
return s;
}
/* ── energy data (needed inside health card) ── */
var energy = selfData.energy || {};
var nCur = energy.current || 0;
var nMax = energy.maximum || 0;
var nPct = nMax > 0 ? Math.min(100, Math.round(nCur / nMax * 100)) : 0;
var nIntv = energy.ticktime || 300;
/* health card */
var hpSec = sec('⚡ Operator Status');
var card = document.createElement('div');
card.style.cssText = 'background:rgba(4,14,28,0.6);border:1px solid rgba(30,90,160,0.35);border-radius:7px;padding:10px 12px;';
var topRow = document.createElement('div');
topRow.style.cssText = 'display:flex;justify-content:space-between;align-items:center;margin-bottom:7px;';
var hpLbl = document.createElement('span');
hpLbl.textContent = 'HEALTH';
hpLbl.style.cssText = 'font-size:10px;font-weight:700;color:rgba(100,170,230,0.85);letter-spacing:.4px;font-family:Arial,sans-serif;';
var hpVal = document.createElement('span');
hpVal.textContent = cur.toLocaleString() + ' / ' + max.toLocaleString();
hpVal.style.cssText = 'font-size:14px;font-weight:700;color:' + valColor + ';font-family:Consolas,monospace;';
topRow.appendChild(hpLbl); topRow.appendChild(hpVal);
var barWrap = document.createElement('div');
barWrap.style.cssText = 'height:7px;background:rgba(8,20,40,0.7);border-radius:4px;overflow:hidden;margin-bottom:8px;border:1px solid rgba(20,70,120,0.4);';
var bar = document.createElement('div');
bar.style.cssText = 'height:100%;border-radius:4px;width:' + pct + '%;background:' + barColor + ';transition:width 0.6s ease;';
barWrap.appendChild(bar);
// Regen sub-label shown under the HP value
var regenRate = inc > 0 ? inc : Math.round(max * 0.05);
var hpSubLbl = document.createElement('div');
hpSubLbl.id = 'wtt-regen-val';
hpSubLbl.style.cssText = 'font-size:9px;color:rgba(80,140,200,0.65);font-family:Consolas,monospace;text-align:right;margin-top:2px;';
if (pct >= 100) { hpSubLbl.textContent = '✓ Full'; }
else { hpSubLbl.textContent = '+' + regenRate + ' HP / ' + fmtSecs(intv) + ' · full in ' + fmtSecs(Math.ceil((max - cur) / regenRate) * intv); }
// ── Energy sub-section inside status card ──
var divider = document.createElement('div');
divider.style.cssText = 'border-top:1px solid rgba(20,80,20,0.3);margin:8px 0 8px;';
var eLbl = document.createElement('span');
eLbl.textContent = 'ENERGY';
eLbl.style.cssText = 'font-size:10px;font-weight:700;color:rgba(100,200,100,0.85);letter-spacing:.4px;font-family:Arial,sans-serif;';
var eVal = document.createElement('span');
eVal.textContent = nCur + ' / ' + nMax;
eVal.style.cssText = 'font-size:14px;font-weight:700;color:' + (nPct >= 75 ? C.okay : nPct >= 40 ? C.travel : C.hosp) + ';font-family:Consolas,monospace;';
var eTopRow = document.createElement('div');
eTopRow.style.cssText = 'display:flex;justify-content:space-between;align-items:center;margin-bottom:7px;';
eTopRow.appendChild(eLbl); eTopRow.appendChild(eVal);
var eBarWrap = document.createElement('div');
eBarWrap.style.cssText = 'height:7px;background:rgba(4,24,8,0.7);border-radius:4px;overflow:hidden;margin-bottom:8px;border:1px solid rgba(20,100,40,0.4);';
var eBar = document.createElement('div');
eBar.style.cssText = 'height:100%;border-radius:4px;width:' + nPct + '%;background:#1e9e3a;transition:width 0.6s ease;';
eBarWrap.appendChild(eBar);
var eRegenRow = document.createElement('div');
eRegenRow.style.cssText = 'display:flex;justify-content:space-between;align-items:center;margin-bottom:6px;';
var eRegenVal = document.createElement('span');
eRegenVal.id = 'wtt-nerve-val';
eRegenVal.style.cssText = 'font-size:9.5px;font-weight:700;color:' + C.okay + ';font-family:Consolas,monospace;letter-spacing:.4px;flex:1;';
var nTicksLeft = (nMax - nCur) * nIntv;
eRegenVal.textContent = nPct >= 100 ? '✓ Full' : '+1 / ' + fmtSecs(nIntv) + ' · full in ' + fmtSecs(nTicksLeft);
var refillBtn = document.createElement('button');
refillBtn.type = 'button';
refillBtn.textContent = '⚡ Refill';
refillBtn.style.cssText = 'padding:3px 9px;border-radius:4px;font-size:9px;font-weight:700;cursor:pointer;font-family:Arial,sans-serif;background:rgba(15,60,15,0.5);border:1px solid rgba(40,140,40,0.5);color:' + C.okay + ';flex-shrink:0;';
refillBtn.addEventListener('mouseover', function(){ refillBtn.style.setProperty('background','rgba(20,90,20,0.7)','important'); });
refillBtn.addEventListener('mouseout', function(){ refillBtn.style.setProperty('background','rgba(15,60,15,0.5)','important'); });
refillBtn.addEventListener('click', function() {
var dest = cfg.refillDest;
var url = dest === 'points'
? 'https://www.torn.com/points.php'
: 'https://www.torn.com/item.php#energy-d-items';
window.location.assign(url);
});
eRegenRow.appendChild(eRegenVal); eRegenRow.appendChild(refillBtn);
// Hits available (25 energy per attack)
var hitsAvail = Math.floor(nCur / 25);
var hitsRow = document.createElement('div');
hitsRow.style.cssText = 'display:flex;justify-content:space-between;align-items:center;margin-top:2px;';
var hitsLbl = document.createElement('span');
hitsLbl.textContent = '⚔ HITS AVAILABLE';
hitsLbl.style.cssText = 'font-size:9px;color:rgba(100,150,100,0.65);font-family:Consolas,monospace;';
var hitsVal = document.createElement('span');
hitsVal.textContent = hitsAvail + ' hits (25 energy each)';
hitsVal.style.cssText = 'font-size:9.5px;font-weight:700;color:' + (hitsAvail >= 5 ? C.okay : hitsAvail >= 2 ? C.travel : C.hosp) + ';font-family:Consolas,monospace;';
hitsRow.appendChild(hitsLbl); hitsRow.appendChild(hitsVal);
card.appendChild(eTopRow); card.appendChild(eBarWrap); card.appendChild(eRegenRow); card.appendChild(hitsRow);
card.appendChild(divider);
card.appendChild(topRow); card.appendChild(barWrap); card.appendChild(hpSubLbl);
hpSec.appendChild(card);
/* start live regen clock */
clearInterval(regenTimer);
if (pct < 100) {
var t0 = Date.now();
var regenRateClock = inc > 0 ? inc : Math.round(max * 0.05);
regenTimer = setInterval(function() {
var el = document.getElementById('wtt-regen-val');
if (!el) { clearInterval(regenTimer); return; }
var elapsed = Math.floor((Date.now() - t0) / 1000);
var gained = Math.floor(elapsed / intv) * regenRateClock;
var now2 = Math.min(cur + gained, max);
if (now2 >= max) { el.textContent = '✓ Full'; el.style.color = 'rgba(80,200,80,0.7)'; clearInterval(regenTimer); return; }
var left = Math.ceil((max - now2) / regenRateClock) * intv - (elapsed % intv);
el.textContent = '+' + regenRateClock + ' HP / ' + fmtSecs(intv) + ' · full in ' + fmtSecs(Math.max(0, left));
}, 1000);
}
/* ── quick actions ── */
var actSec = sec('💊 Quick Actions');
function dashBtn(emoji, label, sublabel, href, cls) {
var a = document.createElement('a');
a.href = href;
a.addEventListener('click', function(e) {
e.preventDefault();
var u = href;
setTimeout(function(){ window.location.assign(u); }, 0);
});
a.style.cssText = 'display:flex;align-items:center;gap:8px;padding:9px 10px;border-radius:7px;text-decoration:none;border:1px solid;transition:opacity 0.2s;';
if (cls === 'medical') { a.style.background = 'rgba(16,46,16,0.55)'; a.style.borderColor = 'rgba(55,135,55,0.45)'; a.style.color = C.okay; }
if (cls === 'short') { a.style.background = 'rgba(75,36,4,0.4)'; a.style.borderColor = 'rgba(175,115,18,0.42)'; a.style.color = C.travel; }
if (cls === 'mid') { a.style.background = 'rgba(55,18,58,0.4)'; a.style.borderColor = 'rgba(135,55,175,0.42)'; a.style.color = '#bb77dd'; }
if (cls === 'long') { a.style.background = 'rgba(4,26,66,0.4)'; a.style.borderColor = 'rgba(36,86,175,0.42)'; a.style.color = '#5888dd'; }
a.addEventListener('mouseover', function() { a.style.opacity = '0.8'; });
a.addEventListener('mouseout', function() { a.style.opacity = '1'; });
var icon = document.createElement('span');
icon.textContent = emoji;
icon.style.cssText = 'font-size:16px;flex-shrink:0;';
var txt = document.createElement('span');
txt.style.cssText = 'display:flex;flex-direction:column;';
var lbl = document.createElement('span');
lbl.textContent = label;
lbl.style.cssText = 'font-size:11px;font-weight:700;letter-spacing:.5px;font-family:Arial,sans-serif;';
var sub = document.createElement('span');
sub.textContent = sublabel;
sub.style.cssText = 'font-size:8.5px;opacity:0.65;font-family:Arial,sans-serif;margin-top:1px;';
txt.appendChild(lbl); txt.appendChild(sub);
a.appendChild(icon); a.appendChild(txt);
return a;
}
actSec.appendChild(dashBtn('🩹', 'MEDICAL ITEMS', 'Open inventory → Medical', 'https://www.torn.com/item.php#medical-items', 'medical'));
/* ── escape section ── */
var escSec = sec('✈ Escape');
var escGrid = document.createElement('div');
escGrid.style.cssText = 'display:grid;grid-template-columns:1fr 1fr;gap:7px;';
// Travel links — navigate to travel page and auto-click the country dropdown
function clickTravelCountry(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;
}
// On travel page load — check URL hash for target country and poll-click it
(function() {
var target = null;
try {
var m = (location.hash || '').match(/wtt_travel=([^&]+)/);
if (m) target = decodeURIComponent(m[1]);
} catch(e) {}
if (!target || (location.pathname + location.search).indexOf('sid=travel') === -1) return;
var _attempts = 0;
var _poll = setInterval(function() {
_attempts++;
if (clickTravelCountry(target)) { clearInterval(_poll); return; }
if (_attempts >= 40) clearInterval(_poll);
}, 300);
})();
function travelBtn(emoji, label, sublabel, country, cls) {
var b = dashBtn(emoji, label, sublabel, 'https://www.torn.com/page.php?sid=travel', cls);
b.onclick = function() {
var onTravelPage = (location.pathname + location.search).indexOf('sid=travel') !== -1;
if (onTravelPage) {
clickTravelCountry(country);
} else {
window.location.href = 'https://www.torn.com/page.php?sid=travel#wtt_travel=' + encodeURIComponent(country);
}
};
return b;
}
escGrid.appendChild(travelBtn('🇲🇽', 'SHORT', 'Mexico · ~1.5h', 'Mexico', 'short'));
escGrid.appendChild(travelBtn('🇯🇵', 'MEDIUM', 'Japan · ~12h', 'Japan', 'mid'));
var longBtn = travelBtn('🇿🇦', 'LONG', 'South Africa · ~16h · max protection', 'South Africa', 'long');
longBtn.style.gridColumn = '1 / -1';
escGrid.appendChild(longBtn);
escSec.appendChild(escGrid);
// live energy tick clock
clearInterval(nerveTimer);
if (nPct < 100 && nIntv > 0) {
var nt0 = Date.now();
nerveTimer = setInterval(function() {
var el = document.getElementById('wtt-nerve-val');
if (!el) { clearInterval(nerveTimer); return; }
var elapsed = Math.floor((Date.now() - nt0) / 1000);
var gained = Math.floor(elapsed / nIntv);
var nowN = Math.min(nCur + gained, nMax);
if (nowN >= nMax) { el.textContent = '✓ FULL'; clearInterval(nerveTimer); return; }
var left = (nMax - nowN) * nIntv - (elapsed % nIntv);
el.textContent = '+1 / ' + fmtSecs(nIntv) + ' · full in ' + fmtSecs(Math.max(0, left));
}, 1000);
}
// ── War card on dashboard (only if confirmed at war) ──
if (warScore) { wrap.appendChild(makeWarCard(true)); }
wrap.appendChild(hpSec);
wrap.appendChild(actSec);
wrap.appendChild(escSec);
body.appendChild(wrap);
}
/* ─────────────────────────────────────────
STATUS BAR
───────────────────────────────────────── */
function renderStatusBar() {
var bar = panelEl ? panelEl.querySelector('#wtt-sbar') : null;
if (!bar) return;
bar.innerHTML = '';
if (activeTab === 'dash') {
var n = document.createElement('span');
n.textContent = selfData ? selfData.name : '…';
n.style.cssText = 'color:' + C.redDim + ';font-weight:700;';
bar.appendChild(n);
} else {
var list = activeTab === 'war' ? enemyRoster : chainCache;
var cnt = document.createElement('span'); cnt.style.color = C.redDim; cnt.style.fontWeight = '700'; cnt.textContent = list.length;
bar.appendChild(cnt);
bar.appendChild(document.createTextNode(' targets'));
var h = list.filter(function(m) { return m.status.type === 'hosp'; }).length;
var j = list.filter(function(m) { return m.status.type === 'jail'; }).length;
var v = list.filter(function(m) { return m.status.type === 'travel'; }).length;
if (h) { var s = document.createElement('span'); s.style.color = C.hosp; s.textContent = ' 🏥' + h; bar.appendChild(s); }
if (j) { var s = document.createElement('span'); s.style.color = C.jail; s.textContent = ' 🔒' + j; bar.appendChild(s); }
if (v) { var s = document.createElement('span'); s.style.color = C.travel; s.textContent = ' ✈' + v; bar.appendChild(s); }
}
if (isLoading) {
var sp = document.createElement('span');
sp.className = 'wtt-spin-anim';
sp.textContent = '◌';
sp.style.cssText = 'margin-left:auto;color:rgba(200,80,80,0.6);';
bar.appendChild(sp);
}
}
/* ─────────────────────────────────────────
CHAIN BADGE on toggle
───────────────────────────────────────── */
function updateChainBadge() {
var old = document.getElementById('wtt-chain-badge');
if (old) old.remove();
if (!factionChain || factionChain.current < 10) return;
var badge = document.createElement('div');
badge.id = 'wtt-chain-badge';
badge.className = 'wtt-blink';
badge.setAttribute('style',
'position:absolute !important;top:-6px !important;right:-8px !important;' +
'background:linear-gradient(135deg,#7a0000,#c01818) !important;' +
'border:1px solid rgba(255,70,70,0.55) !important;border-radius:9px !important;' +
'padding:1px 6px !important;font-size:8.5px !important;font-weight:700 !important;' +
'color:#ffe0e0 !important;font-family:Consolas,monospace !important;' +
'white-space:nowrap !important;line-height:1.5 !important;' +
'pointer-events:none !important;z-index:999992 !important;' +
'box-shadow:0 0 6px rgba(200,30,30,0.55) !important;'
);
var timeout = factionChain.timeout > 0 ? fmtUntil(factionChain.timeout) : '';
badge.textContent = '⛓' + factionChain.current + (timeout ? ' · ' + timeout : '');
toggleEl.style.position = 'fixed'; // ensure relative context
toggleEl.style.overflow = 'visible';
toggleEl.appendChild(badge);
}
/* ─────────────────────────────────────────
CHAIN EXPORT
───────────────────────────────────────── */
function exportChain() {
if (!chainCache.length) { toast('⚠ No chain targets to export'); return; }
var lines = chainCache.map(function(m, i) {
return (i + 1) + '. ' + m.name + ' [' + m.id + '] | Lv' + m.level + ' | ' + m.status.label + ' | https://www.torn.com/profiles.php?XID=' + m.id;
});
var text = '=== WTT Chain Targets — ' + new Date().toLocaleString() + ' ===\n' + lines.join('\n');
if (navigator.clipboard && navigator.clipboard.writeText) {
navigator.clipboard.writeText(text).then(function() { toast('✓ Copied to clipboard!'); }).catch(function() { downloadText(text); });
} else { downloadText(text); }
}
function downloadText(text) {
var a = document.createElement('a');
a.href = 'data:text/plain;charset=utf-8,' + encodeURIComponent(text);
a.download = 'wtt-chain-' + Date.now() + '.txt';
document.body.appendChild(a); a.click(); setTimeout(function() { a.remove(); }, 500);
toast('✓ Chain list downloaded!');
}
/* ─────────────────────────────────────────
ADD CHAIN TARGET
───────────────────────────────────────── */
async function addChainTarget(rawId) {
var id = cleanId(rawId);
console.log('[TargetTracker] addChainTarget id=' + id + ' raw=' + rawId);
if (!id) { toast('⚠ Enter a numeric player ID'); return; }
if (!cfg.apiKey) { toast('⚠ Add your API key in Settings first'); return; }
var ids = cfg.getChainIds();
if (ids.indexOf(id) !== -1) { toast('Already tracking player ' + id); return; }
toast('Looking up player ' + id + '…', 3000);
var player = await fetchPlayer(id);
if (!player) return;
ids.push(id);
cfg.setChainIds(ids);
chainCache.push(player);
toast('✓ Added ' + player.name + ' to chain list');
renderChainBody();
renderStatusBar();
}
/* ─────────────────────────────────────────
MAIN PANEL RENDER
───────────────────────────────────────── */
function syncWarTabVisibility() {
if (!panelEl) return;
var tabWar = panelEl.querySelector('[data-tab="war"]');
if (!tabWar) return;
var inWar = !!warScore;
tabWar.style.display = inWar ? '' : 'none';
// If we're on the war tab but war ended, switch to dash
if (!inWar && activeTab === 'war') {
activeTab = 'dash';
}
}
function renderPanel() {
if (!panelEl) return;
renderStatusBar();
// show/hide filter bar
var filters = document.getElementById('wtt-filters');
if (filters) filters.style.display = activeTab !== 'dash' ? 'flex' : 'none';
if (activeTab === 'dash') renderDashBody();
if (activeTab === 'war') renderWarBody();
if (activeTab === 'chain') renderChainBody();
}
/* ─────────────────────────────────────────
BUILD PANEL
───────────────────────────────────────── */
function buildPanel() {
// Inline style the entire panel shell
panelEl.setAttribute('style',
'position:fixed !important;' +
'width:0 !important;' +
'opacity:0 !important;' +
'max-height:84vh !important;' +
'background:linear-gradient(158deg,rgba(16,4,4,0.98),rgba(9,9,11,0.97)) !important;' +
'border:1px solid rgba(155,18,18,0.4) !important;' +
'border-radius:11px !important;' +
'z-index:999989 !important;' +
'display:flex !important;' +
'flex-direction:column !important;' +
'box-shadow:0 10px 44px 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:#d4d4d4 !important;'
);
// ── HEADER ──
var hdr = document.createElement('div');
hdr.setAttribute('style', 'padding:8px 12px;background:linear-gradient(90deg,rgba(155,18,18,0.3),transparent);border-bottom:1px solid rgba(155,18,18,0.28);display:flex;align-items:center;gap:7px;flex-shrink:0;');
var titleEl = document.createElement('span');
titleEl.textContent = '🎯 Target Tracker';
titleEl.setAttribute('style', 'font-size:10.5px;font-weight:700;letter-spacing:1.5px;text-transform:uppercase;color:#dd4444;flex:1;white-space:nowrap;font-family:Arial,sans-serif;');
// chain timer now lives inside chain tab, not header
function iconBtn(svg, title) {
var 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:rgba(175,175,175,0.55);transition:color 0.18s;');
b.addEventListener('mouseover', function() { b.style.color = '#dd5555'; });
b.addEventListener('mouseout', function() { b.style.color = 'rgba(175,175,175,0.55)'; });
return b;
}
var 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>';
var 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>';
var btnRefresh = iconBtn(SVG_REFRESH, 'Refresh data');
var btnSettings = iconBtn(SVG_GEAR, 'Settings');
btnRefresh.addEventListener('click', function() {
stopPolling();
selfData = null; warRoster = []; chainCache = []; factionChain = null; ownFactionData = null;
lastWarsFetch = 0; // force wars re-fetch immediately
refreshAll().then(startPolling);
toast('Refreshing…', 1500);
});
btnSettings.addEventListener('click', openSettings);
hdr.appendChild(titleEl); hdr.appendChild(btnRefresh); hdr.appendChild(btnSettings);
// ── TABS ──
var tabs = document.createElement('div');
tabs.setAttribute('style', 'display:flex;flex-shrink:0;border-bottom:1px solid rgba(75,8,8,0.5);');
function makeTab(label, key) {
var 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:rgba(175,175,175,0.48);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', function() { if (!t.classList.contains('wtt-tab-active')) t.style.color = 'rgba(215,90,90,0.85)'; });
t.addEventListener('mouseout', function() { if (!t.classList.contains('wtt-tab-active')) t.style.color = 'rgba(175,175,175,0.48)'; });
t.addEventListener('click', function() {
tabs.querySelectorAll('[data-tab]').forEach(function(x) {
x.classList.remove('wtt-tab-active');
x.style.color = 'rgba(175,175,175,0.48)';
x.style.borderBottomColor = 'transparent';
x.style.background = 'transparent';
});
t.classList.add('wtt-tab-active');
t.style.color = '#dd4444';
t.style.borderBottomColor = '#b82828';
t.style.background = 'rgba(130,8,8,0.1)';
activeTab = key; activeFilter = 'all';
setFilterActive('all');
renderPanel();
});
return t;
}
var tabDash = makeTab('📟 Dash', 'dash');
var tabWar = makeTab('⚔ War', 'war');
var tabChain = makeTab('🔗 Chain', 'chain');
// activate dash by default (or war if already on war tab)
var startTab = (activeTab === 'war' && warScore) ? tabWar : tabDash;
startTab.classList.add('wtt-tab-active');
startTab.style.color = '#dd4444'; startTab.style.borderBottomColor = '#b82828'; startTab.style.background = 'rgba(130,8,8,0.1)';
// War tab only visible when at war
tabWar.style.display = warScore ? '' : 'none';
tabs.appendChild(tabDash); tabs.appendChild(tabWar); tabs.appendChild(tabChain);
// ── FILTERS ──
var filterBar = document.createElement('div');
filterBar.id = 'wtt-filters';
filterBar.setAttribute('style', 'display:none;gap:4px;padding:5px 8px;flex-shrink:0;flex-wrap:wrap;border-bottom:1px solid rgba(55,8,8,0.4);');
var FILTERS = [['all','ALL',''],['okay','🟢 OKAY',C.okay],['hosp','🏥 HOSP',C.hosp],['travel','✈ TRAVEL',C.travel],['jail','🔒 JAIL',C.jail]];
function setFilterActive(key) {
filterBar.querySelectorAll('[data-filter]').forEach(function(f) {
var col = FILTERS.find(function(x) { return x[0] === f.dataset.filter; });
var active = f.dataset.filter === key;
f.style.color = active ? (col[2] || C.redDim) : 'rgba(155,155,155,0.72)';
f.style.borderColor = active ? (col[2] || C.redDim) : 'rgba(75,75,75,0.28)';
f.style.background = active ? 'rgba(170,16,16,0.18)' : 'transparent';
});
activeFilter = key;
}
FILTERS.forEach(function(f) {
var btn = document.createElement('span');
btn.dataset.filter = f[0];
btn.textContent = f[1];
btn.setAttribute('style', 'padding:2px 7px;font-size:9px;font-weight:600;border-radius:3px;border:1px solid rgba(75,75,75,0.28);color:rgba(155,155,155,0.72);cursor:pointer;user-select:none;font-family:Consolas,monospace;');
if (f[0] === 'all') { btn.style.color = C.redDim; btn.style.borderColor = C.redDim; btn.style.background = 'rgba(170,16,16,0.18)'; }
btn.addEventListener('click', function() { setFilterActive(f[0]); renderPanel(); });
filterBar.appendChild(btn);
});
// ── STATUS BAR ──
var sbar = document.createElement('div');
sbar.id = 'wtt-sbar';
sbar.setAttribute('style', 'padding:3px 10px;font-size:9px;letter-spacing:.4px;color:rgba(175,175,175,0.5);border-bottom:1px solid rgba(45,8,8,0.4);display:flex;gap:8px;align-items:center;flex-shrink:0;font-family:Consolas,monospace;');
sbar.textContent = 'Loading…';
// ── BODY ──
var body = document.createElement('div');
body.className = 'wtt-body';
panelEl.appendChild(hdr);
panelEl.appendChild(tabs);
panelEl.appendChild(filterBar);
panelEl.appendChild(sbar);
panelEl.appendChild(body);
}
/* ─────────────────────────────────────────
TOGGLE BUTTON
───────────────────────────────────────── */
function buildToggle() {
var SVG = '<svg width="28" height="28" viewBox="0 0 44 44" fill="none">' +
'<circle cx="22" cy="28" r="13.5" stroke="rgba(215,44,44,.88)" stroke-width="1.5" fill="none"/>' +
'<circle cx="22" cy="28" r="9" stroke="rgba(215,44,44,.7)" stroke-width="1.2" fill="none"/>' +
'<circle cx="22" cy="28" r="4.5" stroke="rgba(215,44,44,.9)" stroke-width="1.2" fill="none"/>' +
'<circle cx="22" cy="28" r="1.9" fill="rgba(215,55,55,1)"/>' +
'<line x1="22" y1="13.5" x2="22" y2="18" stroke="rgba(215,44,44,.72)" stroke-width="1.3" stroke-linecap="round"/>' +
'<line x1="22" y1="38" x2="22" y2="42.5" stroke="rgba(215,44,44,.72)" stroke-width="1.3" stroke-linecap="round"/>' +
'<line x1="7.5" y1="28" x2="12" y2="28" stroke="rgba(215,44,44,.72)" stroke-width="1.3" stroke-linecap="round"/>' +
'<line x1="32" y1="28" x2="36.5" y2="28" stroke="rgba(215,44,44,.72)" stroke-width="1.3" stroke-linecap="round"/>' +
'<circle cx="22" cy="7.5" r="3.4" fill="rgba(195,44,44,.88)"/>' +
'<path d="M14.8 15.2 Q16.5 11.5 22 11.5 Q27.5 11.5 29.2 15.2" fill="rgba(195,44,44,.88)" stroke="none"/>' +
'</svg>';
toggleEl.innerHTML = SVG;
toggleEl.title = 'Target Tracker · Right-click = Settings';
toggleEl.setAttribute('style',
'position:fixed !important;' +
'width:44px !important;height:44px !important;' +
'background:radial-gradient(circle at 38% 34%,#3d0b0b,#180202) !important;' +
'border:2px solid rgba(185,28,28,0.75) !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 rgba(180,28,28,0.55),inset 0 0 12px rgba(0,0,0,0.55) !important;' +
'user-select:none !important;touch-action:none !important;overflow:visible !important;'
);
toggleEl.addEventListener('mouseover', function() {
toggleEl.style.setProperty('box-shadow', '0 0 24px rgba(220,55,55,0.75),inset 0 0 12px rgba(0,0,0,0.55)', 'important');
toggleEl.style.setProperty('border-color', 'rgba(220,55,55,0.95)', 'important');
});
toggleEl.addEventListener('mouseout', function() {
toggleEl.style.setProperty('box-shadow', '0 0 16px rgba(180,28,28,0.55),inset 0 0 12px rgba(0,0,0,0.55)', 'important');
toggleEl.style.setProperty('border-color', 'rgba(185,28,28,0.75)', 'important');
});
}
/* ─────────────────────────────────────────
PANEL OPEN/CLOSE + POSITION
───────────────────────────────────────── */
function positionPanel() {
var 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', '308px', '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() {
var SZ = 44, EDGE = 6;
function snap(side, top) {
toggleEl._side = side;
var 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 {
var saved = JSON.parse(store.get('wtt_pos', '{}'));
snap(saved.side || 'right', saved.top || 340);
} catch(e) { snap('right', 340); }
var dragging = false, moved = false, sX, sY, sL, sT;
function start(cx, cy) {
moved = false; dragging = true; sX = cx; sY = cy;
var 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;
var 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;
var r = toggleEl.getBoundingClientRect();
var side = (r.left + SZ / 2) < window.innerWidth / 2 ? 'left' : 'right';
snap(side, r.top);
store.set('wtt_pos', JSON.stringify({ side: side, top: r.top }));
}
toggleEl.addEventListener('mousedown', function(e) { if (e.button !== 0) return; start(e.clientX, e.clientY); });
document.addEventListener('mousemove', function(e) { move(e.clientX, e.clientY); });
document.addEventListener('mouseup', end);
toggleEl.addEventListener('touchstart', function(e) { var t = e.touches[0]; start(t.clientX, t.clientY); }, { passive: true });
toggleEl.addEventListener('touchmove', function(e) { if (!dragging) return; e.preventDefault(); var t = e.touches[0]; move(t.clientX, t.clientY); }, { passive: false });
toggleEl.addEventListener('touchend', end);
toggleEl.addEventListener('click', function() {
if (moved) return;
if (panelOpen) closePanel(); else openPanel();
});
toggleEl.addEventListener('contextmenu', function(e) { e.preventDefault(); openSettings(); });
/* ── Carousel Tooltip ── */
(function buildCarousel() {
var slides = [
{ icon:'🎯', title:'Target Tracker', body:'Track your faction war roster in real-time — statuses, filters, and one-tap attack buttons.' },
{ icon:'⚔', title:'Attack Button', body:'Green ⚔ ATK buttons appear next to Active targets. Tap to go straight to the attack page.' },
{ icon:'🔗', title:'Chain Target List', body:'Add players by ID to your chain list. Statuses update automatically with your war roster data.' },
{ icon:'📟', title:'Dashboard', body:'Monitor your Health & Energy bars and Quick Actions. Use Escape buttons to fast-travel to Mexico, Japan, or South Africa.' },
{ icon:'🔑', title:'API Key Security', body:'Your Full Access API key is masked by default. Tap 👁 in Settings to reveal it. Never share your key with anyone.' },
{ icon:'🔄', title:'Manual Refresh', body:'Tap the ↺ icon in the panel header to instantly re-fetch all statuses from the Torn API.' },
{ icon:'📱', title:'PDA Compatible', body:'Built for Torn PDA on mobile. All navigation uses native links for full app compatibility.' },
{ icon:'⚙', title:'First Time Setup', body:'Open Settings to enter your Full Access API Key, User ID, and Faction ID. Data is stored locally on your device only.' },
];
var cur = 0;
var tip = document.createElement('div');
tip.id = 'wtt-carousel';
tip.setAttribute('style',
'position:fixed !important;bottom:72px !important;right:12px !important;' +
'width:230px !important;background:linear-gradient(145deg,rgba(18,4,4,0.97),rgba(10,10,13,0.97)) !important;' +
'border:1px solid rgba(155,18,18,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:#d4d4d4 !important;' +
'opacity:0 !important;transition:opacity 0.35s ease !important;'
);
var iconEl = document.createElement('div');
var titleEl = document.createElement('div');
var bodyEl = document.createElement('div');
var dotsWrap = document.createElement('div');
var 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:#ff6060 !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(210,210,210,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) {
var 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(220,60,60,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 (var 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();
}
var prevBtn = document.createElement('div');
prevBtn.textContent = '◀';
prevBtn.setAttribute('style',
'cursor:pointer !important;font-size:12px !important;color:rgba(200,80,80,0.8) !important;' +
'padding:3px 8px !important;user-select:none !important;border-radius:4px !important;' +
'border:1px solid rgba(180,40,40,0.3) !important;'
);
prevBtn.addEventListener('click', function(){ clearInterval(autoTimer); go(cur - 1); });
var nextBtn = document.createElement('div');
nextBtn.textContent = '▶';
nextBtn.setAttribute('style',
'cursor:pointer !important;font-size:12px !important;color:rgba(200,80,80,0.8) !important;' +
'padding:3px 8px !important;user-select:none !important;border-radius:4px !important;' +
'border:1px solid rgba(180,40,40,0.3) !important;'
);
nextBtn.addEventListener('click', function(){ clearInterval(autoTimer); go(cur + 1); });
var 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(180,80,80,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);
var autoTimer = setInterval(function(){ go(cur + 1); }, 5000);
// Only show once — dismissed state persisted in localStorage
var SEEN_KEY = 'wtt_tooltip_seen';
var 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);
// Mark seen when user closes OR after last slide auto-advances past end
function markSeen() {
try { localStorage.setItem(SEEN_KEY, '1'); } catch(e) {}
}
closeBtn.addEventListener('click', markSeen);
// Also mark seen after they've cycled through all slides once
var seenCount = 0;
var origGo = go;
go = function(n) {
origGo(n);
seenCount++;
if (seenCount >= slides.length) markSeen();
};
})();
}
/* ─────────────────────────────────────────
CHAIN WATCHER ENGINE
• cwStart() — begin 1s tick + 60s API resync
• cwStop() — clear both timers
• cwTick() — runs every second: counts down, fires audio alerts
• cwResync() — fetches faction/?selections=chain, resets if new hit detected
• cwPlayBeep() — Web Audio API tone (no files needed)
• cwUpdateUI() — updates the floating chain watcher card if visible
───────────────────────────────────────── */
// Threshold definitions (seconds -> label)
var CW_THRESHOLDS = [
{ secs: 240, label: '4:00' },
{ secs: 180, label: '3:00' },
{ secs: 120, label: '2:00' },
{ secs: 60, label: '1:00' },
{ secs: 30, label: '0:30' },
{ secs: 10, label: '0:10' }
];
function cwPlayBeep(secsLeft) {
try {
var ctx = new (window.AudioContext || window.webkitAudioContext)();
var osc = ctx.createOscillator();
var gain = ctx.createGain();
osc.connect(gain);
gain.connect(ctx.destination);
// Pitch gets higher as timer gets lower (more urgent)
var freq = secsLeft <= 10 ? 880 : secsLeft <= 30 ? 660 : secsLeft <= 60 ? 520 : 440;
osc.frequency.setValueAtTime(freq, ctx.currentTime);
osc.type = 'sine';
gain.gain.setValueAtTime(0.25, ctx.currentTime);
gain.gain.exponentialRampToValueAtTime(0.001, ctx.currentTime + 0.35);
osc.start(ctx.currentTime);
osc.stop(ctx.currentTime + 0.35);
// Double-beep for <=10s
if (secsLeft <= 10) {
var osc2 = ctx.createOscillator();
var gain2 = ctx.createGain();
osc2.connect(gain2);
gain2.connect(ctx.destination);
osc2.frequency.setValueAtTime(freq, ctx.currentTime + 0.45);
osc2.type = 'sine';
gain2.gain.setValueAtTime(0.3, ctx.currentTime + 0.45);
gain2.gain.exponentialRampToValueAtTime(0.001, ctx.currentTime + 0.8);
osc2.start(ctx.currentTime + 0.45);
osc2.stop(ctx.currentTime + 0.8);
}
setTimeout(function(){ try { ctx.close(); } catch(e){} }, 1000);
} catch(e) { console.warn('[TargetTracker] Audio failed:', e); }
}
function cwTick() {
if (!cwEnabled) return;
if (cwCooldown) {
// Count down cooldown
cwSecsLeft = Math.max(0, cwSecsLeft - 1);
cwUpdateUI();
return;
}
cwSecsLeft = Math.max(0, cwSecsLeft - 1);
// Check audio thresholds
var enabled = cfg.getCwThresholds();
CW_THRESHOLDS.forEach(function(t) {
if (enabled.indexOf(t.secs) !== -1 && cwSecsLeft === t.secs && !cwAlerted[t.secs]) {
cwAlerted[t.secs] = true;
// ToS: only play audio if the user is actively viewing this page
if (document.visibilityState === 'visible') {
cwPlayBeep(t.secs);
}
}
});
if (cwSecsLeft === 0) {
// Chain expired — enter cooldown
cwCooldown = true;
}
cwUpdateUI();
}
async function cwResync() {
// ToS: only poll while user is actively viewing this page
if (!cwEnabled || !cfg.apiKey || !cfg.factionId) return;
if (document.visibilityState !== 'visible') return;
try {
var d = await gmFetch('https://api.torn.com/faction/' + cfg.factionId + '?selections=chain&key=' + cfg.apiKey);
if (!d || d.error || !d.chain) return;
var chain = d.chain;
var newCount = chain.current || 0;
var newTimeout = chain.timeout || 0;
var newCooldown= chain.cooldown || 0;
// Detect a new hit: chain count went up → reset 5:00 timer + clear alerts
if (newCount > cwCount) {
cwCount = newCount;
cwAlerted = {}; // reset so thresholds fire again on next countdown
cwCooldown = false;
var serverLeft = newTimeout > 0 ? newTimeout - Math.floor(Date.now() / 1000) : 300;
cwSecsLeft = Math.max(0, serverLeft);
// Also update factionChain for the badge
factionChain = { current: newCount, timeout: newTimeout, cooldown: newCooldown, modifier: chain.modifier || 0 };
updateChainBadge();
} else if (newCooldown > 0) {
cwCooldown = true;
cwSecsLeft = newCooldown;
} else if (newTimeout > 0) {
// Drift correction: sync to server time
var serverLeft = newTimeout - Math.floor(Date.now() / 1000);
if (Math.abs(serverLeft - cwSecsLeft) > 5) cwSecsLeft = Math.max(0, serverLeft);
cwCount = newCount;
} else {
// No active chain
cwCount = 0;
cwSecsLeft = 0;
cwCooldown = false;
}
cwUpdateUI();
} catch(e) { console.warn('[TargetTracker] resync error:', e); }
}
function cwStart() {
if (cwTickTimer) clearInterval(cwTickTimer);
if (cwPollTimer) clearInterval(cwPollTimer);
cwAlerted = {};
// Initial pull
cwResync();
cwTickTimer = setInterval(cwTick, 1000);
cwPollTimer = setInterval(cwResync, 60000);
console.log('[TargetTracker] Chain Watcher started');
}
function cwStop() {
if (cwTickTimer) { clearInterval(cwTickTimer); cwTickTimer = null; }
if (cwPollTimer) { clearInterval(cwPollTimer); cwPollTimer = null; }
cwSecsLeft = 0; cwCount = 0; cwCooldown = false; cwAlerted = {};
cwUpdateUI();
console.log('[TargetTracker] Chain Watcher stopped');
}
function cwFmtTime(secs) {
if (secs <= 0) return '0:00';
var m = Math.floor(secs / 60);
var s = secs % 60;
return m + ':' + (s < 10 ? '0' : '') + s;
}
function cwUpdateUI() {
var card = document.getElementById('wtt-cw-card');
if (!card) return;
var timerEl = document.getElementById('wtt-cw-timer');
var countEl = document.getElementById('wtt-cw-count');
var stateEl = document.getElementById('wtt-cw-state');
var barEl = document.getElementById('wtt-cw-bar');
if (!timerEl) return;
if (!cwEnabled || (cwCount === 0 && !cwCooldown)) {
timerEl.textContent = '--:--';
timerEl.style.color = 'rgba(150,100,100,0.6)';
if (countEl) countEl.textContent = '—';
if (stateEl) stateEl.textContent = cwEnabled ? 'Waiting for chain…' : 'Chain Watcher off';
if (barEl) { barEl.style.width = '0%'; barEl.style.background = 'rgba(100,100,100,0.3)'; }
card.style.borderColor = 'rgba(80,20,20,0.4)';
card.className = card.className.replace(/\s*wtt-urgent/g,'');
return;
}
if (cwCooldown) {
timerEl.textContent = cwFmtTime(cwSecsLeft);
timerEl.style.color = '#78a8ff';
if (countEl) countEl.textContent = cwCount.toLocaleString();
if (stateEl) stateEl.textContent = 'COOLDOWN';
if (barEl) { barEl.style.width = '100%'; barEl.style.background = '#78a8ff'; }
card.style.borderColor = 'rgba(60,80,180,0.5)';
card.className = card.className.replace(/\s*wtt-urgent/g,'');
return;
}
var pct = Math.min(100, Math.round(cwSecsLeft / 300 * 100));
// Colour scale: green (>2:00) → orange (2:00–0:30) → red (<0:30)
var color = cwSecsLeft <= 30 ? '#ff4444'
: cwSecsLeft <= 120 ? '#f07020'
: '#55cc55';
timerEl.textContent = cwFmtTime(cwSecsLeft);
timerEl.style.color = color;
if (countEl) countEl.textContent = cwCount.toLocaleString();
if (stateEl) stateEl.textContent = 'CHAINING';
if (barEl) { barEl.style.width = pct + '%'; barEl.style.background = color; }
card.style.borderColor = cwSecsLeft <= 120 ? color : 'rgba(80,20,20,0.4)';
if (cwSecsLeft <= 30 && !card.classList.contains('wtt-urgent')) {
card.classList.add('wtt-urgent');
} else if (cwSecsLeft > 30) {
card.className = card.className.replace(/\s*wtt-urgent/g,'');
}
}
/* ─────────────────────────────────────────
POLL LOOPS — independent timers per tier
───────────────────────────────────────── */
function startPolling() {
// Self/bars: every 60s — 1 call/min
async function selfLoop() {
// ToS: skip API calls when page is not actively being viewed
if (document.visibilityState === 'visible') await refreshSelf();
selfTimer = setTimeout(selfLoop, 60000);
}
// Faction (own + enemy roster): every 45s — 2 calls per cycle
async function factionLoop() {
if (document.visibilityState === 'visible') await refreshFaction();
pollTimer = setTimeout(factionLoop, 45000);
}
// Chain targets: every 90s — N calls (1 per tracked player)
async function chainLoop() {
if (document.visibilityState === 'visible') await refreshChain();
chainTimer = setTimeout(chainLoop, 90000);
}
selfTimer = setTimeout(selfLoop, 60000);
pollTimer = setTimeout(factionLoop, 45000);
chainTimer = setTimeout(chainLoop, 90000);
}
function stopPolling() {
clearTimeout(selfTimer);
clearTimeout(pollTimer);
clearTimeout(chainTimer);
clearTimeout(warsTimer);
}
/* ─────────────────────────────────────────
INIT
───────────────────────────────────────── */
function init() {
if (document.getElementById('wtt-toggle')) return;
try { injectCSS(); } catch(e) { console.warn('[TargetTracker] CSS inject failed (non-fatal):', e); }
toggleEl = document.createElement('div');
toggleEl.id = 'wtt-toggle';
panelEl = document.createElement('div');
panelEl.id = 'wtt-panel';
document.body.appendChild(panelEl);
document.body.appendChild(toggleEl);
buildToggle();
buildPanel();
setupDrag();
// Always show settings on first load if no API key
// Request notification permission for hosp-out alerts
if ('Notification' in window && Notification.permission === 'default') {
Notification.requestPermission().then(function(p) { notifGranted = p === 'granted'; });
} else if ('Notification' in window) {
notifGranted = Notification.permission === 'granted';
}
if (!cfg.apiKey) {
setTimeout(openSettings, 500);
} else {
refreshAll().then(function() {
startPolling();
cwEnabled = cfg.cwEnabled;
if (cwEnabled) cwStart();
});
}
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', function() { setTimeout(init, 400); });
} else {
setTimeout(init, 400);
}
})();