diff --git a/src/battle-scene.ts b/src/battle-scene.ts index 87fa4bd2dec..ed1ec429e11 100644 --- a/src/battle-scene.ts +++ b/src/battle-scene.ts @@ -992,12 +992,10 @@ export default class BattleScene extends SceneBase { this.enemyModifierBar.removeAll(true); for (const p of this.getParty()) { - p.destroySubstitute(); p.destroy(); } this.party = []; for (const p of this.getEnemyParty()) { - p.destroySubstitute(); p.destroy(); } diff --git a/src/data/battler-tags.ts b/src/data/battler-tags.ts index e2292d826b1..b47dc8573f9 100644 --- a/src/data/battler-tags.ts +++ b/src/data/battler-tags.ts @@ -153,9 +153,11 @@ export class BeakBlastChargingTag extends BattlerTag { lapse(pokemon: Pokemon, lapseType: BattlerTagLapseType): boolean { if (lapseType === BattlerTagLapseType.CUSTOM) { const effectPhase = pokemon.scene.getCurrentPhase(); - if (effectPhase instanceof MoveEffectPhase && effectPhase.move.getMove().hasFlag(MoveFlags.MAKES_CONTACT)) { + if (effectPhase instanceof MoveEffectPhase) { const attacker = effectPhase.getPokemon(); - attacker.trySetStatus(StatusEffect.BURN, true, pokemon); + if (effectPhase.move.getMove().checkFlag(MoveFlags.MAKES_CONTACT, attacker, pokemon)) { + attacker.trySetStatus(StatusEffect.BURN, true, pokemon); + } } return true; } @@ -1900,7 +1902,7 @@ export class SubstituteTag extends BattlerTag { const move = moveEffectPhase.move.getMove(); const firstHit = (attacker.turnData.hitCount === attacker.turnData.hitsLeft); - if (firstHit && !move.canIgnoreSubstitute(attacker)) { + if (firstHit && move.hitsSubstitute(attacker, pokemon)) { pokemon.scene.queueMessage(i18next.t("battlerTags:substituteOnHit", { pokemonNameWithAffix: getPokemonNameWithAffix(pokemon) })); } } @@ -1913,7 +1915,6 @@ export class SubstituteTag extends BattlerTag { loadTag(source: BattlerTag | any): void { super.loadTag(source); this.hp = source.hp; - // TODO: load this tag's sprite (or generate a new one upon loading a game) } } diff --git a/src/data/move.ts b/src/data/move.ts index 0ce9a4d9330..e3ecf70664d 100644 --- a/src/data/move.ts +++ b/src/data/move.ts @@ -100,6 +100,9 @@ export enum MoveFlags { * Enables all hits of a multi-hit move to be accuracy checked individually */ CHECK_ALL_HITS = 1 << 17, + /** + * Indicates a move is able to bypass its target's Substitute (if the target has one) + */ IGNORE_SUBSTITUTE = 1 << 18, /** * Indicates a move is able to be redirected to allies in a double battle if the attacker faints @@ -320,15 +323,19 @@ export default class Move implements Localizable { } /** - * Checks if the move can bypass Substitute to directly hit its target + * Checks if the move would hit its target's Substitute instead of the target itself. * @param user The {@linkcode Pokemon} using this move + * @param target The {@linkcode Pokemon} targeted by this move * @returns `true` if the move can bypass the target's Substitute; `false` otherwise. */ - canIgnoreSubstitute(user: Pokemon): boolean { - return this.moveTarget === MoveTarget.USER - || user?.hasAbility(Abilities.INFILTRATOR) - || this.hasFlag(MoveFlags.SOUND_BASED) - || this.hasFlag(MoveFlags.IGNORE_SUBSTITUTE); + hitsSubstitute(user: Pokemon, target: Pokemon | null): boolean { + if (this.moveTarget === MoveTarget.USER || !target?.getTag(BattlerTagType.SUBSTITUTE)) { + return false; + } + + return !user.hasAbility(Abilities.INFILTRATOR) + && !this.hasFlag(MoveFlags.SOUND_BASED) + && !this.hasFlag(MoveFlags.IGNORE_SUBSTITUTE); } /** @@ -607,8 +614,7 @@ export default class Move implements Localizable { // special cases below, eg: if the move flag is MAKES_CONTACT, and the user pokemon has an ability that ignores contact (like "Long Reach"), then overrides and move does not make contact switch (flag) { case MoveFlags.MAKES_CONTACT: - if (user.hasAbilityWithAttr(IgnoreContactAbAttr) || - (target?.getTag(BattlerTagType.SUBSTITUTE) && !this.canIgnoreSubstitute(user))) { + if (user.hasAbilityWithAttr(IgnoreContactAbAttr) || this.hitsSubstitute(user, target)) { return false; } break; @@ -2004,7 +2010,7 @@ export class StatusEffectAttr extends MoveEffectAttr { } apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean { - if (!this.selfTarget && !!target.getTag(SubstituteTag) && !move.canIgnoreSubstitute(user)) { + if (!this.selfTarget && move.hitsSubstitute(user, target)) { return false; } @@ -2100,7 +2106,7 @@ export class StealHeldItemChanceAttr extends MoveEffectAttr { apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): Promise { return new Promise(resolve => { - if (!!target.getTag(SubstituteTag) && !move.canIgnoreSubstitute(user)) { + if (move.hitsSubstitute(user, target)) { return resolve(false); } const rand = Phaser.Math.RND.realInRange(0, 1); @@ -2172,7 +2178,7 @@ export class RemoveHeldItemAttr extends MoveEffectAttr { return false; } - if (!!target.getTag(SubstituteTag) && !move.canIgnoreSubstitute(user)) { + if (move.hitsSubstitute(user, target)) { return false; } @@ -2295,7 +2301,7 @@ export class StealEatBerryAttr extends EatBerryAttr { * @returns {boolean} true if the function succeeds */ apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean { - if (!!target.getTag(SubstituteTag) && !move.canIgnoreSubstitute(user)) { + if (move.hitsSubstitute(user, target)) { return false; } const cancelled = new Utils.BooleanHolder(false); @@ -2348,7 +2354,7 @@ export class HealStatusEffectAttr extends MoveEffectAttr { return false; } - if (!this.selfTarget && !!target.getTag(SubstituteTag) && !move.canIgnoreSubstitute(user)) { + if (!this.selfTarget && move.hitsSubstitute(user, target)) { return false; } @@ -2665,7 +2671,7 @@ export class StatChangeAttr extends MoveEffectAttr { return false; } - if (!this.selfTarget && !!target.getTag(SubstituteTag) && !move.canIgnoreSubstitute(user)) { + if (!this.selfTarget && move.hitsSubstitute(user, target)) { return false; } @@ -2862,7 +2868,7 @@ export class ResetStatsAttr extends MoveEffectAttr { return false; } - if (!!target.getTag(SubstituteTag) && !move.canIgnoreSubstitute(user)) { + if (move.hitsSubstitute(user, target)) { return false; } @@ -4604,7 +4610,7 @@ export class FlinchAttr extends AddBattlerTagAttr { } apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean { - if (!target.getTag(SubstituteTag) || move.canIgnoreSubstitute(user)) { + if (!move.hitsSubstitute(user, target)) { return super.apply(user, target, move, args); } return false; @@ -4617,7 +4623,7 @@ export class ConfuseAttr extends AddBattlerTagAttr { } apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean { - if (!target.getTag(SubstituteTag) || move.canIgnoreSubstitute(user)) { + if (!move.hitsSubstitute(user, target)) { return super.apply(user, target, move, args); } return false; @@ -5113,7 +5119,7 @@ export class ForceSwitchOutAttr extends MoveEffectAttr { const switchOutTarget = (this.user ? user : target); const player = switchOutTarget instanceof PlayerPokemon; - if (!this.user && !!target.getTag(SubstituteTag) && !move.canIgnoreSubstitute(user)) { + if (!this.user && move.hitsSubstitute(user, target)) { return false; } diff --git a/src/field/pokemon.ts b/src/field/pokemon.ts index 7cf6b705988..99a444eeb88 100644 --- a/src/field/pokemon.ts +++ b/src/field/pokemon.ts @@ -2235,7 +2235,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { this.lapseTags(BattlerTagLapseType.HIT); const substitute = this.getTag(SubstituteTag); - if (!!substitute && !move.canIgnoreSubstitute(source)) { + if (substitute && move.hitsSubstitute(source, this)) { substitute.hp -= damage.value; damage.value = 0; } @@ -2310,7 +2310,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { if (!typeless) { applyPreDefendAbAttrs(TypeImmunityAbAttr, this, source, move, cancelled, typeMultiplier); } - if (!!this.getTag(SubstituteTag) && !move.canIgnoreSubstitute(source)) { + if (move.hitsSubstitute(source, this)) { cancelled.value = true; } if (!cancelled.value) { @@ -3326,6 +3326,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { destroy(): void { this.battleInfo?.destroy(); + this.destroySubstitute(); super.destroy(); } diff --git a/src/phases/move-anim-test-phase.ts b/src/phases/move-anim-test-phase.ts index a961f96265f..bb8edd30551 100644 --- a/src/phases/move-anim-test-phase.ts +++ b/src/phases/move-anim-test-phase.ts @@ -33,7 +33,7 @@ export class MoveAnimTestPhase extends BattlePhase { .then(() => { const user = player ? this.scene.getPlayerPokemon()! : this.scene.getEnemyPokemon()!; const target = (player !== (allMoves[moveId] instanceof SelfStatusMove)) ? this.scene.getEnemyPokemon()! : this.scene.getPlayerPokemon()!; - new MoveAnim(moveId, user, target.getBattlerIndex()).play(this.scene, !allMoves[moveId].canIgnoreSubstitute(user), () => { // TODO: are the bangs correct here? + new MoveAnim(moveId, user, target.getBattlerIndex()).play(this.scene, allMoves[moveId].hitsSubstitute(user, target), () => { // TODO: are the bangs correct here? if (player) { this.playMoveAnim(moveQueue, false); } else { diff --git a/src/phases/move-effect-phase.ts b/src/phases/move-effect-phase.ts index 1ac185c780c..bcc856efcb9 100644 --- a/src/phases/move-effect-phase.ts +++ b/src/phases/move-effect-phase.ts @@ -120,7 +120,7 @@ export class MoveEffectPhase extends PokemonPhase { const applyAttrs: Promise[] = []; // Move animation only needs one target - new MoveAnim(move.id as Moves, user, this.getTarget()?.getBattlerIndex()!).play(this.scene, !move.canIgnoreSubstitute(user), () => { // TODO: is the bang correct here? + new MoveAnim(move.id as Moves, user, this.getTarget()!.getBattlerIndex()).play(this.scene, move.hitsSubstitute(user, this.getTarget()!), () => { /** Has the move successfully hit a target (for damage) yet? */ let hasHit: boolean = false; for (const target of targets) { @@ -245,7 +245,7 @@ export class MoveEffectPhase extends PokemonPhase { * 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) && (!target.getTag(BattlerTagType.SUBSTITUTE) || move.canIgnoreSubstitute(user))) { + if (dealsDamage && !target.hasAbilityWithAttr(IgnoreMoveEffectsAbAttr) && !move.hitsSubstitute(user, target)) { const flinched = new Utils.BooleanHolder(false); user.scene.applyModifiers(FlinchChanceModifier, user.isPlayer(), user, flinched); if (flinched.value) { @@ -257,14 +257,19 @@ export class MoveEffectPhase extends PokemonPhase { && (!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(() => { - // If the invoked move is an enemy attack, apply the enemy's status effect-inflicting tags and tokens + // Only apply the following effects if the move was not deflected by a substitute + if (move.hitsSubstitute(user, target)) { + return resolve(); + } + + // If the invoked move is an enemy attack, apply the enemy's status effect-inflicting tokens + if (!user.isPlayer() && this.move.getMove() instanceof AttackMove) { + user.scene.applyShuffledModifiers(this.scene, EnemyAttackStatusEffectChanceModifier, false, target); + } target.lapseTag(BattlerTagType.BEAK_BLAST_CHARGING); if (move.category === MoveCategory.PHYSICAL && user.isPlayer() !== target.isPlayer()) { target.lapseTag(BattlerTagType.SHELL_TRAP); } - if (!user.isPlayer() && this.move.getMove() instanceof AttackMove) { - user.scene.applyShuffledModifiers(this.scene, EnemyAttackStatusEffectChanceModifier, false, target); - } })).then(() => { // Apply the user's post-attack ability effects applyPostAttackAbAttrs(PostAttackAbAttr, user, target, this.move.getMove(), hitResult).then(() => { diff --git a/src/test/battlerTags/substitute.test.ts b/src/test/battlerTags/substitute.test.ts index 83447319d2b..1ce81850c13 100644 --- a/src/test/battlerTags/substitute.test.ts +++ b/src/test/battlerTags/substitute.test.ts @@ -195,7 +195,7 @@ describe("BattlerTag - SubstituteTag", () => { } as MoveEffectPhase; vi.spyOn(mockPokemon.scene, "getCurrentPhase").mockReturnValue(moveEffectPhase); - vi.spyOn(allMoves[Moves.TACKLE], "canIgnoreSubstitute").mockReturnValue(false); + vi.spyOn(allMoves[Moves.TACKLE], "hitsSubstitute").mockReturnValue(true); expect(subject.lapse(mockPokemon, BattlerTagLapseType.HIT)).toBeTruthy();