From 6cfb26c5289c91e3b2b4c195d12092b29ba3aa39 Mon Sep 17 00:00:00 2001 From: Bertie690 Date: Wed, 6 Aug 2025 18:43:24 -0400 Subject: [PATCH] Moved methods around to be in sequential order --- src/phases/move-effect-phase.ts | 665 +++++++++++++++---------------- src/phases/move-reflect-phase.ts | 1 + 2 files changed, 327 insertions(+), 339 deletions(-) diff --git a/src/phases/move-effect-phase.ts b/src/phases/move-effect-phase.ts index 6a43ae73923..a9b2059bfbe 100644 --- a/src/phases/move-effect-phase.ts +++ b/src/phases/move-effect-phase.ts @@ -87,110 +87,11 @@ export class MoveEffectPhase extends PokemonPhase { this.hitChecks = Array(this.targets.length).fill([HitCheckResult.PENDING, 0]); } - /** - * Compute targets and the results of hit checks of the invoked move against all targets, - * organized by battler index. - * - * **This is *not* a pure function**; it has the following side effects - * - `this.hitChecks` - The results of the hit checks against each target - * - `this.moveHistoryEntry` - Sets success or failure based on the hit check results - * - user.turnData.hitCount and user.turnData.hitsLeft - Both set to 1 if the - * move was unsuccessful against all targets - * - * @returns The targets of the invoked move - * @see {@linkcode hitCheck} - */ - private conductHitChecks(user: Pokemon, fieldMove: boolean): Pokemon[] { - /** All Pokemon targeted by this phase's invoked move */ - /** Whether any hit check ended in a success */ - let anySuccess = false; - /** Whether the attack missed all of its targets */ - let allMiss = true; - - let targets = this.getTargets(); - - // For field targeted moves, we only look for the first target that may magic bounce - - for (const [i, target] of targets.entries()) { - const hitCheck = this.hitCheck(target); - // If the move bounced and was a field targeted move, - // then immediately stop processing other targets - if (fieldMove && hitCheck[0] === HitCheckResult.REFLECTED) { - targets = [target]; - this.hitChecks = [hitCheck]; - break; - } - if (hitCheck[0] === HitCheckResult.HIT) { - anySuccess = true; - } else { - allMiss ||= hitCheck[0] === HitCheckResult.MISS; - } - this.hitChecks[i] = hitCheck; - } - - if (anySuccess) { - this.moveHistoryEntry.result = MoveResult.SUCCESS; - } else { - user.turnData.hitCount = 1; - user.turnData.hitsLeft = 1; - this.moveHistoryEntry.result = allMiss ? MoveResult.MISS : MoveResult.FAIL; - } - - return targets; - } - - /** - * Apply the move to each of the resolved targets. - * @param targets - The resolved set of targets of the move - * @throws Error if there was an unexpected hit check result - */ - private applyToTargets(user: Pokemon, targets: Pokemon[]): void { - let firstHit = true; - for (const [i, target] of targets.entries()) { - const [hitCheckResult, effectiveness] = this.hitChecks[i]; - switch (hitCheckResult) { - case HitCheckResult.HIT: - this.applyMoveEffects(target, effectiveness, firstHit); - firstHit = false; - if (isFieldTargeted(this.move)) { - // Stop processing other targets if the move is a field move - return; - } - break; - // biome-ignore lint/suspicious/noFallthroughSwitchClause: The fallthrough is intentional - case HitCheckResult.NO_EFFECT: - globalScene.phaseManager.queueMessage( - i18next.t(this.move.id === MoveId.SHEER_COLD ? "battle:hitResultImmune" : "battle:hitResultNoEffect", { - pokemonName: getPokemonNameWithAffix(target), - }), - ); - case HitCheckResult.NO_EFFECT_NO_MESSAGE: - case HitCheckResult.PROTECTED: - case HitCheckResult.TARGET_NOT_ON_FIELD: - applyMoveAttrs("NoEffectAttr", user, target, this.move); - break; - case HitCheckResult.MISS: - globalScene.phaseManager.queueMessage( - i18next.t("battle:attackMissed", { pokemonNameWithAffix: getPokemonNameWithAffix(target) }), - ); - applyMoveAttrs("MissEffectAttr", user, target, this.move); - break; - case HitCheckResult.REFLECTED: - globalScene.phaseManager.appendNewToPhase("MoveEndPhase", "MoveReflectPhase", target, user, this.move); - break; - case HitCheckResult.PENDING: - case HitCheckResult.ERROR: - throw new Error("Unexpected hit check result"); - } - } - } - public override start(): void { super.start(); /** The Pokemon using this phase's invoked move */ const user = this.getUserPokemon(); - if (!user) { super.end(); return; @@ -285,6 +186,58 @@ export class MoveEffectPhase extends PokemonPhase { this.postAnimCallback(user, targets); } + /** + * Compute targets and the results of hit checks of the invoked move against all targets, + * organized by battler index. + * + * **This is *not* a pure function**; it has the following side effects + * - `this.hitChecks` - The results of the hit checks against each target + * - `this.moveHistoryEntry` - Sets success or failure based on the hit check results + * - user.turnData.hitCount and user.turnData.hitsLeft - Both set to 1 if the + * move was unsuccessful against all targets + * + * @returns The targets of the invoked move + * @see {@linkcode hitCheck} + */ + private conductHitChecks(user: Pokemon, fieldMove: boolean): Pokemon[] { + /** All Pokemon targeted by this phase's invoked move */ + /** Whether any hit check ended in a success */ + let anySuccess = false; + /** Whether the attack missed all of its targets */ + let allMiss = true; + + let targets = this.getTargets(); + + // For field targeted moves, we only look for the first target that may magic bounce + + for (const [i, target] of targets.entries()) { + const hitCheck = this.hitCheck(target); + // If the move bounced and was a field targeted move, + // then immediately stop processing other targets + if (fieldMove && hitCheck[0] === HitCheckResult.REFLECTED) { + targets = [target]; + this.hitChecks = [hitCheck]; + break; + } + if (hitCheck[0] === HitCheckResult.HIT) { + anySuccess = true; + } else { + allMiss ||= hitCheck[0] === HitCheckResult.MISS; + } + this.hitChecks[i] = hitCheck; + } + + if (anySuccess) { + this.moveHistoryEntry.result = MoveResult.SUCCESS; + } else { + user.turnData.hitCount = 1; + user.turnData.hitsLeft = 1; + this.moveHistoryEntry.result = allMiss ? MoveResult.MISS : MoveResult.FAIL; + } + + return targets; + } + /** * Callback to be called after the move animation is played */ @@ -316,121 +269,52 @@ export class MoveEffectPhase extends PokemonPhase { this.end(); } - public override end(): void { - const user = this.getUserPokemon(); - if (!user) { - super.end(); - return; - } - - /** - * If this phase isn't for the invoked move's last strike (and we still have something to hit), - * unshift another MoveEffectPhase for the next strike before ending this phase. - */ - if (--user.turnData.hitsLeft >= 1 && this.getFirstTarget()) { - this.addNextHitPhase(); - super.end(); - return; - } - - /** - * All hits of the move have resolved by now. - * Queue message for multi-strike moves before applying Shell Bell heals & proccing Dancer-like effects. - */ - const hitsTotal = user.turnData.hitCount - Math.max(user.turnData.hitsLeft, 0); - if (hitsTotal > 1 || user.turnData.hitsLeft > 0) { - // Queue message if multiple hits occurred or were slated to occur (such as a Triple Axel miss) - globalScene.phaseManager.queueMessage(i18next.t("battle:attackHitsCount", { count: hitsTotal })); - } - - globalScene.applyModifiers(HitHealModifier, this.player, user); - this.getTargets().forEach(target => { - target.turnData.moveEffectiveness = null; - }); - super.end(); - } - /** - * Applies reactive effects that occur when a Pokémon is hit. - * (i.e. Effect Spore, Disguise, Liquid Ooze, Beak Blast) - * @param user - The {@linkcode Pokemon} using this phase's invoked move - * @param target - {@linkcode Pokemon} the current target of this phase's invoked move - * @param hitResult - The {@linkcode HitResult} of the attempted move - * @param wasCritical - `true` if the move was a critical hit + * Apply the move to each of the resolved targets. + * @param targets - The resolved set of targets of the move + * @throws Error if there was an unexpected hit check result */ - protected applyOnGetHitAbEffects(user: Pokemon, target: Pokemon, hitResult: HitResult, wasCritical = false): void { - const params = { pokemon: target, opponent: user, move: this.move, hitResult }; - applyAbAttrs("PostDefendAbAttr", params); - - if (wasCritical) { - applyAbAttrs("PostReceiveCritStatStageChangeAbAttr", params); - } - target.lapseTags(BattlerTagLapseType.AFTER_HIT); - } - - /** - * Handles checking for and applying Flinches - * @param user - The {@linkcode Pokemon} using this phase's invoked move - * @param target - {@linkcode Pokemon} the current target of this phase's invoked move - * @param dealsDamage - `true` if the attempted move successfully dealt damage - */ - protected applyHeldItemFlinchCheck(user: Pokemon, target: Pokemon, dealsDamage: boolean): void { - if (this.move.hasAttr("FlinchAttr")) { - return; - } - - if ( - dealsDamage && - !target.hasAbilityWithAttr("IgnoreMoveEffectsAbAttr") && - !this.move.hitsSubstitute(user, target) - ) { - const flinched = new BooleanHolder(false); - globalScene.applyModifiers(FlinchChanceModifier, user.isPlayer(), user, flinched); - if (flinched.value) { - target.addTag(BattlerTagType.FLINCHED, undefined, this.move.id, user.id); + private applyToTargets(user: Pokemon, targets: Pokemon[]): void { + let firstHit = true; + for (const [i, target] of targets.entries()) { + const [hitCheckResult, effectiveness] = this.hitChecks[i]; + switch (hitCheckResult) { + case HitCheckResult.HIT: + this.applyMoveEffects(target, effectiveness, firstHit); + firstHit = false; + if (isFieldTargeted(this.move)) { + // Stop processing other targets if the move is a field move + return; + } + break; + // biome-ignore lint/suspicious/noFallthroughSwitchClause: The fallthrough is intentional + case HitCheckResult.NO_EFFECT: + globalScene.phaseManager.queueMessage( + i18next.t(this.move.id === MoveId.SHEER_COLD ? "battle:hitResultImmune" : "battle:hitResultNoEffect", { + pokemonName: getPokemonNameWithAffix(target), + }), + ); + case HitCheckResult.NO_EFFECT_NO_MESSAGE: + case HitCheckResult.PROTECTED: + case HitCheckResult.TARGET_NOT_ON_FIELD: + applyMoveAttrs("NoEffectAttr", user, target, this.move); + break; + case HitCheckResult.MISS: + globalScene.phaseManager.queueMessage( + i18next.t("battle:attackMissed", { pokemonNameWithAffix: getPokemonNameWithAffix(target) }), + ); + applyMoveAttrs("MissEffectAttr", user, target, this.move); + break; + case HitCheckResult.REFLECTED: + globalScene.phaseManager.appendNewToPhase("MoveEndPhase", "MoveReflectPhase", target, user, this.move); + break; + case HitCheckResult.PENDING: + case HitCheckResult.ERROR: + throw new Error("Unexpected hit check result"); } } } - /** Return whether the target is protected by protect or a relevant conditional protection - * @param user - The {@linkcode Pokemon} using this phase's invoked move - * @param target - {@linkcode Pokemon} the target to check for protection - * @param move - The {@linkcode Move} being used - * @returns Whether the pokemon was protected - */ - private protectedCheck(user: Pokemon, target: Pokemon): boolean { - /** 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 BooleanHolder(false); - /** Does the applied conditional protection bypass Protect-ignoring effects? */ - 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.isAllyTarget()) { - globalScene.arena.applyTagsForSide( - ConditionalProtectTag, - targetSide, - false, - hasConditionalProtectApplied, - user, - target, - this.move.id, - bypassIgnoreProtect, - ); - } - - // TODO: Break up this chunky boolean to make it more palatable - return ( - ![MoveTarget.ENEMY_SIDE, MoveTarget.BOTH_SIDES].includes(this.move.moveTarget) && - (bypassIgnoreProtect.value || !this.move.doesFlagEffectApply({ flag: MoveFlags.IGNORE_PROTECT, user, target })) && - (hasConditionalProtectApplied.value || - (!target.findTags(t => t instanceof DamageProtectedTag).length && - target.findTags(t => t instanceof ProtectedTag).some(t => target.lapseTag(t.tagType))) || - (this.move.category !== MoveCategory.STATUS && - target.findTags(t => t instanceof DamageProtectedTag).some(t => target.lapseTag(t.tagType)))) - ); - } - /** * Conduct the hit check and type effectiveness for this move against the target * @@ -451,10 +335,6 @@ export class MoveEffectPhase extends PokemonPhase { const user = this.getUserPokemon(); const move = this.move; - if (!user) { - return [HitCheckResult.ERROR, 0]; - } - // Moves targeting the user bypass all checks if (move.moveTarget === MoveTarget.USER) { return [HitCheckResult.HIT, 1]; @@ -487,8 +367,8 @@ export class MoveEffectPhase extends PokemonPhase { return [HitCheckResult.PROTECTED, 0]; } - // Check for magic bounce - if (isMoveReflectableBy(move, target, this.useMode)) { + // Reflected moves cannot be reflected again + if (isMoveReflectableBy(this.move, target, this.useMode)) { return [HitCheckResult.REFLECTED, 0]; } @@ -559,9 +439,6 @@ export class MoveEffectPhase extends PokemonPhase { */ public checkBypassAccAndInvuln(target: Pokemon) { const user = this.getUserPokemon(); - if (!user) { - return false; - } if (user.hasAbilityWithAttr("AlwaysHitAbAttr") || target.hasAbilityWithAttr("AlwaysHitAbAttr")) { return true; } @@ -593,79 +470,43 @@ export class MoveEffectPhase extends PokemonPhase { return move.getAttrs("HitsTagAttr").some(hta => hta.tagType === semiInvulnerableTag.tagType); } - /** @returns The {@linkcode Pokemon} using this phase's invoked move */ - public getUserPokemon(): Pokemon | null { - // TODO: Make this purely a battler index - if (this.battlerIndex > BattlerIndex.ENEMY_2) { - return globalScene.getPokemonById(this.battlerIndex); - } - return (this.player ? globalScene.getPlayerField() : globalScene.getEnemyField())[this.fieldIndex]; - } - /** - * @returns An array of {@linkcode Pokemon} that are: - * - On-field and active - * - Non-fainted - * - Targeted by this phase's invoked move + * Check whether the target is protected by protect or a relevant conditional protection. + * @param user - The {@linkcode Pokemon} using this phase's invoked move + * @param target - The target {@linkcode Pokemon} to check for protection + * @returns Whether the target was protected */ - public getTargets(): Pokemon[] { - return globalScene.getField(true).filter(p => this.targets.indexOf(p.getBattlerIndex()) > -1); - } - - /** @returns The first active, non-fainted target of this phase's invoked move. */ - public getFirstTarget(): Pokemon | undefined { - return this.getTargets()[0]; - } - - /** - * Removes the given {@linkcode Pokemon} from this phase's target list - * @param target - The {@linkcode Pokemon} to be removed - */ - protected removeTarget(target: Pokemon): void { - const targetIndex = this.targets.indexOf(target.getBattlerIndex()); - if (targetIndex !== -1) { - this.targets.splice(this.targets.indexOf(target.getBattlerIndex()), 1); + private protectedCheck(user: Pokemon, target: Pokemon): boolean { + /** 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 BooleanHolder(false); + /** Does the applied conditional protection bypass Protect-ignoring effects? */ + 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.isAllyTarget()) { + globalScene.arena.applyTagsForSide( + ConditionalProtectTag, + targetSide, + false, + hasConditionalProtectApplied, + user, + target, + this.move.id, + bypassIgnoreProtect, + ); } - } - /** - * Prevents subsequent strikes of this phase's invoked move from occurring - * @param target - If defined, only stop subsequent strikes against this {@linkcode Pokemon} - */ - public stopMultiHit(target?: Pokemon): void { - // If given a specific target, remove the target from subsequent strikes - if (target) { - this.removeTarget(target); - } - const user = this.getUserPokemon(); - if (!user) { - return; - } - // 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) { - user.turnData.hitCount = 1; - user.turnData.hitsLeft = 1; - } - } - - /** - * Unshifts a new `MoveEffectPhase` with the same properties as this phase. - * Used to queue the next hit of multi-strike moves. - */ - protected addNextHitPhase(): void { - globalScene.phaseManager.unshiftNew("MoveEffectPhase", this.battlerIndex, this.targets, this.move, this.useMode); - } - - /** Removes all substitutes that were broken by this phase's invoked move */ - protected updateSubstitutes(): void { - const targets = this.getTargets(); - for (const target of targets) { - const substitute = target.getTag(SubstituteTag); - if (substitute && substitute.hp <= 0) { - target.lapseTag(BattlerTagType.SUBSTITUTE); - } - } + // TODO: Break up this chunky boolean to make it more palatable + return ( + ![MoveTarget.ENEMY_SIDE, MoveTarget.BOTH_SIDES].includes(this.move.moveTarget) && + (bypassIgnoreProtect.value || !this.move.doesFlagEffectApply({ flag: MoveFlags.IGNORE_PROTECT, user, target })) && + (hasConditionalProtectApplied.value || + (!target.findTags(t => t instanceof DamageProtectedTag).length && + target.findTags(t => t instanceof ProtectedTag).some(t => target.lapseTag(t.tagType))) || + (this.move.category !== MoveCategory.STATUS && + target.findTags(t => t instanceof DamageProtectedTag).some(t => target.lapseTag(t.tagType)))) + ); } /** @@ -713,9 +554,6 @@ export class MoveEffectPhase extends PokemonPhase { */ protected applyMoveEffects(target: Pokemon, effectiveness: TypeDamageMultiplier, firstTarget: boolean): void { const user = this.getUserPokemon(); - if (isNullOrUndefined(user)) { - return; - } this.triggerMoveEffects(MoveEffectTrigger.PRE_APPLY, user, target); @@ -739,7 +577,33 @@ export class MoveEffectPhase extends PokemonPhase { } /** - * Sub-method of for {@linkcode applyMoveEffects} that applies damage to the target. + * Apply the result of this phase's move to the given target + * @param user - The {@linkcode Pokemon} using this phase's invoked move + * @param target - The {@linkcode Pokemon} struck by the move + * @param effectiveness - The effectiveness of the move against the target + */ + protected applyMove(user: Pokemon, target: Pokemon, effectiveness: TypeDamageMultiplier): [HitResult, boolean] { + const moveCategory = user.getMoveCategory(target, this.move); + + if (moveCategory === MoveCategory.STATUS) { + return [HitResult.STATUS, false]; + } + + const result = this.applyMoveDamage(user, target, effectiveness); + + if (user.turnData.hitsLeft === 1 || target.isFainted()) { + this.queueHitResultMessage(result[0]); + } + + if (target.isFainted()) { + this.onFaintTarget(user, target); + } + + return result; + } + + /** + * Sub-method of {@linkcode applyMove} that applies damage to the target. * * @param user - The {@linkcode Pokemon} using this phase's invoked move * @param target - The {@linkcode Pokemon} targeted by the move @@ -837,6 +701,29 @@ export class MoveEffectPhase extends PokemonPhase { return [result, isCritical]; } + /** + * Sub-method of {@linkcode applyMove} that queues the hit-result message + * on the final strike of the move against a target + * @param result - The {@linkcode HitResult} of the move + */ + protected queueHitResultMessage(result: HitResult) { + let msg: string | undefined; + switch (result) { + case HitResult.SUPER_EFFECTIVE: + msg = i18next.t("battle:hitResultSuperEffective"); + break; + case HitResult.NOT_VERY_EFFECTIVE: + msg = i18next.t("battle:hitResultNotVeryEffective"); + break; + case HitResult.ONE_HIT_KO: + msg = i18next.t("battle:hitResultOneHitKO"); + break; + } + if (msg) { + globalScene.phaseManager.queueMessage(msg); + } + } + /** * Sub-method of {@linkcode applyMove} that handles the event of a target fainting. * @param user - The {@linkcode Pokemon} using this phase's invoked move @@ -861,55 +748,7 @@ export class MoveEffectPhase extends PokemonPhase { } /** - * Sub-method of {@linkcode applyMove} that queues the hit-result message - * on the final strike of the move against a target - * @param result - The {@linkcode HitResult} of the move - */ - protected queueHitResultMessage(result: HitResult) { - let msg: string | undefined; - switch (result) { - case HitResult.SUPER_EFFECTIVE: - msg = i18next.t("battle:hitResultSuperEffective"); - break; - case HitResult.NOT_VERY_EFFECTIVE: - msg = i18next.t("battle:hitResultNotVeryEffective"); - break; - case HitResult.ONE_HIT_KO: - msg = i18next.t("battle:hitResultOneHitKO"); - break; - } - if (msg) { - globalScene.phaseManager.queueMessage(msg); - } - } - - /** Apply the result of this phase's move to the given target - * @param user - The {@linkcode Pokemon} using this phase's invoked move - * @param target - The {@linkcode Pokemon} struck by the move - * @param effectiveness - The effectiveness of the move against the target - */ - protected applyMove(user: Pokemon, target: Pokemon, effectiveness: TypeDamageMultiplier): [HitResult, boolean] { - const moveCategory = user.getMoveCategory(target, this.move); - - if (moveCategory === MoveCategory.STATUS) { - return [HitResult.STATUS, false]; - } - - const result = this.applyMoveDamage(user, target, effectiveness); - - if (user.turnData.hitsLeft === 1 || target.isFainted()) { - this.queueHitResultMessage(result[0]); - } - - if (target.isFainted()) { - this.onFaintTarget(user, target); - } - - return result; - } - - /** - * Applies all effects aimed at the move's target. + * Sub-method of {@linkcode applyMovetEffects} that applies all effects aimed at the move's target. * To be used when the target is successfully and directly hit by the move. * @param user - The {@linkcode Pokemon} using the move * @param target - The {@linkcode Pokemon} targeted by the move @@ -947,4 +786,152 @@ export class MoveEffectPhase extends PokemonPhase { globalScene.applyModifiers(ContactHeldItemTransferChanceModifier, this.player, user, target); } } + + /** + * Sub-method of {@linkcode applyOnTargetEffects} that applies reactive effects that occur when a Pokémon is hit. + * (i.e. Effect Spore, Disguise, Liquid Ooze, Beak Blast) + * @param user - The {@linkcode Pokemon} using this phase's invoked move + * @param target - {@linkcode Pokemon} the current target of this phase's invoked move + * @param hitResult - The {@linkcode HitResult} of the attempted move + * @param wasCritical - `true` if the move was a critical hit + */ + protected applyOnGetHitAbEffects(user: Pokemon, target: Pokemon, hitResult: HitResult, wasCritical = false): void { + const params = { pokemon: target, opponent: user, move: this.move, hitResult }; + applyAbAttrs("PostDefendAbAttr", params); + + if (wasCritical) { + applyAbAttrs("PostReceiveCritStatStageChangeAbAttr", params); + } + target.lapseTags(BattlerTagLapseType.AFTER_HIT); + } + + /** + * Sub-method of {@linkcode applyOnTargetEffects} that handles checking for and applying flinches. + * @param user - The {@linkcode Pokemon} using this phase's invoked move + * @param target - {@linkcode Pokemon} the current target of this phase's invoked move + * @param dealsDamage - `true` if the attempted move successfully dealt damage + */ + protected applyHeldItemFlinchCheck(user: Pokemon, target: Pokemon, dealsDamage: boolean): void { + if (this.move.hasAttr("FlinchAttr")) { + return; + } + + if ( + dealsDamage && + !target.hasAbilityWithAttr("IgnoreMoveEffectsAbAttr") && + !this.move.hitsSubstitute(user, target) + ) { + const flinched = new BooleanHolder(false); + globalScene.applyModifiers(FlinchChanceModifier, user.isPlayer(), user, flinched); + if (flinched.value) { + target.addTag(BattlerTagType.FLINCHED, undefined, this.move.id, user.id); + } + } + } + + public override end(): void { + const user = this.getUserPokemon(); + + /** + * If this phase isn't for the invoked move's last strike (and we still have something to hit), + * unshift another MoveEffectPhase for the next strike before ending this phase. + */ + if (--user.turnData.hitsLeft >= 1 && this.getFirstTarget()) { + this.addNextHitPhase(); + super.end(); + return; + } + + /** + * All hits of the move have resolved by now. + * Queue message for multi-strike moves before applying Shell Bell heals & proccing Dancer-like effects. + */ + const hitsTotal = user.turnData.hitCount - Math.max(user.turnData.hitsLeft, 0); + if (hitsTotal > 1 || user.turnData.hitsLeft > 0) { + // Queue message if multiple hits occurred or were slated to occur (such as a Triple Axel miss) + globalScene.phaseManager.queueMessage(i18next.t("battle:attackHitsCount", { count: hitsTotal })); + } + + globalScene.applyModifiers(HitHealModifier, this.player, user); + this.getTargets().forEach(target => { + target.turnData.moveEffectiveness = null; + }); + super.end(); + } + + // #region Helpers + + /** + * @returns The {@linkcode Pokemon} using this phase's invoked move. + * Is never null during the move execution itself, as {@linkcode start} ends the phase immediately if a source is missing. + * @todo Delete in favor of {@linkcode PokemonPhase.getPokemon} + */ + public getUserPokemon(): Pokemon { + return super.getPokemon()!; + } + + /** + * @returns An array of {@linkcode Pokemon} that are: + * - On-field and active + * - Non-fainted + * - Targeted by this phase's invoked move + */ + public getTargets(): Pokemon[] { + return globalScene.getField(true).filter(p => this.targets.indexOf(p.getBattlerIndex()) > -1); + } + + /** @returns The first active, non-fainted target of this phase's invoked move. */ + public getFirstTarget(): Pokemon | undefined { + return this.getTargets()[0]; + } + + /** + * Removes the given {@linkcode Pokemon} from this phase's target list + * @param target - The {@linkcode Pokemon} to be removed + */ + protected removeTarget(target: Pokemon): void { + const targetIndex = this.targets.indexOf(target.getBattlerIndex()); + if (targetIndex !== -1) { + this.targets.splice(this.targets.indexOf(target.getBattlerIndex()), 1); + } + } + + /** + * Prevents subsequent strikes of this phase's invoked move from occurring + * @param target - If defined, only stop subsequent strikes against this {@linkcode Pokemon} + */ + public stopMultiHit(target?: Pokemon): void { + // If given a specific target, remove the target from subsequent strikes + if (target) { + this.removeTarget(target); + } + const user = this.getUserPokemon(); + // 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) { + user.turnData.hitCount = 1; + user.turnData.hitsLeft = 1; + } + } + + /** + * Unshifts a new `MoveEffectPhase` with the same properties as this phase. + * Used to queue the next hit of multi-strike moves. + */ + protected addNextHitPhase(): void { + globalScene.phaseManager.unshiftNew("MoveEffectPhase", this.battlerIndex, this.targets, this.move, this.useMode); + } + + /** Removes all substitutes that were broken by this phase's invoked move */ + protected updateSubstitutes(): void { + const targets = this.getTargets(); + for (const target of targets) { + const substitute = target.getTag(SubstituteTag); + if (substitute && substitute.hp <= 0) { + target.lapseTag(BattlerTagType.SUBSTITUTE); + } + } + } + + // # endregion Helpers } diff --git a/src/phases/move-reflect-phase.ts b/src/phases/move-reflect-phase.ts index 4757f6e0684..ee0acd1e2ba 100644 --- a/src/phases/move-reflect-phase.ts +++ b/src/phases/move-reflect-phase.ts @@ -28,6 +28,7 @@ export class MoveReflectPhase extends Phase { } override start(): void { + this.pokemon.turnData.extraTurns++; // Magic Coat takes precedeence over Magic Bounce if both apply at once const magicCoatTag = this.pokemon.getTag(BattlerTagType.MAGIC_COAT) as MagicCoatTag | undefined; if (magicCoatTag) {