Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
190 changes: 146 additions & 44 deletions enhancedautohatchery.user.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,10 @@
// @name [Pokeclicker] Enhanced Auto Hatchery
// @namespace Pokeclicker Scripts
// @author Ephenia (Original/Credit: Drak + Ivan Lay, Optimatum)
// @description Automatically hatches eggs at 100% completion. Adds an On/Off button for auto hatching as well as an option for automatically hatching store bought eggs and dug up fossils.
// @description Auto-hatches eggs at 100% and adds controls: Auto Hatch, PKRS Mode, Auto Egg, Auto Fossil, Shiny Fossils, Filter Only, and a Priority ID queue (e.g., 149,123,552,479.01) supporting alternate forms.
// @copyright https://github.com/Ephenia
// @license GPL-3.0 License
// @version 3.1.4
// @version 3.3.0-priority

// @homepageURL https://github.com/Ephenia/Pokeclicker-Scripts/
// @supportURL https://github.com/Ephenia/Pokeclicker-Scripts/issues
Expand All @@ -23,6 +23,12 @@ var eggState;
var fossilState;
var shinyFossilState;
var pkrsState;
// Filter Only: restricts loading to current hatchery filter
var filterOnlyState;

// NEW: user-defined priority Pokémon IDs (array of numbers)
var priorityIdList = [];

