Greasy Fork

来自缓存

Greasy Fork is available in English.

[Pokeclicker] Catch Filter Fantasia

An experimental catch filter that aims to help you have much better control and will completely change how you capture Pokémon.

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Userscripts ,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name          [Pokeclicker] Catch Filter Fantasia
// @namespace     Pokeclicker Scripts
// @author        Ephenia (Credit: Pastaficionado, umamaistempo)
// @description   An experimental catch filter that aims to help you have much better control and will completely change how you capture Pokémon.
// @copyright     https://github.com/Ephenia
// @license       GPL-3.0 License
// @version       1.9.3

// @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==

const ballNames = ['None', 'Pokeball', 'Greatball', 'Ultraball', 'Masterball', 'Fastball', 'Quickball', 'Timerball', 'Duskball', 'Luxuryball', 'Diveball', 'Lureball', 'Nestball', 'Repeatball', 'Beastball'];
var filterState;
var filterTypes;
var filterBallPref;
var catchFilter;
var filterColor;

function initCatchFilter() {
    const pokeballDisplay = document.getElementById('pokeballSelector');
    const profileModal = document.getElementById('profileModal');
    filterState ? filterColor = true : filterColor = false;

    // Setting custon CSS styles
    addGlobalStyle('#catch-filter-btn { position: absolute; left: 0px; top: 0px; width: auto; height: 41px; }');
    addGlobalStyle('#catch-filter-cont { display: flex; flex-direction: column; justify-content: center; }');
    addGlobalStyle('#filter-results { cursor: pointer; }');
    addGlobalStyle('#filter-results > div { display: flex; align-items: center; justify-content: center; min-height: 34px; }');
    addGlobalStyle('#filter-results > div:hover { background-color: gold; }');
    addGlobalStyle('.filter-pokeball-n { position: absolute; right: 25%; }');
    addGlobalStyle('.filter-pokeball-s { position: absolute; right: 20%; }');
    addGlobalStyle('.filter-shiny { position: absolute; right: 17%; }');
    addGlobalStyle('#filter-btn-cont { display: flex; justify-content: center; flex-wrap: wrap; gap: 10px; }');
    addGlobalStyle('#filter-btn-cont > button { display: flex; justify-content: center; min-width: 107px; }');
    addGlobalStyle('#filter-btn-tools { display: flex; flex-direction: row; justify-content: space-between; align-items: flex-end; column-gap: 10px; }');

    // Creating the button to append to the Pokeballs container
    let frag = new DocumentFragment();
    const btn = document.createElement('button');
    btn.innerText = 'Filter'
    btn.setAttribute('id', 'catch-filter-btn')
    btn.setAttribute('class', 'btn btn-sm btn-primary');
    btn.addEventListener('click', () => { $('#filterModal').modal('show') })
    frag.appendChild(btn);
    pokeballDisplay.appendChild(frag);

    // Creating a modal for the catch filter
    const filterMod = document.createElement('div');
    filterMod.setAttribute("class", "modal noselect fade show");
    filterMod.setAttribute("id", "filterModal");
    filterMod.setAttribute("tabindex", "-1");
    filterMod.setAttribute("aria-labelledby", "filterModal");
    filterMod.setAttribute("aria-labelledby", "filterModal");
    filterMod.setAttribute("aria-modal", "true");
    filterMod.setAttribute("role", "dialog");
    filterMod.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">Catch Filter</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>`
    profileModal.before(filterMod);

    const modalBody = document.querySelector('[id=filterModal] div div [class=modal-body]');
    modalBody.innerHTML = `<button id="catch-filter" class="btn btn-${filterColor ? 'success' : 'danger'}" style="margin-left:20px;">Catch Filter ${filterState ? '[ON]' : '[OFF]'}</button>
    <hr>
    <div id="filter-btn-cont"></div>
    <hr>
    <div id="filter-btn-tools">
    <button id="filter-load" class="btn btn-block btn-primary">Load Filtered</button>
    <button id="filter-route" class="btn btn-block btn-primary">Load Route</button>
    <button id="filter-dungeon" class="btn btn-block btn-primary">Load Dungeon</button>
    <button id="filter-all" class="btn btn-block btn-primary">Filter All</button>
    <button id="unfilter-all" class="btn btn-block btn-primary">Unfilter All</button>
    <button id="reset-ball-filter" class="btn btn-block btn-primary">Reset Balls</button>
    </div>
    <hr>
    <div id="catch-filter-cont">
    <input id="filter-search" type="text" placeholder="Search for a Pokémon...">
    <hr>
    <div id="filter-results"></div>
    </div>`
    // Re-assigning the previous document fragment
    frag = new DocumentFragment();
    filterTypes.forEach((Type, Index) => {
        const typeName = PokemonType[Index];
        const btn = document.createElement('button');
        btn.innerText = `${typeName} ${Type ? '[ON]' : '[OFF]'}`;
        btn.setAttribute('class', `btn btn-${Type ? 'success' : 'danger'}`);
        btn.setAttribute('data-src', Index);
        btn.addEventListener('click', (event) => { toggleTypeFilter(event); });
        frag.appendChild(btn);
    })
    document.getElementById('filter-btn-cont').appendChild(frag);
    document.getElementById('filter-load').addEventListener('click', () => { loadFilteredList(); });
    document.getElementById('filter-route').addEventListener('click', (event) => { filterPokemonRoute(event); });
    document.getElementById('filter-dungeon').addEventListener('click', (event) => { filterPokemonDungeon(event); });
    document.getElementById('filter-all').addEventListener('click', () => { filterAllPoke(); });
    document.getElementById('unfilter-all').addEventListener('click', () => { unfilterAllPoke(); });
    document.getElementById('reset-ball-filter').addEventListener('click', () => { resetBallFilterAll(); });
    document.getElementById('catch-filter').addEventListener('click', (event) => { toggleCatchFilter(event); });
    document.getElementById('filter-search').addEventListener('input', (event) => { filterPokeSearch(event); });

    overloadPokeballMethod();
    loadFilteredList();
}

