diff --git a/src/data/mystery-encounters/encounters/absolute-avarice-encounter.ts b/src/data/mystery-encounters/encounters/absolute-avarice-encounter.ts index acfc8cb16a1..4fd3ba645ad 100644 --- a/src/data/mystery-encounters/encounters/absolute-avarice-encounter.ts +++ b/src/data/mystery-encounters/encounters/absolute-avarice-encounter.ts @@ -38,6 +38,7 @@ import type { BerryType } from "#enums/berry-type"; import { StatStageChangePhase } from "#app/phases/stat-stage-change-phase"; import { Stat } from "#enums/stat"; import i18next from "i18next"; +import { MoveUseType } from "#enums/move-use-type"; /** the i18n namespace for this encounter */ const namespace = "mysteryEncounters/absoluteAvarice"; @@ -303,7 +304,7 @@ export const AbsoluteAvariceEncounter: MysteryEncounter = MysteryEncounterBuilde sourceBattlerIndex: BattlerIndex.ENEMY, targets: [BattlerIndex.ENEMY], move: new PokemonMove(Moves.STUFF_CHEEKS), - ignorePp: true, + useType: MoveUseType.IGNORE_PP, }); await transitionMysteryEncounterIntroVisuals(true, true, 500); diff --git a/src/data/mystery-encounters/encounters/clowning-around-encounter.ts b/src/data/mystery-encounters/encounters/clowning-around-encounter.ts index ce5eb2cfdd1..7fbbce9e395 100644 --- a/src/data/mystery-encounters/encounters/clowning-around-encounter.ts +++ b/src/data/mystery-encounters/encounters/clowning-around-encounter.ts @@ -49,6 +49,7 @@ import { CustomPokemonData } from "#app/data/custom-pokemon-data"; import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/constants"; import { EncounterAnim } from "#enums/encounter-anims"; import { Challenges } from "#enums/challenges"; +import { MoveUseType } from "#enums/move-use-type"; /** the i18n namespace for the encounter */ const namespace = "mysteryEncounters/clowningAround"; @@ -209,19 +210,19 @@ export const ClowningAroundEncounter: MysteryEncounter = MysteryEncounterBuilder sourceBattlerIndex: BattlerIndex.ENEMY, targets: [BattlerIndex.ENEMY_2], move: new PokemonMove(Moves.ROLE_PLAY), - ignorePp: true, + useType: MoveUseType.IGNORE_PP, }, { sourceBattlerIndex: BattlerIndex.ENEMY_2, targets: [BattlerIndex.PLAYER], move: new PokemonMove(Moves.TAUNT), - ignorePp: true, + useType: MoveUseType.IGNORE_PP, }, { sourceBattlerIndex: BattlerIndex.ENEMY_2, targets: [BattlerIndex.PLAYER_2], move: new PokemonMove(Moves.TAUNT), - ignorePp: true, + useType: MoveUseType.IGNORE_PP, }, ); diff --git a/src/data/mystery-encounters/encounters/dancing-lessons-encounter.ts b/src/data/mystery-encounters/encounters/dancing-lessons-encounter.ts index bdd4bfaacaa..2d715632216 100644 --- a/src/data/mystery-encounters/encounters/dancing-lessons-encounter.ts +++ b/src/data/mystery-encounters/encounters/dancing-lessons-encounter.ts @@ -41,6 +41,7 @@ import { PokeballType } from "#enums/pokeball"; import { Species } from "#enums/species"; import { Stat } from "#enums/stat"; import i18next from "i18next"; +import { MoveUseType } from "#enums/move-use-type"; /** the i18n namespace for this encounter */ const namespace = "mysteryEncounters/dancingLessons"; @@ -216,7 +217,7 @@ export const DancingLessonsEncounter: MysteryEncounter = MysteryEncounterBuilder sourceBattlerIndex: BattlerIndex.ENEMY, targets: [BattlerIndex.PLAYER], move: new PokemonMove(Moves.REVELATION_DANCE), - ignorePp: true, + useType: MoveUseType.IGNORE_PP, }); await hideOricorioPokemon(); diff --git a/src/data/mystery-encounters/encounters/fiery-fallout-encounter.ts b/src/data/mystery-encounters/encounters/fiery-fallout-encounter.ts index 0364b98abe2..40180227d86 100644 --- a/src/data/mystery-encounters/encounters/fiery-fallout-encounter.ts +++ b/src/data/mystery-encounters/encounters/fiery-fallout-encounter.ts @@ -48,6 +48,7 @@ import { StatStageChangePhase } from "#app/phases/stat-stage-change-phase"; import { Stat } from "#enums/stat"; import { Ability } from "#app/data/abilities/ability-class"; import { FIRE_RESISTANT_ABILITIES } from "#app/data/mystery-encounters/requirements/requirement-groups"; +import { MoveUseType } from "#enums/move-use-type"; /** the i18n namespace for the encounter */ const namespace = "mysteryEncounters/fieryFallout"; @@ -194,13 +195,13 @@ export const FieryFalloutEncounter: MysteryEncounter = MysteryEncounterBuilder.w sourceBattlerIndex: BattlerIndex.ENEMY, targets: [BattlerIndex.PLAYER], move: new PokemonMove(Moves.FIRE_SPIN), - ignorePp: true, + useType: MoveUseType.IGNORE_PP, }, { sourceBattlerIndex: BattlerIndex.ENEMY_2, targets: [BattlerIndex.PLAYER_2], move: new PokemonMove(Moves.FIRE_SPIN), - ignorePp: true, + useType: MoveUseType.IGNORE_PP, }, ); await initBattleWithEnemyConfig(globalScene.currentBattle.mysteryEncounter!.enemyPartyConfigs[0]); diff --git a/src/data/mystery-encounters/encounters/slumbering-snorlax-encounter.ts b/src/data/mystery-encounters/encounters/slumbering-snorlax-encounter.ts index 2654f6b18d8..69f39333c50 100644 --- a/src/data/mystery-encounters/encounters/slumbering-snorlax-encounter.ts +++ b/src/data/mystery-encounters/encounters/slumbering-snorlax-encounter.ts @@ -31,6 +31,7 @@ import { BerryType } from "#enums/berry-type"; import { Stat } from "#enums/stat"; import { CustomPokemonData } from "#app/data/custom-pokemon-data"; import { randSeedInt } from "#app/utils/common"; +import { MoveUseType } from "#enums/move-use-type"; /** i18n namespace for the encounter */ const namespace = "mysteryEncounters/slumberingSnorlax"; @@ -133,14 +134,12 @@ export const SlumberingSnorlaxEncounter: MysteryEncounter = MysteryEncounterBuil guaranteedModifierTypeFuncs: [modifierTypes.LEFTOVERS], fillRemaining: true, }); - encounter.startOfBattleEffects.push( - { - sourceBattlerIndex: BattlerIndex.ENEMY, - targets: [BattlerIndex.PLAYER], - move: new PokemonMove(Moves.SNORE), - ignorePp: true, - }, - ); + encounter.startOfBattleEffects.push({ + sourceBattlerIndex: BattlerIndex.ENEMY, + targets: [BattlerIndex.PLAYER], + move: new PokemonMove(Moves.SNORE), + useType: MoveUseType.IGNORE_PP, + }); await initBattleWithEnemyConfig(encounter.enemyPartyConfigs[0]); }, ) diff --git a/src/data/mystery-encounters/encounters/the-strong-stuff-encounter.ts b/src/data/mystery-encounters/encounters/the-strong-stuff-encounter.ts index 294f1a78b34..4ddd64964e4 100644 --- a/src/data/mystery-encounters/encounters/the-strong-stuff-encounter.ts +++ b/src/data/mystery-encounters/encounters/the-strong-stuff-encounter.ts @@ -29,6 +29,7 @@ import { CustomPokemonData } from "#app/data/custom-pokemon-data"; import { Stat } from "#enums/stat"; import { StatStageChangePhase } from "#app/phases/stat-stage-change-phase"; import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/constants"; +import { MoveUseType } from "#enums/move-use-type"; /** the i18n namespace for the encounter */ const namespace = "mysteryEncounters/theStrongStuff"; @@ -211,13 +212,13 @@ export const TheStrongStuffEncounter: MysteryEncounter = MysteryEncounterBuilder sourceBattlerIndex: BattlerIndex.ENEMY, targets: [BattlerIndex.PLAYER], move: new PokemonMove(Moves.GASTRO_ACID), - ignorePp: true, + useType: MoveUseType.IGNORE_PP, }, { sourceBattlerIndex: BattlerIndex.ENEMY, targets: [BattlerIndex.PLAYER], move: new PokemonMove(Moves.STEALTH_ROCK), - ignorePp: true, + useType: MoveUseType.IGNORE_PP, }, ); diff --git a/src/data/mystery-encounters/encounters/trash-to-treasure-encounter.ts b/src/data/mystery-encounters/encounters/trash-to-treasure-encounter.ts index 1e1db14705a..c77b10739cf 100644 --- a/src/data/mystery-encounters/encounters/trash-to-treasure-encounter.ts +++ b/src/data/mystery-encounters/encounters/trash-to-treasure-encounter.ts @@ -28,6 +28,7 @@ import { BattlerIndex } from "#app/battle"; import { PokemonMove } from "#app/field/pokemon"; import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/constants"; import { randSeedInt } from "#app/utils/common"; +import { MoveUseType } from "#enums/move-use-type"; /** the i18n namespace for this encounter */ const namespace = "mysteryEncounters/trashToTreasure"; @@ -207,13 +208,13 @@ export const TrashToTreasureEncounter: MysteryEncounter = MysteryEncounterBuilde sourceBattlerIndex: BattlerIndex.ENEMY, targets: [BattlerIndex.PLAYER], move: new PokemonMove(Moves.TOXIC), - ignorePp: true, + useType: MoveUseType.IGNORE_PP, }, { sourceBattlerIndex: BattlerIndex.ENEMY, targets: [BattlerIndex.ENEMY], move: new PokemonMove(Moves.STOCKPILE), - ignorePp: true, + useType: MoveUseType.IGNORE_PP, }, ); await initBattleWithEnemyConfig(encounter.enemyPartyConfigs[0]); diff --git a/src/data/mystery-encounters/encounters/uncommon-breed-encounter.ts b/src/data/mystery-encounters/encounters/uncommon-breed-encounter.ts index f4eec5b0923..5dc7286d257 100644 --- a/src/data/mystery-encounters/encounters/uncommon-breed-encounter.ts +++ b/src/data/mystery-encounters/encounters/uncommon-breed-encounter.ts @@ -38,6 +38,7 @@ import { BerryModifier } from "#app/modifier/modifier"; import { StatStageChangePhase } from "#app/phases/stat-stage-change-phase"; import { Stat } from "#enums/stat"; import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/constants"; +import { MoveUseType } from "#enums/move-use-type"; /** the i18n namespace for the encounter */ const namespace = "mysteryEncounters/uncommonBreed"; @@ -178,7 +179,7 @@ export const UncommonBreedEncounter: MysteryEncounter = MysteryEncounterBuilder. sourceBattlerIndex: BattlerIndex.ENEMY, targets: [target], move: pokemonMove, - ignorePp: true, + useType: MoveUseType.IGNORE_PP, }); } diff --git a/src/data/mystery-encounters/mystery-encounter.ts b/src/data/mystery-encounters/mystery-encounter.ts index e305252ed0f..142eb618b7a 100644 --- a/src/data/mystery-encounters/mystery-encounter.ts +++ b/src/data/mystery-encounters/mystery-encounter.ts @@ -28,14 +28,14 @@ import type { GameModes } from "#app/game-mode"; import type { EncounterAnim } from "#enums/encounter-anims"; import type { Challenges } from "#enums/challenges"; import { globalScene } from "#app/global-scene"; +import type { MoveUseType } from "#enums/move-use-type"; export interface EncounterStartOfBattleEffect { sourcePokemon?: Pokemon; sourceBattlerIndex?: BattlerIndex; targets: BattlerIndex[]; move: PokemonMove; - ignorePp: boolean; - followUp?: boolean; + useType: MoveUseType; // TODO: This should always be ignore PP... } const DEFAULT_MAX_ALLOWED_ENCOUNTERS = 2; @@ -253,7 +253,7 @@ export default class MysteryEncounter implements IMysteryEncounter { */ selectedOption?: MysteryEncounterOption; /** - * Will be set by option select handlers automatically, and can be used to refer to which option was chosen by later phases + * Array containing data pertaining to free moves used at the start of a battle mystery envounter. */ startOfBattleEffects: EncounterStartOfBattleEffect[] = []; /** diff --git a/src/data/mystery-encounters/utils/encounter-phase-utils.ts b/src/data/mystery-encounters/utils/encounter-phase-utils.ts index 0215928bbe8..18482a9c3df 100644 --- a/src/data/mystery-encounters/utils/encounter-phase-utils.ts +++ b/src/data/mystery-encounters/utils/encounter-phase-utils.ts @@ -1,5 +1,4 @@ import type Battle from "#app/battle"; -import { BattlerIndex } from "#app/battle"; import { BattleType } from "#enums/battle-type"; import { biomeLinks, BiomePoolTier } from "#app/data/balance/biomes"; import type MysteryEncounterOption from "#app/data/mystery-encounters/mystery-encounter-option"; @@ -986,26 +985,9 @@ export function handleMysteryEncounterBattleStartEffects() { ) { const effects = encounter.startOfBattleEffects; effects.forEach(effect => { - let source: EnemyPokemon | Pokemon; - if (effect.sourcePokemon) { - source = effect.sourcePokemon; - } else if (!isNullOrUndefined(effect.sourceBattlerIndex)) { - if (effect.sourceBattlerIndex === BattlerIndex.ATTACKER) { - source = globalScene.getEnemyField()[0]; - } else if (effect.sourceBattlerIndex === BattlerIndex.ENEMY) { - source = globalScene.getEnemyField()[0]; - } else if (effect.sourceBattlerIndex === BattlerIndex.ENEMY_2) { - source = globalScene.getEnemyField()[1]; - } else if (effect.sourceBattlerIndex === BattlerIndex.PLAYER) { - source = globalScene.getPlayerField()[0]; - } else if (effect.sourceBattlerIndex === BattlerIndex.PLAYER_2) { - source = globalScene.getPlayerField()[1]; - } - } else { - source = globalScene.getEnemyField()[0]; - } - // @ts-ignore: source cannot be undefined - globalScene.pushPhase(new MovePhase(source, effect.targets, effect.move, effect.followUp, effect.ignorePp)); + const source: EnemyPokemon | Pokemon = + effect.sourcePokemon ?? globalScene.getField()[effect.sourceBattlerIndex ?? 0]; + globalScene.pushPhase(new MovePhase(source, effect.targets, effect.move, effect.useType)); }); // Pseudo turn end phase to reset flinch states, Endure, etc. diff --git a/test/abilities/wimp_out.test.ts b/test/abilities/wimp_out.test.ts index f558efdb103..59ed56022cf 100644 --- a/test/abilities/wimp_out.test.ts +++ b/test/abilities/wimp_out.test.ts @@ -44,7 +44,6 @@ describe("Abilities - Wimp Out", () => { function confirmSwitch(): void { const [pokemon1, pokemon2] = game.scene.getPlayerParty(); - expect(game.phaseInterceptor.log).toContain("SwitchSummonPhase"); expect(pokemon1.species.speciesId).not.toBe(Species.WIMPOD); @@ -56,17 +55,34 @@ describe("Abilities - Wimp Out", () => { function confirmNoSwitch(): void { const [pokemon1, pokemon2] = game.scene.getPlayerParty(); - expect(game.phaseInterceptor.log).not.toContain("SwitchSummonPhase"); - expect(pokemon2.species.speciesId).not.toBe(Species.WIMPOD); - expect(pokemon1.species.speciesId).toBe(Species.WIMPOD); expect(pokemon1.isFainted()).toBe(false); expect(pokemon1.getHpRatio()).toBeLessThan(0.5); + + expect(pokemon2.species.speciesId).not.toBe(Species.WIMPOD); } - it("triggers regenerator passive single time when switching out with wimp out", async () => { + it("should switch user out when falling below 50% HP, cancelling any pending moves", async () => { + await game.classicMode.startBattle([Species.WIMPOD, Species.TYRUNT]); + + const wimpod = game.scene.getPlayerPokemon()!; + wimpod.hp *= 0.51; + + game.move.select(Moves.SPLASH); + game.doSelectPartyPokemon(1); + await game.setTurnOrder([BattlerIndex.ENEMY, BattlerIndex.PLAYER]); + await game.phaseInterceptor.to("DamageAnimPhase", false); + game.phaseInterceptor.clearLogs(); + await game.phaseInterceptor.to("TurnEndPhase"); + + confirmSwitch(); + expect(wimpod.turnData.acted).toBe(false); + expect(game.phaseInterceptor.log).not.toContain("MoveEffectPhase"); + }); + + it("should trigger regenerator passive when switching out", async () => { game.override.passiveAbility(Abilities.REGENERATOR).startingLevel(5).enemyLevel(100); await game.classicMode.startBattle([Species.WIMPOD, Species.TYRUNT]); @@ -80,7 +96,7 @@ describe("Abilities - Wimp Out", () => { confirmSwitch(); }); - it("It makes wild pokemon flee if triggered", async () => { + it("should cause wild pokemon to flee", async () => { game.override.enemyAbility(Abilities.WIMP_OUT); await game.classicMode.startBattle([Species.GOLISOPOD, Species.TYRUNT]); @@ -90,12 +106,11 @@ describe("Abilities - Wimp Out", () => { game.move.select(Moves.FALSE_SWIPE); await game.phaseInterceptor.to("BerryPhase"); - const isVisible = enemyPokemon.visible; - const hasFled = enemyPokemon.switchOutStatus; - expect(!isVisible && hasFled).toBe(true); + expect(enemyPokemon.visible).toBe(false); + expect(enemyPokemon.switchOutStatus).toBe(true); }); - it("Does not trigger when HP already below half", async () => { + it("should not trigger when HP is already below half", async () => { await game.classicMode.startBattle([Species.WIMPOD, Species.TYRUNT]); const wimpod = game.scene.getPlayerPokemon()!; wimpod.hp = 5; @@ -107,7 +122,7 @@ describe("Abilities - Wimp Out", () => { confirmNoSwitch(); }); - it("Trapping moves do not prevent Wimp Out from activating.", async () => { + it("should bypass trapping moves and abilities", async () => { game.override.enemyMoveset([Moves.SPIRIT_SHACKLE]).startingLevel(53).enemyLevel(45); await game.classicMode.startBattle([Species.WIMPOD, Species.TYRUNT]); @@ -122,7 +137,7 @@ describe("Abilities - Wimp Out", () => { confirmSwitch(); }); - it("If this Ability activates due to being hit by U-turn or Volt Switch, the user of that move will not be switched out.", async () => { + it("should block switching from U-Turn on activation", async () => { game.override.startingLevel(95).enemyMoveset([Moves.U_TURN]); await game.classicMode.startBattle([Species.WIMPOD, Species.TYRUNT]); @@ -136,7 +151,7 @@ describe("Abilities - Wimp Out", () => { confirmSwitch(); }); - it("If this Ability does not activate due to being hit by U-turn or Volt Switch, the user of that move will be switched out.", async () => { + it("should not block switching from U-Turn on failed activation", async () => { game.override.startingLevel(190).startingWave(8).enemyMoveset([Moves.U_TURN]); await game.classicMode.startBattle([Species.GOLISOPOD, Species.TYRUNT]); const RIVAL_NINJASK1 = game.scene.getEnemyPokemon()?.id; @@ -145,7 +160,7 @@ describe("Abilities - Wimp Out", () => { expect(game.scene.getEnemyPokemon()?.id !== RIVAL_NINJASK1); }); - it("Dragon Tail and Circle Throw switch out Pokémon before the Ability activates.", async () => { + it("Dragon Tail and Circle Throw switch out Pokémon before the Ability activates", async () => { game.override.startingLevel(69).enemyMoveset([Moves.DRAGON_TAIL]); await game.classicMode.startBattle([Species.WIMPOD, Species.TYRUNT]); @@ -162,7 +177,7 @@ describe("Abilities - Wimp Out", () => { expect(game.scene.getPlayerPokemon()!.species.speciesId).not.toBe(Species.WIMPOD); }); - it("triggers when recoil damage is taken", async () => { + it("should trigger from recoil damage", async () => { game.override.moveset([Moves.HEAD_SMASH]).enemyMoveset([Moves.SPLASH]); await game.classicMode.startBattle([Species.WIMPOD, Species.TYRUNT]); @@ -173,7 +188,7 @@ describe("Abilities - Wimp Out", () => { confirmSwitch(); }); - it("It does not activate when the Pokémon cuts its own HP", async () => { + it("should not activate when the Pokémon cuts its own HP", async () => { game.override.moveset([Moves.SUBSTITUTE]).enemyMoveset([Moves.SPLASH]); await game.classicMode.startBattle([Species.WIMPOD, Species.TYRUNT]); @@ -186,7 +201,19 @@ describe("Abilities - Wimp Out", () => { confirmNoSwitch(); }); - it("Does not trigger when neutralized", async () => { + it("should not trigger from Sheer Force-boosted moves", async () => { + game.override.enemyAbility(Abilities.SHEER_FORCE).enemyMoveset(Moves.SLUDGE_BOMB).startingLevel(95); + await game.classicMode.startBattle([Species.WIMPOD, Species.TYRUNT]); + + game.scene.getPlayerPokemon()!.hp *= 0.51; + + game.move.select(Moves.ENDURE); + await game.phaseInterceptor.to("TurnEndPhase"); + + confirmNoSwitch(); + }); + + it("should not trigger while neutralized", async () => { game.override.enemyAbility(Abilities.NEUTRALIZING_GAS).startingLevel(5); await game.classicMode.startBattle([Species.WIMPOD, Species.TYRUNT]); @@ -223,106 +250,140 @@ describe("Abilities - Wimp Out", () => { }, ); - it("Wimp Out will activate due to weather damage", async () => { - game.override.weather(WeatherType.HAIL).enemyMoveset([Moves.SPLASH]); - await game.classicMode.startBattle([Species.WIMPOD, Species.TYRUNT]); + // TODO: Condense into it.eaches + describe("Post Turn Damage Checks - ", () => { + beforeEach(() => { + game.override.enemyMoveset(Moves.SPLASH); + }); - game.scene.getPlayerPokemon()!.hp *= 0.51; + it("Wimp Out will activate due to weather damage", async () => { + game.override.weather(WeatherType.HAIL); + await game.classicMode.startBattle([Species.WIMPOD, Species.TYRUNT]); - game.move.select(Moves.SPLASH); - game.doSelectPartyPokemon(1); - await game.phaseInterceptor.to("TurnEndPhase"); + game.scene.getPlayerPokemon()!.hp *= 0.51; - confirmSwitch(); + game.move.select(Moves.SPLASH); + game.doSelectPartyPokemon(1); + await game.phaseInterceptor.to("TurnEndPhase"); + + confirmSwitch(); + }); + + it("Wimp Out will activate due to post turn status damage", async () => { + game.override.statusEffect(StatusEffect.POISON); + await game.classicMode.startBattle([Species.WIMPOD, Species.TYRUNT]); + + game.scene.getPlayerPokemon()!.hp *= 0.51; + + game.move.select(Moves.SPLASH); + game.doSelectPartyPokemon(1); + await game.toNextTurn(); + + confirmSwitch(); + }); + + it("Wimp Out will activate due to leech seed", async () => { + game.override.enemyMoveset([Moves.LEECH_SEED]); + await game.classicMode.startBattle([Species.WIMPOD, Species.TYRUNT]); + game.scene.getPlayerPokemon()!.hp *= 0.52; + + game.move.select(Moves.SPLASH); + game.doSelectPartyPokemon(1); + await game.toNextTurn(); + + confirmSwitch(); + }); + + it("Wimp Out will activate due to curse damage", async () => { + game.override.enemySpecies(Species.DUSKNOIR).enemyMoveset([Moves.CURSE]); + await game.classicMode.startBattle([Species.WIMPOD, Species.TYRUNT]); + game.scene.getPlayerPokemon()!.hp *= 0.52; + + game.move.select(Moves.SPLASH); + game.doSelectPartyPokemon(1); + await game.toNextTurn(); + + confirmSwitch(); + }); + + it("Wimp Out will activate due to salt cure damage", async () => { + game.override.enemySpecies(Species.NACLI).enemyMoveset([Moves.SALT_CURE]).enemyLevel(1); + await game.classicMode.startBattle([Species.WIMPOD, Species.TYRUNT]); + game.scene.getPlayerPokemon()!.hp *= 0.7; + + game.move.select(Moves.SPLASH); + game.doSelectPartyPokemon(1); + await game.toNextTurn(); + + confirmSwitch(); + }); + + it("Wimp Out will activate due to damaging trap damage", async () => { + game.override.enemySpecies(Species.MAGIKARP).enemyMoveset([Moves.WHIRLPOOL]).enemyLevel(1); + await game.classicMode.startBattle([Species.WIMPOD, Species.TYRUNT]); + game.scene.getPlayerPokemon()!.hp *= 0.55; + + game.move.select(Moves.SPLASH); + game.doSelectPartyPokemon(1); + await game.toNextTurn(); + + confirmSwitch(); + }); + + it("Wimp Out will activate due to aftermath", async () => { + game.override + .moveset([Moves.THUNDER_PUNCH]) + .enemySpecies(Species.MAGIKARP) + .enemyAbility(Abilities.AFTERMATH) + .enemyMoveset([Moves.SPLASH]) + .enemyLevel(1); + await game.classicMode.startBattle([Species.WIMPOD, Species.TYRUNT]); + game.scene.getPlayerPokemon()!.hp *= 0.51; + + game.move.select(Moves.THUNDER_PUNCH); + game.doSelectPartyPokemon(1); + await game.phaseInterceptor.to("TurnEndPhase"); + + confirmSwitch(); + }); + + it("Wimp Out will activate due to bad dreams", async () => { + game.override.statusEffect(StatusEffect.SLEEP).enemyAbility(Abilities.BAD_DREAMS); + await game.classicMode.startBattle([Species.WIMPOD, Species.TYRUNT]); + + game.scene.getPlayerPokemon()!.hp *= 0.52; + + game.move.select(Moves.SPLASH); + game.doSelectPartyPokemon(1); + await game.toNextTurn(); + + confirmSwitch(); + }); + + it("Activates due to entry hazards", async () => { + game.scene.arena.addTag(ArenaTagType.STEALTH_ROCK, 1, Moves.STEALTH_ROCK, 0, ArenaTagSide.ENEMY); + game.scene.arena.addTag(ArenaTagType.SPIKES, 1, Moves.SPIKES, 0, ArenaTagSide.ENEMY); + game.override.enemySpecies(Species.CENTISKORCH).enemyAbility(Abilities.WIMP_OUT).startingWave(4); + await game.classicMode.startBattle([Species.TYRUNT]); + + expect(game.phaseInterceptor.log).not.toContain("MovePhase"); + expect(game.phaseInterceptor.log).toContain("BattleEndPhase"); + }); + + it("Wimp Out will activate due to Nightmare", async () => { + game.override.enemyMoveset([Moves.NIGHTMARE]).statusEffect(StatusEffect.SLEEP); + await game.classicMode.startBattle([Species.WIMPOD, Species.TYRUNT]); + game.scene.getPlayerPokemon()!.hp *= 0.65; + + game.move.select(Moves.SPLASH); + game.doSelectPartyPokemon(1); + await game.toNextTurn(); + + confirmSwitch(); + }); }); - it("Does not trigger when enemy has sheer force", async () => { - game.override.enemyAbility(Abilities.SHEER_FORCE).enemyMoveset(Moves.SLUDGE_BOMB).startingLevel(95); - await game.classicMode.startBattle([Species.WIMPOD, Species.TYRUNT]); - - game.scene.getPlayerPokemon()!.hp *= 0.51; - - game.move.select(Moves.ENDURE); - await game.phaseInterceptor.to("TurnEndPhase"); - - confirmNoSwitch(); - }); - - it("Wimp Out will activate due to post turn status damage", async () => { - game.override.statusEffect(StatusEffect.POISON).enemyMoveset([Moves.SPLASH]); - await game.classicMode.startBattle([Species.WIMPOD, Species.TYRUNT]); - - game.scene.getPlayerPokemon()!.hp *= 0.51; - - game.move.select(Moves.SPLASH); - game.doSelectPartyPokemon(1); - await game.toNextTurn(); - - confirmSwitch(); - }); - - it("Wimp Out will activate due to bad dreams", async () => { - game.override.statusEffect(StatusEffect.SLEEP).enemyAbility(Abilities.BAD_DREAMS); - await game.classicMode.startBattle([Species.WIMPOD, Species.TYRUNT]); - - game.scene.getPlayerPokemon()!.hp *= 0.52; - - game.move.select(Moves.SPLASH); - game.doSelectPartyPokemon(1); - await game.toNextTurn(); - - confirmSwitch(); - }); - - it("Wimp Out will activate due to leech seed", async () => { - game.override.enemyMoveset([Moves.LEECH_SEED]); - await game.classicMode.startBattle([Species.WIMPOD, Species.TYRUNT]); - game.scene.getPlayerPokemon()!.hp *= 0.52; - - game.move.select(Moves.SPLASH); - game.doSelectPartyPokemon(1); - await game.toNextTurn(); - - confirmSwitch(); - }); - - it("Wimp Out will activate due to curse damage", async () => { - game.override.enemySpecies(Species.DUSKNOIR).enemyMoveset([Moves.CURSE]); - await game.classicMode.startBattle([Species.WIMPOD, Species.TYRUNT]); - game.scene.getPlayerPokemon()!.hp *= 0.52; - - game.move.select(Moves.SPLASH); - game.doSelectPartyPokemon(1); - await game.toNextTurn(); - - confirmSwitch(); - }); - - it("Wimp Out will activate due to salt cure damage", async () => { - game.override.enemySpecies(Species.NACLI).enemyMoveset([Moves.SALT_CURE]).enemyLevel(1); - await game.classicMode.startBattle([Species.WIMPOD, Species.TYRUNT]); - game.scene.getPlayerPokemon()!.hp *= 0.7; - - game.move.select(Moves.SPLASH); - game.doSelectPartyPokemon(1); - await game.toNextTurn(); - - confirmSwitch(); - }); - - it("Wimp Out will activate due to damaging trap damage", async () => { - game.override.enemySpecies(Species.MAGIKARP).enemyMoveset([Moves.WHIRLPOOL]).enemyLevel(1); - await game.classicMode.startBattle([Species.WIMPOD, Species.TYRUNT]); - game.scene.getPlayerPokemon()!.hp *= 0.55; - - game.move.select(Moves.SPLASH); - game.doSelectPartyPokemon(1); - await game.toNextTurn(); - - confirmSwitch(); - }); - - it("Magic Guard passive should not allow indirect damage to trigger Wimp Out", async () => { + it("should not trigger on Magic Guard-prevented damage", async () => { game.scene.arena.addTag(ArenaTagType.STEALTH_ROCK, 1, Moves.STEALTH_ROCK, 0, ArenaTagSide.ENEMY); game.scene.arena.addTag(ArenaTagType.SPIKES, 1, Moves.SPIKES, 0, ArenaTagSide.ENEMY); game.override @@ -331,6 +392,7 @@ describe("Abilities - Wimp Out", () => { .weather(WeatherType.HAIL) .statusEffect(StatusEffect.POISON); await game.classicMode.startBattle([Species.WIMPOD, Species.TYRUNT]); + game.scene.getPlayerPokemon()!.hp *= 0.51; game.move.select(Moves.SPLASH); @@ -341,6 +403,19 @@ describe("Abilities - Wimp Out", () => { expect(game.scene.getPlayerPokemon()!.species.speciesId).toBe(Species.WIMPOD); }); + it("triggers status on the wimp out user before a new pokemon is switched in", async () => { + game.override.enemyMoveset(Moves.SLUDGE_BOMB).startingLevel(80); + await game.classicMode.startBattle([Species.WIMPOD, Species.TYRUNT]); + vi.spyOn(allMoves[Moves.SLUDGE_BOMB], "chance", "get").mockReturnValue(100); + + game.move.select(Moves.SPLASH); + game.doSelectPartyPokemon(1); + await game.phaseInterceptor.to("TurnEndPhase"); + + expect(game.scene.getPlayerParty()[1].status?.effect).toEqual(StatusEffect.POISON); + confirmSwitch(); + }); + it("Wimp Out activating should not cancel a double battle", async () => { game.override.battleStyle("double").enemyAbility(Abilities.WIMP_OUT).enemyMoveset([Moves.SPLASH]).enemyLevel(1); await game.classicMode.startBattle([Species.WIMPOD, Species.TYRUNT]); @@ -361,59 +436,7 @@ describe("Abilities - Wimp Out", () => { expect(enemySecPokemon.hp).toEqual(enemySecPokemon.getMaxHp()); }); - it("Wimp Out will activate due to aftermath", async () => { - game.override - .moveset([Moves.THUNDER_PUNCH]) - .enemySpecies(Species.MAGIKARP) - .enemyAbility(Abilities.AFTERMATH) - .enemyMoveset([Moves.SPLASH]) - .enemyLevel(1); - await game.classicMode.startBattle([Species.WIMPOD, Species.TYRUNT]); - game.scene.getPlayerPokemon()!.hp *= 0.51; - - game.move.select(Moves.THUNDER_PUNCH); - game.doSelectPartyPokemon(1); - await game.phaseInterceptor.to("TurnEndPhase"); - - confirmSwitch(); - }); - - it("Activates due to entry hazards", async () => { - game.scene.arena.addTag(ArenaTagType.STEALTH_ROCK, 1, Moves.STEALTH_ROCK, 0, ArenaTagSide.ENEMY); - game.scene.arena.addTag(ArenaTagType.SPIKES, 1, Moves.SPIKES, 0, ArenaTagSide.ENEMY); - game.override.enemySpecies(Species.CENTISKORCH).enemyAbility(Abilities.WIMP_OUT).startingWave(4); - await game.classicMode.startBattle([Species.TYRUNT]); - - expect(game.phaseInterceptor.log).not.toContain("MovePhase"); - expect(game.phaseInterceptor.log).toContain("BattleEndPhase"); - }); - - it("Wimp Out will activate due to Nightmare", async () => { - game.override.enemyMoveset([Moves.NIGHTMARE]).statusEffect(StatusEffect.SLEEP); - await game.classicMode.startBattle([Species.WIMPOD, Species.TYRUNT]); - game.scene.getPlayerPokemon()!.hp *= 0.65; - - game.move.select(Moves.SPLASH); - game.doSelectPartyPokemon(1); - await game.toNextTurn(); - - confirmSwitch(); - }); - - it("triggers status on the wimp out user before a new pokemon is switched in", async () => { - game.override.enemyMoveset(Moves.SLUDGE_BOMB).startingLevel(80); - await game.classicMode.startBattle([Species.WIMPOD, Species.TYRUNT]); - vi.spyOn(allMoves[Moves.SLUDGE_BOMB], "chance", "get").mockReturnValue(100); - - game.move.select(Moves.SPLASH); - game.doSelectPartyPokemon(1); - await game.phaseInterceptor.to("TurnEndPhase"); - - expect(game.scene.getPlayerParty()[1].status?.effect).toEqual(StatusEffect.POISON); - confirmSwitch(); - }); - - it("triggers after last hit of multi hit move", async () => { + it("triggers after last hit of multi hit moves", async () => { game.override.enemyMoveset(Moves.BULLET_SEED).enemyAbility(Abilities.SKILL_LINK); await game.classicMode.startBattle([Species.WIMPOD, Species.TYRUNT]); @@ -444,6 +467,7 @@ describe("Abilities - Wimp Out", () => { expect(enemyPokemon.turnData.hitCount).toBe(2); confirmSwitch(); }); + it("triggers after last hit of Parental Bond", async () => { game.override.enemyMoveset(Moves.TACKLE).enemyAbility(Abilities.PARENTAL_BOND); await game.classicMode.startBattle([Species.WIMPOD, Species.TYRUNT]); diff --git a/test/data/status_effect.test.ts b/test/data/status_effect.test.ts index 111136bf0a2..e9bb1767b7a 100644 --- a/test/data/status_effect.test.ts +++ b/test/data/status_effect.test.ts @@ -18,6 +18,7 @@ import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vite const pokemonName = "PKM"; const sourceText = "SOURCE"; +// TODO: Make this a giant it.each describe("Status Effect Messages", () => { describe("NONE", () => { const statusEffect = StatusEffect.NONE; @@ -390,7 +391,7 @@ describe("Status Effects", () => { game.move.select(Moves.SPLASH); await game.toNextTurn(); - expect(player.status?.effect).toBeUndefined(); + expect(player.status).toBeFalsy(); expect(player.getLastXMoves(1)[0].result).toBe(MoveResult.SUCCESS); }); }); diff --git a/test/moves/rollout.test.ts b/test/moves/rollout.test.ts index b477fd8274f..87126a4bff1 100644 --- a/test/moves/rollout.test.ts +++ b/test/moves/rollout.test.ts @@ -1,5 +1,4 @@ import { allMoves } from "#app/data/moves/move"; -import { CommandPhase } from "#app/phases/command-phase"; import { Abilities } from "#enums/abilities"; import { Moves } from "#enums/moves"; import { Species } from "#enums/species"; @@ -34,13 +33,9 @@ describe("Moves - Rollout", () => { game.override.enemyMoveset(Moves.SPLASH); }); - it("should double it's dmg on sequential uses but reset after 5", async () => { - game.override.moveset([Moves.ROLLOUT]); - vi.spyOn(allMoves[Moves.ROLLOUT], "accuracy", "get").mockReturnValue(100); //always hit - - const variance = 5; - const turns = 6; - const dmgHistory: number[] = []; + it("should double dmg on sequential uses but reset after 5", async () => { + game.override.moveset(Moves.ROLLOUT); + vi.spyOn(allMoves[Moves.ROLLOUT], "accuracy", "get").mockReturnValue(100); // always hit await game.startBattle(); @@ -49,14 +44,13 @@ describe("Moves - Rollout", () => { const enemyPkm = game.scene.getEnemyParty()[0]; vi.spyOn(enemyPkm, "stats", "get").mockReturnValue([500000, 1, 1, 1, 1, 1]); // HP, ATK, DEF, SPATK, SPDEF, SPD - vi.spyOn(enemyPkm, "getHeldItems").mockReturnValue([]); //no berries - enemyPkm.hp = enemyPkm.getMaxHp(); let previousHp = enemyPkm.hp; + const dmgHistory: number[] = []; - for (let i = 0; i < turns; i++) { + for (let i = 0; i < 6; i++) { game.move.select(Moves.ROLLOUT); - await game.phaseInterceptor.to(CommandPhase); + await game.toNextTurn(); dmgHistory.push(previousHp - enemyPkm.hp); previousHp = enemyPkm.hp; @@ -64,16 +58,12 @@ describe("Moves - Rollout", () => { const [turn1Dmg, turn2Dmg, turn3Dmg, turn4Dmg, turn5Dmg, turn6Dmg] = dmgHistory; - expect(turn2Dmg).toBeGreaterThanOrEqual(turn1Dmg * 2 - variance); - expect(turn2Dmg).toBeLessThanOrEqual(turn1Dmg * 2 + variance); - expect(turn3Dmg).toBeGreaterThanOrEqual(turn2Dmg * 2 - variance); - expect(turn3Dmg).toBeLessThanOrEqual(turn2Dmg * 2 + variance); - expect(turn4Dmg).toBeGreaterThanOrEqual(turn3Dmg * 2 - variance); - expect(turn4Dmg).toBeLessThanOrEqual(turn3Dmg * 2 + variance); - expect(turn5Dmg).toBeGreaterThanOrEqual(turn4Dmg * 2 - variance); - expect(turn5Dmg).toBeLessThanOrEqual(turn4Dmg * 2 + variance); - // reset - expect(turn6Dmg).toBeGreaterThanOrEqual(turn1Dmg - variance); - expect(turn6Dmg).toBeLessThanOrEqual(turn1Dmg + variance); + // 2 sig figs precision is more than enough + expect(turn2Dmg).toBeCloseTo(turn1Dmg * 2); + expect(turn3Dmg).toBeCloseTo(turn2Dmg * 2); + expect(turn4Dmg).toBeCloseTo(turn3Dmg * 2); + expect(turn5Dmg).toBeCloseTo(turn4Dmg * 2); + // reset on turn 6 + expect(turn6Dmg).toBeCloseTo(turn1Dmg); }); });