From 70671875322e7855e9f93bfbc53722c16ad3fbcf Mon Sep 17 00:00:00 2001 From: Yiling Kang Date: Mon, 1 Jul 2024 02:29:39 -0700 Subject: [PATCH 1/8] Initial changes for Synchronize ability --- src/data/ability.ts | 70 +++++++- src/field/pokemon.ts | 3 +- src/test/abilities/synchronize.test.ts | 212 +++++++++++++++++++++++++ 3 files changed, 283 insertions(+), 2 deletions(-) create mode 100644 src/test/abilities/synchronize.test.ts diff --git a/src/data/ability.ts b/src/data/ability.ts index b1f0d2b197c..0aa4c948f75 100644 --- a/src/data/ability.ts +++ b/src/data/ability.ts @@ -1623,6 +1623,68 @@ export class PostDefendStealHeldItemAbAttr extends PostDefendAbAttr { } } +/** + * Base class for defining all {@linkcode Ability} Attributes after a status effect has been set. + * @see {@linkcode applyPostSetStatus()}. + */ +export class PostSetStatusAbAttr extends AbAttr { + /** + * Does nothing after a status condition is set. + * @param pokemon {@linkcode Pokemon} that status condition was set on. + * @param sourcePokemon {@linkcode Pokemon} that that set the status condition. Is null if status was not set by a Pokemon. + * @param passive Whether this ability is a passive. + * @param effect {@linkcode StatusEffect} that was set. + * @param args Set of unique arguments needed by this attribute. + * @returns true if application of the ability succeeds. + */ + applyPostSetStatus( + pokemon: Pokemon, + sourcePokemon: Pokemon = null, + passive: boolean, + effect: StatusEffect, + args: any[]) : boolean | Promise { + return false; + } +} + +/** + * If another Pokemon burns, paralyzes, poisons, or badly poisons this Pokemon, + * that Pokemon receives the same non-volatile status condition as part of this + * ability attribute. For Synchronize ability. + */ +export class SynchronizeStatusAbAttr extends PostSetStatusAbAttr { + /** + * If the StatusEffect that was set is Burn, Paralysis, Poison, or Toxic, and the status + * was set by a source Pokemon, set the source Pokemon's status to the same StatusEffect. + * @param pokemon {@linkcode Pokemon} that status condition was set on. + * @param sourcePokemon {@linkcode Pokemon} that that set the status condition. Is null if status was not set by a Pokemon. + * @param passive Whether this ability is a passive. + * @param effect {@linkcode StatusEffect} that was set. + * @param args Set of unique arguments needed by this attribute. + * @returns true if application of the ability succeeds. + */ + applyPostSetStatus( + pokemon: Pokemon, + sourcePokemon: Pokemon = null, + passive: boolean, + effect: StatusEffect, + args: any[]): boolean { + // Synchronizable statuses + const syncStatuses = new Set([ + StatusEffect.BURN, + StatusEffect.PARALYSIS, + StatusEffect.POISON, + StatusEffect.TOXIC + ]); + + if (sourcePokemon && syncStatuses.has(effect)) { + return sourcePokemon.trySetStatus(effect, true); + } + + return false; + } +} + export class PostVictoryAbAttr extends AbAttr { applyPostVictory(pokemon: Pokemon, passive: boolean, args: any[]): boolean | Promise { return false; @@ -4012,6 +4074,11 @@ export function applyPostMoveUsedAbAttrs(attrType: Constructor(attrType, pokemon, (attr, passive) => attr.applyPostMoveUsed(pokemon, move, source, targets, args), args); } +export function applyPostSetStatusAbAttrs(attrType: Constructor, + pokemon: Pokemon, effect: StatusEffect, sourcePokemon?: Pokemon, ...args: any[]): Promise { + return applyAbAttrsInternal(attrType, pokemon, (attr, passive) => attr.applyPostSetStatus(pokemon, sourcePokemon, passive, effect, args), args); +} + export function applyBattleStatMultiplierAbAttrs(attrType: Constructor, pokemon: Pokemon, battleStat: BattleStat, statValue: Utils.NumberHolder, ...args: any[]): Promise { return applyAbAttrsInternal(attrType, pokemon, (attr, passive) => attr.applyBattleStat(pokemon, passive, battleStat, statValue, args), args); @@ -4239,7 +4306,8 @@ export function initAbilities() { .attr(EffectSporeAbAttr), new Ability(Abilities.SYNCHRONIZE, 3) .attr(SyncEncounterNatureAbAttr) - .unimplemented(), + .attr(SynchronizeStatusAbAttr) + .partial(), // interaction with psycho shift needs work new Ability(Abilities.CLEAR_BODY, 3) .attr(ProtectStatAbAttr) .ignorable(), diff --git a/src/field/pokemon.ts b/src/field/pokemon.ts index 3ee19920ae9..37f7616b511 100644 --- a/src/field/pokemon.ts +++ b/src/field/pokemon.ts @@ -23,7 +23,7 @@ import { BattlerTag, BattlerTagLapseType, EncoreTag, GroundedTag, HelpingHandTag import { WeatherType } from "../data/weather"; import { TempBattleStat } from "../data/temp-battle-stat"; import { ArenaTagSide, WeakenMoveScreenTag, WeakenMoveTypeTag } from "../data/arena-tag"; -import { Ability, AbAttr, BattleStatMultiplierAbAttr, BlockCritAbAttr, BonusCritAbAttr, BypassBurnDamageReductionAbAttr, FieldPriorityMoveImmunityAbAttr, IgnoreOpponentStatChangesAbAttr, MoveImmunityAbAttr, MoveTypeChangeAttr, PreApplyBattlerTagAbAttr, PreDefendFullHpEndureAbAttr, ReceivedMoveDamageMultiplierAbAttr, ReduceStatusEffectDurationAbAttr, StabBoostAbAttr, StatusEffectImmunityAbAttr, TypeImmunityAbAttr, VariableMovePowerAbAttr, WeightMultiplierAbAttr, allAbilities, applyAbAttrs, applyBattleStatMultiplierAbAttrs, applyPreApplyBattlerTagAbAttrs, applyPreAttackAbAttrs, applyPreDefendAbAttrs, applyPreSetStatusAbAttrs, UnsuppressableAbilityAbAttr, SuppressFieldAbilitiesAbAttr, NoFusionAbilityAbAttr, MultCritAbAttr, IgnoreTypeImmunityAbAttr, DamageBoostAbAttr, IgnoreTypeStatusEffectImmunityAbAttr, ConditionalCritAbAttr, applyFieldBattleStatMultiplierAbAttrs, FieldMultiplyBattleStatAbAttr, AllyMoveCategoryPowerBoostAbAttr, FieldMoveTypePowerBoostAbAttr, AddSecondStrikeAbAttr } from "../data/ability"; +import { Ability, AbAttr, BattleStatMultiplierAbAttr, BlockCritAbAttr, BonusCritAbAttr, BypassBurnDamageReductionAbAttr, FieldPriorityMoveImmunityAbAttr, IgnoreOpponentStatChangesAbAttr, MoveImmunityAbAttr, MoveTypeChangeAttr, PreApplyBattlerTagAbAttr, PreDefendFullHpEndureAbAttr, ReceivedMoveDamageMultiplierAbAttr, ReduceStatusEffectDurationAbAttr, StabBoostAbAttr, StatusEffectImmunityAbAttr, TypeImmunityAbAttr, VariableMovePowerAbAttr, WeightMultiplierAbAttr, allAbilities, applyAbAttrs, applyBattleStatMultiplierAbAttrs, applyPreApplyBattlerTagAbAttrs, applyPreAttackAbAttrs, applyPreDefendAbAttrs, applyPreSetStatusAbAttrs, UnsuppressableAbilityAbAttr, SuppressFieldAbilitiesAbAttr, NoFusionAbilityAbAttr, MultCritAbAttr, IgnoreTypeImmunityAbAttr, DamageBoostAbAttr, IgnoreTypeStatusEffectImmunityAbAttr, ConditionalCritAbAttr, applyFieldBattleStatMultiplierAbAttrs, FieldMultiplyBattleStatAbAttr, AllyMoveCategoryPowerBoostAbAttr, FieldMoveTypePowerBoostAbAttr, AddSecondStrikeAbAttr, PostSetStatusAbAttr, applyPostSetStatusAbAttrs } from "../data/ability"; import PokemonData from "../system/pokemon-data"; import { BattlerIndex } from "../battle"; import { Mode } from "../ui/ui"; @@ -2586,6 +2586,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { if (effect !== StatusEffect.FAINT) { this.scene.triggerPokemonFormChange(this, SpeciesFormChangeStatusEffectTrigger, true); + applyPostSetStatusAbAttrs(PostSetStatusAbAttr, this, effect, sourcePokemon); } return true; diff --git a/src/test/abilities/synchronize.test.ts b/src/test/abilities/synchronize.test.ts new file mode 100644 index 00000000000..302312f1f6a --- /dev/null +++ b/src/test/abilities/synchronize.test.ts @@ -0,0 +1,212 @@ +import {afterEach, beforeAll, beforeEach, describe, expect, it, vi} from "vitest"; +import Phaser from "phaser"; +import GameManager from "#app/test/utils/gameManager"; +import * as overrides from "#app/overrides"; +import { + MoveEffectPhase, + TurnEndPhase, +} from "#app/phases"; +import {getMovePosition} from "#app/test/utils/gameManagerUtils"; +import { Abilities } from "#enums/abilities"; +import { Moves } from "#enums/moves"; +import { Species } from "#enums/species"; +import { StatusEffect } from "#app/data/status-effect.js"; + +describe("Abilities - Synchronize", () => { + let phaserGame: Phaser.Game; + let game: GameManager; + + beforeAll(() => { + phaserGame = new Phaser.Game({ + type: Phaser.HEADLESS, + }); + }); + + afterEach(() => { + game.phaseInterceptor.restoreOg(); + }); + + beforeEach(() => { + game = new GameManager(phaserGame); + vi.spyOn(overrides, "SINGLE_BATTLE_OVERRIDE", "get").mockReturnValue(true); + vi.spyOn(overrides, "STARTING_LEVEL_OVERRIDE", "get").mockReturnValue(100); + vi.spyOn(overrides, "STATUS_OVERRIDE", "get").mockReturnValue(StatusEffect.NONE); + vi.spyOn(overrides, "OPP_STATUS_OVERRIDE", "get").mockReturnValue(StatusEffect.NONE); + // Opponent mocks + vi.spyOn(overrides, "OPP_SPECIES_OVERRIDE", "get").mockReturnValue(Species.ALAKAZAM); + vi.spyOn(overrides, "OPP_MOVESET_OVERRIDE", "get").mockReturnValue([Moves.PSYCHIC, Moves.CALM_MIND, Moves.FOCUS_BLAST, Moves.RECOVER]); + vi.spyOn(overrides, "OPP_ABILITY_OVERRIDE", "get").mockReturnValue(Abilities.SYNCHRONIZE); + }, 20000); + + it("does not trigger when no status is applied by opponent Pokemon", async () => { + // Arrange + const moveToUse = Moves.HEADBUTT; + + // Starter mocks + vi.spyOn(overrides, "STARTER_SPECIES_OVERRIDE", "get").mockReturnValue(Species.ZIGZAGOON); + vi.spyOn(overrides, "MOVESET_OVERRIDE", "get").mockReturnValue([moveToUse]); + vi.spyOn(overrides, "ABILITY_OVERRIDE", "get").mockReturnValue(Abilities.PICKUP); + + // Act + await game.startBattle(); + game.doAttack(getMovePosition(game.scene, 0, moveToUse)); + await game.phaseInterceptor.to(TurnEndPhase); + + // Assert + expect(game.scene.getParty()[0].status).toBe(undefined); + expect(game.phaseInterceptor.log).not.toContain("ShowAbilityPhase"); + }, 20000); + + it("sets the status of the source pokemon to Paralysis when paralyzed by it", async () => { + // Arrange + const moveToUse = Moves.THUNDER_WAVE; + + // Starter mocks + vi.spyOn(overrides, "STARTER_SPECIES_OVERRIDE", "get").mockReturnValue(Species.TOGEKISS); + vi.spyOn(overrides, "MOVESET_OVERRIDE", "get").mockReturnValue([moveToUse]); + vi.spyOn(overrides, "ABILITY_OVERRIDE", "get").mockReturnValue(Abilities.SERENE_GRACE); + + // Act + await game.startBattle(); + game.doAttack(getMovePosition(game.scene, 0, moveToUse)); + + await game.phaseInterceptor.to(MoveEffectPhase, false); + vi.spyOn(game.scene.getCurrentPhase() as MoveEffectPhase, "hitCheck").mockReturnValue(true); + + await game.phaseInterceptor.to(TurnEndPhase); + + // Assert + expect(game.scene.getParty()[0].status?.effect).toBe(StatusEffect.PARALYSIS); + expect(game.scene.getEnemyParty()[0].status?.effect).toBe(StatusEffect.PARALYSIS); + expect(game.phaseInterceptor.log).toContain("ShowAbilityPhase"); + }, 20000); + + it("sets the status of the source pokemon to Burned when burn is applied by it", async () => { + // Arrange + const moveToUse = Moves.WILL_O_WISP; + + // Starter mocks + vi.spyOn(overrides, "STARTER_SPECIES_OVERRIDE", "get").mockReturnValue(Species.EEVEE); + vi.spyOn(overrides, "MOVESET_OVERRIDE", "get").mockReturnValue([moveToUse]); + vi.spyOn(overrides, "ABILITY_OVERRIDE", "get").mockReturnValue(Abilities.GUTS); + + // Act + await game.startBattle(); + game.doAttack(getMovePosition(game.scene, 0, moveToUse)); + + await game.phaseInterceptor.to(MoveEffectPhase, false); + vi.spyOn(game.scene.getCurrentPhase() as MoveEffectPhase, "hitCheck").mockReturnValue(true); + + await game.phaseInterceptor.to(TurnEndPhase); + + // Assert + expect(game.scene.getParty()[0].status?.effect).toBe(StatusEffect.BURN); + expect(game.scene.getEnemyParty()[0].status?.effect).toBe(StatusEffect.BURN); + expect(game.phaseInterceptor.log).toContain("ShowAbilityPhase"); + }, 20000); + + it("sets the status of the source pokemon to Poisoned when poison is applied by it", async () => { + // Arrange + const moveToUse = Moves.POISON_POWDER; + + // Starter mocks + vi.spyOn(overrides, "STARTER_SPECIES_OVERRIDE", "get").mockReturnValue(Species.BRELOOM); + vi.spyOn(overrides, "MOVESET_OVERRIDE", "get").mockReturnValue([moveToUse]); + vi.spyOn(overrides, "ABILITY_OVERRIDE", "get").mockReturnValue(Abilities.TECHNICIAN); + + // Act + await game.startBattle(); + game.doAttack(getMovePosition(game.scene, 0, moveToUse)); + + await game.phaseInterceptor.to(MoveEffectPhase, false); + vi.spyOn(game.scene.getCurrentPhase() as MoveEffectPhase, "hitCheck").mockReturnValue(true); + + await game.phaseInterceptor.to(TurnEndPhase); + + // Assert + expect(game.scene.getParty()[0].status?.effect).toBe(StatusEffect.POISON); + expect(game.scene.getEnemyParty()[0].status?.effect).toBe(StatusEffect.POISON); + expect(game.phaseInterceptor.log).toContain("ShowAbilityPhase"); + }, 20000); + + it("sets the status of the source pokemon to Toxic when toxic is applied by it", async () => { + // Arrange + const moveToUse = Moves.TOXIC; + + // Starter mocks + vi.spyOn(overrides, "STARTER_SPECIES_OVERRIDE", "get").mockReturnValue(Species.QUAGSIRE); + vi.spyOn(overrides, "MOVESET_OVERRIDE", "get").mockReturnValue([moveToUse]); + vi.spyOn(overrides, "ABILITY_OVERRIDE", "get").mockReturnValue(Abilities.DAMP); + + // Act + await game.startBattle(); + game.doAttack(getMovePosition(game.scene, 0, moveToUse)); + + await game.phaseInterceptor.to(MoveEffectPhase, false); + vi.spyOn(game.scene.getCurrentPhase() as MoveEffectPhase, "hitCheck").mockReturnValue(true); + + await game.phaseInterceptor.to(TurnEndPhase); + + // Assert + expect(game.scene.getParty()[0].status?.effect).toBe(StatusEffect.TOXIC); + expect(game.scene.getEnemyParty()[0].status?.effect).toBe(StatusEffect.TOXIC); + expect(game.phaseInterceptor.log).toContain("ShowAbilityPhase"); + }, 20000); + + it("does not trigger when Pokemon is statused to non Burn, Paralysis, Poison, or Toxic", async () => { + // Arrange + const moveToUse = Moves.SPORE; + + // Starter mocks + vi.spyOn(overrides, "STARTER_SPECIES_OVERRIDE", "get").mockReturnValue(Species.BRELOOM); + vi.spyOn(overrides, "MOVESET_OVERRIDE", "get").mockReturnValue([moveToUse]); + vi.spyOn(overrides, "ABILITY_OVERRIDE", "get").mockReturnValue(Abilities.POISON_HEAL); + + // Act + await game.startBattle(); + game.doAttack(getMovePosition(game.scene, 0, moveToUse)); + + await game.phaseInterceptor.to(MoveEffectPhase, false); + vi.spyOn(game.scene.getCurrentPhase() as MoveEffectPhase, "hitCheck").mockReturnValue(true); + + await game.phaseInterceptor.to(TurnEndPhase); + + // Assert + expect(game.scene.getParty()[0].status?.effect).toBe(undefined); + expect(game.scene.getEnemyParty()[0].status?.effect).toBe(StatusEffect.SLEEP); + expect(game.phaseInterceptor.log).not.toContain("ShowAbilityPhase"); + }, 20000); + + it("does not trigger when Pokemon is statused by Toxic Spikes", async () => { + // Arrange + const moveToUse = Moves.SPLASH; + + // Starter mocks + vi.spyOn(overrides, "MOVESET_OVERRIDE", "get").mockReturnValue([moveToUse]); + vi.spyOn(overrides, "ABILITY_OVERRIDE", "get").mockReturnValue(Abilities.SYNCHRONIZE); + + vi.spyOn(overrides, "OPP_SPECIES_OVERRIDE", "get").mockReturnValue(Species.BRELOOM); + vi.spyOn(overrides, "OPP_MOVESET_OVERRIDE", "get").mockReturnValue([Moves.TOXIC_SPIKES]); + vi.spyOn(overrides, "OPP_ABILITY_OVERRIDE", "get").mockReturnValue(Abilities.TECHNICIAN); + + // Act + // Turn 1 - Opponent uses spikes, trainer uses splash + // Turn 2 - Opponent uses splash, trainer sends out Alakazam. Alakazam is toxic-ed but Synchronize should not proc + await game.startBattle([Species.MAGIKARP, Species.ALAKAZAM]); + const leadPokemon = game.scene.getPlayerPokemon(); + expect(leadPokemon).not.toBe(undefined); + + game.doAttack(getMovePosition(game.scene, 0, Moves.SPLASH)); // use splash + + await game.toNextTurn(); + game.doSwitchPokemon(1); + game.doAttack(0); + + await game.phaseInterceptor.to(TurnEndPhase); + + // Assert + expect(game.scene.getParty()[0].status?.effect).toBe(StatusEffect.POISON); + expect(game.scene.getEnemyParty()[0].status?.effect).toBe(undefined); + expect(game.phaseInterceptor.log).not.toContain("ShowAbilityPhase"); + }, 20000); +}); From aadc86dd19ae1debe9672c792f3611d73051e69b Mon Sep 17 00:00:00 2001 From: Yiling Kang Date: Mon, 1 Jul 2024 13:00:33 -0700 Subject: [PATCH 2/8] Fix psycho shift interaction causing buggy behaviour --- src/data/ability.ts | 4 ++-- src/data/move.ts | 7 ++++--- src/test/abilities/synchronize.test.ts | 25 +++++++++++++++++++++++++ 3 files changed, 31 insertions(+), 5 deletions(-) diff --git a/src/data/ability.ts b/src/data/ability.ts index 0aa4c948f75..c657582f596 100644 --- a/src/data/ability.ts +++ b/src/data/ability.ts @@ -1678,7 +1678,7 @@ export class SynchronizeStatusAbAttr extends PostSetStatusAbAttr { ]); if (sourcePokemon && syncStatuses.has(effect)) { - return sourcePokemon.trySetStatus(effect, true); + return sourcePokemon.trySetStatus(effect, true, pokemon); } return false; @@ -4307,7 +4307,7 @@ export function initAbilities() { new Ability(Abilities.SYNCHRONIZE, 3) .attr(SyncEncounterNatureAbAttr) .attr(SynchronizeStatusAbAttr) - .partial(), // interaction with psycho shift needs work + .partial(), // interaction with psycho shift needs work, keeping to old Gen interaction for now new Ability(Abilities.CLEAR_BODY, 3) .attr(ProtectStatAbAttr) .ignorable(), diff --git a/src/data/move.ts b/src/data/move.ts index 5ab4b1e8a64..a1c167f0c7e 100644 --- a/src/data/move.ts +++ b/src/data/move.ts @@ -1761,13 +1761,14 @@ export class PsychoShiftEffectAttr extends MoveEffectAttr { return false; } if (!target.status || (target.status.effect === statusToApply && move.chance < 0)) { - const statusAfflictResult = target.trySetStatus(statusToApply, true, user); - if (statusAfflictResult) { + const canSetStatus = target.canSetStatus(statusToApply, true, false, user); + if (canSetStatus) { user.scene.queueMessage(getPokemonMessage(user, getStatusEffectHealText(user.status.effect))); user.resetStatus(); user.updateInfo(); + target.trySetStatus(statusToApply, true, user); } - return statusAfflictResult; + return canSetStatus; } return false; diff --git a/src/test/abilities/synchronize.test.ts b/src/test/abilities/synchronize.test.ts index 302312f1f6a..f34a6b0103a 100644 --- a/src/test/abilities/synchronize.test.ts +++ b/src/test/abilities/synchronize.test.ts @@ -209,4 +209,29 @@ describe("Abilities - Synchronize", () => { expect(game.scene.getEnemyParty()[0].status?.effect).toBe(undefined); expect(game.phaseInterceptor.log).not.toContain("ShowAbilityPhase"); }, 20000); + + it("should activate with Psycho Shift after the move clears the status", async () => { + // Arrange + const moveToUse = Moves.PSYCHO_SHIFT; + + // Starter mocks + vi.spyOn(overrides, "STARTER_SPECIES_OVERRIDE", "get").mockReturnValue(Species.HOOTHOOT); + vi.spyOn(overrides, "MOVESET_OVERRIDE", "get").mockReturnValue([moveToUse]); + vi.spyOn(overrides, "ABILITY_OVERRIDE", "get").mockReturnValue(Abilities.KEEN_EYE); + vi.spyOn(overrides, "STATUS_OVERRIDE", "get").mockReturnValue(StatusEffect.PARALYSIS); + + // Act + await game.startBattle(); + game.doAttack(getMovePosition(game.scene, 0, moveToUse)); + + await game.phaseInterceptor.to(MoveEffectPhase, false); + vi.spyOn(game.scene.getCurrentPhase() as MoveEffectPhase, "hitCheck").mockReturnValue(true); + + await game.phaseInterceptor.to(TurnEndPhase); + + // Assert + expect(game.scene.getParty()[0].status?.effect).toBe(StatusEffect.PARALYSIS); // keeping old gen < V impl for now since it's buggy otherwise + expect(game.scene.getEnemyParty()[0].status?.effect).toBe(StatusEffect.PARALYSIS); + expect(game.phaseInterceptor.log).toContain("ShowAbilityPhase"); + }, 20000); }); From ec82686305f01ea4f438e9947cb3a5613212d802 Mon Sep 17 00:00:00 2001 From: Yiling Kang Date: Mon, 1 Jul 2024 13:31:38 -0700 Subject: [PATCH 3/8] Update to show ability even if opponent pokemon does not get statused --- src/data/ability.ts | 3 ++- src/test/abilities/synchronize.test.ts | 24 ++++++++++++++++++++++++ 2 files changed, 26 insertions(+), 1 deletion(-) diff --git a/src/data/ability.ts b/src/data/ability.ts index c657582f596..4f8da9d8dd5 100644 --- a/src/data/ability.ts +++ b/src/data/ability.ts @@ -1678,7 +1678,8 @@ export class SynchronizeStatusAbAttr extends PostSetStatusAbAttr { ]); if (sourcePokemon && syncStatuses.has(effect)) { - return sourcePokemon.trySetStatus(effect, true, pokemon); + sourcePokemon.trySetStatus(effect, true, pokemon); + return true; } return false; diff --git a/src/test/abilities/synchronize.test.ts b/src/test/abilities/synchronize.test.ts index f34a6b0103a..3b0218f4428 100644 --- a/src/test/abilities/synchronize.test.ts +++ b/src/test/abilities/synchronize.test.ts @@ -210,6 +210,30 @@ describe("Abilities - Synchronize", () => { expect(game.phaseInterceptor.log).not.toContain("ShowAbilityPhase"); }, 20000); + it("shows ability even if it fails to set the status of the opponent Pokemon", async () => { + // Arrange + const moveToUse = Moves.THUNDER_WAVE; + + // Starter mocks + vi.spyOn(overrides, "STARTER_SPECIES_OVERRIDE", "get").mockReturnValue(Species.PIKACHU); + vi.spyOn(overrides, "MOVESET_OVERRIDE", "get").mockReturnValue([moveToUse]); + vi.spyOn(overrides, "ABILITY_OVERRIDE", "get").mockReturnValue(Abilities.STATIC); + + // Act + await game.startBattle(); + game.doAttack(getMovePosition(game.scene, 0, moveToUse)); + + await game.phaseInterceptor.to(MoveEffectPhase, false); + vi.spyOn(game.scene.getCurrentPhase() as MoveEffectPhase, "hitCheck").mockReturnValue(true); + + await game.phaseInterceptor.to(TurnEndPhase); + + // Assert + expect(game.scene.getParty()[0].status?.effect).toBe(undefined); + expect(game.scene.getEnemyParty()[0].status?.effect).toBe(StatusEffect.PARALYSIS); + expect(game.phaseInterceptor.log).toContain("ShowAbilityPhase"); + }, 20000); + it("should activate with Psycho Shift after the move clears the status", async () => { // Arrange const moveToUse = Moves.PSYCHO_SHIFT; From fd47aa8af48ec0cd3c6faa91734b07e5f269736b Mon Sep 17 00:00:00 2001 From: Yiling Kang Date: Wed, 3 Jul 2024 23:20:14 -0700 Subject: [PATCH 4/8] Fix some spacing --- src/data/move.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/data/move.ts b/src/data/move.ts index 80fd160ab1e..54027886dca 100644 --- a/src/data/move.ts +++ b/src/data/move.ts @@ -1769,7 +1769,7 @@ export class PsychoShiftEffectAttr extends MoveEffectAttr { user.updateInfo(); target.trySetStatus(statusToApply, true, user); } - + return canSetStatus; } From 9ec78c47a7be4bff3f100ab4135b9af566676dc9 Mon Sep 17 00:00:00 2001 From: NightKev <34855794+DayKev@users.noreply.github.com> Date: Sun, 8 Sep 2024 18:27:19 -0700 Subject: [PATCH 5/8] Update tests --- src/test/abilities/synchronize.test.ts | 229 +++++-------------------- 1 file changed, 40 insertions(+), 189 deletions(-) diff --git a/src/test/abilities/synchronize.test.ts b/src/test/abilities/synchronize.test.ts index 3b0218f4428..324570b2618 100644 --- a/src/test/abilities/synchronize.test.ts +++ b/src/test/abilities/synchronize.test.ts @@ -1,16 +1,10 @@ -import {afterEach, beforeAll, beforeEach, describe, expect, it, vi} from "vitest"; -import Phaser from "phaser"; +import { StatusEffect } from "#app/data/status-effect"; import GameManager from "#app/test/utils/gameManager"; -import * as overrides from "#app/overrides"; -import { - MoveEffectPhase, - TurnEndPhase, -} from "#app/phases"; -import {getMovePosition} from "#app/test/utils/gameManagerUtils"; import { Abilities } from "#enums/abilities"; import { Moves } from "#enums/moves"; import { Species } from "#enums/species"; -import { StatusEffect } from "#app/data/status-effect.js"; +import Phaser from "phaser"; +import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest"; describe("Abilities - Synchronize", () => { let phaserGame: Phaser.Game; @@ -28,230 +22,87 @@ describe("Abilities - Synchronize", () => { beforeEach(() => { game = new GameManager(phaserGame); - vi.spyOn(overrides, "SINGLE_BATTLE_OVERRIDE", "get").mockReturnValue(true); - vi.spyOn(overrides, "STARTING_LEVEL_OVERRIDE", "get").mockReturnValue(100); - vi.spyOn(overrides, "STATUS_OVERRIDE", "get").mockReturnValue(StatusEffect.NONE); - vi.spyOn(overrides, "OPP_STATUS_OVERRIDE", "get").mockReturnValue(StatusEffect.NONE); - // Opponent mocks - vi.spyOn(overrides, "OPP_SPECIES_OVERRIDE", "get").mockReturnValue(Species.ALAKAZAM); - vi.spyOn(overrides, "OPP_MOVESET_OVERRIDE", "get").mockReturnValue([Moves.PSYCHIC, Moves.CALM_MIND, Moves.FOCUS_BLAST, Moves.RECOVER]); - vi.spyOn(overrides, "OPP_ABILITY_OVERRIDE", "get").mockReturnValue(Abilities.SYNCHRONIZE); + + game.override + .battleType("single") + .startingLevel(100) + .enemySpecies(Species.MAGIKARP) + .enemyAbility(Abilities.SYNCHRONIZE) + .moveset([Moves.SPLASH, Moves.THUNDER_WAVE, Moves.SPORE, Moves.PSYCHO_SHIFT]) + .ability(Abilities.NO_GUARD); }, 20000); it("does not trigger when no status is applied by opponent Pokemon", async () => { - // Arrange - const moveToUse = Moves.HEADBUTT; + await game.classicMode.startBattle([Species.FEEBAS]); - // Starter mocks - vi.spyOn(overrides, "STARTER_SPECIES_OVERRIDE", "get").mockReturnValue(Species.ZIGZAGOON); - vi.spyOn(overrides, "MOVESET_OVERRIDE", "get").mockReturnValue([moveToUse]); - vi.spyOn(overrides, "ABILITY_OVERRIDE", "get").mockReturnValue(Abilities.PICKUP); + game.move.select(Moves.SPLASH); + await game.phaseInterceptor.to("BerryPhase"); - // Act - await game.startBattle(); - game.doAttack(getMovePosition(game.scene, 0, moveToUse)); - await game.phaseInterceptor.to(TurnEndPhase); - - // Assert - expect(game.scene.getParty()[0].status).toBe(undefined); + expect(game.scene.getParty()[0].status).toBeUndefined(); expect(game.phaseInterceptor.log).not.toContain("ShowAbilityPhase"); }, 20000); it("sets the status of the source pokemon to Paralysis when paralyzed by it", async () => { - // Arrange - const moveToUse = Moves.THUNDER_WAVE; + await game.classicMode.startBattle([Species.FEEBAS]); - // Starter mocks - vi.spyOn(overrides, "STARTER_SPECIES_OVERRIDE", "get").mockReturnValue(Species.TOGEKISS); - vi.spyOn(overrides, "MOVESET_OVERRIDE", "get").mockReturnValue([moveToUse]); - vi.spyOn(overrides, "ABILITY_OVERRIDE", "get").mockReturnValue(Abilities.SERENE_GRACE); + game.move.select(Moves.THUNDER_WAVE); + await game.phaseInterceptor.to("BerryPhase"); - // Act - await game.startBattle(); - game.doAttack(getMovePosition(game.scene, 0, moveToUse)); - - await game.phaseInterceptor.to(MoveEffectPhase, false); - vi.spyOn(game.scene.getCurrentPhase() as MoveEffectPhase, "hitCheck").mockReturnValue(true); - - await game.phaseInterceptor.to(TurnEndPhase); - - // Assert expect(game.scene.getParty()[0].status?.effect).toBe(StatusEffect.PARALYSIS); expect(game.scene.getEnemyParty()[0].status?.effect).toBe(StatusEffect.PARALYSIS); expect(game.phaseInterceptor.log).toContain("ShowAbilityPhase"); }, 20000); - it("sets the status of the source pokemon to Burned when burn is applied by it", async () => { - // Arrange - const moveToUse = Moves.WILL_O_WISP; + it("does not trigger on Sleep", async () => { + await game.classicMode.startBattle(); - // Starter mocks - vi.spyOn(overrides, "STARTER_SPECIES_OVERRIDE", "get").mockReturnValue(Species.EEVEE); - vi.spyOn(overrides, "MOVESET_OVERRIDE", "get").mockReturnValue([moveToUse]); - vi.spyOn(overrides, "ABILITY_OVERRIDE", "get").mockReturnValue(Abilities.GUTS); + game.move.select(Moves.SPORE); - // Act - await game.startBattle(); - game.doAttack(getMovePosition(game.scene, 0, moveToUse)); + await game.phaseInterceptor.to("BerryPhase"); - await game.phaseInterceptor.to(MoveEffectPhase, false); - vi.spyOn(game.scene.getCurrentPhase() as MoveEffectPhase, "hitCheck").mockReturnValue(true); - - await game.phaseInterceptor.to(TurnEndPhase); - - // Assert - expect(game.scene.getParty()[0].status?.effect).toBe(StatusEffect.BURN); - expect(game.scene.getEnemyParty()[0].status?.effect).toBe(StatusEffect.BURN); - expect(game.phaseInterceptor.log).toContain("ShowAbilityPhase"); - }, 20000); - - it("sets the status of the source pokemon to Poisoned when poison is applied by it", async () => { - // Arrange - const moveToUse = Moves.POISON_POWDER; - - // Starter mocks - vi.spyOn(overrides, "STARTER_SPECIES_OVERRIDE", "get").mockReturnValue(Species.BRELOOM); - vi.spyOn(overrides, "MOVESET_OVERRIDE", "get").mockReturnValue([moveToUse]); - vi.spyOn(overrides, "ABILITY_OVERRIDE", "get").mockReturnValue(Abilities.TECHNICIAN); - - // Act - await game.startBattle(); - game.doAttack(getMovePosition(game.scene, 0, moveToUse)); - - await game.phaseInterceptor.to(MoveEffectPhase, false); - vi.spyOn(game.scene.getCurrentPhase() as MoveEffectPhase, "hitCheck").mockReturnValue(true); - - await game.phaseInterceptor.to(TurnEndPhase); - - // Assert - expect(game.scene.getParty()[0].status?.effect).toBe(StatusEffect.POISON); - expect(game.scene.getEnemyParty()[0].status?.effect).toBe(StatusEffect.POISON); - expect(game.phaseInterceptor.log).toContain("ShowAbilityPhase"); - }, 20000); - - it("sets the status of the source pokemon to Toxic when toxic is applied by it", async () => { - // Arrange - const moveToUse = Moves.TOXIC; - - // Starter mocks - vi.spyOn(overrides, "STARTER_SPECIES_OVERRIDE", "get").mockReturnValue(Species.QUAGSIRE); - vi.spyOn(overrides, "MOVESET_OVERRIDE", "get").mockReturnValue([moveToUse]); - vi.spyOn(overrides, "ABILITY_OVERRIDE", "get").mockReturnValue(Abilities.DAMP); - - // Act - await game.startBattle(); - game.doAttack(getMovePosition(game.scene, 0, moveToUse)); - - await game.phaseInterceptor.to(MoveEffectPhase, false); - vi.spyOn(game.scene.getCurrentPhase() as MoveEffectPhase, "hitCheck").mockReturnValue(true); - - await game.phaseInterceptor.to(TurnEndPhase); - - // Assert - expect(game.scene.getParty()[0].status?.effect).toBe(StatusEffect.TOXIC); - expect(game.scene.getEnemyParty()[0].status?.effect).toBe(StatusEffect.TOXIC); - expect(game.phaseInterceptor.log).toContain("ShowAbilityPhase"); - }, 20000); - - it("does not trigger when Pokemon is statused to non Burn, Paralysis, Poison, or Toxic", async () => { - // Arrange - const moveToUse = Moves.SPORE; - - // Starter mocks - vi.spyOn(overrides, "STARTER_SPECIES_OVERRIDE", "get").mockReturnValue(Species.BRELOOM); - vi.spyOn(overrides, "MOVESET_OVERRIDE", "get").mockReturnValue([moveToUse]); - vi.spyOn(overrides, "ABILITY_OVERRIDE", "get").mockReturnValue(Abilities.POISON_HEAL); - - // Act - await game.startBattle(); - game.doAttack(getMovePosition(game.scene, 0, moveToUse)); - - await game.phaseInterceptor.to(MoveEffectPhase, false); - vi.spyOn(game.scene.getCurrentPhase() as MoveEffectPhase, "hitCheck").mockReturnValue(true); - - await game.phaseInterceptor.to(TurnEndPhase); - - // Assert - expect(game.scene.getParty()[0].status?.effect).toBe(undefined); + expect(game.scene.getParty()[0].status?.effect).toBeUndefined(); expect(game.scene.getEnemyParty()[0].status?.effect).toBe(StatusEffect.SLEEP); expect(game.phaseInterceptor.log).not.toContain("ShowAbilityPhase"); }, 20000); it("does not trigger when Pokemon is statused by Toxic Spikes", async () => { - // Arrange - const moveToUse = Moves.SPLASH; + game.override + .ability(Abilities.SYNCHRONIZE) + .enemyAbility(Abilities.BALL_FETCH) + .enemyMoveset(Array(4).fill(Moves.TOXIC_SPIKES)); - // Starter mocks - vi.spyOn(overrides, "MOVESET_OVERRIDE", "get").mockReturnValue([moveToUse]); - vi.spyOn(overrides, "ABILITY_OVERRIDE", "get").mockReturnValue(Abilities.SYNCHRONIZE); - - vi.spyOn(overrides, "OPP_SPECIES_OVERRIDE", "get").mockReturnValue(Species.BRELOOM); - vi.spyOn(overrides, "OPP_MOVESET_OVERRIDE", "get").mockReturnValue([Moves.TOXIC_SPIKES]); - vi.spyOn(overrides, "OPP_ABILITY_OVERRIDE", "get").mockReturnValue(Abilities.TECHNICIAN); - - // Act - // Turn 1 - Opponent uses spikes, trainer uses splash - // Turn 2 - Opponent uses splash, trainer sends out Alakazam. Alakazam is toxic-ed but Synchronize should not proc - await game.startBattle([Species.MAGIKARP, Species.ALAKAZAM]); - const leadPokemon = game.scene.getPlayerPokemon(); - expect(leadPokemon).not.toBe(undefined); - - game.doAttack(getMovePosition(game.scene, 0, Moves.SPLASH)); // use splash + await game.classicMode.startBattle([Species.FEEBAS, Species.MILOTIC]); + game.move.select(Moves.SPLASH); await game.toNextTurn(); - game.doSwitchPokemon(1); - game.doAttack(0); - await game.phaseInterceptor.to(TurnEndPhase); + game.doSwitchPokemon(1); + await game.phaseInterceptor.to("BerryPhase"); // Assert expect(game.scene.getParty()[0].status?.effect).toBe(StatusEffect.POISON); - expect(game.scene.getEnemyParty()[0].status?.effect).toBe(undefined); + expect(game.scene.getEnemyParty()[0].status?.effect).toBeUndefined(); expect(game.phaseInterceptor.log).not.toContain("ShowAbilityPhase"); }, 20000); it("shows ability even if it fails to set the status of the opponent Pokemon", async () => { - // Arrange - const moveToUse = Moves.THUNDER_WAVE; + await game.classicMode.startBattle([Species.PIKACHU]); - // Starter mocks - vi.spyOn(overrides, "STARTER_SPECIES_OVERRIDE", "get").mockReturnValue(Species.PIKACHU); - vi.spyOn(overrides, "MOVESET_OVERRIDE", "get").mockReturnValue([moveToUse]); - vi.spyOn(overrides, "ABILITY_OVERRIDE", "get").mockReturnValue(Abilities.STATIC); - - // Act - await game.startBattle(); - game.doAttack(getMovePosition(game.scene, 0, moveToUse)); - - await game.phaseInterceptor.to(MoveEffectPhase, false); - vi.spyOn(game.scene.getCurrentPhase() as MoveEffectPhase, "hitCheck").mockReturnValue(true); - - await game.phaseInterceptor.to(TurnEndPhase); + game.move.select(Moves.THUNDER_WAVE); + await game.phaseInterceptor.to("BerryPhase"); // Assert - expect(game.scene.getParty()[0].status?.effect).toBe(undefined); + expect(game.scene.getParty()[0].status?.effect).toBeUndefined(); expect(game.scene.getEnemyParty()[0].status?.effect).toBe(StatusEffect.PARALYSIS); expect(game.phaseInterceptor.log).toContain("ShowAbilityPhase"); }, 20000); it("should activate with Psycho Shift after the move clears the status", async () => { - // Arrange - const moveToUse = Moves.PSYCHO_SHIFT; + game.override.statusEffect(StatusEffect.PARALYSIS); + await game.classicMode.startBattle(); - // Starter mocks - vi.spyOn(overrides, "STARTER_SPECIES_OVERRIDE", "get").mockReturnValue(Species.HOOTHOOT); - vi.spyOn(overrides, "MOVESET_OVERRIDE", "get").mockReturnValue([moveToUse]); - vi.spyOn(overrides, "ABILITY_OVERRIDE", "get").mockReturnValue(Abilities.KEEN_EYE); - vi.spyOn(overrides, "STATUS_OVERRIDE", "get").mockReturnValue(StatusEffect.PARALYSIS); - - // Act - await game.startBattle(); - game.doAttack(getMovePosition(game.scene, 0, moveToUse)); - - await game.phaseInterceptor.to(MoveEffectPhase, false); - vi.spyOn(game.scene.getCurrentPhase() as MoveEffectPhase, "hitCheck").mockReturnValue(true); - - await game.phaseInterceptor.to(TurnEndPhase); + game.move.select(Moves.PSYCHO_SHIFT); + await game.phaseInterceptor.to("BerryPhase"); // Assert expect(game.scene.getParty()[0].status?.effect).toBe(StatusEffect.PARALYSIS); // keeping old gen < V impl for now since it's buggy otherwise From 84741bad4568fd4eb800cb121ecd45527b5d76c8 Mon Sep 17 00:00:00 2001 From: NightKev <34855794+DayKev@users.noreply.github.com> Date: Sun, 8 Sep 2024 18:30:35 -0700 Subject: [PATCH 6/8] Formatting change --- src/data/ability.ts | 26 ++++++++------------------ 1 file changed, 8 insertions(+), 18 deletions(-) diff --git a/src/data/ability.ts b/src/data/ability.ts index 16649bcd2c2..343183ba2b8 100644 --- a/src/data/ability.ts +++ b/src/data/ability.ts @@ -1755,18 +1755,13 @@ export class PostSetStatusAbAttr extends AbAttr { /** * Does nothing after a status condition is set. * @param pokemon {@linkcode Pokemon} that status condition was set on. - * @param sourcePokemon {@linkcode Pokemon} that that set the status condition. Is null if status was not set by a Pokemon. + * @param sourcePokemon {@linkcode Pokemon} that that set the status condition. Is `null` if status was not set by a Pokemon. * @param passive Whether this ability is a passive. * @param effect {@linkcode StatusEffect} that was set. * @param args Set of unique arguments needed by this attribute. - * @returns true if application of the ability succeeds. + * @returns `true` if application of the ability succeeds. */ - applyPostSetStatus( - pokemon: Pokemon, - sourcePokemon: Pokemon | null = null, - passive: boolean, - effect: StatusEffect, - args: any[]) : boolean | Promise { + applyPostSetStatus(pokemon: Pokemon, sourcePokemon: Pokemon | null = null, passive: boolean, effect: StatusEffect, args: any[]) : boolean | Promise { return false; } } @@ -1778,22 +1773,17 @@ export class PostSetStatusAbAttr extends AbAttr { */ export class SynchronizeStatusAbAttr extends PostSetStatusAbAttr { /** - * If the StatusEffect that was set is Burn, Paralysis, Poison, or Toxic, and the status - * was set by a source Pokemon, set the source Pokemon's status to the same StatusEffect. + * If the `StatusEffect` that was set is Burn, Paralysis, Poison, or Toxic, and the status + * was set by a source Pokemon, set the source Pokemon's status to the same `StatusEffect`. * @param pokemon {@linkcode Pokemon} that status condition was set on. * @param sourcePokemon {@linkcode Pokemon} that that set the status condition. Is null if status was not set by a Pokemon. * @param passive Whether this ability is a passive. * @param effect {@linkcode StatusEffect} that was set. * @param args Set of unique arguments needed by this attribute. - * @returns true if application of the ability succeeds. + * @returns `true` if application of the ability succeeds. */ - applyPostSetStatus( - pokemon: Pokemon, - sourcePokemon: Pokemon | null = null, - passive: boolean, - effect: StatusEffect, - args: any[]): boolean { - // Synchronizable statuses + override applyPostSetStatus(pokemon: Pokemon, sourcePokemon: Pokemon | null = null, passive: boolean, effect: StatusEffect, args: any[]): boolean { + /** Synchronizable statuses */ const syncStatuses = new Set([ StatusEffect.BURN, StatusEffect.PARALYSIS, From 5b53aff5b7ddb21eda5ebd5cedbd1b96aefc0877 Mon Sep 17 00:00:00 2001 From: NightKev <34855794+DayKev@users.noreply.github.com> Date: Sun, 8 Sep 2024 19:16:32 -0700 Subject: [PATCH 7/8] Remove impossible `if` statement --- src/data/move.ts | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/src/data/move.ts b/src/data/move.ts index cfd75591f93..369a0ae0dcd 100644 --- a/src/data/move.ts +++ b/src/data/move.ts @@ -2005,9 +2005,7 @@ export class PsychoShiftEffectAttr extends MoveEffectAttr { if (target.status) { return false; - } - //@ts-ignore - how can target.status.effect be checked when we return `false` before when it's defined? - if (!target.status || (target.status.effect === statusToApply && move.chance < 0)) { // TODO: resolve ts-ignore + } else { const canSetStatus = target.canSetStatus(statusToApply, true, false, user); if (canSetStatus) { @@ -2021,8 +2019,6 @@ export class PsychoShiftEffectAttr extends MoveEffectAttr { return canSetStatus; } - - return false; } getTargetBenefitScore(user: Pokemon, target: Pokemon, move: Move): number { From 580d03972e9fce603b65d45ee56714a0842de5ff Mon Sep 17 00:00:00 2001 From: NightKev <34855794+DayKev@users.noreply.github.com> Date: Sun, 8 Sep 2024 22:31:05 -0700 Subject: [PATCH 8/8] Add `simulated` support --- src/data/ability.ts | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/src/data/ability.ts b/src/data/ability.ts index 343183ba2b8..0073306eb4b 100644 --- a/src/data/ability.ts +++ b/src/data/ability.ts @@ -1761,7 +1761,7 @@ export class PostSetStatusAbAttr extends AbAttr { * @param args Set of unique arguments needed by this attribute. * @returns `true` if application of the ability succeeds. */ - applyPostSetStatus(pokemon: Pokemon, sourcePokemon: Pokemon | null = null, passive: boolean, effect: StatusEffect, args: any[]) : boolean | Promise { + applyPostSetStatus(pokemon: Pokemon, sourcePokemon: Pokemon | null = null, passive: boolean, effect: StatusEffect, simulated: boolean, args: any[]) : boolean | Promise { return false; } } @@ -1782,7 +1782,7 @@ export class SynchronizeStatusAbAttr extends PostSetStatusAbAttr { * @param args Set of unique arguments needed by this attribute. * @returns `true` if application of the ability succeeds. */ - override applyPostSetStatus(pokemon: Pokemon, sourcePokemon: Pokemon | null = null, passive: boolean, effect: StatusEffect, args: any[]): boolean { + override applyPostSetStatus(pokemon: Pokemon, sourcePokemon: Pokemon | null = null, passive: boolean, effect: StatusEffect, simulated:boolean, args: any[]): boolean { /** Synchronizable statuses */ const syncStatuses = new Set([ StatusEffect.BURN, @@ -1792,7 +1792,9 @@ export class SynchronizeStatusAbAttr extends PostSetStatusAbAttr { ]); if (sourcePokemon && syncStatuses.has(effect)) { - sourcePokemon.trySetStatus(effect, true, pokemon); + if (!simulated) { + sourcePokemon.trySetStatus(effect, true, pokemon); + } return true; } @@ -4663,8 +4665,8 @@ export function applyStatMultiplierAbAttrs(attrType: Constructor(attrType, pokemon, (attr, passive) => attr.applyStatStage(pokemon, passive, simulated, stat, statValue, args), args); } export function applyPostSetStatusAbAttrs(attrType: Constructor, - pokemon: Pokemon, effect: StatusEffect, sourcePokemon?: Pokemon | null, ...args: any[]): Promise { - return applyAbAttrsInternal(attrType, pokemon, (attr, passive) => attr.applyPostSetStatus(pokemon, sourcePokemon, passive, effect, args), args); + pokemon: Pokemon, effect: StatusEffect, sourcePokemon?: Pokemon | null, simulated: boolean = false, ...args: any[]): Promise { + return applyAbAttrsInternal(attrType, pokemon, (attr, passive) => attr.applyPostSetStatus(pokemon, sourcePokemon, passive, effect, simulated, args), args); } /**