// ==UserScript== // @name [Pokeclicker] Hatchery Helper Tweaks // @namespace Pokeclicker Scripts // @author wizanyx // @description Tweaks hatchery helpers: cost scaling, max helpers, and bonus controls. // @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/hatcheryHelperTweaks.user.js // @updateURL https://raw.githubusercontent.com/wizanyx/Pokeclicker-Scripts/master/hatcheryHelperTweaks.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_hatcheryHelperTweaks_settings", UI_CONTAINER_ID: "customScriptsContainer", }; /** * Manager for Hatchery Helper tweaks. */ const HatcheryHelperTweaks = { costScalingEnabled: ko.observable(true), maxHelpersEnabled: ko.observable(false), bonusMode: ko.observable("bounded"), bonusMax: ko.observable(50), costScaleMode: ko.observable("fixed50"), initialized: false, _mapGeneratedBonus: -1, _mapGeneratedHatched: 0, load() { try { const json = localStorage.getItem(SETTINGS.STORAGE_KEY); if (json) { const data = JSON.parse(json); if (data.costScalingEnabled !== undefined) { this.costScalingEnabled(data.costScalingEnabled); } else if (data.enabled !== undefined) { this.costScalingEnabled(data.enabled); } if (data.maxHelpersEnabled !== undefined) { this.maxHelpersEnabled(data.maxHelpersEnabled); } if (data.bonusMode !== undefined) { this.bonusMode(data.bonusMode); } if (data.bonusMax !== undefined) { this.bonusMax(data.bonusMax); } if (data.costScaleMode !== undefined) { this.costScaleMode(data.costScaleMode); } } } catch (e) { console.error("[HatcheryHelperTweaks] Failed to load settings:", e); } }, save() { if (!this.initialized) return; localStorage.setItem( SETTINGS.STORAGE_KEY, JSON.stringify({ costScalingEnabled: this.costScalingEnabled(), maxHelpersEnabled: this.maxHelpersEnabled(), bonusMode: this.bonusMode(), bonusMax: this.bonusMax(), costScaleMode: this.costScaleMode(), }), ); }, isUnboundBonus() { return this.bonusMode() === "unbound"; }, getBonusCap() { const cap = Math.floor(Number(this.bonusMax())); if (!Number.isFinite(cap)) return 50; return Math.max(0, cap); }, getRawHatchBonus(hatched) { return Math.floor(Math.sqrt(hatched / 50) * 10) / 10; }, calcHatchBonus(hatched) { const raw = this.getRawHatchBonus(hatched); if (this.isUnboundBonus()) return raw; return Math.min(this.getBonusCap(), raw); }, getCostScaleCap() { if (this.costScaleMode() === "bonusMax") { return Math.max(1, this.getBonusCap()); } return 50; }, getMaxHelpers() { return this.maxHelpersEnabled() ? 4 : 3; }, /** * Returns the scaled cost based on hatch bonus. * @param {number} baseAmount * @param {number} hatchBonus */ getScaledCost(baseAmount, hatchBonus) { const cap = this.getCostScaleCap(); const multiplier = Math.max(0, 1 - hatchBonus / cap); return Math.max(0, Math.round(baseAmount * multiplier)); }, applyMaxHelpers() { const helpers = App.game.breeding.hatcheryHelpers; if (!helpers) return; helpers.MAX_HIRES = this.getMaxHelpers(); if (!helpers._maxHelpersPatched) { helpers._maxHelpersPatched = true; helpers.canHire = ko.pureComputed(() => { return ( helpers.hired().length < Math.min(this.getMaxHelpers(), helpers.hatchery.eggSlots) ); }); } }, applyBonusSettings() { this.resetBonusMap(); this.refreshHelpers(); }, resetBonusMap() { if (!HatcheryHelperMinBonusMap) return; Object.keys(HatcheryHelperMinBonusMap).forEach((key) => { delete HatcheryHelperMinBonusMap[key]; }); this._mapGeneratedBonus = -1; this._mapGeneratedHatched = 0; if (!this.isUnboundBonus()) { this.extendBonusMapTo(this.getBonusCap()); } }, extendBonusMapTo(targetBonus) { if (!HatcheryHelperMinBonusMap) return; let bonus = this._mapGeneratedBonus; let hatched = this._mapGeneratedHatched; const target = Math.max(targetBonus, bonus); while (bonus < target) { const current = this.calcHatchBonus(hatched); if (current > bonus) { HatcheryHelperMinBonusMap[current] = hatched; bonus = current; } hatched++; if (hatched > 5_000_000) break; } this._mapGeneratedBonus = bonus; this._mapGeneratedHatched = hatched; }, ensureBonusMapFor(bonusTarget) { if (this.isUnboundBonus()) { this.extendBonusMapTo(bonusTarget); } }, refreshHelpers() { if (!HatcheryHelpers?.list?.length) return; HatcheryHelpers.list.forEach((helper) => helper.updateBonus()); }, /** * Applies cost scaling to a single helper. * @param {HatcheryHelper} helper */ applyToHelper(helper) { if (!helper || !helper.cost) return; if (helper._baseCostAmount === undefined) { helper._baseCostAmount = helper.cost.amount; } if (this.costScalingEnabled()) { helper.cost.amount = this.getScaledCost( helper._baseCostAmount, helper.hatchBonus(), ); } else { helper.cost.amount = helper._baseCostAmount; } }, /** * Applies scaling to all helpers. */ applyToAll() { if (!HatcheryHelpers?.list?.length) return; HatcheryHelpers.list.forEach((helper) => this.applyToHelper(helper)); }, }; /** * Patch HatcheryHelper bonus updates to keep cost in sync. */ const GameMechanics = { applyPatches() { if (!HatcheryHelper?.prototype?.updateBonus) return; const originalUpdateBonus = HatcheryHelper.prototype.updateBonus; HatcheryHelper.prototype.updateBonus = function () { const hatchBonus = HatcheryHelperTweaks.calcHatchBonus( this.hatched(), ); this.hatchBonus(hatchBonus); this.stepEfficiency(this.stepEfficiencyBase + hatchBonus); this.attackEfficiency(this.attackEfficiencyBase + hatchBonus); HatcheryHelperTweaks.ensureBonusMapFor(hatchBonus + 0.1); this.prevBonus(HatcheryHelperMinBonusMap[hatchBonus] || 0); this.nextBonus( HatcheryHelperMinBonusMap[(hatchBonus * 10 + 1) / 10] || 1, ); HatcheryHelperTweaks.applyToHelper(this); if (originalUpdateBonus) { // Keep any side effects from the original update // without overwriting our custom values. } }; if ( HatcheryHelper?.prototype?.charge && !HatcheryHelper._chargePatched ) { HatcheryHelper._chargePatched = true; const originalCharge = HatcheryHelper.prototype.charge; HatcheryHelper.prototype.charge = function () { if (this.cost?.amount === 0) return; return originalCharge.call(this); }; } }, }; /** * Handles User Interface Injection 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); } }, /** * Injects the script card into the shared container. */ injectScriptCard() { if (document.getElementById("hatcheryHelperTweaksDisplay")) return; const displayDiv = document.createElement("div"); displayDiv.id = "hatcheryHelperTweaksDisplay"; const html = `
Hatchery Helper Tweaks
Cost Scaling
Hatch Bonus
Hiring
`; displayDiv.innerHTML = html; const scriptBody = document.getElementById("customScriptsBody"); scriptBody.appendChild(displayDiv); ko.applyBindings({ HatcheryHelperTweaks }, displayDiv); }, }; /** * Main Initialization Function */ function initializeHatcheryHelperTweaks() { HatcheryHelperTweaks.load(); HatcheryHelperTweaks.costScalingEnabled.subscribe(() => { HatcheryHelperTweaks.applyToAll(); HatcheryHelperTweaks.save(); }); HatcheryHelperTweaks.maxHelpersEnabled.subscribe(() => { HatcheryHelperTweaks.applyMaxHelpers(); HatcheryHelperTweaks.save(); }); HatcheryHelperTweaks.bonusMode.subscribe(() => { HatcheryHelperTweaks.applyBonusSettings(); HatcheryHelperTweaks.save(); }); HatcheryHelperTweaks.bonusMax.subscribe(() => { HatcheryHelperTweaks.applyBonusSettings(); HatcheryHelperTweaks.save(); }); HatcheryHelperTweaks.costScaleMode.subscribe(() => { HatcheryHelperTweaks.applyToAll(); HatcheryHelperTweaks.save(); }); HatcheryHelperTweaks.applyBonusSettings(); HatcheryHelperTweaks.applyToAll(); HatcheryHelperTweaks.applyMaxHelpers(); HatcheryHelperTweaks.initialized = true; UserInterface.injectScriptCard(); } /** * Run patches before game start */ function runPriorityPatches() { GameMechanics.applyPatches(); UserInterface.createContainer(); } // 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.HatcheryHelperTweaks = HatcheryHelperTweaks; } else { window.HatcheryHelperTweaks = HatcheryHelperTweaks; } loadScript( "Hatchery Helper Tweaks", initializeHatcheryHelperTweaks, runPriorityPatches, );