From 0faccad49e8a5118a5273004cfb250d9bae9b566 Mon Sep 17 00:00:00 2001 From: Greenlamp2 <44787002+Greenlamp2@users.noreply.github.com> Date: Tue, 11 Jun 2024 16:12:46 +0200 Subject: [PATCH] [Bug Fix] Corrected PostSummonPhase Trigger Timing for Accurate Stat Changes (#2057) * fix postSummonPhase when opponent does not summon and fix the statsChange from push to unshift * added switch pokemon helper method * added on switch tests for intimidate * fix test by overriding passive in tests * remove a test not needed * cleanup imports --- src/data/ability.ts | 4 +- src/overrides.ts | 2 +- src/phases.ts | 8 +- src/test/abilities/intimidate.test.ts | 182 +++++++++++++++++----- src/test/abilities/intrepid_sword.test.ts | 28 +--- src/test/utils/TextInterceptor.ts | 2 +- src/test/utils/gameManager.ts | 12 ++ 7 files changed, 171 insertions(+), 67 deletions(-) diff --git a/src/data/ability.ts b/src/data/ability.ts index d17bbc519da..5db2e34b2a0 100755 --- a/src/data/ability.ts +++ b/src/data/ability.ts @@ -1625,7 +1625,9 @@ export class PostSummonStatChangeAbAttr extends PostSummonAbAttr { applyPostSummon(pokemon: Pokemon, passive: boolean, args: any[]): boolean { queueShowAbility(pokemon, passive); // TODO: Better solution than manually showing the ability here if (this.selfTarget) { - pokemon.scene.pushPhase(new StatChangePhase(pokemon.scene, pokemon.getBattlerIndex(), true, this.stats, this.levels)); + // we unshift the StatChangePhase to put it right after the showAbility and not at the end of the + // phase list (which could be after CommandPhase for example) + pokemon.scene.unshiftPhase(new StatChangePhase(pokemon.scene, pokemon.getBattlerIndex(), true, this.stats, this.levels)); return true; } for (const opponent of pokemon.getOpponents()) { diff --git a/src/overrides.ts b/src/overrides.ts index f8e3152de98..782cc6552cd 100644 --- a/src/overrides.ts +++ b/src/overrides.ts @@ -79,7 +79,7 @@ export const VARIANT_OVERRIDE: Variant = 0; export const OPP_SPECIES_OVERRIDE: Species | integer = 0; export const OPP_LEVEL_OVERRIDE: number = 0; export const OPP_ABILITY_OVERRIDE: Abilities = Abilities.NONE; -export const OPP_PASSIVE_ABILITY_OVERRIDE = Abilities.NONE; +export const OPP_PASSIVE_ABILITY_OVERRIDE: Abilities = Abilities.NONE; export const OPP_STATUS_OVERRIDE: StatusEffect = StatusEffect.NONE; export const OPP_GENDER_OVERRIDE: Gender = null; export const OPP_MOVESET_OVERRIDE: Array = []; diff --git a/src/phases.ts b/src/phases.ts index 7d60fa67e14..56de1838594 100644 --- a/src/phases.ts +++ b/src/phases.ts @@ -1024,7 +1024,6 @@ export class EncounterPhase extends BattlePhase { }); if (this.scene.currentBattle.battleType !== BattleType.TRAINER) { - enemyField.map(p => this.scene.pushPhase(new PostSummonPhase(this.scene, p.getBattlerIndex()))); const ivScannerModifier = this.scene.findModifier(m => m instanceof IvScannerModifier); if (ivScannerModifier) { enemyField.map(p => this.scene.pushPhase(new ScanIvsPhase(this.scene, p.getBattlerIndex(), Math.min(ivScannerModifier.getStackCount() * 2, 6)))); @@ -1480,7 +1479,10 @@ export class SummonPhase extends PartyMemberPokemonPhase { if (!this.loaded || this.scene.currentBattle.battleType === BattleType.TRAINER || (this.scene.currentBattle.waveIndex % 10) === 1) { this.scene.triggerPokemonFormChange(pokemon, SpeciesFormChangeActiveTrigger, true); - + } + if (pokemon.isPlayer()) { + // postSummon for player only here, since we want the postSummon from opponent to be call in the turnInitPhase + // covering both wild & trainer battles this.queuePostSummon(); } } @@ -1795,6 +1797,8 @@ export class TurnInitPhase extends FieldPhase { start() { super.start(); + const enemyField = this.scene.getEnemyField().filter(p => p.isActive()) as Pokemon[]; + enemyField.map(p => this.scene.unshiftPhase(new PostSummonPhase(this.scene, p.getBattlerIndex()))); this.scene.getPlayerField().forEach(p => { // If this pokemon is in play and evolved into something illegal under the current challenge, force a switch diff --git a/src/test/abilities/intimidate.test.ts b/src/test/abilities/intimidate.test.ts index b8a1d65088f..3182a9c04c6 100644 --- a/src/test/abilities/intimidate.test.ts +++ b/src/test/abilities/intimidate.test.ts @@ -5,16 +5,11 @@ import * as overrides from "#app/overrides"; import {Abilities} from "#app/data/enums/abilities"; import {Species} from "#app/data/enums/species"; import { - CheckSwitchPhase, CommandPhase, MessagePhase, - PostSummonPhase, - ShinySparklePhase, - ShowAbilityPhase, - StatChangePhase, - SummonPhase, - ToggleDoublePositionPhase, TurnInitPhase + CommandPhase, TurnInitPhase } from "#app/phases"; import {Mode} from "#app/ui/ui"; import {BattleStat} from "#app/data/battle-stat"; +import {Moves} from "#app/data/enums/moves"; describe("Abilities - Intimidate", () => { @@ -34,50 +29,161 @@ describe("Abilities - Intimidate", () => { beforeEach(() => { game = new GameManager(phaserGame); vi.spyOn(overrides, "SINGLE_BATTLE_OVERRIDE", "get").mockReturnValue(true); - vi.spyOn(overrides, "OPP_SPECIES_OVERRIDE", "get").mockReturnValue(Species.MIGHTYENA); + vi.spyOn(overrides, "OPP_SPECIES_OVERRIDE", "get").mockReturnValue(Species.RATTATA); vi.spyOn(overrides, "OPP_ABILITY_OVERRIDE", "get").mockReturnValue(Abilities.INTIMIDATE); + vi.spyOn(overrides, "OPP_PASSIVE_ABILITY_OVERRIDE", "get").mockReturnValue(Abilities.HYDRATION); vi.spyOn(overrides, "ABILITY_OVERRIDE", "get").mockReturnValue(Abilities.INTIMIDATE); + vi.spyOn(overrides, "STARTING_WAVE_OVERRIDE", "get").mockReturnValue(3); + vi.spyOn(overrides, "OPP_MOVESET_OVERRIDE", "get").mockReturnValue([Moves.SPLASH,Moves.SPLASH,Moves.SPLASH,Moves.SPLASH]); }); - it("INTIMIDATE", async() => { + it("single - wild with switch", async() => { await game.runToSummon([ Species.MIGHTYENA, - Species.MIGHTYENA, + Species.POOCHYENA, ]); - await game.phaseInterceptor.run(PostSummonPhase); - - - expect(game.scene.getParty()[0].summonData).not.toBeUndefined(); - let battleStatsPokemon = game.scene.getParty()[0].summonData.battleStats; - expect(battleStatsPokemon[BattleStat.ATK]).toBe(0); - await game.phaseInterceptor.run(ShowAbilityPhase); - await game.phaseInterceptor.run(StatChangePhase); - battleStatsPokemon = game.scene.getParty()[0].summonData.battleStats; - expect(battleStatsPokemon[BattleStat.ATK]).toBe(-1); - - - await game.phaseInterceptor.run(SummonPhase); - await game.phaseInterceptor.run(ShinySparklePhase, () => game.isCurrentPhase(ToggleDoublePositionPhase)); - await game.phaseInterceptor.run(ToggleDoublePositionPhase); game.onNextPrompt("CheckSwitchPhase", Mode.CONFIRM, () => { game.setMode(Mode.MESSAGE); game.endPhase(); - }); - await game.phaseInterceptor.run(CheckSwitchPhase); - await game.phaseInterceptor.run(PostSummonPhase); - - + }, () => game.isCurrentPhase(CommandPhase) || game.isCurrentPhase(TurnInitPhase)); + await game.phaseInterceptor.to(CommandPhase, false); + expect(game.scene.getParty()[0].species.speciesId).toBe(Species.MIGHTYENA); let battleStatsOpponent = game.scene.currentBattle.enemyParty[0].summonData.battleStats; - expect(battleStatsOpponent[BattleStat.ATK]).toBe(0); - await game.phaseInterceptor.run(ShowAbilityPhase); - game.scene.moveAnimations = null; // Mandatory to avoid crash - await game.phaseInterceptor.run(StatChangePhase); - battleStatsOpponent = game.scene.currentBattle.enemyParty[0].summonData.battleStats; expect(battleStatsOpponent[BattleStat.ATK]).toBe(-1); + let battleStatsPokemon = game.scene.getParty()[0].summonData.battleStats; + expect(battleStatsPokemon[BattleStat.ATK]).toBe(-1); + await game.switchPokemon(1); + expect(game.scene.getParty()[0].species.speciesId).toBe(Species.POOCHYENA); + battleStatsPokemon = game.scene.getParty()[0].summonData.battleStats; + expect(battleStatsPokemon[BattleStat.ATK]).toBe(-1); - await game.phaseInterceptor.run(MessagePhase); - await game.phaseInterceptor.run(TurnInitPhase); - await game.phaseInterceptor.run(CommandPhase); + battleStatsOpponent = game.scene.currentBattle.enemyParty[0].summonData.battleStats; + expect(battleStatsOpponent[BattleStat.ATK]).toBe(-2); + }, 20000); + + it("single - boss should only trigger once then switch", async() => { + vi.spyOn(overrides, "STARTING_WAVE_OVERRIDE", "get").mockReturnValue(10); + await game.runToSummon([ + Species.MIGHTYENA, + Species.POOCHYENA, + ]); + game.onNextPrompt("CheckSwitchPhase", Mode.CONFIRM, () => { + game.setMode(Mode.MESSAGE); + game.endPhase(); + }, () => game.isCurrentPhase(CommandPhase) || game.isCurrentPhase(TurnInitPhase)); + await game.phaseInterceptor.to(CommandPhase, false); + let battleStatsOpponent = game.scene.currentBattle.enemyParty[0].summonData.battleStats; + expect(battleStatsOpponent[BattleStat.ATK]).toBe(-1); + let battleStatsPokemon = game.scene.getParty()[0].summonData.battleStats; + expect(battleStatsPokemon[BattleStat.ATK]).toBe(-1); + await game.switchPokemon(1); + expect(game.scene.getParty()[0].species.speciesId).toBe(Species.POOCHYENA); + + battleStatsPokemon = game.scene.getParty()[0].summonData.battleStats; + expect(battleStatsPokemon[BattleStat.ATK]).toBe(-1); + + battleStatsOpponent = game.scene.currentBattle.enemyParty[0].summonData.battleStats; + expect(battleStatsOpponent[BattleStat.ATK]).toBe(-2); + }, 20000); + + it("single - trainer should only trigger once with switch", async() => { + vi.spyOn(overrides, "STARTING_WAVE_OVERRIDE", "get").mockReturnValue(5); + await game.runToSummon([ + Species.MIGHTYENA, + Species.POOCHYENA, + ]); + game.onNextPrompt("CheckSwitchPhase", Mode.CONFIRM, () => { + game.setMode(Mode.MESSAGE); + game.endPhase(); + }, () => game.isCurrentPhase(CommandPhase) || game.isCurrentPhase(TurnInitPhase)); + await game.phaseInterceptor.to(CommandPhase, false); + let battleStatsOpponent = game.scene.currentBattle.enemyParty[0].summonData.battleStats; + expect(battleStatsOpponent[BattleStat.ATK]).toBe(-1); + let battleStatsPokemon = game.scene.getParty()[0].summonData.battleStats; + expect(battleStatsPokemon[BattleStat.ATK]).toBe(-1); + await game.switchPokemon(1); + expect(game.scene.getParty()[0].species.speciesId).toBe(Species.POOCHYENA); + + battleStatsPokemon = game.scene.getParty()[0].summonData.battleStats; + expect(battleStatsPokemon[BattleStat.ATK]).toBe(-1); + + battleStatsOpponent = game.scene.currentBattle.enemyParty[0].summonData.battleStats; + expect(battleStatsOpponent[BattleStat.ATK]).toBe(-2); + }, 200000); + + it("double - trainer should only trigger once per pokemon", async() => { + vi.spyOn(overrides, "SINGLE_BATTLE_OVERRIDE", "get").mockReturnValue(false); + vi.spyOn(overrides, "DOUBLE_BATTLE_OVERRIDE", "get").mockReturnValue(true); + vi.spyOn(overrides, "STARTING_WAVE_OVERRIDE", "get").mockReturnValue(5); + await game.runToSummon([ + Species.MIGHTYENA, + Species.POOCHYENA, + ]); + game.onNextPrompt("CheckSwitchPhase", Mode.CONFIRM, () => { + game.setMode(Mode.MESSAGE); + game.endPhase(); + }, () => game.isCurrentPhase(CommandPhase) || game.isCurrentPhase(TurnInitPhase)); + await game.phaseInterceptor.to(CommandPhase, false); + const battleStatsOpponent = game.scene.currentBattle.enemyParty[0].summonData.battleStats; + expect(battleStatsOpponent[BattleStat.ATK]).toBe(-2); + const battleStatsOpponent2 = game.scene.currentBattle.enemyParty[1].summonData.battleStats; + expect(battleStatsOpponent2[BattleStat.ATK]).toBe(-2); + + const battleStatsPokemon = game.scene.getParty()[0].summonData.battleStats; + expect(battleStatsPokemon[BattleStat.ATK]).toBe(-2); + + const battleStatsPokemon2 = game.scene.getParty()[1].summonData.battleStats; + expect(battleStatsPokemon2[BattleStat.ATK]).toBe(-2); + }, 20000); + + it("double - wild: should only trigger once per pokemon", async() => { + vi.spyOn(overrides, "SINGLE_BATTLE_OVERRIDE", "get").mockReturnValue(false); + vi.spyOn(overrides, "DOUBLE_BATTLE_OVERRIDE", "get").mockReturnValue(true); + vi.spyOn(overrides, "STARTING_WAVE_OVERRIDE", "get").mockReturnValue(3); + await game.runToSummon([ + Species.MIGHTYENA, + Species.POOCHYENA, + ]); + game.onNextPrompt("CheckSwitchPhase", Mode.CONFIRM, () => { + game.setMode(Mode.MESSAGE); + game.endPhase(); + }, () => game.isCurrentPhase(CommandPhase) || game.isCurrentPhase(TurnInitPhase)); + await game.phaseInterceptor.to(CommandPhase, false); + const battleStatsOpponent = game.scene.currentBattle.enemyParty[0].summonData.battleStats; + expect(battleStatsOpponent[BattleStat.ATK]).toBe(-2); + const battleStatsOpponent2 = game.scene.currentBattle.enemyParty[1].summonData.battleStats; + expect(battleStatsOpponent2[BattleStat.ATK]).toBe(-2); + + const battleStatsPokemon = game.scene.getParty()[0].summonData.battleStats; + expect(battleStatsPokemon[BattleStat.ATK]).toBe(-2); + + const battleStatsPokemon2 = game.scene.getParty()[1].summonData.battleStats; + expect(battleStatsPokemon2[BattleStat.ATK]).toBe(-2); + }, 20000); + + it("double - boss: should only trigger once per pokemon", async() => { + vi.spyOn(overrides, "SINGLE_BATTLE_OVERRIDE", "get").mockReturnValue(false); + vi.spyOn(overrides, "DOUBLE_BATTLE_OVERRIDE", "get").mockReturnValue(true); + vi.spyOn(overrides, "STARTING_WAVE_OVERRIDE", "get").mockReturnValue(10); + await game.runToSummon([ + Species.MIGHTYENA, + Species.POOCHYENA, + ]); + game.onNextPrompt("CheckSwitchPhase", Mode.CONFIRM, () => { + game.setMode(Mode.MESSAGE); + game.endPhase(); + }, () => game.isCurrentPhase(CommandPhase) || game.isCurrentPhase(TurnInitPhase)); + await game.phaseInterceptor.to(CommandPhase, false); + const battleStatsOpponent = game.scene.currentBattle.enemyParty[0].summonData.battleStats; + expect(battleStatsOpponent[BattleStat.ATK]).toBe(-2); + const battleStatsOpponent2 = game.scene.currentBattle.enemyParty[1].summonData.battleStats; + expect(battleStatsOpponent2[BattleStat.ATK]).toBe(-2); + + const battleStatsPokemon = game.scene.getParty()[0].summonData.battleStats; + expect(battleStatsPokemon[BattleStat.ATK]).toBe(-2); + + const battleStatsPokemon2 = game.scene.getParty()[1].summonData.battleStats; + expect(battleStatsPokemon2[BattleStat.ATK]).toBe(-2); }, 20000); }); diff --git a/src/test/abilities/intrepid_sword.test.ts b/src/test/abilities/intrepid_sword.test.ts index da2beb9c029..3a830df6845 100644 --- a/src/test/abilities/intrepid_sword.test.ts +++ b/src/test/abilities/intrepid_sword.test.ts @@ -5,11 +5,7 @@ import * as overrides from "#app/overrides"; import {Abilities} from "#app/data/enums/abilities"; import {Species} from "#app/data/enums/species"; import { - MessagePhase, - PostSummonPhase, - ShowAbilityPhase, - StatChangePhase, - ToggleDoublePositionPhase + CommandPhase, } from "#app/phases"; import {BattleStat} from "#app/data/battle-stat"; @@ -40,26 +36,10 @@ describe("Abilities - Intrepid Sword", () => { await game.runToSummon([ Species.ZACIAN, ]); - await game.phaseInterceptor.runFrom(PostSummonPhase).to(PostSummonPhase); - expect(game.scene.getParty()[0].summonData).not.toBeUndefined(); - let battleStatsPokemon = game.scene.getParty()[0].summonData.battleStats; - expect(battleStatsPokemon[BattleStat.ATK]).toBe(0); - await game.phaseInterceptor.run(ShowAbilityPhase); - await game.phaseInterceptor.run(StatChangePhase); - battleStatsPokemon = game.scene.getParty()[0].summonData.battleStats; + await game.phaseInterceptor.to(CommandPhase, false); + const battleStatsPokemon = game.scene.getParty()[0].summonData.battleStats; expect(battleStatsPokemon[BattleStat.ATK]).toBe(1); - }, 20000); - - it("INTREPID SWORD on opponent", async() => { - await game.runToSummon([ - Species.ZACIAN, - ]); - let battleStatsOpponent = game.scene.currentBattle.enemyParty[0].summonData.battleStats; - expect(battleStatsOpponent[BattleStat.ATK]).toBe(0); - await game.phaseInterceptor.runFrom(PostSummonPhase).to(ToggleDoublePositionPhase); - await game.phaseInterceptor.run(StatChangePhase); - await game.phaseInterceptor.run(MessagePhase); - battleStatsOpponent = game.scene.currentBattle.enemyParty[0].summonData.battleStats; + const battleStatsOpponent = game.scene.currentBattle.enemyParty[0].summonData.battleStats; expect(battleStatsOpponent[BattleStat.ATK]).toBe(1); }, 20000); }); diff --git a/src/test/utils/TextInterceptor.ts b/src/test/utils/TextInterceptor.ts index e767953f4af..4cb24b39042 100644 --- a/src/test/utils/TextInterceptor.ts +++ b/src/test/utils/TextInterceptor.ts @@ -1,6 +1,6 @@ export default class TextInterceptor { private scene; - private logs = []; + public logs = []; constructor(scene) { this.scene = scene; scene.messageWrapper = this; diff --git a/src/test/utils/gameManager.ts b/src/test/utils/gameManager.ts index faf44e4bd2b..e2ef97ac599 100644 --- a/src/test/utils/gameManager.ts +++ b/src/test/utils/gameManager.ts @@ -28,6 +28,7 @@ import {MockClock} from "#app/test/utils/mocks/mockClock"; import {Command} from "#app/ui/command-ui-handler"; import ModifierSelectUiHandler from "#app/ui/modifier-select-ui-handler"; import {Button} from "#app/enums/buttons"; +import PartyUiHandler, {PartyUiMode} from "#app/ui/party-ui-handler"; /** * Class to manage the game state and transitions between phases. @@ -279,4 +280,15 @@ export default class GameManager { resolve(); }); } + + async switchPokemon(pokemonIndex: number) { + this.onNextPrompt("CommandPhase", Mode.COMMAND, () => { + this.scene.ui.setMode(Mode.PARTY, PartyUiMode.SWITCH, (this.scene.getCurrentPhase() as CommandPhase).getPokemon().getFieldIndex(), null, PartyUiHandler.FilterNonFainted); + }); + this.onNextPrompt("CommandPhase", Mode.PARTY, () => { + (this.scene.getCurrentPhase() as CommandPhase).handleCommand(Command.POKEMON, pokemonIndex, false); + }); + await this.phaseInterceptor.run(CommandPhase); + await this.phaseInterceptor.to(CommandPhase); + } }