[Pokeclicker] Multiple Held Items

Allows pokemon to hold multiple items

이 스크립트를 설치하려면 Tampermonkey, Greasemonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey와 같은 확장 프로그램을 설치해야 합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Userscripts와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 유저 스크립트 관리자 확장 프로그램이 필요합니다.

(이미 유저 스크립트 관리자가 설치되어 있습니다. 설치를 진행합니다!)

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

(이미 유저 스타일 관리자가 설치되어 있습니다. 설치를 진행합니다!)

// ==UserScript==
// @name          [Pokeclicker] Multiple Held Items
// @namespace     Pokeclicker Scripts
// @author        wizanyx
// @description   Allows pokemon to hold multiple items
// @copyright     https://github.com/wizanyx
// @license       GPL-3.0 License
// @version       0.0.1

// @homepageURL   https://github.com/wizanyx/Pokeclicker-Scripts/
// @supportURL    https://github.com/wizanyx/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 SETTINGS = {
    STORAGE_KEY: "pokeclicker_multipleHeldItems_backup",
    SETTINGS_KEY: "pokeclicker_multipleHeldItems_settings",
    UI_CONTAINER_ID: "customScriptsContainer",
};

/**
 * Controller for Multiple Held Items feature.
 * Manages game logic patches, data persistence, and state synchronization.
 */
