YATA

Displays various informations from YATA's API

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

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

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

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

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name         YATA
// @namespace    yata.yt
// @version      0.22.0
// @description  Displays various informations from YATA's API
// @author       Kivou [2000607]
// @match        https://www.torn.com/factions.php*
// @match        https://www.torn.com/preferences.php*
// @match        https://www.torn.com/profiles.php*
// @match        https://www.torn.com/page.php?sid=UserList*
// @icon         https://yata.yt/media/yata-small.png
// @require      https://update.greasyfork.org/scripts/477604/1287854/kiv-lib.js
// @require      https://update.greasyfork.org/scripts/479408/1277647/kib-key.js
// @grant        GM.xmlHttpRequest
// @run-at       document-end
// @license      WTFPL
// ==/UserScript==

// Copyright © 2024 Kivou [2000607] <[email protected]>
// This work is free. You can redistribute it and/or modify it under the
// terms of the Do What The Fuck You Want To Public License, Version 2,
// as published by Sam Hocevar. See http://www.wtfpl.net/ for more details.

console.log("YATA script loaded");

// ------------- //
// SETUP API KEY //
// ------------- //
storeKey("yata", "YATA")

// -------------- //
// OC: NNB + rank //
// -------------- //
const display_nnb = (members, player) => {
    const urlParams = new URLSearchParams(player.children[0].children[0].href.split("?")[1]);
    const lvl = player.children[1].innerText.trim();
    if (members.members && members.members.hasOwnProperty(urlParams.get("XID"))) {
        const m = members.members[urlParams.get("XID")];
        if (m.nnb_share > 0) {
            player.children[1].innerHTML = `<span>#<b>${m.crimes_rank}</b> / <b>${m.nnb}</b> / ${lvl}</span>`;
        } else if (m.nnb_share < 0) {
            player.children[1].innerHTML = `<span title="Not on YATA">#<b>${m.crimes_rank}</b> / <b>!</b> / ${lvl}</span>`;
        } else {
            player.children[1].innerHTML = `<span title="Not sharing NNB">#<b>${m.crimes_rank}</b> / <b>?</b> / ${lvl}</span>`;
        }
    } else {
        player.children[1].innerHTML = `<span title="Not found">#<b>?</b> / <b>err</b> / ${lvl}</span>`;
    }
};

waitFor(document, "div#faction-crimes").then(div => {

    const key = localStorage.getItem('yata-key');

    if (!key) { return; }

    const profile_url = new URLSearchParams(window.location.search);
    const target_id = profile_url.get("XID");

    gmGet(`https://yata.yt/api/v1/faction/members/?key=${key}`, 'nnb').then(members => {

        // triggered if directly landing on crimes
        div.querySelectorAll("ul.details-list, ul.plans-list").forEach(ul => {
            ul.querySelectorAll("ul.item").forEach(player => {
                display_nnb(members, player);
            });
        });
        div.querySelectorAll("ul.title li.level").forEach(t => {
            t.innerHTML = 'Rank / NNB / Level';
        });

        // triggered by clicking on crimes tab
        const callback = (mutations, observer) => {
            [...mutations].forEach(mutation => {
                [...mutation.addedNodes].filter(n => n.className && n.className.includes("faction-crimes-wrap")).forEach(node => {
                    node.querySelectorAll("ul.details-list, ul.plans-list").forEach(ul => {
                        const ocs = ul.querySelectorAll("ul.item");
                        console.log(`[yata] displaying NNB for OC: ${ocs.length}`);
                        ocs.forEach(player => {
                            display_nnb(members, player);
                        });
                    });
                    node.querySelectorAll("ul.title li.level").forEach(t => {
                        t.innerHTML = 'Rank / NNB / Level';
                    });
                });
            });
        };
        const observer = new MutationObserver(callback);
        observer.observe(div, { childList: true });


    }).catch(error => {
        console.warn(`[yata] ${error.message}`);
        if (error.message == "Incorrect key") {
            localStorage.removeItem('key');
        }
    });
});


// ---------------------- //
// PROFILE                //
// ---------------------- //

