Neopets - Karla's TVW Auto Void Essence Collector

Collects all void essence with one click!

// ==UserScript==
// @name         Neopets - Karla's TVW Auto Void Essence Collector
// @namespace    karla@neopointskarla
// @license      GPL3
// @version      0.0.3
// @description  Collects all void essence with one click!
// @author       Karla
// @match        *://*.neopets.com/tvw*
// @icon         https://github.com/karlaneo/neopets-scripts/blob/main/favicon-32x32.png?raw=true
// @grant        GM_xmlhttpRequest
// ==/UserScript==

const maps = [
    {
        "url": "https://www.neopets.com/altador/index.phtml",
        "map": "Altador"
    },
    {
        "url": "https://www.neopets.com/medieval/brightvale.phtml",
        "map": "Brightvale"
    },
    {
        "url": "https://www.neopets.com/medieval/index_evil.phtml",
        "map": "Darigan Citadel"
    },
    {
        "url": "https://www.neopets.com/faerieland/faeriecity.phtml",
        "map": "Faerie City"
    },
    {
        "url": "https://www.neopets.com/faerieland/index.phtml",
        "map": "Faerieland"
    },
    {
        "url": "https://www.neopets.com/halloween/index.phtml",
        "map": "Haunted Woods"
    },
    {
        "url": "https://www.neopets.com/worlds/index_kikolake.phtml",
        "map": "Kiko Lake"
    },
    {
        "url": "https://www.neopets.com/pirates/index.phtml",
        "map": "Krawk Island"
    },
    {
        "url": "https://www.neopets.com/moon/index.phtml",
        "map": "Kreludor"
    },
    {
        "url": "https://www.neopets.com/tropical/index.phtml",
        "map": "Lutari Island"
    },
    {
        "url": "https://www.neopets.com/water/index.phtml",
        "map": "Maraqua"
    },
    {
        "url": "https://www.neopets.com/medieval/index_farm.phtml",
        "map": "Meri Acres Farms"
    },
    {
        "url": "https://www.neopets.com/medieval/index.phtml",
        "map": "Meridell"
    },
    {
        "url": "https://www.neopets.com/medieval/index_castle.phtml",
        "map": "Meridell Castle"
    },
    {
        "url": "https://www.neopets.com/magma/caves.phtml",
        "map": "Moltara Caves"
    },
    {
        "url": "https://www.neopets.com/magma/index.phtml",
        "map": "Moltara City"
    },
    {
        "url": "https://www.neopets.com/island/index.phtml",
        "map": "Mystery Island"
    },
    {
        "url": "https://www.neopets.com/objects.phtml",
        "map": "Neopia Central"
    },
    {
        "url": "https://www.neopets.com/market_bazaar.phtml",
        "map": "Neopian Bazaar"
    },
    {
        "url": "https://www.neopets.com/market_map.phtml",
        "map": "Neopian Marketplace"
    },
    {
        "url": "https://www.neopets.com/market_plaza.phtml",
        "map": "Neopian Plaza"
    },
    {
        "url": "https://www.neopets.com/halloween/neovia.phtml",
        "map": "Neovia"
    },
    {
        "url": "https://www.neopets.com/desert/qasala.phtml",
        "map": "Qasala"
    },
    {
        "url": "https://www.neopets.com/worlds/index_roo.phtml",
        "map": "Roo Island"
    },
    {
        "url": "https://www.neopets.com/desert/sakhmet.phtml",
        "map": "Sakhmet"
    },
    {
        "url": "https://www.neopets.com/shenkuu/index.phtml",
        "map": "Shenkuu"
    },
    {
        "url": "https://www.neopets.com/winter/index.phtml",
        "map": "Terror Mountain: Happy Valley"
    },
    {
        "url": "https://www.neopets.com/winter/icecaves.phtml",
        "map": "Terror Mountain: Ice Caves"
    },
    {
        "url": "https://www.neopets.com/winter/terrormountain.phtml",
        "map": "Terror Mountain: Top of the Mountain"
    },
    {
        "url": "https://www.neopets.com/halloween/index_fair.phtml",
        "map": "The Deserted Fairground"
    },
    {
        "url": "https://www.neopets.com/worlds/index_geraptiku.phtml",
        "map": "The Lost City of Geraptiku"
    },
    {
        "url": "https://www.neopets.com/desert/index.phtml",
        "map": "The Lost Desert"
    },
    {
        "url": "https://www.neopets.com/water/index_ruins.phtml",
        "map": "The Ruins of Maraqua"
    },
    {
        "url": "https://www.neopets.com/prehistoric/index.phtml",
        "map": "Tyrannia"
    },
    {
        "url": "https://www.neopets.com/prehistoric/plateau.phtml",
        "map": "Tyrannian Plateau"
    },
    {
        "url": "https://www.neopets.com/space/hangar.phtml",
        "map": "Virtupets Space Station: Hangar"
    },
    {
        "url": "https://www.neopets.com/space/recreation.phtml",
        "map": "Virtupets Space Station: Recreation Deck"
    },
    {
        "url": "https://www.neopets.com/space/index.phtml",
        "map": "Virtupets Space Station: Supply Deck"
    },
    {
        "url": "https://www.neopets.com/pirates/warfwharf.phtml",
        "map": "Warf Wharf"
    }
];

const sleep = (time) =>
  new Promise((resolve) => setTimeout(resolve, time));

const random_in_range = (start, end) => {
  return Math.floor(Math.random() * (end - start + 1) + start);
};


