diff --git a/src/data/move.ts b/src/data/move.ts index 591e704886a..7f0d08e7d30 100644 --- a/src/data/move.ts +++ b/src/data/move.ts @@ -6607,6 +6607,7 @@ export class RepeatMoveAttr extends OverrideMoveEffectAttr { const lastMove = target.getLastXMoves().find(m => m.move !== Moves.NONE); const movesetMove = target.getMoveset().find(m => m?.moveId === lastMove?.move); const moveTargets = lastMove?.targets!; + // TODO: Add a way of adding moves to list procedurally const unrepeatablemoves = [ // Locking/Continually Executed moves Moves.OUTRAGE, @@ -6624,6 +6625,34 @@ export class RepeatMoveAttr extends OverrideMoveEffectAttr { Moves.FAKE_OUT, Moves.FIRST_IMPRESSION, Moves.MAT_BLOCK, + // Moves with a recharge turn + Moves.HYPER_BEAM, + Moves.ETERNABEAM, + Moves.FRENZY_PLANT, + Moves.BLAST_BURN, + Moves.HYDRO_CANNON, + Moves.GIGA_IMPACT, + Moves.PRISMATIC_LASER, + Moves.ROAR_OF_TIME, + Moves.ROCK_WRECKER, + Moves.METEOR_ASSAULT, + // Charging & 2-turn moves + Moves.DIG, + Moves.FLY, + Moves.BOUNCE, + Moves.SHADOW_FORCE, + Moves.PHANTOM_FORCE, + Moves.DIVE, + Moves.ELECTRO_SHOT, + Moves.ICE_BURN, + Moves.GEOMANCY, + Moves.FREEZE_SHOCK, + Moves.SKY_DROP, + Moves.SKY_ATTACK, + Moves.SKULL_BASH, + Moves.SOLAR_BEAM, + Moves.SOLAR_BLADE, + Moves.METEOR_BEAM, // Other moves Moves.KINGS_SHIELD, Moves.SKETCH, @@ -6631,11 +6660,10 @@ export class RepeatMoveAttr extends OverrideMoveEffectAttr { Moves.MIMIC, Moves.STRUGGLE, // TODO: Add Z-move blockage once zmoves are implemented - // as well as actually blocking move calling moves ]; - if (!targetMoveCopiableCondition(user, target, move) || // called move doesn't exist or is a charging/recharging move - !movesetMove || // called move not in target's moveset (dancer, forgetting the move, etc.) + if (!movesetMove || // called move not in target's moveset (dancer, forgetting the move, etc.) + allMoves[lastMove!.move].isChargingMove() || // called move is a charging/recharging move !moveTargets.length || // called move has no targets unrepeatablemoves.includes(lastMove?.move!)) { // called move is explicitly in the banlist return false; diff --git a/src/test/moves/instruct.test.ts b/src/test/moves/instruct.test.ts index 5839f9abb97..62d2ee5d974 100644 --- a/src/test/moves/instruct.test.ts +++ b/src/test/moves/instruct.test.ts @@ -3,6 +3,7 @@ import { Species } from "#enums/species"; import { BattlerIndex } from "#app/battle"; import GameManager from "#test/utils/gameManager"; import { MoveResult } from "#app/field/pokemon"; +import { Abilities } from "#app/enums/abilities"; import Phaser from "phaser"; import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest"; @@ -24,6 +25,7 @@ describe("Moves - Instruct", () => { game = new GameManager(phaserGame); game.override.battleType("double"); game.override.enemySpecies(Species.MAGIKARP); + game.override.enemyAbility(Abilities.COMPOUND_EYES); game.override.battleType("double"); game.override.enemyLevel(100); game.override.starterSpecies(Species.AMOONGUSS); @@ -39,23 +41,58 @@ describe("Moves - Instruct", () => { await game.setTurnOrder([ BattlerIndex.ENEMY, BattlerIndex.PLAYER ]); await game.phaseInterceptor.to("TurnEndPhase", false); - expect(game.scene.getPlayerPokemon()?.getInverseHp()).toBe(40); // lost 2 hp from 2 attacks - }); - it("should repeat enemy's move through substitute", async () => { - await game.classicMode.startBattle([ Species.AMOONGUSS ]); - game.move.select(Moves.INSTRUCT, BattlerIndex.PLAYER, BattlerIndex.ENEMY); - await game.forceEnemyMove(Moves.SUBSTITUTE, BattlerIndex.ATTACKER); - await game.setTurnOrder([ BattlerIndex.PLAYER, BattlerIndex.ENEMY ]); - await game.phaseInterceptor.to("CommandPhase", false); + // lost 40 hp from 2 attacks + expect(game.scene.getPlayerPokemon()?.getInverseHp()).toBe(40); + const moveUsed = game.scene.getEnemyPokemon()?.moveset.find(m => m?.moveId === Moves.SONIC_BOOM)!; - // fake move history + // used 2 pp due to spanking enemy twice + expect(moveUsed.ppUsed).toBe(2); + + }); + it("should not repeat enemy's out of pp move", async () => { + await game.classicMode.startBattle([ Species.AMOONGUSS ]); + const enemyPokemon = game.scene.getEnemyPokemon(); + const moveUsed = enemyPokemon?.moveset.find(m => m?.moveId === Moves.SONIC_BOOM)!; + moveUsed.ppUsed = moveUsed.getMovePp() - 1; // deduct all but 1 pp game.move.select(Moves.INSTRUCT, BattlerIndex.PLAYER, BattlerIndex.ENEMY); await game.forceEnemyMove(Moves.SONIC_BOOM, BattlerIndex.PLAYER); await game.setTurnOrder([ BattlerIndex.ENEMY, BattlerIndex.PLAYER ]); await game.phaseInterceptor.to("TurnEndPhase", false); - expect(game.scene.getPlayerPokemon()?.getInverseHp()).toBe(40); // lost 40 hp from 2 attacks + // 2nd move call fails due to out of pp + expect(game.scene.getPlayerPokemon()?.getLastXMoves().at(-1)!.result).toBe(MoveResult.SUCCESS); + expect(enemyPokemon?.getLastXMoves().at(-1)!.result).toBe(MoveResult.FAIL); + + // move used all its remaining pp + expect(moveUsed.ppUsed).toBe(moveUsed.getMovePp()); + + }); + it("should not repeat enemy's attack move when moving first", async () => { + await game.classicMode.startBattle([ Species.AMOONGUSS ]); + game.move.select(Moves.INSTRUCT, BattlerIndex.PLAYER, BattlerIndex.ENEMY); + await game.forceEnemyMove(Moves.SONIC_BOOM, BattlerIndex.PLAYER); + await game.setTurnOrder([ BattlerIndex.PLAYER, BattlerIndex.ENEMY ]); + await game.phaseInterceptor.to("TurnEndPhase", false); + + // should fail to execute due to lack of move + expect(game.scene.getPlayerPokemon()!.getLastXMoves().at(-1)!.result).toBe(MoveResult.FAIL); + }); + it("should repeat enemy's move through substitute", async () => { + await game.classicMode.startBattle([ Species.AMOONGUSS ]); + game.move.select(Moves.INSTRUCT, BattlerIndex.PLAYER, BattlerIndex.ENEMY); + await game.forceEnemyMove(Moves.SUBSTITUTE, BattlerIndex.ATTACKER); + await game.setTurnOrder([ BattlerIndex.PLAYER, BattlerIndex.ENEMY ]); + await game.phaseInterceptor.to("TurnEndPhase", false); + await game.phaseInterceptor.to("CommandPhase", false); + + game.move.select(Moves.INSTRUCT, BattlerIndex.PLAYER, BattlerIndex.ENEMY); + await game.forceEnemyMove(Moves.SONIC_BOOM, BattlerIndex.PLAYER); + await game.setTurnOrder([ BattlerIndex.ENEMY, BattlerIndex.PLAYER ]); + await game.phaseInterceptor.to("TurnEndPhase", false); + + // lost 40 hp from 2 attacks + expect(game.scene.getPlayerPokemon()?.getInverseHp()).toBe(40); }); it("should try to repeat enemy's disabled move, but fail", async () => { game.override.moveset([ Moves.INSTRUCT, Moves.SONIC_BOOM, Moves.DISABLE, Moves.SPLASH ]); @@ -66,7 +103,9 @@ describe("Moves - Instruct", () => { await game.setTurnOrder([ BattlerIndex.ENEMY, BattlerIndex.PLAYER, BattlerIndex.PLAYER_2 ]); await game.phaseInterceptor.to("TurnEndPhase", false); - expect(game.scene.getEnemyPokemon()!.getLastXMoves()[0].result).toBe(MoveResult.FAIL); // failed due to disable + // instruction should succeed but move itself should fail + expect(game.scene.getPlayerPokemon()!.getLastXMoves().at(-1)!.result).toBe(MoveResult.SUCCESS); + expect(game.scene.getEnemyPokemon()!.getLastXMoves().at(-1)!.result).toBe(MoveResult.FAIL); }); it("should repeat tormented enemy's move", async () => { await game.classicMode.startBattle([ Species.AMOONGUSS, Species.MIGHTYENA ]); @@ -80,9 +119,11 @@ describe("Moves - Instruct", () => { await game.setTurnOrder([ BattlerIndex.ENEMY, BattlerIndex.PLAYER_2, BattlerIndex.PLAYER ]); await game.phaseInterceptor.to("TurnEndPhase", false); - expect(game.scene.getPlayerPokemon()!.getLastXMoves()[0].result).toBe(MoveResult.SUCCESS); // should work + // instruct and repeated move should work correctly + expect(game.scene.getPlayerPokemon()!.getLastXMoves().at(-1)!.result).toBe(MoveResult.SUCCESS); + expect(game.scene.getEnemyPokemon()!.getLastXMoves().at(-1)!.result).toBe(MoveResult.SUCCESS); }); - it("should not repeat enemy's move thru protect", async () => { + it("should not repeat enemy's move through protect", async () => { await game.classicMode.startBattle([ Species.AMOONGUSS ]); const enemyPokemon = game.scene.getEnemyPokemon()!; // fake move history @@ -92,7 +133,8 @@ describe("Moves - Instruct", () => { await game.forceEnemyMove(Moves.PROTECT); await game.phaseInterceptor.to("TurnEndPhase", false); - expect(game.scene.getPlayerPokemon()!.getLastXMoves()[0].result).toBe(MoveResult.FAIL); // lost no hp as mon protected themself from instruct + // lost no hp as mon protected themself from instruct + expect(game.scene.getPlayerPokemon()!.getLastXMoves().at(-1)!.result).toBe(MoveResult.FAIL); }); it("should not repeat enemy's charging move", async () => { await game.classicMode.startBattle([ Species.DUSKNOIR ]); @@ -104,19 +146,32 @@ describe("Moves - Instruct", () => { await game.setTurnOrder([ BattlerIndex.ENEMY, BattlerIndex.PLAYER ]); await game.phaseInterceptor.to("TurnEndPhase", false); - expect(game.scene.getPlayerPokemon()!.getLastXMoves()[0].result).toBe(MoveResult.FAIL); // hyper beam charging prevented instruct from working - }); - it("should not repeat move not known by target", async () => { - await game.classicMode.startBattle([ Species.DUSKNOIR ]); - const enemyPokemon = game.scene.getEnemyPokemon()!; - enemyPokemon.battleSummonData.moveHistory = [{ move: Moves.ROLLOUT, targets: [ BattlerIndex.PLAYER ], result: MoveResult.SUCCESS, virtual: false }]; + // hyper beam charging prevented instruct from working + expect(game.scene.getPlayerPokemon()!.getLastXMoves().at(-1)!.result).toBe(MoveResult.FAIL); + await game.phaseInterceptor.to("CommandPhase", false); game.move.select(Moves.INSTRUCT, BattlerIndex.PLAYER, BattlerIndex.ENEMY); await game.forceEnemyMove(Moves.HYPER_BEAM); - await game.setTurnOrder([ BattlerIndex.PLAYER, BattlerIndex.ENEMY ]); + await game.setTurnOrder([ BattlerIndex.ENEMY, BattlerIndex.PLAYER ]); await game.phaseInterceptor.to("TurnEndPhase", false); - expect(game.scene.getPlayerPokemon()!.getLastXMoves()[0].result).toBe(MoveResult.FAIL); // hyper beam cannot be instructed + // hyper beam charging prevented instruct from working + expect(game.scene.getPlayerPokemon()!.getLastXMoves().at(-1)!.result).toBe(MoveResult.FAIL); + + }); + it("should not repeat dance move not known by target", async () => { + await game.classicMode.startBattle([ Species.DUSKNOIR, Species.ABOMASNOW ]); + game.override.moveset([ Moves.INSTRUCT, Moves.FIERY_DANCE, Moves.SUBSTITUTE, Moves.TORMENT ]); + game.override.enemyAbility(Abilities.DANCER); + game.move.select(Moves.INSTRUCT, BattlerIndex.PLAYER, BattlerIndex.ENEMY); + game.move.select(Moves.FIERY_DANCE, BattlerIndex.PLAYER_2, BattlerIndex.ENEMY); + await game.forceEnemyMove(Moves.PROTECT, BattlerIndex.ATTACKER); + await game.setTurnOrder([ BattlerIndex.PLAYER_2, BattlerIndex.PLAYER, BattlerIndex.ENEMY ]); + await game.phaseInterceptor.to("TurnEndPhase", false); + + // Pokemon 2 uses dance; dancer reciprocates + // instruct fails as it cannot copy the dance move + expect(game.scene.getPlayerPokemon()!.getLastXMoves().at(-1)!.result).toBe(MoveResult.FAIL); }); it("should repeat ally's attack on enemy", async () => { await game.classicMode.startBattle([ Species.AMOONGUSS, Species.SHUCKLE ]); @@ -128,7 +183,23 @@ describe("Moves - Instruct", () => { await game.phaseInterceptor.to("TurnEndPhase", false); + // used 2 pp and spanked enemy twice const moveUsed = game.scene.getPlayerField()[1]!.getMoveset().find(m => m?.moveId === Moves.SONIC_BOOM)!; - expect(moveUsed.getMove().pp - moveUsed.getMovePp()).toBe(2); // used 2 pp and spanked enemy twice + expect(moveUsed.ppUsed).toBe(2); + expect(game.scene.getEnemyPokemon()!.getInverseHp()).toBe(40); + + }); + it("should repeat ally's friendly fire attack", async () => { + await game.classicMode.startBattle([ Species.AMOONGUSS, Species.SHUCKLE ]); + + game.move.select(Moves.INSTRUCT, BattlerIndex.PLAYER, BattlerIndex.PLAYER_2); + game.move.select(Moves.SONIC_BOOM, BattlerIndex.PLAYER_2, BattlerIndex.PLAYER); + await game.forceEnemyMove(Moves.VINE_WHIP, BattlerIndex.PLAYER_2); + await game.setTurnOrder([ BattlerIndex.PLAYER_2, BattlerIndex.PLAYER, BattlerIndex.ENEMY ]); + + await game.phaseInterceptor.to("TurnEndPhase", false); + + const playerPokemon = game.scene.getPlayerField()[0]!; + expect(playerPokemon.getInverseHp()).toBe(40); // spanked ally twice }); });