Makes attacking, walling, and faction scouting easier. TornPDA compatible.
// ==UserScript==
// @name Attack Helpers
// @namespace http://tampermonkey.net/
// @license NOLICENSE
// @version 2.0.1
// @description Makes attacking, walling, and faction scouting easier. TornPDA compatible.
// @author maximate
// @match https://www.torn.com/*
// @icon https://www.google.com/s2/favicons?sz=64&domain=torn.com
// @grant GM_addStyle
// @grant GM_xmlhttpRequest
// @grant GM.xmlHttpRequest
// @grant unsafeWindow
// @connect api.torn.com
// @connect www.tornstats.com
// @run-at document-end
// ==/UserScript==
(function () {
"use strict";
const PDA_API_KEY = "###PDA-APIKEY###";
const PDA_KEY_READY = PDA_API_KEY && !/^###.+###$/.test(PDA_API_KEY);
const pageWindow = typeof unsafeWindow !== "undefined" ? unsafeWindow : window;
const PREFIX = "maximate.attack.";
const OLD_PREFIX = "miz.";
const FOUND = "data-maximate-found";
const IS_PDA = PDA_KEY_READY || typeof PDA_httpGet === "function" || /TornPDA|Torn PDA/i.test(navigator.userAgent);
const IS_MOBILE = IS_PDA || window.matchMedia("(max-width: 760px)").matches;
let xhr = null;
if (typeof GM_xmlhttpRequest === "function") {
xhr = GM_xmlhttpRequest;
} else if (typeof GM !== "undefined" && typeof GM.xmlHttpRequest === "function") {
xhr = GM.xmlHttpRequest;
}
const DEFAULT_SETTINGS = {
battlestats: true,
wallTimers: true,
wallHosp: true,
executeHelper: true,
fastAttack: false,
clickableAssists: true,
liveFilter: true,
quickActions: true,
mobileTapStats: true,
compactMobile: true,
cacheHours: 24
};
const apiStatus = {
torn: "Not checked",
tornStats: "Not checked",
lastError: ""
};
const readStorage = (key, fallback = "") => {
const current = localStorage[`${PREFIX}${key}`];
if (current !== undefined && current !== null) return current;
const old = localStorage[`${OLD_PREFIX}${key}`];
if (old !== undefined && old !== null) return old;
return fallback;
};
const writeStorage = (key, value) => {
localStorage[`${PREFIX}${key}`] = value;
};
const safeJson = (str, fallback = null) => {
if (str && typeof str === "object") return str;
try {
return JSON.parse(str);
} catch (_e) {
return fallback;
}
};
const readJson = (key, fallback) => safeJson(readStorage(key, ""), fallback);
const writeJson = (key, value) => writeStorage(key, JSON.stringify(value));
const settings = Object.assign({}, DEFAULT_SETTINGS, readJson("settings", {}));
let API_KEY = readStorage("api-key", PDA_KEY_READY ? PDA_API_KEY : "");
let initialized = false;
if (API_KEY && localStorage[`${PREFIX}api-key`] !== API_KEY) writeStorage("api-key", API_KEY);
const saveSettings = () => writeJson("settings", settings);
const cacheLifetime = () => Math.max(1, Number(settings.cacheHours) || 24) * 60 * 60 * 1000;
const formatTime = (seconds) => {
if (!Number.isFinite(seconds) || seconds < 0) return null;
const hours = Math.floor(seconds / 3600);
const minutes = Math.floor((seconds % 3600) / 60);
const secs = Math.round(seconds % 60);
const parts = [];
if (hours) parts.push(`${hours} hr`);
if (minutes) parts.push(`${minutes} min`);
parts.push(`${secs} sec`);
return parts.join(" ");
};
const waitForElement = (selector, root = document) => new Promise((resolve) => {
const existing = root.querySelector(selector);
if (existing) {
resolve(existing);
return;
}
const observer = new MutationObserver(() => {
const node = root.querySelector(selector);
if (!node) return;
observer.disconnect();
resolve(node);
});
observer.observe(document.body, { childList: true, subtree: true });
});
const watchElements = (selector, action, root = document, intervalMs = 300) => {
const scan = () => {
root.querySelectorAll(selector).forEach((node) => {
if (node.getAttribute(FOUND)) return;
const retry = action(node);
if (!retry) node.setAttribute(FOUND, "1");
});
};
scan();
return setInterval(scan, intervalMs);
};
const addStyle = (cssText) => {
if (typeof GM_addStyle === "function") {
GM_addStyle(cssText);
return;
}
const style = document.createElement("style");
style.textContent = cssText;
(document.head || document.documentElement).appendChild(style);
};
const requestText = (url, method = "GET") => new Promise((resolve, reject) => {
if (typeof PDA_httpGet === "function" && String(method).toUpperCase() === "GET") {
PDA_httpGet(url)
.then((data) => resolve(typeof data === "string" ? data : JSON.stringify(data)))
.catch(reject);
return;
}
if (!xhr) {
reject(new Error("GM_xmlhttpRequest is unavailable."));
return;
}
xhr({
method,
url,
onload: (response) => resolve(response.responseText),
onerror: reject
});
});
const stripHtml = (html) => {
const tmp = document.createElement("div");
tmp.innerHTML = html;
return tmp.textContent.replace(/\s+/g, " ").trim();
};
const notify = (message) => {
const existing = document.querySelector(".maximate-toast");
if (existing) existing.remove();
const toast = document.createElement("div");
toast.className = "maximate-toast";
toast.textContent = message;
document.body.appendChild(toast);
setTimeout(() => toast.remove(), 2500);
};
const copyText = async (text) => {
try {
await navigator.clipboard.writeText(text);
notify("Copied");
} catch (_e) {
prompt("Copy this:", text);
}
};
const currentFactionId = () => {
const urlMatch = window.location.href.match(/[?&]ID=(\d+)/);
const facLink = document.querySelector("a[href*='factions.php?step=profile&ID=']");
const linkMatch = facLink && facLink.href.match(/ID=(\d+)/);
const fromUrl = urlMatch && urlMatch[1];
const fromLink = linkMatch && linkMatch[1];
return fromUrl || fromLink || "";
};
const renderStatPopup = (node) => {
if (!settings.mobileTapStats || !IS_MOBILE || !node || !node.dataset || !node.dataset.statHtml) return;
let popup = document.querySelector(".maximate-stat-popover");
if (popup) {
popup.remove();
return;
}
popup = document.createElement("div");
popup.className = "maximate-stat-popover";
popup.innerHTML = `${node.dataset.statHtml}<button type="button">Close</button>`;
popup.querySelector("button").onclick = () => popup.remove();
document.body.appendChild(popup);
};
const settingLabel = (key) => ({
battlestats: "Battlestats",
wallTimers: "Wall timers",
wallHosp: "Wall/hospital checks",
executeHelper: "Execute helper",
fastAttack: "Fast attack",
clickableAssists: "Clickable assists",
liveFilter: "Live member filter",
quickActions: "Target quick actions",
mobileTapStats: "Tap stat popups",
compactMobile: "Compact mobile UI"
}[key] || key);
const pageLabel = () => ({
profile: "Profile page",
loader: "Attack page",
"attack-log": "Attack log",
faction: "Faction page"
}[getPage()] || "No helper features on this page");
const pageHint = () => ({
profile: "Profile pages mainly get enabled attack links and mini-profile stats.",
loader: "Attack pages get wall/hospital status, execute helper, quick actions, assists, and optional fast attack.",
"attack-log": "Attack logs get readable icon hover titles.",
faction: "Faction pages get battlestats, wall timers, cache refresh, and live filters."
}[getPage()] || "Open a faction page, attack page, attack log, or player profile to see feature changes.");
const showSettingsPanel = () => {
const oldPanel = document.querySelector(".maximate-settings-panel");
if (oldPanel) oldPanel.remove();
const cacheCount = Object.keys((battlestatCache && battlestatCache.battleStats) || {}).length;
const stampCount = Object.keys((battlestatCache && battlestatCache.factionStamps) || {}).filter((id) => battlestatCache.factionStamps[id]).length;
const panel = document.createElement("div");
panel.className = "maximate-settings-panel";
panel.innerHTML = `
<div class="maximate-settings-head">
<strong>War Attack Helpers</strong>
<button type="button" data-action="close">x</button>
</div>
<div class="maximate-settings-status">
<div><b>Page:</b> ${pageLabel()}</div>
<div><b>Mode:</b> ${IS_PDA ? "TornPDA/mobile" : "Desktop userscript"}</div>
<div><b>Key:</b> ${API_KEY ? (PDA_KEY_READY ? "TornPDA key" : "Saved key") : "Missing"}</div>
<div><b>Torn API:</b> ${apiStatus.torn === "Not checked" ? "Idle until attack page" : apiStatus.torn}</div>
<div><b>TornStats:</b> ${apiStatus.tornStats === "Not checked" ? "Idle until faction/spy data" : apiStatus.tornStats}</div>
<div><b>Cache:</b> ${cacheCount} players, ${stampCount} factions</div>
<div>${pageHint()}</div>
${apiStatus.lastError ? `<div class="maximate-error">${apiStatus.lastError}</div>` : ""}
</div>
<div class="maximate-settings-grid">
${Object.keys(DEFAULT_SETTINGS).filter((key) => typeof DEFAULT_SETTINGS[key] === "boolean").map((key) => `
<label><input type="checkbox" data-setting="${key}" ${settings[key] ? "checked" : ""}>${settingLabel(key)}</label>
`).join("")}
</div>
<label class="maximate-cache-hours">Cache hours <input type="number" min="1" max="168" data-setting="cacheHours" value="${settings.cacheHours}"></label>
<div class="maximate-settings-actions">
<button type="button" data-action="key">Set key</button>
<button type="button" data-action="refresh-faction">Refresh faction</button>
<button type="button" data-action="clear-cache">Clear cache</button>
<button type="button" data-action="reload">Apply</button>
</div>
`;
panel.addEventListener("change", (evt) => {
const key = evt.target.dataset.setting;
if (!key) return;
settings[key] = evt.target.type === "checkbox" ? evt.target.checked : Number(evt.target.value);
saveSettings();
});
panel.addEventListener("click", async (evt) => {
const action = evt.target.dataset.action;
if (!action) return;
if (action === "close") panel.remove();
if (action === "reload") window.location.reload();
if (action === "key") promptForApiKey();
if (action === "clear-cache") {
battlestatCache.clearAll();
notify("Cache cleared");
showSettingsPanel();
}
if (action === "refresh-faction") {
const factionId = currentFactionId();
if (!factionId) {
notify("No faction found");
return;
}
delete battlestatCache.factionStamps[factionId];
await battlestatCache.loadFaction(factionId, true);
notify("Faction refreshed");
showSettingsPanel();
}
});
document.body.appendChild(panel);
};
const promptForApiKey = () => {
const input = prompt("Please enter the same API key you use for TornStats:", API_KEY || "");
if (input) {
API_KEY = input.trim();
writeStorage("api-key", API_KEY);
notify("API key saved");
if (initialized) window.location.reload();
else init();
}
return false;
};
const getPage = () => {
const href = String(window.location.href);
if (/https?:\/\/www\.torn\.com\/profiles\.php/i.test(href)) return "profile";
if (/https?:\/\/www\.torn\.com\/loader\.php\?sid=attack&/i.test(href)) return "loader";
if (/https?:\/\/www\.torn\.com\/loader\.php\?sid=attackLog&ID=/i.test(href)) return "attack-log";
if (/https?:\/\/www\.torn\.com\/factions\.php/i.test(href)) return "faction";
return "";
};
const getUserId = () => new URLSearchParams(window.location.search).get("user2ID");
const injectInitialHtml = () => {
addStyle(`
:root .dark-mode [class*=modal__] { background: none !important; }
span#wall-status { margin: 0 5px; }
span#hosp-status { display: block; cursor: pointer; }
@media screen and (max-width: 1000px) {
.members-cont .bs { display: none; }
}
.members-cont .level { width: 27px !important; }
.members-cont .id { padding-left: 5px !important; width: 28px !important; }
.members-cont .points { width: 42px !important; }
.finally-bs-stat { font-family: monospace; }
.finally-bs-stat > span { display: inline-block; width: 55px; text-align: right; }
.faction-names { position: relative; }
.finally-bs-api {
position: absolute;
background: var(--main-bg);
text-align: center;
left: 0;
top: 0;
width: 100%;
height: 100%;
}
.finally-bs-api > * { margin: 0 5px; padding: 5px; }
.finally-bs-swap {
position: absolute;
top: 10px;
left: 0;
right: 0;
margin-left: auto;
margin-right: auto;
width: 100px;
cursor: pointer;
}
.finally-bs-activeIcon { display: block !important; }
.finally-bs-asc {
border-bottom: 6px solid var(--sort-arrow-border-color);
border-left: 6px solid transparent;
border-right: 6px solid transparent;
border-top: 0 solid transparent;
height: 0;
top: -4px;
width: 0;
}
.finally-bs-desc {
border-bottom: 0 solid transparent;
border-left: 6px solid transparent;
border-right: 6px solid transparent;
border-top: 6px solid var(--sort-arrow-color);
height: 0;
top: 1px;
width: 0;
}
.finally-bs-col { text-overflow: clip !important; }
.raid-members-list .level:not(.bs) { width: 16px !important; }
body:not(.tt-mobile) .members-list.tt-modified .table-header .member {
width: calc(34% + 85px) !important;
}
.chain-attacks-list .miz-recent-attacks { width: 20px !important; }
.miz-bs-miniprofile { float: right; color: black; }
.miz-max-z { z-index: 999999 !important; }
.miz-name, .miz-attack { text-decoration: none; }
.miz-attack > div { display: inline; }
.maximate-float-btn, .maximate-fast-armed {
position: fixed;
right: 8px;
z-index: 999999;
padding: 7px 9px;
border: 1px solid #777;
background: #202020;
color: #fff;
border-radius: 4px;
font-size: 12px;
line-height: 1;
}
.maximate-float-btn { bottom: 78px; }
.maximate-fast-armed { bottom: 114px; background: #5b1f1f; border-color: #c44; }
.maximate-fast-armed.is-on { background: #245b1f; border-color: #5c5; }
.maximate-toast {
position: fixed;
left: 50%;
bottom: 72px;
transform: translateX(-50%);
z-index: 1000000;
background: #202020;
color: #fff;
border: 1px solid #777;
border-radius: 4px;
padding: 8px 10px;
font-size: 12px;
}
.maximate-settings-panel, .maximate-stat-popover {
position: fixed;
z-index: 1000000;
background: var(--main-bg, #1f1f1f);
color: var(--default-color, #fff);
border: 1px solid #666;
box-shadow: 0 6px 24px rgba(0,0,0,.45);
border-radius: 6px;
}
.maximate-settings-panel {
right: 8px;
bottom: 118px;
width: min(360px, calc(100vw - 16px));
max-height: calc(100vh - 140px);
overflow: auto;
padding: 10px;
font-size: 13px;
}
.maximate-settings-head, .maximate-settings-actions {
display: flex;
gap: 6px;
align-items: center;
justify-content: space-between;
margin-bottom: 8px;
}
.maximate-settings-head button, .maximate-settings-actions button, .maximate-stat-popover button {
border: 1px solid #777;
background: #2a2a2a;
color: #fff;
border-radius: 4px;
padding: 5px 7px;
}
.maximate-settings-status {
display: grid;
gap: 3px;
margin-bottom: 8px;
opacity: .95;
}
.maximate-settings-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 6px;
margin-bottom: 8px;
}
.maximate-settings-grid label, .maximate-cache-hours {
display: flex;
align-items: center;
gap: 6px;
}
.maximate-cache-hours input { width: 60px; }
.maximate-error { color: #ff8d8d; }
.maximate-stat-popover {
left: 10px;
right: 10px;
bottom: 76px;
padding: 12px;
font-size: 14px;
text-align: left;
}
.maximate-stat-popover button {
display: block;
margin-top: 8px;
width: 100%;
}
.maximate-actions {
display: inline-flex;
gap: 4px;
margin-left: 5px;
vertical-align: middle;
}
.maximate-actions button {
border: 1px solid #777;
background: #202020;
color: #fff;
border-radius: 4px;
padding: 2px 5px;
font-size: 11px;
}
.maximate-live-filter {
margin: 8px 0;
border: 1px solid rgba(255,255,255,.18);
background: rgba(22,22,22,.88);
color: #eee;
border-radius: 6px;
overflow: hidden;
box-shadow: 0 2px 10px rgba(0,0,0,.25);
font-size: 12px;
}
.maximate-live-filter .maximate-filter-title {
display: flex;
align-items: center;
justify-content: space-between;
gap: 8px;
padding: 7px 9px;
background: rgba(255,255,255,.07);
cursor: pointer;
font-weight: 700;
}
.maximate-live-filter .maximate-filter-count {
opacity: .78;
font-weight: 400;
font-size: 11px;
}
.maximate-live-filter .maximate-filter-content {
display: none;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 8px;
padding: 9px;
}
.maximate-live-filter.is-open .maximate-filter-content { display: grid; }
.maximate-filter-group strong {
display: block;
margin-bottom: 5px;
color: #f5f5f5;
font-size: 11px;
text-transform: uppercase;
letter-spacing: .02em;
opacity: .82;
}
.maximate-filter-options {
display: flex;
flex-wrap: wrap;
gap: 5px;
}
.maximate-filter-options label {
display: inline-flex;
align-items: center;
gap: 4px;
border: 1px solid rgba(255,255,255,.18);
background: rgba(255,255,255,.06);
border-radius: 4px;
padding: 4px 6px;
line-height: 1;
white-space: nowrap;
cursor: pointer;
}
.maximate-filter-options label:has(input:checked) {
border-color: #e15d3f;
background: rgba(225,93,63,.22);
color: #fff;
}
.maximate-filter-options input {
width: 12px;
height: 12px;
margin: 0;
accent-color: #e15d3f;
}
@media screen and (max-width: 760px) {
.maximate-live-filter .maximate-filter-content {
grid-template-columns: 1fr;
}
}
body.maximate-compact .members-list .bs,
body.maximate-compact .chain-attacks-list .bs {
width: 32px !important;
min-width: 32px !important;
font-size: 11px;
}
`);
if (IS_MOBILE && settings.compactMobile) document.body.classList.add("maximate-compact");
waitForElement(".header-menu .menu-items .menu-item-link a[href='/discord']").then((menuItem) => {
menuItem.innerText = "Max";
menuItem.onclick = showSettingsPanel;
});
const settingsButton = document.createElement("button");
settingsButton.type = "button";
settingsButton.className = "maximate-float-btn";
settingsButton.textContent = API_KEY ? "War" : "War key";
settingsButton.onclick = showSettingsPanel;
document.body.appendChild(settingsButton);
};
const battlestatCache = {
factionStamps: readJson("cache.ts", safeJson(localStorage["miz.cache.ts"], {})),
battleStats: readJson("cache.bs", safeJson(localStorage["miz.cache.bs"], {})),
save() {
writeJson("cache.bs", this.battleStats);
writeJson("cache.ts", this.factionStamps);
},
get(id) {
return this.battleStats[String(id)];
},
clearAll() {
this.factionStamps = {};
this.battleStats = {};
this.save();
},
async loadFaction(id, force = false) {
if (!API_KEY || !id) return;
if (!force && this.factionStamps[id] && this.factionStamps[id] + cacheLifetime() > Date.now()) return;
try {
const response = await requestText(`https://www.tornstats.com/api/v2/${API_KEY}/spy/faction/${id}`);
const data = safeJson(response);
if ((data && data.status === false) || (data && data.error)) {
apiStatus.tornStats = "Error";
apiStatus.lastError = data.message || (data.error && data.error.error) || "TornStats request failed";
return;
}
if (!data || !data.faction || !data.faction.members) {
apiStatus.tornStats = "No spy data";
return;
}
apiStatus.tornStats = "OK";
this.factionStamps[id] = Date.now();
Object.entries(data.faction.members).forEach(([userId, member]) => {
if (member && member.spy) this.battleStats[userId] = member.spy;
});
this.save();
} catch (e) {
apiStatus.tornStats = "Error";
apiStatus.lastError = (e && e.message) || "TornStats request failed";
}
},
init() {
if (!settings.battlestats) return;
watchElements("a[href^='/factions.php?step=profile&ID=']", async (facLink) => {
const href = facLink.getAttribute("href");
const factionId = href && href.replace(/.*?ID=(\d+).*$/, "$1");
await this.loadFaction(factionId);
});
if (getPage() !== "faction") return;
waitForElement("#top-page-links-list").then((elm) => {
if (elm.querySelector(".miz-clear-bs-cache")) return;
const clearCacheButton = document.createElement("a");
clearCacheButton.href = "#";
clearCacheButton.className = "miz-clear-bs-cache right t-clear h c-pointer line-h24";
clearCacheButton.innerHTML = `<span class="icon-wrap svg-icon-wrap"><span class="link-icon-svg"></span></span><span>Clear BS Cache</span>`;
clearCacheButton.onclick = () => {
const facId = window.location.href.replace(/.*?ID=(\d+).*$/, "$1");
delete this.factionStamps[facId];
this.save();
window.location.reload();
return false;
};
elm.append(clearCacheButton);
});
}
};
const abbreviateStat = (value, total = false) => {
let stat = Number.parseInt(value, 10);
if (!Number.isFinite(stat) || stat === 0) return "N/A";
const unknown = stat < 0;
if (unknown) stat = Math.abs(stat);
const units = ["K", "M", "B", "T", "Q"];
for (const unit of units) {
stat /= 1000;
if (stat > 1000) continue;
const digits = total ? (stat >= 100 ? 0 : 1) : 2;
return `${stat.toFixed(digits)}${unit}${unknown ? "+" : ""}`;
}
return `${stat.toFixed(1)}Q${unknown ? "+" : ""}`;
};
const showBattlestats = (id, node) => {
if (!id || !node) return;
if (!settings.battlestats) return;
if (!API_KEY) {
node.innerHTML = "No key";
node.title = "No TornStats API key saved";
return;
}
const spy = battlestatCache.get(id);
if (!spy) {
node.innerHTML = apiStatus.tornStats === "Error" ? "API err" : "No spy";
node.title = apiStatus.lastError || "No cached spy data for this player";
setTimeout(() => showBattlestats(id, node), 5000);
return;
}
const rawTotal = spy.total > 0
? spy.total
: -(Number(spy.strength || 0) + Number(spy.defense || 0) + Number(spy.speed || 0) + Number(spy.dexterity || 0));
const stats = {
total: abbreviateStat(rawTotal, true),
strength: abbreviateStat(spy.strength),
defense: abbreviateStat(spy.defense),
speed: abbreviateStat(spy.speed),
dexterity: abbreviateStat(spy.dexterity)
};
let age = "";
const difference = (Date.now() / 1000) - Number(spy.timestamp || 0);
if (spy.timestamp) {
if (difference > 365 * 24 * 60 * 60) age = `${Math.floor(difference / (365 * 24 * 60 * 60))} years ago`;
else if (difference > 30 * 24 * 60 * 60) age = `${Math.floor(difference / (30 * 24 * 60 * 60))} months ago`;
else if (difference > 24 * 60 * 60) age = `${Math.floor(difference / (24 * 60 * 60))} days ago`;
else if (difference > 60 * 60) age = `${Math.floor(difference / (60 * 60))} hours ago`;
else if (difference > 60) age = `${Math.floor(difference / 60)} minutes ago`;
else age = `${Math.floor(difference)} seconds ago`;
}
const statHtml = `
<div class="finally-bs-stat">
<b>STR</b> <span>${stats.strength}</span><br/>
<b>DEF</b> <span>${stats.defense}</span><br/>
<b>SPD</b> <span>${stats.speed}</span><br/>
<b>DEX</b> <span>${stats.dexterity}</span><br/>
<hr/>
<b>TOT</b> <span class="total">${stats.total}</span><br/>
${age}
</div>`;
node.innerHTML = stats.total;
node.title = statHtml;
node.dataset.statHtml = statHtml;
node.onclick = (evt) => {
if (!IS_MOBILE || !settings.mobileTapStats) return;
evt.stopPropagation();
evt.preventDefault();
renderStatPopup(node);
};
};
const enableAttack = () => {
watchElements(".profile-button-attack", (elm) => {
elm.classList.remove("disabled");
const attackHref = elm.getAttribute("href");
elm.href = (attackHref && attackHref.replace("loader2.php?sid=getInAttack&user2ID=", "loader.php?sid=attack&user2ID=")) || elm.href;
elm.onclick = () => {
window.location = elm.href;
return false;
};
const miniProfile = elm.closest(".profile-mini-root");
const target = miniProfile && miniProfile.querySelector(".main-desc");
if (!target) return;
const id = elm.href.replace(/.*?user2ID=(\d+)/i, "$1");
const bsNode = document.createElement("span");
bsNode.className = "miz-bs-miniprofile";
target.appendChild(bsNode);
showBattlestats(id, bsNode);
if (typeof jQuery !== "undefined") {
jQuery(bsNode).tooltip({
tooltipClass: "white-tooltip miz-max-z",
position: {
my: "center bottom-20",
at: "center top"
}
});
}
});
};
const checkWallAndHosp = () => {
if (!settings.wallHosp) return;
let hospUntil = 0;
let wasInHosp = false;
let wasOnWall = false;
const userId = getUserId();
if (!userId || !API_KEY) return;
const updateHospTime = () => {
const span = document.getElementById("hosp-status");
if (!span || !wasInHosp) return;
const time = formatTime(hospUntil - (Date.now() / 1000));
if (time === null) {
span.innerHTML = "OUT OF HOSPITAL";
document.title = "OUT";
const row = span.parentElement && span.parentElement.parentElement;
if (row) {
row.className = row.className.replace(" red", " green");
row.classList.add("miz-refresh");
}
} else {
span.innerHTML = time;
}
};
const checkStatus = async () => {
const wallSpan = document.getElementById("wall-status");
if (!wallSpan) return;
let data = null;
try {
const response = await requestText(`https://api.torn.com/user/${userId}?selections=basic,icons&key=${API_KEY}`);
data = safeJson(response);
} catch (e) {
apiStatus.torn = "Error";
apiStatus.lastError = (e && e.message) || "Torn API request failed";
return;
}
if (!data || data.error) {
apiStatus.torn = "Error";
apiStatus.lastError = (data && data.error && data.error.error) || "Torn API request failed";
return;
}
apiStatus.torn = "OK";
const icon75 = data.icons && data.icons.icon75;
const icon76 = data.icons && data.icons.icon76;
const wallIcon = icon75 || icon76 || "";
if (wallIcon) {
const wallName = wallIcon.substring(Math.max(0, wallIcon.length - 3));
wasOnWall = true;
wallSpan.style.color = "red";
wallSpan.innerHTML = `ON WALL - <a style="text-decoration: none; color: red;" href="/city.php#terrName=${wallName}">${wallName}</a>`;
} else if (wasOnWall) {
wallSpan.style.color = "red";
wallSpan.innerHTML = "ON WALL";
} else {
wallSpan.style.color = "green";
wallSpan.innerHTML = "OFF WALL";
}
if (data.status && data.status.state === "Hospital") {
hospUntil = Number(data.status.until || 0);
wasInHosp = true;
} else if (wasInHosp) {
hospUntil = 0;
}
};
waitForElement("#defender").then(() => {
const titleH4 = document.querySelector("h4[class^=title___]");
const titleDiv = document.querySelector("div[class^=title___]");
if (titleH4) titleH4.insertAdjacentHTML("afterend", '<span id="wall-status"></span>');
if (titleDiv) titleDiv.insertAdjacentHTML("beforeend", '<span id="hosp-status"></span>');
const hospSpan = document.getElementById("hosp-status");
if (hospSpan) hospSpan.onclick = () => window.location.reload();
checkStatus();
setInterval(updateHospTime, 500);
setInterval(checkStatus, 6000);
});
};
const checkExecute = () => {
if (!settings.executeHelper) return;
waitForElement("#attacker .bonus-attachment-execute").then((elm) => {
const title = elm.getAttribute("title");
const percent = Number.parseFloat(title && title.replace(/.*\s([0-9]+)%.*/i, "$1"));
const targetHealthPercent = Number.isFinite(percent) ? percent / 100 : null;
if (!targetHealthPercent) return;
setInterval(() => {
const healthNode = document.querySelectorAll("[id^=player-health-value]")[1];
const health = healthNode && healthNode.innerText.split("/").map((x) => Number(x.replace(/,/g, "")));
if (!health || !health[1]) return;
if (health[0] / health[1] <= targetHealthPercent) {
const second = document.getElementById("weapon_second");
if (second) second.style.background = "red";
}
}, 500);
});
};
const fastAttack = () => {
if (!settings.fastAttack) return;
let loading = false;
let armed = false;
const armedButton = document.createElement("button");
armedButton.type = "button";
armedButton.className = "maximate-fast-armed";
armedButton.textContent = "Fast off";
armedButton.onclick = () => {
armed = !armed;
armedButton.classList.toggle("is-on", armed);
armedButton.textContent = armed ? "Fast on" : "Fast off";
};
document.body.appendChild(armedButton);
waitForElement("#defender").then((elm) => {
const buttonDiv = elm.querySelector("div[class^=dialogButtons_]");
document.querySelectorAll("#weapon_main, #weapon_second, #weapon_melee, #weapon_temp, #weapon_fists, #weapon_boot").forEach((btn) => {
btn.addEventListener("click", (evt) => {
const attack = buttonDiv && buttonDiv.querySelector(".torn-btn");
const label = (attack && attack.innerText.toLowerCase()) || "";
if (!armed) return;
if (!loading && attack && !evt.target.className.includes("ammo") && (label.startsWith("start fight") || label.startsWith("join"))) {
loading = true;
attack.click();
} else if (!loading && (elm.querySelector("[class^=dialog_] [class*=' red__']") || elm.querySelector("[class^=dialog_] .miz-refresh"))) {
loading = true;
window.location.reload();
}
});
});
});
};
const nameLink = () => {
const userId = getUserId();
if (!userId) return;
waitForElement("#defender [class^='userName___']").then((elm) => {
const name = elm.innerText;
elm.innerHTML = `<a style="color: var(--attack-header-text-color); text-decoration: none;" href="/profiles.php?XID=${userId}">${name}</a> <span class="miz-stats"></span>`;
const bsNode = elm.querySelector(".miz-stats");
showBattlestats(userId, bsNode);
bsNode.onclick = () => {
const facChat = document.querySelector("div[class*='_faction_'] textarea");
if (!facChat) return;
const hospStatus = document.getElementById("hosp-status");
facChat.value = `https://www.torn.com/loader.php?sid=attack&user2ID=${userId} - ${bsNode.innerText}: ${(hospStatus && hospStatus.innerText) || ""}`;
facChat.focus();
};
});
};
const targetQuickActions = () => {
if (!settings.quickActions) return;
const userId = getUserId();
if (!userId) return;
waitForElement("#defender [class^='userName___']").then((elm) => {
if (elm.querySelector(".maximate-actions")) return;
const attackUrl = `https://www.torn.com/loader.php?sid=attack&user2ID=${userId}`;
const profileUrl = `https://www.torn.com/profiles.php?XID=${userId}`;
const actions = document.createElement("span");
actions.className = "maximate-actions";
actions.innerHTML = `
<button type="button" data-action="profile">Profile</button>
<button type="button" data-action="attack">Attack</button>
<button type="button" data-action="copy">Copy</button>
<button type="button" data-action="chat">Chat</button>
`;
actions.addEventListener("click", (evt) => {
evt.stopPropagation();
evt.preventDefault();
const action = evt.target.dataset.action;
if (action === "profile") window.location.href = profileUrl;
if (action === "attack") window.location.href = attackUrl;
if (action === "copy") copyText(attackUrl);
if (action === "chat") {
const facChat = document.querySelector("div[class*='_faction_'] textarea");
if (!facChat) {
copyText(attackUrl);
return;
}
const statsNode = elm.querySelector(".miz-stats");
const hospNode = document.getElementById("hosp-status");
const stats = (statsNode && statsNode.innerText) || "";
const hosp = (hospNode && hospNode.innerText) || "";
facChat.value = `${attackUrl}${stats ? ` - ${stats}` : ""}${hosp ? `: ${hosp}` : ""}`;
facChat.focus();
}
});
elm.appendChild(actions);
});
};
const clickableAssists = () => {
if (!settings.clickableAssists) return;
const oldFetch = pageWindow.fetch;
const userId = getUserId();
let attackers = {};
if (!oldFetch) return;
pageWindow.fetch = async function (...args) {
const url = String(args[0]);
const response = await oldFetch.apply(this, args);
if (!/sid=attackData&mode=json&step=poll/.test(url)) return response;
response.clone().json().then((body) => {
if (!body || !body.DB || !body.DB.currentFightStatistics) return;
attackers = Object.fromEntries(
Object.entries(body.DB.currentFightStatistics).map(([id, value]) => [value.playername, Number.parseInt(id, 10)])
);
}).catch(() => {});
return response;
};
const addLinks = (elm) => {
const startText = elm.innerText;
for (const [name, id] of Object.entries(attackers)) {
if (String(id) === String(userId)) continue;
const bsNode = document.createElement("div");
showBattlestats(id, bsNode);
const newText = elm.innerText.replace(name, `<a class="miz-name" href="/profiles.php?XID=${id}">${name}</a> <a href="/loader.php?sid=attack&user2ID=${id}" class="miz-attack">${bsNode.outerHTML}</a>`);
if (newText !== startText) {
elm.innerHTML = newText;
return true;
}
}
return false;
};
watchElements("#react-root ul[class^='participants'] > li, #react-root ul[aria-describedby='log-header'] > li", (elm) => {
const name = elm.querySelector("[class^='playername']") || elm.querySelector("span[class^='message'] > span");
if (!name) return true;
return !addLinks(name);
});
};
const addLogIconTitles = () => {
watchElements("[class^='attacking-events-']", (elm) => {
elm.title = elm.className.replace("attacking-events-", "");
});
};
const wallTimers = () => {
if (!settings.wallTimers) return;
const updateTimer = (elm) => {
const ourCountNode = elm.querySelector("div.member-count.your-count > div.count");
const theirCountNode = elm.querySelector("div.member-count.enemy-count > div.count");
const defendingIcon = elm.querySelector("div.member-count.your-count > div.count > i");
const timerTextNode = elm.querySelector(".timer");
const scoreNode = elm.querySelector(".score");
const ourCount = Number.parseInt(ourCountNode && ourCountNode.innerText, 10);
const theirCount = Number.parseInt(theirCountNode && theirCountNode.innerText, 10);
const defending = !!(defendingIcon && defendingIcon.classList.contains("shield-icon"));
const timerText = (timerTextNode && timerTextNode.innerText) || "";
const timerArray = timerText.split(":").map(Number);
const score = (scoreNode && scoreNode.innerText.replace(/,/g, "")) || "";
const match = score.match(/(\d+) ?\/ ?(\d+)/);
const timerNode = elm.querySelector(".miz-wall-timer");
if (!timerNode || timerArray.length < 4 || !match || !Number.isFinite(ourCount) || !Number.isFinite(theirCount)) return;
const timeLeft = timerArray[0] * 86400 + timerArray[1] * 3600 + timerArray[2] * 60 + timerArray[3];
const scoreCurrent = Number(match[2]) - Number(match[1]);
const slotsMax = Number(match[2]) * 2 / 100000;
const diffCount = Math.abs(ourCount - theirCount);
const durationMin = Math.ceil(scoreCurrent / slotsMax);
const successCurrent = diffCount ? Math.ceil(scoreCurrent / diffCount) : Infinity;
let textCurrent = "";
if (defending) {
if (durationMin > timeLeft) textCurrent = "WON";
else textCurrent = ourCount >= theirCount ? `WIN ${formatTime(timeLeft - durationMin)}` : `LOSE ${formatTime(successCurrent) || "∞"}`;
} else if (durationMin > timeLeft) {
textCurrent = "LOST";
} else {
textCurrent = ourCount > theirCount ? `WIN ${formatTime(successCurrent) || "∞"}` : `LOSE ${formatTime(timeLeft - durationMin)}`;
}
timerNode.innerHTML = textCurrent;
timerNode.title = durationMin < 0 ? `FAILED: ${formatTime(Math.abs(durationMin))}` : `MIN: ${formatTime(durationMin)}`;
};
watchElements("[class^='status-wrap territoryBox']", (elm) => {
const timerNode = elm.querySelector(".timer");
if (timerNode) timerNode.insertAdjacentHTML("afterend", '<div class="miz-wall-timer">MWT</div>');
updateTimer(elm);
setInterval(() => updateTimer(elm), 1000);
});
};
const factionBattlestats = () => {
if (!settings.battlestats) return;
const previousSort = Number.parseInt(readStorage("faction.sort", localStorage.getItem("miz.faction.sort") || ""), 10) || undefined;
const sortStats = (node, sort) => {
if (!node) return;
const sortIcon = node.parentNode.querySelector(".bs > [class*='sortIcon']");
if (sort) node.finallySort = sort;
else if (node.finallySort === undefined) node.finallySort = 2;
else if (++node.finallySort > 2) node.finallySort = sortIcon ? 1 : 0;
if (sortIcon) {
sortIcon.classList.toggle("finally-bs-activeIcon", node.finallySort > 0);
sortIcon.classList.toggle("finally-bs-asc", node.finallySort === 1);
sortIcon.classList.toggle("finally-bs-desc", node.finallySort === 2);
}
const rows = Array.from(node.querySelectorAll(".table-body > .table-row, .your:not(.row-animation-new), .enemy:not(.row-animation-new)"));
rows.forEach((row, index) => {
if (row.finallyPos === undefined) row.finallyPos = index;
});
rows.sort((a, b) => {
const linkA = a.querySelector('a[href*="XID"]');
const linkB = b.querySelector('a[href*="XID"]');
const idA = linkA && linkA.href.replace(/.*?XID=(\d+)/i, "$1");
const idB = linkB && linkB.href.replace(/.*?XID=(\d+)/i, "$1");
const statsA = battlestatCache.get(idA);
const statsB = battlestatCache.get(idB);
const totalA = (statsA && statsA.total) || a.finallyPos;
const totalB = (statsB && statsB.total) || b.finallyPos;
if (node.finallySort === 1) {
if (totalA <= 100) return 1;
if (totalB <= 100) return -1;
return totalA > totalB ? 1 : -1;
}
if (node.finallySort === 2) return totalB > totalA ? 1 : -1;
return a.finallyPos > b.finallyPos ? 1 : -1;
}).forEach((row) => row.parentNode.appendChild(row));
};
const addHeader = (elm) => {
const titleNode = elm.querySelector(".table-header") || (elm.parentNode && elm.parentNode.querySelector(".title, .c-pointer"));
if (!titleNode || titleNode.querySelector(".bs")) return;
const lvNode = titleNode.querySelector(".level");
if (lvNode && lvNode.childNodes[0]) lvNode.childNodes[0].nodeValue = "Lv";
const bsNode = lvNode ? lvNode.cloneNode(true) : document.createElement("li");
bsNode.classList.add("bs");
if (bsNode.childNodes[0]) bsNode.childNodes[0].nodeValue = "BS";
else bsNode.innerHTML = "BS";
titleNode.insertBefore(bsNode, titleNode.querySelector(".user-icons, .points, .member-icons"));
bsNode.addEventListener("click", () => sortStats(elm));
Array.from(titleNode.children).forEach((child, index) => {
child.addEventListener("click", (e) => {
setTimeout(() => {
const sortIcon = e.target.querySelector("[class*='sortIcon']");
const desc = !sortIcon || sortIcon.className.indexOf("desc") === -1;
writeStorage("faction.sort", desc ? index + 1 : -(index + 1));
}, 100);
});
});
if (Math.abs(previousSort) === 3) sortStats(elm, previousSort > 0 ? 2 : 1);
};
const showStats = (elm) => {
const idElms = elm.querySelectorAll('a[href*="XID"]');
if (!idElms.length) return;
const id = idElms[idElms.length - 1].href.replace(/.*?XID=(\d+)/i, "$1");
let bsNode = elm.querySelector(".bs");
if (!bsNode) {
bsNode = document.createElement("div");
bsNode.className = "table-cell bs level lvl left iconShow finally-bs-col";
const iconsNode = elm.querySelector(".user-icons, .member-icons, .points, .respect");
if (!iconsNode) return;
iconsNode.parentNode.insertBefore(bsNode, iconsNode);
bsNode.classList.add("miz-recent-attacks");
bsNode.addEventListener("click", () => window.open(`loader.php?sid=attack&user2ID=${id}`, "_blank"));
bsNode.addEventListener("dblclick", () => window.open(`loader.php?sid=attack&user2ID=${id}`, "_blank"));
}
showBattlestats(id, bsNode);
const onlineStatusNodes = elm.querySelectorAll("div[class^='userStatusWrap'], div[class*=' userStatusWrap'], .member.icons li.iconShow");
const onlineStatusNode = onlineStatusNodes[onlineStatusNodes.length - 1];
if (onlineStatusNode) onlineStatusNode.addEventListener("click", () => {
const facChat = document.querySelector("div[class*='_faction_'] textarea");
if (!facChat) return;
facChat.value = `https://www.torn.com/loader.php?sid=attack&user2ID=${id} - ${bsNode.innerText}`;
facChat.focus();
});
};
watchElements(".members-list, ul.chain-attacks-list.recent-attacks, ul.chain-attacks-list.current-attacks", (elm) => {
addHeader(elm);
watchElements(".your, .enemy, .table-body > .table-row, :scope > li[class='']", showStats, elm);
});
};
const liveMemberFilter = () => {
if (!settings.liveFilter) return;
const selected = (ids) => ids.filter((id) => document.querySelector(`#${id}:checked`)).map((id) => id.replace("miz-", ""));
const isChecked = (id) => (readStorage(`filter-${id}`, localStorage[`filter-${id}`] || "") === "true") ? ' checked="checked"' : "";
const checkedCount = () => document.querySelectorAll(".maximate-live-filter input[type=checkbox]:checked").length;
const filterRow = (row) => {
if (!row) return;
const states = selected(["miz-online", "miz-offline", "miz-idle"]);
const wallStates = selected(["miz-walloff", "miz-walldef", "miz-wallass"]);
const statuses = selected(["miz-okay", "miz-hospital", "miz-jail", "miz-abroad", "miz-traveling"]);
const stateCell = row.querySelector("[class*=userStatusWrap__]");
const state = stateCell && stateCell.id.includes("online") ? "online"
: stateCell && stateCell.id.includes("offline") ? "offline"
: stateCell && stateCell.id.includes("idle") ? "idle"
: null;
const iconCell = row.querySelector(".table-cell.member-icons.icons");
const wallState = !(iconCell && iconCell.querySelector("[id^=icon75], [id^=icon76]")) ? "walloff"
: iconCell.querySelector("[id^=icon75]") ? "walldef"
: "wallass";
const statusCell = row.querySelector(".table-cell.status span");
const status = ["okay", "hospital", "jail", "abroad", "traveling"].find((x) => statusCell && statusCell.classList.contains(x));
row.style.display = (!states.length || states.includes(state))
&& (!wallStates.length || wallStates.includes(wallState))
&& (!statuses.length || statuses.includes(status))
? "flex"
: "none";
};
const filterRows = () => document.querySelectorAll(".members-list > ul.table-body > li.table-row").forEach(filterRow);
const updateFilterCount = () => {
const countNode = document.querySelector(".maximate-filter-count");
if (!countNode) return;
const count = checkedCount();
countNode.textContent = count ? `${count} active` : "No filters";
};
waitForElement(".faction-info-wrap .members-list").then((elm) => {
if (document.querySelector(".miz-live-filter")) return;
const filterDiv = document.createElement("div");
filterDiv.className = "miz-live-filter maximate-live-filter is-open";
filterDiv.innerHTML = `
<div class="maximate-filter-title">
<span>War Live Filter</span>
<span class="maximate-filter-count">No filters</span>
</div>
<div class="maximate-filter-content">
<div class="maximate-filter-group">
<strong>Activity</strong>
<div class="maximate-filter-options">
<label><input id="miz-online" type="checkbox"${isChecked("miz-online")}>Online</label>
<label><input id="miz-idle" type="checkbox"${isChecked("miz-idle")}>Idle</label>
<label><input id="miz-offline" type="checkbox"${isChecked("miz-offline")}>Offline</label>
</div>
</div>
<div class="maximate-filter-group">
<strong>Wall Status</strong>
<div class="maximate-filter-options">
<label><input id="miz-walloff" type="checkbox"${isChecked("miz-walloff")}>Off wall</label>
<label><input id="miz-walldef" type="checkbox"${isChecked("miz-walldef")}>Defending</label>
<label><input id="miz-wallass" type="checkbox"${isChecked("miz-wallass")}>Assaulting</label>
</div>
</div>
<div class="maximate-filter-group">
<strong>Status</strong>
<div class="maximate-filter-options">
<label><input id="miz-okay" type="checkbox"${isChecked("miz-okay")}>Okay</label>
<label><input id="miz-hospital" type="checkbox"${isChecked("miz-hospital")}>Hospital</label>
<label><input id="miz-jail" type="checkbox"${isChecked("miz-jail")}>Jail</label>
<label><input id="miz-abroad" type="checkbox"${isChecked("miz-abroad")}>Abroad</label>
<label><input id="miz-traveling" type="checkbox"${isChecked("miz-traveling")}>Traveling</label>
</div>
</div>
</div>`;
elm.before(filterDiv);
const observer = new MutationObserver((mutations) => {
const target = mutations[0] && mutations[0].target;
const row = target && target.closest && target.closest(".table-row");
filterRow(row);
});
elm.querySelectorAll(".table-row").forEach((node) => {
observer.observe(node, { attributes: true, childList: true, subtree: true });
});
filterDiv.querySelectorAll("input[type=checkbox]").forEach((node) => {
node.addEventListener("change", (evt) => {
writeStorage(`filter-${evt.target.id}`, evt.target.checked);
filterRows();
updateFilterCount();
});
});
filterDiv.querySelector(".maximate-filter-title").onclick = () => {
filterDiv.classList.toggle("is-open");
};
filterRows();
updateFilterCount();
});
};
const init = () => {
if (initialized) return;
initialized = true;
battlestatCache.init();
enableAttack();
switch (getPage()) {
case "loader":
checkWallAndHosp();
checkExecute();
nameLink();
targetQuickActions();
clickableAssists();
fastAttack();
break;
case "attack-log":
addLogIconTitles();
break;
case "faction":
wallTimers();
factionBattlestats();
liveMemberFilter();
break;
default:
break;
}
};
injectInitialHtml();
init();
})();