waitFor(document, "a.profile-button-report").then(a => {

    const div = document.getElementById("profileroot");
    const key = localStorage.getItem('yata-key');
    const profile_url = new URLSearchParams(a.href.split("#")[0].split("?")[1]);
    const target_id = profile_url.get("userID");

    if(div == null) { return; } // ignore miniprofile
    if (!target_id) { return; }
    if (!key) { return; }

    gmGet(`https://yata.yt/api/v1/bs/${target_id}/?key=${key}`, `bs-${target_id}`).then(bs => {

        let innerHTML = "";
        innerHTML += `<hr class="page-head-delimiter m-top10 m-bottom10">`;
        innerHTML += `<div>`;
        innerHTML += `<b>[YATA]</b> <b>Battle stats</b> ${floatFormat(bs[target_id].total, 3)}`;
        innerHTML += ` | <b>Build</b> ${bs[target_id].type} (${bs[target_id].skewness}%)`;
        innerHTML += `</div>`;
        innerHTML += `<hr class="page-head-delimiter m-top10 m-bottom10">`;
        innerHTML += `<div class="clear"></div>`;
        const bs_node = document.createElement("div");
        bs_node.innerHTML = innerHTML;
        div.querySelector("div.profile-wrapper").insertAdjacentElement('afterend', bs_node);
        console.log(`[yata] battle stats estimate on profile page: ${target_id}`);

    }).catch(error => {
        console.warn(`[yata] ${error.message}`);
        if (error.message == "Incorrect key") {
            localStorage.removeItem('key');
        }
    });

});

// -------------------------- //
// FACTIONS: Helper functions //
// -------------------------- //
const bse_html = (id, bs) => {

    if (bs == "loading") {
        return `<a style="text-decoration: none; display: inline-block;" href="/loader.php?sid=attack&user2ID=${id}" target="_blank"><span title='Loading' style="color: var(--default-red-color);">...</span></a>`;
    }

    if (!bs.hasOwnProperty("type")) {
        return `<a style="text-decoration: none; display: inline-block;" href="/loader.php?sid=attack&user2ID=${id}" target="_blank"><span title='${bs["message"]}' style="color: var(--default-red-color);">err</span></a>`;
    }

    let color = "var(--default-blue-color)";
    if (bs.type == "Offensive" && bs.skewness > 20) {
        color = "var(--default-red-color)";
    } else if (bs.type == "Defensive" && bs.skewness > 20) {
        color = "var(--default-green-color)";
    }
    const title = `Total stats: ${bs.total.toLocaleString("en-GB")} Score: ${bs.score.toLocaleString("en-GB")} Build: ${bs.type} (${bs.skewness}%) Version: ${bs.version}`;
    return `<a style="text-decoration: none; display: inline-block;" href="/loader.php?sid=attack&user2ID=${id}" target="_blank"><span title="${title}" style="color: ${color};">${floatFormat(bs.total, 3)}</span></a>`;
};

// ---------------------- //
// FACTIONS: Members list //
// ---------------------- //
const members_list_filter = (div) => {
    if (div.tagName != "DIV") { return false; }
    if (div.classList.contains("faction-info-wrap")) { return true; }
    return Boolean(div.querySelector("div.faction-info-wrap"));
};

const members_list_display = (members, key) => {
    console.log(`[yata] battle stats estimate for members list: ${members.length}`);
    [...members].forEach(member => {
        const member_id = getPlayerId(member, "honor");

        gmGet(`https://yata.yt/api/v1/bs/${member_id}/?key=${key}`, `bs-${member_id}`).then(bs => {
            const node = document.createElement("span");
            node.innerHTML = bse_html(member_id, bs[member_id]);
            node.style.width = "4em";
            // member.querySelector("div.member-icons").insertAdjacentElement('afterbegin', node);
            member.querySelector("div.position").insertAdjacentElement('afterbegin', node);
        }).catch((error) => {
            const node = document.createElement("span");
            node.innerHTML = bse_html(member_id, error);
            node.style.width = "4em";
            // member.querySelector("div.member-icons").insertAdjacentElement('afterbegin', node);
            member.querySelector("div.position").insertAdjacentElement('afterbegin', node);
        });
    });
};

