This commit is contained in:
Sirz Benjie 2025-04-13 13:56:32 +02:00 committed by GitHub
commit a8828b04dd
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 353 additions and 18 deletions

View File

@ -20,7 +20,6 @@ export class FilterText extends Phaser.GameObjects.Container {
private window: Phaser.GameObjects.NineSlice;
private labels: Phaser.GameObjects.Text[] = [];
private selections: Phaser.GameObjects.Text[] = [];
private selectionStrings: string[] = [];
private rows: FilterTextRow[] = [];
public cursorObj: Phaser.GameObjects.Image;
public numFilters = 0;
@ -112,8 +111,6 @@ export class FilterText extends Phaser.GameObjects.Container {
this.selections.push(filterTypesSelection);
this.add(filterTypesSelection);
this.selectionStrings.push("");
this.calcFilterPositions();
this.numFilters++;
@ -122,7 +119,6 @@ export class FilterText extends Phaser.GameObjects.Container {
resetSelection(index: number): void {
this.selections[index].setText(this.defaultText);
this.selectionStrings[index] = "";
this.onChange();
}
@ -204,6 +200,17 @@ export class FilterText extends Phaser.GameObjects.Container {
return this.selections[row].getWrappedText()[0];
}
/**
* Forcibly set the selection text for a specific filter row and then call the `onChange` function
*
* @param row - The filter row to set the text for
* @param value - The text to set for the filter row
*/
setValue(row: FilterTextRow, value: string) {
this.selections[row].setText(value);
this.onChange();
}
/**
* Find the nearest filter to the provided container on the y-axis
* @param container the StarterContainer to compare position against

View File

@ -37,10 +37,9 @@ import { addWindow } from "./ui-theme";
import type { OptionSelectConfig } from "./abstact-option-select-ui-handler";
import { FilterText, FilterTextRow } from "./filter-text";
import { allAbilities } from "#app/data/ability";
import { starterPassiveAbilities } from "#app/data/balance/passives";
import { allMoves } from "#app/data/moves/move";
import { speciesTmMoves } from "#app/data/balance/tms";
import { pokemonPrevolutions, pokemonStarters } from "#app/data/balance/pokemon-evolutions";
import { pokemonStarters } from "#app/data/balance/pokemon-evolutions";
import { Biome } from "#enums/biome";
import { globalScene } from "#app/global-scene";
@ -174,7 +173,6 @@ export default class PokedexUiHandler extends MessageUiHandler {
private scrollCursor: number;
private oldCursor = -1;
private allSpecies: PokemonSpecies[] = [];
private lastSpecies: PokemonSpecies;
private speciesLoaded: Map<Species, boolean> = new Map<Species, boolean>();
private pokerusSpecies: PokemonSpecies[] = [];
@ -493,12 +491,11 @@ export default class PokedexUiHandler extends MessageUiHandler {
for (const species of allSpecies) {
this.speciesLoaded.set(species.speciesId, false);
this.allSpecies.push(species);
}
// Here code to declare 81 containers
for (let i = 0; i < 81; i++) {
const pokemonContainer = new PokedexMonContainer(this.allSpecies[i]).setVisible(false);
const pokemonContainer = new PokedexMonContainer(allSpecies[i]).setVisible(false);
const pos = calcStarterPosition(i);
pokemonContainer.setPosition(pos.x, pos.y);
this.iconAnimHandler.addOrUpdate(pokemonContainer.icon, PokemonIconAnimMode.NONE);
@ -1342,7 +1339,7 @@ export default class PokedexUiHandler extends MessageUiHandler {
this.filteredPokemonData = [];
this.allSpecies.forEach(species => {
allSpecies.forEach(species => {
const starterId = this.getStarterSpeciesId(species.speciesId);
const currentDexAttr = this.getCurrentDexProps(species.speciesId);
@ -1412,12 +1409,11 @@ export default class PokedexUiHandler extends MessageUiHandler {
// Ability filter
const abilities = [species.ability1, species.ability2, species.abilityHidden].map(a => allAbilities[a].name);
const passiveId = starterPassiveAbilities.hasOwnProperty(species.speciesId)
? species.speciesId
: starterPassiveAbilities.hasOwnProperty(starterId)
? starterId
: pokemonPrevolutions[starterId];
const passives = starterPassiveAbilities[passiveId];
// get the passive ability for the species
const passives = [species.getPassiveAbility()];
for (const form of species.forms) {
passives.push(form.getPassiveAbility());
}
const selectedAbility1 = this.filterText.getValue(FilterTextRow.ABILITY_1);
const fitsFormAbility1 = species.forms.some(form =>

View File

@ -20,6 +20,8 @@ import KeyboardPlugin = Phaser.Input.Keyboard.KeyboardPlugin;
import GamepadPlugin = Phaser.Input.Gamepad.GamepadPlugin;
import EventEmitter = Phaser.Events.EventEmitter;
import UpdateList = Phaser.GameObjects.UpdateList;
import { PokedexMonContainer } from "#app/ui/pokedex-mon-container";
import MockContainer from "./mocks/mocksContainer/mockContainer";
window.URL.createObjectURL = (blob: Blob) => {
blobToString(blob).then((data: string) => {
@ -58,6 +60,10 @@ export default class GameWrapper {
}
};
BattleScene.prototype.addPokemonIcon = () => new Phaser.GameObjects.Container(this.scene);
// Pokedex container is not actually mocking container, but the sprites they contain are mocked.
// We need to mock the remove function to not throw an error when removing a sprite.
PokedexMonContainer.prototype.remove = MockContainer.prototype.remove;
}
setScene(scene: BattleScene) {

View File

@ -308,5 +308,14 @@ export default class MockText implements MockGameObject {
return this.list;
}
/**
* Runs the word wrap algorithm on the text, then returns an array of the lines
*/
getWrappedText() {
// Returns the wrapped text.
// return this.phaserText.getWrappedText();
return this.runWordWrap(this.text).split("\n");
}
on(_event: string | symbol, _fn: Function, _context?: any) {}
}

View File

@ -204,6 +204,7 @@ export default class PhaseInterceptor {
private phaseFrom;
private inProgress;
private originalSetMode;
private originalSetOverlayMode;
private originalSuperEnd;
/**
@ -441,6 +442,7 @@ export default class PhaseInterceptor {
*/
initPhases() {
this.originalSetMode = UI.prototype.setMode;
this.originalSetOverlayMode = UI.prototype.setOverlayMode;
this.originalSuperEnd = Phase.prototype.end;
UI.prototype.setMode = (mode, ...args) => this.setMode.call(this, mode, ...args);
Phase.prototype.end = () => this.superEndPhase.call(this);
@ -507,6 +509,18 @@ export default class PhaseInterceptor {
return ret;
}
/**
* mock to set overlay mode
* @param mode - The {@linkcode Mode} to set.
* @param args - Additional arguments to pass to the original method.
*/
setOverlayMode(mode: Mode, ...args: unknown[]): Promise<void> {
const instance = this.scene.ui;
console.log("setOverlayMode", `${Mode[mode]} (=${mode})`, args);
const ret = this.originalSetOverlayMode.apply(instance, [mode, ...args]);
return ret;
}
/**
* Method to start the prompt handler.
*/
@ -571,6 +585,7 @@ export default class PhaseInterceptor {
phase.prototype.start = this.phases[phase.name].start;
}
UI.prototype.setMode = this.originalSetMode;
UI.prototype.setOverlayMode = this.originalSetOverlayMode;
Phase.prototype.end = this.originalSuperEnd;
clearInterval(this.promptInterval);
clearInterval(this.interval);

View File

@ -3,7 +3,7 @@ import { initLoggedInUser } from "#app/account";
import { initAbilities } from "#app/data/ability";
import { initBiomes } from "#app/data/balance/biomes";
import { initEggMoves } from "#app/data/balance/egg-moves";
import { initPokemonPrevolutions } from "#app/data/balance/pokemon-evolutions";
import { initPokemonPrevolutions, initPokemonStarters } from "#app/data/balance/pokemon-evolutions";
import { initMoves } from "#app/data/moves/move";
import { initMysteryEncounters } from "#app/data/mystery-encounters/mystery-encounters";
import { initPokemonForms } from "#app/data/pokemon-forms";
@ -84,7 +84,6 @@ export function initTestFile() {
HTMLCanvasElement.prototype.getContext = () => mockContext;
// Initialize all of these things if and only if they have not been initialized yet
// initSpecies();
if (!wasInitialized) {
wasInitialized = true;
initVouchers();
@ -99,6 +98,8 @@ export function initTestFile() {
initAbilities();
initLoggedInUser();
initMysteryEncounters();
// init the pokemon starters for the pokedex
initPokemonStarters();
}
manageListeners();

301
test/ui/pokedex.test.ts Normal file
View File

@ -0,0 +1,301 @@
import { Mode } from "#app/ui/ui";
import GameManager from "#test/testUtils/gameManager";
import Phaser from "phaser";
import { afterEach, beforeAll, beforeEach, describe, expect, it, type MockInstance, vi } from "vitest";
import PokedexUiHandler from "#app/ui/pokedex-ui-handler";
import { FilterTextRow } from "#app/ui/filter-text";
import { allAbilities } from "#app/data/ability";
import { Abilities } from "#enums/abilities";
import { Species } from "#enums/species";
import { allSpecies, getPokemonSpecies, type PokemonForm } from "#app/data/pokemon-species";
import { Button } from "#enums/buttons";
import { DropDownColumn } from "#app/ui/filter-bar";
import type PokemonSpecies from "#app/data/pokemon-species";
import { PokemonType } from "#enums/pokemon-type";
/**
* Return all permutations of elements from an array
*/
function permutations<T>(array: T[], length: number): T[][] {
if (length === 0) {
return [[]];
}
return array.flatMap((item, index) =>
permutations([...array.slice(0, index), ...array.slice(index + 1)], length - 1).map(perm => [item, ...perm]),
);
}
describe("UI - Pokedex", () => {
let phaserGame: Phaser.Game;
let game: GameManager;
const mocks: MockInstance[] = [];
beforeAll(() => {
phaserGame = new Phaser.Game({
type: Phaser.HEADLESS,
});
});
afterEach(() => {
while (mocks.length > 0) {
mocks.pop()?.mockRestore();
}
game.phaseInterceptor.restoreOg();
});
beforeEach(() => {
game = new GameManager(phaserGame);
});
/**
* Run the game to open the pokedex UI.
* @returns The handler for the pokedex UI.
*/
async function runToOpenPokedex(): Promise<PokedexUiHandler> {
// Open the pokedex UI.
await game.runToTitle();
await game.phaseInterceptor.setOverlayMode(Mode.POKEDEX);
// Get the handler for the current UI.
const handler = game.scene.ui.getHandler();
expect(handler).toBeInstanceOf(PokedexUiHandler);
return handler as PokedexUiHandler;
}
/**
* Compute a set of pokemon that have a specific ability in allAbilities
* @param ability - The ability to filter for
*/
function getSpeciesWithAbility(ability: Abilities): Set<Species> {
const speciesSet = new Set<Species>();
for (const pkmn of allSpecies) {
if (
[pkmn.ability1, pkmn.ability2, pkmn.getPassiveAbility(), pkmn.abilityHidden].includes(ability) ||
pkmn.forms.some(form =>
[form.ability1, form.ability2, form.abilityHidden, form.getPassiveAbility()].includes(ability),
)
) {
speciesSet.add(pkmn.speciesId);
}
}
return speciesSet;
}
/**
* Compute a set of pokemon that have one of the specified type(s)
*
* Includes all forms of the pokemon
* @param types - The types to filter for
*/
function getSpeciesWithType(...types: PokemonType[]): Set<Species> {
const speciesSet = new Set<Species>();
const tySet = new Set<PokemonType>(types);
// get the pokemon and its forms
outer: for (const pkmn of allSpecies) {
// @ts-expect-error We know that type2 might be null.
if (tySet.has(pkmn.type1) || tySet.has(pkmn.type2)) {
speciesSet.add(pkmn.speciesId);
continue;
}
for (const form of pkmn.forms) {
// @ts-expect-error We know that type2 might be null.
if (tySet.has(form.type1) || tySet.has(form.type2)) {
speciesSet.add(pkmn.speciesId);
continue outer;
}
}
}
return speciesSet;
}
/**
* Create mocks for the abilities of a species.
* This is used to set the abilities of a species to a specific value.
* All abilities are optional. Not providing one will set it to NONE.
*
* This will override the ability of the pokemon species only, unless set forms is true
*
* @param species - The species to set the abilities for
* @param ability - The ability to set for the first ability
* @param ability2 - The ability to set for the second ability
* @param hidden - The ability to set for the hidden ability
* @param passive - The ability to set for the passive ability
* @param setForms - Whether to also overwrite the abilities for each of the species' forms (defaults to `true`)
*/
function createAbilityMocks(
species: Species,
{
ability = Abilities.NONE,
ability2 = Abilities.NONE,
hidden = Abilities.NONE,
passive = Abilities.NONE,
setForms = true,
}: {
ability?: Abilities;
ability2?: Abilities;
hidden?: Abilities;
passive?: Abilities;
setForms?: boolean;
},
) {
const pokemon = getPokemonSpecies(species);
const checks: [PokemonSpecies | PokemonForm] = [pokemon];
if (setForms) {
checks.push(...pokemon.forms);
}
for (const p of checks) {
mocks.push(vi.spyOn(p, "ability1", "get").mockReturnValue(ability));
mocks.push(vi.spyOn(p, "ability2", "get").mockReturnValue(ability2));
mocks.push(vi.spyOn(p, "abilityHidden", "get").mockReturnValue(hidden));
mocks.push(vi.spyOn(p, "getPassiveAbility").mockReturnValue(passive));
}
}
/***************************
* Tests for Filters *
***************************/
it("should filter to show only the pokemon with an ability when filtering by ability", async () => {
// await game.importData("test/testUtils/saves/everything.prsv");
const pokedexHandler = await runToOpenPokedex();
// Get name of overgrow
const overgrow = allAbilities[Abilities.OVERGROW].name;
// @ts-expect-error `filterText` is private
pokedexHandler.filterText.setValue(FilterTextRow.ABILITY_1, overgrow);
// filter all species to be the pokemon that have overgrow
const overgrowSpecies = getSpeciesWithAbility(Abilities.OVERGROW);
// @ts-expect-error - `filteredPokemonData` is private
const filteredSpecies = new Set(pokedexHandler.filteredPokemonData.map(pokemon => pokemon.species.speciesId));
expect(filteredSpecies).toEqual(overgrowSpecies);
});
it("should filter to show only pokemon with ability and passive when filtering by 2 abilities", async () => {
// Setup mocks for the ability and passive combinations
const whitelist: Species[] = [];
const blacklist: Species[] = [];
const filter_ab1 = Abilities.OVERGROW;
const filter_ab2 = Abilities.ADAPTABILITY;
const ab1_instance = allAbilities[filter_ab1];
const ab2_instance = allAbilities[filter_ab2];
// Create a species with passive set and each "ability" field
const baseObj = {
ability: Abilities.BALL_FETCH,
ability2: Abilities.NONE,
hidden: Abilities.BLAZE,
passive: Abilities.TORRENT,
};
// Mock pokemon to have the exhaustive combination of the two selected abilities
const attrs: (keyof typeof baseObj)[] = ["ability", "ability2", "hidden", "passive"];
for (const [idx, value] of permutations(attrs, 2).entries()) {
createAbilityMocks(Species.BULBASAUR + idx, {
...baseObj,
[value[0]]: filter_ab1,
[value[1]]: filter_ab2,
});
if (value.includes("passive")) {
whitelist.push(Species.BULBASAUR + idx);
} else {
blacklist.push(Species.BULBASAUR + idx);
}
}
const pokedexHandler = await runToOpenPokedex();
// @ts-expect-error `filterText` is private
pokedexHandler.filterText.setValue(FilterTextRow.ABILITY_1, ab1_instance.name);
// @ts-expect-error `filterText` is private
pokedexHandler.filterText.setValue(FilterTextRow.ABILITY_2, ab2_instance.name);
let whiteListCount = 0;
// @ts-expect-error `filteredPokemonData` is private
for (const species of pokedexHandler.filteredPokemonData) {
expect(blacklist, "entry must have one of the abilities as a passive").not.toContain(species.species.speciesId);
const rawAbility = [species.species.ability1, species.species.ability2, species.species.abilityHidden];
const rawPassive = species.species.getPassiveAbility();
const c1 = rawPassive === ab1_instance.id && rawAbility.includes(ab2_instance.id);
const c2 = c1 || (rawPassive === ab2_instance.id && rawAbility.includes(ab1_instance.id));
expect(c2, "each filtered entry should have the ability and passive combination").toBe(true);
if (whitelist.includes(species.species.speciesId)) {
whiteListCount++;
}
}
expect(whiteListCount).toBe(whitelist.length);
});
it("should filter to show only the pokemon with a type when filtering by a single type", async () => {
const pokedexHandler = await runToOpenPokedex();
// @ts-expect-error - `filterBar` is private
pokedexHandler.filterBar.getFilter(DropDownColumn.TYPES).toggleOptionState(PokemonType.NORMAL + 1);
const expectedPokemon = getSpeciesWithType(PokemonType.NORMAL);
// @ts-expect-error - `filteredPokemonData` is private
const filteredPokemon = new Set(pokedexHandler.filteredPokemonData.map(pokemon => pokemon.species.speciesId));
expect(filteredPokemon).toEqual(expectedPokemon);
});
// Todo: Pokemon with a mega that adds a type do not show up in the filter, e.g. pinsir.
it.todo("should show only the pokemon with one of the types when filtering by multiple types", async () => {
const pokedexHandler = await runToOpenPokedex();
// @ts-expect-error - `filterBar` is private
pokedexHandler.filterBar.getFilter(DropDownColumn.TYPES).toggleOptionState(PokemonType.NORMAL + 1);
// @ts-expect-error - `filterBar` is private
pokedexHandler.filterBar.getFilter(DropDownColumn.TYPES).toggleOptionState(PokemonType.FLYING + 1);
const expectedPokemon = getSpeciesWithType(PokemonType.NORMAL, PokemonType.FLYING);
// @ts-expect-error - `filteredPokemonData` is private
const filteredPokemon = new Set(pokedexHandler.filteredPokemonData.map(pokemon => pokemon.species.speciesId));
expect(filteredPokemon).toEqual(expectedPokemon);
});
/****************************
* Tests for UI Input *
****************************/
// TODO: fix cursor wrapping
it.todo(
"should wrap the cursor to the top when moving to an empty entry when there are more than 81 pokemon",
async () => {
const pokedexHandler = await runToOpenPokedex();
// Filter by gen 2 so we can pan a specific amount.
// @ts-expect-error `filterBar` is private
pokedexHandler.filterBar.getFilter(DropDownColumn.GEN).options[2].toggleOptionState();
pokedexHandler.updateStarters();
// @ts-expect-error - `filteredPokemonData` is private
expect(pokedexHandler.filteredPokemonData.length, "pokemon in gen2").toBe(100);
// Let's try to pan to the right to see what the pokemon it points to is.
// pan to the right once and down 11 times
pokedexHandler.processInput(Button.RIGHT);
// Nab the pokemon that is selected for comparison later.
// @ts-expect-error - `lastSpecies` is private
const selectedPokemon = pokedexHandler.lastSpecies.speciesId;
for (let i = 0; i < 11; i++) {
pokedexHandler.processInput(Button.DOWN);
}
// @ts-expect-error `lastSpecies` is private
expect(selectedPokemon).toEqual(pokedexHandler.lastSpecies.speciesId);
},
);
});