Torn Weapon Effects Highlighter

Highlights and explain on hover effects.

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

})();