From 53e52aa275c3c84ecd70fcd4e0df23e07e534515 Mon Sep 17 00:00:00 2001 From: Acelynn Zhang Date: Mon, 28 Jul 2025 16:16:15 -0400 Subject: [PATCH 1/2] Fix behavior of ally-targeting moves during sleep --- src/phases/move-phase.ts | 23 +++++++++++++++-------- test/data/status-effect.test.ts | 31 ++++++++++++++++++++++++++++++- 2 files changed, 45 insertions(+), 9 deletions(-) diff --git a/src/phases/move-phase.ts b/src/phases/move-phase.ts index 5e85401db77..5606610d16d 100644 --- a/src/phases/move-phase.ts +++ b/src/phases/move-phase.ts @@ -128,14 +128,8 @@ export class MovePhase extends PokemonPhase { `color:${MOVE_COLOR}`, ); - // Check if move is unusable (e.g. running out of PP due to a mid-turn Spite - // or the user no longer being on field), ending the phase early if not. - if (!this.canMove(true)) { - if (this.pokemon.isActive(true)) { - this.fail(); - this.showMoveText(); - this.showFailedText(); - } + if (!this.pokemon.isActive(true)) { + this.cancel(); this.end(); return; } @@ -163,6 +157,7 @@ export class MovePhase extends PokemonPhase { this.resolveCounterAttackTarget(); + // Check status cancellation from sleep, freeze, etc. this.resolvePreMoveStatusEffects(); this.lapsePreMoveAndMoveTags(); @@ -188,6 +183,18 @@ export class MovePhase extends PokemonPhase { const targets = this.getActiveTargetPokemon(); const moveQueue = this.pokemon.getMoveQueue(); + // Check if move is unusable (e.g. running out of PP due to a mid-turn Spite + // or the user no longer being on field) + + if (!this.canMove(true)) { + if (this.pokemon.isActive(true)) { + this.fail(); + this.showMoveText(); + this.showFailedText(); + } + return; + } + if ( (targets.length === 0 && !this.move.getMove().hasAttr("AddArenaTrapTagAttr")) || (moveQueue.length > 0 && moveQueue[0].move === MoveId.NONE) diff --git a/test/data/status-effect.test.ts b/test/data/status-effect.test.ts index 20e164ce4d7..854bd2c8636 100644 --- a/test/data/status-effect.test.ts +++ b/test/data/status-effect.test.ts @@ -353,7 +353,7 @@ describe("Status Effects", () => { beforeEach(() => { game = new GameManager(phaserGame); game.override - .moveset([MoveId.SPLASH]) + .moveset([MoveId.SPLASH, MoveId.DRAGON_CHEER]) .ability(AbilityId.BALL_FETCH) .battleStyle("single") .criticalHits(false) @@ -390,6 +390,35 @@ describe("Status Effects", () => { expect(player.status).toBeFalsy(); expect(player.getLastXMoves(1)[0].result).toBe(MoveResult.SUCCESS); }); + + it("Sleep turns should tick down when failing to use ally-targeting moves", async () => { + await game.classicMode.startBattle([SpeciesId.FEEBAS]); + + const player = game.field.getPlayerPokemon(); + player.status = new Status(StatusEffect.SLEEP, 0, 4); + + game.move.select(MoveId.DRAGON_CHEER); + await game.toNextTurn(); + + expect(player.status.effect).toBe(StatusEffect.SLEEP); + + game.move.select(MoveId.DRAGON_CHEER); + await game.toNextTurn(); + + expect(player.status.effect).toBe(StatusEffect.SLEEP); + + game.move.select(MoveId.DRAGON_CHEER); + await game.toNextTurn(); + + expect(player.status.effect).toBe(StatusEffect.SLEEP); + expect(player.getLastXMoves(1)[0].result).toBe(MoveResult.FAIL); + + game.move.select(MoveId.DRAGON_CHEER); + await game.toNextTurn(); + + expect(player.status).toBeFalsy(); + expect(player.getLastXMoves(1)[0].result).toBe(MoveResult.FAIL); + }); }); describe("Behavior", () => { From 83ad5f16a251563c401c4a29f2c771ff82cf71e6 Mon Sep 17 00:00:00 2001 From: Bertie690 Date: Thu, 14 Aug 2025 16:21:55 -0400 Subject: [PATCH 2/2] removed duplicate checks for `isActive` --- src/phases/move-phase.ts | 16 ++++++++-------- test/data/status-effect.test.ts | 17 ++++++----------- 2 files changed, 14 insertions(+), 19 deletions(-) diff --git a/src/phases/move-phase.ts b/src/phases/move-phase.ts index 5606610d16d..e50c3143d41 100644 --- a/src/phases/move-phase.ts +++ b/src/phases/move-phase.ts @@ -98,15 +98,16 @@ export class MovePhase extends PokemonPhase { } /** - * Checks if the pokemon is active, if the move is usable, and that the move is targeting something. + * Check if the current Move is usable and targeting at least 1 active pokemon. * @param ignoreDisableTags `true` to not check if the move is disabled * @returns `true` if all the checks pass */ public canMove(ignoreDisableTags = false): boolean { + const targets = this.getActiveTargetPokemon(); return ( this.pokemon.isActive(true) && this.move.isUsable(this.pokemon, isIgnorePP(this.useMode), ignoreDisableTags) - && this.targets.length > 0 + && (targets.length > 0 || this.move.getMove().hasAttr("AddArenaTrapTagAttr")) ); } @@ -128,9 +129,9 @@ export class MovePhase extends PokemonPhase { `color:${MOVE_COLOR}`, ); + // If the target isn't on field (such as due to leaving the field from Whirlwind/etc), do nothing. if (!this.pokemon.isActive(true)) { - this.cancel(); - this.end(); + super.end(); return; } @@ -178,9 +179,8 @@ export class MovePhase extends PokemonPhase { this.end(); } - /** Check for cancellation edge cases - no targets remaining, or {@linkcode MoveId.NONE} is in the queue */ + /** Check for cancellation edge cases - no targets remaining, out of PP, or {@linkcode MoveId.NONE} is in the queue */ protected resolveFinalPreMoveCancellationChecks(): void { - const targets = this.getActiveTargetPokemon(); const moveQueue = this.pokemon.getMoveQueue(); // Check if move is unusable (e.g. running out of PP due to a mid-turn Spite @@ -196,12 +196,12 @@ export class MovePhase extends PokemonPhase { } if ( - (targets.length === 0 && !this.move.getMove().hasAttr("AddArenaTrapTagAttr")) + (this.targets.length === 0 && !this.move.getMove().hasAttr("AddArenaTrapTagAttr")) || (moveQueue.length > 0 && moveQueue[0].move === MoveId.NONE) ) { this.showMoveText(); this.showFailedText(); - this.cancel(); + this.fail(); } } diff --git a/test/data/status-effect.test.ts b/test/data/status-effect.test.ts index 854bd2c8636..de90ed18562 100644 --- a/test/data/status-effect.test.ts +++ b/test/data/status-effect.test.ts @@ -395,28 +395,23 @@ describe("Status Effects", () => { await game.classicMode.startBattle([SpeciesId.FEEBAS]); const player = game.field.getPlayerPokemon(); - player.status = new Status(StatusEffect.SLEEP, 0, 4); - - game.move.select(MoveId.DRAGON_CHEER); - await game.toNextTurn(); - - expect(player.status.effect).toBe(StatusEffect.SLEEP); - - game.move.select(MoveId.DRAGON_CHEER); - await game.toNextTurn(); - - expect(player.status.effect).toBe(StatusEffect.SLEEP); + // Set sleep turns to 2 for brevity + player.status = new Status(StatusEffect.SLEEP, 0, 2); + game.move.changeMoveset(player, MoveId.DRAGON_CHEER); game.move.select(MoveId.DRAGON_CHEER); await game.toNextTurn(); expect(player.status.effect).toBe(StatusEffect.SLEEP); + expect(player.getMoveset()[0].ppUsed).toBe(0); expect(player.getLastXMoves(1)[0].result).toBe(MoveResult.FAIL); game.move.select(MoveId.DRAGON_CHEER); await game.toNextTurn(); + // Sleep was cured, move failed as normal and consumed PP expect(player.status).toBeFalsy(); + expect(player.getMoveset()[0].ppUsed).toBe(1); expect(player.getLastXMoves(1)[0].result).toBe(MoveResult.FAIL); }); });