Inline execute perk tracker - injects directly into Torn's attack UI
// ==UserScript==
// @name Torn Execute Tracker
// @namespace torn-execute-tracker
// @version 2.1
// @description Inline execute perk tracker - injects directly into Torn's attack UI
// @author Solenya [4053619]
// @match https://www.torn.com/page.php?sid=attack*
// @grant GM_getValue
// @grant GM_setValue
// @run-at document-idle
// ==/UserScript==
(function () {
'use strict';
let executeThreshold = parseFloat(GM_getValue('et_threshold', 25));
function detectExecuteBonus() {
// 1. Check by class name first (most reliable)
const byClass = document.querySelector('[class*="bonus-attachment-execute"]');
if (byClass) {
const desc = byClass.getAttribute('data-bonus-attachment-description') || '';
const match = desc.match(/(\d+)%/);
if (match) return parseInt(match[1]);
}
const bonusEls = document.querySelectorAll('[data-bonus-attachment-description]');
for (const el of bonusEls) {
const desc = el.getAttribute('data-bonus-attachment-description') || '';
const match = desc.match(/execute\D+(\d+)%/i) || desc.match(/(\d+)%\D+execute/i);
if (match) return parseInt(match[1]);
}
return null;
}
function getEnemyHealth() {
const enemyWrapper = document.querySelector('[class*="headerWrapper"][class*="rose"]');
if (!enemyWrapper) return null;
const healthIcon = enemyWrapper.querySelector('[class*="iconHealth"]');
if (!healthIcon) return null;
const span = healthIcon.nextElementSibling;
if (!span) return null;
const text = span.textContent.trim();
const match = text.match(/([\d,]+)\s*\/\s*([\d,]+)/);
if (!match) return null;
return {
current: parseInt(match[1].replace(/,/g, '')),
max: parseInt(match[2].replace(/,/g, '')),
};
}
const style = document.createElement('style');
style.textContent = `
/* Tick mark on the health bar */
#et-tick {
position: absolute;
top: 0;
width: 2px;
height: 100%;
background: #58a6ff;
border-radius: 1px;
pointer-events: none;
z-index: 10;
transition: left 0.2s;
}
/* Container injected below the health bar */
#et-inline-wrap {
padding: 4px 10px 5px 10px;
display: flex;
flex-direction: column;
gap: 3px;
}
/* Status row: current % on left, threshold badge on right */
#et-status-row {
display: flex;
align-items: center;
justify-content: space-between;
font-size: 11px;
}
#et-pct-label {
color: #8b949e;
font-weight: 500;
font-family: 'Segoe UI', system-ui, sans-serif;
min-width: 38px;
}
#et-threshold-badge {
display: flex;
align-items: center;
gap: 4px;
background: rgba(88,166,255,0.1);
border: 1px solid rgba(88,166,255,0.25);
border-radius: 4px;
padding: 1px 6px;
font-size: 10px;
color: #58a6ff;
font-family: 'Segoe UI', system-ui, sans-serif;
cursor: pointer;
user-select: none;
transition: background 0.15s, border-color 0.15s;
}
#et-threshold-badge:hover {
background: rgba(88,166,255,0.2);
border-color: rgba(88,166,255,0.5);
}
#et-threshold-badge svg {
opacity: 0.6;
flex-shrink: 0;
}
/* Execute alert — hidden by default */
#et-execute-alert {
display: none;
padding: 5px 10px;
background: #da3633;
border-radius: 0 0 4px 4px;
text-align: center;
font-weight: 700;
font-size: 12px;
letter-spacing: 0.5px;
color: #fff;
font-family: 'Segoe UI', system-ui, sans-serif;
animation: et-pulse 0.55s ease-in-out infinite alternate;
}
#et-execute-alert.et-visible { display: block; }
@keyframes et-pulse {
from { background: #da3633; }
to { background: #f85149; }
}
/* Settings panel */
#et-settings {
display: none;
align-items: center;
gap: 5px;
padding: 4px 0 2px 0;
flex-wrap: wrap;
}
#et-settings.et-open { display: flex; }
#et-settings label {
font-size: 11px;
color: #8b949e;
font-family: 'Segoe UI', system-ui, sans-serif;
white-space: nowrap;
}
#et-threshold-input {
width: 46px;
background: rgba(255,255,255,0.06);
border: 1px solid #30363d;
color: #e6edf3;
border-radius: 4px;
padding: 2px 5px;
font-size: 12px;
text-align: center;
font-family: 'Segoe UI', system-ui, sans-serif;
}
#et-threshold-input:focus {
outline: none;
border-color: #58a6ff;
}
#et-save-btn, #et-detect-btn {
background: rgba(255,255,255,0.06);
border: 1px solid #30363d;
color: #c9d1d9;
border-radius: 4px;
padding: 2px 7px;
font-size: 11px;
cursor: pointer;
font-family: 'Segoe UI', system-ui, sans-serif;
transition: background 0.15s, border-color 0.15s;
white-space: nowrap;
}
#et-save-btn:hover { background: #1f6feb; border-color: #1f6feb; color: #fff; }
#et-detect-btn:hover { background: rgba(255,255,255,0.1); border-color: #484f58; }
#et-settings-msg {
font-size: 10px;
color: #d29922;
font-family: 'Segoe UI', system-ui, sans-serif;
width: 100%;
}
/* Make the Torn health bar wrap position:relative so the tick works */
[class*="pbWrap"] {
position: relative !important;
}
`;
document.head.appendChild(style);
// ─── Build + inject inline UI into the enemy header ────────────────────────
let injected = false;
function injectUI() {
if (injected) return true;
const bottomSection = document.querySelector('[class*="bottomSection"]');
if (!bottomSection) return false;
// Try to place tick on enemy health bar if it already exists
const pbWrap = document.querySelector('[class*="headerWrapper"][class*="rose"] [class*="pbWrap"]');
if (pbWrap && !document.getElementById('et-tick')) {
const tick = document.createElement('div');
tick.id = 'et-tick';
tick.style.left = `${executeThreshold}%`;
pbWrap.appendChild(tick);
}
const wrap = document.createElement('div');
wrap.id = 'et-inline-wrap';
wrap.innerHTML = `
<div id="et-status-row">
<span id="et-pct-label">—</span>
<span id="et-threshold-badge" title="Click to change threshold">
<svg width="9" height="9" viewBox="0 0 16 16" fill="currentColor">
<path d="M8 4.754a3.246 3.246 0 1 0 0 6.492 3.246 3.246 0 0 0 0-6.492zM5.754 8a2.246 2.246 0 1 1 4.492 0 2.246 2.246 0 0 1-4.492 0z"/>
<path d="M9.796 1.343c-.527-1.79-3.065-1.79-3.592 0l-.094.319a.873.873 0 0 1-1.255.52l-.292-.16c-1.64-.892-3.433.902-2.54 2.541l.159.292a.873.873 0 0 1-.52 1.255l-.319.094c-1.79.527-1.79 3.065 0 3.592l.319.094a.873.873 0 0 1 .52 1.255l-.16.292c-.892 1.64.901 3.434 2.541 2.54l.292-.159a.873.873 0 0 1 1.255.52l.094.319c.527 1.79 3.065 1.79 3.592 0l.094-.319a.873.873 0 0 1 1.255-.52l.292.16c1.64.893 3.434-.902 2.54-2.541l-.159-.292a.873.873 0 0 1 .52-1.255l.319-.094c1.79-.527 1.79-3.065 0-3.592l-.319-.094a.873.873 0 0 1-.52-1.255l.16-.292c.893-1.64-.902-3.433-2.541-2.54l-.292.159a.873.873 0 0 1-1.255-.52l-.094-.319zm-2.633.283c.246-.835 1.428-.835 1.674 0l.094.319a1.873 1.873 0 0 0 2.693 1.115l.291-.16c.764-.415 1.6.42 1.184 1.185l-.159.292a1.873 1.873 0 0 0 1.116 2.692l.318.094c.835.246.835 1.428 0 1.674l-.319.094a1.873 1.873 0 0 0-1.115 2.693l.16.291c.415.764-.42 1.6-1.185 1.184l-.291-.159a1.873 1.873 0 0 0-2.693 1.116l-.094.318c-.246.835-1.428.835-1.674 0l-.094-.319a1.873 1.873 0 0 0-2.692-1.115l-.292.16c-.764.415-1.6-.42-1.184-1.185l.159-.291A1.873 1.873 0 0 0 1.945 8.93l-.319-.094c-.835-.246-.835-1.428 0-1.674l.319-.094A1.873 1.873 0 0 0 3.06 4.474l-.16-.292c-.415-.764.42-1.6 1.185-1.184l.292.159a1.873 1.873 0 0 0 2.692-1.115l.094-.319z"/>
</svg>
Execute ≤ ${executeThreshold}%
</span>
</div>
<div id="et-settings">
<label>Threshold %</label>
<input id="et-threshold-input" type="number" min="1" max="99" value="${executeThreshold}">
<button id="et-detect-btn">Auto-detect</button>
<button id="et-save-btn">Save</button>
<span id="et-settings-msg"></span>
</div>
<div id="et-execute-alert">⚔ EXECUTE!</div>
`;
bottomSection.appendChild(wrap);
// ── Wire up settings toggle
document.getElementById('et-threshold-badge').addEventListener('click', () => {
document.getElementById('et-settings').classList.toggle('et-open');
document.getElementById('et-settings-msg').textContent = '';
});
// ── Save
document.getElementById('et-save-btn').addEventListener('click', () => {
const val = parseFloat(document.getElementById('et-threshold-input').value);
const msg = document.getElementById('et-settings-msg');
if (isNaN(val) || val <= 0 || val >= 100) {
msg.style.color = '#f85149';
msg.textContent = '⚠ Enter 1–99';
return;
}
executeThreshold = val;
GM_setValue('et_threshold', val);
document.getElementById('et-tick').style.left = `${val}%`;
document.getElementById('et-threshold-badge').lastChild.textContent = ` Execute ≤ ${val}%`;
msg.style.color = '#238636';
msg.textContent = '✓ Saved';
updateDisplay();
setTimeout(() => {
document.getElementById('et-settings').classList.remove('et-open');
}, 700);
});
// ── Auto-detect
document.getElementById('et-detect-btn').addEventListener('click', () => {
const msg = document.getElementById('et-settings-msg');
const detected = detectExecuteBonus();
if (detected !== null) {
document.getElementById('et-threshold-input').value = detected;
msg.style.color = '#238636';
msg.textContent = `Detected: ${detected}%`;
} else {
msg.style.color = '#f85149';
msg.textContent = 'No execute bonus found';
}
});
injected = true;
return true;
}
// ─── Update the inline display ─────────────────────────────────────────────
function updateDisplay() {
if (!injected) return;
const hp = getEnemyHealth();
const pctEl = document.getElementById('et-pct-label');
const alertEl = document.getElementById('et-execute-alert');
if (!pctEl || !alertEl) return;
if (!hp || hp.max === 0) {
pctEl.textContent = '—';
alertEl.classList.remove('et-visible');
return;
}
const pct = (hp.current / hp.max) * 100;
pctEl.textContent = pct.toFixed(1) + '%';
if (pct <= executeThreshold) {
alertEl.classList.add('et-visible');
pctEl.style.color = '#f85149';
} else {
alertEl.classList.remove('et-visible');
pctEl.style.color = pct <= executeThreshold * 1.5 ? '#d29922' : '#8b949e';
}
}
// ─── Targeted observer on enemy HP span only ───────────────────────────────
let activeObserver = null;
function attachObserver() {
if (activeObserver) { activeObserver.disconnect(); activeObserver = null; }
const enemyWrapper = document.querySelector('[class*="headerWrapper"][class*="rose"]');
if (!enemyWrapper) return false;
const healthIcon = enemyWrapper.querySelector('[class*="iconHealth"]');
if (!healthIcon) return false;
const span = healthIcon.nextElementSibling;
if (!span) return false;
activeObserver = new MutationObserver(updateDisplay);
activeObserver.observe(span, { characterData: true, childList: true, subtree: true });
return true;
}
// ─── Init loop: wait for enemy panel, inject, attach observer ─────────────
const initInterval = setInterval(() => {
if (injectUI()) {
clearInterval(initInterval);
attachObserver();
updateDisplay();
// Auto-detect execute bonus once weapons are rendered
setTimeout(() => {
const detected = detectExecuteBonus();
if (detected !== null && detected !== executeThreshold) {
executeThreshold = detected;
GM_setValue('et_threshold', detected);
const tick = document.getElementById('et-tick');
const badge = document.getElementById('et-threshold-badge');
const input = document.getElementById('et-threshold-input');
if (tick) tick.style.left = `${detected}%`;
if (input) input.value = detected;
if (badge) badge.lastChild.textContent = ` Execute ≤ ${detected}%`;
updateDisplay();
} else if (detected === null) {
// No execute bonus — open settings so user can set manually
const settings = document.getElementById('et-settings');
const msg = document.getElementById('et-settings-msg');
if (settings) settings.classList.add('et-open');
if (msg) { msg.style.color = '#d29922'; msg.textContent = '⚠ Set % manually'; }
}
}, 1500);
}
}, 500);
// Re-check every 4s in case a new fight starts (panel gets replaced in DOM)
setInterval(() => {
if (!activeObserver || !document.getElementById('et-inline-wrap')) {
injected = false;
injectUI();
attachObserver();
}
// Re-try tick if enemy panel appeared after the initial inject
if (!document.getElementById('et-tick')) {
const pbWrap = document.querySelector('[class*="headerWrapper"][class*="rose"] [class*="pbWrap"]');
if (pbWrap) {
const tick = document.createElement('div');
tick.id = 'et-tick';
tick.style.left = `${executeThreshold}%`;
pbWrap.appendChild(tick);
}
}
updateDisplay();
}, 4000);
})();