[Bug] Fix moves/abilities which modify abilities (#5146)

* Add setAbility method to pokemon.ts

* Edit SwitchAbilitiesAttr to use setAbility

* Change AbilityGiveAttr to use setAbility

* Rename setAbility to be more accurate

* Fix AbilityCopyAttr

* Fix AbilityChangeAttr

* Fix Transform

* Fix imposter

* Fix PostDefendAbilityGiveAbAttr

* Actually fix imposter

* Actually fix transform

* Fix CopyFaintedAllyAbilityAbAttr

* Fix Trace

* Fix PostDefendAbilitySwapAbAttr

* Add tests for skill swap

* Add tests for doodle

* Add tests for entrainment

* Add tests for role play

* Add test for simple beam

* Add test for transform

* Add test for imposter

* Add tests for mummy

* Add tests for trace

* Add tests for wandering spirit

* Consider legendary weather when changing ability

* Ensure that passives are not (re)applied when main abilities change

* Add general ability swap test cases

* Fix test name

* Add NoMidTurnActivationAttr

* Remove NoMidTurnActivationAttr from illusion

* Remove extraneous call to triggerWeatherBasedFormChanges

* Fix primal weather clearing

* Change "MidTurn" to "OnGain"

* Change NoOnGainActivationAttr to a field in PostSummonAbAttr

* Add passive support

* Remove redundant parentheses

Co-authored-by: NightKev <34855794+DayKev@users.noreply.github.com>

---------

Co-authored-by: damocleas <damocleas25@gmail.com>
Co-authored-by: NightKev <34855794+DayKev@users.noreply.github.com>
This commit is contained in:
Dean 2025-02-19 00:02:39 -08:00 committed by GitHub
parent 7fafccf8de
commit 9cc1b17745
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
14 changed files with 661 additions and 61 deletions

View File

@ -1044,9 +1044,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;
}
@ -1071,7 +1071,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;
@ -1908,8 +1908,8 @@ export class CopyFaintedAllyAbilityAbAttr extends PostKnockOutAbAttr {
applyPostKnockOut(pokemon: Pokemon, passive: boolean, simulated: boolean, knockedOut: Pokemon, args: any[]): boolean | Promise<boolean> {
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;
}
@ -1993,6 +1993,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
@ -2330,7 +2345,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();
}
@ -2427,7 +2442,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<boolean> {
@ -2462,7 +2477,6 @@ export class PostSummonTransformAbAttr extends PostSummonAbAttr {
}
pokemon.summonData.speciesForm = target.getSpeciesForm();
pokemon.summonData.ability = target.getAbility().id;
pokemon.summonData.gender = target.getGender();
// Copy all stats (except HP)
@ -2492,6 +2506,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);
@ -4852,53 +4868,72 @@ async function applyAbAttrsInternal<TAttr extends AbAttr>(
showAbilityInstant: boolean = false,
simulated: boolean = false,
messages: string[] = [],
gainedMidTurn: boolean = false
) {
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))) {
continue;
}
const ability = passive ? pokemon.getPassiveAbility() : pokemon.getAbility();
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 (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);
}
}
messages.push(message!);
}
}
applySingleAbAttrs(pokemon, passive, attrType, applyFunc, args, gainedMidTurn, simulated, showAbilityInstant, messages);
globalScene.clearPhaseQueueSplice();
}
}
async function applySingleAbAttrs<TAttr extends AbAttr>(
pokemon: Pokemon,
passive: boolean,
attrType: Constructor<TAttr>,
applyFunc: AbAttrApplyFunc<TAttr>,
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;
}
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 (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);
}
}
messages.push(message!);
}
}
}
class ForceSwitchOutHelper {
constructor(private switchType: SwitchType) {}
@ -5285,6 +5320,21 @@ export function applyPostItemLostAbAttrs(attrType: Constructor<PostItemLostAbAtt
return applyAbAttrsInternal<PostItemLostAbAttr>(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 applyOnGainAbAttrs(pokemon: Pokemon, passive: boolean = false, simulated: boolean = false, ...args: any[]): void {
applySingleAbAttrs<PostSummonAbAttr>(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 applyOnLoseClearWeatherAbAttrs(pokemon: Pokemon, passive: boolean = false, simulated: boolean = false, ...args: any[]): void {
applySingleAbAttrs<PreLeaveFieldClearWeatherAbAttr>(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));
globalScene.clearPhaseQueueSplice();

View File

@ -7521,11 +7521,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;
}
@ -7548,13 +7548,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;
@ -7585,10 +7585,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.setTempAbility(user.getAbility());
return true;
}
@ -7603,15 +7603,14 @@ 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.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)
globalScene.arena.triggerWeatherBasedFormChanges();
return true;
}
@ -7690,7 +7689,6 @@ export class TransformAttr extends MoveEffectAttr {
const promises: Promise<void>[] = [];
user.summonData.speciesForm = target.getSpeciesForm();
user.summonData.ability = target.getAbility().id;
user.summonData.gender = target.getGender();
// Power Trick's effect will not preserved after using Transform
@ -7723,6 +7721,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);

View File

@ -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, 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, 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";
@ -1481,6 +1481,22 @@ 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
*
* Also clears primal weather if it is from the ability being changed
* @param ability New Ability
*/
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);
}
/**
* Checks if a pokemon has a passive either from:
* - bought with starter candy

View File

@ -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);
});
});

View File

@ -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);
});
});

View File

@ -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);
});
});

View File

@ -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);
});
});

View File

@ -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("Test Ability Swapping", () => {
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
});
});

View File

@ -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);
});
});

View File

@ -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);
});
});

View File

@ -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);
});
});

View File

@ -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);
});
});

View File

@ -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);
});
});

View File

@ -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);
});
});