Torn Weapon Effects Highlighter

Highlights and explain on hover effects.

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

You will need to install an extension such as Tampermonkey to install this script.

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

您需要先安裝使用者腳本管理器擴充功能後才能安裝該腳本。

(我已經安裝了使用者腳本管理器,讓我安裝!)

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

// ==UserScript==
// @name         Torn Weapon Effects Highlighter
// @namespace    http://tampermonkey.net/
// @version      1.1
// @description  Highlights and explain on hover effects.
// @author       aquagloop
// @match        https://www.torn.com/*
// @grant        none
// @license      MIT
// ==/UserScript==

(function() {
    'use strict';

    const weaponEffects = {
        'blindfire': 'Expends remaining ammunition in current clip with reduced accuracy',
        'burn': 'Burning DOT effect over 3 turns (45% initial damage)',
        'demoralized': '10% debuff to all opponent stats (stacks up to 5x)',
        'emasculate': 'Grants percentage of max happy on finishing hit',
        'freeze': '50% debuff to opponent speed and dexterity',
        'hazardous': 'Take percentage of damage you deal',
        'laceration': 'Devastating DOT effect over 9 turns (90% initial damage)',
        'poisoned': 'Long DOT effect over 19 turns (95% initial damage)',
        'severe burning': 'Short DOT effect over 3 turns (45% initial damage)',
        'shock': 'Causes opponent to miss next turn',
        'sleep': 'Enemy misses turns until damaged',
        'smash': 'Double damage but requires recharge turn',
        'spray': 'Empty full clip for double damage',
        'storage': 'Allows two temporary items in fight',
        'toxin': '25% debuff to random opponent stat (stacks up to 3x)',
        'achilles': 'Increased foot damage',
        'assassinate': 'Increased damage on first turn',
        'backstab': 'Double damage when opponent distracted',
        'berserk': 'Increased damage but reduced hit chance',
        'bleed': 'Bleeding DOT effect over 9 turns (45% initial damage)',
        'blindside': 'Increased damage if target has full life',
        'bloodlust': 'Life regenerated by percentage of damage dealt',
        'comeback': 'Increased damage while under 25% life',
        'conserve': 'Increased ammo conservation',
        'cripple': 'Reduces dexterity by 25% (stacks up to 3x for -75% total)',
        'crusher': 'Increased head damage',
        'cupid': 'Increased heart damage',
        'deadeye': 'Increased critical hit damage',
        'deadly': 'Chance of deadly hit (+500% damage)',
        'disarm': 'Disables opponent weapon for multiple turns',
        'double-edged': 'Double damage at cost of self injury',
        'double tap': 'Hit twice in single turn',
        'empower': 'Increased strength while using weapon',
        'eviscerate': 'Opponent receives extra damage under effect',
        'execute': 'Instant defeat when opponent below threshold life',
        'expose': 'Increased critical hit rate',
        'finale': 'Increased damage for every turn weapon not used',
        'focus': 'Hit chance increase for successive misses',
        'frenzy': 'Damage and accuracy increase on successive hits',
        'fury': 'Hit twice in single turn',
        'grace': 'Increased hit chance but reduced damage',
        'home run': 'Deflect incoming temporary items',
        'irradiate': 'Apply radiation poisoning on finishing hit',
        'motivation': 'Increase all stats by 10% (stacks 5x)',
        'paralyzed': '50% chance of missing turns for 300 seconds',
        'parry': 'Block incoming melee attacks',
        'penetrate': 'Ignore percentage of enemy armor',
        'plunder': 'Increase money mugged on finishing hit',
        'powerful': 'Increased damage',
        'proficience': 'Increase XP gained on finishing hit',
        'puncture': 'Ignore armor completely',
        'quicken': 'Increased speed while using weapon',
        'rage': 'Hit 2-8 times in single turn',
        'revitalize': 'Restore energy spent attacking on finishing hit',
        'roshambo': 'Increased groin damage',
        'slow': 'Reduce opponent speed by 25% (stacks 3x)',
        'smurf': 'Damage increase for each level under opponent',
        'specialist': 'Increased damage but limited to single clip',
        'stricken': 'Increased hospital time on final hit',
        'stun': 'Cause opponent to miss next turn',
        'suppress': '25% chance for opponent to miss future turns',
        'sure shot': 'Guaranteed hit',
        'throttle': 'Increased throat damage',
        'warlord': 'Increases respect gained',
        'weaken': 'Reduce opponent defense by 25% (stacks 3x)',
        'wind-up': 'Increased damage after spending turn to wind up',
        'wither': 'Reduce opponent strength by 25% (stacks 3x)'
    };

    const effectPatterns = [
        { regex: /\bcrippled\b/gi, key: 'cripple' },
        { regex: /\bweakened\b/gi, key: 'weaken' },
        { regex: /\bwithered\b/gi, key: 'wither' },
        { regex: /\bslowed\b/gi, key: 'slow' },
        { regex: /\bdemoralized\b/gi, key: 'demoralized' },
        { regex: /\bfrozen\b/gi, key: 'freeze' },
        { regex: /\beviscerated\b/gi, key: 'eviscerate' },
        { regex: /\bstunned\b/gi, key: 'stun' },
        { regex: /\bpoisoned\b/gi, key: 'poisoned' },
        { regex: /\bbleeding\b/gi, key: 'bleed' },
        { regex: /\bburning\b/gi, key: 'burn' },
        { regex: /powerful hit/gi, key: 'powerful' },
        { regex: /double damage/gi, key: 'backstab' },
        { regex: /punctured through armor/gi, key: 'puncture' },
        { regex: /ignores armor/gi, key: 'puncture' },
        { regex: /deadly hit/gi, key: 'deadly' },
        { regex: /fired \d+ rounds.*hitting.*\d+ times/gi, key: 'rage' },
        { regex: /executed/gi, key: 'execute' },
        { regex: /finishing blow/gi, key: 'execute' }
    ];

    const css = `
        .wep-highlight {
            display: inline;
            background-color: #add8e6; /* Light Blue */
            color: #ffffff;             /* White Text */
            border: 1px solid #88b0c2;   /* Darker Blue Border */
            border-radius: 2px;
            padding: 0 2px;
            cursor: help;
        }
        .wep-highlight:hover {
            background-color: #87ceeb; /* Sky Blue on hover */
        }
    `;

    const styleTag = document.createElement('style');
    styleTag.textContent = css;
    document.head.appendChild(styleTag);

    function makeHighlightSpan(matchedText, effectKey) {
        const span = document.createElement('span');
        span.className = 'wep-highlight';
        span.setAttribute('title', `${effectKey.toUpperCase()}: ${weaponEffects[effectKey]}`);
        span.textContent = matchedText;
        return span;
    }

    function replaceInTextNodes(node, regex, effectKey) {
        if (node.nodeType === Node.ELEMENT_NODE && node.classList.contains('wep-highlight')) {
            return;
        }
        if (node.nodeType === Node.TEXT_NODE) {
            const text = node.nodeValue;
            if (!regex.test(text)) return;

            const frag = document.createDocumentFragment();
            let lastIndex = 0;

            text.replace(regex, (match, ...args) => {
                const matchIndex = args[args.length - 2];
                if (matchIndex > lastIndex) {
                    frag.appendChild(document.createTextNode(text.slice(lastIndex, matchIndex)));
                }
                const hlSpan = makeHighlightSpan(match, effectKey);
                frag.appendChild(hlSpan);
                lastIndex = matchIndex + match.length;
            });

            if (lastIndex < text.length) {
                frag.appendChild(document.createTextNode(text.slice(lastIndex)));
            }
            node.parentNode.replaceChild(frag, node);
            return;
        }

        if (node.nodeType === Node.ELEMENT_NODE) {
            Array.from(node.childNodes).forEach(child => replaceInTextNodes(child, regex, effectKey));
        }
    }

    function highlightInsideMessage(msgNode) {
        const rawText = msgNode.innerText;
        if (!rawText || !rawText.trim()) return;

        Object.keys(weaponEffects).forEach(key => {
            const regex = new RegExp(`\\b${key}\\b`, 'gi');
            if (regex.test(rawText)) {
                replaceInTextNodes(msgNode, regex, key);
            }
        });

        effectPatterns.forEach(({ regex, key }) => {
            if (regex.test(rawText)) {
                replaceInTextNodes(msgNode, regex, key);
            }
        });
    }

    function processNode(node) {
        if (node.nodeType !== Node.ELEMENT_NODE) return;

        const messageNodes = node.matches('.message')
            ? [node]
            : Array.from(node.querySelectorAll('.message'));

        messageNodes.forEach(highlightInsideMessage);
    }

    function processExistingLog() {
        document.querySelectorAll('.log-list.overview li').forEach(li => {
            const msg = li.querySelector('.message');
            if (msg) highlightInsideMessage(msg);
        });
    }

    function watchForNewLines() {
        const container = document.querySelector('.log-list.overview') || document.body;
        const observer = new MutationObserver(mutations => {
            mutations.forEach(mutation => {
                mutation.addedNodes.forEach(node => {
                    if (node.nodeType !== Node.ELEMENT_NODE) return;

                    if (node.tagName === 'LI') {
                        const msg = node.querySelector('.message');
                        if (msg) highlightInsideMessage(msg);
                    } else {
                        const newLis = node.querySelectorAll?.('li') || [];
                        newLis.forEach(li => {
                            const msg = li.querySelector('.message');
                            if (msg) highlightInsideMessage(msg);
                        });
                    }
                });
            });
        });
        observer.observe(container, { childList: true, subtree: true });
    }

    function init() {
        processExistingLog();
        watchForNewLines();
    }

    if (document.readyState === 'loading') {
        document.addEventListener('DOMContentLoaded', init);
    } else {
        init();
    }

})();