From b072f91649ab2a3dcac5ff4aaf4e3e3c98e6ccfb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?In=C3=AAs=20Antunes?= Date: Sun, 11 May 2025 21:47:55 +0100 Subject: [PATCH] =?UTF-8?q?[BUG]=20Fixes=20#5288=20Clowning=20Around=20doe?= =?UTF-8?q?sn=E2=80=99t=20check=20if=20Pokemon=20already=20=20has=20the=20?= =?UTF-8?q?ability?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When offering the same ability a Pokemon already has, a menu should now appear informing the user. The user can then choose a different Pokemon or proceed to give the same ability anyway. --- .../encounters/clowning-around-encounter.ts | 42 ++++++++++++++- .../utils/encounter-pokemon-utils.ts | 24 ++++++--- .../clowning-around-encounter.test.ts | 54 +++++++++++++++++++ 3 files changed, 110 insertions(+), 10 deletions(-) diff --git a/src/data/mystery-encounters/encounters/clowning-around-encounter.ts b/src/data/mystery-encounters/encounters/clowning-around-encounter.ts index ce5eb2cfdd1..46f39610ee7 100644 --- a/src/data/mystery-encounters/encounters/clowning-around-encounter.ts +++ b/src/data/mystery-encounters/encounters/clowning-around-encounter.ts @@ -467,14 +467,52 @@ function displayYesNoOptions(resolve) { globalScene.ui.setModeWithoutClear(UiMode.OPTION_SELECT, config, null, true); } +function handleRepeatedAbility(resolve, pokemon: PlayerPokemon, ability: Abilities) { + showEncounterText("Your pokemon already has this ability. Are you sure you want to apply it?"); + const fullOptions = [ + { + label: i18next.t("menu:yes"), + handler: () => { + applyAbilityOverrideToPokemon(pokemon, ability, true); + globalScene.ui.setMode(UiMode.MESSAGE).then(() => resolve(true)); + return true; + }, + }, + { + label: i18next.t("menu:no"), + handler: () => { + onYesAbilitySwap(resolve); + return true; + }, + }, + ]; + + const config: OptionSelectConfig = { + options: fullOptions, + maxOptions: 7, + yOffset: 0, + }; + globalScene.ui.setModeWithoutClear(UiMode.OPTION_SELECT, config, null, true); +} + function onYesAbilitySwap(resolve) { const onPokemonSelected = (pokemon: PlayerPokemon) => { // Do ability swap const encounter = globalScene.currentBattle.mysteryEncounter!; - applyAbilityOverrideToPokemon(pokemon, encounter.misc.ability); + // Choose a random ability, to give a pokemon on the end of a battle + const randomAbility = encounter.misc.ability; + const newAbility = applyAbilityOverrideToPokemon(pokemon, randomAbility, false); encounter.setDialogueToken("chosenPokemon", pokemon.getNameToRender()); - globalScene.ui.setMode(UiMode.MESSAGE).then(() => resolve(true)); + + globalScene.ui.setMode(UiMode.MESSAGE).then(() => { + // if Pokemon already has the same Ability + if (!newAbility) { + handleRepeatedAbility(resolve, pokemon, randomAbility); + } else { + resolve(true); + } + }); }; const onPokemonNotSelected = () => { diff --git a/src/data/mystery-encounters/utils/encounter-pokemon-utils.ts b/src/data/mystery-encounters/utils/encounter-pokemon-utils.ts index a6a87b4ab9a..0d0e5c9863a 100644 --- a/src/data/mystery-encounters/utils/encounter-pokemon-utils.ts +++ b/src/data/mystery-encounters/utils/encounter-pokemon-utils.ts @@ -1023,14 +1023,22 @@ export function isPokemonValidForEncounterOptionSelection( /** * Permanently overrides the ability (not passive) of a pokemon. * If the pokemon is a fusion, instead overrides the fused pokemon's ability. + * @param ability + * @param pokemon + * @param flagAcceptAbility */ -export function applyAbilityOverrideToPokemon(pokemon: Pokemon, ability: Abilities) { - if (pokemon.isFusion()) { - if (!pokemon.fusionCustomPokemonData) { - pokemon.fusionCustomPokemonData = new CustomPokemonData(); - } - pokemon.fusionCustomPokemonData.ability = ability; - } else { - pokemon.customPokemonData.ability = ability; +export function applyAbilityOverrideToPokemon(pokemon: Pokemon, ability: Abilities, flagAcceptAbility: boolean) { + const isFusion = pokemon.isFusion(); + const data = isFusion + ? (pokemon.fusionCustomPokemonData ??= new CustomPokemonData()) + : (pokemon.customPokemonData ??= new CustomPokemonData()); + + const shouldOverride = data.ability !== ability || flagAcceptAbility; + + if (shouldOverride) { + data.ability = ability; + return true; } + + return false; } diff --git a/test/mystery-encounter/encounters/clowning-around-encounter.test.ts b/test/mystery-encounter/encounters/clowning-around-encounter.test.ts index afc4a83e9bf..661cb8d38e5 100644 --- a/test/mystery-encounter/encounters/clowning-around-encounter.test.ts +++ b/test/mystery-encounter/encounters/clowning-around-encounter.test.ts @@ -37,6 +37,7 @@ import { CommandPhase } from "#app/phases/command-phase"; import { MovePhase } from "#app/phases/move-phase"; import { SelectModifierPhase } from "#app/phases/select-modifier-phase"; import { NewBattlePhase } from "#app/phases/new-battle-phase"; +import { allAbilities } from "#app/data/data-lists"; const namespace = "mysteryEncounters/clowningAround"; const defaultParty = [Species.LAPRAS, Species.GENGAR, Species.ABRA]; @@ -234,6 +235,59 @@ describe("Clowning Around - Mystery Encounter", () => { const leadPokemon = scene.getPlayerParty()[0]; expect(leadPokemon.customPokemonData?.ability).toBe(abilityToTrain); }); + + it("should let the player know their pokemon already has the ability and accept it", async () => { + await game.runToMysteryEncounter(MysteryEncounterType.CLOWNING_AROUND, defaultParty); + await runMysteryEncounterToEnd(game, 1, undefined, true); + await skipBattleRunMysteryEncounterRewardsPhase(game); + await game.phaseInterceptor.to(SelectModifierPhase, false); + expect(scene.getCurrentPhase()?.constructor.name).toBe(SelectModifierPhase.name); + await game.phaseInterceptor.run(SelectModifierPhase); + + const leadPokemon = scene.getPlayerParty()[0]; + // offer Prankster abiliy for winning the battle + const abilityToTrain = Abilities.PRANKSTER; + + game.onNextPrompt("PostMysteryEncounterPhase", UiMode.MESSAGE, () => { + game.scene.ui.getHandler().processInput(Button.ACTION); + }); + + // Give fto the first Pokemon the Prankster ability + vi.spyOn(leadPokemon, "getAbility").mockReturnValue(allAbilities[Abilities.PRANKSTER]); + + // Run to ability train option selection + const optionSelectUiHandler = game.scene.ui.handlers[UiMode.OPTION_SELECT] as OptionSelectUiHandler; + vi.spyOn(optionSelectUiHandler, "show"); + const partyUiHandler = game.scene.ui.handlers[UiMode.PARTY] as PartyUiHandler; + vi.spyOn(partyUiHandler, "show"); + const optionSelectUiHandlerRepeatedAbility = game.scene.ui.handlers[ + UiMode.OPTION_SELECT + ] as OptionSelectUiHandler; + vi.spyOn(optionSelectUiHandlerRepeatedAbility, "show"); + game.endPhase(); + await game.phaseInterceptor.to(PostMysteryEncounterPhase); + expect(scene.getCurrentPhase()?.constructor.name).toBe(PostMysteryEncounterPhase.name); + + // Wait for Yes/No confirmation to appear -> accepting the Ability + await vi.waitFor(() => expect(optionSelectUiHandler.show).toHaveBeenCalled()); + // Select "Yes" on train ability + optionSelectUiHandler.processInput(Button.ACTION); + // Select first pokemon in party to train + await vi.waitFor(() => expect(partyUiHandler.show).toHaveBeenCalled()); + partyUiHandler.processInput(Button.ACTION); + // Click "Select" on Pokemon + partyUiHandler.processInput(Button.ACTION); + // Wait for Yes/No confirmation to appear -> Accepting the Ability, besides being repeated + await vi.waitFor(() => expect(optionSelectUiHandlerRepeatedAbility.show).toHaveBeenCalled()); + // Select "Yes" on train the same ability as before + optionSelectUiHandlerRepeatedAbility.processInput(Button.ACTION); + + // Stop next battle before it runs + await game.phaseInterceptor.to(NewBattlePhase, false); + //game.override.ability(Abilities.PRANKSTER); + + expect(leadPokemon.getAbility().id).toBe(abilityToTrain); + }); }); describe("Option 2 - Remain Unprovoked", () => {