Tricks of the Trade

Assign Tricks of the Trade damage to the rogue who cast it on Warcraft Logs

// ==UserScript==
// @name         Tricks of the Trade
// @version      0.1.4
// @grant        none
// @author       Raul
// @license      GPL-3.0-or-later
// @description  Assign Tricks of the Trade damage to the rogue who cast it on Warcraft Logs
// @match        *://*.warcraftlogs.com/reports/*
// @namespace    https://github.com/rfere/tricks-of-the-trade
// @supportURL   https://github.com/rfere/tricks-of-the-trade
// @icon         https://wow.zamimg.com/images/wow/icons/large/ability_rogue_tricksofthetrade.jpg
// ==/UserScript==

(function() {
    'use strict';

    const tricksRegexp = /.+ \((.+)\)/
    const locale = window.location.hostname.match(/(\w+).+/)[1];

    const localeMap = {
        'classic': {
            'millions': 'm',
            'thousands': 'k'
        },
        'de': {
            'millions': 'Mio',
            'thousands': 't'
        },
        'fr': {
            'millions': 'kk',
            'thousands': 'k'
        },
        'it': {
            'millions': 'mln',
            'thousands': 'k'
        },
        'ru': {
            'millions': 'млн',
            'thousands': 'тыс'
        },
        'ko': {
            'millions': 'M',
            'thousands': 'K'
        },
        'tw': {
            'millions': '百萬',
            'thousands': '千'
        },
        'cn': {
            'millions': '百万',
            'thousands': '千'
        },
        get es() {
            return this.classic;
        },
        get br() {
            return this.classic;
        },
    };

    const parseNumber = (num) => {
        const millions = localeMap[locale].millions;
        const thousands = localeMap[locale].thousands;
        return num.endsWith(millions) ? num.replace(millions, '') * 1000000
            : (num.endsWith(thousands) ? num.replace(thousands, '') * 1000 : parseFloat(num.replace(',', '')))
    }

    const localizeTotal = (damage) => {
        const currentLocale = localeMap[locale];
        const fractionDigits = damage >= 1000000 ? 2 : (damage >= 1000 ? 1 : 0);
        return new Intl.NumberFormat('en-US', {
            notation: 'compact',
            compactDisplay: 'short',
            maximumFractionDigits: fractionDigits,
            minimumFractionDigits: fractionDigits
        }).format(damage).replace('M', currentLocale.millions).replace('K', currentLocale.thousands);
    };

    const createPetBar = (width, tooltipId) => {
        const petBar = document.createElement('div');
        petBar.classList.add('Pet-bg');
        petBar.style['border-left'] = '1px solid black';
        petBar.style['min-width'] = '2px';
        petBar.style.position = 'absolute';
        petBar.style.right = '0px';
        petBar.style.top = '0px';
        petBar.style.bottom = '0px';
        petBar.style.color = 'white';
        petBar.style['text-align'] = 'center';
        petBar.style.cursor = 'pointer';
        petBar.style.width = width;
        petBar.onclick = () => {
            window.location.href = `${window.location.href}&source=${tooltipId}`
        };
        return petBar;
    };

    const sortPlayersByDamage = (table, playerRows) => {
        let swapped
        for (let i = 0; i < playerRows.length - 1; i++) {
            swapped = false
            for (let j = 0; j < playerRows.length - i - 1; j++) {
                const a = playerRows[j]
                const b = playerRows[j + 1]
                const aDamage = parseNumber(a.querySelector('.main-per-second-amount').innerText)
                const bDamage = parseNumber(b.querySelector('.main-per-second-amount').innerText)
                if (aDamage < bDamage) {
                    swapped = true;
                    a.parentNode.insertBefore(b.parentNode.removeChild(b), a)
                    playerRows = table.querySelectorAll('tr.odd, tr.even')
                }
            }

            if (!swapped) break
        }
    };

    const doJustice = () => {
        const table = document.querySelector('table.summary-table');
        const playerRows = table.querySelectorAll('tr.odd, tr.even');
        const fightDuration = parseNumber(table.querySelector('tr.totals td.num.ui-state-default').innerText.replace('s', ''))
        const topPlayerDamage = parseNumber(playerRows[0].querySelector('.report-amount-total').innerText);
        const totalEncounterDamage = parseNumber(table.querySelector('tr.totals span.report-amount-total').innerText);
        const tricksRows = Array.from(playerRows).filter(x => Array.from(x.getElementsByTagName('a')).filter(y => y.innerText.match(tricksRegexp)).length);
        let tricksters = [];

        tricksRows.forEach(tricksRow => {
            const anchor = tricksRow.querySelector('a.TricksOfTheTrade');
            const name = anchor.innerText.match(tricksRegexp)[1];
            let tricksDamage = tricksRow.querySelector('.report-amount-total').innerText;
            const tooltipId = anchor.oncontextmenu.toString().match(/setFilterSource\('(\d+)/)[1];
            tricksDamage = parseNumber(tricksDamage);
            tricksters.push({name, tricksDamage, tooltipId});
            tricksRow.remove();
        });

        tricksters.forEach(trickster => {
            const row = Array.from(playerRows).filter(x => Array.from(x.getElementsByTagName('a')).filter(y => y.innerText == trickster.name).length)[0];
            const totalDamageElement = row.querySelector('.report-amount-total');
            let totalDamage = parseNumber(totalDamageElement.innerText);
            totalDamage += trickster.tricksDamage;
            const percentDamageElement = row.querySelector('div.report-amount-percent');
            let percentDamage = parseNumber(percentDamageElement.innerText);
            percentDamage = (totalDamage / totalEncounterDamage * 100).toFixed(2) + '%';
            const dpsElement = row.querySelector('.main-per-second-amount');
            let dps = parseNumber(dpsElement.innerText);
            dps = (totalDamage / fightDuration).toFixed(1);
            dpsElement.innerText = (+dps).toLocaleString('en-US', {minimumFractionDigits: 1});
            percentDamageElement.innerText = percentDamage;
            totalDamageElement.innerText = localizeTotal(totalDamage);
            const damageBarElement = row.querySelector('div.Rogue-bg');
            damageBarElement.style.width = (totalDamage / topPlayerDamage * 100) + '%';
            const petBar = createPetBar((trickster.tricksDamage / totalDamage * 100) + '%', trickster.tooltipId);
            const tooltip = Array.from(document.getElementsByTagName('div')).filter(x => x.id.includes(`popup-tooltip-${trickster.tooltipId}`))[0];
            createTipped(petBar, tooltip);
            damageBarElement.children[0].appendChild(petBar);
        });

        sortPlayersByDamage(table, table.querySelectorAll('tr.odd, tr.even'));
    };

    (new MutationObserver((changes, observer) => {
        if (document.querySelector('div#table-container table.summary-table tr.totals')) {
            doJustice();
        }
    })).observe(document.querySelector('div#table-container'), {
        childList: true,
        subtree: true
    });
})();