All-in-One Faction HUD – v25.0: Age in Tagen, Enemy Bounty, Bank raus, Bounty-Filter bereinigt, Code aufgeräumt
// ==UserScript==
// @name SHAYA UI
// @namespace https://www.torn.com/
// @version 25.0
// @description All-in-One Faction HUD – v25.0: Age in Tagen, Enemy Bounty, Bank raus, Bounty-Filter bereinigt, Code aufgeräumt
// @author xShaYaKaZ
// @match https://www.torn.com/*
// @grant GM_xmlhttpRequest
// @grant GM_setValue
// @grant GM_getValue
// @connect api.torn.com
// ==/UserScript==
(function () {
'use strict';
const VERSION = '25.0';
const SCRIPT_TITLE = "SHAYA UI";
const API_BASE = 'https://api.torn.com';
const REFRESH = {
PERSONAL: 15000,
FACTION: 30000,
CHAIN: 8000,
TICK: 1000,
NETWORTH: 60000,
COMPANY: 60000,
ENEMY: 30000,
};
const TRAVEL_WARN_SEC = 420;
const TRAVEL_CRIT_SEC = 120;
const CHAIN_WARN_SEC = 60;
const CHAIN_START_HIT = 10;
const INACTIVE_LIMIT = 3;
const SHOPPING_TIME_SEC = 15;
const HAPPY_JUMP_THRESHOLD = 151;
const DESTINATIONS = {
'Mexico': { flag: '🇲🇽' },
'Cayman Islands': { flag: '🇰🇾' },
'Canada': { flag: '🇨🇦' },
'Hawaii': { flag: '🇺🇸' },
'United Kingdom': { flag: '🇬🇧' },
'Argentina': { flag: '🇦🇷' },
'Switzerland': { flag: '🇨🇭' },
'Japan': { flag: '🇯🇵' },
'China': { flag: '🇨🇳' },
'UAE': { flag: '🇦🇪' },
'South Africa': { flag: '🇿🇦' },
'Torn': { flag: '🏠' },
'Torn City': { flag: '🏠' },
};
const DEST_TOP_ITEMS = {
'Mexico': ['Jaguar Plushie', 'Dahlia'],
'United Kingdom': ['Xanax', 'Red Fox Plushie'],
'Japan': ['Xanax', 'Cherry Blossom'],
'South Africa': ['Lion Plushie', 'African Violet'],
'Cayman Islands': ['Beer', 'Flower'],
'Canada': ['Wine', 'Beer'],
'Hawaii': ['Flower', 'Cannabis'],
'Argentina': ['Cocaine', 'Cannabis'],
'Switzerland': ['Ecstasy', 'Wine'],
'China': ['Opium', 'Heroin'],
'UAE': ['Cannabis', 'Laptop'],
};
const THEMES = {
obsidian: { name:'⬛ Obsidian', bg:'#0a0a0b', bgCard:'#111114', bgEl:'#18181c', border:'#2a2a30', accent:'#6366f1', accent2:'#818cf8', text:'#f1f5f9', textMuted:'#64748b', textDim:'#94a3b8', green:'#10b981', red:'#ef4444', amber:'#f59e0b', blue:'#3b82f6', chain:'#ef4444', travel:'#6366f1', online:'#10b981' },
slate: { name:'🔵 Slate', bg:'#0f172a', bgCard:'#1e293b', bgEl:'#293548', border:'#334155', accent:'#38bdf8', accent2:'#7dd3fc', text:'#f8fafc', textMuted:'#64748b', textDim:'#94a3b8', green:'#22c55e', red:'#f87171', amber:'#fbbf24', blue:'#38bdf8', chain:'#f87171', travel:'#38bdf8', online:'#22c55e' },
emerald: { name:'🟢 Emerald', bg:'#030a06', bgCard:'#071210', bgEl:'#0d1e17', border:'#1a3a28', accent:'#10b981', accent2:'#34d399', text:'#ecfdf5', textMuted:'#4b7a60', textDim:'#6ee7b7', green:'#10b981', red:'#ef4444', amber:'#f59e0b', blue:'#3b82f6', chain:'#ef4444', travel:'#10b981', online:'#10b981' },
crimson: { name:'🔴 Crimson', bg:'#0a0305', bgCard:'#150509', bgEl:'#1f070c', border:'#3a0d14', accent:'#f43f5e', accent2:'#fb7185', text:'#fff1f2', textMuted:'#7a3040', textDim:'#fca5a5', green:'#22c55e', red:'#f43f5e', amber:'#fb923c', blue:'#60a5fa', chain:'#f43f5e', travel:'#fb923c', online:'#22c55e' },
};
// ═══════════════════════════════════════════════════════════════
// CSS
// ═══════════════════════════════════════════════════════════════
const buildCSS = () => `
@import url('https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;600&family=DM+Sans:wght@400;500;600;700&display=swap');
:root { --bg:#0a0a0b; --bg-card:#111114; --bg-el:#18181c; --border:#2a2a30; --accent:#6366f1; --accent2:#818cf8; --text:#f1f5f9; --muted:#64748b; --dim:#94a3b8; --green:#10b981; --red:#ef4444; --amber:#f59e0b; --blue:#3b82f6; --chain:#ef4444; --travel:#6366f1; --online:#10b981; --r:12px; --r-sm:8px; --r-lg:16px; --font:'DM Sans',sans-serif; --mono:'JetBrains Mono',monospace; }
* { box-sizing:border-box; margin:0; padding:0; }
#thud-overlay { position:fixed; z-index:99999; font-family:var(--font); font-size:13px; color:var(--text); background:var(--bg); border:1px solid var(--border); border-radius:20px; box-shadow:0 32px 80px rgba(0,0,0,.95),0 0 0 1px rgba(255,255,255,.03); display:flex; flex-direction:column; overflow:hidden; min-width:300px; min-height:200px; user-select:none; }
#thud-overlay.minimized { min-height:0 !important; height:auto !important; border-radius:12px; }
#thud-overlay.minimized .thud-tabs, #thud-overlay.minimized .thud-body, #thud-overlay.minimized .thud-resize-se, #thud-overlay.minimized .thud-resize-e, #thud-overlay.minimized .thud-resize-s { display:none !important; }
#thud-overlay.minimized .thud-mini { display:flex !important; }
#thud-overlay:not(.minimized) .thud-mini { display:none !important; }
.thud-header { display:flex; align-items:center; gap:8px; padding:10px 14px; background:linear-gradient(180deg,rgba(255,255,255,.025) 0%,transparent 100%); border-bottom:1px solid var(--border); cursor:grab; flex-shrink:0; }
.thud-header:active { cursor:grabbing; }
.thud-header-logo { width:32px; height:32px; flex-shrink:0; display:flex; align-items:center; justify-content:center; filter:drop-shadow(0 2px 8px rgba(239,68,68,.5)); }
.thud-header-logo svg { width:32px; height:32px; }
.thud-header-info { flex:1; min-width:0; }
.thud-header-title { font-size:13px; font-weight:800; color:var(--text); letter-spacing:1px; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; text-transform:uppercase; }
.thud-header-sub { font-size:9px; color:var(--muted); font-family:var(--mono); margin-top:1px; }
.thud-header-actions { display:flex; gap:4px; flex-shrink:0; align-items:center; }
.thud-btn { background:var(--bg-el); border:1px solid var(--border); color:var(--dim); border-radius:7px; padding:5px 8px; cursor:pointer; font-size:11px; font-family:var(--font); font-weight:600; transition:all .15s; display:flex; align-items:center; gap:4px; white-space:nowrap; }
.thud-btn:hover { border-color:var(--accent); color:var(--text); background:rgba(99,102,241,.1); }
.thud-btn.active { border-color:var(--green); color:var(--green); background:rgba(16,185,129,.08); }
.thud-btn-close { background:rgba(239,68,68,.12); border:1px solid rgba(239,68,68,.25); color:var(--red); border-radius:7px; padding:5px 8px; cursor:pointer; font-size:12px; font-weight:700; transition:all .15s; display:flex; align-items:center; line-height:1; }
.thud-btn-close:hover { background:rgba(239,68,68,.3); border-color:var(--red); }
.pip { width:6px; height:6px; border-radius:50%; background:var(--green); box-shadow:0 0 6px var(--green); animation:pipPulse 2s ease-in-out infinite; }
@keyframes pipPulse { 0%,100%{opacity:1} 50%{opacity:.3} }
.thud-tabs { display:flex; background:var(--bg); border-bottom:1px solid var(--border); flex-shrink:0; overflow-x:auto; overflow-y:hidden; scrollbar-width:none; gap:2px; padding:5px 6px 0; cursor:default; }
.thud-tabs::-webkit-scrollbar { display:none; }
.thud-tab { display:flex; flex-direction:column; align-items:center; justify-content:center; gap:2px; padding:6px 8px 8px; cursor:pointer; color:var(--muted); border-bottom:2px solid transparent; border-radius:7px 7px 0 0; transition:all .15s; white-space:nowrap; min-width:40px; flex-shrink:0; }
.thud-tab .ti { font-size:13px; line-height:1; }
.thud-tab .tl { font-size:7.5px; font-weight:700; letter-spacing:.7px; text-transform:uppercase; }
.thud-tab:hover { color:var(--dim); background:rgba(255,255,255,.04); }
.thud-tab.active { color:var(--accent2); border-bottom-color:var(--accent); background:rgba(99,102,241,.06); }
.thud-mini { display:none; flex-direction:column; align-items:stretch; }
.thud-mini-bar { display:flex; align-items:center; gap:6px; padding:5px 10px; flex-wrap:nowrap; overflow:hidden; border-top:1px solid var(--border); }
.thud-mini-chip { display:flex; align-items:center; gap:3px; font-size:10px; font-weight:700; flex-shrink:0; background:var(--bg-el); border:1px solid var(--border); border-radius:6px; padding:2px 6px; }
.thud-mini-val { color:var(--text); font-family:var(--mono); font-size:10px; }
.thud-mini-lbl { color:var(--muted); font-size:9px; }
.thud-mini-tabs { display:flex; border-top:1px solid rgba(255,255,255,.04); width:100%; }
.thud-mini-tab { flex:1; padding:4px 2px; text-align:center; font-size:9px; font-weight:700; color:var(--muted); cursor:pointer; border-right:1px solid rgba(255,255,255,.04); letter-spacing:.3px; text-transform:uppercase; transition:all .15s; }
.thud-mini-tab:last-child { border-right:none; }
.thud-mini-tab:hover { color:var(--accent2); background:rgba(99,102,241,.06); }
.thud-body { flex:1 1 0; overflow-y:scroll !important; min-height:0; display:flex; flex-direction:column; gap:10px; padding:12px 14px 14px; pointer-events:auto; scrollbar-width:thin; scrollbar-color:var(--border) transparent; }
.thud-body::-webkit-scrollbar { width:4px; }
.thud-body::-webkit-scrollbar-thumb { background:var(--border); border-radius:2px; }
.card { background:var(--bg-card); border:1px solid var(--border); border-radius:var(--r); overflow:hidden; flex-shrink:0; }
.card-header { display:flex; align-items:center; justify-content:space-between; padding:10px 14px; border-bottom:1px solid rgba(255,255,255,.05); }
.card-title { font-size:10px; font-weight:700; letter-spacing:.8px; text-transform:uppercase; color:var(--dim); }
.card-title.accent { color:var(--accent2); } .card-title.green { color:var(--green); } .card-title.red { color:var(--red); } .card-title.amber { color:var(--amber); } .card-title.blue { color:var(--blue); }
.card-body { padding:12px 14px; display:flex; flex-direction:column; gap:8px; }
.card-body.p0 { padding:0; }
.badge { padding:2px 7px; border-radius:20px; font-size:9px; font-weight:700; letter-spacing:.5px; }
.badge-green { background:rgba(16,185,129,.12); color:var(--green); border:1px solid rgba(16,185,129,.2); }
.badge-red { background:rgba(239,68,68,.12); color:var(--red); border:1px solid rgba(239,68,68,.2); }
.badge-amber { background:rgba(245,158,11,.12); color:var(--amber); border:1px solid rgba(245,158,11,.2); }
.badge-blue { background:rgba(59,130,246,.12); color:var(--blue); border:1px solid rgba(59,130,246,.2); }
.badge-muted { background:rgba(255,255,255,.06); color:var(--muted); border:1px solid rgba(255,255,255,.08); }
.row { display:flex; align-items:center; justify-content:space-between; padding:6px 0; border-bottom:1px solid rgba(255,255,255,.04); }
.row:last-child { border-bottom:none; }
.row-label { font-size:12px; color:var(--dim); font-weight:500; }
.row-value { font-size:12px; font-family:var(--mono); font-weight:600; color:var(--text); }
.bar { height:4px; background:rgba(255,255,255,.06); border-radius:3px; overflow:hidden; }
.bar-lg { height:6px; }
.bar-fill { height:100%; border-radius:3px; transition:width .4s; }
.bar-block { display:flex; flex-direction:column; gap:5px; }
.bar-block-head { display:flex; justify-content:space-between; align-items:center; }
.bar-block-lbl { font-size:12px; color:var(--muted); font-weight:600; }
.bar-block-val { font-size:13px; font-family:var(--mono); font-weight:700; }
.bar-block-sub { font-size:10px; color:var(--muted); }
.stat-grid { display:grid; gap:8px; }
.stat-grid-3 { grid-template-columns:repeat(3,1fr); }
.stat-grid-4 { grid-template-columns:repeat(4,1fr); }
.stat-card { background:var(--bg-el); border:1px solid var(--border); border-radius:var(--r-sm); padding:10px 6px; text-align:center; }
.sv { font-size:17px; font-weight:700; font-family:var(--mono); line-height:1; }
.sv-green { color:var(--green); } .sv-red { color:var(--red); } .sv-amber { color:var(--amber); } .sv-blue { color:var(--blue); }
.sl { font-size:9px; color:var(--muted); text-transform:uppercase; letter-spacing:.5px; margin-top:4px; }
.stat-chip { text-align:center; }
.sn { font-size:15px; font-weight:700; font-family:var(--mono); }
.status-summary { display:flex; gap:10px; flex-wrap:wrap; justify-content:center; padding:10px 0 4px; border-top:1px solid rgba(255,255,255,.05); margin-top:6px; }
.ql-grid { display:grid; grid-template-columns:repeat(4,1fr); gap:6px; padding:2px 0 4px; }
.ql-card { background:var(--bg-el); border:1px solid var(--border); border-radius:10px; padding:10px 4px 8px; cursor:pointer; text-align:center; display:flex; flex-direction:column; align-items:center; gap:4px; transition:all .18s; text-decoration:none; }
.ql-card:hover { border-color:var(--accent); background:rgba(99,102,241,.1); transform:translateY(-2px); box-shadow:0 6px 20px rgba(99,102,241,.15); }
.ql-icon { font-size:20px; line-height:1; }
.ql-name { font-size:9px; font-weight:800; color:var(--dim); text-align:center; line-height:1.2; }
.travel-card { border:1px solid rgba(99,102,241,.15); border-radius:var(--r); overflow:hidden; flex-shrink:0; }
.travel-card.home { border-color:rgba(16,185,129,.15); }
.travel-card.warn { border-color:rgba(245,158,11,.3); }
.travel-card.crit { border-color:rgba(239,68,68,.4); animation:travelCritPulse 1s ease-in-out infinite alternate; }
.travel-card.ok { border-color:rgba(99,102,241,.3); }
@keyframes travelCritPulse { from{box-shadow:none} to{box-shadow:0 0 20px rgba(239,68,68,.3)} }
.travel-card-top { display:flex; align-items:center; justify-content:space-between; padding:8px 14px; font-size:10px; font-weight:700; letter-spacing:.8px; text-transform:uppercase; background:rgba(0,0,0,.15); border-bottom:1px solid rgba(255,255,255,.05); }
.travel-card-body { padding:12px 14px; }
.travel-dest { font-size:18px; font-weight:800; letter-spacing:-.3px; }
.travel-clock { font-size:28px; font-weight:800; font-family:var(--mono); line-height:1; }
.travel-prog { height:3px; background:rgba(255,255,255,.06); border-radius:2px; overflow:hidden; margin-top:10px; }
.travel-prog-fill { height:100%; border-radius:2px; background:var(--travel); transition:width .5s; }
.travel-prog-fill.warn { background:var(--amber); }
.travel-prog-fill.crit { background:var(--red); }
.smart-tipp { background:linear-gradient(135deg,rgba(99,102,241,.09),rgba(129,140,248,.03)); border:1px solid rgba(99,102,241,.25); border-left:3px solid var(--accent); border-radius:var(--r); padding:9px 12px; font-size:12px; color:var(--dim); line-height:1.6; flex-shrink:0; display:flex; align-items:center; gap:8px; }
.smart-tipp-icon { font-size:15px; flex-shrink:0; }
.smart-tipp-text { flex:1; }
.member-grid { display:grid; grid-template-columns:repeat(2,1fr); gap:2px 8px; }
.member-row { display:flex; align-items:center; gap:5px; padding:4px 0; border-bottom:1px solid rgba(255,255,255,.04); }
.member-row:last-child { border-bottom:none; }
.member-name { flex:1; font-size:11px; font-weight:600; color:var(--text); overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
.member-status { font-size:9px; color:var(--muted); font-weight:600; white-space:nowrap; }
.status-dot { width:6px; height:6px; border-radius:50%; flex-shrink:0; }
.sd-online { background:var(--online); box-shadow:0 0 5px var(--online); }
.sd-idle { background:var(--amber); }
.sd-traveling { background:var(--travel); }
.sd-hospital { background:var(--red); }
.sd-jail { background:#f97316; }
.sd-offline { background:rgba(255,255,255,.12); }
.offline-row { display:flex; align-items:center; gap:8px; padding:6px 14px; border-bottom:1px solid rgba(255,255,255,.04); }
.offline-row:last-child { border-bottom:none; }
.offline-name { flex:1; font-size:11px; color:var(--muted); overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
.offline-ago { font-size:10px; font-family:var(--mono); font-weight:700; }
.ago-m { color:var(--amber); } .ago-h { color:var(--red); } .ago-d { color:var(--muted); }
.bounty-row { display:flex; align-items:flex-start; gap:10px; padding:10px 14px; border-bottom:1px solid rgba(255,255,255,.04); }
.bounty-row:last-child { border-bottom:none; }
.bounty-row.is-hospital { background:rgba(239,68,68,.06); border-left:3px solid var(--red); }
.bounty-reward { font-family:var(--mono); font-size:12px; font-weight:700; color:var(--amber); white-space:nowrap; }
.bounty-link { display:inline-flex; align-items:center; padding:5px 10px; border-radius:6px; background:rgba(239,68,68,.15); border:1px solid rgba(239,68,68,.25); color:var(--red); font-size:11px; font-weight:700; text-decoration:none; white-space:nowrap; transition:all .15s; }
.bounty-link:hover { background:rgba(239,68,68,.3); }
.bounty-difficulty { padding:2px 6px; border-radius:4px; font-size:9px; font-weight:800; }
.diff-easy { background:rgba(16,185,129,.15); color:var(--green); }
.diff-medium { background:rgba(245,158,11,.15); color:var(--amber); }
.diff-hard { background:rgba(239,68,68,.15); color:var(--red); }
.sort-bar { display:flex; align-items:center; gap:5px; flex-wrap:wrap; }
.bounty-info-row { display:flex; gap:5px; flex-wrap:wrap; align-items:center; margin-top:4px; }
.bounty-tag { font-size:9px; font-weight:700; padding:1px 5px; border-radius:4px; white-space:nowrap; }
.tag-hosp { background:rgba(239,68,68,.25); color:var(--red); border:1px solid rgba(239,68,68,.5); font-size:10px; padding:2px 7px; border-radius:5px; animation:hospTagPulse 1.5s ease-in-out infinite; }
@keyframes hospTagPulse { 0%,100%{box-shadow:none} 50%{box-shadow:0 0 6px rgba(239,68,68,.5)} }
.tag-jail { background:rgba(245,158,11,.15); color:var(--amber); border:1px solid rgba(245,158,11,.2); }
.tag-travel { background:rgba(99,102,241,.15); color:var(--accent2); border:1px solid rgba(99,102,241,.2); }
.tag-ok { background:rgba(16,185,129,.10); color:var(--green); border:1px solid rgba(16,185,129,.15); }
.bounty-search { width:100%; background:var(--bg); border:1px solid var(--border); border-radius:var(--r-sm); padding:7px 11px; color:var(--text); font-family:var(--font); font-size:12px; outline:none; transition:border-color .15s; }
.bounty-search:focus { border-color:var(--accent); }
.level-filter-row { display:flex; align-items:center; gap:6px; flex-wrap:wrap; }
.level-input { background:var(--bg); border:1px solid var(--border); border-radius:6px; padding:4px 7px; color:var(--accent2); font-family:var(--mono); font-size:11px; width:55px; text-align:center; outline:none; }
.level-input:focus { border-color:var(--accent); }
.reward-input { background:var(--bg); border:1px solid var(--border); border-radius:6px; padding:4px 7px; color:var(--amber); font-family:var(--mono); font-size:11px; width:70px; text-align:center; outline:none; }
.bounty-load-more-inline { display:inline-flex; align-items:center; gap:4px; padding:3px 9px; border-radius:6px; background:rgba(99,102,241,.15); border:1px solid rgba(99,102,241,.3); color:var(--accent2); font-size:10px; font-weight:700; cursor:pointer; font-family:var(--font); transition:all .15s; }
.bounty-load-more-inline:hover { background:rgba(99,102,241,.28); }
.company-kpi-grid { display:grid; grid-template-columns:repeat(2,1fr); gap:7px; margin-bottom:8px; }
.company-kpi { background:var(--bg-el); border:1px solid var(--border); border-radius:var(--r-sm); padding:9px 7px; text-align:center; }
.kv { font-size:13px; font-weight:700; font-family:var(--mono); line-height:1; }
.kl { font-size:9px; color:var(--muted); text-transform:uppercase; letter-spacing:.5px; margin-top:4px; }
.company-emp-row { display:flex; align-items:center; gap:8px; padding:7px 14px; border-bottom:1px solid rgba(255,255,255,.04); }
.company-emp-row:last-child { border-bottom:none; }
.company-emp-name { flex:1; font-size:12px; font-weight:600; color:var(--text); overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
.company-emp-pos { font-size:10px; color:var(--muted); white-space:nowrap; min-width:60px; }
.company-emp-eff { font-size:11px; font-family:var(--mono); font-weight:700; white-space:nowrap; text-align:right; min-width:36px; }
.nw-ticker { background:linear-gradient(135deg,rgba(245,158,11,.07),rgba(251,191,36,.02)); border:1px solid rgba(245,158,11,.15); border-radius:var(--r); padding:16px 18px; flex-shrink:0; }
.nw-main-val { font-size:26px; font-weight:800; font-family:var(--mono); color:var(--amber); }
.nw-change { font-size:11px; font-family:var(--mono); font-weight:700; }
.nw-change.pos { color:var(--green); } .nw-change.neg { color:var(--red); }
.settings-section { display:flex; flex-direction:column; gap:0; }
.settings-section-title { font-size:9px; font-weight:700; color:var(--muted); text-transform:uppercase; letter-spacing:.8px; padding:12px 14px 7px; border-top:1px solid rgba(255,255,255,.05); }
.settings-section:first-child .settings-section-title { border-top:none; padding-top:4px; }
.alarm-row-item { display:flex; align-items:center; gap:10px; padding:9px 14px; border-bottom:1px solid rgba(255,255,255,.04); }
.alarm-lbl { font-size:12px; font-weight:600; color:var(--text); }
.alarm-desc { font-size:10px; color:var(--muted); margin-top:1px; }
.toggle { position:relative; display:inline-block; width:34px; height:19px; flex-shrink:0; }
.toggle input { opacity:0; width:0; height:0; }
.toggle-slider { position:absolute; inset:0; background:rgba(255,255,255,.1); border-radius:19px; transition:.2s; }
.toggle-slider::before { content:''; position:absolute; height:13px; width:13px; left:3px; bottom:3px; background:var(--dim); border-radius:50%; transition:.2s; }
.toggle input:checked + .toggle-slider { background:var(--accent); }
.toggle input:checked + .toggle-slider::before { transform:translateX(15px); background:white; }
.theme-grid { display:grid; grid-template-columns:repeat(2,1fr); gap:5px; }
.theme-btn { padding:9px 5px; border-radius:var(--r-sm); border:1px solid var(--border); background:var(--bg-el); font-size:10px; font-weight:600; color:var(--muted); cursor:pointer; font-family:var(--font); transition:all .15s; text-align:center; }
.theme-btn.active,.theme-btn:hover { border-color:var(--accent); color:var(--accent2); background:rgba(99,102,241,.08); }
.vis-grid { display:flex; flex-direction:column; gap:0; }
.vis-row { display:flex; align-items:center; justify-content:space-between; padding:8px 10px; background:rgba(0,0,0,.15); border-radius:var(--r-sm); border:1px solid rgba(255,255,255,.04); }
.vis-lbl { font-size:11px; color:var(--dim); font-weight:500; }
.api-key-row { display:flex; align-items:center; gap:10px; padding:10px 12px; background:rgba(0,0,0,.2); border-radius:var(--r-sm); border:1px solid rgba(255,255,255,.04); }
.api-key-status { flex:1; font-size:12px; font-weight:600; }
.api-key-status.set { color:var(--green); } .api-key-status.unset { color:var(--red); }
.sound-profile-grid { display:grid; grid-template-columns:repeat(3,1fr); gap:5px; }
.sp-btn { padding:9px 5px; border-radius:var(--r-sm); border:1px solid var(--border); background:var(--bg-el); font-size:10px; font-weight:600; color:var(--muted); cursor:pointer; font-family:var(--font); transition:all .15s; text-align:center; }
.sp-btn:hover,.sp-btn.active { border-color:var(--accent); color:var(--accent2); background:rgba(99,102,241,.08); }
.section-header { font-size:9px; font-weight:700; color:var(--muted); text-transform:uppercase; letter-spacing:.8px; padding:3px 0 7px; display:flex; align-items:center; gap:7px; }
.section-header::after { content:''; flex:1; height:1px; background:rgba(255,255,255,.06); }
#thud-toasts { position:fixed; bottom:20px; left:20px; z-index:999998; display:flex; flex-direction:column-reverse; gap:10px; pointer-events:none; width:340px; }
.toast { display:flex; align-items:flex-start; gap:14px; background:var(--bg-card); border-radius:var(--r); padding:16px; pointer-events:all; border:1px solid var(--border); border-left:4px solid var(--green); box-shadow:0 16px 60px rgba(0,0,0,.95); animation:toastIn .25s cubic-bezier(.34,1.56,.64,1); }
.toast.warn { border-left-color:var(--amber); }
.toast.crit { border-left-color:var(--red); }
.toast.info { border-left-color:var(--blue); }
.toast.travel { border-left-color:var(--travel); }
.toast.landed { border-left-color:var(--red); background:rgba(239,68,68,.08); }
.toast.enemy-alert { border-left-color:#ff4400; background:rgba(255,68,0,.12); box-shadow:0 16px 60px rgba(0,0,0,.95),0 0 50px rgba(255,68,0,.5),0 0 0 2px #ff4400; animation:toastIn .25s cubic-bezier(.34,1.56,.64,1), enemyPulse .6s ease-in-out infinite alternate; }
@keyframes enemyPulse { from{box-shadow:0 16px 60px rgba(0,0,0,.95),0 0 30px rgba(255,68,0,.4)} to{box-shadow:0 16px 60px rgba(0,0,0,.95),0 0 70px rgba(255,68,0,.8),0 0 0 2px #ff4400} }
@keyframes toastIn { from{opacity:0;transform:translateX(-20px) scale(.95)} to{opacity:1;transform:translateX(0) scale(1)} }
@keyframes toastOut { from{opacity:1;transform:translateX(0) scale(1)} to{opacity:0;transform:translateX(-16px) scale(.95)} }
.toast.leaving { animation:toastOut .2s ease forwards; }
.toast-icon { font-size:22px; flex-shrink:0; margin-top:1px; }
.toast-body { flex:1; min-width:0; }
.toast-title { font-size:14px; font-weight:800; color:var(--text); letter-spacing:.3px; }
.toast-msg { font-size:12px; color:var(--dim); margin-top:4px; line-height:1.5; }
.toast-x { background:transparent; border:1px solid var(--border); color:var(--muted); border-radius:5px; width:20px; height:20px; cursor:pointer; font-size:10px; display:flex; align-items:center; justify-content:center; flex-shrink:0; }
.thud-modal-bg { position:fixed; inset:0; background:rgba(0,0,0,.85); z-index:999999; display:flex; align-items:center; justify-content:center; }
.thud-modal { background:var(--bg-card); border:1px solid var(--border); border-radius:var(--r-lg); padding:26px; width:340px; box-shadow:0 32px 80px rgba(0,0,0,.9); }
.thud-modal h3 { font-size:15px; font-weight:700; color:var(--text); margin-bottom:7px; }
.thud-modal p { font-size:12px; color:var(--muted); margin-bottom:16px; line-height:1.7; }
.thud-modal input { width:100%; background:var(--bg); border:1px solid var(--border); border-radius:var(--r-sm); padding:10px 12px; color:var(--text); font-family:var(--mono); font-size:12px; outline:none; margin-bottom:11px; }
.thud-modal input:focus { border-color:var(--accent); }
.thud-modal textarea { width:100%; background:var(--bg); border:1px solid var(--border); border-radius:var(--r-sm); padding:10px 12px; color:var(--text); font-family:var(--font); font-size:12px; outline:none; margin-bottom:11px; min-height:70px; resize:vertical; }
.modal-btns { display:flex; gap:8px; justify-content:flex-end; }
.modal-save { background:var(--green); color:#000; border:none; border-radius:var(--r-sm); padding:9px 18px; font-size:12px; font-weight:700; cursor:pointer; font-family:var(--font); }
.modal-cancel { background:var(--bg-el); color:var(--muted); border:1px solid var(--border); border-radius:var(--r-sm); padding:9px 14px; font-size:12px; font-weight:600; cursor:pointer; font-family:var(--font); }
.thud-resize-se { position:absolute; right:0; bottom:0; width:18px; height:18px; cursor:se-resize; z-index:10; }
.thud-resize-se::after { content:''; position:absolute; right:4px; bottom:4px; width:7px; height:7px; border-right:2px solid var(--muted); border-bottom:2px solid var(--muted); opacity:.2; }
.thud-resize-e { position:absolute; right:-3px; top:20px; bottom:20px; width:8px; cursor:ew-resize; z-index:9; }
.thud-resize-s { position:absolute; left:20px; right:20px; bottom:-3px; height:8px; cursor:ns-resize; z-index:9; }
#thud-fab { position:fixed; z-index:99998; width:36px; height:36px; border-radius:10px; background:linear-gradient(135deg,#1a0505,#2a0a0a); border:1px solid rgba(239,68,68,.3); box-shadow:0 4px 16px rgba(0,0,0,.85); cursor:grab; display:flex; align-items:center; justify-content:center; transition:all .2s; overflow:hidden; }
#thud-fab:active { cursor:grabbing; transform:scale(.93); }
#thud-fab:hover { border-color:rgba(239,68,68,.7); box-shadow:0 4px 20px rgba(239,68,68,.4); }
#thud-fab.on { border-color:rgba(239,68,68,.5); box-shadow:0 4px 20px rgba(239,68,68,.35); }
#thud-fab svg { width:24px; height:24px; filter:drop-shadow(0 0 4px rgba(239,68,68,.8)); }
.chain-block { padding:8px 14px; border-bottom:1px solid var(--border); flex-shrink:0; }
.chain-idle { display:flex; align-items:center; }
.chain-active-row { display:flex; align-items:center; }
.chain-hits { font-family:var(--mono); font-size:18px; font-weight:800; color:var(--chain); }
.chain-hits-unit { font-size:9px; color:var(--muted); font-weight:600; text-transform:uppercase; align-self:flex-end; margin-bottom:2px; }
.chain-divider { width:1px; height:24px; background:rgba(255,255,255,.1); }
.chain-timer { font-family:var(--mono); font-size:16px; font-weight:800; color:var(--green); }
.chain-timer.warn { color:var(--chain); }
.chain-mult { font-family:var(--mono); font-size:15px; font-weight:800; color:var(--amber); }
.chain-prog { height:3px; background:rgba(255,255,255,.06); border-radius:2px; overflow:hidden; margin-top:6px; }
.chain-prog-fill { height:100%; border-radius:2px; background:var(--chain); transition:width .5s; }
.pulse-dot { width:7px; height:7px; border-radius:50%; background:var(--chain); box-shadow:0 0 8px var(--chain); animation:pipPulse 1s ease-in-out infinite; flex-shrink:0; }
.war-grid { display:grid; grid-template-columns:repeat(4,1fr); gap:7px; }
.war-cell { background:var(--bg-el); border:1px solid var(--border); border-radius:var(--r-sm); padding:12px 5px; text-align:center; }
.war-cell .wv { font-weight:700; font-size:20px; line-height:1; font-family:var(--mono); }
.war-cell .wl { font-size:9px; color:var(--muted); text-transform:uppercase; letter-spacing:.5px; margin-top:5px; }
.enemy-row { padding:12px 14px; border-bottom:1px solid rgba(255,255,255,.05); display:flex; flex-direction:column; gap:6px; }
.enemy-row:last-child { border-bottom:none; }
.enemy-row:hover { background:rgba(255,255,255,.02); }
.enemy-row.in-hospital { background:rgba(239,68,68,.06); border-left:3px solid var(--red); }
.enemy-row.traveling { background:rgba(99,102,241,.05); border-left:3px solid var(--travel); }
.enemy-top { display:flex; align-items:center; gap:8px; flex-wrap:wrap; }
.enemy-name-link { font-size:13px; font-weight:800; color:var(--text); text-decoration:none; }
.enemy-name-link:hover { color:var(--red); }
.enemy-level { font-size:10px; font-family:var(--mono); font-weight:700; color:var(--amber); }
.enemy-meta { display:flex; gap:6px; flex-wrap:wrap; align-items:center; }
.enemy-tag { font-size:9px; font-weight:700; padding:2px 6px; border-radius:4px; white-space:nowrap; }
.etag-hosp { background:rgba(239,68,68,.2); color:var(--red); border:1px solid rgba(239,68,68,.4); }
.etag-travel { background:rgba(99,102,241,.15); color:var(--accent2); border:1px solid rgba(99,102,241,.3); }
.etag-ok { background:rgba(16,185,129,.1); color:var(--green); border:1px solid rgba(16,185,129,.2); }
.etag-jail { background:rgba(245,158,11,.1); color:var(--amber); border:1px solid rgba(245,158,11,.2); }
.etag-job { background:rgba(59,130,246,.08); color:var(--blue); border:1px solid rgba(59,130,246,.15); }
.enemy-note { font-size:10px; color:var(--muted); font-style:italic; padding:4px 8px; background:rgba(0,0,0,.2); border-radius:4px; border-left:2px solid var(--amber); }
.enemy-actions { display:flex; gap:5px; flex-wrap:wrap; }
.enemy-action-btn { display:inline-flex; align-items:center; gap:3px; padding:4px 10px; border-radius:6px; font-size:10px; font-weight:700; text-decoration:none; cursor:pointer; border:none; font-family:var(--font); transition:all .15s; }
.eab-attack { background:rgba(239,68,68,.15); border:1px solid rgba(239,68,68,.25); color:var(--red); }
.eab-attack:hover { background:rgba(239,68,68,.3); }
.eab-profile { background:rgba(255,255,255,.06); border:1px solid rgba(255,255,255,.1); color:var(--dim); }
.eab-profile:hover { border-color:var(--accent); color:var(--accent2); }
.eab-note { background:rgba(245,158,11,.1); border:1px solid rgba(245,158,11,.2); color:var(--amber); }
.eab-note:hover { background:rgba(245,158,11,.2); }
.eab-remove { background:rgba(255,255,255,.04); border:1px solid rgba(255,255,255,.08); color:var(--muted); }
.eab-remove:hover { background:rgba(239,68,68,.15); border-color:var(--red); color:var(--red); }
.enemy-add-row { display:flex; gap:6px; align-items:center; }
.enemy-id-input { flex:1; background:var(--bg); border:1px solid var(--border); border-radius:var(--r-sm); padding:7px 11px; color:var(--text); font-family:var(--mono); font-size:12px; outline:none; }
.enemy-id-input:focus { border-color:var(--red); }
.loading-state { display:flex; align-items:center; justify-content:center; padding:40px; color:var(--muted); font-size:12px; letter-spacing:1px; animation:opPulse 1.5s ease-in-out infinite; }
@keyframes opPulse { 0%,100%{opacity:1} 50%{opacity:.3} }
.empty-state { display:flex; flex-direction:column; align-items:center; justify-content:center; padding:36px 18px; color:var(--muted); gap:10px; text-align:center; }
.empty-state .es-icon { font-size:32px; opacity:.2; }
.empty-state .es-text { font-size:12px; letter-spacing:.5px; font-weight:600; }
.empty-state .es-sub { font-size:11px; opacity:.6; }
.toggle-more { display:inline-flex; align-items:center; gap:5px; padding:5px 11px; border-radius:20px; border:1px solid var(--border); color:var(--muted); cursor:pointer; font-size:10px; font-weight:600; transition:all .15s; margin-top:7px; }
.toggle-more:hover { border-color:var(--accent); color:var(--accent2); }
.ts { font-family:var(--mono); font-size:10px; color:var(--muted); }
.fullscreen-alert { position:fixed; inset:0; background:rgba(2,4,8,.97); display:none; flex-direction:column; align-items:center; justify-content:center; z-index:9999999; }
.fullscreen-alert.active { display:flex; animation:fadeIn .4s ease; }
@keyframes fadeIn { from{opacity:0} to{opacity:1} }
.fs-box { text-align:center; padding:48px; border-radius:20px; max-width:420px; background:rgba(99,102,241,.06); border:2px solid var(--accent); box-shadow:0 0 80px rgba(99,102,241,.3); }
.fs-title { font-size:20px; font-weight:700; letter-spacing:2px; text-transform:uppercase; margin:16px 0; }
.fs-msg { font-size:15px; color:var(--dim); line-height:1.6; }
.fs-close { margin-top:24px; background:rgba(99,102,241,.2); border:1px solid var(--accent); color:var(--accent2); border-radius:var(--r-sm); padding:12px 28px; font-size:12px; font-weight:700; cursor:pointer; font-family:var(--font); }
.scan-btn { display:inline-flex; align-items:center; gap:5px; padding:7px 14px; border-radius:var(--r-sm); background:rgba(99,102,241,.15); border:1px solid rgba(99,102,241,.3); color:var(--accent2); font-size:11px; font-weight:700; cursor:pointer; font-family:var(--font); transition:all .15s; }
.scan-btn:hover { background:rgba(99,102,241,.3); }
.scan-btn:disabled { opacity:.4; cursor:not-allowed; }
.spinning { animation:spin 1s linear infinite; }
@keyframes spin { to { transform:rotate(360deg); } }
.sort-btn { padding:4px 9px; border-radius:6px; border:1px solid var(--border); background:var(--bg-el); color:var(--muted); font-size:10px; font-weight:700; cursor:pointer; font-family:var(--font); transition:all .15s; }
.sort-btn.active { border-color:var(--accent); color:var(--accent2); background:rgba(99,102,241,.1); }
.threshold-input { background:var(--bg); border:1px solid var(--border); border-radius:5px; padding:3px 7px; color:var(--accent2); font-family:var(--mono); font-size:11px; width:50px; text-align:center; outline:none; }
.threshold-input:focus { border-color:var(--accent); }
.fab-pin-row { display:flex; align-items:center; justify-content:space-between; padding:9px 14px; border-bottom:1px solid rgba(255,255,255,.04); }
.fab-pin-lbl { font-size:12px; font-weight:600; color:var(--text); }
.fab-pin-desc { font-size:10px; color:var(--muted); margin-top:1px; }
`;
const SHAYA_LOGO_SVG = `<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><defs><linearGradient id="sg" x1="0%" y1="0%" x2="100%" y2="100%"><stop offset="0%" style="stop-color:#ff2222"/><stop offset="50%" style="stop-color:#cc0000"/><stop offset="100%" style="stop-color:#880000"/></linearGradient></defs><polygon points="62,10 38,10 28,28 52,28 40,48 58,48 30,90 72,90 80,70 56,70 68,50 50,50" fill="url(#sg)"/><polygon points="68,8 74,14 66,18" fill="#ff4444" opacity="0.8"/><polygon points="30,8 24,16 34,14" fill="#dd2222" opacity="0.7"/><polygon points="26,88 20,82 30,80" fill="#ff3333" opacity="0.75"/><polygon points="74,92 80,84 70,86" fill="#cc1111" opacity="0.7"/></svg>`;
const FAB_LOGO_SVG = `<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><defs><linearGradient id="fg" x1="0%" y1="0%" x2="100%" y2="100%"><stop offset="0%" style="stop-color:#ff3333"/><stop offset="100%" style="stop-color:#aa0000"/></linearGradient></defs><polygon points="62,10 38,10 28,28 52,28 40,48 58,48 30,90 72,90 80,70 56,70 68,50 50,50" fill="url(#fg)"/><polygon points="68,8 74,14 66,18" fill="#ff5555" opacity="0.9"/><polygon points="30,8 24,16 34,14" fill="#ee2222" opacity="0.8"/><polygon points="26,88 20,82 30,80" fill="#ff4444" opacity="0.8"/><polygon points="74,92 80,84 70,86" fill="#cc1111" opacity="0.75"/></svg>`;
// ═══════════════════════════════════════════════════════════════
// UTILITY
// ═══════════════════════════════════════════════════════════════
class Utils {
static fmtMoney(n) {
if (!n && n !== 0) return '—';
const abs = Math.abs(n), sign = n < 0 ? '-' : '';
if (abs >= 1e9) return sign + '$' + (abs / 1e9).toFixed(2) + 'B';
if (abs >= 1e6) return sign + '$' + (abs / 1e6).toFixed(2) + 'M';
if (abs >= 1e3) return sign + '$' + (abs / 1e3).toFixed(1) + 'K';
return sign + '$' + abs.toLocaleString('de-DE');
}
static fmtStat(n) {
if (!n && n !== 0) return '—';
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(1) + 'K';
return n.toLocaleString('de-DE');
}
static fmtTravel(sec) { if (sec <= 0) return '—'; const h = Math.floor(sec/3600), m = Math.ceil((sec%3600)/60); return h > 0 ? `${h}h ${m}m` : (m <= 0 ? '< 1m' : `${m}m`); }
static fmtHM(sec) { if (!sec || sec <= 0) return '—'; const h = Math.floor(sec/3600), m = Math.floor((sec%3600)/60); if (h > 0) return `${h}h ${m}m`; if (m > 0) return `${m}m`; return '< 1m'; }
static fmtDhm(sec) { if (!sec || sec <= 0) return 'Fertig'; const d=Math.floor(sec/86400),h=Math.floor((sec%86400)/3600),m=Math.floor((sec%3600)/60); if(d>0)return`${d}T ${h}h`; if(h>0)return`${h}h ${m}m`; return`${m}m`; }
static fmtSec(sec) { if (!sec || sec <= 0) return '0s'; const h=Math.floor(sec/3600),m=Math.floor((sec%3600)/60),s=sec%60; if(h>0)return`${h}h ${m}m`; if(m>0)return`${m}m ${s}s`; return`${s}s`; }
static arrTime(secLeft) { if (!secLeft || secLeft <= 0) return '—'; return new Date(Date.now() + secLeft*1000).toLocaleTimeString('de-DE',{hour:'2-digit',minute:'2-digit'}); }
static tctTime() { return new Date().toLocaleTimeString('de-DE',{hour:'2-digit',minute:'2-digit',timeZone:'UTC'}); }
static pctBar(pct, color, size = '') { const w = Math.min(100,Math.max(0,pct||0)); return `<div class="bar ${size}"><div class="bar-fill" style="width:${w}%;background:${color};"></div></div>`; }
static escHtml(s) { return String(s||'').replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>'); }
static inactiveAgo(ts) {
if (!ts) return { text:'?', cls:'ago-d' };
const s=Math.floor(Date.now()/1000-ts), m=Math.floor(s/60), h=Math.floor(s/3600), d=Math.floor(s/86400);
if (s < 3600) return { text:m+'m', cls:'ago-m' };
if (h < 24) return { text:h+'h', cls:'ago-h' };
return { text:d+'d', cls:'ago-d' };
}
static now() { return Math.floor(Date.now()/1000); }
static buildDonut(data, total) {
const r=28,cx=32,cy=32,circ=2*Math.PI*r; let off=0;
const segs = data.map(({count,color}) => {
if (!count||!total) return '';
const dash=(count/total)*circ;
const seg=`<circle cx="${cx}" cy="${cy}" r="${r}" fill="none" stroke="${color}" stroke-width="5" stroke-dasharray="${dash} ${circ-dash}" stroke-dashoffset="${-off}" stroke-linecap="butt"/>`;
off+=dash; return seg;
}).filter(Boolean);
if (!segs.length) segs.push(`<circle cx="${cx}" cy="${cy}" r="${r}" fill="none" stroke="#2a2a30" stroke-width="5"/>`);
return `<svg viewBox="0 0 64 64" xmlns="http://www.w3.org/2000/svg" style="width:52px;height:52px;transform:rotate(-90deg)"><circle cx="${cx}" cy="${cy}" r="${r}" fill="none" stroke="#18181c" stroke-width="5"/>${segs.join('')}</svg>`;
}
}
// ═══════════════════════════════════════════════════════════════
// AUDIO
// ═══════════════════════════════════════════════════════════════
class AudioManager {
constructor(store) { this.store = store; this._ctx = null; }
get ctx() { if (!this._ctx) { try { this._ctx = new (window.AudioContext||window.webkitAudioContext)(); } catch(e){} } return this._ctx; }
play(type) {
if (!this.store.get('sound') && type !== 'enemy') return;
if (!this.ctx) return;
const c=this.ctx, now=c.currentTime, p=this.store.get('soundProfile')||'classic';
const note=(freq,start,dur,vol,wave='sine')=>{const osc=c.createOscillator(),g=c.createGain();osc.connect(g);g.connect(c.destination);osc.type=wave;osc.frequency.value=freq;g.gain.setValueAtTime(0,now+start);g.gain.linearRampToValueAtTime(vol,now+start+0.03);g.gain.exponentialRampToValueAtTime(0.001,now+start+dur);osc.start(now+start);osc.stop(now+start+dur+0.05);};
if (type==='enemy') { [0,.1,.2,.3].forEach(o=>note(880,o,.08,.25,'sawtooth')); note(440,.05,.3,.15,'square'); return; }
if (p==='subtle') { if(type==='travel_warn'){note(440,0,.2,.08);note(550,.25,.2,.08);}else if(type==='travel_crit'){[0,.15,.3].forEach(o=>note(660,o,.12,.1));}else if(type==='full'){note(330,0,.3,.08);}else note(400,0,.25,.06); return; }
if (p==='military') { if(type==='travel_crit'){[0,.12,.24,.36].forEach(o=>note(1400,o,.07,.2,'sawtooth'));}else{note(1000,0,.05,.15,'square');note(800,.08,.05,.1,'square');} return; }
if (type==='travel_warn') { [0,.22].forEach(o=>note(660,o,.2,.25)); }
else if (type==='travel_crit') { [0,.16,.32].forEach(o=>note(880,o,.13,.18,'square')); }
else if (type==='landing') { [0,.15,.3].forEach(o=>note(520,o,.15,.2)); }
else if (type==='full') { const osc=c.createOscillator(),g=c.createGain();osc.connect(g);g.connect(c.destination);osc.type='sine';osc.frequency.setValueAtTime(440,now);osc.frequency.linearRampToValueAtTime(880,now+.4);g.gain.setValueAtTime(0,now);g.gain.linearRampToValueAtTime(.25,now+.08);g.gain.linearRampToValueAtTime(0,now+.5);osc.start(now);osc.stop(now+.55); }
else { note(520,0,.4,.18); }
}
}
// ═══════════════════════════════════════════════════════════════
// STORE / TOAST / API
// ═══════════════════════════════════════════════════════════════
class Store {
get(key, def) { const v = GM_getValue('thud_'+key, undefined); return v !== undefined ? v : def; }
set(key, val) { GM_setValue('thud_'+key, val); return val; }
}
class ToastManager {
constructor() { this._fired = {}; }
show(icon, title, msg, style='', duration=10000) {
let c = document.getElementById('thud-toasts');
if (!c) { c = document.createElement('div'); c.id='thud-toasts'; document.body.appendChild(c); }
const t = document.createElement('div');
t.className = `toast ${style}`;
t.innerHTML = `<div class="toast-icon">${icon}</div><div class="toast-body"><div class="toast-title">${title}</div><div class="toast-msg">${msg}</div></div><button class="toast-x">✕</button>`;
t.querySelector('.toast-x').addEventListener('click', () => { t.classList.add('leaving'); setTimeout(()=>t.remove(),220); });
c.appendChild(t);
setTimeout(() => { if (t.parentNode) { t.classList.add('leaving'); setTimeout(()=>t.remove(),220); } }, duration);
}
showLandingTimer(destName) {
let c = document.getElementById('thud-toasts');
if (!c) { c = document.createElement('div'); c.id='thud-toasts'; document.body.appendChild(c); }
const t = document.createElement('div'); t.className='toast landed';
const topItems = (DEST_TOP_ITEMS[destName]||[]).slice(0,2).join(' · ');
t.innerHTML = `<div class="toast-icon">🛬</div><div class="toast-body"><div class="toast-title">GELANDET: ${Utils.escHtml(destName)}</div><div class="toast-msg">⏱ <span id="thud-land-timer">15</span>s Einkaufszeit${topItems?`<br>🎯 ${topItems}`:''}</div></div><button class="toast-x">✕</button>`;
t.querySelector('.toast-x').addEventListener('click', ()=>{ t.classList.add('leaving'); setTimeout(()=>t.remove(),220); });
c.appendChild(t);
let rem = SHOPPING_TIME_SEC;
const iv = setInterval(()=>{ rem--; const el=t.querySelector('#thud-land-timer'); if(el)el.textContent=rem; if(rem<=0){clearInterval(iv);if(t.parentNode){t.classList.add('leaving');setTimeout(()=>t.remove(),220);}} },1000);
hud.audio.play('landing');
}
fire(key, icon, title, msg, style, audio) { if(this._fired[key])return; this._fired[key]=true; hud.audio.play(audio||'notify'); this.show(icon,title,msg,style); }
reset(key) { delete this._fired[key]; }
}
class TornAPI {
constructor(store) { this.store = store; }
get key() { return this.store.get('apiKey',''); }
call(url, cb) {
if (!this.key) { cb(null,'NO_KEY'); return; }
GM_xmlhttpRequest({ method:'GET', url,
onload: r => { try { const d=JSON.parse(r.responseText); if(d.error)cb(null,d.error.code); else cb(d,null); } catch(e){ cb(null,'PARSE'); } },
onerror: () => cb(null,'NET'),
});
}
v2(path, cb) { this.call(`${API_BASE}/v2/${path}&key=${this.key}`, cb); }
userBasicAndProfile(cb) { this.v2('user?selections=basic,profile', cb); }
userTravel(cb) { this.v2('user?selections=travel', cb); }
userBars(cb) { this.v2('user?selections=bars', cb); }
userCooldowns(cb) { this.v2('user?selections=cooldowns', cb); }
userEducation(cb) { this.v2('user?selections=education', cb); }
userBattleStats(cb) { this.v2('user?selections=battlestats', cb); }
userWorkStats(cb) { this.v2('user?selections=workstats', cb); }
userNetworth(cb) { this.v2('user?selections=networth', cb); }
factionBasicMembers(cb) { this.v2('faction?selections=basic,members', cb); }
factionChain(cb) { this.v2('faction?selections=chain', cb); }
companyProfile(cb) { this.v2('company?selections=profile', cb); }
companyEmployees(cb) { this.v2('company?selections=employees', cb); }
tornBountiesPage(offset, cb) { this.v2(`torn?selections=bounties&offset=${offset}`, cb); }
playerProfile(playerId, cb) { this.call(`${API_BASE}/v2/user/${playerId}?selections=basic,profile&key=${this.key}`, cb); }
}
// ═══════════════════════════════════════════════════════════════
// STATE
// ═══════════════════════════════════════════════════════════════
class StateManager {
constructor(store) {
this.store = store;
this.userBasic=null; this.userProfile=null; this.userTravel=null;
this.bars=null; this.cooldowns=null; this.education=null;
this.battleStats=null; this.workStats=null;
this.faction={}; this.members={}; this.chain=null; this.chainEndTime=null;
this.companyProfile=null; this.companyEmployees=[];
this.networthData=null; this.networthHistory=store.get('nwHistory',[]);
// Bounty
this.bountyData=null; this.bountyAllData=null; this.bountyLastFetch=0;
this.bountySort=store.get('bountySort','reward');
this.bountySearch='';
this.bountyFilterHosp=store.get('bountyFilterHosp',false);
this.bountyLevelMin=store.get('bountyLevelMin',1);
this.bountyLevelMax=store.get('bountyLevelMax',100);
this.bountyRewardMin=store.get('bountyRewardMin',0);
this.bountyRewardMax=store.get('bountyRewardMax',0);
this.bountyDisplayCount=80; this.bountyLoadedPages=0;
this.bountyLoading=false; this.bountyScanningAll=false;
// Enemy
this.enemies=store.get('enemies',[]);
this.enemyData={};
this.enemyFetchRunning=false;
this._enemyTravelAlerted=store.get('_enemyTravelAlerted',{});
// Misc
this.lastUpdate=null; this.uptimeSec=0;
this.showAllOnline=false; this.showAllOffline=false;
this._fsAlertFired=false; this._landingFired=false;
this._lastTravelSec=0; this._prevTravelDest='';
this.energyThresholds=store.get('energyThresholds',[50,75,100]);
this.nerveThresholds=store.get('nerveThresholds',[50,100]);
this._energyAlerted=store.get('_energyAlerted',{});
this._nerveAlerted=store.get('_nerveAlerted',{});
}
setBountySort(v) { this.bountySort=v; this.store.set('bountySort',v); }
setBountyFilterHosp(v) { this.bountyFilterHosp=v; this.store.set('bountyFilterHosp',v); }
setBountyLevelMin(v) { this.bountyLevelMin=Number(v)||1; this.store.set('bountyLevelMin',this.bountyLevelMin); }
setBountyLevelMax(v) { this.bountyLevelMax=Number(v)||100; this.store.set('bountyLevelMax',this.bountyLevelMax); }
setBountyRewardMin(v) { this.bountyRewardMin=Number(v)||0; this.store.set('bountyRewardMin',this.bountyRewardMin); }
setBountyRewardMax(v) { this.bountyRewardMax=Number(v)||0; this.store.set('bountyRewardMax',this.bountyRewardMax); }
addEnemy(id, name, note) { if(this.enemies.find(e=>String(e.id)===String(id)))return false; this.enemies.push({id:String(id),name:name||`#${id}`,note:note||'',addedAt:Utils.now()}); this.store.set('enemies',this.enemies); return true; }
removeEnemy(id) { this.enemies=this.enemies.filter(e=>String(e.id)!==String(id)); delete this.enemyData[id]; this.store.set('enemies',this.enemies); }
updateEnemyNote(id, note) { const e=this.enemies.find(e=>String(e.id)===String(id)); if(e){e.note=note;this.store.set('enemies',this.enemies);} }
updateEnemyData(id, data) { this.enemyData[String(id)]=data; }
get alarms() { return this.store.get('alarms',{}); }
setAlarm(k,v) { const a=this.alarms; a[k]=v; this.store.set('alarms',a); }
isAlarm(k) { const d=ALARM_DEFAULTS[k]; return this.alarms[k]!==undefined?this.alarms[k]:(d?d.enabled:false); }
get visibility() { return this.store.get('visibility',{}); }
isVisible(k) { const v=this.visibility; return v[k]!==undefined?v[k]:(VISIBILITY_DEFAULTS[k]?VISIBILITY_DEFAULTS[k].enabled:true); }
setVisible(k,v) { const vis=this.visibility; vis[k]=v; this.store.set('visibility',vis); }
get travelAlerts() { return this.store.get('travelAlerts',{}); }
isTravelAlert(k) { const t=this.travelAlerts; return t[k]!==undefined?t[k]:true; }
setTravelAlert(k,v) { const t=this.travelAlerts; t[k]=v; this.store.set('travelAlerts',t); }
setEnergyThresholds(arr) { this.energyThresholds=arr; this.store.set('energyThresholds',arr); }
setNerveThresholds(arr) { this.nerveThresholds=arr; this.store.set('nerveThresholds',arr); }
}
const ALARM_DEFAULTS = {
travel_warn: { enabled:true, label:'✈ Travel Warnung', desc:'Alarm wenn Ankunft < 7 Min' },
travel_crit: { enabled:true, label:'✈ Travel Kritisch', desc:'Alarm wenn Ankunft < 2 Min' },
landing_timer: { enabled:true, label:'🛬 Landing Timer', desc:'15s Countdown bei Landung' },
energy_full: { enabled:true, label:'⚡ Energie Voll', desc:'Alarm wenn Energie 100%' },
energy_thresh: { enabled:true, label:'⚡ Energie Schwellwert', desc:'Alarm bei konfigurierten Werten' },
energy_over151: { enabled:false, label:'⚡ Energie über 151', desc:'Alarm wenn Energie > 151 (Happy Jump)' },
nerve_full: { enabled:true, label:'🧠 Nerve Voll', desc:'Alarm wenn Nerve 100%' },
nerve_thresh: { enabled:true, label:'🧠 Nerve Schwellwert', desc:'Alarm bei konfigurierten Werten' },
hp_crit: { enabled:true, label:'❤ HP unter 50%', desc:'Toast-Alarm' },
hospital_free: { enabled:true, label:'🏥 Krankenhaus Frei', desc:'Alarm letzte 60 Sek' },
jail_free: { enabled:true, label:'🚔 Gefängnis Frei', desc:'Alarm letzte 60 Sek' },
drug_ready: { enabled:false, label:'💊 Drogen Bereit', desc:'Alarm wenn Drug-CD weg' },
booster_ready: { enabled:false, label:'🧪 Booster Bereit', desc:'Alarm wenn Booster-CD weg' },
medical_ready: { enabled:false, label:'💉 Medical Bereit', desc:'Alarm wenn Medical-CD weg' },
chain_warn: { enabled:true, label:'⛓ Chain Warnung', desc:'Alarm wenn Chain < 1 Min' },
enemy_travel: { enabled:true, label:'🎯 Enemy reist ab', desc:'Alarm wenn Enemy-Target abreist' },
};
const VISIBILITY_DEFAULTS = {
show_happy: { enabled:true, label:'😊 Happy Bar' },
show_jail: { enabled:true, label:'🚔 Gefängnis' },
show_hospital: { enabled:true, label:'🏥 Krankenhaus' },
show_drug_cd: { enabled:true, label:'💊 Drug CD' },
show_booster_cd: { enabled:true, label:'🧪 Booster CD' },
show_medical_cd: { enabled:true, label:'💉 Medical CD' },
show_education: { enabled:true, label:'📚 Lehrgang' },
show_tip: { enabled:true, label:'💡 Smart Tipp' },
show_quick_links: { enabled:true, label:'🔗 Quick Links' },
show_tct_time: { enabled:true, label:'🕒 TCT Zeit' },
show_battle_stats:{ enabled:true, label:'⚔ Battle Stats (Ich)' },
};
const TABS_DEF = [
{ id:'personal', icon:'👤', label:'Ich' },
{ id:'faction', icon:'⚔', label:'Fraktion' },
{ id:'bounty', icon:'🎯', label:'Bounty' },
{ id:'enemy', icon:'💀', label:'Enemy' },
{ id:'company', icon:'🏢', label:'Firma' },
{ id:'networth', icon:'💰', label:'Vermögen' },
{ id:'notifications', icon:'🔔', label:'Alarme' },
{ id:'settings', icon:'⚙', label:'Settings' },
];
// ═══════════════════════════════════════════════════════════════
// ALARM CHECKER
// ═══════════════════════════════════════════════════════════════
class AlarmChecker {
constructor(state, toast) { this.state=state; this.toast=toast; }
run() {
const s=this.state, now=Utils.now(), t=this.toast;
if (!s.userBasic && !s.userProfile) return;
const travel=s.userTravel?.travel||{};
const tSec=Math.max(0,Number(travel.time_left)||0), dest=travel.destination||'';
// Landing timer
if (s._lastTravelSec>5 && tSec<=5 && s._prevTravelDest && s._prevTravelDest!=='Torn' && s._prevTravelDest!=='Torn City') {
if (!s._landingFired && s.isAlarm('landing_timer')) { s._landingFired=true; t.showLandingTimer(s._prevTravelDest); }
}
if (tSec>5) { s._landingFired=false; if(dest) s._prevTravelDest=dest; }
s._lastTravelSec=tSec;
const flying=tSec>0;
if (!flying) { t.reset('travel_warn'); t.reset('travel_crit'); s._fsAlertFired=false; }
else {
if (tSec>TRAVEL_WARN_SEC) t.reset('travel_warn');
if (tSec>TRAVEL_CRIT_SEC) t.reset('travel_crit');
if (s.isAlarm('travel_warn') && tSec<=TRAVEL_WARN_SEC && tSec>TRAVEL_CRIT_SEC && s.isTravelAlert('toast'))
t.fire('travel_warn','✈','ANKUNFT BALD',`${dest} — noch ${Utils.fmtHM(tSec)}`,'travel','travel_warn');
if (s.isAlarm('travel_crit') && tSec<=TRAVEL_CRIT_SEC) {
if (s.isTravelAlert('toast')) t.fire('travel_crit','⚠','KRITISCH – KURZ VOR ANKUNFT',`${dest} landet in ${Utils.fmtTravel(tSec)}!`,'crit','travel_crit');
if (s.isTravelAlert('fullscreen') && !s._fsAlertFired) {
s._fsAlertFired=true;
const fs=document.getElementById('thud-fs-alert');
if (fs) { document.getElementById('thud-fs-msg').innerHTML=`${dest} — Ankunft in ca. 1 Minute!`; fs.classList.add('active'); setTimeout(()=>fs.classList.remove('active'),12000); }
}
}
}
const energy=s.bars?.energy;
if (energy) {
const pct=Math.round((energy.current/energy.maximum)*100);
if (pct<100) t.reset('energy_full');
if (s.isAlarm('energy_full') && pct>=100 && energy.current<=HAPPY_JUMP_THRESHOLD) t.fire('energy_full','⚡','ENERGIE VOLL!',`${energy.current}/${energy.maximum} — Gym oder Chains!`,'warn','full');
if (energy.current>HAPPY_JUMP_THRESHOLD) { if(s.isAlarm('energy_over151')) t.fire('energy_over151','⚡',`ENERGIE ÜBER ${HAPPY_JUMP_THRESHOLD}!`,`${energy.current} Energie — Happy Jump Zone!`,'crit','notify'); }
else { t.reset('energy_over151'); }
if (s.isAlarm('energy_thresh')) {
for (const thresh of (s.energyThresholds||[])) {
if (thresh>=100) continue;
if (pct>=thresh && !s._energyAlerted[thresh]) { s._energyAlerted[thresh]=true; t.show('⚡',`ENERGIE ${thresh}%`,`${energy.current}/${energy.maximum} Energie erreicht!`,'warn',8000); hud.audio.play('notify'); }
else if (pct<thresh-10) { delete s._energyAlerted[thresh]; }
}
}
}
const nerve=s.bars?.nerve;
if (nerve) {
const pct=Math.round((nerve.current/nerve.maximum)*100);
if (pct<100) t.reset('nerve_full');
if (s.isAlarm('nerve_full') && pct>=100) t.fire('nerve_full','🧠','NERVE VOLL!',`${nerve.current}/${nerve.maximum} — Crimes abarbeiten!`,'warn','full');
if (s.isAlarm('nerve_thresh')) {
for (const thresh of (s.nerveThresholds||[])) {
if (thresh>=100) continue;
if (pct>=thresh && !s._nerveAlerted[thresh]) { s._nerveAlerted[thresh]=true; t.show('🧠',`NERVE ${thresh}%`,`${nerve.current}/${nerve.maximum} Nerve erreicht!`,'info',8000); hud.audio.play('notify'); }
else if (pct<thresh-10) { delete s._nerveAlerted[thresh]; }
}
}
}
const life=s.bars?.life||s.userProfile?.life||{};
if (life.maximum) { const p=(life.current/life.maximum)*100; if(p>=50)t.reset('hp_crit'); if(s.isAlarm('hp_crit')&&p<50)t.fire('hp_crit','❤','HP KRITISCH!',`Nur noch ${Math.round(p)}% HP! Medical nutzen!`,'crit','notify'); }
const statusObj=s.userProfile?.status||{};
const hospState=(statusObj.state||'').toLowerCase();
const hospUntil=Number(statusObj.until??0);
const hospSec=hospState==='hospital'&&hospUntil>now?hospUntil-now:0;
if (hospSec>60) t.reset('hospital_free');
if (s.isAlarm('hospital_free')&&hospSec>0&&hospSec<=60) t.fire('hospital_free','🏥','KRANKENHAUS FREI','Du wirst gleich entlassen!','','notify');
const jailUntil=(statusObj.state||'').toLowerCase()==='jail'?Number(statusObj.until??0):0;
const jailSec=jailUntil>now?jailUntil-now:0;
if (jailSec>60) t.reset('jail_free');
if (s.isAlarm('jail_free')&&jailSec>0&&jailSec<=60) t.fire('jail_free','🚔','GEFÄNGNIS FREI','Du wirst gleich entlassen!','crit','notify');
const cd=s.cooldowns?.cooldowns||{};
const _cdVal=(v)=>Number(typeof v==='object'?(v?.cooldown_until??0):(v??0));
const drugSec=Math.max(0,_cdVal(cd.drug)-now), boostSec=Math.max(0,_cdVal(cd.booster)-now), medSec=Math.max(0,_cdVal(cd.medical)-now);
if (drugSec>120) t.reset('drug_ready');
if (boostSec>120) t.reset('booster_ready');
if (medSec>120) t.reset('medical_ready');
if (s.isAlarm('drug_ready') && _cdVal(cd.drug)>0 && drugSec<=0) t.fire('drug_ready', '💊','DROGEN BEREIT', 'Drug-Cooldown abgelaufen!', 'info','notify');
if (s.isAlarm('booster_ready') && _cdVal(cd.booster)>0 && boostSec<=0) t.fire('booster_ready','🧪','BOOSTER BEREIT', 'Booster-Cooldown abgelaufen!','info','notify');
if (s.isAlarm('medical_ready') && _cdVal(cd.medical)>0 && medSec<=0) t.fire('medical_ready','💉','MEDICAL BEREIT', 'Cooldown abgelaufen!', 'info','notify');
if (s.isAlarm('enemy_travel')) {
for (const enemy of s.enemies) {
const ed=s.enemyData[enemy.id]; if(!ed) continue;
const eStat=(ed.status?.state||'').toLowerCase(), eDest=ed.status?.description||'';
const travelKey=`${enemy.id}_${eDest}`;
if (eStat==='traveling' && !s._enemyTravelAlerted[travelKey]) {
s._enemyTravelAlerted[travelKey]=true; hud.store.set('_enemyTravelAlerted',s._enemyTravelAlerted);
hud.audio.play('enemy'); t.show('⚔',`⚠ ENEMY REIST AB!`,`${enemy.name} fliegt: ${eDest}`,'enemy-alert',20000);
}
if (eStat!=='traveling') { Object.keys(s._enemyTravelAlerted).forEach(k=>{ if(k.startsWith(enemy.id+'_'))delete s._enemyTravelAlerted[k]; }); }
}
}
}
}
// ═══════════════════════════════════════════════════════════════
// CHAIN MANAGER
// ═══════════════════════════════════════════════════════════════
class ChainManager {
constructor(state) { this.state=state; this._tick=null; }
process(chain) {
const s=this.state;
if (chain&&chain.current>0) { s.chain=chain; s.chainEndTime=Date.now()+Number(chain.timeout)*1000; this.startTick(); }
else { s.chain=null; s.chainEndTime=null; this.stopTick(); }
this.renderSection();
if (hud.store.get('minimized',false)) hud.renderer.renderMini();
}
startTick() { if(!this._tick) this._tick=setInterval(()=>this.tick(),1000); }
stopTick() { if(this._tick){clearInterval(this._tick);this._tick=null;} }
tick() {
const s=this.state; if(!s.chainEndTime){this.stopTick();return;}
const rem=Math.max(0,s.chainEndTime-Date.now()), m=Math.floor(rem/60000), warn=rem<=CHAIN_WARN_SEC*1000;
const el=document.getElementById('thud-chain-timer');
if(el){el.textContent=`${m}m`;warn?el.classList.add('warn'):el.classList.remove('warn');}
if(warn&&s.isAlarm('chain_warn'))hud.toasts.fire('chain_warn','⛓','CHAIN LÄUFT AB!',`Nur noch ${m}m!`,'crit','travel_crit');
if(!warn)hud.toasts.reset('chain_warn');
if(rem===0){s.chain=null;s.chainEndTime=null;this.stopTick();this.renderSection();}
}
renderSection() {
const el=document.getElementById('thud-chain-block'); if(!el)return;
const c=this.state.chain;
if(!c||c.current===0){el.className='chain-block';el.innerHTML=`<div class="chain-idle"><div class="pulse-dot" style="opacity:.2;animation:none;background:var(--muted)"></div><span style="letter-spacing:.5px;font-size:10px;font-weight:600;margin-left:8px;">Keine aktive Chain</span></div>`;return;}
const ms=[10,25,50,100,250,500,1000,2500,5000,10000,25000,50000,100000];
const nM=ms.find(x=>x>c.current)||c.current, pM=ms[ms.indexOf(nM)-1]||0;
const prog=((c.current-pM)/(nM-pM))*100;
const mt=[[10,1],[25,1.5],[50,2],[100,2.5],[250,3],[500,3.5],[1000,4]];
let mul=1; for(const[thresh,v]of mt)if(c.current>=thresh)mul=v;
const rem=Math.max(0,this.state.chainEndTime-Date.now()), m=Math.floor(rem/60000), warn=rem<=CHAIN_WARN_SEC*1000;
const isWU=c.current<CHAIN_START_HIT, hits=isWU?c.current:c.current-CHAIN_START_HIT;
el.className='chain-block active';
el.innerHTML=`<div class="chain-active-row"><div class="pulse-dot"></div><span style="font-size:9px;font-weight:700;letter-spacing:1px;color:var(--chain);margin-left:5px;">${isWU?'WARMUP':'CHAIN'}</span><div class="chain-hits" style="margin-left:7px;">${hits.toLocaleString()}</div><div class="chain-hits-unit" style="margin-left:2px;">hits</div><div class="chain-divider" style="margin:0 10px;"></div><div style="display:flex;flex-direction:column;align-items:center;"><div class="chain-timer ${warn?'warn':''}" id="thud-chain-timer">${m}m</div><div style="font-size:7px;color:var(--muted);text-transform:uppercase;margin-top:1px;">timeout</div></div><div style="display:flex;flex-direction:column;align-items:center;margin-left:10px;"><div class="chain-mult">×${mul}</div><div style="font-size:7px;color:var(--muted);text-transform:uppercase;margin-top:1px;">mult</div></div></div><div class="chain-prog"><div class="chain-prog-fill" style="width:${Math.max(1,Math.min(100,prog))}%"></div></div>`;
}
}
// ═══════════════════════════════════════════════════════════════
// FACTION HELPER
// ═══════════════════════════════════════════════════════════════
class FactionHelper {
static getStatus(member) {
const state=(typeof member.status==='object'?member.status?.state:member.status||'').toLowerCase();
const lastStatus=(member.last_action?.status||'').toLowerCase();
if(state.includes('hospital'))return'hospital';
if(state.includes('travel')||state.includes('abroad'))return'traveling';
if(state.includes('jail'))return'jail';
if(lastStatus==='idle')return'idle';
if(lastStatus==='online')return'online';
const a=Date.now()/1000-(member.last_action?.timestamp||0);
if(a<120)return'online'; if(a<600)return'idle'; return'offline';
}
static statusOrder(s) { return{online:0,traveling:1,idle:2,hospital:3,jail:4,offline:5}[s]??6; }
static statusLabel(s) { return{online:'Online',idle:'AFK',traveling:'Travel',hospital:'Hosp.',jail:'Jail',offline:'Offline'}[s]||s; }
static parseTravelRoute(member) {
const s=typeof member.status==='object'?member.status:{};
const desc=(s.description||s.state||'').trim();
const toM=desc.match(/to\s+([A-Za-z\s]+?)(?:\s*(?:from|$)|\.|,)/i);
const frM=desc.match(/from\s+([A-Za-z\s]+?)(?:\s*(?:to|$)|\.|,)/i);
let to='Abroad',fr='Torn City';
const norm=(str)=>{const l=str.trim().replace(/[.,;:!?]+$/,''); return l.toLowerCase().includes('torn')?'Torn City':l.length>2?l:'Abroad';};
if(toM?.[1]) to=norm(toM[1]);
if(frM?.[1]) fr=frM[1].trim().toLowerCase().includes('torn')?'Torn City':frM[1].trim();
return{from:fr,to,dir:to==='Torn City'&&fr!=='Torn City'?'home':'abroad'};
}
}
// ═══════════════════════════════════════════════════════════════
// DATA FETCHER
// ═══════════════════════════════════════════════════════════════
class DataFetcher {
constructor(api, state, toast, renderer) { this.api=api; this.state=state; this.toast=toast; this.renderer=renderer; }
_opt(fetchFn, cb) { fetchFn((d,err)=>{ if(err===7||err===16)return; if(d)cb(d); }); }
_afterPersonal() { setTimeout(()=>{ this.state.lastUpdate=new Date(); hud.alarmChecker.run(); if(hud.activeTab==='personal')this.renderer.render(); },600); }
fetchPersonal() {
if (!this.api.key) { this.renderer.render(); return; }
this.api.userBasicAndProfile(d=>{ if(d&&(d.profile||d.basic)){this.state.userBasic=d.profile||d.basic||d;this.state.userProfile=d.profile||d;this._afterPersonal();} });
this.api.userTravel(d=>{ if(d)this.state.userTravel=d; });
this._opt(cb=>this.api.userBars(cb), d=>{ this.state.bars=d?.bars||d||null; });
this._opt(cb=>this.api.userCooldowns(cb), d=>{ this.state.cooldowns=d||null; });
this._opt(cb=>this.api.userEducation(cb), d=>{
const edu=d?.education||d||null;
if(edu&&typeof edu==='object'){const until=edu.current?.until??edu.until??edu.ends??0;this.state.education={until,raw:edu};}
else this.state.education=edu;
});
this.api.userBattleStats((d,err)=>{
if(err||!d)return;
const _bv=(v)=>{if(!v&&v!==0)return 0;if(typeof v==='number')return v;if(typeof v==='object')return Number(v.modifier_value??v.value??v.current??0);return Number(v)||0;};
let raw=null;
if(d.battlestats&&typeof d.battlestats==='object')raw=d.battlestats;
else if(d.battle_stats&&typeof d.battle_stats==='object')raw=d.battle_stats;
else if('strength'in d||'defense'in d)raw=d;
if(raw)this.state.battleStats={strength:_bv(raw.strength),defense:_bv(raw.defense),speed:_bv(raw.speed),dexterity:_bv(raw.dexterity)};
});
this._opt(cb=>this.api.userWorkStats(cb), d=>{
if(d)this.state.workStats={manual:d.manual_labor??d.workstats?.manual_labor??0,intelligence:d.intelligence??d.workstats?.intelligence??0,endurance:d.endurance??d.workstats?.endurance??0};
});
}
fetchFaction() {
if(!this.api.key)return;
this.api.factionBasicMembers(data=>{
if(!data)return;
const members=data.members||[];
if(Array.isArray(members)){this.state.members={};members.forEach(m=>{this.state.members[m.id]=m;});}
else this.state.members=members;
this.state.faction={...this.state.faction,...(data.faction||data)};
const el=document.getElementById('thud-faction-name');
const n=this.state.faction.name||this.state.faction.faction?.name;
if(el&&n)el.textContent=`${SCRIPT_TITLE} — ${n.toUpperCase()}`;
if(hud.activeTab==='faction')this.renderer.render();
if(hud.store.get('minimized',false))this.renderer.renderMini();
});
}
fetchChain() { if(!this.api.key)return; this.api.factionChain(data=>{ if(data)hud.chainManager.process(data.chain||null); }); }
fetchCompany() {
if(!this.api.key)return;
this.api.companyProfile((data,err)=>{ if(err===7||err===16)return; if(data)this.state.companyProfile=data.profile||data.company||data; });
this.api.companyEmployees((data,err)=>{
if(err===7||err===16)return;
if(data){const empl=data.employees||[];this.state.companyEmployees=Array.isArray(empl)?empl:Object.values(empl);}
if(hud.activeTab==='company')this.renderer.render();
});
}
fetchNetworth() {
if(!this.api.key)return;
this.api.userNetworth((data,err)=>{
if(err===7||err===16||!data)return;
const nw=data.networth||null; if(!nw)return;
const total=nw.total||0, now=Utils.now(), hist=this.state.networthHistory;
hist.push({ts:now,val:total}); if(hist.length>120)hist.shift();
this.state.networthHistory=hist; hud.store.set('nwHistory',hist);
this.state.networthData=nw;
if(hud.activeTab==='networth')this.renderer.render();
});
}
fetchBountiesPage(offset, onDone) { this.api.tornBountiesPage(offset,data=>{ const list=data?.bounties; const arr=Array.isArray(list)?list:(list?Object.values(list):[]); onDone(arr); }); }
fetchBountiesInitial(onDone) {
const s=this.state; s.bountyLoading=true; s.bountyAllData=[]; s.bountyLoadedPages=0;
this.fetchBountiesPage(0,arr=>{ s.bountyAllData=this._dedup(arr); s.bountyLoadedPages=1; s.bountyData=s.bountyAllData; s.bountyLastFetch=Utils.now(); s.bountyLoading=false; onDone(s.bountyAllData); });
}
fetchBountiesNextPage(onDone) {
const s=this.state; if(s.bountyLoading)return; s.bountyLoading=true;
this.fetchBountiesPage(s.bountyLoadedPages*100,arr=>{ s.bountyLoading=false; if(!arr.length){onDone(false);return;} s.bountyAllData=this._dedup([...(s.bountyAllData||[]),...arr]); s.bountyLoadedPages++; s.bountyData=s.bountyAllData; onDone(true); });
}
fetchBountiesAllPages(onProgress, onDone) {
const s=this.state; if(s.bountyScanningAll)return; s.bountyScanningAll=true; s.bountyLoading=true; s.bountyAllData=[]; s.bountyLoadedPages=0;
const MAX=20;
const go=(offset)=>{
if(s.bountyLoadedPages>=MAX){s.bountyScanningAll=false;s.bountyLoading=false;s.bountyData=s.bountyAllData;onDone(s.bountyAllData);return;}
this.fetchBountiesPage(offset,arr=>{ if(!arr.length){s.bountyScanningAll=false;s.bountyLoading=false;s.bountyData=s.bountyAllData;onDone(s.bountyAllData);return;} s.bountyAllData=this._dedup([...s.bountyAllData,...arr]);s.bountyLoadedPages++;s.bountyLastFetch=Utils.now();if(onProgress)onProgress(s.bountyAllData.length,s.bountyLoadedPages);go(s.bountyLoadedPages*100); });
};
go(0);
}
_dedup(arr) { const seen=new Map(); for(const b of arr){const id=b.target_id??b.id??Math.random();if(!seen.has(id))seen.set(id,b);} return Array.from(seen.values()); }
fetchEnemyData() {
const s=this.state;
if(s.enemyFetchRunning||!s.enemies.length||!this.api.key)return;
s.enemyFetchRunning=true;
let pending=s.enemies.length;
const done=()=>{ pending--; if(pending<=0){s.enemyFetchRunning=false;if(hud.activeTab==='enemy')hud.renderer.render();} };
s.enemies.forEach(enemy=>{
this.api.playerProfile(enemy.id,(data,err)=>{
if(data){
const basic=data.basic||{}, profile=data.profile||{};
const merged=Object.assign({},data,basic,profile);
if(!merged.age&&merged.registration_timestamp) merged.age=Math.floor((Utils.now()-merged.registration_timestamp)/86400);
if(!merged.age&&profile.age) merged.age=profile.age;
if(!merged.age&&basic.age) merged.age=basic.age;
const fac=merged.faction||profile.faction||basic.faction||null;
if(fac){const facId=Number(fac.faction_id??fac.id??0);merged._factionId=facId>0?facId:null;merged._factionName=facId>0?(fac.faction_name??fac.name??''):null;}
else{merged._factionId=null;merged._factionName=null;}
// Fetch open bounty on this enemy from bounty data
merged._bountyReward=s._getEnemyBountyReward(enemy.id);
s.updateEnemyData(enemy.id,merged);
const liveName=merged.name??merged.player_name;
if(liveName&&enemy.name!==liveName){enemy.name=liveName;hud.store.set('enemies',s.enemies);}
}
done();
});
});
}
fetchAll() { this.fetchPersonal(); this.fetchFaction(); this.fetchChain(); this.fetchCompany(); this.fetchNetworth(); this.fetchEnemyData(); }
}
// Helper: get bounty reward for enemy id from loaded bounty data
StateManager.prototype._getEnemyBountyReward = function(id) {
const data = this.bountyData || this.bountyAllData || [];
for (const b of data) {
const tid = String(b.target_id ?? b.id ?? '');
if (tid === String(id)) return Number(b.reward ?? b.amount ?? 0);
}
return 0;
};
// ═══════════════════════════════════════════════════════════════
// RENDERER
// ═══════════════════════════════════════════════════════════════
class Renderer {
constructor(state) { this.state=state; }
get body() { return document.getElementById('thud-body'); }
render() {
switch(hud.activeTab){
case 'personal': return this.renderPersonal();
case 'faction': return this.renderFaction();
case 'bounty': return this.renderBounty();
case 'enemy': return this.renderEnemy();
case 'company': return this.renderCompany();
case 'networth': return this.renderNetworth();
case 'notifications': return this.renderNotifications();
case 'settings': return this.renderSettings();
default: return this.renderPersonal();
}
}
renderMini() {
const b=document.getElementById('thud-mini'); if(!b)return;
const m=Object.values(this.state.members);
const online=m.filter(x=>FactionHelper.getStatus(x)!=='offline').length;
const c=this.state.chain, energy=this.state.bars?.energy, nerve=this.state.bars?.nerve;
const tSec=Math.max(0,Number(this.state.userTravel?.travel?.time_left)||0);
const tDest=this.state.userTravel?.travel?.destination||'';
let chips='';
chips+=`<div class="thud-mini-chip"><span style="width:5px;height:5px;border-radius:50%;background:var(--online);box-shadow:0 0 4px var(--online);display:inline-block;"></span><span class="thud-mini-val">${online}</span><span class="thud-mini-lbl">/${m.length}</span></div>`;
if(energy){const ep=Math.round((energy.current/energy.maximum)*100);const ec=ep>=100?'var(--red)':ep>=75?'var(--amber)':'var(--blue)';chips+=`<div class="thud-mini-chip"><span style="font-size:9px;">⚡</span><span class="thud-mini-val" style="color:${ec};">${energy.current}</span></div>`;}
if(nerve){const np=Math.round((nerve.current/nerve.maximum)*100);const nc=np>=100?'var(--red)':'var(--dim)';chips+=`<div class="thud-mini-chip"><span style="font-size:9px;">🧠</span><span class="thud-mini-val" style="color:${nc};">${nerve.current}</span></div>`;}
if(c&&c.current>0){const rem=Math.max(0,this.state.chainEndTime-Date.now());const mc=Math.floor(rem/60000);const warn=rem<=CHAIN_WARN_SEC*1000;chips+=`<div class="thud-mini-chip"><span style="font-size:9px;">⛓</span><span class="thud-mini-val" style="color:${warn?'var(--red)':'var(--chain)'};">${c.current}</span><span class="thud-mini-lbl">${mc}m</span></div>`;}
if(tSec>0){const dInfo=DESTINATIONS[tDest]||{flag:'✈'};const tw=tSec<=TRAVEL_WARN_SEC;chips+=`<div class="thud-mini-chip"><span style="font-size:11px;">${dInfo.flag}</span><span class="thud-mini-val" style="color:${tw?'var(--amber)':'var(--travel)'};">${Utils.fmtHM(tSec)}</span></div>`;}
b.innerHTML=`<div class="thud-mini-bar">${chips}</div><div class="thud-mini-tabs"><div class="thud-mini-tab" data-mini-tab="personal">👤 Ich</div><div class="thud-mini-tab" data-mini-tab="faction">⚔ Frak</div><div class="thud-mini-tab" data-mini-tab="bounty">🎯 BNT</div><div class="thud-mini-tab" data-mini-tab="enemy">💀 ENM</div><div class="thud-mini-tab" data-mini-tab="networth">💰 NW</div></div>`;
b.querySelectorAll('[data-mini-tab]').forEach(btn=>btn.addEventListener('click',()=>{hud.store.set('minimized',false);document.getElementById('thud-overlay')?.classList.remove('minimized');hud.switchTab(btn.dataset.miniTab);}));
}
// ─── PERSONAL TAB ───
renderPersonal() {
const body=this.body; if(!body)return;
const s=this.state;
if(!s.userBasic&&!s.userProfile){body.innerHTML=!hud.store.get('apiKey')?this._noKeyState():this._loadingState();return;}
const now=Utils.now(), basic=s.userBasic||{}, profile=s.userProfile||{};
const travel=s.userTravel?.travel||{};
const life=s.bars?.life||profile.life||basic.life||{};
const hpC=Number(life.current??0), hpM=Number(life.maximum??0), hp=hpM>0?(hpC/hpM)*100:0;
// Hospital fix: v2 status.state + status.until
const statusObj=profile.status||basic.status||{};
const statusState=typeof statusObj==='string'?statusObj:(statusObj.state||'Okay');
const hospState=statusState.toLowerCase()==='hospital', jailState=statusState.toLowerCase()==='jail';
const hospUntil=hospState?Number(statusObj.until??0):0, jailUntil=jailState?Number(statusObj.until??0):0;
const hospSec=hospUntil>now?hospUntil-now:0, jailSec=jailUntil>now?jailUntil-now:0;
const energy=s.bars?.energy, nerve=s.bars?.nerve, happy=s.bars?.happy;
const ePct=energy?(energy.current/energy.maximum)*100:0;
const nPct=nerve?(nerve.current/nerve.maximum)*100:0;
const haPct=happy?(happy.current/happy.maximum)*100:0;
const tSec=Math.max(0,Number(travel.time_left)||0), flying=tSec>0;
const destInfo=DESTINATIONS[travel.destination]||{flag:'✈'};
const tDest=travel.destination||'';
let tvCls='home',tvBadge='🏠 TORN CITY – BEREIT',tvBadgeColor='var(--green)';
if(flying){if(tSec<=TRAVEL_CRIT_SEC){tvCls='crit';tvBadge='⚠ KURZ VOR ANKUNFT';tvBadgeColor='var(--red)';}else if(tSec<=TRAVEL_WARN_SEC){tvCls='warn';tvBadge='✈ ANKUNFT BALD';tvBadgeColor='var(--amber)';}else{tvCls='ok';tvBadge='✈ UNTERWEGS';tvBadgeColor='var(--travel)';}}
const tClockColor=tSec<=TRAVEL_CRIT_SEC?'var(--red)':tSec<=TRAVEL_WARN_SEC?'var(--amber)':'var(--travel)';
const tProgCls=tSec<=TRAVEL_CRIT_SEC?'crit':tSec<=TRAVEL_WARN_SEC?'warn':'';
const hpColor=hp<50?'var(--red)':hp<70?'var(--amber)':'var(--green)';
const eColor=ePct>=100?'var(--red)':ePct>=75?'var(--amber)':'var(--travel)';
const nColor=nPct>=100?'var(--red)':nPct>=75?'var(--amber)':'#9333ea';
const eMin=energy?.fulltime?Math.max(0,Math.ceil((energy.fulltime-now)/60)):null;
const nMin=nerve?.fulltime?Math.max(0,Math.ceil((nerve.fulltime-now)/60)):null;
const cd=s.cooldowns?.cooldowns||{};
const _cd=(v)=>Number(typeof v==='object'?(v?.cooldown_until??0):(v??0));
const drugSec=Math.max(0,_cd(cd.drug)-now), medSec=Math.max(0,_cd(cd.medical)-now), boostSec=Math.max(0,_cd(cd.booster)-now);
const edu=s.education, eduLeft=edu?Math.max(0,Number(edu.until??0)-now):0;
// Cash only (bank removed)
const nwObj=s.networthData||{};
const wallet=Number(nwObj.wallet??nwObj.cash??0);
const stocks=Number(nwObj.stockmarket??nwObj.stocks??0);
const totalMoney=wallet+stocks;
const level=basic.level||profile.level||'?', name=basic.name||profile.name||'—';
const tip=this._smartTip(hp,energy,nerve,tSec,hospSec,jailSec);
const smartTippHtml=(tip&&s.isVisible('show_tip'))?`<div class="smart-tipp"><span class="smart-tipp-icon">${tip.icon}</span><span class="smart-tipp-text">${tip.text}</span></div>`:'';
const DEFAULT_QL=[{label:'Stock\nMarket',icon:'📈',url:'https://www.torn.com/page.php?sid=stocks'},{label:'Casino',icon:'🎰',url:'https://www.torn.com/page.php?sid=slots'},{label:'Bazaar',icon:'🏪',url:'https://www.torn.com/bazaar.php?userId=0'},{label:'Forum',icon:'💬',url:'https://www.torn.com/forums.php'}];
const quickLinks=hud.store.get('customQuickLinks',null)||DEFAULT_QL;
const qlHtml=s.isVisible('show_quick_links')?`<div><div class="section-header">🔗 QUICK LINKS</div><div class="ql-grid">${quickLinks.map(({label,icon,url})=>`<a class="ql-card" href="${url}" target="_blank"><span class="ql-icon">${icon}</span><span class="ql-name">${label.replace('\n','<br>')}</span></a>`).join('')}</div></div>`:'';
const bs=s.battleStats;
const bsT=bs?(Number(bs.strength||0)+Number(bs.defense||0)+Number(bs.speed||0)+Number(bs.dexterity||0)):0;
const bsHtml=(s.isVisible('show_battle_stats')&&bs)?`<div class="card"><div class="card-header"><span class="card-title red">⚔ BATTLE STATS</span><span class="badge badge-amber">${Utils.fmtStat(bsT)} total</span></div><div class="card-body"><div class="stat-grid stat-grid-4"><div class="stat-card"><div class="sv sv-red">${Utils.fmtStat(bs.strength||0)}</div><div class="sl">STR</div></div><div class="stat-card"><div class="sv sv-blue">${Utils.fmtStat(bs.defense||0)}</div><div class="sl">DEF</div></div><div class="stat-card"><div class="sv sv-green">${Utils.fmtStat(bs.speed||0)}</div><div class="sl">SPD</div></div><div class="stat-card"><div class="sv sv-amber">${Utils.fmtStat(bs.dexterity||0)}</div><div class="sl">DEX</div></div></div></div></div>`:'';
const tctHtml=s.isVisible('show_tct_time')?`<span style="color:var(--accent2);font-family:var(--mono);">TCT ${Utils.tctTime()}</span>`:'';
body.innerHTML=`
${smartTippHtml}
<div style="display:flex;align-items:center;justify-content:space-between;padding:2px 2px 0;">
<div><div style="font-size:14px;font-weight:700;color:var(--text);">${Utils.escHtml(name)}</div><div style="font-size:10px;color:var(--muted);margin-top:2px;">Level ${level} · ${Utils.escHtml(statusState)}</div></div>
<div style="text-align:right;"><div style="font-family:var(--mono);font-size:15px;font-weight:700;color:var(--amber);">${Utils.fmtMoney(totalMoney)}</div><div style="font-size:9px;color:var(--muted);margin-top:1px;">💵 ${Utils.fmtMoney(wallet)}${stocks?` · 📈 ${Utils.fmtMoney(stocks)}`:''}</div></div>
</div>
<div class="travel-card ${tvCls}">
<div class="travel-card-top" style="color:${tvBadgeColor};"><span>${tvBadge}</span>${flying?`<span style="font-size:18px;">${destInfo.flag}</span>`:''}</div>
<div class="travel-card-body">
${!flying?`<div style="display:flex;align-items:center;gap:12px;"><span style="font-size:24px;opacity:.2;">🏠</span><div><div style="font-size:14px;font-weight:700;color:var(--green);">TORN CITY</div><div style="font-size:9px;color:var(--muted);text-transform:uppercase;letter-spacing:1.5px;margin-top:2px;">BEREIT ZUM ABFLUG</div></div></div>`
:`<div style="display:flex;align-items:center;gap:10px;margin-bottom:10px;"><span style="font-size:22px;">${destInfo.flag}</span><div class="travel-dest" style="color:${tClockColor};">${Utils.escHtml(tDest||'Unbekannt')}</div></div><div style="display:flex;justify-content:space-between;align-items:flex-end;"><div class="travel-clock" style="color:${tClockColor};">${Utils.fmtHM(tSec)}</div><div style="text-align:right;"><div style="font-size:9px;color:var(--muted);text-transform:uppercase;letter-spacing:1px;">ANKUNFT</div><div style="font-family:var(--mono);font-size:15px;font-weight:700;color:var(--dim);margin-top:1px;">${Utils.arrTime(tSec)}</div></div></div><div class="travel-prog"><div class="travel-prog-fill ${tProgCls}" style="width:0%;"></div></div>`}
</div>
</div>
<div class="card">
<div class="card-header"><span class="card-title accent">◈ BARS & STATUS</span><span class="badge badge-${statusState==='Okay'?'green':statusState==='Hospital'?'red':statusState==='Jail'?'amber':'muted'}">${Utils.escHtml(statusState)}</span></div>
<div class="card-body">
<div class="stat-grid stat-grid-3" style="margin-bottom:12px;">
<div class="stat-card"><div class="sv ${hp>=70?'sv-green':hp>=50?'sv-amber':'sv-red'}">${hpM>0?Math.round(hp)+'%':'—'}</div><div class="sl">HP</div></div>
<div class="stat-card"><div class="sv ${ePct>=100?'sv-red':ePct>=75?'sv-amber':'sv-blue'}">${energy?energy.current:'—'}</div><div class="sl">Energie</div></div>
<div class="stat-card"><div class="sv ${nPct>=100?'sv-red':nPct>=75?'sv-amber':'sv-blue'}">${nerve?nerve.current:'—'}</div><div class="sl">Nerve</div></div>
</div>
<div class="bar-block"><div class="bar-block-head"><span class="bar-block-lbl">❤ HP</span><span class="bar-block-val" style="color:${hpColor}">${hpC} / ${hpM||'?'}</span></div>${Utils.pctBar(hp,hpColor,'bar-lg')}</div>
<div class="bar-block"><div class="bar-block-head"><span class="bar-block-lbl">⚡ Energie</span><span class="bar-block-val" style="color:${eColor}">${energy?`${energy.current} / ${energy.maximum}`:'—'}</span></div>${Utils.pctBar(ePct,eColor)}${eMin!==null&&eMin>0?`<div class="bar-block-sub">Voll in ${eMin} Min</div>`:''}</div>
<div class="bar-block"><div class="bar-block-head"><span class="bar-block-lbl">🧠 Nerve</span><span class="bar-block-val" style="color:${nColor}">${nerve?`${nerve.current} / ${nerve.maximum}`:'—'}</span></div>${Utils.pctBar(nPct,nColor)}${nMin!==null&&nMin>0?`<div class="bar-block-sub">Voll in ${nMin} Min</div>`:''}</div>
${happy&&s.isVisible('show_happy')?`<div class="bar-block"><div class="bar-block-head"><span class="bar-block-lbl">😊 Happy</span><span class="bar-block-val" style="color:var(--amber)">${happy.current} / ${happy.maximum}</span></div>${Utils.pctBar(haPct,'var(--amber)')}</div>`:''}
</div>
</div>
${bsHtml}
${qlHtml}
<div class="card">
<div class="card-header"><span class="card-title">⏱ COOLDOWNS & STATUS</span></div>
<div class="card-body">
${s.isVisible('show_hospital')?`<div class="row"><span class="row-label">🏥 Krankenhaus</span><span class="row-value" style="color:${hospSec>0?'var(--red)':'var(--green)'}">${hospSec>0?Utils.fmtSec(hospSec):'FREI'}</span></div>`:''}
${s.isVisible('show_jail')?`<div class="row"><span class="row-label">🚔 Gefängnis</span><span class="row-value" style="color:${jailSec>0?'var(--amber)':'var(--green)'}">${jailSec>0?Utils.fmtSec(jailSec):'FREI'}</span></div>`:''}
${s.isVisible('show_drug_cd')?`<div class="row"><span class="row-label">💊 Drogen CD</span><span class="row-value" style="color:${drugSec>0?'var(--amber)':'var(--green)'}">${drugSec>0?Utils.fmtSec(drugSec):'BEREIT'}</span></div>`:''}
${s.isVisible('show_medical_cd')?`<div class="row"><span class="row-label">💉 Medical CD</span><span class="row-value" style="color:${medSec>0?'var(--amber)':'var(--green)'}">${medSec>0?Utils.fmtSec(medSec):'BEREIT'}</span></div>`:''}
${s.isVisible('show_booster_cd')?`<div class="row"><span class="row-label">🧪 Booster CD</span><span class="row-value" style="color:${boostSec>0?'var(--amber)':'var(--green)'}">${boostSec>0?Utils.fmtSec(boostSec):'BEREIT'}</span></div>`:''}
${s.isVisible('show_education')&&edu?(eduLeft>0?`<div class="row"><span class="row-label">📚 Kurs</span><span class="row-value">${Utils.fmtDhm(eduLeft)}</span></div>`:`<div class="row"><span class="row-label">📚 Lehrgang</span><span class="row-value" style="color:var(--green);">ABGESCHLOSSEN ✓</span></div>`):''}
</div>
</div>
<div style="display:flex;justify-content:space-between;align-items:center;padding:2px 0;flex-shrink:0;" class="ts">
<span style="display:flex;align-items:center;gap:5px;"><div class="pip"></div>${s.lastUpdate?.toLocaleTimeString('de-DE')||'—'}</span>
${tctHtml}
<span>⬆ ${this._uptimeStr()}</span>
</div>`;
}
_smartTip(hp, energy, nerve, tSec, hospSec, jailSec) {
if(jailSec>0)return{icon:'🚔',text:`Du sitzt noch ${Utils.fmtSec(jailSec)} im Knast.`};
if(hospSec>0&&hospSec<300)return{icon:'🏥',text:`Nur noch ${Utils.fmtSec(hospSec)} im Krankenhaus.`};
if(hp<50&&hp>0)return{icon:'❤',text:`HP sehr niedrig (${Math.round(hp)}%). Medical Items nutzen!`};
if(energy?.current>=energy?.maximum)return{icon:'⚡',text:'Energie voll! Chains oder Gym nutzen!'};
if(nerve?.current>=nerve?.maximum)return{icon:'🧠',text:'Nerve voll! Crimes abarbeiten!'};
if(tSec>0&&tSec<300)return{icon:'✈',text:`Fast da! Ankunft in ${Utils.fmtHM(tSec)}.`};
if(energy&&(energy.current/energy.maximum)>0.85)return{icon:'💡',text:'Energie fast voll – nächste Chain planen?'};
return null;
}
_uptimeStr() { const s=this.state.uptimeSec,h=Math.floor(s/3600),m=Math.floor((s%3600)/60); return h>0?`${h}h ${m}m`:`${m}m`; }
// ─── FACTION TAB ───
renderFaction() {
const body=this.body; if(!body)return;
const m=Object.values(this.state.members), t=m.length;
const cap=this.state.faction.max_members||this.state.faction.capacity||t||50;
const groups={online:[],idle:[],traveling:[],hospital:[],jail:[],offline:[]};
m.forEach(x=>groups[FactionHelper.getStatus(x)].push(x));
const active=[...groups.online,...groups.traveling,...groups.idle,...groups.hospital,...groups.jail];
const sa=active.sort((a,b)=>FactionHelper.statusOrder(FactionHelper.getStatus(a))-FactionHelper.statusOrder(FactionHelper.getStatus(b)));
const so=[...groups.offline].sort((a,b)=>(a.last_action?.timestamp||0)-(b.last_action?.timestamp||0));
const sl=20,ol=INACTIVE_LIMIT;
const sOn=this.state.showAllOnline?sa:sa.slice(0,sl);
const sOf=this.state.showAllOffline?so:so.slice(0,ol);
const eOn=sa.length-sl,eOf=so.length-ol;
const rd=groups.online.length+groups.idle.length,oc=active.length;
const donutSVG=Utils.buildDonut([{count:groups.online.length,color:'var(--green)'},{count:groups.idle.length,color:'var(--amber)'},{count:groups.traveling.length,color:'var(--travel)'},{count:groups.hospital.length,color:'var(--red)'},{count:groups.jail.length,color:'#f97316'},{count:groups.offline.length,color:'var(--bg-el)'}],t);
body.innerHTML=`
<div class="chain-block" id="thud-chain-block"><div class="chain-idle"><div class="pulse-dot" style="opacity:.2;animation:none;background:var(--muted);"></div><span style="font-size:10px;font-weight:600;margin-left:7px;letter-spacing:.5px;">Keine aktive Chain</span></div></div>
<div class="card"><div class="card-header"><div style="display:flex;align-items:center;gap:8px;"><span class="card-title green">● ONLINE</span><span class="badge badge-green">${oc}</span></div><div style="position:relative;display:flex;align-items:center;justify-content:center;">${donutSVG}<div style="position:absolute;text-align:center;pointer-events:none;"><div style="font-size:13px;font-weight:700;color:var(--text);">${oc}</div><div style="font-size:8px;color:var(--muted);">/${cap}</div></div></div></div>
<div class="card-body"><div class="member-grid">${sOn.map(x=>{const st=FactionHelper.getStatus(x);return`<div class="member-row"><span class="status-dot sd-${st}"></span><span class="member-name">${Utils.escHtml(x.name)}</span><span class="member-status">${FactionHelper.statusLabel(st)}</span></div>`;}).join('')}</div>${eOn>0&&!this.state.showAllOnline?`<span class="toggle-more" id="thud-more-online">▾ ${eOn} mehr</span>`:this.state.showAllOnline&&sa.length>sl?`<span class="toggle-more" id="thud-less-online">▴ Weniger</span>`:''}<div class="status-summary"><div class="stat-chip"><div class="sn" style="color:var(--green);">${groups.online.length}</div><div class="sl">Online</div></div><div class="stat-chip"><div class="sn" style="color:var(--amber);">${groups.idle.length}</div><div class="sl">AFK</div></div><div class="stat-chip"><div class="sn" style="color:var(--travel);">${groups.traveling.length}</div><div class="sl">Travel</div></div><div class="stat-chip"><div class="sn" style="color:var(--red);">${groups.hospital.length}</div><div class="sl">Hosp.</div></div><div class="stat-chip"><div class="sn" style="color:#f97316;">${groups.jail.length}</div><div class="sl">Jail</div></div><div class="stat-chip"><div class="sn" style="color:var(--muted);">${groups.offline.length}</div><div class="sl">Offline</div></div></div></div></div>
<div class="card"><div class="card-header"><span class="card-title red">⚔ WAR READY</span></div><div class="card-body"><div class="war-grid"><div class="war-cell"><div class="wv" style="color:var(--green);">${rd}</div><div class="wl">Ready</div></div><div class="war-cell"><div class="wv" style="color:var(--red);">${groups.hospital.length}</div><div class="wl">Hosp.</div></div><div class="war-cell"><div class="wv" style="color:var(--travel);">${groups.traveling.length}</div><div class="wl">Travel</div></div><div class="war-cell"><div class="wv" style="color:var(--amber);">${t>0?Math.round(rd/t*100):0}%</div><div class="wl">Bereit</div></div></div></div></div>
${groups.traveling.length>0?`<div class="card"><div class="card-header"><span class="card-title blue">✈ UNTERWEGS (${groups.traveling.length})</span></div><div class="card-body p0">${groups.traveling.map(x=>{const{from,to,dir}=FactionHelper.parseTravelRoute(x);const fC=from.toLowerCase().includes('torn')?'var(--green)':'var(--travel)';const tC=to.toLowerCase().includes('torn')?'var(--green)':'var(--travel)';return`<div style="display:flex;align-items:center;gap:10px;padding:9px 14px;border-bottom:1px solid rgba(255,255,255,.04);"><span style="font-size:18px;">${dir==='home'?'🏠':'✈'}</span><div style="flex:1;min-width:0;"><div style="font-size:12px;font-weight:600;color:var(--text);">${Utils.escHtml(x.name)}</div><div style="font-size:10px;margin-top:2px;display:flex;align-items:center;gap:5px;"><span style="color:${fC};">${Utils.escHtml(from)}</span><span style="color:var(--muted);">→</span><span style="color:${tC};">${Utils.escHtml(to)}</span></div></div></div>`;}).join('')}</div></div>`:''}
<div class="card"><div class="card-header"><span class="card-title">💤 OFFLINE (${groups.offline.length})</span></div><div class="card-body p0">${sOf.map(x=>{const a=Utils.inactiveAgo(x.last_action?.timestamp||0);return`<div class="offline-row"><span class="status-dot sd-offline"></span><span class="offline-name">${Utils.escHtml(x.name)}</span><span class="offline-ago ${a.cls}">${a.text}</span></div>`;}).join('')}${eOf>0&&!this.state.showAllOffline?`<span class="toggle-more" style="margin:7px 14px 12px;" id="thud-more-offline">▾ ${eOf} weitere</span>`:this.state.showAllOffline&&so.length>ol?`<span class="toggle-more" style="margin:7px 14px 12px;" id="thud-less-offline">▴ Weniger</span>`:''}</div></div>`;
hud.chainManager.renderSection();
if(this.state.chain?.current>0)hud.chainManager.startTick();
document.getElementById('thud-more-online')?.addEventListener('click',()=>{this.state.showAllOnline=true;this.renderFaction();});
document.getElementById('thud-less-online')?.addEventListener('click',()=>{this.state.showAllOnline=false;this.renderFaction();});
document.getElementById('thud-more-offline')?.addEventListener('click',()=>{this.state.showAllOffline=true;this.renderFaction();});
document.getElementById('thud-less-offline')?.addEventListener('click',()=>{this.state.showAllOffline=false;this.renderFaction();});
}
// ─── BOUNTY TAB ───
renderBounty() {
const body=this.body; if(!body)return;
const s=this.state, data=s.bountyData, sort=s.bountySort, now=Utils.now();
const doScan=()=>{ hud.fetcher.fetchBountiesInitial(()=>{s.bountySearch='';s.bountyDisplayCount=80;this.renderBounty();}); };
if(!data){
body.innerHTML=`<div class="card" style="flex-shrink:0;"><div class="card-header"><span class="card-title red">🎯 BOUNTY RADAR</span></div></div><div class="empty-state"><div class="es-icon">🎯</div><div class="es-text">Keine Bounties geladen</div><div class="es-sub">Klick "Laden" um die Torn Bounty-Liste zu holen</div></div><button class="scan-btn" id="thud-bounty-scan" style="align-self:center;">🔍 Bounty-Liste laden</button>`;
document.getElementById('thud-bounty-scan')?.addEventListener('click',doScan); return;
}
const lvMin=Number(s.bountyLevelMin)||1, lvMax=Number(s.bountyLevelMax)||100;
const rwMin=Number(s.bountyRewardMin)||0, rwMax=Number(s.bountyRewardMax)||0;
const q=(s.bountySearch||'').toLowerCase().trim();
let filtered=data.slice();
if(q)filtered=filtered.filter(b=>{const name=(b.target_name??b.name??'').toLowerCase(),reason=(b.reason??'').toLowerCase();return name.includes(q)||reason.includes(q);});
filtered=filtered.filter(b=>{const lvl=Number(b.target_level??b.level??0);return lvl>=lvMin&&lvl<=lvMax;});
if(rwMin>0||rwMax>0){filtered=filtered.filter(b=>{const r=Number(b.reward??b.amount??0);if(rwMin>0&&r<rwMin)return false;if(rwMax>0&&r>rwMax)return false;return true;});}
if(s.bountyFilterHosp)filtered=filtered.filter(b=>!this._isHospital(b));
if(sort==='reward')filtered.sort((a,b)=>(b.reward??b.amount??0)-(a.reward??a.amount??0));
if(sort==='level') filtered.sort((a,b)=>(a.target_level??a.level??0)-(b.target_level??b.level??0));
if(sort==='time') filtered.sort((a,b)=>(a.valid_until??a.expires??0)-(b.valid_until??b.expires??0));
if(!s.bountyFilterHosp)filtered.sort((a,b)=>(this._isHospital(a)?0:1)-(this._isHospital(b)?0:1));
const displayCount=s.bountyDisplayCount||80, visible=filtered.slice(0,displayCount), hasMoreVisible=filtered.length>displayCount;
const hospCount=data.filter(b=>this._isHospital(b)).length, isFiltered=filtered.length!==data.length;
const searchHadFocus=document.activeElement?.id==='thud-bounty-search';
const searchCursorPos=searchHadFocus?(document.activeElement.selectionStart??0):0;
const rwMinD=rwMin>0?(rwMin>=1e6?(rwMin/1e6).toFixed(1)+'M':rwMin>=1e3?(rwMin/1e3).toFixed(0)+'K':rwMin):'';
const rwMaxD=rwMax>0?(rwMax>=1e6?(rwMax/1e6).toFixed(1)+'M':rwMax>=1e3?(rwMax/1e3).toFixed(0)+'K':rwMax):'';
body.innerHTML=`
<div class="card" style="flex-shrink:0;">
<div class="card-header" style="flex-wrap:wrap;gap:6px;">
<span class="card-title red">🎯 BOUNTY RADAR</span>
<div style="display:flex;align-items:center;gap:5px;flex-wrap:wrap;">
<span class="badge badge-red">${data.length} total</span>
${hospCount>0?`<span class="badge badge-red">🏥 ${hospCount}</span>`:''}
${isFiltered?`<span class="badge badge-amber">${filtered.length} gefiltert</span>`:''}
${!s.bountyLoading?`<button class="bounty-load-more-inline" id="thud-bounty-more">▾ +Mehr (S.${s.bountyLoadedPages})</button>`:`<span class="bounty-load-more-inline" style="opacity:.5;cursor:default;"><span class="spinning">↺</span>${s.bountyScanningAll?' Scan...':''}</span>`}
<button class="scan-btn" id="thud-bounty-scan-all" style="padding:4px 10px;font-size:10px;background:rgba(245,158,11,.15);border-color:rgba(245,158,11,.3);color:var(--amber);" ${s.bountyLoading?'disabled':''} title="Alle Seiten laden">⚡ Alle</button>
<button class="scan-btn" id="thud-bounty-scan" style="padding:4px 10px;font-size:10px;" ${s.bountyLoading?'disabled':''}>${s.bountyLoading?'<span class="spinning">↺</span>':'↺'}</button>
</div>
</div>
<div class="card-body" style="padding:9px 14px 10px;gap:7px;">
<input type="text" class="bounty-search" id="thud-bounty-search" placeholder="🔍 Name oder Grund..." value="${Utils.escHtml(s.bountySearch||'')}">
<div class="level-filter-row">
<span style="font-size:10px;color:var(--muted);font-weight:700;">LV:</span>
<input type="number" class="level-input" id="thud-lvl-min" min="1" max="100" value="${lvMin}">
<span style="color:var(--muted);font-size:10px;">–</span>
<input type="number" class="level-input" id="thud-lvl-max" min="1" max="100" value="${lvMax}">
<button class="sort-btn" id="thud-lvl-apply">✓</button>
</div>
<div class="level-filter-row">
<span style="font-size:10px;color:var(--amber);font-weight:700;">💰:</span>
<input type="text" class="reward-input" id="thud-rw-min" placeholder="Min" value="${rwMinD}">
<span style="color:var(--muted);font-size:10px;">–</span>
<input type="text" class="reward-input" id="thud-rw-max" placeholder="Max" value="${rwMaxD}">
<button class="sort-btn" id="thud-rw-apply" style="border-color:rgba(245,158,11,.3);color:var(--amber);">✓</button>
${(rwMin>0||rwMax>0)?`<button class="sort-btn" id="thud-rw-clear" style="color:var(--muted);">✕</button>`:''}
</div>
<div class="sort-bar">
<span style="font-size:9px;color:var(--muted);font-weight:700;">SORT:</span>
<button class="sort-btn ${sort==='reward'?'active':''}" data-sort="reward">💰 Reward</button>
<button class="sort-btn ${sort==='level'?'active':''}" data-sort="level">⚔ Level</button>
<button class="sort-btn ${sort==='time'?'active':''}" data-sort="time">⏱ Zeit</button>
</div>
<div class="sort-bar">
<span style="font-size:9px;color:var(--muted);font-weight:700;">FILTER:</span>
<button class="sort-btn ${s.bountyFilterHosp?'active':''}" data-filter="hosp">🏥 ${s.bountyFilterHosp?'Hosp. ausgebl.':'Hosp. anzeigen'}</button>
</div>
</div>
</div>
<div class="card" style="flex-shrink:0;"><div class="card-body p0" id="thud-bounty-list">${this._buildBountyRows(visible,now)}</div></div>
<div class="ts" style="text-align:center;flex-shrink:0;">${data.length} Bounties · Seite ${s.bountyLoadedPages} · ${s.bountyLastFetch?new Date(s.bountyLastFetch*1000).toLocaleTimeString('de-DE'):'—'}</div>`;
if(searchHadFocus){const inp=document.getElementById('thud-bounty-search');if(inp){inp.focus();try{inp.setSelectionRange(searchCursorPos,searchCursorPos);}catch(e){}}}
document.getElementById('thud-bounty-scan')?.addEventListener('click',doScan);
document.getElementById('thud-bounty-scan-all')?.addEventListener('click',()=>{if(s.bountyScanningAll||s.bountyLoading)return;hud.fetcher.fetchBountiesAllPages(()=>this.renderBounty(),(all)=>{s.bountySearch='';s.bountyDisplayCount=80;hud.toasts.show('✅','Alle Bounties geladen',`${all.length} Bounties`,'info',5000);this.renderBounty();});this.renderBounty();});
document.getElementById('thud-bounty-search')?.addEventListener('input',e=>{s.bountySearch=e.target.value;s.bountyDisplayCount=80;this._renderBountyListOnly(s,now);});
document.getElementById('thud-lvl-apply')?.addEventListener('click',()=>{s.setBountyLevelMin(Number(document.getElementById('thud-lvl-min')?.value)||1);s.setBountyLevelMax(Number(document.getElementById('thud-lvl-max')?.value)||100);s.bountyDisplayCount=80;this.renderBounty();});
const parseR=(str)=>{if(!str||!str.trim())return 0;str=str.trim().toUpperCase().replace(/\s/g,'').replace(',','.');const m=str.match(/^([\d.]+)([KMB]?)$/);if(!m)return 0;return Math.round(parseFloat(m[1])*({K:1e3,M:1e6,B:1e9,'':1}[m[2]]||1));};
document.getElementById('thud-rw-apply')?.addEventListener('click',()=>{s.setBountyRewardMin(parseR(document.getElementById('thud-rw-min')?.value));s.setBountyRewardMax(parseR(document.getElementById('thud-rw-max')?.value));s.bountyDisplayCount=80;this.renderBounty();});
document.getElementById('thud-rw-clear')?.addEventListener('click',()=>{s.setBountyRewardMin(0);s.setBountyRewardMax(0);s.bountyDisplayCount=80;this.renderBounty();});
body.querySelectorAll('[data-sort]').forEach(btn=>btn.addEventListener('click',()=>{s.setBountySort(btn.dataset.sort);s.bountyDisplayCount=80;this.renderBounty();}));
body.querySelectorAll('[data-filter]').forEach(btn=>btn.addEventListener('click',()=>{if(btn.dataset.filter==='hosp')s.setBountyFilterHosp(!s.bountyFilterHosp);s.bountyDisplayCount=80;this.renderBounty();}));
document.getElementById('thud-bounty-more')?.addEventListener('click',()=>{if(hasMoreVisible){s.bountyDisplayCount+=80;this._renderBountyListOnly(s,now);}else{hud.fetcher.fetchBountiesNextPage(has=>{if(!has)hud.toasts.show('ℹ','Alle geladen',`${s.bountyData.length} Bounties`,'info',4000);else s.bountyDisplayCount+=80;this.renderBounty();});}});
body.querySelectorAll('[data-bounty-add-enemy]').forEach(btn=>btn.addEventListener('click',()=>{const id=btn.dataset.bountyAddEnemy,name=btn.dataset.bountyName||`#${id}`;if(!s.enemies.find(e=>String(e.id)===String(id))){s.addEnemy(String(id),name,'');hud.toasts.show('💀','Enemy hinzugefügt',`${name} aus Bounty-Liste`,'crit',3000);hud.fetcher.fetchEnemyData();}else{hud.toasts.show('⚠','Bereits in Enemy-Liste',name,'warn',2000);}}));
}
_isHospital(b) {
const so=(typeof b.target?.status==='object'&&b.target.status)||(typeof b.status==='object'&&b.status)||{};
const raw=so.state??so.description??b.target_state??b.state??'';
return raw.toLowerCase().includes('hospital');
}
_buildBountyRows(visible, now) {
if(!visible||!visible.length)return`<div class="empty-state"><div class="es-icon">🔍</div><div class="es-text">Keine Ergebnisse</div><div class="es-sub">Filter anpassen oder Bounties neu laden</div></div>`;
return visible.map(b=>{
const tid=b.target_id??b.id, tname=b.target_name??b.name??`#${tid}`;
const lvl=b.target_level??b.level??0, reward=b.reward??b.amount??0;
const validUntil=b.valid_until??b.expires??0, timeLeft=validUntil>0?Math.max(0,validUntil-now):0;
const reason=b.reason?Utils.escHtml(b.reason.substring(0,80)):'';
const so=(typeof b.target?.status==='object'&&b.target.status)||(typeof b.status==='object'&&b.status)||{};
const rawState=so.state??so.description??b.target_state??b.state??'';
const ss=rawState.toLowerCase(), isH=ss.includes('hospital');
const hospUntil=Number(so.until??so.hospital_timestamp??0);
const hospLeft=isH&&hospUntil>now?hospUntil-now:0;
let statusTag='';
if(isH)statusTag=`<span class="bounty-tag tag-hosp">🏥 HOSPITAL${hospLeft>0?` · ${Utils.fmtSec(hospLeft)}`:''}</span>`;
else if(ss.includes('travel')||ss.includes('abroad'))statusTag=`<span class="bounty-tag tag-travel">✈ TRAVEL</span>`;
else if(ss.includes('jail'))statusTag=`<span class="bounty-tag tag-jail">🚔 JAIL</span>`;
else if(ss&&ss!=='okay')statusTag=`<span class="bounty-tag tag-ok">✓ ${Utils.escHtml(rawState)}</span>`;
let diff,dCls;
if(lvl<=15){diff='EASY';dCls='diff-easy';}else if(lvl<=50){diff='MID';dCls='diff-medium';}else{diff='HARD';dCls='diff-hard';}
return`<div class="bounty-row${isH?' is-hospital':''}">
<div style="flex:1;min-width:0;">
<div style="display:flex;align-items:center;gap:5px;flex-wrap:wrap;">
<a href="https://www.torn.com/profiles.php?XID=${tid}" target="_blank" style="color:var(--text);text-decoration:none;font-weight:700;font-size:13px;">${Utils.escHtml(tname)}</a>
<span class="${dCls} bounty-difficulty">${diff}</span>
<span style="font-size:10px;font-family:var(--mono);color:var(--muted);">Lv ${lvl}</span>
</div>
<div class="bounty-info-row">${statusTag}</div>
${reason?`<div style="font-size:10px;color:var(--muted);margin-top:2px;font-style:italic;">"${reason}"</div>`:''}
${timeLeft>0?`<div style="font-size:9px;color:var(--amber);font-family:var(--mono);margin-top:1px;">⏱ ${Utils.fmtSec(timeLeft)}</div>`:''}
</div>
<div style="display:flex;flex-direction:column;align-items:flex-end;gap:4px;flex-shrink:0;">
<span class="bounty-reward">${Utils.fmtMoney(reward)}</span>
<a class="bounty-link" href="https://www.torn.com/page.php?sid=attack&user2ID=${tid}" target="_blank">⚔ Angriff</a>
<button class="bounty-link" style="background:rgba(239,68,68,.12);border:1px solid rgba(239,68,68,.25);color:var(--red);cursor:pointer;font-family:var(--font);" data-bounty-add-enemy="${tid}" data-bounty-name="${Utils.escHtml(tname)}">💀 Enemy</button>
</div>
</div>`;
}).join('');
}
_renderBountyListOnly(s, now) {
const listEl=document.getElementById('thud-bounty-list'); if(!listEl){this.renderBounty();return;}
const q=(s.bountySearch||'').toLowerCase().trim();
const lvMin=Number(s.bountyLevelMin)||1,lvMax=Number(s.bountyLevelMax)||100;
const rwMin=Number(s.bountyRewardMin)||0,rwMax=Number(s.bountyRewardMax)||0;
let filtered=(s.bountyData||[]).slice();
if(q)filtered=filtered.filter(b=>{const n=(b.target_name??b.name??'').toLowerCase(),r=(b.reason??'').toLowerCase();return n.includes(q)||r.includes(q);});
filtered=filtered.filter(b=>{const l=Number(b.target_level??b.level??0);return l>=lvMin&&l<=lvMax;});
if(rwMin>0||rwMax>0)filtered=filtered.filter(b=>{const r=Number(b.reward??b.amount??0);if(rwMin>0&&r<rwMin)return false;if(rwMax>0&&r>rwMax)return false;return true;});
if(s.bountyFilterHosp)filtered=filtered.filter(b=>!this._isHospital(b));
const sort=s.bountySort;
if(sort==='reward')filtered.sort((a,b)=>(b.reward??b.amount??0)-(a.reward??a.amount??0));
if(sort==='level') filtered.sort((a,b)=>(a.target_level??a.level??0)-(b.target_level??b.level??0));
if(sort==='time') filtered.sort((a,b)=>(a.valid_until??a.expires??0)-(b.valid_until??b.expires??0));
if(!s.bountyFilterHosp)filtered.sort((a,b)=>(this._isHospital(a)?0:1)-(this._isHospital(b)?0:1));
listEl.innerHTML=this._buildBountyRows(filtered.slice(0,s.bountyDisplayCount||80),now);
}
// ─── ENEMY TAB ───
renderEnemy() {
const body=this.body; if(!body)return;
const s=this.state, now=Utils.now(), enemies=s.enemies||[];
const addEnemy=(id,note)=>{
if(!id||isNaN(parseInt(id))){hud.toasts.show('⚠','Ungültige ID','Bitte gültige Spieler-ID eingeben','warn',3000);return;}
hud.fetcher.api.playerProfile(id,(data)=>{
const p=data?.profile??data;
const name=p?.name??p?.player_name??`#${id}`;
if(s.addEnemy(String(id),name,note)){hud.toasts.show('💀',`Enemy hinzugefügt`,`${name} (ID: ${id})`,'crit',4000);hud.fetcher.fetchEnemyData();this.renderEnemy();}
else hud.toasts.show('⚠','Bereits vorhanden',`ID ${id} ist bereits in der Liste`,'warn',3000);
});
};
const renderRow=(enemy)=>{
const ed=s.enemyData[enemy.id]||{};
const statusObj=ed.status||{}, ss=(statusObj.state||'').toLowerCase();
const isH=ss==='hospital', isTr=ss==='traveling'||ss==='abroad', isJ=ss==='jail';
const hospUntil=isH?Number(statusObj.until??0):0, hospSec=hospUntil>now?hospUntil-now:0;
const level=ed.level??'?';
// Age in TAGEN (kein y/m Format)
const ageDays=Number(ed.age??0);
const facId=ed._factionId??null, facName=ed._factionName??null;
const jobCo=ed.job?.company_name??ed.job?.name??null, jobPos=ed.job?.position??null;
const travelDesc=statusObj.description||'';
const lastTs=ed.last_action?.timestamp??0;
const isOnline=(ed.last_action?.status||'').toLowerCase()==='online'||(lastTs>0&&(now-lastTs)<120);
const lastAgo=lastTs>0?Utils.inactiveAgo(lastTs):null;
// Bounty reward on this enemy
const bountyR=s._getEnemyBountyReward(enemy.id);
let statusTag='',rowClass='enemy-row';
if(isH){statusTag=`<span class="enemy-tag etag-hosp">🏥 HOSPITAL${hospSec>0?` · ${Utils.fmtSec(hospSec)}`:''}</span>`;rowClass+=' in-hospital';}
else if(isTr){statusTag=`<span class="enemy-tag etag-travel">✈ ${Utils.escHtml(travelDesc)||'Reist'}</span>`;rowClass+=' traveling';}
else if(isJ)statusTag=`<span class="enemy-tag etag-jail">🚔 JAIL</span>`;
else if(ss)statusTag=`<span class="enemy-tag etag-ok">✓ OKAY</span>`;
const lastHtml=lastAgo?`<span class="enemy-tag" style="background:rgba(255,255,255,.04);color:${isOnline?'var(--green)':'var(--muted)'};border:1px solid rgba(255,255,255,.08);">⏱ ${isOnline?'Online':lastAgo.text+' ago'}</span>`:'';
const facTag=facId?`<span class="enemy-tag" style="background:rgba(255,255,255,.05);color:var(--muted);border:1px solid rgba(255,255,255,.08);">⚔ ${Utils.escHtml(facName||'Faction')}</span>`:
`<span class="enemy-tag" style="background:rgba(16,185,129,.08);color:var(--green);border:1px solid rgba(16,185,129,.2);">✓ No Faction</span>`;
// Age: "Age 1569" in grün, gleicher Style wie Lv
const ageHtml=ageDays>0?`<span class="enemy-level" style="color:var(--green);">Age ${ageDays}</span>`:'';
// Bounty: zeige wenn vorhanden
const bountyHtml=bountyR>0?`<span class="enemy-level" style="color:var(--amber);">🎯 ${Utils.fmtMoney(bountyR)}</span>`:'';
return`<div class="${rowClass}" data-enemy-id="${enemy.id}">
<div class="enemy-top">
<a class="enemy-name-link" href="https://www.torn.com/profiles.php?XID=${enemy.id}" target="_blank">${Utils.escHtml(enemy.name)}</a>
<span class="enemy-level">Lv ${level}</span>
${ageHtml}
${bountyHtml}
</div>
<div class="enemy-meta">
${statusTag}${lastHtml}${facTag}
${jobCo?`<span class="enemy-tag etag-job">🏢 ${Utils.escHtml(jobCo)}${jobPos?` · ${Utils.escHtml(jobPos)}`:''}</span>`:''}
</div>
${enemy.note?`<div class="enemy-note">📝 ${Utils.escHtml(enemy.note)}</div>`:''}
<div class="enemy-actions">
<a class="enemy-action-btn eab-attack" href="https://www.torn.com/page.php?sid=attack&user2ID=${enemy.id}" target="_blank">⚔ Angriff</a>
<a class="enemy-action-btn eab-profile" href="https://www.torn.com/profiles.php?XID=${enemy.id}" target="_blank">👤 Profil</a>
<button class="enemy-action-btn eab-note" data-note-enemy="${enemy.id}">📝 Notiz</button>
<button class="enemy-action-btn eab-remove" data-remove-enemy="${enemy.id}">✕ Entfernen</button>
</div>
</div>`;
};
body.innerHTML=`
<div class="card" style="flex-shrink:0;">
<div class="card-header"><span class="card-title red">💀 ENEMY TRACKER</span><div style="display:flex;gap:5px;align-items:center;"><span class="badge badge-red">${enemies.length} Enemies</span><button class="scan-btn" id="thud-enemy-refresh" style="padding:4px 10px;font-size:10px;">${s.enemyFetchRunning?'<span class="spinning">↺</span>':'↺ Refresh'}</button></div></div>
<div class="card-body" style="padding:10px 14px;gap:7px;"><div style="font-size:10px;color:var(--muted);margin-bottom:2px;">Spieler-ID eingeben:</div><div class="enemy-add-row"><input type="number" class="enemy-id-input" id="thud-enemy-id" placeholder="Spieler ID (z.B. 123456)" min="1"><button class="scan-btn" id="thud-enemy-add" style="white-space:nowrap;">+ Hinzufügen</button></div></div>
</div>
${enemies.length===0?`<div class="empty-state"><div class="es-icon">💀</div><div class="es-text">Keine Enemies</div><div class="es-sub">Füge Spieler-IDs hinzu um sie zu tracken</div></div>`:`<div class="card" style="flex-shrink:0;"><div class="card-header"><span class="card-title amber">⚠ ENEMY LIST (${enemies.length})</span><span style="font-size:9px;color:var(--muted);">Auto-Refresh 30s</span></div><div class="card-body p0">${enemies.map(e=>renderRow(e)).join('')}</div></div>`}
<div style="background:rgba(239,68,68,.06);border:1px solid rgba(239,68,68,.15);border-radius:var(--r);padding:9px 12px;font-size:10px;color:var(--muted);flex-shrink:0;">⚠ Enemy-Reise-Alarm → <b style="color:var(--text);">Alarme → Enemy reist ab</b></div>`;
document.getElementById('thud-enemy-refresh')?.addEventListener('click',()=>{hud.fetcher.fetchEnemyData();this.renderEnemy();});
document.getElementById('thud-enemy-add')?.addEventListener('click',()=>{const id=document.getElementById('thud-enemy-id')?.value?.trim();addEnemy(id,'');});
document.getElementById('thud-enemy-id')?.addEventListener('keydown',e=>{if(e.key==='Enter'){addEnemy(e.target.value?.trim(),'');}});
body.querySelectorAll('[data-remove-enemy]').forEach(btn=>btn.addEventListener('click',()=>{const id=btn.dataset.removeEnemy;const en=s.enemies.find(e=>String(e.id)===String(id));if(confirm(`Enemy "${en?.name||id}" wirklich entfernen?`)){s.removeEnemy(id);this.renderEnemy();}}));
body.querySelectorAll('[data-note-enemy]').forEach(btn=>btn.addEventListener('click',()=>{const id=btn.dataset.noteEnemy;const en=s.enemies.find(e=>String(e.id)===String(id));const modal=document.createElement('div');modal.className='thud-modal-bg';modal.innerHTML=`<div class="thud-modal"><h3>📝 Notiz für ${Utils.escHtml(en?.name||id)}</h3><p>Füge eine persönliche Notiz hinzu:</p><textarea id="thud-note-input" placeholder="Deine Notiz...">${Utils.escHtml(en?.note||'')}</textarea><div class="modal-btns"><button class="modal-cancel" id="thud-note-cancel">Abbrechen</button><button class="modal-save" id="thud-note-save">Speichern</button></div></div>`;document.body.appendChild(modal);document.getElementById('thud-note-save').onclick=()=>{s.updateEnemyNote(id,document.getElementById('thud-note-input').value);modal.remove();this.renderEnemy();};document.getElementById('thud-note-cancel').onclick=()=>modal.remove();modal.addEventListener('click',e=>{if(e.target===modal)modal.remove();});}));
}
// ─── COMPANY TAB ───
renderCompany() {
const body=this.body; if(!body)return;
const cp=this.state.companyProfile, employees=this.state.companyEmployees||[], ws=this.state.workStats;
const wsHtml=ws?`<div class="card" style="flex-shrink:0;"><div class="card-header"><span class="card-title blue">💼 DEINE WORKING STATS</span></div><div class="card-body"><div class="stat-grid stat-grid-3"><div class="stat-card"><div class="sv sv-amber">${Utils.fmtStat(ws.manual)}</div><div class="sl">🛠 Manual</div></div><div class="stat-card"><div class="sv sv-blue">${Utils.fmtStat(ws.intelligence)}</div><div class="sl">🧠 Intel</div></div><div class="stat-card"><div class="sv sv-green">${Utils.fmtStat(ws.endurance)}</div><div class="sl">🏃 Endure</div></div></div></div></div>`:'';
if(!cp){body.innerHTML=`${wsHtml}<div class="empty-state"><div class="es-icon">🏢</div><div class="es-text">Keine Firmendaten</div><div class="es-sub">Du musst Inhaber oder Direktor sein.</div></div>`;return;}
const name=cp.name||'—', type=cp.type?.name||(typeof cp.type==='string'?cp.type:'')||cp.company_type||'—', rating=cp.rating??'—';
const _s=(v)=>{if(v===null||v===undefined)return null;if(typeof v==='number')return v;if(typeof v==='object')return v.value??v.current??v.amount??null;return null;};
const dailyIncome=Number(cp.income?.daily??cp.daily_income??cp.daily_revenue??0);
const weeklyProfit=Number(cp.income?.weekly??cp.weekly_profit??0);
const empHired=cp.employees?.hired??cp.employees_hired??employees.length??0;
const empCap=cp.employees?.capacity??cp.employees_capacity??'—';
const effs=employees.map(e=>Number(e.effectiveness?.working_stats?.total??e.effectiveness?.total??e.working_stats?.total??0)).filter(v=>v>0);
const avgEff=effs.length?Math.round(effs.reduce((a,v)=>a+v,0)/effs.length):(Number(_s(cp.efficiency))||0);
body.innerHTML=`${wsHtml}
<div class="card" style="flex-shrink:0;"><div class="card-header"><span class="card-title amber">🏢 ${Utils.escHtml(name)}</span><span class="badge badge-amber">${Utils.escHtml(String(type))}</span></div>
<div class="card-body"><div class="company-kpi-grid"><div class="company-kpi"><div class="kv" style="color:var(--green);">${Utils.fmtMoney(dailyIncome)}</div><div class="kl">Täglich</div></div><div class="company-kpi"><div class="kv" style="color:var(--amber);">${Utils.fmtMoney(weeklyProfit)}</div><div class="kl">Wöchentlich</div></div><div class="company-kpi"><div class="kv" style="color:var(--blue);">${empHired}/${empCap}</div><div class="kl">Mitarbeiter</div></div><div class="company-kpi"><div class="kv" style="color:${avgEff>=60?'var(--green)':avgEff>=30?'var(--amber)':'var(--muted)'};">${avgEff>0?avgEff+'%':'—'}</div><div class="kl">Effizienz Ø</div></div></div>${rating!=='—'?`<div class="row"><span class="row-label">⭐ Rating</span><span class="row-value" style="color:var(--amber);">${rating} / 10</span></div>`:''}</div></div>
${employees.length>0?`<div class="card" style="flex-shrink:0;"><div class="card-header"><span class="card-title green">👷 MITARBEITER</span><span class="badge badge-green">${avgEff>0?avgEff+'%':'—'} Ø</span></div><div class="card-body p0">${employees.slice(0,40).map(e=>{const eff=Number(e.effectiveness?.working_stats?.total??e.effectiveness?.total??e.working_stats?.total??0);const pos=(()=>{const p=e.position??e.role??null;if(!p)return'—';if(typeof p==='string')return p;if(typeof p==='object')return p.name??p.title??'—';return'—';})();const online=((Date.now()/1000-(e.last_action?.timestamp||0))<120);const ec=eff>=60?'var(--green)':eff>=30?'var(--amber)':eff>0?'var(--red)':'var(--muted)';return`<div class="company-emp-row"><span class="status-dot sd-${online?'online':'offline'}" style="flex-shrink:0;"></span><span class="company-emp-name">${Utils.escHtml(e.name||`#${e.id}`)}</span><span class="company-emp-pos">${Utils.escHtml(pos)}</span><span class="company-emp-eff" style="color:${ec};">${eff>0?eff+'%':'—'}</span></div>`;}).join('')}</div></div>`:''}`;
}
// ─── NETWORTH TAB ───
renderNetworth() {
const body=this.body; if(!body)return;
const nw=this.state.networthData, hist=this.state.networthHistory;
if(!nw){body.innerHTML=`<div class="empty-state"><div class="es-icon">💰</div><div class="es-text">Lade Vermögensdaten...</div><div class="es-sub">Benötigt Full Access API Key</div></div>`;return;}
const now=Utils.now(), oh=hist.filter(h=>h.ts>=now-3600);
const change=oh.length>=2?hist[hist.length-1].val-oh[0].val:0;
const wallet=Number(nw.wallet??nw.cash??0), bank=Number(nw.bank??0), stocks=Number(nw.stockmarket??nw.stocks??0);
const totalLiquid=wallet+bank+stocks;
const bazaar=Number(nw.bazaar??0), props=Number(nw.properties??0), piggy=Number(nw.piggybank??0), fvault=Number(nw.factionvault??0);
body.innerHTML=`
<div class="nw-ticker"><div style="font-size:9px;color:var(--muted);text-transform:uppercase;letter-spacing:1px;margin-bottom:4px;">LIQUIDES VERMÖGEN</div><div style="display:flex;align-items:baseline;gap:10px;"><div class="nw-main-val">${Utils.fmtMoney(totalLiquid)}</div>${change!==0?`<div class="nw-change ${change>0?'pos':'neg'}">${change>0?'▲':'▼'} ${Utils.fmtMoney(Math.abs(change))} / 1h</div>`:'<div class="nw-change" style="color:var(--muted);">± —</div>'}</div></div>
<div class="card" style="flex-shrink:0;"><div class="card-header"><span class="card-title green">💵 LIQUIDITÄT</span></div><div class="card-body"><div class="row"><span class="row-label">💵 Cash</span><span class="row-value" style="color:var(--green);">${Utils.fmtMoney(wallet)}</span></div><div class="row"><span class="row-label">🏦 Bank</span><span class="row-value" style="color:var(--blue);">${Utils.fmtMoney(bank)}</span></div><div class="row"><span class="row-label">📈 Aktien</span><span class="row-value" style="color:var(--amber);">${Utils.fmtMoney(stocks)}</span></div></div></div>
${(bazaar||props||piggy||fvault)?`<div class="card" style="flex-shrink:0;"><div class="card-header"><span class="card-title">📦 WEITERE ASSETS</span></div><div class="card-body">${bazaar?`<div class="row"><span class="row-label">🏪 Bazaar</span><span class="row-value">${Utils.fmtMoney(bazaar)}</span></div>`:''}${props?`<div class="row"><span class="row-label">🏠 Immobilien</span><span class="row-value">${Utils.fmtMoney(props)}</span></div>`:''}${piggy?`<div class="row"><span class="row-label">🐷 Sparschwein</span><span class="row-value">${Utils.fmtMoney(piggy)}</span></div>`:''}${fvault?`<div class="row"><span class="row-label">🏛 Faction Vault</span><span class="row-value">${Utils.fmtMoney(fvault)}</span></div>`:''}</div></div>`:''}
${hist.length>=2?`<div class="card" style="flex-shrink:0;"><div class="card-header"><span class="card-title green">📈 VERLAUF</span></div><div class="card-body">${this._simpleChart(hist)}</div></div>`:''}`;
}
_simpleChart(hist) {
if(hist.length<2)return'';
const vals=hist.map(h=>h.val), mn=Math.min(...vals), mx=Math.max(...vals), range=mx-mn||1;
const W=280,H=60;
const pts=hist.map((h,i)=>`${((i/(hist.length-1))*W).toFixed(1)},${(H-((h.val-mn)/range)*H).toFixed(1)}`).join(' ');
return`<svg viewBox="0 0 ${W} ${H}" style="width:100%;height:56px;overflow:visible;"><polyline points="${pts}" fill="none" stroke="${vals[vals.length-1]>=vals[0]?'var(--green)':'var(--red)'}" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg>`;
}
// ─── NOTIFICATIONS TAB ───
renderNotifications() {
const body=this.body; if(!body)return;
const s=this.state;
const ag=(title,keys)=>{const rows=keys.filter(k=>ALARM_DEFAULTS[k]).map(key=>{const def=ALARM_DEFAULTS[key],on=s.isAlarm(key);return`<div class="alarm-row-item"><div style="flex:1;min-width:0;"><div class="alarm-lbl">${def.label}</div><div class="alarm-desc">${def.desc}</div></div><label class="toggle"><input type="checkbox" data-alarm="${key}" ${on?'checked':''}><span class="toggle-slider"></span></label></div>`;}).join('');return`<div class="settings-section"><div class="settings-section-title">${title}</div>${rows}</div>`;};
const eT=(s.energyThresholds||[50,75,100]).join(', '), nT=(s.nerveThresholds||[50,100]).join(', ');
body.innerHTML=`
<div class="card" style="flex-shrink:0;"><div class="card-body"><div style="display:flex;gap:6px;margin-bottom:12px;flex-wrap:wrap;"><button class="thud-btn ${s.store.get('sound',true)?'active':''}" id="thud-toggle-sound">${s.store.get('sound',true)?'🔊 Sound AN':'🔇 Sound AUS'}</button><button class="thud-btn ${s.store.get('notif',true)?'active':''}" id="thud-toggle-notif">${s.store.get('notif',true)?'🔔 Toasts AN':'🔕 Toasts AUS'}</button></div><div class="settings-section-title">SOUND-PROFIL</div><div class="sound-profile-grid">${['classic','military','subtle'].map(p=>`<button class="sp-btn ${s.store.get('soundProfile','classic')===p?'active':''}" data-sp="${p}">${{classic:'🎵 Classic',military:'🪖 Military',subtle:'🔉 Subtle'}[p]}</button>`).join('')}</div></div></div>
<div class="card" style="flex-shrink:0;">${ag('✈ TRAVEL',['travel_warn','travel_crit','landing_timer'])}${ag('⚡ ENERGIE',['energy_full','energy_thresh','energy_over151'])}${ag('🧠 NERVE',['nerve_full','nerve_thresh'])}${ag('❤ HP',['hp_crit'])}${ag('⏱ COOLDOWNS',['hospital_free','jail_free','drug_ready','booster_ready','medical_ready'])}${ag('⛓ CHAIN',['chain_warn'])}${ag('💀 ENEMY',['enemy_travel'])}</div>
<div class="card" style="flex-shrink:0;"><div class="card-body" style="padding:12px 14px;gap:10px;"><div class="settings-section-title" style="border:none;padding:0 0 8px;">⚡ ENERGIE SCHWELLWERTE</div><div style="font-size:11px;color:var(--muted);margin-bottom:4px;">Alarme bei diesen % Werten · kein Alarm über ${HAPPY_JUMP_THRESHOLD}</div><div style="display:flex;gap:8px;align-items:center;"><input type="text" class="threshold-input" id="thud-e-thresh" style="width:130px;" value="${eT}" placeholder="50,75,100"><button class="sort-btn" id="thud-e-thresh-save">✓ Speichern</button></div></div></div>
<div class="card" style="flex-shrink:0;"><div class="card-body" style="padding:12px 14px;gap:10px;"><div class="settings-section-title" style="border:none;padding:0 0 8px;">🧠 NERVE SCHWELLWERTE</div><div style="display:flex;gap:8px;align-items:center;"><input type="text" class="threshold-input" id="thud-n-thresh" style="width:130px;" value="${nT}" placeholder="50,100"><button class="sort-btn" id="thud-n-thresh-save">✓ Speichern</button></div></div></div>
<div class="card" style="flex-shrink:0;"><div class="card-body"><div class="settings-section-title">TRAVEL ALERT TYP</div>${[['toast','🔔 Toast'],['fullscreen','🖥 Vollbild']].map(([k,lbl])=>`<div class="alarm-row-item"><div class="alarm-lbl">${lbl}</div><label class="toggle"><input type="checkbox" data-ta="${k}" ${s.isTravelAlert(k)?'checked':''}><span class="toggle-slider"></span></label></div>`).join('')}</div></div>`;
body.querySelectorAll('[data-alarm]').forEach(el=>el.addEventListener('change',e=>s.setAlarm(e.target.dataset.alarm,e.target.checked)));
body.querySelectorAll('[data-ta]').forEach(el=>el.addEventListener('change',e=>s.setTravelAlert(e.target.dataset.ta,e.target.checked)));
body.querySelectorAll('[data-sp]').forEach(btn=>btn.addEventListener('click',()=>{hud.store.set('soundProfile',btn.dataset.sp);hud.audio.play('notify');this.renderNotifications();}));
document.getElementById('thud-toggle-sound')?.addEventListener('click',()=>{hud.store.set('sound',!hud.store.get('sound',true));this.renderNotifications();});
document.getElementById('thud-toggle-notif')?.addEventListener('click',()=>{hud.store.set('notif',!hud.store.get('notif',true));this.renderNotifications();});
document.getElementById('thud-e-thresh-save')?.addEventListener('click',()=>{const raw=document.getElementById('thud-e-thresh')?.value||'';const arr=raw.split(',').map(x=>parseInt(x.trim())).filter(x=>!isNaN(x)&&x>0&&x<=100);s.setEnergyThresholds([...new Set(arr)].sort((a,b)=>a-b));hud.toasts.show('✅','Energie-Schwellwerte gespeichert',arr.join(', ')+'%','info',3000);});
document.getElementById('thud-n-thresh-save')?.addEventListener('click',()=>{const raw=document.getElementById('thud-n-thresh')?.value||'';const arr=raw.split(',').map(x=>parseInt(x.trim())).filter(x=>!isNaN(x)&&x>0&&x<=100);s.setNerveThresholds([...new Set(arr)].sort((a,b)=>a-b));hud.toasts.show('✅','Nerve-Schwellwerte gespeichert',arr.join(', ')+'%','info',3000);});
}
// ─── SETTINGS TAB ───
renderSettings() {
const body=this.body; if(!body)return;
const s=this.state, fabPinned=hud.store.get('fabPinned',false);
const visRows=Object.entries(VISIBILITY_DEFAULTS).map(([k,v])=>`<div class="vis-row" style="margin-bottom:3px;"><span class="vis-lbl">${v.label}</span><label class="toggle"><input type="checkbox" data-vis="${k}" ${s.isVisible(k)?'checked':''}><span class="toggle-slider"></span></label></div>`).join('');
const curLinks=hud.store.get('customQuickLinks',null)||[{label:'Stock\nMarket',icon:'📈',url:'https://www.torn.com/page.php?sid=stocks'},{label:'Casino',icon:'🎰',url:'https://www.torn.com/page.php?sid=slots'},{label:'Bazaar',icon:'🏪',url:'https://www.torn.com/bazaar.php?userId=0'},{label:'Forum',icon:'💬',url:'https://www.torn.com/forums.php'}];
body.innerHTML=`
<div class="card" style="flex-shrink:0;"><div class="card-body"><div class="settings-section-title">THEME</div><div class="theme-grid">${Object.entries(THEMES).map(([k,t])=>`<button class="theme-btn ${hud.store.get('theme','obsidian')===k?'active':''}" data-theme="${k}">${t.name}</button>`).join('')}</div></div></div>
<div class="card" style="flex-shrink:0;"><div class="card-header"><span class="card-title">🔴 FAB BUTTON</span></div><div class="card-body" style="padding:4px 0 8px;gap:0;"><div class="fab-pin-row"><div><div class="fab-pin-lbl">📌 Position fixieren</div><div class="fab-pin-desc">Button lässt sich nicht mehr verschieben</div></div><label class="toggle"><input type="checkbox" id="thud-fab-pin" ${fabPinned?'checked':''}><span class="toggle-slider"></span></label></div><div style="padding:7px 14px;display:flex;gap:6px;align-items:center;"><button class="sort-btn" id="thud-fab-reset">↺ Position zurücksetzen</button></div></div></div>
<div class="card" style="flex-shrink:0;"><div class="card-body"><div class="settings-section-title">SICHTBARE ELEMENTE</div><div class="vis-grid">${visRows}</div></div></div>
<div class="card" style="flex-shrink:0;"><div class="card-body" style="gap:8px;"><div class="settings-section-title">🔗 QUICK LINKS KONFIGURIEREN</div><div style="font-size:10px;color:var(--muted);margin-bottom:4px;">JSON-Array mit label, icon, url.</div><textarea id="thud-ql-json" style="width:100%;background:var(--bg);border:1px solid var(--border);border-radius:var(--r-sm);padding:8px;color:var(--text);font-family:var(--mono);font-size:10px;min-height:120px;resize:vertical;">${Utils.escHtml(JSON.stringify(curLinks,null,2))}</textarea><div style="display:flex;gap:6px;"><button class="sort-btn" id="thud-ql-save" style="border-color:rgba(16,185,129,.3);color:var(--green);">✓ Speichern</button><button class="sort-btn" id="thud-ql-reset" style="color:var(--muted);">↺ Standard</button></div></div></div>
<div class="card" style="flex-shrink:0;"><div class="card-body"><div class="settings-section-title">API KEY</div><div class="api-key-row"><span class="api-key-status ${hud.store.get('apiKey')?'set':'unset'}">${hud.store.get('apiKey')?'✓ API Key gesetzt':'⚠ Kein API Key'}</span><button class="thud-btn" id="thud-api-key-btn">🔑 Ändern</button></div></div></div>
<div class="card" style="flex-shrink:0;"><div class="card-body"><div class="settings-section-title">TASTENKÜRZEL</div><div style="display:grid;grid-template-columns:1fr 1fr;gap:6px;font-size:11px;color:var(--dim);"><span><kbd style="background:var(--bg-el);border:1px solid var(--border);border-radius:3px;padding:1px 5px;font-family:var(--mono);font-size:9px;">M</kbd> Minimieren</span><span><kbd style="background:var(--bg-el);border:1px solid var(--border);border-radius:3px;padding:1px 5px;font-family:var(--mono);font-size:9px;">H</kbd> Ausblenden</span><span><kbd style="background:var(--bg-el);border:1px solid var(--border);border-radius:3px;padding:1px 5px;font-family:var(--mono);font-size:9px;">R</kbd> Refresh</span><span><kbd style="background:var(--bg-el);border:1px solid var(--border);border-radius:3px;padding:1px 5px;font-family:var(--mono);font-size:9px;">S</kbd> Settings</span></div></div></div>
<div class="ts" style="text-align:center;flex-shrink:0;">${SCRIPT_TITLE} v${VERSION} · by xShaYaKaZ</div>`;
body.querySelectorAll('[data-theme]').forEach(btn=>btn.addEventListener('click',()=>{hud.store.set('theme',btn.dataset.theme);applyTheme(btn.dataset.theme);this.renderSettings();}));
body.querySelectorAll('[data-vis]').forEach(el=>el.addEventListener('change',e=>s.setVisible(e.target.dataset.vis,e.target.checked)));
document.getElementById('thud-api-key-btn')?.addEventListener('click',openApiModal);
document.getElementById('thud-fab-pin')?.addEventListener('change',e=>hud.store.set('fabPinned',e.target.checked));
document.getElementById('thud-fab-reset')?.addEventListener('click',()=>{const fab=document.getElementById('thud-fab');if(fab){fab.style.left='10px';fab.style.top='10px';}hud.store.set('fabPos',{l:10,t:10});hud.toasts.show('✅','FAB zurückgesetzt','Links oben','info',2000);});
document.getElementById('thud-ql-save')?.addEventListener('click',()=>{try{const raw=document.getElementById('thud-ql-json')?.value||'[]';const parsed=JSON.parse(raw);if(!Array.isArray(parsed))throw new Error('Kein Array');hud.store.set('customQuickLinks',parsed);hud.toasts.show('✅','Quick Links gespeichert',`${parsed.length} Links`,'info',3000);this.renderSettings();}catch(e){hud.toasts.show('⚠','JSON Fehler',e.message,'warn',4000);}});
document.getElementById('thud-ql-reset')?.addEventListener('click',()=>{hud.store.set('customQuickLinks',null);hud.toasts.show('✅','Quick Links zurückgesetzt','Standard wiederhergestellt','info',3000);this.renderSettings();});
}
_noKeyState() { return`<div class="empty-state" style="padding:48px 24px;"><div class="es-icon">🔑</div><div class="es-text">KEIN API KEY</div><div class="es-sub">Klick um deinen Torn API Key einzugeben</div><button class="scan-btn" style="margin-top:14px;" onclick="document.getElementById('thud-api-key-btn-header').click()">🔑 API Key eingeben</button></div>`; }
_loadingState() { return`<div class="loading-state">LADE DATEN…</div>`; }
}
// ═══════════════════════════════════════════════════════════════
// THEME / MODAL
// ═══════════════════════════════════════════════════════════════
function applyTheme(key) {
const t=THEMES[key]||THEMES.obsidian, r=document.documentElement.style;
r.setProperty('--bg',t.bg); r.setProperty('--bg-card',t.bgCard); r.setProperty('--bg-el',t.bgEl);
r.setProperty('--border',t.border); r.setProperty('--accent',t.accent); r.setProperty('--accent2',t.accent2);
r.setProperty('--text',t.text); r.setProperty('--muted',t.textMuted); r.setProperty('--dim',t.textDim);
r.setProperty('--green',t.green); r.setProperty('--red',t.red); r.setProperty('--amber',t.amber);
r.setProperty('--blue',t.blue); r.setProperty('--chain',t.chain); r.setProperty('--travel',t.travel); r.setProperty('--online',t.online);
}
function openApiModal() {
const m=document.createElement('div'); m.className='thud-modal-bg';
m.innerHTML=`<div class="thud-modal"><h3>⚙ API Key</h3><p>Torn → Settings → API Key</p><input id="thud-api-input" type="text" placeholder="dein_api_key" value="${hud.store.get('apiKey','')}"><div class="modal-btns"><button class="modal-cancel" id="thud-modal-cancel">Abbrechen</button><button class="modal-save" id="thud-modal-save">Speichern</button></div></div>`;
document.body.appendChild(m);
document.getElementById('thud-modal-save').onclick=()=>{const k=document.getElementById('thud-api-input').value.trim();if(k){hud.store.set('apiKey',k);m.remove();hud.fetcher.fetchAll();hud.renderer.render();}};
document.getElementById('thud-modal-cancel').onclick=()=>m.remove();
m.addEventListener('click',e=>{if(e.target===m)m.remove();});
}
// ═══════════════════════════════════════════════════════════════
// HUD CONTROLLER
// ═══════════════════════════════════════════════════════════════
const hud = {
store: new Store(),
state:null, api:null, audio:null, toasts:null,
chainManager:null, alarmChecker:null, fetcher:null, renderer:null,
activeTab:null,
init() {
this.state = new StateManager(this.store);
this.api = new TornAPI(this.store);
this.audio = new AudioManager(this.store);
this.toasts = new ToastManager();
this.chainManager = new ChainManager(this.state);
this.alarmChecker = new AlarmChecker(this.state, this.toasts);
this.renderer = new Renderer(this.state);
this.activeTab = this.store.get('activeTab','personal');
this._injectCSS(); this._buildUI();
applyTheme(this.store.get('theme','obsidian'));
this._setupIntervals(); this._setupHotkeys();
if(!this.api.key) setTimeout(openApiModal,600);
else this.fetcher.fetchAll();
},
_injectCSS() { const st=document.createElement('style'); st.textContent=buildCSS(); document.head.appendChild(st); },
_buildUI() {
if(!document.getElementById('thud-toasts')){ const tc=document.createElement('div'); tc.id='thud-toasts'; document.body.appendChild(tc); }
const fs=document.createElement('div'); fs.id='thud-fs-alert'; fs.className='fullscreen-alert';
fs.innerHTML=`<div class="fs-box"><div style="font-size:52px;">✈️</div><div class="fs-title">ANKUNFT IN KÜRZE</div><div class="fs-msg" id="thud-fs-msg"></div><button class="fs-close">OK</button></div>`;
document.body.appendChild(fs); fs.querySelector('.fs-close').addEventListener('click',()=>fs.classList.remove('active'));
const overlay=document.createElement('div'); overlay.id='thud-overlay';
const tabsHtml=TABS_DEF.map(t=>`<div class="thud-tab ${this.activeTab===t.id?'active':''}" data-tab="${t.id}"><span class="ti">${t.icon}</span><span class="tl">${t.label}</span></div>`).join('');
overlay.innerHTML=`
<div class="thud-header" id="thud-header">
<div class="thud-header-logo">${SHAYA_LOGO_SVG}</div>
<div class="thud-header-info"><div class="thud-header-title" id="thud-faction-name">${SCRIPT_TITLE}</div><div class="thud-header-sub">v${VERSION} · xShaYaKaZ</div></div>
<div class="thud-header-actions"><div class="pip"></div><button class="thud-btn" id="thud-btn-refresh" title="Refresh [R]">↺</button><button class="thud-btn" id="thud-api-key-btn-header" title="API Key">🔑</button><button class="thud-btn" id="thud-btn-minimize" title="[M]">—</button><button class="thud-btn-close" id="thud-btn-close" title="[H]">✕</button></div>
</div>
<div class="thud-tabs" id="thud-tabs">${tabsHtml}</div>
<div class="thud-mini" id="thud-mini"></div>
<div class="thud-body" id="thud-body"><div class="loading-state">LADE…</div></div>
<div class="thud-resize-se" id="thud-rse"></div>
<div class="thud-resize-e" id="thud-re"></div>
<div class="thud-resize-s" id="thud-rs"></div>`;
document.body.appendChild(overlay);
const pos=this.store.get('pos',null);
if(pos){overlay.style.left=pos.l+'px';overlay.style.top=pos.t+'px';}else{overlay.style.right='20px';overlay.style.top='60px';}
overlay.style.width=this.store.get('width',400)+'px';
overlay.style.height=(this.store.get('height',null)||560)+'px';
if(!this.store.get('visible',true))overlay.style.display='none';
if(this.store.get('minimized',false))overlay.classList.add('minimized');
const fab=document.createElement('div'); fab.id='thud-fab'; fab.title=`${SCRIPT_TITLE} [H]`; fab.innerHTML=FAB_LOGO_SVG;
document.body.appendChild(fab);
const fPos=this.store.get('fabPos',null);
if(fPos){fab.style.left=fPos.l+'px';fab.style.top=fPos.t+'px';}else{fab.style.left='10px';fab.style.top='10px';}
if(this.store.get('visible',true))fab.classList.add('on');
this._setupDrag(overlay); this._setupResize(overlay); this._setupFab(fab);
this._setupTabWheelScroll(); this._setupScrollFix(overlay);
document.getElementById('thud-tabs').addEventListener('click',e=>{const t=e.target.closest('.thud-tab');if(!t)return;this.switchTab(t.dataset.tab);});
document.getElementById('thud-btn-refresh').addEventListener('click',()=>this.fetcher.fetchAll());
document.getElementById('thud-btn-minimize').addEventListener('click',()=>this.toggleMinimize());
document.getElementById('thud-btn-close').addEventListener('click',()=>this.toggleVisible());
document.getElementById('thud-api-key-btn-header').addEventListener('click',openApiModal);
this.fetcher=new DataFetcher(this.api,this.state,this.toasts,this.renderer);
this.renderer.render();
},
_setupTabWheelScroll() { const tabs=document.getElementById('thud-tabs');if(!tabs)return;tabs.addEventListener('wheel',e=>{e.preventDefault();tabs.scrollLeft+=e.deltaY!==0?e.deltaY:e.deltaX;},{passive:false}); },
_setupScrollFix(overlay) {
const getBody=()=>document.getElementById('thud-body');
overlay.addEventListener('wheel',e=>{const t=e.target.closest('#thud-tabs');if(t)return;const b=getBody();if(!b)return;b.scrollTop+=e.deltaY;e.preventDefault();e.stopPropagation();},{passive:false});
let ty=0;
overlay.addEventListener('touchstart',e=>{ty=e.touches[0].clientY;},{passive:true});
overlay.addEventListener('touchmove',e=>{const b=getBody();if(!b)return;b.scrollTop+=ty-e.touches[0].clientY;ty=e.touches[0].clientY;e.preventDefault();},{passive:false});
},
switchTab(tabId) { this.activeTab=tabId; this.store.set('activeTab',tabId); document.querySelectorAll('.thud-tab').forEach(el=>el.classList.toggle('active',el.dataset.tab===tabId)); this.renderer.render(); setTimeout(()=>{const b=document.getElementById('thud-body');if(b)b.scrollTop=0;},50); },
toggleMinimize() { const v=!this.store.get('minimized',false); this.store.set('minimized',v); const o=document.getElementById('thud-overlay');if(o)o.classList.toggle('minimized',v); if(v)this.renderer.renderMini(); },
toggleVisible() { const v=!this.store.get('visible',true); this.store.set('visible',v); const o=document.getElementById('thud-overlay');if(o)o.style.display=v?'':'none'; const fab=document.getElementById('thud-fab');if(fab)v?fab.classList.add('on'):fab.classList.remove('on'); },
_setupDrag(el) {
let drag=false,ox=0,oy=0;
document.getElementById('thud-header').addEventListener('mousedown',e=>{if(e.target.tagName==='BUTTON'||e.target.closest('button'))return;e.preventDefault();const r=el.getBoundingClientRect();ox=e.clientX-r.left;oy=e.clientY-r.top;drag=true;});
document.addEventListener('mousemove',e=>{if(!drag)return;el.style.left=Math.max(0,Math.min(e.clientX-ox,window.innerWidth-el.offsetWidth))+'px';el.style.top=Math.max(0,Math.min(e.clientY-oy,window.innerHeight-40))+'px';el.style.right='auto';});
document.addEventListener('mouseup',()=>{if(!drag)return;drag=false;this.store.set('pos',{l:parseInt(el.style.left),t:parseInt(el.style.top)});});
},
_setupResize(el) {
let rSE=false,rE=false,rS=false,sx=0,sy=0,sw=0,sh=0;
document.getElementById('thud-rse').addEventListener('mousedown',e=>{rSE=true;sx=e.clientX;sy=e.clientY;sw=el.offsetWidth;sh=el.offsetHeight;e.preventDefault();e.stopPropagation();});
document.getElementById('thud-re').addEventListener('mousedown',e=>{rE=true;sx=e.clientX;sw=el.offsetWidth;e.preventDefault();e.stopPropagation();});
document.getElementById('thud-rs').addEventListener('mousedown',e=>{rS=true;sy=e.clientY;sh=el.offsetHeight;e.preventDefault();e.stopPropagation();});
document.addEventListener('mousemove',e=>{if(rSE){el.style.width=Math.max(300,Math.min(720,sw+(e.clientX-sx)))+'px';el.style.height=Math.max(250,sh+(e.clientY-sy))+'px';}else if(rE){el.style.width=Math.max(300,Math.min(720,sw+(e.clientX-sx)))+'px';}else if(rS){el.style.height=Math.max(250,sh+(e.clientY-sy))+'px';}});
document.addEventListener('mouseup',()=>{if(rSE||rE||rS){this.store.set('width',parseInt(el.style.width));if(el.style.height)this.store.set('height',parseInt(el.style.height));}rSE=rE=rS=false;});
},
_setupFab(fab) {
let drag=false,ox=0,oy=0,moved=false,sx=0,sy=0;
fab.addEventListener('mousedown',e=>{e.preventDefault();drag=true;moved=false;const r=fab.getBoundingClientRect();ox=e.clientX-r.left;oy=e.clientY-r.top;sx=e.clientX;sy=e.clientY;});
document.addEventListener('mousemove',e=>{if(!drag)return;if(hud.store.get('fabPinned',false)){drag=false;return;}if(Math.abs(e.clientX-sx)>4||Math.abs(e.clientY-sy)>4)moved=true;fab.style.left=Math.max(0,Math.min(window.innerWidth-40,e.clientX-ox))+'px';fab.style.top=Math.max(0,Math.min(window.innerHeight-40,e.clientY-oy))+'px';});
document.addEventListener('mouseup',()=>{if(!drag)return;drag=false;if(!moved){this.toggleVisible();return;}this.store.set('fabPos',{l:parseInt(fab.style.left),t:parseInt(fab.style.top)});});
},
_setupIntervals() {
setInterval(()=>hud.fetcher.fetchPersonal(),REFRESH.PERSONAL);
setInterval(()=>hud.fetcher.fetchFaction(), REFRESH.FACTION);
setInterval(()=>hud.fetcher.fetchChain(), REFRESH.CHAIN);
setInterval(()=>hud.fetcher.fetchNetworth(),REFRESH.NETWORTH);
setInterval(()=>hud.fetcher.fetchCompany(), REFRESH.COMPANY);
setInterval(()=>hud.fetcher.fetchEnemyData(),REFRESH.ENEMY);
setInterval(()=>this._tick(), REFRESH.TICK);
},
_setupHotkeys() {
document.addEventListener('keydown',e=>{ if(e.target.tagName==='INPUT'||e.target.tagName==='TEXTAREA')return; if(e.key==='m'||e.key==='M')this.toggleMinimize(); if(e.key==='h'||e.key==='H')this.toggleVisible(); if(e.key==='r'||e.key==='R')this.fetcher.fetchAll(); if(e.key==='s'||e.key==='S')this.switchTab('settings'); });
},
_tick() {
this.state.uptimeSec++;
const td=this.state.userTravel?.travel;
if(td?.time_left>0)td.time_left=Math.max(0,Number(td.time_left)-1);
this.alarmChecker.run();
const min=this.store.get('minimized',false);
if(!min&&this.activeTab==='personal')this.renderer.renderPersonal();
if(min)this.renderer.renderMini();
},
};
if(document.readyState==='loading'){ document.addEventListener('DOMContentLoaded',()=>hud.init()); }
else { hud.init(); }
})();