[Pokeclicker] Infinite Seasonal Events

Adds in toggable options to have seasonal events infinitely run. Events can also run simultaneously with one another. Includes a custom event as well.

// ==UserScript==
// @name          [Pokeclicker] Infinite Seasonal Events
// @namespace     Pokeclicker Scripts
// @author        Ephenia
// @description   Adds in toggable options to have seasonal events infinitely run. Events can also run simultaneously with one another. Includes a custom event as well.
// @copyright     https://github.com/Ephenia
// @license       GPL-3.0 License
// @version       1.3.2

// @homepageURL   https://github.com/Ephenia/Pokeclicker-Scripts/
// @supportURL    https://github.com/Ephenia/Pokeclicker-Scripts/issues

// @match         https://www.pokeclicker.com/
// @icon          https://www.google.com/s2/favicons?domain=pokeclicker.com
// @grant         unsafeWindow
// @run-at        document-idle
// ==/UserScript==

var activeSeasonalEvents = {};

//Removed setTimeout, opted to make it load like the other scrips, also helps with notifications
function initEvents() {
    const NUM_EVENTS = 10;
    const startDate = new Date(new Date().getFullYear(), -1);
    const endDate = new Date(new Date().getFullYear(), 10000);

    App.game.specialEvents.newEvent('Ephenia\'s Gift',
        'Encounter Ribombee that roams across all regions.<br/>A special thanks for using my scripts!',
        startDate,
        () => {
            GameHelper.enumNumbers(GameConstants.Region).filter(i => i != GameConstants.Region.none).forEach(region => {
                RoamingPokemonList.add(region, 0, new RoamingPokemon('Ribombee')); // Currently adding in base subregions only
            });
        },
        endDate,
        () => {
            GameHelper.enumNumbers(GameConstants.Region).filter(i => i != GameConstants.Region.none).forEach(region => {
                RoamingPokemonList.remove(region, 0, 'Ribombee');
            });
        },
        true
    );

    //Testing loading events in init
    for (const event of App.game.specialEvents.events) {
        let eventActive = loadSetting(event.title, false);
        activeSeasonalEvents[event.title] = eventActive;
    }

    for (const event of App.game.specialEvents.events) {
        event.startTime = startDate;
        event.endTime = endDate;

        if (!event.hasStarted() && activeSeasonalEvents[event.title]) {
            event.start();
        }
    }

    if (App.game.specialEvents.events.length != NUM_EVENTS) {
        Notifier.notify({
            title: '[Outdated] Infinite Seasonal Events',
            message: `Please contact <a href="https://github.com/Ephenia/Pokeclicker-Scripts" target="_blank">Ephenia</a> so that this script can be updated!`,
            type: NotificationConstants.NotificationOption.danger,
            timeout: 1000000
        });
    }

    var eventLi = document.createElement('li');
    eventLi.innerHTML = `<a class="dropdown-item" href="#eventModal" data-toggle="modal">Toggle Events</a>`;
    for (const node of document.querySelectorAll('#startMenu ul li')) {
        if (node.querySelector('a[href="#eventsModal"]')) {
            node.after(eventLi);
            break;
        }
    }

    var eventMod = document.createElement('div');
    eventMod.setAttribute("class", "modal noselect fade show");
    eventMod.setAttribute("id", "eventModal");
    eventMod.setAttribute("tabindex", "-1");
    eventMod.setAttribute("aria-labelledby", "eventModal");
    eventMod.setAttribute("aria-labelledby", "eventModal");
    eventMod.setAttribute("aria-modal", "true");
    eventMod.setAttribute("role", "dialog");

    eventMod.innerHTML = `<div class="modal-dialog modal-dialog-scrollable modal-dialog-centered modal-lg" role="document">
    <div class="modal-content">
        <div class="modal-header" style="justify-content: space-around;">
            <h5 class="modal-title">Toggle Events</h5>
            <button type="button" class="close" data-dismiss="modal" aria-label="Close">
                <span aria-hidden="true">×</span>
            </button>
        </div>
        <div class="modal-body">
        </div>
    </div>`

    document.getElementById('profileModal').before(eventMod);

    var modalBody = document.querySelector('[id=eventModal] div div [class=modal-body]')
    //Event order seems to change on startup so this loads them independent of order, also loads any new events without images
    for (const event of App.game.specialEvents.events) {
        let title = event.title;
        let description = event.description;
        switch(title){
            case "Flying Pikachu":
                modalBody.innerHTML +=
                    `<div id="event-${title}" class="event-select" data-value="${title}"><b>${title}</b><br>${description}<br>
                <img src="assets/images/pokemon/25.1.png">
                <img src="assets/images/pokemon/21.01.png">
                </div><hr>`
                break
            case "Mewtwo strikes back!":
                modalBody.innerHTML +=
                    `<div id="event-${title}" class="event-select" data-value="${title}"><b>${title}</b><br>${description}<br>
                <img src="assets/images/pokemon/150.03.png">
                <img src="assets/images/pokemon/1.01.png">
                <img src="assets/images/pokemon/4.01.png">
                <img src="assets/images/pokemon/7.01.png">
                </div><hr>`
                break
            case "Halloween!":
                modalBody.innerHTML +=
                    `<div id="event-${title}" class="event-select" data-value="${title}"><b>${title}</b><br>${description}<br>
                <img src="assets/images/pokemon/1.02.png">
                <img src="assets/images/pokemon/25.12.png">
                <img src="assets/images/pokemon/175.01.png"><br>
                <img src="assets/images/pokemon/92.png">
                <img src="assets/images/pokemon/200.png">
                <img src="assets/images/pokemon/353.png">
                <img src="assets/images/pokemon/355.png">
                </div><hr>`
                break
            case "Let's GO!":
                modalBody.innerHTML +=
                    `<div id="event-${title}" class="event-select" data-value="${title}"><b>${title}</b><br>${description}<br>
                <img src="assets/images/pokemon/25.13.png">
                <img src="assets/images/pokemon/133.02.png">
                </div><hr>`
                break
            case "Merry Christmas!":
                modalBody.innerHTML +=
                    `<div id="event-${title}" class="event-select" data-value="${title}"><b>${title}</b><br>${description}<br>
                <img src="assets/images/pokemon/143.02.png">
                <img src="assets/images/pokemon/251.01.png">
                <img src="assets/images/pokemon/446.01.png">
                </div><hr>`
                break
            case "Hoopa Day":
                modalBody.innerHTML +=
                    `<div id="event-${title}" class="event-select" data-value="${title}"><b>${title}</b><br>${description}<br>
                <i>(Note that this event only adds a special joke questline and doesn't add Hoopa as an additional roamer. Hoopa is available without this event.)</i><br>
                <img src="assets/images/pokemon/720.png">
                </div><hr>`
                break
            case "Lunar New Year":
                modalBody.innerHTML +=
                    `<div id="event-${title}" class="event-select" data-value="${title}"><b>${title}</b><br>${description}<br>
                <img src="assets/images/pokemon/666.png">
                <img src="assets/images/pokemon/666.19.png"><br>
                <img src="assets/images/pokemon/666.01.png">
                <img src="assets/images/pokemon/666.02.png">
                <img src="assets/images/pokemon/666.03.png">
                <img src="assets/images/pokemon/666.04.png">
                <img src="assets/images/pokemon/666.05.png">
                <img src="assets/images/pokemon/666.06.png">
                <img src="assets/images/pokemon/666.07.png">
                <img src="assets/images/pokemon/666.08.png">
                <img src="assets/images/pokemon/666.09.png">
                <img src="assets/images/pokemon/666.1.png">
                <img src="assets/images/pokemon/666.11.png">
                <img src="assets/images/pokemon/666.12.png">
                <img src="assets/images/pokemon/666.13.png">
                <img src="assets/images/pokemon/666.14.png">
                <img src="assets/images/pokemon/666.15.png">
                <img src="assets/images/pokemon/666.16.png">
                <img src="assets/images/pokemon/666.17.png">
                <img src="assets/images/pokemon/666.18.png">
                </div><hr>`
                break
            case "Easter":
                modalBody.innerHTML +=
                    `<div id="event-${title}" class="event-select" data-value="${title}"><b>${title}</b><br>${description}<br>
                <img src="assets/images/pokemon/175.02.png">
                </div><hr>`
                break
            case "Golden Week":
                modalBody.innerHTML +=
                    `<div id="event-${title}" class="event-select" data-value="${title}"><b>${title}</b><br>${description}<br>
                <img src="assets/images/pokemon/1.03.png">
                </div><hr>`
                break
            case "Ephenia's Gift":
                modalBody.innerHTML +=
                    `<div id="event-${title}" class="event-select" data-value="${title}"><b>${title}</b><br>${description}<br>
                <img src="assets/images/pokemon/743.png">
                </div><hr>`
                break
            default:
                modalBody.innerHTML +=
                    `<div id="event-${title}" class="event-select" data-value="${title}"><b>${title}</b><br>${description}<br><br>New event, Pokemon images coming soon, if not open an issue on github
                </div><hr>`
        }
    }

    for (const event of App.game.specialEvents.events) {
        if (activeSeasonalEvents[event.title]) {
            document.getElementById('event-'+event.title).style = "background-color: rgba(93, 226, 60, 0.5)";
        }
        document.getElementById('event-'+event.title).addEventListener('click', toggleEvent);
    }

    addGlobalStyle('.event-select { cursor: pointer; }');
    addGlobalStyle('.event-select:hover { background-color: rgba(48, 197, 255, 0.5); }');
}

