diff --git a/src/data/ability.ts b/src/data/ability.ts index cb3db27ddc1..015e3cfea31 100644 --- a/src/data/ability.ts +++ b/src/data/ability.ts @@ -1115,13 +1115,6 @@ export class VariableMovePowerAbAttr extends PreAttackAbAttr { } } -export class FieldPreventExplosiveMovesAbAttr extends AbAttr { - apply(pokemon: Pokemon, passive: boolean, cancelled: Utils.BooleanHolder, args: any[]): boolean | Promise { - cancelled.value = true; - return true; - } -} - /** * Multiplies a BattleStat if the checked Pokemon lacks this ability. * If this ability cannot stack, a BooleanHolder can be used to prevent this from stacking. @@ -3639,10 +3632,11 @@ export class PostFaintContactDamageAbAttr extends PostFaintAbAttr { applyPostFaint(pokemon: Pokemon, passive: boolean, attacker: Pokemon, move: Move, hitResult: HitResult, args: any[]): boolean { if (move.checkFlag(MoveFlags.MAKES_CONTACT, attacker, pokemon)) { const cancelled = new Utils.BooleanHolder(false); - pokemon.scene.getField(true).map(p=>applyAbAttrs(FieldPreventExplosiveMovesAbAttr, p, cancelled)); + pokemon.scene.getField(true).forEach(p => applyAbAttrs(PreventPostFaintContactDamageAbAttr, p, cancelled)); if (cancelled.value || attacker.hasAbilityWithAttr(BlockNonDirectDamageAbAttr)) { return false; } + attacker.damageAndUpdate(Math.ceil(attacker.getMaxHp() * (1 / this.damageRatio)), HitResult.OTHER); attacker.turnData.damageTaken += Math.ceil(attacker.getMaxHp() * (1 / this.damageRatio)); return true; @@ -3656,6 +3650,19 @@ export class PostFaintContactDamageAbAttr extends PostFaintAbAttr { } } +/** + * Prevents the effects of another Pokemon's {@link PostFaintContactDamageAbAttr} ability. + * {@linkcode apply} always returns true. + */ +export class PreventPostFaintContactDamageAbAttr extends AbAttr { + showAbility: boolean = false; + + apply(pokemon: Pokemon, passive: boolean, cancelled: Utils.BooleanHolder, args: any[]): boolean { + cancelled.value = true; + return true; + } +} + /** * Attribute used for abilities (Innards Out) that damage the opponent based on how much HP the last attack used to knock out the owner of the ability. */ @@ -3711,6 +3718,36 @@ export class RedirectTypeMoveAbAttr extends RedirectMoveAbAttr { export class BlockRedirectAbAttr extends AbAttr { } +/** + * Prevents other Pokemon from using moves that match the given {@linkcode moveCondition}. + * + * @param args [0] {@linkcode Move} The move being checked + * @param args [1] {@linkcode Pokemon} The user of the attack + */ +export class FieldPreventMovesAbAttr extends AbAttr { + public moveCondition: (Moves) => boolean; + + constructor(moveCondition: (Moves) => boolean) { + super(); + this.moveCondition = moveCondition; + } + + /** @param args See {@linkcode FieldPreventMovesAbAttr}. */ + apply(pokemon: Pokemon, passive: boolean, cancelled: Utils.BooleanHolder, args: any[]): boolean { + if (this.moveCondition((args[0] as Move).id)) { + cancelled.value = true; + return true; + } + + return false; + } + + /** @param args See {@linkcode FieldPreventMovesAbAttr}. */ + getTriggerMessage(pokemon: Pokemon, abilityName: string, ...args: any[]): string { + return (getPokemonMessage(args[1] as Pokemon, ` cannot use ${(args[0] as Move).name}`)); + } +} + export class ReduceStatusEffectDurationAbAttr extends AbAttr { private statusEffect: StatusEffect; @@ -4137,7 +4174,7 @@ async function applyAbAttrsInternal( } if (!quiet) { - const message = attr.getTriggerMessage(pokemon, ability.name, args); + const message = attr.getTriggerMessage(pokemon, (!passive ? pokemon.getAbility() : pokemon.getPassiveAbility()).name, args); if (message) { pokemon.scene.queueMessage(message); } @@ -4320,7 +4357,8 @@ export function initAbilities() { .attr(BlockOneHitKOAbAttr) .ignorable(), new Ability(Abilities.DAMP, 3) - .attr(FieldPreventExplosiveMovesAbAttr) + .attr(FieldPreventMovesAbAttr, (move) => [Moves.EXPLOSION, Moves.SELF_DESTRUCT, Moves.MIND_BLOWN, Moves.MISTY_EXPLOSION].includes(move)) + .attr(PreventPostFaintContactDamageAbAttr) .ignorable(), new Ability(Abilities.LIMBER, 3) .attr(StatusEffectImmunityAbAttr, StatusEffect.PARALYSIS) diff --git a/src/data/move.ts b/src/data/move.ts index 0b0af00a370..bf54e5b0128 100644 --- a/src/data/move.ts +++ b/src/data/move.ts @@ -10,7 +10,7 @@ import { Constructor } from "#app/utils"; import * as Utils from "../utils"; import { WeatherType } from "./weather"; import { ArenaTagSide, ArenaTrapTag, WeakenMoveTypeTag } from "./arena-tag"; -import { UnswappableAbilityAbAttr, UncopiableAbilityAbAttr, UnsuppressableAbilityAbAttr, BlockRecoilDamageAttr, BlockOneHitKOAbAttr, IgnoreContactAbAttr, MaxMultiHitAbAttr, applyAbAttrs, BlockNonDirectDamageAbAttr, MoveAbilityBypassAbAttr, ReverseDrainAbAttr, FieldPreventExplosiveMovesAbAttr, ForceSwitchOutImmunityAbAttr, BlockItemTheftAbAttr, applyPostAttackAbAttrs, ConfusionOnStatusEffectAbAttr, HealFromBerryUseAbAttr, IgnoreProtectOnContactAbAttr, IgnoreMoveEffectsAbAttr, applyPreDefendAbAttrs, MoveEffectChanceMultiplierAbAttr, WonderSkinAbAttr, applyPreAttackAbAttrs, MoveTypeChangeAttr, UserFieldMoveTypePowerBoostAbAttr, FieldMoveTypePowerBoostAbAttr, AllyMoveCategoryPowerBoostAbAttr, VariableMovePowerAbAttr } from "./ability"; +import { UnswappableAbilityAbAttr, UncopiableAbilityAbAttr, UnsuppressableAbilityAbAttr, BlockRecoilDamageAttr, BlockOneHitKOAbAttr, IgnoreContactAbAttr, MaxMultiHitAbAttr, applyAbAttrs, BlockNonDirectDamageAbAttr, MoveAbilityBypassAbAttr, ReverseDrainAbAttr, BlockItemTheftAbAttr, applyPostAttackAbAttrs, ConfusionOnStatusEffectAbAttr, HealFromBerryUseAbAttr, IgnoreProtectOnContactAbAttr, ForceSwitchOutImmunityAbAttr, IgnoreMoveEffectsAbAttr, applyPreDefendAbAttrs, MoveEffectChanceMultiplierAbAttr, WonderSkinAbAttr, applyPreAttackAbAttrs, MoveTypeChangeAttr, UserFieldMoveTypePowerBoostAbAttr, FieldMoveTypePowerBoostAbAttr, AllyMoveCategoryPowerBoostAbAttr, VariableMovePowerAbAttr } from "./ability"; import { allAbilities } from "./ability"; import { PokemonHeldItemModifier, BerryModifier, PreserveBerryModifier, PokemonMoveAccuracyBoosterModifier, AttackTypeBoosterModifier, PokemonMultiHitModifier } from "../modifier/modifier"; import { BattlerIndex, BattleType } from "../battle"; @@ -96,6 +96,7 @@ export enum MoveFlags { * Indicates a move is able to be redirected to allies in a double battle if the attacker faints */ REDIRECT_COUNTER = 1 << 18, + EXPLOSIVE_MOVE = 1 << 19 } type MoveConditionFunc = (user: Pokemon, target: Pokemon, move: Move) => boolean; @@ -520,6 +521,17 @@ export default class Move implements Localizable { return this; } + /** + * Sets the {@linkcode MoveFlags.EXPLOSIVE_MOVE} flag for the calling Move + * @param explosiveMove The value (boolean) to set the flag to + * example: @see {@linkcode Moves.EXPLOSION} + * @returns The {@linkcode Move} that called this function + */ + explosiveMove(explosiveMove?: boolean): this { + this.setFlag(MoveFlags.EXPLOSIVE_MOVE, explosiveMove); + return this; + } + /** * Sets the {@linkcode MoveFlags.TRIAGE_MOVE} flag for the calling Move * @param triageMove The value (boolean) to set the flag to @@ -5916,16 +5928,6 @@ const failOnBossCondition: MoveConditionFunc = (user, target, move) => !target.i const failOnMaxCondition: MoveConditionFunc = (user, target, move) => !target.isMax(); -const failIfDampCondition: MoveConditionFunc = (user, target, move) => { - const cancelled = new Utils.BooleanHolder(false); - user.scene.getField(true).map(p=>applyAbAttrs(FieldPreventExplosiveMovesAbAttr, p, cancelled)); - // Queue a message if an ability prevented usage of the move - if (cancelled.value) { - user.scene.queueMessage(i18next.t("moveTriggers:cannotUseMove", {pokemonName: getPokemonNameWithAffix(user), moveName: move.name})); - } - return !cancelled.value; -}; - const userSleptOrComatoseCondition: MoveConditionFunc = (user: Pokemon, target: Pokemon, move: Move) => user.status?.effect === StatusEffect.SLEEP || user.hasAbility(Abilities.COMATOSE); const targetSleptOrComatoseCondition: MoveConditionFunc = (user: Pokemon, target: Pokemon, move: Move) => target.status?.effect === StatusEffect.SLEEP || target.hasAbility(Abilities.COMATOSE); @@ -6470,7 +6472,7 @@ export function initMoves() { new AttackMove(Moves.SELF_DESTRUCT, Type.NORMAL, MoveCategory.PHYSICAL, 200, 100, 5, -1, 0, 1) .attr(SacrificialAttr) .makesContact(false) - .condition(failIfDampCondition) + .explosiveMove() .target(MoveTarget.ALL_NEAR_OTHERS), new AttackMove(Moves.EGG_BOMB, Type.NORMAL, MoveCategory.PHYSICAL, 100, 75, 10, -1, 0, 1) .makesContact(false) @@ -6560,9 +6562,9 @@ export function initMoves() { new AttackMove(Moves.CRABHAMMER, Type.WATER, MoveCategory.PHYSICAL, 100, 90, 10, -1, 0, 1) .attr(HighCritAttr), new AttackMove(Moves.EXPLOSION, Type.NORMAL, MoveCategory.PHYSICAL, 250, 100, 5, -1, 0, 1) - .condition(failIfDampCondition) .attr(SacrificialAttr) .makesContact(false) + .explosiveMove() .target(MoveTarget.ALL_NEAR_OTHERS), new AttackMove(Moves.FURY_SWIPES, Type.NORMAL, MoveCategory.PHYSICAL, 18, 80, 15, -1, 0, 1) .attr(MultiHitAttr), @@ -8184,8 +8186,8 @@ export function initMoves() { .ignoresVirtual(), /* End Unused */ new AttackMove(Moves.MIND_BLOWN, Type.FIRE, MoveCategory.SPECIAL, 150, 100, 5, -1, 0, 7) - .condition(failIfDampCondition) .attr(HalfSacrificialAttr) + .explosiveMove() .target(MoveTarget.ALL_NEAR_OTHERS), new AttackMove(Moves.PLASMA_FISTS, Type.ELECTRIC, MoveCategory.PHYSICAL, 100, 100, 15, -1, 0, 7) .punchingMove() @@ -8466,7 +8468,7 @@ export function initMoves() { .attr(SacrificialAttr) .target(MoveTarget.ALL_NEAR_OTHERS) .attr(MovePowerMultiplierAttr, (user, target, move) => user.scene.arena.getTerrainType() === TerrainType.MISTY && user.isGrounded() ? 1.5 : 1) - .condition(failIfDampCondition) + .explosiveMove() .makesContact(false), new AttackMove(Moves.GRASSY_GLIDE, Type.GRASS, MoveCategory.PHYSICAL, 55, 100, 20, -1, 0, 8) .attr(IncrementMovePriorityAttr,(user,target,move) =>user.scene.arena.getTerrainType()===TerrainType.GRASSY&&user.isGrounded()), diff --git a/src/field/pokemon.ts b/src/field/pokemon.ts index 588b6839d70..9c52e250a3d 100644 --- a/src/field/pokemon.ts +++ b/src/field/pokemon.ts @@ -22,8 +22,8 @@ import { BattleStat } from "../data/battle-stat"; import { BattlerTag, BattlerTagLapseType, EncoreTag, GroundedTag, HighestStatBoostTag, TypeImmuneTag, getBattlerTag, SemiInvulnerableTag, TypeBoostTag, ExposedTag } from "../data/battler-tags"; import { WeatherType } from "../data/weather"; import { TempBattleStat } from "../data/temp-battle-stat"; -import { ArenaTagSide, NoCritTag, WeakenMoveScreenTag } from "../data/arena-tag"; -import { Ability, AbAttr, BattleStatMultiplierAbAttr, BlockCritAbAttr, BonusCritAbAttr, BypassBurnDamageReductionAbAttr, FieldPriorityMoveImmunityAbAttr, IgnoreOpponentStatChangesAbAttr, MoveImmunityAbAttr, PreDefendFullHpEndureAbAttr, ReceivedMoveDamageMultiplierAbAttr, ReduceStatusEffectDurationAbAttr, StabBoostAbAttr, StatusEffectImmunityAbAttr, TypeImmunityAbAttr, WeightMultiplierAbAttr, allAbilities, applyAbAttrs, applyBattleStatMultiplierAbAttrs, applyPreApplyBattlerTagAbAttrs, applyPreAttackAbAttrs, applyPreDefendAbAttrs, applyPreSetStatusAbAttrs, UnsuppressableAbilityAbAttr, SuppressFieldAbilitiesAbAttr, NoFusionAbilityAbAttr, MultCritAbAttr, IgnoreTypeImmunityAbAttr, DamageBoostAbAttr, IgnoreTypeStatusEffectImmunityAbAttr, ConditionalCritAbAttr, applyFieldBattleStatMultiplierAbAttrs, FieldMultiplyBattleStatAbAttr, AddSecondStrikeAbAttr, IgnoreOpponentEvasionAbAttr, UserFieldStatusEffectImmunityAbAttr, UserFieldBattlerTagImmunityAbAttr, BattlerTagImmunityAbAttr } from "../data/ability"; +import { ArenaTagSide, WeakenMoveScreenTag, WeakenMoveTypeTag } from "../data/arena-tag"; +import { Ability, AbAttr, BattleStatMultiplierAbAttr, BlockCritAbAttr, BonusCritAbAttr, BypassBurnDamageReductionAbAttr, FieldPriorityMoveImmunityAbAttr, FieldVariableMovePowerAbAttr, IgnoreOpponentStatChangesAbAttr, MoveImmunityAbAttr, MoveTypeChangeAttr, PreApplyBattlerTagAbAttr, PreDefendFullHpEndureAbAttr, ReceivedMoveDamageMultiplierAbAttr, ReduceStatusEffectDurationAbAttr, StabBoostAbAttr, StatusEffectImmunityAbAttr, TypeImmunityAbAttr, VariableMovePowerAbAttr, WeightMultiplierAbAttr, allAbilities, applyAbAttrs, applyBattleStatMultiplierAbAttrs, applyPreApplyBattlerTagAbAttrs, applyPreAttackAbAttrs, applyPreDefendAbAttrs, applyPreSetStatusAbAttrs, UnsuppressableAbilityAbAttr, SuppressFieldAbilitiesAbAttr, NoFusionAbilityAbAttr, MultCritAbAttr, IgnoreTypeImmunityAbAttr, DamageBoostAbAttr, IgnoreTypeStatusEffectImmunityAbAttr, ConditionalCritAbAttr, applyFieldBattleStatMultiplierAbAttrs, FieldMultiplyBattleStatAbAttr, FieldPreventMovesAbAttr } from "../data/ability"; import PokemonData from "../system/pokemon-data"; import { BattlerIndex } from "../battle"; import { Mode } from "../ui/ui"; @@ -1942,6 +1942,11 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { typeMultiplier.value = 0; } + // Check if other Pokemon on the field have an ability that prevents this move + for (const other of this.scene.getField(true).filter(p => source.id !== p.id)) { + applyAbAttrs(FieldPreventMovesAbAttr, other, cancelled, move, source); + } + // Apply arena tags for conditional protection if (!move.checkFlag(MoveFlags.IGNORE_PROTECT, source, this) && !move.isAllyTarget()) { this.scene.arena.applyTagsForSide(ArenaTagType.QUICK_GUARD, defendingSide, cancelled, this, move.priority); diff --git a/src/phases.ts b/src/phases.ts index 73413b248a4..ddd57018c42 100644 --- a/src/phases.ts +++ b/src/phases.ts @@ -25,7 +25,7 @@ import { Starter } from "./ui/starter-select-ui-handler"; import { Gender } from "./data/gender"; import { Weather, WeatherType, getRandomWeatherType, getTerrainBlockMessage, getWeatherDamageMessage, getWeatherLapseMessage } from "./data/weather"; import { ArenaTagSide, ArenaTrapTag, MistTag, TrickRoomTag } from "./data/arena-tag"; -import { CheckTrappedAbAttr, PostAttackAbAttr, PostBattleAbAttr, PostDefendAbAttr, PostSummonAbAttr, PostTurnAbAttr, PostWeatherLapseAbAttr, PreSwitchOutAbAttr, PreWeatherDamageAbAttr, ProtectStatAbAttr, RedirectMoveAbAttr, BlockRedirectAbAttr, RunSuccessAbAttr, StatChangeMultiplierAbAttr, SuppressWeatherEffectAbAttr, SyncEncounterNatureAbAttr, applyAbAttrs, applyCheckTrappedAbAttrs, applyPostAttackAbAttrs, applyPostBattleAbAttrs, applyPostDefendAbAttrs, applyPostSummonAbAttrs, applyPostTurnAbAttrs, applyPostWeatherLapseAbAttrs, applyPreStatChangeAbAttrs, applyPreSwitchOutAbAttrs, applyPreWeatherEffectAbAttrs, IncrementMovePriorityAbAttr, applyPostVictoryAbAttrs, PostVictoryAbAttr, BlockNonDirectDamageAbAttr as BlockNonDirectDamageAbAttr, applyPostKnockOutAbAttrs, PostKnockOutAbAttr, PostBiomeChangeAbAttr, applyPostFaintAbAttrs, PostFaintAbAttr, IncreasePpAbAttr, PostStatChangeAbAttr, applyPostStatChangeAbAttrs, AlwaysHitAbAttr, PreventBerryUseAbAttr, StatChangeCopyAbAttr, PokemonTypeChangeAbAttr, applyPreAttackAbAttrs, applyPostMoveUsedAbAttrs, PostMoveUsedAbAttr, MaxMultiHitAbAttr, HealFromBerryUseAbAttr, IgnoreMoveEffectsAbAttr, BlockStatusDamageAbAttr, BypassSpeedChanceAbAttr, AddSecondStrikeAbAttr } from "./data/ability"; +import { CheckTrappedAbAttr, PostAttackAbAttr, PostBattleAbAttr, PostDefendAbAttr, PostSummonAbAttr, PostTurnAbAttr, PostWeatherLapseAbAttr, PreSwitchOutAbAttr, PreWeatherDamageAbAttr, ProtectStatAbAttr, RedirectMoveAbAttr, BlockRedirectAbAttr, RunSuccessAbAttr, StatChangeMultiplierAbAttr, SuppressWeatherEffectAbAttr, SyncEncounterNatureAbAttr, applyAbAttrs, applyCheckTrappedAbAttrs, applyPostAttackAbAttrs, applyPostBattleAbAttrs, applyPostDefendAbAttrs, applyPostSummonAbAttrs, applyPostTurnAbAttrs, applyPostWeatherLapseAbAttrs, applyPreStatChangeAbAttrs, applyPreSwitchOutAbAttrs, applyPreWeatherEffectAbAttrs, IncrementMovePriorityAbAttr, applyPostVictoryAbAttrs, PostVictoryAbAttr, BlockNonDirectDamageAbAttr as BlockNonDirectDamageAbAttr, applyPostKnockOutAbAttrs, PostKnockOutAbAttr, PostBiomeChangeAbAttr, applyPostFaintAbAttrs, PostFaintAbAttr, IncreasePpAbAttr, PostStatChangeAbAttr, applyPostStatChangeAbAttrs, AlwaysHitAbAttr, PreventBerryUseAbAttr, StatChangeCopyAbAttr, PokemonTypeChangeAbAttr, applyPreAttackAbAttrs, applyPostMoveUsedAbAttrs, PostMoveUsedAbAttr, MaxMultiHitAbAttr, HealFromBerryUseAbAttr, IgnoreMoveEffectsAbAttr, BlockStatusDamageAbAttr, BypassSpeedChanceAbAttr, AddSecondStrikeAbAttr, FieldPreventMovesAbAttr } from "./data/ability"; import { Unlockables, getUnlockableName } from "./system/unlockables"; import { getBiomeKey } from "./field/arena"; import { BattleType, BattlerIndex, TurnCommand } from "./battle"; @@ -3048,6 +3048,15 @@ export class MoveEffectPhase extends PokemonPhase { return this.end(); } + // Check if other Pokemon on the field have an ability that prevents this move + const prevented = new Utils.BooleanHolder(false); + for (const other of this.scene.getField(true).filter(p => user.id !== p.id)) { + applyAbAttrs(FieldPreventMovesAbAttr, other, prevented, move, user as Pokemon); + } + if (prevented.value) { + return this.end(); + } + /** All move effect attributes are chained together in this array to be applied asynchronously. */ const applyAttrs: Promise[] = []; diff --git a/src/test/abilities/aftermath.test.ts b/src/test/abilities/aftermath.test.ts new file mode 100644 index 00000000000..db857e87c95 --- /dev/null +++ b/src/test/abilities/aftermath.test.ts @@ -0,0 +1,80 @@ +import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; +import GameManager from "#test/utils/gameManager"; +import { getMovePosition } from "#test/utils/gameManagerUtils"; +import * as overrides from "#app/overrides"; +import { Moves } from "#enums/moves"; +import { Abilities } from "#enums/abilities"; +import { Species } from "#enums/species"; +import { TurnEndPhase } from "#app/phases.js"; + +const TIMEOUT = 20 * 1000; + +describe("Abilities - Aftermath", () => { + let phaserGame: Phaser.Game; + let game: GameManager; + + beforeAll(() => { + phaserGame = new Phaser.Game({ + type: Phaser.HEADLESS, + }); + }); + + afterEach(() => { + game.phaseInterceptor.restoreOg(); + }); + + beforeEach(() => { + game = new GameManager(phaserGame); + const moveToUse = Moves.SPLASH; + vi.spyOn(overrides, "SINGLE_BATTLE_OVERRIDE", "get").mockReturnValue(true); + vi.spyOn(overrides, "ABILITY_OVERRIDE", "get").mockReturnValue(Abilities.BATTLE_BOND); + vi.spyOn(overrides, "MOVESET_OVERRIDE", "get").mockReturnValue([moveToUse]); + vi.spyOn(overrides, "OPP_MOVESET_OVERRIDE", "get").mockReturnValue([Moves.TACKLE, Moves.TACKLE, Moves.TACKLE, Moves.TACKLE]); + }); + + it("deals 25% of attacker's HP as damage to attacker when defeated by contact move", async () => { + const moveToUse = Moves.TACKLE; + const enemyAbility = Abilities.AFTERMATH; + + vi.spyOn(overrides, "ABILITY_OVERRIDE", "get").mockReturnValue(Abilities.NONE); + 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.BIDOOF); + vi.spyOn(overrides, "OPP_ABILITY_OVERRIDE", "get").mockReturnValue(enemyAbility); + + await game.startBattle(); + + game.scene.getEnemyParty()[0].hp = 1; + + game.doAttack(getMovePosition(game.scene, 0, moveToUse)); + + await game.phaseInterceptor.to(TurnEndPhase); + + expect(game.phaseInterceptor.log).toContain("FaintPhase"); + expect(game.phaseInterceptor.log).toContain("ShowAbilityPhase"); + expect(game.scene.getParty()[0].hp).toBeCloseTo(Math.floor(game.scene.getParty()[0].getMaxHp() * 0.75)); + }, TIMEOUT); + + it("does not activate on non-contact moves", async () => { + const moveToUse = Moves.WATER_GUN; + const enemyAbility = Abilities.AFTERMATH; + + vi.spyOn(overrides, "ABILITY_OVERRIDE", "get").mockReturnValue(Abilities.NONE); + 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.BIDOOF); + vi.spyOn(overrides, "OPP_ABILITY_OVERRIDE", "get").mockReturnValue(enemyAbility); + + await game.startBattle(); + + game.scene.getEnemyParty()[0].hp = 1; + + game.doAttack(getMovePosition(game.scene, 0, moveToUse)); + + await game.phaseInterceptor.to(TurnEndPhase); + + expect(game.phaseInterceptor.log).toContain("FaintPhase"); + expect(game.phaseInterceptor.log).not.toContain("ShowAbilityPhase"); + expect(game.scene.getParty()[0].getHpRatio()).toBeCloseTo(1); + }, TIMEOUT); +}); diff --git a/src/test/abilities/damp.test.ts b/src/test/abilities/damp.test.ts new file mode 100644 index 00000000000..71c4c7b6315 --- /dev/null +++ b/src/test/abilities/damp.test.ts @@ -0,0 +1,100 @@ +import { Abilities } from "#app/enums/abilities.js"; +import { Moves } from "#app/enums/moves"; +import { Species } from "#app/enums/species"; +import * as overrides from "#app/overrides"; +import { TurnEndPhase } from "#app/phases"; +import GameManager from "#app/test/utils/gameManager"; +import { getMovePosition } from "#app/test/utils/gameManagerUtils"; +import Phaser from "phaser"; +import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; + +const TIMEOUT = 20 * 1000; + +describe("Abilities - Damp", () => { + 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("prevents self-destruction effect on explosive attacks", async() => { + const moveToUse = Moves.EXPLOSION; + const enemyAbility = Abilities.DAMP; + + vi.spyOn(overrides, "ABILITY_OVERRIDE", "get").mockReturnValue(Abilities.NONE); + 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.BIDOOF); + vi.spyOn(overrides, "OPP_ABILITY_OVERRIDE", "get").mockReturnValue(enemyAbility); + + await game.startBattle(); + + game.doAttack(getMovePosition(game.scene, 0, moveToUse)); + + await game.phaseInterceptor.to(TurnEndPhase); + + expect(game.phaseInterceptor.log).toContain("ShowAbilityPhase"); + expect(game.phaseInterceptor.log).not.toContain("FaintPhase"); + }, TIMEOUT); + + // Invalid if aftermath.test.ts has a failure. + it("silently prevents Aftermath from triggering", async() => { + const moveToUse = Moves.TACKLE; + const playerAbility = Abilities.DAMP; + const enemyAbility = Abilities.AFTERMATH; + + vi.spyOn(overrides, "ABILITY_OVERRIDE", "get").mockReturnValue(playerAbility); + 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.BIDOOF); + vi.spyOn(overrides, "OPP_ABILITY_OVERRIDE", "get").mockReturnValue(enemyAbility); + + await game.startBattle(); + + game.scene.getEnemyParty()[0].hp = 1; + + game.doAttack(getMovePosition(game.scene, 0, moveToUse)); + + await game.phaseInterceptor.to(TurnEndPhase); + + expect(game.phaseInterceptor.log).toContain("FaintPhase"); + expect(game.phaseInterceptor.log).not.toContain("ShowAbilityPhase"); + expect(game.scene.getParty()[0].getHpRatio()).toBe(1); + }, TIMEOUT); + + // Ensures fix of #1476. + it("does not show ability popup during AI calculations", async() => { + const moveToUse = Moves.EXPLOSION; + const enemyAbility = Abilities.DAMP; + + vi.spyOn(overrides, "ABILITY_OVERRIDE", "get").mockReturnValue(Abilities.NONE); + 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.BIDOOF); + vi.spyOn(overrides, "OPP_ABILITY_OVERRIDE", "get").mockReturnValue(enemyAbility); + + await game.startBattle(); + + game.doAttack(getMovePosition(game.scene, 0, moveToUse)); + + await game.phaseInterceptor.to(TurnEndPhase); + + expect(game.phaseInterceptor.log).toContain("ShowAbilityPhase"); + }, TIMEOUT); + + // TODO Test some of the other AbAttrs that use `args` + // BattlerTagImmunityAbAttr, StatusEffectImmunityAbAttr +});