Loadout Reveal

NST: Loadout reveal for those in gunshops

// ==UserScript==
// @name         Loadout Reveal
// @namespace    http://tampermonkey.net/
// @version      0.3
// @description  NST: Loadout reveal for those in gunshops
// @author       Hwa [2466470]
// @match        https://www.torn.com/loader.php?sid=attack*
// @icon         https://www.google.com/s2/favicons?sz=64&domain=torn.com
// @grant        none
// @license      MIT
// ==/UserScript==

(function() {
    'use strict';

    let alreadyExtracted = false;

    function debounce(func, delay) {
        let timeout;
        return (...args) => {
            clearTimeout(timeout);
            timeout = setTimeout(() => func(...args), delay);
        };
    }

    function getArmorPrefix(armor) {
        const parts = ['helmet', 'chest', 'gloves', 'pants', 'boots'];
        const names = parts.map(part => armor[part]);

        // Return null if any part is missing
        if (names.some(name => !name)) return null;

        const prefixes = names.map(name => name.split(' ')[0]);
        const allSame = prefixes.every(prefix => prefix === prefixes[0]);

        return allSame ? prefixes[0] : null;
    }

    function formatWeaponData(weaponData) {
        if (!weaponData || typeof weaponData !== 'object') return "N/A";

        const { weapon, damage, accuracy, bonus_attachments } = weaponData;
        if (!weapon) return "N/A";

        let mainLine = `${weapon}, damage: ${damage}, accuracy: ${accuracy}`;

        let bonusesText = '';
        if (Array.isArray(bonus_attachments) && bonus_attachments.length > 0) {
            bonusesText = '<br>Bonuses:';
            for (const bonus of bonus_attachments) {
                const title = bonus.title.toLowerCase();
                const description = bonus.description.replace('+', '').trim();
                bonusesText += `<br>- ${title} (${description})`;
            }
        }

        return mainLine + bonusesText;
    }

    function createDataTable(dataObj) {
        const container = document.createElement('div');
        container.style.display = 'flex';
        container.style.alignItems = 'flex-start';
        container.style.marginBottom = '10px';
        container.style.gap = '10px'; // space between table and button


        const table = document.createElement('table');
        table.id = "player_loadout_table";
        table.style.borderCollapse = 'collapse';
        table.style.background = '#111';
        table.style.color = '#fff';
        table.style.fontFamily = 'monospace';
        table.style.zIndex = '9999';
        table.style.position = 'relative';

        for (const [key, value] of Object.entries(dataObj)) {
            const row = document.createElement('tr');

            const keyCell = document.createElement('td');
            keyCell.textContent = key;
            keyCell.style.border = '1px solid #888';
            keyCell.style.padding = '4px';
            keyCell.style.color = '#fff';
            keyCell.style.background = '#222';

            const valueCell = document.createElement('td');

            if (value && typeof value === 'object' && value.weapon) {
                valueCell.innerHTML = formatWeaponData(value);
            } else if (typeof value === 'object') {
                // For other objects, just stringify nicely with indentation
                valueCell.textContent = JSON.stringify(value, null, 2);
            } else {
                valueCell.textContent = value;
            }
            valueCell.style.border = '1px solid #888';
            valueCell.style.padding = '4px';
            valueCell.style.color = '#fff';
            valueCell.style.background = '#333';

            row.appendChild(keyCell);
            row.appendChild(valueCell);
            table.appendChild(row);
        }

        const button = document.createElement('button');
        button.textContent = 'Copy';
        button.style.marginLeft = '5px';
        button.style.padding = '2px 5px';
        button.style.backgroundColor = '#444';
        button.style.color = '#fff';
        button.style.border = '1px solid #888';
        button.style.borderRadius = '4px';
        button.style.cursor = 'pointer';
        button.style.fontFamily = 'monospace';
        button.style.fontSize = '14px';

        // Copy logic
        button.addEventListener('click', () => {
            const rows = Array.from(table.querySelectorAll('tr')).map(row => {
                const cells = row.querySelectorAll('td');
                const label = cells[0].textContent.trim();
                const content = cells[1].innerText.trim(); // innerText preserves line breaks
                return `${label}: ${content}`;
            });
            const text = rows.join('\n');
            navigator.clipboard.writeText(text).then(() => {
                button.textContent = 'Copied!';
                setTimeout(() => (button.textContent = 'Copy'), 2000);
            });
        });

        // Append table and button to container
        container.appendChild(table);
        container.appendChild(button);

        return container;
    }


    function insertTable(data) {
        const table = createDataTable(data);
        const container = document.querySelector('[class^="coreWrap"]')
        const childToInsertAfter = container.querySelector('[class^="logStatsWrap__"]'); // choose the child element you want
        if (childToInsertAfter && container) {
            // Insert after the chosen child:
            childToInsertAfter.after(table);
        } else if (container) {
            // fallback: append at the end
            container.appendChild(table);
        }
    }


    function extractData() {
        if (alreadyExtracted) return;

        const players = document.querySelectorAll('[class^="player___"]');

        let secondPlayer = null;
        // Check if there are at least two elements
        if (players.length >= 2) {
            secondPlayer = players[1]; // 0-based index, so 1 is the second element
            console.log("Second player found");
        } else {
            console.log("Less than two elements found");
        }

        if (secondPlayer) {
            alreadyExtracted = true;
            const nameEl = secondPlayer.querySelector('[id^="playername_"]');

            let xid = "N/A";
            if (xid === "N/A" && window.location.href.includes('user2ID=')) {
                const match = window.location.href.match(/user2ID=(\d+)/);
                if (match) xid = match[1];
            }

            const armorAreas = secondPlayer.querySelectorAll('area');
            const armor = {};

            ['Helmet', 'Chest', 'Gloves', 'Pants', 'Boots'].forEach(part => {
                const area = Array.from(armorAreas).find(el => el.alt === part);
                if (area) {
                    armor[part.toLowerCase()] = area.title || "N/A";
                }
            });


            const weaponListDiv = secondPlayer.querySelector('div[class^="weaponList"]');
            const weaponMainDiv = weaponListDiv.querySelector('#weapon_main');

            let primary = {};
            if (weaponMainDiv) {
                // Weapon name from img alt (replace spaces with hyphen or keep as is)
                const weaponImg = weaponMainDiv.querySelector('figure[class^="weaponImage"] img');
                const weaponName = weaponImg ? weaponImg.alt.trim() : "N/A";

                // Damage value
                const damageSpan = weaponMainDiv.querySelector('[id^="damage-value_"]');
                const damage = damageSpan ? damageSpan.textContent.trim() : "N/A";

                // Accuracy value
                const accuracySpan = weaponMainDiv.querySelector('span[id^="accuracy_"]');
                const accuracy = accuracySpan ? accuracySpan.textContent.trim() : "N/A";

                // Bonus attachments (titles and descriptions)
                const bonusAttachments = [...weaponMainDiv.querySelectorAll('[class^="props"] i[data-bonus-attachment-title]')]
                .map(i => {
                    return {
                        title: i.getAttribute('data-bonus-attachment-title') || "",
                        description: i.getAttribute('data-bonus-attachment-description') || ""
                    };
                })
                // filter out empty titles if needed
                .filter(b => b.title);

                primary = {
                    weapon: weaponName,
                    damage: damage,
                    accuracy: accuracy,
                    bonus_attachments: bonusAttachments
                };
            }

            const weaponSecondaryDiv = weaponListDiv.querySelector('#weapon_second');
            let secondary = {};
            if (weaponSecondaryDiv) {
                // Weapon name from img alt (replace spaces with hyphen or keep as is)
                const weaponImg = weaponSecondaryDiv.querySelector('figure[class^="weaponImage"] img');
                const weaponName = weaponImg ? weaponImg.alt.trim() : "N/A";

                // Damage value
                const damageSpan = weaponSecondaryDiv.querySelector('[id^="damage-value_"]');
                const damage = damageSpan ? damageSpan.textContent.trim() : "N/A";

                // Accuracy value
                const accuracySpan = weaponSecondaryDiv.querySelector('span[id^="accuracy_"]');
                const accuracy = accuracySpan ? accuracySpan.textContent.trim() : "N/A";

                // Bonus attachments (titles and descriptions)
                const bonusAttachments = [...weaponSecondaryDiv.querySelectorAll('[class^="props"] i[data-bonus-attachment-title]')]
                .map(i => {
                    return {
                        title: i.getAttribute('data-bonus-attachment-title') || "",
                        description: i.getAttribute('data-bonus-attachment-description') || ""
                    };
                })
                // filter out empty titles if needed
                .filter(b => b.title);

                secondary = {
                    weapon: weaponName,
                    damage: damage,
                    accuracy: accuracy,
                    bonus_attachments: bonusAttachments
                };
            }

            const weaponMeleeDiv = weaponListDiv.querySelector('#weapon_melee');
            let melee = {};
            if (weaponMeleeDiv) {
                // Weapon name from img alt (replace spaces with hyphen or keep as is)
                const weaponImg = weaponMeleeDiv.querySelector('figure[class^="weaponImage"] img');
                const weaponName = weaponImg ? weaponImg.alt.trim() : "N/A";

                // Damage value
                const damageSpan = weaponMeleeDiv.querySelector('[id^="damage-value_"]');
                const damage = damageSpan ? damageSpan.textContent.trim() : "N/A";

                // Accuracy value
                const accuracySpan = weaponMeleeDiv.querySelector('span[id^="accuracy_"]');
                const accuracy = accuracySpan ? accuracySpan.textContent.trim() : "N/A";

                // Bonus attachments (titles and descriptions)
                const bonusAttachments = [...weaponMeleeDiv.querySelectorAll('[class^="props"] i[data-bonus-attachment-title]')]
                .map(i => {
                    return {
                        title: i.getAttribute('data-bonus-attachment-title') || "",
                        description: i.getAttribute('data-bonus-attachment-description') || ""
                    };
                })
                // filter out empty titles if needed
                .filter(b => b.title);

                melee = {
                    weapon: weaponName,
                    damage: damage,
                    accuracy: accuracy,
                    bonus_attachments: bonusAttachments
                };
            }

            const weaponTempDiv = weaponListDiv.querySelector('#weapon_temp');
            const weaponTemp = weaponTempDiv?.querySelector('figure[class^="weaponImage"] img')?.alt.trim() || "N/A";

            const armorPrefix = getArmorPrefix(armor);

            const data = {
                Name: nameEl ? `${nameEl.textContent.trim()} [${xid}]` : "N/A",
                ...(armorPrefix
                    ? { "Full Set": armorPrefix }
                    : {
                    Helmet: armor.helmet || "N/A",
                    Chest: armor.chest || "N/A",
                    Gloves: armor.gloves || "N/A",
                    Pants: armor.pants || "N/A",
                    Boots: armor.boots || "N/A"
                }),

                Primary: (primary.weapon && primary.weapon.trim() !== "") ? primary : "N/A",
                Secondary: (secondary.weapon && secondary.weapon.trim() !== "") ? secondary : "N/A",
                Melee: (melee.weapon && melee.weapon.trim() !== "") ? melee : "N/A",
                Temp: weaponTemp,
            };

            console.log("✅ Torn Fight Report Extracted Data:", data);
            return data

        }

    }

    const observer = new MutationObserver(
        debounce(() => {
            const nameElement = document.querySelector('[id^="playername_"]');
            const playerElements = document.querySelectorAll('[class^="player___"]');

            if (nameElement && playerElements.length >= 2) {
                console.log("Checking!")
                const indicatorsWrap = playerElements[1].querySelector('div[class^="indicatorsContainer"]');

                if (indicatorsWrap && indicatorsWrap.children.length > 0) {
                    observer.disconnect();
                    const data = extractData();
                    insertTable(data);
                }
            }
        }, 1000) // only checks every 1s
    );

    observer.observe(document.body, { childList: true, subtree: true });
})();