From 40e15ec3c4a997827b65841dc76d66f55c8e275d Mon Sep 17 00:00:00 2001 From: Zach Day Date: Sun, 16 Jun 2024 01:28:13 -0400 Subject: [PATCH 01/15] Squashed commit of the following: Merge branch 'main' into damp_test Cleanup tests Add tests for interaction with Aftermath Prevent ability popup when suppressing Aftermath damage Adds test for damp visual functionality Fix missing import Prevent explosion effect and animation with Damp Split the PostFaintContactDamage portion of Damp into its own AbAttr Standardize Damp effect via MoveAttr --- src/data/ability.ts | 58 +++++++++++++--- src/data/move.ts | 32 +++++---- src/field/pokemon.ts | 7 +- src/phases.ts | 11 ++- src/test/abilities/aftermath.test.ts | 80 +++++++++++++++++++++ src/test/abilities/damp.test.ts | 100 +++++++++++++++++++++++++++ 6 files changed, 261 insertions(+), 27 deletions(-) create mode 100644 src/test/abilities/aftermath.test.ts create mode 100644 src/test/abilities/damp.test.ts diff --git a/src/data/ability.ts b/src/data/ability.ts index 3280f81dd6d..759a18d9dc6 100755 --- a/src/data/ability.ts +++ b/src/data/ability.ts @@ -1051,13 +1051,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. @@ -3208,10 +3201,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) { 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; @@ -3225,6 +3219,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. */ @@ -3280,6 +3287,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; @@ -3647,7 +3684,7 @@ function applyAbAttrsInternal(attrType: { new(...args: any } } if (!quiet) { - const message = attr.getTriggerMessage(pokemon, (!passive ? pokemon.getAbility() : pokemon.getPassiveAbility()).name, args); + const message = attr.getTriggerMessage(pokemon, (!passive ? pokemon.getAbility() : pokemon.getPassiveAbility()).name, ...args); if (message) { if (isAsync) { pokemon.scene.ui.showText(message, null, () => pokemon.scene.ui.showText(null, 0), null, true); @@ -3852,7 +3889,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 3116bf58f56..35ad27ed6d4 100644 --- a/src/data/move.ts +++ b/src/data/move.ts @@ -9,7 +9,7 @@ import { Type } from "./type"; import * as Utils from "../utils"; import { WeatherType } from "./weather"; import { ArenaTagSide, ArenaTrapTag } from "./arena-tag"; -import { UnswappableAbilityAbAttr, UncopiableAbilityAbAttr, UnsuppressableAbilityAbAttr, BlockRecoilDamageAttr, BlockOneHitKOAbAttr, IgnoreContactAbAttr, MaxMultiHitAbAttr, applyAbAttrs, BlockNonDirectDamageAbAttr, applyPreSwitchOutAbAttrs, PreSwitchOutAbAttr, applyPostDefendAbAttrs, PostDefendContactApplyStatusEffectAbAttr, MoveAbilityBypassAbAttr, ReverseDrainAbAttr, FieldPreventExplosiveMovesAbAttr, ForceSwitchOutImmunityAbAttr, BlockItemTheftAbAttr, applyPostAttackAbAttrs, ConfusionOnStatusEffectAbAttr, HealFromBerryUseAbAttr, IgnoreProtectOnContactAbAttr } from "./ability"; +import { UnswappableAbilityAbAttr, UncopiableAbilityAbAttr, UnsuppressableAbilityAbAttr, BlockRecoilDamageAttr, BlockOneHitKOAbAttr, IgnoreContactAbAttr, MaxMultiHitAbAttr, applyAbAttrs, BlockNonDirectDamageAbAttr, applyPreSwitchOutAbAttrs, PreSwitchOutAbAttr, applyPostDefendAbAttrs, PostDefendContactApplyStatusEffectAbAttr, MoveAbilityBypassAbAttr, ReverseDrainAbAttr, BlockItemTheftAbAttr, applyPostAttackAbAttrs, ConfusionOnStatusEffectAbAttr, HealFromBerryUseAbAttr, IgnoreProtectOnContactAbAttr, ForceSwitchOutImmunityAbAttr } from "./ability"; import { allAbilities } from "./ability"; import { PokemonHeldItemModifier, BerryModifier, PreserveBerryModifier } from "../modifier/modifier"; import { BattlerIndex } from "../battle"; @@ -90,6 +90,7 @@ export enum MoveFlags { * Enables all hits of a multi-hit move to be accuracy checked individually */ CHECK_ALL_HITS = 1 << 17, + EXPLOSIVE_MOVE = 1 << 18 } type MoveConditionFunc = (user: Pokemon, target: Pokemon, move: Move) => boolean; @@ -503,6 +504,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 @@ -5360,16 +5372,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(getPokemonMessage(user, ` cannot use ${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); @@ -5821,7 +5823,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) @@ -5911,9 +5913,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), @@ -7521,8 +7523,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() @@ -7793,7 +7795,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 396f3f3e539..d0fd4958387 100644 --- a/src/field/pokemon.ts +++ b/src/field/pokemon.ts @@ -22,7 +22,7 @@ import { BattlerTag, BattlerTagLapseType, EncoreTag, HelpingHandTag, HighestStat import { WeatherType } from "../data/weather"; import { TempBattleStat } from "../data/temp-battle-stat"; 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 } from "../data/ability"; +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"; @@ -1728,6 +1728,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()) { const defendingSide = this.isPlayer() ? ArenaTagSide.PLAYER : ArenaTagSide.ENEMY; diff --git a/src/phases.ts b/src/phases.ts index ad5819edf30..ae0ad4df35b 100644 --- a/src/phases.ts +++ b/src/phases.ts @@ -26,7 +26,7 @@ import { Gender } from "./data/gender"; import { Weather, WeatherType, getRandomWeatherType, getTerrainBlockMessage, getWeatherDamageMessage, getWeatherLapseMessage } from "./data/weather"; import { TempBattleStat } from "./data/temp-battle-stat"; import { ArenaTagSide, ArenaTrapTag, MistTag, TrickRoomTag } from "./data/arena-tag"; -import { CheckTrappedAbAttr, IgnoreOpponentStatChangesAbAttr, IgnoreOpponentEvasionAbAttr, 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, BattleStatMultiplierAbAttr, applyBattleStatMultiplierAbAttrs, IncrementMovePriorityAbAttr, applyPostVictoryAbAttrs, PostVictoryAbAttr, BlockNonDirectDamageAbAttr as BlockNonDirectDamageAbAttr, applyPostKnockOutAbAttrs, PostKnockOutAbAttr, PostBiomeChangeAbAttr, applyPostFaintAbAttrs, PostFaintAbAttr, IncreasePpAbAttr, PostStatChangeAbAttr, applyPostStatChangeAbAttrs, AlwaysHitAbAttr, PreventBerryUseAbAttr, StatChangeCopyAbAttr, PokemonTypeChangeAbAttr, applyPreAttackAbAttrs, applyPostMoveUsedAbAttrs, PostMoveUsedAbAttr, MaxMultiHitAbAttr, HealFromBerryUseAbAttr, WonderSkinAbAttr, applyPreDefendAbAttrs } from "./data/ability"; +import { CheckTrappedAbAttr, IgnoreOpponentStatChangesAbAttr, IgnoreOpponentEvasionAbAttr, 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, BattleStatMultiplierAbAttr, applyBattleStatMultiplierAbAttrs, IncrementMovePriorityAbAttr, applyPostVictoryAbAttrs, PostVictoryAbAttr, BlockNonDirectDamageAbAttr as BlockNonDirectDamageAbAttr, applyPostKnockOutAbAttrs, PostKnockOutAbAttr, PostBiomeChangeAbAttr, applyPostFaintAbAttrs, PostFaintAbAttr, IncreasePpAbAttr, PostStatChangeAbAttr, applyPostStatChangeAbAttrs, AlwaysHitAbAttr, PreventBerryUseAbAttr, StatChangeCopyAbAttr, PokemonTypeChangeAbAttr, applyPreAttackAbAttrs, applyPostMoveUsedAbAttrs, PostMoveUsedAbAttr, MaxMultiHitAbAttr, HealFromBerryUseAbAttr, WonderSkinAbAttr, applyPreDefendAbAttrs, FieldPreventMovesAbAttr } from "./data/ability"; import { Unlockables, getUnlockableName } from "./system/unlockables"; import { getBiomeKey } from "./field/arena"; import { BattleType, BattlerIndex, TurnCommand } from "./battle"; @@ -2873,6 +2873,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(); + } + const applyAttrs: Promise[] = []; // Move animation only needs one target 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 +}); From 164b0d4cbbba43150ddfd29f8b4d3bf9200ffd0b Mon Sep 17 00:00:00 2001 From: Zach Day Date: Sun, 16 Jun 2024 02:21:35 -0400 Subject: [PATCH 02/15] String fix --- 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 759a18d9dc6..0e580a5dfbc 100755 --- a/src/data/ability.ts +++ b/src/data/ability.ts @@ -3313,7 +3313,7 @@ export class FieldPreventMovesAbAttr extends AbAttr { /** @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}`)); + return (getPokemonMessage(args[1] as Pokemon, ` cannot use ${(args[0] as Move).name}.`)); } } From 221821291d9a99026d4ab1c35ff414ef93fe67fe Mon Sep 17 00:00:00 2001 From: Zach Day Date: Sun, 16 Jun 2024 02:25:41 -0400 Subject: [PATCH 03/15] Remove EXPLOSIVE tag, Damp can decide which moves to suppress --- src/data/move.ts | 16 ---------------- 1 file changed, 16 deletions(-) diff --git a/src/data/move.ts b/src/data/move.ts index 35ad27ed6d4..8571ac69562 100644 --- a/src/data/move.ts +++ b/src/data/move.ts @@ -90,7 +90,6 @@ export enum MoveFlags { * Enables all hits of a multi-hit move to be accuracy checked individually */ CHECK_ALL_HITS = 1 << 17, - EXPLOSIVE_MOVE = 1 << 18 } type MoveConditionFunc = (user: Pokemon, target: Pokemon, move: Move) => boolean; @@ -504,17 +503,6 @@ 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 @@ -5823,7 +5811,6 @@ export function initMoves() { new AttackMove(Moves.SELF_DESTRUCT, Type.NORMAL, MoveCategory.PHYSICAL, 200, 100, 5, -1, 0, 1) .attr(SacrificialAttr) .makesContact(false) - .explosiveMove() .target(MoveTarget.ALL_NEAR_OTHERS), new AttackMove(Moves.EGG_BOMB, Type.NORMAL, MoveCategory.PHYSICAL, 100, 75, 10, -1, 0, 1) .makesContact(false) @@ -5915,7 +5902,6 @@ export function initMoves() { new AttackMove(Moves.EXPLOSION, Type.NORMAL, MoveCategory.PHYSICAL, 250, 100, 5, -1, 0, 1) .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), @@ -7524,7 +7510,6 @@ export function initMoves() { /* End Unused */ new AttackMove(Moves.MIND_BLOWN, Type.FIRE, MoveCategory.SPECIAL, 150, 100, 5, -1, 0, 7) .attr(HalfSacrificialAttr) - .explosiveMove() .target(MoveTarget.ALL_NEAR_OTHERS), new AttackMove(Moves.PLASMA_FISTS, Type.ELECTRIC, MoveCategory.PHYSICAL, 100, 100, 15, -1, 0, 7) .punchingMove() @@ -7795,7 +7780,6 @@ 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) - .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()), From 36f567b627976ed45d546175b9dce7bcb4eff55f Mon Sep 17 00:00:00 2001 From: Zach Day Date: Sun, 16 Jun 2024 02:32:49 -0400 Subject: [PATCH 04/15] Cleanup tests --- src/test/abilities/damp.test.ts | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/test/abilities/damp.test.ts b/src/test/abilities/damp.test.ts index 71c4c7b6315..1efde2bbaa1 100644 --- a/src/test/abilities/damp.test.ts +++ b/src/test/abilities/damp.test.ts @@ -94,7 +94,4 @@ describe("Abilities - Damp", () => { expect(game.phaseInterceptor.log).toContain("ShowAbilityPhase"); }, TIMEOUT); - - // TODO Test some of the other AbAttrs that use `args` - // BattlerTagImmunityAbAttr, StatusEffectImmunityAbAttr }); From 6f7cc71a9b17398d7296fd24e17a80bb4be074d1 Mon Sep 17 00:00:00 2001 From: Zach Day Date: Sun, 16 Jun 2024 17:30:52 -0400 Subject: [PATCH 05/15] Localize strings --- src/data/ability.ts | 3 ++- src/locales/en/ability-trigger.ts | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/data/ability.ts b/src/data/ability.ts index 0e580a5dfbc..981fb4020db 100755 --- a/src/data/ability.ts +++ b/src/data/ability.ts @@ -3313,7 +3313,8 @@ export class FieldPreventMovesAbAttr extends AbAttr { /** @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}.`)); + return i18next.t("abilityTriggers:movePrevented", + { moveUser: getPokemonNameWithAffix(args[1] as Pokemon), moveName: (args[0] as Move).name }); } } diff --git a/src/locales/en/ability-trigger.ts b/src/locales/en/ability-trigger.ts index a99053785ab..52f333f69a9 100644 --- a/src/locales/en/ability-trigger.ts +++ b/src/locales/en/ability-trigger.ts @@ -6,5 +6,6 @@ export const abilityTriggers: SimpleTranslationEntries = { "windPowerCharged": "Being hit by {{moveName}} charged {{pokemonName}} with power!", "perishBody": "{{pokemonName}}'s {{abilityName}}\nwill faint both pokemon in 3 turns!", "poisonHeal": "{{pokemonName}}'s {{abilityName}}\nrestored its HP a little!", - "iceFaceAvoidedDamage": "{{pokemonName}} avoided\ndamage with {{abilityName}}!" + "iceFaceAvoidedDamage": "{{pokemonName}} avoided\ndamage with {{abilityName}}!", + "movePrevented": "{{moveUser}} cannot use {{moveName}}!" } as const; From f70ddd21c9089c1eac7aac303cbc25865225a4f4 Mon Sep 17 00:00:00 2001 From: Zach Day Date: Sun, 16 Jun 2024 17:48:51 -0400 Subject: [PATCH 06/15] Remove redundant attr application --- src/field/pokemon.ts | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/src/field/pokemon.ts b/src/field/pokemon.ts index d0fd4958387..b06eab043ba 100644 --- a/src/field/pokemon.ts +++ b/src/field/pokemon.ts @@ -16,13 +16,13 @@ import { initMoveAnim, loadMoveAnimAssets } from "../data/battle-anims"; import { Status, StatusEffect, getRandomStatus } from "../data/status-effect"; import { pokemonEvolutions, pokemonPrevolutions, SpeciesFormEvolution, SpeciesEvolutionCondition, FusionSpeciesFormEvolution } from "../data/pokemon-evolutions"; import { reverseCompatibleTms, tmSpecies, tmPoolTiers } from "../data/tms"; -import { DamagePhase, FaintPhase, LearnMovePhase, ObtainStatusEffectPhase, StatChangePhase, SwitchSummonPhase, ToggleDoublePositionPhase } from "../phases"; +import { DamagePhase, FaintPhase, LearnMovePhase, ObtainStatusEffectPhase, StatChangePhase, SwitchSummonPhase, ToggleDoublePositionPhase } from "../phases"; import { BattleStat } from "../data/battle-stat"; import { BattlerTag, BattlerTagLapseType, EncoreTag, HelpingHandTag, HighestStatBoostTag, TypeBoostTag, getBattlerTag } from "../data/battler-tags"; import { WeatherType } from "../data/weather"; import { TempBattleStat } from "../data/temp-battle-stat"; 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 { 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 } from "../data/ability"; import PokemonData from "../system/pokemon-data"; import { BattlerIndex } from "../battle"; import { Mode } from "../ui/ui"; @@ -1728,11 +1728,6 @@ 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()) { const defendingSide = this.isPlayer() ? ArenaTagSide.PLAYER : ArenaTagSide.ENEMY; From 9a90742627d819d41e2c66a4199e9f8d36ebd944 Mon Sep 17 00:00:00 2001 From: Zach Day Date: Sun, 16 Jun 2024 17:49:21 -0400 Subject: [PATCH 07/15] Update move history entry to FAIL when prevented by Damp --- src/overrides.ts | 8 ++++---- src/phases.ts | 2 ++ 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/src/overrides.ts b/src/overrides.ts index 6ae3af64299..01a01c7c050 100644 --- a/src/overrides.ts +++ b/src/overrides.ts @@ -1,4 +1,3 @@ -import { WeatherType } from "./data/weather"; import { Variant } from "./data/variant"; import { TempBattleStat } from "./data/temp-battle-stat"; import { Nature } from "./data/nature"; @@ -16,6 +15,7 @@ import { Biome } from "#enums/biome"; import { Moves } from "#enums/moves"; import { Species } from "#enums/species"; import { TimeOfDay } from "#enums/time-of-day"; +import { WeatherType } from "./data/weather"; /** * Overrides for testing different in game situations @@ -75,7 +75,7 @@ export const STARTING_LEVEL_OVERRIDE: integer = 0; * @example SPECIES_OVERRIDE = Species.Bulbasaur; */ export const STARTER_SPECIES_OVERRIDE: Species | integer = 0; -export const ABILITY_OVERRIDE: Abilities = Abilities.NONE; +export const ABILITY_OVERRIDE: Abilities = Abilities.DAMP; export const PASSIVE_ABILITY_OVERRIDE: Abilities = Abilities.NONE; export const STATUS_OVERRIDE: StatusEffect = StatusEffect.NONE; export const GENDER_OVERRIDE: Gender = null; @@ -87,13 +87,13 @@ export const VARIANT_OVERRIDE: Variant = 0; * OPPONENT / ENEMY OVERRIDES */ -export const OPP_SPECIES_OVERRIDE: Species | integer = 0; +export const OPP_SPECIES_OVERRIDE: Species | integer = Species.RATTATA; export const OPP_LEVEL_OVERRIDE: number = 0; export const OPP_ABILITY_OVERRIDE: Abilities = Abilities.NONE; export const OPP_PASSIVE_ABILITY_OVERRIDE: Abilities = Abilities.NONE; export const OPP_STATUS_OVERRIDE: StatusEffect = StatusEffect.NONE; export const OPP_GENDER_OVERRIDE: Gender = null; -export const OPP_MOVESET_OVERRIDE: Array = []; +export const OPP_MOVESET_OVERRIDE: Array = [Moves.EXPLOSION, Moves.SELF_DESTRUCT, Moves.MISTY_EXPLOSION, Moves.MIND_BLOWN]; export const OPP_SHINY_OVERRIDE: boolean = false; export const OPP_VARIANT_OVERRIDE: Variant = 0; export const OPP_IVS_OVERRIDE: integer | integer[] = []; diff --git a/src/phases.ts b/src/phases.ts index ae0ad4df35b..a5019ae1f18 100644 --- a/src/phases.ts +++ b/src/phases.ts @@ -2879,6 +2879,8 @@ export class MoveEffectPhase extends PokemonPhase { applyAbAttrs(FieldPreventMovesAbAttr, other, prevented, move, user as Pokemon); } if (prevented.value) { + // Just bail out of the move if it got prevented. No need to manually show message; the Attr handles that. + moveHistoryEntry.result = MoveResult.FAIL; return this.end(); } From 76e484f3ce4e0230ac6d03b00872a9af80765561 Mon Sep 17 00:00:00 2001 From: Zach Day Date: Sun, 16 Jun 2024 17:50:19 -0400 Subject: [PATCH 08/15] Revert overrides change --- src/overrides.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/overrides.ts b/src/overrides.ts index 01a01c7c050..6ae3af64299 100644 --- a/src/overrides.ts +++ b/src/overrides.ts @@ -1,3 +1,4 @@ +import { WeatherType } from "./data/weather"; import { Variant } from "./data/variant"; import { TempBattleStat } from "./data/temp-battle-stat"; import { Nature } from "./data/nature"; @@ -15,7 +16,6 @@ import { Biome } from "#enums/biome"; import { Moves } from "#enums/moves"; import { Species } from "#enums/species"; import { TimeOfDay } from "#enums/time-of-day"; -import { WeatherType } from "./data/weather"; /** * Overrides for testing different in game situations @@ -75,7 +75,7 @@ export const STARTING_LEVEL_OVERRIDE: integer = 0; * @example SPECIES_OVERRIDE = Species.Bulbasaur; */ export const STARTER_SPECIES_OVERRIDE: Species | integer = 0; -export const ABILITY_OVERRIDE: Abilities = Abilities.DAMP; +export const ABILITY_OVERRIDE: Abilities = Abilities.NONE; export const PASSIVE_ABILITY_OVERRIDE: Abilities = Abilities.NONE; export const STATUS_OVERRIDE: StatusEffect = StatusEffect.NONE; export const GENDER_OVERRIDE: Gender = null; @@ -87,13 +87,13 @@ export const VARIANT_OVERRIDE: Variant = 0; * OPPONENT / ENEMY OVERRIDES */ -export const OPP_SPECIES_OVERRIDE: Species | integer = Species.RATTATA; +export const OPP_SPECIES_OVERRIDE: Species | integer = 0; export const OPP_LEVEL_OVERRIDE: number = 0; export const OPP_ABILITY_OVERRIDE: Abilities = Abilities.NONE; export const OPP_PASSIVE_ABILITY_OVERRIDE: Abilities = Abilities.NONE; export const OPP_STATUS_OVERRIDE: StatusEffect = StatusEffect.NONE; export const OPP_GENDER_OVERRIDE: Gender = null; -export const OPP_MOVESET_OVERRIDE: Array = [Moves.EXPLOSION, Moves.SELF_DESTRUCT, Moves.MISTY_EXPLOSION, Moves.MIND_BLOWN]; +export const OPP_MOVESET_OVERRIDE: Array = []; export const OPP_SHINY_OVERRIDE: boolean = false; export const OPP_VARIANT_OVERRIDE: Variant = 0; export const OPP_IVS_OVERRIDE: integer | integer[] = []; From ebd11eecd7d3ad1ee210c77834e409fe957d3bbe Mon Sep 17 00:00:00 2001 From: Zach Day Date: Mon, 17 Jun 2024 16:13:27 -0400 Subject: [PATCH 09/15] Don't splat args to getTriggerMessage --- src/data/ability.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/data/ability.ts b/src/data/ability.ts index 981fb4020db..508e68a3af3 100755 --- a/src/data/ability.ts +++ b/src/data/ability.ts @@ -3312,7 +3312,7 @@ export class FieldPreventMovesAbAttr extends AbAttr { } /** @param args See {@linkcode FieldPreventMovesAbAttr}. */ - getTriggerMessage(pokemon: Pokemon, abilityName: string, ...args: any[]): string { + getTriggerMessage(pokemon: Pokemon, abilityName: string, args: any[]): string { return i18next.t("abilityTriggers:movePrevented", { moveUser: getPokemonNameWithAffix(args[1] as Pokemon), moveName: (args[0] as Move).name }); } @@ -3685,7 +3685,7 @@ function applyAbAttrsInternal(attrType: { new(...args: any } } if (!quiet) { - const message = attr.getTriggerMessage(pokemon, (!passive ? pokemon.getAbility() : pokemon.getPassiveAbility()).name, ...args); + const message = attr.getTriggerMessage(pokemon, (!passive ? pokemon.getAbility() : pokemon.getPassiveAbility()).name, args); if (message) { if (isAsync) { pokemon.scene.ui.showText(message, null, () => pokemon.scene.ui.showText(null, 0), null, true); From 7406415e7d08b118cc265a2d53641422bc1429cd Mon Sep 17 00:00:00 2001 From: Zach Day Date: Mon, 17 Jun 2024 16:30:36 -0400 Subject: [PATCH 10/15] Use array as parameter for FieldPreventMovesAbAttr --- src/data/ability.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/data/ability.ts b/src/data/ability.ts index 508e68a3af3..fb84067ac9a 100755 --- a/src/data/ability.ts +++ b/src/data/ability.ts @@ -3294,16 +3294,16 @@ export class BlockRedirectAbAttr extends AbAttr { } * @param args [1] {@linkcode Pokemon} The user of the attack */ export class FieldPreventMovesAbAttr extends AbAttr { - public moveCondition: (Moves) => boolean; + public preventedMoves: Moves[]; - constructor(moveCondition: (Moves) => boolean) { + constructor(moves: Moves[]) { super(); - this.moveCondition = moveCondition; + this.preventedMoves = moves; } /** @param args See {@linkcode FieldPreventMovesAbAttr}. */ apply(pokemon: Pokemon, passive: boolean, cancelled: Utils.BooleanHolder, args: any[]): boolean { - if (this.moveCondition((args[0] as Move).id)) { + if (this.preventedMoves.includes((args[0] as Move).id)) { cancelled.value = true; return true; } @@ -3890,7 +3890,7 @@ export function initAbilities() { .attr(BlockOneHitKOAbAttr) .ignorable(), new Ability(Abilities.DAMP, 3) - .attr(FieldPreventMovesAbAttr, (move) => [Moves.EXPLOSION, Moves.SELF_DESTRUCT, Moves.MIND_BLOWN, Moves.MISTY_EXPLOSION].includes(move)) + .attr(FieldPreventMovesAbAttr, [Moves.EXPLOSION, Moves.SELF_DESTRUCT, Moves.MIND_BLOWN, Moves.MISTY_EXPLOSION]) .attr(PreventPostFaintContactDamageAbAttr) .ignorable(), new Ability(Abilities.LIMBER, 3) From 3834e1b0d3528c28f5b5d1c523ba20991af0b2d7 Mon Sep 17 00:00:00 2001 From: Zach Day Date: Mon, 17 Jun 2024 17:16:30 -0400 Subject: [PATCH 11/15] Update test for #1476 fix --- src/test/abilities/damp.test.ts | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/test/abilities/damp.test.ts b/src/test/abilities/damp.test.ts index 1efde2bbaa1..159447c1e4e 100644 --- a/src/test/abilities/damp.test.ts +++ b/src/test/abilities/damp.test.ts @@ -2,7 +2,7 @@ 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 { TurnEndPhase, TurnStartPhase } from "#app/phases"; import GameManager from "#app/test/utils/gameManager"; import { getMovePosition } from "#app/test/utils/gameManagerUtils"; import Phaser from "phaser"; @@ -77,21 +77,21 @@ describe("Abilities - Damp", () => { // Ensures fix of #1476. it("does not show ability popup during AI calculations", async() => { - const moveToUse = Moves.EXPLOSION; - const enemyAbility = Abilities.DAMP; + const moveToUse = Moves.SPLASH; + const playerAbility = Abilities.DAMP; - vi.spyOn(overrides, "ABILITY_OVERRIDE", "get").mockReturnValue(Abilities.NONE); + 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_MOVESET_OVERRIDE", "get").mockReturnValue([Moves.EXPLOSION, Moves.SELF_DESTRUCT, Moves.MIND_BLOWN, Moves.MISTY_EXPLOSION]); vi.spyOn(overrides, "OPP_SPECIES_OVERRIDE", "get").mockReturnValue(Species.BIDOOF); - vi.spyOn(overrides, "OPP_ABILITY_OVERRIDE", "get").mockReturnValue(enemyAbility); + vi.spyOn(overrides, "OPP_ABILITY_OVERRIDE", "get").mockReturnValue(Abilities.NONE); await game.startBattle(); game.doAttack(getMovePosition(game.scene, 0, moveToUse)); - await game.phaseInterceptor.to(TurnEndPhase); + await game.phaseInterceptor.to(TurnStartPhase); - expect(game.phaseInterceptor.log).toContain("ShowAbilityPhase"); + expect(game.phaseInterceptor.log).not.toContain("ShowAbilityPhase"); }, TIMEOUT); }); From 3ec7a1930e545513abbf4134a97b1ca4a49826aa Mon Sep 17 00:00:00 2001 From: Zach Day Date: Wed, 19 Jun 2024 16:28:58 -0400 Subject: [PATCH 12/15] Fix explosive moves used by a mon with Damp not getting suppressed --- src/phases.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/phases.ts b/src/phases.ts index e311ab956ac..1ae15dc6caf 100644 --- a/src/phases.ts +++ b/src/phases.ts @@ -2904,9 +2904,7 @@ export class MoveEffectPhase extends PokemonPhase { // 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); - } + this.scene.getField(true).forEach(p => applyAbAttrs(FieldPreventMovesAbAttr, p, prevented, move, user)); if (prevented.value) { // Just bail out of the move if it got prevented. No need to manually show message; the Attr handles that. moveHistoryEntry.result = MoveResult.FAIL; From bdfa74a944891c2e64fa6fcc6a1615743763a69a Mon Sep 17 00:00:00 2001 From: Zach Day Date: Wed, 19 Jun 2024 16:36:13 -0400 Subject: [PATCH 13/15] Add unit tests for mon with Damp using explosive move --- src/test/abilities/damp.test.ts | 23 ++++++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/src/test/abilities/damp.test.ts b/src/test/abilities/damp.test.ts index 159447c1e4e..075f7d9b5d8 100644 --- a/src/test/abilities/damp.test.ts +++ b/src/test/abilities/damp.test.ts @@ -30,7 +30,7 @@ describe("Abilities - Damp", () => { vi.spyOn(overrides, "NEVER_CRIT_OVERRIDE", "get").mockReturnValue(true); }); - it("prevents self-destruction effect on explosive attacks", async() => { + it("prevents explosive attacks used by others", async() => { const moveToUse = Moves.EXPLOSION; const enemyAbility = Abilities.DAMP; @@ -50,6 +50,26 @@ describe("Abilities - Damp", () => { expect(game.phaseInterceptor.log).not.toContain("FaintPhase"); }, TIMEOUT); + it("prevents explosive attacks used by the battler with Damp", async() => { + const moveToUse = Moves.EXPLOSION; + const playerAbility = Abilities.DAMP; + + 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(Abilities.NONE); + + 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; @@ -75,6 +95,7 @@ describe("Abilities - Damp", () => { 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.SPLASH; From 3845787c44d64a65bcdafee4d3aa839380e8ae87 Mon Sep 17 00:00:00 2001 From: Zach Day Date: Sat, 29 Jun 2024 17:39:24 -0400 Subject: [PATCH 14/15] Clean up comments --- src/phases.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/phases.ts b/src/phases.ts index 2ccfee82a73..3fbd73d768e 100644 --- a/src/phases.ts +++ b/src/phases.ts @@ -2905,11 +2905,9 @@ 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); this.scene.getField(true).forEach(p => applyAbAttrs(FieldPreventMovesAbAttr, p, prevented, move, user)); if (prevented.value) { - // Just bail out of the move if it got prevented. No need to manually show message; the Attr handles that. moveHistoryEntry.result = MoveResult.FAIL; return this.end(); } From 9d59d1236c9a3f3e5713e4a7de05cdf08c23de3d Mon Sep 17 00:00:00 2001 From: Zach Day Date: Sat, 29 Jun 2024 17:42:29 -0400 Subject: [PATCH 15/15] Add translation stubs --- src/locales/de/ability-trigger.ts | 1 + src/locales/es/ability-trigger.ts | 1 + src/locales/fr/ability-trigger.ts | 1 + src/locales/it/ability-trigger.ts | 1 + src/locales/ko/ability-trigger.ts | 1 + src/locales/pt_BR/ability-trigger.ts | 1 + src/locales/zh_CN/ability-trigger.ts | 1 + src/locales/zh_TW/ability-trigger.ts | 1 + 8 files changed, 8 insertions(+) diff --git a/src/locales/de/ability-trigger.ts b/src/locales/de/ability-trigger.ts index d6c48555d51..5c7dbf694d3 100644 --- a/src/locales/de/ability-trigger.ts +++ b/src/locales/de/ability-trigger.ts @@ -5,6 +5,7 @@ export const abilityTriggers: SimpleTranslationEntries = { "badDreams": "{{pokemonName}} ist in einem Alptraum gefangen!", "costar": "{{pokemonName}} kopiert die Statusveränderungen von {{allyName}}!", "iceFaceAvoidedDamage": "{{pokemonName}} wehrt Schaden mit {{abilityName}} ab!", + "movePrevented": "{{moveUser}} cannot use {{moveName}}!", "trace": "{{pokemonName}} kopiert {{abilityName}} von {{targetName}}!", "windPowerCharged": "Der Treffer durch {{moveName}} läd die Stärke von {{pokemonName}} auf!", "quickDraw": "{{pokemonName}} can act faster than normal, thanks to its Quick Draw!", diff --git a/src/locales/es/ability-trigger.ts b/src/locales/es/ability-trigger.ts index 5c09c3832c0..307769cc70f 100644 --- a/src/locales/es/ability-trigger.ts +++ b/src/locales/es/ability-trigger.ts @@ -5,6 +5,7 @@ export const abilityTriggers: SimpleTranslationEntries = { "badDreams": "¡{{pokemonName}} está atormentado!", "costar": "{{pokemonName}} copied {{allyName}}'s stat changes!", "iceFaceAvoidedDamage": "¡{{pokemonNameWithAffix}} evitó\ndaño con {{abilityName}}!", + "movePrevented": "{{moveUser}} cannot use {{moveName}}!", "trace": "{{pokemonName}} copied {{targetName}}'s\n{{abilityName}}!", "windPowerCharged": "¡{{pokemonName}} se ha cargado de electricidad gracias a {{moveName}}!", "quickDraw": "{{pokemonName}} can act faster than normal, thanks to its Quick Draw!", diff --git a/src/locales/fr/ability-trigger.ts b/src/locales/fr/ability-trigger.ts index f99ff15c26f..31c4ffe377b 100644 --- a/src/locales/fr/ability-trigger.ts +++ b/src/locales/fr/ability-trigger.ts @@ -5,6 +5,7 @@ export const abilityTriggers: SimpleTranslationEntries = { "badDreams": "{{pokemonName}} a le sommeil agité !", "costar": "{{pokemonName}} copie les changements de stats\nde {{allyName}} !", "iceFaceAvoidedDamage": "{{pokemonName}} évite les dégâts\navec {{abilityName}} !", + "movePrevented": "{{moveUser}} cannot use {{moveName}}!", "perishBody": "{{abilityName}} de {{pokemonName}}\nmettra les deux Pokémon K.O. dans trois tours !", "poisonHeal": "{{abilityName}} de {{pokemonName}}\nrestaure un peu ses PV !", "trace": "{{pokemonName}} copie le talent {{abilityName}}\nde {{targetName}} !", diff --git a/src/locales/it/ability-trigger.ts b/src/locales/it/ability-trigger.ts index 1f6fcfb1258..f2b4c7a5b2e 100644 --- a/src/locales/it/ability-trigger.ts +++ b/src/locales/it/ability-trigger.ts @@ -5,6 +5,7 @@ export const abilityTriggers: SimpleTranslationEntries = { "badDreams": "{{pokemonName}} è tormentato dagli incubi!", "costar": "{{pokemonName}} copied {{allyName}}'s stat changes!", "iceFaceAvoidedDamage": "{{pokemonName}} ha evitato\ni danni grazie a {{abilityName}}!", + "movePrevented": "{{moveUser}} cannot use {{moveName}}!", "trace": "{{pokemonName}} copied {{targetName}}'s\n{{abilityName}}!", "windPowerCharged": "Venire colpito da {{moveName}} ha caricato {{pokemonName}}!", "quickDraw":"{{pokemonName}} can act faster than normal, thanks to its Quick Draw!", diff --git a/src/locales/ko/ability-trigger.ts b/src/locales/ko/ability-trigger.ts index 58ba7bf9aa6..6ef810b9143 100644 --- a/src/locales/ko/ability-trigger.ts +++ b/src/locales/ko/ability-trigger.ts @@ -5,6 +5,7 @@ export const abilityTriggers: SimpleTranslationEntries = { "badDreams": "{{pokemonName}}[[는]]\n나이트메어 때문에 시달리고 있다!", "costar": "{{pokemonName}} copied {{allyName}}'s stat changes!", "iceFaceAvoidedDamage": "{{pokemonName}}[[는]] {{abilityName}} 때문에\n데미지를 받지 않는다!", + "movePrevented": "{{moveUser}} cannot use {{moveName}}!", "perishBody": "{{pokemonName}}의 {{abilityName}} 때문에\n양쪽 포켓몬 모두는 3턴 후에 쓰러져 버린다!", "poisonHeal": "{{pokemonName}}[[는]] {{abilityName}}[[로]]인해\n조금 회복했다.", "trace": "{{pokemonName}} copied {{targetName}}'s\n{{abilityName}}!", diff --git a/src/locales/pt_BR/ability-trigger.ts b/src/locales/pt_BR/ability-trigger.ts index 4e3d6d11487..19eb2b0b093 100644 --- a/src/locales/pt_BR/ability-trigger.ts +++ b/src/locales/pt_BR/ability-trigger.ts @@ -5,6 +5,7 @@ export const abilityTriggers: SimpleTranslationEntries = { "badDreams": "{{pokemonName}} está tendo pesadelos!", "costar": "{{pokemonName}} copiou as mudanças\nde atributo de {{allyName}}!", "iceFaceAvoidedDamage": "{{pokemonName}} evitou\ndanos com sua {{abilityName}}!", + "movePrevented": "{{moveUser}} cannot use {{moveName}}!", "perishBody": "{{abilityName}} de {{pokemonName}}\nirá desmaiar ambos os Pokémon em 3 turnos!", "poisonHeal": "{{abilityName}} de {{pokemonName}}\nrestaurou seus PS um pouco!", "trace": "{{pokemonName}} copiou {{abilityName}}\nde {{targetName}}!", diff --git a/src/locales/zh_CN/ability-trigger.ts b/src/locales/zh_CN/ability-trigger.ts index fef4c718d3a..91bd5e17add 100644 --- a/src/locales/zh_CN/ability-trigger.ts +++ b/src/locales/zh_CN/ability-trigger.ts @@ -5,6 +5,7 @@ export const abilityTriggers: SimpleTranslationEntries = { "badDreams": "{{pokemonName}} 被折磨着!", "costar": "{{pokemonName}} copied {{allyName}}'s stat changes!", "iceFaceAvoidedDamage": "{{pokemonName}} 因为 {{abilityName}}\n避免了伤害!", + "movePrevented": "{{moveUser}} cannot use {{moveName}}!", "trace": "{{pokemonName}} copied {{targetName}}'s\n{{abilityName}}!", "windPowerCharged": "受 {{moveName}} 的影响, {{pokemonName}} 提升了能力!", "quickDraw":"因为速击效果发动,\n{{pokemonName}}比平常出招更快了!", diff --git a/src/locales/zh_TW/ability-trigger.ts b/src/locales/zh_TW/ability-trigger.ts index c436e5021f7..604ad70ebc9 100644 --- a/src/locales/zh_TW/ability-trigger.ts +++ b/src/locales/zh_TW/ability-trigger.ts @@ -5,6 +5,7 @@ export const abilityTriggers: SimpleTranslationEntries = { "badDreams": "{{pokemonName}} 被折磨着!", "costar": "{{pokemonName}} 複製了 {{allyName}} 的\n能力變化!", "iceFaceAvoidedDamage": "{{pokemonName}} 因爲 {{abilityName}}\n避免了傷害!", + "movePrevented": "{{moveUser}} cannot use {{moveName}}!", "trace": "{{pokemonName}} 複製了 {{targetName}} 的\n{{abilityName}}!", "windPowerCharged": "受 {{moveName}} 的影響, {{pokemonName}} 提升了能力!", "quickDraw":"{{pokemonName}} can act faster than normal, thanks to its Quick Draw!",