diff --git a/src/data/abilities/ability.ts b/src/data/abilities/ability.ts index 6e6c00b8a43..d20eddcb3cb 100644 --- a/src/data/abilities/ability.ts +++ b/src/data/abilities/ability.ts @@ -4438,6 +4438,7 @@ export class PostDancingMoveAbAttr extends PostMoveUsedAbAttr { simulated: boolean, args: any[]): void { if (!simulated) { + dancer.turnData.extraTurns++; // If the move is an AttackMove or a StatusMove the Dancer must replicate the move on the source of the Dance if (move.getMove() instanceof AttackMove || move.getMove() instanceof StatusMove) { const target = this.getTarget(dancer, source, targets); diff --git a/src/data/battler-tags.ts b/src/data/battler-tags.ts index 4d99fd18ac0..456f519a34c 100644 --- a/src/data/battler-tags.ts +++ b/src/data/battler-tags.ts @@ -649,20 +649,14 @@ class NoRetreatTag extends TrappedTag { */ export class FlinchedTag extends BattlerTag { constructor(sourceMove: MoveId) { - super(BattlerTagType.FLINCHED, [BattlerTagLapseType.PRE_MOVE, BattlerTagLapseType.TURN_END], 0, sourceMove); - } - - onAdd(pokemon: Pokemon): void { - super.onAdd(pokemon); - - applyAbAttrs(FlinchEffectAbAttr, pokemon, null); + super(BattlerTagType.FLINCHED, [BattlerTagLapseType.PRE_MOVE, BattlerTagLapseType.TURN_END], 1, sourceMove); } /** - * Cancels the Pokemon's next Move on the turn this tag is applied - * @param pokemon The {@linkcode Pokemon} with this tag - * @param lapseType The {@linkcode BattlerTagLapseType lapse type} used for this function call - * @returns `false` (This tag is always removed after applying its effects) + * Cancels the flinched Pokemon's currently used move this turn if called mid-execution, or removes the tag at end of turn. + * @param pokemon - The {@linkcode Pokemon} with this tag. + * @param lapseType - The {@linkcode BattlerTagLapseType | lapse type} used for this function call. + * @returns Whether the tag should remain active. */ lapse(pokemon: Pokemon, lapseType: BattlerTagLapseType): boolean { if (lapseType === BattlerTagLapseType.PRE_MOVE) { @@ -672,6 +666,8 @@ export class FlinchedTag extends BattlerTag { pokemonNameWithAffix: getPokemonNameWithAffix(pokemon), }), ); + applyAbAttrs(FlinchEffectAbAttr, pokemon, null); + return true; } return super.lapse(pokemon, lapseType); diff --git a/test/abilities/dancer.test.ts b/test/abilities/dancer.test.ts index 7b4edb84789..086c69300b4 100644 --- a/test/abilities/dancer.test.ts +++ b/test/abilities/dancer.test.ts @@ -1,8 +1,10 @@ import { BattlerIndex } from "#app/battle"; +import { MoveResult } from "#app/field/pokemon"; import type { MovePhase } from "#app/phases/move-phase"; import { AbilityId } from "#enums/ability-id"; import { MoveId } from "#enums/move-id"; import { SpeciesId } from "#enums/species-id"; +import { Stat } from "#enums/stat"; import GameManager from "#test/testUtils/gameManager"; import Phaser from "phaser"; import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest"; @@ -99,4 +101,43 @@ describe("Abilities - Dancer", () => { expect(currentPhase.pokemon).toBe(oricorio); expect(currentPhase.move.moveId).toBe(MoveId.REVELATION_DANCE); }); + + it("should not break subsequent last hit only moves", async () => { + game.override.battleStyle("single"); + await game.classicMode.startBattle([SpeciesId.ORICORIO, SpeciesId.FEEBAS]); + + const [oricorio, feebas] = game.scene.getPlayerParty(); + + game.move.use(MoveId.BATON_PASS); + game.doSelectPartyPokemon(1); + await game.move.forceEnemyMove(MoveId.SWORDS_DANCE); + await game.setTurnOrder([BattlerIndex.ENEMY, BattlerIndex.PLAYER]); + await game.phaseInterceptor.to("TurnEndPhase"); + + expect(game.phaseInterceptor.log).toContain("SwitchSummonPhase"); + expect(game.field.getPlayerPokemon()).toBe(feebas); + expect(feebas.getStatStage(Stat.ATK)).toBe(2); + expect(oricorio.isOnField()).toBe(false); + expect(oricorio.visible).toBe(false); + }); + + it("should not trigger while flinched", async () => { + game.override.battleStyle("double").moveset(MoveId.SPLASH).enemyMoveset([MoveId.SWORDS_DANCE, MoveId.FAKE_OUT]); + await game.classicMode.startBattle([SpeciesId.ORICORIO]); + + const oricorio = game.scene.getPlayerPokemon()!; + expect(oricorio).toBeDefined(); + + // get faked out and copy swords dance + game.move.select(MoveId.SPLASH); + await game.move.forceEnemyMove(MoveId.SWORDS_DANCE); + await game.move.forceEnemyMove(MoveId.FAKE_OUT, BattlerIndex.PLAYER); + await game.phaseInterceptor.to("TurnEndPhase"); + + expect(oricorio.getLastXMoves(-1)[0]).toMatchObject({ + move: MoveId.NONE, + result: MoveResult.FAIL, + }); + expect(oricorio.getStatStage(Stat.ATK)).toBe(0); + }); }); diff --git a/test/moves/instruct.test.ts b/test/moves/instruct.test.ts index 56ac8d9d04d..4a701ed6ac5 100644 --- a/test/moves/instruct.test.ts +++ b/test/moves/instruct.test.ts @@ -1,13 +1,15 @@ import { BattlerIndex } from "#app/battle"; +import { allMoves } from "#app/data/data-lists"; import type Pokemon from "#app/field/pokemon"; import { MoveResult } from "#app/field/pokemon"; import type { MovePhase } from "#app/phases/move-phase"; import { AbilityId } from "#enums/ability-id"; import { MoveId } from "#enums/move-id"; import { SpeciesId } from "#enums/species-id"; +import { Stat } from "#enums/stat"; import GameManager from "#test/testUtils/gameManager"; import Phaser from "phaser"; -import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest"; +import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; describe("Moves - Instruct", () => { let phaserGame: Phaser.Game; @@ -34,7 +36,8 @@ describe("Moves - Instruct", () => { game.override .battleStyle("single") .enemySpecies(SpeciesId.SHUCKLE) - .enemyAbility(AbilityId.NO_GUARD) + .enemyAbility(AbilityId.BALL_FETCH) + .passiveAbility(AbilityId.NO_GUARD) .enemyLevel(100) .startingLevel(100) .disableCrits(); @@ -536,4 +539,27 @@ describe("Moves - Instruct", () => { expect(ivysaur.turnData.attacksReceived.length).toBe(15); }); + + it("should respect prior flinches and trigger Steadfast", async () => { + game.override.battleStyle("double"); + vi.spyOn(allMoves[MoveId.AIR_SLASH], "chance", "get").mockReturnValue(100); + await game.classicMode.startBattle([SpeciesId.AUDINO, SpeciesId.ABRA]); + + // Fake enemy 1 having attacked prior + const [, player2, enemy1, enemy2] = game.scene.getField(); + enemy1.pushMoveHistory({ move: MoveId.ABSORB, targets: [BattlerIndex.PLAYER] }); + game.field.mockAbility(enemy1, AbilityId.STEADFAST); + + game.move.use(MoveId.AIR_SLASH, BattlerIndex.PLAYER, BattlerIndex.ENEMY); + game.move.use(MoveId.INSTRUCT, BattlerIndex.PLAYER_2, BattlerIndex.ENEMY); + await game.move.forceEnemyMove(MoveId.ABSORB); + await game.move.forceEnemyMove(MoveId.INSTRUCT, BattlerIndex.ENEMY); + await game.setTurnOrder([BattlerIndex.PLAYER, BattlerIndex.ENEMY, BattlerIndex.PLAYER_2, BattlerIndex.ENEMY_2]); + await game.toEndOfTurn(); + + expect(enemy1.getLastXMoves(-1).map(m => m.move)).toEqual([MoveId.NONE, MoveId.NONE, MoveId.NONE, MoveId.ABSORB]); + expect(enemy1.getStatStage(Stat.SPD)).toBe(3); + expect(player2.getLastXMoves()[0].result).toBe(MoveResult.SUCCESS); + expect(enemy2.getLastXMoves()[0].result).toBe(MoveResult.SUCCESS); + }); });