From 365e77d32878ff927965d583d2408de0ea234ce3 Mon Sep 17 00:00:00 2001 From: Christopher Schmidt Date: Sat, 26 Oct 2024 15:34:38 -0400 Subject: [PATCH] Creates moveHistory in Battle to track all moves used, adjusts mirror move to use this, writes unit tests --- src/battle.ts | 3 ++ src/data/move.ts | 10 ++-- src/phases/move-phase.ts | 4 ++ src/test/moves/mirror_move.test.ts | 86 ++++++++++++++++++++++++++++++ 4 files changed, 100 insertions(+), 3 deletions(-) create mode 100644 src/test/moves/mirror_move.test.ts diff --git a/src/battle.ts b/src/battle.ts index ee293abb0d9..49bb2d9f042 100644 --- a/src/battle.ts +++ b/src/battle.ts @@ -88,6 +88,9 @@ export default class Battle { public enemyFaints: number = 0; public playerFaintsHistory: FaintLogEntry[] = []; public enemyFaintsHistory: FaintLogEntry[] = []; + /** The list of moves used since the beginning of the battle */ + public moveHistory: TurnMove[] = []; + public mysteryEncounterType?: MysteryEncounterType; /** If the current battle is a Mystery Encounter, this will always be defined */ diff --git a/src/data/move.ts b/src/data/move.ts index 8161d79f9ed..26d457c1fb7 100644 --- a/src/data/move.ts +++ b/src/data/move.ts @@ -6502,14 +6502,18 @@ export class CopyMoveAttr extends CallMoveAttr { } apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): Promise { - const lastMove = this.mirrorMove ? user.turnData.attacksReceived[0]?.move : user.scene.currentBattle.lastMove; - return super.apply(user, target, allMoves[lastMove], args); + if (this.mirrorMove) { + const lastMove = user.scene.currentBattle.moveHistory.filter(m => m.targets.includes(user.getBattlerIndex()))[0].move; + return super.apply(user, target, allMoves[lastMove], args); + } else { + return super.apply(user, target, allMoves[user.scene.currentBattle.lastMove], args); + } } getCondition(): MoveConditionFunc { return (user, target, move) => { if (this.mirrorMove) { - if (user.turnData.attacksReceived.length === 0) { + if (user.scene.currentBattle.moveHistory.filter(m => m.targets.includes(user.getBattlerIndex())).length === 0) { return false; } } else if (user.scene.currentBattle.lastMove === undefined) { diff --git a/src/phases/move-phase.ts b/src/phases/move-phase.ts index b1504882f30..2529a8f3c20 100644 --- a/src/phases/move-phase.ts +++ b/src/phases/move-phase.ts @@ -274,6 +274,10 @@ export class MovePhase extends BattlePhase { // The last move used is unaffected by moves that fail if (success) { this.scene.currentBattle.lastMove = this.move.moveId; + this.scene.currentBattle.moveHistory.unshift({ + move: this.move.moveId, + targets: this.targets + }); } } diff --git a/src/test/moves/mirror_move.test.ts b/src/test/moves/mirror_move.test.ts new file mode 100644 index 00000000000..b12d59ef508 --- /dev/null +++ b/src/test/moves/mirror_move.test.ts @@ -0,0 +1,86 @@ +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 } from "vitest"; + +describe("Moves - Mirror Move", () => { + 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.MIRROR_MOVE, Moves.SPLASH ]) + .ability(Abilities.BALL_FETCH) + .battleType("single") + .disableCrits() + .enemySpecies(Species.MAGIKARP) + .enemyAbility(Abilities.BALL_FETCH) + .enemyMoveset(Moves.SPLASH); + }); + + it("should use the last move targeted at the user", async () => { + game.override.enemyMoveset(Moves.TACKLE); + await game.classicMode.startBattle([ Species.FEEBAS ]); + + game.move.select(Moves.MIRROR_MOVE); + await game.setTurnOrder([ BattlerIndex.ENEMY, BattlerIndex.PLAYER ]); + await game.toNextTurn(); + + expect(game.scene.getEnemyPokemon()!.isFullHp()).toBeFalsy(); + }); + + it("should apply secondary effects of a move", async () => { + game.override.enemyMoveset(Moves.ACID_SPRAY); + await game.classicMode.startBattle([ Species.FEEBAS ]); + + game.move.select(Moves.MIRROR_MOVE); + await game.setTurnOrder([ BattlerIndex.ENEMY, BattlerIndex.PLAYER ]); + await game.toNextTurn(); + + expect(game.scene.getEnemyPokemon()!.getStatStage(Stat.SPDEF)).toBe(-2); + }); + + it("should fail if the user has never been targeted", { repeats: 10 }, async () => { + game.override + .battleType("double") + .startingLevel(100) + .enemyMoveset([ Moves.TACKLE, Moves.SPLASH ]); + await game.classicMode.startBattle([ Species.FEEBAS, Species.MAGIKARP ]); + + game.move.select(Moves.MIRROR_MOVE); + game.move.select(Moves.SPLASH, 1); + await game.forceEnemyMove(Moves.TACKLE, BattlerIndex.PLAYER_2); + await game.forceEnemyMove(Moves.SPLASH); + await game.setTurnOrder([ BattlerIndex.ENEMY, BattlerIndex.ENEMY_2, BattlerIndex.PLAYER_2, BattlerIndex.PLAYER ]); + await game.toNextTurn(); + + expect(game.scene.getPlayerField()![0].getLastXMoves()[0].result).toBe(MoveResult.FAIL); + }); + + it("should copy status moves that target the user", async () => { + game.override.enemyMoveset(Moves.GROWL); + await game.classicMode.startBattle([ Species.FEEBAS ]); + + game.move.select(Moves.MIRROR_MOVE); + await game.setTurnOrder([ BattlerIndex.ENEMY, BattlerIndex.PLAYER ]); + await game.toNextTurn(); + + expect(game.scene.getEnemyPokemon()!.getStatStage(Stat.ATK)).toBe(-1); + }); +});