diff --git a/src/data/move.ts b/src/data/move.ts index b6f55ed14b8..5ab4b1e8a64 100644 --- a/src/data/move.ts +++ b/src/data/move.ts @@ -1,5 +1,5 @@ import { ChargeAnim, MoveChargeAnim, initMoveAnim, loadMoveAnimAssets } from "./battle-anims"; -import { BattleEndPhase, MoveEffectPhase, MovePhase, NewBattlePhase, PartyStatusCurePhase, PokemonHealPhase, StatChangePhase, SwitchSummonPhase } from "../phases"; +import { BattleEndPhase, MovePhase, NewBattlePhase, PartyStatusCurePhase, PokemonHealPhase, StatChangePhase, SwitchSummonPhase } from "../phases"; import { BattleStat, getBattleStatName } from "./battle-stat"; import { EncoreTag, SemiInvulnerableTag } from "./battler-tags"; import { getPokemonMessage, getPokemonNameWithAffix } from "../messages"; @@ -813,12 +813,15 @@ export class MoveEffectAttr extends MoveAttr { public firstHitOnly: boolean; /** Should this effect only apply on the last hit? */ public lastHitOnly: boolean; + /** Should this effect only apply on the first target hit? */ + public firstTargetOnly: boolean; - constructor(selfTarget?: boolean, trigger?: MoveEffectTrigger, firstHitOnly: boolean = false, lastHitOnly: boolean = false) { + constructor(selfTarget?: boolean, trigger?: MoveEffectTrigger, firstHitOnly: boolean = false, lastHitOnly: boolean = false, firstTargetOnly: boolean = false) { super(selfTarget); this.trigger = trigger !== undefined ? trigger : MoveEffectTrigger.POST_APPLY; this.firstHitOnly = firstHitOnly; this.lastHitOnly = lastHitOnly; + this.firstTargetOnly = firstTargetOnly; } /** @@ -2330,8 +2333,8 @@ export class StatChangeAttr extends MoveEffectAttr { private condition: MoveConditionFunc; private showMessage: boolean; - constructor(stats: BattleStat | BattleStat[], levels: integer, selfTarget?: boolean, condition?: MoveConditionFunc, showMessage: boolean = true, firstHitOnly: boolean = false, moveEffectTrigger: MoveEffectTrigger = MoveEffectTrigger.HIT) { - super(selfTarget, moveEffectTrigger, firstHitOnly); + constructor(stats: BattleStat | BattleStat[], levels: integer, selfTarget?: boolean, condition?: MoveConditionFunc, showMessage: boolean = true, firstHitOnly: boolean = false, moveEffectTrigger: MoveEffectTrigger = MoveEffectTrigger.HIT, firstTargetOnly: boolean = false) { + super(selfTarget, moveEffectTrigger, firstHitOnly, false, firstTargetOnly); this.stats = typeof(stats) === "number" ? [ stats as BattleStat ] : stats as BattleStat[]; @@ -5513,25 +5516,6 @@ const userSleptOrComatoseCondition: MoveConditionFunc = (user: Pokemon, target: const targetSleptOrComatoseCondition: MoveConditionFunc = (user: Pokemon, target: Pokemon, move: Move) => target.status?.effect === StatusEffect.SLEEP || target.hasAbility(Abilities.COMATOSE); -/** - * Condition to apply effects only upon applying the move to its last target. - * Currently only used for Make It Rain. - * @param {Pokemon} user The user of the move. - * @param {Pokemon} target The current target of the move. - * @param {Move} move The move to which this condition applies. - * @returns true if the target is the last target to which the move applies. - */ -const lastTargetOnlyCondition: MoveConditionFunc = (user: Pokemon, target: Pokemon, move: Move) => { - const effectPhase = user.scene.getCurrentPhase(); - const targetIndex = target.getFieldIndex() + (target.isPlayer() ? 0 : BattlerIndex.ENEMY); - - if (effectPhase instanceof MoveEffectPhase) { - const activeTargets = effectPhase.getTargets(); - return (activeTargets.length === 0 || targetIndex >= activeTargets.at(-1).getBattlerIndex()); - } - return false; -}; - export type MoveAttrFilter = (attr: MoveAttr) => boolean; function applyMoveAttrsInternal(attrFilter: MoveAttrFilter, user: Pokemon, target: Pokemon, move: Move, args: any[]): Promise { @@ -8279,7 +8263,7 @@ export function initMoves() { .attr(RemoveScreensAttr), new AttackMove(Moves.MAKE_IT_RAIN, Type.STEEL, MoveCategory.SPECIAL, 120, 100, 5, -1, 0, 9) .attr(MoneyAttr) - .attr(StatChangeAttr, BattleStat.SPATK, -1, true, lastTargetOnlyCondition, true, false) + .attr(StatChangeAttr, BattleStat.SPATK, -1, true, null, true, false, MoveEffectTrigger.HIT, true) .target(MoveTarget.ALL_NEAR_ENEMIES), new AttackMove(Moves.PSYBLADE, Type.PSYCHIC, MoveCategory.PHYSICAL, 80, 100, 15, -1, 0, 9) .attr(MovePowerMultiplierAttr, (user, target, move) => user.scene.arena.getTerrainType() === TerrainType.ELECTRIC && user.isGrounded() ? 1.5 : 1) diff --git a/src/phases.ts b/src/phases.ts index 263a1673671..b14a84526c7 100644 --- a/src/phases.ts +++ b/src/phases.ts @@ -2923,6 +2923,7 @@ 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); @@ -2956,8 +2957,8 @@ export class MoveEffectPhase extends PokemonPhase { target.addTag(BattlerTagType.FLINCHED, undefined, this.move.moveId, user.id); } } - Utils.executeIf(!isProtected && !chargeEffect, () => applyFilteredMoveAttrs((attr: MoveAttr) => attr instanceof MoveEffectAttr && (attr as MoveEffectAttr).trigger === MoveEffectTrigger.HIT && (!attr.firstHitOnly || firstHit) && (!attr.lastHitOnly || lastHit), - user, target, this.move.getMove()).then(() => { + 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(() => { return Utils.executeIf(!target.isFainted() || target.canApplyAbility(), () => applyPostDefendAbAttrs(PostDefendAbAttr, target, user, this.move.getMove(), hitResult).then(() => { if (!user.isPlayer() && this.move.getMove() instanceof AttackMove) { user.scene.applyShuffledModifiers(this.scene, EnemyAttackStatusEffectChanceModifier, false, target); diff --git a/src/test/moves/make_it_rain.test.ts b/src/test/moves/make_it_rain.test.ts index b46efecab79..a700ff10aae 100644 --- a/src/test/moves/make_it_rain.test.ts +++ b/src/test/moves/make_it_rain.test.ts @@ -13,6 +13,8 @@ import { getMovePosition } from "#app/test/utils/gameManagerUtils"; import { Abilities } from "#enums/abilities"; import { BattleStat } from "#app/data/battle-stat.js"; +const TIMEOUT = 20 * 1000; + describe("Moves - Make It Rain", () => { let phaserGame: Phaser.Game; let game: GameManager; @@ -57,7 +59,7 @@ describe("Moves - Make It Rain", () => { await game.phaseInterceptor.to(MoveEndPhase); expect(playerPokemon[0].summonData.battleStats[BattleStat.SPATK]).toBe(-1); - }); + }, TIMEOUT); it("should apply effects even if the target faints", async () => { vi.spyOn(overrides, "OPP_LEVEL_OVERRIDE", "get").mockReturnValue(1); // ensures the enemy will faint @@ -78,5 +80,27 @@ describe("Moves - Make It Rain", () => { expect(enemyPokemon.isFainted()).toBe(true); expect(playerPokemon.summonData.battleStats[BattleStat.SPATK]).toBe(-1); - }); + }, TIMEOUT); + + it("should reduce Sp. Atk. once after KOing two enemies", async () => { + vi.spyOn(overrides, "OPP_LEVEL_OVERRIDE", "get").mockReturnValue(1); // ensures the enemy will faint + + 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); + + enemyPokemon.forEach(p => expect(p.isFainted()).toBe(true)); + expect(playerPokemon[0].summonData.battleStats[BattleStat.SPATK]).toBe(-1); + }, TIMEOUT); });