diff --git a/src/data/move.ts b/src/data/move.ts index f9c26735341..4dbc279daca 100644 --- a/src/data/move.ts +++ b/src/data/move.ts @@ -5476,8 +5476,12 @@ export class RandomMoveAttr extends CallMoveAttr { * Invalid moves are indicated by what is passed in to invalidMoves: @constant {invalidMetronomeMoves} */ apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): Promise { - const moveIds = Utils.getEnumValues(Moves).filter(m => !this.invalidMoves.includes(m) && !allMoves[m].name.endsWith(" (N)")); - const moveId = moveIds[user.randSeedInt(moveIds.length)]; + const moveIds = Utils.getEnumValues(Moves).map(m => !this.invalidMoves.includes(m) && !allMoves[m].name.endsWith(" (N)") ? m : Moves.NONE); + let moveId: Moves = Moves.NONE; + do { + moveId = moveIds[user.randSeedInt(moveIds.length)]; + } + while (moveId === Moves.NONE); return super.apply(user, target, allMoves[moveId], args); } } diff --git a/src/test/moves/metronome.test.ts b/src/test/moves/metronome.test.ts new file mode 100644 index 00000000000..14ff81c6b71 --- /dev/null +++ b/src/test/moves/metronome.test.ts @@ -0,0 +1,65 @@ +import { SemiInvulnerableTag } from "#app/data/battler-tags"; +import { Abilities } from "#app/enums/abilities"; +import { Moves } from "#enums/moves"; +import { Species } from "#enums/species"; +import GameManager from "#test/utils/gameManager"; +import Phaser from "phaser"; +import { afterEach, beforeAll, beforeEach, describe, it, expect, vi } from "vitest"; + +describe("Moves - Metronome", () => { + let phaserGame: Phaser.Game; + let game: GameManager; + + beforeAll(() => { + phaserGame = new Phaser.Game({ + type: Phaser.HEADLESS, + }); + }); + + afterEach(() => { + game.phaseInterceptor.restoreOg(); + }); + + beforeEach(() => { + game = new GameManager(phaserGame); + game.override + .moveset([Moves.METRONOME]) + .battleType("single") + .startingLevel(100) + .enemyLevel(100) + .enemySpecies(Species.SHUCKLE) + .enemyMoveset(Moves.SPLASH) + .enemyAbility(Abilities.BALL_FETCH); + }); + + it("should have one semi-invulnerable turn and deal damage on the second turn when a semi-invulnerable move is called", async () => { + await game.classicMode.startBattle([Species.REGIELEKI]); + const player = game.scene.getPlayerPokemon()!; + const enemy = game.scene.getEnemyPokemon()!; + vi.spyOn(player, "randSeedInt").mockReturnValue(Moves.DIVE); + + game.move.select(Moves.METRONOME); + await game.toNextTurn(); + + expect(player.getTag(SemiInvulnerableTag)).toBeTruthy(); + await game.move.forceHit(); // Force hit on Dive, required due to randSeedInt mock making hitCheck return false every time. + + await game.toNextTurn(); + expect(player.getTag(SemiInvulnerableTag)).toBeFalsy(); + expect(enemy.isFullHp()).toBeFalsy(); + }); + + // FAILS UNTIL KEV'S MOVE PHASE REFACTOR IS PUT IN + // it("should apply secondary effects of a move", async () => { + // await game.classicMode.startBattle([Species.REGIELEKI]); + // const player = game.scene.getPlayerPokemon()!; + // vi.spyOn(player, "randSeedInt").mockReturnValue(Moves.WOOD_HAMMER); + + // game.move.select(Moves.METRONOME); + // await game.phaseInterceptor.to("MoveEffectPhase"); // Metronome has its own MoveEffectPhase, followed by Wood Hammer's MoveEffectPhase + // await game.move.forceHit(); // Calls forceHit on Wood Hammer's MoveEffectPhase, required due to randSeedInt mock making hitCheck return false every time. + // await game.toNextTurn(); + + // expect(player.isFullHp()).toBeFalsy(); + // }); +});