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

})();