YATA

Displays various informations (NNB/Stats)

当前为 2023-10-14 提交的版本,查看 最新版本

// ==UserScript==
// @name         YATA
// @namespace    yata.yt
// @version      0.7
// @grant        GM_addStyle
// @description  Displays various informations (NNB/Stats)
// @author       Kivou [2000607]
// @grant        GM.xmlHttpRequest
// @match        https://www.torn.com/factions.php*
// @match        https://www.torn.com/preferences.php*
// @match        https://www.torn.com/profiles.php*
// @icon         https://yata.yt/media/yata-small.png
// @run-at       document-end
// @license      WTFPL
// ==/UserScript==

// Copyright © 2023 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.


// ---------------- //
// HELPER FUNCTIONS //
// ---------------- //

const waitForElement = (target, selector) => {
    return new Promise(resolve => {
        if (target.querySelector(selector)) {
            return resolve(target.querySelector(selector));
        }
        const observer = new MutationObserver(mutations => {
            if (target.querySelector(selector)) {
                resolve(target.querySelector(selector));
                observer.disconnect();
            }
        });
        observer.observe(document.body, {
            childList: true,
            subtree: true
        });
    });
};

const waitForElements = (target, selector) => {
    return new Promise(resolve => {
        if (target.querySelector(selector)) {
            return resolve(target.querySelectorAll(selector));
        }
        const observer = new MutationObserver(mutations => {
            if (target.querySelector(selector)) {
                resolve(target.querySelectorAll(selector));
                observer.disconnect();
            }
        });
        observer.observe(document.body, {
            childList: true,
            subtree: true
        });
    });
};


const gmGet = async (url, cache_key) => {

    if (cache_key) {
        const cachedData = localStorage.getItem(cache_key);
        const cachedTimestamp = parseInt(localStorage.getItem(cache_key + "_timestamp"));
        if (cachedData && cachedTimestamp && (Date.now() - cachedTimestamp) < 60 * 60 * 1000) {
            return JSON.parse(cachedData);
        }
    }

    return new Promise((resolve, reject) => {
        GM.xmlHttpRequest({
            url,
            method: "GET",
            onload: (response) => {
                resolve(new Response(response.response, { statusText: response.statusText, status: response.status }));
            },
            onerror: (error) => {
                reject(error);
            }
        });
    })
        .catch((error) => {
            throw { message: "critical error", code: error.status };
        })
        .then((response) => {
            const result = response.json();
            return result.then((body) => {
                if (typeof body.error == 'undefined') {
                    localStorage.setItem(cache_key, JSON.stringify(body));
                    localStorage.setItem(cache_key + "_timestamp", Date.now());
                    return body;
                } else {
                    throw { message: body.error.error, code: body.error.code };
                }
            });
        });
};

