diff --git a/src/data/pokemon-species.ts b/src/data/pokemon-species.ts index e0fc8df823c..d011454e03c 100644 --- a/src/data/pokemon-species.ts +++ b/src/data/pokemon-species.ts @@ -147,7 +147,7 @@ export abstract class PokemonSpeciesForm { this.height = height; this.weight = weight; this.ability1 = ability1; - this.ability2 = ability2; + this.ability2 = ability2 === Abilities.NONE ? ability1 : ability2; this.abilityHidden = abilityHidden; this.baseTotal = baseTotal; this.baseStats = [ baseHp, baseAtk, baseDef, baseSpatk, baseSpdef, baseSpd ]; diff --git a/src/evolution-phase.ts b/src/evolution-phase.ts index 28edeee1a77..7633fbb3fdd 100644 --- a/src/evolution-phase.ts +++ b/src/evolution-phase.ts @@ -219,7 +219,7 @@ export class EvolutionPhase extends Phase { this.scene.time.delayedCall(900, () => { evolutionHandler.canCancel = false; - this.pokemon.evolve(this.evolution).then(() => { + this.pokemon.evolve(this.evolution, this.pokemon.species).then(() => { const levelMoves = this.pokemon.getLevelMoves(this.lastLevel + 1, true); for (const lm of levelMoves) { this.scene.unshiftPhase(new LearnMovePhase(this.scene, this.scene.getParty().indexOf(this.pokemon), lm[1])); diff --git a/src/field/pokemon.ts b/src/field/pokemon.ts index f6f71384825..78433ece72b 100644 --- a/src/field/pokemon.ts +++ b/src/field/pokemon.ts @@ -3303,9 +3303,10 @@ export class PlayerPokemon extends Pokemon { }); } - evolve(evolution: SpeciesFormEvolution): Promise { + evolve(evolution: SpeciesFormEvolution, preEvolution: PokemonSpeciesForm): Promise { return new Promise(resolve => { this.pauseEvolutions = false; + // Handles Nincada evolving into Ninjask + Shedinja this.handleSpecialEvolutions(evolution); const isFusion = evolution instanceof FusionSpeciesFormEvolution; if (!isFusion) { @@ -3323,16 +3324,26 @@ export class PlayerPokemon extends Pokemon { } this.generateName(); if (!isFusion) { - // Prevent pokemon with an illegal ability value from breaking things too badly const abilityCount = this.getSpeciesForm().getAbilityCount(); - if (this.abilityIndex >= abilityCount) { + const preEvoAbilityCount = preEvolution.getAbilityCount(); + if ([0, 1, 2].includes(this.abilityIndex)) { + // Handles cases where a Pokemon with 3 abilities evolves into a Pokemon with 2 abilities (ie: Eevee -> any Eeveelution) + if (this.abilityIndex === 2 && preEvoAbilityCount === 3 && abilityCount === 2) { + this.abilityIndex = 1; + } + } else { // Prevent pokemon with an illegal ability value from breaking things console.warn("this.abilityIndex is somehow an illegal value, please report this"); console.warn(this.abilityIndex); this.abilityIndex = 0; } } else { // Do the same as above, but for fusions const abilityCount = this.getFusionSpeciesForm().getAbilityCount(); - if (this.fusionAbilityIndex >= abilityCount) { + const preEvoAbilityCount = preEvolution.getAbilityCount(); + if ([0, 1, 2].includes(this.fusionAbilityIndex)) { + if (this.fusionAbilityIndex === 2 && preEvoAbilityCount === 3 && abilityCount === 2) { + this.fusionAbilityIndex = 1; + } + } else { console.warn("this.fusionAbilityIndex is somehow an illegal value, please report this"); console.warn(this.fusionAbilityIndex); this.fusionAbilityIndex = 0; @@ -3379,7 +3390,7 @@ export class PlayerPokemon extends Pokemon { newPokemon.fusionLuck = this.fusionLuck; this.scene.getParty().push(newPokemon); - newPokemon.evolve(!isFusion ? newEvolution : new FusionSpeciesFormEvolution(this.id, newEvolution)); + newPokemon.evolve((!isFusion ? newEvolution : new FusionSpeciesFormEvolution(this.id, newEvolution)), evoSpecies); const modifiers = this.scene.findModifiers(m => m instanceof PokemonHeldItemModifier && m.pokemonId === this.id, true) as PokemonHeldItemModifier[]; modifiers.forEach(m => { diff --git a/src/test/evolution.test.ts b/src/test/evolution.test.ts new file mode 100644 index 00000000000..945e8363231 --- /dev/null +++ b/src/test/evolution.test.ts @@ -0,0 +1,87 @@ +import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; +import Phaser from "phaser"; +import GameManager from "#app/test/utils/gameManager"; +import { Species } from "#app/enums/species.js"; +import { Abilities } from "#app/enums/abilities.js"; +import Overrides from "#app/overrides"; +import { pokemonEvolutions } from "#app/data/pokemon-evolutions.js"; + +describe("Evolution", () => { + let phaserGame: Phaser.Game; + let game: GameManager; + const TIMEOUT = 1000 * 20; + + beforeAll(() => { + phaserGame = new Phaser.Game({ + type: Phaser.HEADLESS, + }); + }); + + afterEach(() => { + game.phaseInterceptor.restoreOg(); + }); + + beforeEach(() => { + game = new GameManager(phaserGame); + + vi.spyOn(Overrides, "BATTLE_TYPE_OVERRIDE", "get").mockReturnValue("single"); + + vi.spyOn(Overrides, "OPP_SPECIES_OVERRIDE", "get").mockReturnValue(Species.MAGIKARP); + vi.spyOn(Overrides, "OPP_ABILITY_OVERRIDE", "get").mockReturnValue(Abilities.BALL_FETCH); + + vi.spyOn(Overrides, "STARTING_LEVEL_OVERRIDE", "get").mockReturnValue(60); + }); + + it("should keep hidden ability after evolving", async () => { + await game.runToSummon([Species.EEVEE, Species.TRAPINCH]); + + const eevee = game.scene.getParty()[0]; + const trapinch = game.scene.getParty()[1]; + eevee.abilityIndex = 2; + trapinch.abilityIndex = 2; + + eevee.evolve(pokemonEvolutions[Species.EEVEE][6], eevee.getSpeciesForm()); + expect(eevee.abilityIndex).toBe(2); + + trapinch.evolve(pokemonEvolutions[Species.TRAPINCH][0], trapinch.getSpeciesForm()); + expect(trapinch.abilityIndex).toBe(1); + }, TIMEOUT); + + it("should keep same ability slot after evolving", async () => { + await game.runToSummon([Species.BULBASAUR, Species.CHARMANDER]); + + const bulbasaur = game.scene.getParty()[0]; + const charmander = game.scene.getParty()[1]; + bulbasaur.abilityIndex = 0; + charmander.abilityIndex = 1; + + bulbasaur.evolve(pokemonEvolutions[Species.BULBASAUR][0], bulbasaur.getSpeciesForm()); + expect(bulbasaur.abilityIndex).toBe(0); + + charmander.evolve(pokemonEvolutions[Species.CHARMANDER][0], charmander.getSpeciesForm()); + expect(charmander.abilityIndex).toBe(1); + }, TIMEOUT); + + it("should handle illegal abilityIndex values", async () => { + await game.runToSummon([Species.SQUIRTLE]); + + const squirtle = game.scene.getPlayerPokemon(); + squirtle.abilityIndex = 5; + + squirtle.evolve(pokemonEvolutions[Species.SQUIRTLE][0], squirtle.getSpeciesForm()); + expect(squirtle.abilityIndex).toBe(0); + }, TIMEOUT); + + it("should handle nincada's unique evolution", async () => { + await game.runToSummon([Species.NINCADA]); + + const nincada = game.scene.getPlayerPokemon(); + nincada.abilityIndex = 2; + + nincada.evolve(pokemonEvolutions[Species.NINCADA][0], nincada.getSpeciesForm()); + const ninjask = game.scene.getParty()[0]; + const shedinja = game.scene.getParty()[1]; + expect(ninjask.abilityIndex).toBe(2); + expect(shedinja.abilityIndex).toBe(1); + }, TIMEOUT); +}); diff --git a/src/ui/starter-select-ui-handler.ts b/src/ui/starter-select-ui-handler.ts index 174a46745cd..1307f46d28f 100644 --- a/src/ui/starter-select-ui-handler.ts +++ b/src/ui/starter-select-ui-handler.ts @@ -1574,9 +1574,10 @@ export default class StarterSelectUiHandler extends MessageUiHandler { break; } } else if (newAbilityIndex === 1) { - if (abilityAttr & (this.lastSpecies.ability2 ? AbilityAttr.ABILITY_2 : AbilityAttr.ABILITY_HIDDEN)) { - break; + if (this.lastSpecies.ability1 === this.lastSpecies.ability2) { + newAbilityIndex = (newAbilityIndex + 1) % abilityCount; } + break; } else { if (abilityAttr & AbilityAttr.ABILITY_HIDDEN) { break;