diff --git a/src/data/ability.ts b/src/data/ability.ts index 5e5231176b5..33ecbd42ef7 100644 --- a/src/data/ability.ts +++ b/src/data/ability.ts @@ -53,6 +53,7 @@ export class Ability implements Localizable { public description: string; public generation: integer; public isBypassFaint: boolean; + public isBypassOnField: boolean; public isIgnorable: boolean; public attrs: AbAttr[]; public conditions: AbAttrCondition[]; @@ -113,6 +114,11 @@ export class Ability implements Localizable { return this; } + bypassOnField(): Ability { + this.isBypassOnField = true; + return this; + } + ignorable(): Ability { this.isIgnorable = true; return this; @@ -5422,7 +5428,8 @@ export function initAbilities() { .attr(ProtectStatAbAttr) .ignorable(), new Ability(Abilities.NATURAL_CURE, 3) - .attr(PreSwitchOutResetStatusAbAttr), + .attr(PreSwitchOutResetStatusAbAttr) + .bypassOnField(), new Ability(Abilities.LIGHTNING_ROD, 3) .attr(RedirectTypeMoveAbAttr, Type.ELECTRIC) .attr(TypeImmunityStatStageChangeAbAttr, Type.ELECTRIC, Stat.SPATK, 1) @@ -5768,7 +5775,8 @@ export function initAbilities() { new Ability(Abilities.POISON_TOUCH, 5) .attr(PostAttackContactApplyStatusEffectAbAttr, 30, StatusEffect.POISON), new Ability(Abilities.REGENERATOR, 5) - .attr(PreSwitchOutHealAbAttr), + .attr(PreSwitchOutHealAbAttr) + .bypassOnField(), new Ability(Abilities.BIG_PECKS, 5) .attr(ProtectStatAbAttr, Stat.DEF) .ignorable(), @@ -5914,19 +5922,22 @@ export function initAbilities() { .attr(PostBiomeChangeWeatherChangeAbAttr, WeatherType.HEAVY_RAIN) .attr(PreSwitchOutClearWeatherAbAttr) .attr(PostFaintClearWeatherAbAttr) - .bypassFaint(), + .bypassFaint() + .bypassOnField(), new Ability(Abilities.DESOLATE_LAND, 6) .attr(PostSummonWeatherChangeAbAttr, WeatherType.HARSH_SUN) .attr(PostBiomeChangeWeatherChangeAbAttr, WeatherType.HARSH_SUN) .attr(PreSwitchOutClearWeatherAbAttr) .attr(PostFaintClearWeatherAbAttr) - .bypassFaint(), + .bypassFaint() + .bypassOnField(), new Ability(Abilities.DELTA_STREAM, 6) .attr(PostSummonWeatherChangeAbAttr, WeatherType.STRONG_WINDS) .attr(PostBiomeChangeWeatherChangeAbAttr, WeatherType.STRONG_WINDS) .attr(PreSwitchOutClearWeatherAbAttr) .attr(PostFaintClearWeatherAbAttr) - .bypassFaint(), + .bypassFaint() + .bypassOnField(), new Ability(Abilities.STAMINA, 7) .attr(PostDefendStatStageChangeAbAttr, (target, user, move) => move.category !== MoveCategory.STATUS, Stat.DEF, 1), new Ability(Abilities.WIMP_OUT, 7) @@ -6263,6 +6274,7 @@ export function initAbilities() { .attr(NoFusionAbilityAbAttr) .attr(PostBattleInitFormChangeAbAttr, () => 0) .attr(PreSwitchOutFormChangeAbAttr, (pokemon) => !pokemon.isFainted() ? 1 : pokemon.formIndex) + .bypassOnField() .bypassFaint(), new Ability(Abilities.COMMANDER, 9) .attr(CommanderAbAttr) diff --git a/src/field/pokemon.ts b/src/field/pokemon.ts index 1b987ba34f6..c2593861e6b 100644 --- a/src/field/pokemon.ts +++ b/src/field/pokemon.ts @@ -1494,7 +1494,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { return false; } } - return (this.hp > 0 || ability.isBypassFaint) && !ability.conditions.find(condition => !condition(this)); + return (((this.isOnField() || ability.isBypassOnField) && this.hp > 0) || ability.isBypassFaint) && !ability.conditions.find(condition => !condition(this)); } /** diff --git a/src/test/abilities/arena_trap.test.ts b/src/test/abilities/arena_trap.test.ts index 12b9673080d..745576fc274 100644 --- a/src/test/abilities/arena_trap.test.ts +++ b/src/test/abilities/arena_trap.test.ts @@ -45,16 +45,27 @@ describe("Abilities - Arena Trap", () => { expect(enemy).toBe(game.scene.getEnemyPokemon()); }); - it("should guarantee double battle with any one LURE", async () => { + it("should increase the chance of double battles", async () => { game.override - .startingModifier([ - { name: "LURE" }, - ]) - .startingWave(2); + .moveset(Moves.SPLASH) + .ability(Abilities.ARENA_TRAP) + .enemySpecies(Species.SUNKERN) + .enemyAbility(Abilities.BALL_FETCH) + .enemyMoveset(Moves.SPLASH) + .startingWave(9); + vi.spyOn(game.scene, "getDoubleBattleChance"); await game.classicMode.startBattle(); - expect(game.scene.getEnemyField().length).toBe(2); + game.move.select(Moves.SPLASH); + await game.doKillOpponents(); + await game.toNextWave(); + expect(game.scene.getDoubleBattleChance).toHaveLastReturnedWith(8); + + game.move.select(Moves.SPLASH); + await game.doKillOpponents(); + await game.toNextWave(); + expect(game.scene.getDoubleBattleChance).toHaveLastReturnedWith(2); }); /** diff --git a/src/test/abilities/commander.test.ts b/src/test/abilities/commander.test.ts index 70568639b61..6f2e8db48a5 100644 --- a/src/test/abilities/commander.test.ts +++ b/src/test/abilities/commander.test.ts @@ -223,3 +223,45 @@ describe("Abilities - Commander", () => { expect(enemy.isFullHp()).toBeTruthy(); }); }); + +describe("Abilities - Commander", () => { + let phaserGame: Phaser.Game; + let game: GameManager; + + beforeAll(() => { + phaserGame = new Phaser.Game({ + type: Phaser.HEADLESS, + }); + }); + + afterEach(() => { + game.phaseInterceptor.restoreOg(); + }); + + beforeEach(() => { + game = new GameManager(phaserGame); + }); + + it("should increase the chance of double battles", async () => { + game.override + .moveset(Moves.SPLASH) + .ability(Abilities.COMMANDER) + .enemySpecies(Species.SUNKERN) + .enemyAbility(Abilities.BALL_FETCH) + .enemyMoveset(Moves.SPLASH) + .startingWave(9); + + vi.spyOn(game.scene, "getDoubleBattleChance"); + await game.classicMode.startBattle(); + + game.move.select(Moves.SPLASH); + await game.doKillOpponents(); + await game.toNextWave(); + expect(game.scene.getDoubleBattleChance).toHaveLastReturnedWith(8); + + game.move.select(Moves.SPLASH); + await game.doKillOpponents(); + await game.toNextWave(); + expect(game.scene.getDoubleBattleChance).toHaveLastReturnedWith(2); + }); +}); diff --git a/src/test/abilities/illuminate.test.ts b/src/test/abilities/illuminate.test.ts index 4f7d3d83b51..cd55d7ee945 100644 --- a/src/test/abilities/illuminate.test.ts +++ b/src/test/abilities/illuminate.test.ts @@ -1,9 +1,10 @@ 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, it, expect } from "vitest"; +import { afterEach, beforeAll, beforeEach, describe, it, expect, vi } from "vitest"; describe("Abilities - Illuminate", () => { let phaserGame: Phaser.Game; @@ -44,15 +45,26 @@ describe("Abilities - Illuminate", () => { expect(player.getStatStage(Stat.ACC)).toBe(0); }); - it("should guarantee double battle with any one LURE", async () => { + it("should increase the chance of double battles", async () => { game.override - .startingModifier([ - { name: "LURE" }, - ]) - .startingWave(2); + .moveset(Moves.SPLASH) + .ability(Abilities.ILLUMINATE) + .enemySpecies(Species.SUNKERN) + .enemyAbility(Abilities.BALL_FETCH) + .enemyMoveset(Moves.SPLASH) + .startingWave(9); + vi.spyOn(game.scene, "getDoubleBattleChance"); await game.classicMode.startBattle(); - expect(game.scene.getEnemyField().length).toBe(2); + game.move.select(Moves.SPLASH); + await game.doKillOpponents(); + await game.toNextWave(); + expect(game.scene.getDoubleBattleChance).toHaveLastReturnedWith(8); + + game.move.select(Moves.SPLASH); + await game.doKillOpponents(); + await game.toNextWave(); + expect(game.scene.getDoubleBattleChance).toHaveLastReturnedWith(2); }); }); diff --git a/src/test/abilities/no_guard.test.ts b/src/test/abilities/no_guard.test.ts index b0b454dd560..1afcba86276 100644 --- a/src/test/abilities/no_guard.test.ts +++ b/src/test/abilities/no_guard.test.ts @@ -53,15 +53,28 @@ describe("Abilities - No Guard", () => { expect(moveEffectPhase.hitCheck).toHaveReturnedWith(true); }); - it("should guarantee double battle with any one LURE", async () => { - game.override - .startingModifier([ - { name: "LURE" }, - ]) - .startingWave(2); + it("should increase the chance of double battles", async () => { + game.override + .moveset(Moves.SPLASH) + .ability(Abilities.NO_GUARD) + .enemySpecies(Species.SUNKERN) + .enemyAbility(Abilities.BALL_FETCH) + .enemyMoveset(Moves.SPLASH) + .startingWave(9); + + vi.spyOn(game.scene, "getDoubleBattleChance"); await game.classicMode.startBattle(); - expect(game.scene.getEnemyField().length).toBe(2); + game.move.select(Moves.SPLASH); + await game.doKillOpponents(); + await game.toNextWave(); + expect(game.scene.getDoubleBattleChance).toHaveLastReturnedWith(8); + + game.move.select(Moves.SPLASH); + await game.doKillOpponents(); + await game.toNextWave(); + expect(game.scene.getDoubleBattleChance).toHaveLastReturnedWith(2); }); + }); diff --git a/src/test/abilities/parental_bond.test.ts b/src/test/abilities/parental_bond.test.ts index c2f54fa4cfc..081402e8a5c 100644 --- a/src/test/abilities/parental_bond.test.ts +++ b/src/test/abilities/parental_bond.test.ts @@ -488,7 +488,6 @@ describe("Abilities - Parental Bond", () => { game.doSwitchPokemon(2); await game.toNextTurn(); - // TODO: Update hit count to 1 once Future Sight is fixed to not activate abilities if user is off the field - expect(enemyPokemon.damageAndUpdate).toHaveBeenCalledTimes(2); + expect(enemyPokemon.damageAndUpdate).toHaveBeenCalledTimes(1); }); }); diff --git a/src/test/abilities/pastel_veil.test.ts b/src/test/abilities/pastel_veil.test.ts index dd8360493a1..3851414039f 100644 --- a/src/test/abilities/pastel_veil.test.ts +++ b/src/test/abilities/pastel_veil.test.ts @@ -55,8 +55,6 @@ describe("Abilities - Pastel Veil", () => { const magikarp = game.scene.getPlayerField()[0]; ponyta.abilityIndex = 1; - expect(ponyta.hasAbility(Abilities.PASTEL_VEIL)).toBe(true); - game.move.select(Moves.SPLASH); game.move.select(Moves.TOXIC_THREAD, 1, BattlerIndex.PLAYER); diff --git a/src/test/moves/effectiveness.test.ts b/src/test/moves/effectiveness.test.ts index 7742178f595..c7be5e365b6 100644 --- a/src/test/moves/effectiveness.test.ts +++ b/src/test/moves/effectiveness.test.ts @@ -21,6 +21,8 @@ function testMoveEffectiveness(game: GameManager, move: Moves, targetSpecies: Sp const user = game.scene.addPlayerPokemon(getPokemonSpecies(Species.SNORLAX), 5); const target = game.scene.addEnemyPokemon(getPokemonSpecies(targetSpecies), 5, TrainerSlot.NONE); + game.scene.field.add(user); + game.scene.field.add(target); if (teraType !== undefined) { overrideHeldItems(target, false);