// ==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 // @downloadURL https://raw.githubusercontent.com/wizanyx/Pokeclicker-Scripts/master/multipleHeldItems.user.js // @updateURL https://raw.githubusercontent.com/wizanyx/Pokeclicker-Scripts/master/multipleHeldItems.user.js // @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 = `
Scripts
`; leftColumn.appendChild(div); } }, injectScriptCard() { // Check if card already exists if (document.getElementById("multipleHeldItemsDisplay")) return; const displayDiv = document.createElement("div"); displayDiv.id = "multipleHeldItemsDisplay"; const html = `
Multiple Held Items
`; 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 = `
Held Items (Multiple)
Primary
`; 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 = ` `; 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 = `
`; 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) { const details = error?.stack || error?.message || error?.toString?.() || String(error); console.error( `Error while initializing '${scriptName}' userscript:\n${error}`, ); console.error(details); 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.\n\n${details}`, 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(); });