Greasy Fork is available in English.
Add Execute Under X HP to Secondary Weapon - Changes colour when ready
// ==UserScript==
// @name Execute HP
// @namespace http://tampermonkey.net/
// @version 1.4.0
// @description Add Execute Under X HP to Secondary Weapon - Changes colour when ready
// @author Stig [2648238]
// @match https://www.torn.com/page.php?sid=attack&user2ID=*
// @grant GM_registerMenuCommand
// ==/UserScript==
(function () {
'use strict';
const STORAGE_KEY = 'executeHP_percent';
const DEFAULT_PERCENT = 29;
function readPercent() {
const raw = parseFloat(localStorage.getItem(STORAGE_KEY));
return Number.isFinite(raw) && raw > 0 && raw <= 100 ? raw : null;
}
function getPercent() {
return readPercent() ?? DEFAULT_PERCENT;
}
function setPercent(value) {
localStorage.setItem(STORAGE_KEY, String(value));
}
// Validate and store a user-entered value. Returns the parsed number, or
// null if invalid (caller decides what to do).
function commitPercent(input) {
const v = parseFloat(input);
if (!Number.isFinite(v) || v <= 0 || v > 100) return null;
setPercent(v);
return v;
}
// Prompt the user for a percentage. Used on first run and from the menu.
// Returns true if a new value was saved.
function promptForPercent() {
const current = readPercent();
const answer = window.prompt(
'Execute HP — enter your execute % (1–100):',
current !== null ? String(current) : String(DEFAULT_PERCENT)
);
if (answer === null) return false; // user cancelled
const saved = commitPercent(answer.trim());
if (saved === null) {
window.alert('Invalid value — please enter a number between 1 and 100.');
return false;
}
return true;
}
const ceilPct = (value, pct) => Math.ceil(value * (pct / 100));
const sel = {
opponentHeader: 'div[class*="headerWrapper"][class*="rose"]',
healthIcon: '[class*="iconHealth"]',
weaponSecond: '#weapon_second',
weaponBottom: '[class*="bottom"]',
};
function getHealthSpan() {
const header = document.querySelector(sel.opponentHeader);
if (!header) return null;
const entry = header.querySelector(sel.healthIcon)?.closest('[class*="entry"]');
return entry?.querySelector('span') ?? null;
}
function getHealth() {
const span = getHealthSpan();
if (!span) return { current: 0, max: 0 };
const [current, max] = span.textContent
.split('/')
.map((v) => parseInt(v.replace(/,/g, '').trim(), 10));
return !isNaN(current) && !isNaN(max) ? { current, max } : { current: 0, max: 0 };
}
let injected = false;
let labelEl = null; // ref so menu/prompt changes refresh the display
function injectStyle() {
if (document.getElementById('execute-hp-style')) return;
const style = document.createElement('style');
style.id = 'execute-hp-style';
style.textContent = `
.custom-execute-under {
position: absolute;
top: 70px;
left: 24px;
font-size: 10px;
color: red;
font-weight: normal;
}
.custom-execute-under.ready {
color: #00FF00;
font-weight: bold;
left: 45px;
}
`;
document.head.appendChild(style);
}
function tryInject() {
if (injected) return true;
const weaponSecond = document.querySelector(sel.weaponSecond);
const targetDiv = weaponSecond?.querySelector(sel.weaponBottom);
if (!targetDiv) return false;
const { max } = getHealth();
if (max === 0) return false; // health not loaded yet; wait for next mutation
injectStyle();
const newDiv = document.createElement('div');
newDiv.className = 'custom-execute-under';
targetDiv.parentNode.insertBefore(newDiv, targetDiv.nextSibling);
labelEl = newDiv;
recompute();
// Observe the whole opponent header rather than the span itself:
// Torn replaces the span node on each hit, which would detach an
// observer bound directly to it.
const header = document.querySelector(sel.opponentHeader);
if (header) {
new MutationObserver(recompute).observe(header, {
childList: true,
subtree: true,
characterData: true,
});
}
injected = true;
return true;
}
function recompute() {
if (!labelEl) return;
const { current, max } = getHealth();
const underHP = ceilPct(max, getPercent());
if (current > 0 && current <= underHP) {
labelEl.textContent = 'Execute Now!';
labelEl.classList.add('ready');
} else {
labelEl.textContent = `Execute Under: ${underHP} HP`;
labelEl.classList.remove('ready');
}
}
// --- Tampermonkey menu command ---
if (typeof GM_registerMenuCommand === 'function') {
GM_registerMenuCommand('Set Execute %', () => {
if (promptForPercent()) recompute();
});
}
// --- First-run popup: only prompt if nothing valid is stored yet ---
if (readPercent() === null) {
promptForPercent(); // saves default if they accept/cancel into a valid state
}
if (!tryInject()) {
const observer = new MutationObserver(() => {
if (tryInject()) observer.disconnect();
});
observer.observe(document.body, { childList: true, subtree: true });
}
})();