Dibs

Claim dibs in territory wars to avoid wasting E

Você precisará instalar uma extensão como Tampermonkey, Greasemonkey ou Violentmonkey para instalar este script.

Você precisará instalar uma extensão como Tampermonkey para instalar este script.

Você precisará instalar uma extensão como Tampermonkey ou Violentmonkey para instalar este script.

Você precisará instalar uma extensão como Tampermonkey ou Userscripts para instalar este script.

Você precisará instalar uma extensão como o Tampermonkey para instalar este script.

Você precisará instalar um gerenciador de scripts de usuário para instalar este script.

(Eu já tenho um gerenciador de scripts de usuário, me deixe instalá-lo!)

Você precisará instalar uma extensão como o Stylus para instalar este estilo.

Você precisará instalar uma extensão como o Stylus para instalar este estilo.

Você precisará instalar uma extensão como o Stylus para instalar este estilo.

Você precisará instalar um gerenciador de estilos de usuário para instalar este estilo.

Você precisará instalar um gerenciador de estilos de usuário para instalar este estilo.

Você precisará instalar um gerenciador de estilos de usuário para instalar este estilo.

(Eu já possuo um gerenciador de estilos de usuário, me deixar fazer a instalação!)

// ==UserScript==
// @name         Dibs
// @namespace    https://tornicorn.rocks/
// @version      0.8.1
// @description  Claim dibs in territory wars to avoid wasting E
// @author       sullengenie [1946152], Kessica [440210]
// @run-at       document-start
// @match        https://www.torn.com/factions.php?step=your
// @grant        GM_getValue
// @grant        GM_setValue
// @grant        GM_xmlhttpRequest
// @grant        unsafeWindow
// @connect      tornicorn.rocks
// ==/UserScript==

