Tracks enemy faction users' status with live timers
// ==UserScript==
// @name Torn War Tracker
// @namespace http://tampermonkey.net/
// @version 1.53
// @description Tracks enemy faction users' status with live timers
// @match https://www.torn.com/*
// @grant GM_notification
// @icon https://www.google.com/s2/favicons?sz=64&domain=torn.com
// ==/UserScript==
(function () {
'use strict';
let pollInterval = null;
let renderInterval = null;
const tracked = new Map();
const LS = {
faction: "ht_faction",
api: "ht_api"
};
const UI_LAYOUT_KEY = "ht_ui_layout"
const STATE_KEY = "ht_state";
window.addEventListener("load", init);
function init() {
createUI();
loadSettings();
restoreState();
}
function makeDraggable(panel, handle) {
let dragging = false;
let offsetX = 0;
let offsetY = 0;
handle.style.cursor = "move";
handle.addEventListener("mousedown", (e) => {
dragging = true;
offsetX = e.clientX - panel.offsetLeft;
offsetY = e.clientY - panel.offsetTop;
});
document.addEventListener("mousemove", (e) => {
if (!dragging) return;
panel.style.left = `${e.clientX - offsetX}px`;
panel.style.top = `${e.clientY - offsetY}px`;
});
document.addEventListener("mouseup", () => {
dragging = false;
saveLayout(panel);
});
}
function makeResizable(panel) {
const resizer = document.createElement("div");
Object.assign(resizer.style, {
width: "10px",
height: "10px",
position: "absolute",
right: "0",
bottom: "0",
cursor: "se-resize",
background: "#666",
zIndex: 999999
});
panel.appendChild(resizer);
let resizing = false;
let startX = 0;
let startY = 0;
let startW = 0;
let startH = 0;
const minW = 200;
const minH = 150;
resizer.addEventListener("mousedown", (e) => {
resizing = true;
startX = e.clientX;
startY = e.clientY;
startW = panel.offsetWidth;
startH = panel.offsetHeight;
e.preventDefault();
e.stopPropagation();
});
document.addEventListener("mousemove", (e) => {
if (!resizing) return;
const newW = startW + (e.clientX - startX);
const newH = startH + (e.clientY - startY);
panel.style.width = Math.max(minW, newW) + "px";
panel.style.height = Math.max(minH, newH) + "px";
});
document.addEventListener("mouseup", () => {
resizing = false;
saveLayout(panel);
});
}
function createUI() {
const panel = document.createElement("div");
panel.innerHTML = `
<div id="hdr" style="
font-weight:700;
font-size:14px;
padding-bottom:10px;
margin-bottom:10px;
border-bottom:1px solid #343a46;
display:flex;
justify-content:space-between;
align-items:center;
">
<span>⚔️ War Tracker</span>
<span id="liveDot">●</span>
</div>
<input id="factionId" placeholder="Enemy Faction ID" class="wt-input"><br><br>
<input id="apiKey" placeholder="Public API Key" type="password" class="wt-input"><br><br>
<div id="btnRow">
<button id="startBtn">Start</button>
<button id="stopBtn">Stop</button>
</div>
<div id="status" style="
background:#252932;
border:1px solid #343a46;
border-radius:8px;
padding:6px 8px;
color:#9aa4b2;
margin-bottom:8px;
">
Idle
</div>
<div id="list" style="flex:1;overflow:auto;margin-top:10px;"></div>
`;
Object.assign(panel.style, {
position: "fixed",
top: "120px",
left: "20px",
right: "auto",
width: "260px",
height: "400px",
background: "#1e2128",
border: "1px solid #343a46",
boxShadow: "0 8px 30px rgba(0,0,0,.35)",
borderRadius: "12px",
color: "white",
padding: "10px",
fontSize: "12px",
zIndex: 999999,
overflow: "hidden",
display: "flex",
flexDirection: "column"
});
panel.style.left = "20px";
panel.style.right = "auto";
document.body.appendChild(panel);
restoreLayout(panel);
document.getElementById("liveDot").style.color = "#55ff88";
makeDraggable(panel, document.getElementById("hdr"));
makeResizable(panel);
document.getElementById("startBtn").onclick = start;
document.getElementById("stopBtn").onclick = stop;
const startBtn = document.getElementById("startBtn");
const stopBtn = document.getElementById("stopBtn");
Object.assign(document.getElementById("btnRow").style, {
display: "flex",
gap: "8px",
marginBottom: "10px"
});
Object.assign(startBtn.style, {
flex: "1",
backgroundColor: "#28a745",
color: "white",
border: "none",
padding: "8px 12px",
borderRadius: "8px",
cursor: "pointer",
fontWeight: "600",
transition: "all .15s ease"
});
Object.assign(stopBtn.style, {
flex: "1",
backgroundColor: "#dc3545",
color: "white",
border: "none",
padding: "8px 12px",
borderRadius: "8px",
cursor: "pointer",
fontWeight: "600",
transition: "all .15s ease"
});
startBtn.onmouseenter = () => {
startBtn.style.transform = "translateY(-1px)";
startBtn.style.boxShadow = "0 3px 10px rgba(40,167,69,.45)";
};
startBtn.onmouseleave = () => {
startBtn.style.transform = "translateY(0)";
startBtn.style.boxShadow = "none";
};
stopBtn.onmouseenter = () => {
stopBtn.style.transform = "translateY(-1px)";
stopBtn.style.boxShadow = "0 3px 10px rgba(220,53,69,.45)";
};
stopBtn.onmouseleave = () => {
stopBtn.style.transform = "translateY(0)";
stopBtn.style.boxShadow = "none";
};
const style = document.createElement("style");
style.textContent = `
#list a {
color: white;
text-decoration: none;
font-weight:600;
transition:.15s;
}
#list a:visited {
color: white;
}
#list a:hover {
color: #bb88ff;
text-decoration: underline;
}
.wt-input {
width:100%;
box-sizing:border-box;
background:#252932;
border:1px solid #343a46;
color:white;
border-radius:8px;
padding:8px;
margin-bottom:8px;
}
#list, #hdr, .wt-input {
box-sizing: border-box;
}
`;
document.head.appendChild(style);
}
function loadSettings() {
const f = localStorage.getItem(LS.faction);
const a = localStorage.getItem(LS.api);
if (f) document.getElementById("factionId").value = f;
if (a) document.getElementById("apiKey").value = a;
}
function saveLayout(panel) {
localStorage.setItem(UI_LAYOUT_KEY, JSON.stringify({
left: panel.offsetLeft,
top: panel.offsetTop,
width: panel.offsetWidth,
height: panel.offsetHeight
}));
}
function restoreLayout(panel) {
const raw = localStorage.getItem(UI_LAYOUT_KEY);
if (!raw) return;
try {
const s = JSON.parse(raw);
panel.style.left = (s.left ?? 20) + "px";
panel.style.top = (s.top ?? 120) + "px";
panel.style.width = (s.width ?? 260) + "px";
panel.style.height = (s.height ?? 400) + "px";
panel.style.right = "auto";
} catch (e) {
console.error("layout restore failed", e);
}
}
function saveState(isRunning) {
localStorage.setItem(STATE_KEY, JSON.stringify({
running: isRunning,
data: Array.from(tracked.entries())
}));
}
function restoreState() {
const raw = localStorage.getItem(STATE_KEY);
if (!raw) return;
try {
const state = JSON.parse(raw);
if (state.data) {
tracked.clear();
for (const [id, p] of state.data) {
tracked.set(id, p);
}
render();
}
if (state.running) {
setTimeout(() => {
start();
refreshData();
}, 500);
}
} catch (e) {
console.error("restore failed", e);
}
}
function saveSettings() {
localStorage.setItem(LS.faction, document.getElementById("factionId").value);
localStorage.setItem(LS.api, document.getElementById("apiKey").value);
}
function formatTime(sec) {
sec = Math.max(0, sec);
const days = Math.floor(sec / 86400);
sec %= 86400;
const hours = Math.floor(sec / 3600);
sec %= 3600;
const mins = Math.floor(sec / 60);
const secs = sec % 60;
const parts = [];
if (days) parts.push(`${days}d`);
if (hours) parts.push(`${hours}h`);
if (mins) parts.push(`${mins}m`);
if (secs) parts.push(`${secs}s`);
const filtered = parts.filter(Boolean);
if (!filtered.length) return "0s";
return filtered.slice(0, 2).join(" ");
}
async function fetchFaction(fid, key) {
const url = `https://api.torn.com/faction/${fid}?selections=basic&key=${key}`;
const res = await fetch(url);
return await res.json();
}
async function refreshData() {
const fid = document.getElementById("factionId").value;
const key = document.getElementById("apiKey").value;
if (!fid || !key) return;
saveSettings();
const data = await fetchFaction(fid, key);
if (!data || !data.members) {
document.getElementById("status").innerText = "API Error";
return;
}
tracked.clear();
for (const [id, m] of Object.entries(data.members)) {
if (m.status?.state === "Hospital") {
tracked.set(id, {
id,
name: m.name,
until: m.status.until,
details: m.status.details,
online: m.last_action.status
});
} else if (m.status?.state === "Okay") {
tracked.set(id, {
id,
name: m.name,
until: 0,
online: m.last_action.status
});
}
}
saveState(true);
document.getElementById("status").innerText =
`Tracking ${tracked.size} users`;
}
function emoji(p) {
if (p.online === "Online") {
return "🟢"
} else if (p.online === "Idle") {
return "🟡"
} else {
return "⚫"
}
}
function render() {
const list = document.getElementById("list");
const now = Math.floor(Date.now() / 1000);
const arr = Array.from(tracked.values());
arr.sort((a, b) => a.until - b.until);
let html = "";
const ok = arr.filter(p => p.until <= now);
const hosp = arr.filter(p => p.until > now);
html += `
<div style="
color:#9aa4b2;
font-size:11px;
font-weight:700;
text-transform:uppercase;
letter-spacing:.5px;
margin-bottom:8px;
">
✅ Available (${ok.length})
</div>
`;
if (!ok.length) {
html += `<div>None</div>`;
} else {
ok.forEach(p => {
html += `<div style="background:#252932; border:1px solid #343a46; border-radius:8px; padding:8px; margin-bottom:6px;">${emoji(p)} <b><a href="https://www.torn.com/profiles.php?XID=${p.id}">${p.name}</a></b> - <span style="color:#55ff55">Okay</span><br>ㅤ</div>`;
});
}
// html += `<hr style="border:1px solid #444;">`;
html += `
<div style="
color:#9aa4b2;
font-size:11px;
font-weight:700;
text-transform:uppercase;
letter-spacing:.5px;
margin:12px 0 8px;
">
🏥 Hospital (${hosp.length})
</div>
`;
if (!hosp.length) {
html += `<div>None</div>`;
} else {
hosp.forEach(p => {
const remaining = p.until - now;
let timeColor;
if (remaining <=61) {
timeColor = "#4da6ff"; // blue
} else if (remaining <= 301) {
timeColor = "#efbb2b"; // yellow
} else if (remaining <= 901) {
timeColor = "#ff9900"; // orange
} else {
timeColor = "#ff5555"; // red
}
html += `
<div style="background:#252932; border:1px solid #343a46; border-radius:8px; padding:8px; margin-bottom:6px;">
${emoji(p)} <b><a href="https://www.torn.com/profiles.php?XID=${p.id}">${p.name}</a></b> - <span style="color:#adadad">${p.details}</span><br>
<span style="color:${timeColor}">
🕒 ${formatTime(remaining-1)}
</span>
</div>
`;
});
}
list.innerHTML = html;
}
function start() {
const fid = document.getElementById("factionId").value;
const key = document.getElementById("apiKey").value;
if (!fid || !key) {
alert("Enter faction ID and API key");
return;
}
stop();
saveSettings();
refreshData();
pollInterval = setInterval(refreshData, 30000);
renderInterval = setInterval(render, 1000);
saveState(true);
document.getElementById("status").innerText = "Running...";
}
function stop() {
clearInterval(pollInterval);
clearInterval(renderInterval);
pollInterval = null;
renderInterval = null;
saveState(false);
document.getElementById("status").innerText = "Stopped";
}
})();