diff --git a/src/data/move.ts b/src/data/move.ts index 6d2e09212ff..9189d58417f 100644 --- a/src/data/move.ts +++ b/src/data/move.ts @@ -6108,7 +6108,7 @@ export class ForceSwitchOutAttr extends MoveEffectAttr { return false; } - // Don't allow wild mons to flee with U-turn et al + // Don't allow wild mons to flee with U-turn et al. if (this.selfSwitch && !user.isPlayer() && move.category !== MoveCategory.STATUS) { return false; } @@ -7068,7 +7068,20 @@ export class RepeatMoveAttr extends MoveEffectAttr { // (mainly for alternating double/single battle shenanigans) // Rampaging moves (e.g. Outrage) are not included due to being incompatible with Instruct // TODO: Fix this once dragon darts gets smart targeting - const moveTargets = movesetMove.getMove().isMultiTarget() ? getMoveTargets(target, lastMove.move).targets : lastMove.targets!; + let moveTargets = movesetMove.getMove().isMultiTarget() ? getMoveTargets(target, lastMove.move).targets : lastMove.targets; + + /** In the event the instructed move's only target is a fainted opponent, redirect it to an alive ally if possible + Normally, all yet-unexecuted move phases would swap over when the enemy in question faints + (see `redirectPokemonMoves` in `battle-scene.ts`), + but since instruct adds a new move phase pre-emptively, we need to handle this interaction manually. + */ + const firstTarget = globalScene.getField()[moveTargets[0]]; + if (globalScene.currentBattle.double && moveTargets.length === 1 && firstTarget.isFainted() && firstTarget !== target.getAlly()) { + const ally = firstTarget.getAlly(); + if (ally.isActive()) { // ally exists, is not dead and can sponge the blast + moveTargets = [ ally.getBattlerIndex() ]; + } + } globalScene.queueMessage(i18next.t("moveTriggers:instructingMove", { userPokemonName: getPokemonNameWithAffix(user), diff --git a/src/test/moves/instruct.test.ts b/src/test/moves/instruct.test.ts index a2d66d0786d..da53bb04268 100644 --- a/src/test/moves/instruct.test.ts +++ b/src/test/moves/instruct.test.ts @@ -43,7 +43,9 @@ describe("Moves - Instruct", () => { }); it("should repeat target's last used move", async () => { - game.override.moveset(Moves.INSTRUCT); + game.override + .moveset(Moves.INSTRUCT) + .enemyLevel(1000); // ensures shuckle no die await game.classicMode.startBattle([ Species.AMOONGUSS ]); const enemy = game.scene.getEnemyPokemon()!; @@ -170,6 +172,33 @@ describe("Moves - Instruct", () => { expect(enemyPokemon.getMoveHistory().length).toBe(1); }); + it("should redirect attacking moves if enemy faints", async () => { + game.override + .battleType("double") + .enemyMoveset(Moves.SPLASH) + .enemySpecies(Species.MAGIKARP) + .enemyLevel(1); + await game.classicMode.startBattle([ Species.HISUI_ELECTRODE, Species.KOMMO_O ]); + + const [ electrode, kommo_o ] = game.scene.getPlayerField()!; + game.move.changeMoveset(electrode, Moves.CHLOROBLAST); + game.move.changeMoveset(kommo_o, Moves.INSTRUCT); + + game.move.select(Moves.CHLOROBLAST, BattlerIndex.PLAYER); + game.move.select(Moves.INSTRUCT, BattlerIndex.PLAYER_2, BattlerIndex.PLAYER); + await game.setTurnOrder([ BattlerIndex.PLAYER, BattlerIndex.PLAYER_2, BattlerIndex.ENEMY, BattlerIndex.ENEMY_2 ]); + await game.phaseInterceptor.to("BerryPhase"); + + // Chloroblast always deals 50% max HP% recoil UNLESS you whiff + // due to lack of targets or similar, + // so all we have to do is check whether electrode fainted or not. + // Naturally, both karps should also be dead as well. + expect(electrode.isFainted()).toBe(true); + const [ karp1, karp2 ] = game.scene.getEnemyField()!; + expect(karp1.isFainted()).toBe(true); + expect(karp2.isFainted()).toBe(true); + }), + it("should not repeat move when switching out", async () => { game.override .enemyMoveset(Moves.INSTRUCT) diff --git a/src/test/utils/gameManager.ts b/src/test/utils/gameManager.ts index cce1fc52e88..8e595212898 100644 --- a/src/test/utils/gameManager.ts +++ b/src/test/utils/gameManager.ts @@ -459,9 +459,9 @@ export default class GameManager { } /** - * Intercepts `TurnStartPhase` and mocks the getSpeedOrder's return value {@linkcode TurnStartPhase.getSpeedOrder} - * Used to modify the turn order. - * Note: This *DOES NOT* account for priority. + * Intercepts `TurnStartPhase` and mocks {@linkcode TurnStartPhase.getSpeedOrder}'s return value. + * Used to manually modify Pokemon turn order. + * Note: This *DOES NOT* account for priority, only speed. * @param {BattlerIndex[]} order The turn order to set * @example * ```ts @@ -475,7 +475,7 @@ export default class GameManager { } /** - * Removes all held items from enemy pokemon + * Removes all held items from enemy pokemon. */ removeEnemyHeldItems(): void { this.scene.clearEnemyHeldItemModifiers();