Skip to content

fix(move): resolve Parting Shot failing logic and switch out bugs#7152

Open
EduFabrizio wants to merge 1 commit intopagefaultgames:betafrom
EduFabrizio:fix/parting-shot
Open

fix(move): resolve Parting Shot failing logic and switch out bugs#7152
EduFabrizio wants to merge 1 commit intopagefaultgames:betafrom
EduFabrizio:fix/parting-shot

Conversation

@EduFabrizio
Copy link
Copy Markdown

Closes #5379

What are the changes the user will see?

  • Parting Shot will no longer say "But it failed!" when the user has no eligible Pokémon in their party to switch into. It will correctly drop the target's stats and end the turn.
  • The move will no longer force a switch if the stat-drop is blocked by abilities like Clear Body, Mist, Good as Gold, or if the target's stats are already at -6/-6.
  • The move correctly bypasses Substitute (due to being sound-based).

Why am I making these changes?
To resolve the core logic bugs associated with Parting Shot. The cause of the bug was that the move's attributes were decoupled. The switch was triggered unconditionally by ForceSwitchOutAttr, while the move would also unconditionally fail if the party was empty due to the same attribute's default conditions blocking the execution from the start.

What are the changes from a developer perspective?

  • Architectural Fix: Removed ForceSwitchOutAttr from the base move definition. Created a custom PartingShotAttr that directly inherits from StatStageChangeAttr.
  • Phase Simulation: Within apply(), the move now silently predicts if the stat-drop will be blocked by using applyAbAttrs(..., { simulated: true }) and checking stat bounds. The SwitchPhase is conditionally queued only if this simulation guarantees a successful drop and the user has a valid party.
  • AI Preservation: Overrode getUserBenefitScore to sum both the stat-drop and switch tactical scores, preserving the enemy AI's ability to evaluate the move correctly.
  • Test Suite: Fixed legacy .todo automated tests (which suffered from FaintPhase GameManager desyncs and assertion typos) and added new deterministic test coverage for empty parties, Contrary, and Substitute.

Screenshots/Videos
https://youtu.be/TBsUvtFkbv0

How to test the changes?
Run the automated test suite locally:
pnpm run test:silent test/tests/moves/parting-shot.test.ts

Checklist

The PR content is correctly formatted:

  • I'm using beta as my base branch
  • The current branch is not named beta, main or the name of another long-lived feature branch
  • I have provided a clear explanation of the changes within the PR description
  • The PR title matches the Conventional Commits format (as described in CONTRIBUTING.md)
  • The PR is self-contained and cannot be split into smaller PRs
  • There is no overlap with another open PR

The PR has been confirmed to work correctly:

  • I have tested the changes manually
  • The full automated test suite still passes (use pnpm test:silent to test locally)
  • I have created new automated tests (pnpm test:create) or updated existing tests related to the PR's changes if necessary
  • I have provided screenshots/videos of the changes (if applicable)

Are there any localization additions or changes? If so:

  • I have created an associated PR on the locales repository

Does this require any additions or changes to in-game assets? If so:

  • I have created an associated PR on the assets repository

@EduFabrizio EduFabrizio requested a review from a team as a code owner March 16, 2026 18:29
@Madmadness65 Madmadness65 added Move Affects a move P2 Bug Minor. Non crashing Incorrect move/ability/interaction labels Mar 16, 2026
Comment thread src/data/moves/move.ts Outdated
Comment thread src/data/moves/move.ts
Comment thread src/data/moves/move.ts Outdated
Comment thread src/data/moves/move.ts Outdated
Comment thread src/data/moves/move.ts Outdated
Comment on lines +3975 to +4018
override apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean {
const statDropTriggered = super.apply(user, target, move, args);

if (statDropTriggered) {
let willDrop = false;

const isBlockedByMist = !!globalScene.arena.getTagOnSide(
ArenaTagType.MIST,
target.isPlayer() ? ArenaTagSide.PLAYER : ArenaTagSide.ENEMY,
);

if (!isBlockedByMist) {
const hasContrary = target.hasAbility(AbilityId.CONTRARY);
const stageMod = hasContrary ? 1 : -1;

// Silently simulate immunities to predict if the stat drop will be blocked
const cancelledAtk = new BooleanHolder(false);
applyAbAttrs("ProtectStatAbAttr", {
pokemon: target,
stat: Stat.ATK,
stages: -1,
cancelled: cancelledAtk,
simulated: true,
});
const canChangeAtk =
!cancelledAtk.value
&& target.getStatStage(Stat.ATK) + stageMod >= -6
&& target.getStatStage(Stat.ATK) + stageMod <= 6;

const cancelledSpAtk = new BooleanHolder(false);
applyAbAttrs("ProtectStatAbAttr", {
pokemon: target,
stat: Stat.SPATK,
stages: -1,
cancelled: cancelledSpAtk,
simulated: true,
});
const canChangeSpAtk =
!cancelledSpAtk.value
&& target.getStatStage(Stat.SPATK) + stageMod >= -6
&& target.getStatStage(Stat.SPATK) + stageMod <= 6;

willDrop = canChangeAtk || canChangeSpAtk;
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@EduFabrizio
IIRC, these condition checks should be placed inside getCondition as otherwise the move will no-op without "failing" (for the handful of moves and effects that care about it).

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Bertie690 Thank you very much for the review and suggestions! I have pushed an update that implements your requested changes:

Denested the apply method logic using early returns.

Stored ForceSwitchOutAttr privately in the class constructor rather than re-creating it every time.

Moved the detailed explanation to the class description and added the TODO regarding the duplicated stat checks.

Regarding the suggestion to move the condition checks into getCondition():
I looked into doing this, but moving these checks to .condition() causes the move to fail outright and bypasses the ShowAbilityPhase entirely. If we do that, the player won't see the ability pop-up (e.g., "Clear Body prevented stat loss"), which breaks parity with the official games (this was a blocker point raised in a previous PR attempt for this move #3471).

By keeping it inside apply() and using the simulated: true check, we allow the base StatStageChangeAttr to naturally queue the ShowAbilityPhase if an immunity is hit, while accurately preventing the switch-out. Let me know if this denested approach looks good to you!

Fixes bug where Parting Shot unconditionally fails if the user has
no eligible party members. Reconstructs PartingShotAttr to extend
StatStageChangeAttr, simulating stat immunity (Clear Body, etc)
internally before queueing the SwitchPhase. Also fixes existing
automated tests logic.

Signed-off-by: Eduardo Siqueira <eduardosiqueira@tecnico.ulisboa.pt>
@EduFabrizio
Copy link
Copy Markdown
Author

Hi @Bertie690 . I just wanted to drop a quick ping to let you know that I've pushed the requested changes (denesting the logic, optimizing the attribute instantiation, and updating the docs/TODOs) and replied to the getCondition() thread above.

Whenever you have a moment, please let me know if the updates look good to you or if there is anything else you'd like me to adjust. Thanks again for your time and the great feedback!

@DayKev DayKev requested a review from Bertie690 April 5, 2026 01:03
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Move Affects a move P2 Bug Minor. Non crashing Incorrect move/ability/interaction

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug] Parting Shot Switches when Stat-Drop does not Succeed, Fails Outright with no Available Party

3 participants