diff --git a/src/data/battler-tags.ts b/src/data/battler-tags.ts index a08631877cb..7a185d67f38 100644 --- a/src/data/battler-tags.ts +++ b/src/data/battler-tags.ts @@ -558,6 +558,13 @@ export class PowderTag extends BattlerTag { pokemon.scene.queueMessage(i18next.t("battlerTags:powderOnAdd", { pokemonNameWithAffix: getPokemonNameWithAffix(pokemon) })); } + /** + * Applies Powder's effects before the tag owner uses a Fire-type move. + * Also causes the tag to expire at the end of turn. + * @param pokemon {@linkcode Pokemon} the owner of this tag + * @param lapseType {@linkcode BattlerTagLapseType} the type of lapse functionality to carry out + * @returns `true` if the tag should not expire after this lapse; `false` otherwise. + */ lapse(pokemon: Pokemon, lapseType: BattlerTagLapseType): boolean { if (lapseType === BattlerTagLapseType.PRE_MOVE) { const movePhase = pokemon.scene.getCurrentPhase(); @@ -565,7 +572,12 @@ export class PowderTag extends BattlerTag { const move = movePhase.move.getMove(); if (move.type === Type.FIRE) { movePhase.cancel(); - pokemon.damageAndUpdate(Math.floor(pokemon.getMaxHp() / 4), HitResult.OTHER); + + const cancelDamage = new Utils.BooleanHolder(false); + applyAbAttrs(BlockNonDirectDamageAbAttr, pokemon, cancelDamage); + if (!cancelDamage.value) { + pokemon.damageAndUpdate(Math.floor(pokemon.getMaxHp() / 4), HitResult.OTHER); + } pokemon.scene.queueMessage(i18next.t("battlerTags:powderLapse")); } diff --git a/src/test/moves/powder.test.ts b/src/test/moves/powder.test.ts new file mode 100644 index 00000000000..f2c245cd141 --- /dev/null +++ b/src/test/moves/powder.test.ts @@ -0,0 +1,145 @@ +import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest"; +import Phaser from "phaser"; +import GameManager from "#test/utils/gameManager"; +import { Abilities } from "#app/enums/abilities"; +import { Moves } from "#app/enums/moves"; +import { Species } from "#app/enums/species"; +import { getMovePosition } from "#app/test/utils/gameManagerUtils"; +import { BerryPhase } from "#app/phases/berry-phase"; +import { MoveResult } from "#app/field/pokemon"; +import { Type } from "#app/data/type"; +import { MoveEffectPhase } from "#app/phases/move-effect-phase"; +import { StatusEffect } from "#app/enums/status-effect"; + +const TIMEOUT = 20 * 1000; + +describe("Moves - Powder", () => { + 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"); + + game.override.enemySpecies(Species.SNORLAX); + game.override.enemyLevel(100); + game.override.enemyMoveset(Array(4).fill(Moves.EMBER)); + game.override.enemyAbility(Abilities.INSOMNIA); + + game.override.startingLevel(100); + game.override.moveset([Moves.POWDER, Moves.SPLASH, Moves.FIERY_DANCE]); + }); + + it( + "should cancel the target's Fire-type move and damage the target", + async () => { + await game.startBattle([Species.CHARIZARD]); + + const enemyPokemon = game.scene.getEnemyPokemon()!; + + game.doAttack(getMovePosition(game.scene, 0, Moves.POWDER)); + + await game.phaseInterceptor.to(BerryPhase, false); + expect(enemyPokemon.getLastXMoves()[0].result).toBe(MoveResult.FAIL); + expect(enemyPokemon.hp).toBe(Math.ceil(3 * enemyPokemon.getMaxHp() / 4)); + }, TIMEOUT + ); + + it.todo("should not cancel Fire-type moves after the turn it's used"); + + it.todo("should have no effect against Grass-type Pokemon"); + + it.todo("should have no effect against Pokemon with Overcoat"); + + it( + "should not damage the target if the target has Magic Guard", + async () => { + game.override.enemyAbility(Abilities.MAGIC_GUARD); + + await game.startBattle([Species.CHARIZARD]); + + const enemyPokemon = game.scene.getEnemyPokemon()!; + + game.doAttack(getMovePosition(game.scene, 0, Moves.POWDER)); + + await game.phaseInterceptor.to(BerryPhase, false); + expect(enemyPokemon.getLastXMoves()[0].result).toBe(MoveResult.FAIL); + expect(enemyPokemon.hp).toBe(enemyPokemon.getMaxHp()); + }, TIMEOUT + ); + + it( + "should not prevent the target from thawing out with its Fire-type move", + async () => { + game.override.enemyStatusEffect(StatusEffect.FREEZE); + + await game.startBattle([Species.CHARIZARD]); + + const enemyPokemon = game.scene.getEnemyPokemon()!; + + game.doAttack(getMovePosition(game.scene, 0, Moves.POWDER)); + + await game.phaseInterceptor.to(BerryPhase, false); + expect(enemyPokemon.status?.effect).not.toBe(StatusEffect.FREEZE); + expect(enemyPokemon.getLastXMoves()[0].result).toBe(MoveResult.FAIL); + expect(enemyPokemon.hp).toBe(Math.ceil(3 * enemyPokemon.getMaxHp() / 4)); + } + ); + + it( + "should not allow a target with Protean to change to Fire type", + async () => { + game.override.enemyAbility(Abilities.PROTEAN); + + await game.startBattle([Species.CHARIZARD]); + + const enemyPokemon = game.scene.getEnemyPokemon()!; + + game.doAttack(getMovePosition(game.scene, 0, Moves.POWDER)); + + await game.phaseInterceptor.to(BerryPhase, false); + expect(enemyPokemon.getLastXMoves()[0].result).toBe(MoveResult.FAIL); + expect(enemyPokemon.hp).toBeLessThan(enemyPokemon.getMaxHp()); + expect(enemyPokemon.summonData?.types).not.toBe(Type.FIRE); + }, TIMEOUT + ); + + it.skip( + "should cancel Fire-type moves generated by the target's Dancer ability", + async () => { + game.override + .enemySpecies(Species.BLASTOISE) + .enemyAbility(Abilities.DANCER); + + await game.startBattle([Species.CHARIZARD]); + + const playerPokemon = game.scene.getPlayerPokemon()!; + const enemyPokemon = game.scene.getEnemyPokemon()!; + + game.doAttack(getMovePosition(game.scene, 0, Moves.FIERY_DANCE)); + + await game.phaseInterceptor.to(MoveEffectPhase); + const enemyStartingHp = enemyPokemon.hp; + + await game.phaseInterceptor.to(BerryPhase, false); + // player should not take damage + expect(playerPokemon.hp).toBe(playerPokemon.getMaxHp()); + // enemy should have taken damage from player's Fiery Dance + 2 Powder procs + expect(enemyPokemon.hp).toBe(enemyStartingHp - 2*Math.floor(enemyPokemon.getMaxHp() / 4)); + }, TIMEOUT + ); + + it.todo("should cancel Hidden Power if it becomes a Fire-type move"); + + it.todo("should cancel Shell Trap and damage the target, even if the move would fail"); +});