[Bug] Fix Make It Rain applying stat change twice after KO'ing both opponents (#2696)

* Revert Make It Rain stat change behavior

* ESLint

* Add double KO unit test
This commit is contained in:
innerthunder 2024-06-29 10:59:26 -07:00 committed by GitHub
parent b82c85899e
commit a48429de09
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 37 additions and 28 deletions

View File

@ -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<void> {
@ -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)

View File

@ -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);

View File

@ -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);
});