Armory Loan Highlighter

Save trade data and all matching items in armory are highlighted. Also highlights on add to armory and add items to trade.

// ==UserScript==
// @name         Armory Loan Highlighter
// @namespace    http://tampermonkey.net/
// @version      2024-07-23-2
// @description  Save trade data and all matching items in armory are highlighted. Also highlights on add to armory and add items to trade.
// @author       oleisen
// @match        https://www.torn.com/trade.php
// @match        https://www.torn.com/factions.php?step=your*
// @icon         https://www.google.com/s2/favicons?sz=64&domain=torn.com
// @license      MIT
// @grant        none
// ==/UserScript==

(function() {
    'use strict';

    const oldItems = JSON.parse(localStorage.getItem("armory-loan-data") ?? "[]");
    const saveData = (data) => {
        localStorage.setItem("armory-loan-data", JSON.stringify(data));
        oldItems = data;
    }

    //Check either faction item or the trade view.
    const checkFacItems = (wrapper, isItemAdd = false) => {
        console.log("Checking items");
        Array.from(wrapper.children).forEach((item) => {
            const name = item.querySelector(isItemAdd ? ".name-wrap" : ".name")?.innerText;
            const modifiers = item.querySelector(isItemAdd ? ".bonuses-wrap" : ".bonuses")?.children;
            if (!modifiers) return;
            //Fix for deposit pages where for some reason armor is in 2nd cell......
            const damageOrArmor = isItemAdd && modifiers[0]?.innerText === "" ? modifiers[1]?.innerText : modifiers[0]?.innerText;
            const accuracy = isItemAdd && modifiers[0]?.innerText === "" ? modifiers[0]?.innerText : modifiers[1]?.innerText;
            const quality = item.querySelector(".last.bonus-increase")?.innerText;

            if (name && damageOrArmor) {
                const bonus = item.querySelector(isItemAdd ? ".bonuses-wrap" : ".bonuses").querySelector(".bonus");
                const bonuses = Array.from(bonus.children).reduce((bonuses, i) => {
                    if (i.title != "") {
                        bonuses.push(i.title);
                    }
                    return bonuses;
                }, []);
                const matchBonuses = (oldItem, bonuses) => {
                    return bonuses && bonuses.length === oldItem.bonuses.filter(bonus => bonuses.find(bonusText => bonusText.includes(bonus.value.trim()))).length;
                }
                const existing = oldItems.find(oldItem => oldItem.name === name && (!accuracy && Number(damageOrArmor) == Number(oldItem?.armor) || (accuracy && Number(damageOrArmor) === Number(oldItem?.damage)) && Number(accuracy) === Number(oldItem?.accuracy)) && matchBonuses(oldItem, bonuses));
                if (existing) {
                    const used = item.querySelector(".used");

                    item.style.backgroundColor = "darkgreen";
                    const btn = item.querySelector(".checkbox-css");
                    if (!btn.classList.contains("has-been-clicked")) {
                        btn.classList.add("has-been-clicked");
                        console.log(quality);
                        if (!(used && used.innerText === "Equipped")) {
                            btn.click();
                        }
                    }


                }
            }


        });
    }

    const getTradeItems = (wrapper) => {
        return Array.from(wrapper.querySelectorAll(".cont li ul li")).reduce((items, item) => {
            const doc = item?.querySelector(".name");
            if (item && doc) {
                const details = doc.querySelector(".networth-info-icon");
                if (details) {
                    // Extract the name
                    const name = doc.childNodes[0].nodeValue.trim();

                    // Extract the tooltip
                    const tooltipElement = doc.querySelector('i.networth-info-icon');
                    const tooltip = tooltipElement ? tooltipElement.getAttribute('title') : null;

                    // Extract the quality using regex
                    const qualityMatch = tooltip ? tooltip.match(/(\d+\.\d+%)/) : null;
                    const quality = qualityMatch ? qualityMatch[1] : null;


                    // Extract the damage and accuracy using regex
                    const damageMatch = tooltip ? tooltip.match(/bonus-attachment-item-damage-bonus'><\/i><span>([\d.]+)<\/span>/) : null;
                    const damage = damageMatch ? damageMatch[1] : null;

                    const accuracyMatch = tooltip ? tooltip.match(/bonus-attachment-item-accuracy-bonus'><\/i><span>([\d.]+)<\/span>/) : null;
                    const accuracy = accuracyMatch ? accuracyMatch[1] : null;

                    // Extract the armor rating using regex
                    const armorMatch = tooltip ? tooltip.match(/bonus-attachment-item-defence-bonus'><\/i><span>([\d.]+)<\/span>/) : null;
                    const armor = armorMatch ? armorMatch[1] : null;


                    // Extract bonuses using regex
                    const bonusMatches = tooltip ? [...tooltip.matchAll(/<b>([^<]+)<\/b><br\/>([^<]+)<\/br>/g)] : [];
                    const bonuses = bonusMatches.map(match => ({
                        name: match[1],
                        value: match[2].trim()
                    }));

                    // Output the extracted bonuses
                    bonuses.forEach(bonus => {
                        console.log(`Bonus Name: ${bonus.name}`);
                        console.log(`Bonus Value: ${bonus.value}`);
                    });
                    const matchBonuses = (oldItem, bonuses) => {
                        return bonuses && bonuses.length === oldItem.bonuses.filter(bonus => bonuses.find(({value: bonusText}) => bonusText.includes(bonus.value))).length;
                    }
                    const existing = oldItems.find(oldItem => oldItem.name === name && (armor && Number(armor) == Number(oldItem?.armor) && String(quality) === String(oldItem?.quality) || (accuracy && Number(damage) === Number(oldItem?.damage)) && Number(accuracy) === Number(oldItem?.accuracy)) && matchBonuses(oldItem, bonuses));
                    if (existing) {
                        item.style.backgroundColor = "rgba(0, 100, 0, 0.5)";
                    }

                    items.push({name, quality, bonuses, ...(armor ? {armor} : {damage, accuracy})});

                }
            }
            return items;
        }, []);
    }

    //This is for the add to trade and deposit to armory page
    const renderItems = (wrapper) => {
        const itemLists = wrapper.querySelectorAll(".items-cont");
        Array.from(itemLists).forEach(theList => checkFacItems(theList, true));
    }

    if (location.href.includes("trade.php")) {
        const checkTrade = (wrapper) => {

            const items = getTradeItems(wrapper);

            if (!document.querySelector("#fac-armory-trade-btn")) {
                const div = document.createElement("div");
                div.style.width = "100%";
                div.style.display = "flex";
                div.style.justifyContent = "center";
                div.style.padding = "1rem";
                const button = document.createElement("button");
                button.id = "fac-armory-trade-btn";
                button.innerText = "Save items";
                button.style.backgroundColor = "darkblue";
                button.style.margin = "auto";
                button.style.padding = "10px";
                button.style.color = "white";
                button.style.borderRadius = "10px";
                button.onmouseover = () => {
                    button.style.cursor = "pointer";
                }
                button.addEventListener("click", (e) => {
                    const items = getTradeItems(wrapper);
                    button.style.backgroundColor = "rgba(0, 100, 0, 0.5)";
                    button.innerText = `Saved ${items.length} items!`;
                    saveData(items);
                });
                div.appendChild(button);
                wrapper.parentElement.appendChild(div);
            }



        }

        //For when adding to trade

        let obv = null;
        //Setup an observer that will either check for the trade iteslf or the add items part of it.
        const observ = (wrapper) => {
            const isItems = location.href.includes("#step=add");

            if (isItems) {
                const i = setTimeout(() => {
                    renderItems(document.querySelector(".category-wrap"));
                }, 750);

                obv= new MutationObserver((_, observer) => {
                    renderItems(document.querySelector(".category-wrap"));
                });


            } else {
                obv= new MutationObserver((_, observer) => {
                    checkTrade(wrapper);
                });
                checkTrade(wrapper);
            }
            console.log("Setting obsever on", wrapper);
            if (obv) obv.observe(wrapper, { attributes: true, childList: true, subtree: true, characterData: true });
        }

        //Watch the trade
        const watchForLoad = () => {
            const observer = new MutationObserver((_, observer) => {
                const isItems = location.href.includes("#step=add");
                //Is the add to trade view
                if (isItems) {
                    let wrapper = document.querySelector(".category-wrap");
                    let item = document.querySelector(".primary-items");
                    console.log(wrapper);
                    if (wrapper && item) {
                        observ(wrapper);
                        observer.disconnect();
                    }
                    //The trade list
                } else {
                    let wrapper = document.querySelector(".trade-cont");
                    if (wrapper) {
                        observ(wrapper);
                        observer.disconnect();
                    }
                }

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

        window.addEventListener("hashchange", (event) => {
            if (obv) obv.disconnect();
            watchForLoad();
        })
        watchForLoad();


        //Below is for checking faction armory ->
    } else if (oldItems.length != 0) {
        let obv = null;
        //Set up the listener for the page load
        const observ = (itemList, isItemDonation) => {
            if (location.href.includes("&type=1#/tab=armoury")) {
                if (isItemDonation) {
                    const i = setTimeout(() => {
                        renderItems(document.querySelector(".category-wrap"));
                    }, 780);

                    obv= new MutationObserver((_, observer) => {
                        console.log("Rerendering the list");
                        renderItems(document.querySelector(".category-wrap"));
                    });
                    //Item Donation page
                } else {
                    //Faction item list page
                    obv= new MutationObserver((_, observer) => {
                        checkFacItems(itemList);
                    });
                    checkFacItems(itemList);
                }
                console.log("Setting fac obsever on", itemList, isItemDonation);
                if (obv) obv.observe(itemList, { attributes: true, childList: true, subtree: true, characterData: true });
            }
        }



        const watchForLoad = () => {

            const observer = new MutationObserver((_, observer) => {
                //Is donate (this is default as well)
                if (location.href.includes("sub=donate") || !location.href.includes("sub=")) {
                    let wrapper = document.querySelector(".category-wrap");
                    let item = document.querySelector(".primary-items");
                    console.log(wrapper);
                    if (wrapper && item) {
                        observ(wrapper, true);
                        observer.disconnect();
                    }
                    //Is the normal faction list
                } else {
                    let wrapper = document.querySelector(".item-list");
                    if (wrapper) {
                        observ(wrapper, false);
                        observer.disconnect();
                    }
                }
            });
            observer.observe(document, { subtree: true, childList: true });
        }
        watchForLoad();
        //Watch for URL changes
        window.addEventListener("hashchange", (event) => {
            if (location.href.includes("&type=1#/tab=armoury")) {
                //console.log("Reconnecting observer");
                obv.disconnect();
                watchForLoad();

            }
        })
    }
})();