Neopets: Battledome Item Logger

Logs what prizes you have received at the Battledome, not including neopoints, and exports it with the formatting of your choice

// ==UserScript==
// @name         Neopets: Battledome Item Logger
// @namespace    https://github.com/saahphire/NeopetsUserscripts
// @version      1.0.1
// @description  Logs what prizes you have received at the Battledome, not including neopoints, and exports it with the formatting of your choice
// @author       saahphire
// @homepageURL  https://github.com/saahphire/NeopetsUserscripts
// @homepage     https://github.com/saahphire/NeopetsUserscripts
// @match        *://*.neopets.com/dome/arena.phtml*
// @match        *://*.neopets.com/dome/record.phtml
// @icon         https://www.google.com/s2/favicons?sz=64&domain=neopets.com
// @license      The Unlicense
// @grant        GM.setValue
// @grant        GM.getValue
// @grant        GM.setClipboard
// ==/UserScript==

/*
•:•.•:•.•:•:•:•:•:•:•:••:•.•:•.•:•:•:•:•:•:•:•:•.•:•.•:•:•:•:•:•:•:••:•.•:•.•:•.•:•:•:•:•:•:•:•:•.•:•:•.•:•.••:•.•:•.••:
........................................................................................................................
☆ ⠂⠄⠄⠂⠁⠁⠂⠄⠄⠂✦ ⠂⠄⠄⠂⠁⠁⠂⠄⠄⠂☆ ⠂⠄⠄⠂⠁⠁⠂⠄⠄⠂✦ ⠂⠄⠄⠂⠁⠁⠂⠄⠂⠄⠄⠂☆ ⠂⠄⠄⠂⠁⠁⠂⠄⠄⠂✦ ⠂⠄⠄⠂⠁⠁⠂⠄⠂⠄⠄⠂☆ ⠂⠄⠄⠂⠁⠁⠂⠄⠄⠂✦
    This script does the following:
    - Remembers every item you have received as a prize for battledome fights
    - Adds a button to the Records page https://www.neopets.com/dome/record.phtml so you can see them
    - Allows you to filter by start and end time and date
    - Allows you to copy all results by clicking the monotype text
    - Configurable result formatting (edit resultFormat)
    - Configurable time formatting (edit timeFormat)
    
    Both results and filters are in NST. The start filter is inclusive, the end filter is not (filtering from X to Y
    includes X but not Y).

    This was originally made for the guild Keepers of Neopia's November 2025 Keeper Kombat challenge, but maybe others
    will find it useful.

    ✦ ⌇ saahphire
☆ ⠂⠄⠄⠂⠁⠁⠂⠄⠄⠂✦ ⠂⠄⠄⠂⠁⠁⠂⠄⠄⠂☆ ⠂⠄⠄⠂⠁⠁⠂⠄⠄⠂✦ ⠂⠄⠄⠂⠁⠁⠂⠄⠂⠄⠄⠂☆ ⠂⠄⠄⠂⠁⠁⠂⠄⠄⠂✦ ⠂⠄⠄⠂⠁⠁⠂⠄⠂⠄⠄⠂☆ ⠂⠄⠄⠂⠁⠁⠂⠄⠄⠂✦
........................................................................................................................
•:•.•:•.•:•:•:•:•:•:•:••:•.•:•.•:•:•:•:•:•:•:•:•.•:•.•:•:•:•:•:•:•:••:•.•:•.•:•.•:•:•:•:•:•:•:•:•.•:•:•.•:•.••:•.•:•.••:
*/

const resultFormat = {
    // Anything that should come before your repetitions. You may leave this blank as "", but don't delete it.
    prefix: "<ul>",
    // What your repetitions should look like, replacing {{time}} with the formatted time and {{item}} with the item's name.
    repeat: "<li>{{time}} - {{item}}</li>\n",
    // Anything that should come before your repetitions. You may leave this blank as "", but don't delete it.
    suffix: "</ul>"
}

const timeFormat = {
    // possible values: "numeric" (3), "2-digit" (03), "long" (March), "short" (Mar), "narrow" (M), "none" ()
    month: "short",
    // possible values: "numeric" (3), "2-digit" (03), "none" ()
    day: "numeric",
    // possible values: "numeric" (3), "2-digit" (03), "none" ()
    hour: "2-digit",
    // possible values: "numeric" (3), "2-digit" (03), "none" ()
    minute: "numeric",
    // possible values: "numeric" (3), "2-digit" (03), "none" ()
    second: "none",
    // possible values: true (8PM), false (20)
    hour12: false
}

const getFormat = () => {
    const format = timeFormat;
    Object.entries(format).forEach(([key, value]) => {
        if(value === "none") delete format[key];
    })
    format.timeZone = "-07:00";
    format.dayPeriod = "narrow";
    return format;
}

const getFilteredPrizes = async (filterLower, filterHigher) => {
    const allPrizes = await GM.getValue("prizes", []);
    const start = filterLower ? (new Date(`${filterLower}-0700`)).getTime() : null;
    const end = filterHigher ? (new Date(`${filterHigher}-0700`)).getTime() : null;
    return allPrizes.filter(([time, _]) => (!start || time >= start) && (!end || time < end));
}