function toggleEvent() {
    var title = this.getAttribute('data-value');
    activeSeasonalEvents[title] = !activeSeasonalEvents[title];
    localStorage.setItem(title, activeSeasonalEvents[title]);
    if (activeSeasonalEvents[title]) {
        this.style = "background-color: rgba(93, 226, 60, 0.5)";
        App.game.specialEvents.events.find((event) => (event.title === title)).start();
    } else {
        this.style = "";
        App.game.specialEvents.events.find((event) => (event.title === title)).end();
    }
}

function loadSetting(key, defaultVal) {
    var val;
    try {
        val = JSON.parse(localStorage.getItem(key));
        if (val == null || typeof val !== typeof defaultVal) {
            throw new Error;
        }
    } catch {
        val = defaultVal;
        localStorage.setItem(key, defaultVal);
    }
    return val;
}

function addGlobalStyle(css) {
    var head, style;
    head = document.getElementsByTagName('head')[0];
    if (!head) { return; }
    style = document.createElement('style');
    style.type = 'text/css';
    style.innerHTML = css;
    head.appendChild(style);
}

function loadEpheniaScript(scriptName, initFunction) {
    const windowObject = !App.isUsingClient ? unsafeWindow : window;
    // Inject handlers if they don't exist yet
    if (windowObject.epheniaScriptInitializers === undefined) {
        windowObject.epheniaScriptInitializers = {};
        const oldInit = Preload.hideSplashScreen;
        var hasInitialized = false;

        // Initializes scripts once enough of the game has loaded
        Preload.hideSplashScreen = function (...args) {
            var result = oldInit.apply(this, args);
            if (App.game && !hasInitialized) {
                // Initialize all attached userscripts
                Object.entries(windowObject.epheniaScriptInitializers).forEach(([scriptName, initFunction]) => {
                    try {
                        initFunction();
                    } catch (e) {
                        console.error(`Error while initializing '${scriptName}' userscript:\n${e}`);
                        Notifier.notify({
                            type: NotificationConstants.NotificationOption.warning,
                            title: scriptName,
                            message: `The '${scriptName}' userscript crashed while loading. Check for updates or disable the script, then restart the game.\n\nReport script issues to the script developer, not to the Pokéclicker team.`,
                            timeout: GameConstants.DAY,
                        });
                    }
                });
                hasInitialized = true;
            }
            return result;
        }
    }

    // Prevent issues with duplicate script names
    if (windowObject.epheniaScriptInitializers[scriptName] !== undefined) {
        console.warn(`Duplicate '${scriptName}' userscripts found!`);
        Notifier.notify({
            type: NotificationConstants.NotificationOption.warning,
            title: scriptName,
            message: `Duplicate '${scriptName}' userscripts detected. This could cause unpredictable behavior and is not recommended.`,
            timeout: GameConstants.DAY,
        });
        let number = 2;
        while (windowObject.epheniaScriptInitializers[`${scriptName} ${number}`] !== undefined) {
            number++;
        }
        scriptName = `${scriptName} ${number}`;
    }
    // Add initializer for this particular script
    windowObject.epheniaScriptInitializers[scriptName] = initFunction;
}

if (!App.isUsingClient || localStorage.getItem('infiniteseasonalevents') === 'true') {
    loadEpheniaScript('infiniteseasonalevents', initEvents);
}