From dd72c5e189c5d20bb630854edc6740477f7158f4 Mon Sep 17 00:00:00 2001 From: PigeonBar <56974298+PigeonBar@users.noreply.github.com> Date: Tue, 3 Dec 2024 01:28:57 -0500 Subject: [PATCH] [Move][Beta] Powder edge cases (#4960) * [Move][Beta] Powder edge cases * Fix Heavy Rain check to account for weather suppression Co-authored-by: NightKev <34855794+DayKev@users.noreply.github.com> * "{Pokemon} used {Fire-type move}!" now displays before Powder activation Co-authored-by: innerthunder <168692175+innerthunder@users.noreply.github.com> * Make `showMoveText()` and `showFailedText()` public for now --------- Co-authored-by: NightKev <34855794+DayKev@users.noreply.github.com> Co-authored-by: innerthunder <168692175+innerthunder@users.noreply.github.com> --- src/data/battler-tags.ts | 6 +- src/data/move.ts | 3 +- src/phases/move-phase.ts | 4 +- src/test/moves/powder.test.ts | 139 ++++++++++++++++++++++++++++++---- 4 files changed, 133 insertions(+), 19 deletions(-) diff --git a/src/data/battler-tags.ts b/src/data/battler-tags.ts index 5e6ee334db7..0c0b8e9e034 100644 --- a/src/data/battler-tags.ts +++ b/src/data/battler-tags.ts @@ -885,8 +885,10 @@ export class PowderTag extends BattlerTag { const movePhase = pokemon.scene.getCurrentPhase(); if (movePhase instanceof MovePhase) { const move = movePhase.move.getMove(); - if (pokemon.getMoveType(move) === Type.FIRE) { - movePhase.cancel(); + const weather = pokemon.scene.arena.weather; + if (pokemon.getMoveType(move) === Type.FIRE && !(weather && weather.weatherType === WeatherType.HEAVY_RAIN && !weather.isEffectSuppressed(pokemon.scene))) { + movePhase.fail(); + movePhase.showMoveText(); pokemon.scene.unshiftPhase(new CommonAnimPhase(pokemon.scene, pokemon.getBattlerIndex(), pokemon.getBattlerIndex(), CommonAnim.POWDER)); diff --git a/src/data/move.ts b/src/data/move.ts index 27f7829a920..5a8ad208467 100644 --- a/src/data/move.ts +++ b/src/data/move.ts @@ -9734,8 +9734,7 @@ export function initMoves() { new StatusMove(Moves.POWDER, Type.BUG, 100, 20, -1, 1, 6) .attr(AddBattlerTagAttr, BattlerTagType.POWDER, false, true) .ignoresSubstitute() - .powderMove() - .edgeCase(), // does not cancel Fire-type moves generated by Dancer + .powderMove(), new ChargingSelfStatusMove(Moves.GEOMANCY, Type.FAIRY, -1, 10, -1, 0, 6) .chargeText(i18next.t("moveTriggers:isChargingPower", { pokemonName: "{USER}" })) .attr(StatStageChangeAttr, [ Stat.SPATK, Stat.SPDEF, Stat.SPD ], 2, true) diff --git a/src/phases/move-phase.ts b/src/phases/move-phase.ts index 005cdbe1716..089386bee00 100644 --- a/src/phases/move-phase.ts +++ b/src/phases/move-phase.ts @@ -529,7 +529,7 @@ export class MovePhase extends BattlePhase { * Displays the move's usage text to the player, unless it's a charge turn (ie: {@link Moves.SOLAR_BEAM Solar Beam}), * the pokemon is on a recharge turn (ie: {@link Moves.HYPER_BEAM Hyper Beam}), or a 2-turn move was interrupted (ie: {@link Moves.FLY Fly}). */ - protected showMoveText(): void { + public showMoveText(): void { if (this.move.moveId === Moves.NONE) { return; } @@ -545,7 +545,7 @@ export class MovePhase extends BattlePhase { applyMoveAttrs(PreMoveMessageAttr, this.pokemon, this.pokemon.getOpponents()[0], this.move.getMove()); } - protected showFailedText(failedText?: string): void { + public showFailedText(failedText?: string): void { this.scene.queueMessage(failedText ?? i18next.t("battle:attackFailed")); } } diff --git a/src/test/moves/powder.test.ts b/src/test/moves/powder.test.ts index 5c0f318d620..a1db2bced3a 100644 --- a/src/test/moves/powder.test.ts +++ b/src/test/moves/powder.test.ts @@ -5,10 +5,11 @@ import { Abilities } from "#enums/abilities"; import { Moves } from "#enums/moves"; import { Species } from "#enums/species"; import { BerryPhase } from "#app/phases/berry-phase"; -import { MoveResult } from "#app/field/pokemon"; +import { MoveResult, PokemonMove } from "#app/field/pokemon"; import { Type } from "#enums/type"; import { MoveEffectPhase } from "#app/phases/move-effect-phase"; import { StatusEffect } from "#enums/status-effect"; +import { BattlerIndex } from "#app/battle"; describe("Moves - Powder", () => { let phaserGame: Phaser.Game; @@ -34,21 +35,25 @@ describe("Moves - Powder", () => { .enemyMoveset(Moves.EMBER) .enemyAbility(Abilities.INSOMNIA) .startingLevel(100) - .moveset([ Moves.POWDER, Moves.SPLASH, Moves.FIERY_DANCE ]); + .moveset([ Moves.POWDER, Moves.SPLASH, Moves.FIERY_DANCE, Moves.ROAR ]); }); it( - "should cancel the target's Fire-type move and damage the target", + "should cancel the target's Fire-type move, damage the target, and still consume the target's PP", async () => { + // Cannot use enemy moveset override for this test, since it interferes with checking PP + game.override.enemyMoveset([]); await game.classicMode.startBattle([ Species.CHARIZARD ]); const enemyPokemon = game.scene.getEnemyPokemon()!; + enemyPokemon.moveset = [ new PokemonMove(Moves.EMBER) ]; game.move.select(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)); + expect(enemyPokemon.moveset[0]!.ppUsed).toBe(1); await game.toNextTurn(); @@ -57,6 +62,7 @@ describe("Moves - Powder", () => { await game.phaseInterceptor.to(BerryPhase, false); expect(enemyPokemon.getLastXMoves()[0].result).toBe(MoveResult.SUCCESS); expect(enemyPokemon.hp).toBe(Math.ceil(3 * enemyPokemon.getMaxHp() / 4)); + expect(enemyPokemon.moveset[0]!.ppUsed).toBe(2); }); it( @@ -107,6 +113,22 @@ describe("Moves - Powder", () => { expect(enemyPokemon.hp).toBe(enemyPokemon.getMaxHp()); }); + it( + "should not damage the target if Primordial Sea is active", + async () => { + game.override.enemyAbility(Abilities.PRIMORDIAL_SEA); + + await game.classicMode.startBattle([ Species.CHARIZARD ]); + + const enemyPokemon = game.scene.getEnemyPokemon()!; + + game.move.select(Moves.POWDER); + + await game.phaseInterceptor.to(BerryPhase, false); + expect(enemyPokemon.getLastXMoves()[0].result).toBe(MoveResult.FAIL); + expect(enemyPokemon.hp).toBe(enemyPokemon.getMaxHp()); + }); + it( "should not prevent the target from thawing out with Flame Wheel", async () => { @@ -144,29 +166,60 @@ describe("Moves - Powder", () => { expect(enemyPokemon.summonData?.types).not.toBe(Type.FIRE); }); - // TODO: Implement this interaction to pass this test - it.skip( + it( "should cancel Fire-type moves generated by the target's Dancer ability", async () => { game.override + .battleType("double") .enemySpecies(Species.BLASTOISE) .enemyAbility(Abilities.DANCER); + await game.classicMode.startBattle([ Species.CHARIZARD, Species.CHARIZARD ]); + + const playerPokemon = game.scene.getPlayerPokemon()!; + const enemyPokemon = game.scene.getEnemyPokemon()!; + + // Turn 1: Roar away 1 opponent + game.move.select(Moves.ROAR, 0, BattlerIndex.ENEMY_2); + game.move.select(Moves.SPLASH, 1); + await game.toNextTurn(); + await game.toNextTurn(); // Requires game.toNextTurn() twice due to double battle + + // Turn 2: Enemy should activate Powder twice: From using Ember, and from copying Fiery Dance via Dancer + playerPokemon.hp = playerPokemon.getMaxHp(); + game.move.select(Moves.FIERY_DANCE, 0, BattlerIndex.ENEMY); + game.move.select(Moves.POWDER, 1, BattlerIndex.ENEMY); + + await game.phaseInterceptor.to(MoveEffectPhase); + const enemyStartingHp = enemyPokemon.hp; + + await game.phaseInterceptor.to(BerryPhase, false); + + + // player should not take damage + expect(enemyPokemon.getLastXMoves()[0].result).toBe(MoveResult.FAIL); + expect(playerPokemon.hp).toBe(playerPokemon.getMaxHp()); + // enemy should have taken damage from player's Fiery Dance + 2 Powder procs + expect(enemyPokemon.hp).toBe(enemyStartingHp - playerPokemon.turnData.totalDamageDealt - 2 * Math.floor(enemyPokemon.getMaxHp() / 4)); + }); + + it( + "should cancel Fiery Dance, then prevent it from triggering Dancer", + async () => { + game.override.ability(Abilities.DANCER) + .enemyMoveset(Moves.FIERY_DANCE); + await game.classicMode.startBattle([ Species.CHARIZARD ]); const playerPokemon = game.scene.getPlayerPokemon()!; const enemyPokemon = game.scene.getEnemyPokemon()!; - game.move.select(Moves.FIERY_DANCE); - - await game.phaseInterceptor.to(MoveEffectPhase); - const enemyStartingHp = enemyPokemon.hp; + game.move.select(Moves.POWDER); 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)); + expect(enemyPokemon.getLastXMoves()[0].result).toBe(MoveResult.FAIL); + expect(enemyPokemon.hp).toBe(Math.ceil(3 * enemyPokemon.getMaxHp() / 4)); + expect(playerPokemon.getLastXMoves()[0].move).toBe(Moves.POWDER); }); it( @@ -202,4 +255,64 @@ describe("Moves - Powder", () => { expect(enemyPokemon.getLastXMoves()[0].result).toBe(MoveResult.FAIL); expect(enemyPokemon.hp).toBe(Math.ceil(3 * enemyPokemon.getMaxHp() / 4)); }); + + it( + "should cancel Grass Pledge if used after ally's Fire Pledge", + async () => { + game.override.enemyMoveset([ Moves.FIRE_PLEDGE, Moves.GRASS_PLEDGE ]) + .battleType("double"); + + await game.classicMode.startBattle([ Species.CHARIZARD, Species.CHARIZARD ]); + const enemyPokemon = game.scene.getEnemyPokemon()!; + + game.move.select(Moves.POWDER, 0, BattlerIndex.ENEMY); + game.move.select(Moves.SPLASH, 1); + await game.forceEnemyMove(Moves.GRASS_PLEDGE, BattlerIndex.PLAYER); + await game.forceEnemyMove(Moves.FIRE_PLEDGE, BattlerIndex.PLAYER); + await game.setTurnOrder([ BattlerIndex.PLAYER, BattlerIndex.PLAYER_2, BattlerIndex.ENEMY_2, BattlerIndex.ENEMY ]); + + await game.phaseInterceptor.to(BerryPhase, false); + expect(enemyPokemon.getLastXMoves()[0].result).toBe(MoveResult.FAIL); + expect(enemyPokemon.hp).toBe(Math.ceil(3 * enemyPokemon.getMaxHp() / 4)); + }); + + it( + "should cancel Fire Pledge if used before ally's Water Pledge", + async () => { + game.override.enemyMoveset([ Moves.FIRE_PLEDGE, Moves.WATER_PLEDGE ]) + .battleType("double"); + + await game.classicMode.startBattle([ Species.CHARIZARD, Species.CHARIZARD ]); + const enemyPokemon = game.scene.getEnemyPokemon()!; + + game.move.select(Moves.POWDER, 0, BattlerIndex.ENEMY); + game.move.select(Moves.SPLASH, 1); + await game.forceEnemyMove(Moves.FIRE_PLEDGE, BattlerIndex.PLAYER); + await game.forceEnemyMove(Moves.WATER_PLEDGE, BattlerIndex.PLAYER); + await game.setTurnOrder([ BattlerIndex.PLAYER, BattlerIndex.PLAYER_2, BattlerIndex.ENEMY, BattlerIndex.ENEMY_2 ]); + + await game.phaseInterceptor.to(BerryPhase, false); + expect(enemyPokemon.getLastXMoves()[0].result).toBe(MoveResult.FAIL); + expect(enemyPokemon.hp).toBe(Math.ceil(3 * enemyPokemon.getMaxHp() / 4)); + }); + + it( + "should NOT cancel Fire Pledge if used after ally's Water Pledge", + async () => { + game.override.enemyMoveset([ Moves.FIRE_PLEDGE, Moves.WATER_PLEDGE ]) + .battleType("double"); + + await game.classicMode.startBattle([ Species.CHARIZARD, Species.CHARIZARD ]); + const enemyPokemon = game.scene.getEnemyPokemon()!; + + game.move.select(Moves.POWDER, 0, BattlerIndex.ENEMY); + game.move.select(Moves.SPLASH, 1); + await game.forceEnemyMove(Moves.FIRE_PLEDGE, BattlerIndex.PLAYER); + await game.forceEnemyMove(Moves.WATER_PLEDGE, BattlerIndex.PLAYER); + await game.setTurnOrder([ BattlerIndex.PLAYER, BattlerIndex.PLAYER_2, BattlerIndex.ENEMY_2, BattlerIndex.ENEMY ]); + + await game.phaseInterceptor.to(BerryPhase, false); + expect(enemyPokemon.getLastXMoves()[0].result).toBe(MoveResult.SUCCESS); + expect(enemyPokemon.hp).toBe(enemyPokemon.getMaxHp()); + }); });