function toggleTypeFilter(event) {
    const elem = event.target;
    const index = +elem.getAttribute('data-src');
    const typeName = PokemonType[index];
    filterTypes[index] ? filterTypes[index] = false : filterTypes[index] = true;
    elem.setAttribute('class', `btn btn-${filterTypes[index] ? 'success' : 'danger'}`);
    elem.innerText = `${typeName} ${filterTypes[index] ? '[ON]' : '[OFF]'}`;
    localStorage.setItem('filterTypes', JSON.stringify(filterTypes));
}

function loadFilteredList() {
    if (catchFilter.length != 0) {
        document.getElementById('filter-results').innerHTML = '';
        const frag = new DocumentFragment();
        for (const id of catchFilter) {
            const findPoke = pokemonList.find(p => p.id == id);
            const pokeIndex = pokemonList.indexOf(findPoke);
            const ballPrefN = filterBallPref[pokeIndex].normal;
            const ballPrefS = filterBallPref[pokeIndex].shiny;
            const div = document.createElement('div');
            div.innerHTML = `${findPoke.name}
            <img src="assets/images/pokeball/${ballNames[ballPrefN]}.svg" class="filter-pokeball-n pokeball-small pokeball-selected" ball-pref="${pokeIndex}" pref-type="normal">
            <img src="assets/images/pokeball/${ballNames[ballPrefS]}.svg" class="filter-pokeball-s pokeball-small pokeball-selected" ball-pref="${pokeIndex}" pref-type="shiny">
            <div class="filter-shiny">✨</div>`;
            div.setAttribute('data-src', findPoke.id);
            if (catchFilter.includes(findPoke.id)) {
                div.setAttribute('style', 'background-color: yellowgreen;');
            }
            div.addEventListener('click', (event) => { toggleFilteredPoke(event);loadFilteredList(); });
            frag.appendChild(div);
        }
        document.getElementById('filter-results').appendChild(frag);
        setRightClick();
    } else {
        document.getElementById('filter-results').innerHTML = '<b style="color: red">Your filtered list is empty.</b>';
    }
}

