diff --git a/src/phases/move-phase.ts b/src/phases/move-phase.ts index c2fd975db08..b1504882f30 100644 --- a/src/phases/move-phase.ts +++ b/src/phases/move-phase.ts @@ -246,11 +246,6 @@ export class MovePhase extends BattlePhase { this.scene.eventTarget.dispatchEvent(new MoveUsedEvent(this.pokemon?.id, this.move.getMove(), this.move.ppUsed)); } - // Update the battle's "last move" pointer, unless we're currently mimicking a move. - if (!allMoves[this.move.moveId].hasAttr(CopyMoveAttr)) { - this.scene.currentBattle.lastMove = this.move.moveId; - } - /** * Determine if the move is successful (meaning that its damage/effects can be attempted) * by checking that all of the following are true: @@ -274,6 +269,14 @@ export class MovePhase extends BattlePhase { const success = passesConditions && !failedDueToWeather && !failedDueToTerrain; + // Update the battle's "last move" pointer, unless we're currently mimicking a move. + if (!allMoves[this.move.moveId].hasAttr(CopyMoveAttr)) { + // The last move used is unaffected by moves that fail + if (success) { + this.scene.currentBattle.lastMove = this.move.moveId; + } + } + /** * If the move has not failed, trigger ability-based user type changes and then execute it. * diff --git a/src/test/moves/assist.test.ts b/src/test/moves/assist.test.ts index ea1cefc23e9..0e665cabd74 100644 --- a/src/test/moves/assist.test.ts +++ b/src/test/moves/assist.test.ts @@ -30,6 +30,7 @@ describe("Moves - Assist", () => { .battleType("single") .disableCrits() .enemySpecies(Species.MAGIKARP) + .enemyLevel(100) .enemyAbility(Abilities.BALL_FETCH) .enemyMoveset(Moves.SPLASH); }); @@ -39,7 +40,6 @@ describe("Moves - Assist", () => { .battleType("double") .enemyMoveset(Moves.SWORDS_DANCE); await game.classicMode.startBattle([ Species.FEEBAS, Species.SHUCKLE ]); - const leftPlayer = game.scene.getPlayerPokemon()!; game.move.select(Moves.ASSIST, 0); game.move.select(Moves.SKETCH, 1); @@ -47,7 +47,7 @@ describe("Moves - Assist", () => { // Player_2 uses Sketch, copies Swords Dance, Player_1 uses Assist, uses Player_2's Sketched Swords Dance await game.toNextTurn(); - expect(leftPlayer.getStatStage(Stat.ATK)).toBe(2); // Stat raised from Assist -> Swords Dance + expect(game.scene.getPlayerPokemon()!.getStatStage(Stat.ATK)).toBe(2); // Stat raised from Assist -> Swords Dance }); it("should fail if there are no usable moves", async () => { @@ -57,4 +57,14 @@ describe("Moves - Assist", () => { await game.toNextTurn(); expect(game.scene.getPlayerPokemon()!.getLastXMoves()[0].result).toBe(MoveResult.FAIL); }); + + it("should apply secondary effects of a move", async () => { + game.override.moveset([ Moves.ASSIST, Moves.WOOD_HAMMER, Moves.WOOD_HAMMER, Moves.WOOD_HAMMER ]); + await game.classicMode.startBattle([ Species.FEEBAS ]); + + game.move.select(Moves.ASSIST, 0); + await game.toNextTurn(); + + expect(game.scene.getPlayerPokemon()!.isFullHp()).toBeFalsy(); // should receive recoil damage from Wood Hammer + }); }); diff --git a/src/test/moves/copycat.test.ts b/src/test/moves/copycat.test.ts new file mode 100644 index 00000000000..60f9c5e3f68 --- /dev/null +++ b/src/test/moves/copycat.test.ts @@ -0,0 +1,88 @@ +import { BattlerIndex } from "#app/battle"; +import { Stat } from "#app/enums/stat"; +import { MoveResult } from "#app/field/pokemon"; +import { Abilities } from "#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, expect, it, vi } from "vitest"; + +describe("Moves - Copycat", () => { + 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.COPYCAT, Moves.SPIKY_SHIELD, Moves.SWORDS_DANCE, Moves.SPLASH ]) + .ability(Abilities.BALL_FETCH) + .battleType("single") + .disableCrits() + .starterSpecies(Species.FEEBAS) + .enemySpecies(Species.MAGIKARP) + .enemyAbility(Abilities.BALL_FETCH) + .enemyMoveset(Moves.SPLASH); + }); + + it("should copy the last move successfully executed", async () => { + game.override.enemyMoveset(Moves.SUCKER_PUNCH); + await game.classicMode.startBattle(); + + game.move.select(Moves.SWORDS_DANCE); + await game.toNextTurn(); + + game.move.select(Moves.COPYCAT); // Last successful move should be Swords Dance + await game.toNextTurn(); + + expect(game.scene.getPlayerPokemon()!.getStatStage(Stat.ATK)).toBe(4); + }); + + it("should fail when the last move used is not a valid Copycat move", async () => { + game.override.enemyMoveset(Moves.PROTECT); // Protect is not a valid move for Copycat to copy + await game.classicMode.startBattle(); + + game.move.select(Moves.SPIKY_SHIELD); // Spiky Shield is not a valid move for Copycat to copy + await game.toNextTurn(); + + game.move.select(Moves.COPYCAT); + await game.toNextTurn(); + + expect(game.scene.getPlayerPokemon()!.getLastXMoves()[0].result).toBe(MoveResult.FAIL); + }); + + it("should copy the called move when the last move successfully calls another", async () => { + game.override + .moveset([ Moves.SPLASH, Moves.METRONOME ]) + .enemyMoveset(Moves.COPYCAT); + await game.classicMode.startBattle(); + vi.spyOn(game.scene.getPlayerPokemon()!, "randSeedInt").mockReturnValue(Moves.SWORDS_DANCE); + + game.move.select(Moves.METRONOME); + await game.setTurnOrder([ BattlerIndex.PLAYER, BattlerIndex.ENEMY ]); // Player moves first, so enemy can copy Swords Dance + await game.toNextTurn(); + + expect(game.scene.getEnemyPokemon()!.getStatStage(Stat.ATK)).toBe(2); + }); + + it("should apply secondary effects of a move", async () => { + game.override.enemyMoveset(Moves.ACID_SPRAY); // Secondary effect lowers SpDef by 2 stages + await game.classicMode.startBattle(); + + game.move.select(Moves.COPYCAT); + await game.setTurnOrder([ BattlerIndex.ENEMY, BattlerIndex.PLAYER ]); + await game.toNextTurn(); + + expect(game.scene.getEnemyPokemon()!.getStatStage(Stat.SPDEF)).toBe(-2); + }); +});