Merge branch 'beta' into fix-level-location

This commit is contained in:
Sirz Benjie 2025-04-25 15:32:01 -05:00 committed by GitHub
commit 192b7135f5
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
27 changed files with 725 additions and 115 deletions

18
package-lock.json generated
View File

@ -18,8 +18,8 @@
"i18next-korean-postposition-processor": "^1.0.0",
"json-stable-stringify": "^1.2.0",
"jszip": "^3.10.1",
"phaser": "^3.70.0",
"phaser3-rex-plugins": "^1.80.14"
"phaser": "^3.88.2",
"phaser3-rex-plugins": "^1.80.15"
},
"devDependencies": {
"@biomejs/biome": "1.9.4",
@ -48,7 +48,7 @@
"vitest-canvas-mock": "^0.3.3"
},
"engines": {
"node": ">=20.0.0"
"node": ">=22.0.0"
}
},
"node_modules/@ampproject/remapping": {
@ -6227,18 +6227,18 @@
}
},
"node_modules/phaser": {
"version": "3.80.1",
"resolved": "https://registry.npmjs.org/phaser/-/phaser-3.80.1.tgz",
"integrity": "sha512-VQGAWoDOkEpAWYkI+PUADv5Ql+SM0xpLuAMBJHz9tBcOLqjJ2wd8bUhxJgOqclQlLTg97NmMd9MhS75w16x1Cw==",
"version": "3.88.2",
"resolved": "https://registry.npmjs.org/phaser/-/phaser-3.88.2.tgz",
"integrity": "sha512-UBgd2sAFuRJbF2xKaQ5jpMWB8oETncChLnymLGHcrnT53vaqiGrQWbUKUDBawKLm24sghjKo4Bf+/xfv8espZQ==",
"license": "MIT",
"dependencies": {
"eventemitter3": "^5.0.1"
}
},
"node_modules/phaser3-rex-plugins": {
"version": "1.80.14",
"resolved": "https://registry.npmjs.org/phaser3-rex-plugins/-/phaser3-rex-plugins-1.80.14.tgz",
"integrity": "sha512-eHi3VgryO9umNu6D1yQU5IS6tH4TyC2Y6RgJ495nNp37X2fdYnmYpBfgFg+YaumvtaoOvCkUVyi/YqWNPf2X2A==",
"version": "1.80.15",
"resolved": "https://registry.npmjs.org/phaser3-rex-plugins/-/phaser3-rex-plugins-1.80.15.tgz",
"integrity": "sha512-Ur973N1W5st6XEYBcJko8eTcEbdDHMM+m7VqvT3j/EJeJwYyJ3bVb33JJDsFgefk3A2iAz2itP/UY7CzxJOJVA==",
"license": "MIT",
"dependencies": {
"dagre": "^0.8.5",

View File

@ -63,8 +63,8 @@
"i18next-korean-postposition-processor": "^1.0.0",
"json-stable-stringify": "^1.2.0",
"jszip": "^3.10.1",
"phaser": "^3.70.0",
"phaser3-rex-plugins": "^1.80.14"
"phaser": "^3.88.2",
"phaser3-rex-plugins": "^1.80.15"
},
"engines": {
"node": ">=22.0.0"

@ -1 +1 @@
Subproject commit e98f0eb9c2022bc78b53f0444424c636498e725a
Subproject commit 18c1963ef309612a5a7fef76f9879709a7202189

View File

@ -151,7 +151,6 @@ import { NextEncounterPhase } from "#app/phases/next-encounter-phase";
import { PokemonAnimPhase } from "#app/phases/pokemon-anim-phase";
import { QuietFormChangePhase } from "#app/phases/quiet-form-change-phase";
import { ReturnPhase } from "#app/phases/return-phase";
import { SelectBiomePhase } from "#app/phases/select-biome-phase";
import { ShowTrainerPhase } from "#app/phases/show-trainer-phase";
import { SummonPhase } from "#app/phases/summon-phase";
import { SwitchPhase } from "#app/phases/switch-phase";
@ -1298,6 +1297,16 @@ export default class BattleScene extends SceneBase {
return Math.max(doubleChance.value, 1);
}
isNewBiome(currentBattle = this.currentBattle) {
const isWaveIndexMultipleOfTen = !(currentBattle.waveIndex % 10);
const isEndlessOrDaily = this.gameMode.hasShortBiomes || this.gameMode.isDaily;
const isEndlessFifthWave = this.gameMode.hasShortBiomes && currentBattle.waveIndex % 5 === 0;
const isWaveIndexMultipleOfFiftyMinusOne = currentBattle.waveIndex % 50 === 49;
const isNewBiome =
isWaveIndexMultipleOfTen || isEndlessFifthWave || (isEndlessOrDaily && isWaveIndexMultipleOfFiftyMinusOne);
return isNewBiome;
}
// TODO: ...this never actually returns `null`, right?
newBattle(
waveIndex?: number,
@ -1385,9 +1394,9 @@ export default class BattleScene extends SceneBase {
if (double === undefined && newWaveIndex > 1) {
if (newBattleType === BattleType.WILD && !this.gameMode.isWaveFinal(newWaveIndex)) {
newDouble = !randSeedInt(this.getDoubleBattleChance(newWaveIndex, playerField));
}
} else if (double === undefined && newBattleType === BattleType.TRAINER) {
} else if (newBattleType === BattleType.TRAINER) {
newDouble = newTrainer?.variant === TrainerVariant.DOUBLE;
}
} else if (!battleConfig) {
newDouble = !!double;
}
@ -1461,12 +1470,7 @@ export default class BattleScene extends SceneBase {
}
if (!waveIndex && lastBattle) {
const isWaveIndexMultipleOfTen = !(lastBattle.waveIndex % 10);
const isEndlessOrDaily = this.gameMode.hasShortBiomes || this.gameMode.isDaily;
const isEndlessFifthWave = this.gameMode.hasShortBiomes && lastBattle.waveIndex % 5 === 0;
const isWaveIndexMultipleOfFiftyMinusOne = lastBattle.waveIndex % 50 === 49;
const isNewBiome =
isWaveIndexMultipleOfTen || isEndlessFifthWave || (isEndlessOrDaily && isWaveIndexMultipleOfFiftyMinusOne);
const isNewBiome = this.isNewBiome(lastBattle);
const resetArenaState =
isNewBiome ||
[BattleType.TRAINER, BattleType.MYSTERY_ENCOUNTER].includes(this.currentBattle.battleType) ||
@ -1515,7 +1519,6 @@ export default class BattleScene extends SceneBase {
if (!this.gameMode.hasRandomBiomes && !isNewBiome) {
this.pushPhase(new NextEncounterPhase());
} else {
this.pushPhase(new SelectBiomePhase());
this.pushPhase(new NewBiomeEncounterPhase());
const newMaxExpLevel = this.getMaxExpLevel();

View File

@ -31,29 +31,7 @@ import type { CustomModifierSettings } from "#app/modifier/modifier-type";
import { ModifierTier } from "#app/modifier/modifier-tier";
import type { MysteryEncounterType } from "#enums/mystery-encounter-type";
import { BattleType } from "#enums/battle-type";
export enum ClassicFixedBossWaves {
TOWN_YOUNGSTER = 5,
RIVAL_1 = 8,
RIVAL_2 = 25,
EVIL_GRUNT_1 = 35,
RIVAL_3 = 55,
EVIL_GRUNT_2 = 62,
EVIL_GRUNT_3 = 64,
EVIL_ADMIN_1 = 66,
RIVAL_4 = 95,
EVIL_GRUNT_4 = 112,
EVIL_ADMIN_2 = 114,
EVIL_BOSS_1 = 115,
RIVAL_5 = 145,
EVIL_BOSS_2 = 165,
ELITE_FOUR_1 = 182,
ELITE_FOUR_2 = 184,
ELITE_FOUR_3 = 186,
ELITE_FOUR_4 = 188,
CHAMPION = 190,
RIVAL_6 = 195,
}
import { ClassicFixedBossWaves } from "#enums/fixed-boss-waves";
export enum BattlerIndex {
ATTACKER = -1,

View File

@ -72,6 +72,7 @@ import type { AbAttrCondition, PokemonDefendCondition, PokemonStatStageChangeCon
import type { BattlerIndex } from "#app/battle";
import type Move from "#app/data/moves/move";
import type { ArenaTrapTag, SuppressAbilitiesTag } from "#app/data/arena-tag";
import { SelectBiomePhase } from "#app/phases/select-biome-phase";
export class BlockRecoilDamageAttr extends AbAttr {
constructor() {
@ -5483,6 +5484,11 @@ class ForceSwitchOutHelper {
if (switchOutTarget.hp) {
globalScene.pushPhase(new BattleEndPhase(false));
if (globalScene.gameMode.hasRandomBiomes || globalScene.isNewBiome()) {
globalScene.pushPhase(new SelectBiomePhase());
}
globalScene.pushPhase(new NewBattlePhase());
}
}
@ -7221,7 +7227,7 @@ export function initAbilities() {
new Ability(Abilities.CURIOUS_MEDICINE, 8)
.attr(PostSummonClearAllyStatStagesAbAttr),
new Ability(Abilities.TRANSISTOR, 8)
.attr(MoveTypePowerBoostAbAttr, PokemonType.ELECTRIC),
.attr(MoveTypePowerBoostAbAttr, PokemonType.ELECTRIC, 1.3),
new Ability(Abilities.DRAGONS_MAW, 8)
.attr(MoveTypePowerBoostAbAttr, PokemonType.DRAGON),
new Ability(Abilities.CHILLING_NEIGH, 8)

View File

@ -8,7 +8,8 @@ import { speciesStarterCosts } from "#app/data/balance/starters";
import type Pokemon from "#app/field/pokemon";
import { PokemonMove } from "#app/field/pokemon";
import type { FixedBattleConfig } from "#app/battle";
import { ClassicFixedBossWaves, getRandomTrainerFunc } from "#app/battle";
import { getRandomTrainerFunc } from "#app/battle";
import { ClassicFixedBossWaves } from "#enums/fixed-boss-waves";
import { BattleType } from "#enums/battle-type";
import Trainer, { TrainerVariant } from "#app/field/trainer";
import { PokemonType } from "#enums/pokemon-type";

View File

@ -123,6 +123,7 @@ import { MoveEffectTrigger } from "#enums/MoveEffectTrigger";
import { MultiHitType } from "#enums/MultiHitType";
import { invalidAssistMoves, invalidCopycatMoves, invalidMetronomeMoves, invalidMirrorMoveMoves, invalidSleepTalkMoves } from "./invalid-moves";
import { TrainerVariant } from "#app/field/trainer";
import { SelectBiomePhase } from "#app/phases/select-biome-phase";
type MoveConditionFunc = (user: Pokemon, target: Pokemon, move: Move) => boolean;
type UserMoveConditionFunc = (user: Pokemon, move: Move) => boolean;
@ -2458,14 +2459,7 @@ export class StatusEffectAttr extends MoveEffectAttr {
const statusCheck = moveChance < 0 || moveChance === 100 || user.randSeedInt(100) < moveChance;
if (statusCheck) {
const pokemon = this.selfTarget ? user : target;
if (pokemon.status && !this.overrideStatus) {
return false;
}
if (user !== target && target.isSafeguarded(user)) {
if (move.category === MoveCategory.STATUS) {
globalScene.queueMessage(i18next.t("moveTriggers:safeguard", { targetName: getPokemonNameWithAffix(target) }));
}
if (user !== target && move.category === MoveCategory.STATUS && !target.canSetStatus(this.effect, false, false, user, true)) {
return false;
}
if (((!pokemon.status || this.overrideStatus) || (pokemon.status.effect === this.effect && moveChance < 0))
@ -6332,6 +6326,11 @@ export class ForceSwitchOutAttr extends MoveEffectAttr {
if (!allyPokemon?.isActive(true) && switchOutTarget.hp) {
globalScene.pushPhase(new BattleEndPhase(false));
if (globalScene.gameMode.hasRandomBiomes || globalScene.isNewBiome()) {
globalScene.pushPhase(new SelectBiomePhase());
}
globalScene.pushPhase(new NewBattlePhase());
}
}

View File

@ -1,6 +1,8 @@
import { startingWave } from "#app/starting-wave";
import { globalScene } from "#app/global-scene";
import { PartyMemberStrength } from "#enums/party-member-strength";
import { GameModes } from "#app/game-mode";
import { ClassicFixedBossWaves } from "#enums/fixed-boss-waves";
export class TrainerPartyTemplate {
public size: number;
@ -222,20 +224,19 @@ export const trainerPartyTemplates = {
*/
export function getEvilGruntPartyTemplate(): TrainerPartyTemplate {
const waveIndex = globalScene.currentBattle?.waveIndex;
if (waveIndex < 40) {
switch (waveIndex) {
case ClassicFixedBossWaves.EVIL_GRUNT_1:
return trainerPartyTemplates.TWO_AVG;
}
if (waveIndex < 63) {
case ClassicFixedBossWaves.EVIL_GRUNT_2:
return trainerPartyTemplates.THREE_AVG;
}
if (waveIndex < 65) {
case ClassicFixedBossWaves.EVIL_GRUNT_3:
return trainerPartyTemplates.TWO_AVG_ONE_STRONG;
}
if (waveIndex < 112) {
case ClassicFixedBossWaves.EVIL_ADMIN_1:
return trainerPartyTemplates.GYM_LEADER_4; // 3avg 1 strong 1 stronger
}
default:
return trainerPartyTemplates.GYM_LEADER_5; // 3 avg 2 strong 1 stronger
}
}
export function getWavePartyTemplate(...templates: TrainerPartyTemplate[]) {
const { currentBattle, gameMode } = globalScene;
@ -245,6 +246,30 @@ export function getWavePartyTemplate(...templates: TrainerPartyTemplate[]) {
}
export function getGymLeaderPartyTemplate() {
const { currentBattle, gameMode } = globalScene;
switch (gameMode.modeId) {
case GameModes.DAILY:
if (currentBattle?.waveIndex <= 20) {
return trainerPartyTemplates.GYM_LEADER_2
}
return trainerPartyTemplates.GYM_LEADER_3;
case GameModes.CHALLENGE: // In the future, there may be a ChallengeType to call here. For now, use classic's.
case GameModes.CLASSIC:
if (currentBattle?.waveIndex <= 20) {
return trainerPartyTemplates.GYM_LEADER_1; // 1 avg 1 strong
}
else if (currentBattle?.waveIndex <= 30) {
return trainerPartyTemplates.GYM_LEADER_2; // 1 avg 1 strong 1 stronger
}
else if (currentBattle?.waveIndex <= 60) { // 50 and 60
return trainerPartyTemplates.GYM_LEADER_3; // 2 avg 1 strong 1 stronger
}
else if (currentBattle?.waveIndex <= 90) { // 80 and 90
return trainerPartyTemplates.GYM_LEADER_4; // 3 avg 1 strong 1 stronger
}
// 110+
return trainerPartyTemplates.GYM_LEADER_5; // 3 avg 2 strong 1 stronger
default:
return getWavePartyTemplate(
trainerPartyTemplates.GYM_LEADER_1,
trainerPartyTemplates.GYM_LEADER_2,
@ -253,3 +278,4 @@ export function getGymLeaderPartyTemplate() {
trainerPartyTemplates.GYM_LEADER_5,
);
}
}

View File

@ -0,0 +1,22 @@
export enum ClassicFixedBossWaves {
TOWN_YOUNGSTER = 5,
RIVAL_1 = 8,
RIVAL_2 = 25,
EVIL_GRUNT_1 = 35,
RIVAL_3 = 55,
EVIL_GRUNT_2 = 62,
EVIL_GRUNT_3 = 64,
EVIL_ADMIN_1 = 66,
RIVAL_4 = 95,
EVIL_GRUNT_4 = 112,
EVIL_ADMIN_2 = 114,
EVIL_BOSS_1 = 115,
RIVAL_5 = 145,
EVIL_BOSS_2 = 165,
ELITE_FOUR_1 = 182,
ELITE_FOUR_2 = 184,
ELITE_FOUR_3 = 186,
ELITE_FOUR_4 = 188,
CHAMPION = 190,
RIVAL_6 = 195
}

View File

@ -248,6 +248,7 @@ import { PLAYER_PARTY_MAX_SIZE } from "#app/constants";
import { CustomPokemonData } from "#app/data/custom-pokemon-data";
import { SwitchType } from "#enums/switch-type";
import { SpeciesFormKey } from "#enums/species-form-key";
import {getStatusEffectOverlapText } from "#app/data/status-effect";
import {
BASE_HIDDEN_ABILITY_CHANCE,
BASE_SHINY_CHANCE,
@ -5364,6 +5365,18 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
);
}
queueImmuneMessage(quiet: boolean, effect?: StatusEffect): void {
if (!effect || quiet) {
return;
}
const message = effect && this.status?.effect === effect
? getStatusEffectOverlapText(effect ?? StatusEffect.NONE, getPokemonNameWithAffix(this))
: i18next.t("abilityTriggers:moveImmunity", {
pokemonNameWithAffix: getPokemonNameWithAffix(this),
});
globalScene.queueMessage(message);
}
/**
* Checks if a status effect can be applied to the Pokemon.
*
@ -5382,6 +5395,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
): boolean {
if (effect !== StatusEffect.FAINT) {
if (overrideStatus ? this.status?.effect === effect : this.status) {
this.queueImmuneMessage(quiet, effect);
return false;
}
if (
@ -5389,18 +5403,11 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
!ignoreField &&
globalScene.arena.terrain?.terrainType === TerrainType.MISTY
) {
this.queueImmuneMessage(quiet, effect);
return false;
}
}
if (
sourcePokemon &&
sourcePokemon !== this &&
this.isSafeguarded(sourcePokemon)
) {
return false;
}
const types = this.getTypes(true, true);
switch (effect) {
@ -5434,12 +5441,14 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
if (this.isOfType(PokemonType.POISON) || this.isOfType(PokemonType.STEEL)) {
if (poisonImmunity.includes(true)) {
this.queueImmuneMessage(quiet, effect);
return false;
}
}
break;
case StatusEffect.PARALYSIS:
if (this.isOfType(PokemonType.ELECTRIC)) {
this.queueImmuneMessage(quiet, effect);
return false;
}
break;
@ -5448,6 +5457,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
this.isGrounded() &&
globalScene.arena.terrain?.terrainType === TerrainType.ELECTRIC
) {
this.queueImmuneMessage(quiet, effect);
return false;
}
break;
@ -5460,11 +5470,13 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
globalScene.arena.weather.weatherType,
))
) {
this.queueImmuneMessage(quiet, effect);
return false;
}
break;
case StatusEffect.BURN:
if (this.isOfType(PokemonType.FIRE)) {
this.queueImmuneMessage(quiet, effect);
return false;
}
break;
@ -5499,6 +5511,19 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
return false;
}
if (
sourcePokemon &&
sourcePokemon !== this &&
this.isSafeguarded(sourcePokemon)
) {
if(!quiet){
globalScene.queueMessage(
i18next.t("moveTriggers:safeguard", { targetName: getPokemonNameWithAffix(this)
}));
}
return false;
}
return true;
}
@ -5510,7 +5535,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
sourceText: string | null = null,
overrideStatus?: boolean
): boolean {
if (!this.canSetStatus(effect, asPhase, overrideStatus, sourcePokemon)) {
if (!this.canSetStatus(effect, false, overrideStatus, sourcePokemon)) {
return false;
}
if (this.isFainted() && effect !== StatusEffect.FAINT) {

View File

@ -93,7 +93,7 @@ const startGame = async (manifest?: any) => {
dom: {
createContainer: true,
},
pixelArt: true,
antialias: false,
pipeline: [InvertPostFX] as unknown as Phaser.Types.Core.PipelineConfig,
scene: [LoadingScene, BattleScene],
version: version,

View File

@ -14,6 +14,7 @@ import { BattleEndPhase } from "./battle-end-phase";
import { NewBattlePhase } from "./new-battle-phase";
import { PokemonPhase } from "./pokemon-phase";
import { globalScene } from "#app/global-scene";
import { SelectBiomePhase } from "./select-biome-phase";
export class AttemptRunPhase extends PokemonPhase {
/** For testing purposes: this is to force the pokemon to fail and escape */
@ -59,6 +60,11 @@ export class AttemptRunPhase extends PokemonPhase {
});
globalScene.pushPhase(new BattleEndPhase(false));
if (globalScene.gameMode.hasRandomBiomes || globalScene.isNewBiome()) {
globalScene.pushPhase(new SelectBiomePhase());
}
globalScene.pushPhase(new NewBattlePhase());
} else {
playerPokemon.turnData.failedRunAway = true;

View File

@ -27,6 +27,7 @@ import { IvScannerModifier } from "../modifier/modifier";
import { Phase } from "../phase";
import { UiMode } from "#enums/ui-mode";
import { isNullOrUndefined, randSeedItem } from "#app/utils/common";
import { SelectBiomePhase } from "./select-biome-phase";
/**
* Will handle (in order):
@ -612,6 +613,10 @@ export class PostMysteryEncounterPhase extends Phase {
*/
continueEncounter() {
const endPhase = () => {
if (globalScene.gameMode.hasRandomBiomes || globalScene.isNewBiome()) {
globalScene.pushPhase(new SelectBiomePhase());
}
globalScene.pushPhase(new NewBattlePhase());
this.end();
};

View File

@ -14,9 +14,10 @@ export class SelectBiomePhase extends BattlePhase {
super.start();
const currentBiome = globalScene.arena.biomeType;
const nextWaveIndex = globalScene.currentBattle.waveIndex + 1;
const setNextBiome = (nextBiome: Biome) => {
if (globalScene.currentBattle.waveIndex % 10 === 1) {
if (nextWaveIndex % 10 === 1) {
globalScene.applyModifiers(MoneyInterestModifier, true);
globalScene.unshiftPhase(new PartyHealPhase(false));
}
@ -25,13 +26,13 @@ export class SelectBiomePhase extends BattlePhase {
};
if (
(globalScene.gameMode.isClassic && globalScene.gameMode.isWaveFinal(globalScene.currentBattle.waveIndex + 9)) ||
(globalScene.gameMode.isDaily && globalScene.gameMode.isWaveFinal(globalScene.currentBattle.waveIndex)) ||
(globalScene.gameMode.hasShortBiomes && !(globalScene.currentBattle.waveIndex % 50))
(globalScene.gameMode.isClassic && globalScene.gameMode.isWaveFinal(nextWaveIndex + 9)) ||
(globalScene.gameMode.isDaily && globalScene.gameMode.isWaveFinal(nextWaveIndex)) ||
(globalScene.gameMode.hasShortBiomes && !(nextWaveIndex % 50))
) {
setNextBiome(Biome.END);
} else if (globalScene.gameMode.hasRandomBiomes) {
setNextBiome(this.generateNextBiome());
setNextBiome(this.generateNextBiome(nextWaveIndex));
} else if (Array.isArray(biomeLinks[currentBiome])) {
const biomes: Biome[] = (biomeLinks[currentBiome] as (Biome | [Biome, number])[])
.filter(b => !Array.isArray(b) || !randSeedInt(b[1]))
@ -59,14 +60,14 @@ export class SelectBiomePhase extends BattlePhase {
} else if (biomeLinks.hasOwnProperty(currentBiome)) {
setNextBiome(biomeLinks[currentBiome] as Biome);
} else {
setNextBiome(this.generateNextBiome());
setNextBiome(this.generateNextBiome(nextWaveIndex));
}
}
generateNextBiome(): Biome {
if (!(globalScene.currentBattle.waveIndex % 50)) {
generateNextBiome(waveIndex: number): Biome {
if (!(waveIndex % 50)) {
return Biome.END;
}
return globalScene.generateRandomBiome(globalScene.currentBattle.waveIndex);
return globalScene.generateRandomBiome(waveIndex);
}
}

View File

@ -19,6 +19,10 @@ export class SwitchBiomePhase extends BattlePhase {
return this.end();
}
// Before switching biomes, make sure to set the last encounter for other phases that need it too.
globalScene.lastEnemyTrainer = globalScene.currentBattle?.trainer ?? null;
globalScene.lastMysteryEncounter = globalScene.currentBattle?.mysteryEncounter;
globalScene.tweens.add({
targets: [globalScene.arenaEnemy, globalScene.lastEnemyTrainer],
x: "+=300",

View File

@ -1,5 +1,5 @@
import type { BattlerIndex } from "#app/battle";
import { ClassicFixedBossWaves } from "#app/battle";
import { ClassicFixedBossWaves } from "#enums/fixed-boss-waves";
import { BattleType } from "#enums/battle-type";
import type { CustomModifierSettings } from "#app/modifier/modifier-type";
import { modifierTypes } from "#app/modifier/modifier-type";
@ -15,6 +15,7 @@ import { TrainerVictoryPhase } from "./trainer-victory-phase";
import { handleMysteryEncounterVictory } from "#app/data/mystery-encounters/utils/encounter-phase-utils";
import { globalScene } from "#app/global-scene";
import { timedEventManager } from "#app/global-event-manager";
import { SelectBiomePhase } from "./select-biome-phase";
export class VictoryPhase extends PokemonPhase {
/** If true, indicates that the phase is intended for EXP purposes only, and not to continue a battle to next phase */
@ -111,6 +112,11 @@ export class VictoryPhase extends PokemonPhase {
globalScene.pushPhase(new AddEnemyBuffModifierPhase());
}
}
if (globalScene.gameMode.hasRandomBiomes || globalScene.isNewBiome()) {
globalScene.pushPhase(new SelectBiomePhase());
}
globalScene.pushPhase(new NewBattlePhase());
} else {
globalScene.currentBattle.battleType = BattleType.CLEAR;

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/data-lists";
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

@ -186,7 +186,7 @@ describe("Abilities - Disguise", () => {
await game.toNextTurn();
game.move.select(Moves.SPLASH);
await game.doKillOpponents();
await game.phaseInterceptor.to("PartyHealPhase");
await game.phaseInterceptor.to("QuietFormChangePhase");
expect(mimikyu1.formIndex).toBe(disguisedForm);
});

View File

@ -163,6 +163,7 @@ describe("Moves - Whirlwind", () => {
it("should not pull in the other trainer's pokemon in a partner trainer battle", async () => {
game.override
.startingWave(2)
.battleType(BattleType.TRAINER)
.randomTrainer({
trainerType: TrainerType.BREEDER,

View File

@ -21,6 +21,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";
// biome-ignore lint/style/noNamespaceImport: Necessary in order to mock the var
import * as bypassLoginModule from "#app/global-vars/bypass-login";
@ -61,6 +63,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

@ -205,6 +205,7 @@ export default class PhaseInterceptor {
private phaseFrom;
private inProgress;
private originalSetMode;
private originalSetOverlayMode;
private originalSuperEnd;
/**
@ -442,6 +443,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);
@ -508,6 +510,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: UiMode, ...args: unknown[]): Promise<void> {
const instance = this.scene.ui;
console.log("setOverlayMode", `${UiMode[mode]} (=${mode})`, args);
const ret = this.originalSetOverlayMode.apply(instance, [mode, ...args]);
return ret;
}
/**
* Method to start the prompt handler.
*/
@ -572,6 +586,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);

File diff suppressed because one or more lines are too long

View File

@ -3,7 +3,7 @@ import { initLoggedInUser } from "#app/account";
import { initAbilities } from "#app/data/abilities/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";
@ -85,7 +85,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;
initI18n();
@ -101,6 +100,8 @@ export function initTestFile() {
initAbilities();
initLoggedInUser();
initMysteryEncounters();
// init the pokemon starters for the pokedex
initPokemonStarters();
}
manageListeners();

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

@ -0,0 +1,492 @@
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/data-lists";
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";
import { UiMode } from "#enums/ui-mode";
/*
Information for the `data_pokedex_tests.psrv`:
Caterpie - Shiny 0
Rattata - Shiny 1
Ekans - Shiny 2
Chikorita has enough candies to unlock passive
Cyndaquil has first cost reduction unlocked, enough candies to buy the second
Totodile has first cost reduction unlocked, not enough candies to buy the second
Treecko has both cost reduction unlocked
Torchic has enough candies to do anything
Mudkip has passive unlocked
Turtwig has enough candies to purchase an egg
*/
/**
* 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(UiMode.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);
});
it("filtering for unlockable cost reduction only shows species with sufficient candies", async () => {
// load the save file
await game.importData("./test/testUtils/saves/data_pokedex_tests.prsv");
const pokedexHandler = await runToOpenPokedex();
// @ts-expect-error - `filterBar` is private
const filter = pokedexHandler.filterBar.getFilter(DropDownColumn.UNLOCKS);
// Cycling 4 times to get to the "can unlock" for cost reduction
for (let i = 0; i < 4; i++) {
// index 1 is the cost reduction
filter.toggleOptionState(1);
}
const expectedPokemon = new Set([
Species.CHIKORITA,
Species.CYNDAQUIL,
Species.TORCHIC,
Species.TURTWIG,
Species.EKANS,
Species.MUDKIP,
]);
expect(
// @ts-expect-error - `filteredPokemonData` is private
pokedexHandler.filteredPokemonData.every(pokemon =>
expectedPokemon.has(pokedexHandler.getStarterSpeciesId(pokemon.species.speciesId)),
),
).toBe(true);
});
it("filtering by passive unlocked only shows species that have their passive", async () => {
await game.importData("./test/testUtils/saves/data_pokedex_tests.prsv");
const pokedexHandler = await runToOpenPokedex();
// @ts-expect-error - `filterBar` is private
const filter = pokedexHandler.filterBar.getFilter(DropDownColumn.UNLOCKS);
filter.toggleOptionState(0); // cycle to Passive: Yes
expect(
// @ts-expect-error - `filteredPokemonData` is private
pokedexHandler.filteredPokemonData.every(
pokemon => pokedexHandler.getStarterSpeciesId(pokemon.species.speciesId) === Species.MUDKIP,
),
).toBe(true);
});
it("filtering for pokemon that can unlock passive shows only species with sufficient candies", async () => {
await game.importData("./test/testUtils/saves/data_pokedex_tests.prsv");
const pokedexHandler = await runToOpenPokedex();
// @ts-expect-error - `filterBar` is private
const filter = pokedexHandler.filterBar.getFilter(DropDownColumn.UNLOCKS);
// Cycling 4 times to get to the "can unlock" for passive
const expectedPokemon = new Set([
Species.EKANS,
Species.CHIKORITA,
Species.CYNDAQUIL,
Species.TORCHIC,
Species.TURTWIG,
]);
// cycling twice to get to the "can unlock" for passive
filter.toggleOptionState(0);
filter.toggleOptionState(0);
expect(
// @ts-expect-error - `filteredPokemonData` is private
pokedexHandler.filteredPokemonData.every(pokemon =>
expectedPokemon.has(pokedexHandler.getStarterSpeciesId(pokemon.species.speciesId)),
),
).toBe(true);
});
it("filtering for pokemon that have any cost reduction shows only the species that have unlocked a cost reduction", async () => {
await game.importData("./test/testUtils/saves/data_pokedex_tests.prsv");
const pokedexHandler = await runToOpenPokedex();
const expectedPokemon = new Set([Species.TREECKO, Species.CYNDAQUIL, Species.TOTODILE]);
// @ts-expect-error - `filterBar` is private
const filter = pokedexHandler.filterBar.getFilter(DropDownColumn.UNLOCKS);
// Cycle 1 time for cost reduction
filter.toggleOptionState(1);
expect(
// @ts-expect-error - `filteredPokemonData` is private
pokedexHandler.filteredPokemonData.every(pokemon =>
expectedPokemon.has(pokedexHandler.getStarterSpeciesId(pokemon.species.speciesId)),
),
).toBe(true);
});
it("filtering for pokemon that have a single cost reduction shows only the species that have unlocked a single cost reduction", async () => {
await game.importData("./test/testUtils/saves/data_pokedex_tests.prsv");
const pokedexHandler = await runToOpenPokedex();
const expectedPokemon = new Set([Species.CYNDAQUIL, Species.TOTODILE]);
// @ts-expect-error - `filterBar` is private
const filter = pokedexHandler.filterBar.getFilter(DropDownColumn.UNLOCKS);
// Cycle 2 times for one cost reduction
filter.toggleOptionState(1);
filter.toggleOptionState(1);
expect(
// @ts-expect-error - `filteredPokemonData` is private
pokedexHandler.filteredPokemonData.every(pokemon =>
expectedPokemon.has(pokedexHandler.getStarterSpeciesId(pokemon.species.speciesId)),
),
).toBe(true);
});
it("filtering for pokemon that have two cost reductions sorts only shows the species that have unlocked both cost reductions", async () => {
await game.importData("./test/testUtils/saves/data_pokedex_tests.prsv");
const pokedexHandler = await runToOpenPokedex();
// @ts-expect-error - `filterBar` is private
const filter = pokedexHandler.filterBar.getFilter(DropDownColumn.UNLOCKS);
// Cycle 3 time for two cost reductions
filter.toggleOptionState(1);
filter.toggleOptionState(1);
filter.toggleOptionState(1);
expect(
// @ts-expect-error - `filteredPokemonData` is private
pokedexHandler.filteredPokemonData.every(
pokemon => pokedexHandler.getStarterSpeciesId(pokemon.species.speciesId) === Species.TREECKO,
),
).toBe(true);
});
it("filtering by shiny status shows the caught pokemon with the selected shiny tier", async () => {
await game.importData("./test/testUtils/saves/data_pokedex_tests.prsv");
const pokedexHandler = await runToOpenPokedex();
// @ts-expect-error - `filterBar` is private
const filter = pokedexHandler.filterBar.getFilter(DropDownColumn.CAUGHT);
filter.toggleOptionState(3);
// @ts-expect-error - `filteredPokemonData` is private
let filteredPokemon = pokedexHandler.filteredPokemonData.map(pokemon => pokemon.species.speciesId);
// Red shiny
expect(filteredPokemon.length).toBe(1);
expect(filteredPokemon[0], "tier 1 shiny").toBe(Species.CATERPIE);
// tier 2 shiny
filter.toggleOptionState(3);
filter.toggleOptionState(2);
// @ts-expect-error - `filteredPokemonData` is private
filteredPokemon = pokedexHandler.filteredPokemonData.map(pokemon => pokemon.species.speciesId);
expect(filteredPokemon.length).toBe(1);
expect(filteredPokemon[0], "tier 2 shiny").toBe(Species.RATTATA);
filter.toggleOptionState(2);
filter.toggleOptionState(1);
// @ts-expect-error - `filteredPokemonData` is private
filteredPokemon = pokedexHandler.filteredPokemonData.map(pokemon => pokemon.species.speciesId);
expect(filteredPokemon.length).toBe(1);
expect(filteredPokemon[0], "tier 3 shiny").toBe(Species.EKANS);
// filter by no shiny
filter.toggleOptionState(1);
filter.toggleOptionState(4);
// @ts-expect-error - `filteredPokemonData` is private
filteredPokemon = pokedexHandler.filteredPokemonData.map(pokemon => pokemon.species.speciesId);
expect(filteredPokemon.length).toBe(27);
expect(filteredPokemon, "not shiny").not.toContain(Species.CATERPIE);
expect(filteredPokemon, "not shiny").not.toContain(Species.RATTATA);
expect(filteredPokemon, "not shiny").not.toContain(Species.EKANS);
});
/****************************
* 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);
},
);
});