Torn Execute Tracker

Inline execute perk tracker - injects directly into Torn's attack UI

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Userscripts ,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==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);

})();