Torn — War Performance Report

War report & payout calculator for Torn factions. Made by Vladaa.

スクリプトをインストールするには、Tampermonkey, GreasemonkeyViolentmonkey のような拡張機能のインストールが必要です。

You will need to install an extension such as Tampermonkey to install this script.

スクリプトをインストールするには、TampermonkeyViolentmonkey のような拡張機能のインストールが必要です。

スクリプトをインストールするには、TampermonkeyUserscripts のような拡張機能のインストールが必要です。

このスクリプトをインストールするには、Tampermonkeyなどの拡張機能をインストールする必要があります。

このスクリプトをインストールするには、ユーザースクリプト管理ツールの拡張機能をインストールする必要があります。

(ユーザースクリプト管理ツールは設定済みなのでインストール!)

このスタイルをインストールするには、Stylusなどの拡張機能をインストールする必要があります。

このスタイルをインストールするには、Stylus などの拡張機能をインストールする必要があります。

このスタイルをインストールするには、Stylus tなどの拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

(ユーザースタイル管理ツールは設定済みなのでインストール!)

このスクリプトの質問や評価の投稿はこちら通報はこちらへお寄せください
// ==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>⚔️ &nbsp;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 &amp; 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();
  }

})();