Skip to content

Commit

Permalink
Hatchery: Add Category sorting priority
Browse files Browse the repository at this point in the history
The player can now prioritize hatching for a specific category.
The category is the first filter, so it has the most priority.
  • Loading branch information
Xwaler authored and Farigh committed Jun 30, 2024
1 parent dd8236e commit 0641fb8
Show file tree
Hide file tree
Showing 6 changed files with 266 additions and 2 deletions.
2 changes: 1 addition & 1 deletion Readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ Pokeclicker-automation aims at automating some recurring tasks that can be a bit
This script collection does not aim at cheating.
It will never perform actions that the game would not allow.

Last known compatible pokeclicker version: 0.10.19
Last known compatible pokeclicker version: 0.10.20

For more details, please refer to the [wiki](../../wiki)

Expand Down
163 changes: 162 additions & 1 deletion src/lib/Hatchery.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ class AutomationHatchery
PrioritizedSortingDescending: "Hatchery-PrioritizedSortingDescending",
PrioritizedType: "Hatchery-PrioritizedType",
PrioritizedRegion: "Hatchery-PrioritizedRegion",
PrioritizedCategory: "Hatchery-PrioritizedCategory",
RegionalDebuffRegion: "Hatchery-RegionalDebuffRegion"
};

Expand Down Expand Up @@ -60,6 +61,9 @@ class AutomationHatchery
// Set default regional debuff to None
Automation.Utils.LocalStorage.setDefaultValue(this.Settings.RegionalDebuffRegion, GameConstants.Region.none);

// Set default category priority to Any
Automation.Utils.LocalStorage.setDefaultValue(this.Settings.PrioritizedCategory, -1);

this.__internal__buildRegionDataList();
this.__internal__buildSeviiIslandPokemonLists();
this.__internal__buildMenu();
Expand Down Expand Up @@ -120,6 +124,7 @@ class AutomationHatchery
static __internal__hatcheryContainer = null;
static __internal__autoHatcheryLoop = null;
static __internal__regionSelectElem = null;
static __internal__categorySelectElem = null;
static __internal__lockedRegions = [];

static __internal__seviiIslandPokemonIds = [];
Expand All @@ -134,6 +139,7 @@ class AutomationHatchery
static __internal__sortingFunctions = [];
static __internal__sortRegionSetting = null;
static __internal__sortTypeSetting = null;
static __internal__sortCategoryIdSetting = null;
static __internal__sortMegaEvolutionsFirstSetting = null;
static __internal__sortNotShinyFirstSetting = null;
static __internal__sortNotAlternateFormFirstSetting = null;
Expand Down Expand Up @@ -211,6 +217,14 @@ class AutomationHatchery
const sortingSelector = this.__internal__buildSortingSelectorList();
categoryContainer.appendChild(sortingSelector);

/*************************\
|* Category prioritizing *|
\*************************/

// Add category selector
const categorySelector = this.__internal__buildCategorySelectorList();
categoryContainer.appendChild(categorySelector);

/***********************\
|* Region prioritizing *|
\***********************/
Expand Down Expand Up @@ -294,7 +308,6 @@ class AutomationHatchery
}
}


/**
* @brief Builds the type selector drop-down list
*
Expand Down Expand Up @@ -367,7 +380,121 @@ class AutomationHatchery

return container;
}
/**
* @brief Updates the category selector list in case of in-game category edition
*
* When a category is modified, created or deleted, update it's respective select option
*/
static __internal__updateCategorySelector(isInitialization)
{
const previouslySelectedCategoryId = parseInt(Automation.Utils.LocalStorage.getValue(this.Settings.PrioritizedCategory));

if (!isInitialization)
{
// Remove any deleted categories
const ingameCategories = PokemonCategories.categories().map((category) => category.id);
for (let optionIndex = this.__internal__categorySelectElem.options.length - 1; optionIndex > 1; optionIndex--)
{
const optionValue = parseInt(this.__internal__categorySelectElem.options[optionIndex].value);
if (!ingameCategories.includes(optionValue))
{
// If deleted category was used as filter, fallback to the "Any" one
if (previouslySelectedCategoryId == optionValue)
{
Automation.Utils.LocalStorage.setValue(this.Settings.PrioritizedCategory, -1);
}
this.__internal__categorySelectElem.options.remove(optionIndex);
}
}

// Update existing options.
for (const option of this.__internal__categorySelectElem.options)
{
// Skip the internal "Any" option
if (option.id == -1)
{
continue;
}

const ingameCategory = PokemonCategories.categories().find((category) => category.id == parseInt(option.value))
if (option.textContent != ingameCategory.name())
{
option.textContent = ingameCategory.name();
}
}
}

// Add option for new categories
const existingOptionValues = [...this.__internal__categorySelectElem.options].map((opt) => parseInt(opt.value));
for (const category of PokemonCategories.categories())
{
if (!existingOptionValues.includes(category.id))
{
const opt = document.createElement("option");

// Set the category name as the content
opt.textContent = category.name();

opt.value = category.id;
opt.id = category.id;

// Restore the previouly selected option
if (isInitialization && (category.id == previouslySelectedCategoryId))
{
// Restore previous session selected element
opt.selected = true;
}

this.__internal__categorySelectElem.options.add(opt);
}
}
}