var pkrsHatcherySearchTime = 0;
var numMonsWithPkrsCached;
var autoHatcheryCachedList = [];
Expand All @@ -36,32 +42,54 @@ function initAutoHatch() {
Auto Hatch [${hatchState ? 'ON' : 'OFF'}]
</button>`

breedingModal.querySelector('.modal-header').querySelectorAll('button')[1].outerHTML += `<button id="pkrs-mode" class="btn btn-${pkrsState ? 'success' : 'danger'}" style="margin-left:20px;">
PKRS Mode [${pkrsState ? 'ON' : 'OFF'}]
// Controls row in modal header
const headerBtnContainer = breedingModal.querySelector('.modal-header').querySelectorAll('button')[1];
headerBtnContainer.outerHTML += `
<button id="pkrs-mode" class="btn btn-${pkrsState ? 'success' : 'danger'}" style="margin-left:20px;">
PKRS Mode [${pkrsState ? 'ON' : 'OFF'}]
</button>
<button id="auto-egg" class="btn btn-${eggState ? 'success' : 'danger'}" style="margin-left:20px;">
Auto Egg [${eggState ? 'ON' : 'OFF'}]
Auto Egg [${eggState ? 'ON' : 'OFF'}]
</button>
<button id="auto-fossil" class="btn btn-${fossilState ? 'success' : 'danger'}" style="margin-left:20px;">
Auto Fossil [${fossilState ? 'ON' : 'OFF'}]
Auto Fossil [${fossilState ? 'ON' : 'OFF'}]
</button>
<button id="shiny-fossils" class="btn btn-${shinyFossilState ? 'success' : 'danger'}" style="margin-left:20px;">
Shiny Fossils [${shinyFossilState ? 'ON' : 'OFF'}]
</button>`;
Shiny Fossils [${shinyFossilState ? 'ON' : 'OFF'}]
</button>
<button id="filter-only" class="btn btn-${filterOnlyState ? 'success' : 'danger'}" style="margin-left:20px;">
Filter Only [${filterOnlyState ? 'ON' : 'OFF'}]
</button>
<!-- Priority input controls -->
<div id="priority-controls" style="display:inline-flex; align-items:center; margin-left:20px; gap:8px;">
<input id="priority-ids" type="text" inputmode="numeric" pattern="[0-9.,\\s-]*" placeholder="Priority IDs e.g. 149,123,552,479.01" style="max-width:260px; padding:3px 6px; font-size:12px;">
<button id="priority-save" class="btn btn-primary btn-sm">Save Priority</button>
</div>
<span id="priority-status" style="margin-left:10px; font-size:12px; opacity:.8;"></span>
`;

document.getElementById('auto-hatch-start').addEventListener('click', toggleAutoHatch);
document.getElementById('auto-egg').addEventListener('click', toggleEgg);
document.getElementById('auto-fossil').addEventListener('click', toggleFossil);
document.getElementById('shiny-fossils').addEventListener('click', toggleShinyFossil);
document.getElementById('pkrs-mode').addEventListener('click', togglePKRS);
document.getElementById('filter-only').addEventListener('click', toggleFilterOnly);

document.getElementById('auto-hatch-start').addEventListener('click', event => { toggleAutoHatch(event); });
document.getElementById('auto-egg').addEventListener('click', event => { toggleEgg(event); });
document.getElementById('auto-fossil').addEventListener('click', event => { toggleFossil(event); });
document.getElementById('shiny-fossils').addEventListener('click', event => { toggleShinyFossil(event); });
document.getElementById('pkrs-mode').addEventListener('click', event => { togglePKRS(event); });
// Load/render priority list UI
renderPriorityUiFromStorage();
document.getElementById('priority-save').addEventListener('click', savePriorityFromInput);

addGlobalStyle('.eggSlot.disabled { pointer-events: unset !important; }');

// Initialize list since the game won't until the hatchery menu opens
autoHatcheryCachedList = BreedingController.hatcherySortedFilteredList();

// Immediately refresh the cached list when the filtered list or sort settings change
const listUpdateObservables = [BreedingController.hatcheryFilteredList, Settings.getSetting('hatcherySort').observableValue, Settings.getSetting('hatcherySortDirection').observableValue];
const listUpdateObservables = [
BreedingController.hatcheryFilteredList,
Settings.getSetting('hatcherySort').observableValue,
Settings.getSetting('hatcherySortDirection').observableValue
];
listUpdateObservables.forEach(observable => observable.subscribe(() => {
autoHatcheryCachedList = BreedingController.hatcherySortedFilteredList();
}));
Expand Down Expand Up @@ -114,6 +142,84 @@ function togglePKRS(event) {
localStorage.setItem('pokerusModeState', pkrsState);
}

function toggleFilterOnly(event) {
const element = event.target;
filterOnlyState = !filterOnlyState;
element.classList.replace(...(filterOnlyState ? ['btn-danger', 'btn-success'] : ['btn-success', 'btn-danger']));
element.textContent = `Filter Only [${filterOnlyState ? 'ON' : 'OFF'}]`;
localStorage.setItem('filterOnly', filterOnlyState);
}

// ---------- Priority IDs: storage, parsing, UI ----------

function renderPriorityUiFromStorage() {
// Load from localStorage
const raw = localStorage.getItem('priorityMonIds') || '[]';
try {
const arr = JSON.parse(raw);
if (Array.isArray(arr)) priorityIdList = arr.map(n => Number(n)).filter(n => Number.isFinite(n) && n > 0);
} catch {
priorityIdList = [];
}
const input = document.getElementById('priority-ids');
const status = document.getElementById('priority-status');
input.value = priorityIdList.join(', ');
status.textContent = priorityIdList.length ? `Priority: [${priorityIdList.join(', ')}]` : 'Priority: (none)';
}

function savePriorityFromInput() {
const input = document.getElementById('priority-ids');
const status = document.getElementById('priority-status');
// Accept comma/space separated, tolerate extra chars - now supports decimal IDs
const parts = input.value.split(/[^0-9.]+/).filter(Boolean);
const parsed = parts.map(n => Number(n)).filter(n => Number.isFinite(n) && n > 0 && !isNaN(n));
// De-duplicate while preserving order
const seen = new Set();
priorityIdList = parsed.filter(n => (seen.has(n) ? false : (seen.add(n), true)));

localStorage.setItem('priorityMonIds', JSON.stringify(priorityIdList));
status.textContent = priorityIdList.length ? `Priority: [${priorityIdList.join(', ')}]` : 'Priority: (none)';
Notifier.notify({
type: NotificationConstants.NotificationOption.success,
title: '[Auto Hatchery]',
message: `Saved priority IDs: ${priorityIdList.length ? priorityIdList.join(', ') : '(none)'}`,
timeout: GameConstants.SECOND * 3,
});
}

// Try adding the next available priority mon (highest priority first)
function autoHatchPriority() {
if (!priorityIdList.length) return false;

// Build quick lookup from party by Pokédex ID -> first hatchable instance
// We'll scan per priority to keep order strict.
for (let id of priorityIdList) {
// Find a hatchable party member whose dex ID matches
// Support both integer IDs (479) and decimal IDs (479.01) for alternate forms
const mon = App.game.party.caughtPokemon.find(p => {
if (!p || p.breeding || !p.isHatchable()) return false;
// pokemonMap[p.name].id is available elsewhere in script (type was used earlier)
const data = pokemonMap[p.name];
if (!data) return false;

// Exact match for decimal IDs (e.g., 479.01)
if (data.id === id) return true;

// For integer IDs, match both base forms and alternate forms
// This allows 479 to match both 479 (base) and 479.01, 479.02, etc. (alternates)
if (Number.isInteger(id) && Math.floor(data.id) === id) return true;

return false;
});
if (mon) {
return App.game.breeding.addPokemonToHatchery(mon);
}
}
return false;
}

// --------------------------------------------------------

function bindAutoHatcher() {
const progressEggsOld = Breeding.prototype.progressEggs;
Breeding.prototype.progressEggs = function progressEggs(...args) {
Expand Down Expand Up @@ -141,15 +247,18 @@ function autoHatcher() {
}

while (App.game.breeding.hasFreeEggSlot()) {
// Attempts enabled autoHatch methods in order until one succeeds
// (subsequent autoHatch methods aren't called due to short-circuiting)
let success = pkrsState && autoHatchPkrs();
// Priority list ALWAYS comes first (strict order)
let success = autoHatchPriority();

// Then PKRS / Eggs / Fossils
success ||= pkrsState && autoHatchPkrs();
success ||= eggState && autoHatchEgg();
success ||= fossilState && autoHatchFossil();

// Finally, standard list (respects Filter Only toggle)
success ||= autoHatchMon();
if (!success) {
break;
}

if (!success) break;
}
}

Expand All @@ -158,11 +267,9 @@ function autoHatchPkrs() {
if (!App.game.keyItems.hasKeyItem(KeyItemType.Pokerus_virus)) {
return false;
}
// No need to search if we already know there aren't party members to infect
if (numMonsWithPkrsCached == App.game.party.caughtPokemon.length) {
return false;
}
// If we couldn't find a uninfected/contagious pair, wait a while before trying again
if (Date.now() - pkrsHatcherySearchTime < delayAfterFailure) {
return false;
}
Expand All @@ -171,8 +278,6 @@ function autoHatchPkrs() {
let contagious = {};
let foundPair = false;
let infectedCount = 0;
// Find first uninfected/contagious pair sharing a type
// Ideally the uninfected mon is dual-type to accelerate future spreading
for (let mon of App.game.party.caughtPokemon) {
infectedCount += mon.pokerus > GameConstants.Pokerus.Uninfected;
if (mon.breeding || mon.level < 100) {
Expand All @@ -194,21 +299,17 @@ function autoHatchPkrs() {
checkMatch = true;
}
}
// Stop searching upon finding a infectable dual-type
if (checkMatch) {
for (let type of types) {
if (type in uninfectedDual && type in contagious) {
foundPair = {'uninfected': uninfectedDual[type], 'contagious': contagious[type]};
}
}
if (foundPair) {
break;
}
if (foundPair) break;
}
}
if (!foundPair) {
numMonsWithPkrsCached = infectedCount;
// No infectable dual-type pokemon found, try a monotype
for (let type of GameHelper.enumNumbers(PokemonType)) {
if (type in uninfectedMono && type in contagious) {
foundPair = {'uninfected': uninfectedMono[type], 'contagious': contagious[type]};
Expand Down Expand Up @@ -236,35 +337,35 @@ function autoHatchEgg() {
}

function autoHatchFossil() {
// Fossils in inventory with amount > 0
let fossilList = UndergroundItems.list.filter(it => it.valueType === UndergroundItemValueType.Fossil && player.itemList[it.itemName]() > 0);
if (fossilList.length == 0) {
return false;
}
let priorityList = fossilList.filter(f => {
let priorityList = fossilList.filter(f => {
const caughtStatus = PartyController.getCaughtStatusByName(GameConstants.FossilToPokemon[f.name]);
return caughtStatus == CaughtStatus.NotCaught || (shinyFossilState && caughtStatus == CaughtStatus.Caught);
});
if (priorityList.length) {
fossilList = priorityList;
}
let fossilToUse = fossilList[Math.floor(Math.random() * fossilList.length)];
// Workaround as sellMineItem returns null
let before = player.amountOfItem(fossilToUse.itemName)
UndergroundController.sellMineItem(fossilToUse);
let after = player.amountOfItem(fossilToUse.itemName);
return before > after;
}

// Respects Filter Only toggle
function autoHatchMon() {
let toHatch = autoHatcheryCachedList.find(p => p.isHatchable());
if (!toHatch) {
// Nothing matches the hatchery filters
toHatch = App.game.party.caughtPokemon.find(p => p.isHatchable());
if (filterOnlyState) {
if (!toHatch) return false;
return App.game.breeding.addPokemonToHatchery(toHatch);
}
if (!toHatch) {
return false;
toHatch = App.game.party.caughtPokemon.find(p => p.isHatchable());
}
if (!toHatch) return false;
return App.game.breeding.addPokemonToHatchery(toHatch);
}

Expand All @@ -273,6 +374,14 @@ eggState = loadSetting('autoEgg', false);
fossilState = loadSetting('autoFossil', false);
shinyFossilState = loadSetting('shinyFossil', false);
pkrsState = loadSetting('pokerusModeState', false);
filterOnlyState = loadSetting('filterOnly', false);

// Load priority list once at boot (UI will re-render it)
try {
const raw = localStorage.getItem('priorityMonIds') || '[]';
const arr = JSON.parse(raw);
priorityIdList = Array.isArray(arr) ? arr.map(n => Number(n)).filter(n => Number.isFinite(n) && n > 0) : [];
} catch { priorityIdList = []; }

function loadSetting(key, defaultVal) {
var val;
Expand Down Expand Up @@ -309,17 +418,14 @@ function loadEpheniaScript(scriptName, initFunction, priorityFunction) {
});
}
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();
Expand All @@ -333,31 +439,27 @@ function loadEpheniaScript(scriptName, initFunction, priorityFunction) {
}
}

// 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,
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;
}
});
Expand Down