From 31406b35fd385ef0a184f5584ff28e9d416c4c5b Mon Sep 17 00:00:00 2001 From: Zach Day Date: Wed, 12 Jun 2024 14:46:45 -0400 Subject: [PATCH] [Move] Fix type immunities granted by abilities only applying to attacks (#2145) * Fix type immunity given by abilities only applying to attacking moves * Add tests for type immunity granted by abilities * Use Sap Sipper as base for testing --- src/data/ability.ts | 10 ++- src/test/abilities/sap_sipper.test.ts | 116 ++++++++++++++++++++++++++ 2 files changed, 124 insertions(+), 2 deletions(-) create mode 100644 src/test/abilities/sap_sipper.test.ts diff --git a/src/data/ability.ts b/src/data/ability.ts index 5db2e34b2a0..c974f2c31d0 100755 --- a/src/data/ability.ts +++ b/src/data/ability.ts @@ -9,7 +9,7 @@ import { BattlerTag } from "./battler-tags"; import { BattlerTagType } from "./enums/battler-tag-type"; import { StatusEffect, getNonVolatileStatusEffects, getStatusEffectDescriptor, getStatusEffectHealText } from "./status-effect"; import { Gender } from "./gender"; -import Move, { AttackMove, MoveCategory, MoveFlags, MoveTarget, StatusMoveTypeImmunityAttr, FlinchAttr, OneHitKOAttr, HitHealAttr, allMoves, StatusMove, SelfStatusMove, VariablePowerAttr, applyMoveAttrs, IncrementMovePriorityAttr } from "./move"; +import Move, { AttackMove, MoveCategory, MoveFlags, MoveTarget, FlinchAttr, OneHitKOAttr, HitHealAttr, allMoves, StatusMove, SelfStatusMove, VariablePowerAttr, applyMoveAttrs, IncrementMovePriorityAttr } from "./move"; import { ArenaTagSide, ArenaTrapTag } from "./arena-tag"; import { ArenaTagType } from "./enums/arena-tag-type"; import { Stat, getStatName } from "./pokemon-stat"; @@ -357,6 +357,7 @@ export class TypeImmunityAbAttr extends PreDefendAbAttr { } /** + * Applies immunity if this ability grants immunity to the type of the given move. * @param pokemon {@linkcode Pokemon} the defending Pokemon * @param passive N/A * @param attacker {@linkcode Pokemon} the attacking Pokemon @@ -366,7 +367,12 @@ export class TypeImmunityAbAttr extends PreDefendAbAttr { * @param args [1] {@linkcode Utils.NumberHolder} type of move being defended against in case it has changed from default type */ applyPreDefend(pokemon: Pokemon, passive: boolean, attacker: Pokemon, move: Move, cancelled: Utils.BooleanHolder, args: any[]): boolean { - if ((move instanceof AttackMove || move.getAttrs(StatusMoveTypeImmunityAttr).find(attr => attr.immuneType === this.immuneType)) && move.type === this.immuneType) { + // Field moves should ignore immunity + if ([ MoveTarget.BOTH_SIDES, MoveTarget.ENEMY_SIDE, MoveTarget.USER_SIDE ].includes(move.moveTarget)) { + return false; + } + + if (move.type === this.immuneType) { (args[0] as Utils.NumberHolder).value = 0; return true; } diff --git a/src/test/abilities/sap_sipper.test.ts b/src/test/abilities/sap_sipper.test.ts new file mode 100644 index 00000000000..5207ae8bb43 --- /dev/null +++ b/src/test/abilities/sap_sipper.test.ts @@ -0,0 +1,116 @@ +import {afterEach, beforeAll, beforeEach, describe, expect, it, vi} from "vitest"; +import Phaser from "phaser"; +import GameManager from "#app/test/utils/gameManager"; +import * as overrides from "#app/overrides"; +import {Species} from "#app/data/enums/species"; +import { + CommandPhase, + EnemyCommandPhase, TurnEndPhase, +} from "#app/phases"; +import {Mode} from "#app/ui/ui"; +import {Moves} from "#app/data/enums/moves"; +import {getMovePosition} from "#app/test/utils/gameManagerUtils"; +import {Command} from "#app/ui/command-ui-handler"; +import { Abilities } from "#app/data/enums/abilities.js"; +import { BattleStat } from "#app/data/battle-stat.js"; +import { TerrainType } from "#app/data/terrain.js"; + +// See also: ArenaTypeAbAttr +describe("Abilities - Sap Sipper", () => { + let phaserGame: Phaser.Game; + let game: GameManager; + + beforeAll(() => { + phaserGame = new Phaser.Game({ + type: Phaser.HEADLESS, + }); + }); + + afterEach(() => { + game.phaseInterceptor.restoreOg(); + }); + + beforeEach(() => { + game = new GameManager(phaserGame); + vi.spyOn(overrides, "SINGLE_BATTLE_OVERRIDE", "get").mockReturnValue(true); + vi.spyOn(overrides, "NEVER_CRIT_OVERRIDE", "get").mockReturnValue(true); + }); + + it("raise attack 1 level and block effects when activated against a grass attack", async() => { + const moveToUse = Moves.LEAFAGE; + const enemyAbility = Abilities.SAP_SIPPER; + + vi.spyOn(overrides, "MOVESET_OVERRIDE", "get").mockReturnValue([moveToUse]); + vi.spyOn(overrides, "OPP_MOVESET_OVERRIDE", "get").mockReturnValue([Moves.SPLASH, Moves.NONE, Moves.NONE, Moves.NONE]); + vi.spyOn(overrides, "OPP_SPECIES_OVERRIDE", "get").mockReturnValue(Species.DUSKULL); + vi.spyOn(overrides, "OPP_ABILITY_OVERRIDE", "get").mockReturnValue(enemyAbility); + + await game.startBattle(); + + const startingOppHp = game.scene.currentBattle.enemyParty[0].hp; + + game.onNextPrompt("CommandPhase", Mode.COMMAND, () => { + game.scene.ui.setMode(Mode.FIGHT, (game.scene.getCurrentPhase() as CommandPhase).getFieldIndex()); + }); + game.onNextPrompt("CommandPhase", Mode.FIGHT, () => { + const movePosition = getMovePosition(game.scene, 0, moveToUse); + (game.scene.getCurrentPhase() as CommandPhase).handleCommand(Command.FIGHT, movePosition, false); + }); + + await game.phaseInterceptor.runFrom(EnemyCommandPhase).to(TurnEndPhase); + + expect(startingOppHp - game.scene.getEnemyParty()[0].hp).toBe(0); + expect(game.scene.getEnemyParty()[0].summonData.battleStats[BattleStat.ATK]).toBe(1); + }); + + it("raise attack 1 level and block effects when activated against a grass status move", async() => { + const moveToUse = Moves.SPORE; + const enemyAbility = Abilities.SAP_SIPPER; + + vi.spyOn(overrides, "MOVESET_OVERRIDE", "get").mockReturnValue([moveToUse]); + vi.spyOn(overrides, "OPP_MOVESET_OVERRIDE", "get").mockReturnValue([Moves.SPLASH, Moves.NONE, Moves.NONE, Moves.NONE]); + vi.spyOn(overrides, "OPP_SPECIES_OVERRIDE", "get").mockReturnValue(Species.RATTATA); + vi.spyOn(overrides, "OPP_ABILITY_OVERRIDE", "get").mockReturnValue(enemyAbility); + + await game.startBattle(); + + game.onNextPrompt("CommandPhase", Mode.COMMAND, () => { + game.scene.ui.setMode(Mode.FIGHT, (game.scene.getCurrentPhase() as CommandPhase).getFieldIndex()); + }); + game.onNextPrompt("CommandPhase", Mode.FIGHT, () => { + const movePosition = getMovePosition(game.scene, 0, moveToUse); + (game.scene.getCurrentPhase() as CommandPhase).handleCommand(Command.FIGHT, movePosition, false); + }); + + await game.phaseInterceptor.runFrom(EnemyCommandPhase).to(TurnEndPhase); + + expect(game.scene.getEnemyParty()[0].status).toBeUndefined(); + expect(game.scene.getEnemyParty()[0].summonData.battleStats[BattleStat.ATK]).toBe(1); + }); + + it("do not activate against status moves that target the field", async() => { + const moveToUse = Moves.GRASSY_TERRAIN; + const enemyAbility = Abilities.SAP_SIPPER; + + vi.spyOn(overrides, "MOVESET_OVERRIDE", "get").mockReturnValue([moveToUse]); + vi.spyOn(overrides, "OPP_MOVESET_OVERRIDE", "get").mockReturnValue([Moves.SPLASH, Moves.NONE, Moves.NONE, Moves.NONE]); + vi.spyOn(overrides, "OPP_SPECIES_OVERRIDE", "get").mockReturnValue(Species.RATTATA); + vi.spyOn(overrides, "OPP_ABILITY_OVERRIDE", "get").mockReturnValue(enemyAbility); + + await game.startBattle(); + + game.onNextPrompt("CommandPhase", Mode.COMMAND, () => { + game.scene.ui.setMode(Mode.FIGHT, (game.scene.getCurrentPhase() as CommandPhase).getFieldIndex()); + }); + game.onNextPrompt("CommandPhase", Mode.FIGHT, () => { + const movePosition = getMovePosition(game.scene, 0, moveToUse); + (game.scene.getCurrentPhase() as CommandPhase).handleCommand(Command.FIGHT, movePosition, false); + }); + + await game.phaseInterceptor.runFrom(EnemyCommandPhase).to(TurnEndPhase); + + expect(game.scene.arena.terrain).toBeDefined(); + expect(game.scene.arena.terrain.terrainType).toBe(TerrainType.GRASSY); + expect(game.scene.getEnemyParty()[0].summonData.battleStats[BattleStat.ATK]).toBe(0); + }); +});