const display_player = (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>`;
    }
};

function nFormatter(num, digits) {
    const lookup = [
        { value: 1, symbol: "" },
        { value: 1e3, symbol: "k" },
        { value: 1e6, symbol: "m" },
        { value: 1e9, symbol: "b" },
        { value: 1e12, symbol: "t" },
        { value: 1e15, symbol: "q" }
    ];
    const rx = /\.0+$|(\.[0-9]*[1-9])0+$/;
    var item = lookup.slice().reverse().find(function (item) {
        return num >= item.value;
    });
    return item ? (num / item.value).toPrecision(digits).replace(rx, "$1") + item.symbol : "0";
}

const display_status = (page, element) => {
    const key = localStorage.getItem('key');
    let innerHTML = "";

    if (page == "preferences") {
        innerHTML += `<div>`;
        innerHTML += `<b>[YATA]</b> API key used <span id="yata-api-key" style="font-family: monospace; font-weight: bold;">${key}</span>`;
        if (key) {
            innerHTML += ` | Status <b id="yata-status" style="color: var(--default-green-color); font-weight: bold;">enabled</b>`;
            innerHTML += ` | Click <span id="yata-api-key-rm" class="t-blue" style="cursor: pointer;">here to disable</span> the script`;
        } else {
            innerHTML += ` | Status <b id="yata-status" style="color: var(--default-red-color); font-weight: bold;">disabled</b>`;
            innerHTML += ` | Click on a key to enable the script`;
        }
        innerHTML += `</div>`;
        innerHTML += `<div class="clear"></div>`;
        innerHTML += `<hr class="page-head-delimiter m-top10">`;
    } else if (page == "faction") {
        innerHTML += `<hr class="page-head-delimiter m-top10 m-bottom10">`;
        innerHTML += `<div>`;
        if (key) {
            innerHTML += `<b>[YATA]</b> <b style="color: var(--default-green-color)">Enabled</b>`;
            innerHTML += ` | Visit <a href="/preferences.php#tab=api" class="t-blue">preferences</a> to change your key or disable the script`;
        } else {
            innerHTML += `<b>[YATA]</b> <b style="color: var(--default-red-color)">API key not found</b>`;
            innerHTML += ` | Visit <a href="/preferences.php#tab=api" class="t-blue">preferences</a> to enable the script`;
        }
        innerHTML += `</div>`;
        innerHTML += `<div class="clear"></div>`;
    } else if (page == "profiles") {
        innerHTML += `<hr class="page-head-delimiter m-top10 m-bottom10">`;
        innerHTML += `<div>`;
        if (key) {
            innerHTML += `<b>[YATA]</b> <b style="color: var(--default-green-color)">Displaying stats estimate</b>`;
            innerHTML += ` | Visit <a href="/preferences.php#tab=api" class="t-blue">preferences</a> to change your key or disable the script`;
        } else {
            innerHTML += `<b>[YATA]</b> <b style="color: var(--default-red-color)">API key not found</b>`;
            innerHTML += ` | Visit <a href="/preferences.php#tab=api" class="t-blue">preferences</a> to enable the script`;
        }
        innerHTML += `</div>`;
        innerHTML += `<div class="clear"></div>`;
    }
    element.innerHTML = innerHTML;
};

//
const display_faction = document.createElement("div");

// ------------- //
// SETUP API KEY //
// ------------- //

waitForElement(document, "div.preferences-container").then(div => {

    let injected = false;
    const display_element = document.createElement("div");

    // triggered by clicking on crimes tab
    const callback = (mutations, observer) => {
        [...mutations].forEach(mutation => {
            [...mutation.addedNodes].filter(n => n.className && n.className.includes("keyRow___")).forEach(node => {
                const key_node = node.querySelector("input");
                key_node.style.cursor = "pointer";
                // if(!localStorage.getItem('key')) {
                //     localStorage.setItem('key', key_node.value);
                //     document.getElementById("yata-api-key").innerHTML = localStorage.getItem('key')
                // }
            });
        });
        if (!injected) {
            display_status("preferences", display_element);
            div.insertAdjacentElement('beforebegin', display_element);
            injected = true;
        }
    };
    const observer = new MutationObserver(callback);
    observer.observe(div, { childList: true, subtree: true });

    document.querySelector("div.content-wrapper").addEventListener('click', e => {
        const button = e.target;
        if (button.tagName == 'INPUT' && button.id.includes('key-row')) {
            localStorage.setItem('key', button.value);
        }
        else if (button.tagName == 'SPAN' && button.id == 'yata-api-key-rm') {
            localStorage.clear();
        }
        display_status("preferences", display_element);
    });

});


// ----------- //
// FILL UP NNB //
// ----------- //

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

    const API_KEY = localStorage.getItem('key');

    display_status("faction", display_faction);
    div.insertAdjacentElement('beforebegin', display_faction);

    if (!API_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=${API_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_player(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 => {
                        ul.querySelectorAll("ul.item").forEach(player => {
                            display_player(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 => {
        alert(`[yata - oc] ${error.message}`);
        if (error.code == 4) {
            localStorage.removeItem('key');
        }
    });
});


// ---------------------- //
// DISPLAY STATS ESTIMATE //
// Profile                //
// ---------------------- //

waitForElement(document, "div#profileroot").then(div => {

    const API_KEY = localStorage.getItem('key');
    const profile_url = new URLSearchParams(window.location.search);
    const target_id = profile_url.get("XID");

    const display_element = document.createElement("div");
    display_status("profiles", display_element);
    div.insertAdjacentElement('afterend', display_element);

    if (!API_KEY) { return; }

    gmGet(`https://yata.yt/api/v1/bs/${target_id}/?key=${API_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> ${nFormatter(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);

    }).catch(error => {
        alert(`[yata - oc] ${error.message}`);
        if (error.code == 4) {
            localStorage.removeItem('key');
        }
    });

});


// ---------------------- //
// DISPLAY STATS ESTIMATE //
// Member list            //
// ---------------------- //