const formatValues = (allPrizes) => {
    const repeats = allPrizes.map(([time, item]) => {
        const formattedTime = (new Date(time)).toLocaleString([], getFormat());
        return resultFormat.repeat.replaceAll("{{time}}", formattedTime).replaceAll("{{item}}", item);
    });
    return `${resultFormat.prefix}${repeats.join("")}${resultFormat.suffix}`;
}

const exportResult = async (output, filterLower, filterHigher) => {
    const allPrizes = await getFilteredPrizes(filterLower, filterHigher);
    const formatted = formatValues(allPrizes)
    output.textContent = formatted;
    output.addEventListener("click", () => {
        GM.setClipboard(formatted);
        output.classList.add("copied");
        setTimeout(() => output.classList.remove("copied"), 500);
    });
}

const grabItems = async () => {
    const prizes = document.querySelectorAll(".prizname");
    if(prizes.length === 0) return;
    const allPrizes = await GM.getValue("prizes", []);
    const now = (new Date()).getTime();
    prizes.forEach(prize => {
        if(prize.textContent.match(/\d+ Neopoints/) || prize.textContent === "inventory") return;
        allPrizes.push([now, prize.textContent]);
    });
    GM.setValue("prizes", allPrizes);
}

const addLog = () => {
    document.getElementById("BDFR").insertAdjacentHTML("beforeBegin", `
    <style>.saah-bd-item-logger {
        width: 75%;
        left: 50%;
        transform: translateX(-50%);
        position: relative;
    }
    .saah-bd-item-logger summary {
        border-image-slice: 40% 40% 40% 40% fill;
        border-image-width: 20px 20px 20px 20px;
        border-image-outset: 0px 0px 0px 0px;
        border-image-repeat: stretch stretch;
        border-image-source: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='6.063mm' height='6.063mm' version='1.1' viewBox='0 0 6.063 6.063' xml:space='preserve'%3E%3Cpath d='m0.13218 0.13218v5.3908h5.7986l5.168e-4 -5.3908z' fill='%23ffc600' stroke='%23000' stroke-linecap='round' stroke-width='.26436'/%3E%3Cpath d='m1.1108 0.65112 1.8385 0.0042m1.263e-4 0.0084 2.1822 0.0042v0.87685' fill='none' stroke='%23fff' stroke-width='.13122'/%3E%3Cpath d='m2.9491 5.523h2.4978v0.5412h-2.4978m0 0h-1.7872v-0.5412h1.7872' fill-opacity='.50196' stroke-linecap='round' stroke-width='.14778'/%3E%3C/svg%3E");
        border-style: solid;
        display: inline;
        font-family: Cafeteria, Arial Black, sans-serif;
        font-size: 1.25em;
        padding: 0.15em 0.5em 0.25em;
        color: white;
        text-shadow: -3px 3px black, 1px 1px black, -1px 1px black, 1px -1px black, -1px -1px black;
        cursor: pointer;
    }
    .saah-bd-item-logger summary:is(:active, :hover, :focus) {
        border-image-source: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='6.063mm' height='6.063mm' version='1.1' viewBox='0 0 6.063 6.063' xml:space='preserve'%3E%3Cpath d='m0.13218 0.13218v5.3908h5.7986l5.168e-4 -5.3908z' fill='%23fff000' stroke='%23000' stroke-linecap='round' stroke-width='.26436'/%3E%3Cpath d='m1.1108 0.65112 1.8385 0.0042m1.263e-4 0.0084 2.1822 0.0042v0.87685' fill='none' stroke='%23fff' stroke-width='.13122'/%3E%3Cpath d='m2.9491 5.523h2.4978v0.5412h-2.4978m0 0h-1.7872v-0.5412h1.7872' fill-opacity='.50196' stroke-linecap='round' stroke-width='.14778'/%3E%3C/svg%3E");
    }

    .saah-bd-item-logger input {
        margin: 1em;
    }
    .saah-bd-item-log {
        position: relative;
        display: block;
    }
    .saah-bd-item-log.copied::after {
        content: "✔️ Copied";
        font-size: 3em;
        position: absolute;
        width: 100%;
        height: 100%;
        background: white;
        opacity: 0.75;
        inset-block-start: 0;
        inset-inline-start: 0;
    }</style>
    <details class="saah-bd-item-logger">
    <summary>Battledome Item Log</summary>
    <label>Start Time: <input type="datetime-local" id="bd-item-start"></label>
    <label>End Time: <input type="datetime-local" id="bd-item-end"></label>
    <p>Click your text to copy!</p>
    <pre><code class="saah-bd-item-log"></code></pre>
    </details>`);
    return document.getElementsByClassName("saah-bd-item-log")[0];
}

(function() {
    'use strict';
    if(window.location.href.match(/arena\.phtml/)) {
        const observer = new MutationObserver(grabItems);
        observer.observe(document.getElementById("bd_rewardsloot"), {childList: true, subtree: true});
    }
    else {
        const output = addLog();
        const start = document.getElementById("bd-item-start");
        const end = document.getElementById("bd-item-end");
        [start, end].forEach(time => time.addEventListener("change", () => exportResult(output, start.value, end.value)));
        exportResult(output, start.value, end.value);
    }
})();