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 1/5] =?UTF-8?q?[BUG]=20Fixes=20#5288=20Clowning=20Around?= =?UTF-8?q?=20doesn=E2=80=99t=20check=20if=20Pokemon=20already=20=20has=20?= =?UTF-8?q?the=20ability?= 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", () => { From b82d4af75f8d1075d91b3e63019357f7c2af3b49 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?In=C3=AAs=20Antunes?= Date: Sat, 7 Jun 2025 18:49:27 +0100 Subject: [PATCH 2/5] [Fix]: Updated applyAbilityOverrideToPokemon in other files and 'repeated ability message' with a new i18n key --- .../mystery-encounters/encounters/clowning-around-encounter.ts | 2 +- .../mystery-encounters/encounters/fiery-fallout-encounter.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/data/mystery-encounters/encounters/clowning-around-encounter.ts b/src/data/mystery-encounters/encounters/clowning-around-encounter.ts index 46f39610ee7..88cf6334fcd 100644 --- a/src/data/mystery-encounters/encounters/clowning-around-encounter.ts +++ b/src/data/mystery-encounters/encounters/clowning-around-encounter.ts @@ -468,7 +468,7 @@ function displayYesNoOptions(resolve) { } function handleRepeatedAbility(resolve, pokemon: PlayerPokemon, ability: Abilities) { - showEncounterText("Your pokemon already has this ability. Are you sure you want to apply it?"); + showEncounterText(`${namespace}:option.1.repeated_ability`); const fullOptions = [ { label: i18next.t("menu:yes"), diff --git a/src/data/mystery-encounters/encounters/fiery-fallout-encounter.ts b/src/data/mystery-encounters/encounters/fiery-fallout-encounter.ts index 0364b98abe2..39b6d1d3f9d 100644 --- a/src/data/mystery-encounters/encounters/fiery-fallout-encounter.ts +++ b/src/data/mystery-encounters/encounters/fiery-fallout-encounter.ts @@ -243,7 +243,7 @@ export const FieryFalloutEncounter: MysteryEncounter = MysteryEncounterBuilder.w queueEncounterMessage(`${namespace}:option.2.target_burned`); // Also permanently change the burned Pokemon's ability to Heatproof - applyAbilityOverrideToPokemon(chosenPokemon, Abilities.HEATPROOF); + applyAbilityOverrideToPokemon(chosenPokemon, Abilities.HEATPROOF, false); } } From 04bb1c4c093ab492e7e5b2f3945f2235577040e6 Mon Sep 17 00:00:00 2001 From: NightKev <34855794+DayKev@users.noreply.github.com> Date: Sun, 8 Jun 2025 04:33:10 -0700 Subject: [PATCH 3/5] Fix merge issues --- .../mystery-encounters/encounters/fiery-fallout-encounter.ts | 2 +- .../encounters/clowning-around-encounter.test.ts | 5 ++--- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/src/data/mystery-encounters/encounters/fiery-fallout-encounter.ts b/src/data/mystery-encounters/encounters/fiery-fallout-encounter.ts index b5e1da2b2ed..0a57805ef94 100644 --- a/src/data/mystery-encounters/encounters/fiery-fallout-encounter.ts +++ b/src/data/mystery-encounters/encounters/fiery-fallout-encounter.ts @@ -243,7 +243,7 @@ export const FieryFalloutEncounter: MysteryEncounter = MysteryEncounterBuilder.w queueEncounterMessage(`${namespace}:option.2.target_burned`); // Also permanently change the burned Pokemon's ability to Heatproof - applyAbilityOverrideToPokemon(chosenPokemon, Abilities.HEATPROOF, false); + applyAbilityOverrideToPokemon(chosenPokemon, AbilityId.HEATPROOF, false); } } diff --git a/test/mystery-encounter/encounters/clowning-around-encounter.test.ts b/test/mystery-encounter/encounters/clowning-around-encounter.test.ts index c834a972714..845ebe4dc91 100644 --- a/test/mystery-encounter/encounters/clowning-around-encounter.test.ts +++ b/test/mystery-encounter/encounters/clowning-around-encounter.test.ts @@ -246,14 +246,14 @@ describe("Clowning Around - Mystery Encounter", () => { const leadPokemon = scene.getPlayerParty()[0]; // offer Prankster abiliy for winning the battle - const abilityToTrain = Abilities.PRANKSTER; + const abilityToTrain = AbilityId.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]); + vi.spyOn(leadPokemon, "getAbility").mockReturnValue(allAbilities[AbilityId.PRANKSTER]); // Run to ability train option selection const optionSelectUiHandler = game.scene.ui.handlers[UiMode.OPTION_SELECT] as OptionSelectUiHandler; @@ -284,7 +284,6 @@ describe("Clowning Around - Mystery Encounter", () => { // Stop next battle before it runs await game.phaseInterceptor.to(NewBattlePhase, false); - //game.override.ability(Abilities.PRANKSTER); expect(leadPokemon.getAbility().id).toBe(abilityToTrain); }); From 07e14ac67cfbf0b44374014827755be9d2875768 Mon Sep 17 00:00:00 2001 From: NightKev <34855794+DayKev@users.noreply.github.com> Date: Sun, 8 Jun 2025 04:38:15 -0700 Subject: [PATCH 4/5] Fix missed merge conflicts --- .../mystery-encounters/encounters/clowning-around-encounter.ts | 2 +- src/data/mystery-encounters/utils/encounter-pokemon-utils.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/data/mystery-encounters/encounters/clowning-around-encounter.ts b/src/data/mystery-encounters/encounters/clowning-around-encounter.ts index 3096b50afc8..701accd9198 100644 --- a/src/data/mystery-encounters/encounters/clowning-around-encounter.ts +++ b/src/data/mystery-encounters/encounters/clowning-around-encounter.ts @@ -467,7 +467,7 @@ function displayYesNoOptions(resolve) { globalScene.ui.setModeWithoutClear(UiMode.OPTION_SELECT, config, null, true); } -function handleRepeatedAbility(resolve, pokemon: PlayerPokemon, ability: Abilities) { +function handleRepeatedAbility(resolve, pokemon: PlayerPokemon, ability: AbilityId) { showEncounterText(`${namespace}:option.1.repeated_ability`); const fullOptions = [ { diff --git a/src/data/mystery-encounters/utils/encounter-pokemon-utils.ts b/src/data/mystery-encounters/utils/encounter-pokemon-utils.ts index 51ffb0c0adc..a08b09a81fc 100644 --- a/src/data/mystery-encounters/utils/encounter-pokemon-utils.ts +++ b/src/data/mystery-encounters/utils/encounter-pokemon-utils.ts @@ -1027,7 +1027,7 @@ export function isPokemonValidForEncounterOptionSelection( * @param flagAcceptAbility */ -export function applyAbilityOverrideToPokemon(pokemon: Pokemon, ability: Abilities, flagAcceptAbility: boolean) { +export function applyAbilityOverrideToPokemon(pokemon: Pokemon, ability: AbilityId, flagAcceptAbility: boolean) { const isFusion = pokemon.isFusion(); const data = isFusion ? (pokemon.fusionCustomPokemonData ??= new CustomPokemonData()) From cbeea8221941052bab1c726914c458252d63d3b9 Mon Sep 17 00:00:00 2001 From: NightKev <34855794+DayKev@users.noreply.github.com> Date: Sun, 8 Jun 2025 04:44:21 -0700 Subject: [PATCH 5/5] Fix remaining merge issues --- .../encounters/clowning-around-encounter.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/mystery-encounter/encounters/clowning-around-encounter.test.ts b/test/mystery-encounter/encounters/clowning-around-encounter.test.ts index eda3f9235ce..4a28c2461a7 100644 --- a/test/mystery-encounter/encounters/clowning-around-encounter.test.ts +++ b/test/mystery-encounter/encounters/clowning-around-encounter.test.ts @@ -241,7 +241,7 @@ describe("Clowning Around - Mystery Encounter", () => { await runMysteryEncounterToEnd(game, 1, undefined, true); await skipBattleRunMysteryEncounterRewardsPhase(game); await game.phaseInterceptor.to(SelectModifierPhase, false); - expect(scene.getCurrentPhase()?.constructor.name).toBe(SelectModifierPhase.name); + expect(scene.phaseManager.getCurrentPhase()?.constructor.name).toBe(SelectModifierPhase.name); await game.phaseInterceptor.run(SelectModifierPhase); const leadPokemon = scene.getPlayerParty()[0]; @@ -266,7 +266,7 @@ describe("Clowning Around - Mystery Encounter", () => { vi.spyOn(optionSelectUiHandlerRepeatedAbility, "show"); game.endPhase(); await game.phaseInterceptor.to(PostMysteryEncounterPhase); - expect(scene.getCurrentPhase()?.constructor.name).toBe(PostMysteryEncounterPhase.name); + expect(scene.phaseManager.getCurrentPhase()?.constructor.name).toBe(PostMysteryEncounterPhase.name); // Wait for Yes/No confirmation to appear -> accepting the Ability await vi.waitFor(() => expect(optionSelectUiHandler.show).toHaveBeenCalled());