const HeldItemsManager = {
    backupData: null,
    enabled: ko.observable(true),

    /**
     * Saves the current held items state to local storage.
     * Prevents data loss by backing up assignment.
     */
    save() {
        if (!App.game || !App.game.party) return;
        const backup = {};
        let hasData = false;
        App.game.party.caughtPokemon.forEach((p) => {
            if (p.heldItems && p.heldItems().length > 0) {
                backup[p.id] = p.heldItems().map((i) => i.name);
                hasData = true;
            }
        });
        if (hasData) {
            localStorage.setItem(SETTINGS.STORAGE_KEY, JSON.stringify(backup));
        }

        // Save settings
        localStorage.setItem(
            SETTINGS.SETTINGS_KEY,
            JSON.stringify({
                enabled: this.enabled(),
            }),
        );
    },

    /**
     * Loads backup data from local storage.
     */
    load() {
        // Load Settings
        try {
            const settings = localStorage.getItem(SETTINGS.SETTINGS_KEY);
            if (settings) {
                const data = JSON.parse(settings);
                this.enabled(data.enabled !== undefined ? data.enabled : true);
            }
        } catch (e) {
            console.error("[MultipleHeldItems] Failed to load settings", e);
        }

        if (Party.multipleHeldItemsPatched) return;
        try {
            const json = localStorage.getItem(SETTINGS.STORAGE_KEY);
            if (json) {
                this.backupData = JSON.parse(json);
                console.log("[MultipleHeldItems] Backup loaded.");
                Party.multipleHeldItemsPatched = true;
            }
        } catch (e) {
            console.error("[MultipleHeldItems] Failed to load backup", e);
        }
    },

    /**
     * Augments a specific Pokemon instance to support multiple held items.
     * Monkey-patches observables and computed properties for item bonuses.
     * @param {PartyPokemon} pokemon - The pokemon instance to patch.
     */
    augmentPokemon(pokemon) {
        if (pokemon._multipleHeldItemsPatched) return;
        pokemon._multipleHeldItemsPatched = true;

        if (!pokemon.heldItems) {
            pokemon.heldItems = ko.observableArray([]);
        }

        // Migration: Sync initial heldItem to heldItems list if not present
        if (
            pokemon.heldItem() &&
            !pokemon.heldItems().some((i) => i.name == pokemon.heldItem().name)
        ) {
            pokemon.heldItems.push(pokemon.heldItem());
        }

        // Patch heldItemAttackBonus to sum up modifiers
        if (pokemon.heldItemAttackBonus) {
            pokemon.heldItemAttackBonus.dispose();
            pokemon.heldItemAttackBonus = ko.pureComputed(() => {
                if (!HeldItemsManager.enabled()) {
                    // Fallback to original single item behavior
                    const item = pokemon.heldItem();
                    return item && item.attackBonus ? item.attackBonus : 1;
                }
                return pokemon.heldItems().reduce((acc, item) => {
                    const bonus =
                        item && item.attackBonus ? item.attackBonus : 1;
                    return acc * bonus;
                }, 1);
            });
        }

        // Patch clickAttackBonus
        if (pokemon.clickAttackBonus) {
            pokemon.clickAttackBonus.dispose();
            pokemon.clickAttackBonus = ko.pureComputed(() => {
                const bonus =
                    1 +
                    +pokemon.shiny +
                    +(pokemon.pokerus >= GameConstants.Pokerus.Resistant) +
                    +(pokemon.shadow == GameConstants.ShadowStatus.Purified);

                let itemBonus = 1;

                if (!HeldItemsManager.enabled()) {
                    // Fallback
                    const item = pokemon.heldItem();
                    itemBonus =
                        item && item.clickAttackBonus
                            ? item.clickAttackBonus
                            : 1;
                } else {
                    itemBonus = pokemon.heldItems().reduce((acc, item) => {
                        const bonus =
                            item && item.clickAttackBonus
                                ? item.clickAttackBonus
                                : 1;
                        return acc * bonus;
                    }, 1);
                }
                return bonus * itemBonus;
            });
        }

        // Patch _canUseHeldItem to check all items and remove invalid ones
        if (pokemon._canUseHeldItem) {
            pokemon._canUseHeldItem.dispose();
            ko.computed(() => {
                const items = [...pokemon.heldItems()];
                items.forEach((item) => {
                    if (item && item.canUse && !item.canUse(pokemon)) {
                        pokemon.addOrRemoveHeldItem(item);
                    }
                });
            });
            pokemon._canUseHeldItem = ko.pureComputed(() => {
                if (!HeldItemsManager.enabled()) {
                    const item = pokemon.heldItem();
                    return !item || !item.canUse || item.canUse(pokemon);
                }
                return pokemon
                    .heldItems()
                    .every(
                        (item) => item && item.canUse && item.canUse(pokemon),
                    );
            });
        }

        // Patch giveHeldItem
        pokemon.giveHeldItem = function (heldItem) {
            if (!heldItem) return;

            const alreadyHolding = pokemon
                .heldItems()
                .some((i) => i.name == heldItem.name);

            if (alreadyHolding) {
                if (Settings.getSetting("confirmChangeHeldItem").value) {
                    Notifier.confirm({
                        title: "Remove held item",
                        message:
                            "Held items are one time use only.\nRemoved items will be lost.\nAre you sure you want to remove it?",
                        confirm: "Remove",
                        type: NotificationConstants.NotificationOption.warning,
                    }).then((confirmed) => {
                        if (confirmed) {
                            pokemon.addOrRemoveHeldItem(heldItem);
                        }
                    });
                } else {
                    pokemon.addOrRemoveHeldItem(heldItem);
                }
                return;
            }

            if (heldItem.canUse && !heldItem.canUse(pokemon)) {
                Notifier.notify({
                    message: `This Pokémon cannot use ${heldItem.displayName}.`,
                    type: NotificationConstants.NotificationOption.warning,
                });
                return;
            }
            if (player.amountOfItem(heldItem.name) < 1) {
                Notifier.notify({
                    message: `You don't have any ${heldItem.displayName} left.`,
                    type: NotificationConstants.NotificationOption.warning,
                });
                return;
            }

            pokemon.addOrRemoveHeldItem(heldItem);
        };
    },

    /**
     * Applies prototype override patches to PartyPokemon, Party, and Controllers.
     * Hooks into JSON serialization and calculation methods.
     */
    applyPrototypePatches() {
        // Hook Prototype methods
        const oldFromJSON = PartyPokemon.prototype.fromJSON;
        PartyPokemon.prototype.fromJSON = function (json) {
            oldFromJSON.call(this, json);

            if (!this.heldItems) {
                this.heldItems = ko.observableArray([]);
            }

            if (json && json.heldItems) {
                const items = json.heldItems
                    .map((name) => ItemList[name])
                    .filter((i) => i instanceof HeldItem);
                this.heldItems(items);

                if (!this.heldItem() && items.length > 0) {
                    this.heldItem(items[items.length - 1]);
                }
            }
        };

        const oldToJSON = PartyPokemon.prototype.toJSON;
        PartyPokemon.prototype.toJSON = function () {
            const json = oldToJSON.call(this);
            json.heldItems = this.heldItems().map((i) => i.name);
            return json;
        };

        const oldPartyToJSON = Party.prototype.toJSON;
        Party.prototype.toJSON = function () {
            const json = oldPartyToJSON.call(this);
            json.multipleHeldItemsPatched = Party.multipleHeldItemsPatched;
            return json;
        };

        const oldPartyFromJSON = Party.prototype.fromJSON;
        Party.prototype.fromJSON = function (json) {
            Party.multipleHeldItemsPatched =
                json.multipleHeldItemsPatched || false;
            oldPartyFromJSON.call(this, json);
        };

        // Override getExpMultiplier logic
        PartyPokemon.prototype.getExpMultiplier = function () {
            let result = 1;
            if (HeldItemsManager.enabled() && this.heldItems) {
                this.heldItems().forEach((item) => {
                    if (item && item instanceof ExpGainedBonusHeldItem) {
                        result *= item.gainedBonus;
                    }
                });
            } else if (
                this.heldItem &&
                this.heldItem() &&
                this.heldItem() instanceof ExpGainedBonusHeldItem
            ) {
                result *= this.heldItem().gainedBonus;
            }
            return result;
        };

        // Override calculateEffortPoints logic
        if (Party.prototype.calculateEffortPoints) {
            Party.prototype.calculateEffortPoints = function (
                pokemon,
                shiny,
                shadow,
                number = GameConstants.BASE_EP_YIELD,
                ignore = false,
            ) {
                if (pokemon.pokerus < GameConstants.Pokerus.Contagious) {
                    return 0;
                }

                if (ignore) {
                    return 0;
                }

                let EPNum = number * App.game.multiplier.getBonus("ev");

                if (
                    HeldItemsManager.enabled() &&
                    pokemon.heldItems &&
                    pokemon.heldItems().length > 0
                ) {
                    pokemon.heldItems().forEach((item) => {
                        if (item && item instanceof EVsGainedBonusHeldItem) {
                            EPNum *= item.gainedBonus;
                        }
                    });
                } else if (
                    pokemon.heldItem() &&
                    pokemon.heldItem() instanceof EVsGainedBonusHeldItem
                ) {
                    EPNum *= pokemon.heldItem().gainedBonus;
                }

                if (shiny) {
                    EPNum *= GameConstants.SHINY_EP_MODIFIER;
                }

                if (shadow == GameConstants.ShadowStatus.Shadow) {
                    EPNum *= GameConstants.SHADOW_EP_MODIFIER;
                }

                return Math.floor(EPNum);
            };
        }

        // Patch addOrRemoveHeldItem
        PartyPokemon.prototype.addOrRemoveHeldItem = function (heldItem) {
            const existing = this.heldItems().find(
                (i) => i.name == heldItem.name,
            );
            if (existing) {
                this.heldItems.remove(existing);
                if (this.heldItem() && this.heldItem().name == heldItem.name) {
                    this.heldItem(
                        this.heldItems().length
                            ? this.heldItems()[this.heldItems().length - 1]
                            : undefined,
                    );
                }
            } else {
                player.loseItem(heldItem.name, 1);
                this.heldItems.push(heldItem);
                if (!this.heldItem()) {
                    this.heldItem(heldItem);
                }
            }
        };

        // Patch PartyController.getHeldItemFilteredList
        PartyController.getHeldItemFilteredList = function () {
            return App.game.party.caughtPokemon.filter((pokemon) => {
                if (pokemon.id <= 0) return false;

                const selectedItem = HeldItem.heldItemSelected();
                if (!selectedItem || !selectedItem.canUse(pokemon))
                    return false;

                const searchFilterSetting = Settings.getSetting(
                    "heldItemSearchFilter",
                );
                if (searchFilterSetting.observableValue() != "") {
                    const regex = searchFilterSetting.regex();
                    let match;
                    if (
                        Settings.getSetting(
                            "heldItemDropdownPokemonOrItem",
                        ).observableValue() === "pokemon"
                    ) {
                        match = PokemonHelper.matchPokemonByNames(
                            regex,
                            pokemon.name,
                            pokemon,
                        );
                    } else {
                        match = pokemon
                            .heldItems()
                            .some((h) => regex.test(h.displayName));
                    }
                    if (!match) return false;
                }

                if (
                    Settings.getSetting(
                        "heldItemRegionFilter",
                    ).observableValue() > -2
                ) {
                    if (
                        PokemonHelper.calcNativeRegion(pokemon.name) !==
                        Settings.getSetting(
                            "heldItemRegionFilter",
                        ).observableValue()
                    ) {
                        return false;
                    }
                }
                const type1 =
                    Settings.getSetting("heldItemTypeFilter").observableValue();
                const type2 = Settings.getSetting(
                    "heldItemType2Filter",
                ).observableValue();
                if (type1 !== -2 || type2 !== -2) {
                    const { type: types } = pokemonMap[pokemon.name];
                    if ([type1, type2].includes(PokemonType.None)) {
                        const type = type1 == PokemonType.None ? type2 : type1;
                        if (
                            !BreedingController.isPureType(
                                pokemon,
                                type === -2 ? null : type,
                            )
                        ) {
                            return false;
                        }
                    } else if (
                        (type1 !== -2 && !types.includes(type1)) ||
                        (type2 !== -2 && !types.includes(type2))
                    ) {
                        return false;
                    }
                }

                if (
                    Settings.getSetting(
                        "heldItemHideHoldingPokemon",
                    ).observableValue() &&
                    pokemon.heldItems().length > 0
                ) {
                    return false;
                }
                if (
                    Settings.getSetting(
                        "heldItemHideHoldingThisItem",
                    ).observableValue() &&
                    pokemon.heldItems().some((i) => i.name == selectedItem.name)
                ) {
                    return false;
                }

                return true;
            });
        };
    },
};

