[Bug] Dancer no longer breaks "last hit only" moves, respects flinch + steadfast (#5945)

* WIP

* Fixed Dancer last hit, flinch move interaction

* Fixed steadfast interaction

* Fixed comment + flaky test

---------

Co-authored-by: NightKev <34855794+DayKev@users.noreply.github.com>
This commit is contained in:
Bertie690 2025-06-06 23:50:16 -04:00 committed by GitHub
parent 88e4ab978b
commit a818c2b33f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 77 additions and 13 deletions

View File

@ -4438,6 +4438,7 @@ export class PostDancingMoveAbAttr extends PostMoveUsedAbAttr {
simulated: boolean, simulated: boolean,
args: any[]): void { args: any[]): void {
if (!simulated) { 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 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) { if (move.getMove() instanceof AttackMove || move.getMove() instanceof StatusMove) {
const target = this.getTarget(dancer, source, targets); const target = this.getTarget(dancer, source, targets);

View File

@ -649,20 +649,14 @@ class NoRetreatTag extends TrappedTag {
*/ */
export class FlinchedTag extends BattlerTag { export class FlinchedTag extends BattlerTag {
constructor(sourceMove: MoveId) { constructor(sourceMove: MoveId) {
super(BattlerTagType.FLINCHED, [BattlerTagLapseType.PRE_MOVE, BattlerTagLapseType.TURN_END], 0, sourceMove); super(BattlerTagType.FLINCHED, [BattlerTagLapseType.PRE_MOVE, BattlerTagLapseType.TURN_END], 1, sourceMove);
}
onAdd(pokemon: Pokemon): void {
super.onAdd(pokemon);
applyAbAttrs(FlinchEffectAbAttr, pokemon, null);
} }
/** /**
* Cancels the Pokemon's next Move on the turn this tag is applied * 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 pokemon - The {@linkcode Pokemon} with this tag.
* @param lapseType The {@linkcode BattlerTagLapseType lapse type} used for this function call * @param lapseType - The {@linkcode BattlerTagLapseType | lapse type} used for this function call.
* @returns `false` (This tag is always removed after applying its effects) * @returns Whether the tag should remain active.
*/ */
lapse(pokemon: Pokemon, lapseType: BattlerTagLapseType): boolean { lapse(pokemon: Pokemon, lapseType: BattlerTagLapseType): boolean {
if (lapseType === BattlerTagLapseType.PRE_MOVE) { if (lapseType === BattlerTagLapseType.PRE_MOVE) {
@ -672,6 +666,8 @@ export class FlinchedTag extends BattlerTag {
pokemonNameWithAffix: getPokemonNameWithAffix(pokemon), pokemonNameWithAffix: getPokemonNameWithAffix(pokemon),
}), }),
); );
applyAbAttrs(FlinchEffectAbAttr, pokemon, null);
return true;
} }
return super.lapse(pokemon, lapseType); return super.lapse(pokemon, lapseType);

View File

@ -1,8 +1,10 @@
import { BattlerIndex } from "#app/battle"; import { BattlerIndex } from "#app/battle";
import { MoveResult } from "#app/field/pokemon";
import type { MovePhase } from "#app/phases/move-phase"; import type { MovePhase } from "#app/phases/move-phase";
import { AbilityId } from "#enums/ability-id"; import { AbilityId } from "#enums/ability-id";
import { MoveId } from "#enums/move-id"; import { MoveId } from "#enums/move-id";
import { SpeciesId } from "#enums/species-id"; import { SpeciesId } from "#enums/species-id";
import { Stat } from "#enums/stat";
import GameManager from "#test/testUtils/gameManager"; import GameManager from "#test/testUtils/gameManager";
import Phaser from "phaser"; import Phaser from "phaser";
import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest"; import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest";
@ -99,4 +101,43 @@ describe("Abilities - Dancer", () => {
expect(currentPhase.pokemon).toBe(oricorio); expect(currentPhase.pokemon).toBe(oricorio);
expect(currentPhase.move.moveId).toBe(MoveId.REVELATION_DANCE); 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);
});
}); });

View File

@ -1,13 +1,15 @@
import { BattlerIndex } from "#app/battle"; import { BattlerIndex } from "#app/battle";
import { allMoves } from "#app/data/data-lists";
import type Pokemon from "#app/field/pokemon"; import type Pokemon from "#app/field/pokemon";
import { MoveResult } from "#app/field/pokemon"; import { MoveResult } from "#app/field/pokemon";
import type { MovePhase } from "#app/phases/move-phase"; import type { MovePhase } from "#app/phases/move-phase";
import { AbilityId } from "#enums/ability-id"; import { AbilityId } from "#enums/ability-id";
import { MoveId } from "#enums/move-id"; import { MoveId } from "#enums/move-id";
import { SpeciesId } from "#enums/species-id"; import { SpeciesId } from "#enums/species-id";
import { Stat } from "#enums/stat";
import GameManager from "#test/testUtils/gameManager"; import GameManager from "#test/testUtils/gameManager";
import Phaser from "phaser"; 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", () => { describe("Moves - Instruct", () => {
let phaserGame: Phaser.Game; let phaserGame: Phaser.Game;
@ -34,7 +36,8 @@ describe("Moves - Instruct", () => {
game.override game.override
.battleStyle("single") .battleStyle("single")
.enemySpecies(SpeciesId.SHUCKLE) .enemySpecies(SpeciesId.SHUCKLE)
.enemyAbility(AbilityId.NO_GUARD) .enemyAbility(AbilityId.BALL_FETCH)
.passiveAbility(AbilityId.NO_GUARD)
.enemyLevel(100) .enemyLevel(100)
.startingLevel(100) .startingLevel(100)
.disableCrits(); .disableCrits();
@ -536,4 +539,27 @@ describe("Moves - Instruct", () => {
expect(ivysaur.turnData.attacksReceived.length).toBe(15); 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);
});
}); });