diff --git a/src/data/ability.ts b/src/data/ability.ts index b08f7a22c52..345da9d1b30 100644 --- a/src/data/ability.ts +++ b/src/data/ability.ts @@ -334,7 +334,7 @@ export class ReceivedMoveDamageMultiplierAbAttr extends PreDefendAbAttr { * Reduces the damage dealt to an allied Pokemon. Used by Friend Guard. * @see {@linkcode applyPreDefend} */ -export class FriendGuardAbAttr extends PreDefendAbAttr { +export class AlliedFieldDamageReductionAbAttr extends PreDefendAbAttr { private damageMultiplier: number; constructor(damageMultiplier: number) { @@ -5331,7 +5331,7 @@ export function initAbilities() { new Ability(Abilities.HEALER, 5) .conditionalAttr(pokemon => pokemon.getAlly() && Utils.randSeedInt(10) < 3, PostTurnResetStatusAbAttr, true), new Ability(Abilities.FRIEND_GUARD, 5) - .attr(FriendGuardAbAttr, 0.75) + .attr(AlliedFieldDamageReductionAbAttr, 0.75) .ignorable(), new Ability(Abilities.WEAK_ARMOR, 5) .attr(PostDefendStatStageChangeAbAttr, (target, user, move) => move.category === MoveCategory.PHYSICAL, Stat.DEF, -1) diff --git a/src/field/pokemon.ts b/src/field/pokemon.ts index 6425336a284..a22e16c9db5 100644 --- a/src/field/pokemon.ts +++ b/src/field/pokemon.ts @@ -22,7 +22,7 @@ import { reverseCompatibleTms, tmSpecies, tmPoolTiers } from "#app/data/balance/ import { BattlerTag, BattlerTagLapseType, EncoreTag, GroundedTag, HighestStatBoostTag, SubstituteTag, TypeImmuneTag, getBattlerTag, SemiInvulnerableTag, TypeBoostTag, MoveRestrictionBattlerTag, ExposedTag, DragonCheerTag, CritBoostTag, TrappedTag, TarShotTag, AutotomizedTag, PowerTrickTag } from "../data/battler-tags"; import { WeatherType } from "#app/data/weather"; import { ArenaTagSide, NoCritTag, WeakenMoveScreenTag } from "#app/data/arena-tag"; -import { Ability, AbAttr, StatMultiplierAbAttr, BlockCritAbAttr, BonusCritAbAttr, BypassBurnDamageReductionAbAttr, FieldPriorityMoveImmunityAbAttr, IgnoreOpponentStatStagesAbAttr, MoveImmunityAbAttr, PreDefendFullHpEndureAbAttr, ReceivedMoveDamageMultiplierAbAttr, ReduceStatusEffectDurationAbAttr, StabBoostAbAttr, StatusEffectImmunityAbAttr, TypeImmunityAbAttr, WeightMultiplierAbAttr, allAbilities, applyAbAttrs, applyStatMultiplierAbAttrs, applyPreApplyBattlerTagAbAttrs, applyPreAttackAbAttrs, applyPreDefendAbAttrs, applyPreSetStatusAbAttrs, UnsuppressableAbilityAbAttr, SuppressFieldAbilitiesAbAttr, NoFusionAbilityAbAttr, MultCritAbAttr, IgnoreTypeImmunityAbAttr, DamageBoostAbAttr, IgnoreTypeStatusEffectImmunityAbAttr, ConditionalCritAbAttr, applyFieldStatMultiplierAbAttrs, FieldMultiplyStatAbAttr, AddSecondStrikeAbAttr, UserFieldStatusEffectImmunityAbAttr, UserFieldBattlerTagImmunityAbAttr, BattlerTagImmunityAbAttr, MoveTypeChangeAbAttr, FullHpResistTypeAbAttr, applyCheckTrappedAbAttrs, CheckTrappedAbAttr, PostSetStatusAbAttr, applyPostSetStatusAbAttrs, InfiltratorAbAttr, FriendGuardAbAttr } from "#app/data/ability"; +import { Ability, AbAttr, StatMultiplierAbAttr, BlockCritAbAttr, BonusCritAbAttr, BypassBurnDamageReductionAbAttr, FieldPriorityMoveImmunityAbAttr, IgnoreOpponentStatStagesAbAttr, MoveImmunityAbAttr, PreDefendFullHpEndureAbAttr, ReceivedMoveDamageMultiplierAbAttr, StabBoostAbAttr, StatusEffectImmunityAbAttr, TypeImmunityAbAttr, WeightMultiplierAbAttr, allAbilities, applyAbAttrs, applyStatMultiplierAbAttrs, applyPreApplyBattlerTagAbAttrs, applyPreAttackAbAttrs, applyPreDefendAbAttrs, applyPreSetStatusAbAttrs, UnsuppressableAbilityAbAttr, SuppressFieldAbilitiesAbAttr, NoFusionAbilityAbAttr, MultCritAbAttr, IgnoreTypeImmunityAbAttr, DamageBoostAbAttr, IgnoreTypeStatusEffectImmunityAbAttr, ConditionalCritAbAttr, applyFieldStatMultiplierAbAttrs, FieldMultiplyStatAbAttr, AddSecondStrikeAbAttr, UserFieldStatusEffectImmunityAbAttr, UserFieldBattlerTagImmunityAbAttr, BattlerTagImmunityAbAttr, MoveTypeChangeAbAttr, FullHpResistTypeAbAttr, applyCheckTrappedAbAttrs, CheckTrappedAbAttr, PostSetStatusAbAttr, applyPostSetStatusAbAttrs, InfiltratorAbAttr, AlliedFieldDamageReductionAbAttr } from "#app/data/ability"; import PokemonData from "#app/system/pokemon-data"; import { BattlerIndex } from "#app/battle"; import { Mode } from "#app/ui/ui"; @@ -2674,7 +2674,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { /** Additionally apply friend guard damage reduction if ally has it. */ if (this.scene.currentBattle.double && this.getAlly()?.isActive(true)) { - applyPreDefendAbAttrs(FriendGuardAbAttr, this.getAlly(), source, move, cancelled, simulated, damage); + applyPreDefendAbAttrs(AlliedFieldDamageReductionAbAttr, this.getAlly(), source, move, cancelled, simulated, damage); } } diff --git a/src/test/abilities/friend_guard.test.ts b/src/test/abilities/friend_guard.test.ts index 3918062ff51..01efe5f23dc 100644 --- a/src/test/abilities/friend_guard.test.ts +++ b/src/test/abilities/friend_guard.test.ts @@ -3,15 +3,14 @@ import { Species } from "#enums/species"; import { Abilities } from "#enums/abilities"; import GameManager from "#test/utils/gameManager"; import Phaser from "phaser"; -import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest"; +import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; import { BattlerIndex } from "#app/battle"; -import { PlayerPokemon } from "#app/field/pokemon"; +import { allAbilities } from "#app/data/ability"; describe("Moves - Friend Guard", () => { let phaserGame: Phaser.Game; let game: GameManager; - let hp1: number; - let hp2: number; + beforeAll(() => { phaserGame = new Phaser.Game({ type: Phaser.HEADLESS, @@ -27,32 +26,84 @@ describe("Moves - Friend Guard", () => { game.override .battleType("double") .enemyAbility(Abilities.BALL_FETCH) - .enemyMoveset([ Moves.TACKLE, Moves.SPLASH ]) + .enemyMoveset([ Moves.TACKLE, Moves.SPLASH, Moves.DRAGON_RAGE ]) .enemySpecies(Species.SHUCKLE) - .moveset([ Moves.SPLASH ]); + .moveset([ Moves.SPLASH ]) + .startingLevel(100); }); - it("first part of test, getting hp without friend guard", async () => { - await game.classicMode.startBattle([ Species.BULBASAUR, Species.BULBASAUR ]); + it("should reduce damage that other allied Pokémon receive from attacks (from any Pokémon) by 25%", async () => { + await game.classicMode.startBattle([ Species.BULBASAUR, Species.CHARMANDER ]); const [ player1, player2 ] = game.scene.getPlayerField(); + const maxHP = player1.hp; game.move.select(Moves.SPLASH); game.move.select(Moves.SPLASH, 1); await game.forceEnemyMove(Moves.TACKLE, BattlerIndex.PLAYER); await game.forceEnemyMove(Moves.SPLASH); await game.toNextTurn(); - hp1 = party[0].hp; - }); + const hp1 = player1.hp; + + // Reset HP to maxHP + player1.hp = maxHP; + + vi.spyOn(player2, "getAbility").mockReturnValue(allAbilities[Abilities.FRIEND_GUARD]); - it("second part of test, getting hp with friend guard and comparing", async () => { - game.override.ability(Abilities.FRIEND_GUARD); - await game.classicMode.startBattle([ Species.BULBASAUR, Species.BULBASAUR ]); - const party = game.scene.getParty()! as PlayerPokemon[]; game.move.select(Moves.SPLASH); game.move.select(Moves.SPLASH, 1); await game.forceEnemyMove(Moves.TACKLE, BattlerIndex.PLAYER); await game.forceEnemyMove(Moves.SPLASH); await game.toNextTurn(); - hp2 = party[0].hp; + const hp2 = player1.hp; expect(hp2).toBeGreaterThan(hp1); }); + + it("should NOT reduce damage to pokemon with friend guard", async () => { + await game.classicMode.startBattle([ Species.BULBASAUR, Species.CHARMANDER ]); + const [ , player2 ] = game.scene.getPlayerField(); + const maxHP = player2.hp; + game.move.select(Moves.SPLASH); + game.move.select(Moves.SPLASH, 1); + await game.forceEnemyMove(Moves.TACKLE, BattlerIndex.PLAYER_2); + await game.forceEnemyMove(Moves.SPLASH); + await game.toNextTurn(); + const hp1 = player2.hp; + + // Reset HP to maxHP + player2.hp = maxHP; + + vi.spyOn(player2, "getAbility").mockReturnValue(allAbilities[Abilities.FRIEND_GUARD]); + + game.move.select(Moves.SPLASH); + game.move.select(Moves.SPLASH, 1); + await game.forceEnemyMove(Moves.TACKLE, BattlerIndex.PLAYER_2); + await game.forceEnemyMove(Moves.SPLASH); + await game.toNextTurn(); + const hp2 = player2.hp; + expect(hp2).toBe(hp1); + }); + + it("should NOT reduce damage from fixed damage attacks (i.e. dragon rage, sonic boom, etc)", async () => { + await game.classicMode.startBattle([ Species.BULBASAUR, Species.CHARMANDER ]); + const [ player1, player2 ] = game.scene.getPlayerField(); + const maxHP = player1.hp; + game.move.select(Moves.SPLASH); + game.move.select(Moves.SPLASH, 1); + await game.forceEnemyMove(Moves.DRAGON_RAGE, BattlerIndex.PLAYER); + await game.forceEnemyMove(Moves.SPLASH); + await game.toNextTurn(); + const hp1 = player1.hp; + + // Reset HP to maxHP + player1.hp = maxHP; + + vi.spyOn(player2, "getAbility").mockReturnValue(allAbilities[Abilities.FRIEND_GUARD]); + + game.move.select(Moves.SPLASH); + game.move.select(Moves.SPLASH, 1); + await game.forceEnemyMove(Moves.DRAGON_RAGE, BattlerIndex.PLAYER); + await game.forceEnemyMove(Moves.SPLASH); + await game.toNextTurn(); + const hp2 = player1.hp; + expect(hp2).toBe(hp1); + }); });