Norse Mythology Edition — Single API Key Engine, Tactical Cards, FF Scouter, War Room SOS, TornPDA Touch Support
This script should not be not be installed directly. It is a library for other scripts to include with the meta directive // @require https://update.greatest.deepsurf.us/scripts/571894/1786926/Valhalla%3A%20Warrior%27s%20Hall.js
// ==UserScript==
// @name Valhalla: Warrior's Hall
// @namespace http://tampermonkey.net/
// @version 12.0
// @description Norse Mythology Edition — Single API Key Engine, Tactical Cards, FF Scouter, War Room SOS, TornPDA Touch Support
// @author You
// @match https://www.torn.com/*
// @grant GM_addStyle
// @grant GM_setValue
// @grant GM_getValue
// @grant GM_xmlhttpRequest
// @connect api.torn.com
// @connect www.tornstats.com
// @connect ffscouter.com
// @connect pythonanywhere.com
// ==/UserScript==
(function() {
'use strict';
const BACKEND_URL = "https://surajsharma128.pythonanywhere.com";
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
// ᚢ NORSE MYTHOLOGY STYLING ᚢ
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
GM_addStyle(`
@import url('https://fonts.googleapis.com/css2?family=MedievalSharp&display=swap');
/* ── Scrollbars ── */
#vwh-wrap ::-webkit-scrollbar { width: 5px; height: 5px; }
#vwh-wrap ::-webkit-scrollbar-track { background: #0a0a12; }
#vwh-wrap ::-webkit-scrollbar-thumb { background: linear-gradient(#3a2800, #c9a84c, #3a2800); border-radius: 2px; }
#vwh-wrap ::-webkit-scrollbar-thumb:hover { background: #c9a84c; }
/* ── Root wrapper ── */
#vwh-wrap {
position: fixed; top: 10px; right: 10px; z-index: 99999;
width: 310px; max-height: 92vh;
display: flex; flex-direction: column; overflow: hidden;
background:
linear-gradient(160deg, #0f0d1a 0%, #110e1f 40%, #0d0b16 100%);
border: 2px solid #4a3800;
border-radius: 4px;
outline: 1px solid #1a1530;
box-shadow:
0 0 0 1px #c9a84c22,
inset 0 0 60px rgba(0,0,0,0.9),
0 20px 60px rgba(0,0,0,0.95),
0 0 30px rgba(139,26,26,0.15);
font-family: 'Georgia', 'Palatino Linotype', serif;
color: #c8b89a;
font-size: 13px;
transition: width 0.3s ease, box-shadow 0.3s;
}
@media (min-width: 600px) { #vwh-wrap { width: 460px; top: 55px; right: 18px; } }
@media (min-width: 900px) { #vwh-wrap { width: 530px; } }
#vwh-wrap.compact { width: 280px !important; font-size: 12px; }
#vwh-wrap.minimized #vwh-body { display: none !important; }
/* ── Corner rune decorations ── */
#vwh-wrap::before, #vwh-wrap::after {
content: '᛭'; position: absolute; color: #c9a84c44;
font-size: 22px; line-height: 1; pointer-events: none; z-index: 1;
}
#vwh-wrap::before { top: 3px; left: 5px; }
#vwh-wrap::after { bottom: 3px; right: 5px; }
/* ── Header ── */
.vwh-header {
background:
linear-gradient(180deg, #1e1500 0%, #130f00 50%, #0d0a00 100%);
border-bottom: 1px solid #4a3800;
box-shadow: 0 2px 12px rgba(0,0,0,0.8), inset 0 1px 0 #c9a84c33;
padding: 11px 13px 9px;
display: flex; justify-content: space-between; align-items: center;
cursor: grab; user-select: none; -webkit-user-select: none;
touch-action: none;
position: relative;
}
.vwh-header:active { cursor: grabbing; }
.vwh-header-title {
display: flex; align-items: center; gap: 7px;
font-size: 12px; text-transform: uppercase; letter-spacing: 2.5px;
color: #c9a84c; font-weight: bold;
text-shadow: 0 0 12px #c9a84c88, 0 2px 3px #000;
}
.vwh-header-rune {
font-size: 18px; color: #c9a84c; opacity: 0.9;
text-shadow: 0 0 8px #c9a84caa;
animation: vwh-runeflicker 4s ease-in-out infinite;
}
@keyframes vwh-runeflicker {
0%,100% { opacity: 0.9; text-shadow: 0 0 8px #c9a84caa; }
50% { opacity: 0.6; text-shadow: 0 0 4px #c9a84c66; }
}
.vwh-header-sub {
font-size: 9px; color: #6a5530; letter-spacing: 1px;
text-transform: uppercase; margin-top: 1px;
}
/* ── Header buttons ── */
.vwh-ctrl { display: flex; gap: 3px; cursor: default; }
.vwh-hbtn {
background: linear-gradient(180deg, #1e1500, #0d0900);
color: #7a6040; border: 1px solid #3a2800;
width: 28px; height: 28px; border-radius: 3px;
cursor: pointer; font-size: 12px; font-weight: bold;
display: flex; align-items: center; justify-content: center;
transition: all 0.2s; -webkit-tap-highlight-color: transparent;
touch-action: manipulation;
text-shadow: none; box-shadow: inset 0 1px 0 rgba(255,255,255,0.05);
}
.vwh-hbtn:hover, .vwh-hbtn:active { background: linear-gradient(180deg, #3a2800, #1e1500); color: #c9a84c; border-color: #c9a84c; box-shadow: 0 0 6px #c9a84c44; }
.vwh-hbtn.pinned { background: linear-gradient(180deg, #4a3000, #2a1800); color: #c9a84c; border-color: #c9a84c; box-shadow: 0 0 8px #c9a84c66; }
/* ── Divider ── */
.vwh-rune-divider {
text-align: center; font-size: 10px; color: #3a2800;
letter-spacing: 3px; padding: 4px 0;
background: #0d0a00;
border-bottom: 1px solid #1a1200;
user-select: none;
}
/* ── Tabs ── */
.vwh-tabs {
display: flex; overflow-x: auto; white-space: nowrap;
background: #080610; border-bottom: 1px solid #2a1e00;
-webkit-overflow-scrolling: touch;
scrollbar-width: none;
}
.vwh-tabs::-webkit-scrollbar { display: none; }
.vwh-tab {
flex: 0 0 auto; padding: 9px 13px;
font-size: 10px; text-transform: uppercase; letter-spacing: 1px;
font-weight: bold; color: #4a3820; cursor: pointer;
border-bottom: 2px solid transparent;
transition: all 0.2s; background: transparent;
min-height: 38px; display: flex; align-items: center;
-webkit-tap-highlight-color: transparent; touch-action: manipulation;
}
.vwh-tab:hover, .vwh-tab:active { color: #a08040; background: #0f0b00; }
.vwh-tab.active {
color: #c9a84c;
background: linear-gradient(180deg, #1a1200 0%, #0d0900 100%);
border-bottom: 2px solid #c9a84c;
text-shadow: 0 0 8px #c9a84c66;
}
.vwh-tab-dot { margin-right: 4px; font-size: 12px; }
/* ── Content area ── */
.vwh-content { padding: 13px; overflow-y: auto; flex-grow: 1; position: relative; -webkit-overflow-scrolling: touch; }
.vwh-pane { display: none; }
.vwh-pane.active { display: block; animation: vwh-fadepane 0.2s ease; }
@keyframes vwh-fadepane { from { opacity: 0; transform: translateY(4px); } to { opacity: 1; transform: translateY(0); } }
/* ── Norse buttons ── */
.vwh-btn {
background: linear-gradient(180deg, #2a2010 0%, #181008 50%, #0e0a04 100%);
color: #c8b89a; border: 1px solid #3a2800;
padding: 7px 12px; border-radius: 3px; cursor: pointer;
font-family: 'Georgia', serif; font-weight: bold;
text-transform: uppercase; font-size: 11px; letter-spacing: 1px;
text-shadow: 0 1px 2px #000;
box-shadow: inset 0 1px 0 rgba(201,168,76,0.1), inset 0 -1px 0 rgba(0,0,0,0.5);
transition: all 0.18s; min-height: 34px;
-webkit-tap-highlight-color: transparent; touch-action: manipulation;
}
.vwh-btn:hover:not(:disabled), .vwh-btn:active:not(:disabled) {
background: linear-gradient(180deg, #3d3018, #221808);
color: #e8d8a0; border-color: #6a5020;
box-shadow: inset 0 1px 0 rgba(201,168,76,0.2), 0 0 8px rgba(201,168,76,0.1);
}
.vwh-btn:disabled { opacity: 0.4; cursor: not-allowed; }
.vwh-btn-gold {
background: linear-gradient(180deg, #4a3800 0%, #2e2200 50%, #1e1600 100%);
border-color: #c9a84c; color: #c9a84c;
box-shadow: inset 0 1px 0 rgba(201,168,76,0.2), 0 0 6px rgba(201,168,76,0.1);
}
.vwh-btn-gold:hover:not(:disabled), .vwh-btn-gold:active:not(:disabled) {
background: linear-gradient(180deg, #6a5010, #3a2800);
color: #ffd700; box-shadow: 0 0 14px rgba(201,168,76,0.3);
}
.vwh-btn-blood {
background: linear-gradient(180deg, #3a0a0a 0%, #220606 50%, #140404 100%);
border-color: #8b1a1a; color: #ff7070;
}
.vwh-btn-blood:hover:not(:disabled), .vwh-btn-blood:active:not(:disabled) {
background: linear-gradient(180deg, #541010, #2e0808);
box-shadow: 0 0 10px rgba(139,26,26,0.4);
}
.vwh-btn-amber {
background: linear-gradient(180deg, #3d2800, #1e1200);
border-color: #b86820; color: #e88830;
}
.vwh-btn-amber:hover:not(:disabled), .vwh-btn-amber:active:not(:disabled) {
background: linear-gradient(180deg, #5a3800, #2e1800);
box-shadow: 0 0 10px rgba(184,104,32,0.3);
}
.vwh-btn-forest {
background: linear-gradient(180deg, #0a2810, #060f08);
border-color: #2a7a2a; color: #5ad65a;
}
.vwh-btn-forest:hover:not(:disabled), .vwh-btn-forest:active:not(:disabled) {
box-shadow: 0 0 10px rgba(90,214,90,0.2);
}
/* ── Inputs ── */
.vwh-input {
background: linear-gradient(180deg, #06040e, #090712);
color: #c8b89a; border: 1px solid #2e2200;
padding: 8px 10px; width: 100%; border-radius: 3px;
font-family: 'Georgia', serif; font-size: 12px;
box-shadow: inset 0 2px 6px rgba(0,0,0,0.7);
transition: border-color 0.2s; -webkit-appearance: none;
}
.vwh-input:focus { border-color: #c9a84c; outline: none; box-shadow: inset 0 2px 6px rgba(0,0,0,0.7), 0 0 6px rgba(201,168,76,0.15); }
.vwh-input::placeholder { color: #3a2e1a; }
/* ── Respect/score bar ── */
.vwh-score-bar {
height: 26px; border-radius: 3px; overflow: hidden; position: relative;
margin: 10px 0; display: flex;
border: 1px solid #2a1e00;
box-shadow: inset 0 2px 8px rgba(0,0,0,0.8), 0 0 8px rgba(0,0,0,0.5);
}
.vwh-score-blue {
background: linear-gradient(90deg, #0a1e3a, #1a3a6e);
height: 100%; display: flex; align-items: center; padding-left: 8px;
font-weight: bold; font-size: 11px; transition: width 0.6s ease;
text-shadow: 0 1px 3px #000; border-right: 1px solid #0a0814;
}
.vwh-score-red {
background: linear-gradient(270deg, #3a0a0a, #6e1a1a);
height: 100%; flex: 1; display: flex; align-items: center;
justify-content: flex-end; padding-right: 8px;
font-weight: bold; font-size: 11px; transition: width 0.6s ease;
text-shadow: 0 1px 3px #000; border-left: 1px solid #0a0814;
}
.vwh-score-label {
position: absolute; width: 100%; text-align: center;
line-height: 26px; font-size: 11px; font-weight: bold; z-index: 2;
pointer-events: none; text-transform: uppercase; letter-spacing: 1px;
color: #e8d090; text-shadow: 0 0 6px #000, 1px 1px 2px #000;
}
/* ── Parchment section header ── */
.vwh-section-head {
font-size: 10px; text-transform: uppercase; letter-spacing: 2px;
color: #6a5020; border-bottom: 1px solid #2a1e00;
padding-bottom: 5px; margin-bottom: 10px;
display: flex; justify-content: space-between; align-items: center;
}
.vwh-section-head span { color: #c9a84c; }
/* ── Player cards ── */
.vwh-cards { display: flex; flex-direction: column; gap: 7px; max-height: 320px; overflow-y: auto; padding-right: 3px; -webkit-overflow-scrolling: touch; }
.vwh-card {
background: linear-gradient(160deg, #0e0c18 0%, #0b0918 100%);
border: 1px solid #2a1e00;
border-left: 3px solid #3a2800;
border-radius: 3px; padding: 9px 10px;
box-shadow: inset 0 1px 0 rgba(201,168,76,0.04), 0 2px 8px rgba(0,0,0,0.5);
transition: border-left-color 0.2s;
}
.vwh-card:hover { border-left-color: #c9a84c88; }
.vwh-card-top {
display: flex; justify-content: space-between; align-items: flex-start;
margin-bottom: 7px;
}
.vwh-card-name {
color: #d4c090; text-decoration: none; font-weight: bold;
font-size: 13px; text-shadow: 0 1px 2px #000;
}
.vwh-card-name:hover { color: #ffd700; text-shadow: 0 0 8px #c9a84c88; }
.vwh-status-pill {
font-size: 9px; font-weight: bold; text-transform: uppercase; letter-spacing: 1px;
padding: 2px 6px; border-radius: 2px;
border: 1px solid currentColor; background: rgba(0,0,0,0.5);
}
.vwh-card-acts { display: flex; gap: 4px; margin-bottom: 7px; flex-wrap: wrap; }
.vwh-card-stats {
font-size: 11px; color: #7a6848;
background: linear-gradient(180deg, #080612, #060410);
padding: 5px 8px; border-radius: 2px;
border: 1px solid #1a1200; min-height: 20px;
}
/* ── Norse box / parchment panel ── */
.vwh-panel {
background: linear-gradient(160deg, #0b0918 0%, #080614 100%);
border: 1px solid #2a1e00;
border-radius: 3px; padding: 13px;
margin-bottom: 12px;
box-shadow: inset 0 0 20px rgba(0,0,0,0.6);
position: relative;
}
.vwh-panel::before {
content: ''; position: absolute; top: 0; left: 0; right: 0;
height: 1px;
background: linear-gradient(90deg, transparent, #c9a84c22, transparent);
}
.vwh-panel-title {
color: #c9a84c; font-size: 13px; font-weight: bold;
text-transform: uppercase; letter-spacing: 2px;
text-shadow: 0 0 10px #c9a84c44;
margin-bottom: 10px;
}
/* ── War Room card ── */
.vwh-wr-card {
background: linear-gradient(160deg, #120818 0%, #0e0614 100%);
border: 1px solid #3a0a0a; border-left: 3px solid #8b1a1a;
border-radius: 3px; padding: 10px 11px; margin-bottom: 9px;
box-shadow: inset 0 0 15px rgba(139,26,26,0.05), 0 2px 8px rgba(0,0,0,0.5);
transition: opacity 0.3s;
}
/* ── Setup / API screen ── */
.vwh-setup { padding: 25px 20px; text-align: center; }
.vwh-setup-rune {
font-size: 36px; color: #c9a84c; display: block; margin-bottom: 8px;
text-shadow: 0 0 20px #c9a84c66;
animation: vwh-runeflicker 3s ease-in-out infinite;
}
.vwh-big-title {
color: #c9a84c; font-size: 16px; font-weight: bold;
text-transform: uppercase; letter-spacing: 3px;
text-shadow: 0 0 15px #c9a84c44, 0 2px 4px #000;
margin-bottom: 6px;
}
.vwh-subtitle { color: #5a4830; font-size: 11px; letter-spacing: 1px; margin-bottom: 20px; }
.vwh-field-label { font-size: 10px; text-transform: uppercase; letter-spacing: 1.5px; color: #6a5020; margin-bottom: 4px; text-align: left; }
/* ── Message/loading box ── */
#vwh-msg { text-align: center; padding: 35px 20px; display: none; }
.vwh-msg-rune { font-size: 32px; color: #c9a84c44; margin-bottom: 10px; animation: vwh-runeflicker 2s infinite; }
.vwh-msg-title { color: #c9a84c; text-transform: uppercase; letter-spacing: 2px; font-size: 15px; margin-bottom: 8px; text-shadow: 0 0 10px #c9a84c33; }
.vwh-msg-body { font-size: 12px; color: #5a4830; line-height: 1.6; }
.vwh-error-box { color: #ff7070; background: #12040a; padding: 10px; border: 1px solid #5a1010; border-radius: 3px; margin-top: 12px; text-align: left; font-family: monospace; font-size: 11px; }
/* ── Toast notifications ── */
#vwh-toasts { position: absolute; bottom: 12px; left: 50%; transform: translateX(-50%); z-index: 99999; display: flex; flex-direction: column; gap: 4px; width: 88%; align-items: center; pointer-events: none; }
.vwh-toast {
background: linear-gradient(180deg, #1a2e0a, #0e1a06);
color: #8ad65a; border: 1px solid #2a4a10;
padding: 6px 14px; border-radius: 2px; font-size: 10px; font-weight: bold;
text-transform: uppercase; letter-spacing: 1.5px;
box-shadow: 0 4px 12px rgba(0,0,0,0.8), 0 0 8px rgba(90,214,90,0.1);
animation: vwh-toastin 0.3s ease forwards;
}
.vwh-toast.err { background: linear-gradient(180deg, #2e0a0a, #180606); color: #ff7070; border-color: #5a1010; box-shadow: 0 0 8px rgba(255,80,80,0.1); }
@keyframes vwh-toastin { from { opacity: 0; transform: translateY(6px); } to { opacity: 1; transform: translateY(0); } }
/* ── Misc ── */
.vwh-dim { color: #3a2e1a; font-style: italic; font-size: 11px; }
.vwh-gold { color: #c9a84c; }
.vwh-blood { color: #ff7070; }
.vwh-rune-hr { border: none; border-top: 1px solid #1e1600; margin: 10px 0; position: relative; text-align: center; }
.vwh-rune-hr::after { content: '᛭'; position: absolute; top: -7px; left: 50%; transform: translateX(-50%); background: #0e0c18; padding: 0 6px; color: #2a1e00; font-size: 11px; }
.vwh-stat-badge { color: #8a7040; font-weight: bold; font-size: 11px; }
.vwh-highlight { color: #c9a84c; font-size: 15px; font-weight: bold; text-shadow: 0 0 8px #c9a84c44; }
.vwh-w-sm { width: 65px !important; }
.vwh-w-md { width: 105px !important; }
/* ── Knotwork top border on cards ── */
.vwh-card-knotwork {
height: 3px; margin: -13px -13px 11px;
background: repeating-linear-gradient(90deg, #c9a84c11 0px, #c9a84c22 4px, transparent 4px, transparent 8px);
border-radius: 3px 3px 0 0;
}
`);
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
// HTML STRUCTURE
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
const html = `
<div id="vwh-wrap" style="display:none;">
<div class="vwh-header" id="vwh-drag">
<div>
<div class="vwh-header-title">
<span class="vwh-header-rune">ᚢ</span>
VALHALLA: WARRIOR'S HALL
<span class="vwh-header-rune">ᚢ</span>
</div>
<div class="vwh-header-sub">ᚠ ᚢ ᚦ ᚨ ᚱ ᚲ · MEMBER EDITION · ᚠ ᚢ ᚦ ᚨ ᚱ ᚲ</div>
</div>
<div class="vwh-ctrl">
<button id="vwh-pin" class="vwh-hbtn" title="Pin to all pages">📌</button>
<button id="vwh-help" class="vwh-hbtn" title="Odin's Codex">📜</button>
<button id="vwh-cfg" class="vwh-hbtn" title="Forge Settings">⚙️</button>
<button id="vwh-size" class="vwh-hbtn" title="Toggle size">◱</button>
<button id="vwh-min" class="vwh-hbtn" title="Minimize">—</button>
</div>
</div>
<div class="vwh-rune-divider">ᛟ ᛜ ᛝ ᛟ ᛜ ᛝ ᛟ ᛜ ᛝ ᛟ</div>
<div id="vwh-body">
<!-- HELP SCREEN -->
<div id="vwh-screen-help" class="vwh-setup" style="display:none; text-align:left;">
<span class="vwh-setup-rune">ᚷ</span>
<div class="vwh-big-title">Odin's Codex</div>
<div class="vwh-subtitle">The sacred knowledge of Valhalla</div>
<ul style="color:#7a6040; font-size:12px; line-height:1.9; padding-left:18px; margin-bottom:18px;">
<li><span class="vwh-gold">📌 Global Pin</span> — Manifests the Hall on every Torn page.</li>
<li><span class="vwh-gold">⚔️ Battlefield</span> — Live war with FF Scouter intelligence. Auto-scouts every 15s.</li>
<li><span class="vwh-gold">📜 Sagas</span> — Load past war reports by ID.</li>
<li><span class="vwh-gold">🎯 Hitlist</span> — Mark enemies for priority slaying.</li>
<li><span class="vwh-gold">🚨 War Room</span> — Real-time SOS signals. Call your shield-brothers.</li>
<li><span class="vwh-blood">⚔️ SOS</span> — Sends a target to the War Room instantly.</li>
</ul>
<button id="vwh-help-close" class="vwh-btn vwh-btn-gold" style="width:100%;">CLOSE THE CODEX</button>
</div>
<!-- API SETUP SCREEN -->
<div id="vwh-screen-api" class="vwh-setup">
<span class="vwh-setup-rune">ᚠ</span>
<div class="vwh-big-title">Prove Your Worth</div>
<div class="vwh-subtitle">Only the worthy enter Valhalla</div>
<div class="vwh-field-label">⚔ Primary API Key (FF Scouter-registered)</div>
<div style="margin-bottom:5px; font-size:10px; color:#4a3010; line-height:1.5;">
This key powers Torn data + FF Scouter stats.<br>
<span class="vwh-blood">✦ Register at FF Scouter first!</span><br>
<a href="https://ffscouter.com" target="_blank" style="color:#8a6020; text-decoration:none; border-bottom:1px dashed #8a6020;">→ ffscouter.com</a>
</div>
<input type="password" id="vwh-key-torn" class="vwh-input" placeholder="Torn API Key — 16 runes" style="margin-bottom:13px; text-align:center;">
<div class="vwh-field-label">🛡 TornStats API (Optional)</div>
<input type="password" id="vwh-key-ts" class="vwh-input" placeholder="TornStats Key" style="margin-bottom:15px; text-align:center;">
<div style="display:flex; gap:8px; justify-content:center;">
<button id="vwh-api-save" class="vwh-btn vwh-btn-gold" style="padding:9px 20px;">ENTER VALHALLA</button>
<button id="vwh-api-cancel" class="vwh-btn" style="display:none; padding:9px 16px;">RETREAT</button>
</div>
</div>
<!-- MAIN APP -->
<div id="vwh-screen-app" style="display:none; flex-direction:column; height:100%;">
<div class="vwh-tabs">
<div class="vwh-tab active" data-pane="p-live"><span class="vwh-tab-dot">⚔️</span>Battle</div>
<div class="vwh-tab" data-pane="p-past"><span class="vwh-tab-dot">📜</span>Sagas</div>
<div class="vwh-tab" data-pane="p-hitlist"><span class="vwh-tab-dot">🎯</span>Hitlist</div>
<div class="vwh-tab" data-pane="p-warroom" style="color:#8b1a1a;"><span class="vwh-tab-dot">🚨</span>War Room</div>
</div>
<div class="vwh-content">
<div id="vwh-toasts"></div>
<div id="vwh-msg">
<div class="vwh-msg-rune">ᚱ</div>
<div id="vwh-msg-title" class="vwh-msg-title">Consulting the Norns…</div>
<div id="vwh-msg-body" class="vwh-msg-body">The threads of fate are being read.</div>
</div>
<div id="vwh-data" style="display:none;">
<!-- ⚔️ BATTLEFIELD -->
<div id="p-live" class="vwh-pane active">
<div id="live-empty" class="vwh-dim" style="text-align:center; padding:20px; display:none;">
ᚹ Peace upon Midgard. No battle rages. ᚹ
</div>
<div id="live-wrap">
<div class="vwh-section-head">
<span>⚔ Battle Log</span>
<span>RSP: <strong id="live-my-rsp" class="vwh-gold">—</strong> | ATK: <strong id="live-my-atk" class="vwh-gold">—</strong></span>
</div>
<div class="vwh-score-bar">
<div class="vwh-score-label" id="live-bar-lbl">Clan A vs Clan B</div>
<div class="vwh-score-blue" id="live-bar-l" style="width:50%;">0</div>
<div class="vwh-score-red" id="live-bar-r" style="width:50%;">0</div>
</div>
<div id="live-cards" class="vwh-cards"></div>
</div>
</div>
<!-- 📜 SAGAS -->
<div id="p-past" class="vwh-pane">
<div class="vwh-panel">
<div class="vwh-panel-title">ᚱ Recall a Saga</div>
<p style="font-size:11px; color:#5a4030; margin-bottom:11px;">Enter a Ranked War Report ID to summon a past battle.</p>
<div style="display:flex; gap:6px;">
<input type="number" id="saga-id-input" class="vwh-input" placeholder="Report ID">
<button id="saga-load" class="vwh-btn vwh-btn-gold">SUMMON</button>
</div>
<div id="saga-status" style="font-size:11px; margin-top:7px; color:#c9a84c;"></div>
</div>
<div id="past-empty" class="vwh-dim" style="text-align:center; padding:15px;">No saga summoned.</div>
<div id="past-wrap" style="display:none;">
<div class="vwh-section-head">
<span>📜 Archived Saga</span>
<span>RSP: <strong id="past-my-rsp" class="vwh-gold">—</strong> | ATK: <strong id="past-my-atk" class="vwh-gold">—</strong></span>
</div>
<div class="vwh-score-bar">
<div class="vwh-score-label" id="past-bar-lbl">Clan A vs Clan B</div>
<div class="vwh-score-blue" id="past-bar-l" style="width:50%;">0</div>
<div class="vwh-score-red" id="past-bar-r" style="width:50%;">0</div>
</div>
<div id="past-cards" class="vwh-cards"></div>
</div>
</div>
<!-- 🎯 HITLIST -->
<div id="p-hitlist" class="vwh-pane">
<div class="vwh-section-head"><span>🎯 Marked for Valhalla</span></div>
<div id="hitlist-cards" class="vwh-cards" style="max-height:420px;"></div>
<div id="hitlist-empty" class="vwh-dim" style="text-align:center; padding:20px;">No enemies marked.</div>
</div>
<!-- 🚨 WAR ROOM -->
<div id="p-warroom" class="vwh-pane">
<div class="vwh-panel" style="border-color:#3a0a0a;">
<div class="vwh-panel-title" style="color:#ff7070;">🚨 Summon Shield-Brothers</div>
<div style="display:flex; gap:6px;">
<input type="number" id="wr-manual-id" class="vwh-input" placeholder="Enemy ID — manual SOS">
<button id="wr-manual-btn" class="vwh-btn vwh-btn-blood" style="width:80px;">SOS</button>
</div>
</div>
<div id="wr-list">
<div class="vwh-dim" style="text-align:center; padding:18px;">ᚹ No battle-calls echo in the Hall. ᚹ</div>
</div>
</div>
</div><!-- /vwh-data -->
</div><!-- /vwh-content -->
</div><!-- /vwh-screen-app -->
</div><!-- /vwh-body -->
</div>
`;
document.body.appendChild(Object.assign(document.createElement('div'), {innerHTML: html}));
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
// STATE
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
let KEYS = { torn: GM_getValue('vwh_key_torn',''), ts: GM_getValue('vwh_key_ts','') };
let MEM = { lastReport: GM_getValue('vwh_mem_report', null) };
let IS_PINNED = GM_getValue('vwh_is_pinned', false);
let APP = { uid: null, enemyFacId: null };
let radarTimer = null, syncTimer = null;
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
// PIN & VISIBILITY
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
const wrap = document.getElementById('vwh-wrap');
if (IS_PINNED || /factions\.php|warfare\.php/.test(location.href)) {
wrap.style.display = 'flex';
if (IS_PINNED) document.getElementById('vwh-pin').classList.add('pinned');
}
document.getElementById('vwh-pin').addEventListener('click', function(e) {
IS_PINNED = !IS_PINNED;
GM_setValue('vwh_is_pinned', IS_PINNED);
e.currentTarget.classList.toggle('pinned', IS_PINNED);
});
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
// DRAG — MOUSE + TOUCH
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
const dragHandle = document.getElementById('vwh-drag');
let dragging = false, dX = 0, dY = 0;
function dragStart(cx, cy) {
dragging = true;
const r = wrap.getBoundingClientRect();
dX = cx - r.left; dY = cy - r.top;
wrap.style.right = 'auto';
}
function dragMove(cx, cy) {
if (!dragging) return;
wrap.style.left = Math.max(0, Math.min(cx - dX, window.innerWidth - wrap.offsetWidth)) + 'px';
wrap.style.top = Math.max(0, Math.min(cy - dY, window.innerHeight - wrap.offsetHeight)) + 'px';
}
dragHandle.addEventListener('mousedown', function(e) { if (!e.target.closest('.vwh-ctrl')) dragStart(e.clientX, e.clientY); });
document.addEventListener('mousemove', function(e) { dragMove(e.clientX, e.clientY); });
document.addEventListener('mouseup', function() { dragging = false; });
dragHandle.addEventListener('touchstart', function(e) { if (!e.target.closest('.vwh-ctrl')) { const t=e.touches[0]; dragStart(t.clientX, t.clientY); } }, {passive:true});
document.addEventListener('touchmove', function(e) { if (!dragging) return; e.preventDefault(); const t=e.touches[0]; dragMove(t.clientX, t.clientY); }, {passive:false});
document.addEventListener('touchend', function() { dragging = false; });
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
// UI CONTROLS
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
document.getElementById('vwh-min').addEventListener('click', function() { wrap.classList.toggle('minimized'); });
document.getElementById('vwh-size').addEventListener('click', function() { wrap.classList.toggle('compact'); });
document.getElementById('vwh-help').addEventListener('click', function() {
document.getElementById('vwh-screen-app').style.display = 'none';
document.getElementById('vwh-screen-api').style.display = 'none';
document.getElementById('vwh-screen-help').style.display = 'block';
});
document.getElementById('vwh-help-close').addEventListener('click', function() { boot(); });
document.querySelectorAll('.vwh-tab').forEach(function(tab) {
tab.addEventListener('click', function(e) {
document.querySelectorAll('.vwh-tab').forEach(function(t) { t.classList.remove('active'); });
document.querySelectorAll('.vwh-pane').forEach(function(p) { p.classList.remove('active'); });
e.currentTarget.classList.add('active');
document.getElementById(e.currentTarget.dataset.pane).classList.add('active');
});
});
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
// TOAST
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
function toast(msg, err) {
var c = document.getElementById('vwh-toasts');
var t = document.createElement('div');
t.className = 'vwh-toast' + (err ? ' err' : '');
t.textContent = msg;
c.appendChild(t);
setTimeout(function() { t.remove(); }, 2500);
}
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
// NETWORK
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
function gmGet(url) { return new Promise(function(ok,fail) { GM_xmlhttpRequest({method:"GET",url:url,onload:function(r){try{ok(JSON.parse(r.responseText));}catch(e){fail(e);}},onerror:fail}); }); }
function gmPost(url,data) { return new Promise(function(ok,fail) { GM_xmlhttpRequest({method:"POST",url:url,headers:{"Content-Type":"application/json"},data:JSON.stringify(data),onload:function(r){try{ok(JSON.parse(r.responseText));}catch(e){fail(e);}},onerror:fail}); }); }
function gmDel(url) { return new Promise(function(ok,fail) { GM_xmlhttpRequest({method:"DELETE",url:url,onload:function(r){try{ok(JSON.parse(r.responseText));}catch(e){fail(e);}},onerror:fail}); }); }
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
// FF SCOUTER HELPERS
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
function ffDifficulty(ff) {
if (ff<=1) return 'Prey'; if (ff<=2) return 'Easy'; if (ff<=3.5) return 'Worthy';
if (ff<=4.5) return 'Dangerous'; return 'Valkyrie-Level';
}
function ffColor(v) {
var r,g,b;
if(v<=1){r=0x28;g=0x28;b=0xc6;}
else if(v<=3){var t=(v-1)/2;r=0x28;g=Math.round(0x28+(0xc6-0x28)*t);b=Math.round(0xc6-(0xc6-0x28)*t);}
else if(v<=5){var t=(v-3)/2;r=Math.round(0x28+(0xc6-0x28)*t);g=Math.round(0xc6-(0xc6-0x28)*t);b=0x28;}
else{r=0xc6;g=0x28;b=0x28;}
return '#'+((1<<24)+(r<<16)+(g<<8)+b).toString(16).slice(1).toUpperCase();
}
function ffContrast(hex) {
var r=parseInt(hex.slice(1,3),16),g=parseInt(hex.slice(3,5),16),b=parseInt(hex.slice(5,7),16);
return (r*.299+g*.587+b*.114)>126?'#000':'#fff';
}
function ffHTML(ff, est) {
var bg=ffColor(ff),tc=ffContrast(bg),d=ffDifficulty(ff);
return '<span style="color:#7a6040;font-weight:bold;margin-right:5px;">ᚠF:</span>'
+ '<span style="background:'+bg+';color:'+tc+';font-weight:bold;padding:1px 6px;border-radius:2px;">'+ff.toFixed(2)+' — '+d+'</span>'
+ '<span style="font-size:10px;color:#5a4828;margin-left:7px;">Est: <strong style="color:#b09050;">'+( est||'Unknown')+'</strong></span>';
}
function fmtStats(raw) {
if(raw===null||raw===undefined) return 'N/A';
var n=parseInt(raw.toString().replace(/,/g,''));
if(isNaN(n)) return 'N/A';
if(n>=1e9) return (n/1e9).toFixed(2)+'b';
if(n>=1e6) return (n/1e6).toFixed(2)+'m';
if(n>=1e3) return (n/1e3).toFixed(2)+'k';
return n.toString();
}
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
// BOOT / SETTINGS
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
function boot() {
document.getElementById('vwh-screen-help').style.display = 'none';
if (KEYS.torn && KEYS.torn.length===16) {
document.getElementById('vwh-screen-api').style.display = 'none';
document.getElementById('vwh-screen-app').style.display = 'flex';
document.getElementById('vwh-cfg').style.display = 'inline-flex';
dataSync();
if (!radarTimer) radarTimer = setInterval(syncWarRoom, 1500);
if (!syncTimer) syncTimer = setInterval(liveAutoSync, 15000);
} else {
openCfg(false);
}
}
function openCfg(canCancel) {
document.getElementById('vwh-screen-api').style.display = 'block';
document.getElementById('vwh-screen-app').style.display = 'none';
document.getElementById('vwh-api-cancel').style.display = canCancel ? 'inline-block' : 'none';
document.getElementById('vwh-key-torn').value = KEYS.torn||'';
document.getElementById('vwh-key-ts').value = KEYS.ts||'';
}
document.getElementById('vwh-api-save').addEventListener('click', function() {
var t = document.getElementById('vwh-key-torn').value.trim();
if (t.length!==16) { alert('⚔ Torn API Key must be exactly 16 runes (characters).'); return; }
KEYS.torn = t; KEYS.ts = document.getElementById('vwh-key-ts').value.trim();
GM_setValue('vwh_key_torn', KEYS.torn); GM_setValue('vwh_key_ts', KEYS.ts);
boot();
});
document.getElementById('vwh-cfg').addEventListener('click', function() { openCfg(true); });
document.getElementById('vwh-api-cancel').addEventListener('click', function() { boot(); });
function showMsg(title, body, isErr, rawErr) {
isErr = isErr||false;
document.getElementById('vwh-data').style.display = 'none';
var box = document.getElementById('vwh-msg'); box.style.display = 'block';
document.getElementById('vwh-msg-title').textContent = title;
document.getElementById('vwh-msg-title').style.color = isErr ? '#ff7070' : '#c9a84c';
var html = body;
if (isErr && rawErr) {
html += '<div class="vwh-error-box">'+(rawErr.message||rawErr)+'</div>';
html += '<button class="vwh-btn vwh-btn-gold" style="margin-top:14px;" id="vwh-reboot">ᚱ REBOOT</button>';
}
document.getElementById('vwh-msg-body').innerHTML = html;
if (isErr && rawErr) {
var rb = document.getElementById('vwh-reboot');
if (rb) rb.addEventListener('click', function() { location.reload(); });
}
}
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
// DATA SYNC
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
async function dataSync() {
showMsg('Consulting the Norns…', 'Reading the threads of fate from Torn API…');
try {
var user = await gmGet('https://api.torn.com/user/?selections=profile&key='+KEYS.torn);
if (user.error) throw new Error('User API: '+user.error.error);
APP.uid = user.player_id;
var facId = user.faction?.faction_id;
if (!facId) throw new Error('You are not in a Faction, Warrior.');
document.getElementById('vwh-msg').style.display = 'none';
document.getElementById('vwh-data').style.display = 'block';
var hasLive = false;
try {
var rw = await gmGet('https://api.torn.com/v2/faction/'+facId+'/ranked_wars?key='+KEYS.torn);
var wars = rw.ranked_wars || rw.faction?.ranked_wars || {};
var wk = Object.keys(wars);
if (wk.length > 0) {
var wd = wars[wk[0]];
if (Object.keys(wd.factions).length===2 && wd.factions[facId]?.members) {
hasLive = true;
await buildWar(wd.factions, facId, 'live');
}
}
} catch(e) { console.warn('[VWH] Live war skipped:', e); }
if (!hasLive) {
document.getElementById('live-wrap').style.display = 'none';
document.getElementById('live-empty').style.display = 'block';
}
if (MEM.lastReport) await loadReport(MEM.lastReport, facId);
} catch(err) { showMsg('ᚷ Odin Refuses Entry', 'The runes cannot be read.', true, err); }
}
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
// REPORT LOADER
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
async function loadReport(id, facId) {
try {
var d;
try { d = await gmGet('https://api.torn.com/v2/torn/'+id+'/ranked_war_report?key='+KEYS.torn); if(d.error) throw new Error(d.error.error); }
catch(e) { d = await gmGet('https://api.torn.com/torn/'+id+'?selections=rankedwarreport&key='+KEYS.torn); if(d.error) throw new Error(d.error.error); }
var rpt = d.ranked_war_report||d.rankedwarreport||d.report||d;
await buildWar(rpt.factions, facId, 'past');
document.getElementById('saga-status').textContent = 'ᚱ Saga #'+id+' summoned from the mists.';
} catch(err) {
document.getElementById('saga-status').textContent = 'ᚷ Error: '+err.message;
document.getElementById('saga-status').style.color = '#ff7070';
}
}
document.getElementById('saga-load').addEventListener('click', async function() {
var id = document.getElementById('saga-id-input').value.trim();
if (!id) return;
document.getElementById('saga-status').textContent = 'ᚱ Summoning saga from the mists…';
document.getElementById('saga-status').style.color = '#c9a84c';
try {
var u = await gmGet('https://api.torn.com/user/?selections=profile&key='+KEYS.torn);
var facId = u.faction?.faction_id;
GM_setValue('vwh_mem_report', id); MEM.lastReport = id;
await loadReport(id, facId);
} catch(err) { document.getElementById('saga-status').textContent='ᚷ Error: '+err.message; document.getElementById('saga-status').style.color='#ff7070'; }
});
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
// WAR INTERFACE BUILDER
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
async function buildWar(factions, myFacId, mode) {
var px = mode==='live' ? 'live' : 'past';
document.getElementById(px+'-empty').style.display = 'none';
document.getElementById(px+'-wrap').style.display = 'block';
var fKeys = Object.keys(factions);
var usId = myFacId, themId = fKeys.find(function(k){return k!=usId;});
if (!factions[usId]) { usId=fKeys[0]; themId=fKeys[1]; }
if (mode==='live') APP.enemyFacId = themId;
var us = factions[usId], them = factions[themId];
var rspEl = document.getElementById(px+'-my-rsp'), atkEl = document.getElementById(px+'-my-atk');
if (APP.uid && us.members?.[APP.uid]) {
rspEl.textContent = us.members[APP.uid].score.toFixed(2);
atkEl.textContent = us.members[APP.uid].attacks;
} else { rspEl.textContent='N/A'; atkEl.textContent='N/A'; }
var sUs=us.score||0, sTh=them.score||0, tot=sUs+sTh||1;
document.getElementById(px+'-bar-l').style.width = (sUs/tot*100)+'%';
document.getElementById(px+'-bar-l').textContent = Math.floor(sUs);
document.getElementById(px+'-bar-r').style.width = (sTh/tot*100)+'%';
document.getElementById(px+'-bar-r').textContent = Math.floor(sTh);
document.getElementById(px+'-bar-lbl').textContent = us.name+' ᚹ '+them.name;
var liveRoster = {};
try { var ed=await gmGet('https://api.torn.com/v2/faction/'+themId+'/members?key='+KEYS.torn); liveRoster=ed.members||{}; } catch(e){}
var eMap = {};
if (Array.isArray(liveRoster)) { liveRoster.forEach(function(m){eMap[m.id||m.player_id]=m.name||m.player_name||m.username;}); }
else { for(var k in liveRoster){eMap[k]=liveRoster[k].name||liveRoster[k].player_name||liveRoster[k].username;} }
var list = mode==='past' ? them.members : (Array.isArray(liveRoster)?eMap:liveRoster);
// FF Scouter bulk
var ffData = {};
if (KEYS.torn && Object.keys(list).length>0) {
try {
var fd = await gmGet('https://ffscouter.com/api/v1/get-stats?key='+KEYS.torn+'&targets='+Object.keys(list).join(','));
if (Array.isArray(fd)) { fd.forEach(function(r){if(r.player_id)ffData[r.player_id]={ff:r.fair_fight,est:r.bs_estimate_human};}); }
} catch(e) { console.error('[VWH] FFScouter failed:', e); }
}
document.getElementById(px+'-cards').innerHTML = '';
for (var id in list) {
var nameRaw = list[id].name||list[id].player_name;
var name = nameRaw || eMap[id] || ('Target ['+id+']');
var status = {state:'Unknown'};
if (Array.isArray(liveRoster)) { var m=liveRoster.find(function(x){return x.id==id||x.player_id==id;}); if(m&&m.status)status=m.status; }
else if (liveRoster[id]?.status) status=liveRoster[id].status;
injectCard(name, id, status, px, ffData);
}
}
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
// ENEMY CARD
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
function injectCard(name, id, statusObj, prefix, ffData) {
var container = document.getElementById(prefix+'-cards');
var card = document.createElement('div');
card.className = 'vwh-card';
card.id = 'card-'+prefix+'-'+id;
var sColor = '#6a5828';
var state = statusObj?.state||'Unknown';
if (state==='Hospital') sColor='#c03030';
else if (state==='Okay') sColor='#3a8a3a';
else if (state==='Jail') sColor='#c9a84c';
var sText = state;
if (statusObj?.until) sText += ' ('+Math.ceil((statusObj.until-(Date.now()/1000))/60)+'m)';
card.innerHTML =
'<div class="vwh-card-knotwork"></div>'+
'<div class="vwh-card-top">'+
'<a href="/profiles.php?XID='+id+'" target="_blank" class="vwh-card-name">'+name+' <span style="color:#4a3820;">['+id+']</span></a>'+
'<span class="vwh-status-pill" id="sp-'+prefix+'-'+id+'" style="color:'+sColor+';">'+sText+'</span>'+
'</div>'+
'<div class="vwh-card-acts">'+
'<button class="vwh-btn vwh-btn-blood btn-atk" style="padding:5px 10px; font-size:10px;">⚔ ATK</button>'+
'<button class="vwh-btn vwh-btn-amber btn-sos" style="padding:5px 10px; font-size:10px;">🚨 SOS</button>'+
'<button class="vwh-btn btn-mark" style="padding:5px 10px; font-size:10px;" title="Mark for Hitlist">🎯 MARK</button>'+
'</div>'+
'<div class="vwh-card-stats" id="ff-'+prefix+'-'+id+'"><span class="vwh-dim">ᚱ Consulting the Norns…</span></div>';
container.appendChild(card);
card.querySelector('.btn-atk').addEventListener('click', function() { window.open('/loader.php?sid=attack&user2ID='+id); });
card.querySelector('.btn-mark').addEventListener('click', function() {
var hl = document.getElementById('hitlist-cards');
var he = document.getElementById('hitlist-empty');
var clone = card.cloneNode(true);
clone.id = 'card-hl-'+id;
clone.querySelector('.btn-mark').remove();
clone.querySelector('.btn-sos').remove();
var del = document.createElement('button');
del.className='vwh-btn vwh-btn-blood'; del.style='padding:5px 10px;font-size:10px;';
del.textContent='✕ REMOVE';
del.addEventListener('click', function() { clone.remove(); if(!hl.children.length){he.style.display='block';} });
clone.querySelector('.vwh-card-acts').appendChild(del);
var cloneAtk = clone.querySelector('.btn-atk');
if (cloneAtk) cloneAtk.addEventListener('click', function() { window.open('/loader.php?sid=attack&user2ID='+id); });
hl.appendChild(clone);
he.style.display='none';
toast('ᚷ '+name+' marked for Valhalla!');
});
card.querySelector('.btn-sos').addEventListener('click', function() {
var caller = document.querySelector('.menu-value___3hOM0')?.innerText || 'A Viking';
toast('🚨 SOS sent to the War Room!');
document.querySelectorAll('.vwh-tab').forEach(function(t){t.classList.remove('active');});
document.querySelectorAll('.vwh-pane').forEach(function(p){p.classList.remove('active');});
document.querySelector('[data-pane="p-warroom"]').classList.add('active');
document.getElementById('p-warroom').classList.add('active');
gmPost(BACKEND_URL+'/api/sos', {target_id:id, target_name:name, caller_name:caller})
.then(function(){syncWarRoom();})
.catch(function(){ toast('SOS network failure', true); });
});
// Populate FF banner
var banner = card.querySelector('#ff-'+prefix+'-'+id);
var ffi = ffData?.[id];
if (ffi && ffi.ff!==null) {
banner.innerHTML = ffHTML(ffi.ff, ffi.est);
} else if (KEYS.ts) {
(async function() {
var stats=null;
try { var d=await gmGet('https://www.tornstats.com/api/v2/'+KEYS.ts+'/spy/'+id); if(d.status&&d.spy)stats=d.spy.total; } catch(e){}
var fs=fmtStats(stats);
banner.innerHTML = fs!=='N/A'
? '<span style="color:#6a5020;font-weight:bold;margin-right:5px;">ᛏ Stats:</span><span style="color:#a08040;font-weight:bold;">'+fs+'</span>'
: '<span class="vwh-dim">ᚷ No intelligence found in the archives.</span>';
})();
} else {
banner.innerHTML = '<span class="vwh-dim">ᚷ No intelligence found.</span>';
}
}
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
// LIVE AUTO-SYNC (15s)
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
async function liveAutoSync() {
if (!APP.enemyFacId) return;
try {
var d = await gmGet('https://api.torn.com/v2/faction/'+APP.enemyFacId+'/members?key='+KEYS.torn);
for (var id in (d.members||{})) {
var m=d.members[id], badge=document.getElementById('sp-live-'+id);
if (badge && m.status) {
var txt=m.status.state;
if(m.status.until) txt+=' ('+Math.ceil((m.status.until-(Date.now()/1000))/60)+'m)';
badge.textContent=txt;
badge.style.color=m.status.state==='Hospital'?'#c03030':m.status.state==='Okay'?'#3a8a3a':'#c9a84c';
}
}
} catch(e) { console.error('[VWH] Auto-sync failed:', e); }
}
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
// WAR ROOM
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
document.getElementById('wr-manual-btn').addEventListener('click', async function() {
var inp = document.getElementById('wr-manual-id'), id=inp.value.trim(); if(!id)return;
var btn=this, orig=btn.textContent;
btn.textContent='SENT!'; btn.disabled=true;
btn.classList.remove('vwh-btn-blood'); btn.classList.add('vwh-btn-forest');
inp.value='';
toast('🚨 SOS dispatched to the Hall!');
var caller = document.querySelector('.menu-value___3hOM0')?.innerText||'A Viking';
gmPost(BACKEND_URL+'/api/sos', {target_id:id, target_name:'Enemy ['+id+']', caller_name:caller})
.then(function(){syncWarRoom();})
.catch(function(){toast('SOS network error',true);});
setTimeout(function(){
btn.classList.remove('vwh-btn-forest'); btn.classList.add('vwh-btn-blood');
btn.textContent=orig; btn.disabled=false;
}, 700);
});
async function syncWarRoom() {
try {
var data = await gmGet(BACKEND_URL+'/api/sos');
var list = document.getElementById('wr-list');
if (!data.targets || !data.targets.length) {
list.innerHTML='<div class="vwh-dim" style="text-align:center;padding:18px;">ᚹ No battle-calls echo in the Hall. ᚹ</div>';
return;
}
var ffData={};
if (KEYS.torn && data.targets.length) {
try {
var fd=await gmGet('https://ffscouter.com/api/v1/get-stats?key='+KEYS.torn+'&targets='+data.targets.map(function(t){return t.target_id;}).join(','));
if(Array.isArray(fd)){fd.forEach(function(r){if(r.player_id)ffData[r.player_id]={ff:r.fair_fight,est:r.bs_estimate_human};});}
} catch(e){}
}
list.innerHTML='';
data.targets.forEach(function(t) {
var card=document.createElement('div'); card.className='vwh-wr-card';
card.innerHTML=
'<div style="font-size:10px;color:#4a2010;margin-bottom:5px;">ᚨ Spotted by: <strong style="color:#7a4020;">'+t.called_by+'</strong></div>'+
'<div style="margin-bottom:7px;">'+
'<a href="/profiles.php?XID='+t.target_id+'" target="_blank" class="vwh-card-name">🎯 '+t.target_name+' <span style="color:#4a3820;">['+t.target_id+']</span></a>'+
'</div>'+
'<div class="vwh-card-acts">'+
'<button class="vwh-btn vwh-btn-blood wr-atk" style="padding:5px 10px;font-size:10px;">⚔ ATTACK</button>'+
'<button class="vwh-btn vwh-btn-gold wr-resolve" data-id="'+t.target_id+'" style="padding:5px 10px;font-size:10px;">✓ RESOLVED</button>'+
'</div>'+
'<div id="wrff-'+t.target_id+'" class="vwh-card-stats" style="margin-top:6px;display:none;"></div>'+
'<div class="vwh-rune-hr"></div>'+
'<div style="display:flex;gap:6px;align-items:center;flex-wrap:wrap;">'+
'<input type="time" step="1" class="vwh-input wr-time" style="width:110px;padding:5px;">'+
'<button class="vwh-btn wr-copy" style="padding:5px 10px;font-size:10px;">📋 COPY DISPATCH</button>'+
'</div>';
list.appendChild(card);
card.querySelector('.wr-atk').addEventListener('click', function() { window.open('/loader.php?sid=attack&user2ID='+t.target_id); });
card.querySelector('.wr-resolve').addEventListener('click', function(e) {
var tid=e.currentTarget.dataset.id;
card.style.opacity='0.4'; toast('Target vanquished!');
gmDel(BACKEND_URL+'/api/sos/'+tid).then(function(){card.remove();}).catch(function(){card.style.opacity='1';toast('Failed to resolve',true);});
});
card.querySelector('.wr-copy').addEventListener('click', function() {
var tv=card.querySelector('.wr-time').value;
var ts=tv?' — Sync at [b]'+tv+' TCT[/b]':'';
var msg='🚨 [b]SHIELD-BROTHERS, TO ARMS:[/b] [url=https://www.torn.com/loader.php?sid=attack&user2ID='+t.target_id+']'+t.target_name+' ['+t.target_id+'][/url]'+ts+' ⚔';
navigator.clipboard.writeText(msg).then(function(){toast('Battle-call copied!');});
});
var banner=card.querySelector('#wrff-'+t.target_id);
var ffi=ffData[t.target_id];
if(ffi&&ffi.ff!==null){banner.style.display='block';banner.innerHTML=ffHTML(ffi.ff,ffi.est);}
else if(KEYS.ts){
(async function(){
try{var d=await gmGet('https://www.tornstats.com/api/v2/'+KEYS.ts+'/spy/'+t.target_id);if(d.status&&d.spy){var fs=fmtStats(d.spy.total);if(fs!=='N/A'){banner.style.display='block';banner.innerHTML='<span style="color:#6a5020;font-weight:bold;margin-right:5px;">ᛏ Stats:</span><span style="color:#a08040;font-weight:bold;">'+fs+'</span>';}}}catch(e){}
})();
}
});
} catch(e){ console.log('[VWH] War Room offline:', e); }
}
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
// LAUNCH
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
if (document.readyState==='loading') {
document.addEventListener('DOMContentLoaded', function(){if(wrap.style.display!=='none')boot();});
} else { if(wrap.style.display!=='none')boot(); }
})();