async function autoCollect(mapData) {
    const statusEl = document.querySelector('#k-status');
    let jellyneoLinks = [];
    try {
        statusEl.innerHTML = 'Attempting to load maps from jellyneo...';
        jellyneoLinks = await new Promise(function(resolve, reject) {
            GM_xmlhttpRequest({
                method: "GET",
                url: "https://www.jellyneo.net/?go=the_void_within&id=essence_collection#locations",
                onload: function(response) {
                    const div = document.createElement('div');
                    div.innerHTML = response.responseText;
                    resolve([...div.querySelectorAll('.alert-box a[href^="https://www.neopets.com"]')].map(n => n.href));
                },
                onerror: function(err) {
                    reject();
                },
            });
        });
    } catch (e) {
        console.log(e);
        statusEl.innerHTML = 'Load from jellyneo failed, checking all maps...';
    }
    if (jellyneoLinks.length > 0) {
        statusEl.innerHTML = 'Load from jellyneo successful, only checking maps with void essence';
        mapData = mapData.filter(({ url }) => jellyneoLinks.indexOf(url) > -1);
    }
    try {
        for (let i = 0; i < mapData.length; i += 1) {
            const { url, map } = mapData[i];
            statusEl.innerHTML = `Checking ${map}...`;
            const res = await fetch(url, {
                "headers": {
                    "accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7",
                    "accept-language": "en-US,en;q=0.9,zh-CN;q=0.8,zh;q=0.7,zh-TW;q=0.6,es;q=0.5",
                    "cache-control": "no-cache",
                },
                "body": null,
                "method": "GET",
                "mode": "cors",
                "credentials": "include"
            });
            const html = await res.text();
            const ckMatch = html.match(/getCK\(\)\s*{\s*return\s*'([^']+)'/);
            const ck = ckMatch ? ckMatch[1] : null;

            const arrayMatch = html.match(/placeEssenceOnMap\((\[.*?\])\)/s);
            let essenceData = [];

            if (arrayMatch) {
                try {
                    const parsed = JSON.parse(arrayMatch[1]);
                    essenceData = parsed.map(e => ({
                        id: e.id,
                        hash: e.hash,
                        day: e.day
                    }));
                } catch (e) {
                    console.error("Failed to parse essence array:", e);
                }
            }

            if (essenceData.length === 0) {
                statusEl.innerHTML = `${map} no void essence, checking next map`;
            } else {
                if (!ck) {
                    throw new Error('Error! No ck found');
                }
                statusEl.innerHTML = `${map} has ${essenceData.length} void essences, collecting...`;
                await sleep(random_in_range(1000, 2000));
                for (let j = 0; j < essenceData.length; j += 1) {
                    const { hash, id, day } = essenceData[j];
                    if (!hash || !id || !day) {
                        throw new Error('Error! Invalid void essence data');
                    }
                    statusEl.innerHTML = `${map} collecting essence id ${id}...`;
                    const formData = new FormData();
                    formData.append('hash', hash);
                    formData.append('id', id);
                    formData.append('day', day);
                    formData.append('_ref_ck', ck);
                    const collectRes = await fetch("https://www.neopets.com/np-templates/ajax/plots/tvw/void-collection/collect_void.php", {
                        "headers": {
                            "accept": "*/*",
                            "accept-language": "en-US,en;q=0.9,zh-CN;q=0.8,zh;q=0.7,zh-TW;q=0.6,es;q=0.5",
                            "cache-control": "no-cache",
                            "x-requested-with": "XMLHttpRequest"
                        },
                        "referrer": "https://www.neopets.com/pirates/index.phtml",
                        "body": formData,
                        "method": "POST",
                        "mode": "cors",
                        "credentials": "include"
                    }).then((res) => res.json());
                    if (!collectRes.success) {
                        throw new Error('Error! Collect void essence failed');
                    }
                    statusEl.innerHTML = `${map} essence id ${id} collected successfully`;
                    const counter = vcItem.querySelector('.vc-progress-amt');
                    const [currentCount] = counter.innerHTML.split('/');
                    counter.innerHTML = `${+currentCount + 1}/10`;
                    if (currentCount == 9) {
                        document.querySelector('#k-button').disabled = true;
                        statusEl.innerHTML = 'Auto collector finished successfully!';
                        return;
                    }
                    vcItem.querySelector('.vc-progress-bar').style.width = `calc(${+currentCount + 1} / 10 * 100%)`;
                    await sleep(random_in_range(1000, 2000));
                }
            }


            await sleep(random_in_range(1000, 2000));
        }
    } catch (e) {
        console.log(e);
        statusEl.innerHTML = e.message;
    }
}

let vcItem;
(function() {
    'use strict';

    // Your code here...
    vcItem = document.querySelector('.vc-item.locked')?.previousElementSibling || Array.from(document.querySelectorAll('.vc-item')).pop();
    const panel = document.querySelector('#VoidCollectionModule');
    const button = document.createElement('button');
    const counter = vcItem.querySelector('.vc-progress-amt');
    const [currentCount] = counter.innerHTML.split('/');
    button.id = 'k-button';
    button.className = 'button-default__2020 button-purple__2020 btn-single__2020 plothub-button';
    console.log(counter, currentCount);
    button.innerHTML = currentCount == 10 ? 'Collect Completed' : 'Auto Collect';
    button.disabled = currentCount == 10;
    panel.appendChild(button);
    button.addEventListener('click', function() { autoCollect(maps) });
    const status = document.createElement('div');
    status.id = 'k-status';
    status.style.textAlign = 'center';
    status.style.marginTop = '10px';
    status.style.coloc = '#0E0134';
    status.style.fontWeight = 'bold';
    panel.appendChild(status);
})();