// -------------- //
// FACTIONS: Wars //
// -------------- //
const _is_attack_link = (node) => {
    return node.tagName == "A" && node.classList.contains("t-blue") || node.tagName == "SPAN" && node.classList.contains("t-gray-9")
}

const wars_list_filter = (node) => {
    if(_is_attack_link(node)) { return true; }
    if (node.tagName != "DIV") { return false; }
    return node.tagName == "DIV" && node.classList.contains("faction-war");
};

const wars_list_display = (members, type, key) => {
    console.log(`[yata] battle stats estimate for ${type}: ${members.length}`);

    [...members].forEach(member => {
        // STEP 1: get target ID

        let member_id = undefined;

        if (type == "wall") {
            member_id = getPlayerId(member, "username");
        } else {
            member_id = getPlayerId(member, "honor");
        }

        // STEP 2: make call
        gmGet(`https://yata.yt/api/v1/bs/${member_id}/?key=${key}`, `bs-${member_id}`).then(bs => {
            const node = document.createElement("span");
            node.innerHTML = bse_html(member_id, bs[member_id]);

            if (type == "wall") {
                const step = new URLSearchParams(window.location.search).get('step');
                if (step == "your") {
                    // own faction wall: replace attack link
                    node.style.paddingRight = "0.5em";
                    member.children[4].innerHTML = node.outerHTML;
                } else {
                    // waching another faction wall: replace ID
                    member.children[1].innerHTML = node.outerHTML;
                }
            } else if (type == "rank-right") {
                // prepend to level
                node.style.paddingRight = "1em";
                member.children[1].insertAdjacentElement('afterbegin', node);
            } else if (type == "rank-left") {
                // replace link
                member.children[4].children[0].style.display = "None"
                node.style.paddingRight = "0.5";
                member.children[4].insertAdjacentElement('afterbegin', node);
$            } else {
                // replace attack link
                node.style.paddingRight = "0.5em";
                member.children[5].innerHTML = node.outerHTML;
            }
        }).catch((error) => {
            const node = document.createElement("span");
            node.innerHTML = bse_html(member_id, error);

            if (type == "wall") {
                // replace attack link
                node.style.paddingRight = "0.5em";
                member.children[4].innerHTML = node.outerHTML;
            } else if (type == "rank-right") {
                // prepend to level
                node.style.paddingRight = "1em";
                member.children[1].insertAdjacentElement('afterbegin', node);
            } else if (type == "rank-left") {
                // replace link
                node.style.paddingRight = "0.5";
                member.children[4].innerHTML = node.outerHTML;
            } else {
                // replace attack link
                node.style.paddingRight = "0.5em";
                member.children[5].innerHTML = node.outerHTML;
            }
        });
    });
};

const chain_list_filter = (node) => {
    if (node.tagName != "UL") { return false; }
    return node.tagName == "UL" && node.classList.contains("chain-attacks-list");
};

const chain_list_display = (attacks, key) => {
    console.log(`[yata] battle stats estimate for chain: ${attacks.length}`);

    const _f = (e) => { return Boolean(e.querySelector("div.right-player div[class^=userInfoBox]")); };
    [...attacks].filter(_f).forEach(attack => {

        const target = attack.querySelector("div.right-player");
        const target_id = getPlayerId(target, 'icon')
        const respect = attack.querySelector("div.respect");

        gmGet(`https://yata.yt/api/v1/bs/${target_id}/?key=${key}`, `bs-${target_id}`).then(bs => {
            const node = document.createElement("span");
            node.innerHTML = bse_html(target_id, bs[target_id]);
            node.style.float = 'right';
            respect.insertAdjacentElement('beforeend', node);
        }).catch((error) => {
            const node = document.createElement("span");
            node.innerHTML = bse_html(target_id, error);
            node.style.float = 'right';
            respect.insertAdjacentElement('beforeend', node);
        });

    });
};