function filterPokemonRoute(event) {
    const elem = event.target;
    document.getElementById('filter-results').innerHTML = '';
    let routePoke;
    try {routePoke = Routes.getRoute(player.region, player.route).pokemon
         const frag = new DocumentFragment();
         //Using a side array to ensure adding pokemons only once in results (multiple occurences can occur with special)
         let routePokesList = [];
         for (const area in routePoke) {
            const routeArea = routePoke[area];
            if (routeArea.length > 0) {
                //Special Route Pokemons (Weather, Quests, ...)
                if (area ==="special") {
                    for (const speRoute of routeArea) {
                        const speRoutePokes = speRoute.pokemon;
                        for (const poke of speRoutePokes) {
                            const findPoke = pokemonList.find(p => p.name == poke);
                            if (!routePokesList.includes(findPoke.id)) {
                                routePokesList.push(findPoke.id);
                                const pokeIndex = pokemonList.indexOf(findPoke);
                                const ballPrefN = filterBallPref[pokeIndex].normal;
                                const ballPrefS = filterBallPref[pokeIndex].shiny;
                                const div = document.createElement('div');
                                div.innerHTML = `${findPoke.name}
                                <img src="assets/images/pokeball/${ballNames[ballPrefN]}.svg" class="filter-pokeball-n pokeball-small pokeball-selected" ball-pref="${pokeIndex}" pref-type="normal">
                                <img src="assets/images/pokeball/${ballNames[ballPrefS]}.svg" class="filter-pokeball-s pokeball-small pokeball-selected" ball-pref="${pokeIndex}" pref-type="shiny">
                                <div class="filter-shiny">✨</div>`;
                                div.setAttribute('data-src', findPoke.id);
                                if (catchFilter.includes(findPoke.id)) {
                                    div.setAttribute('style', 'background-color: yellowgreen;');
                                }
                                div.addEventListener('click', (event) => { toggleFilteredPoke(event); });
                                frag.appendChild(div);
                            }
                        }
                    }
                }
                else {
                    const thisArea = routePoke[area];
                    for (const poke of routeArea) {
                        const findPoke = pokemonList.find(p => p.name == poke);
                        if (!routePokesList.includes(findPoke.id)) {
                            routePokesList.push(findPoke.id);
                            const pokeIndex = pokemonList.indexOf(findPoke);
                            const ballPrefN = filterBallPref[pokeIndex].normal;
                            const ballPrefS = filterBallPref[pokeIndex].shiny;
                            const div = document.createElement('div');
                            div.innerHTML = `${findPoke.name}
                            <img src="assets/images/pokeball/${ballNames[ballPrefN]}.svg" class="filter-pokeball-n pokeball-small pokeball-selected" ball-pref="${pokeIndex}" pref-type="normal">
                            <img src="assets/images/pokeball/${ballNames[ballPrefS]}.svg" class="filter-pokeball-s pokeball-small pokeball-selected" ball-pref="${pokeIndex}" pref-type="shiny">
                            <div class="filter-shiny">✨</div>`;
                            div.setAttribute('data-src', findPoke.id);
                            if (catchFilter.includes(findPoke.id)) {
                                div.setAttribute('style', 'background-color: yellowgreen;');
                            }
                            div.addEventListener('click', (event) => { toggleFilteredPoke(event); });
                            frag.appendChild(div);
                        }
                    }
                }
            }
         }
         if (routePokesList.length > 0) {
            document.getElementById('filter-results').appendChild(frag);
            setRightClick();
        }
    } catch (err) {
        document.getElementById('filter-results').innerHTML = '<b style="color: red">You are not on a route.</b>';
    };
}