/**
* @brief Builds the category selector drop-down list
*
* @returns the created element
*/
static __internal__buildCategorySelectorList()
{
const tooltip = "The pokémons of the selected category will be added in priority"

const container = document.createElement("div");
container.style.paddingLeft = "10px";
container.style.paddingRight = "10px";
container.classList.add("hasAutomationTooltip");
container.setAttribute("automation-tooltip-text", tooltip);

const label = "Prioritize pokémon of the following category:";
container.appendChild(document.createTextNode(label));

this.__internal__categorySelectElem = Automation.Menu.createDropDownListElement("selectedCategory-Hatchery");
this.__internal__categorySelectElem.style.position = "relative";
this.__internal__categorySelectElem.style.bottom = "2px";
this.__internal__categorySelectElem.style.width = "110px";
this.__internal__categorySelectElem.style.marginLeft = "4px";
this.__internal__categorySelectElem.style.paddingLeft = "3px";
container.appendChild(this.__internal__categorySelectElem);

// Add the "Any" category
let opt = document.createElement("option");
opt.textContent = "Any";
opt.value = -1;
opt.id = -1;
this.__internal__categorySelectElem.options.add(opt);

// Populate the list with categories
this.__internal__updateCategorySelector(true);
setInterval(function() { this.__internal__updateCategorySelector(false) }.bind(this), 10000); // Check every 10 seconds

// Update the local storage if the value is changed by the user
this.__internal__categorySelectElem.onchange = function()
{
Automation.Utils.LocalStorage.setValue(this.Settings.PrioritizedCategory, this.__internal__categorySelectElem.value);
}.bind(this);

return container;
}

/**
* @brief Builds the sorting selector drop-down list
Expand Down Expand Up @@ -671,6 +798,9 @@ class AutomationHatchery
*/
static __internal__buildSortingFunctionsList()
{
// Category priority
this.__internal__sortingFunctions.push(this.__internal__sortByCategory.bind(this));

// Region priority
this.__internal__sortingFunctions.push(this.__internal__sortByRegion.bind(this));

Expand Down Expand Up @@ -851,6 +981,7 @@ class AutomationHatchery
this.__internal__sortNotAlternateFormFirstSetting = (Automation.Utils.LocalStorage.getValue(this.Settings.NotAlternateFormFirst) === "true");
this.__internal__sortRegionSetting = Automation.Utils.LocalStorage.getValue(this.Settings.PrioritizedRegion);
this.__internal__sortTypeSetting = parseInt(Automation.Utils.LocalStorage.getValue(this.Settings.PrioritizedType));
this.__internal__sortCategoryIdSetting = parseInt(Automation.Utils.LocalStorage.getValue(this.Settings.PrioritizedCategory));

// Initialize the sort by attribute function
this.__internal__setAttributeSortingFunction();
Expand Down Expand Up @@ -902,6 +1033,36 @@ class AutomationHatchery
}
}

/**
* @brief Sorts the given @p a and @p b pokémons depending on their category
*
* @param a: The 1st pokémon to compare
* @param b: The 2nd pokémon to compare
*
* @returns -1 if @p a is in the selected category and not @p b,
* 1 if @p b is in the selected category and not @p a,
* 0 otherwise
*/
static __internal__sortByCategory(a, b)
{
if (this.__internal__sortCategoryIdSetting != -1)
{
const isACategoryValid = a.category.includes(this.__internal__sortCategoryIdSetting);
const isBCategoryValid = b.category.includes(this.__internal__sortCategoryIdSetting);

if (isACategoryValid && !isBCategoryValid)
{
return -1;
}
if (!isACategoryValid && isBCategoryValid)
{
return 1;
}
}

return 0;
}

/**
* @brief Sorts the given @p a and @p b pokémons depending on their region
*
Expand Down
1 change: 1 addition & 0 deletions tst/imports/Pokeclicker.import.js
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,7 @@ import "tst/stubs/MapHelper.pokeclicker.stub.js";
import "tst/stubs/Player.pokeclicker.stub.js";
import "tst/stubs/Save.pokeclicker.stub.js";
import "tst/stubs/Statistics.pokeclicker.stub.js";
import "tst/stubs/Category.pokeclicker.stub.js";

// Import the main class
import "tst/stubs/App.pokeclicker.stub.js";
16 changes: 16 additions & 0 deletions tst/stubs/Category.pokeclicker.stub.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
// Stub of https://github.com/pokeclicker/pokeclicker/blob/9828628acc9142075e6108b54b5b992bc2196282/src/modules/party/Category.ts#L19
class PokemonCategories {
/***************************\
|* Pokéclicker interface *|
\***************************/