// ------------------- //
// FACTIONS: observers //
// ------------------- //
const callback_factions = (key) => {
    return (mutations, observer) => {

        const tab = window.location.hash.replace("#/tab=", "");
        if (["territory", "rank", "upgrades", "armoury", "controls"].includes(tab)) { return; }

        [...mutations].forEach(mutation => {

            [...mutation.addedNodes].forEach(node => {

                // members list
                if (members_list_filter(node)) {
                    const members = node.querySelectorAll("li.table-row");
                    if (members.length) { members_list_display(members, key); }
                }

                // chains
                if (chain_list_filter(node)) {
                    chain_list_display(node.childNodes, key);

                    // observe new attacks
                    const callback_chain = (key) => {
                        return (mutations, observer) => {
                            [...mutations].forEach(mutation => {
                                const attacks = [...mutation.addedNodes];
                                chain_list_display(attacks, key);
                            });
                        };
                    };
                    const walls_observer = new MutationObserver(callback_chain(key));
                    walls_observer.observe(node, { childList: true });
                }

                // wars
                if (wars_list_filter(node)) {
                    // force display none for links
                    if(_is_attack_link(node)) { node.style.display = "None"; return true; }

                    // 2 lists for RW, 1 for walls and 1 for raids
                    [...node.querySelectorAll("div.members-cont > ul.members-list")].forEach(faction => {
                        const members = faction.querySelectorAll("li.your, li.enemy");

                        if (tab.includes("rank")) { // RW
                            if (Boolean(members[0].querySelector("div.attack").offsetParent)) {
                                wars_list_display(members, "rank-left", key);
                            } else {
                                wars_list_display(members, "rank-right", key);
                            }
                        } else if (tab.includes("raid")) {  // Raids
                            wars_list_display(members, "raid", key);
                        } else {  // walls
                            wars_list_display(members, "wall", key);

                            // observe wall jumps
                            const callback_walls = (key) => {
                                return (mutations, observer) => {
                                    const _f = (e) => { return e.className.includes("your") || e.className.includes("enemy"); };
                                    [...mutations].forEach(mutation => {
                                        const members = [...mutation.addedNodes].filter(_f);
                                        wars_list_display(members, "wall", key);
                                    });
                                };
                            };
                            const walls_observer = new MutationObserver(callback_walls(key));
                            walls_observer.observe(faction, { childList: true });
                        }
                    });
                }

            });
        });
    };
};

waitFor(document, "div#factions").then(factions => {

    const key = localStorage.getItem('yata-key');
    if (!key) { return; }

    const obs = new MutationObserver(callback_factions(key));
    obs.observe(factions, { childList: true, subtree: true });

});


// ------ //
// SEARCH //
// ------ //

const search_list_display = (players, key) => {
    console.log(`[yata] battle stats estimate for search list: ${players.length}`);

    const _f = (e) => {
        return e.tagName == "LI" &&
            !e.classList.contains("last") &&
            !e.querySelector("[id^=icon15]") &&
            !e.querySelector("[id^=icon70]") &&
            !e.querySelector("[id^=icon77]") &&
            !e.querySelector("[id^=icon71]");
    };
    [...players].filter(_f).forEach(player => {

        const target_id = getPlayerId(player, "username")
        const level = player.querySelector("span.level");
        const node = document.createElement("span");
        node.innerHTML = bse_html(target_id, "loading");
        node.style.float = 'left';
        level.insertAdjacentElement('afterbegin', node);

        gmGet(`https://yata.yt/api/v1/bs/${target_id}/?key=${key}`, `bs-${target_id}`).then(bs => {
            node.innerHTML = bse_html(target_id, bs[target_id]);
        }).catch((error) => {
            node.innerHTML = bse_html(target_id, error);
        });

    });
};

const callback_search = (key) => {
    return (mutations, observer) =>{
        [...mutations].filter(m => m.addedNodes.length).forEach(players => {
            search_list_display(players.addedNodes, key);
        })
    }
}

waitFor(document, "div.userlist-wrapper").then(list => {

    const key = localStorage.getItem('yata-key');
    if (!key) { return; }

    const ul = list.querySelector("ul.user-info-list-wrap");
    if (!ul) { return; }


    const obs = new MutationObserver(callback_search(key));
    obs.observe(ul, { childList: true });

})