From 66bc83fce4149f5cfec45b1f79f8d2356e8059a6 Mon Sep 17 00:00:00 2001 From: Sirz Benjie <142067137+SirzBenjie@users.noreply.github.com> Date: Sat, 29 Mar 2025 23:32:35 -0500 Subject: [PATCH] [Ability] Flower Veil implementation (#5327) * [WIP] flower veil implementation Signed-off-by: Sirz Benjie <142067137+SirzBenjie@users.noreply.github.com> * Remove promises Signed-off-by: Sirz Benjie <142067137+SirzBenjie@users.noreply.github.com> * Fully implement Flower Veil Signed-off-by: Sirz Benjie <142067137+SirzBenjie@users.noreply.github.com> * Fix ally interaction for battler tag * Condense and cleanup test files * Remove a console.log message * Remove stray excess import * Update doc comments and apply kev's suggestions from code review Co-authored-by: NightKev <34855794+DayKev@users.noreply.github.com> * Remove duplicated test --------- Signed-off-by: Sirz Benjie <142067137+SirzBenjie@users.noreply.github.com> Co-authored-by: NightKev <34855794+DayKev@users.noreply.github.com> --- src/data/ability.ts | 149 +++++++++++++++++++++-- src/data/battler-tags.ts | 2 + src/data/moves/move.ts | 2 +- src/field/pokemon.ts | 33 +++-- src/phases/stat-stage-change-phase.ts | 20 ++++ test/abilities/flower_veil.test.ts | 166 ++++++++++++++++++++++++++ 6 files changed, 349 insertions(+), 23 deletions(-) create mode 100644 test/abilities/flower_veil.test.ts diff --git a/src/data/ability.ts b/src/data/ability.ts index 6ffdc1f5403..942002766d5 100644 --- a/src/data/ability.ts +++ b/src/data/ability.ts @@ -3204,6 +3204,7 @@ export class ConfusionOnStatusEffectAbAttr extends PostAttackAbAttr { } export class PreSetStatusAbAttr extends AbAttr { + /** Return whether the ability attribute can be applied */ canApplyPreSetStatus( pokemon: Pokemon, passive: boolean, @@ -3228,7 +3229,7 @@ export class PreSetStatusAbAttr extends AbAttr { * Provides immunity to status effects to specified targets. */ export class PreSetStatusEffectImmunityAbAttr extends PreSetStatusAbAttr { - private immuneEffects: StatusEffect[]; + protected immuneEffects: StatusEffect[]; /** * @param immuneEffects - The status effects to which the Pokémon is immune. @@ -3282,6 +3283,92 @@ export class StatusEffectImmunityAbAttr extends PreSetStatusEffectImmunityAbAttr */ export class UserFieldStatusEffectImmunityAbAttr extends PreSetStatusEffectImmunityAbAttr { } +/** + * Conditionally provides immunity to status effects to the user's field. + * + * Used by {@linkcode Abilities.FLOWER_VEIL | Flower Veil}. + * @extends UserFieldStatusEffectImmunityAbAttr + * + */ +export class ConditionalUserFieldStatusEffectImmunityAbAttr extends UserFieldStatusEffectImmunityAbAttr { + /** + * The condition for the field immunity to be applied. + * @param target The target of the status effect + * @param source The source of the status effect + */ + protected condition: (target: Pokemon, source: Pokemon | null) => boolean; + + /** + * Evaluate the condition to determine if the {@linkcode ConditionalUserFieldStatusEffectImmunityAbAttr} can be applied. + * @param pokemon The pokemon with the ability + * @param passive unused + * @param simulated Whether the ability is being simulated + * @param effect The status effect being applied + * @param cancelled Holds whether the status effect was cancelled by a prior effect + * @param args `Args[0]` is the target of the status effect, `Args[1]` is the source. + * @returns Whether the ability can be applied to cancel the status effect. + */ + override canApplyPreSetStatus(pokemon: Pokemon, passive: boolean, simulated: boolean, effect: StatusEffect, cancelled: Utils.BooleanHolder, args: [Pokemon, Pokemon | null, ...any]): boolean { + return (!cancelled.value && effect !== StatusEffect.FAINT && this.immuneEffects.length < 1 || this.immuneEffects.includes(effect)) && this.condition(args[0], args[1]); + } + + constructor(condition: (target: Pokemon, source: Pokemon | null) => boolean, ...immuneEffects: StatusEffect[]) { + super(...immuneEffects); + + this.condition = condition; + } +} + +/** + * Conditionally provides immunity to stat drop effects to the user's field. + * + * Used by {@linkcode Abilities.FLOWER_VEIL | Flower Veil}. + */ +export class ConditionalUserFieldProtectStatAbAttr extends PreStatStageChangeAbAttr { + /** {@linkcode BattleStat} to protect or `undefined` if **all** {@linkcode BattleStat} are protected */ + protected protectedStat?: BattleStat; + + /** If the method evaluates to true, the stat will be protected. */ + protected condition: (target: Pokemon) => boolean; + + constructor(condition: (target: Pokemon) => boolean, protectedStat?: BattleStat) { + super(); + this.condition = condition; + } + + /** + * Determine whether the {@linkcode ConditionalUserFieldProtectStatAbAttr} can be applied. + * @param pokemon The pokemon with the ability + * @param passive unused + * @param simulated Unused + * @param stat The stat being affected + * @param cancelled Holds whether the stat change was already prevented. + * @param args Args[0] is the target pokemon of the stat change. + * @returns + */ + override canApplyPreStatStageChange(pokemon: Pokemon, passive: boolean, simulated: boolean, stat: BattleStat, cancelled: Utils.BooleanHolder, args: [Pokemon, ...any]): boolean { + const target = args[0]; + if (!target) { + return false; + } + return !cancelled.value && (Utils.isNullOrUndefined(this.protectedStat) || stat === this.protectedStat) && this.condition(target); + } + + /** + * Apply the {@linkcode ConditionalUserFieldStatusEffectImmunityAbAttr} to an interaction + * @param _pokemon The pokemon the stat change is affecting (unused) + * @param _passive unused + * @param _simulated unused + * @param stat The stat being affected + * @param cancelled Will be set to true if the stat change is prevented + * @param _args unused + */ + override applyPreStatStageChange(_pokemon: Pokemon, _passive: boolean, _simulated: boolean, _stat: BattleStat, cancelled: Utils.BooleanHolder, _args: any[]): void { + cancelled.value = true; + } +} + + export class PreApplyBattlerTagAbAttr extends AbAttr { canApplyPreApplyBattlerTag( pokemon: Pokemon, @@ -3308,8 +3395,8 @@ export class PreApplyBattlerTagAbAttr extends AbAttr { * Provides immunity to BattlerTags {@linkcode BattlerTag} to specified targets. */ export class PreApplyBattlerTagImmunityAbAttr extends PreApplyBattlerTagAbAttr { - private immuneTagTypes: BattlerTagType[]; - private battlerTag: BattlerTag; + protected immuneTagTypes: BattlerTagType[]; + protected battlerTag: BattlerTag; constructor(immuneTagTypes: BattlerTagType | BattlerTagType[]) { super(true); @@ -3320,7 +3407,7 @@ export class PreApplyBattlerTagImmunityAbAttr extends PreApplyBattlerTagAbAttr { override canApplyPreApplyBattlerTag(pokemon: Pokemon, passive: boolean, simulated: boolean, tag: BattlerTag, cancelled: Utils.BooleanHolder, args: any[]): boolean { this.battlerTag = tag; - return this.immuneTagTypes.includes(tag.tagType); + return !cancelled.value && this.immuneTagTypes.includes(tag.tagType); } override applyPreApplyBattlerTag(pokemon: Pokemon, passive: boolean, simulated: boolean, tag: BattlerTag, cancelled: Utils.BooleanHolder, args: any[]): void { @@ -3348,6 +3435,30 @@ export class BattlerTagImmunityAbAttr extends PreApplyBattlerTagImmunityAbAttr { */ export class UserFieldBattlerTagImmunityAbAttr extends PreApplyBattlerTagImmunityAbAttr { } +export class ConditionalUserFieldBattlerTagImmunityAbAttr extends UserFieldBattlerTagImmunityAbAttr { + private condition: (target: Pokemon) => boolean; + + /** + * Determine whether the {@linkcode ConditionalUserFieldBattlerTagImmunityAbAttr} can be applied by passing the target pokemon to the condition. + * @param pokemon The pokemon owning the ability + * @param passive unused + * @param simulated whether the ability is being simulated (unused) + * @param tag The {@linkcode BattlerTag} being applied + * @param cancelled Holds whether the tag was previously cancelled (unused) + * @param args Args[0] is the target that the tag is attempting to be applied to + * @returns Whether the ability can be used to cancel the battler tag + */ + override canApplyPreApplyBattlerTag(pokemon: Pokemon, passive: boolean, simulated: boolean, tag: BattlerTag, cancelled: Utils.BooleanHolder, args: [Pokemon, ...any]): boolean { + return super.canApplyPreApplyBattlerTag(pokemon, passive, simulated, tag, cancelled, args) && this.condition(args[0]); + } + + constructor(condition: (target: Pokemon) => boolean, immuneTagTypes: BattlerTagType | BattlerTagType[]) { + super(immuneTagTypes); + + this.condition = condition; + } +} + export class BlockCritAbAttr extends AbAttr { constructor() { super(false); @@ -5856,19 +5967,20 @@ export function applyPreLeaveFieldAbAttrs( ); } -export function applyPreStatStageChangeAbAttrs( - attrType: Constructor, +export function applyPreStatStageChangeAbAttrs ( + attrType: Constructor, pokemon: Pokemon | null, stat: BattleStat, cancelled: Utils.BooleanHolder, simulated = false, ...args: any[] ): void { - applyAbAttrsInternal( + applyAbAttrsInternal( attrType, pokemon, (attr, passive) => attr.applyPreStatStageChange(pokemon, passive, simulated, stat, cancelled, args), - (attr, passive) => attr.canApplyPreStatStageChange(pokemon, passive, simulated, stat, cancelled, args), args, + (attr, passive) => attr.canApplyPreStatStageChange(pokemon, passive, simulated, stat, cancelled, args), + args, simulated, ); } @@ -5921,7 +6033,8 @@ export function applyPreApplyBattlerTagAbAttrs( attrType, pokemon, (attr, passive) => attr.applyPreApplyBattlerTag(pokemon, passive, simulated, tag, cancelled, args), - (attr, passive) => attr.canApplyPreApplyBattlerTag(pokemon, passive, simulated, tag, cancelled, args), args, + (attr, passive) => attr.canApplyPreApplyBattlerTag(pokemon, passive, simulated, tag, cancelled, args), + args, simulated, ); } @@ -5938,7 +6051,8 @@ export function applyPreWeatherEffectAbAttrs( attrType, pokemon, (attr, passive) => attr.applyPreWeatherEffect(pokemon, passive, simulated, weather, cancelled, args), - (attr, passive) => attr.canApplyPreWeatherEffect(pokemon, passive, simulated, weather, cancelled, args), args, + (attr, passive) => attr.canApplyPreWeatherEffect(pokemon, passive, simulated, weather, cancelled, args), + args, simulated, ); } @@ -6670,8 +6784,19 @@ export function initAbilities() { .attr(UserFieldBattlerTagImmunityAbAttr, [ BattlerTagType.INFATUATED, BattlerTagType.TAUNT, BattlerTagType.DISABLED, BattlerTagType.TORMENT, BattlerTagType.HEAL_BLOCK ]) .ignorable(), new Ability(Abilities.FLOWER_VEIL, 6) - .ignorable() - .unimplemented(), + .attr(ConditionalUserFieldStatusEffectImmunityAbAttr, (target: Pokemon, source: Pokemon | null) => { + return source ? target.getTypes().includes(PokemonType.GRASS) && target.id !== source.id : false; + }) + .attr(ConditionalUserFieldBattlerTagImmunityAbAttr, + (target: Pokemon) => { + return target.getTypes().includes(PokemonType.GRASS); + }, + [ BattlerTagType.DROWSY ], + ) + .attr(ConditionalUserFieldProtectStatAbAttr, (target: Pokemon) => { + return target.getTypes().includes(PokemonType.GRASS); + }) + .ignorable(), new Ability(Abilities.CHEEK_POUCH, 6) .attr(HealFromBerryUseAbAttr, 1 / 3), new Ability(Abilities.PROTEAN, 6) diff --git a/src/data/battler-tags.ts b/src/data/battler-tags.ts index b139faaeb88..c391c4010b8 100644 --- a/src/data/battler-tags.ts +++ b/src/data/battler-tags.ts @@ -5,6 +5,7 @@ import { BlockNonDirectDamageAbAttr, FlinchEffectAbAttr, ProtectStatAbAttr, + ConditionalUserFieldProtectStatAbAttr, ReverseDrainAbAttr, } from "#app/data/ability"; import { ChargeAnim, CommonAnim, CommonBattleAnim, MoveChargeAnim } from "#app/data/battle-anims"; @@ -3024,6 +3025,7 @@ export class MysteryEncounterPostSummonTag extends BattlerTag { if (lapseType === BattlerTagLapseType.CUSTOM) { const cancelled = new BooleanHolder(false); applyAbAttrs(ProtectStatAbAttr, pokemon, cancelled); + applyAbAttrs(ConditionalUserFieldProtectStatAbAttr, pokemon, cancelled, false, pokemon); if (!cancelled.value) { if (pokemon.mysteryEncounterBattleEffects) { pokemon.mysteryEncounterBattleEffects(pokemon); diff --git a/src/data/moves/move.ts b/src/data/moves/move.ts index aeab8a6490b..6148c72bf86 100644 --- a/src/data/moves/move.ts +++ b/src/data/moves/move.ts @@ -8692,7 +8692,7 @@ export function initMoves() { new SelfStatusMove(Moves.REST, PokemonType.PSYCHIC, -1, 5, -1, 0, 1) .attr(StatusEffectAttr, StatusEffect.SLEEP, true, 3, true) .attr(HealAttr, 1, true) - .condition((user, target, move) => !user.isFullHp() && user.canSetStatus(StatusEffect.SLEEP, true, true)) + .condition((user, target, move) => !user.isFullHp() && user.canSetStatus(StatusEffect.SLEEP, true, true, user)) .triageMove(), new AttackMove(Moves.ROCK_SLIDE, PokemonType.ROCK, MoveCategory.PHYSICAL, 75, 90, 10, 30, 0, 1) .attr(FlinchAttr) diff --git a/src/field/pokemon.ts b/src/field/pokemon.ts index aba13b2e51b..6861d61ed76 100644 --- a/src/field/pokemon.ts +++ b/src/field/pokemon.ts @@ -4766,6 +4766,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { stubTag, cancelled, true, + this, ), ); @@ -4793,18 +4794,25 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { newTag, cancelled, ); + if (cancelled.value) { + return false; + } - const userField = this.getAlliedField(); - userField.forEach(pokemon => + for (const pokemon of this.getAlliedField()) { applyPreApplyBattlerTagAbAttrs( UserFieldBattlerTagImmunityAbAttr, pokemon, newTag, cancelled, - ), - ); + false, + this + ); + if (cancelled.value) { + return false; + } + } - if (!cancelled.value && newTag.canAdd(this)) { + if (newTag.canAdd(this)) { this.summonData.tags.push(newTag); newTag.onAdd(this); @@ -5448,17 +5456,22 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { cancelled, quiet, ); + if (cancelled.value) { + return false; + } - const userField = this.getAlliedField(); - userField.forEach(pokemon => + for (const pokemon of this.getAlliedField()) { applyPreSetStatusAbAttrs( UserFieldStatusEffectImmunityAbAttr, pokemon, effect, cancelled, - quiet, - ), - ); + quiet, this, sourcePokemon, + ) + if (cancelled.value) { + break; + } + } if (cancelled.value) { return false; diff --git a/src/phases/stat-stage-change-phase.ts b/src/phases/stat-stage-change-phase.ts index 71b50fa9dce..f58744ef5ce 100644 --- a/src/phases/stat-stage-change-phase.ts +++ b/src/phases/stat-stage-change-phase.ts @@ -4,6 +4,7 @@ import { applyAbAttrs, applyPostStatStageChangeAbAttrs, applyPreStatStageChangeAbAttrs, + ConditionalUserFieldProtectStatAbAttr, PostStatStageChangeAbAttr, ProtectStatAbAttr, ReflectStatStageChangeAbAttr, @@ -151,6 +152,25 @@ export class StatStageChangePhase extends PokemonPhase { if (!cancelled.value && !this.selfTarget && stages.value < 0) { applyPreStatStageChangeAbAttrs(ProtectStatAbAttr, pokemon, stat, cancelled, simulate); + applyPreStatStageChangeAbAttrs( + ConditionalUserFieldProtectStatAbAttr, + pokemon, + stat, + cancelled, + simulate, + pokemon, + ); + const ally = pokemon.getAlly(); + if (ally) { + applyPreStatStageChangeAbAttrs( + ConditionalUserFieldProtectStatAbAttr, + ally, + stat, + cancelled, + simulate, + pokemon, + ); + } /** Potential stat reflection due to Mirror Armor, does not apply to Octolock end of turn effect */ if ( diff --git a/test/abilities/flower_veil.test.ts b/test/abilities/flower_veil.test.ts new file mode 100644 index 00000000000..c26a952acff --- /dev/null +++ b/test/abilities/flower_veil.test.ts @@ -0,0 +1,166 @@ +import { BattlerIndex } from "#app/battle"; +import { Abilities } from "#enums/abilities"; +import { Moves } from "#enums/moves"; +import { Species } from "#enums/species"; +import { Stat } from "#enums/stat"; +import { StatusEffect } from "#enums/status-effect"; +import GameManager from "#test/testUtils/gameManager"; +import Phaser from "phaser"; +import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; +import { allMoves } from "#app/data/moves/move"; +import { BattlerTagType } from "#enums/battler-tag-type"; +import { allAbilities } from "#app/data/ability"; + +describe("Abilities - Flower Veil", () => { + let phaserGame: Phaser.Game; + let game: GameManager; + + beforeAll(() => { + phaserGame = new Phaser.Game({ + type: Phaser.HEADLESS, + }); + }); + + afterEach(() => { + game.phaseInterceptor.restoreOg(); + }); + + beforeEach(() => { + game = new GameManager(phaserGame); + game.override + .moveset([Moves.SPLASH]) + .enemySpecies(Species.BULBASAUR) + .ability(Abilities.FLOWER_VEIL) + .battleType("single") + .disableCrits() + .enemySpecies(Species.MAGIKARP) + .enemyAbility(Abilities.BALL_FETCH) + .enemyMoveset(Moves.SPLASH); + }); + + /*********************************************** + * Tests for proper handling of status effects * + ***********************************************/ + it("should not prevent any source of self-inflicted status conditions", async () => { + game.override + .enemyMoveset([Moves.TACKLE, Moves.SPLASH]) + .moveset([Moves.REST, Moves.SPLASH]) + .startingHeldItems([{ name: "FLAME_ORB" }]); + await game.classicMode.startBattle([Species.BULBASAUR]); + const user = game.scene.getPlayerPokemon()!; + game.move.select(Moves.REST); + await game.forceEnemyMove(Moves.TACKLE); + await game.setTurnOrder([BattlerIndex.ENEMY, BattlerIndex.PLAYER]); + await game.toNextTurn(); + expect(user.status?.effect).toBe(StatusEffect.SLEEP); + + // remove sleep status so we can get burn from the orb + user.resetStatus(); + game.move.select(Moves.SPLASH); + await game.forceEnemyMove(Moves.SPLASH); + await game.toNextTurn(); + expect(user.status?.effect).toBe(StatusEffect.BURN); + }); + + it("should prevent drowsiness from yawn for a grass user and its grass allies", async () => { + game.override.enemyMoveset([Moves.YAWN]).moveset([Moves.SPLASH]).battleType("double"); + await game.classicMode.startBattle([Species.BULBASAUR, Species.BULBASAUR]); + + // Clear the ability of the ally to isolate the test + const ally = game.scene.getPlayerField()[1]!; + vi.spyOn(ally, "getAbility").mockReturnValue(allAbilities[Abilities.BALL_FETCH]); + game.move.select(Moves.SPLASH); + game.move.select(Moves.SPLASH); + await game.forceEnemyMove(Moves.YAWN, BattlerIndex.PLAYER); + await game.forceEnemyMove(Moves.YAWN, BattlerIndex.PLAYER_2); + + await game.phaseInterceptor.to("BerryPhase"); + const user = game.scene.getPlayerPokemon()!; + expect(user.getTag(BattlerTagType.DROWSY)).toBeFalsy(); + expect(ally.getTag(BattlerTagType.DROWSY)).toBeFalsy(); + }); + + it("should prevent status conditions from moves like Thunder Wave for a grass user and its grass allies", async () => { + game.override.enemyMoveset([Moves.THUNDER_WAVE]).moveset([Moves.SPLASH]).battleType("double"); + vi.spyOn(allMoves[Moves.THUNDER_WAVE], "accuracy", "get").mockReturnValue(100); + await game.classicMode.startBattle([Species.BULBASAUR]); + + game.move.select(Moves.SPLASH); + await game.forceEnemyMove(Moves.THUNDER_WAVE); + await game.toNextTurn(); + expect(game.scene.getPlayerPokemon()!.status).toBeUndefined(); + vi.spyOn(allMoves[Moves.THUNDER_WAVE], "accuracy", "get").mockClear(); + }); + + it("should not prevent status conditions for a non-grass user and its non-grass allies", async () => { + game.override.enemyMoveset([Moves.THUNDER_WAVE]).moveset([Moves.SPLASH]).battleType("double"); + await game.classicMode.startBattle([Species.MAGIKARP, Species.MAGIKARP]); + const [user, ally] = game.scene.getPlayerField(); + vi.spyOn(allMoves[Moves.THUNDER_WAVE], "accuracy", "get").mockReturnValue(100); + // Clear the ally ability to isolate the test + vi.spyOn(ally, "getAbility").mockReturnValue(allAbilities[Abilities.BALL_FETCH]); + game.move.select(Moves.SPLASH); + game.move.select(Moves.SPLASH); + await game.forceEnemyMove(Moves.THUNDER_WAVE, BattlerIndex.PLAYER); + await game.forceEnemyMove(Moves.THUNDER_WAVE, BattlerIndex.PLAYER_2); + await game.phaseInterceptor.to("BerryPhase"); + expect(user.status?.effect).toBe(StatusEffect.PARALYSIS); + expect(ally.status?.effect).toBe(StatusEffect.PARALYSIS); + }); + + /******************************************* + * Tests for proper handling of stat drops * + *******************************************/ + + it("should prevent the status drops from enemies for the a grass user and its grass allies", async () => { + game.override.enemyMoveset([Moves.GROWL]).moveset([Moves.SPLASH]).battleType("double"); + await game.classicMode.startBattle([Species.BULBASAUR, Species.BULBASAUR]); + const [user, ally] = game.scene.getPlayerField(); + // Clear the ally ability to isolate the test + vi.spyOn(ally, "getAbility").mockReturnValue(allAbilities[Abilities.BALL_FETCH]); + game.move.select(Moves.SPLASH); + game.move.select(Moves.SPLASH); + await game.phaseInterceptor.to("BerryPhase"); + expect(user.getStatStage(Stat.ATK)).toBe(0); + expect(ally.getStatStage(Stat.ATK)).toBe(0); + }); + + it("should not prevent status drops for a non-grass user and its non-grass allies", async () => { + game.override.enemyMoveset([Moves.GROWL]).moveset([Moves.SPLASH]).battleType("double"); + await game.classicMode.startBattle([Species.MAGIKARP, Species.MAGIKARP]); + const [user, ally] = game.scene.getPlayerField(); + // Clear the ally ability to isolate the test + vi.spyOn(ally, "getAbility").mockReturnValue(allAbilities[Abilities.BALL_FETCH]); + game.move.select(Moves.SPLASH); + game.move.select(Moves.SPLASH); + await game.phaseInterceptor.to("BerryPhase"); + expect(user.getStatStage(Stat.ATK)).toBe(-2); + expect(ally.getStatStage(Stat.ATK)).toBe(-2); + }); + + it("should not prevent self-inflicted stat drops from moves like Close Combat for a user or its allies", async () => { + game.override.moveset([Moves.CLOSE_COMBAT]).battleType("double"); + await game.classicMode.startBattle([Species.BULBASAUR, Species.BULBASAUR]); + const [user, ally] = game.scene.getPlayerField(); + // Clear the ally ability to isolate the test + vi.spyOn(ally, "getAbility").mockReturnValue(allAbilities[Abilities.BALL_FETCH]); + + game.move.select(Moves.CLOSE_COMBAT, 0, BattlerIndex.ENEMY); + game.move.select(Moves.CLOSE_COMBAT, 1, BattlerIndex.ENEMY_2); + await game.phaseInterceptor.to("BerryPhase"); + expect(user.getStatStage(Stat.DEF)).toBe(-1); + expect(user.getStatStage(Stat.SPDEF)).toBe(-1); + expect(ally.getStatStage(Stat.DEF)).toBe(-1); + expect(ally.getStatStage(Stat.SPDEF)).toBe(-1); + }); + + it("should prevent the drops while retaining the boosts from spicy extract", async () => { + game.override.enemyMoveset([Moves.SPICY_EXTRACT]).moveset([Moves.SPLASH]); + await game.classicMode.startBattle([Species.BULBASAUR]); + const user = game.scene.getPlayerPokemon()!; + game.move.select(Moves.SPLASH); + await game.phaseInterceptor.to("BerryPhase"); + expect(user.getStatStage(Stat.ATK)).toBe(2); + expect(user.getStatStage(Stat.DEF)).toBe(0); + }); +});