const add_bs_to_members_list = (line, key) => {

    const url = line.children[0].querySelector("a[id$=-user]").href.split("?")[1];
    const profile_url = new URLSearchParams(url);
    const target_id = profile_url.get("XID");

    gmGet(`https://yata.yt/api/v1/bs/${target_id}/?key=${key}`, `bs-${target_id}`).then(bs => {
        let color = "var(--default-blue-color)";
        if (bs[target_id].type == "Offensive" && bs[target_id].skewness > 20) {
            color = "var(--default-red-color)";
        } else if (bs[target_id].type == "Defensive" && bs[target_id].skewness > 20) {
            color = "var(--default-green-color)";
        }
        const title = `Total stats: ${bs[target_id].total.toLocaleString("en-GB")} Build: ${bs[target_id].type} (${bs[target_id].skewness}%)`;
        const innerHTML = `<div title="${title}" style="color: ${color}; width: 5em; display: inline-block; cursor: help;">${nFormatter(bs[target_id].total, 3)}</div>`;
        const bs_node = document.createElement("div");
        bs_node.innerHTML = innerHTML;
        line.children[3].insertAdjacentElement('afterbegin', bs_node);
    });
};

waitForElement(document, "div#faction-info, div.members-list").then(div => {

    const API_KEY = localStorage.getItem('key');

    // display_status("faction", display_faction)
    // div.insertAdjacentElement('beforebegin', display_faction);

    if (!API_KEY) { return; }

    // triggered if directly landing on info tab
    waitForElements(div, "li.table-row").then(list => {
        list.forEach(line => add_bs_to_members_list(line, API_KEY));
    });

    // triggered by clicking on info tab
    const callback = (mutations, observer) => {
        [...mutations].forEach(mutation => {
            [...mutation.addedNodes].filter(node => node.id == "react-root-faction-info").forEach(node => {
                node.querySelectorAll("li.table-row").forEach(line => {
                    add_bs_to_members_list(line, API_KEY);
                });
            });
        });
    };
    const observer = new MutationObserver(callback);
    observer.observe(div, { childList: true });
});

// ---------------------- //
// DISPLAY STATS ESTIMATE //
// Walls                  //
// ---------------------- //

const add_bs_to_wall = (line, key) => {

    const url = line.querySelector("a.user.name").href.split("?")[1];
    const profile_url = new URLSearchParams(url);
    const target_id = profile_url.get("XID");
    gmGet(`https://yata.yt/api/v1/bs/${target_id}/?key=${key}`, `bs-${target_id}`).then(bs => {
        let color = "var(--default-blue-color)";
        if (bs[target_id].type == "Offensive" && bs[target_id].skewness > 20) {
            color = "var(--default-red-color)";
        } else if (bs[target_id].type == "Defensive" && bs[target_id].skewness > 20) {
            color = "var(--default-green-color)";
        }
        const title = `Total stats: ${bs[target_id].total.toLocaleString("en-GB")} Build: ${bs[target_id].type} (${bs[target_id].skewness}%)`;
        const innerHTML = `<div title="${title}" style="color: ${color}; display: inline-block; cursor: help;">${nFormatter(bs[target_id].total, 3)}</div>`;
        line.children[1].innerHTML = innerHTML;
    });
};


waitForElement(document, "ul#faction_war_list_id").then(ul => {

    const API_KEY = localStorage.getItem('key');

    if (!API_KEY) { return; }


    const _f = (element) => {
        return element.className.includes("your") || element.className.includes("enemy");
    };

    const walls_callback = (mutations, observer) => {

        console.log("mutations on the wall");
        console.log(mutations);
        [...mutations].forEach(mutation => {

            // player jump off the wall
            // [...mutation.removedNodes].filter(_f).forEach(e => {
            //     const user = e.querySelectorAll("a[class^=user]");
            //     const faction_id = get_faction_id(user[0].href);
            //     const player_id = get_player_id(user[1].href);
            //     console.log(`[kiv - off the wall] Player ${player_id} faction ${faction_id} jumped off the wall`);
            //     enable_member(player_id);
            // });

            // player jump on the wall
            [...mutation.addedNodes].filter(_f).forEach(member => {
                console.log("Added nodes");
                console.log(member);
                add_bs_to_wall(member, API_KEY);
            });
        });
    };

    const wars_callback = (mutations, observer) => {
        [...mutations].forEach(mutation => {
            [...mutation.addedNodes].filter(node => node.className == "faction-war").forEach(node => {
                const wall = ul.querySelector("div.members-cont > ul.members-list");
                console.log(wall);

                const walls_observer = new MutationObserver(walls_callback);
                walls_observer.observe(wall, { childList: true });

                wall.querySelectorAll("li.your").forEach(member => {
                    add_bs_to_wall(member, API_KEY);
                });

                // walls_observer.disconnect();
            });
        });
    };



    const wars_observer = new MutationObserver(wars_callback);
    wars_observer.observe(ul, { childList: true, subtree: true });

});

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址