From d53a9fb0e39b0b48992b052d12f868d2d36c56ee Mon Sep 17 00:00:00 2001 From: NightKev <34855794+DayKev@users.noreply.github.com> Date: Fri, 4 Oct 2024 04:49:24 -0700 Subject: [PATCH] Add `hitsSubstitute()` checks to all `PostDefendAbAttr`s Also fix comment indentation in `MoveEffectPhase` --- src/data/ability.ts | 138 +++++++------- src/phases/move-effect-phase.ts | 231 ++++++++++++------------ src/test/abilities/serene_grace.test.ts | 4 +- src/test/abilities/sheer_force.test.ts | 14 +- src/test/abilities/shield_dust.test.ts | 4 +- src/test/moves/dynamax_cannon.test.ts | 10 +- 6 files changed, 204 insertions(+), 197 deletions(-) diff --git a/src/data/ability.ts b/src/data/ability.ts index c85016b3c12..5d36be7f878 100644 --- a/src/data/ability.ts +++ b/src/data/ability.ts @@ -634,15 +634,15 @@ export class ReverseDrainAbAttr extends PostDefendAbAttr { * Examples include: Absorb, Draining Kiss, Bitter Blade, etc. * Also displays a message to show this ability was activated. * @param pokemon {@linkcode Pokemon} with this ability - * @param passive N/A + * @param _passive N/A * @param attacker {@linkcode Pokemon} that is attacking this Pokemon * @param move {@linkcode PokemonMove} that is being used - * @param hitResult N/A - * @args N/A + * @param _hitResult N/A + * @param _args N/A * @returns true if healing should be reversed on a healing move, false otherwise. */ - applyPostDefend(pokemon: Pokemon, passive: boolean, simulated: boolean, attacker: Pokemon, move: Move, hitResult: HitResult, args: any[]): boolean { - if (move.hasAttr(HitHealAttr)) { + override applyPostDefend(pokemon: Pokemon, _passive: boolean, simulated: boolean, attacker: Pokemon, move: Move, _hitResult: HitResult, _args: any[]): boolean { + if (move.hasAttr(HitHealAttr) && !move.hitsSubstitute(attacker, pokemon)) { if (!simulated) { pokemon.scene.queueMessage(i18next.t("abilityTriggers:reverseDrain", { pokemonNameWithAffix: getPokemonNameWithAffix(attacker) })); } @@ -669,8 +669,8 @@ export class PostDefendStatStageChangeAbAttr extends PostDefendAbAttr { this.allOthers = allOthers; } - applyPostDefend(pokemon: Pokemon, passive: boolean, simulated: boolean, attacker: Pokemon, move: Move, hitResult: HitResult, args: any[]): boolean { - if (this.condition(pokemon, attacker, move)) { + override applyPostDefend(pokemon: Pokemon, _passive: boolean, simulated: boolean, attacker: Pokemon, move: Move, _hitResult: HitResult, _args: any[]): boolean { + if (this.condition(pokemon, attacker, move) && !move.hitsSubstitute(attacker, pokemon)) { if (simulated) { return true; } @@ -707,13 +707,13 @@ export class PostDefendHpGatedStatStageChangeAbAttr extends PostDefendAbAttr { this.selfTarget = selfTarget; } - applyPostDefend(pokemon: Pokemon, passive: boolean, simulated: boolean, attacker: Pokemon, move: Move, hitResult: HitResult, args: any[]): boolean { - const hpGateFlat: integer = Math.ceil(pokemon.getMaxHp() * this.hpGate); + override applyPostDefend(pokemon: Pokemon, _passive: boolean, simulated: boolean, attacker: Pokemon, move: Move, _hitResult: HitResult, _args: any[]): boolean { + const hpGateFlat: number = Math.ceil(pokemon.getMaxHp() * this.hpGate); const lastAttackReceived = pokemon.turnData.attacksReceived[pokemon.turnData.attacksReceived.length - 1]; const damageReceived = lastAttackReceived?.damage || 0; - if (this.condition(pokemon, attacker, move) && (pokemon.hp <= hpGateFlat && (pokemon.hp + damageReceived) > hpGateFlat)) { - if (!simulated ) { + if (this.condition(pokemon, attacker, move) && (pokemon.hp <= hpGateFlat && (pokemon.hp + damageReceived) > hpGateFlat) && !move.hitsSubstitute(attacker, pokemon)) { + if (!simulated) { pokemon.scene.unshiftPhase(new StatStageChangePhase(pokemon.scene, (this.selfTarget ? pokemon : attacker).getBattlerIndex(), true, this.stats, this.stages)); } return true; @@ -734,8 +734,8 @@ export class PostDefendApplyArenaTrapTagAbAttr extends PostDefendAbAttr { this.tagType = tagType; } - applyPostDefend(pokemon: Pokemon, passive: boolean, simulated: boolean, attacker: Pokemon, move: Move, hitResult: HitResult, args: any[]): boolean { - if (this.condition(pokemon, attacker, move)) { + override applyPostDefend(pokemon: Pokemon, _passive: boolean, simulated: boolean, attacker: Pokemon, move: Move, _hitResult: HitResult, _args: any[]): boolean { + if (this.condition(pokemon, attacker, move) && !move.hitsSubstitute(attacker, pokemon)) { const tag = pokemon.scene.arena.getTag(this.tagType) as ArenaTrapTag; if (!pokemon.scene.arena.getTag(this.tagType) || tag.layers < tag.maxLayers) { if (!simulated) { @@ -758,8 +758,8 @@ export class PostDefendApplyBattlerTagAbAttr extends PostDefendAbAttr { this.tagType = tagType; } - applyPostDefend(pokemon: Pokemon, passive: boolean, simulated: boolean, attacker: Pokemon, move: Move, hitResult: HitResult, args: any[]): boolean { - if (this.condition(pokemon, attacker, move)) { + override applyPostDefend(pokemon: Pokemon, _passive: boolean, simulated: boolean, attacker: Pokemon, move: Move, _hitResult: HitResult, _args: any[]): boolean { + if (this.condition(pokemon, attacker, move) && !move.hitsSubstitute(attacker, pokemon)) { if (!pokemon.getTag(this.tagType) && !simulated) { pokemon.addTag(this.tagType, undefined, undefined, pokemon.id); pokemon.scene.queueMessage(i18next.t("abilityTriggers:windPowerCharged", { pokemonName: getPokemonNameWithAffix(pokemon), moveName: move.name })); @@ -771,8 +771,8 @@ export class PostDefendApplyBattlerTagAbAttr extends PostDefendAbAttr { } export class PostDefendTypeChangeAbAttr extends PostDefendAbAttr { - applyPostDefend(pokemon: Pokemon, passive: boolean, simulated: boolean, attacker: Pokemon, move: Move, hitResult: HitResult, args: any[]): boolean { - if (hitResult < HitResult.NO_EFFECT) { + override applyPostDefend(pokemon: Pokemon, _passive: boolean, simulated: boolean, attacker: Pokemon, move: Move, hitResult: HitResult, _args: any[]): boolean { + if (hitResult < HitResult.NO_EFFECT && !move.hitsSubstitute(attacker, pokemon)) { if (simulated) { return true; } @@ -787,7 +787,7 @@ export class PostDefendTypeChangeAbAttr extends PostDefendAbAttr { return false; } - getTriggerMessage(pokemon: Pokemon, abilityName: string, ...args: any[]): string { + override getTriggerMessage(pokemon: Pokemon, abilityName: string, ..._args: any[]): string { return i18next.t("abilityTriggers:postDefendTypeChange", { pokemonNameWithAffix: getPokemonNameWithAffix(pokemon), abilityName, @@ -805,8 +805,8 @@ export class PostDefendTerrainChangeAbAttr extends PostDefendAbAttr { this.terrainType = terrainType; } - applyPostDefend(pokemon: Pokemon, passive: boolean, simulated: boolean, attacker: Pokemon, move: Move, hitResult: HitResult, args: any[]): boolean { - if (hitResult < HitResult.NO_EFFECT) { + override applyPostDefend(pokemon: Pokemon, _passive: boolean, simulated: boolean, attacker: Pokemon, move: Move, hitResult: HitResult, _args: any[]): boolean { + if (hitResult < HitResult.NO_EFFECT && !move.hitsSubstitute(attacker, pokemon)) { if (simulated) { return pokemon.scene.arena.terrain?.terrainType !== (this.terrainType || undefined); } else { @@ -829,8 +829,9 @@ export class PostDefendContactApplyStatusEffectAbAttr extends PostDefendAbAttr { this.effects = effects; } - applyPostDefend(pokemon: Pokemon, passive: boolean, simulated: boolean, attacker: Pokemon, move: Move, hitResult: HitResult, args: any[]): boolean { - if (move.checkFlag(MoveFlags.MAKES_CONTACT, attacker, pokemon) && !attacker.status && (this.chance === -1 || pokemon.randSeedInt(100) < this.chance)) { + override applyPostDefend(pokemon: Pokemon, _passive: boolean, simulated: boolean, attacker: Pokemon, move: Move, _hitResult: HitResult, _args: any[]): boolean { + if (move.checkFlag(MoveFlags.MAKES_CONTACT, attacker, pokemon) && !attacker.status + && (this.chance === -1 || pokemon.randSeedInt(100) < this.chance) && !move.hitsSubstitute(attacker, pokemon)) { const effect = this.effects.length === 1 ? this.effects[0] : this.effects[pokemon.randSeedInt(this.effects.length)]; if (simulated) { return attacker.canSetStatus(effect, true, false, pokemon); @@ -869,8 +870,8 @@ export class PostDefendContactApplyTagChanceAbAttr extends PostDefendAbAttr { this.turnCount = turnCount; } - applyPostDefend(pokemon: Pokemon, passive: boolean, simulated: boolean, attacker: Pokemon, move: Move, hitResult: HitResult, args: any[]): boolean { - if (move.checkFlag(MoveFlags.MAKES_CONTACT, attacker, pokemon) && pokemon.randSeedInt(100) < this.chance) { + override applyPostDefend(pokemon: Pokemon, _passive: boolean, simulated: boolean, attacker: Pokemon, move: Move, _hitResult: HitResult, _args: any[]): boolean { + if (move.checkFlag(MoveFlags.MAKES_CONTACT, attacker, pokemon) && pokemon.randSeedInt(100) < this.chance && !move.hitsSubstitute(attacker, pokemon)) { if (simulated) { return attacker.canAddTag(this.tagType); } else { @@ -893,7 +894,11 @@ export class PostDefendCritStatStageChangeAbAttr extends PostDefendAbAttr { this.stages = stages; } - applyPostDefend(pokemon: Pokemon, passive: boolean, simulated: boolean, attacker: Pokemon, move: Move, hitResult: HitResult, args: any[]): boolean { + override applyPostDefend(pokemon: Pokemon, _passive: boolean, simulated: boolean, attacker: Pokemon, move: Move, _hitResult: HitResult, _args: any[]): boolean { + if (move.hitsSubstitute(attacker, pokemon)) { + return false; + } + if (!simulated) { pokemon.scene.unshiftPhase(new StatStageChangePhase(pokemon.scene, pokemon.getBattlerIndex(), true, [ this.stat ], this.stages)); } @@ -901,7 +906,7 @@ export class PostDefendCritStatStageChangeAbAttr extends PostDefendAbAttr { return true; } - getCondition(): AbAttrCondition { + override getCondition(): AbAttrCondition { return (pokemon: Pokemon) => pokemon.turnData.attacksReceived.length !== 0 && pokemon.turnData.attacksReceived[pokemon.turnData.attacksReceived.length - 1].critical; } } @@ -915,8 +920,9 @@ export class PostDefendContactDamageAbAttr extends PostDefendAbAttr { this.damageRatio = damageRatio; } - applyPostDefend(pokemon: Pokemon, passive: boolean, simulated: boolean, attacker: Pokemon, move: Move, hitResult: HitResult, args: any[]): boolean { - if (!simulated && move.checkFlag(MoveFlags.MAKES_CONTACT, attacker, pokemon) && !attacker.hasAbilityWithAttr(BlockNonDirectDamageAbAttr)) { + override applyPostDefend(pokemon: Pokemon, _passive: boolean, simulated: boolean, attacker: Pokemon, move: Move, _hitResult: HitResult, _args: any[]): boolean { + if (!simulated && move.checkFlag(MoveFlags.MAKES_CONTACT, attacker, pokemon) + && !attacker.hasAbilityWithAttr(BlockNonDirectDamageAbAttr) && !move.hitsSubstitute(attacker, pokemon)) { attacker.damageAndUpdate(Utils.toDmgValue(attacker.getMaxHp() * (1 / this.damageRatio)), HitResult.OTHER); attacker.turnData.damageTaken += Utils.toDmgValue(attacker.getMaxHp() * (1 / this.damageRatio)); return true; @@ -925,7 +931,7 @@ export class PostDefendContactDamageAbAttr extends PostDefendAbAttr { return false; } - getTriggerMessage(pokemon: Pokemon, abilityName: string, ...args: any[]): string { + override getTriggerMessage(pokemon: Pokemon, abilityName: string, ..._args: any[]): string { return i18next.t("abilityTriggers:postDefendContactDamage", { pokemonNameWithAffix: getPokemonNameWithAffix(pokemon), abilityName @@ -948,8 +954,8 @@ export class PostDefendPerishSongAbAttr extends PostDefendAbAttr { this.turns = turns; } - applyPostDefend(pokemon: Pokemon, passive: boolean, simulated: boolean, attacker: Pokemon, move: Move, hitResult: HitResult, args: any[]): boolean { - if (move.checkFlag(MoveFlags.MAKES_CONTACT, attacker, pokemon)) { + override applyPostDefend(pokemon: Pokemon, _passive: boolean, simulated: boolean, attacker: Pokemon, move: Move, _hitResult: HitResult, _args: any[]): boolean { + if (move.checkFlag(MoveFlags.MAKES_CONTACT, attacker, pokemon) && !move.hitsSubstitute(attacker, pokemon)) { if (pokemon.getTag(BattlerTagType.PERISH_SONG) || attacker.getTag(BattlerTagType.PERISH_SONG)) { return false; } else { @@ -963,24 +969,24 @@ export class PostDefendPerishSongAbAttr extends PostDefendAbAttr { return false; } - getTriggerMessage(pokemon: Pokemon, abilityName: string, ...args: any[]): string { + override getTriggerMessage(pokemon: Pokemon, abilityName: string, ..._args: any[]): string { return i18next.t("abilityTriggers:perishBody", {pokemonName: getPokemonNameWithAffix(pokemon), abilityName: abilityName}); } } export class PostDefendWeatherChangeAbAttr extends PostDefendAbAttr { private weatherType: WeatherType; - protected condition: PokemonDefendCondition | null; + protected condition?: PokemonDefendCondition; constructor(weatherType: WeatherType, condition?: PokemonDefendCondition) { super(); this.weatherType = weatherType; - this.condition = condition ?? null; + this.condition = condition; } - applyPostDefend(pokemon: Pokemon, passive: boolean, simulated: boolean, attacker: Pokemon, move: Move, hitResult: HitResult, args: any[]): boolean { - if (this.condition !== null && !this.condition(pokemon, attacker, move)) { + override applyPostDefend(pokemon: Pokemon, _passive: boolean, simulated: boolean, attacker: Pokemon, move: Move, _hitResult: HitResult, _args: any[]): boolean { + if (this.condition && !this.condition(pokemon, attacker, move) || move.hitsSubstitute(attacker, pokemon)) { return false; } if (!pokemon.scene.arena.weather?.isImmutable()) { @@ -999,8 +1005,9 @@ export class PostDefendAbilitySwapAbAttr extends PostDefendAbAttr { super(); } - applyPostDefend(pokemon: Pokemon, passive: boolean, simulated: boolean, attacker: Pokemon, move: Move, hitResult: HitResult, args: any[]): boolean { - if (move.checkFlag(MoveFlags.MAKES_CONTACT, attacker, pokemon) && !attacker.getAbility().hasAttr(UnswappableAbilityAbAttr)) { + override applyPostDefend(pokemon: Pokemon, _passive: boolean, simulated: boolean, attacker: Pokemon, move: Move, _hitResult: HitResult, args: any[]): boolean { + if (move.checkFlag(MoveFlags.MAKES_CONTACT, attacker, pokemon) + && !attacker.getAbility().hasAttr(UnswappableAbilityAbAttr) && !move.hitsSubstitute(attacker, pokemon)) { if (!simulated) { const tempAbilityId = attacker.getAbility().id; attacker.summonData.ability = pokemon.getAbility().id; @@ -1012,7 +1019,7 @@ export class PostDefendAbilitySwapAbAttr extends PostDefendAbAttr { return false; } - getTriggerMessage(pokemon: Pokemon, abilityName: string, ...args: any[]): string { + override getTriggerMessage(pokemon: Pokemon, _abilityName: string, ..._args: any[]): string { return i18next.t("abilityTriggers:postDefendAbilitySwap", { pokemonNameWithAffix: getPokemonNameWithAffix(pokemon) }); } } @@ -1025,8 +1032,9 @@ export class PostDefendAbilityGiveAbAttr extends PostDefendAbAttr { this.ability = ability; } - applyPostDefend(pokemon: Pokemon, passive: boolean, simulated: boolean, attacker: Pokemon, move: Move, hitResult: HitResult, args: any[]): boolean { - if (move.checkFlag(MoveFlags.MAKES_CONTACT, attacker, pokemon) && !attacker.getAbility().hasAttr(UnsuppressableAbilityAbAttr) && !attacker.getAbility().hasAttr(PostDefendAbilityGiveAbAttr)) { + override applyPostDefend(pokemon: Pokemon, _passive: boolean, simulated: boolean, attacker: Pokemon, move: Move, _hitResult: HitResult, _args: any[]): boolean { + if (move.checkFlag(MoveFlags.MAKES_CONTACT, attacker, pokemon) && !attacker.getAbility().hasAttr(UnsuppressableAbilityAbAttr) + && !attacker.getAbility().hasAttr(PostDefendAbilityGiveAbAttr) && !move.hitsSubstitute(attacker, pokemon)) { if (!simulated) { attacker.summonData.ability = this.ability; } @@ -1037,7 +1045,7 @@ export class PostDefendAbilityGiveAbAttr extends PostDefendAbAttr { return false; } - getTriggerMessage(pokemon: Pokemon, abilityName: string, ...args: any[]): string { + override getTriggerMessage(pokemon: Pokemon, abilityName: string, ..._args: any[]): string { return i18next.t("abilityTriggers:postDefendAbilityGive", { pokemonNameWithAffix: getPokemonNameWithAffix(pokemon), abilityName @@ -1056,8 +1064,8 @@ export class PostDefendMoveDisableAbAttr extends PostDefendAbAttr { this.chance = chance; } - applyPostDefend(pokemon: Pokemon, passive: boolean, simulated: boolean, attacker: Pokemon, move: Move, hitResult: HitResult, args: any[]): boolean { - if (attacker.getTag(BattlerTagType.DISABLED) === null) { + override applyPostDefend(pokemon: Pokemon, _passive: boolean, simulated: boolean, attacker: Pokemon, move: Move, _hitResult: HitResult, _args: any[]): boolean { + if (attacker.getTag(BattlerTagType.DISABLED) === null && !move.hitsSubstitute(attacker, pokemon)) { if (move.checkFlag(MoveFlags.MAKES_CONTACT, attacker, pokemon) && (this.chance === -1 || pokemon.randSeedInt(100) < this.chance)) { if (simulated) { return true; @@ -1724,17 +1732,17 @@ export class PostAttackApplyBattlerTagAbAttr extends PostAttackAbAttr { } export class PostDefendStealHeldItemAbAttr extends PostDefendAbAttr { - private condition: PokemonDefendCondition | null; + private condition?: PokemonDefendCondition; constructor(condition?: PokemonDefendCondition) { super(); - this.condition = condition ?? null; + this.condition = condition; } - applyPostDefend(pokemon: Pokemon, passive: boolean, simulated: boolean, attacker: Pokemon, move: Move, hitResult: HitResult, args: any[]): Promise { + override applyPostDefend(pokemon: Pokemon, _passive: boolean, simulated: boolean, attacker: Pokemon, move: Move, hitResult: HitResult, _args: any[]): Promise { return new Promise(resolve => { - if (!simulated && hitResult < HitResult.NO_EFFECT && (!this.condition || this.condition(pokemon, attacker, move))) { + if (!simulated && hitResult < HitResult.NO_EFFECT && (!this.condition || this.condition(pokemon, attacker, move)) && !move.hitsSubstitute(attacker, pokemon)) { const heldItems = this.getTargetHeldItems(attacker).filter(i => i.isTransferable); if (heldItems.length) { const stolenItem = heldItems[pokemon.randSeedInt(heldItems.length)]; @@ -4476,7 +4484,7 @@ export class PostSummonStatStageChangeOnArenaAbAttr extends PostSummonStatStageC export class FormBlockDamageAbAttr extends ReceivedMoveDamageMultiplierAbAttr { private multiplier: number; private tagType: BattlerTagType; - private recoilDamageFunc: ((pokemon: Pokemon) => number) | undefined; + private recoilDamageFunc?: ((pokemon: Pokemon) => number); private triggerMessageFunc: (pokemon: Pokemon, abilityName: string) => string; constructor(condition: PokemonDefendCondition, multiplier: number, tagType: BattlerTagType, triggerMessageFunc: (pokemon: Pokemon, abilityName: string) => string, recoilDamageFunc?: (pokemon: Pokemon) => number) { @@ -4492,16 +4500,16 @@ export class FormBlockDamageAbAttr extends ReceivedMoveDamageMultiplierAbAttr { * Applies the pre-defense ability to the Pokémon. * Removes the appropriate `BattlerTagType` when hit by an attack and is in its defense form. * - * @param {Pokemon} pokemon The Pokémon with the ability. - * @param {boolean} passive n/a - * @param {Pokemon} attacker The attacking Pokémon. - * @param {PokemonMove} move The move being used. - * @param {Utils.BooleanHolder} cancelled n/a - * @param {any[]} args Additional arguments. - * @returns {boolean} Whether the immunity was applied. + * @param pokemon The Pokémon with the ability. + * @param _passive n/a + * @param attacker The attacking Pokémon. + * @param move The move being used. + * @param _cancelled n/a + * @param args Additional arguments. + * @returns `true` if the immunity was applied. */ - applyPreDefend(pokemon: Pokemon, passive: boolean, simulated: boolean, attacker: Pokemon, move: Move, cancelled: Utils.BooleanHolder, args: any[]): boolean { - if (this.condition(pokemon, attacker, move)) { + override applyPreDefend(pokemon: Pokemon, _passive: boolean, simulated: boolean, attacker: Pokemon, move: Move, _cancelled: Utils.BooleanHolder, args: any[]): boolean { + if (this.condition(pokemon, attacker, move) && !move.hitsSubstitute(attacker, pokemon)) { if (!simulated) { (args[0] as Utils.NumberHolder).value = this.multiplier; pokemon.removeTag(this.tagType); @@ -4517,12 +4525,12 @@ export class FormBlockDamageAbAttr extends ReceivedMoveDamageMultiplierAbAttr { /** * Gets the message triggered when the Pokémon avoids damage using the form-changing ability. - * @param {Pokemon} pokemon The Pokémon with the ability. - * @param {string} abilityName The name of the ability. - * @param {...any} args n/a - * @returns {string} The trigger message. + * @param pokemon The Pokémon with the ability. + * @param abilityName The name of the ability. + * @param _args n/a + * @returns The trigger message. */ - getTriggerMessage(pokemon: Pokemon, abilityName: string, ...args: any[]): string { + getTriggerMessage(pokemon: Pokemon, abilityName: string, ..._args: any[]): string { return this.triggerMessageFunc(pokemon, abilityName); } } @@ -5504,7 +5512,7 @@ export function initAbilities() { // Add BattlerTagType.DISGUISE if the pokemon is in its disguised form .conditionalAttr(pokemon => pokemon.formIndex === 0, PostSummonAddBattlerTagAbAttr, BattlerTagType.DISGUISE, 0, false) .attr(FormBlockDamageAbAttr, - (target, user, move) => !!target.getTag(BattlerTagType.DISGUISE) && target.getMoveEffectiveness(user, move) > 0 && !move.hitsSubstitute(user, target), 0, BattlerTagType.DISGUISE, + (target, user, move) => !!target.getTag(BattlerTagType.DISGUISE) && target.getMoveEffectiveness(user, move) > 0, 0, BattlerTagType.DISGUISE, (pokemon, abilityName) => i18next.t("abilityTriggers:disguiseAvoidedDamage", { pokemonNameWithAffix: getPokemonNameWithAffix(pokemon), abilityName: abilityName }), (pokemon) => Utils.toDmgValue(pokemon.getMaxHp() / 8)) .attr(PostBattleInitFormChangeAbAttr, () => 0) @@ -5667,7 +5675,7 @@ export function initAbilities() { // When weather changes to HAIL or SNOW while pokemon is fielded, add BattlerTagType.ICE_FACE .attr(PostWeatherChangeAddBattlerTagAttr, BattlerTagType.ICE_FACE, 0, WeatherType.HAIL, WeatherType.SNOW) .attr(FormBlockDamageAbAttr, - (target, user, move) => move.category === MoveCategory.PHYSICAL && !!target.getTag(BattlerTagType.ICE_FACE) && !move.hitsSubstitute(user, target), 0, BattlerTagType.ICE_FACE, + (target, user, move) => move.category === MoveCategory.PHYSICAL && !!target.getTag(BattlerTagType.ICE_FACE), 0, BattlerTagType.ICE_FACE, (pokemon, abilityName) => i18next.t("abilityTriggers:iceFaceAvoidedDamage", { pokemonNameWithAffix: getPokemonNameWithAffix(pokemon), abilityName: abilityName })) .attr(PostBattleInitFormChangeAbAttr, () => 0) .bypassFaint() diff --git a/src/phases/move-effect-phase.ts b/src/phases/move-effect-phase.ts index 33f2394cd1e..0fd3fd6dbc8 100644 --- a/src/phases/move-effect-phase.ts +++ b/src/phases/move-effect-phase.ts @@ -12,7 +12,7 @@ import Pokemon, { PokemonMove, MoveResult, HitResult } from "#app/field/pokemon" import { getPokemonNameWithAffix } from "#app/messages"; import { PokemonMultiHitModifier, FlinchChanceModifier, EnemyAttackStatusEffectChanceModifier, ContactHeldItemTransferChanceModifier, HitHealModifier } from "#app/modifier/modifier"; import i18next from "i18next"; -import * as Utils from "#app/utils"; +import { BooleanHolder, NumberHolder, executeIf } from "#app/utils"; import { PokemonPhase } from "./pokemon-phase"; import { Type } from "#app/data/type"; @@ -24,10 +24,10 @@ export class MoveEffectPhase extends PokemonPhase { super(scene, battlerIndex); this.move = move; /** - * In double battles, if the right Pokemon selects a spread move and the left Pokemon dies - * with no party members available to switch in, then the right Pokemon takes the index - * of the left Pokemon and gets hit unless this is checked. - */ + * In double battles, if the right Pokemon selects a spread move and the left Pokemon dies + * with no party members available to switch in, then the right Pokemon takes the index + * of the left Pokemon and gets hit unless this is checked. + */ if (targets.includes(battlerIndex) && this.move.getMove().moveTarget === MoveTarget.ALL_NEAR_OTHERS) { const i = targets.indexOf(battlerIndex); targets.splice(i, i + 1); @@ -35,7 +35,7 @@ export class MoveEffectPhase extends PokemonPhase { this.targets = targets; } - start() { + public start() { super.start(); /** The Pokemon using this phase's invoked move */ @@ -49,15 +49,15 @@ export class MoveEffectPhase extends PokemonPhase { } /** - * Does an effect from this move override other effects on this turn? - * e.g. Charging moves (Fly, etc.) on their first turn of use. - */ - const overridden = new Utils.BooleanHolder(false); + * Does an effect from this move override other effects on this turn? + * e.g. Charging moves (Fly, etc.) on their first turn of use. + */ + const overridden = new BooleanHolder(false); /** The {@linkcode Move} object from {@linkcode allMoves} invoked by this phase */ const move = this.move.getMove(); // Assume single target for override - applyMoveAttrs(OverrideMoveEffectAttr, user, this.getTarget() ?? null, move, overridden, this.move.virtual).then(() => { + applyMoveAttrs(OverrideMoveEffectAttr, user, this.getFirstTarget() ?? null, move, overridden, this.move.virtual).then(() => { // If other effects were overriden, stop this phase before they can be applied if (overridden.value) { return this.end(); @@ -66,19 +66,19 @@ export class MoveEffectPhase extends PokemonPhase { user.lapseTags(BattlerTagLapseType.MOVE_EFFECT); /** - * If this phase is for the first hit of the invoked move, - * resolve the move's total hit count. This block combines the - * effects of the move itself, Parental Bond, and Multi-Lens to do so. - */ + * If this phase is for the first hit of the invoked move, + * resolve the move's total hit count. This block combines the + * effects of the move itself, Parental Bond, and Multi-Lens to do so. + */ if (user.turnData.hitsLeft === undefined) { - const hitCount = new Utils.IntegerHolder(1); + const hitCount = new NumberHolder(1); // Assume single target for multi hit - applyMoveAttrs(MultiHitAttr, user, this.getTarget() ?? null, move, hitCount); + applyMoveAttrs(MultiHitAttr, user, this.getFirstTarget() ?? null, move, hitCount); // If Parental Bond is applicable, double the hit count - applyPreAttackAbAttrs(AddSecondStrikeAbAttr, user, null, move, false, targets.length, hitCount, new Utils.IntegerHolder(0)); + applyPreAttackAbAttrs(AddSecondStrikeAbAttr, user, null, move, false, targets.length, hitCount, new NumberHolder(0)); // If Multi-Lens is applicable, multiply the hit count by 1 + the number of Multi-Lenses held by the user if (move instanceof AttackMove && !move.hasAttr(FixedDamageAttr)) { - this.scene.applyModifiers(PokemonMultiHitModifier, user.isPlayer(), user, hitCount, new Utils.IntegerHolder(0)); + this.scene.applyModifiers(PokemonMultiHitModifier, user.isPlayer(), user, hitCount, new NumberHolder(0)); } // Set the user's relevant turnData fields to reflect the final hit count user.turnData.hitCount = hitCount.value; @@ -86,16 +86,16 @@ export class MoveEffectPhase extends PokemonPhase { } /** - * Log to be entered into the user's move history once the move result is resolved. - * Note that `result` (a {@linkcode MoveResult}) logs whether the move was successfully - * used in the sense of "Does it have an effect on the user?". - */ + * Log to be entered into the user's move history once the move result is resolved. + * Note that `result` (a {@linkcode MoveResult}) logs whether the move was successfully + * used in the sense of "Does it have an effect on the user?". + */ const moveHistoryEntry = { move: this.move.moveId, targets: this.targets, result: MoveResult.PENDING, virtual: this.move.virtual }; /** - * Stores results of hit checks of the invoked move against all targets, organized by battler index. - * @see {@linkcode hitCheck} - */ + * Stores results of hit checks of the invoked move against all targets, organized by battler index. + * @see {@linkcode hitCheck} + */ const targetHitChecks = Object.fromEntries(targets.map(p => [p.getBattlerIndex(), this.hitCheck(p)])); const hasActiveTargets = targets.some(t => t.isActive(true)); @@ -103,15 +103,15 @@ export class MoveEffectPhase extends PokemonPhase { const isImmune = targets[0].hasAbilityWithAttr(TypeImmunityAbAttr) && (targets[0].getAbility()?.getAttrs(TypeImmunityAbAttr)?.[0]?.getImmuneType() === user.getMoveType(move)); /** - * If no targets are left for the move to hit (FAIL), or the invoked move is single-target - * (and not random target) and failed the hit check against its target (MISS), log the move - * as FAILed or MISSed (depending on the conditions above) and end this phase. - */ + * If no targets are left for the move to hit (FAIL), or the invoked move is single-target + * (and not random target) and failed the hit check against its target (MISS), log the move + * as FAILed or MISSed (depending on the conditions above) and end this phase. + */ if (!hasActiveTargets || (!move.hasAttr(VariableTargetAttr) && !move.isMultiTarget() && !targetHitChecks[this.targets[0]] && !targets[0].getTag(ProtectedTag) && !isImmune)) { this.stopMultiHit(); if (hasActiveTargets) { - this.scene.queueMessage(i18next.t("battle:attackMissed", { pokemonNameWithAffix: this.getTarget()? getPokemonNameWithAffix(this.getTarget()!) : "" })); + this.scene.queueMessage(i18next.t("battle:attackMissed", { pokemonNameWithAffix: this.getFirstTarget()? getPokemonNameWithAffix(this.getFirstTarget()!) : "" })); moveHistoryEntry.result = MoveResult.MISS; applyMoveAttrs(MissEffectAttr, user, null, move); } else { @@ -127,7 +127,7 @@ export class MoveEffectPhase extends PokemonPhase { const playOnEmptyField = this.scene.currentBattle?.mysteryEncounter?.hasBattleAnimationsWithoutTargets ?? false; // Move animation only needs one target - new MoveAnim(move.id as Moves, user, this.getTarget()!.getBattlerIndex()!, playOnEmptyField).play(this.scene, move.hitsSubstitute(user, this.getTarget()!), () => { + new MoveAnim(move.id as Moves, user, this.getFirstTarget()!.getBattlerIndex()!, playOnEmptyField).play(this.scene, move.hitsSubstitute(user, this.getFirstTarget()!), () => { /** Has the move successfully hit a target (for damage) yet? */ let hasHit: boolean = false; for (const target of targets) { @@ -135,9 +135,9 @@ export class MoveEffectPhase extends PokemonPhase { /** The {@linkcode ArenaTagSide} to which the target belongs */ const targetSide = target.isPlayer() ? ArenaTagSide.PLAYER : ArenaTagSide.ENEMY; /** Has the invoked move been cancelled by conditional protection (e.g Quick Guard)? */ - const hasConditionalProtectApplied = new Utils.BooleanHolder(false); + const hasConditionalProtectApplied = new BooleanHolder(false); /** Does the applied conditional protection bypass Protect-ignoring effects? */ - const bypassIgnoreProtect = new Utils.BooleanHolder(false); + const bypassIgnoreProtect = new BooleanHolder(false); /** If the move is not targeting a Pokemon on the user's side, try to apply conditional protection effects */ if (!this.move.getMove().isAllyTarget()) { this.scene.arena.applyTagsForSide(ConditionalProtectTag, targetSide, hasConditionalProtectApplied, user, target, move.id, bypassIgnoreProtect); @@ -152,9 +152,9 @@ export class MoveEffectPhase extends PokemonPhase { const isImmune = target.hasAbilityWithAttr(TypeImmunityAbAttr) && (target.getAbility()?.getAttrs(TypeImmunityAbAttr)?.[0]?.getImmuneType() === user.getMoveType(move)); /** - * If the move missed a target, stop all future hits against that target - * and move on to the next target (if there is one). - */ + * If the move missed a target, stop all future hits against that target + * and move on to the next target (if there is one). + */ if (!isImmune && !isProtected && !targetHitChecks[target.getBattlerIndex()]) { this.stopMultiHit(target); this.scene.queueMessage(i18next.t("battle:attackMissed", { pokemonNameWithAffix: getPokemonNameWithAffix(target) })); @@ -175,23 +175,23 @@ export class MoveEffectPhase extends PokemonPhase { } /** - * Since all fail/miss checks have applied, the move is considered successfully applied. - * It's worth noting that if the move has no effect or is protected against, this assignment - * is overwritten and the move is logged as a FAIL. - */ + * Since all fail/miss checks have applied, the move is considered successfully applied. + * It's worth noting that if the move has no effect or is protected against, this assignment + * is overwritten and the move is logged as a FAIL. + */ moveHistoryEntry.result = MoveResult.SUCCESS; /** - * Stores the result of applying the invoked move to the target. - * If the target is protected, the result is always `NO_EFFECT`. - * Otherwise, the hit result is based on type effectiveness, immunities, - * and other factors that may negate the attack or status application. - * - * Internally, the call to {@linkcode Pokemon.apply} is where damage is calculated - * (for attack moves) and the target's HP is updated. However, this isn't - * made visible to the user until the resulting {@linkcode DamagePhase} - * is invoked. - */ + * Stores the result of applying the invoked move to the target. + * If the target is protected, the result is always `NO_EFFECT`. + * Otherwise, the hit result is based on type effectiveness, immunities, + * and other factors that may negate the attack or status application. + * + * Internally, the call to {@linkcode Pokemon.apply} is where damage is calculated + * (for attack moves) and the target's HP is updated. However, this isn't + * made visible to the user until the resulting {@linkcode DamagePhase} + * is invoked. + */ const hitResult = !isProtected ? target.apply(user, move) : HitResult.NO_EFFECT; /** Does {@linkcode hitResult} indicate that damage was dealt to the target? */ @@ -209,44 +209,44 @@ export class MoveEffectPhase extends PokemonPhase { } /** - * If the move has no effect on the target (i.e. the target is protected or immune), - * change the logged move result to FAIL. - */ + * If the move has no effect on the target (i.e. the target is protected or immune), + * change the logged move result to FAIL. + */ if (hitResult === HitResult.NO_EFFECT) { moveHistoryEntry.result = MoveResult.FAIL; } /** Does this phase represent the invoked move's last strike? */ - const lastHit = (user.turnData.hitsLeft === 1 || !this.getTarget()?.isActive()); + const lastHit = (user.turnData.hitsLeft === 1 || !this.getFirstTarget()?.isActive()); /** - * If the user can change forms by using the invoked move, - * it only changes forms after the move's last hit - * (see Relic Song's interaction with Parental Bond when used by Meloetta). - */ + * If the user can change forms by using the invoked move, + * it only changes forms after the move's last hit + * (see Relic Song's interaction with Parental Bond when used by Meloetta). + */ if (lastHit) { this.scene.triggerPokemonFormChange(user, SpeciesFormChangePostMoveTrigger); } /** - * Create a Promise that applys *all* effects from the invoked move's MoveEffectAttrs. - * These are ordered by trigger type (see {@linkcode MoveEffectTrigger}), and each trigger - * type requires different conditions to be met with respect to the move's hit result. - */ + * Create a Promise that applys *all* effects from the invoked move's MoveEffectAttrs. + * These are ordered by trigger type (see {@linkcode MoveEffectTrigger}), and each trigger + * type requires different conditions to be met with respect to the move's hit result. + */ applyAttrs.push(new Promise(resolve => { // Apply all effects with PRE_MOVE triggers (if the target isn't immune to the move) - applyFilteredMoveAttrs((attr: MoveAttr) => attr instanceof MoveEffectAttr && attr.trigger === MoveEffectTrigger.PRE_APPLY && (!attr.firstHitOnly || firstHit) && (!attr.lastHitOnly || lastHit) && hitResult !== HitResult.NO_EFFECT, - user, target, move).then(() => { + applyFilteredMoveAttrs((attr: MoveAttr) => attr instanceof MoveEffectAttr && attr.trigger === MoveEffectTrigger.PRE_APPLY + && (!attr.firstHitOnly || firstHit) && (!attr.lastHitOnly || lastHit) && hitResult !== HitResult.NO_EFFECT, user, target, move).then(() => { // All other effects require the move to not have failed or have been cancelled to trigger if (hitResult !== HitResult.FAIL) { /** Are the move's effects tied to the first turn of a charge move? */ - const chargeEffect = !!move.getAttrs(ChargeAttr).find(ca => ca.usedChargeEffect(user, this.getTarget() ?? null, move)); + const chargeEffect = !!move.getAttrs(ChargeAttr).find(ca => ca.usedChargeEffect(user, this.getFirstTarget() ?? null, move)); /** - * If the invoked move's effects are meant to trigger during the move's "charge turn," - * ignore all effects after this point. - * Otherwise, apply all self-targeted POST_APPLY effects. - */ - Utils.executeIf(!chargeEffect, () => applyFilteredMoveAttrs((attr: MoveAttr) => attr instanceof MoveEffectAttr && attr.trigger === MoveEffectTrigger.POST_APPLY + * If the invoked move's effects are meant to trigger during the move's "charge turn," + * ignore all effects after this point. + * Otherwise, apply all self-targeted POST_APPLY effects. + */ + executeIf(!chargeEffect, () => applyFilteredMoveAttrs((attr: MoveAttr) => attr instanceof MoveEffectAttr && attr.trigger === MoveEffectTrigger.POST_APPLY && attr.selfTarget && (!attr.firstHitOnly || firstHit) && (!attr.lastHitOnly || lastHit), user, target, move)).then(() => { // All effects past this point require the move to have hit the target if (hitResult !== HitResult.NO_EFFECT) { @@ -254,21 +254,21 @@ export class MoveEffectPhase extends PokemonPhase { applyFilteredMoveAttrs((attr: MoveAttr) => attr instanceof MoveEffectAttr && (attr as MoveEffectAttr).trigger === MoveEffectTrigger.POST_APPLY && !(attr as MoveEffectAttr).selfTarget && (!attr.firstHitOnly || firstHit) && (!attr.lastHitOnly || lastHit), user, target, this.move.getMove()).then(() => { /** - * If the move hit, and the target doesn't have Shield Dust, - * apply the chance to flinch the target gained from King's Rock - */ + * If the move hit, and the target doesn't have Shield Dust, + * apply the chance to flinch the target gained from King's Rock + */ if (dealsDamage && !target.hasAbilityWithAttr(IgnoreMoveEffectsAbAttr) && !move.hitsSubstitute(user, target)) { - const flinched = new Utils.BooleanHolder(false); + const flinched = new BooleanHolder(false); user.scene.applyModifiers(FlinchChanceModifier, user.isPlayer(), user, flinched); if (flinched.value) { target.addTag(BattlerTagType.FLINCHED, undefined, this.move.moveId, user.id); } } // If the move was not protected against, apply all HIT effects - Utils.executeIf(!isProtected && !chargeEffect, () => applyFilteredMoveAttrs((attr: MoveAttr) => attr instanceof MoveEffectAttr && (attr as MoveEffectAttr).trigger === MoveEffectTrigger.HIT - && (!attr.firstHitOnly || firstHit) && (!attr.lastHitOnly || lastHit) && (!attr.firstTargetOnly || firstTarget), user, target, this.move.getMove()).then(() => { + executeIf(!isProtected && !chargeEffect, () => applyFilteredMoveAttrs((attr: MoveAttr) => attr instanceof MoveEffectAttr && (attr as MoveEffectAttr).trigger === MoveEffectTrigger.HIT + && (!attr.firstHitOnly || firstHit) && (!attr.lastHitOnly || lastHit) && (!attr.firstTargetOnly || firstTarget), user, target, this.move.getMove()).then(() => { // Apply the target's post-defend ability effects (as long as the target is active or can otherwise apply them) - return Utils.executeIf(!target.isFainted() || target.canApplyAbility(), () => applyPostDefendAbAttrs(PostDefendAbAttr, target, user, this.move.getMove(), hitResult).then(() => { + return executeIf(!target.isFainted() || target.canApplyAbility(), () => applyPostDefendAbAttrs(PostDefendAbAttr, target, user, this.move.getMove(), hitResult).then(() => { // Only apply the following effects if the move was not deflected by a substitute if (move.hitsSubstitute(user, target)) { return resolve(); @@ -286,9 +286,9 @@ export class MoveEffectPhase extends PokemonPhase { // Apply the user's post-attack ability effects applyPostAttackAbAttrs(PostAttackAbAttr, user, target, this.move.getMove(), hitResult).then(() => { /** - * If the invoked move is an attack, apply the user's chance to - * steal an item from the target granted by Grip Claw - */ + * If the invoked move is an attack, apply the user's chance to + * steal an item from the target granted by Grip Claw + */ if (this.move.getMove() instanceof AttackMove) { this.scene.applyModifiers(ContactHeldItemTransferChanceModifier, this.player, user, target); } @@ -309,7 +309,7 @@ export class MoveEffectPhase extends PokemonPhase { })); } // Apply the move's POST_TARGET effects on the move's last hit, after all targeted effects have resolved - const postTarget = (user.turnData.hitsLeft === 1 || !this.getTarget()?.isActive()) ? + const postTarget = (user.turnData.hitsLeft === 1 || !this.getFirstTarget()?.isActive()) ? applyFilteredMoveAttrs((attr: MoveAttr) => attr instanceof MoveEffectAttr && attr.trigger === MoveEffectTrigger.POST_TARGET, user, null, move) : null; @@ -340,17 +340,17 @@ export class MoveEffectPhase extends PokemonPhase { }); } - end() { + public end() { const user = this.getUserPokemon(); /** - * If this phase isn't for the invoked move's last strike, - * unshift another MoveEffectPhase for the next strike. - * Otherwise, queue a message indicating the number of times the move has struck - * (if the move has struck more than once), then apply the heal from Shell Bell - * to the user. - */ + * If this phase isn't for the invoked move's last strike, + * unshift another MoveEffectPhase for the next strike. + * Otherwise, queue a message indicating the number of times the move has struck + * (if the move has struck more than once), then apply the heal from Shell Bell + * to the user. + */ if (user) { - if (user.turnData.hitsLeft && --user.turnData.hitsLeft >= 1 && this.getTarget()?.isActive()) { + if (user.turnData.hitsLeft && --user.turnData.hitsLeft >= 1 && this.getFirstTarget()?.isActive()) { this.scene.unshiftPhase(this.getNewHitPhase()); } else { // Queue message for number of hits made by multi-move @@ -370,11 +370,11 @@ export class MoveEffectPhase extends PokemonPhase { } /** - * Resolves whether this phase's invoked move hits or misses the given target - * @param target {@linkcode Pokemon} the Pokemon targeted by the invoked move - * @returns `true` if the move does not miss the target; `false` otherwise - */ - hitCheck(target: Pokemon): boolean { + * Resolves whether this phase's invoked move hits or misses the given target + * @param target {@linkcode Pokemon} the Pokemon targeted by the invoked move + * @returns `true` if the move does not miss the target; `false` otherwise + */ + public hitCheck(target: Pokemon): boolean { // Moves targeting the user and entry hazards can't miss if ([MoveTarget.USER, MoveTarget.ENEMY_SIDE].includes(this.move.getMove().moveTarget)) { return true; @@ -425,7 +425,7 @@ export class MoveEffectPhase extends PokemonPhase { } /** Returns the {@linkcode Pokemon} using this phase's invoked move */ - getUserPokemon(): Pokemon | undefined { + public getUserPokemon(): Pokemon | undefined { if (this.battlerIndex > BattlerIndex.ENEMY_2) { return this.scene.getPokemonById(this.battlerIndex) ?? undefined; } @@ -433,20 +433,20 @@ export class MoveEffectPhase extends PokemonPhase { } /** Returns an array of all {@linkcode Pokemon} targeted by this phase's invoked move */ - getTargets(): Pokemon[] { + public getTargets(): Pokemon[] { return this.scene.getField(true).filter(p => this.targets.indexOf(p.getBattlerIndex()) > -1); } /** Returns the first target of this phase's invoked move */ - getTarget(): Pokemon | undefined { + public getFirstTarget(): Pokemon { return this.getTargets()[0]; } /** - * Removes the given {@linkcode Pokemon} from this phase's target list - * @param target {@linkcode Pokemon} the Pokemon to be removed - */ - removeTarget(target: Pokemon): void { + * Removes the given {@linkcode Pokemon} from this phase's target list + * @param target {@linkcode Pokemon} the Pokemon to be removed + */ + private removeTarget(target: Pokemon): void { const targetIndex = this.targets.findIndex(ind => ind === target.getBattlerIndex()); if (targetIndex !== -1) { this.targets.splice(this.targets.findIndex(ind => ind === target.getBattlerIndex()), 1); @@ -454,27 +454,26 @@ export class MoveEffectPhase extends PokemonPhase { } /** - * Prevents subsequent strikes of this phase's invoked move from occurring - * @param target {@linkcode Pokemon} if defined, only stop subsequent - * strikes against this Pokemon - */ - stopMultiHit(target?: Pokemon): void { - /** If given a specific target, remove the target from subsequent strikes */ + * Prevents subsequent strikes of this phase's invoked move from occurring + * @param target {@linkcode Pokemon} if defined, only stop subsequent + * strikes against this Pokemon + */ + public stopMultiHit(target?: Pokemon): void { + // If given a specific target, remove the target from subsequent strikes if (target) { this.removeTarget(target); } - /** - * If no target specified, or the specified target was the last of this move's - * targets, completely cancel all subsequent strikes. - */ - if (!target || this.targets.length === 0 ) { - this.getUserPokemon()!.turnData.hitCount = 1; // TODO: is the bang correct here? - this.getUserPokemon()!.turnData.hitsLeft = 1; // TODO: is the bang correct here? + // If no target specified, or the specified target was the last of this move's + // targets, completely cancel all subsequent strikes. + const user = this.getUserPokemon(); + if (user && (!target || this.targets.length === 0)) { + user.turnData.hitCount = 1; + user.turnData.hitsLeft = 1; } } /** Returns a new MoveEffectPhase with the same properties as this phase */ - getNewHitPhase() { + private getNewHitPhase() { return new MoveEffectPhase(this.scene, this.battlerIndex, this.targets, this.move); } } diff --git a/src/test/abilities/serene_grace.test.ts b/src/test/abilities/serene_grace.test.ts index e06288b9de9..8d38b2d15c7 100644 --- a/src/test/abilities/serene_grace.test.ts +++ b/src/test/abilities/serene_grace.test.ts @@ -57,7 +57,7 @@ describe("Abilities - Serene Grace", () => { const chance = new Utils.IntegerHolder(move.chance); console.log(move.chance + " Their ability is " + phase.getUserPokemon()!.getAbility().name); - applyAbAttrs(MoveEffectChanceMultiplierAbAttr, phase.getUserPokemon()!, null, false, chance, move, phase.getTarget(), false); + applyAbAttrs(MoveEffectChanceMultiplierAbAttr, phase.getUserPokemon()!, null, false, chance, move, phase.getFirstTarget(), false); expect(chance.value).toBe(30); }, 20000); @@ -83,7 +83,7 @@ describe("Abilities - Serene Grace", () => { expect(move.id).toBe(Moves.AIR_SLASH); const chance = new Utils.IntegerHolder(move.chance); - applyAbAttrs(MoveEffectChanceMultiplierAbAttr, phase.getUserPokemon()!, null, false, chance, move, phase.getTarget(), false); + applyAbAttrs(MoveEffectChanceMultiplierAbAttr, phase.getUserPokemon()!, null, false, chance, move, phase.getFirstTarget(), false); expect(chance.value).toBe(60); }, 20000); diff --git a/src/test/abilities/sheer_force.test.ts b/src/test/abilities/sheer_force.test.ts index 69b47e1eaae..a874d20c18c 100644 --- a/src/test/abilities/sheer_force.test.ts +++ b/src/test/abilities/sheer_force.test.ts @@ -59,8 +59,8 @@ describe("Abilities - Sheer Force", () => { const power = new Utils.IntegerHolder(move.power); const chance = new Utils.IntegerHolder(move.chance); - applyAbAttrs(MoveEffectChanceMultiplierAbAttr, phase.getUserPokemon()!, null, false, chance, move, phase.getTarget(), false); - applyPreAttackAbAttrs(MovePowerBoostAbAttr, phase.getUserPokemon()!, phase.getTarget()!, move, false, power); + applyAbAttrs(MoveEffectChanceMultiplierAbAttr, phase.getUserPokemon()!, null, false, chance, move, phase.getFirstTarget(), false); + applyPreAttackAbAttrs(MovePowerBoostAbAttr, phase.getUserPokemon()!, phase.getFirstTarget()!, move, false, power); expect(chance.value).toBe(0); expect(power.value).toBe(move.power * 5461 / 4096); @@ -92,8 +92,8 @@ describe("Abilities - Sheer Force", () => { const power = new Utils.IntegerHolder(move.power); const chance = new Utils.IntegerHolder(move.chance); - applyAbAttrs(MoveEffectChanceMultiplierAbAttr, phase.getUserPokemon()!, null, false, chance, move, phase.getTarget(), false); - applyPreAttackAbAttrs(MovePowerBoostAbAttr, phase.getUserPokemon()!, phase.getTarget()!, move, false, power); + applyAbAttrs(MoveEffectChanceMultiplierAbAttr, phase.getUserPokemon()!, null, false, chance, move, phase.getFirstTarget(), false); + applyPreAttackAbAttrs(MovePowerBoostAbAttr, phase.getUserPokemon()!, phase.getFirstTarget()!, move, false, power); expect(chance.value).toBe(-1); expect(power.value).toBe(move.power); @@ -125,8 +125,8 @@ describe("Abilities - Sheer Force", () => { const power = new Utils.IntegerHolder(move.power); const chance = new Utils.IntegerHolder(move.chance); - applyAbAttrs(MoveEffectChanceMultiplierAbAttr, phase.getUserPokemon()!, null, false, chance, move, phase.getTarget(), false); - applyPreAttackAbAttrs(MovePowerBoostAbAttr, phase.getUserPokemon()!, phase.getTarget()!, move, false, power); + applyAbAttrs(MoveEffectChanceMultiplierAbAttr, phase.getUserPokemon()!, null, false, chance, move, phase.getFirstTarget(), false); + applyPreAttackAbAttrs(MovePowerBoostAbAttr, phase.getUserPokemon()!, phase.getFirstTarget()!, move, false, power); expect(chance.value).toBe(-1); expect(power.value).toBe(move.power); @@ -160,7 +160,7 @@ describe("Abilities - Sheer Force", () => { const power = new Utils.IntegerHolder(move.power); const chance = new Utils.IntegerHolder(move.chance); const user = phase.getUserPokemon()!; - const target = phase.getTarget()!; + const target = phase.getFirstTarget()!; const opponentType = target.getTypes()[0]; applyAbAttrs(MoveEffectChanceMultiplierAbAttr, user, null, false, chance, move, target, false); diff --git a/src/test/abilities/shield_dust.test.ts b/src/test/abilities/shield_dust.test.ts index 8a0b769827d..a1ceff29521 100644 --- a/src/test/abilities/shield_dust.test.ts +++ b/src/test/abilities/shield_dust.test.ts @@ -57,8 +57,8 @@ describe("Abilities - Shield Dust", () => { expect(move.id).toBe(Moves.AIR_SLASH); const chance = new Utils.IntegerHolder(move.chance); - applyAbAttrs(MoveEffectChanceMultiplierAbAttr, phase.getUserPokemon()!, null, false, chance, move, phase.getTarget(), false); - applyPreDefendAbAttrs(IgnoreMoveEffectsAbAttr, phase.getTarget()!, phase.getUserPokemon()!, null, null, false, chance); + applyAbAttrs(MoveEffectChanceMultiplierAbAttr, phase.getUserPokemon()!, null, false, chance, move, phase.getFirstTarget(), false); + applyPreDefendAbAttrs(IgnoreMoveEffectsAbAttr, phase.getFirstTarget()!, phase.getUserPokemon()!, null, null, false, chance); expect(chance.value).toBe(0); }, 20000); diff --git a/src/test/moves/dynamax_cannon.test.ts b/src/test/moves/dynamax_cannon.test.ts index 6ac0befdb36..43113243554 100644 --- a/src/test/moves/dynamax_cannon.test.ts +++ b/src/test/moves/dynamax_cannon.test.ts @@ -81,7 +81,7 @@ describe("Moves - Dynamax Cannon", () => { const phase = game.scene.getCurrentPhase() as MoveEffectPhase; expect(phase.move.moveId).toBe(dynamaxCannon.id); // Force level cap to be 100 - vi.spyOn(phase.getTarget()!.scene, "getMaxExpLevel").mockReturnValue(100); + vi.spyOn(phase.getFirstTarget()!.scene, "getMaxExpLevel").mockReturnValue(100); await game.phaseInterceptor.to(DamagePhase, false); expect(dynamaxCannon.calculateBattlePower).toHaveLastReturnedWith(120); }, 20000); @@ -98,7 +98,7 @@ describe("Moves - Dynamax Cannon", () => { const phase = game.scene.getCurrentPhase() as MoveEffectPhase; expect(phase.move.moveId).toBe(dynamaxCannon.id); // Force level cap to be 100 - vi.spyOn(phase.getTarget()!.scene, "getMaxExpLevel").mockReturnValue(100); + vi.spyOn(phase.getFirstTarget()!.scene, "getMaxExpLevel").mockReturnValue(100); await game.phaseInterceptor.to(DamagePhase, false); expect(dynamaxCannon.calculateBattlePower).toHaveLastReturnedWith(140); }, 20000); @@ -115,7 +115,7 @@ describe("Moves - Dynamax Cannon", () => { const phase = game.scene.getCurrentPhase() as MoveEffectPhase; expect(phase.move.moveId).toBe(dynamaxCannon.id); // Force level cap to be 100 - vi.spyOn(phase.getTarget()!.scene, "getMaxExpLevel").mockReturnValue(100); + vi.spyOn(phase.getFirstTarget()!.scene, "getMaxExpLevel").mockReturnValue(100); await game.phaseInterceptor.to(DamagePhase, false); expect(dynamaxCannon.calculateBattlePower).toHaveLastReturnedWith(160); }, 20000); @@ -132,7 +132,7 @@ describe("Moves - Dynamax Cannon", () => { const phase = game.scene.getCurrentPhase() as MoveEffectPhase; expect(phase.move.moveId).toBe(dynamaxCannon.id); // Force level cap to be 100 - vi.spyOn(phase.getTarget()!.scene, "getMaxExpLevel").mockReturnValue(100); + vi.spyOn(phase.getFirstTarget()!.scene, "getMaxExpLevel").mockReturnValue(100); await game.phaseInterceptor.to(DamagePhase, false); expect(dynamaxCannon.calculateBattlePower).toHaveLastReturnedWith(180); }, 20000); @@ -149,7 +149,7 @@ describe("Moves - Dynamax Cannon", () => { const phase = game.scene.getCurrentPhase() as MoveEffectPhase; expect(phase.move.moveId).toBe(dynamaxCannon.id); // Force level cap to be 100 - vi.spyOn(phase.getTarget()!.scene, "getMaxExpLevel").mockReturnValue(100); + vi.spyOn(phase.getFirstTarget()!.scene, "getMaxExpLevel").mockReturnValue(100); await game.phaseInterceptor.to(DamagePhase, false); expect(dynamaxCannon.calculateBattlePower).toHaveLastReturnedWith(200); }, 20000);