From 3f97c9e39fc65b594e7f0c8cd6ebf2c5b01f6e5f Mon Sep 17 00:00:00 2001 From: muscode Date: Sun, 3 Nov 2024 20:58:12 -0600 Subject: [PATCH] [Ability] Implement Unburden (#4534) * unburden implemented * Used tag instead of stat changes for Unburden * added documentation and neutralizing gas test * accounted for unburden in getEffectiveStat * fixed doubling speed in two places * merge conflict resolve * changed documentation wording, added test for stuff cheeks * refactor unburden * merge * remove console logs * Update src/data/ability.ts Co-authored-by: innerthunder <168692175+innerthunder@users.noreply.github.com> * add neut gas check Co-authored-by: innerthunder <168692175+innerthunder@users.noreply.github.com> * Added test for NeutGas user entering while unburden activated * add spaces to tests, removed passive and [] from applyPostItemLostAbAttrs * added line breaks after test cases --------- Co-authored-by: innerthunder <168692175+innerthunder@users.noreply.github.com> --- src/battle-scene.ts | 16 +- src/data/ability.ts | 42 ++++- src/data/battler-tags.ts | 18 ++ src/data/berry.ts | 8 +- src/data/move.ts | 6 +- src/enums/battler-tag-type.ts | 1 + src/field/pokemon.ts | 3 + src/test/abilities/unburden.test.ts | 245 ++++++++++++++++++++++++++++ 8 files changed, 333 insertions(+), 6 deletions(-) create mode 100644 src/test/abilities/unburden.test.ts diff --git a/src/battle-scene.ts b/src/battle-scene.ts index c731677775d..415a8fe9aaf 100644 --- a/src/battle-scene.ts +++ b/src/battle-scene.ts @@ -15,7 +15,7 @@ import { addTextObject, getTextColor, TextStyle } from "#app/ui/text"; import { allMoves } from "#app/data/move"; import { getDefaultModifierTypeForTier, getEnemyModifierTypesForWave, getLuckString, getLuckTextTint, getModifierPoolForType, getModifierType, getPartyLuckValue, ModifierPoolType, modifierTypes, PokemonHeldItemModifierType } from "#app/modifier/modifier-type"; import AbilityBar from "#app/ui/ability-bar"; -import { allAbilities, applyAbAttrs, applyPostBattleInitAbAttrs, BlockItemTheftAbAttr, DoubleBattleChanceAbAttr, PostBattleInitAbAttr } from "#app/data/ability"; +import { allAbilities, applyAbAttrs, applyPostBattleInitAbAttrs, applyPostItemLostAbAttrs, BlockItemTheftAbAttr, DoubleBattleChanceAbAttr, PostBattleInitAbAttr, PostItemLostAbAttr } from "#app/data/ability"; import Battle, { BattleType, FixedBattleConfig } from "#app/battle"; import { GameMode, GameModes, getGameMode } from "#app/game-mode"; import FieldSpritePipeline from "#app/pipelines/field-sprite"; @@ -2601,9 +2601,19 @@ export default class BattleScene extends SceneBase { const addModifier = () => { if (!matchingModifier || this.removeModifier(matchingModifier, !target.isPlayer())) { if (target.isPlayer()) { - this.addModifier(newItemModifier, ignoreUpdate, playSound, false, instant).then(() => resolve(true)); + this.addModifier(newItemModifier, ignoreUpdate, playSound, false, instant).then(() => { + if (source) { + applyPostItemLostAbAttrs(PostItemLostAbAttr, source, false); + } + resolve(true); + }); } else { - this.addEnemyModifier(newItemModifier, ignoreUpdate, instant).then(() => resolve(true)); + this.addEnemyModifier(newItemModifier, ignoreUpdate, instant).then(() => { + if (source) { + applyPostItemLostAbAttrs(PostItemLostAbAttr, source, false); + } + resolve(true); + }); } } else { resolve(false); diff --git a/src/data/ability.ts b/src/data/ability.ts index 8ec0f296068..8eeb5af52b8 100644 --- a/src/data/ability.ts +++ b/src/data/ability.ts @@ -3857,6 +3857,41 @@ export class PostDancingMoveAbAttr extends PostMoveUsedAbAttr { } } +/** + * Triggers after the Pokemon loses or consumes an item + * @extends AbAttr + */ +export class PostItemLostAbAttr extends AbAttr { + applyPostItemLost(pokemon: Pokemon, simulated: boolean, args: any[]): boolean | Promise { + return false; + } +} + +/** + * Applies a Battler Tag to the Pokemon after it loses or consumes item + * @extends PostItemLostAbAttr + */ +export class PostItemLostApplyBattlerTagAbAttr extends PostItemLostAbAttr { + private tagType: BattlerTagType; + constructor(tagType: BattlerTagType) { + super(true); + this.tagType = tagType; + } + /** + * Adds the last used Pokeball back into the player's inventory + * @param pokemon {@linkcode Pokemon} with this ability + * @param args N/A + * @returns true if BattlerTag was applied + */ + applyPostItemLost(pokemon: Pokemon, simulated: boolean, args: any[]): boolean | Promise { + if (!pokemon.getTag(this.tagType) && !simulated) { + pokemon.addTag(this.tagType); + return true; + } + return false; + } +} + export class StatStageChangeMultiplierAbAttr extends AbAttr { private multiplier: integer; @@ -5228,6 +5263,11 @@ export function applyPostFaintAbAttrs(attrType: Constructor, return applyAbAttrsInternal(attrType, pokemon, (attr, passive) => attr.applyPostFaint(pokemon, passive, simulated, attacker, move, hitResult, args), args, false, simulated); } +export function applyPostItemLostAbAttrs(attrType: Constructor, + pokemon: Pokemon, simulated: boolean = false, ...args: any[]): Promise { + return applyAbAttrsInternal(attrType, pokemon, (attr, passive) => attr.applyPostItemLost(pokemon, simulated, args), args); +} + function queueShowAbility(pokemon: Pokemon, passive: boolean): void { pokemon.scene.unshiftPhase(new ShowAbilityPhase(pokemon.scene, pokemon.id, passive)); pokemon.scene.clearPhaseQueueSplice(); @@ -5521,7 +5561,7 @@ export function initAbilities() { new Ability(Abilities.ANGER_POINT, 4) .attr(PostDefendCritStatStageChangeAbAttr, Stat.ATK, 6), new Ability(Abilities.UNBURDEN, 4) - .unimplemented(), + .attr(PostItemLostApplyBattlerTagAbAttr, BattlerTagType.UNBURDEN), new Ability(Abilities.HEATPROOF, 4) .attr(ReceivedTypeDamageMultiplierAbAttr, Type.FIRE, 0.5) .attr(ReduceBurnDamageAbAttr, 0.5) diff --git a/src/data/battler-tags.ts b/src/data/battler-tags.ts index d671c56ab26..3051c2061a8 100644 --- a/src/data/battler-tags.ts +++ b/src/data/battler-tags.ts @@ -1573,6 +1573,22 @@ export class AbilityBattlerTag extends BattlerTag { } } +/** + * Tag used by Unburden to double speed + * @extends AbilityBattlerTag + */ +export class UnburdenTag extends AbilityBattlerTag { + constructor() { + super(BattlerTagType.UNBURDEN, Abilities.UNBURDEN, BattlerTagLapseType.CUSTOM, 1); + } + onAdd(pokemon: Pokemon): void { + super.onAdd(pokemon); + } + onRemove(pokemon: Pokemon): void { + super.onRemove(pokemon); + } +} + export class TruantTag extends AbilityBattlerTag { constructor() { super(BattlerTagType.TRUANT, Abilities.TRUANT, BattlerTagLapseType.MOVE, 1); @@ -2934,6 +2950,8 @@ export function getBattlerTag(tagType: BattlerTagType, turnCount: number, source return new ThroatChoppedTag(); case BattlerTagType.GORILLA_TACTICS: return new GorillaTacticsTag(); + case BattlerTagType.UNBURDEN: + return new UnburdenTag(); case BattlerTagType.SUBSTITUTE: return new SubstituteTag(sourceMove, sourceId); case BattlerTagType.AUTOTOMIZED: diff --git a/src/data/berry.ts b/src/data/berry.ts index 7243c4c1b2e..d2bbd0fdd1c 100644 --- a/src/data/berry.ts +++ b/src/data/berry.ts @@ -2,7 +2,7 @@ import { getPokemonNameWithAffix } from "../messages"; import Pokemon, { HitResult } from "../field/pokemon"; import { getStatusEffectHealText } from "./status-effect"; import * as Utils from "../utils"; -import { DoubleBerryEffectAbAttr, ReduceBerryUseThresholdAbAttr, applyAbAttrs } from "./ability"; +import { DoubleBerryEffectAbAttr, PostItemLostAbAttr, ReduceBerryUseThresholdAbAttr, applyAbAttrs, applyPostItemLostAbAttrs } from "./ability"; import i18next from "i18next"; import { BattlerTagType } from "#enums/battler-tag-type"; import { BerryType } from "#enums/berry-type"; @@ -75,6 +75,7 @@ export function getBerryEffectFunc(berryType: BerryType): BerryEffectFunc { applyAbAttrs(DoubleBerryEffectAbAttr, pokemon, null, false, hpHealed); pokemon.scene.unshiftPhase(new PokemonHealPhase(pokemon.scene, pokemon.getBattlerIndex(), hpHealed.value, i18next.t("battle:hpHealBerry", { pokemonNameWithAffix: getPokemonNameWithAffix(pokemon), berryName: getBerryName(berryType) }), true)); + applyPostItemLostAbAttrs(PostItemLostAbAttr, pokemon, false); }; case BerryType.LUM: return (pokemon: Pokemon) => { @@ -86,6 +87,7 @@ export function getBerryEffectFunc(berryType: BerryType): BerryEffectFunc { } pokemon.resetStatus(true, true); pokemon.updateInfo(); + applyPostItemLostAbAttrs(PostItemLostAbAttr, pokemon, false); }; case BerryType.LIECHI: case BerryType.GANLON: @@ -101,6 +103,7 @@ export function getBerryEffectFunc(berryType: BerryType): BerryEffectFunc { const statStages = new Utils.NumberHolder(1); applyAbAttrs(DoubleBerryEffectAbAttr, pokemon, null, false, statStages); pokemon.scene.unshiftPhase(new StatStageChangePhase(pokemon.scene, pokemon.getBattlerIndex(), true, [ stat ], statStages.value)); + applyPostItemLostAbAttrs(PostItemLostAbAttr, pokemon, false); }; case BerryType.LANSAT: return (pokemon: Pokemon) => { @@ -108,6 +111,7 @@ export function getBerryEffectFunc(berryType: BerryType): BerryEffectFunc { pokemon.battleData.berriesEaten.push(berryType); } pokemon.addTag(BattlerTagType.CRIT_BOOST); + applyPostItemLostAbAttrs(PostItemLostAbAttr, pokemon, false); }; case BerryType.STARF: return (pokemon: Pokemon) => { @@ -118,6 +122,7 @@ export function getBerryEffectFunc(berryType: BerryType): BerryEffectFunc { const stages = new Utils.NumberHolder(2); applyAbAttrs(DoubleBerryEffectAbAttr, pokemon, null, false, stages); pokemon.scene.unshiftPhase(new StatStageChangePhase(pokemon.scene, pokemon.getBattlerIndex(), true, [ randStat ], stages.value)); + applyPostItemLostAbAttrs(PostItemLostAbAttr, pokemon, false); }; case BerryType.LEPPA: return (pokemon: Pokemon) => { @@ -128,6 +133,7 @@ export function getBerryEffectFunc(berryType: BerryType): BerryEffectFunc { if (ppRestoreMove !== undefined) { ppRestoreMove!.ppUsed = Math.max(ppRestoreMove!.ppUsed - 10, 0); pokemon.scene.queueMessage(i18next.t("battle:ppHealBerry", { pokemonNameWithAffix: getPokemonNameWithAffix(pokemon), moveName: ppRestoreMove!.getName(), berryName: getBerryName(berryType) })); + applyPostItemLostAbAttrs(PostItemLostAbAttr, pokemon, false); } }; } diff --git a/src/data/move.ts b/src/data/move.ts index 43a57db1b6d..9edcbceccf3 100644 --- a/src/data/move.ts +++ b/src/data/move.ts @@ -8,7 +8,7 @@ import { Constructor, NumberHolder } from "#app/utils"; import * as Utils from "../utils"; import { WeatherType } from "./weather"; import { ArenaTagSide, ArenaTrapTag, WeakenMoveTypeTag } from "./arena-tag"; -import { allAbilities, AllyMoveCategoryPowerBoostAbAttr, applyAbAttrs, applyPostAttackAbAttrs, applyPreAttackAbAttrs, applyPreDefendAbAttrs, BlockItemTheftAbAttr, BlockNonDirectDamageAbAttr, BlockOneHitKOAbAttr, BlockRecoilDamageAttr, ConfusionOnStatusEffectAbAttr, FieldMoveTypePowerBoostAbAttr, FieldPreventExplosiveMovesAbAttr, ForceSwitchOutImmunityAbAttr, HealFromBerryUseAbAttr, IgnoreContactAbAttr, IgnoreMoveEffectsAbAttr, IgnoreProtectOnContactAbAttr, InfiltratorAbAttr, MaxMultiHitAbAttr, MoveAbilityBypassAbAttr, MoveEffectChanceMultiplierAbAttr, MoveTypeChangeAbAttr, PostDamageForceSwitchAbAttr, ReverseDrainAbAttr, UncopiableAbilityAbAttr, UnsuppressableAbilityAbAttr, UnswappableAbilityAbAttr, UserFieldMoveTypePowerBoostAbAttr, VariableMovePowerAbAttr, WonderSkinAbAttr } from "./ability"; +import { allAbilities, AllyMoveCategoryPowerBoostAbAttr, applyAbAttrs, applyPostAttackAbAttrs, applyPostItemLostAbAttrs, applyPreAttackAbAttrs, applyPreDefendAbAttrs, BlockItemTheftAbAttr, BlockNonDirectDamageAbAttr, BlockOneHitKOAbAttr, BlockRecoilDamageAttr, ConfusionOnStatusEffectAbAttr, FieldMoveTypePowerBoostAbAttr, FieldPreventExplosiveMovesAbAttr, ForceSwitchOutImmunityAbAttr, HealFromBerryUseAbAttr, IgnoreContactAbAttr, IgnoreMoveEffectsAbAttr, IgnoreProtectOnContactAbAttr, InfiltratorAbAttr, MaxMultiHitAbAttr, MoveAbilityBypassAbAttr, MoveEffectChanceMultiplierAbAttr, MoveTypeChangeAbAttr, PostDamageForceSwitchAbAttr, PostItemLostAbAttr, ReverseDrainAbAttr, UncopiableAbilityAbAttr, UnsuppressableAbilityAbAttr, UnswappableAbilityAbAttr, UserFieldMoveTypePowerBoostAbAttr, VariableMovePowerAbAttr, WonderSkinAbAttr } from "./ability"; import { AttackTypeBoosterModifier, BerryModifier, PokemonHeldItemModifier, PokemonMoveAccuracyBoosterModifier, PokemonMultiHitModifier, PreserveBerryModifier } from "../modifier/modifier"; import { BattlerIndex, BattleType } from "../battle"; import { TerrainType } from "./terrain"; @@ -2402,6 +2402,8 @@ export class RemoveHeldItemAttr extends MoveEffectAttr { // Decrease item amount and update icon !--removedItem.stackCount; target.scene.updateModifiers(target.isPlayer()); + applyPostItemLostAbAttrs(PostItemLostAbAttr, target, false); + if (this.berriesOnly) { user.scene.queueMessage(i18next.t("moveTriggers:incineratedItem", { pokemonName: getPokemonNameWithAffix(user), targetName: getPokemonNameWithAffix(target), itemName: removedItem.type.name })); @@ -2481,6 +2483,7 @@ export class EatBerryAttr extends MoveEffectAttr { eatBerry(consumer: Pokemon) { getBerryEffectFunc(this.chosenBerry!.berryType)(consumer); // consumer eats the berry applyAbAttrs(HealFromBerryUseAbAttr, consumer, new Utils.BooleanHolder(false)); + applyPostItemLostAbAttrs(PostItemLostAbAttr, consumer, false); } } @@ -2516,6 +2519,7 @@ export class StealEatBerryAttr extends EatBerryAttr { } // if the target has berries, pick a random berry and steal it this.chosenBerry = heldBerries[user.randSeedInt(heldBerries.length)]; + applyPostItemLostAbAttrs(PostItemLostAbAttr, target, false); const message = i18next.t("battle:stealEatBerry", { pokemonName: user.name, targetName: target.name, berryName: this.chosenBerry.type.name }); user.scene.queueMessage(message); this.reduceBerryModifier(target); diff --git a/src/enums/battler-tag-type.ts b/src/enums/battler-tag-type.ts index 680dedb93cc..0eace9a1607 100644 --- a/src/enums/battler-tag-type.ts +++ b/src/enums/battler-tag-type.ts @@ -74,6 +74,7 @@ export enum BattlerTagType { DRAGON_CHEER = "DRAGON_CHEER", NO_RETREAT = "NO_RETREAT", GORILLA_TACTICS = "GORILLA_TACTICS", + UNBURDEN = "UNBURDEN", THROAT_CHOPPED = "THROAT_CHOPPED", TAR_SHOT = "TAR_SHOT", BURNED_UP = "BURNED_UP", diff --git a/src/field/pokemon.ts b/src/field/pokemon.ts index 2214dfecac5..c4d2424f677 100644 --- a/src/field/pokemon.ts +++ b/src/field/pokemon.ts @@ -982,6 +982,9 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { if (this.status && this.status.effect === StatusEffect.PARALYSIS) { ret >>= 1; } + if (this.getTag(BattlerTagType.UNBURDEN) && !this.scene.getField(true).some(pokemon => pokemon !== this && pokemon.hasAbilityWithAttr(SuppressFieldAbilitiesAbAttr))) { + ret *= 2; + } break; } diff --git a/src/test/abilities/unburden.test.ts b/src/test/abilities/unburden.test.ts new file mode 100644 index 00000000000..33604efc534 --- /dev/null +++ b/src/test/abilities/unburden.test.ts @@ -0,0 +1,245 @@ +import { Abilities } from "#enums/abilities"; +import { Moves } from "#enums/moves"; +import { Species } from "#enums/species"; +import GameManager from "#test/utils/gameManager"; +import Phaser from "phaser"; +import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; +import { Stat } from "#enums/stat"; +import { BerryType } from "#app/enums/berry-type"; +import { allMoves, StealHeldItemChanceAttr } from "#app/data/move"; + + +describe("Abilities - Unburden", () => { + 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 + .battleType("single") + .starterSpecies(Species.TREECKO) + .startingLevel(1) + .moveset([ Moves.POPULATION_BOMB, Moves.KNOCK_OFF, Moves.PLUCK, Moves.THIEF ]) + .startingHeldItems([ + { name: "BERRY", count: 1, type: BerryType.SITRUS }, + { name: "BERRY", count: 2, type: BerryType.APICOT }, + { name: "BERRY", count: 2, type: BerryType.LUM }, + ]) + .enemySpecies(Species.NINJASK) + .enemyLevel(100) + .enemyMoveset([ Moves.FALSE_SWIPE ]) + .enemyAbility(Abilities.UNBURDEN) + .enemyPassiveAbility(Abilities.NO_GUARD) + .enemyHeldItems([ + { name: "BERRY", type: BerryType.SITRUS, count: 1 }, + { name: "BERRY", type: BerryType.LUM, count: 1 }, + ]); + }); + + it("should activate when a berry is eaten", async () => { + await game.classicMode.startBattle(); + + const playerPokemon = game.scene.getPlayerPokemon()!; + playerPokemon.abilityIndex = 2; + const playerHeldItems = playerPokemon.getHeldItems().length; + const initialPlayerSpeed = playerPokemon.getStat(Stat.SPD); + + game.move.select(Moves.FALSE_SWIPE); + await game.toNextTurn(); + + expect(playerPokemon.getHeldItems().length).toBeLessThan(playerHeldItems); + expect(playerPokemon.getEffectiveStat(Stat.SPD)).toBeCloseTo(initialPlayerSpeed * 2); + }); + + it("should activate when a berry is stolen", async () => { + await game.classicMode.startBattle(); + + const enemyPokemon = game.scene.getEnemyPokemon()!; + const enemyHeldItemCt = enemyPokemon.getHeldItems().length; + const initialEnemySpeed = enemyPokemon.getStat(Stat.SPD); + + game.move.select(Moves.PLUCK); + await game.toNextTurn(); + + expect(enemyPokemon.getHeldItems().length).toBeLessThan(enemyHeldItemCt); + expect(enemyPokemon.getEffectiveStat(Stat.SPD)).toBeCloseTo(initialEnemySpeed * 2); + }); + + it("should activate when an item is knocked off", async () => { + await game.classicMode.startBattle(); + + const enemyPokemon = game.scene.getEnemyPokemon()!; + const enemyHeldItemCt = enemyPokemon.getHeldItems().length; + const initialEnemySpeed = enemyPokemon.getStat(Stat.SPD); + + game.move.select(Moves.KNOCK_OFF); + await game.toNextTurn(); + + expect(enemyPokemon.getHeldItems().length).toBeLessThan(enemyHeldItemCt); + expect(enemyPokemon.getEffectiveStat(Stat.SPD)).toBeCloseTo(initialEnemySpeed * 2); + }); + + it("should activate when an item is stolen via attacking ability", async () => { + game.override + .ability(Abilities.MAGICIAN) + .startingHeldItems([ + { name: "MULTI_LENS", count: 3 }, + ]); + await game.classicMode.startBattle(); + + const enemyPokemon = game.scene.getEnemyPokemon()!; + const enemyHeldItemCt = enemyPokemon.getHeldItems().length; + const initialEnemySpeed = enemyPokemon.getStat(Stat.SPD); + + game.move.select(Moves.POPULATION_BOMB); + await game.toNextTurn(); + + expect(enemyPokemon.getHeldItems().length).toBeLessThan(enemyHeldItemCt); + expect(enemyPokemon.getEffectiveStat(Stat.SPD)).toBeCloseTo(initialEnemySpeed * 2); + }); + + it("should activate when an item is stolen via defending ability", async () => { + game.override + .startingLevel(45) + .enemyAbility(Abilities.PICKPOCKET) + .startingHeldItems([ + { name: "MULTI_LENS", count: 3 }, + { name: "SOUL_DEW", count: 1 }, + { name: "LUCKY_EGG", count: 1 }, + ]); + await game.classicMode.startBattle(); + + const playerPokemon = game.scene.getPlayerPokemon()!; + playerPokemon.abilityIndex = 2; + const playerHeldItems = playerPokemon.getHeldItems().length; + const initialPlayerSpeed = playerPokemon.getStat(Stat.SPD); + + game.move.select(Moves.POPULATION_BOMB); + await game.toNextTurn(); + + expect(playerPokemon.getHeldItems().length).toBeLessThan(playerHeldItems); + expect(playerPokemon.getEffectiveStat(Stat.SPD)).toBeCloseTo(initialPlayerSpeed * 2); + }); + + it("should activate when an item is stolen via move", async () => { + vi.spyOn(allMoves[Moves.THIEF], "attrs", "get").mockReturnValue([ new StealHeldItemChanceAttr(1.0) ]); // give Thief 100% steal rate + game.override.startingHeldItems([ + { name: "MULTI_LENS", count: 3 }, + ]); + await game.classicMode.startBattle(); + + const enemyPokemon = game.scene.getEnemyPokemon()!; + const enemyHeldItemCt = enemyPokemon.getHeldItems().length; + const initialEnemySpeed = enemyPokemon.getStat(Stat.SPD); + + game.move.select(Moves.THIEF); + await game.toNextTurn(); + + expect(enemyPokemon.getHeldItems().length).toBeLessThan(enemyHeldItemCt); + expect(enemyPokemon.getEffectiveStat(Stat.SPD)).toBeCloseTo(initialEnemySpeed * 2); + }); + + it("should activate when an item is stolen via grip claw", async () => { + game.override + .startingLevel(5) + .startingHeldItems([ + { name: "GRIP_CLAW", count: 5 }, + { name: "MULTI_LENS", count: 3 }, + ]) + .enemyHeldItems([ + { name: "SOUL_DEW", count: 1 }, + { name: "LUCKY_EGG", count: 1 }, + { name: "LEFTOVERS", count: 1 }, + { name: "GRIP_CLAW", count: 1 }, + { name: "MULTI_LENS", count: 1 }, + { name: "BERRY", type: BerryType.SITRUS, count: 1 }, + { name: "BERRY", type: BerryType.LUM, count: 1 }, + ]); + await game.classicMode.startBattle(); + + const enemyPokemon = game.scene.getEnemyPokemon()!; + const enemyHeldItemCt = enemyPokemon.getHeldItems().length; + const initialEnemySpeed = enemyPokemon.getStat(Stat.SPD); + + while (enemyPokemon.getHeldItems().length === enemyHeldItemCt) { + game.move.select(Moves.POPULATION_BOMB); + await game.toNextTurn(); + } + + expect(enemyPokemon.getHeldItems().length).toBeLessThan(enemyHeldItemCt); + expect(enemyPokemon.getEffectiveStat(Stat.SPD)).toBeCloseTo(initialEnemySpeed * 2); + }); + + it("should not activate when a neutralizing ability is present", async () => { + game.override.enemyAbility(Abilities.NEUTRALIZING_GAS); + await game.classicMode.startBattle(); + + const playerPokemon = game.scene.getPlayerPokemon()!; + const playerHeldItems = playerPokemon.getHeldItems().length; + const initialPlayerSpeed = playerPokemon.getStat(Stat.SPD); + + game.move.select(Moves.FALSE_SWIPE); + await game.toNextTurn(); + + expect(playerPokemon.getHeldItems().length).toBeLessThan(playerHeldItems); + expect(playerPokemon.getEffectiveStat(Stat.SPD)).toBeCloseTo(initialPlayerSpeed); + }); + + it("should activate when a move that consumes a berry is used", async () => { + game.override.enemyMoveset([ Moves.STUFF_CHEEKS ]); + await game.classicMode.startBattle(); + + const enemyPokemon = game.scene.getEnemyPokemon()!; + const enemyHeldItemCt = enemyPokemon.getHeldItems().length; + const initialEnemySpeed = enemyPokemon.getStat(Stat.SPD); + + game.move.select(Moves.STUFF_CHEEKS); + await game.toNextTurn(); + + expect(enemyPokemon.getHeldItems().length).toBeLessThan(enemyHeldItemCt); + expect(enemyPokemon.getEffectiveStat(Stat.SPD)).toBeCloseTo(initialEnemySpeed * 2); + }); + + it("should deactivate when a neutralizing gas user enters the field", async () => { + game.override + .battleType("double") + .moveset([ Moves.SPLASH ]); + await game.classicMode.startBattle([ Species.TREECKO, Species.MEOWTH, Species.WEEZING ]); + + const playerPokemon = game.scene.getParty(); + const treecko = playerPokemon[0]; + const weezing = playerPokemon[2]; + treecko.abilityIndex = 2; + weezing.abilityIndex = 1; + const playerHeldItems = treecko.getHeldItems().length; + const initialPlayerSpeed = treecko.getStat(Stat.SPD); + + game.move.select(Moves.SPLASH); + game.move.select(Moves.SPLASH); + await game.forceEnemyMove(Moves.FALSE_SWIPE, 0); + await game.forceEnemyMove(Moves.FALSE_SWIPE, 0); + await game.phaseInterceptor.to("TurnEndPhase"); + + expect(treecko.getHeldItems().length).toBeLessThan(playerHeldItems); + expect(treecko.getEffectiveStat(Stat.SPD)).toBeCloseTo(initialPlayerSpeed * 2); + + await game.toNextTurn(); + game.move.select(Moves.SPLASH); + game.doSwitchPokemon(2); + await game.phaseInterceptor.to("TurnEndPhase"); + + expect(treecko.getHeldItems().length).toBeLessThan(playerHeldItems); + expect(treecko.getEffectiveStat(Stat.SPD)).toBeCloseTo(initialPlayerSpeed); + }); + +});