/**
 * Manages UI injections and customizations.
 */
const UserInterface = {
    /**
     * Injects the shared scripts container into the Left Column.
     */
    createContainer() {
        if (document.getElementById(SETTINGS.UI_CONTAINER_ID)) return;

        const leftColumn = document.getElementById("left-column");
        if (leftColumn) {
            const div = document.createElement("div");
            div.id = SETTINGS.UI_CONTAINER_ID;
            div.className = "card sortable border-secondary mb-3";
            div.innerHTML = `
                <div class="card-header p-0" data-toggle="collapse" href="#customScriptsBody">
                    <span>Scripts</span>
                </div>
                <div id="customScriptsBody" class="card-body p-0 show"></div>
            `;
            leftColumn.appendChild(div);
        }
    },

    injectScriptCard() {
        // Check if card already exists
        if (document.getElementById("multipleHeldItemsDisplay")) return;

        const displayDiv = document.createElement("div");
        displayDiv.id = "multipleHeldItemsDisplay";

        const html = `
            <div class="card-header p-0 border-top" data-toggle="collapse" href="#multipleHeldItemsInner">
                <span>Multiple Held Items</span>
            </div>
            <div id="multipleHeldItemsInner" class="collapse show">
                <div class="card-body p-0">
                     <button class="btn btn-block"
                        style="border-radius: 0;"
                        data-bind="
                            click: function() { HeldItemsManager.enabled(!HeldItemsManager.enabled()); },
                            class: HeldItemsManager.enabled() ? 'btn-success' : 'btn-danger',
                            text: 'Enabled [' + (HeldItemsManager.enabled() ? 'ON' : 'OFF') + ']'
                        ">
                    </button>
                </div>
            </div>
        `;

        displayDiv.innerHTML = html;
        const scriptBody = document.getElementById("customScriptsBody");
        scriptBody.appendChild(displayDiv);
        ko.applyBindings({ HeldItemsManager }, displayDiv);
    },

    injectStatsModal() {
        if (document.getElementById("multipleHeldItemsModalDisplay")) return;

        const target = document.querySelector(
            "#pokemonStatisticsModal .modal-body .col-12.col-lg-6",
        );
        if (!target) return;

        const html = `
        <div id="multipleHeldItemsModalDisplay" class="mt-2">
            <!-- ko if: App.game.party.getPokemon(App.game.statistics.selectedPokemonID()) -->
            <table class="table table-striped table-hover table-bordered table-sm m-0 mb-2">
                <thead>
                    <tr class="bg-secondary">
                        <th colspan="2">Held Items (Multiple)</th>
                    </tr>
                </thead>
                <tbody data-bind="foreach: App.game.party.getPokemon(App.game.statistics.selectedPokemonID()).heldItems">
                    <tr>
                        <td class="text-left align-middle">
                            <span data-bind="text: displayName"></span>
                            <!-- ko if: App.game.party.getPokemon(App.game.statistics.selectedPokemonID()).heldItem() && App.game.party.getPokemon(App.game.statistics.selectedPokemonID()).heldItem().name == $data.name -->
                                <span class="badge badge-primary float-right" style="font-size: 0.8em; margin-top: 3px;">Primary</span>
                            <!-- /ko -->
                            <!-- ko if: !App.game.party.getPokemon(App.game.statistics.selectedPokemonID()).heldItem() || App.game.party.getPokemon(App.game.statistics.selectedPokemonID()).heldItem().name != $data.name -->
                                 <button class="btn btn-sm btn-secondary float-right" style="padding: 0px 6px; font-size: 0.8em;"
                                     data-bind="click: function() { App.game.party.getPokemon(App.game.statistics.selectedPokemonID()).heldItem($data) }">
                                     Set Primary
                                 </button>
                            <!-- /ko -->
                        </td>
                        <td class="text-center align-middle tight">
                            <button class="btn btn-sm btn-danger" style="padding: 0px 6px;" 
                                data-bind="click: function() { App.game.party.getPokemon(App.game.statistics.selectedPokemonID()).addOrRemoveHeldItem($data) }">
                                &#x00d7;
                            </button>
                        </td>
                    </tr>
                </tbody>
            </table>
            <!-- /ko -->
        </div>`;

        const div = document.createElement("div");
        div.innerHTML = html;
        target.appendChild(div);
        ko.applyBindings({}, div);
    },

    injectBatchButton() {
        const modalId = "heldItemModal";
        const modal = document.getElementById(modalId);
        if (!modal) return;

        const inject = () => {
            const sticky = modal.querySelector(".sticky-top");
            if (!sticky) return;
            if (document.getElementById("heldItemBatchGiveContainer")) return;

            const container = document.createElement("div");
            container.id = "heldItemBatchGiveContainer";
            container.className = "px-1 mt-1 pb-2";

            const html = `
            <!-- ko if: Settings.getSetting('heldItemHideHoldingThisItem') && Settings.getSetting('heldItemHideHoldingThisItem').observableValue() -->
            <button class="btn btn-block btn-success" data-bind="click: function() {
                const item = HeldItem.heldItemSelected();
                if (!item) return;

                const visiblePokemon = ko.unwrap(PartyController.getHeldItemSortedList());
                
                if (visiblePokemon.length === 0) {
                    Notifier.notify({ message: 'No Pokémon visible to give items to!', type: NotificationConstants.NotificationOption.warning });
                    return;
                }

                if (player.amountOfItem(item.name) <= 0) {
                    Notifier.notify({ message: 'You have no ' + item.displayName + ' left!', type: NotificationConstants.NotificationOption.danger });
                    return;
                }

                Notifier.confirm({
                        title: 'Batch Give Item',
                        message: 'Give ' + item.displayName + ' to all ' + visiblePokemon.length + ' visible Pokémon?<br/>(Stops if you run out of items)',
                        confirm: 'Give All',
                        type: NotificationConstants.NotificationOption.warning,
                    }).then((confirmed) => {
                        if (confirmed) {
                            let count = 0;
                            const list = [...visiblePokemon];
                            for (const p of list) {
                                if (player.amountOfItem(item.name) <= 0) break;
                                if (!p.heldItems().some(i => i.name == item.name)) {
                                    p.addOrRemoveHeldItem(item);
                                    count++;
                                }
                            }
                            Notifier.notify({ message: 'Gave ' + item.displayName + ' to ' + count + ' Pokémon.', type: NotificationConstants.NotificationOption.success });
                        }
                    });
            }">
                Give <strong data-bind="text: HeldItem.heldItemSelected()?.displayName"></strong> to All Visible
            </button>
            <!-- /ko -->
            `;

            container.innerHTML = html;
            sticky.appendChild(container);
            ko.applyBindings({}, container);
        };

        inject();
        const observer = new MutationObserver(() => inject());
        observer.observe(modal, { childList: true, subtree: true });
    },

    injectHeldItemModalObserver() {
        const modalId = "heldItemModal";
        const modal = document.getElementById(modalId);
        if (!modal) return;

        const tbody = modal.querySelector("table tbody");
        if (!tbody) return;

        const observer = new MutationObserver((mutations) => {
            mutations.forEach((mutation) => {
                mutation.addedNodes.forEach((node) => {
                    if (node.nodeName === "TR") {
                        const td = node.querySelectorAll("td")[1];
                        if (td) {
                            ko.cleanNode(td);
                            td.innerHTML = `
                           <!-- ko foreach: $data.heldItems -->
                               <div style="display: inline-block; margin-right: 2px;"
                                    data-bind="tooltip: {
                                        title: $data.displayName,
                                        trigger: 'hover',
                                        placement: 'bottom'
                                    }">
                                   <img width="20" data-bind="attr: { src: $data.image }" />
                               </div>
                           <!-- /ko -->
                           `;
                            const context = ko.contextFor(node);
                            if (context) {
                                ko.applyBindings(context, td);
                            }
                        }
                    }
                });
            });
        });

        observer.observe(tbody, { childList: true });
    },
};