function filterPokemonDungeon(event) {
    const elem = event.target;
    document.getElementById('filter-results').innerHTML = '';
    let validDungeon;
    try {validDungeon = DungeonRunner.dungeon;
         const dungeonPoke = validDungeon.enemyList;
         const dungeonBoss = validDungeon.bossList;
         const frag = new DocumentFragment();

         for (const enemy of dungeonPoke) {
             let pokeStr;
             if (typeof enemy == 'string') {
                 pokeStr = enemy;
             } else if (typeof enemy.pokemon == 'string') {
                 pokeStr = enemy.pokemon;
             }
             if (typeof pokeStr != 'undefined') {
                 const findPoke = pokemonList.find(p => p.name == pokeStr);
                 const pokeIndex = pokemonList.indexOf(findPoke);
                 const ballPrefN = filterBallPref[pokeIndex].normal;
                 const ballPrefS = filterBallPref[pokeIndex].shiny;
                 const div = document.createElement('div');
                 div.innerHTML = `${findPoke.name}
                 <img src="assets/images/pokeball/${ballNames[ballPrefN]}.svg" class="filter-pokeball-n pokeball-small pokeball-selected" ball-pref="${pokeIndex}" pref-type="normal">
                 <img src="assets/images/pokeball/${ballNames[ballPrefS]}.svg" class="filter-pokeball-s pokeball-small pokeball-selected" ball-pref="${pokeIndex}" pref-type="shiny">
                 <div class="filter-shiny">✨</div>`;
                 div.setAttribute('data-src', findPoke.id);
                 if (catchFilter.includes(findPoke.id)) {
                     div.setAttribute('style', 'background-color: yellowgreen;');
                 }
                 div.addEventListener('click', (event) => { toggleFilteredPoke(event); });
                 frag.appendChild(div);
             }
         }

         for (const enemy of dungeonBoss) {
             let pokeStr;
             const construct = enemy.constructor.name;
             if (construct == 'DungeonBossPokemon') {
                 pokeStr = enemy.name;
             }
             if (typeof pokeStr != 'undefined') {
                 const findPoke = pokemonList.find(p => p.name == pokeStr);
                 const pokeIndex = pokemonList.indexOf(findPoke);
                 const ballPrefN = filterBallPref[pokeIndex].normal;
                 const ballPrefS = filterBallPref[pokeIndex].shiny;
                 const div = document.createElement('div');
                 div.innerHTML = `${findPoke.name}
                 <img src="assets/images/pokeball/${ballNames[ballPrefN]}.svg" class="filter-pokeball-n pokeball-small pokeball-selected" ball-pref="${pokeIndex}" pref-type="normal">
                 <img src="assets/images/pokeball/${ballNames[ballPrefS]}.svg" class="filter-pokeball-s pokeball-small pokeball-selected" ball-pref="${pokeIndex}" pref-type="shiny">
                 <div class="filter-shiny">✨</div>`;
                 div.setAttribute('data-src', findPoke.id);
                 if (catchFilter.includes(findPoke.id)) {
                     div.setAttribute('style', 'background-color: yellowgreen;');
                 }
                 div.addEventListener('click', (event) => { toggleFilteredPoke(event); });
                 frag.appendChild(div);
             }
         }
         document.getElementById('filter-results').appendChild(frag);
         setRightClick();

        } catch (err) {
            document.getElementById('filter-results').innerHTML = '<b style="color: red">You are not in a dungeon or dungeon info cannot be found.</b>';
        };
}

function filterAllPoke() {
    catchFilter = [];
    for (const poke of pokemonList) {
        catchFilter.push(poke.id);
    }
    localStorage.setItem('catchFilter', JSON.stringify(catchFilter));
    filterPokeSearch(true);
}

function unfilterAllPoke() {
    catchFilter = [];
    localStorage.setItem('catchFilter', JSON.stringify(catchFilter));
    filterPokeSearch(true);
}

function resetBallFilterAll() {
    filterBallPref = new Array(pokemonList.length).fill({normal: 0, shiny: 0}, 0, pokemonList.length);
    localStorage.setItem('filterBallPref', JSON.stringify(filterBallPref));
    filterPokeSearch(true);
}

