[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,
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);

View File

@ -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);

View File

@ -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);
});
});

View File

@ -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);
});
});