(function() {
    'use strict';

    const joinTimestamps = {};
    const dibsCalls = {};
    const currentDibs = {};

    function uuidv4() {
        return ([1e7]+-1e3+-4e3+-8e3+-1e11).replace(/[018]/g, c => (c ^ crypto.getRandomValues(new Uint8Array(1))[0] & 15 >> c / 4).toString(16));
    }

    const getRequesterId = () => {
        let storedId = GM_getValue('requesterId');
        if (storedId === undefined) {
            storedId = uuidv4();
            GM_setValue('requesterId', storedId);
        }
        return storedId;
    };

    const requesterId = getRequesterId();

    const createHtml = (html) => document.createRange().createContextualFragment(html);
    const insertBefore = (nodes, target) => {
        target.parentNode.insertBefore(nodes, target);
        return target.previousSibling;
    };
    const hide = (element) => { element.style.display = 'none'; };
    const show = (element) => { element.style.display = 'inline'; };

    const getPlayerIdFromRow = (rowNode) => {
        let nameButton = rowNode.querySelector('a.name');
        return parseInt(nameButton.getAttribute('href').split('=', 2)[1]);
    };

    const clearCurrentDibs = () => {
        delete currentDibs.id;
        if (currentDibs.timeoutId) {
            clearInterval(currentDibs.timeoutId);
            delete currentDibs.timeoutId;
        }
    };

    const showAttackButton = (attackButton, dibsButton) => {
        show(attackButton);
        hide(dibsButton);
    };

    const hideAttackButton = (attackButton, dibsButton) => {
        hide(attackButton);
        show(dibsButton);
    };

    const dibsSuccess = (dibsCalls, id) => {
        if (currentDibs.id) {
            hide(dibsCalls[currentDibs.id].attackButton);
            show(dibsCalls[currentDibs.id].dibsButton);
            clearCurrentDibs();
        }
        currentDibs.id = id;
        show(dibsCalls[id].attackButton);
        hide(dibsCalls[id].dibsButton);
        let timeoutId = setTimeout(() => {
            hide(dibsCalls[id].attackButton);
            show(dibsCalls[id].dibsButton);
            clearCurrentDibs();
        }, 30000);
        currentDibs.timeoutId = timeoutId;
    };

    const createDibsFragment = () => {
        let dibsFragment = createHtml(`<a class="c-pointer">Dibs!</a>`);
        return dibsFragment;
    };

    const createCountdownFragment = (timer) => {
        return createHtml(`<span style="color: #666; display: none">${timer}</span>`);
    };

    const addCountdownFragment = (countdownFragment, dibsButton) => {
        return insertBefore(countdownFragment, dibsButton);
    };

    const replaceWithDibsButton = (attackButton, id) => {
        let dibsFragment = createDibsFragment();
        let dibsButton = insertBefore(dibsFragment, attackButton);
        dibsButton.addEventListener('click', () => {
            let joinTimestamp = joinTimestamps[id];
            console.debug('Clicked on enemy with id ' + id + ' and timestamp ' + joinTimestamp);

            let start = Date.now();
            GM_xmlhttpRequest({
                method: 'POST',
                url: `https://tornicorn.rocks/dibs?target-id=${id}&timestamp=${joinTimestamp}&user-id=${requesterId}`,
                timeout: 5000,
                onload: (response) => {
                    if (response.status === 200) {
                        dibsSuccess(dibsCalls, id);
                        console.debug('Lock acquired', Date.now() - start);
                    } else if (response.status === 400) {
                        setDibsTimer(dibsCalls, id);
                    } else {
                        console.error('Unexpected response', response);
                    }
                }
            });
        });
        hide(attackButton);
        return dibsButton;
    };

    const clearDibsTimer = (dibsCalls, id) => {
        clearInterval(dibsCalls[id].intervalId);
        if (dibsCalls[id].countdownNode) {
            hide(dibsCalls[id].countdownNode);
        }
        if (dibsCalls[id].dibsButton) {
            show(dibsCalls[id].dibsButton);
        }
        delete dibsCalls[id].intervalId;
        delete dibsCalls[id].timer;
    };

    const rejectionMessage = 'Nope!';

    const updateDibsTimer = (dibsCalls, id) => {
        let secsRemaining = dibsCalls[id].timer;
        if (secsRemaining <= 1) {
            clearDibsTimer(dibsCalls, id);
        } else if (secsRemaining >= 1) {
            dibsCalls[id].timer -= 1;
            if (dibsCalls[id].countdownNode) {
                dibsCalls[id].countdownNode.innerText = (secsRemaining > 25 ? rejectionMessage : secsRemaining - 1);
            }
        } else {
            console.error('Unexpected dibs state');
            console.error(dibsCalls);
        }
    };

    const startCountdown = (dibsCalls, id) => {
        const dibs = dibsCalls[id];
        dibs.countdownNode.innerText = rejectionMessage;
        show(dibs.countdownNode);
        hide(dibs.dibsButton);
        dibs.intervalId = setInterval(() => updateDibsTimer(dibsCalls, id), 1000);
    };

    const setDibsTimer = (dibsCalls, id) => {
        dibsCalls[id].timer = 30;
        startCountdown(dibsCalls, id);
    };

    const resumeDibsTimer = (dibsCalls, id) => {
        startCountdown(dibsCalls, id);
    };

    const handleAddedEnemy = (rowNode) => {
        let attackButton = rowNode.querySelector('.attack a');
        let id = getPlayerIdFromRow(rowNode);
        let dibsButton = replaceWithDibsButton(attackButton, id);
        let countdownNode = addCountdownFragment(createCountdownFragment(30), dibsButton);
        // Clear old timer updater if it exists
        if (dibsCalls[id] && dibsCalls[id].intervalId) {
            clearInterval(dibsCalls[id].intervalId);
        }
        let {timer,joinTimestamp} = dibsCalls[id] || {};
        dibsCalls[id] = {attackButton, dibsButton, countdownNode, joinTimestamp: joinTimestamps[id]};
        if (timer && joinTimestamp === joinTimestamps[id]) {
            // If someone else still has dibs
            dibsCalls[id].timer = timer;
            resumeDibsTimer(dibsCalls, id);
        } else if (currentDibs[id] === id) {
            // If we have dibs
            hide(dibsButton);
            show(attackButton);
        }
    };

    const handleRemovedEnemy = (rowNode) => {
        // NOTE: We do not remove timer or joinTimestamp so they persist for
        // when this tab is reopened
        let attackButton = rowNode.querySelector('.attack a');
        let id = getPlayerIdFromRow(rowNode);
        delete dibsCalls[id].attackButton;
        delete dibsCalls[id].dibsButton;
        delete dibsCalls[id].countdownNode;
        if (dibsCalls[id].intervalId) {
            clearInterval(dibsCalls[id].intervalId);
        }
        if (currentDibs.id === id) {
            clearCurrentDibs();
        }
    };

    const waitForPageLoad = () => {
        return new Promise(function(resolve, reject) {
            console.debug('Waiting for page load');
            let target = document.getElementById('war-react-root').firstChild;
            console.debug(target);
            console.debug(target.childNodes.length);
            if (document.querySelector('.f-war-list')) {
                resolve(true);
            }
            let observer = new MutationObserver(function(mutations) {
                for (let mutation of mutations) {
                    console.debug(mutation);
                    if (mutation.addedNodes.length > 0) {
                        resolve(true);
                        return;
                    }
                }
            });
            observer.observe(target, {childList: true});
        });
    };

    const watchMembersList = (memberList) => {
        let observer = new MutationObserver(function(mutations) {
            for (let mutation of mutations) {
                console.debug('Member mutation');
                console.debug(mutation);
                for (let node of mutation.addedNodes) {
                    if (node.classList.contains('enemy')) {
                        console.debug('New enemy spotted!');
                        console.debug(node);
                        handleAddedEnemy(node);
                        // let attackButton = node.querySelector('.attack a');
                        // let nameButton = node.querySelector('a.name');
                        // let id = parseInt(nameButton.getAttribute('href').split('=', 2)[1]);
                        // let dibsButton = replaceWithDibsButton(attackButton, id, joinTimestamps[id]);
                        // console.debug('Enemy id: ' + id);
                    }
                }
                for (let node of mutation.removedNodes) {
                    if (node.classList.contains('enemy')) {
                        console.debug('Enemy off the wall!');
                        console.debug(node);
                        handleRemovedEnemy(node);
                        // let nameButton = node.querySelector('a.name');
                        // let id = parseInt(nameButton.getAttribute('href').split('=', 2)[1]);
                        // console.debug('Enemy id: ' + id);
                    }
                }
            }
        });
        observer.observe(memberList, {childList: true});
    };

    const waitForDescriptionLoad = () => {
        return new Promise(function(resolve, reject) {
            let target = document.querySelector('.desc-wrap');
            console.debug(target);
            console.debug('War list');
            console.debug(document.querySelector('.war-list'));
            console.debug('Descriptions');
            console.debug(document.querySelector('.desc-wrap'));
            console.debug('Faction war');
            console.debug(document.querySelector('.faction-war'));
            const factionWar = document.querySelector('.faction-war');
            if (factionWar) {
                console.debug('Faction war found!');
                console.debug(factionWar.querySelector('.members-list'));
                watchMembersList(factionWar.querySelector('.members-list'));
                for (let enemyNode of factionWar.querySelectorAll('.members-list .enemy:not(.row-animation-new)')) {
                    console.debug('Enemy on the wall!', enemyNode);
                    handleAddedEnemy(enemyNode);
                    // let attackButton = enemyNode.querySelector('.attack a');
                    // let nameButton = enemyNode.querySelector('a.name');
                    // let id = parseInt(nameButton.getAttribute('href').split('=', 2)[1]);
                    // let dibsButton = replaceWithDibsButton(attackButton, id, joinTimestamps[id]);
                    // console.debug('Group enemy id: ' + id);
                }
            }
            let observer = new MutationObserver(function(mutations) {
                for (let mutation of mutations) {
                    console.debug('Description load mutation');
                    console.debug(mutation);
                    for (let node of mutation.addedNodes) {
                        console.debug(node);
                        if (node.classList.contains('faction-war')) {
                            console.debug('Faction war!');
                            console.debug(node.querySelector('.members-list'));
                            watchMembersList(node.querySelector('.members-list'));
                            for (let enemyNode of node.querySelectorAll('.members-list .enemy:not(.row-animation-new)')) {
                                console.debug('Enemy on the wall!', enemyNode);
                                handleAddedEnemy(enemyNode);
                                // let attackButton = enemyNode.querySelector('.attack a');
                                // let nameButton = enemyNode.querySelector('a.name');
                                // let id = parseInt(nameButton.getAttribute('href').split('=', 2)[1]);
                                // let dibsButton = replaceWithDibsButton(attackButton, id, joinTimestamps[id]);
                                // console.debug('Group enemy id: ' + id);
                            }
                        }
                    }
                    for (let node of mutation.removedNodes) {
                        console.debug(node);
                        if (node.classList.contains('faction-war')) {
                            console.debug('Faction war!');
                            console.debug(node.querySelector('.members-list'));
                            for (let enemyNode of node.querySelectorAll('.members-list .enemy:not(.row-animation-new)')) {
                                console.debug('Enemy on the wall!', enemyNode);
                                handleRemovedEnemy(enemyNode);
                            }
                        }
                    }
                }
            });
            observer.observe(target, {childList: true});
        });
    };

    const watchForDescriptionChanges = () => {
        console.debug('Watching for description changes');
        let warList = document.querySelector('.f-war-list');
        console.debug(warList);
        let observer = new MutationObserver(function(mutations) {
            for (let mutation of mutations) {
                console.debug('Description change');
                console.debug(mutation);
                for (let node of mutation.addedNodes) {
                    if (node.classList.contains('descriptions')) {
                        waitForDescriptionLoad();
                    }
                }
            }
        });
        observer.observe(warList, {childList: true});
    };

    const noop = () => {};

    const customFetch = (fetch, {
        onrequest = noop,
        onresponse = noop,
        onresult = noop,
        onbody = [],
    }) => async (input, init) => {
        onrequest(input, init);
        const response = await fetch(input, init);
        onresponse(response);

        for (const handler of onbody) {
            if (handler.match(response)) {
                Promise.resolve(handler.execute(response.clone()))
                    .then((result) => onresult(result));
            }
        }

        return response;
    };

    const interceptFetch = (options) => (unsafeWindow.fetch = customFetch(fetch, options));

    // usage

    interceptFetch({
        //onrequest: (input, init) => console.debug('FETCH CALL', input, init),
        //onresponse: (response) => console.debug('FETCH RESPONSE', response),

        onbody: [{
            match: (response) => response.url.startsWith('https://www.torn.com/faction_wars.php'),
            execute: (response) => response.json().then((json) => {
                if (json.warDesc && json.warDesc.members) {
                    for (let member of json.warDesc.members) {
                        if (member !== 0) {
                            joinTimestamps[member.userID] = member.joinTimestamp;
                        }
                    }
                }
            })
        }],
    });

    var pollId;
    pollId = setInterval(() => {
        console.debug('polling');
        if (document && document.getElementById('war-react-root') && document.getElementById('war-react-root').firstChild !== null) {
            console.debug('done polling');
            console.debug(document.getElementById('war-react-root').firstChild);
            clearInterval(pollId);
            waitForPageLoad().then(() => {
                watchForDescriptionChanges();
                console.debug('Moving along');
                console.debug(document.querySelector('.desc-wrap'));
                if (document.querySelector('.desc-wrap') !== null) {
                    waitForDescriptionLoad().then(() => console.debug('Description Loaded!'));
                }
            });
        }
    }, 100);
})();