static __categories = [
{ id: 0, name: () => 'None', color: () => '#333333' },
{ id: 1, name: () => 'Favorite', color: () => '#e74c3c' }
];

static categories()
{
return this.__categories
}
}
32 changes: 32 additions & 0 deletions tst/stubs/Pokemon/PartyPokemon.pokeclicker.stub.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ class PartyPokemon
{
this.attackBonusPercent = 0;
this.attackBonusAmount = 0;
this.category = [0]
this.breading = false;
this.baseAttack = baseAttack;
this.effortPoints = 0;
Expand Down Expand Up @@ -41,4 +42,35 @@ class PartyPokemon
const power = App.game.challenges.list.slowEVs.active() ? GameConstants.EP_CHALLENGE_MODIFIER : 1;
return Math.floor(this.effortPoints / GameConstants.EP_EV_RATIO / power);
}

/***************************\
|* Test-only interface *|
\***************************/

__addCategory(id)
{
if (id == 0)
{
this.category = [0];
}
else if (!this.category.includes(id))
{
this.category.push(id);
}
}

__removeCategory(id)
{
if ((id == 0) && (this.category.length == 1))
{
// Can't remove None category without another category present
return;
}

const index = this.category.indexOf(id);
if (index > -1)
{
this.category.splice(index, 1);
}
}
}
54 changes: 54 additions & 0 deletions tst/tests/Hachery/AutoHatchery.test.in.js
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@ function resetSettings()
Automation.Utils.LocalStorage.setValue(Automation.Hatchery.Settings.PrioritizedType, PokemonType.None);
Automation.Utils.LocalStorage.setValue(Automation.Hatchery.Settings.RegionalDebuffRegion, GameConstants.Region.none);
Automation.Utils.LocalStorage.setValue(Automation.Hatchery.Settings.PrioritizedSorting, SortOptions.breedingEfficiency);
Automation.Utils.LocalStorage.setValue(Automation.Hatchery.Settings.PrioritizedCategory, -1);
}

/************************\
Expand Down Expand Up @@ -123,6 +124,7 @@ beforeAll(() =>
expect(Automation.Utils.LocalStorage.getValue(Automation.Hatchery.Settings.PrioritizedRegion)).toBe("-1");
expect(Automation.Utils.LocalStorage.getValue(Automation.Hatchery.Settings.PrioritizedType)).toBe("-1");
expect(Automation.Utils.LocalStorage.getValue(Automation.Hatchery.Settings.RegionalDebuffRegion)).toBe("-1");
expect(Automation.Utils.LocalStorage.getValue(Automation.Hatchery.Settings.PrioritizedCategory)).toBe("-1");
});

beforeEach(() =>
Expand Down Expand Up @@ -598,3 +600,55 @@ describe(`${AutomationTestUtils.categoryPrefix}Party pokémon breeding order > R
}
});
});

describe(`${AutomationTestUtils.categoryPrefix}Party pokémon breeding order > Category priority`, () =>
{
/** @note Those orders might change if new pokemon were to be added, or their value changed */
let categoryTestCases =
[
{ categoryId: -1, categoryName: "Any", expectedPokemons: [ "Sawk", "Pikachu", "Throh", "Hitmonlee" ] },
{
categoryId: PokemonCategories.categories()[0].id,
categoryName: PokemonCategories.categories()[0].name(),
expectedPokemons: [ "Sawk", "Throh", "Hitmonlee", "Qwilfish"]
},
{
categoryId: PokemonCategories.categories()[1].id,
categoryName: PokemonCategories.categories()[1].name(),
expectedPokemons: [ "Pikachu", "Machop", "Sawk", "Throh" ]
},
];

test.each(categoryTestCases)("Test breeding order with category set to $categoryName", (testCase) =>
{
addSomePokemonsToThePlayersParty();

// Put the all pokemons to lvl 100
for (const pokemon of App.game.party.caughtPokemon)
{
pokemon.level = 100;
}

// Put some pokemons in the favorite category
for (const pokemon of App.game.party.caughtPokemon)
{
if (["Machop", "Pikachu"].includes(pokemonMap[pokemon.id].name))
{
pokemon.__addCategory(PokemonCategories.categories()[1].id);
pokemon.__removeCategory(PokemonCategories.categories()[0].id);
}
}

// Set the category priority
Automation.Utils.LocalStorage.setValue(Automation.Hatchery.Settings.PrioritizedCategory, testCase.categoryId);

// Simulate the loop
Automation.Hatchery.__internal__hatcheryLoop();

for (const i of testCase.expectedPokemons.keys())
{
expect(App.game.breeding.__eggList[i].pokemon).toEqual(testCase.expectedPokemons[i]);
expect(App.game.breeding.__eggList[i].isNone()).toBe(false);
}
});
});

0 comments on commit 0641fb8

Please sign in to comment.