Torn NPC Attack Time Newsfeed

Add NPC attack time to the news ticker using Loot Rangers for Torn

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

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

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

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

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name         Torn NPC Attack Time Newsfeed
// @namespace    npc.timing
// @version      v1.1.5
// @description  Add NPC attack time to the news ticker using Loot Rangers for Torn
// @author       IceBlueFire [776]
// @license      MIT
// @match        https://www.torn.com/*
// @exclude      https://www.torn.com/newspaper.php
// @exclude      https://www.torn.com/item.php
// @icon         https://www.google.com/s2/favicons?sz=64&domain=torn.com
// @grant        unsafeWindow
// @grant        GM_xmlhttpRequest
// @require      https://code.jquery.com/jquery-1.8.2.min.js
// @connect      api.lzpt.io
// ==/UserScript==

/******************** CONFIG SETTINGS ********************/
const color = "#8abeef"; // Any hex-code for the color to appear in the news feed as
const format = 24; // Time format. 12 = 12:00 AM format; 24 = 23:59 format
const local = false; // Adjust the timer to be local time or not. true = local; false = UTC

/****************** END CONFIG SETTINGS *******************/

const lzpt = getAttackTimes();
const { fetch: originalFetch } = unsafeWindow;
unsafeWindow.fetch = async (...args) => {
    var [resource, config] = args;
    var response = await originalFetch(resource, config);
    if(response.url.indexOf('?sid=newsTicker') === -1) return response;
    const json = () => response.clone().json()
    .then((data) => {
        data = { ...data };
        lzpt.then(function(result) {
            var attackOrder = '';
            var attackString = '';
            var attackLink = '';
            var attackTarget = 0;

            // If there's no clear time set
            if(result.time.clear == 0  && result.time.attack === false) {
                attackString = result.time.reason ? 'NPC attacking will resume after '+result.time.reason : 'No attack currently set.';
            } else {
                // Build the string for the attack order
                $.each(result.order, function(key, value) {
                    if(result.npcs[value].next){
                        // If there's an attack happening right now, cross out NPCs that are in the hospital
                        if(result.time.attack === true) {
                            if(result.npcs[value].hosp_out >= result.time.current) {
                                attackOrder += '<span style="text-decoration: line-through">'+result.npcs[value].name+'</span>, ';
                            } else {
                                attackOrder += result.npcs[value].name+', ';
                            }
                        } else {
                            attackOrder += result.npcs[value].name+', ';
                        }
                    }
                    // Adjust the current target based on if an attack is going and who isn't in the hospital yet
                    if(result.time.attack === true) {
                        if(result.npcs[value].hosp_out <= result.time.current) { // Check if the NPC is currently out of the hospital
                            if(attackTarget == 0) {
                                attackTarget = value;
                            }
                        }
                    }
                });

                // Check if target has been set, otherwise default to first in attack order
                if(attackTarget == 0) {
                    attackTarget = result.order[0];
                }

                // Clean up the attack order string
                attackOrder = attackOrder.slice(0, -2)+'.';

                // Check if an attack is currently happening and adjust the message accordingly
                if(result.time.attack === true) {
                    attackString = 'NPC attack is underway! Get in there and get some loot!';
                    attackLink = 'loader.php?sid=attack&user2ID='+attackTarget;
                } else {
                    attackString = 'NPC attack set for '+utcformat(result.time.clear)+'. Order is: '+attackOrder;
                    attackLink = 'loader.php?sid=attack&user2ID='+attackTarget;
                }
            }

            // Insert the custom news item into the news ticker
            let attackItem = {ID: 0, headline: '<span style="color:'+color+'; font-weight: bold;" id="icey-npctimer">'+attackString+'</span>', countdown: true, endTime: result.time.clear, link: attackLink, isGlobal: true, type: 'generalMessage'};
            data.headlines.unshift(attackItem);
        }, function(err) {
            console.log(err); // Error: "It broke"
        });

        return data
    })

    response.json = json;
    response.text = async () =>JSON.stringify(await json());

    return response;
};

