War report & payout calculator for Torn factions. Made by Vladaa.
// ==UserScript==
// @name Torn — War Performance Report
// @namespace https://torn.com
// @version 1.0
// @description War report & payout calculator for Torn factions. Made by Vladaa.
// @author Vladaa
// @license MIT
// @match https://www.torn.com/*
// @grant GM_xmlhttpRequest
// @grant GM_setValue
// @grant GM_getValue
// @connect api.torn.com
// ==/UserScript==
// MIT License
// Copyright (c) 2026 Vladaa
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
(function () {
"use strict";
// ─────────────────────────────────────────
// API ENDPOINTS
// ─────────────────────────────────────────
const URL_USER = k => `https://api.torn.com/v2/user/?selections=faction&key=${k}`;
const URL_WARS = (k, o=0) => `https://api.torn.com/v2/faction/rankedwars?limit=20&offset=${o}&key=${k}`;
const URL_WAR_REPORT = (k, id) => `https://api.torn.com/v2/faction/${id}/rankedwarreport?key=${k}`;
// ─────────────────────────────────────────
// STATE
// ─────────────────────────────────────────
let _wars = [], _reportData = null, _myFid = null, _sortState = {};
// ─────────────────────────────────────────
// HELPERS
// ─────────────────────────────────────────
function fmtTime(ts) {
if (!ts) return "Ongoing";
return new Date(ts * 1000).toLocaleString("sv-SE", {
year:"numeric",month:"2-digit",day:"2-digit",hour:"2-digit",minute:"2-digit"
}).replace("T"," ");
}
const fmtMoney = n => "$" + Math.round(n).toLocaleString("en-US");
const fmtMoneyDec = n => "$" + n.toLocaleString("en-US",{minimumFractionDigits:2,maximumFractionDigits:2});
function setStatus(msg, type="") {
const el = $("wrStatus");
if (!el) return;
el.textContent = msg;
el.className = "wr-status" + (type ? " wr-status-"+type : "");
}
function $(id) { return document.getElementById("wr-"+id); }
// ─────────────────────────────────────────
// GM XHR WRAPPER (bypasses CORS)
// ─────────────────────────────────────────
function apiFetch(url) {
return new Promise((resolve, reject) => {
GM_xmlhttpRequest({
method: "GET",
url,
onload(res) {
try {
const data = JSON.parse(res.responseText);
if (data.error) reject(new Error("Torn API: " + data.error.error));
else resolve(data);
} catch(e) { reject(new Error("Invalid response from API")); }
},
onerror() { reject(new Error("Network error — check your connection")); }
});
});
}
async function fetchFactionId(key) {
const data = await apiFetch(URL_USER(key));
const fid = data?.faction?.id;
if (!fid) throw new Error("Could not retrieve faction ID. Are you in a faction?");
return fid;
}
async function fetchWars(key) { return (await apiFetch(URL_WARS(key))).rankedwars ?? []; }
async function fetchWarReport(key,id) { return (await apiFetch(URL_WAR_REPORT(key,id))).rankedwarreport ?? {}; }
// ─────────────────────────────────────────
// PARSE REPORT
// ─────────────────────────────────────────
function parseReport(report, myFid) {
const factions = report.factions ?? [];
const myFaction = factions.find(f => f.id === myFid) ?? factions[0] ?? {};
const enemy = factions.find(f => f.id !== myFid) ?? factions[1] ?? {};
const wid = report.winner;
const result = wid === myFaction.id ? "Victory 🏆" : wid == null ? "Ongoing ⚔️" : "Defeat 💀";
return { start: fmtTime(report.start), end: fmtTime(report.end), result, my: myFaction, enemy };
}
// ─────────────────────────────────────────
// TEXT REPORT
// ─────────────────────────────────────────
function generateTextReport(data) {
const my = data.my, en = data.enemy;
const lines = [
`⚔️ War Report — ${my.name??'?'} vs ${en.name??'?'}`,
`Date: ${data.start} → ${data.end}`,
`Result: ${data.result}`,
`Score: ${(my.score??0).toLocaleString()} vs ${(en.score??0).toLocaleString()}`,
`Attacks: ${my.attacks??0} | Respect: ${(my.rewards?.respect??0).toLocaleString()}`,
"", "── Member Contributions ──",
];
const members = [...(my.members??[])].sort((a,b)=>b.score-a.score);
members.filter(m=>m.attacks>0).forEach(m =>
lines.push(` ${m.name.padEnd(22)} Lvl ${String(m.level).padEnd(4)} Attacks: ${String(m.attacks).padEnd(5)} Score: ${m.score.toFixed(1)}`)
);
const nc = members.filter(m=>m.attacks===0);
if (nc.length) {
lines.push(`\n── Did Not Participate (${nc.length}) ──`);
nc.forEach(m => lines.push(` ${m.name.padEnd(22)} Lvl ${m.level}`));
}
return lines.join("\n");
}
// ─────────────────────────────────────────
// TABLE SORT
// ─────────────────────────────────────────
function sortTable(tbody, col) {
const key = tbody.id+"_"+col;
const rev = _sortState[key] ?? false;
_sortState[key] = !rev;
const rows = [...tbody.querySelectorAll("tr")];
rows.sort((a,b) => {
const av=a.dataset[col]??"", bv=b.dataset[col]??"";
const an=parseFloat(av.replace(/[$,]/g,"")), bn=parseFloat(bv.replace(/[$,]/g,""));
if (!isNaN(an)&&!isNaN(bn)) return rev?an-bn:bn-an;
return rev?av.localeCompare(bv):bv.localeCompare(av);
});
rows.forEach(r=>tbody.appendChild(r));
}
// ─────────────────────────────────────────
// POPULATE TABLES
// ─────────────────────────────────────────
function populateMemberTable(tbody, members) {
tbody.innerHTML = "";
[...members].sort((a,b)=>b.score-a.score).forEach((m,i) => {
const p = m.attacks > 0;
const tr = document.createElement("tr");
tr.className = p ? "wr-row-yes" : "wr-row-no";
tr.dataset.rank=i+1; tr.dataset.name=m.name;
tr.dataset.level=m.level; tr.dataset.attacks=m.attacks;
tr.dataset.score=m.score; tr.dataset.participated=p?1:0;
tr.innerHTML = `<td class="wr-c wr-muted">${i+1}</td><td>${m.name}</td>
<td class="wr-c">${m.level}</td><td class="wr-c">${m.attacks}</td>
<td class="wr-c">${m.score.toFixed(1)}</td><td class="wr-c">${p?"Yes":"No"}</td>`;
tbody.appendChild(tr);
});
}
// ─────────────────────────────────────────
// PAYOUT
// ─────────────────────────────────────────
function getCacheValues() {
const vals = {};
document.querySelectorAll("[data-cache]").forEach(inp => {
vals[inp.dataset.cache] = parseFloat(inp.value.replace(/[,$]/g,""))||0;
});
return vals;
}
function populatePayout(data) {
const my=data.my, items=my.rewards?.items??[], cvals=getCacheValues();
let totalLoot=0;
const lines=[];
for (const it of items) {
const v=(cvals[it.name]??0)*(it.quantity??0);
totalLoot+=v;
lines.push(`${it.name} ×${it.quantity} → ${fmtMoney(v)}`);
}
$("lootItems").textContent = lines.length ? lines.join("\n") : "No cache items found.";
const totalAtt=(my.members??[]).reduce((s,m)=>s+m.attacks,0);
const perHit=totalAtt>0?totalLoot/totalAtt:0;
$("lsTotalLoot").textContent=fmtMoney(totalLoot);
$("lsTotalAttacks").textContent=totalAtt.toLocaleString();
$("lsPerHit").textContent=fmtMoneyDec(perHit);
const tbody=$("bodyPayout");
tbody.innerHTML="";
[...(my.members??[])].filter(m=>m.attacks>0).sort((a,b)=>b.attacks-a.attacks).forEach((m,i)=>{
const pay=m.attacks*perHit;
const tr=document.createElement("tr");
tr.dataset.rank=i+1; tr.dataset.name=m.name;
tr.dataset.attacks=m.attacks; tr.dataset.payout=pay;
tr.innerHTML=`<td class="wr-c wr-muted">${i+1}</td><td>${m.name}</td>
<td class="wr-c">${m.attacks}</td><td class="wr-c">${fmtMoneyDec(pay)}</td>`;
tbody.appendChild(tr);
});
$("btnPayoutCsv").disabled=false;
}
// ─────────────────────────────────────────
// UPDATE DISPLAY
// ─────────────────────────────────────────
function updateDisplay(data) {
const my=data.my, en=data.enemy;
$("valResult").textContent = data.result;
$("valScore").textContent = `${(my.score??0).toLocaleString()} vs ${(en.score??0).toLocaleString()}`;
$("valAttacks").textContent = String(my.attacks??0);
$("valRespect").textContent = (my.rewards?.respect??0).toLocaleString();
const rank=my.rank??{};
$("valRank").textContent = `${rank.before??'?'} → ${rank.after??'?'}`;
populateMemberTable($("bodyMy"), my.members??[]);
populateMemberTable($("bodyEnemy"), en.members??[]);
populatePayout(data);
$("btnCsv").disabled=$("btnCopy").disabled=false;
$("footerTs").textContent="Last updated: "+new Date().toLocaleTimeString();
}
// ─────────────────────────────────────────
// LOAD WARS
// ─────────────────────────────────────────
async function loadWars() {
const key=$("apiKey").value.trim();
if (!key){setStatus("Please enter your Torn API key.","error");return;}
GM_setValue("tornApiKey", key);
const btn=$("btnLoadWars");
btn.disabled=true; btn.textContent="⏳ Loading…";
setStatus("Fetching faction & wars…");
try {
_myFid = await fetchFactionId(key);
_wars = await fetchWars(key);
const sel=$("warSelect");
sel.innerHTML="";
_wars.forEach((w,idx)=>{
const names=(w.factions??[]).map(f=>f.name).join(" vs ");
const res=w.winner===_myFid?"✅":w.winner==null?"⚔️":"❌";
const opt=document.createElement("option");
opt.value=idx;
opt.textContent=`${res} [${w.id}] ${fmtTime(w.start)} — ${names}`;
sel.appendChild(opt);
});
sel.disabled=false;
$("btnLoadReport").disabled=false;
setStatus(`Loaded ${_wars.length} wars.`,"ok");
} catch(e){ setStatus(e.message,"error"); }
finally { btn.disabled=false; btn.textContent="📋 Load Wars"; }
}
// ─────────────────────────────────────────
// LOAD REPORT
// ─────────────────────────────────────────
async function loadReport() {
const key=$("apiKey").value.trim();
const idx=parseInt($("warSelect").value,10);
if (isNaN(idx)||!_wars.length){setStatus("Please load wars and select one.","error");return;}
const btn=$("btnLoadReport");
btn.disabled=true; btn.textContent="⏳ Loading…";
setStatus("Fetching war report…");
try {
const raw=await fetchWarReport(key,_wars[idx].id);
_reportData=parseReport(raw,_myFid);
updateDisplay(_reportData);
setStatus("Report loaded.","ok");
} catch(e){ setStatus(e.message,"error"); }
finally { btn.disabled=false; btn.textContent="⚔️ Load Report"; }
}
// ─────────────────────────────────────────
// EXPORTS
// ─────────────────────────────────────────
function downloadCSV(filename, rows) {
const csv=rows.map(r=>r.map(c=>`"${String(c??"").replace(/"/g,'""')}"`).join(",")).join("\r\n");
const a=document.createElement("a");
a.href="data:text/csv;charset=utf-8,\uFEFF"+encodeURIComponent(csv);
a.download=filename; a.click();
}
function exportCSV() {
if (!_reportData) return;
const rows=[];
for (const [lbl,fkey] of [["MY FACTION","my"],["ENEMY FACTION","enemy"]]) {
const f=_reportData[fkey];
rows.push([lbl,f.name??""]);
rows.push(["Rank","Member","Level","Attacks","Score","Participated"]);
[...(f.members??[])].sort((a,b)=>b.score-a.score).forEach((m,i)=>
rows.push([i+1,m.name,m.level,m.attacks,m.score.toFixed(1),m.attacks>0?"Yes":"No"])
);
rows.push([]);
}
const ts=new Date().toISOString().slice(0,16).replace(/[:.]/g,"-");
downloadCSV(`war_report_${ts}.csv`,rows);
setStatus("CSV downloaded.","ok");
}
function exportPayoutCSV() {
if (!_reportData) return;
const my=_reportData.my, cvals=getCacheValues();
const items=my.rewards?.items??[];
const totalLoot=items.reduce((s,it)=>s+(cvals[it.name]??0)*(it.quantity??0),0);
const totalAtt=(my.members??[]).reduce((s,m)=>s+m.attacks,0);
const perHit=totalAtt>0?totalLoot/totalAtt:0;
const rows=[
["War Payout —",my.name??""],[" Total loot value",fmtMoney(totalLoot)],
["Total attacks",totalAtt],["Value per hit",fmtMoneyDec(perHit)],[],
["Rank","Member","Attacks","Payout"],
];
[...(my.members??[])].filter(m=>m.attacks>0).sort((a,b)=>b.attacks-a.attacks)
.forEach((m,i)=>rows.push([i+1,m.name,m.attacks,fmtMoneyDec(m.attacks*perHit)]));
const ts=new Date().toISOString().slice(0,16).replace(/[:.]/g,"-");
downloadCSV(`war_payout_${ts}.csv`,rows);
setStatus("Payout CSV downloaded.","ok");
}
// ─────────────────────────────────────────
// STYLES
// ─────────────────────────────────────────
const CSS = `
@import url('https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:wght@400;500&family=IBM+Plex+Sans:wght@400;500;600&display=swap');
#wr-fab {
position:fixed; bottom:24px; right:24px; z-index:999998;
width:48px; height:48px; border-radius:50%;
background:#1a1a2e; border:2px solid #4a8fe8;
color:#4a8fe8; font-size:20px; cursor:pointer;
display:flex; align-items:center; justify-content:center;
box-shadow:0 4px 20px rgba(0,0,0,.6);
transition:transform .15s, box-shadow .15s;
}
#wr-fab:hover { transform:scale(1.1); box-shadow:0 6px 28px rgba(74,143,232,.4); }
#wr-panel {
position:fixed; bottom:82px; right:24px; z-index:999999;
width:780px; max-height:85vh;
background:#111114; border:1px solid #33333d; border-radius:10px;
display:none; flex-direction:column;
font-family:'IBM Plex Sans',sans-serif; font-size:13px; color:#d4d4e0;
box-shadow:0 8px 40px rgba(0,0,0,.8);
overflow:hidden;
}
#wr-panel.wr-open { display:flex; }
.wr-header {
display:flex; align-items:center; justify-content:space-between;
padding:10px 16px; background:#1a1a1f; border-bottom:1px solid #33333d;
flex-shrink:0;
}
.wr-header h1 { font-size:13px; font-weight:600; color:#fff; }
.wr-header-right { display:flex; align-items:center; gap:10px; }
.wr-made-by { font-size:11px; color:#6b6b7e; font-family:'IBM Plex Mono',monospace; }
.wr-close {
background:none; border:none; color:#6b6b7e; font-size:18px;
cursor:pointer; padding:0 4px; line-height:1;
}
.wr-close:hover { color:#fff; }
.wr-config-row, .wr-war-row {
display:flex; align-items:center; gap:8px;
padding:8px 16px; background:#1a1a1f; border-bottom:1px solid #33333d;
flex-shrink:0;
}
.wr-config-row label, .wr-war-row label { font-size:11px; color:#6b6b7e; white-space:nowrap; }
.wr-config-row input[type=text], .wr-war-row select {
flex:1; background:#111114; border:1px solid #33333d; color:#d4d4e0;
font-family:'IBM Plex Mono',monospace; font-size:12px;
padding:5px 10px; border-radius:4px; outline:none;
}
.wr-config-row input[type=text]:focus { border-color:#4a8fe8; }
.wr-war-row select { font-family:'IBM Plex Sans',sans-serif; cursor:pointer; }
.wr-btn {
font-family:'IBM Plex Sans',sans-serif; font-size:12px; font-weight:500;
padding:5px 14px; border:1px solid; border-radius:4px;
cursor:pointer; white-space:nowrap; transition:opacity .15s;
}
.wr-btn:disabled { opacity:.35; cursor:default; }
.wr-btn:not(:disabled):hover { opacity:.8; }
.wr-btn-blue { background:#1e3560; color:#4a8fe8; border-color:#4a8fe8; }
.wr-btn-red { background:#361616; color:#e85c5c; border-color:#e85c5c; }
.wr-btn-green { background:#163326; color:#4ec97a; border-color:#4ec97a; }
.wr-btn-purple { background:#2a1a40; color:#a878e8; border-color:#a878e8; }
.wr-btn-amber { background:#3a2a10; color:#e8a84a; border-color:#e8a84a; }
.wr-summary {
display:flex; background:#22222a; border-bottom:1px solid #33333d; flex-shrink:0;
}
.wr-stat { flex:1; padding:7px 12px; border-right:1px solid #33333d; }
.wr-stat:last-child { border-right:none; }
.wr-stat .lbl { font-size:10px; color:#6b6b7e; text-transform:uppercase; letter-spacing:.06em; }
.wr-stat .val { font-size:13px; font-weight:600; margin-top:2px; }
.wr-col-white { color:#fff; }
.wr-col-blue { color:#4a8fe8; }
.wr-col-amber { color:#e8a84a; }
.wr-col-green { color:#4ec97a; }
.wr-col-purple { color:#a878e8; }
.wr-tabs { display:flex; background:#1a1a1f; border-bottom:1px solid #33333d; flex-shrink:0; }
.wr-tab {
padding:7px 16px; background:none; border:none;
border-bottom:2px solid transparent; color:#6b6b7e;
font-size:12px; font-weight:500; cursor:pointer;
font-family:'IBM Plex Sans',sans-serif;
}
.wr-tab.active { color:#4a8fe8; border-bottom-color:#4a8fe8; }
.wr-tab:not(.active):hover { color:#d4d4e0; }
.wr-tab-panel { display:none; flex-direction:column; overflow:hidden; }
.wr-tab-panel.active { display:flex; }
.wr-table-wrap { overflow-y:auto; max-height:260px; }
.wr-table-wrap table { width:100%; border-collapse:collapse; font-size:12px; }
.wr-table-wrap thead th {
position:sticky; top:0; background:#22222a; color:#6b6b7e;
font-weight:500; font-size:11px; text-transform:uppercase;
letter-spacing:.05em; padding:6px 10px; text-align:left;
border-bottom:1px solid #33333d; cursor:pointer; user-select:none;
}
.wr-table-wrap thead th:hover { color:#d4d4e0; }
.wr-c { text-align:center; }
.wr-muted { color:#6b6b7e; font-family:'IBM Plex Mono',monospace; font-size:11px; }
.wr-table-wrap tbody tr { border-bottom:1px solid #22222a; }
.wr-table-wrap tbody tr:hover { background:#22222a; }
.wr-table-wrap tbody td { padding:5px 10px; }
.wr-row-yes td { color:#4ec97a; }
.wr-row-no td { color:#e85c5c; }
.wr-payout-top { display:flex; gap:10px; padding:10px 12px; border-bottom:1px solid #33333d; flex-shrink:0; }
.wr-cache-panel, .wr-loot-panel {
background:#22222a; border:1px solid #33333d; border-radius:6px; padding:10px 12px;
}
.wr-cache-panel { min-width:270px; }
.wr-loot-panel { flex:1; }
.wr-cache-panel h3, .wr-loot-panel h3 {
font-size:11px; text-transform:uppercase; letter-spacing:.06em;
color:#6b6b7e; margin-bottom:8px;
}
.wr-cache-row { display:flex; align-items:center; justify-content:space-between; margin-bottom:5px; }
.wr-cache-row label { font-size:12px; }
.wr-cache-row input[type=number] {
width:110px; background:#111114; border:1px solid #33333d; color:#d4d4e0;
font-family:'IBM Plex Mono',monospace; font-size:12px;
padding:3px 8px; border-radius:4px; outline:none; text-align:right;
}
.wr-cache-row input[type=number]:focus { border-color:#4a8fe8; }
.wr-loot-items {
font-family:'IBM Plex Mono',monospace; font-size:12px;
white-space:pre; min-height:50px; color:#d4d4e0; line-height:1.8;
}
.wr-loot-divider { border:none; border-top:1px solid #33333d; margin:8px 0; }
.wr-loot-stat { display:flex; justify-content:space-between; font-size:12px; margin-bottom:4px; }
.wr-loot-stat .ls-lbl { color:#6b6b7e; }
.wr-loot-stat .ls-val { font-weight:600; font-family:'IBM Plex Mono',monospace; }
.wr-action {
display:flex; align-items:center; gap:8px; padding:8px 12px;
border-top:1px solid #33333d; background:#1a1a1f; flex-shrink:0;
}
.wr-status { flex:1; font-size:11px; font-family:'IBM Plex Mono',monospace; color:#6b6b7e; }
.wr-status-error { color:#e85c5c; }
.wr-status-ok { color:#4ec97a; }
.wr-footer {
text-align:right; padding:4px 16px; font-size:10px;
color:#6b6b7e; border-top:1px solid #33333d;
font-family:'IBM Plex Mono',monospace; flex-shrink:0;
}
#wr-panel ::-webkit-scrollbar { width:6px; }
#wr-panel ::-webkit-scrollbar-track { background:#1a1a1f; }
#wr-panel ::-webkit-scrollbar-thumb { background:#33333d; border-radius:3px; }
`;
// ─────────────────────────────────────────
// BUILD UI
// ─────────────────────────────────────────
function buildUI() {
const style = document.createElement("style");
style.textContent = CSS;
document.head.appendChild(style);
// FAB button
const fab = document.createElement("button");
fab.id = "wr-fab";
fab.title = "War Report";
fab.textContent = "⚔️";
document.body.appendChild(fab);
// Panel
const panel = document.createElement("div");
panel.id = "wr-panel";
panel.innerHTML = `
<div class="wr-header">
<h1>⚔️ War Performance Report</h1>
<div class="wr-header-right">
<span class="wr-made-by">made by Vladaa</span>
<button class="wr-close" id="wr-closeBtn">✕</button>
</div>
</div>
<div class="wr-config-row">
<label>Torn API Key</label>
<input type="password" id="wr-apiKey" placeholder="Enter your 16-character API key…" autocomplete="off">
<button class="wr-btn wr-btn-blue" id="wr-btnToggleKey" title="Show/hide key">👁</button>
<button class="wr-btn wr-btn-blue" id="wr-btnLoadWars">📋 Load Wars</button>
</div>
<div class="wr-war-row">
<label>Select War:</label>
<select id="wr-warSelect" disabled><option>— load wars first —</option></select>
<button class="wr-btn wr-btn-red" id="wr-btnLoadReport" disabled>⚔️ Load Report</button>
</div>
<div class="wr-summary">
<div class="wr-stat"><div class="lbl">Result</div> <div class="val wr-col-white" id="wr-valResult">—</div></div>
<div class="wr-stat"><div class="lbl">Score</div> <div class="val wr-col-blue" id="wr-valScore">—</div></div>
<div class="wr-stat"><div class="lbl">Attacks</div> <div class="val wr-col-amber" id="wr-valAttacks">—</div></div>
<div class="wr-stat"><div class="lbl">Respect</div> <div class="val wr-col-green" id="wr-valRespect">—</div></div>
<div class="wr-stat"><div class="lbl">Rank</div> <div class="val wr-col-purple" id="wr-valRank">—</div></div>
</div>
<div class="wr-tabs">
<button class="wr-tab active" data-tab="my">My Faction</button>
<button class="wr-tab" data-tab="enemy">Enemy Faction</button>
<button class="wr-tab" data-tab="payout">💰 War Payout</button>
</div>
<div class="wr-tab-panel active" id="wr-tab-my">
<div class="wr-table-wrap">
<table><thead><tr>
<th class="wr-c" data-col="rank">#</th>
<th data-col="name">Member</th>
<th class="wr-c" data-col="level">Lvl</th>
<th class="wr-c" data-col="attacks">Attacks</th>
<th class="wr-c" data-col="score">Score</th>
<th class="wr-c" data-col="participated">Active</th>
</tr></thead><tbody id="wr-bodyMy"></tbody></table>
</div>
</div>
<div class="wr-tab-panel" id="wr-tab-enemy">
<div class="wr-table-wrap">
<table><thead><tr>
<th class="wr-c" data-col="rank">#</th>
<th data-col="name">Member</th>
<th class="wr-c" data-col="level">Lvl</th>
<th class="wr-c" data-col="attacks">Attacks</th>
<th class="wr-c" data-col="score">Score</th>
<th class="wr-c" data-col="participated">Active</th>
</tr></thead><tbody id="wr-bodyEnemy"></tbody></table>
</div>
</div>
<div class="wr-tab-panel" id="wr-tab-payout">
<div class="wr-payout-top">
<div class="wr-cache-panel">
<h3>Cache Values ($)</h3>
<div class="wr-cache-row"><label>Armor Cache</label> <input type="number" data-cache="Armor Cache" value="500000"></div>
<div class="wr-cache-row"><label>Heavy Arms Cache</label> <input type="number" data-cache="Heavy Arms Cache" value="750000"></div>
<div class="wr-cache-row"><label>Melee Cache</label> <input type="number" data-cache="Melee Cache" value="300000"></div>
<div class="wr-cache-row"><label>Small Arms Cache</label> <input type="number" data-cache="Small Arms Cache" value="400000"></div>
<div class="wr-cache-row"><label>Medium Arms Cache</label> <input type="number" data-cache="Medium Arms Cache" value="600000"></div>
<div class="wr-cache-row"><label>Special Cache</label> <input type="number" data-cache="Special Cache" value="1000000"></div>
<div style="margin-top:10px">
<button class="wr-btn wr-btn-amber" id="wr-btnRecalc">🔄 Recalculate</button>
</div>
</div>
<div class="wr-loot-panel">
<h3>War Loot & Payout Summary</h3>
<div class="wr-loot-items" id="wr-lootItems">Load a war report first.</div>
<hr class="wr-loot-divider">
<div class="wr-loot-stat"><span class="ls-lbl">Total loot value</span> <span class="ls-val wr-col-green" id="wr-lsTotalLoot">—</span></div>
<div class="wr-loot-stat"><span class="ls-lbl">Total attacks</span> <span class="ls-val wr-col-amber" id="wr-lsTotalAttacks">—</span></div>
<div class="wr-loot-stat"><span class="ls-lbl">Value per hit</span> <span class="ls-val wr-col-blue" id="wr-lsPerHit">—</span></div>
</div>
</div>
<div class="wr-table-wrap">
<table><thead><tr>
<th class="wr-c" data-col="rank">#</th>
<th data-col="name">Member</th>
<th class="wr-c" data-col="attacks">Attacks</th>
<th class="wr-c" data-col="payout">Payout</th>
</tr></thead><tbody id="wr-bodyPayout"></tbody></table>
</div>
<div style="padding:6px 12px;text-align:right;flex-shrink:0">
<button class="wr-btn wr-btn-green" id="wr-btnPayoutCsv" disabled>💾 Export Payout CSV</button>
</div>
</div>
<div class="wr-action">
<span class="wr-status" id="wr-wrStatus"></span>
<button class="wr-btn wr-btn-green" id="wr-btnCsv" disabled>💾 Export CSV</button>
<button class="wr-btn wr-btn-purple" id="wr-btnCopy" disabled>📋 Copy Report</button>
</div>
<div class="wr-footer" id="wr-footerTs"></div>
`;
document.body.appendChild(panel);
// FAB toggle
fab.addEventListener("click", () => panel.classList.toggle("wr-open"));
$("closeBtn").addEventListener("click", () => panel.classList.remove("wr-open"));
// Restore saved key
const saved = GM_getValue("tornApiKey", "");
if (saved) $("apiKey").value = saved;
// Tabs
panel.querySelectorAll(".wr-tab").forEach(btn => {
btn.addEventListener("click", () => {
panel.querySelectorAll(".wr-tab").forEach(b=>b.classList.remove("active"));
panel.querySelectorAll(".wr-tab-panel").forEach(p=>p.classList.remove("active"));
btn.classList.add("active");
document.getElementById("wr-tab-"+btn.dataset.tab).classList.add("active");
});
});
// Table sort
[
["wr-tab-my thead", "wr-bodyMy"],
["wr-tab-enemy thead", "wr-bodyEnemy"],
["wr-tab-payout thead", "wr-bodyPayout"],
].forEach(([thSel, tbId]) => {
const thead = panel.querySelector("#"+thSel.trim().replace(" "," #").split(" #")[0]+" thead") ||
document.querySelector("#"+thSel.trim().split(" ")[0]+" thead");
if (!thead) return;
const tbody = document.getElementById(tbId);
thead.querySelectorAll("th[data-col]").forEach(th =>
th.addEventListener("click", () => sortTable(tbody, th.dataset.col))
);
});
// Wire up buttons
$("btnToggleKey").addEventListener("click", () => {
const inp = $("apiKey");
const show = inp.type === "password";
inp.type = show ? "text" : "password";
$("btnToggleKey").textContent = show ? "🙈" : "👁";
});
$("btnLoadWars").addEventListener("click", loadWars);
$("btnLoadReport").addEventListener("click", loadReport);
$("btnRecalc").addEventListener("click", () => {
if (!_reportData){setStatus("Load a war report first.","error");return;}
populatePayout(_reportData); setStatus("Recalculated.","ok");
});
$("btnCsv").addEventListener("click", exportCSV);
$("btnPayoutCsv").addEventListener("click", exportPayoutCSV);
$("btnCopy").addEventListener("click", () => {
if (!_reportData) return;
navigator.clipboard.writeText(generateTextReport(_reportData))
.then(()=>setStatus("Copied to clipboard.","ok"))
.catch(()=>setStatus("Clipboard access denied.","error"));
});
$("apiKey").addEventListener("keydown", e => { if(e.key==="Enter") loadWars(); });
// Bind table sort properly
["my","enemy","payout"].forEach(tab => {
const thead = document.querySelector(`#wr-tab-${tab} thead`);
const tbodyId = tab==="payout" ? "wr-bodyPayout" : tab==="my" ? "wr-bodyMy" : "wr-bodyEnemy";
if (!thead) return;
thead.querySelectorAll("th[data-col]").forEach(th =>
th.addEventListener("click", () => sortTable(document.getElementById(tbodyId), th.dataset.col))
);
});
}
// ─────────────────────────────────────────
// START
// ─────────────────────────────────────────
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", buildUI);
} else {
buildUI();
}
})();