Greasy Fork is available in English.

Torn NPC Attack Time Newsfeed

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

// ==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);
            }
        })
    });
}