diff --git a/src/data/abilities/ability.ts b/src/data/abilities/ability.ts index 70195d6a152..66e7981b904 100644 --- a/src/data/abilities/ability.ts +++ b/src/data/abilities/ability.ts @@ -6542,13 +6542,26 @@ export class PostFaintContactDamageAbAttr extends PostFaintAbAttr { _hitResult?: HitResult, ..._args: any[] ): boolean { - const diedToDirectDamage = - move !== undefined && - attacker !== undefined && - move.doesFlagEffectApply({ flag: MoveFlags.MAKES_CONTACT, user: attacker, target: pokemon }); + // Return early if ability user did not die to a direct-contact attack. + if ( + move === undefined || + attacker === undefined || + !move.doesFlagEffectApply({ flag: MoveFlags.MAKES_CONTACT, user: attacker, target: pokemon }) + ) { + return false; + } + const cancelled = new BooleanHolder(false); - globalScene.getField(true).map(p => applyAbAttrs("FieldPreventExplosiveMovesAbAttr", p, cancelled, simulated)); - return !(!diedToDirectDamage || cancelled.value || attacker!.hasAbilityWithAttr("BlockNonDirectDamageAbAttr")); + // TODO: This should be in speed order + globalScene.getField(true).forEach(p => applyAbAttrs("FieldPreventExplosiveMovesAbAttr", p, cancelled, simulated)); + + if (cancelled.value) { + return false; + } + + // TODO: Does aftermath display text if the attacker has Magic Guard? + applyAbAttrs("BlockNonDirectDamageAbAttr", attacker, cancelled); + return !cancelled.value; } override applyPostFaint( @@ -6557,15 +6570,15 @@ export class PostFaintContactDamageAbAttr extends PostFaintAbAttr { simulated: boolean, attacker?: Pokemon, _move?: Move, - _hitResult?: HitResult, - ..._args: any[] ): void { - if (!simulated) { - attacker!.damageAndUpdate(toDmgValue(attacker!.getMaxHp() * (1 / this.damageRatio)), { - result: HitResult.INDIRECT, - }); - attacker!.turnData.damageTaken += toDmgValue(attacker!.getMaxHp() * (1 / this.damageRatio)); + if (simulated) { + return; } + + attacker!.damageAndUpdate(toDmgValue(attacker!.getMaxHp() * (1 / this.damageRatio)), { + result: HitResult.INDIRECT, + }); + attacker!.turnData.damageTaken += toDmgValue(attacker!.getMaxHp() * (1 / this.damageRatio)); } getTriggerMessage(pokemon: Pokemon, abilityName: string, ..._args: any[]): string { @@ -6577,27 +6590,38 @@ export class PostFaintContactDamageAbAttr extends PostFaintAbAttr { } /** - * Attribute used for abilities (Innards Out) that damage the opponent based on how much HP the last attack used to knock out the owner of the ability. + * Attribute used for abilities that damage an opponent who faints the ability holder + * equal to the amount of damage the last attack inflicted. + * + * Used for {@linkcode Abilities.INNARDS_OUT}. */ export class PostFaintHPDamageAbAttr extends PostFaintAbAttr { override applyPostFaint( pokemon: Pokemon, _passive: boolean, - simulated: boolean, + _simulated: boolean, attacker?: Pokemon, move?: Move, - _hitResult?: HitResult, - ..._args: any[] ): void { - //If the mon didn't die to indirect damage - if (move !== undefined && attacker !== undefined && !simulated) { - const damage = pokemon.turnData.attacksReceived[0].damage; - attacker.damageAndUpdate(damage, { result: HitResult.INDIRECT }); - attacker.turnData.damageTaken += damage; + // return early if the user died to indirect damage, target has magic guard or was KO'd by an ally + if (move === undefined || attacker === undefined || attacker.getAlly() === pokemon) { + return; } + + // TODO: Confirm that magic guard's flyout shows here? + const cancelled = new BooleanHolder(false); + applyAbAttrs("BlockNonDirectDamageAbAttr", attacker, cancelled); + if (cancelled.value) { + return; + } + + const damage = pokemon.turnData.attacksReceived[0].damage; + attacker.damageAndUpdate(damage, { result: HitResult.INDIRECT }); + attacker.turnData.damageTaken += damage; } - getTriggerMessage(pokemon: Pokemon, abilityName: string, ..._args: any[]): string { + // Oddly, Innards Out still shows a flyout if the effect was blocked due to Magic Guard... + override getTriggerMessage(pokemon: Pokemon, abilityName: string): string { return i18next.t("abilityTriggers:postFaintHpDamage", { pokemonNameWithAffix: getPokemonNameWithAffix(pokemon), abilityName, diff --git a/test/abilities/innards-out.test.ts b/test/abilities/innards-out.test.ts new file mode 100644 index 00000000000..0c3efb7c1aa --- /dev/null +++ b/test/abilities/innards-out.test.ts @@ -0,0 +1,62 @@ +import { AbilityId } from "#enums/ability-id"; +import { MoveId } from "#enums/move-id"; +import { SpeciesId } from "#enums/species-id"; +import GameManager from "#test/testUtils/gameManager"; +import Phaser from "phaser"; +import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest"; + +describe("Abilities - Innards Out", () => { + 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 + .ability(AbilityId.BALL_FETCH) + .battleStyle("single") + .criticalHits(false) + .enemySpecies(SpeciesId.MAGIKARP) + .enemyAbility(AbilityId.INNARDS_OUT) + .enemyMoveset(MoveId.SPLASH) + .startingLevel(100); + }); + + it("should damage opppnents that faint the ability holder for equal damage", async () => { + await game.classicMode.startBattle([SpeciesId.FEEBAS]); + + const magikarp = game.field.getEnemyPokemon(); + magikarp.hp = 20; + game.move.use(MoveId.X_SCISSOR); + await game.toEndOfTurn(); + + expect(magikarp.isFainted()).toBe(true); + const feebas = game.field.getPlayerPokemon(); + expect(feebas.getInverseHp()).toBe(20); + }); + + it("should not damage an ally in Double Battles", async () => { + game.override.battleStyle("double"); + await game.classicMode.startBattle([SpeciesId.FEEBAS]); + + const [magikarp1, magikarp2] = game.scene.getEnemyField(); + magikarp1.hp = 1; + + game.move.use(MoveId.PROTECT); + await game.move.forceEnemyMove(MoveId.SPLASH); + await game.move.forceEnemyMove(MoveId.SURF); + await game.toEndOfTurn(); + + expect(magikarp1.isFainted()).toBe(true); + expect(magikarp2.getInverseHp()).toBe(0); + }); +}); diff --git a/test/abilities/magic_guard.test.ts b/test/abilities/magic_guard.test.ts index f135a761bba..b10616abe7c 100644 --- a/test/abilities/magic_guard.test.ts +++ b/test/abilities/magic_guard.test.ts @@ -1,19 +1,18 @@ -import { getArenaTag } from "#app/data/arena-tag"; +import { BattlerIndex } from "#enums/battler-index"; import { ArenaTagSide } from "#enums/arena-tag-side"; +import { allMoves } from "#app/data/data-lists"; import { getStatusEffectCatchRateMultiplier } from "#app/data/status-effect"; -import { TurnEndPhase } from "#app/phases/turn-end-phase"; +import { toDmgValue } from "#app/utils/common"; import { AbilityId } from "#enums/ability-id"; import { ArenaTagType } from "#enums/arena-tag-type"; -import { BattlerTagType } from "#enums/battler-tag-type"; import { MoveId } from "#enums/move-id"; import { SpeciesId } from "#enums/species-id"; import { StatusEffect } from "#enums/status-effect"; -import { WeatherType } from "#enums/weather-type"; import GameManager from "#test/testUtils/gameManager"; import Phaser from "phaser"; -import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest"; +import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; -describe("Abilities - Magic Guard", () => { +describe("AbilityId - Magic Guard", () => { let phaserGame: Phaser.Game; let game: GameManager; @@ -29,404 +28,142 @@ describe("Abilities - Magic Guard", () => { beforeEach(() => { game = new GameManager(phaserGame); - game.override - /** Player Pokemon overrides */ .ability(AbilityId.MAGIC_GUARD) - .moveset([MoveId.SPLASH]) + .enemySpecies(SpeciesId.BLISSEY) + .enemyAbility(AbilityId.NO_GUARD) .startingLevel(100) - /** Enemy Pokemon overrides */ - .enemySpecies(SpeciesId.SNORLAX) - .enemyAbility(AbilityId.INSOMNIA) - .enemyMoveset(MoveId.SPLASH) .enemyLevel(100); }); //Bulbapedia Reference: https://bulbapedia.bulbagarden.net/wiki/Magic_Guard_(Ability) - it("ability should prevent damage caused by weather", async () => { - game.override.weather(WeatherType.SANDSTORM); - + it.each<{ name: string; move?: MoveId; enemyMove?: MoveId }>([ + { name: "Non-Volatile Status Conditions", enemyMove: MoveId.TOXIC }, + { name: "Volatile Status Conditions", enemyMove: MoveId.LEECH_SEED }, + { name: "Crash Damage", move: MoveId.HIGH_JUMP_KICK }, + { name: "Variable Recoil Moves", move: MoveId.DOUBLE_EDGE }, + { name: "HP% Recoil Moves", move: MoveId.CHLOROBLAST }, + ])("should prevent damage from $name", async ({ move = MoveId.SPLASH, enemyMove = MoveId.SPLASH }) => { await game.classicMode.startBattle([SpeciesId.MAGIKARP]); + // force a miss on HJK + vi.spyOn(allMoves[MoveId.HIGH_JUMP_KICK], "accuracy", "get").mockReturnValue(0); - const leadPokemon = game.scene.getPlayerPokemon()!; + game.move.use(move); + await game.move.forceEnemyMove(enemyMove); + await game.toEndOfTurn(); - const enemyPokemon = game.scene.getEnemyPokemon()!; - expect(enemyPokemon).toBeDefined(); - - game.move.select(MoveId.SPLASH); - - await game.phaseInterceptor.to(TurnEndPhase); - - /** - * Expect: - * - The player Pokemon (with Magic Guard) has not taken damage from weather - * - The enemy Pokemon (without Magic Guard) has taken damage from weather - */ - expect(leadPokemon.hp).toBe(leadPokemon.getMaxHp()); - expect(enemyPokemon.hp).toBeLessThan(enemyPokemon.getMaxHp()); + const magikarp = game.field.getPlayerPokemon(); + expect(magikarp.hp).toBe(magikarp.getMaxHp()); }); - it("ability should prevent damage caused by status effects but other non-damage effects still apply", async () => { - //Toxic keeps track of the turn counters -> important that Magic Guard keeps track of post-Toxic turns - game.override.statusEffect(StatusEffect.POISON); + it.each<{ abName: string; move?: MoveId; enemyMove?: MoveId; passive?: AbilityId; enemyAbility?: AbilityId }>([ + { abName: "Bad Dreams", enemyMove: MoveId.SPORE, enemyAbility: AbilityId.BAD_DREAMS }, + { abName: "Aftermath", move: MoveId.PSYCHIC_FANGS, enemyAbility: AbilityId.AFTERMATH }, + { abName: "Innards Out", move: MoveId.PSYCHIC_FANGS, enemyAbility: AbilityId.INNARDS_OUT }, + { abName: "Rough Skin", move: MoveId.PSYCHIC_FANGS, enemyAbility: AbilityId.ROUGH_SKIN }, + { abName: "Dry Skin", move: MoveId.SUNNY_DAY, passive: AbilityId.DRY_SKIN }, + { abName: "Liquid Ooze", move: MoveId.DRAIN_PUNCH, enemyAbility: AbilityId.LIQUID_OOZE }, + ])( + "should prevent damage from $abName", + async ({ + move = MoveId.SPLASH, + enemyMove = MoveId.SPLASH, + passive = AbilityId.BALL_FETCH, + enemyAbility = AbilityId.BALL_FETCH, + }) => { + game.override.enemyLevel(1).passiveAbility(passive).enemyAbility(enemyAbility); + await game.classicMode.startBattle([SpeciesId.MAGIKARP]); + game.move.use(move); + await game.move.forceEnemyMove(enemyMove); + await game.toEndOfTurn(); + + const magikarp = game.field.getPlayerPokemon(); + expect(magikarp.hp).toBe(magikarp.getMaxHp()); + }, + ); + + it.each<{ name: string; move?: MoveId; enemyMove?: MoveId }>([ + { name: "Struggle recoil", move: MoveId.STRUGGLE }, + { name: "Self-induced HP cutting", move: MoveId.BELLY_DRUM }, + { name: "Confusion self-damage", enemyMove: MoveId.CONFUSE_RAY }, + ])("should not prevent damage from $name", async ({ move = MoveId.SPLASH, enemyMove = MoveId.SPLASH }) => { + game.override.confusionActivation(true); await game.classicMode.startBattle([SpeciesId.MAGIKARP]); - const leadPokemon = game.scene.getPlayerPokemon()!; + game.move.use(move); + await game.move.forceEnemyMove(enemyMove); + await game.setTurnOrder([BattlerIndex.ENEMY, BattlerIndex.PLAYER]); // For confuse ray + await game.toEndOfTurn(); - game.move.select(MoveId.SPLASH); - - await game.phaseInterceptor.to(TurnEndPhase); - - /** - * Expect: - * - The player Pokemon (with Magic Guard) has not taken damage from poison - * - The Pokemon's CatchRateMultiplier should be 1.5 - */ - expect(leadPokemon.hp).toBe(leadPokemon.getMaxHp()); - expect(getStatusEffectCatchRateMultiplier(leadPokemon.status!.effect)).toBe(1.5); - }); - - it("ability effect should not persist when the ability is replaced", async () => { - game.override.enemyMoveset(MoveId.WORRY_SEED).statusEffect(StatusEffect.POISON); - - await game.classicMode.startBattle([SpeciesId.MAGIKARP]); - - const leadPokemon = game.scene.getPlayerPokemon()!; - - game.move.select(MoveId.SPLASH); - - await game.phaseInterceptor.to(TurnEndPhase); - - /** - * Expect: - * - The player Pokemon (that just lost its Magic Guard ability) has taken damage from poison - */ + const leadPokemon = game.field.getPlayerPokemon(); expect(leadPokemon.hp).toBeLessThan(leadPokemon.getMaxHp()); }); - it("Magic Guard prevents damage caused by burn but other non-damaging effects are still applied", async () => { - game.override.enemyStatusEffect(StatusEffect.BURN).enemyAbility(AbilityId.MAGIC_GUARD); - + it("should preserve toxic turn count and deal appropriate damage when disabled", async () => { + game.override.statusEffect(StatusEffect.TOXIC); await game.classicMode.startBattle([SpeciesId.MAGIKARP]); - game.move.select(MoveId.SPLASH); + game.move.use(MoveId.SPLASH); + await game.move.forceEnemyMove(MoveId.SPLASH); + await game.toNextTurn(); - const enemyPokemon = game.scene.getEnemyPokemon()!; + const magikarp = game.field.getPlayerPokemon(); + expect(magikarp.hp).toBe(magikarp.getMaxHp()); + expect(magikarp.status?.toxicTurnCount).toBe(1); - await game.phaseInterceptor.to(TurnEndPhase); + // have a few turns pass + game.move.use(MoveId.SPLASH); + await game.toNextTurn(); + game.move.use(MoveId.SPLASH); + await game.toNextTurn(); + game.move.use(MoveId.SPLASH); + await game.toNextTurn(); + expect(magikarp.status?.toxicTurnCount).toBe(4); - /** - * Expect: - * - The enemy Pokemon (with Magic Guard) has not taken damage from burn - * - The enemy Pokemon's physical attack damage is halved (TBD) - * - The enemy Pokemon's hypothetical CatchRateMultiplier should be 1.5 - */ - expect(enemyPokemon.hp).toBe(enemyPokemon.getMaxHp()); - expect(getStatusEffectCatchRateMultiplier(enemyPokemon.status!.effect)).toBe(1.5); + game.move.use(MoveId.SPLASH); + await game.move.forceEnemyMove(MoveId.GASTRO_ACID); + await game.toNextTurn(); + + expect(magikarp.status?.toxicTurnCount).toBe(5); + expect(magikarp.getHpRatio(true)).toBeCloseTo(11 / 16, 1); }); - it("Magic Guard prevents damage caused by toxic but other non-damaging effects are still applied", async () => { - game.override.enemyStatusEffect(StatusEffect.TOXIC).enemyAbility(AbilityId.MAGIC_GUARD); - + it("should preserve burn physical damage halving & status catch boost", async () => { await game.classicMode.startBattle([SpeciesId.MAGIKARP]); - game.move.select(MoveId.SPLASH); + // NB: Burn applies directly to the physical dmg formula, so we can't just check attack here + game.move.use(MoveId.TACKLE); + await game.move.forceEnemyMove(MoveId.WILL_O_WISP); + await game.setTurnOrder([BattlerIndex.PLAYER, BattlerIndex.ENEMY]); + await game.toNextTurn(); - const enemyPokemon = game.scene.getEnemyPokemon()!; + const magikarp = game.field.getPlayerPokemon(); + expect(magikarp.hp).toBe(magikarp.getMaxHp()); + expect(magikarp.status?.effect).toBe(StatusEffect.BURN); - const toxicStartCounter = enemyPokemon.status!.toxicTurnCount; - //should be 0 + const blissey = game.field.getEnemyPokemon(); + const prevDmg = blissey.getInverseHp(); + blissey.hp = blissey.getMaxHp(); - await game.phaseInterceptor.to(TurnEndPhase); + expect(getStatusEffectCatchRateMultiplier(magikarp.status!.effect)).toBe(1.5); - /** - * Expect: - * - The enemy Pokemon (with Magic Guard) has not taken damage from toxic - * - The enemy Pokemon's status effect duration should be incremented - * - The enemy Pokemon's hypothetical CatchRateMultiplier should be 1.5 - */ - expect(enemyPokemon.hp).toBe(enemyPokemon.getMaxHp()); - expect(enemyPokemon.status!.toxicTurnCount).toBeGreaterThan(toxicStartCounter); - expect(getStatusEffectCatchRateMultiplier(enemyPokemon.status!.effect)).toBe(1.5); + game.move.use(MoveId.TACKLE); + await game.toNextTurn(); + + const burntDmg = blissey.getInverseHp(); + expect(burntDmg).toBeCloseTo(toDmgValue(prevDmg / 2), 0); }); - it("Magic Guard prevents damage caused by entry hazards", async () => { - //Adds and applies Spikes to both sides of the arena - const newTag = getArenaTag(ArenaTagType.SPIKES, 5, MoveId.SPIKES, 0, 0, ArenaTagSide.BOTH)!; - game.scene.arena.tags.push(newTag); - - await game.classicMode.startBattle([SpeciesId.MAGIKARP]); - const leadPokemon = game.scene.getPlayerPokemon()!; - - game.move.select(MoveId.SPLASH); - - const enemyPokemon = game.scene.getEnemyPokemon()!; - - await game.phaseInterceptor.to(TurnEndPhase); - - /** - * Expect: - * - The player Pokemon (with Magic Guard) has not taken damage from spikes - * - The enemy Pokemon (without Magic Guard) has taken damage from spikes - */ - expect(leadPokemon.hp).toBe(leadPokemon.getMaxHp()); - expect(enemyPokemon.hp).toBeLessThan(enemyPokemon.getMaxHp()); - }); - - it("Magic Guard does not prevent poison from Toxic Spikes", async () => { - //Adds and applies Spikes to both sides of the arena - const playerTag = getArenaTag(ArenaTagType.TOXIC_SPIKES, 5, MoveId.TOXIC_SPIKES, 0, 0, ArenaTagSide.PLAYER)!; - const enemyTag = getArenaTag(ArenaTagType.TOXIC_SPIKES, 5, MoveId.TOXIC_SPIKES, 0, 0, ArenaTagSide.ENEMY)!; - game.scene.arena.tags.push(playerTag); - game.scene.arena.tags.push(enemyTag); - - await game.classicMode.startBattle([SpeciesId.MAGIKARP]); - const leadPokemon = game.scene.getPlayerPokemon()!; - - game.move.select(MoveId.SPLASH); - - const enemyPokemon = game.scene.getEnemyPokemon()!; - - await game.phaseInterceptor.to(TurnEndPhase); - - /** - * Expect: - * - Both Pokemon gain the poison status effect - * - The player Pokemon (with Magic Guard) has not taken damage from poison - * - The enemy Pokemon (without Magic Guard) has taken damage from poison - */ - expect(leadPokemon.status!.effect).toBe(StatusEffect.POISON); - expect(enemyPokemon.status!.effect).toBe(StatusEffect.POISON); - expect(leadPokemon.hp).toBe(leadPokemon.getMaxHp()); - expect(enemyPokemon.hp).toBeLessThan(enemyPokemon.getMaxHp()); - }); - - it("Magic Guard prevents against damage from volatile status effects", async () => { - await game.classicMode.startBattle([SpeciesId.DUSKULL]); - game.override.moveset([MoveId.CURSE]).enemyAbility(AbilityId.MAGIC_GUARD); - - const leadPokemon = game.scene.getPlayerPokemon()!; - - game.move.select(MoveId.CURSE); - - const enemyPokemon = game.scene.getEnemyPokemon()!; - - await game.phaseInterceptor.to(TurnEndPhase); - - /** - * Expect: - * - The player Pokemon (with Magic Guard) has cut its HP to inflict curse - * - The enemy Pokemon (with Magic Guard) is cursed - * - The enemy Pokemon (with Magic Guard) does not lose HP from being cursed - */ - expect(leadPokemon.hp).toBeLessThan(leadPokemon.getMaxHp()); - expect(enemyPokemon.getTag(BattlerTagType.CURSED)).not.toBe(undefined); - expect(enemyPokemon.hp).toBe(enemyPokemon.getMaxHp()); - }); - - it("Magic Guard prevents crash damage", async () => { - game.override.moveset([MoveId.HIGH_JUMP_KICK]); + it("should prevent damage from entry hazards, but not Toxic Spikes poison", async () => { + game.scene.arena.addTag(ArenaTagType.SPIKES, -1, MoveId.SPIKES, 0, ArenaTagSide.PLAYER); + game.scene.arena.addTag(ArenaTagType.TOXIC_SPIKES, -1, MoveId.TOXIC_SPIKES, 0, ArenaTagSide.PLAYER); await game.classicMode.startBattle([SpeciesId.MAGIKARP]); - const leadPokemon = game.scene.getPlayerPokemon()!; - - game.move.select(MoveId.HIGH_JUMP_KICK); - await game.move.forceMiss(); - - await game.phaseInterceptor.to(TurnEndPhase); - - /** - * Expect: - * - The player Pokemon (with Magic Guard) misses High Jump Kick but does not lose HP as a result - */ - expect(leadPokemon.hp).toBe(leadPokemon.getMaxHp()); - }); - - it("Magic Guard prevents damage from recoil", async () => { - game.override.moveset([MoveId.TAKE_DOWN]); - await game.classicMode.startBattle([SpeciesId.MAGIKARP]); - - const leadPokemon = game.scene.getPlayerPokemon()!; - - game.move.select(MoveId.TAKE_DOWN); - - await game.phaseInterceptor.to(TurnEndPhase); - - /** - * Expect: - * - The player Pokemon (with Magic Guard) uses a recoil move but does not lose HP from recoil - */ - expect(leadPokemon.hp).toBe(leadPokemon.getMaxHp()); - }); - - it("Magic Guard does not prevent damage from Struggle's recoil", async () => { - game.override.moveset([MoveId.STRUGGLE]); - await game.classicMode.startBattle([SpeciesId.MAGIKARP]); - - const leadPokemon = game.scene.getPlayerPokemon()!; - - game.move.select(MoveId.STRUGGLE); - - await game.phaseInterceptor.to(TurnEndPhase); - - /** - * Expect: - * - The player Pokemon (with Magic Guard) uses Struggle but does lose HP from Struggle's recoil - */ - expect(leadPokemon.hp).toBeLessThan(leadPokemon.getMaxHp()); - }); - - //This tests different move attributes than the recoil tests above - it("Magic Guard prevents self-damage from attacking moves", async () => { - game.override.moveset([MoveId.STEEL_BEAM]); - await game.classicMode.startBattle([SpeciesId.MAGIKARP]); - - const leadPokemon = game.scene.getPlayerPokemon()!; - - game.move.select(MoveId.STEEL_BEAM); - - await game.phaseInterceptor.to(TurnEndPhase); - - /** - * Expect: - * - The player Pokemon (with Magic Guard) uses a move with an HP cost but does not lose HP from using it - */ - expect(leadPokemon.hp).toBe(leadPokemon.getMaxHp()); - }); - - /* - it("Magic Guard does not prevent self-damage from confusion", async () => { - await game.classicMode.startBattle([SpeciesId.MAGIKARP]); - - game.move.select(MoveId.CHARM); - - await game.phaseInterceptor.to(TurnEndPhase); - }); -*/ - - it("Magic Guard does not prevent self-damage from non-attacking moves", async () => { - game.override.moveset([MoveId.BELLY_DRUM]); - await game.classicMode.startBattle([SpeciesId.MAGIKARP]); - - const leadPokemon = game.scene.getPlayerPokemon()!; - - game.move.select(MoveId.BELLY_DRUM); - - await game.phaseInterceptor.to(TurnEndPhase); - - /** - * Expect: - * - The player Pokemon (with Magic Guard) uses a non-attacking move with an HP cost and thus loses HP from using it - */ - expect(leadPokemon.hp).toBeLessThan(leadPokemon.getMaxHp()); - }); - - it("Magic Guard prevents damage from abilities with PostTurnHurtIfSleepingAbAttr", async () => { - //Tests the ability Bad Dreams - game.override.statusEffect(StatusEffect.SLEEP); - //enemy pokemon is given Spore just in case player pokemon somehow awakens during test - game.override - .enemyMoveset([MoveId.SPORE, MoveId.SPORE, MoveId.SPORE, MoveId.SPORE]) - .enemyAbility(AbilityId.BAD_DREAMS); - - await game.classicMode.startBattle([SpeciesId.MAGIKARP]); - - const leadPokemon = game.scene.getPlayerPokemon()!; - - game.move.select(MoveId.SPLASH); - - await game.phaseInterceptor.to(TurnEndPhase); - - /** - * Expect: - * - The player Pokemon (with Magic Guard) should not lose HP due to this ability attribute - * - The player Pokemon is asleep - */ - expect(leadPokemon.hp).toBe(leadPokemon.getMaxHp()); - expect(leadPokemon.status!.effect).toBe(StatusEffect.SLEEP); - }); - - it("Magic Guard prevents damage from abilities with PostFaintContactDamageAbAttr", async () => { - //Tests the abilities Innards Out/Aftermath - game.override.moveset([MoveId.TACKLE]).enemyAbility(AbilityId.AFTERMATH); - - await game.classicMode.startBattle([SpeciesId.MAGIKARP]); - - const leadPokemon = game.scene.getPlayerPokemon()!; - - const enemyPokemon = game.scene.getEnemyPokemon()!; - enemyPokemon.hp = 1; - - game.move.select(MoveId.TACKLE); - await game.phaseInterceptor.to(TurnEndPhase); - - /** - * Expect: - * - The player Pokemon (with Magic Guard) should not lose HP due to this ability attribute - * - The enemy Pokemon has fainted - */ - expect(enemyPokemon.hp).toBe(0); - expect(leadPokemon.hp).toBe(leadPokemon.getMaxHp()); - }); - - it("Magic Guard prevents damage from abilities with PostDefendContactDamageAbAttr", async () => { - //Tests the abilities Iron Barbs/Rough Skin - game.override.moveset([MoveId.TACKLE]).enemyAbility(AbilityId.IRON_BARBS); - - await game.classicMode.startBattle([SpeciesId.MAGIKARP]); - - const leadPokemon = game.scene.getPlayerPokemon()!; - - const enemyPokemon = game.scene.getEnemyPokemon()!; - - game.move.select(MoveId.TACKLE); - await game.phaseInterceptor.to(TurnEndPhase); - - /** - * Expect: - * - The player Pokemon (with Magic Guard) should not lose HP due to this ability attribute - * - The player Pokemon's move should have connected - */ - expect(enemyPokemon.hp).toBeLessThan(enemyPokemon.getMaxHp()); - expect(leadPokemon.hp).toBe(leadPokemon.getMaxHp()); - }); - - it("Magic Guard prevents damage from abilities with ReverseDrainAbAttr", async () => { - //Tests the ability Liquid Ooze - game.override.moveset([MoveId.ABSORB]).enemyAbility(AbilityId.LIQUID_OOZE); - - await game.classicMode.startBattle([SpeciesId.MAGIKARP]); - - const leadPokemon = game.scene.getPlayerPokemon()!; - - const enemyPokemon = game.scene.getEnemyPokemon()!; - - game.move.select(MoveId.ABSORB); - await game.phaseInterceptor.to(TurnEndPhase); - - /** - * Expect: - * - The player Pokemon (with Magic Guard) should not lose HP due to this ability attribute - * - The player Pokemon's move should have connected - */ - expect(enemyPokemon.hp).toBeLessThan(enemyPokemon.getMaxHp()); - expect(leadPokemon.hp).toBe(leadPokemon.getMaxHp()); - }); - - it("Magic Guard prevents HP loss from abilities with PostWeatherLapseDamageAbAttr", async () => { - game.override.passiveAbility(AbilityId.SOLAR_POWER).weather(WeatherType.SUNNY); - - await game.classicMode.startBattle([SpeciesId.MAGIKARP]); - const leadPokemon = game.scene.getPlayerPokemon()!; - game.move.select(MoveId.SPLASH); - await game.phaseInterceptor.to(TurnEndPhase); - - /** - * Expect: - * - The player Pokemon (with Magic Guard) should not lose HP due to this ability attribute - */ - expect(leadPokemon.hp).toBe(leadPokemon.getMaxHp()); + // Magic guard prevented damage but not poison + const player = game.field.getPlayerPokemon(); + expect(player.hp).toBe(player.getMaxHp()); + expect(player.status?.effect).toBe(StatusEffect.POISON); }); }); diff --git a/test/battle/battle.test.ts b/test/battle/battle.test.ts index bf2c3968aa6..71cd367dde2 100644 --- a/test/battle/battle.test.ts +++ b/test/battle/battle.test.ts @@ -26,7 +26,7 @@ import Phaser from "phaser"; import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; import { BiomeId } from "#enums/biome-id"; -describe("Test Battle Phase", () => { +describe("Phase - Battle Phase", () => { let phaserGame: Phaser.Game; let game: GameManager; @@ -197,47 +197,25 @@ describe("Test Battle Phase", () => { await game.phaseInterceptor.runFrom(SelectGenderPhase).to(SummonPhase); }); - it("2vs1", async () => { - game.override.battleStyle("single"); - game.override.enemySpecies(SpeciesId.MIGHTYENA); - game.override.enemyAbility(AbilityId.HYDRATION); - game.override.ability(AbilityId.HYDRATION); - await game.classicMode.startBattle([SpeciesId.BLASTOISE, SpeciesId.CHARIZARD]); - expect(game.scene.ui?.getMode()).toBe(UiMode.COMMAND); - expect(game.scene.phaseManager.getCurrentPhase()!.constructor.name).toBe(CommandPhase.name); - }, 20000); + it.each([ + { name: "1v1", double: false, qty: 1 }, + { name: "2v1", double: false, qty: 2 }, + { name: "2v2", double: true, qty: 2 }, + { name: "4v2", double: true, qty: 4 }, + ])("should not crash when starting $name battle", async ({ double, qty }) => { + game.override + .battleStyle(double ? "double" : "single") + .enemySpecies(SpeciesId.MIGHTYENA) + .enemyAbility(AbilityId.HYDRATION) + .ability(AbilityId.HYDRATION); - it("1vs1", async () => { - game.override.battleStyle("single"); - game.override.enemySpecies(SpeciesId.MIGHTYENA); - game.override.enemyAbility(AbilityId.HYDRATION); - game.override.ability(AbilityId.HYDRATION); - await game.classicMode.startBattle([SpeciesId.BLASTOISE]); - expect(game.scene.ui?.getMode()).toBe(UiMode.COMMAND); - expect(game.scene.phaseManager.getCurrentPhase()!.constructor.name).toBe(CommandPhase.name); - }, 20000); + await game.classicMode.startBattle( + [SpeciesId.BLASTOISE, SpeciesId.CHARIZARD, SpeciesId.DARKRAI, SpeciesId.GABITE].slice(0, qty), + ); - it("2vs2", async () => { - game.override.battleStyle("double"); - game.override.enemySpecies(SpeciesId.MIGHTYENA); - game.override.enemyAbility(AbilityId.HYDRATION); - game.override.ability(AbilityId.HYDRATION); - game.override.startingWave(3); - await game.classicMode.startBattle([SpeciesId.BLASTOISE, SpeciesId.CHARIZARD]); expect(game.scene.ui?.getMode()).toBe(UiMode.COMMAND); - expect(game.scene.phaseManager.getCurrentPhase()!.constructor.name).toBe(CommandPhase.name); - }, 20000); - - it("4vs2", async () => { - game.override.battleStyle("double"); - game.override.enemySpecies(SpeciesId.MIGHTYENA); - game.override.enemyAbility(AbilityId.HYDRATION); - game.override.ability(AbilityId.HYDRATION); - game.override.startingWave(3); - await game.classicMode.startBattle([SpeciesId.BLASTOISE, SpeciesId.CHARIZARD, SpeciesId.DARKRAI, SpeciesId.GABITE]); - expect(game.scene.ui?.getMode()).toBe(UiMode.COMMAND); - expect(game.scene.phaseManager.getCurrentPhase()!.constructor.name).toBe(CommandPhase.name); - }, 20000); + expect(game.scene.phaseManager.getCurrentPhase()).toBeInstanceOf(CommandPhase); + }); it("kill opponent pokemon", async () => { const moveToUse = MoveId.SPLASH; diff --git a/test/testUtils/helpers/overridesHelper.ts b/test/testUtils/helpers/overridesHelper.ts index 3bf0fbbda47..2016da61ea5 100644 --- a/test/testUtils/helpers/overridesHelper.ts +++ b/test/testUtils/helpers/overridesHelper.ts @@ -209,9 +209,9 @@ export class OverridesHelper extends GameManagerHelper { } /** - * Override the player pokemon's {@linkcode StatusEffect | status-effect} + * Override the player pokemon's initial {@linkcode StatusEffect | status-effect}, * @param statusEffect - The {@linkcode StatusEffect | status-effect} to set - * @returns + * @returns `this` */ public statusEffect(statusEffect: StatusEffect): this { vi.spyOn(Overrides, "STATUS_OVERRIDE", "get").mockReturnValue(statusEffect); @@ -401,9 +401,9 @@ export class OverridesHelper extends GameManagerHelper { } /** - * Override the enemy {@linkcode StatusEffect | status-effect} for enemy pokemon + * Override the enemy pokemon's initial {@linkcode StatusEffect | status-effect}. * @param statusEffect - The {@linkcode StatusEffect | status-effect} to set - * @returns + * @returns `this` */ public enemyStatusEffect(statusEffect: StatusEffect): this { vi.spyOn(Overrides, "OPP_STATUS_OVERRIDE", "get").mockReturnValue(statusEffect);