diff --git a/src/data/move.ts b/src/data/move.ts index 670f881b85c..94027dc1e42 100644 --- a/src/data/move.ts +++ b/src/data/move.ts @@ -6784,10 +6784,8 @@ export class RepeatMoveAttr extends MoveEffectAttr { getCondition(): MoveConditionFunc { return (user, target, move) => { - // TODO: Confirm behavior of instructing move known by target but called by another move const lastMove = target.getLastXMoves(-1).find(m => m.move !== Moves.NONE); const movesetMove = target.getMoveset().find(m => m?.moveId === lastMove?.move); - // TODO: Add a way of adding moves to list procedurally rather than a pre-defined blacklist const uninstructableMoves = [ // Locking/Continually Executed moves Moves.OUTRAGE, @@ -6844,7 +6842,7 @@ export class RepeatMoveAttr extends MoveEffectAttr { ]; if (!lastMove?.move // no move to instruct - || !movesetMove // called move not in target's moveset (dancer, forgetting the move, etc.) + || !movesetMove // called move not in target's moveset (forgetting the move, etc.) || movesetMove.ppUsed === movesetMove.getMovePp() // move out of pp || allMoves[lastMove.move].isChargingMove() // called move is a charging/recharging move || uninstructableMoves.includes(lastMove.move)) { // called move is in the banlist diff --git a/src/overrides.ts b/src/overrides.ts index 85be47d95cc..563981e43dd 100644 --- a/src/overrides.ts +++ b/src/overrides.ts @@ -21,7 +21,7 @@ import { WeatherType } from "#enums/weather-type"; * * Any override added here will be used instead of the value in {@linkcode DefaultOverrides} * - * If an override name starts with "STARTING", it will apply when a new run begins + * If an override name starts with "STARTING", it will only apply when a new run begins. * * @example * ``` @@ -31,14 +31,19 @@ import { WeatherType } from "#enums/weather-type"; * } * ``` */ -const overrides = {} satisfies Partial>; +const overrides = { + OPP_MOVESET_OVERRIDE: Moves.INSTRUCT, + XP_MULTIPLIER_OVERRIDE: 50, + BATTLE_TYPE_OVERRIDE: "single", + ITEM_REWARD_OVERRIDE: [{ name: "MEMORY_MUSHROOM", count: 1 }] +} satisfies Partial>; /** * If you need to add Overrides values for local testing do that inside {@linkcode overrides} * --- * Defaults for Overrides that are used when testing different in game situations * - * If an override name starts with "STARTING", it will apply when a new run begins + * If an override name starts with "STARTING", it will only apply when a new run begins. */ class DefaultOverrides { // ----------------- diff --git a/src/phases/learn-move-phase.ts b/src/phases/learn-move-phase.ts index fefda384092..54ed08ce38d 100644 --- a/src/phases/learn-move-phase.ts +++ b/src/phases/learn-move-phase.ts @@ -25,7 +25,7 @@ export enum LearnMoveType { export class LearnMovePhase extends PlayerPartyMemberPokemonPhase { private moveId: Moves; private messageMode: Mode; - private learnMoveType; + private learnMoveType: LearnMoveType; private cost: number; constructor(scene: BattleScene, partyMemberIndex: integer, moveId: Moves, learnMoveType: LearnMoveType = LearnMoveType.LEARN_MOVE, cost: number = -1) { diff --git a/src/test/moves/instruct.test.ts b/src/test/moves/instruct.test.ts index beca1c7484b..4222e6efcae 100644 --- a/src/test/moves/instruct.test.ts +++ b/src/test/moves/instruct.test.ts @@ -3,6 +3,7 @@ import { Button } from "#app/enums/buttons"; import type Pokemon from "#app/field/pokemon"; import { MoveResult } from "#app/field/pokemon"; import type { MovePhase } from "#app/phases/move-phase"; +import { Mode } from "#app/ui/ui"; import { Abilities } from "#enums/abilities"; import { Moves } from "#enums/moves"; import { Species } from "#enums/species"; @@ -39,18 +40,18 @@ describe("Moves - Instruct", () => { .enemyLevel(100) .startingLevel(100) .ability(Abilities.BALL_FETCH) - .moveset([ Moves.INSTRUCT, Moves.SONIC_BOOM, Moves.SPLASH, Moves.TORMENT ]) .disableCrits(); }); it("should repeat target's last used move", async () => { + game.override.moveset(Moves.INSTRUCT); await game.classicMode.startBattle([ Species.AMOONGUSS ]); const enemy = game.scene.getEnemyPokemon()!; game.move.changeMoveset(enemy, Moves.SONIC_BOOM); - game.move.select(Moves.INSTRUCT, BattlerIndex.PLAYER, BattlerIndex.ENEMY); - await game.forceEnemyMove(Moves.SONIC_BOOM, BattlerIndex.PLAYER); + game.move.select(Moves.INSTRUCT); + await game.forceEnemyMove(Moves.SONIC_BOOM); await game.setTurnOrder([ BattlerIndex.ENEMY, BattlerIndex.PLAYER ]); await game.phaseInterceptor.to("MovePhase"); // enemy attacks us @@ -65,11 +66,12 @@ describe("Moves - Instruct", () => { expect(currentPhase.move.moveId).toBe(Moves.SONIC_BOOM); await game.phaseInterceptor.to("TurnEndPhase", false); - expect(game.scene.getPlayerPokemon()?.getInverseHp()).toBe(40); instructSuccess(enemy, Moves.SONIC_BOOM); + expect(game.scene.getPlayerPokemon()?.getInverseHp()).toBe(40); }); it("should repeat enemy's move through substitute", async () => { + game.override.moveset([ Moves.INSTRUCT, Moves.SPLASH ]); await game.classicMode.startBattle([ Species.AMOONGUSS ]); const enemy = game.scene.getEnemyPokemon()!; @@ -85,34 +87,34 @@ describe("Moves - Instruct", () => { await game.setTurnOrder([ BattlerIndex.ENEMY, BattlerIndex.PLAYER ]); await game.phaseInterceptor.to("TurnEndPhase", false); - expect(game.scene.getPlayerPokemon()?.getInverseHp()).toBe(40); instructSuccess(game.scene.getEnemyPokemon()!, Moves.SONIC_BOOM); + expect(game.scene.getPlayerPokemon()?.getInverseHp()).toBe(40); }); it("should repeat ally's attack on enemy", async () => { game.override .battleType("double") - .moveset([]); + .enemyMoveset(Moves.SPLASH); await game.classicMode.startBattle([ Species.AMOONGUSS, Species.SHUCKLE ]); const [ amoonguss, shuckle ] = game.scene.getPlayerField(); - game.move.changeMoveset(amoonguss, Moves.INSTRUCT); - game.move.changeMoveset(shuckle, Moves.SONIC_BOOM); + game.move.changeMoveset(amoonguss, [ Moves.INSTRUCT, Moves.SONIC_BOOM ]); + game.move.changeMoveset(shuckle, [ Moves.INSTRUCT, Moves.SONIC_BOOM ]); game.move.select(Moves.INSTRUCT, BattlerIndex.PLAYER, BattlerIndex.PLAYER_2); game.move.select(Moves.SONIC_BOOM, BattlerIndex.PLAYER_2, BattlerIndex.ENEMY); - await game.forceEnemyMove(Moves.SPLASH); - await game.forceEnemyMove(Moves.SPLASH); await game.setTurnOrder([ BattlerIndex.PLAYER_2, BattlerIndex.PLAYER, BattlerIndex.ENEMY, BattlerIndex.ENEMY_2 ]); await game.phaseInterceptor.to("TurnEndPhase", false); - expect(game.scene.getEnemyField()[0].getInverseHp()).toBe(40); instructSuccess(shuckle, Moves.SONIC_BOOM); + expect(game.scene.getEnemyField()[0].getInverseHp()).toBe(40); }); // TODO: Enable test case once gigaton hammer (and blood moon) are reworked it.todo("should repeat enemy's Gigaton Hammer", async () => { - game.override.enemyLevel(5); + game.override + .moveset(Moves.INSTRUCT) + .enemyLevel(5); await game.classicMode.startBattle([ Species.AMOONGUSS ]); const enemy = game.scene.getEnemyPokemon()!; @@ -138,7 +140,8 @@ describe("Moves - Instruct", () => { game.move.select(Moves.INSTRUCT); await game.setTurnOrder([ BattlerIndex.PLAYER, BattlerIndex.ENEMY ]); await game.phaseInterceptor.to("MovePhase"); - await game.move.forceStatusActivation(true); // force enemy's instructed move to bork and then immediately thaw out + // force enemy's instructed move to bork and then immediately thaw out + await game.move.forceStatusActivation(true); await game.move.forceStatusActivation(false); await game.phaseInterceptor.to("TurnEndPhase", false); @@ -147,26 +150,10 @@ describe("Moves - Instruct", () => { expect(game.scene.getPlayerPokemon()?.getInverseHp()).toBe(40); }); - it("should repeat move with no targets, but move should immediately fail", async () => { - game.override.battleType("double"); - await game.classicMode.startBattle([ Species.BRUTE_BONNET, Species.VOLCARONA ]); - - const [ , volcarona ] = game.scene.getPlayerField(); - game.move.changeMoveset(volcarona, [ Moves.INSTRUCT, Moves.SPLASH, Moves.BUG_BITE ]); - game.move.select(Moves.SPLASH, BattlerIndex.PLAYER); - game.move.select(Moves.BUG_BITE, BattlerIndex.PLAYER_2, BattlerIndex.PLAYER); - await game.forceEnemyMove(Moves.INSTRUCT, BattlerIndex.PLAYER_2); - await game.forceEnemyMove(Moves.SPLASH); - await game.setTurnOrder([ BattlerIndex.PLAYER, BattlerIndex.PLAYER_2, BattlerIndex.ENEMY, BattlerIndex.ENEMY_2 ]); - await game.toNextTurn(); - - // attack #2 failed due to brute bonnet having already fainted - instructSuccess(volcarona, Moves.BUG_BITE); - expect(volcarona.getLastXMoves(-1)[0].result).toBe(MoveResult.FAIL); - }); - it("should not repeat enemy's out of pp move", async () => { - game.override.enemySpecies(Species.UNOWN); + game.override + .moveset(Moves.INSTRUCT) + .enemySpecies(Species.UNOWN); await game.classicMode.startBattle([ Species.AMOONGUSS ]); const enemyPokemon = game.scene.getEnemyPokemon()!; @@ -179,13 +166,15 @@ describe("Moves - Instruct", () => { await game.setTurnOrder([ BattlerIndex.ENEMY, BattlerIndex.PLAYER ]); await game.phaseInterceptor.to("TurnEndPhase", false); - const playerMove = game.scene.getPlayerPokemon()!.getLastXMoves()!; - expect(playerMove[0].result).toBe(MoveResult.FAIL); + const playerMoves = game.scene.getPlayerPokemon()!.getLastXMoves(-1)!; + expect(playerMoves[0].result).toBe(MoveResult.FAIL); expect(enemyPokemon.getMoveHistory().length).toBe(1); }); it("should fail if no move has yet been used by target", async () => { - game.override.enemyMoveset(Moves.SPLASH); + game.override + .moveset(Moves.INSTRUCT) + .enemyMoveset(Moves.SPLASH); await game.classicMode.startBattle([ Species.AMOONGUSS ]); game.move.select(Moves.INSTRUCT); @@ -209,25 +198,28 @@ describe("Moves - Instruct", () => { game.move.select(Moves.INSTRUCT, BattlerIndex.PLAYER, BattlerIndex.ENEMY); game.move.select(Moves.DISABLE, BattlerIndex.PLAYER_2, BattlerIndex.ENEMY); await game.forceEnemyMove(Moves.SONIC_BOOM, BattlerIndex.PLAYER); - await game.forceEnemyMove(Moves.SPLASH); await game.setTurnOrder([ BattlerIndex.ENEMY, BattlerIndex.PLAYER_2, BattlerIndex.PLAYER, BattlerIndex.ENEMY_2 ]); await game.phaseInterceptor.to("TurnEndPhase", false); expect(game.scene.getPlayerField()[0].getLastXMoves()[0].result).toBe(MoveResult.SUCCESS); - const enemyMove = game.scene.getEnemyPokemon()!.getLastXMoves()[0]; + const enemyMove = game.scene.getEnemyField()[0]!.getLastXMoves()[0]; expect(enemyMove.result).toBe(MoveResult.FAIL); - expect(game.scene.getEnemyPokemon()!.getMoveset().find(m => m?.moveId === Moves.SONIC_BOOM)?.ppUsed).toBe(1); + expect(game.scene.getEnemyField()[0].getMoveset().find(m => m?.moveId === Moves.SONIC_BOOM)?.ppUsed).toBe(1); }); it("should allow for dancer copying of instructed dance move", async () => { - game.override.battleType("double"); + game.override + .battleType("double") + .enemyMoveset([ Moves.INSTRUCT, Moves.SPLASH ]); await game.classicMode.startBattle([ Species.ORICORIO, Species.VOLCARONA ]); - const [ , volcarona ] = game.scene.getPlayerField(); + const [ oricorio, volcarona ] = game.scene.getPlayerField(); + game.move.changeMoveset(oricorio, Moves.SPLASH); game.move.changeMoveset(volcarona, Moves.FIERY_DANCE); + game.move.select(Moves.SPLASH, BattlerIndex.PLAYER); game.move.select(Moves.FIERY_DANCE, BattlerIndex.PLAYER_2, BattlerIndex.ENEMY); - await game.forceEnemyMove(Moves.INSTRUCT, BattlerIndex.PLAYER); + await game.forceEnemyMove(Moves.INSTRUCT, BattlerIndex.PLAYER_2); await game.forceEnemyMove(Moves.SPLASH); await game.setTurnOrder([ BattlerIndex.PLAYER, BattlerIndex.PLAYER_2, BattlerIndex.ENEMY, BattlerIndex.ENEMY_2 ]); await game.phaseInterceptor.to("TurnEndPhase", false); @@ -237,36 +229,36 @@ describe("Moves - Instruct", () => { }); it("should not repeat enemy's move through protect", async () => { + game.override.moveset([ Moves.INSTRUCT ]); await game.classicMode.startBattle([ Species.AMOONGUSS ]); - const MoveToUse = Moves.PROTECT; - const enemyPokemon = game.scene.getEnemyPokemon()!; - game.move.changeMoveset(enemyPokemon, MoveToUse); + const enemy = game.scene.getEnemyPokemon()!; + game.move.changeMoveset(enemy, Moves.PROTECT); game.move.select(Moves.INSTRUCT); - await game.forceEnemyMove(Moves.PROTECT); await game.setTurnOrder([ BattlerIndex.ENEMY, BattlerIndex.PLAYER ]); await game.phaseInterceptor.to("TurnEndPhase", false); - expect(enemyPokemon.getLastXMoves(-1)[0].move).toBe(Moves.PROTECT); - expect(enemyPokemon.getLastXMoves(-1)[1]).toBeUndefined(); // undefined because protect failed - expect(enemyPokemon.getMoveset().find(m => m?.moveId === Moves.PROTECT)?.ppUsed).toBe(1); + expect(enemy.getLastXMoves(-1)[0].move).toBe(Moves.PROTECT); + expect(enemy.getLastXMoves(-1)[1]).toBeUndefined(); // undefined because instruct failed and didn't repeat + expect(enemy.getMoveset().find(m => m?.moveId === Moves.PROTECT)?.ppUsed).toBe(1); }); it("should not repeat enemy's charging move", async () => { game.override - .enemyMoveset([ Moves.SONIC_BOOM, Moves.HYPER_BEAM ]) - .enemyLevel(5); + .moveset([ Moves.INSTRUCT ]) + .enemyMoveset([ Moves.SONIC_BOOM, Moves.HYPER_BEAM ]); await game.classicMode.startBattle([ Species.SHUCKLE ]); const player = game.scene.getPlayerPokemon()!; - const enemyPokemon = game.scene.getEnemyPokemon()!; - enemyPokemon.battleSummonData.moveHistory = [{ move: Moves.SONIC_BOOM, targets: [ BattlerIndex.PLAYER ], result: MoveResult.SUCCESS, virtual: false }]; + const enemy = game.scene.getEnemyPokemon()!; + enemy.battleSummonData.moveHistory = [{ move: Moves.SONIC_BOOM, targets: [ BattlerIndex.PLAYER ], result: MoveResult.SUCCESS, virtual: false }]; game.move.select(Moves.INSTRUCT); await game.forceEnemyMove(Moves.HYPER_BEAM); await game.setTurnOrder([ BattlerIndex.ENEMY, BattlerIndex.PLAYER ]); await game.toNextTurn(); + // instruct fails at copying last move due to charging turn (rather than instructing sonic boom) expect(player.getLastXMoves()[0].result).toBe(MoveResult.FAIL); game.move.select(Moves.INSTRUCT); @@ -276,42 +268,52 @@ describe("Moves - Instruct", () => { expect(player.getLastXMoves()[0].result).toBe(MoveResult.FAIL); }); + // TODO: Clean test code up once learn move utility function is added + // to reduce jankiness and decrease likelihood of future borks it("should not repeat move since forgotten by target", async () => { game.override - .battleType("double") - .startingWave(199) // disables level cap - .enemyLevel(50) - .startingLevel(62) - .enemySpecies(Species.WURMPLE); // 1 level before learning hydro pump - await game.classicMode.startBattle([ Species.LUGIA ]); - const lugia = game.scene.getPlayerPokemon()!; - lugia.addExp(14647); + .enemyLevel(5) + .xpMultiplier(50) + .enemySpecies(Species.WURMPLE) + .enemyMoveset(Moves.INSTRUCT); + await game.classicMode.startBattle([ Species.REGIELEKI ]); - game.move.changeMoveset(lugia, [ Moves.BRAVE_BIRD, Moves.SPLASH, Moves.AEROBLAST, Moves.FURY_CUTTER ]); + const regieleki = game.scene.getPlayerPokemon()!; + // fill out moveset with random moves + game.move.changeMoveset(regieleki, [ Moves.ELECTRO_DRIFT, Moves.SPLASH, Moves.ICE_BEAM, Moves.ANCIENT_POWER ]); - game.move.select(Moves.BRAVE_BIRD, BattlerIndex.PLAYER, BattlerIndex.ENEMY); - await game.forceEnemyMove(Moves.SPLASH); - await game.forceEnemyMove(Moves.INSTRUCT, BattlerIndex.PLAYER); - await game.phaseInterceptor.to("LearnMovePhase", false); - while (game.isCurrentPhase("LearnMovePhase")) { + game.move.select(Moves.ELECTRO_DRIFT); + await game.setTurnOrder([ BattlerIndex.PLAYER, BattlerIndex.ENEMY ]); + game.phaseInterceptor.to("FaintPhase"); + // setup macro to mash enter and learn hydro pump in slot 1 + game.onNextPrompt("LearnMovePhase", Mode.CONFIRM, () => { game.scene.ui.getHandler().processInput(Button.ACTION); // mash enter to learn level up move - } - await game.phaseInterceptor.to("TurnEndPhase", false); + game.onNextPrompt("LearnMovePhase", Mode.SUMMARY, () => { + game.scene.ui.getHandler().processInput(Button.ACTION); + game.onNextPrompt("LearnMovePhase", Mode.CONFIRM, () => { + game.scene.ui.getHandler().processInput(Button.ACTION); + }); + }); + }); - expect(lugia.getLastXMoves()[0].result).toBe(MoveResult.SUCCESS); + await game.toNextWave(); + + game.move.select(Moves.SPLASH); + await game.setTurnOrder([ BattlerIndex.ENEMY, BattlerIndex.PLAYER ]); + await game.phaseInterceptor.to("TurnEndPhase", false); expect(game.scene.getEnemyField()[0].getLastXMoves()[0].result).toBe(MoveResult.FAIL); }); it("should disregard priority of instructed move on use", async () => { game.override .enemyMoveset([ Moves.SPLASH, Moves.WHIRLWIND ]) - .disableTrainerWaves(); + .moveset(Moves.INSTRUCT); await game.classicMode.startBattle([ Species.LUCARIO, Species.BANETTE ]); const enemyPokemon = game.scene.getEnemyPokemon()!; enemyPokemon.battleSummonData.moveHistory = [{ move: Moves.WHIRLWIND, targets: [ BattlerIndex.PLAYER ], result: MoveResult.SUCCESS, virtual: false }]; - game.move.select(Moves.INSTRUCT, BattlerIndex.PLAYER, BattlerIndex.ENEMY_2); + game.move.select(Moves.INSTRUCT); await game.forceEnemyMove(Moves.SPLASH); await game.phaseInterceptor.to("TurnEndPhase", false); @@ -322,14 +324,13 @@ describe("Moves - Instruct", () => { expect(game.scene.getPlayerPokemon()?.species.speciesId).toBe(Species.BANETTE); }); - it("should respect moves' original priority in psychic terrain", async () => { + it("should respect moves' original priority for psychic terrain", async () => { game.override. battleType("double") .moveset([ Moves.QUICK_ATTACK, Moves.SPLASH, Moves.INSTRUCT ]) .enemyMoveset([ Moves.SPLASH, Moves.PSYCHIC_TERRAIN ]); await game.classicMode.startBattle([ Species.BANETTE, Species.KLEFKI ]); - const banette = game.scene.getPlayerPokemon(); game.move.select(Moves.QUICK_ATTACK, BattlerIndex.PLAYER, BattlerIndex.ENEMY); // succeeds due to terrain no game.move.select(Moves.SPLASH, BattlerIndex.PLAYER_2); await game.forceEnemyMove(Moves.SPLASH); @@ -340,15 +341,42 @@ describe("Moves - Instruct", () => { game.move.select(Moves.INSTRUCT, BattlerIndex.PLAYER_2, BattlerIndex.PLAYER); await game.setTurnOrder([ BattlerIndex.PLAYER_2, BattlerIndex.PLAYER, BattlerIndex.ENEMY, BattlerIndex.ENEMY_2 ]); await game.phaseInterceptor.to("TurnEndPhase", false); + // quick attack failed when instructed - expect(banette?.getLastXMoves(-1)[1].move).toBe(Moves.QUICK_ATTACK); - expect(banette?.getLastXMoves(-1)[1].result).toBe(MoveResult.FAIL); + const banette = game.scene.getPlayerPokemon()!; + expect(banette.getLastXMoves(-1)[1].move).toBe(Moves.QUICK_ATTACK); + expect(banette.getLastXMoves(-1)[1].result).toBe(MoveResult.FAIL); + }); + + it("should still work w/ prankster in psychic terrain", async () => { + game.override. + battleType("double") + .enemyMoveset([ Moves.SPLASH, Moves.PSYCHIC_TERRAIN ]) + .ability(Abilities.PRANKSTER); + await game.classicMode.startBattle([ Species.BANETTE, Species.KLEFKI ]); + + const [ banette, klefki ] = game.scene.getPlayerField()!; + game.move.changeMoveset(banette, Moves.VINE_WHIP); + game.move.changeMoveset(klefki, [ Moves.INSTRUCT, Moves.SPLASH ]); + + game.move.select(Moves.VINE_WHIP, BattlerIndex.PLAYER, BattlerIndex.ENEMY); // succeeds due to terrain + game.move.select(Moves.SPLASH, BattlerIndex.PLAYER_2); + await game.forceEnemyMove(Moves.SPLASH); + await game.forceEnemyMove(Moves.PSYCHIC_TERRAIN); + await game.toNextTurn(); + + game.move.select(Moves.SPLASH, BattlerIndex.PLAYER); + game.move.select(Moves.INSTRUCT, BattlerIndex.PLAYER_2, BattlerIndex.PLAYER); + await game.setTurnOrder([ BattlerIndex.PLAYER_2, BattlerIndex.PLAYER, BattlerIndex.ENEMY, BattlerIndex.ENEMY_2 ]); + await game.phaseInterceptor.to("TurnEndPhase", false); + instructSuccess(banette, Moves.VINE_WHIP); }); it("should cause spread moves to correctly hit targets in doubles after singles", async () => { game.override .battleType("even-doubles") .moveset([ Moves.BREAKING_SWIPE, Moves.INSTRUCT, Moves.SPLASH ]) + .enemyMoveset(Moves.SONIC_BOOM) .enemySpecies(Species.AXEW) .startingLevel(500); await game.classicMode.startBattle([ Species.KORAIDON, Species.KLEFKI ]); @@ -356,22 +384,19 @@ describe("Moves - Instruct", () => { const koraidon = game.scene.getPlayerField()[0]!; game.move.select(Moves.BREAKING_SWIPE); - await game.forceEnemyMove(Moves.SONIC_BOOM); await game.phaseInterceptor.to("TurnEndPhase", false); - expect (koraidon.getInverseHp()).toBe(0); + expect(koraidon.getInverseHp()).toBe(0); expect(koraidon.getLastXMoves(-1)[0].targets).toEqual([ BattlerIndex.ENEMY ]); await game.toNextWave(); game.move.select(Moves.SPLASH, BattlerIndex.PLAYER); game.move.select(Moves.INSTRUCT, BattlerIndex.PLAYER_2, BattlerIndex.PLAYER); - await game.forceEnemyMove(Moves.SONIC_BOOM, BattlerIndex.PLAYER); - await game.forceEnemyMove(Moves.SONIC_BOOM, BattlerIndex.PLAYER); await game.setTurnOrder([ BattlerIndex.PLAYER_2, BattlerIndex.PLAYER, BattlerIndex.ENEMY, BattlerIndex.ENEMY_2 ]); await game.phaseInterceptor.to("TurnEndPhase", false); // did not take damage since enemies died beforehand; // last move used hit both enemies expect(koraidon.getInverseHp()).toBe(0); - expect(koraidon.getLastXMoves(-1)[1].targets).toMatchObject([ BattlerIndex.ENEMY, BattlerIndex.ENEMY_2 ]); + expect(koraidon.getLastXMoves(-1)[1].targets?.sort()).toEqual([ BattlerIndex.ENEMY, BattlerIndex.ENEMY_2 ]); }); it("should cause AoE moves to correctly hit everyone in doubles after singles", async () => { @@ -379,13 +404,13 @@ describe("Moves - Instruct", () => { .battleType("even-doubles") .moveset([ Moves.BRUTAL_SWING, Moves.INSTRUCT, Moves.SPLASH ]) .enemySpecies(Species.AXEW) + .enemyMoveset(Moves.SONIC_BOOM) .startingLevel(500); await game.classicMode.startBattle([ Species.KORAIDON, Species.KLEFKI ]); const koraidon = game.scene.getPlayerField()[0]!; game.move.select(Moves.BRUTAL_SWING); - await game.forceEnemyMove(Moves.SONIC_BOOM); await game.setTurnOrder([ BattlerIndex.PLAYER, BattlerIndex.ENEMY ]); await game.phaseInterceptor.to("TurnEndPhase", false); expect(koraidon.getInverseHp()).toBe(0); @@ -394,22 +419,18 @@ describe("Moves - Instruct", () => { game.move.select(Moves.SPLASH, BattlerIndex.PLAYER); game.move.select(Moves.INSTRUCT, BattlerIndex.PLAYER_2, BattlerIndex.PLAYER); - await game.forceEnemyMove(Moves.SONIC_BOOM, BattlerIndex.PLAYER); - await game.forceEnemyMove(Moves.SONIC_BOOM, BattlerIndex.PLAYER); await game.setTurnOrder([ BattlerIndex.PLAYER_2, BattlerIndex.PLAYER, BattlerIndex.ENEMY, BattlerIndex.ENEMY_2 ]); await game.phaseInterceptor.to("TurnEndPhase", false); // did not take damage since enemies died beforehand; // last move used hit everything around it expect(koraidon.getInverseHp()).toBe(0); - expect(koraidon.getLastXMoves(-1)[1].targets).toEqual([ BattlerIndex.ENEMY, BattlerIndex.ENEMY_2, BattlerIndex.PLAYER_2 ]); - expect(game.scene.getPlayerField()[1].getInverseHp()).toBeGreaterThan(0); - expect(game.scene.getEnemyField()[0].getInverseHp()).toBeGreaterThan(0); - expect(game.scene.getEnemyField()[1].getInverseHp()).toBeGreaterThan(0); + expect(koraidon.getLastXMoves(-1)[1].targets?.sort()).toEqual([ BattlerIndex.PLAYER_2, BattlerIndex.ENEMY, BattlerIndex.ENEMY_2 ]); }); it("should cause multi-hit moves to hit the appropriate number of times in singles", async () => { game.override .enemyAbility(Abilities.SKILL_LINK) + .moveset([ Moves.SPLASH, Moves.INSTRUCT ]) .enemyMoveset(Moves.BULLET_SEED); await game.classicMode.startBattle([ Species.BULBASAUR ]); @@ -436,6 +457,7 @@ describe("Moves - Instruct", () => { game.override .battleType("double") .enemyAbility(Abilities.SKILL_LINK) + .moveset([ Moves.SPLASH, Moves.INSTRUCT ]) .enemyMoveset([ Moves.BULLET_SEED, Moves.SPLASH ]) .enemyLevel(5); await game.classicMode.startBattle([ Species.BULBASAUR, Species.IVYSAUR ]); diff --git a/src/test/utils/gameManager.ts b/src/test/utils/gameManager.ts index fe8d06c2c3b..42f8d5ac542 100644 --- a/src/test/utils/gameManager.ts +++ b/src/test/utils/gameManager.ts @@ -129,9 +129,10 @@ export default class GameManager { /** * Adds an action to be executed on the next prompt. + * This can be used to (among other things) simulate inputs or run functions mid-phase. * @param phaseTarget - The target phase. * @param mode - The mode to wait for. - * @param callback - The callback to execute. + * @param callback - The callback function to execute on next prompt. * @param expireFn - Optional function to determine if the prompt has expired. */ onNextPrompt(phaseTarget: string, mode: Mode, callback: () => void, expireFn?: () => void, awaitingActionInput: boolean = false) { @@ -400,6 +401,11 @@ export default class GameManager { return updateUserInfo(); } + /** + * Faints a player or enemy pokemon instantly by setting their HP to 0. + * @param pokemon The player/enemy pokemon being fainted + * @returns A promise that resolves once the fainted pokemon's FaintPhase finishes running. + */ async killPokemon(pokemon: PlayerPokemon | EnemyPokemon) { return new Promise(async (resolve, reject) => { pokemon.hp = 0; diff --git a/src/test/utils/helpers/moveHelper.ts b/src/test/utils/helpers/moveHelper.ts index 4b6ccbab737..1c567a35d37 100644 --- a/src/test/utils/helpers/moveHelper.ts +++ b/src/test/utils/helpers/moveHelper.ts @@ -75,9 +75,10 @@ export class MoveHelper extends GameManagerHelper { } /** - * Used when the normal moveset override can't be used (such as when it's necessary to check updated properties of the moveset). + * Changes a pokemon's moveset to the given move(s). + * Used when the normal moveset override can't be used (such as when it's necessary to check or update properties of the moveset). * @param pokemon - The {@linkcode Pokemon} being modified - * @param moveset - The {@linkcode Moves} (single or array) to set the Pokemon's moveset to + * @param moveset - The {@linkcode Moves} (single or array) to change the Pokemon's moveset to */ public changeMoveset(pokemon: Pokemon, moveset: Moves | Moves[]): void { if (!Array.isArray(moveset)) { diff --git a/src/ui/ui.ts b/src/ui/ui.ts index fc8fa94c848..15e028c5ce7 100644 --- a/src/ui/ui.ts +++ b/src/ui/ui.ts @@ -277,6 +277,10 @@ export default class UI extends Phaser.GameObjects.Container { return true; } + /** Process a player input of a button (delivering it to the current UI handler for processing) + * @param button The {@linkcode Button} being inputted + * @returns true if the input attempt succeeds + */ processInput(button: Button): boolean { if (this.overlayActive) { return false;