diff --git a/src/data/abilities/ability.ts b/src/data/abilities/ability.ts index 37271933701..0087e502ce3 100644 --- a/src/data/abilities/ability.ts +++ b/src/data/abilities/ability.ts @@ -465,7 +465,9 @@ type PreDefendAbAttrCondition = (pokemon: Pokemon, attacker: Pokemon, move: Move * Often extended by other interfaces to add more parameters. * Used, e.g. by {@linkcode PreDefendAbAttr} and {@linkcode PostAttackAbAttr} */ -export interface AugmentMoveInteractionAbAttrParams extends AbAttrParamsWithCancel { +// TODO: Consider making this not require `cancelled`, as many abilities do not do anything with the parameter. +// Leaving it in bloats callsites. +export interface AugmentMoveInteractionAbAttrParams extends AbAttrBaseParams { /** The move used by (or against, for defend attributes) */ move: Move; /** The pokemon on the other side of the interaction*/ @@ -593,6 +595,8 @@ export class ReceivedTypeDamageMultiplierAbAttr extends ReceivedMoveDamageMultip export interface TypeMultiplierAbAttrParams extends AugmentMoveInteractionAbAttrParams { /** Holds the type multiplier of an attack. In the case of an immunity, this value will be set to 0. */ typeMultiplier: NumberHolder; + /** Its particular meaning depends on the ability attribute, though usually means that the "no effect" message should not be played */ + cancelled: BooleanHolder; } /** @@ -778,8 +782,13 @@ export class FullHpResistTypeAbAttr extends PreDefendAbAttr { } } +export interface FieldPriorityMoveImmunityAbAttrParams extends AugmentMoveInteractionAbAttrParams { + /** Holds whether the pokemon is immune to the move being used */ + cancelled: BooleanHolder; +} + export class FieldPriorityMoveImmunityAbAttr extends PreDefendAbAttr { - override canApply({ move, opponent: attacker }: AugmentMoveInteractionAbAttrParams): boolean { + override canApply({ move, opponent: attacker }: FieldPriorityMoveImmunityAbAttrParams): boolean { return ( !(move.moveTarget === MoveTarget.USER || move.moveTarget === MoveTarget.NEAR_ALLY) && move.getPriority(attacker) > 0 && @@ -787,11 +796,17 @@ export class FieldPriorityMoveImmunityAbAttr extends PreDefendAbAttr { ); } - override apply({ cancelled }: AugmentMoveInteractionAbAttrParams): void { + override apply({ cancelled }: FieldPriorityMoveImmunityAbAttrParams): void { cancelled.value = true; } } +export interface MoveImmunityAbAttrParams extends AugmentMoveInteractionAbAttrParams { + /** Holds whether the standard "no effect" message (due to a type-based immunity) should be suppressed */ + cancelled: BooleanHolder; +} +// TODO: Consider examining whether the this move immunity ability attribute +// can be merged with the MoveTypeMultiplierAbAttr in some way. export class MoveImmunityAbAttr extends PreDefendAbAttr { private immuneCondition: PreDefendAbAttrCondition; @@ -801,15 +816,17 @@ export class MoveImmunityAbAttr extends PreDefendAbAttr { this.immuneCondition = immuneCondition; } - override canApply({ pokemon, opponent: attacker, move }: AugmentMoveInteractionAbAttrParams): boolean { + override canApply({ pokemon, opponent: attacker, move }: MoveImmunityAbAttrParams): boolean { + // TODO: Investigate whether this method should be checking against `cancelled`, specifically + // if not checking this results in multiple flyouts showing when multiple abilities block the move. return this.immuneCondition(pokemon, attacker, move); } - override apply({ cancelled }: AugmentMoveInteractionAbAttrParams): void { + override apply({ cancelled }: MoveImmunityAbAttrParams): void { cancelled.value = true; } - override getTriggerMessage({ pokemon }: AugmentMoveInteractionAbAttrParams, _abilityName: string): string { + override getTriggerMessage({ pokemon }: MoveImmunityAbAttrParams, _abilityName: string): string { return i18next.t("abilityTriggers:moveImmunity", { pokemonNameWithAffix: getPokemonNameWithAffix(pokemon) }); } } @@ -847,13 +864,13 @@ export class MoveImmunityStatStageChangeAbAttr extends MoveImmunityAbAttr { this.stages = stages; } - override canApply(params: AugmentMoveInteractionAbAttrParams): boolean { + override canApply(params: MoveImmunityAbAttrParams): boolean { // TODO: Evaluate whether it makes sense to check against simulated here. // We likely want to check 'simulated' when the apply method enqueues the phase return !params.simulated && super.canApply(params); } - override apply(params: AugmentMoveInteractionAbAttrParams): void { + override apply(params: MoveImmunityAbAttrParams): void { super.apply(params); // TODO: We probably should not unshift the phase if this is simulated globalScene.phaseManager.unshiftNew( @@ -1479,8 +1496,14 @@ export class IgnoreMoveEffectsAbAttr extends PreDefendAbAttr { } } +export interface FieldPreventExplosiveMovesAbAttrParams extends AbAttrBaseParams { + /** Holds whether the explosive move should be prevented*/ + cancelled: BooleanHolder; +} + export class FieldPreventExplosiveMovesAbAttr extends AbAttr { - override apply({ cancelled }: AugmentMoveInteractionAbAttrParams): void { + // TODO: investigate whether we need to check against `cancelled` in a `canApply` method + override apply({ cancelled }: FieldPreventExplosiveMovesAbAttrParams): void { cancelled.value = true; } } @@ -5327,8 +5350,6 @@ export class PostFaintContactDamageAbAttr extends PostFaintAbAttr { pokemon: otherPokemon, simulated, cancelled, - move, - opponent: pokemon, }); } return !(!diedToDirectDamage || cancelled.value || attacker.hasAbilityWithAttr("BlockNonDirectDamageAbAttr")); diff --git a/src/data/moves/move.ts b/src/data/moves/move.ts index 93b59780f6e..e2ddf5696f1 100644 --- a/src/data/moves/move.ts +++ b/src/data/moves/move.ts @@ -88,6 +88,7 @@ import { isVirtual, MoveUseMode } from "#enums/move-use-mode"; import { ChargingMove, MoveAttrMap, MoveAttrString, MoveKindString, MoveClassMap } from "#app/@types/move-types"; import { applyMoveAttrs } from "./apply-attrs"; import { frenzyMissFunc, getMoveTargets } from "./move-utils"; +import { AbAttrBaseParams, AbAttrParamsWithCancel, PreAttackModifyPowerAbAttrParams } from "../abilities/ability"; type MoveConditionFunc = (user: Pokemon, target: Pokemon, move: Move) => boolean; export type UserMoveConditionFunc = (user: Pokemon, move: Move) => boolean; @@ -339,7 +340,7 @@ export default abstract class Move implements Localizable { const bypassed = new BooleanHolder(false); // TODO: Allow this to be simulated - applyAbAttrs("InfiltratorAbAttr", user, null, false, bypassed); + applyAbAttrs("InfiltratorAbAttr", {pokemon: user, bypassed}); return !bypassed.value && !this.hasFlag(MoveFlags.SOUND_BASED) @@ -637,7 +638,7 @@ export default abstract class Move implements Localizable { case MoveFlags.IGNORE_ABILITIES: if (user.hasAbilityWithAttr("MoveAbilityBypassAbAttr")) { const abilityEffectsIgnored = new BooleanHolder(false); - applyAbAttrs("MoveAbilityBypassAbAttr", user, abilityEffectsIgnored, false, this); + applyAbAttrs("MoveAbilityBypassAbAttr", {pokemon: user, cancelled: abilityEffectsIgnored, move: this}); if (abilityEffectsIgnored.value) { return true; } @@ -754,7 +755,7 @@ export default abstract class Move implements Localizable { const moveAccuracy = new NumberHolder(this.accuracy); applyMoveAttrs("VariableAccuracyAttr", user, target, this, moveAccuracy); - applyPreDefendAbAttrs("WonderSkinAbAttr", target, user, this, { value: false }, simulated, moveAccuracy); + applyAbAttrs("WonderSkinAbAttr", {pokemon: target, opponent: user, move: this, simulated, accuracy: moveAccuracy}); if (moveAccuracy.value === -1) { return moveAccuracy.value; @@ -797,17 +798,25 @@ export default abstract class Move implements Localizable { const typeChangeMovePowerMultiplier = new NumberHolder(1); const typeChangeHolder = new NumberHolder(this.type); - applyPreAttackAbAttrs("MoveTypeChangeAbAttr", source, target, this, true, typeChangeHolder, typeChangeMovePowerMultiplier); + applyAbAttrs("MoveTypeChangeAbAttr", {pokemon: source, opponent: target, move: this, simulated: true, moveType: typeChangeHolder, power: typeChangeMovePowerMultiplier}); const sourceTeraType = source.getTeraType(); if (source.isTerastallized && sourceTeraType === this.type && power.value < 60 && this.priority <= 0 && !this.hasAttr("MultiHitAttr") && !globalScene.findModifier(m => m instanceof PokemonMultiHitModifier && m.pokemonId === source.id)) { power.value = 60; } - applyPreAttackAbAttrs("VariableMovePowerAbAttr", source, target, this, simulated, power); + const abAttrParams: PreAttackModifyPowerAbAttrParams = { + pokemon: source, + opponent: target, + simulated, + power, + move: this, + } + + applyAbAttrs("VariableMovePowerAbAttr", abAttrParams); const ally = source.getAlly(); if (!isNullOrUndefined(ally)) { - applyPreAttackAbAttrs("AllyMoveCategoryPowerBoostAbAttr", ally, target, this, simulated, power); + applyAbAttrs("AllyMoveCategoryPowerBoostAbAttr", {...abAttrParams, pokemon: ally}); } const fieldAuras = new Set( @@ -819,11 +828,12 @@ export default abstract class Move implements Localizable { .flat(), ); for (const aura of fieldAuras) { - aura.applyPreAttack(source, null, simulated, target, this, [ power ]); + // TODO: Refactor the fieldAura attribute so that its apply method is not directly called + aura.apply({pokemon: source, simulated, opponent: target, move: this, power}); } const alliedField: Pokemon[] = source.isPlayer() ? globalScene.getPlayerField() : globalScene.getEnemyField(); - alliedField.forEach(p => applyPreAttackAbAttrs("UserFieldMoveTypePowerBoostAbAttr", p, target, this, simulated, power)); + alliedField.forEach(p => applyAbAttrs("UserFieldMoveTypePowerBoostAbAttr", {pokemon: p, opponent: target, move: this, simulated, power})); power.value *= typeChangeMovePowerMultiplier.value; @@ -850,7 +860,7 @@ export default abstract class Move implements Localizable { const priority = new NumberHolder(this.priority); applyMoveAttrs("IncrementMovePriorityAttr", user, null, this, priority); - applyAbAttrs("ChangeMovePriorityAbAttr", user, null, simulated, this, priority); + applyAbAttrs("ChangeMovePriorityAbAttr", {pokemon: user, simulated, move: this, priority}); return priority.value; } @@ -1302,7 +1312,7 @@ export class MoveEffectAttr extends MoveAttr { getMoveChance(user: Pokemon, target: Pokemon, move: Move, selfEffect?: Boolean, showAbility?: Boolean): number { const moveChance = new NumberHolder(this.effectChanceOverride ?? move.chance); - applyAbAttrs("MoveEffectChanceMultiplierAbAttr", user, null, !showAbility, moveChance, move); + applyAbAttrs("MoveEffectChanceMultiplierAbAttr", {pokemon: user, simulated: !showAbility, chance: moveChance, move}); if ((!move.hasAttr("FlinchAttr") || moveChance.value <= move.chance) && !move.hasAttr("SecretPowerAttr")) { const userSide = user.isPlayer() ? ArenaTagSide.PLAYER : ArenaTagSide.ENEMY; @@ -1310,7 +1320,7 @@ export class MoveEffectAttr extends MoveAttr { } if (!selfEffect) { - applyPreDefendAbAttrs("IgnoreMoveEffectsAbAttr", target, user, null, null, !showAbility, moveChance); + applyAbAttrs("IgnoreMoveEffectsAbAttr", {pokemon: target, move, simulated: !showAbility, chance: moveChance}); } return moveChance.value; } @@ -1688,8 +1698,9 @@ export class RecoilAttr extends MoveEffectAttr { const cancelled = new BooleanHolder(false); if (!this.unblockable) { - applyAbAttrs("BlockRecoilDamageAttr", user, cancelled); - applyAbAttrs("BlockNonDirectDamageAbAttr", user, cancelled); + const abAttrParams: AbAttrParamsWithCancel = {pokemon: user, cancelled}; + applyAbAttrs("BlockRecoilDamageAttr", abAttrParams); + applyAbAttrs("BlockNonDirectDamageAbAttr", abAttrParams); } if (cancelled.value) { @@ -1822,7 +1833,7 @@ export class HalfSacrificialAttr extends MoveEffectAttr { const cancelled = new BooleanHolder(false); // Check to see if the Pokemon has an ability that blocks non-direct damage - applyAbAttrs("BlockNonDirectDamageAbAttr", user, cancelled); + applyAbAttrs("BlockNonDirectDamageAbAttr", {pokemon: user, cancelled}); if (!cancelled.value) { user.damageAndUpdate(toDmgValue(user.getMaxHp() / 2), { result: HitResult.INDIRECT, ignoreSegments: true }); globalScene.phaseManager.queueMessage(i18next.t("moveTriggers:cutHpPowerUpMove", { pokemonName: getPokemonNameWithAffix(user) })); // Queue recoil message @@ -2021,7 +2032,7 @@ export class FlameBurstAttr extends MoveEffectAttr { const cancelled = new BooleanHolder(false); if (!isNullOrUndefined(targetAlly)) { - applyAbAttrs("BlockNonDirectDamageAbAttr", targetAlly, cancelled); + applyAbAttrs("BlockNonDirectDamageAbAttr", {pokemon: targetAlly, cancelled}); } if (cancelled.value || !targetAlly || targetAlly.switchOutStatus) { @@ -2393,7 +2404,7 @@ export class MultiHitAttr extends MoveAttr { { const rand = user.randBattleSeedInt(20); const hitValue = new NumberHolder(rand); - applyAbAttrs("MaxMultiHitAbAttr", user, null, false, hitValue); + applyAbAttrs("MaxMultiHitAbAttr", {pokemon: user, hits: hitValue}); if (hitValue.value >= 13) { return 2; } else if (hitValue.value >= 6) { @@ -2501,7 +2512,7 @@ export class StatusEffectAttr extends MoveEffectAttr { } if (((!pokemon.status || this.overrideStatus) || (pokemon.status.effect === this.effect && moveChance < 0)) && pokemon.trySetStatus(this.effect, true, user, this.turnsRemaining, null, this.overrideStatus, quiet)) { - applyPostAttackAbAttrs("ConfusionOnStatusEffectAbAttr", user, target, move, null, false, this.effect); + applyAbAttrs("ConfusionOnStatusEffectAbAttr", {pokemon: user, opponent: target, move, effect: this.effect}); return true; } } @@ -2658,7 +2669,7 @@ export class RemoveHeldItemAttr extends MoveEffectAttr { // Check for abilities that block item theft // TODO: This should not trigger if the target would faint beforehand const cancelled = new BooleanHolder(false); - applyAbAttrs("BlockItemTheftAbAttr", target, cancelled); + applyAbAttrs("BlockItemTheftAbAttr", {pokemon: target, cancelled}); if (cancelled.value) { return false; @@ -2775,8 +2786,8 @@ export class EatBerryAttr extends MoveEffectAttr { protected eatBerry(consumer: Pokemon, berryOwner: Pokemon = consumer, updateHarvest = consumer === berryOwner) { // consumer eats berry, owner triggers unburden and similar effects getBerryEffectFunc(this.chosenBerry.berryType)(consumer); - applyPostItemLostAbAttrs("PostItemLostAbAttr", berryOwner, false); - applyAbAttrs("HealFromBerryUseAbAttr", consumer, new BooleanHolder(false)); + applyAbAttrs("PostItemLostAbAttr", {pokemon: berryOwner}); + applyAbAttrs("HealFromBerryUseAbAttr", {pokemon: consumer}); consumer.recordEatenBerry(this.chosenBerry.berryType, updateHarvest); } } @@ -2801,7 +2812,7 @@ export class StealEatBerryAttr extends EatBerryAttr { apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean { // check for abilities that block item theft const cancelled = new BooleanHolder(false); - applyAbAttrs("BlockItemTheftAbAttr", target, cancelled); + applyAbAttrs("BlockItemTheftAbAttr", {pokemon: target, cancelled}); if (cancelled.value === true) { return false; } @@ -2815,7 +2826,7 @@ export class StealEatBerryAttr extends EatBerryAttr { // pick a random berry and eat it this.chosenBerry = heldBerries[user.randBattleSeedInt(heldBerries.length)]; - applyPostItemLostAbAttrs("PostItemLostAbAttr", target, false); + applyAbAttrs("PostItemLostAbAttr", {pokemon: target}); const message = i18next.t("battle:stealEatBerry", { pokemonName: user.name, targetName: target.name, berryName: this.chosenBerry.type.name }); globalScene.phaseManager.queueMessage(message); this.reduceBerryModifier(target); @@ -3006,7 +3017,7 @@ export class OneHitKOAttr extends MoveAttr { getCondition(): MoveConditionFunc { return (user, target, move) => { const cancelled = new BooleanHolder(false); - applyAbAttrs("BlockOneHitKOAbAttr", target, cancelled); + applyAbAttrs("BlockOneHitKOAbAttr", {pokemon: target, cancelled}); return !cancelled.value && user.level >= target.level; }; } @@ -5418,7 +5429,7 @@ export class NoEffectAttr extends MoveAttr { const crashDamageFunc = (user: Pokemon, move: Move) => { const cancelled = new BooleanHolder(false); - applyAbAttrs("BlockNonDirectDamageAbAttr", user, cancelled); + applyAbAttrs("BlockNonDirectDamageAbAttr", {pokemon: user, cancelled}); if (cancelled.value) { return false; } @@ -6417,9 +6428,9 @@ export class ForceSwitchOutAttr extends MoveEffectAttr { } getFailedText(_user: Pokemon, target: Pokemon, _move: Move): string | undefined { - const blockedByAbility = new BooleanHolder(false); - applyAbAttrs("ForceSwitchOutImmunityAbAttr", target, blockedByAbility); - if (blockedByAbility.value) { + const cancelled = new BooleanHolder(false); + applyAbAttrs("ForceSwitchOutImmunityAbAttr", {pokemon: target, cancelled}); + if (cancelled.value) { return i18next.t("moveTriggers:cannotBeSwitchedOut", { pokemonName: getPokemonNameWithAffix(target) }); } } @@ -6458,7 +6469,7 @@ export class ForceSwitchOutAttr extends MoveEffectAttr { } const blockedByAbility = new BooleanHolder(false); - applyAbAttrs("ForceSwitchOutImmunityAbAttr", target, blockedByAbility); + applyAbAttrs("ForceSwitchOutImmunityAbAttr", {pokemon: target, cancelled: blockedByAbility}); if (blockedByAbility.value) { return false; } @@ -7967,7 +7978,7 @@ const failIfSingleBattle: MoveConditionFunc = (user, target, move) => globalScen const failIfDampCondition: MoveConditionFunc = (user, target, move) => { const cancelled = new BooleanHolder(false); - globalScene.getField(true).map(p=>applyAbAttrs("FieldPreventExplosiveMovesAbAttr", p, cancelled)); + globalScene.getField(true).map(p=>applyAbAttrs("FieldPreventExplosiveMovesAbAttr", {pokemon: p, cancelled})); // Queue a message if an ability prevented usage of the move if (cancelled.value) { globalScene.phaseManager.queueMessage(i18next.t("moveTriggers:cannotUseMove", { pokemonName: getPokemonNameWithAffix(user), moveName: move.name })); diff --git a/src/field/pokemon.ts b/src/field/pokemon.ts index b918580d31e..b6c8eca47f2 100644 --- a/src/field/pokemon.ts +++ b/src/field/pokemon.ts @@ -2278,14 +2278,12 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { applyMoveAttrs("VariableMoveTypeAttr", this, null, move, moveTypeHolder); - const cancelled = new BooleanHolder(false); const power = new NumberHolder(move.power); applyAbAttrs("MoveTypeChangeAbAttr", { pokemon: this, move, simulated, moveType: moveTypeHolder, - cancelled, power, opponent: this, }); @@ -3747,7 +3745,6 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { opponent: this, move, simulated, - cancelled: new BooleanHolder(false), multiplier: multiStrikeEnhancementMultiplier, }); } @@ -3855,8 +3852,6 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { move, simulated, damage, - // cancelled isn't necessary for this ability attribute, but is required by the interface - cancelled: new BooleanHolder(false), }); } @@ -3872,7 +3867,6 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { pokemon: this, opponent: source, move, - cancelled, simulated, damage, }; diff --git a/src/phases/attempt-run-phase.ts b/src/phases/attempt-run-phase.ts index 5e24f3474a6..ecd64380c31 100644 --- a/src/phases/attempt-run-phase.ts +++ b/src/phases/attempt-run-phase.ts @@ -1,4 +1,4 @@ -import { applyAbAttrs, applyPreLeaveFieldAbAttrs } from "#app/data/abilities/apply-ab-attrs"; +import { applyAbAttrs } from "#app/data/abilities/apply-ab-attrs"; import { Stat } from "#enums/stat"; import { StatusEffect } from "#enums/status-effect"; import type { PlayerPokemon, EnemyPokemon } from "#app/field/pokemon"; @@ -25,10 +25,10 @@ export class AttemptRunPhase extends PokemonPhase { this.attemptRunAway(playerField, enemyField, escapeChance); - applyAbAttrs("RunSuccessAbAttr", playerPokemon, null, false, escapeChance); + applyAbAttrs("RunSuccessAbAttr", { pokemon: playerPokemon, chance: escapeChance }); if (playerPokemon.randBattleSeedInt(100) < escapeChance.value && !this.forceFailEscape) { - enemyField.forEach(enemyPokemon => applyPreLeaveFieldAbAttrs("PreLeaveFieldAbAttr", enemyPokemon)); + enemyField.forEach(enemyPokemon => applyAbAttrs("PreLeaveFieldAbAttr", { pokemon: enemyPokemon })); globalScene.playSound("se/flee"); globalScene.phaseManager.queueMessage(i18next.t("battle:runAwaySuccess"), null, true, 500); @@ -38,14 +38,11 @@ export class AttemptRunPhase extends PokemonPhase { alpha: 0, duration: 250, ease: "Sine.easeIn", - onComplete: () => - // biome-ignore lint/complexity/noForEach: TODO - enemyField.forEach(enemyPokemon => enemyPokemon.destroy()), + onComplete: () => enemyField.forEach(enemyPokemon => enemyPokemon.destroy()), }); globalScene.clearEnemyHeldItemModifiers(); - // biome-ignore lint/complexity/noForEach: TODO enemyField.forEach(enemyPokemon => { enemyPokemon.hideInfo().then(() => enemyPokemon.destroy()); enemyPokemon.hp = 0;