/**
 * Main Initialization Function
 */
function initializeMultipleHeldItems() {
    // Load persisted data
    HeldItemsManager.load();

    // Augment existing party members based on loaded backup or current state
    const backup = HeldItemsManager.backupData;

    if (App.game.party && App.game.party.caughtPokemon) {
        App.game.party.caughtPokemon.forEach((p) => {
            HeldItemsManager.augmentPokemon(p);

            // Restore from backup if needed
            if (backup && backup[p.id]) {
                const items = backup[p.id]
                    .map((name) => ItemList[name])
                    .filter((i) => i instanceof HeldItem);

                items.forEach((item) => {
                    if (!p.heldItems().some((i) => i.name == item.name)) {
                        p.heldItems.push(item);
                    }
                });
                // Sync visual state
                if (!p.HeldItem() && items.length > 0) {
                    p.heldItem(items[items.length - 1]);
                }
            }
        });

        // Watch for new Pokemon catches to augment them immediately
        App.game.party._caughtPokemon.subscribe(
            (changes) => {
                changes.forEach((change) => {
                    if (change.status === "added") {
                        HeldItemsManager.augmentPokemon(change.value);
                    }
                });
            },
            null,
            "arrayChange",
        );
    }

    // Initialize UI Components
    UserInterface.injectScriptCard();
    UserInterface.injectStatsModal();
    UserInterface.injectBatchButton();
    UserInterface.injectHeldItemModalObserver();

    // Setup Auto-Save
    setInterval(() => HeldItemsManager.save(), 60000);
    setTimeout(() => HeldItemsManager.save(), 5000);
}

