From 3241332a3ce0e4307e196d29de947dc5a0d6345d Mon Sep 17 00:00:00 2001 From: PrabbyDD <147005742+PrabbyDD@users.noreply.github.com> Date: Mon, 28 Oct 2024 13:49:04 -0700 Subject: [PATCH 1/2] [Ability] Implementing Friend Guard (#4718) * implementing friend guard ability * Update src/data/ability.ts Co-authored-by: NightKev <34855794+DayKev@users.noreply.github.com> * Update src/data/ability.ts Co-authored-by: NightKev <34855794+DayKev@users.noreply.github.com> * Update src/data/ability.ts Co-authored-by: NightKev <34855794+DayKev@users.noreply.github.com> * Update src/test/abilities/friend_guard.test.ts Co-authored-by: NightKev <34855794+DayKev@users.noreply.github.com> * Update src/field/pokemon.ts Co-authored-by: innerthunder <168692175+innerthunder@users.noreply.github.com> * more tests and fixing other issues * more tests and fixing other issues * Update Friend Guard test with damage mocks --------- Co-authored-by: NightKev <34855794+DayKev@users.noreply.github.com> Co-authored-by: innerthunder <168692175+innerthunder@users.noreply.github.com> Co-authored-by: innerthunder --- src/data/ability.ts | 28 +++++- src/field/pokemon.ts | 8 +- src/test/abilities/friend_guard.test.ts | 120 ++++++++++++++++++++++++ 3 files changed, 153 insertions(+), 3 deletions(-) create mode 100644 src/test/abilities/friend_guard.test.ts diff --git a/src/data/ability.ts b/src/data/ability.ts index 01e99966ff8..58824603bc3 100644 --- a/src/data/ability.ts +++ b/src/data/ability.ts @@ -330,6 +330,30 @@ export class ReceivedMoveDamageMultiplierAbAttr extends PreDefendAbAttr { } } +/** + * Reduces the damage dealt to an allied Pokemon. Used by Friend Guard. + * @see {@linkcode applyPreDefend} + */ +export class AlliedFieldDamageReductionAbAttr extends PreDefendAbAttr { + private damageMultiplier: number; + + constructor(damageMultiplier: number) { + super(); + this.damageMultiplier = damageMultiplier; + } + + /** + * Handles the damage reduction + * @param args + * - `[0]` {@linkcode Utils.NumberHolder} - The damage being dealt + */ + override applyPreDefend(_pokemon: Pokemon, _passive: boolean, _simulated: boolean, _attacker: Pokemon, _move: Move, _cancelled: Utils.BooleanHolder, args: any[]): boolean { + const damage = args[0] as Utils.NumberHolder; + damage.value = Utils.toDmgValue(damage.value * this.damageMultiplier); + return true; + } +} + export class ReceivedTypeDamageMultiplierAbAttr extends ReceivedMoveDamageMultiplierAbAttr { constructor(moveType: Type, damageMultiplier: number) { super((target, user, move) => user.getMoveType(move) === moveType, damageMultiplier); @@ -5310,8 +5334,8 @@ export function initAbilities() { new Ability(Abilities.HEALER, 5) .conditionalAttr(pokemon => pokemon.getAlly() && Utils.randSeedInt(10) < 3, PostTurnResetStatusAbAttr, true), new Ability(Abilities.FRIEND_GUARD, 5) - .ignorable() - .unimplemented(), + .attr(AlliedFieldDamageReductionAbAttr, 0.75) + .ignorable(), new Ability(Abilities.WEAK_ARMOR, 5) .attr(PostDefendStatStageChangeAbAttr, (target, user, move) => move.category === MoveCategory.PHYSICAL, Stat.DEF, -1) .attr(PostDefendStatStageChangeAbAttr, (target, user, move) => move.category === MoveCategory.PHYSICAL, Stat.SPD, 2), diff --git a/src/field/pokemon.ts b/src/field/pokemon.ts index 4cb8390d4f4..278a28937ef 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, 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 } 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"; @@ -2667,9 +2667,15 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { this.scene.applyModifiers(EnemyDamageReducerModifier, false, damage); } + /** Apply this Pokemon's post-calc defensive modifiers (e.g. Fur Coat) */ if (!ignoreAbility) { applyPreDefendAbAttrs(ReceivedMoveDamageMultiplierAbAttr, this, source, move, cancelled, simulated, damage); + + /** Additionally apply friend guard damage reduction if ally has it. */ + if (this.scene.currentBattle.double && this.getAlly()?.isActive(true)) { + applyPreDefendAbAttrs(AlliedFieldDamageReductionAbAttr, this.getAlly(), source, move, cancelled, simulated, damage); + } } // This attribute may modify damage arbitrarily, so be careful about changing its order of application. diff --git a/src/test/abilities/friend_guard.test.ts b/src/test/abilities/friend_guard.test.ts new file mode 100644 index 00000000000..4ce64468c43 --- /dev/null +++ b/src/test/abilities/friend_guard.test.ts @@ -0,0 +1,120 @@ +import { Moves } from "#enums/moves"; +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, vi } from "vitest"; +import { BattlerIndex } from "#app/battle"; +import { allAbilities } from "#app/data/ability"; +import { allMoves, MoveCategory } from "#app/data/move"; + +describe("Moves - Friend Guard", () => { + 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("double") + .enemyAbility(Abilities.BALL_FETCH) + .enemyMoveset([ Moves.TACKLE, Moves.SPLASH, Moves.DRAGON_RAGE ]) + .enemySpecies(Species.SHUCKLE) + .moveset([ Moves.SPLASH ]) + .startingLevel(100); + }); + + 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 spy = vi.spyOn(player1, "getAttackDamage"); + + const enemy1 = game.scene.getEnemyField()[0]; + + 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(); + + // Get the last return value from `getAttackDamage` + const turn1Damage = spy.mock.results[spy.mock.results.length - 1].value.damage; + // Making sure the test is controlled; turn 1 damage is equal to base damage (after rounding) + expect(turn1Damage).toBe(Math.floor(player1.getBaseDamage(enemy1, allMoves[Moves.TACKLE], MoveCategory.PHYSICAL))); + + 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); + await game.forceEnemyMove(Moves.SPLASH); + await game.toNextTurn(); + + // Get the last return value from `getAttackDamage` + const turn2Damage = spy.mock.results[spy.mock.results.length - 1].value.damage; + // With the ally's Friend Guard, damage should have been reduced from base damage by 25% + expect(turn2Damage).toBe(Math.floor(player1.getBaseDamage(enemy1, allMoves[Moves.TACKLE], MoveCategory.PHYSICAL) * 0.75)); + }); + + it("should NOT reduce damage to pokemon with friend guard", async () => { + await game.classicMode.startBattle([ Species.BULBASAUR, Species.CHARMANDER ]); + + const player2 = game.scene.getPlayerField()[1]; + const spy = vi.spyOn(player2, "getAttackDamage"); + + 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 turn1Damage = spy.mock.results[spy.mock.results.length - 1].value.damage; + + 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 turn2Damage = spy.mock.results[spy.mock.results.length - 1].value.damage; + expect(turn2Damage).toBe(turn1Damage); + }); + + it("should NOT reduce damage from fixed damage attacks", async () => { + await game.classicMode.startBattle([ Species.BULBASAUR, Species.CHARMANDER ]); + + const [ player1, player2 ] = game.scene.getPlayerField(); + const spy = vi.spyOn(player1, "getAttackDamage"); + + 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 turn1Damage = spy.mock.results[spy.mock.results.length - 1].value.damage; + expect(turn1Damage).toBe(40); + + 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 turn2Damage = spy.mock.results[spy.mock.results.length - 1].value.damage; + expect(turn2Damage).toBe(40); + }); +}); From 13377806ff74508e358e51f0d9cdfaed1d81ef54 Mon Sep 17 00:00:00 2001 From: AJ Fontaine <36677462+Fontbane@users.noreply.github.com> Date: Mon, 28 Oct 2024 18:58:46 -0400 Subject: [PATCH 2/2] [Refactor] Change attack type booster names to enum (#4725) --- src/modifier/modifier-type.ts | 62 ++++++++++++----------------------- 1 file changed, 21 insertions(+), 41 deletions(-) diff --git a/src/modifier/modifier-type.ts b/src/modifier/modifier-type.ts index cf2d74483f9..e68e9a06fae 100644 --- a/src/modifier/modifier-type.ts +++ b/src/modifier/modifier-type.ts @@ -502,45 +502,25 @@ export class BerryModifierType extends PokemonHeldItemModifierType implements Ge } } -function getAttackTypeBoosterItemName(type: Type) { - switch (type) { - case Type.NORMAL: - return "Silk Scarf"; - case Type.FIGHTING: - return "Black Belt"; - case Type.FLYING: - return "Sharp Beak"; - case Type.POISON: - return "Poison Barb"; - case Type.GROUND: - return "Soft Sand"; - case Type.ROCK: - return "Hard Stone"; - case Type.BUG: - return "Silver Powder"; - case Type.GHOST: - return "Spell Tag"; - case Type.STEEL: - return "Metal Coat"; - case Type.FIRE: - return "Charcoal"; - case Type.WATER: - return "Mystic Water"; - case Type.GRASS: - return "Miracle Seed"; - case Type.ELECTRIC: - return "Magnet"; - case Type.PSYCHIC: - return "Twisted Spoon"; - case Type.ICE: - return "Never-Melt Ice"; - case Type.DRAGON: - return "Dragon Fang"; - case Type.DARK: - return "Black Glasses"; - case Type.FAIRY: - return "Fairy Feather"; - } +enum AttackTypeBoosterItem { + SILK_SCARF, + BLACK_BELT, + SHARP_BEAK, + POISON_BARB, + SOFT_SAND, + HARD_STONE, + SILVER_POWDER, + SPELL_TAG, + METAL_COAT, + CHARCOAL, + MYSTIC_WATER, + MIRACLE_SEED, + MAGNET, + TWISTED_SPOON, + NEVER_MELT_ICE, + DRAGON_FANG, + BLACK_GLASSES, + FAIRY_FEATHER } export class AttackTypeBoosterModifierType extends PokemonHeldItemModifierType implements GeneratedPersistentModifierType { @@ -548,7 +528,7 @@ export class AttackTypeBoosterModifierType extends PokemonHeldItemModifierType i public boostPercent: integer; constructor(moveType: Type, boostPercent: integer) { - super("", `${getAttackTypeBoosterItemName(moveType)?.replace(/[ \-]/g, "_").toLowerCase()}`, + super("", `${AttackTypeBoosterItem[moveType]?.toLowerCase()}`, (_type, args) => new AttackTypeBoosterModifier(this, (args[0] as Pokemon).id, moveType, boostPercent)); this.moveType = moveType; @@ -556,7 +536,7 @@ export class AttackTypeBoosterModifierType extends PokemonHeldItemModifierType i } get name(): string { - return i18next.t(`modifierType:AttackTypeBoosterItem.${getAttackTypeBoosterItemName(this.moveType)?.replace(/[ \-]/g, "_").toLowerCase()}`); + return i18next.t(`modifierType:AttackTypeBoosterItem.${AttackTypeBoosterItem[this.moveType]?.toLowerCase()}`); } getDescription(scene: BattleScene): string {