Faction war coordination
Ezt a szkriptet nem ajánlott közvetlenül telepíteni. Ez egy könyvtár más szkriptek számára, amik tartalmazzák a // @require https://update.greasyfork.org/scripts/575828/1811042/Torn%20War%20Coordinator.js hivatkozást.
// ==UserScript==
// @name Torn War Coordinator
// @namespace torn.warcoord
// @version 4.3.0
// @description Faction war coordination
// @author Your Faction
// @match https://www.torn.com/*
// @grant GM_setValue
// @grant GM_getValue
// @grant GM_addStyle
// @grant GM_xmlhttpRequest
// @grant GM_notification
// @connect camofdestin.workers.dev
// @connect workers.dev
// @connect api.torn.com
// @run-at document-idle
// ==/UserScript==
const SERVER_URL = "https://torn-war.camofdestin.workers.dev";
const FACTION_KEY = "Moonlit";
(function () {
"use strict";
function detectMe() {
const link = document.querySelector('a[href*="/profiles.php?XID="]');
if (link) {
const m = link.href.match(/XID=(\d+)/);
const name = link.textContent.trim();
if (m) return { id: m[1], name };
}
return GM_getValue("me", null);
}
let ME = detectMe();
if (ME) GM_setValue("me", ME);
if (!ME) setTimeout(() => { ME = detectMe(); if (ME) GM_setValue("me", ME); }, 3000);
let ADMIN_KEY = GM_getValue("admin_key", "");
let knownAttackIds = new Set(JSON.parse(GM_getValue("known_attacks", "[]")));
function api(path, opts = {}) {
return new Promise((resolve, reject) => {
const headers = { "Content-Type": "application/json", "X-Faction-Key": FACTION_KEY };
if (ADMIN_KEY) headers["X-Admin-Key"] = ADMIN_KEY;
GM_xmlhttpRequest({
method: opts.method || "GET",
url: SERVER_URL + path,
headers,
data: opts.body ? JSON.stringify(opts.body) : undefined,
onload: (res) => {
try {
const data = JSON.parse(res.responseText);
if (res.status === 401) resolve({ __error: data.error || "Unauthorized" });
else resolve(data);
} catch (e) { resolve({ __error: "Invalid response" }); }
},
onerror: () => reject(new Error("Network error")),
});
});
}
function tornApi(endpoint, keyOverride) {
const apiKey = keyOverride || GM_getValue("torn_api_key", "");
if (!apiKey) return Promise.reject(new Error("No Torn API key set"));
return new Promise((resolve, reject) => {
GM_xmlhttpRequest({
method: "GET",
url: `https://api.torn.com/${endpoint}${endpoint.includes("?") ? "&" : "?"}key=${apiKey}`,
onload: (res) => {
try {
const data = JSON.parse(res.responseText);
if (data.error) reject(new Error(data.error.error || "Torn API error"));
else resolve(data);
} catch (e) { reject(new Error("Bad Torn API response")); }
},
onerror: () => reject(new Error("Torn API network error")),
});
});
}
function scrapeChainCount() {
const bar = document.querySelector('[class*="bar-chain"], [class*="chain-bar"], #barChain');
if (!bar) return null;
const text = bar.textContent || "";
const m = text.match(/(\d+)\s*\/\s*(\d+)/);
if (!m) return null;
const c = parseInt(m[1], 10);
if (!c) return null;
return { chainCount: c };
}
setInterval(async () => {
const c = scrapeChainCount();
if (c) { try { await api("/chain-update", { method: "POST", body: c }); } catch {} }
}, 8000);
async function checkAutoHit() {
const apiKey = GM_getValue("my_personal_api_key", "");
if (!apiKey || !ME || !lastState) return;
const enemyIds = new Set((lastState.enemyFaction?.members || []).map(e => String(e.id)));
if (!enemyIds.size) return;
try {
const data = await tornApi(`user/?selections=attacks`, apiKey);
const attacks = data.attacks || {};
const recent = Object.entries(attacks).filter(([attackId, a]) => {
if (knownAttackIds.has(attackId)) return false;
if (String(a.attacker_id) !== String(ME.id)) return false;
if (!enemyIds.has(String(a.defender_id))) return false;
if (a.result !== "Hospitalized" && a.result !== "Mugged" && a.result !== "Attacked" && a.result !== "Special") return false;
const ts = (a.timestamp_ended || 0) * 1000;
return Date.now() - ts < 5 * 60 * 1000;
});
for (const [attackId, a] of recent) {
knownAttackIds.add(attackId);
const target = (lastState.enemyFaction.members.find(e => String(e.id) === String(a.defender_id)))?.name || a.defender_name;
await api("/auto-hit", { method: "POST", body: { id: ME.id, name: ME.name, target, attackId } });
}
const arr = Array.from(knownAttackIds).slice(-200);
knownAttackIds = new Set(arr);
GM_setValue("known_attacks", JSON.stringify(arr));
} catch (e) {}
}
setInterval(checkAutoHit, 60000);
let audioCtx = null;
function beep(freq, dur, vol) {
try {
if (!audioCtx) audioCtx = new (window.AudioContext || window.webkitAudioContext)();
const osc = audioCtx.createOscillator();
const gain = audioCtx.createGain();
osc.frequency.value = freq;
gain.gain.setValueAtTime(vol, audioCtx.currentTime);
gain.gain.exponentialRampToValueAtTime(0.001, audioCtx.currentTime + dur / 1000);
osc.connect(gain).connect(audioCtx.destination);
osc.start(); osc.stop(audioCtx.currentTime + dur / 1000);
} catch {}
}
function alarmTurn() { beep(1200,150,0.4); setTimeout(()=>beep(1600,150,0.4),180); setTimeout(()=>beep(1200,200,0.4),360); }
function alarmWarning() { beep(600,300,0.5); setTimeout(()=>beep(900,300,0.5),350); }
function notify(t, b) { try { if (typeof GM_notification === "function") GM_notification({ title: t, text: b, timeout: 8000 }); } catch {} }
GM_addStyle(`
#wc-panel { position: fixed; top: 80px; right: 12px; width: 380px; height: 600px; min-width: 280px; min-height: 150px; background: linear-gradient(180deg,#1a0f0a,#0a0503); border: 1px solid #5a2a18; border-radius: 8px; color: #e8d8c8; font-family: 'Trebuchet MS',sans-serif; font-size: 12px; box-shadow: 0 8px 32px rgba(0,0,0,.7); z-index: 99999; overflow: hidden; display: flex; flex-direction: column; }
#wc-panel.collapsed { height: 36px !important; min-height: 36px !important; }
#wc-panel.flash-turn { animation: wc-flash 0.5s 6; }
@keyframes wc-flash { 0%,100%{box-shadow:0 0 0 0 rgba(255,140,60,.9);} 50%{box-shadow:0 0 40px 20px rgba(255,140,60,.9);} }
#wc-header { padding: 8px 12px; background: linear-gradient(90deg,#6b1d0e,#2a0c05); border-bottom: 1px solid #5a2a18; cursor: move; user-select: none; display: flex; justify-content: space-between; align-items: center; font-weight: 700; letter-spacing: 1px; text-transform: uppercase; touch-action: none; flex-shrink: 0; }
#wc-header .wc-title { color: #ffb38a; font-size: 11px; }
#wc-header .wc-toggle { color: #ffb38a; font-size: 16px; cursor: pointer; padding: 0 6px; }
#wc-body { padding: 10px; overflow-y: auto; flex: 1; }
.wc-resize-h { position: absolute; z-index: 100000; }
.wc-resize-e { right: 0; top: 36px; bottom: 14px; width: 6px; cursor: ew-resize; }
.wc-resize-w { left: 0; top: 36px; bottom: 14px; width: 6px; cursor: ew-resize; }
.wc-resize-s { bottom: 0; left: 14px; right: 14px; height: 6px; cursor: ns-resize; }
.wc-resize-se { right: 0; bottom: 0; width: 18px; height: 18px; cursor: nwse-resize; background: linear-gradient(135deg,transparent 50%,#ff8c3c 50%); }
.wc-resize-sw { left: 0; bottom: 0; width: 14px; height: 14px; cursor: nesw-resize; }
#wc-panel.collapsed .wc-resize-h { display: none; }
.wc-section { margin-bottom: 12px; }
.wc-section-title { font-size: 10px; color: #c47a55; text-transform: uppercase; letter-spacing: 1.5px; margin-bottom: 6px; border-bottom: 1px solid #3a1a10; padding-bottom: 3px; }
.wc-stat-row { display: flex; justify-content: space-between; padding: 3px 0; }
.wc-stat-row .label { color: #a08070; }
.wc-stat-row .val { color: #ffd8b8; font-weight: 700; }
.wc-mybanner { background: linear-gradient(135deg,#c44a08,#ff6b1c,#c44a08); background-size: 200% 200%; animation: wc-flow 2s infinite, wc-pulse 0.8s infinite; border: 2px solid #ffb38a; border-radius: 8px; padding: 14px; text-align: center; margin-bottom: 10px; color: #fff; }
@keyframes wc-flow { 0%,100%{background-position:0% 50%;} 50%{background-position:100% 50%;} }
@keyframes wc-pulse { 0%,100%{transform:scale(1);} 50%{transform:scale(1.02);} }
.wc-mybanner .label { font-size: 10px; letter-spacing: 3px; }
.wc-mybanner .number { font-size: 56px; font-weight: 900; line-height: 1; margin: 4px 0; font-family: Georgia,serif; }
.wc-mybanner .name { font-size: 14px; font-weight: 700; }
.wc-mybanner.warn { animation: wc-warn 0.4s infinite; }
@keyframes wc-warn { 0%,100%{background:#c41818;} 50%{background:#ff3838;} }
.wc-targets-list { background: #1a0a05; border: 1px solid #4a1808; border-radius: 6px; padding: 8px; margin-top: 8px; }
.wc-targets-list .label { font-size: 9px; color: #ffb38a; letter-spacing: 1.5px; text-transform: uppercase; margin-bottom: 4px; }
.wc-target-link { display: block; padding: 6px 8px; margin: 3px 0; background: #2a1408; border: 1px solid #5a2a18; border-radius: 4px; color: #ffd8b8; text-decoration: none; font-weight: 700; }
.wc-target-link:hover { background: #6b2810; color: #fff; }
.wc-current-other { background: linear-gradient(90deg,#4a1808,#2a0c05); border: 1px solid #6b2810; border-radius: 6px; padding: 10px; text-align: center; margin-bottom: 8px; }
.wc-current-other .label { font-size: 9px; color: #c47a55; letter-spacing: 2px; text-transform: uppercase; }
.wc-current-other .number { font-size: 32px; font-weight: 700; color: #ffd8b8; font-family: Georgia,serif; }
.wc-current-other .name { font-size: 13px; color: #fff5ec; }
.wc-rogue-alert { background: #4a0808; border: 2px solid #ff3838; border-radius: 6px; padding: 8px; margin-bottom: 8px; color: #ffaaaa; }
.wc-hold-banner { background: #6b0808; border: 2px solid #c41818; border-radius: 6px; padding: 10px; margin-bottom: 8px; text-align: center; color: #ffaaaa; font-weight: 700; animation: wc-hold 1s infinite; }
@keyframes wc-hold { 0%,100%{opacity:1;} 50%{opacity:0.7;} }
.wc-queue-item { display: flex; justify-content: space-between; align-items: center; padding: 5px 8px; border-radius: 4px; margin: 2px 0; background: #1f100a; border-left: 3px solid transparent; gap: 4px; font-size: 11px; }
.wc-queue-item.current { border-left-color: #ff8c3c; background: #2a1408; }
.wc-queue-item.next { border-left-color: #c47a55; }
.wc-queue-item.is-me { color: #ffd8b8; font-weight: 700; }
.wc-queue-num { color: #ff8c3c; font-weight: 700; min-width: 38px; font-family: Georgia,serif; }
.wc-queue-name { flex: 1; }
.wc-queue-energy { color: #1ac442; font-size: 9px; min-width: 30px; text-align: right; }
.wc-queue-energy.med { color: #ffb800; }
.wc-queue-energy.low { color: #ff6b1c; }
.wc-queue-energy.stale { opacity: 0.5; }
.wc-queue-targets-count { color: #c47a55; font-size: 9px; }
.wc-queue-hits { color: #888; font-size: 10px; }
.wc-queue-actions { display: flex; gap: 2px; }
button.wc-btn { background: #4a1808; color: #ffd8b8; border: 1px solid #6b2810; padding: 6px 10px; border-radius: 4px; cursor: pointer; font-size: 11px; font-weight: 700; text-transform: uppercase; font-family: inherit; }
button.wc-btn:hover { background: #6b2810; }
button.wc-btn.primary { background: #c44a08; border-color: #ff6b1c; }
button.wc-btn.success { background: #0a5a1a; border-color: #1ac442; }
button.wc-btn.danger { background: #6b0808; border-color: #c41818; }
button.wc-btn.tiny { padding: 2px 5px; font-size: 9px; }
.wc-btn-row { display: flex; gap: 4px; flex-wrap: wrap; }
.wc-toggle-active { width: 100%; padding: 10px; font-size: 13px; margin-bottom: 8px; }
.wc-toggle-active.active { background: #0a5a1a; border-color: #1ac442; }
.wc-claim-admin-btn { width: 100%; padding: 10px; margin-bottom: 8px; }
.wc-energy-row { display: flex; gap: 4px; align-items: center; margin-bottom: 8px; padding: 6px; background: #1f100a; border-radius: 4px; flex-wrap: wrap; }
.wc-energy-row label { font-size: 10px; color: #c47a55; }
.wc-log { font-size: 10px; color: #a08070; max-height: 100px; overflow-y: auto; background: #0a0503; padding: 6px; border-radius: 4px; }
.wc-log-entry { padding: 1px 0; border-bottom: 1px dotted #2a1410; }
.wc-admin-section { background: #2a0a05; padding: 8px; border-radius: 4px; margin-top: 8px; }
.wc-admin-section .wc-section-title { color: #ff8c3c; }
.wc-admin-row { display: flex; gap: 4px; margin-bottom: 4px; align-items: center; }
.wc-admin-row label { font-size: 10px; color: #c47a55; min-width: 80px; }
input.wc-input, textarea.wc-input { background: #0a0503; color: #e8d8c8; border: 1px solid #3a1a10; border-radius: 3px; padding: 4px; font-size: 11px; font-family: monospace; box-sizing: border-box; flex: 1; }
.wc-error { color: #ff6b6b; font-size: 10px; padding: 4px; }
.wc-timer-bar { height: 6px; background: #1f100a; border-radius: 3px; overflow: hidden; margin-top: 4px; }
.wc-timer-fill { height: 100%; transition: width 1s linear, background 0.3s; }
.wc-split { display: flex; gap: 8px; margin-top: 6px; flex-wrap: wrap; }
.wc-split-col { flex: 1; min-width: 200px; background: #0a0503; padding: 6px; border-radius: 4px; max-height: 400px; overflow-y: auto; }
.wc-split-col h4 { margin: 0 0 6px 0; font-size: 10px; color: #ffb38a; text-transform: uppercase; letter-spacing: 1px; border-bottom: 1px solid #3a1a10; padding-bottom: 3px; }
.wc-faction-row { display: flex; justify-content: space-between; align-items: center; padding: 3px 4px; border-bottom: 1px solid #1f100a; gap: 4px; font-size: 10px; }
.wc-faction-row:hover { background: #1f100a; }
.wc-faction-name { flex: 1; color: #ffd8b8; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.wc-faction-status { font-size: 9px; min-width: 45px; text-align: right; }
.wc-faction-energy { color: #1ac442; font-size: 9px; min-width: 28px; text-align: right; }
.wc-faction-energy.med { color: #ffb800; }
.wc-faction-energy.low { color: #ff6b1c; }
.wc-faction-energy.stale { opacity: 0.4; }
.wc-assigned-badge { color: #c47a55; font-size: 9px; }
.wc-assign-modal { position: fixed; top: 50%; left: 50%; transform: translate(-50%,-50%); background: #1a0f0a; border: 2px solid #ff8c3c; border-radius: 8px; padding: 16px; z-index: 100000; width: 380px; max-height: 80vh; overflow-y: auto; }
.wc-assign-modal h3 { margin: 0 0 8px 0; color: #ffb38a; font-size: 14px; }
.wc-assign-row { display: flex; align-items: center; padding: 4px 6px; border-bottom: 1px solid #2a1410; }
.wc-modal-overlay { position: fixed; top: 0; left: 0; right: 0; bottom: 0; background: rgba(0,0,0,.7); z-index: 99998; }
`);
const panel = document.createElement("div");
panel.id = "wc-panel";
panel.innerHTML = `
<div id="wc-header"><span class="wc-title">⚔ War Coordinator</span><span class="wc-toggle">−</span></div>
<div id="wc-body">Loading...</div>
<div class="wc-resize-h wc-resize-e" data-dir="e"></div>
<div class="wc-resize-h wc-resize-w" data-dir="w"></div>
<div class="wc-resize-h wc-resize-s" data-dir="s"></div>
<div class="wc-resize-h wc-resize-sw" data-dir="sw"></div>
<div class="wc-resize-h wc-resize-se" data-dir="se"></div>
`;
document.body.appendChild(panel);
const header = panel.querySelector("#wc-header");
const body = panel.querySelector("#wc-body");
const toggleBtn = panel.querySelector(".wc-toggle");
const savedPos = GM_getValue("panel_pos", null);
if (savedPos?.top !== undefined) {
panel.style.top = savedPos.top + "px";
panel.style.left = savedPos.left + "px";
panel.style.right = "auto";
}
const savedSize = GM_getValue("panel_size", null);
if (savedSize?.width) panel.style.width = savedSize.width + "px";
if (savedSize?.height) panel.style.height = savedSize.height + "px";
let dragging = false, dox = 0, doy = 0, dmoved = false;
function dragStart(x, y, t) {
if (t === toggleBtn) return false;
const r = panel.getBoundingClientRect();
dox = x - r.left; doy = y - r.top;
dragging = true; dmoved = false;
return true;
}
function dragMove(x, y) {
if (!dragging) return;
dmoved = true;
panel.style.left = Math.max(0, Math.min(window.innerWidth - 50, x - dox)) + "px";
panel.style.top = Math.max(0, Math.min(window.innerHeight - 40, y - doy)) + "px";
panel.style.right = "auto";
}
function dragEnd() {
if (!dragging) return;
dragging = false;
if (dmoved) {
const r = panel.getBoundingClientRect();
GM_setValue("panel_pos", { top: r.top, left: r.left });
}
}
let resizing = null;
function resizeStart(x, y, dir) {
const r = panel.getBoundingClientRect();
resizing = { dir, sx: x, sy: y, sw: r.width, sh: r.height, sl: r.left, st: r.top };
}
function resizeMove(x, y) {
if (!resizing) return;
const { dir, sx, sy, sw, sh, sl, st } = resizing;
const dx = x - sx, dy = y - sy;
let w = sw, h = sh, l = sl, t = st;
if (dir.includes("e")) w = Math.max(280, sw + dx);
if (dir.includes("w")) { w = Math.max(280, sw - dx); l = sl + (sw - w); }
if (dir.includes("s")) h = Math.max(150, sh + dy);
panel.style.width = w + "px";
panel.style.height = h + "px";
panel.style.left = l + "px";
panel.style.top = t + "px";
panel.style.right = "auto";
}
function resizeEnd() {
if (!resizing) return;
const r = panel.getBoundingClientRect();
GM_setValue("panel_size", { width: r.width, height: r.height });
GM_setValue("panel_pos", { top: r.top, left: r.left });
resizing = null;
}
header.addEventListener("mousedown", (e) => { if (dragStart(e.clientX, e.clientY, e.target)) e.preventDefault(); });
panel.querySelectorAll(".wc-resize-h").forEach(h => {
h.addEventListener("mousedown", (e) => { e.stopPropagation(); e.preventDefault(); resizeStart(e.clientX, e.clientY, h.dataset.dir); });
});
document.addEventListener("mousemove", (e) => {
if (resizing) resizeMove(e.clientX, e.clientY);
else dragMove(e.clientX, e.clientY);
});
document.addEventListener("mouseup", () => {
if (resizing) resizeEnd();
else dragEnd();
});
toggleBtn.addEventListener("click", (e) => {
e.stopPropagation();
panel.classList.toggle("collapsed");
toggleBtn.textContent = panel.classList.contains("collapsed") ? "+" : "−";
});
let lastState = null, lastTurnId = null, warningFiredFor = null;
function fmtTime(s) { if (s == null || s < 0) return "--:--"; const m = Math.floor(s/60), x = s%60; return `${m}:${String(x).padStart(2,"0")}`; }
function liveTimer(s) {
if (!s.chainTimerAt || !s.chainTimerSec) return null;
const elapsed = Math.floor((Date.now() - s.chainTimerAt) / 1000);
const remaining = s.chainTimerSec - elapsed;
if (remaining < 0) return 0;
if (remaining > s.chainTimerSec) return null;
return remaining;
}
function timerColor(s) { if (s<=30) return "#ff3838"; if (s<=60) return "#ff6b1c"; if (s<=120) return "#ffb800"; return "#1ac442"; }
function statusColor(s) { if (s==="Okay") return "#1ac442"; if (s==="Hospital") return "#ff6b1c"; if (s==="Traveling"||s==="Abroad") return "#888"; return "#aaa"; }
function energyClass(e) { if (e>=150) return ""; if (e>=50) return "med"; return "low"; }
function energyStale(t) { return Date.now() - (t||0) > 30*60*1000; }
async function claimAdmin() {
const c = prompt("Enter War Dispatcher password:");
if (!c) return;
ADMIN_KEY = c.trim();
const res = await api("/verify-admin");
if (res?.admin) { GM_setValue("admin_key", ADMIN_KEY); alert("You are now War Dispatcher."); refresh(); }
else { ADMIN_KEY = ""; alert("Wrong password."); }
}
function setApiKey() { const cur = GM_getValue("torn_api_key",""); const c = prompt("Dispatcher Torn API key:", cur); if (c===null) return; GM_setValue("torn_api_key", c.trim()); alert("Saved."); }
function setMyPersonalApiKey() { const cur = GM_getValue("my_personal_api_key",""); const c = prompt("YOUR Limited Access key for auto-hit:", cur); if (c===null) return; GM_setValue("my_personal_api_key", c.trim()); alert(c.trim() ? "Auto-hit ON." : "Auto-hit OFF."); }
async function pullMyFaction() {
try {
const data = await tornApi(`v2/faction/48480/members?`);
const members = (data.members||[]).map(m => ({ id: String(m.id), name: m.name, level: m.level, status: m.status?.state||"Okay", statusUntil: m.status?.until||0 }));
if (!members.length) return alert("No members");
await api("/admin/set-my-faction", { method: "POST", body: { factionId: 48480, factionName: "Moon Lust", members } });
alert(`Pulled ${members.length} members.`); refresh();
} catch (e) { alert("Error: " + e.message); }
}
async function pullEnemyFaction() {
const fid = prompt("Enter ENEMY faction ID:");
if (!fid) return;
const factionId = parseInt(fid.trim(), 10);
if (!factionId) return alert("Invalid ID");
try {
const data = await tornApi(`v2/faction/${factionId}/members?`);
const members = (data.members||[]).map(m => ({ id: String(m.id), name: m.name, level: m.level, status: m.status?.state||"Okay", statusUntil: m.status?.until||0 }));
if (!members.length) return alert("No members");
await api("/admin/set-enemy-faction", { method: "POST", body: { factionId, members } });
alert(`Pulled ${members.length} enemies.`); refresh();
} catch (e) { alert("Error: " + e.message); }
}
async function refreshStatuses() {
if (!lastState) return;
try {
const tasks = [
lastState.myFaction?.id ? tornApi(`v2/faction/${lastState.myFaction.id}/members?`) : Promise.resolve(null),
lastState.enemyFaction?.id ? tornApi(`v2/faction/${lastState.enemyFaction.id}/members?`) : Promise.resolve(null),
];
const [my, en] = await Promise.all(tasks);
const b = {};
if (my?.members) b.myMembers = my.members.map(m => ({ id: String(m.id), name: m.name, level: m.level, status: m.status?.state||"Okay", statusUntil: m.status?.until||0 }));
if (en?.members) b.enemyMembers = en.members.map(m => ({ id: String(m.id), name: m.name, level: m.level, status: m.status?.state||"Okay", statusUntil: m.status?.until||0 }));
await api("/admin/refresh-statuses", { method: "POST", body: b });
refresh();
} catch (e) { alert("Refresh failed: " + e.message); }
}
function openAssignModal(eId, eName) {
if (!lastState) return;
const ov = document.createElement("div"); ov.className = "wc-modal-overlay";
const md = document.createElement("div"); md.className = "wc-assign-modal";
const cur = (lastState.assignments && lastState.assignments[eId]) || [];
const rows = lastState.roster.map(m => {
const ch = cur.includes(m.id) ? "checked" : "";
const er = lastState.energyReports?.[m.id];
const ee = er && !energyStale(er.reportedAt) ? ` <span style="color:#1ac442;">${er.energy}E</span>` : "";
return `<div class="wc-assign-row"><label style="flex:1;cursor:pointer;"><input type="checkbox" data-mid="${m.id}" ${ch}> ${m.name}${ee}</label></div>`;
}).join("");
md.innerHTML = `<h3>Assign target: ${eName}</h3>
${rows || '<div style="color:#888;">No members</div>'}
<div style="display:flex;gap:6px;margin-top:12px;">
<button class="wc-btn primary" id="wc-asv" style="flex:1;">Save</button>
<button class="wc-btn" id="wc-acn">Cancel</button>
</div>`;
document.body.appendChild(ov); document.body.appendChild(md);
const cl = () => { ov.remove(); md.remove(); };
ov.onclick = cl; md.querySelector("#wc-acn").onclick = cl;
md.querySelector("#wc-asv").onclick = async () => {
const ids = Array.from(md.querySelectorAll("input[type=checkbox]:checked")).map(c => c.dataset.mid);
await api("/admin/assign", { method: "POST", body: { enemyId: eId, memberIds: ids } });
cl(); refresh();
};
}
async function quickClaimHit() {
if (!ME) return;
await api("/claim-hit", { method: "POST", body: { id: ME.id, name: ME.name } });
refresh();
}
function render(state) {
if (state?.__error) { body.innerHTML = `<div class="wc-error">Error: ${state.__error}</div>`; return; }
if (!state) { body.innerHTML = '<div class="wc-error">Could not reach server</div>'; return; }
lastState = state;
const isAdmin = !!state.isAdmin;
const meId = ME?.id;
const meInRoster = state.roster.find(m => m.id === meId);
const meActive = !!meInRoster?.active;
const currentId = state.queue[state.currentIndex];
const current = state.roster.find(m => m.id === currentId);
const isMyTurn = currentId === meId;
const timerSec = liveTimer(state);
const myEnergy = state.energyReports?.[meId];
if (currentId && currentId !== lastTurnId) {
warningFiredFor = null;
if (isMyTurn) {
panel.classList.remove("flash-turn"); void panel.offsetWidth; panel.classList.add("flash-turn");
alarmTurn();
notify("YOUR TURN", `Call out #${meInRoster?.assignedNumber || "?"}`);
}
}
lastTurnId = currentId;
if (isMyTurn && timerSec !== null && timerSec > 0 && timerSec <= state.warningSec && warningFiredFor !== currentId) {
warningFiredFor = currentId; alarmWarning();
notify("Time warning", `${fmtTime(timerSec)} left`);
}
const myTargetIds = [];
if (state.assignments && state.enemyFaction?.members) {
Object.keys(state.assignments).forEach(eid => { if (state.assignments[eid].includes(meId)) myTargetIds.push(eid); });
}
const myTargets = myTargetIds.map(eid => state.enemyFaction.members.find(e => e.id === eid)).filter(Boolean);
const queueHtml = state.queue.map((id, i) => {
const m = state.roster.find(r => r.id === id);
if (!m) return "";
const cls = ["wc-queue-item",
i === state.currentIndex ? "current" : "",
i === (state.currentIndex + 1) % state.queue.length ? "next" : "",
id === meId ? "is-me" : ""].filter(Boolean).join(" ");
const er = state.energyReports?.[m.id];
const stale = er ? energyStale(er.reportedAt) : false;
const eHtml = er ? `<span class="wc-queue-energy ${energyClass(er.energy)} ${stale ? "stale" : ""}">${er.energy}E</span>` : `<span class="wc-queue-energy stale">--</span>`;
let tCount = 0;
if (state.assignments) Object.keys(state.assignments).forEach(eid => { if (state.assignments[eid].includes(m.id)) tCount++; });
const adminBtns = isAdmin ? `<div class="wc-queue-actions">
<button class="wc-btn tiny" data-jump="${m.id}">▶</button>
<button class="wc-btn tiny" data-up="${m.id}">↑</button>
<button class="wc-btn tiny" data-down="${m.id}">↓</button>
<button class="wc-btn tiny danger" data-remove="${m.id}">✕</button>
</div>` : "";
return `<div class="${cls}">
<span class="wc-queue-num">#${m.assignedNumber || "?"}</span>
<span class="wc-queue-name">${m.name}</span>
${eHtml}
${tCount ? `<span class="wc-queue-targets-count">🎯${tCount}</span>` : ""}
<span class="wc-queue-hits">${m.hits || 0}</span>
${adminBtns}
</div>`;
}).join("");
const inactiveHtml = state.roster.filter(m => !m.active).map(m =>
`<div class="wc-queue-item" style="opacity:.5"><span class="wc-queue-name">${m.name}</span><span class="wc-queue-hits">inactive</span></div>`
).join("");
const logHtml = (state.log || []).slice(0, 8).map(e => `<div class="wc-log-entry">${e.msg}</div>`).join("");
const showHoldBanner = state.hitLock && meActive && !isMyTurn && current;
const warnNow = isMyTurn && timerSec !== null && timerSec > 0 && timerSec <= state.warningSec;
const timerDisplay = timerSec !== null ? fmtTime(timerSec) : "--:--";
const timerColorVal = timerSec !== null ? timerColor(timerSec) : "#666";
const timerBarWidth = timerSec !== null ? Math.min(100, (timerSec / 300) * 100) : 0;
let splitHtml = "";
if (isAdmin && (state.myFaction?.members?.length || state.enemyFaction?.members?.length)) {
const myRows = (state.myFaction?.members || []).map(m => {
const er = state.energyReports?.[m.id];
const stale = er ? energyStale(er.reportedAt) : false;
const e = er ? `<span class="wc-faction-energy ${energyClass(er.energy)} ${stale ? "stale" : ""}">${er.energy}E</span>` : `<span class="wc-faction-energy stale">--</span>`;
return `<div class="wc-faction-row">
<span class="wc-faction-name">${m.name} <span style="color:#666;">L${m.level}</span></span>
${e}
<span class="wc-faction-status" style="color:${statusColor(m.status)}">${m.status}</span>
</div>`;
}).join("");
const enemyRows = (state.enemyFaction?.members || []).map(e => {
const assigned = (state.assignments && state.assignments[e.id]) || [];
const names = assigned.map(mid => state.roster.find(m => m.id === mid)?.name).filter(Boolean);
return `<div class="wc-faction-row">
<span class="wc-faction-name">${e.name} <span style="color:#666;">L${e.level}</span></span>
<span class="wc-faction-status" style="color:${statusColor(e.status)}">${e.status}</span>
<span class="wc-assigned-badge">${names.length ? "→" + names.length : ""}</span>
<button class="wc-btn tiny primary" data-assign-enemy="${e.id}" data-assign-name="${e.name}">Assign</button>
</div>`;
}).join("");
splitHtml = `<div class="wc-split">
<div class="wc-split-col"><h4>${state.myFaction?.name || "My Faction"} (${state.myFaction?.members?.length || 0})</h4>${myRows || '<div style="color:#666;font-size:10px;">Click "Pull MY Faction"</div>'}</div>
<div class="wc-split-col"><h4>${state.enemyFaction?.name || "Enemy"} (${state.enemyFaction?.members?.length || 0})</h4>${enemyRows || '<div style="color:#666;font-size:10px;">Click "Pull Enemy Faction"</div>'}</div>
</div>`;
}
body.innerHTML = `
${state.rogueAlert ? `<div class="wc-rogue-alert">
<div style="font-weight:700;color:#ff6b6b;">ROGUE HIT</div>
<div>Chain at <b>${state.rogueAlert.actual}</b>, expected <b>${state.rogueAlert.expected}</b>.</div>
${isAdmin ? `<div style="margin-top:6px;display:flex;gap:4px;">
<button class="wc-btn primary" id="wc-sync-chain">Sync Numbers</button>
<button class="wc-btn" id="wc-dismiss-rogue">Dismiss</button>
</div>` : ""}
</div>` : ""}
${isMyTurn && current ? `<div class="wc-mybanner ${warnNow ? "warn" : ""}">
<div class="label">${warnNow ? "HIT NOW" : "YOUR TURN"}</div>
<div class="number">#${current.assignedNumber || "?"}</div>
<div class="name">${current.name}</div>
<div style="font-size:11px;margin-top:4px;">${timerDisplay}</div>
</div>` : current ? `<div class="wc-current-other">
<div class="label">Currently Up</div>
<div class="number">#${current.assignedNumber || "?"}</div>
<div class="name">${current.name}</div>
</div>` : ""}
${showHoldBanner ? `<div class="wc-hold-banner">HOLD - #${current.assignedNumber} IS UP</div>` : ""}
${myTargets.length ? `<div class="wc-targets-list">
<div class="label">🎯 Your Assigned Targets - Click to Attack</div>
${myTargets.map(t => `<a class="wc-target-link" href="https://www.torn.com/loader.php?sid=attack&user2ID=${t.id}" target="_blank">${t.name} <span style="float:right;color:${statusColor(t.status)};font-size:10px;">${t.status}</span></a>`).join("")}
</div>` : ""}
<div class="wc-section">
<div class="wc-section-title">${state.warTitle || "War"}</div>
<div class="wc-stat-row"><span class="label">Chain</span><span class="val">${state.chainCount || 0}</span></div>
<div class="wc-stat-row"><span class="label">Hits Logged</span><span class="val">${state.hitsLogged || 0}</span></div>
<div class="wc-stat-row"><span class="label">Timer</span><span class="val" id="wc-timer-text" style="color:${timerColorVal}">${timerDisplay}</span></div>
<div class="wc-timer-bar"><div class="wc-timer-fill" id="wc-timer-fill" style="width:${timerBarWidth}%;background:${timerColorVal}"></div></div>
<div class="wc-stat-row" style="margin-top:6px;"><span class="label">Active</span><span class="val">${state.queue.length} / ${state.roster.length}</span></div>
</div>
${ME ? `
<div class="wc-energy-row">
<label>My Energy:</label>
<input type="number" class="wc-input" id="wc-energy-input" value="${myEnergy?.energy || ''}" placeholder="?" style="max-width:60px;">
<button class="wc-btn" id="wc-energy-save">Save</button>
<button class="wc-btn primary" id="wc-claim-hit">I Got My Hit</button>
</div>
<button class="wc-btn wc-toggle-active ${meActive ? "active" : ""}" id="wc-toggle-me">${meActive ? "I'M IN THE CHAIN" : "Join the chain"}</button>
` : `<div class="wc-error">Detecting Torn ID...</div>`}
<div class="wc-section">
<div class="wc-section-title">Chain Order</div>
${queueHtml || '<div style="color:#6a4a3a;font-size:11px;">No one active.</div>'}
</div>
${inactiveHtml ? `<div class="wc-section"><div class="wc-section-title">Inactive</div>${inactiveHtml}</div>` : ""}
${logHtml ? `<div class="wc-section"><div class="wc-section-title">Recent</div><div class="wc-log">${logHtml}</div></div>` : ""}
${!isAdmin ? `<button class="wc-btn wc-claim-admin-btn" id="wc-claim-admin">🛡️ Become War Dispatcher</button>
<div style="margin-top:8px;font-size:10px;color:#6a4a3a;text-align:center;">
<button class="wc-btn tiny" id="wc-set-personal-key">${GM_getValue("my_personal_api_key", "") ? "Auto-hit ON ✓" : "Enable auto-hit"}</button>
</div>` : `
<div class="wc-admin-section">
<div class="wc-section-title">Dispatcher <button class="wc-btn tiny" id="wc-release-admin" style="float:right">Step Down</button></div>
<div class="wc-btn-row" style="margin-bottom:6px;">
<button class="wc-btn primary" id="wc-next">Next ▶</button>
<button class="wc-btn primary" id="wc-reset-timer">⏱ Start 5:00</button>
<button class="wc-btn ${state.hitLock ? "success" : ""}" id="wc-toggle-lock">${state.hitLock ? "Lock ON" : "Lock OFF"}</button>
<button class="wc-btn" id="wc-reset-hits">Reset Hits</button>
<button class="wc-btn danger" id="wc-reset-all">Reset All</button>
</div>
<div class="wc-admin-row"><label>Start #</label><input type="number" class="wc-input" id="wc-start-num" value="${state.startingNumber}" min="1"><button class="wc-btn" id="wc-set-start">Set</button></div>
<div class="wc-admin-row"><label>Warn (s)</label><input type="number" class="wc-input" id="wc-warn-sec" value="${state.warningSec}" min="10" max="290"><button class="wc-btn" id="wc-set-warn">Set</button></div>
<div style="border-top:1px solid #3a1a10;margin-top:10px;padding-top:8px;">
<div style="font-size:10px;color:#c47a55;margin-bottom:4px;">🎯 Faction Data</div>
<div class="wc-btn-row">
<button class="wc-btn" id="wc-set-apikey">Set API Key</button>
<button class="wc-btn primary" id="wc-pull-my">Pull MY Faction</button>
<button class="wc-btn primary" id="wc-pull-enemy">Pull Enemy Faction</button>
<button class="wc-btn" id="wc-refresh-statuses">Refresh Statuses</button>
</div>
${splitHtml}
</div>
${state.discordConfigured ? `<div style="border-top:1px solid #3a1a10;margin-top:10px;padding-top:8px;">
<div style="font-size:10px;color:#c47a55;margin-bottom:4px;">Discord</div>
<div class="wc-btn-row">
<button class="wc-btn ${state.discordEnabled ? "success" : ""}" id="wc-toggle-discord">${state.discordEnabled ? "Discord ON" : "Discord OFF"}</button>
<button class="wc-btn primary" id="wc-start-war">Announce Start</button>
</div>
</div>` : ""}
</div>`}
`;
const tBtn = body.querySelector("#wc-toggle-me");
if (tBtn) tBtn.onclick = async () => { await api("/toggle-active", { method: "POST", body: { id: ME.id, name: ME.name, active: !meActive } }); refresh(); };
const eBtn = body.querySelector("#wc-energy-save");
if (eBtn) eBtn.onclick = async () => {
const v = body.querySelector("#wc-energy-input").value.trim();
if (!v) return;
await api("/report-energy", { method: "POST", body: { id: ME.id, name: ME.name, energy: parseInt(v, 10) } });
refresh();
};
const claimBtn2 = body.querySelector("#wc-claim-hit");
if (claimBtn2) claimBtn2.onclick = quickClaimHit;
const claimAdminBtn = body.querySelector("#wc-claim-admin");
if (claimAdminBtn) claimAdminBtn.onclick = claimAdmin;
const personalKeyBtn = body.querySelector("#wc-set-personal-key");
if (personalKeyBtn) personalKeyBtn.onclick = setMyPersonalApiKey;
const releaseBtn = body.querySelector("#wc-release-admin");
if (releaseBtn) releaseBtn.onclick = () => { ADMIN_KEY = ""; GM_setValue("admin_key", ""); refresh(); };
if (isAdmin) {
body.querySelector("#wc-next").onclick = async () => { await api("/admin/next", { method: "POST", body: {} }); refresh(); };
body.querySelector("#wc-reset-timer").onclick = async () => { await api("/admin/reset-timer", { method: "POST", body: {} }); refresh(); };
body.querySelector("#wc-toggle-lock").onclick = async () => { await api("/admin/toggle-lock", { method: "POST", body: {} }); refresh(); };
body.querySelector("#wc-reset-hits").onclick = async () => { if (confirm("Reset hits?")) { await api("/admin/reset", { method: "POST", body: { what: "hits" } }); refresh(); } };
body.querySelector("#wc-reset-all").onclick = async () => { if (confirm("FULL RESET?")) { await api("/admin/reset", { method: "POST", body: { what: "all" } }); refresh(); } };
body.querySelector("#wc-set-start").onclick = async () => { const n = parseInt(body.querySelector("#wc-start-num").value, 10); await api("/admin/set-starting-number", { method: "POST", body: { startingNumber: n } }); refresh(); };
body.querySelector("#wc-set-warn").onclick = async () => { const n = parseInt(body.querySelector("#wc-warn-sec").value, 10); await api("/admin/set-warning", { method: "POST", body: { warningSec: n } }); refresh(); };
body.querySelector("#wc-set-apikey").onclick = setApiKey;
body.querySelector("#wc-pull-my").onclick = pullMyFaction;
body.querySelector("#wc-pull-enemy").onclick = pullEnemyFaction;
body.querySelector("#wc-refresh-statuses").onclick = refreshStatuses;
body.querySelectorAll("[data-assign-enemy]").forEach(b => {
b.onclick = (e) => { e.stopPropagation(); openAssignModal(b.dataset.assignEnemy, b.dataset.assignName); };
});
body.querySelectorAll("[data-jump]").forEach(b => { b.onclick = async (e) => { e.stopPropagation(); await api("/admin/jump-to", { method: "POST", body: { id: b.dataset.jump } }); refresh(); }; });
body.querySelectorAll("[data-up]").forEach(b => { b.onclick = async (e) => { e.stopPropagation(); await api("/admin/move", { method: "POST", body: { id: b.dataset.up, direction: "up" } }); refresh(); }; });
body.querySelectorAll("[data-down]").forEach(b => { b.onclick = async (e) => { e.stopPropagation(); await api("/admin/move", { method: "POST", body: { id: b.dataset.down, direction: "down" } }); refresh(); }; });
body.querySelectorAll("[data-remove]").forEach(b => { b.onclick = async (e) => { e.stopPropagation(); if (confirm("Remove?")) { await api("/admin/remove-from-queue", { method: "POST", body: { id: b.dataset.remove } }); refresh(); } }; });
const syncBtn = body.querySelector("#wc-sync-chain");
if (syncBtn) syncBtn.onclick = async () => { await api("/admin/sync-chain", { method: "POST", body: {} }); refresh(); };
const dismissBtn = body.querySelector("#wc-dismiss-rogue");
if (dismissBtn) dismissBtn.onclick = async () => { await api("/admin/dismiss-rogue", { method: "POST", body: {} }); refresh(); };
const dBtn = body.querySelector("#wc-toggle-discord");
if (dBtn) dBtn.onclick = async () => { await api("/admin/toggle-discord", { method: "POST", body: {} }); refresh(); };
const swBtn = body.querySelector("#wc-start-war");
if (swBtn) swBtn.onclick = async () => { if (confirm("Announce to Discord?")) await api("/admin/start-war", { method: "POST", body: {} }); };
}
}
setInterval(() => {
if (!lastState) return;
const sec = liveTimer(lastState);
const text = body.querySelector("#wc-timer-text");
const fill = body.querySelector("#wc-timer-fill");
if (sec === null) {
if (text) { text.textContent = "--:--"; text.style.color = "#666"; }
if (fill) { fill.style.width = "0%"; }
} else {
if (text) { text.textContent = fmtTime(sec); text.style.color = timerColor(sec); }
if (fill) { fill.style.width = Math.min(100, (sec / 300) * 100) + "%"; fill.style.background = timerColor(sec); }
}
}, 1000);
async function refresh() {
try { const state = await api("/state"); render(state); }
catch (e) { body.innerHTML = `<div class="wc-error">Connection error: ${e.message}</div>`; }
}
refresh();
setInterval(refresh, 6000);
})();