function toggleCatchFilter(event) {
    const elem = event.target;
    filterState ? filterColor = false : filterColor = true;
    filterState = filterColor;
    elem.setAttribute('class', `btn btn-${filterColor ? 'success' : 'danger'}`);
    elem.innerText = `Catch Filter ${filterState ? "[ON]" : "[OFF]"}`;
    localStorage.setItem('filterState', filterState);
}

function filterPokeSearch(event) {
    document.getElementById('filter-results').innerHTML = '';
    let pokeStr;
    try { pokeStr = event.target.value.toLowerCase(); } catch (err) { pokeStr = document.getElementById('filter-search').value; };
    const filterList = pokemonList.filter(p => p.name.toLowerCase().includes(pokeStr));
    const frag = new DocumentFragment();
    for (const poke of filterList) {
        const findPoke = pokemonList.find(p => p.name == poke.name);
        const pokeIndex = pokemonList.indexOf(findPoke);
        const ballPrefN = filterBallPref[pokeIndex].normal;
        const ballPrefS = filterBallPref[pokeIndex].shiny;
        const div = document.createElement('div');
        div.innerHTML = `${findPoke.name}
        <img src="assets/images/pokeball/${ballNames[ballPrefN]}.svg" class="filter-pokeball-n pokeball-small pokeball-selected" ball-pref="${pokeIndex}" pref-type="normal">
        <img src="assets/images/pokeball/${ballNames[ballPrefS]}.svg" class="filter-pokeball-s pokeball-small pokeball-selected" ball-pref="${pokeIndex}" pref-type="shiny">
        <div class="filter-shiny">✨</div>`;
        div.setAttribute('data-src', poke.id);
        if (catchFilter.includes(poke.id)) {
            div.setAttribute('style', 'background-color: yellowgreen;');
        }
        div.addEventListener('click', (event) => { toggleFilteredPoke(event); });
        frag.appendChild(div);
    }
    document.getElementById('filter-results').appendChild(frag);
    setRightClick();
}

function toggleFilteredPoke(event) {
    const elem = event.target;
    const id = +elem.getAttribute('data-src');
    if (elem.hasAttribute('ball-pref')) {
        const ballPref = +elem.getAttribute('ball-pref');
        const prefType = elem.getAttribute('pref-type');
        filterBallPref[ballPref][prefType] < ballNames.length - 1 ? filterBallPref[ballPref][prefType]++ : filterBallPref[ballPref][prefType] = 0;
        elem.src = `assets/images/pokeball/${ballNames[filterBallPref[ballPref][prefType]]}.svg`
        localStorage.setItem('filterBallPref', JSON.stringify(filterBallPref));
        return;
    }
    const idExists = catchFilter.indexOf(id);
    if (idExists == -1) {
        catchFilter.push(id);
        elem.setAttribute('style', 'background-color: yellowgreen;');
    } else {
        catchFilter.splice(idExists, 1);
        elem.removeAttribute('style');
    }
    localStorage.setItem('catchFilter', JSON.stringify(catchFilter));
}

function setRightClick() {
    const ballElemsN = document.getElementsByClassName('filter-pokeball-n');
    const ballElemsS = document.getElementsByClassName('filter-pokeball-s');
    for (let i = 0; i < ballElemsN.length; i++) {
        ballElemsN[i].addEventListener('contextmenu', (event) => { event.preventDefault();resetBallFilter(event); });
    }
    for (let ii = 0; ii < ballElemsS.length; ii++) {
        ballElemsS[ii].addEventListener('contextmenu', (event) => { event.preventDefault();resetBallFilter(event); });
    }
}

function resetBallFilter(event) {
    const elem = event.target;
    const ballPref = +elem.getAttribute('ball-pref');
    const prefType = elem.getAttribute('pref-type');
    filterBallPref[ballPref][prefType] = 0;
    elem.src = `assets/images/pokeball/${ballNames[filterBallPref[ballPref][prefType]]}.svg`
    localStorage.setItem('filterBallPref', JSON.stringify(filterBallPref));
}

