From 58b83b1a5543d481b54dcb77cd7f2eda7082cb77 Mon Sep 17 00:00:00 2001 From: innerthunder Date: Sat, 16 Nov 2024 15:30:27 -0800 Subject: [PATCH] Bug fixes + test fixes --- src/field/pokemon.ts | 2 +- src/phases/move-effect-phase.ts | 224 +++++++++++++------------ src/test/abilities/galvanize.test.ts | 25 ++- src/test/abilities/no_guard.test.ts | 5 +- src/test/abilities/sheer_force.test.ts | 46 ++--- src/test/abilities/tera_shell.test.ts | 13 +- src/test/items/dire_hit.test.ts | 3 +- src/test/items/leek.test.ts | 1 - src/test/items/scope_lens.test.ts | 3 +- src/test/moves/tera_blast.test.ts | 19 +-- src/test/utils/helpers/moveHelper.ts | 14 +- 11 files changed, 174 insertions(+), 181 deletions(-) diff --git a/src/field/pokemon.ts b/src/field/pokemon.ts index 807405c6a99..e068021b61a 100644 --- a/src/field/pokemon.ts +++ b/src/field/pokemon.ts @@ -3447,7 +3447,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { if (effect === StatusEffect.SLEEP || effect === StatusEffect.FREEZE) { const currentPhase = this.scene.getCurrentPhase(); if (currentPhase instanceof MoveEffectPhase && currentPhase.getUserPokemon() === this) { - this.turnData.hitCount -= this.turnData.hitsLeft - 1; + this.turnData.hitCount = 1; this.turnData.hitsLeft = 1; } } diff --git a/src/phases/move-effect-phase.ts b/src/phases/move-effect-phase.ts index 531b753227d..6b2581e9ef7 100644 --- a/src/phases/move-effect-phase.ts +++ b/src/phases/move-effect-phase.ts @@ -81,8 +81,6 @@ export class MoveEffectPhase extends PokemonPhase { private firstHit: boolean; /** Is this the last strike of a move? */ private lastHit: boolean; - /** Is this the first target to be hit by this strike? */ - private firstTarget: boolean = true; constructor(scene: BattleScene, battlerIndex: BattlerIndex, targets: BattlerIndex[], move: PokemonMove) { super(scene, battlerIndex); @@ -136,90 +134,6 @@ export class MoveEffectPhase extends PokemonPhase { user.lapseTags(BattlerTagLapseType.MOVE_EFFECT); - this.moveHistoryEntry = { move: move.id, targets: this.targets, result: MoveResult.PENDING, virtual: this.move.virtual }; - - targets.forEach((t, i) => this.hitChecks[i] = this.hitCheck(t)); - - if (!targets.some(t => t.isActive(true))) { - this.scene.queueMessage(i18next.t("battle:attackFailed")); - this.moveHistoryEntry.result = MoveResult.FAIL; - } - - if (this.hitChecks.some(hc => hc[0] === HitCheckResult.HIT)) { - this.moveHistoryEntry.result = MoveResult.SUCCESS; - } else if (this.hitChecks.every(hc => hc[0] === HitCheckResult.MISS)) { - this.moveHistoryEntry.result = MoveResult.MISS; - } else { - this.moveHistoryEntry.result = MoveResult.FAIL; - } - - // If the move has a post-target effect (e.g. Explosion), but doesn't - // successfully hit a target, play the move's animation and return - if (move.getAttrs(MoveEffectAttr).some(attr => attr.trigger === MoveEffectTrigger.POST_TARGET) - && this.hitChecks.every(hc => hc[1] === 0, this)) { - const playOnEmptyField = this.scene.currentBattle?.mysteryEncounter?.hasBattleAnimationsWithoutTargets ?? false; - return new MoveAnim(move.id, user, this.getFirstTarget()!.getBattlerIndex(), playOnEmptyField).play(this.scene, false, () => - this.triggerMoveEffects(MoveEffectTrigger.POST_TARGET, user, null).then(() => this.end()) - ); - } - - // If this phase represents the first strike of the given move, - // log the move in the user's move history. - if (user.turnData.hitsLeft === -1) { - user.pushMoveHistory(this.moveHistoryEntry); - } - - console.log(this.hitChecks); - - targets.forEach((target, i) => { - const [ hitCheckResult, effectiveness ] = this.hitChecks[i]; - - switch (hitCheckResult) { - case HitCheckResult.HIT: - this.applyMoveEffects(target, effectiveness); - this.firstTarget = false; - break; - case HitCheckResult.NO_EFFECT: - if (move.id === Moves.SHEER_COLD) { - this.scene.queueMessage(i18next.t("battle:hitResultImmune", { pokemonName: getPokemonNameWithAffix(target) })); - } else { - this.scene.queueMessage(i18next.t("battle:hitResultNoEffect", { pokemonName: getPokemonNameWithAffix(target) })); - } - case HitCheckResult.PROTECTED: - case HitCheckResult.NO_EFFECT_NO_MESSAGE: - applyMoveAttrs(NoEffectAttr, user, target, move); - break; - case HitCheckResult.MISS: - this.scene.queueMessage(i18next.t("battle:attackMissed", { pokemonNameWithAffix: this.getFirstTarget() ? getPokemonNameWithAffix(this.getFirstTarget()!) : "" })); - applyMoveAttrs(MissEffectAttr, user, target, move); - break; - case HitCheckResult.PENDING: - case HitCheckResult.ERROR: - console.log(`Unexpected hit check result ${HitCheckResult[hitCheckResult]}. Aborting phase.`); - return this.end(); - } - }); - - const doPostTarget = this.lastHit ? this.triggerMoveEffects(MoveEffectTrigger.POST_TARGET, user, null) : Promise.resolve(); - doPostTarget.then(() => { - this.updateSubstitutes(); - this.end(); - }); - } - - protected applyMoveEffects(target: Pokemon, effectiveness: TypeDamageMultiplier): void { - const user = this.getUserPokemon(); - const move = this.move.getMove(); - - if (isNullOrUndefined(user)) { - return; - } - - // prevent field-targeted moves from activating multiple times - if (move.isFieldTarget() && target !== this.getTargets()[this.targets.length - 1]) { - return; - } - /** * If this phase is for the first hit of the invoked move, * resolve the move's total hit count. This block combines the @@ -238,13 +152,109 @@ export class MoveEffectPhase extends PokemonPhase { user.turnData.hitsLeft = hitCount.value; } - this.firstHit = user.turnData.hitsLeft === user.turnData.hitCount; - this.lastHit = user.turnData.hitsLeft === 1 || !this.getTargets().some(t => t.isActive(true)); + this.moveHistoryEntry = { move: move.id, targets: this.targets, result: MoveResult.PENDING, virtual: this.move.virtual }; - const playOnEmptyField = this.scene.currentBattle?.mysteryEncounter?.hasBattleAnimationsWithoutTargets ?? false; - return new MoveAnim(move.id as Moves, user, this.getFirstTarget()!.getBattlerIndex()!, playOnEmptyField).play(this.scene, move.hitsSubstitute(user, this.getFirstTarget()!), async () => { - await this.triggerMoveEffects(MoveEffectTrigger.PRE_APPLY, user, target); + targets.forEach((t, i) => this.hitChecks[i] = this.hitCheck(t)); + if (!targets.some(t => t.isActive(true))) { + this.scene.queueMessage(i18next.t("battle:attackFailed")); + this.moveHistoryEntry.result = MoveResult.FAIL; + } + + if (this.hitChecks.some(hc => hc[0] === HitCheckResult.HIT)) { + this.moveHistoryEntry.result = MoveResult.SUCCESS; + } else { + user.turnData.hitCount = 1; + user.turnData.hitsLeft = 1; + + if (this.hitChecks.every(hc => hc[0] === HitCheckResult.MISS)) { + this.moveHistoryEntry.result = MoveResult.MISS; + } else { + this.moveHistoryEntry.result = MoveResult.FAIL; + } + } + + this.firstHit = user.turnData.hitCount === user.turnData.hitsLeft; + this.lastHit = user.turnData.hitsLeft === 1 || !targets.some(t => t.isActive(true)); + + // If the move successfully hit at least 1 target, or the move has a + // post-target effect, play the move's animation + const tryPlayAnim = (this.moveHistoryEntry.result === MoveResult.SUCCESS || move.getAttrs(MoveEffectAttr).some(attr => attr.trigger === MoveEffectTrigger.POST_TARGET)) + ? this.playMoveAnim(user) + : Promise.resolve(); + + tryPlayAnim.then(() => { + // If this phase represents the first strike of the given move, + // log the move in the user's move history. + if (this.firstHit) { + user.pushMoveHistory(this.moveHistoryEntry); + } + + const applyPromises: Promise[] = []; + + for (const target of targets) { + const [ hitCheckResult, effectiveness ] = this.hitChecks[targets.indexOf(target)]; + + switch (hitCheckResult) { + case HitCheckResult.HIT: + applyPromises.push(this.applyMoveEffects(target, effectiveness)); + break; + case HitCheckResult.NO_EFFECT: + if (move.id === Moves.SHEER_COLD) { + this.scene.queueMessage(i18next.t("battle:hitResultImmune", { pokemonName: getPokemonNameWithAffix(target) })); + } else { + this.scene.queueMessage(i18next.t("battle:hitResultNoEffect", { pokemonName: getPokemonNameWithAffix(target) })); + } + case HitCheckResult.PROTECTED: + case HitCheckResult.NO_EFFECT_NO_MESSAGE: + applyMoveAttrs(NoEffectAttr, user, target, move); + break; + case HitCheckResult.MISS: + this.scene.queueMessage(i18next.t("battle:attackMissed", { pokemonNameWithAffix: this.getFirstTarget() ? getPokemonNameWithAffix(this.getFirstTarget()!) : "" })); + applyMoveAttrs(MissEffectAttr, user, target, move); + break; + case HitCheckResult.PENDING: + case HitCheckResult.ERROR: + console.log(`Unexpected hit check result ${HitCheckResult[hitCheckResult]}. Aborting phase.`); + return this.end(); + } + } + + Promise.allSettled(applyPromises) + .then(() => executeIf(this.lastHit, () => this.triggerMoveEffects(MoveEffectTrigger.POST_TARGET, user, null))) + .then(() => { + this.updateSubstitutes(); + this.end(); + }); + }); + } + + protected playMoveAnim(user: Pokemon): Promise { + return new Promise((resolve) => { + const move = this.move.getMove(); + const firstTargetPokemon = this.getFirstTarget() ?? null; + const playOnEmptyField = this.scene.currentBattle?.mysteryEncounter?.hasBattleAnimationsWithoutTargets ?? false; + new MoveAnim(move.id, user, firstTargetPokemon!.getBattlerIndex(), playOnEmptyField) + .play(this.scene, move.hitsSubstitute(user, firstTargetPokemon), () => resolve()); + }); + } + + protected applyMoveEffects(target: Pokemon, effectiveness: TypeDamageMultiplier): Promise { + const user = this.getUserPokemon(); + const move = this.move.getMove(); + + const firstTarget = target === this.getTargets().find((_, i) => this.hitChecks[i][1] > 0); + + if (isNullOrUndefined(user)) { + return Promise.resolve(); + } + + // prevent field-targeted moves from activating multiple times + if (move.isFieldTarget() && target !== this.getTargets()[this.targets.length - 1]) { + return Promise.resolve(); + } + + return this.triggerMoveEffects(MoveEffectTrigger.PRE_APPLY, user, target).then(() => { const hitResult = this.applyMove(target, effectiveness); /** Does {@linkcode hitResult} indicate that damage was dealt to the target? */ @@ -255,19 +265,20 @@ export class MoveEffectPhase extends PokemonPhase { HitResult.ONE_HIT_KO ].includes(hitResult); - await this.triggerMoveEffects(MoveEffectTrigger.POST_APPLY, user, target); - this.applyHeldItemFlinchCheck(user, target, dealsDamage); - await this.triggerMoveEffects(MoveEffectTrigger.HIT, user, target); - await this.applyOnGetHitAbEffects(user, target, hitResult); - await applyPostAttackAbAttrs(PostAttackAbAttr, user, target, move, hitResult); + return this.triggerMoveEffects(MoveEffectTrigger.POST_APPLY, user, target, firstTarget) + .then(() => this.applyHeldItemFlinchCheck(user, target, dealsDamage)) + .then(() => this.triggerMoveEffects(MoveEffectTrigger.HIT, user, target, firstTarget)) + .then(() => this.applyOnGetHitAbEffects(user, target, hitResult)) + .then(() => applyPostAttackAbAttrs(PostAttackAbAttr, user, target, move, hitResult)) + .then(() => { + if (move instanceof AttackMove) { + this.scene.applyModifiers(ContactHeldItemTransferChanceModifier, this.player, user, target); + } - if (move instanceof AttackMove) { - this.scene.applyModifiers(ContactHeldItemTransferChanceModifier, this.player, user, target); - } - - if (this.lastHit) { - this.scene.triggerPokemonFormChange(user, SpeciesFormChangePostMoveTrigger); - } + if (this.lastHit) { + this.scene.triggerPokemonFormChange(user, SpeciesFormChangePostMoveTrigger); + } + }); }); } @@ -307,18 +318,19 @@ export class MoveEffectPhase extends PokemonPhase { * @param triggerType The {@linkcode MoveEffectTrigger} being applied * @param user The {@linkcode Pokemon} using the move * @param target The {@linkcode Pokemon} targeted by the move + * @param firstTarget Whether the target is the first to be hit by the current strike * @param selfTarget If defined, limits the effects triggered to either self-targeted * effects (if set to `true`) or targeted effects (if set to `false`). * @returns a `Promise` applying the relevant move effects. */ - protected triggerMoveEffects(triggerType: MoveEffectTrigger, user: Pokemon, target: Pokemon | null, selfTarget?: boolean): Promise { + protected triggerMoveEffects(triggerType: MoveEffectTrigger, user: Pokemon, target: Pokemon | null, firstTarget?: boolean | null, selfTarget?: boolean): Promise { return applyFilteredMoveAttrs((attr: MoveAttr) => attr instanceof MoveEffectAttr && attr.trigger === triggerType && (isNullOrUndefined(selfTarget) || (attr.selfTarget === selfTarget)) && (!attr.firstHitOnly || this.firstHit) && (!attr.lastHitOnly || this.lastHit) - && (!attr.firstTargetOnly || this.firstTarget), + && (!attr.firstTargetOnly || (firstTarget ?? true)), user, target, this.move.getMove()); } @@ -565,7 +577,7 @@ export class MoveEffectPhase extends PokemonPhase { } } - if (alwaysHit || target.getTag(BattlerTagType.TELEKINESIS) && !move.hasAttr(OneHitKOAttr)) { + if (alwaysHit || (target.getTag(BattlerTagType.TELEKINESIS) && !move.hasAttr(OneHitKOAttr))) { return [ HitCheckResult.HIT, effectiveness ]; } diff --git a/src/test/abilities/galvanize.test.ts b/src/test/abilities/galvanize.test.ts index 80e767866ea..b40fe591b2f 100644 --- a/src/test/abilities/galvanize.test.ts +++ b/src/test/abilities/galvanize.test.ts @@ -4,7 +4,6 @@ import { Type } from "#enums/type"; import { Abilities } from "#app/enums/abilities"; import { Moves } from "#app/enums/moves"; import { Species } from "#app/enums/species"; -import { HitResult } from "#app/field/pokemon"; import GameManager from "#test/utils/gameManager"; import Phaser from "phaser"; import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; @@ -39,13 +38,13 @@ describe("Abilities - Galvanize", () => { }); it("should change Normal-type attacks to Electric type and boost their power", async () => { - await game.startBattle(); + await game.classicMode.startBattle([ Species.MAGIKARP ]); const playerPokemon = game.scene.getPlayerPokemon()!; vi.spyOn(playerPokemon, "getMoveType"); const enemyPokemon = game.scene.getEnemyPokemon()!; - vi.spyOn(enemyPokemon, "apply"); + vi.spyOn(enemyPokemon, "getMoveEffectiveness"); const move = allMoves[Moves.TACKLE]; vi.spyOn(move, "calculateBattlePower"); @@ -55,7 +54,7 @@ describe("Abilities - Galvanize", () => { await game.phaseInterceptor.to("BerryPhase", false); expect(playerPokemon.getMoveType).toHaveLastReturnedWith(Type.ELECTRIC); - expect(enemyPokemon.apply).toHaveReturnedWith(HitResult.EFFECTIVE); + expect(enemyPokemon.getMoveEffectiveness).toHaveReturnedWith(1); expect(move.calculateBattlePower).toHaveReturnedWith(48); expect(enemyPokemon.hp).toBeLessThan(enemyPokemon.getMaxHp()); }); @@ -63,13 +62,13 @@ describe("Abilities - Galvanize", () => { it("should cause Normal-type attacks to activate Volt Absorb", async () => { game.override.enemyAbility(Abilities.VOLT_ABSORB); - await game.startBattle(); + await game.classicMode.startBattle([ Species.MAGIKARP ]); const playerPokemon = game.scene.getPlayerPokemon()!; vi.spyOn(playerPokemon, "getMoveType"); const enemyPokemon = game.scene.getEnemyPokemon()!; - vi.spyOn(enemyPokemon, "apply"); + vi.spyOn(enemyPokemon, "getMoveEffectiveness"); enemyPokemon.hp = Math.floor(enemyPokemon.getMaxHp() * 0.8); @@ -78,37 +77,37 @@ describe("Abilities - Galvanize", () => { await game.phaseInterceptor.to("BerryPhase", false); expect(playerPokemon.getMoveType).toHaveLastReturnedWith(Type.ELECTRIC); - expect(enemyPokemon.apply).toHaveReturnedWith(HitResult.NO_EFFECT); + expect(enemyPokemon.getMoveEffectiveness).toHaveReturnedWith(0); expect(enemyPokemon.hp).toBe(enemyPokemon.getMaxHp()); }); it("should not change the type of variable-type moves", async () => { game.override.enemySpecies(Species.MIGHTYENA); - await game.startBattle([ Species.ESPEON ]); + await game.classicMode.startBattle([ Species.ESPEON ]); const playerPokemon = game.scene.getPlayerPokemon()!; vi.spyOn(playerPokemon, "getMoveType"); const enemyPokemon = game.scene.getEnemyPokemon()!; - vi.spyOn(enemyPokemon, "apply"); + vi.spyOn(enemyPokemon, "getMoveEffectiveness"); game.move.select(Moves.REVELATION_DANCE); await game.phaseInterceptor.to("BerryPhase", false); expect(playerPokemon.getMoveType).not.toHaveLastReturnedWith(Type.ELECTRIC); - expect(enemyPokemon.apply).toHaveReturnedWith(HitResult.NO_EFFECT); + expect(enemyPokemon.getMoveEffectiveness).toHaveReturnedWith(0); expect(enemyPokemon.hp).toBe(enemyPokemon.getMaxHp()); }); it("should affect all hits of a Normal-type multi-hit move", async () => { - await game.startBattle(); + await game.classicMode.startBattle([ Species.MAGIKARP ]); const playerPokemon = game.scene.getPlayerPokemon()!; vi.spyOn(playerPokemon, "getMoveType"); const enemyPokemon = game.scene.getEnemyPokemon()!; - vi.spyOn(enemyPokemon, "apply"); + vi.spyOn(enemyPokemon, "getMoveEffectiveness"); game.move.select(Moves.FURY_SWIPES); await game.setTurnOrder([ BattlerIndex.PLAYER, BattlerIndex.ENEMY ]); @@ -126,6 +125,6 @@ describe("Abilities - Galvanize", () => { expect(enemyPokemon.hp).toBeLessThan(enemyStartingHp); } - expect(enemyPokemon.apply).not.toHaveReturnedWith(HitResult.NO_EFFECT); + expect(enemyPokemon.getMoveEffectiveness).not.toHaveReturnedWith(0); }); }); diff --git a/src/test/abilities/no_guard.test.ts b/src/test/abilities/no_guard.test.ts index b0b454dd560..f738f42bc9e 100644 --- a/src/test/abilities/no_guard.test.ts +++ b/src/test/abilities/no_guard.test.ts @@ -1,5 +1,5 @@ import { BattlerIndex } from "#app/battle"; -import { MoveEffectPhase } from "#app/phases/move-effect-phase"; +import { HitCheckResult, MoveEffectPhase } from "#app/phases/move-effect-phase"; import { MoveEndPhase } from "#app/phases/move-end-phase"; import { Abilities } from "#enums/abilities"; import { Moves } from "#enums/moves"; @@ -28,6 +28,7 @@ describe("Abilities - No Guard", () => { .moveset(Moves.ZAP_CANNON) .ability(Abilities.NO_GUARD) .enemyLevel(200) + .enemySpecies(Species.SNORLAX) .enemyAbility(Abilities.BALL_FETCH) .enemyMoveset(Moves.SPLASH); }); @@ -50,7 +51,7 @@ describe("Abilities - No Guard", () => { await game.phaseInterceptor.to(MoveEndPhase); - expect(moveEffectPhase.hitCheck).toHaveReturnedWith(true); + expect(moveEffectPhase.hitCheck).toHaveReturnedWith([ HitCheckResult.HIT, 1 ]); }); it("should guarantee double battle with any one LURE", async () => { diff --git a/src/test/abilities/sheer_force.test.ts b/src/test/abilities/sheer_force.test.ts index f2d53965f52..1b42b605521 100644 --- a/src/test/abilities/sheer_force.test.ts +++ b/src/test/abilities/sheer_force.test.ts @@ -1,16 +1,17 @@ import { BattlerIndex } from "#app/battle"; -import { applyAbAttrs, applyPostDefendAbAttrs, applyPreAttackAbAttrs, MoveEffectChanceMultiplierAbAttr, MovePowerBoostAbAttr, PostDefendTypeChangeAbAttr } from "#app/data/ability"; +import { applyAbAttrs, applyPreAttackAbAttrs, MoveEffectChanceMultiplierAbAttr, MovePowerBoostAbAttr } from "#app/data/ability"; import { MoveEffectPhase } from "#app/phases/move-effect-phase"; import { NumberHolder } from "#app/utils"; import { Abilities } from "#enums/abilities"; import { Moves } from "#enums/moves"; import { Species } from "#enums/species"; import { Stat } from "#enums/stat"; +import { Type } from "#enums/type"; import GameManager from "#test/utils/gameManager"; import Phaser from "phaser"; import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest"; import { allMoves } from "#app/data/move"; -import { HitResult } from "#app/field/pokemon"; + describe("Abilities - Sheer Force", () => { let phaserGame: Phaser.Game; @@ -130,40 +131,25 @@ describe("Abilities - Sheer Force", () => { it("Sheer Force Disabling Specific Abilities", async () => { const moveToUse = Moves.CRUSH_CLAW; - game.override.enemyAbility(Abilities.COLOR_CHANGE); - game.override.startingHeldItems([{ name: "KINGS_ROCK", count: 1 }]); - game.override.ability(Abilities.SHEER_FORCE); - await game.startBattle([ Species.PIDGEOT ]); + game.override + .startingLevel(100) + .enemyLevel(100) + .enemySpecies(Species.SHUCKLE) + .enemyAbility(Abilities.COLOR_CHANGE) + .ability(Abilities.SHEER_FORCE); + await game.classicMode.startBattle([ Species.PIDGEOT ]); - game.scene.getEnemyPokemon()!.stats[Stat.DEF] = 10000; - expect(game.scene.getPlayerPokemon()!.formIndex).toBe(0); + const player = game.scene.getPlayerPokemon()!; + const enemy = game.scene.getEnemyPokemon()!; game.move.select(moveToUse); - await game.setTurnOrder([ BattlerIndex.PLAYER, BattlerIndex.ENEMY ]); - await game.phaseInterceptor.to(MoveEffectPhase, false); - - const phase = game.scene.getCurrentPhase() as MoveEffectPhase; - const move = phase.move.getMove(); - expect(move.id).toBe(Moves.CRUSH_CLAW); - - //Disable color change due to being hit by Sheer Force - const power = new NumberHolder(move.power); - const chance = new NumberHolder(move.chance); - const user = phase.getUserPokemon()!; - const target = phase.getFirstTarget()!; - const opponentType = target.getTypes()[0]; - - applyAbAttrs(MoveEffectChanceMultiplierAbAttr, user, null, false, chance, move, target, false); - applyPreAttackAbAttrs(MovePowerBoostAbAttr, user, target, move, false, power); - applyPostDefendAbAttrs(PostDefendTypeChangeAbAttr, target, user, move, HitResult.EFFECTIVE); - - expect(chance.value).toBe(0); - expect(power.value).toBe(move.power * 5461 / 4096); - expect(target.getTypes().length).toBe(2); - expect(target.getTypes()[0]).toBe(opponentType); + await game.move.forceHit(); + await game.phaseInterceptor.to("MoveEndPhase", false); + expect(player.battleData.abilitiesApplied.includes(Abilities.SHEER_FORCE)).toBeTruthy(); + expect(enemy.getTypes()).toEqual([ Type.BUG, Type.ROCK ]); }, 20000); it("Two Pokemon with abilities disabled by Sheer Force hitting each other should not cause a crash", async () => { diff --git a/src/test/abilities/tera_shell.test.ts b/src/test/abilities/tera_shell.test.ts index 01382d0fd9a..612a704a0ab 100644 --- a/src/test/abilities/tera_shell.test.ts +++ b/src/test/abilities/tera_shell.test.ts @@ -2,7 +2,6 @@ import { BattlerIndex } from "#app/battle"; import { Abilities } from "#app/enums/abilities"; import { Moves } from "#app/enums/moves"; import { Species } from "#app/enums/species"; -import { HitResult } from "#app/field/pokemon"; import GameManager from "#test/utils/gameManager"; import Phaser from "phaser"; import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; @@ -98,12 +97,12 @@ describe("Abilities - Tera Shell", () => { await game.classicMode.startBattle([ Species.CHARIZARD ]); const playerPokemon = game.scene.getPlayerPokemon()!; - vi.spyOn(playerPokemon, "apply"); + vi.spyOn(playerPokemon, "getMoveEffectiveness"); game.move.select(Moves.SPLASH); await game.phaseInterceptor.to("BerryPhase", false); - expect(playerPokemon.apply).toHaveLastReturnedWith(HitResult.EFFECTIVE); + expect(playerPokemon.getMoveEffectiveness).toHaveLastReturnedWith(1); expect(playerPokemon.hp).toBe(playerPokemon.getMaxHp() - 40); } ); @@ -116,7 +115,9 @@ describe("Abilities - Tera Shell", () => { await game.classicMode.startBattle([ Species.SNORLAX ]); const playerPokemon = game.scene.getPlayerPokemon()!; - vi.spyOn(playerPokemon, "apply"); + vi.spyOn(playerPokemon, "getMoveEffectiveness"); + + const enemyPokemon = game.scene.getEnemyPokemon()!; game.move.select(Moves.SPLASH); @@ -124,9 +125,9 @@ describe("Abilities - Tera Shell", () => { await game.move.forceHit(); for (let i = 0; i < 2; i++) { await game.phaseInterceptor.to("MoveEffectPhase"); - expect(playerPokemon.apply).toHaveLastReturnedWith(HitResult.NOT_VERY_EFFECTIVE); + expect(playerPokemon.getMoveEffectiveness).toHaveLastReturnedWith(0.5); } - expect(playerPokemon.apply).toHaveReturnedTimes(2); + expect(enemyPokemon.turnData.hitCount).toBe(2); } ); }); diff --git a/src/test/items/dire_hit.test.ts b/src/test/items/dire_hit.test.ts index 601552de7f1..70c25bf85ed 100644 --- a/src/test/items/dire_hit.test.ts +++ b/src/test/items/dire_hit.test.ts @@ -36,8 +36,7 @@ describe("Items - Dire Hit", () => { .enemyMoveset(Moves.SPLASH) .moveset([ Moves.POUND ]) .startingHeldItems([{ name: "DIRE_HIT" }]) - .battleType("single") - .disableCrits(); + .battleType("single"); }, 20000); diff --git a/src/test/items/leek.test.ts b/src/test/items/leek.test.ts index 901b353b3d3..65d46b43011 100644 --- a/src/test/items/leek.test.ts +++ b/src/test/items/leek.test.ts @@ -28,7 +28,6 @@ describe("Items - Leek", () => { .enemyMoveset([ Moves.SPLASH, Moves.SPLASH, Moves.SPLASH, Moves.SPLASH ]) .startingHeldItems([{ name: "LEEK" }]) .moveset([ Moves.TACKLE ]) - .disableCrits() .battleType("single"); }); diff --git a/src/test/items/scope_lens.test.ts b/src/test/items/scope_lens.test.ts index e39517ceae9..d780ca9b490 100644 --- a/src/test/items/scope_lens.test.ts +++ b/src/test/items/scope_lens.test.ts @@ -27,8 +27,7 @@ describe("Items - Scope Lens", () => { .enemyMoveset(Moves.SPLASH) .moveset([ Moves.POUND ]) .startingHeldItems([{ name: "SCOPE_LENS" }]) - .battleType("single") - .disableCrits(); + .battleType("single"); }, 20000); diff --git a/src/test/moves/tera_blast.test.ts b/src/test/moves/tera_blast.test.ts index 311ac0f0d0e..9d726768ceb 100644 --- a/src/test/moves/tera_blast.test.ts +++ b/src/test/moves/tera_blast.test.ts @@ -3,7 +3,6 @@ import { Stat } from "#enums/stat"; import { allMoves } from "#app/data/move"; import { Type } from "#enums/type"; import { Abilities } from "#app/enums/abilities"; -import { HitResult } from "#app/field/pokemon"; import { Moves } from "#enums/moves"; import { Species } from "#enums/species"; import GameManager from "#test/utils/gameManager"; @@ -47,21 +46,21 @@ describe("Moves - Tera Blast", () => { game.override .enemySpecies(Species.FURRET) .startingHeldItems([{ name: "TERA_SHARD", type: Type.FIGHTING }]); - await game.startBattle(); + await game.classicMode.startBattle(); const enemyPokemon = game.scene.getEnemyPokemon()!; - vi.spyOn(enemyPokemon, "apply"); + vi.spyOn(enemyPokemon, "getMoveEffectiveness"); game.move.select(Moves.TERA_BLAST); await game.setTurnOrder([ BattlerIndex.PLAYER, BattlerIndex.ENEMY ]); await game.phaseInterceptor.to("MoveEffectPhase"); - expect(enemyPokemon.apply).toHaveReturnedWith(HitResult.SUPER_EFFECTIVE); + expect(enemyPokemon.getMoveEffectiveness).toHaveReturnedWith(2); }, 20000); it("increases power if user is Stellar tera type", async () => { game.override.startingHeldItems([{ name: "TERA_SHARD", type: Type.STELLAR }]); - await game.startBattle(); + await game.classicMode.startBattle(); game.move.select(Moves.TERA_BLAST); await game.setTurnOrder([ BattlerIndex.PLAYER, BattlerIndex.ENEMY ]); @@ -73,23 +72,23 @@ describe("Moves - Tera Blast", () => { it("is super effective against terastallized targets if user is Stellar tera type", async () => { game.override.startingHeldItems([{ name: "TERA_SHARD", type: Type.STELLAR }]); - await game.startBattle(); + await game.classicMode.startBattle(); const enemyPokemon = game.scene.getEnemyPokemon()!; - vi.spyOn(enemyPokemon, "apply"); + vi.spyOn(enemyPokemon, "getMoveEffectiveness"); vi.spyOn(enemyPokemon, "isTerastallized").mockReturnValue(true); game.move.select(Moves.TERA_BLAST); await game.setTurnOrder([ BattlerIndex.PLAYER, BattlerIndex.ENEMY ]); await game.phaseInterceptor.to("MoveEffectPhase"); - expect(enemyPokemon.apply).toHaveReturnedWith(HitResult.SUPER_EFFECTIVE); + expect(enemyPokemon.getMoveEffectiveness).toHaveReturnedWith(2); }); // Currently abilities are bugged and can't see when a move's category is changed it.skip("uses the higher stat of the user's Atk and SpAtk for damage calculation", async () => { game.override.enemyAbility(Abilities.TOXIC_DEBRIS); - await game.startBattle(); + await game.classicMode.startBattle(); const playerPokemon = game.scene.getPlayerPokemon()!; playerPokemon.stats[Stat.ATK] = 100; @@ -102,7 +101,7 @@ describe("Moves - Tera Blast", () => { it("causes stat drops if user is Stellar tera type", async () => { game.override.startingHeldItems([{ name: "TERA_SHARD", type: Type.STELLAR }]); - await game.startBattle(); + await game.classicMode.startBattle(); const playerPokemon = game.scene.getPlayerPokemon()!; diff --git a/src/test/utils/helpers/moveHelper.ts b/src/test/utils/helpers/moveHelper.ts index 8d7c4bf8ef9..168151e77de 100644 --- a/src/test/utils/helpers/moveHelper.ts +++ b/src/test/utils/helpers/moveHelper.ts @@ -14,26 +14,24 @@ import { vi } from "vitest"; */ export class MoveHelper extends GameManagerHelper { /** - * Intercepts {@linkcode MoveEffectPhase} and mocks the - * {@linkcode MoveEffectPhase.hitCheck | hitCheck}'s return value to `true`. - * Used to force a move to hit. + * Intercepts {@linkcode MoveEffectPhase} and mocks the phase's move's + * accuracy to -1, guaranteeing a hit. */ public async forceHit(): Promise { await this.game.phaseInterceptor.to(MoveEffectPhase, false); const moveEffectPhase = this.game.scene.getCurrentPhase() as MoveEffectPhase; - vi.spyOn(moveEffectPhase.move.getMove(), "accuracy", "get").mockReturnValue(-1); + vi.spyOn(moveEffectPhase.move.getMove(), "calculateBattleAccuracy").mockReturnValue(-1); } /** - * Intercepts {@linkcode MoveEffectPhase} and mocks the - * {@linkcode MoveEffectPhase.hitCheck | hitCheck}'s return value to `false`. - * Used to force a move to miss. + * Intercepts {@linkcode MoveEffectPhase} and mocks the phase's move's accuracy + * to 0, guaranteeing a miss. * @param firstTargetOnly - Whether the move should force miss on the first target only, in the case of multi-target moves. */ public async forceMiss(firstTargetOnly: boolean = false): Promise { await this.game.phaseInterceptor.to(MoveEffectPhase, false); const moveEffectPhase = this.game.scene.getCurrentPhase() as MoveEffectPhase; - const accuracy = vi.spyOn(moveEffectPhase.move.getMove(), "accuracy", "get"); + const accuracy = vi.spyOn(moveEffectPhase.move.getMove(), "calculateBattleAccuracy"); if (firstTargetOnly) { accuracy.mockReturnValueOnce(0);