diff --git a/src/data/move.ts b/src/data/move.ts index 13282a1a3d1..a00d81e5980 100644 --- a/src/data/move.ts +++ b/src/data/move.ts @@ -7905,7 +7905,7 @@ export function initMoves() { .makesContact(false) .partial(), new AttackMove(Moves.CLANGING_SCALES, Type.DRAGON, MoveCategory.SPECIAL, 110, 100, 5, -1, 0, 7) - .attr(StatChangeAttr, BattleStat.DEF, -1, true) + .attr(StatChangeAttr, BattleStat.DEF, -1, true, null, true, false, MoveEffectTrigger.HIT, true) .soundBased() .target(MoveTarget.ALL_NEAR_ENEMIES), new AttackMove(Moves.DRAGON_HAMMER, Type.DRAGON, MoveCategory.PHYSICAL, 90, 100, 15, -1, 0, 7), diff --git a/src/phases.ts b/src/phases.ts index 362f5f22cc2..ac233896b68 100644 --- a/src/phases.ts +++ b/src/phases.ts @@ -2966,6 +2966,8 @@ export class MoveEffectPhase extends PokemonPhase { // Move animation only needs one target new MoveAnim(move.id as Moves, user, this.getTarget()?.getBattlerIndex()).play(this.scene, () => { + /** Has the move successfully hit a target (for damage) yet? */ + let hasHit: boolean = false; for (const target of targets) { if (!targetHitChecks[target.getBattlerIndex()]) { this.stopMultiHit(target); @@ -2981,7 +2983,6 @@ export class MoveEffectPhase extends PokemonPhase { const isProtected = !this.move.getMove().checkFlag(MoveFlags.IGNORE_PROTECT, user, target) && target.findTags(t => t instanceof ProtectedTag).find(t => target.lapseTag(t.tagType)); const firstHit = (user.turnData.hitsLeft === user.turnData.hitCount); - const firstTarget = (moveHistoryEntry.result === MoveResult.PENDING); if (firstHit) { user.pushMoveHistory(moveHistoryEntry); @@ -2991,6 +2992,18 @@ export class MoveEffectPhase extends PokemonPhase { const hitResult = !isProtected ? target.apply(user, move) : HitResult.NO_EFFECT; + const dealsDamage = [ + HitResult.EFFECTIVE, + HitResult.SUPER_EFFECTIVE, + HitResult.NOT_VERY_EFFECTIVE, + HitResult.ONE_HIT_KO + ].includes(hitResult); + + const firstTarget = dealsDamage && !hasHit; + if (firstTarget) { + hasHit = true; + } + const lastHit = (user.turnData.hitsLeft === 1 || !this.getTarget()?.isActive()); if (lastHit) { @@ -3008,7 +3021,7 @@ export class MoveEffectPhase extends PokemonPhase { if (hitResult !== HitResult.NO_EFFECT) { 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 (hitResult < HitResult.NO_EFFECT && !target.hasAbilityWithAttr(IgnoreMoveEffectsAbAttr)) { + if (dealsDamage && !target.hasAbilityWithAttr(IgnoreMoveEffectsAbAttr)) { const flinched = new Utils.BooleanHolder(false); user.scene.applyModifiers(FlinchChanceModifier, user.isPlayer(), user, flinched); if (flinched.value) { diff --git a/src/test/moves/make_it_rain.test.ts b/src/test/moves/make_it_rain.test.ts index 83543c4e530..fcfb1daa2cf 100644 --- a/src/test/moves/make_it_rain.test.ts +++ b/src/test/moves/make_it_rain.test.ts @@ -1,6 +1,6 @@ import { BattleStat } from "#app/data/battle-stat.js"; import { - CommandPhase, + MoveEffectPhase, MoveEndPhase, StatChangePhase, } from "#app/phases"; @@ -10,7 +10,7 @@ import { Abilities } from "#enums/abilities"; import { Moves } from "#enums/moves"; import { Species } from "#enums/species"; import Phaser from "phaser"; -import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest"; +import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; import { SPLASH_ONLY } from "../utils/testUtils"; const TIMEOUT = 20 * 1000; @@ -44,16 +44,8 @@ describe("Moves - Make It Rain", () => { await game.startBattle([Species.CHARIZARD, Species.BLASTOISE]); const playerPokemon = game.scene.getPlayerField(); - expect(playerPokemon.length).toBe(2); - playerPokemon.forEach(p => expect(p).toBeDefined()); - - const enemyPokemon = game.scene.getEnemyField(); - expect(enemyPokemon.length).toBe(2); - enemyPokemon.forEach(p => expect(p).toBeDefined()); game.doAttack(getMovePosition(game.scene, 0, Moves.MAKE_IT_RAIN)); - - await game.phaseInterceptor.to(CommandPhase); game.doAttack(getMovePosition(game.scene, 1, Moves.SPLASH)); await game.phaseInterceptor.to(MoveEndPhase); @@ -68,10 +60,7 @@ describe("Moves - Make It Rain", () => { await game.startBattle([Species.CHARIZARD]); const playerPokemon = game.scene.getPlayerPokemon(); - expect(playerPokemon).toBeDefined(); - const enemyPokemon = game.scene.getEnemyPokemon(); - expect(enemyPokemon).toBeDefined(); game.doAttack(getMovePosition(game.scene, 0, Moves.MAKE_IT_RAIN)); @@ -87,14 +76,9 @@ describe("Moves - Make It Rain", () => { await game.startBattle([Species.CHARIZARD, Species.BLASTOISE]); const playerPokemon = game.scene.getPlayerField(); - playerPokemon.forEach(p => expect(p).toBeDefined()); - const enemyPokemon = game.scene.getEnemyField(); - enemyPokemon.forEach(p => expect(p).toBeDefined()); game.doAttack(getMovePosition(game.scene, 0, Moves.MAKE_IT_RAIN)); - - await game.phaseInterceptor.to(CommandPhase); game.doAttack(getMovePosition(game.scene, 1, Moves.SPLASH)); await game.phaseInterceptor.to(StatChangePhase); @@ -102,4 +86,23 @@ describe("Moves - Make It Rain", () => { enemyPokemon.forEach(p => expect(p.isFainted()).toBe(true)); expect(playerPokemon[0].summonData.battleStats[BattleStat.SPATK]).toBe(-1); }, TIMEOUT); + + it("should reduce Sp. Atk if it only hits the second target", async () => { + await game.startBattle([Species.CHARIZARD, Species.BLASTOISE]); + + const playerPokemon = game.scene.getPlayerField(); + + game.doAttack(getMovePosition(game.scene, 0, Moves.MAKE_IT_RAIN)); + game.doAttack(getMovePosition(game.scene, 1, Moves.SPLASH)); + + await game.phaseInterceptor.to(MoveEffectPhase, false); + + // Make Make It Rain miss the first target + const moveEffectPhase = game.scene.getCurrentPhase() as MoveEffectPhase; + vi.spyOn(moveEffectPhase, "hitCheck").mockReturnValueOnce(false); + + await game.phaseInterceptor.to(MoveEndPhase); + + expect(playerPokemon[0].summonData.battleStats[BattleStat.SPATK]).toBe(-1); + }, TIMEOUT); });