From 7d2cadbb45f9fdda614ad41fafbe9013273f7f27 Mon Sep 17 00:00:00 2001 From: Dean Date: Fri, 17 Jan 2025 22:34:47 -0800 Subject: [PATCH 01/36] Add setAbility method to pokemon.ts --- src/field/pokemon.ts | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/src/field/pokemon.ts b/src/field/pokemon.ts index 432f0a92fec8..db1a7aaf4ec6 100644 --- a/src/field/pokemon.ts +++ b/src/field/pokemon.ts @@ -32,7 +32,7 @@ import { BattlerTag, BattlerTagLapseType, EncoreTag, GroundedTag, HighestStatBoo import { WeatherType } from "#enums/weather-type"; import { ArenaTagSide, NoCritTag, WeakenMoveScreenTag } from "#app/data/arena-tag"; import type { Ability, AbAttr } from "#app/data/ability"; -import { 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, PostDamageAbAttr, applyPostDamageAbAttrs, CommanderAbAttr, applyPostItemLostAbAttrs, PostItemLostAbAttr } from "#app/data/ability"; +import { 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, PostDamageAbAttr, applyPostDamageAbAttrs, CommanderAbAttr, applyPostItemLostAbAttrs, PostItemLostAbAttr, applyPostSummonAbAttrs, PostSummonAbAttr } from "#app/data/ability"; import type PokemonData from "#app/system/pokemon-data"; import { BattlerIndex } from "#app/battle"; import { Mode } from "#app/ui/ui"; @@ -1430,6 +1430,21 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { return abilityAttrs; } + /** + * Sets the {@linkcode Pokemon}'s ability and activates it if it normally activates on summon + * @param ability New Ability + */ + public setAbility(ability: Ability): void { + this.summonData.ability = ability.id; + + applyPostSummonAbAttrs(PostSummonAbAttr, this) + .then(() => { + const field = this.isPlayer() ? globalScene.getPlayerField() : globalScene.getEnemyField(); + field.forEach((p) => applyAbAttrs(CommanderAbAttr, p, null, false)); + }); + + } + /** * Checks if a pokemon has a passive either from: * - bought with starter candy From 5767c7b303330a734f298a2c2e6dc84d08f8033d Mon Sep 17 00:00:00 2001 From: Dean Date: Fri, 17 Jan 2025 22:35:17 -0800 Subject: [PATCH 02/36] Edit SwitchAbilitiesAttr to use setAbility --- src/data/move.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/data/move.ts b/src/data/move.ts index 572fbf4c2acf..1d2de5816282 100644 --- a/src/data/move.ts +++ b/src/data/move.ts @@ -7490,11 +7490,12 @@ export class SwitchAbilitiesAttr extends MoveEffectAttr { return false; } - const tempAbilityId = user.getAbility().id; - user.summonData.ability = target.getAbility().id; - target.summonData.ability = tempAbilityId; + const tempAbility = user.getAbility(); globalScene.queueMessage(i18next.t("moveTriggers:swappedAbilitiesWithTarget", { pokemonName: getPokemonNameWithAffix(user) })); + + user.setAbility(target.getAbility()); + target.setAbility(tempAbility); // Swaps Forecast/Flower Gift from Castform/Cherrim globalScene.arena.triggerWeatherBasedFormChangesToNormal(); // Swaps Forecast/Flower Gift to Castform/Cherrim (edge case) From bdd570e415f00bae6c96d1bd15b62f3b82bf5dae Mon Sep 17 00:00:00 2001 From: Dean Date: Fri, 17 Jan 2025 22:36:46 -0800 Subject: [PATCH 03/36] Change AbilityGiveAttr to use setAbility --- src/data/move.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/data/move.ts b/src/data/move.ts index 1d2de5816282..326d7d131ccf 100644 --- a/src/data/move.ts +++ b/src/data/move.ts @@ -7472,10 +7472,10 @@ export class AbilityGiveAttr extends MoveEffectAttr { return false; } - target.summonData.ability = user.getAbility().id; - globalScene.queueMessage(i18next.t("moveTriggers:acquiredAbility", { pokemonName: getPokemonNameWithAffix(target), abilityName: allAbilities[user.getAbility().id].name })); + target.setAbility(user.getAbility()); + return true; } From 8fa550ba310ee384a22406086a03586cea4d2f8a Mon Sep 17 00:00:00 2001 From: Dean Date: Fri, 17 Jan 2025 22:42:45 -0800 Subject: [PATCH 04/36] Rename setAbility to be more accurate --- src/data/move.ts | 6 +++--- src/field/pokemon.ts | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/data/move.ts b/src/data/move.ts index 326d7d131ccf..cf22a9e08bdf 100644 --- a/src/data/move.ts +++ b/src/data/move.ts @@ -7474,7 +7474,7 @@ export class AbilityGiveAttr extends MoveEffectAttr { globalScene.queueMessage(i18next.t("moveTriggers:acquiredAbility", { pokemonName: getPokemonNameWithAffix(target), abilityName: allAbilities[user.getAbility().id].name })); - target.setAbility(user.getAbility()); + target.setTempAbility(user.getAbility()); return true; } @@ -7494,8 +7494,8 @@ export class SwitchAbilitiesAttr extends MoveEffectAttr { globalScene.queueMessage(i18next.t("moveTriggers:swappedAbilitiesWithTarget", { pokemonName: getPokemonNameWithAffix(user) })); - user.setAbility(target.getAbility()); - target.setAbility(tempAbility); + user.setTempAbility(target.getAbility()); + target.setTempAbility(tempAbility); // Swaps Forecast/Flower Gift from Castform/Cherrim globalScene.arena.triggerWeatherBasedFormChangesToNormal(); // Swaps Forecast/Flower Gift to Castform/Cherrim (edge case) diff --git a/src/field/pokemon.ts b/src/field/pokemon.ts index db1a7aaf4ec6..b5a80a0f34b9 100644 --- a/src/field/pokemon.ts +++ b/src/field/pokemon.ts @@ -1434,7 +1434,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { * Sets the {@linkcode Pokemon}'s ability and activates it if it normally activates on summon * @param ability New Ability */ - public setAbility(ability: Ability): void { + public setTempAbility(ability: Ability): void { this.summonData.ability = ability.id; applyPostSummonAbAttrs(PostSummonAbAttr, this) From 9b3265939e18541b414ffc4e90ef1e8b444eb5e3 Mon Sep 17 00:00:00 2001 From: Dean Date: Fri, 17 Jan 2025 22:45:12 -0800 Subject: [PATCH 05/36] Fix AbilityCopyAttr --- src/data/move.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/data/move.ts b/src/data/move.ts index cf22a9e08bdf..fc678b5b2b31 100644 --- a/src/data/move.ts +++ b/src/data/move.ts @@ -7435,13 +7435,13 @@ export class AbilityCopyAttr extends MoveEffectAttr { return false; } - user.summonData.ability = target.getAbility().id; - globalScene.queueMessage(i18next.t("moveTriggers:copiedTargetAbility", { pokemonName: getPokemonNameWithAffix(user), targetName: getPokemonNameWithAffix(target), abilityName: allAbilities[target.getAbility().id].name })); + user.setTempAbility(target.getAbility()); + if (this.copyToPartner && globalScene.currentBattle?.double && user.getAlly().hp) { - user.getAlly().summonData.ability = target.getAbility().id; globalScene.queueMessage(i18next.t("moveTriggers:copiedTargetAbility", { pokemonName: getPokemonNameWithAffix(user.getAlly()), targetName: getPokemonNameWithAffix(target), abilityName: allAbilities[target.getAbility().id].name })); + user.getAlly().setTempAbility(target.getAbility()); } return true; From 9388b5dc6701f5881f0a8ad661c34e946dc879d7 Mon Sep 17 00:00:00 2001 From: Dean Date: Fri, 17 Jan 2025 22:47:10 -0800 Subject: [PATCH 06/36] Fix AbilityChangeAttr --- src/data/move.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/data/move.ts b/src/data/move.ts index fc678b5b2b31..08d41c0b3be0 100644 --- a/src/data/move.ts +++ b/src/data/move.ts @@ -7408,11 +7408,11 @@ export class AbilityChangeAttr extends MoveEffectAttr { const moveTarget = this.selfTarget ? user : target; - moveTarget.summonData.ability = this.ability; - globalScene.triggerPokemonFormChange(moveTarget, SpeciesFormChangeRevertWeatherFormTrigger); - globalScene.queueMessage(i18next.t("moveTriggers:acquiredAbility", { pokemonName: getPokemonNameWithAffix((this.selfTarget ? user : target)), abilityName: allAbilities[this.ability].name })); + moveTarget.setTempAbility(allAbilities[this.ability]); + globalScene.triggerPokemonFormChange(moveTarget, SpeciesFormChangeRevertWeatherFormTrigger); + return true; } From f8689c45cb9616696255dd7e378d40d998a9dd3f Mon Sep 17 00:00:00 2001 From: Dean Date: Fri, 17 Jan 2025 22:50:08 -0800 Subject: [PATCH 07/36] Fix Transform --- 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 08d41c0b3be0..d9a00b7bb6d9 100644 --- a/src/data/move.ts +++ b/src/data/move.ts @@ -7578,7 +7578,7 @@ export class TransformAttr extends MoveEffectAttr { const promises: Promise[] = []; user.summonData.speciesForm = target.getSpeciesForm(); - user.summonData.ability = target.getAbility().id; + user.setTempAbility(target.getAbility()); user.summonData.gender = target.getGender(); // Power Trick's effect will not preserved after using Transform From bd53472c6847625469e2e7dee65a919f0e9ea2ee Mon Sep 17 00:00:00 2001 From: Dean Date: Fri, 17 Jan 2025 22:51:10 -0800 Subject: [PATCH 08/36] Fix imposter --- src/data/ability.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/data/ability.ts b/src/data/ability.ts index 0e8b3c2392d9..68cd2c99cdfb 100644 --- a/src/data/ability.ts +++ b/src/data/ability.ts @@ -2474,7 +2474,7 @@ export class PostSummonTransformAbAttr extends PostSummonAbAttr { } pokemon.summonData.speciesForm = target.getSpeciesForm(); - pokemon.summonData.ability = target.getAbility().id; + pokemon.setTempAbility(target.getAbility()); pokemon.summonData.gender = target.getGender(); // Copy all stats (except HP) From e8ea7d0c80216d334984adfb02270d94d3084496 Mon Sep 17 00:00:00 2001 From: Dean Date: Fri, 17 Jan 2025 22:51:59 -0800 Subject: [PATCH 09/36] Fix PostDefendAbilityGiveAbAttr --- src/data/ability.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/data/ability.ts b/src/data/ability.ts index 68cd2c99cdfb..ffb7ebb2eb11 100644 --- a/src/data/ability.ts +++ b/src/data/ability.ts @@ -1083,7 +1083,7 @@ export class PostDefendAbilityGiveAbAttr extends PostDefendAbAttr { if (move.checkFlag(MoveFlags.MAKES_CONTACT, attacker, pokemon) && !attacker.getAbility().hasAttr(UnsuppressableAbilityAbAttr) && !attacker.getAbility().hasAttr(PostDefendAbilityGiveAbAttr) && !move.hitsSubstitute(attacker, pokemon)) { if (!simulated) { - attacker.summonData.ability = this.ability; + attacker.setTempAbility(allAbilities[this.ability]); } return true; From 93fd0285be4235948d3681982956d9c0c3c5d05b Mon Sep 17 00:00:00 2001 From: Dean Date: Fri, 17 Jan 2025 22:54:16 -0800 Subject: [PATCH 10/36] Actually fix imposter --- src/data/ability.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/data/ability.ts b/src/data/ability.ts index ffb7ebb2eb11..4b31c8a11a23 100644 --- a/src/data/ability.ts +++ b/src/data/ability.ts @@ -2474,7 +2474,6 @@ export class PostSummonTransformAbAttr extends PostSummonAbAttr { } pokemon.summonData.speciesForm = target.getSpeciesForm(); - pokemon.setTempAbility(target.getAbility()); pokemon.summonData.gender = target.getGender(); // Copy all stats (except HP) @@ -2504,6 +2503,8 @@ export class PostSummonTransformAbAttr extends PostSummonAbAttr { promises.push(pokemon.loadAssets(false).then(() => { pokemon.playAnim(); pokemon.updateInfo(); + // If the new ability activates immediately, it needs to happen after all the transform animations + pokemon.setTempAbility(target.getAbility()); })); await Promise.all(promises); From 7634fe562c55016aaf4fd73624ad89fabe9ed177 Mon Sep 17 00:00:00 2001 From: Dean Date: Fri, 17 Jan 2025 22:55:02 -0800 Subject: [PATCH 11/36] Actually fix transform --- src/data/move.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/data/move.ts b/src/data/move.ts index d9a00b7bb6d9..7c35ce95b3fb 100644 --- a/src/data/move.ts +++ b/src/data/move.ts @@ -7578,7 +7578,6 @@ export class TransformAttr extends MoveEffectAttr { const promises: Promise[] = []; user.summonData.speciesForm = target.getSpeciesForm(); - user.setTempAbility(target.getAbility()); user.summonData.gender = target.getGender(); // Power Trick's effect will not preserved after using Transform @@ -7611,6 +7610,8 @@ export class TransformAttr extends MoveEffectAttr { promises.push(user.loadAssets(false).then(() => { user.playAnim(); user.updateInfo(); + // If the new ability activates immediately, it needs to happen after all the transform animations + user.setTempAbility(target.getAbility()); })); await Promise.all(promises); From 936568f264f2909ea456f3586afe1171ebf70012 Mon Sep 17 00:00:00 2001 From: Dean Date: Fri, 17 Jan 2025 22:55:59 -0800 Subject: [PATCH 12/36] Fix CopyFaintedAllyAbilityAbAttr --- src/data/ability.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/data/ability.ts b/src/data/ability.ts index 4b31c8a11a23..6602c9502a6e 100644 --- a/src/data/ability.ts +++ b/src/data/ability.ts @@ -1920,8 +1920,8 @@ export class CopyFaintedAllyAbilityAbAttr extends PostKnockOutAbAttr { applyPostKnockOut(pokemon: Pokemon, passive: boolean, simulated: boolean, knockedOut: Pokemon, args: any[]): boolean | Promise { if (pokemon.isPlayer() === knockedOut.isPlayer() && !knockedOut.getAbility().hasAttr(UncopiableAbilityAbAttr)) { if (!simulated) { - pokemon.summonData.ability = knockedOut.getAbility().id; globalScene.queueMessage(i18next.t("abilityTriggers:copyFaintedAllyAbility", { pokemonNameWithAffix: getPokemonNameWithAffix(knockedOut), abilityName: allAbilities[knockedOut.getAbility().id].name })); + pokemon.setTempAbility(knockedOut.getAbility()); } return true; } From 2a971e73ab10d9f5dd7b920000af283903a739a9 Mon Sep 17 00:00:00 2001 From: Dean Date: Fri, 17 Jan 2025 23:57:51 -0800 Subject: [PATCH 13/36] Fix Trace --- src/data/ability.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/data/ability.ts b/src/data/ability.ts index 6602c9502a6e..3ce0c260a3fb 100644 --- a/src/data/ability.ts +++ b/src/data/ability.ts @@ -2342,7 +2342,7 @@ export class PostSummonCopyAbilityAbAttr extends PostSummonAbAttr { if (!simulated) { this.target = target!; this.targetAbilityName = allAbilities[target!.getAbility().id].name; - pokemon.summonData.ability = target!.getAbility().id; + pokemon.setTempAbility(target!.getAbility()); setAbilityRevealed(target!); pokemon.updateInfo(); } From 34d291958607fc37e615812fe234ef62e93872d0 Mon Sep 17 00:00:00 2001 From: Dean Date: Fri, 17 Jan 2025 23:59:09 -0800 Subject: [PATCH 14/36] Fix PostDefendAbilitySwapAbAttr --- src/data/ability.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/data/ability.ts b/src/data/ability.ts index 3ce0c260a3fb..99d41fed1c90 100644 --- a/src/data/ability.ts +++ b/src/data/ability.ts @@ -1056,9 +1056,9 @@ export class PostDefendAbilitySwapAbAttr extends PostDefendAbAttr { if (move.checkFlag(MoveFlags.MAKES_CONTACT, attacker, pokemon) && !attacker.getAbility().hasAttr(UnswappableAbilityAbAttr) && !move.hitsSubstitute(attacker, pokemon)) { if (!simulated) { - const tempAbilityId = attacker.getAbility().id; - attacker.summonData.ability = pokemon.getAbility().id; - pokemon.summonData.ability = tempAbilityId; + const tempAbility = attacker.getAbility(); + attacker.setTempAbility(pokemon.getAbility()); + pokemon.setTempAbility(tempAbility); } return true; } From 4171ecd513c5c1000b316587f6b2079b3064daeb Mon Sep 17 00:00:00 2001 From: Dean Date: Sat, 18 Jan 2025 11:30:35 -0800 Subject: [PATCH 15/36] Add tests for skill swap --- src/test/moves/skill_swap.test.ts | 56 +++++++++++++++++++++++++++++++ 1 file changed, 56 insertions(+) create mode 100644 src/test/moves/skill_swap.test.ts diff --git a/src/test/moves/skill_swap.test.ts b/src/test/moves/skill_swap.test.ts new file mode 100644 index 000000000000..9c0f0b75ade7 --- /dev/null +++ b/src/test/moves/skill_swap.test.ts @@ -0,0 +1,56 @@ +import { Stat } from "#app/enums/stat"; +import { Abilities } from "#enums/abilities"; +import { Moves } from "#enums/moves"; +import { Species } from "#enums/species"; +import GameManager from "#test/utils/gameManager"; +import Phaser from "phaser"; +import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest"; + +describe("Moves - Skill Swap", () => { + 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 + .moveset([ Moves.SPLASH, Moves.SKILL_SWAP ]) + .ability(Abilities.BALL_FETCH) + .battleType("single") + .disableCrits() + .enemySpecies(Species.MAGIKARP) + .enemyAbility(Abilities.BALL_FETCH) + .enemyMoveset(Moves.SPLASH); + }); + + it("should swap the two abilities", async () => { + game.override.ability(Abilities.ADAPTABILITY); + await game.classicMode.startBattle([ Species.FEEBAS ]); + + game.move.select(Moves.SKILL_SWAP); + await game.phaseInterceptor.to("BerryPhase"); + + expect(game.scene.getPlayerPokemon()?.getAbility().id).toBe(Abilities.BALL_FETCH); + expect(game.scene.getEnemyPokemon()?.getAbility().id).toBe(Abilities.ADAPTABILITY); + }); + + it("should activate post-summon abilities", async () => { + game.override.ability(Abilities.INTIMIDATE); + await game.classicMode.startBattle([ Species.FEEBAS ]); + + game.move.select(Moves.SKILL_SWAP); + await game.phaseInterceptor.to("BerryPhase"); + + // player atk should be -1 after opponent gains intimidate and it activates + expect(game.scene.getPlayerPokemon()?.getStatStage(Stat.ATK)).toBe(-1); + }); +}); From fc1f7430b94eba34cb3818504ba71c87ae93fce3 Mon Sep 17 00:00:00 2001 From: Dean Date: Sat, 18 Jan 2025 11:38:15 -0800 Subject: [PATCH 16/36] Add tests for doodle --- src/test/moves/doodle.test.ts | 70 +++++++++++++++++++++++++++++++++++ 1 file changed, 70 insertions(+) create mode 100644 src/test/moves/doodle.test.ts diff --git a/src/test/moves/doodle.test.ts b/src/test/moves/doodle.test.ts new file mode 100644 index 000000000000..258abda392ae --- /dev/null +++ b/src/test/moves/doodle.test.ts @@ -0,0 +1,70 @@ +import { BattlerIndex } from "#app/battle"; +import { Stat } from "#app/enums/stat"; +import { Abilities } from "#enums/abilities"; +import { Moves } from "#enums/moves"; +import { Species } from "#enums/species"; +import GameManager from "#test/utils/gameManager"; +import Phaser from "phaser"; +import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest"; + +describe("Moves - Doodle", () => { + 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 + .moveset([ Moves.SPLASH, Moves.DOODLE ]) + .ability(Abilities.ADAPTABILITY) + .battleType("single") + .disableCrits() + .enemySpecies(Species.MAGIKARP) + .enemyAbility(Abilities.BALL_FETCH) + .enemyMoveset(Moves.SPLASH); + }); + + it("should copy the opponent's ability in singles", async () => { + await game.classicMode.startBattle([ Species.FEEBAS ]); + + game.move.select(Moves.DOODLE); + await game.phaseInterceptor.to("BerryPhase"); + + expect(game.scene.getPlayerPokemon()?.getAbility().id).toBe(Abilities.BALL_FETCH); + }); + + it("should copy the opponent's ability to itself and its ally in doubles", async () => { + game.override.battleType("double"); + await game.classicMode.startBattle([ Species.FEEBAS, Species.MAGIKARP ]); + + game.move.select(Moves.DOODLE, 0, BattlerIndex.ENEMY); + game.move.select(Moves.SPLASH, 1); + await game.phaseInterceptor.to("BerryPhase"); + + expect(game.scene.getPlayerField()[0].getAbility().id).toBe(Abilities.BALL_FETCH); + expect(game.scene.getPlayerField()[1].getAbility().id).toBe(Abilities.BALL_FETCH); + }); + + it("should activate post-summon abilities", async () => { + game.override.battleType("double") + .enemyAbility(Abilities.INTIMIDATE); + + await game.classicMode.startBattle([ Species.FEEBAS, Species.MAGIKARP ]); + + game.move.select(Moves.DOODLE, 0, BattlerIndex.ENEMY); + game.move.select(Moves.SPLASH, 1); + await game.phaseInterceptor.to("BerryPhase"); + + // Enemies should have been intimidated twice + expect(game.scene.getEnemyPokemon()?.getStatStage(Stat.ATK)).toBe(-2); + }); +}); From fa1c937a452471879e0560f90e5f49a3833d5dbb Mon Sep 17 00:00:00 2001 From: Dean Date: Sat, 18 Jan 2025 11:41:37 -0800 Subject: [PATCH 17/36] Add tests for entrainment --- src/test/moves/entrainment.test.ts | 53 ++++++++++++++++++++++++++++++ 1 file changed, 53 insertions(+) create mode 100644 src/test/moves/entrainment.test.ts diff --git a/src/test/moves/entrainment.test.ts b/src/test/moves/entrainment.test.ts new file mode 100644 index 000000000000..5d0991c8dfd8 --- /dev/null +++ b/src/test/moves/entrainment.test.ts @@ -0,0 +1,53 @@ +import { Stat } from "#app/enums/stat"; +import { Abilities } from "#enums/abilities"; +import { Moves } from "#enums/moves"; +import { Species } from "#enums/species"; +import GameManager from "#test/utils/gameManager"; +import Phaser from "phaser"; +import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest"; + +describe("Moves - Entrainment", () => { + 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 + .moveset([ Moves.SPLASH, Moves.ENTRAINMENT ]) + .ability(Abilities.ADAPTABILITY) + .battleType("single") + .disableCrits() + .enemySpecies(Species.MAGIKARP) + .enemyAbility(Abilities.BALL_FETCH) + .enemyMoveset(Moves.SPLASH); + }); + + it("gives its ability to the target", async () => { + await game.classicMode.startBattle([ Species.FEEBAS ]); + + game.move.select(Moves.ENTRAINMENT); + await game.phaseInterceptor.to("BerryPhase"); + + expect(game.scene.getEnemyPokemon()?.getAbility().id).toBe(Abilities.ADAPTABILITY); + }); + + it("should activate post-summon abilities", async () => { + game.override.ability(Abilities.INTIMIDATE); + await game.classicMode.startBattle([ Species.FEEBAS ]); + + game.move.select(Moves.ENTRAINMENT); + await game.phaseInterceptor.to("BerryPhase"); + + expect(game.scene.getPlayerPokemon()?.getStatStage(Stat.ATK)).toBe(-1); + }); +}); From a43cf9292ec2706aa08c3dd6138c4b5d6e304067 Mon Sep 17 00:00:00 2001 From: Dean Date: Sat, 18 Jan 2025 11:47:19 -0800 Subject: [PATCH 18/36] Add tests for role play --- src/test/moves/role_play.test.ts | 53 ++++++++++++++++++++++++++++++++ 1 file changed, 53 insertions(+) create mode 100644 src/test/moves/role_play.test.ts diff --git a/src/test/moves/role_play.test.ts b/src/test/moves/role_play.test.ts new file mode 100644 index 000000000000..a37f4faac9bb --- /dev/null +++ b/src/test/moves/role_play.test.ts @@ -0,0 +1,53 @@ +import { Stat } from "#app/enums/stat"; +import { Abilities } from "#enums/abilities"; +import { Moves } from "#enums/moves"; +import { Species } from "#enums/species"; +import GameManager from "#test/utils/gameManager"; +import Phaser from "phaser"; +import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest"; + +describe("Moves - Role Play", () => { + 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 + .moveset([ Moves.SPLASH, Moves.ROLE_PLAY ]) + .ability(Abilities.ADAPTABILITY) + .battleType("single") + .disableCrits() + .enemySpecies(Species.MAGIKARP) + .enemyAbility(Abilities.BALL_FETCH) + .enemyMoveset(Moves.SPLASH); + }); + + it("should set the user's ability to the target's ability", async () => { + await game.classicMode.startBattle([ Species.FEEBAS ]); + + game.move.select(Moves.ROLE_PLAY); + await game.phaseInterceptor.to("BerryPhase"); + + expect(game.scene.getPlayerPokemon()?.getAbility().id).toBe(Abilities.BALL_FETCH); + }); + + it("should activate post-summon abilities", async () => { + game.override.enemyAbility(Abilities.INTIMIDATE); + await game.classicMode.startBattle([ Species.FEEBAS ]); + + game.move.select(Moves.ROLE_PLAY); + await game.phaseInterceptor.to("BerryPhase"); + + expect(game.scene.getEnemyPokemon()?.getStatStage(Stat.ATK)).toBe(-1); + }); +}); From 5ded51adb1578fdbe1ee91b8448ca10a173e0c4b Mon Sep 17 00:00:00 2001 From: Dean Date: Sat, 18 Jan 2025 11:47:28 -0800 Subject: [PATCH 19/36] Add test for simple beam --- src/test/moves/simple_beam.test.ts | 42 ++++++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) create mode 100644 src/test/moves/simple_beam.test.ts diff --git a/src/test/moves/simple_beam.test.ts b/src/test/moves/simple_beam.test.ts new file mode 100644 index 000000000000..b4566669e8dd --- /dev/null +++ b/src/test/moves/simple_beam.test.ts @@ -0,0 +1,42 @@ +import { Abilities } from "#enums/abilities"; +import { Moves } from "#enums/moves"; +import { Species } from "#enums/species"; +import GameManager from "#test/utils/gameManager"; +import Phaser from "phaser"; +import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest"; + +describe("Moves - Simple Beam", () => { + 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 + .moveset([ Moves.SPLASH, Moves.SIMPLE_BEAM ]) + .ability(Abilities.BALL_FETCH) + .battleType("single") + .disableCrits() + .enemySpecies(Species.MAGIKARP) + .enemyAbility(Abilities.BALL_FETCH) + .enemyMoveset(Moves.SPLASH); + }); + + it("sets the target's ability to simple", async () => { + await game.classicMode.startBattle([ Species.FEEBAS ]); + + game.move.select(Moves.SIMPLE_BEAM); + await game.phaseInterceptor.to("BerryPhase"); + + expect(game.scene.getEnemyPokemon()?.getAbility().id).toBe(Abilities.SIMPLE); + }); +}); From c1f7b552ead132a8fd39162013c2392e8e38e3e1 Mon Sep 17 00:00:00 2001 From: Dean Date: Sat, 18 Jan 2025 11:51:25 -0800 Subject: [PATCH 20/36] Add test for transform --- src/test/moves/transform.test.ts | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/test/moves/transform.test.ts b/src/test/moves/transform.test.ts index adb97b42af78..ffe935aa61bc 100644 --- a/src/test/moves/transform.test.ts +++ b/src/test/moves/transform.test.ts @@ -116,4 +116,16 @@ describe("Moves - Transform", () => { } }); }); + + it("should activate its ability if it copies one that activates on summon", async () => { + game.override.enemyAbility(Abilities.INTIMIDATE) + .ability(Abilities.BALL_FETCH); + + await game.classicMode.startBattle([ Species.DITTO ]); + game.move.select(Moves.TRANSFORM); + + await game.phaseInterceptor.to("BerryPhase"); + + expect(game.scene.getEnemyPokemon()?.getStatStage(Stat.ATK)).toBe(-1); + }); }); From 0e61801bf6a1a9285872210774bc2126cb8a2d4e Mon Sep 17 00:00:00 2001 From: Dean Date: Sat, 18 Jan 2025 11:57:15 -0800 Subject: [PATCH 21/36] Add test for imposter --- src/test/abilities/imposter.test.ts | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/test/abilities/imposter.test.ts b/src/test/abilities/imposter.test.ts index 3445b3b322ce..98f30c60505a 100644 --- a/src/test/abilities/imposter.test.ts +++ b/src/test/abilities/imposter.test.ts @@ -116,4 +116,15 @@ describe("Abilities - Imposter", () => { } }); }); + + it("should activate its ability if it copies one that activates on summon", async () => { + game.override.enemyAbility(Abilities.INTIMIDATE); + + await game.classicMode.startBattle([ Species.DITTO ]); + + game.move.select(Moves.TACKLE); + await game.phaseInterceptor.to("MoveEndPhase"); + + expect(game.scene.getEnemyPokemon()?.getStatStage(Stat.ATK)).toBe(-1); + }); }); From 3773f7df67f7926c6bbdba5b2be999fdacaf352a Mon Sep 17 00:00:00 2001 From: Dean Date: Sat, 18 Jan 2025 12:01:50 -0800 Subject: [PATCH 22/36] Add tests for mummy --- src/test/abilities/mummy.test.ts | 52 ++++++++++++++++++++++++++++++++ 1 file changed, 52 insertions(+) create mode 100644 src/test/abilities/mummy.test.ts diff --git a/src/test/abilities/mummy.test.ts b/src/test/abilities/mummy.test.ts new file mode 100644 index 000000000000..bf93cdcf61d1 --- /dev/null +++ b/src/test/abilities/mummy.test.ts @@ -0,0 +1,52 @@ +import { Abilities } from "#enums/abilities"; +import { Moves } from "#enums/moves"; +import { Species } from "#enums/species"; +import GameManager from "#test/utils/gameManager"; +import Phaser from "phaser"; +import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest"; + +describe("Abilities - Mummy", () => { + 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 + .moveset([ Moves.SPLASH ]) + .ability(Abilities.MUMMY) + .battleType("single") + .disableCrits() + .enemySpecies(Species.MAGIKARP) + .enemyAbility(Abilities.BALL_FETCH) + .enemyMoveset(Moves.TACKLE); + }); + + it("should set the enemy's ability to mummy when hit by a contact move", async () => { + await game.classicMode.startBattle([ Species.FEEBAS ]); + + game.move.select(Moves.SPLASH); + await game.phaseInterceptor.to("BerryPhase"); + + expect(game.scene.getEnemyPokemon()?.getAbility().id).toBe(Abilities.MUMMY); + }); + + it("should not change the enemy's ability hit by a non-contact move", async () => { + game.override.enemyMoveset(Moves.EARTHQUAKE); + await game.classicMode.startBattle([ Species.FEEBAS ]); + + game.move.select(Moves.SPLASH); + await game.phaseInterceptor.to("BerryPhase"); + + expect(game.scene.getEnemyPokemon()?.getAbility().id).toBe(Abilities.BALL_FETCH); + }); +}); From 33deb7f27fa78f39badbbec5cd9aa81688ea5b29 Mon Sep 17 00:00:00 2001 From: Dean Date: Sat, 18 Jan 2025 12:07:21 -0800 Subject: [PATCH 23/36] Add tests for trace --- src/test/abilities/trace.test.ts | 53 ++++++++++++++++++++++++++++++++ 1 file changed, 53 insertions(+) create mode 100644 src/test/abilities/trace.test.ts diff --git a/src/test/abilities/trace.test.ts b/src/test/abilities/trace.test.ts new file mode 100644 index 000000000000..ffa76f597693 --- /dev/null +++ b/src/test/abilities/trace.test.ts @@ -0,0 +1,53 @@ +import { Stat } from "#app/enums/stat"; +import { Abilities } from "#enums/abilities"; +import { Moves } from "#enums/moves"; +import { Species } from "#enums/species"; +import GameManager from "#test/utils/gameManager"; +import Phaser from "phaser"; +import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest"; + +describe("Abilities - Trace", () => { + 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 + .moveset([ Moves.SPLASH ]) + .ability(Abilities.TRACE) + .battleType("single") + .disableCrits() + .enemySpecies(Species.MAGIKARP) + .enemyAbility(Abilities.BALL_FETCH) + .enemyMoveset(Moves.SPLASH); + }); + + it("should copy the opponent's ability", async () => { + await game.classicMode.startBattle([ Species.FEEBAS ]); + + game.move.select(Moves.SPLASH); + await game.phaseInterceptor.to("BerryPhase"); + + expect(game.scene.getPlayerPokemon()?.getAbility().id).toBe(Abilities.BALL_FETCH); + }); + + it("should activate a copied post-summon ability", async () => { + game.override.enemyAbility(Abilities.INTIMIDATE); + await game.classicMode.startBattle([ Species.FEEBAS ]); + + game.move.select(Moves.SPLASH); + await game.phaseInterceptor.to("BerryPhase"); + + expect(game.scene.getEnemyPokemon()?.getStatStage(Stat.ATK)).toBe(-1); + }); +}); From dc8ff078e7ddef90cf4e9e8d119d9c2ce01b1b1d Mon Sep 17 00:00:00 2001 From: Dean Date: Sat, 18 Jan 2025 12:11:20 -0800 Subject: [PATCH 24/36] Add tests for wandering spirit --- src/test/abilities/wandering_spirit.test.ts | 65 +++++++++++++++++++++ 1 file changed, 65 insertions(+) create mode 100644 src/test/abilities/wandering_spirit.test.ts diff --git a/src/test/abilities/wandering_spirit.test.ts b/src/test/abilities/wandering_spirit.test.ts new file mode 100644 index 000000000000..4bc9298353f1 --- /dev/null +++ b/src/test/abilities/wandering_spirit.test.ts @@ -0,0 +1,65 @@ +import { Stat } from "#app/enums/stat"; +import { Abilities } from "#enums/abilities"; +import { Moves } from "#enums/moves"; +import { Species } from "#enums/species"; +import GameManager from "#test/utils/gameManager"; +import Phaser from "phaser"; +import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest"; + +describe("Abilities - Wandering Spirit", () => { + 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 + .moveset([ Moves.SPLASH ]) + .ability(Abilities.WANDERING_SPIRIT) + .battleType("single") + .disableCrits() + .enemySpecies(Species.MAGIKARP) + .enemyAbility(Abilities.BALL_FETCH) + .enemyMoveset(Moves.TACKLE); + }); + + it("should exchange abilities when hit with a contact move", async () => { + await game.classicMode.startBattle([ Species.FEEBAS ]); + + game.move.select(Moves.SPLASH); + await game.phaseInterceptor.to("BerryPhase"); + + expect(game.scene.getPlayerPokemon()?.getAbility().id).toBe(Abilities.BALL_FETCH); + expect(game.scene.getEnemyPokemon()?.getAbility().id).toBe(Abilities.WANDERING_SPIRIT); + }); + + it("should not exchange abilities when hit with a non-contact move", async () => { + game.override.enemyMoveset(Moves.EARTHQUAKE); + await game.classicMode.startBattle([ Species.FEEBAS ]); + + game.move.select(Moves.SPLASH); + await game.phaseInterceptor.to("BerryPhase"); + + expect(game.scene.getPlayerPokemon()?.getAbility().id).toBe(Abilities.WANDERING_SPIRIT); + expect(game.scene.getEnemyPokemon()?.getAbility().id).toBe(Abilities.BALL_FETCH); + }); + + it("should activate post-summon abilities", async () => { + game.override.enemyAbility(Abilities.INTIMIDATE); + await game.classicMode.startBattle([ Species.FEEBAS ]); + + game.move.select(Moves.SPLASH); + await game.phaseInterceptor.to("BerryPhase"); + + expect(game.scene.getEnemyPokemon()?.getStatStage(Stat.ATK)).toBe(-1); + }); +}); From 0e9af6c9cffff4151d7d59308fdfd7e244e65141 Mon Sep 17 00:00:00 2001 From: Dean Date: Sat, 18 Jan 2025 12:53:54 -0800 Subject: [PATCH 25/36] Consider legendary weather when changing ability --- src/field/pokemon.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/field/pokemon.ts b/src/field/pokemon.ts index b5a80a0f34b9..32db4f2b727d 100644 --- a/src/field/pokemon.ts +++ b/src/field/pokemon.ts @@ -32,7 +32,7 @@ import { BattlerTag, BattlerTagLapseType, EncoreTag, GroundedTag, HighestStatBoo import { WeatherType } from "#enums/weather-type"; import { ArenaTagSide, NoCritTag, WeakenMoveScreenTag } from "#app/data/arena-tag"; import type { Ability, AbAttr } from "#app/data/ability"; -import { 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, PostDamageAbAttr, applyPostDamageAbAttrs, CommanderAbAttr, applyPostItemLostAbAttrs, PostItemLostAbAttr, applyPostSummonAbAttrs, PostSummonAbAttr } from "#app/data/ability"; +import { 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, PostDamageAbAttr, applyPostDamageAbAttrs, CommanderAbAttr, applyPostItemLostAbAttrs, PostItemLostAbAttr, applyPostSummonAbAttrs, PostSummonAbAttr, PreSwitchOutAbAttr, applyPreSwitchOutAbAttrs } from "#app/data/ability"; import type PokemonData from "#app/system/pokemon-data"; import { BattlerIndex } from "#app/battle"; import { Mode } from "#app/ui/ui"; @@ -1432,9 +1432,15 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { /** * Sets the {@linkcode Pokemon}'s ability and activates it if it normally activates on summon + * Clears * @param ability New Ability */ public setTempAbility(ability: Ability): void { + const legendaryWeather = [ Abilities.DESOLATE_LAND, Abilities.PRIMORDIAL_SEA, Abilities.DELTA_STREAM ]; + // If the ability to set is also a legendary weather, no need to clear old weather + if (legendaryWeather.includes(this.getAbility().id) && !legendaryWeather.includes(ability.id)) { + applyPreSwitchOutAbAttrs(PreSwitchOutAbAttr, this); + } this.summonData.ability = ability.id; applyPostSummonAbAttrs(PostSummonAbAttr, this) From 220d8260c879ba9f4905958b29f78522a42688ae Mon Sep 17 00:00:00 2001 From: Dean Date: Sat, 18 Jan 2025 22:10:11 -0800 Subject: [PATCH 26/36] Ensure that passives are not (re)applied when main abilities change --- src/data/ability.ts | 18 +++++++++++++++++- src/field/pokemon.ts | 19 +++++-------------- 2 files changed, 22 insertions(+), 15 deletions(-) diff --git a/src/data/ability.ts b/src/data/ability.ts index 99d41fed1c90..33f700a02486 100644 --- a/src/data/ability.ts +++ b/src/data/ability.ts @@ -4867,9 +4867,10 @@ async function applyAbAttrsInternal( showAbilityInstant: boolean = false, simulated: boolean = false, messages: string[] = [], + considerPassive: boolean = true ) { for (const passive of [ false, true ]) { - if (!pokemon?.canApplyAbility(passive) || (passive && pokemon.getPassiveAbility().id === pokemon.getAbility().id)) { + if (!pokemon?.canApplyAbility(passive) || (passive && (pokemon.getPassiveAbility().id === pokemon.getAbility().id || !considerPassive))) { continue; } @@ -5295,6 +5296,21 @@ export function applyPostItemLostAbAttrs(attrType: Constructor(attrType, pokemon, (attr, passive) => attr.applyPostItemLost(pokemon, simulated, args), args); } +/** + * Applies abilities when they become active mid-turn (ability switch) + * + * Ignores passives as they don't change and shouldn't be reapplied when main abilities change + */ +export function applyMidTurnAbAttrs(pokemon: Pokemon, simulated: boolean = false, ...args: any[]): Promise { + return applyAbAttrsInternal(PostSummonAbAttr, pokemon, (attr, passive) => attr.applyPostSummon(pokemon, passive, simulated, args), args, false, simulated, [], false); +} + +/** + * Clears primal weather during the turn if {@linkcode pokemon}'s ability corresponds to one + */ +export function applyMidTurnClearWeatherAbAttrs(pokemon: Pokemon, simulated: boolean = false, ...args: any[]): Promise { + return applyAbAttrsInternal(PreSwitchOutClearWeatherAbAttr, pokemon, (attr, passive) => attr.applyPreSwitchOut(pokemon, passive, simulated, args), args, true, simulated, [], false); +} function queueShowAbility(pokemon: Pokemon, passive: boolean): void { globalScene.unshiftPhase(new ShowAbilityPhase(pokemon.id, passive)); globalScene.clearPhaseQueueSplice(); diff --git a/src/field/pokemon.ts b/src/field/pokemon.ts index 32db4f2b727d..8c1083b424da 100644 --- a/src/field/pokemon.ts +++ b/src/field/pokemon.ts @@ -32,7 +32,7 @@ import { BattlerTag, BattlerTagLapseType, EncoreTag, GroundedTag, HighestStatBoo import { WeatherType } from "#enums/weather-type"; import { ArenaTagSide, NoCritTag, WeakenMoveScreenTag } from "#app/data/arena-tag"; import type { Ability, AbAttr } from "#app/data/ability"; -import { 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, PostDamageAbAttr, applyPostDamageAbAttrs, CommanderAbAttr, applyPostItemLostAbAttrs, PostItemLostAbAttr, applyPostSummonAbAttrs, PostSummonAbAttr, PreSwitchOutAbAttr, applyPreSwitchOutAbAttrs } from "#app/data/ability"; +import { 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, PostDamageAbAttr, applyPostDamageAbAttrs, CommanderAbAttr, applyPostItemLostAbAttrs, PostItemLostAbAttr, applyMidTurnAbAttrs, applyMidTurnClearWeatherAbAttrs } from "#app/data/ability"; import type PokemonData from "#app/system/pokemon-data"; import { BattlerIndex } from "#app/battle"; import { Mode } from "#app/ui/ui"; @@ -1432,23 +1432,14 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { /** * Sets the {@linkcode Pokemon}'s ability and activates it if it normally activates on summon - * Clears + * + * Also clears primal weather if it is from the ability being changed * @param ability New Ability */ public setTempAbility(ability: Ability): void { - const legendaryWeather = [ Abilities.DESOLATE_LAND, Abilities.PRIMORDIAL_SEA, Abilities.DELTA_STREAM ]; - // If the ability to set is also a legendary weather, no need to clear old weather - if (legendaryWeather.includes(this.getAbility().id) && !legendaryWeather.includes(ability.id)) { - applyPreSwitchOutAbAttrs(PreSwitchOutAbAttr, this); - } + applyMidTurnClearWeatherAbAttrs(this); this.summonData.ability = ability.id; - - applyPostSummonAbAttrs(PostSummonAbAttr, this) - .then(() => { - const field = this.isPlayer() ? globalScene.getPlayerField() : globalScene.getEnemyField(); - field.forEach((p) => applyAbAttrs(CommanderAbAttr, p, null, false)); - }); - + applyMidTurnAbAttrs(this); } /** From 495e396a943336594eac0f1f1bfed4f6cd6a666d Mon Sep 17 00:00:00 2001 From: Dean Date: Wed, 29 Jan 2025 22:52:27 -0800 Subject: [PATCH 27/36] Add general ability swap test cases --- src/test/battle/ability_swap.test.ts | 67 ++++++++++++++++++++++++++++ 1 file changed, 67 insertions(+) create mode 100644 src/test/battle/ability_swap.test.ts diff --git a/src/test/battle/ability_swap.test.ts b/src/test/battle/ability_swap.test.ts new file mode 100644 index 000000000000..ed7f0a2b690e --- /dev/null +++ b/src/test/battle/ability_swap.test.ts @@ -0,0 +1,67 @@ +import { allAbilities } from "#app/data/ability"; +import { Stat } from "#app/enums/stat"; +import { Abilities } from "#enums/abilities"; +import { Moves } from "#enums/moves"; +import { Species } from "#enums/species"; +import GameManager from "#test/utils/gameManager"; +import Phaser from "phaser"; +import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest"; + +describe("Moves - Aaa", () => { + 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 + .moveset([ Moves.SPLASH ]) + .ability(Abilities.BALL_FETCH) + .battleType("single") + .disableCrits() + .enemySpecies(Species.MAGIKARP) + .enemyAbility(Abilities.BALL_FETCH) + .enemyMoveset(Moves.SPLASH); + }); + + it("should activate post-summon abilities", async () => { + await game.classicMode.startBattle([ Species.FEEBAS ]); + + game.move.select(Moves.SPLASH); + game.scene.getPlayerPokemon()?.setTempAbility(allAbilities[Abilities.INTIMIDATE]); + await game.phaseInterceptor.to("BerryPhase"); + + expect(game.scene.getEnemyPokemon()?.getStatStage(Stat.ATK)).toBe(-1); + }); + + it("should remove primal weather when the setter's ability is removed", async () => { + game.override.ability(Abilities.DESOLATE_LAND); + await game.classicMode.startBattle([ Species.FEEBAS ]); + + game.move.select(Moves.SPLASH); + game.scene.getPlayerPokemon()?.setTempAbility(allAbilities[Abilities.BALL_FETCH]); + await game.phaseInterceptor.to("BerryPhase"); + + expect(game.scene.arena.weather?.weatherType).toBeUndefined(); + }); + + it("should not activate passive abilities", async () => { + game.override.passiveAbility(Abilities.INTREPID_SWORD); + await game.classicMode.startBattle([ Species.FEEBAS ]); + + game.move.select(Moves.SPLASH); + game.scene.getPlayerPokemon()?.setTempAbility(allAbilities[Abilities.BALL_FETCH]); + await game.phaseInterceptor.to("BerryPhase"); + + expect(game.scene.getPlayerPokemon()?.getStatStage(Stat.ATK)).toBe(1); // would be 2 if passive activated again + }); +}); From 62a797a6ae6ba5bd39d927d8d15bc22e6525f201 Mon Sep 17 00:00:00 2001 From: Dean Date: Wed, 29 Jan 2025 22:58:24 -0800 Subject: [PATCH 28/36] Fix test name --- src/test/battle/ability_swap.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/test/battle/ability_swap.test.ts b/src/test/battle/ability_swap.test.ts index ed7f0a2b690e..9ce7a36c16ee 100644 --- a/src/test/battle/ability_swap.test.ts +++ b/src/test/battle/ability_swap.test.ts @@ -7,7 +7,7 @@ import GameManager from "#test/utils/gameManager"; import Phaser from "phaser"; import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest"; -describe("Moves - Aaa", () => { +describe("Test Ability Swapping", () => { let phaserGame: Phaser.Game; let game: GameManager; From af3532cdd0094ac34f8e4474f51241e30e5c2140 Mon Sep 17 00:00:00 2001 From: Dean Date: Tue, 4 Feb 2025 12:48:35 -0800 Subject: [PATCH 29/36] Add NoMidTurnActivationAttr --- src/data/ability.ts | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/src/data/ability.ts b/src/data/ability.ts index 23f4a2d467f3..7123210ddc25 100644 --- a/src/data/ability.ts +++ b/src/data/ability.ts @@ -4524,6 +4524,15 @@ export class NoFusionAbilityAbAttr extends AbAttr { } } +/** + * Attach to a post-summon ability if it shouldn't be activated when it is obtained or activated during the battle + */ +export class NoMidTurnActivationAttr extends AbAttr { + constructor() { + super(false); + } +} + export class IgnoreTypeImmunityAbAttr extends AbAttr { private defenderType: Type; private allowedMoveTypes: Type[]; @@ -4867,10 +4876,11 @@ async function applyAbAttrsInternal( showAbilityInstant: boolean = false, simulated: boolean = false, messages: string[] = [], - considerPassive: boolean = true + midTurn: boolean = false /** Ignore passives and abilities with {@linkcode NoMidTurnActivationAttr} */ ) { for (const passive of [ false, true ]) { - if (!pokemon?.canApplyAbility(passive) || (passive && (pokemon.getPassiveAbility().id === pokemon.getAbility().id || !considerPassive))) { + if (!pokemon?.canApplyAbility(passive) || (passive && (pokemon.getPassiveAbility().id === pokemon.getAbility().id || midTurn)) + || (midTurn && pokemon.getAbility().hasAttr(NoMidTurnActivationAttr))) { continue; } @@ -5302,14 +5312,14 @@ export function applyPostItemLostAbAttrs(attrType: Constructor { - return applyAbAttrsInternal(PostSummonAbAttr, pokemon, (attr, passive) => attr.applyPostSummon(pokemon, passive, simulated, args), args, false, simulated, [], false); + return applyAbAttrsInternal(PostSummonAbAttr, pokemon, (attr, passive) => attr.applyPostSummon(pokemon, passive, simulated, args), args, false, simulated, [], true); } /** * Clears primal weather during the turn if {@linkcode pokemon}'s ability corresponds to one */ export function applyMidTurnClearWeatherAbAttrs(pokemon: Pokemon, simulated: boolean = false, ...args: any[]): Promise { - return applyAbAttrsInternal(PreSwitchOutClearWeatherAbAttr, pokemon, (attr, passive) => attr.applyPreSwitchOut(pokemon, passive, simulated, args), args, true, simulated, [], false); + return applyAbAttrsInternal(PreSwitchOutClearWeatherAbAttr, pokemon, (attr, passive) => attr.applyPreSwitchOut(pokemon, passive, simulated, [ ...args, true ]), args, true, simulated, [], true); } function queueShowAbility(pokemon: Pokemon, passive: boolean): void { globalScene.unshiftPhase(new ShowAbilityPhase(pokemon.id, passive)); @@ -5804,6 +5814,7 @@ export function initAbilities() { new Ability(Abilities.ILLUSION, 5) .attr(UncopiableAbilityAbAttr) .attr(UnswappableAbilityAbAttr) + .attr(NoMidTurnActivationAttr) .unimplemented(), new Ability(Abilities.IMPOSTER, 5) .attr(PostSummonTransformAbAttr) From b68670ebbce2cc8d36b3ce5f29d318be054ae8ec Mon Sep 17 00:00:00 2001 From: Dean Date: Tue, 4 Feb 2025 12:57:50 -0800 Subject: [PATCH 30/36] Remove NoMidTurnActivationAttr from illusion --- src/data/ability.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/data/ability.ts b/src/data/ability.ts index 7123210ddc25..7cdf19241034 100644 --- a/src/data/ability.ts +++ b/src/data/ability.ts @@ -5814,7 +5814,6 @@ export function initAbilities() { new Ability(Abilities.ILLUSION, 5) .attr(UncopiableAbilityAbAttr) .attr(UnswappableAbilityAbAttr) - .attr(NoMidTurnActivationAttr) .unimplemented(), new Ability(Abilities.IMPOSTER, 5) .attr(PostSummonTransformAbAttr) From 58f532f153a626090304e4a958517faae2bcedf6 Mon Sep 17 00:00:00 2001 From: Dean Date: Mon, 10 Feb 2025 08:13:43 -0800 Subject: [PATCH 31/36] Remove extraneous call to triggerWeatherBasedFormChanges --- src/data/move.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/data/move.ts b/src/data/move.ts index 2a7c65e1e124..9cf9b1a34690 100644 --- a/src/data/move.ts +++ b/src/data/move.ts @@ -7534,8 +7534,6 @@ export class SwitchAbilitiesAttr extends MoveEffectAttr { target.setTempAbility(tempAbility); // Swaps Forecast/Flower Gift from Castform/Cherrim globalScene.arena.triggerWeatherBasedFormChangesToNormal(); - // Swaps Forecast/Flower Gift to Castform/Cherrim (edge case) - globalScene.arena.triggerWeatherBasedFormChanges(); return true; } From 89a406828e6664e1536d3645ab5c0fbbdc15a759 Mon Sep 17 00:00:00 2001 From: Dean Date: Mon, 10 Feb 2025 08:31:22 -0800 Subject: [PATCH 32/36] Fix primal weather clearing --- src/data/ability.ts | 2 +- src/field/pokemon.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/data/ability.ts b/src/data/ability.ts index a9d3939d0e5c..422d04e9c10b 100644 --- a/src/data/ability.ts +++ b/src/data/ability.ts @@ -5315,7 +5315,7 @@ export function applyMidTurnAbAttrs(pokemon: Pokemon, simulated: boolean = false * Clears primal weather during the turn if {@linkcode pokemon}'s ability corresponds to one */ export function applyMidTurnClearWeatherAbAttrs(pokemon: Pokemon, simulated: boolean = false, ...args: any[]): Promise { - return applyAbAttrsInternal(PreSwitchOutClearWeatherAbAttr, pokemon, (attr, passive) => attr.applyPreSwitchOut(pokemon, passive, simulated, [ ...args, true ]), args, true, simulated, [], true); + return applyAbAttrsInternal(PreLeaveFieldClearWeatherAbAttr, pokemon, (attr, passive) => attr.applyPreLeaveField(pokemon, passive, simulated, [ ...args, true ]), args, true, simulated, [], true); } function queueShowAbility(pokemon: Pokemon, passive: boolean): void { globalScene.unshiftPhase(new ShowAbilityPhase(pokemon.id, passive)); diff --git a/src/field/pokemon.ts b/src/field/pokemon.ts index 0ffa2d7399bc..1fd3870017fc 100644 --- a/src/field/pokemon.ts +++ b/src/field/pokemon.ts @@ -31,7 +31,7 @@ import { BattlerTag, BattlerTagLapseType, EncoreTag, GroundedTag, HighestStatBoo import { WeatherType } from "#enums/weather-type"; import { ArenaTagSide, NoCritTag, WeakenMoveScreenTag } from "#app/data/arena-tag"; import type { Ability, AbAttr } from "#app/data/ability"; -import { 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, PostDamageAbAttr, applyPostDamageAbAttrs, CommanderAbAttr, applyPostItemLostAbAttrs, PostItemLostAbAttr, applyMidTurnAbAttrs, applyMidTurnClearWeatherAbAttrs, PreLeaveFieldAbAttr, applyPreLeaveFieldAbAttrs } from "#app/data/ability"; +import { 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, PostDamageAbAttr, applyPostDamageAbAttrs, CommanderAbAttr, applyPostItemLostAbAttrs, PostItemLostAbAttr, applyMidTurnAbAttrs, PreLeaveFieldAbAttr, applyPreLeaveFieldAbAttrs, applyMidTurnClearWeatherAbAttrs } from "#app/data/ability"; import type PokemonData from "#app/system/pokemon-data"; import { BattlerIndex } from "#app/battle"; import { Mode } from "#app/ui/ui"; From 93963aa00c38ecaf9da3eccb60b020e9ab0581d8 Mon Sep 17 00:00:00 2001 From: Dean Date: Sat, 15 Feb 2025 13:25:37 -0800 Subject: [PATCH 33/36] Change "MidTurn" to "OnGain" --- src/data/ability.ts | 12 ++++++------ src/field/pokemon.ts | 6 +++--- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/src/data/ability.ts b/src/data/ability.ts index 23af869394b3..b4b1014999f0 100644 --- a/src/data/ability.ts +++ b/src/data/ability.ts @@ -4525,7 +4525,7 @@ export class NoFusionAbilityAbAttr extends AbAttr { /** * Attach to a post-summon ability if it shouldn't be activated when it is obtained or activated during the battle */ -export class NoMidTurnActivationAttr extends AbAttr { +export class NoOnGainActivationAttr extends AbAttr { constructor() { super(false); } @@ -4874,11 +4874,11 @@ async function applyAbAttrsInternal( showAbilityInstant: boolean = false, simulated: boolean = false, messages: string[] = [], - midTurn: boolean = false /** Ignore passives and abilities with {@linkcode NoMidTurnActivationAttr} */ + gainedMidTurn: boolean = false /** Ignore passives and abilities with {@linkcode NoOnGainActivationAttr} */ ) { for (const passive of [ false, true ]) { - if (!pokemon?.canApplyAbility(passive) || (passive && (pokemon.getPassiveAbility().id === pokemon.getAbility().id || midTurn)) - || (midTurn && pokemon.getAbility().hasAttr(NoMidTurnActivationAttr))) { + if (!pokemon?.canApplyAbility(passive) || (passive && (pokemon.getPassiveAbility().id === pokemon.getAbility().id || gainedMidTurn)) + || (gainedMidTurn && pokemon.getAbility().hasAttr(NoOnGainActivationAttr))) { continue; } @@ -5314,14 +5314,14 @@ export function applyPostItemLostAbAttrs(attrType: Constructor { +export function applyOnGainAbAttrs(pokemon: Pokemon, simulated: boolean = false, ...args: any[]): Promise { return applyAbAttrsInternal(PostSummonAbAttr, pokemon, (attr, passive) => attr.applyPostSummon(pokemon, passive, simulated, args), args, false, simulated, [], true); } /** * Clears primal weather during the turn if {@linkcode pokemon}'s ability corresponds to one */ -export function applyMidTurnClearWeatherAbAttrs(pokemon: Pokemon, simulated: boolean = false, ...args: any[]): Promise { +export function applyOnGainClearWeatherAbAttrs(pokemon: Pokemon, simulated: boolean = false, ...args: any[]): Promise { return applyAbAttrsInternal(PreLeaveFieldClearWeatherAbAttr, pokemon, (attr, passive) => attr.applyPreLeaveField(pokemon, passive, simulated, [ ...args, true ]), args, true, simulated, [], true); } function queueShowAbility(pokemon: Pokemon, passive: boolean): void { diff --git a/src/field/pokemon.ts b/src/field/pokemon.ts index 746a99231436..cb9788fe7fd7 100644 --- a/src/field/pokemon.ts +++ b/src/field/pokemon.ts @@ -64,7 +64,7 @@ import { BattlerTag, BattlerTagLapseType, EncoreTag, GroundedTag, HighestStatBoo import { WeatherType } from "#enums/weather-type"; import { ArenaTagSide, NoCritTag, WeakenMoveScreenTag } from "#app/data/arena-tag"; import type { Ability, AbAttr } from "#app/data/ability"; -import { 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, PostDamageAbAttr, applyPostDamageAbAttrs, CommanderAbAttr, applyPostItemLostAbAttrs, PostItemLostAbAttr, applyMidTurnAbAttrs, PreLeaveFieldAbAttr, applyPreLeaveFieldAbAttrs, applyMidTurnClearWeatherAbAttrs } from "#app/data/ability"; +import { 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, PostDamageAbAttr, applyPostDamageAbAttrs, CommanderAbAttr, applyPostItemLostAbAttrs, PostItemLostAbAttr, applyOnGainAbAttrs, PreLeaveFieldAbAttr, applyPreLeaveFieldAbAttrs, applyOnGainClearWeatherAbAttrs } from "#app/data/ability"; import type PokemonData from "#app/system/pokemon-data"; import { BattlerIndex } from "#app/battle"; import { Mode } from "#app/ui/ui"; @@ -1458,9 +1458,9 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { * @param ability New Ability */ public setTempAbility(ability: Ability): void { - applyMidTurnClearWeatherAbAttrs(this); + applyOnGainClearWeatherAbAttrs(this); this.summonData.ability = ability.id; - applyMidTurnAbAttrs(this); + applyOnGainAbAttrs(this); } /** From e45a03c494f71275d3b5e2cb4de1d864a071db52 Mon Sep 17 00:00:00 2001 From: Dean Date: Mon, 17 Feb 2025 00:49:59 -0800 Subject: [PATCH 34/36] Change NoOnGainActivationAttr to a field in PostSummonAbAttr --- src/data/ability.ts | 28 +++++++++++++++++----------- 1 file changed, 17 insertions(+), 11 deletions(-) diff --git a/src/data/ability.ts b/src/data/ability.ts index b4b1014999f0..fde032e10ae8 100644 --- a/src/data/ability.ts +++ b/src/data/ability.ts @@ -2005,6 +2005,21 @@ export class PostIntimidateStatStageChangeAbAttr extends AbAttr { * @see {@linkcode applyPostSummon()} */ export class PostSummonAbAttr extends AbAttr { + /** Should the ability activate when gained in battle? This will almost always be true */ + private activateOnGain: boolean; + + constructor(showAbility: boolean = true, activateOnGain: boolean = true) { + super(showAbility); + this.activateOnGain = activateOnGain; + } + + /** + * @returns Whether the ability should activate when gained in battle + */ + shouldActivateOnGain(): boolean { + return this.activateOnGain; + } + /** * Applies ability post summon (after switching in) * @param pokemon {@linkcode Pokemon} with this ability @@ -2439,7 +2454,7 @@ export class PostSummonCopyAllyStatsAbAttr extends PostSummonAbAttr { */ export class PostSummonTransformAbAttr extends PostSummonAbAttr { constructor() { - super(true); + super(true, false); } async applyPostSummon(pokemon: Pokemon, _passive: boolean, simulated: boolean, _args: any[]): Promise { @@ -4522,15 +4537,6 @@ export class NoFusionAbilityAbAttr extends AbAttr { } } -/** - * Attach to a post-summon ability if it shouldn't be activated when it is obtained or activated during the battle - */ -export class NoOnGainActivationAttr extends AbAttr { - constructor() { - super(false); - } -} - export class IgnoreTypeImmunityAbAttr extends AbAttr { private defenderType: Type; private allowedMoveTypes: Type[]; @@ -4878,7 +4884,7 @@ async function applyAbAttrsInternal( ) { for (const passive of [ false, true ]) { if (!pokemon?.canApplyAbility(passive) || (passive && (pokemon.getPassiveAbility().id === pokemon.getAbility().id || gainedMidTurn)) - || (gainedMidTurn && pokemon.getAbility().hasAttr(NoOnGainActivationAttr))) { + || (gainedMidTurn && pokemon.getAbility().getAttrs(PostSummonAbAttr).some(a => !a.shouldActivateOnGain()))) { continue; } From ad7522b956394872fd946e524fc52e2f52a6e792 Mon Sep 17 00:00:00 2001 From: Dean Date: Mon, 17 Feb 2025 22:47:35 -0800 Subject: [PATCH 35/36] Add passive support --- src/data/ability.ts | 93 ++++++++++++++++++++++++++------------------ src/field/pokemon.ts | 14 ++++--- 2 files changed, 64 insertions(+), 43 deletions(-) diff --git a/src/data/ability.ts b/src/data/ability.ts index 063c393ba726..f91cd685222e 100644 --- a/src/data/ability.ts +++ b/src/data/ability.ts @@ -4868,52 +4868,69 @@ async function applyAbAttrsInternal( showAbilityInstant: boolean = false, simulated: boolean = false, messages: string[] = [], - gainedMidTurn: boolean = false /** Ignore passives and abilities with {@linkcode NoOnGainActivationAttr} */ + gainedMidTurn: boolean = false ) { for (const passive of [ false, true ]) { - if (!pokemon?.canApplyAbility(passive) || (passive && (pokemon.getPassiveAbility().id === pokemon.getAbility().id || gainedMidTurn)) - || (gainedMidTurn && pokemon.getAbility().getAttrs(PostSummonAbAttr).some(a => !a.shouldActivateOnGain()))) { + if (!pokemon?.canApplyAbility(passive) || (passive && (pokemon.getPassiveAbility().id === pokemon.getAbility().id))) { continue; } - const ability = passive ? pokemon.getPassiveAbility() : pokemon.getAbility(); - for (const attr of ability.getAttrs(attrType)) { - const condition = attr.getCondition(); - if (condition && !condition(pokemon)) { - continue; - } + applySingleAbAttrs(pokemon, passive, attrType, applyFunc, args, gainedMidTurn, simulated, showAbilityInstant, messages); + globalScene.clearPhaseQueueSplice(); + } +} - globalScene.setPhaseQueueSplice(); +async function applySingleAbAttrs( + pokemon: Pokemon, + passive: boolean, + attrType: Constructor, + applyFunc: AbAttrApplyFunc, + args: any[], + gainedMidTurn: boolean = false, + simulated: boolean = false, + showAbilityInstant: boolean = false, + messages: string[] = [] +) { + const ability = passive ? pokemon.getPassiveAbility() : pokemon.getAbility(); + if (gainedMidTurn && ability.getAttrs(attrType).some(attr => attr instanceof PostSummonAbAttr && !attr.shouldActivateOnGain())) { + return; + } - let result = applyFunc(attr, passive); - // TODO Remove this when promises get reworked - if (result instanceof Promise) { - result = await result; + for (const attr of ability.getAttrs(attrType)) { + const condition = attr.getCondition(); + if ((condition && !condition(pokemon))) { + continue; + } + + globalScene.setPhaseQueueSplice(); + + let result = applyFunc(attr, passive); + // TODO Remove this when promises get reworked + if (result instanceof Promise) { + result = await result; + } + if (result) { + if (pokemon.summonData && !pokemon.summonData.abilitiesApplied.includes(ability.id)) { + pokemon.summonData.abilitiesApplied.push(ability.id); } - if (result) { - if (pokemon.summonData && !pokemon.summonData.abilitiesApplied.includes(ability.id)) { - pokemon.summonData.abilitiesApplied.push(ability.id); - } - if (pokemon.battleData && !simulated && !pokemon.battleData.abilitiesApplied.includes(ability.id)) { - pokemon.battleData.abilitiesApplied.push(ability.id); - } - if (attr.showAbility && !simulated) { - if (showAbilityInstant) { - globalScene.abilityBar.showAbility(pokemon, passive); - } else { - queueShowAbility(pokemon, passive); - } + if (pokemon.battleData && !simulated && !pokemon.battleData.abilitiesApplied.includes(ability.id)) { + pokemon.battleData.abilitiesApplied.push(ability.id); + } + if (attr.showAbility && !simulated) { + if (showAbilityInstant) { + globalScene.abilityBar.showAbility(pokemon, passive); + } else { + queueShowAbility(pokemon, passive); } - const message = attr.getTriggerMessage(pokemon, ability.name, args); - if (message) { - if (!simulated) { - globalScene.queueMessage(message); - } + } + const message = attr.getTriggerMessage(pokemon, ability.name, args); + if (message) { + if (!simulated) { + globalScene.queueMessage(message); } - messages.push(message!); } + messages.push(message!); } - globalScene.clearPhaseQueueSplice(); } } @@ -5308,15 +5325,15 @@ export function applyPostItemLostAbAttrs(attrType: Constructor { - return applyAbAttrsInternal(PostSummonAbAttr, pokemon, (attr, passive) => attr.applyPostSummon(pokemon, passive, simulated, args), args, false, simulated, [], true); +export function applyOnGainAbAttrs(pokemon: Pokemon, passive: boolean = false, simulated: boolean = false, ...args: any[]): void { + applySingleAbAttrs(pokemon, passive, PostSummonAbAttr, (attr, passive) => attr.applyPostSummon(pokemon, passive, simulated, args), args, true, simulated); } /** * Clears primal weather during the turn if {@linkcode pokemon}'s ability corresponds to one */ -export function applyOnGainClearWeatherAbAttrs(pokemon: Pokemon, simulated: boolean = false, ...args: any[]): Promise { - return applyAbAttrsInternal(PreLeaveFieldClearWeatherAbAttr, pokemon, (attr, passive) => attr.applyPreLeaveField(pokemon, passive, simulated, [ ...args, true ]), args, true, simulated, [], true); +export function applyOnLoseClearWeatherAbAttrs(pokemon: Pokemon, passive: boolean = false, simulated: boolean = false, ...args: any[]): void { + applySingleAbAttrs(pokemon, passive, PreLeaveFieldClearWeatherAbAttr, (attr, passive) => attr.applyPreLeaveField(pokemon, passive, simulated, [ ...args, true ]), args, true, simulated); } function queueShowAbility(pokemon: Pokemon, passive: boolean): void { globalScene.unshiftPhase(new ShowAbilityPhase(pokemon.id, passive)); diff --git a/src/field/pokemon.ts b/src/field/pokemon.ts index 91f3ede277c6..714f1ec70260 100644 --- a/src/field/pokemon.ts +++ b/src/field/pokemon.ts @@ -64,7 +64,7 @@ import { BattlerTag, BattlerTagLapseType, EncoreTag, GroundedTag, HighestStatBoo import { WeatherType } from "#enums/weather-type"; import { ArenaTagSide, NoCritTag, WeakenMoveScreenTag } from "#app/data/arena-tag"; import type { Ability, AbAttr } from "#app/data/ability"; -import { 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, PostDamageAbAttr, applyPostDamageAbAttrs, CommanderAbAttr, applyPostItemLostAbAttrs, PostItemLostAbAttr, applyOnGainAbAttrs, PreLeaveFieldAbAttr, applyPreLeaveFieldAbAttrs, applyOnGainClearWeatherAbAttrs } from "#app/data/ability"; +import { 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, PostDamageAbAttr, applyPostDamageAbAttrs, CommanderAbAttr, applyPostItemLostAbAttrs, PostItemLostAbAttr, applyOnGainAbAttrs, PreLeaveFieldAbAttr, applyPreLeaveFieldAbAttrs, applyOnLoseClearWeatherAbAttrs } from "#app/data/ability"; import type PokemonData from "#app/system/pokemon-data"; import { BattlerIndex } from "#app/battle"; import { Mode } from "#app/ui/ui"; @@ -1487,10 +1487,14 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { * Also clears primal weather if it is from the ability being changed * @param ability New Ability */ - public setTempAbility(ability: Ability): void { - applyOnGainClearWeatherAbAttrs(this); - this.summonData.ability = ability.id; - applyOnGainAbAttrs(this); + public setTempAbility(ability: Ability, passive: boolean = false): void { + applyOnLoseClearWeatherAbAttrs(this, passive); + if (passive) { + this.summonData.passiveAbility = ability.id; + } else { + this.summonData.ability = ability.id; + } + applyOnGainAbAttrs(this, passive); } /** From be85721aa40753cb81191a9aba47f15f0d805761 Mon Sep 17 00:00:00 2001 From: Dean <69436131+emdeann@users.noreply.github.com> Date: Tue, 18 Feb 2025 01:14:52 -0800 Subject: [PATCH 36/36] Remove redundant parentheses Co-authored-by: NightKev <34855794+DayKev@users.noreply.github.com> --- src/data/ability.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/data/ability.ts b/src/data/ability.ts index f91cd685222e..940b5f0c7d7a 100644 --- a/src/data/ability.ts +++ b/src/data/ability.ts @@ -4898,7 +4898,7 @@ async function applySingleAbAttrs( for (const attr of ability.getAttrs(attrType)) { const condition = attr.getCondition(); - if ((condition && !condition(pokemon))) { + if (condition && !condition(pokemon)) { continue; }