function modifyContent() {
    return new Promise((resolve, reject) => {
        var ticker = document.querySelector('.news-ticker-countdown');
        ticker.style.color = color;
        var wrap = ticker.parentNode.parentNode.parentNode;
        var svg = wrap.children[0];
        svg.setAttribute('fill', color);
        svg.setAttribute('viewBox', "0 0 24 24");
        svg.setAttribute('height', '14');
        svg.setAttribute('width', '14');
        svg.children[0].setAttribute('d', 'M17.457 3L21 3.003l.002 3.523-5.467 5.466 2.828 2.829 1.415-1.414 1.414 1.414-2.474 2.475 2.828 2.829-1.414 1.414-2.829-2.829-2.475 2.475-1.414-1.414 1.414-1.415-2.829-2.828-2.828 2.828 1.415 1.415-1.414 1.414-2.475-2.475-2.829 2.829-1.414-1.414 2.829-2.83-2.475-2.474 1.414-1.414 1.414 1.413 2.827-2.828-5.46-5.46L3 3l3.546.003 5.453 5.454L17.457 3zm-7.58 10.406L7.05 16.234l.708.707 2.827-2.828-.707-.707zm9.124-8.405h-.717l-4.87 4.869.706.707 4.881-4.879v-.697zm-14 0v.7l11.241 11.241.707-.707L5.716 5.002l-.715-.001z');
        // console.log(svg);
        resolve('Content updated');
    });
}

const newstickerObserver = new MutationObserver((mutationsList, observer) => {
    if ($(".news-ticker-slide #icey-npctimer").length == 1) { // If it's showing the slide for NPCs
        // Once changes are observed, disconnect the observer to avoid infinite loop
        newstickerObserver.disconnect();

        // Modify the content of .news-ticker-wrapper
        modifyContent()
            .then(() => {
            // Re-observe the element after modifications and asynchronous operations are complete
            startNewstickerObserver();
        })
            .catch(error => console.error('Error updating content:', error));
    }
});

function startNewstickerObserver() {
    const target = document.querySelector('.news-ticker-slider-wrapper');
    if (target) {
        newstickerObserver.observe(target, {
            childList: true, // Set true if children of the target node are being added or removed.
            attributes: false, // Set true if attributes of the target node are being modified.
            subtree: true, // Set true if changes to descendants of the target node are to be observed.
            characterData: false // Set true if data of the target node itself is being modified.
        });
    }
}

/******************** HELPER FUNCTIONS ********************/

function waitForElm(selector) {
    return new Promise(resolve => {
        if (document.querySelector(selector)) {
            return resolve(document.querySelector(selector));
        }

        const observer = new MutationObserver(mutations => {
            if (document.querySelector(selector)) {
                observer.disconnect();
                resolve(document.querySelector(selector));
            }
        });

        // If you get "parameter 1 is not of type 'Node'" error, see https://stackoverflow.com/a/77855838/492336
        observer.observe(document.body, {
            childList: true,
            subtree: true
        });
    });
}

// Make sure the news ticker with the injected div is loaded
waitForElm('#icey-npctimer').then((elm) => {
    startNewstickerObserver();
    //console.log('Element is ready');
});

// Format the time in the appropriate fashion
function utcformat(d){
    d= new Date(d * 1000);
    if(local) {
        var tail= ' LT', D= [d.getFullYear(), d.getMonth()+1, d.getDate()],
            T= [d.getHours(), d.getMinutes(), d.getSeconds()];
    } else {
        var tail= ' TCT', D= [d.getUTCFullYear(), d.getUTCMonth()+1, d.getUTCDate()],
            T= [d.getUTCHours(), d.getUTCMinutes(), d.getUTCSeconds()];
    }
    if(format == 12) {
        /* 12 hour format */
        if(+T[0]> 12){
            T[0]-= 12;
            tail= 'PM '+tail;
        }
        else tail= 'AM '+tail;
    }
    var i= 3;
    while(i){
        --i;
        if(D[i]<10) D[i]= '0'+D[i];
        if(T[i]<10) T[i]= '0'+T[i];
    }
    return T.join(':')+ tail;
}

// Fetch the NPC details from Loot Rangers
async function getAttackTimes() {
    return new Promise(resolve => {
        const request_url = `https://api.lzpt.io/loot`;
        GM_xmlhttpRequest ({
            method:     "GET",
            url:        request_url,
            headers:    {
                "Content-Type": "application/json"
            },
            onload: response => {
                try {
                    const data = JSON.parse(response.responseText);
                    if(!data) {
                        console.log('No response from Loot Rangers');
                    } else {
                        return resolve(data)
                    }
                }
                catch (e) {
                    console.error(e);
                }

            },
            onerror: (e) => {
                console.error(e);
            }
        })
    });
}