War target tools: travel warnings, available targets, med targets, sorting, shortlists
// ==UserScript==
// @name Torn War Tools
// @namespace https://www.torn.com/
// @version 2.3
// @description War target tools: travel warnings, available targets, med targets, sorting, shortlists
// @author Systoned
// @match https://www.torn.com/*
// @grant GM_setValue
// @grant GM_getValue
// @grant GM_xmlhttpRequest
// @connect api.torn.com
// ==/UserScript==
(function () {
'use strict';
// -- Settings ---------------------------------------------------------
function getApiKey() { return GM_getValue('wtw_api_key', ''); }
function getEnemyFactionId() { return GM_getValue('wtw_enemy_faction_id', ''); }
function setApiKey(v) { GM_setValue('wtw_api_key', v.trim()); }
function setEnemyFactionId(v) { GM_setValue('wtw_enemy_faction_id', v.trim()); }
function isConfigured() { return getApiKey() !== '' && getEnemyFactionId() !== ''; }
// -- Shortlist --------------------------------------------------------
function getShortlistKey() { return 'wtw_shortlist_' + getEnemyFactionId(); }
function getShortlist() {
try { return JSON.parse(GM_getValue(getShortlistKey(), '[]')); }
catch (e) { return []; }
}
function saveShortlist(list) { GM_setValue(getShortlistKey(), JSON.stringify(list)); }
function isShortlisted(id) { return getShortlist().indexOf(id) !== -1; }
function toggleShortlist(id) {
var list = getShortlist();
var idx = list.indexOf(id);
if (idx === -1) { list.push(id); } else { list.splice(idx, 1); }
saveShortlist(list);
}
function clearShortlistForFaction(factionId) {
GM_setValue('wtw_shortlist_' + factionId, '[]');
}
// -- Cache ------------------------------------------------------------
var memberCache = { data: null, time: 0 };
var CACHE_TTL = 60000; // 60 seconds
function getCachedMembers() {
if (memberCache.data && (Date.now() - memberCache.time) < CACHE_TTL) {
return Promise.resolve(memberCache.data);
}
return fetchEnemyMembers().then(function (members) {
memberCache.data = members;
memberCache.time = Date.now();
return members;
});
}
function prefetchMembers() {
if (!isConfigured()) return;
getCachedMembers().catch(function () {});
}
// -- API --------------------------------------------------------------
function fetchEnemyMembers() {
return new Promise(function (resolve, reject) {
GM_xmlhttpRequest({
method: 'GET',
url: 'https://api.torn.com/v2/faction/' + getEnemyFactionId() + '/members?striptags=true',
headers: {
'Authorization': 'ApiKey ' + getApiKey(),
'Accept': 'application/json'
},
onload: function (r) {
try {
var data = JSON.parse(r.responseText);
if (data.error) { reject(data.error.error || 'API error'); return; }
resolve(data.members || []);
} catch (e) { reject('Parse error'); }
},
onerror: function () { reject('Network error'); }
});
});
}
// -- Threat / Target Logic --------------------------------------------
function findThreats(members, countryName) {
var threats = [];
var cl = countryName.toLowerCase();
for (var i = 0; i < members.length; i++) {
var m = members[i];
var state = m.status && m.status.state;
var desc = m.status && m.status.description;
if (!desc) continue;
var dl = desc.toLowerCase();
if (state === 'Traveling' && dl.indexOf('traveling to ' + cl) !== -1) {
threats.push({ id: m.id, name: m.name, level: m.level, type: 'traveling', description: desc });
}
if (state === 'Abroad' && dl.indexOf('in ' + cl) !== -1) {
threats.push({ id: m.id, name: m.name, level: m.level, type: 'abroad', description: desc });
}
}
return threats;
}
var currentSort = 'level'; // 'level' or 'activity'
var sortAsc = false; // false = descending (default), true = ascending
var activityOrder = { 'Online': 0, 'Idle': 1, 'Offline': 2, 'Unknown': 3 };
function parseRelativeTime(str) {
// Convert "X minutes ago", "X hours ago" etc to seconds
if (!str || str === 'Unknown') return Infinity;
var match = str.match(/(\d+)\s*(second|minute|hour|day|week|month|year)/i);
if (!match) return Infinity;
var num = parseInt(match[1], 10);
var unit = match[2].toLowerCase();
var multipliers = { second: 1, minute: 60, hour: 3600, day: 86400, week: 604800, month: 2592000, year: 31536000 };
return num * (multipliers[unit] || 1);
}
function getAvailableTargets(members, sortMode, ascending) {
var targets = [];
for (var i = 0; i < members.length; i++) {
var m = members[i];
var state = m.status && m.status.state;
if (state === 'Okay') {
targets.push({
id: m.id, name: m.name, level: m.level,
lastAction: m.last_action ? m.last_action.relative : 'Unknown',
actionStatus: m.last_action ? m.last_action.status : 'Unknown',
lastActionSeconds: parseRelativeTime(m.last_action ? m.last_action.relative : 'Unknown')
});
}
}
if (sortMode === 'activity') {
targets.sort(function (a, b) {
var ao = activityOrder[a.actionStatus] !== undefined ? activityOrder[a.actionStatus] : 3;
var bo = activityOrder[b.actionStatus] !== undefined ? activityOrder[b.actionStatus] : 3;
if (ao !== bo) return ascending ? bo - ao : ao - bo;
// Within same status group, sort by recency
var recency = a.lastActionSeconds - b.lastActionSeconds;
return ascending ? -recency : recency;
});
} else {
targets.sort(function (a, b) {
return ascending ? a.level - b.level : b.level - a.level;
});
}
// Pin shortlisted to top
var shortlist = getShortlist();
if (shortlist.length > 0) {
var pinned = [];
var rest = [];
for (var j = 0; j < targets.length; j++) {
if (shortlist.indexOf(targets[j].id) !== -1) {
pinned.push(targets[j]);
} else {
rest.push(targets[j]);
}
}
targets = pinned.concat(rest);
}
return targets;
}
function getMedTargets(members) {
var targets = [];
var now = Math.floor(Date.now() / 1000);
for (var i = 0; i < members.length; i++) {
var m = members[i];
var state = m.status && m.status.state;
if (state === 'Hospital') {
var until = m.status.until || 0;
var remaining = until - now;
if (remaining < 0) remaining = 0;
targets.push({
id: m.id, name: m.name, level: m.level,
description: m.status.description || '',
until: until,
remaining: remaining
});
}
}
targets.sort(function (a, b) { return a.remaining - b.remaining; });
return targets;
}
function formatTime(seconds) {
if (seconds <= 0) return 'Now';
var h = Math.floor(seconds / 3600);
var m = Math.floor((seconds % 3600) / 60);
var s = seconds % 60;
var parts = [];
if (h > 0) parts.push(h + 'h');
if (m > 0) parts.push(m + 'm');
if (h === 0) parts.push(s + 's');
return parts.join(' ');
}
// -- Styles -----------------------------------------------------------
function injectStyles() {
var style = document.createElement('style');
style.textContent = [
// Modal overlay
'.wtw-overlay {',
' position: fixed;',
' top: 0; left: 0; right: 0; bottom: 0;',
' background: rgba(0, 0, 0, 0.75);',
' z-index: 999999;',
' display: flex;',
' align-items: center;',
' justify-content: center;',
' padding: 20px;',
'}',
'.wtw-modal {',
' background: #1a1a1a;',
' border: 1px solid #444;',
' border-radius: 8px;',
' padding: 20px;',
' max-width: 500px;',
' width: 100%;',
' max-height: 80vh;',
' overflow-y: auto;',
' color: #e0e0e0;',
' font-family: Arial, Helvetica, sans-serif;',
' font-size: 13px;',
'}',
'.wtw-modal-warn { border-color: #cc3333; }',
// Header
'.wtw-header {',
' display: flex;',
' justify-content: space-between;',
' align-items: center;',
' margin: 0 0 16px 0;',
'}',
'.wtw-header-title {',
' font-size: 16px;',
' font-weight: bold;',
' color: #ccc;',
'}',
'.wtw-close {',
' background: none;',
' border: none;',
' color: #666;',
' font-size: 20px;',
' cursor: pointer;',
' padding: 0 4px;',
' line-height: 1;',
'}',
'.wtw-close:hover { color: #ccc; }',
// Buttons row
'.wtw-actions {',
' display: flex;',
' gap: 8px;',
' margin: 0 0 16px 0;',
' flex-wrap: wrap;',
'}',
'.wtw-action-btn {',
' flex: 1;',
' min-width: 120px;',
' padding: 8px 12px;',
' border: 1px solid #444;',
' background: #222;',
' color: #ccc;',
' border-radius: 4px;',
' cursor: pointer;',
' font-size: 12px;',
' font-weight: bold;',
' text-align: center;',
'}',
'.wtw-action-btn:hover { border-color: #666; color: #fff; }',
'.wtw-action-btn.wtw-active { border-color: #4a4; color: #4a4; }',
// Settings form
'.wtw-form-row { margin: 0 0 10px 0; }',
'.wtw-form-row label {',
' display: block;',
' color: #999;',
' margin: 0 0 3px 0;',
' font-size: 11px;',
'}',
'.wtw-form-row input {',
' width: 100%;',
' box-sizing: border-box;',
' background: #111;',
' border: 1px solid #444;',
' color: #ccc;',
' padding: 6px 8px;',
' border-radius: 3px;',
' font-size: 12px;',
' font-family: monospace;',
'}',
'.wtw-form-row input:focus { outline: none; border-color: #666; }',
'.wtw-form-buttons { display: flex; gap: 8px; margin-top: 12px; }',
'.wtw-form-btn {',
' padding: 6px 16px;',
' border: none;',
' border-radius: 3px;',
' font-size: 12px;',
' cursor: pointer;',
' font-weight: bold;',
'}',
'.wtw-form-save { background: #4a4; color: #fff; }',
'.wtw-form-save:hover { background: #5b5; }',
'.wtw-form-cancel { background: #333; color: #999; }',
'.wtw-form-cancel:hover { background: #444; color: #ccc; }',
// Settings bar (shown when configured)
'.wtw-settings-bar {',
' display: flex;',
' align-items: center;',
' justify-content: space-between;',
' padding: 6px 0;',
' margin: 0 0 12px 0;',
' border-bottom: 1px solid #333;',
' font-size: 11px;',
'}',
'.wtw-settings-left {',
' display: flex;',
' align-items: center;',
' gap: 8px;',
'}',
'.wtw-dot {',
' width: 8px; height: 8px;',
' border-radius: 50%;',
' background: #4a4;',
'}',
'.wtw-settings-edit {',
' background: none;',
' border: 1px solid #444;',
' color: #999;',
' padding: 2px 8px;',
' border-radius: 3px;',
' cursor: pointer;',
' font-size: 11px;',
'}',
'.wtw-settings-edit:hover { border-color: #666; color: #ccc; }',
// Target list
'.wtw-list { margin: 0; padding: 0; list-style: none; }',
'.wtw-list-item {',
' background: #222;',
' border-left: 3px solid #444;',
' padding: 8px 12px;',
' margin: 0 0 6px 0;',
' border-radius: 0 4px 4px 0;',
'}',
'.wtw-list-item-ok { border-left-color: #4a4; }',
'.wtw-list-item-hosp { border-left-color: #cc8800; }',
'.wtw-list-item-threat-traveling { border-left-color: #cc8800; }',
'.wtw-list-item-threat-abroad { border-left-color: #cc3333; }',
'.wtw-list-name {',
' font-weight: bold;',
' color: #fff;',
'}',
'.wtw-list-name a { color: #fff; text-decoration: underline; }',
'.wtw-list-name a:hover { color: #cc3333; }',
'.wtw-list-detail {',
' color: #999;',
' font-size: 11px;',
' margin-top: 2px;',
'}',
'.wtw-tag {',
' display: inline-block;',
' padding: 1px 6px;',
' border-radius: 3px;',
' font-size: 10px;',
' font-weight: bold;',
' margin-left: 6px;',
'}',
'.wtw-tag-online { background: #1a331a; color: #4a4; }',
'.wtw-tag-idle { background: #332e1a; color: #cc8800; }',
'.wtw-tag-offline { background: #2a2a2a; color: #666; }',
'.wtw-tag-traveling { background: #332200; color: #cc8800; }',
'.wtw-tag-abroad { background: #331111; color: #cc3333; }',
// Status / empty
'.wtw-status {',
' text-align: center;',
' color: #666;',
' padding: 20px 0;',
'}',
'.wtw-count {',
' color: #999;',
' font-size: 11px;',
' margin: 0 0 10px 0;',
'}',
// Travel warning title
'.wtw-warn-title {',
' color: #cc3333;',
' font-size: 16px;',
' font-weight: bold;',
' margin: 0 0 4px 0;',
'}',
'.wtw-warn-sub {',
' color: #999;',
' font-size: 12px;',
' margin: 0 0 14px 0;',
'}',
'.wtw-dismiss {',
' display: block;',
' width: 100%;',
' padding: 10px;',
' border: none;',
' border-radius: 4px;',
' background: #cc3333;',
' color: #fff;',
' font-size: 13px;',
' font-weight: bold;',
' cursor: pointer;',
' margin-top: 14px;',
'}',
'.wtw-dismiss:hover { background: #aa2222; }',
// Sidebar icon
'.wtw-sidebar-icon path { fill: currentColor; }',
// Sort controls
'.wtw-sort-bar {',
' display: flex;',
' align-items: center;',
' gap: 6px;',
' margin: 0 0 10px 0;',
' font-size: 11px;',
' color: #666;',
'}',
'.wtw-sort-label { color: #666; }',
'.wtw-sort-btn {',
' background: none;',
' border: 1px solid #333;',
' color: #999;',
' padding: 2px 8px;',
' border-radius: 3px;',
' cursor: pointer;',
' font-size: 11px;',
'}',
'.wtw-sort-btn:hover { border-color: #555; color: #ccc; }',
'.wtw-sort-btn.wtw-sort-active { border-color: #4a4; color: #4a4; }',
// Shortlist checkbox
'.wtw-shortlist-cb {',
' float: right;',
' cursor: pointer;',
' width: 14px;',
' height: 14px;',
' accent-color: #4a4;',
' margin: 2px 0 0 0;',
'}',
// Shortlist header
'.wtw-list-header {',
' display: flex;',
' justify-content: space-between;',
' font-size: 10px;',
' color: #666;',
' padding: 0 12px 4px 12px;',
' text-transform: uppercase;',
'}',
// Sort arrow
'.wtw-sort-arrow {',
' margin-left: 3px;',
' font-size: 9px;',
'}',
// Pinned separator
'.wtw-pinned-sep {',
' border: none;',
' border-top: 1px dashed #444;',
' margin: 8px 0;',
'}'
].join('\n');
document.head.appendChild(style);
}
// -- Modal Framework --------------------------------------------------
var currentOverlay = null;
function closeModal() {
if (currentOverlay) {
currentOverlay.remove();
currentOverlay = null;
}
}
function createOverlay(warnStyle) {
closeModal();
var overlay = document.createElement('div');
overlay.className = 'wtw-overlay';
overlay.addEventListener('click', function (e) {
if (e.target === overlay) closeModal();
});
var modal = document.createElement('div');
modal.className = 'wtw-modal' + (warnStyle ? ' wtw-modal-warn' : '');
overlay.appendChild(modal);
document.body.appendChild(overlay);
currentOverlay = overlay;
return modal;
}
// -- Main War Modal ---------------------------------------------------
var currentView = null;
function openWarModal() {
var modal = createOverlay(false);
var header = document.createElement('div');
header.className = 'wtw-header';
var title = document.createElement('span');
title.className = 'wtw-header-title';
title.textContent = 'War Tools';
var closeBtn = document.createElement('button');
closeBtn.className = 'wtw-close';
closeBtn.textContent = '\u00D7';
closeBtn.addEventListener('click', closeModal);
header.appendChild(title);
header.appendChild(closeBtn);
modal.appendChild(header);
var content = document.createElement('div');
content.id = 'wtw-modal-content';
modal.appendChild(content);
if (!isConfigured()) {
renderSettingsForm(content, false);
} else {
renderConfiguredView(content);
}
}
function renderConfiguredView(container) {
container.innerHTML = '';
// Settings bar
var bar = document.createElement('div');
bar.className = 'wtw-settings-bar';
var left = document.createElement('div');
left.className = 'wtw-settings-left';
var dot = document.createElement('span');
dot.className = 'wtw-dot';
var info = document.createElement('span');
info.style.color = '#999';
info.textContent = 'Enemy Faction: ' + getEnemyFactionId();
left.appendChild(dot);
left.appendChild(info);
var editBtn = document.createElement('button');
editBtn.className = 'wtw-settings-edit';
editBtn.textContent = 'Edit';
editBtn.addEventListener('click', function () {
renderSettingsForm(container, true);
});
bar.appendChild(left);
bar.appendChild(editBtn);
container.appendChild(bar);
// Action buttons
var actions = document.createElement('div');
actions.className = 'wtw-actions';
var availBtn = document.createElement('button');
availBtn.className = 'wtw-action-btn';
availBtn.textContent = 'Available Targets';
availBtn.addEventListener('click', function () {
setActiveBtn(availBtn);
loadAvailableTargets(listArea);
});
var medBtn = document.createElement('button');
medBtn.className = 'wtw-action-btn';
medBtn.textContent = 'Med Targets';
medBtn.addEventListener('click', function () {
setActiveBtn(medBtn);
loadMedTargets(listArea);
});
actions.appendChild(availBtn);
actions.appendChild(medBtn);
container.appendChild(actions);
var listArea = document.createElement('div');
listArea.id = 'wtw-list-area';
container.appendChild(listArea);
function setActiveBtn(active) {
var btns = actions.querySelectorAll('.wtw-action-btn');
for (var i = 0; i < btns.length; i++) {
btns[i].classList.remove('wtw-active');
}
active.classList.add('wtw-active');
}
}
function renderSettingsForm(container, hasCancel) {
container.innerHTML = '';
var row1 = document.createElement('div');
row1.className = 'wtw-form-row';
var lbl1 = document.createElement('label');
lbl1.textContent = 'Torn API Key';
var inp1 = document.createElement('input');
inp1.type = 'password';
inp1.placeholder = 'Enter your API key';
inp1.value = getApiKey();
row1.appendChild(lbl1);
row1.appendChild(inp1);
var row2 = document.createElement('div');
row2.className = 'wtw-form-row';
var lbl2 = document.createElement('label');
lbl2.textContent = 'Enemy Faction ID';
var inp2 = document.createElement('input');
inp2.type = 'text';
inp2.placeholder = 'e.g. 32395';
inp2.value = getEnemyFactionId();
row2.appendChild(lbl2);
row2.appendChild(inp2);
var buttons = document.createElement('div');
buttons.className = 'wtw-form-buttons';
var saveBtn = document.createElement('button');
saveBtn.className = 'wtw-form-btn wtw-form-save';
saveBtn.textContent = 'Save';
saveBtn.addEventListener('click', function () {
var key = inp1.value.trim();
var fid = inp2.value.trim();
if (!key) { inp1.style.borderColor = '#cc3333'; return; }
if (!fid || isNaN(fid)) { inp2.style.borderColor = '#cc3333'; return; }
var oldFid = getEnemyFactionId();
setApiKey(key);
setEnemyFactionId(fid);
if (oldFid && oldFid !== fid) { clearShortlistForFaction(oldFid); }
memberCache.data = null;
memberCache.time = 0;
prefetchMembers();
renderConfiguredView(container);
});
buttons.appendChild(saveBtn);
if (hasCancel) {
var cancelBtn = document.createElement('button');
cancelBtn.className = 'wtw-form-btn wtw-form-cancel';
cancelBtn.textContent = 'Cancel';
cancelBtn.addEventListener('click', function () {
renderConfiguredView(container);
});
buttons.appendChild(cancelBtn);
}
container.appendChild(row1);
container.appendChild(row2);
container.appendChild(buttons);
}
// -- List Renderers ---------------------------------------------------
function loadAvailableTargets(container) {
container.innerHTML = '';
var status = document.createElement('div');
status.className = 'wtw-status';
status.textContent = 'Loading...';
container.appendChild(status);
getCachedMembers().then(function (members) {
container.innerHTML = '';
var targets = getAvailableTargets(members, currentSort, sortAsc);
var shortlist = getShortlist();
// Sort bar
var sortBar = document.createElement('div');
sortBar.className = 'wtw-sort-bar';
var sortLabel = document.createElement('span');
sortLabel.className = 'wtw-sort-label';
sortLabel.textContent = 'Sort:';
sortBar.appendChild(sortLabel);
var levelBtn = document.createElement('button');
levelBtn.className = 'wtw-sort-btn' + (currentSort === 'level' ? ' wtw-sort-active' : '');
levelBtn.innerHTML = 'Level' + (currentSort === 'level' ? '<span class="wtw-sort-arrow">' + (sortAsc ? '▲' : '▼') + '</span>' : '');
levelBtn.addEventListener('click', function () {
if (currentSort === 'level') {
sortAsc = !sortAsc;
} else {
currentSort = 'level';
sortAsc = false;
}
loadAvailableTargets(container);
});
sortBar.appendChild(levelBtn);
var actBtn = document.createElement('button');
actBtn.className = 'wtw-sort-btn' + (currentSort === 'activity' ? ' wtw-sort-active' : '');
actBtn.innerHTML = 'Last Active' + (currentSort === 'activity' ? '<span class="wtw-sort-arrow">' + (sortAsc ? '▲' : '▼') + '</span>' : '');
actBtn.addEventListener('click', function () {
if (currentSort === 'activity') {
sortAsc = !sortAsc;
} else {
currentSort = 'activity';
sortAsc = false;
}
loadAvailableTargets(container);
});
sortBar.appendChild(actBtn);
container.appendChild(sortBar);
var count = document.createElement('div');
count.className = 'wtw-count';
var pinnedCount = 0;
for (var c = 0; c < targets.length; c++) {
if (shortlist.indexOf(targets[c].id) !== -1) pinnedCount++;
}
count.textContent = targets.length + ' available target' + (targets.length !== 1 ? 's' : '') +
(pinnedCount > 0 ? ' (' + pinnedCount + ' shortlisted)' : '');
container.appendChild(count);
if (targets.length === 0) {
var empty = document.createElement('div');
empty.className = 'wtw-status';
empty.textContent = 'No available targets';
container.appendChild(empty);
return;
}
// Column header
var listHeader = document.createElement('div');
listHeader.className = 'wtw-list-header';
var targetLabel = document.createElement('span');
targetLabel.textContent = 'Target';
var shortlistLabel = document.createElement('span');
shortlistLabel.textContent = 'Shortlist';
listHeader.appendChild(targetLabel);
listHeader.appendChild(shortlistLabel);
container.appendChild(listHeader);
var list = document.createElement('ul');
list.className = 'wtw-list';
var separatorAdded = false;
for (var i = 0; i < targets.length; i++) {
var t = targets[i];
var isPinned = shortlist.indexOf(t.id) !== -1;
// Add separator after last pinned item
if (!isPinned && !separatorAdded && pinnedCount > 0) {
var sep = document.createElement('hr');
sep.className = 'wtw-pinned-sep';
list.appendChild(sep);
separatorAdded = true;
}
var li = document.createElement('li');
li.className = 'wtw-list-item wtw-list-item-ok';
var cb = document.createElement('input');
cb.type = 'checkbox';
cb.className = 'wtw-shortlist-cb';
cb.checked = isPinned;
cb.title = isPinned ? 'Remove from shortlist' : 'Add to shortlist';
(function (tid) {
cb.addEventListener('change', function () {
toggleShortlist(tid);
loadAvailableTargets(container);
});
})(t.id);
li.appendChild(cb);
var nameEl = document.createElement('div');
nameEl.className = 'wtw-list-name';
var link = document.createElement('a');
link.href = 'https://www.torn.com/profiles.php?XID=' + t.id;
link.target = '_blank';
link.textContent = t.name;
nameEl.appendChild(link);
var tag = document.createElement('span');
var tagClass = 'wtw-tag wtw-tag-' + t.actionStatus.toLowerCase();
tag.className = tagClass;
tag.textContent = t.actionStatus;
nameEl.appendChild(tag);
var detail = document.createElement('div');
detail.className = 'wtw-list-detail';
detail.textContent = 'Level ' + t.level + ' \u2014 ' + t.lastAction;
li.appendChild(nameEl);
li.appendChild(detail);
list.appendChild(li);
}
container.appendChild(list);
}).catch(function (err) {
container.innerHTML = '';
var errEl = document.createElement('div');
errEl.className = 'wtw-status';
errEl.style.color = '#cc3333';
errEl.textContent = 'Error: ' + err;
container.appendChild(errEl);
});
}
function loadMedTargets(container) {
container.innerHTML = '';
var status = document.createElement('div');
status.className = 'wtw-status';
status.textContent = 'Loading...';
container.appendChild(status);
getCachedMembers().then(function (members) {
container.innerHTML = '';
var targets = getMedTargets(members);
var count = document.createElement('div');
count.className = 'wtw-count';
count.textContent = targets.length + ' hospitalised member' + (targets.length !== 1 ? 's' : '');
container.appendChild(count);
if (targets.length === 0) {
var empty = document.createElement('div');
empty.className = 'wtw-status';
empty.textContent = 'No members in hospital';
container.appendChild(empty);
return;
}
var list = document.createElement('ul');
list.className = 'wtw-list';
for (var i = 0; i < targets.length; i++) {
var t = targets[i];
var li = document.createElement('li');
li.className = 'wtw-list-item wtw-list-item-hosp';
var isPinned = isShortlisted(t.id);
var cb = document.createElement('input');
cb.type = 'checkbox';
cb.className = 'wtw-shortlist-cb';
cb.checked = isPinned;
cb.title = isPinned ? 'Remove from shortlist' : 'Add to shortlist';
(function (tid) {
cb.addEventListener('change', function () {
toggleShortlist(tid);
loadMedTargets(container);
});
})(t.id);
li.appendChild(cb);
var nameEl = document.createElement('div');
nameEl.className = 'wtw-list-name';
var link = document.createElement('a');
link.href = 'https://www.torn.com/profiles.php?XID=' + t.id;
link.target = '_blank';
link.textContent = t.name;
nameEl.appendChild(link);
var tag = document.createElement('span');
tag.className = 'wtw-tag wtw-tag-idle';
tag.textContent = formatTime(t.remaining);
nameEl.appendChild(tag);
var detail = document.createElement('div');
detail.className = 'wtw-list-detail';
detail.textContent = 'Level ' + t.level + ' \u2014 ' + t.description;
li.appendChild(nameEl);
li.appendChild(detail);
list.appendChild(li);
}
container.appendChild(list);
}).catch(function (err) {
container.innerHTML = '';
var errEl = document.createElement('div');
errEl.className = 'wtw-status';
errEl.style.color = '#cc3333';
errEl.textContent = 'Error: ' + err;
container.appendChild(errEl);
});
}
// -- Travel Warning Modal ---------------------------------------------
function showTravelWarning(countryName, threats) {
var modal = createOverlay(true);
var title = document.createElement('div');
title.className = 'wtw-warn-title';
title.textContent = 'War Targets in ' + countryName;
var sub = document.createElement('div');
sub.className = 'wtw-warn-sub';
sub.textContent = threats.length + ' enemy member' + (threats.length !== 1 ? 's' : '') + ' detected:';
modal.appendChild(title);
modal.appendChild(sub);
var list = document.createElement('ul');
list.className = 'wtw-list';
for (var i = 0; i < threats.length; i++) {
var t = threats[i];
var li = document.createElement('li');
li.className = 'wtw-list-item wtw-list-item-threat-' + t.type;
var nameEl = document.createElement('div');
nameEl.className = 'wtw-list-name';
var link = document.createElement('a');
link.href = 'https://www.torn.com/profiles.php?XID=' + t.id;
link.target = '_blank';
link.textContent = t.name;
nameEl.appendChild(link);
var tag = document.createElement('span');
tag.className = 'wtw-tag wtw-tag-' + t.type;
tag.textContent = t.type === 'traveling' ? 'EN ROUTE' : 'IN COUNTRY';
nameEl.appendChild(tag);
var detail = document.createElement('div');
detail.className = 'wtw-list-detail';
detail.textContent = 'Level ' + t.level + ' \u2014 ' + t.description;
li.appendChild(nameEl);
li.appendChild(detail);
list.appendChild(li);
}
modal.appendChild(list);
var dismiss = document.createElement('button');
dismiss.className = 'wtw-dismiss';
dismiss.textContent = 'Dismiss';
dismiss.addEventListener('click', closeModal);
modal.appendChild(dismiss);
}
// -- Sidebar Injection ------------------------------------------------
var warSvg = '<svg xmlns="http://www.w3.org/2000/svg" class="wtw-sidebar-icon" stroke="transparent" stroke-width="0" width="16" height="16" viewBox="0 0 16 16"><path d="M8,0L0,3v5c0,4.4,3.4,8.5,8,10c4.6-1.5,8-5.6,8-10V3L8,0z M12,9H9v3H7V9H4V7h3V4h2v3h3V9z"/></svg>';
function injectDesktopSidebar() {
var travelNav = document.querySelector('#nav-travel_agency');
if (!travelNav) return false;
if (document.querySelector('#nav-war_tools')) return true;
var wrapper = document.createElement('div');
wrapper.className = travelNav.className;
wrapper.id = 'nav-war_tools';
var row = document.createElement('div');
row.className = 'area-row___iBD8N';
var link = document.createElement('a');
link.href = '#';
link.className = travelNav.querySelector('a') ? travelNav.querySelector('a').className : '';
link.addEventListener('click', function (e) {
e.preventDefault();
openWarModal();
});
var iconWrap = document.createElement('span');
iconWrap.className = 'svgIconWrap___AMIqR';
var iconInner = document.createElement('span');
iconInner.className = 'defaultIcon___iiNis mobile___paLva';
iconInner.innerHTML = warSvg;
iconWrap.appendChild(iconInner);
var nameSpan = document.createElement('span');
nameSpan.className = 'linkName___FoKha';
nameSpan.textContent = 'War Tools';
link.appendChild(iconWrap);
link.appendChild(nameSpan);
row.appendChild(link);
wrapper.appendChild(row);
travelNav.parentNode.insertBefore(wrapper, travelNav.nextSibling);
return true;
}
function injectMobileSidebar() {
var travelMobile = document.querySelector('#sidebar #nav-travel_agency');
if (!travelMobile) return false;
if (document.querySelector('#nav-war_tools_mobile')) return true;
var slide = travelMobile.closest('[class^="swiper-slide"]');
if (!slide) return false;
var newSlide = document.createElement('div');
newSlide.className = slide.className;
newSlide.style.cssText = slide.style.cssText;
var wrapper = document.createElement('div');
wrapper.className = travelMobile.className;
wrapper.id = 'nav-war_tools_mobile';
var row = document.createElement('div');
row.className = 'area-row___iBD8N';
var link = document.createElement('a');
link.href = '#';
link.tabIndex = 0;
var mobileLink = travelMobile.querySelector('a');
link.className = mobileLink ? mobileLink.className : '';
link.addEventListener('click', function (e) {
e.preventDefault();
openWarModal();
});
var iconWrap = document.createElement('span');
iconWrap.className = 'svgIconWrap___AMIqR';
var iconInner = document.createElement('span');
iconInner.className = 'defaultIcon___iiNis mobile___paLva';
iconInner.innerHTML = warSvg;
iconWrap.appendChild(iconInner);
var nameSpan = document.createElement('span');
nameSpan.textContent = 'War';
link.appendChild(iconWrap);
link.appendChild(nameSpan);
row.appendChild(link);
wrapper.appendChild(row);
newSlide.appendChild(wrapper);
slide.parentNode.insertBefore(newSlide, slide.nextSibling);
return true;
}
function injectSidebar() {
injectDesktopSidebar();
injectMobileSidebar();
}
// -- Travel Page: Destination Click Interception ----------------------
function isTravelPage() {
return window.location.href.indexOf('sid=travel') !== -1 ||
window.location.href.indexOf('travelagency') !== -1;
}
function getCountryFromDesktopButton(el) {
var s = el.querySelector('[class^="country"]');
if (s) return s.textContent.trim();
var n = el.querySelector('[class^="name"]');
if (n) {
var t = n.textContent.trim();
var d = t.indexOf(' -');
return d > 0 ? t.substring(0, d).trim() : t;
}
return null;
}
function getCountryFromMapPin(el) {
var label = el.getAttribute('aria-label');
if (label) {
var d = label.indexOf(' -');
return d > 0 ? label.substring(0, d).trim() : label.trim();
}
var parent = el.closest('[class^="destinationLabel"]');
if (parent) {
var radio = parent.querySelector('input[aria-label]');
if (radio) {
var rl = radio.getAttribute('aria-label');
var di = rl.indexOf(' -');
return di > 0 ? rl.substring(0, di).trim() : rl.trim();
}
}
return null;
}
function extractCountryFromClick(e) {
var target = e.target;
// Desktop list
var desktopBtn = target.closest('button[class^="expandButton"]');
if (desktopBtn) {
var dest = desktopBtn.closest('[class^="destination"]');
return getCountryFromDesktopButton(dest || desktopBtn);
}
// Map label
var mapLabel = target.closest('[class^="destinationLabel"]');
if (mapLabel) {
var radio = mapLabel.querySelector('input[aria-label]');
if (radio) return getCountryFromMapPin(radio);
}
// Map pin
var pin = target.closest('[class^="pin"]');
if (pin) {
var pl = pin.closest('[class^="destinationLabel"]');
if (pl) {
var pr = pl.querySelector('input[aria-label]');
if (pr) return getCountryFromMapPin(pr);
}
}
return null;
}
var travelCheckInProgress = false;
function attachTravelListeners() {
document.addEventListener('click', function (e) {
if (!isTravelPage()) return;
if (!isConfigured()) return;
if (travelCheckInProgress) return;
if (e.target.closest('#wtw-container') || e.target.closest('.wtw-overlay')) return;
var country = extractCountryFromClick(e);
if (!country) return;
// If cache is ready, check synchronously
if (memberCache.data && (Date.now() - memberCache.time) < CACHE_TTL) {
var threats = findThreats(memberCache.data, country);
if (threats.length > 0) {
e.preventDefault();
e.stopPropagation();
e.stopImmediatePropagation();
showTravelWarning(country, threats);
}
// No threats: click passes through naturally
return;
}
// No cache -- need to fetch, must block
e.preventDefault();
e.stopPropagation();
e.stopImmediatePropagation();
travelCheckInProgress = true;
var loadOverlay = createOverlay(false);
var loadModal = loadOverlay.querySelector('.wtw-modal');
var loadMsg = document.createElement('div');
loadMsg.className = 'wtw-status';
loadMsg.textContent = 'Checking war targets...';
loadModal.appendChild(loadMsg);
getCachedMembers().then(function (members) {
closeModal();
travelCheckInProgress = false;
var threats = findThreats(members, country);
if (threats.length > 0) {
showTravelWarning(country, threats);
}
// No threats: user clicks again, cache is now ready, passes through
}).catch(function (err) {
closeModal();
travelCheckInProgress = false;
console.error('[WTW] Fetch error:', err);
// Fail open -- don't block travel on error
});
}, true);
}
// -- Init -------------------------------------------------------------
injectStyles();
function init() {
injectSidebar();
attachTravelListeners();
prefetchMembers();
// Re-inject sidebar if SPA removes it
new MutationObserver(function () {
if (!document.querySelector('#nav-war_tools')) {
injectDesktopSidebar();
}
if (!document.querySelector('#nav-war_tools_mobile')) {
injectMobileSidebar();
}
}).observe(document.body, { childList: true, subtree: true });
// Refresh cache periodically
setInterval(function () {
if (isConfigured()) prefetchMembers();
}, CACHE_TTL);
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init);
} else {
setTimeout(init, 300);
}
console.log('[War Tools] v2.1 loaded');
})();