diff --git a/src/battle.ts b/src/battle.ts index 0a6147aa064..00f3b04f2ab 100644 --- a/src/battle.ts +++ b/src/battle.ts @@ -70,6 +70,10 @@ export class Battle { public battleScore = 0; public postBattleLoot: PokemonHeldItemModifier[] = []; public escapeAttempts = 0; + /** + * A tracker of the last {@linkcode MoveId} successfully used this battle. + * + */ public lastMove: MoveId = MoveId.NONE; public battleSeed: string = randomString(16, true); private battleSeedState: string | null = null; diff --git a/src/data/battler-tags.ts b/src/data/battler-tags.ts index c8ddfe32f0b..0b12dc0fbbd 100644 --- a/src/data/battler-tags.ts +++ b/src/data/battler-tags.ts @@ -1129,6 +1129,7 @@ export class EncoreTag extends MoveRestrictionBattlerTag { constructor(sourceId: number) { super( BattlerTagType.ENCORE, + // TODO: This should trigger on turn end [BattlerTagLapseType.CUSTOM, BattlerTagLapseType.AFTER_MOVE], 3, MoveId.ENCORE, diff --git a/src/data/moves/move.ts b/src/data/moves/move.ts index 7b7396cfdee..bba58d7fdde 100644 --- a/src/data/moves/move.ts +++ b/src/data/moves/move.ts @@ -6795,7 +6795,7 @@ export abstract class CallMoveAttr extends OverrideMoveEffectAttr { /** * Whether this move should target the user; default `true`. * If `true`, will unleash non-spread moves against a random eligible target, - * or else the move's selected target. + * as opposed to the move's selected target. */ override selfTarget = true, ) { @@ -6806,7 +6806,7 @@ export abstract class CallMoveAttr extends OverrideMoveEffectAttr { * Abstract function yielding the move to be used. * @param user - The {@linkcode Pokemon} using the move * @param target - The {@linkcode Pokemon} being targeted by the move - * @returns The MoveId that will be called and used. + * @returns The {@linkcode MoveId} that will be called and used. */ protected abstract getMove(user: Pokemon, target: Pokemon): MoveId; @@ -6830,92 +6830,10 @@ export abstract class CallMoveAttr extends OverrideMoveEffectAttr { } } -/** - * Attribute to call a random move among moves not in a banlist. - * Used for {@linkcode MoveId.METRONOME}. - */ -export class RandomMoveAttr extends CallMoveAttr { - constructor( - /** - * A {@linkcode ReadonlySet} containing all moves that this {@linkcode MoveAttr} cannot copy, - * in addition to unimplemented moves and {@linkcode MoveId.NONE}. - * The move will fail if the chosen move is inside this banlist (if it exists). - */ - protected readonly invalidMoves: ReadonlySet, - ) { - super(true); - } - - /** - * Pick a random move to execute, barring unimplemented moves and ones - * in this move's {@linkcode invalidMetronomeMoves | exclusion list}. - * Overridden as public to allow tests to override move choice using mocks. - * - * @param user - The {@linkcode Pokemon} using the move - * @returns The {@linkcode MoveId} that will be called. - */ - public override getMove(user: Pokemon): MoveId { - const moveIds = getEnumValues(MoveId).filter(m => m !== MoveId.NONE && !this.invalidMoves.has(m) && !allMoves[m].name.endsWith(" (N)")); - return moveIds[user.randBattleSeedInt(moveIds.length)]; - } -} - -/** - * Attribute used to call a random move in the user or party's moveset. - * Used for {@linkcode MoveId.ASSIST} and {@linkcode MoveId.SLEEP_TALK} - * - * Fails if the user has no callable moves. - */ -export class RandomMovesetMoveAttr extends RandomMoveAttr { - /** - * The previously-selected {@linkcode MoveId} for this attribute. - * Reset to {@linkcode MoveId.NONE} after successful use. - */ - private selectedMove: MoveId = MoveId.NONE - constructor(invalidMoves: ReadonlySet, - /** - * Whether to consider all moves from the user's party (`true`) or the user's own moveset (`false`); - * default `false`. - */ - private includeParty = false, - ) { - super(invalidMoves); - } - - /** - * Select a random move from either the user's or its party members' movesets, - * or return an already-selected one if one exists. - * - * @param user - The {@linkcode Pokemon} using the move. - * @returns The {@linkcode MoveId} that will be called. - */ - override getMove(user: Pokemon): MoveId { - if (this.selectedMove) { - const m = this.selectedMove; - this.selectedMove = MoveId.NONE; - return m; - } - - // includeParty will be true for Assist, false for Sleep Talk - const allies: Pokemon[] = this.includeParty - ? (user.isPlayer() ? globalScene.getPlayerParty() : globalScene.getEnemyParty()).filter(p => p !== user) - : [ user ]; - - // Assist & Sleep Talk consider duplicate moves for their selection (hence why we use an array instead of a set) - const moveset = allies.flatMap(p => p.moveset); - const eligibleMoves = moveset.filter(m => m.moveId !== MoveId.NONE && !this.invalidMoves.has(m.moveId) && !m.getMove().name.endsWith(" (N)")); - this.selectedMove = eligibleMoves[user.randBattleSeedInt(eligibleMoves.length)]?.moveId ?? MoveId.NONE; // will fail if 0 length array - return this.selectedMove; - } - - override getCondition(): MoveConditionFunc { - return (user) => this.getMove(user) !== MoveId.NONE; - } -} /** * Attribute to call a different move based on the current terrain and biome. - * Used by {@linkcode MoveId.NATURE_POWER} + * Used by {@linkcode MoveId.NATURE_POWER}. */ export class NaturePowerAttr extends CallMoveAttr { constructor() { @@ -6924,7 +6842,6 @@ export class NaturePowerAttr extends CallMoveAttr { override getMove(user: Pokemon): MoveId { const moveId = this.getMoveIdForTerrain(globalScene.arena.getTerrainType(), globalScene.arena.biomeType) - // Unshift a phase to load the move's animation (in case it isn't already), then use the move. globalScene.phaseManager.queueMessage(i18next.t("moveTriggers:naturePowerUse", { pokemonName: getPokemonNameWithAffix(user), moveName: allMoves[moveId].name, @@ -7024,34 +6941,40 @@ export class NaturePowerAttr extends CallMoveAttr { default: // Fallback for no match biome satisfies never; - console.warn(`NaturePowerAttr lacks defined move to use for current biome ${toReadableString(BiomeId[biome])}; consider adding an appropriate move to the attribute's selection table.`) + console.warn(`NaturePowerAttr lacks defined move to use for current biome ${toTitleCase(BiomeId[biome])}; consider adding an appropriate move to the attribute's selection table.`) return MoveId.TRI_ATTACK; } } } /** - * Attribute used to copy the last move executed. - * Used for {@linkcode MoveId.COPYCAT} and {@linkcode MoveId.MIRROR_MOVE}. + * Abstract class to encompass move-copying-moves with a banlist of invalid moves. */ -export class CopyMoveAttr extends CallMoveAttr { +abstract class CallMoveAttrWithBanlist extends CallMoveAttr { + /** + * A {@linkcode ReadonlySet} containing all moves that this {@linkcode MoveAttr} cannot copy, + * in addition to unimplemented moves and {@linkcode MoveId.NONE}. + * The move should fail if the chosen move is inside this banlist. + */ + protected readonly invalidMoves: ReadonlySet + constructor( - /** - * A {@linkcode ReadonlySet} containing all moves that this {@linkcode MoveAttr} cannot copy, - * in addition to unimplemented moves and {@linkcode MoveId.NONE}. - * The move will fail if the chosen move is inside this banlist (if it exists). - */ - protected readonly invalidMoves: ReadonlySet, + invalidMoves: ReadonlySet, selfTarget = true, ) { super(selfTarget); + this.invalidMoves = invalidMoves; } +} - /** - * If `selfTarget` is `true`, grab the last successful move used by anyone. - * Otherwise, select the last move used by the target. - */ +/** + * Attribute used to copy the last move executed, either globally or by the specific target. + * Used for {@linkcode MoveId.COPYCAT} and {@linkcode MoveId.MIRROR_MOVE}. + */ +export class CopyMoveAttr extends CallMoveAttrWithBanlist { override getMove(_user: Pokemon, target: Pokemon): MoveId { + // If `selfTarget` is `true`, return the last successful move used by anyone on-field. + // Otherwise, select the last move used by the target specifically. return this.selfTarget ? globalScene.currentBattle.lastMove : target.getLastXMoves()[0]?.move ?? MoveId.NONE @@ -7065,7 +6988,85 @@ export class CopyMoveAttr extends CallMoveAttr { } } +/** + * Attribute to call a random move among moves not in a banlist. + * Used for {@linkcode MoveId.METRONOME}. + */ +export class RandomMoveAttr extends CallMoveAttrWithBanlist { + constructor(invalidMoves: ReadonlySet) { + super(invalidMoves, true); + } + /** + * Pick a random move to execute, barring unimplemented moves and ones + * in this move's {@linkcode invalidMetronomeMoves | exclusion list}. + * Overridden as public to allow tests to override move choice using mocks. + * + * @param user - The {@linkcode Pokemon} using the move + * @returns The {@linkcode MoveId} that will be called. + */ + public override getMove(user: Pokemon): MoveId { + const moveIds = getEnumValues(MoveId).filter(m => m !== MoveId.NONE && !this.invalidMoves.has(m) && !allMoves[m].name.endsWith(" (N)")); + return moveIds[user.randBattleSeedInt(moveIds.length)]; + } +} + +/** + * Attribute used to call a random move in the user or its allies' moveset. + * Used for {@linkcode MoveId.ASSIST} and {@linkcode MoveId.SLEEP_TALK}. + * + * Fails if the user has no callable moves. + */ +export class RandomMovesetMoveAttr extends RandomMoveAttr { + /** + * The previously-selected {@linkcode MoveId} for this attribute, or `MoveId.NONE` if none could be found. + * Reset to {@linkcode MoveId.NONE} after a successful use. + * @defaultValue `MoveId.NONE` + */ + private selectedMove: MoveId = MoveId.NONE + /** + * Whether to consider moves from the user's other party members (`true`) + * or the user's own moveset (`false`). + * @defaultValue `false`. + */ + private includeParty = false; + constructor(invalidMoves: ReadonlySet, includeParty = false) { + super(invalidMoves); + this.includeParty = includeParty + } + + /** + * Select a random move from either the user's or its party members' movesets, + * or return an already-selected one if one exists. + * + * @param user - The {@linkcode Pokemon} using the move + * @returns The {@linkcode MoveId} that will be called. + */ + override getMove(user: Pokemon): MoveId { + // If we already have a selected move from the condition function, + // re-use and reset it rather than generating another random move + if (this.selectedMove) { + const m = this.selectedMove; + this.selectedMove = MoveId.NONE; + return m; + } + + // `includeParty` will be true for Assist, false for Sleep Talk + const allies: Pokemon[] = this.includeParty + ? (user.isPlayer() ? globalScene.getPlayerParty() : globalScene.getEnemyParty()).filter(p => p !== user) + : [ user ]; + + // Assist & Sleep Talk consider duplicate moves for their selection (hence why we use an array instead of a set) + const moveset = allies.flatMap(p => p.moveset); + const eligibleMoves = moveset.filter(m => m.moveId !== MoveId.NONE && !this.invalidMoves.has(m.moveId) && !m.getMove().name.endsWith(" (N)")); + this.selectedMove = eligibleMoves[user.randBattleSeedInt(eligibleMoves.length)]?.moveId ?? MoveId.NONE; // will fail if 0 length array + return this.selectedMove; + } + + override getCondition(): MoveConditionFunc { + return (user) => this.getMove(user) !== MoveId.NONE; + } +} /** * Attribute used for moves that cause the target to repeat their last used move. diff --git a/src/phases/move-end-phase.ts b/src/phases/move-end-phase.ts index fd893c445ff..843d14ee26c 100644 --- a/src/phases/move-end-phase.ts +++ b/src/phases/move-end-phase.ts @@ -7,15 +7,26 @@ import { PokemonPhase } from "#phases/pokemon-phase"; export class MoveEndPhase extends PokemonPhase { public readonly phaseName = "MoveEndPhase"; + /** + * Whether the current move was a follow-up attack or not. + * Used to prevent ticking down Encore and similar effects when copying moves. + */ private wasFollowUp: boolean; + /** + * Whether the current move successfully executed and showed usage text. + * Used to update the "last move used" tracker after successful move usage. + */ + private passedPreUsageChecks: boolean; /** Targets from the preceding MovePhase */ private targets: Pokemon[]; - constructor(battlerIndex: BattlerIndex, targets: Pokemon[], wasFollowUp = false) { + + constructor(battlerIndex: BattlerIndex, targets: Pokemon[], wasFollowUp: boolean, passedPreUsageChecks: boolean) { super(battlerIndex); this.targets = targets; this.wasFollowUp = wasFollowUp; + this.passedPreUsageChecks = passedPreUsageChecks; } start() { @@ -26,6 +37,12 @@ export class MoveEndPhase extends PokemonPhase { pokemon.lapseTags(BattlerTagLapseType.AFTER_MOVE); } + // Update the "last move used" counter for Copycat and co. + if (this.passedPreUsageChecks) { + // TODO: Make this check a move in flight instead of a hackjob + globalScene.currentBattle.lastMove = pokemon.getLastXMoves()[0].move; + } + // Remove effects which were set on a Pokemon which removes them on summon (i.e. via Mold Breaker) globalScene.arena.setIgnoreAbilities(false); for (const target of this.targets) { diff --git a/src/phases/move-phase.ts b/src/phases/move-phase.ts index 98566c32ed2..94cff99cd48 100644 --- a/src/phases/move-phase.ts +++ b/src/phases/move-phase.ts @@ -42,6 +42,12 @@ export class MovePhase extends BattlePhase { protected failed = false; /** Whether the current move should fail and retain PP. */ protected cancelled = false; + /** + * Whether the move was interrupted prior to showing usage text. + * Used to set the "last move" pointer after move end. + * @defaultValue `true` + */ + private wasPreInterrupted = true; public get pokemon(): Pokemon { return this._pokemon; @@ -294,6 +300,10 @@ export class MovePhase extends BattlePhase { const moveQueue = this.pokemon.getMoveQueue(); const move = this.move.getMove(); + // Set flag to update Copycat's "last move" counter + // TODO: Verify interaction with a failed Focus Punch + this.wasPreInterrupted = false; + // form changes happen even before we know that the move wll execute. globalScene.triggerPokemonFormChange(this.pokemon, SpeciesFormChangePreMoveTrigger); @@ -367,11 +377,6 @@ export class MovePhase extends BattlePhase { const move = this.move.getMove(); const targets = this.getActiveTargetPokemon(); - // Update the battle's "last move" pointer unless we're currently mimicking a move or triggering Dancer. - if (!move.hasAttr("CopyMoveAttr") && !isReflected(this.useMode)) { - globalScene.currentBattle.lastMove = move.id; - } - // Trigger ability-based user type changes, display move text and then execute move effects. // TODO: Investigate whether PokemonTypeChangeAbAttr can drop the "opponent" parameter applyAbAttrs("PokemonTypeChangeAbAttr", { pokemon: this.pokemon, move, opponent: targets[0] }); @@ -459,6 +464,9 @@ export class MovePhase extends BattlePhase { const move = this.move.getMove(); const targets = this.getActiveTargetPokemon(); + // Set flag to update Copycat's "last move" counter + this.wasPreInterrupted = false; + if (!move.applyConditions(this.pokemon, targets[0], move)) { this.failMove(true); return; @@ -490,6 +498,7 @@ export class MovePhase extends BattlePhase { this.pokemon.getBattlerIndex(), this.getActiveTargetPokemon(), isVirtual(this.useMode), + this.wasPreInterrupted, ); super.end(); @@ -698,4 +707,4 @@ export class MovePhase extends BattlePhase { public showFailedText(failedText = i18next.t("battle:attackFailed")): void { globalScene.phaseManager.queueMessage(failedText); } -} \ No newline at end of file +} diff --git a/test/moves/copycat.test.ts b/test/moves/copycat.test.ts index 5e988ac1f5a..3a24f4f89d6 100644 --- a/test/moves/copycat.test.ts +++ b/test/moves/copycat.test.ts @@ -2,9 +2,9 @@ import { AbilityId } from "#enums/ability-id"; import { BattlerIndex } from "#enums/battler-index"; import { MoveId } from "#enums/move-id"; import { MoveResult } from "#enums/move-result"; -import { MoveUseMode } from "#enums/move-use-mode"; import { SpeciesId } from "#enums/species-id"; import { Stat } from "#enums/stat"; +import { StatusEffect } from "#enums/status-effect"; import { GameManager } from "#test/test-utils/game-manager"; import Phaser from "phaser"; import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest"; @@ -26,7 +26,6 @@ describe("Moves - Copycat", () => { beforeEach(() => { game = new GameManager(phaserGame); game.override - .moveset([MoveId.COPYCAT, MoveId.SPIKY_SHIELD, MoveId.SWORDS_DANCE, MoveId.SPLASH]) .ability(AbilityId.BALL_FETCH) .battleStyle("single") .criticalHits(false) @@ -35,24 +34,40 @@ describe("Moves - Copycat", () => { .enemyMoveset(MoveId.SPLASH); }); - it("should copy the last move successfully executed", async () => { + it("should copy the last move successfully executed by any Pokemon", async () => { await game.classicMode.startBattle([SpeciesId.FEEBAS]); - game.move.select(MoveId.SWORDS_DANCE); - await game.move.forceEnemyMove(MoveId.SPLASH); + game.move.use(MoveId.COPYCAT); + await game.move.forceEnemyMove(MoveId.SWORDS_DANCE); await game.setTurnOrder([BattlerIndex.ENEMY, BattlerIndex.PLAYER]); - await game.toNextTurn(); - - game.move.select(MoveId.COPYCAT); // Last successful move should be Swords Dance - await game.move.forceEnemyMove(MoveId.SUCKER_PUNCH); - await game.setTurnOrder([BattlerIndex.ENEMY, BattlerIndex.PLAYER]); - await game.toNextTurn(); + await game.toEndOfTurn(); const player = game.field.getPlayerPokemon(); - expect(player.getStatStage(Stat.ATK)).toBe(4); + expect(player.getStatStage(Stat.ATK)).toBe(2); expect(player.getLastXMoves()[0].move).toBe(MoveId.SWORDS_DANCE); }); + it('should update "last move" tracker for moves failing conditions, but not pre-move interrupts', async () => { + game.override.enemyStatusEffect(StatusEffect.SLEEP); + await game.classicMode.startBattle([SpeciesId.FEEBAS]); + + game.move.use(MoveId.SUCKER_PUNCH); + await game.move.forceEnemyMove(MoveId.SPLASH); + await game.setTurnOrder([BattlerIndex.ENEMY, BattlerIndex.PLAYER]); + await game.phaseInterceptor.to("MoveEndPhase"); + + // Enemy is asleep and should not have updated tracker + expect(game.scene.currentBattle.lastMove).toBe(MoveId.NONE); + + await game.phaseInterceptor.to("MoveEndPhase"); + + // Player sucker punch failed conditions, but still updated tracker + expect(game.scene.currentBattle.lastMove).toBe(MoveId.SUCKER_PUNCH); + + const player = game.field.getPlayerPokemon(); + expect(player.getLastXMoves()[0].result).toBe(MoveResult.FAIL); + }); + it("should fail if no prior moves have been made", async () => { await game.classicMode.startBattle([SpeciesId.FEEBAS]); diff --git a/test/moves/metronome.test.ts b/test/moves/metronome.test.ts index 189cddd367b..6c74c213062 100644 --- a/test/moves/metronome.test.ts +++ b/test/moves/metronome.test.ts @@ -8,7 +8,6 @@ import { MoveResult } from "#enums/move-result"; import { MoveUseMode } from "#enums/move-use-mode"; import { SpeciesId } from "#enums/species-id"; import { Stat } from "#enums/stat"; -import type { RandomMoveAttr } from "#moves/move"; import { GameManager } from "#test/test-utils/game-manager"; import Phaser from "phaser"; import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; @@ -132,7 +131,7 @@ describe("Moves - Metronome", () => { expect(solarBeamMove).toBeDefined(); game.move.use(MoveId.METRONOME); - await game.move.forceEnemyMove(MoveId.SPITE) + await game.move.forceEnemyMove(MoveId.SPITE); await game.setTurnOrder([BattlerIndex.PLAYER, BattlerIndex.ENEMY]); await game.toEndOfTurn(); @@ -181,7 +180,7 @@ describe("Moves - Metronome", () => { const enemyPokemon = game.field.getEnemyPokemon(); game.move.use(MoveId.METRONOME); - await game.toEndOfTurn() + await game.toEndOfTurn(); const isVisible = enemyPokemon.visible; const hasFled = enemyPokemon.switchOutStatus; diff --git a/test/moves/mirror-move.test.ts b/test/moves/mirror-move.test.ts index 6420ad37f50..cefe8fd71b6 100644 --- a/test/moves/mirror-move.test.ts +++ b/test/moves/mirror-move.test.ts @@ -39,13 +39,14 @@ describe("Moves - Mirror Move", () => { await game.classicMode.startBattle([SpeciesId.FEEBAS, SpeciesId.MAGIKARP]); game.move.use(MoveId.MIRROR_MOVE, BattlerIndex.PLAYER, BattlerIndex.ENEMY); - game.move.use(MoveId.SWORDS_DANCE, BattlerIndex.PLAYER_2, BattlerIndex.ENEMY_2); + game.move.use(MoveId.MIRROR_MOVE, BattlerIndex.PLAYER_2, BattlerIndex.ENEMY_2); await game.move.forceEnemyMove(MoveId.TACKLE, BattlerIndex.PLAYER_2); await game.move.forceEnemyMove(MoveId.SWORDS_DANCE); await game.setTurnOrder([BattlerIndex.ENEMY, BattlerIndex.ENEMY_2, BattlerIndex.PLAYER_2, BattlerIndex.PLAYER]); await game.toNextTurn(); - // Feebas copied enemy tackle against it + // Feebas copied enemy tackle against it; + // player 2 copied enemy swords dance and used it on itself const [feebas, magikarp] = game.scene.getPlayerField(); expect(feebas.getLastXMoves()[0]).toMatchObject({ move: MoveId.TACKLE, diff --git a/test/moves/nature-power.test.ts b/test/moves/nature-power.test.ts index e0a70a2dc10..e13bd0d9b57 100644 --- a/test/moves/nature-power.test.ts +++ b/test/moves/nature-power.test.ts @@ -1,12 +1,13 @@ import { allMoves } from "#app/data/data-lists"; import { TerrainType } from "#app/data/terrain"; import { getPokemonNameWithAffix } from "#app/messages"; -import { getEnumValues, toReadableString } from "#app/utils/common"; import { AbilityId } from "#enums/ability-id"; import { BiomeId } from "#enums/biome-id"; import { MoveId } from "#enums/move-id"; import { SpeciesId } from "#enums/species-id"; -import GameManager from "#test/testUtils/gameManager"; +import { GameManager } from "#test/test-utils/game-manager"; +import { getEnumValues } from "#utils/enums"; +import { toTitleCase } from "#utils/strings"; import i18next from "i18next"; import Phaser from "phaser"; import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest"; @@ -43,7 +44,7 @@ describe("Move - Nature Power", () => { it.each( getEnumValues(BiomeId).map(biome => ({ move: getNaturePowerType(TerrainType.NONE, biome), - moveName: toReadableString(MoveId[getNaturePowerType(TerrainType.NONE, biome)]), + moveName: toTitleCase(MoveId[getNaturePowerType(TerrainType.NONE, biome)]), biome, biomeName: BiomeId[biome], })), @@ -68,11 +69,11 @@ describe("Move - Nature Power", () => { it.todo.each( getEnumValues(TerrainType).map(terrain => ({ move: getNaturePowerType(terrain, BiomeId.TOWN), - moveName: toReadableString(MoveId[getNaturePowerType(terrain, BiomeId.TOWN)]), + moveName: toTitleCase(MoveId[getNaturePowerType(terrain, BiomeId.TOWN)]), terrain: terrain, terrainName: TerrainType[terrain], })), - )("should select $moveName if the current terrain is $terrainName", async ({ move /* terrain */ }) => { + )("should select $moveName if the current terrain is $terrainName", async ({ move /*, terrain */ }) => { // game.override.terrain(terrainType); await game.classicMode.startBattle([SpeciesId.FEEBAS]); diff --git a/test/moves/sketch.test.ts b/test/moves/sketch.test.ts index 34e76b26881..59e00434ee6 100644 --- a/test/moves/sketch.test.ts +++ b/test/moves/sketch.test.ts @@ -1,15 +1,13 @@ -import { allMoves } from "#data/data-lists"; import { AbilityId } from "#enums/ability-id"; import { BattlerIndex } from "#enums/battler-index"; import { MoveId } from "#enums/move-id"; import { MoveResult } from "#enums/move-result"; import { SpeciesId } from "#enums/species-id"; import { StatusEffect } from "#enums/status-effect"; -import { RandomMoveAttr } from "#moves/move"; import { PokemonMove } from "#moves/pokemon-move"; import { GameManager } from "#test/test-utils/game-manager"; import Phaser from "phaser"; -import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; +import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest"; describe("Moves - Sketch", () => { let phaserGame: Phaser.Game; @@ -81,20 +79,18 @@ describe("Moves - Sketch", () => { }); it("should sketch moves that call other moves", async () => { - const randomMoveAttr = allMoves[MoveId.METRONOME].findAttr( - attr => attr instanceof RandomMoveAttr, - ) as RandomMoveAttr; - vi.spyOn(randomMoveAttr, "getMove").mockReturnValue(MoveId.FALSE_SWIPE); - - game.override.enemyMoveset([MoveId.METRONOME]); + game.move.forceMetronomeMove(MoveId.FALSE_SWIPE); await game.classicMode.startBattle([SpeciesId.REGIELEKI]); - const playerPokemon = game.scene.getPlayerPokemon()!; - playerPokemon.moveset = [new PokemonMove(MoveId.SKETCH)]; + + const playerPokemon = game.field.getPlayerPokemon()!; + game.move.changeMoveset(playerPokemon, MoveId.SKETCH); // Opponent uses Metronome -> False Swipe, then player uses Sketch, which should sketch Metronome game.move.select(MoveId.SKETCH); + await game.move.forceEnemyMove(MoveId.METRONOME); await game.setTurnOrder([BattlerIndex.ENEMY, BattlerIndex.PLAYER]); await game.phaseInterceptor.to("TurnEndPhase"); + expect(playerPokemon.getLastXMoves()[0].result).toBe(MoveResult.SUCCESS); expect(playerPokemon.moveset[0]?.moveId).toBe(MoveId.METRONOME); expect(playerPokemon.hp).toBeLessThan(playerPokemon.getMaxHp()); // Make sure opponent actually used False Swipe diff --git a/test/test-utils/helpers/move-helper.ts b/test/test-utils/helpers/move-helper.ts index 15605edf31d..fc2f883f85f 100644 --- a/test/test-utils/helpers/move-helper.ts +++ b/test/test-utils/helpers/move-helper.ts @@ -331,7 +331,7 @@ export class MoveHelper extends GameManagerHelper { * @returns The spy that for Metronome that was mocked (Usually unneeded). */ public forceMetronomeMove(move: MoveId, once = false): MockInstance { - const spy = vi.spyOn(allMoves[MoveId.METRONOME].getAttrs("RandomMoveAttr")[0], "getMoveOverride"); + const spy = vi.spyOn(allMoves[MoveId.METRONOME].getAttrs("RandomMoveAttr")[0], "getMove"); if (once) { spy.mockReturnValueOnce(move); } else {