// Hook to run prototype patches before main init
function runPatches() {
    HeldItemsManager.applyPrototypePatches();
}

// Loader
function loadScript(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.ScriptInitializers === undefined) {
        windowObject.ScriptInitializers = {};
        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.ScriptInitializers).forEach(
                    ([scriptName, initFunction]) => {
                        try {
                            initFunction();
                            console.log(`'${scriptName}' userscript loaded.`);
                        } catch (e) {
                            reportScriptError(scriptName, e);
                        }
                    },
                );
                hasInitialized = true;
            }
            return result;
        };
    }

    // Prevent issues with duplicate script names
    if (windowObject.ScriptInitializers[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.ScriptInitializers[`${scriptName} ${number}`] !==
            undefined
        ) {
            number++;
        }
        scriptName = `${scriptName} ${number}`;
    }
    // Add initializer for this particular script
    windowObject.ScriptInitializers[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.ScriptInitializers[scriptName] = () => null;
            }
        });
    }
}

if (!App.isUsingClient) {
    unsafeWindow.HeldItemsManager = HeldItemsManager;
} else {
    window.HeldItemsManager = HeldItemsManager;
}

loadScript("Multiple Held Items", initializeMultipleHeldItems, () => {
    runPatches()
    UserInterface.createContainer();
});