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
This commit is contained in:
Zach Day 2024-06-16 01:28:13 -04:00
parent 6d515d58b5
commit 914b41aaf7
6 changed files with 262 additions and 28 deletions

View File

@ -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<boolean> {
cancelled.value = true;
return true;
}
}
/** /**
* Multiplies a BattleStat if the checked Pokemon lacks this ability. * 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. * 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 { applyPostFaint(pokemon: Pokemon, passive: boolean, attacker: Pokemon, move: Move, hitResult: HitResult, args: any[]): boolean {
if (move.checkFlag(MoveFlags.MAKES_CONTACT, attacker, pokemon)) { if (move.checkFlag(MoveFlags.MAKES_CONTACT, attacker, pokemon)) {
const cancelled = new Utils.BooleanHolder(false); 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)) { if (cancelled.value || attacker.hasAbilityWithAttr(BlockNonDirectDamageAbAttr)) {
return false; return false;
} }
attacker.damageAndUpdate(Math.ceil(attacker.getMaxHp() * (1 / this.damageRatio)), HitResult.OTHER); attacker.damageAndUpdate(Math.ceil(attacker.getMaxHp() * (1 / this.damageRatio)), HitResult.OTHER);
attacker.turnData.damageTaken += Math.ceil(attacker.getMaxHp() * (1 / this.damageRatio)); attacker.turnData.damageTaken += Math.ceil(attacker.getMaxHp() * (1 / this.damageRatio));
return true; 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. * 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 { } 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 { export class ReduceStatusEffectDurationAbAttr extends AbAttr {
private statusEffect: StatusEffect; private statusEffect: StatusEffect;
@ -4137,7 +4174,7 @@ async function applyAbAttrsInternal<TAttr extends AbAttr>(
} }
if (!quiet) { if (!quiet) {
const message = attr.getTriggerMessage(pokemon, ability.name, args); const message = attr.getTriggerMessage(pokemon, (!passive ? pokemon.getAbility() : pokemon.getPassiveAbility()).name, args);
if (message) { if (message) {
pokemon.scene.queueMessage(message); pokemon.scene.queueMessage(message);
} }
@ -4320,7 +4357,8 @@ export function initAbilities() {
.attr(BlockOneHitKOAbAttr) .attr(BlockOneHitKOAbAttr)
.ignorable(), .ignorable(),
new Ability(Abilities.DAMP, 3) 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(), .ignorable(),
new Ability(Abilities.LIMBER, 3) new Ability(Abilities.LIMBER, 3)
.attr(StatusEffectImmunityAbAttr, StatusEffect.PARALYSIS) .attr(StatusEffectImmunityAbAttr, StatusEffect.PARALYSIS)

View File

@ -10,7 +10,7 @@ import { Constructor } from "#app/utils";
import * as Utils from "../utils"; import * as Utils from "../utils";
import { WeatherType } from "./weather"; import { WeatherType } from "./weather";
import { ArenaTagSide, ArenaTrapTag, WeakenMoveTypeTag } from "./arena-tag"; 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 { allAbilities } from "./ability";
import { PokemonHeldItemModifier, BerryModifier, PreserveBerryModifier, PokemonMoveAccuracyBoosterModifier, AttackTypeBoosterModifier, PokemonMultiHitModifier } from "../modifier/modifier"; import { PokemonHeldItemModifier, BerryModifier, PreserveBerryModifier, PokemonMoveAccuracyBoosterModifier, AttackTypeBoosterModifier, PokemonMultiHitModifier } from "../modifier/modifier";
import { BattlerIndex, BattleType } from "../battle"; 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 * Indicates a move is able to be redirected to allies in a double battle if the attacker faints
*/ */
REDIRECT_COUNTER = 1 << 18, REDIRECT_COUNTER = 1 << 18,
EXPLOSIVE_MOVE = 1 << 19
} }
type MoveConditionFunc = (user: Pokemon, target: Pokemon, move: Move) => boolean; type MoveConditionFunc = (user: Pokemon, target: Pokemon, move: Move) => boolean;
@ -520,6 +521,17 @@ export default class Move implements Localizable {
return this; 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 * Sets the {@linkcode MoveFlags.TRIAGE_MOVE} flag for the calling Move
* @param triageMove The value (boolean) to set the flag to * @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 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 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); 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) new AttackMove(Moves.SELF_DESTRUCT, Type.NORMAL, MoveCategory.PHYSICAL, 200, 100, 5, -1, 0, 1)
.attr(SacrificialAttr) .attr(SacrificialAttr)
.makesContact(false) .makesContact(false)
.condition(failIfDampCondition) .explosiveMove()
.target(MoveTarget.ALL_NEAR_OTHERS), .target(MoveTarget.ALL_NEAR_OTHERS),
new AttackMove(Moves.EGG_BOMB, Type.NORMAL, MoveCategory.PHYSICAL, 100, 75, 10, -1, 0, 1) new AttackMove(Moves.EGG_BOMB, Type.NORMAL, MoveCategory.PHYSICAL, 100, 75, 10, -1, 0, 1)
.makesContact(false) .makesContact(false)
@ -6560,9 +6562,9 @@ export function initMoves() {
new AttackMove(Moves.CRABHAMMER, Type.WATER, MoveCategory.PHYSICAL, 100, 90, 10, -1, 0, 1) new AttackMove(Moves.CRABHAMMER, Type.WATER, MoveCategory.PHYSICAL, 100, 90, 10, -1, 0, 1)
.attr(HighCritAttr), .attr(HighCritAttr),
new AttackMove(Moves.EXPLOSION, Type.NORMAL, MoveCategory.PHYSICAL, 250, 100, 5, -1, 0, 1) new AttackMove(Moves.EXPLOSION, Type.NORMAL, MoveCategory.PHYSICAL, 250, 100, 5, -1, 0, 1)
.condition(failIfDampCondition)
.attr(SacrificialAttr) .attr(SacrificialAttr)
.makesContact(false) .makesContact(false)
.explosiveMove()
.target(MoveTarget.ALL_NEAR_OTHERS), .target(MoveTarget.ALL_NEAR_OTHERS),
new AttackMove(Moves.FURY_SWIPES, Type.NORMAL, MoveCategory.PHYSICAL, 18, 80, 15, -1, 0, 1) new AttackMove(Moves.FURY_SWIPES, Type.NORMAL, MoveCategory.PHYSICAL, 18, 80, 15, -1, 0, 1)
.attr(MultiHitAttr), .attr(MultiHitAttr),
@ -8184,8 +8186,8 @@ export function initMoves() {
.ignoresVirtual(), .ignoresVirtual(),
/* End Unused */ /* End Unused */
new AttackMove(Moves.MIND_BLOWN, Type.FIRE, MoveCategory.SPECIAL, 150, 100, 5, -1, 0, 7) new AttackMove(Moves.MIND_BLOWN, Type.FIRE, MoveCategory.SPECIAL, 150, 100, 5, -1, 0, 7)
.condition(failIfDampCondition)
.attr(HalfSacrificialAttr) .attr(HalfSacrificialAttr)
.explosiveMove()
.target(MoveTarget.ALL_NEAR_OTHERS), .target(MoveTarget.ALL_NEAR_OTHERS),
new AttackMove(Moves.PLASMA_FISTS, Type.ELECTRIC, MoveCategory.PHYSICAL, 100, 100, 15, -1, 0, 7) new AttackMove(Moves.PLASMA_FISTS, Type.ELECTRIC, MoveCategory.PHYSICAL, 100, 100, 15, -1, 0, 7)
.punchingMove() .punchingMove()
@ -8466,7 +8468,7 @@ export function initMoves() {
.attr(SacrificialAttr) .attr(SacrificialAttr)
.target(MoveTarget.ALL_NEAR_OTHERS) .target(MoveTarget.ALL_NEAR_OTHERS)
.attr(MovePowerMultiplierAttr, (user, target, move) => user.scene.arena.getTerrainType() === TerrainType.MISTY && user.isGrounded() ? 1.5 : 1) .attr(MovePowerMultiplierAttr, (user, target, move) => user.scene.arena.getTerrainType() === TerrainType.MISTY && user.isGrounded() ? 1.5 : 1)
.condition(failIfDampCondition) .explosiveMove()
.makesContact(false), .makesContact(false),
new AttackMove(Moves.GRASSY_GLIDE, Type.GRASS, MoveCategory.PHYSICAL, 55, 100, 20, -1, 0, 8) 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()), .attr(IncrementMovePriorityAttr,(user,target,move) =>user.scene.arena.getTerrainType()===TerrainType.GRASSY&&user.isGrounded()),

View File

@ -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 { BattlerTag, BattlerTagLapseType, EncoreTag, GroundedTag, HighestStatBoostTag, TypeImmuneTag, getBattlerTag, SemiInvulnerableTag, TypeBoostTag, ExposedTag } from "../data/battler-tags";
import { WeatherType } from "../data/weather"; import { WeatherType } from "../data/weather";
import { TempBattleStat } from "../data/temp-battle-stat"; import { TempBattleStat } from "../data/temp-battle-stat";
import { ArenaTagSide, NoCritTag, WeakenMoveScreenTag } from "../data/arena-tag"; import { ArenaTagSide, WeakenMoveScreenTag, WeakenMoveTypeTag } 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 { 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 PokemonData from "../system/pokemon-data";
import { BattlerIndex } from "../battle"; import { BattlerIndex } from "../battle";
import { Mode } from "../ui/ui"; import { Mode } from "../ui/ui";
@ -1942,6 +1942,11 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
typeMultiplier.value = 0; 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 // Apply arena tags for conditional protection
if (!move.checkFlag(MoveFlags.IGNORE_PROTECT, source, this) && !move.isAllyTarget()) { if (!move.checkFlag(MoveFlags.IGNORE_PROTECT, source, this) && !move.isAllyTarget()) {
this.scene.arena.applyTagsForSide(ArenaTagType.QUICK_GUARD, defendingSide, cancelled, this, move.priority); this.scene.arena.applyTagsForSide(ArenaTagType.QUICK_GUARD, defendingSide, cancelled, this, move.priority);

View File

@ -25,7 +25,7 @@ import { Starter } from "./ui/starter-select-ui-handler";
import { Gender } from "./data/gender"; import { Gender } from "./data/gender";
import { Weather, WeatherType, getRandomWeatherType, getTerrainBlockMessage, getWeatherDamageMessage, getWeatherLapseMessage } from "./data/weather"; import { Weather, WeatherType, getRandomWeatherType, getTerrainBlockMessage, getWeatherDamageMessage, getWeatherLapseMessage } from "./data/weather";
import { ArenaTagSide, ArenaTrapTag, MistTag, TrickRoomTag } from "./data/arena-tag"; 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 { Unlockables, getUnlockableName } from "./system/unlockables";
import { getBiomeKey } from "./field/arena"; import { getBiomeKey } from "./field/arena";
import { BattleType, BattlerIndex, TurnCommand } from "./battle"; import { BattleType, BattlerIndex, TurnCommand } from "./battle";
@ -3048,6 +3048,15 @@ export class MoveEffectPhase extends PokemonPhase {
return this.end(); 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. */ /** All move effect attributes are chained together in this array to be applied asynchronously. */
const applyAttrs: Promise<void>[] = []; const applyAttrs: Promise<void>[] = [];

View File

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

View File

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