function overloadPokeballMethod() {
    const hasBall = function(ballId) {
        return App.game.pokeballs.pokeballs[ballId].quantity() > 0;
    }

    const newMethod = function(id, isShiny, isShadow, encounterType) {
        const pokemon = PokemonHelper.getPokemonById(id);
        const type1 = pokemon.type1;
        const type2 = pokemon.type2;

        // FIXME: we could just use a map with the changes we want
        const pokeIndex = pokemonList.findIndex(p => p.id == pokemon.id);

        // FIXME: the values of pokeballs for the choices on the modal are
        //   offset by -1
        let ballPrefN = filterBallPref[pokeIndex].normal - 1;
        let ballPrefS = filterBallPref[pokeIndex].shiny - 1;
        const overrideBallN = ballPrefN !== GameConstants.Pokeball.None;
        const overrideBallS = ballPrefS !== GameConstants.Pokeball.None;

        const isAllowed = catchFilter.includes(id) || filterTypes[type1] || filterTypes[type2]

        if (filterState && isAllowed && isShiny && overrideBallS && hasBall(ballPrefS)) {
            return ballPrefS;
        } else if (filterState && isAllowed && !isShiny && overrideBallN && hasBall(ballPrefN)) {
            return ballPrefN;
        } else if (filterState && !isAllowed) {
            return GameConstants.Pokeball.None;
        } else {
            return App.game.pokeballs.oldCalculatePokeballToUse(id, isShiny, isShadow, encounterType);
        }
    }

    // HACK: doing this to keep the function inside the pokeballs object
    //   otherwise it will not have correct access to `this`. Doing this this
    //   way to just overload the function instead of rewriting it.
    App.game.pokeballs.oldCalculatePokeballToUse = App.game.pokeballs.calculatePokeballToUse;
    App.game.pokeballs.calculatePokeballToUse = newMethod;
}

if (!localStorage.getItem('filterState')) {
    localStorage.setItem('filterState', false);
}
if (!localStorage.getItem('filterTypes')) {
    const typeArray = new Array(18).fill(false, 0, 18);
    localStorage.setItem('filterTypes', JSON.stringify(typeArray));
}
if (!localStorage.getItem('filterBallPref')) {
    const prefArray = new Array(pokemonList.length).fill({normal: 0, shiny: 0}, 0, pokemonList.length);
    localStorage.setItem('filterBallPref', JSON.stringify(prefArray));
}
if (!localStorage.getItem('catchFilter')) {
    localStorage.setItem('catchFilter', JSON.stringify([]));
}
filterState = JSON.parse(localStorage.getItem('filterState'));
filterTypes = JSON.parse(localStorage.getItem('filterTypes'));
catchFilter = JSON.parse(localStorage.getItem('catchFilter'));
filterBallPref = JSON.parse(localStorage.getItem('filterBallPref'));

if (filterBallPref.length != pokemonList.length) {
    const diff = pokemonList.length - filterBallPref.length;
    const newPoke = new Array(diff).fill({normal: 0, shiny: 0}, 0, diff);
    filterBallPref = filterBallPref.concat(newPoke);
    localStorage.setItem('filterBallPref', JSON.stringify(filterBallPref));
}

const fixIt = filterBallPref.filter(e => typeof e == 'number');
if (fixIt.length != 0) {
    for (const index in filterBallPref) {
        if (typeof filterBallPref[index] == 'number') {
            filterBallPref[index] = {normal: 0, shiny: 0};
        }
    }
    localStorage.setItem('filterBallPref', JSON.stringify(filterBallPref));
}

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, priorityFunction) {
    function reportScriptError(scriptName, error) {
        console.error(`Error while initializing '${scriptName}' userscript:\n${error}`);
        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,
        });
    }
    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) {
                        reportScriptError(scriptName, e);
                    }
                });
                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;
    // Run any functions that need to execute before the game starts
    if (priorityFunction) {
        $(document).ready(() => {
            try {
                priorityFunction();
            } catch (e) {
                reportScriptError(scriptName, e);
                // Remove main initialization function  
                windowObject.epheniaScriptInitializers[scriptName] = () => null;
            }
        });
    }
}

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