diff --git a/src/data/ability.ts b/src/data/ability.ts index cc6d6653a0d..a6af526720c 100644 --- a/src/data/ability.ts +++ b/src/data/ability.ts @@ -2550,7 +2550,7 @@ export class CommanderAbAttr extends AbAttr { override apply(pokemon: Pokemon, passive: boolean, simulated: boolean, cancelled: null, args: any[]): boolean { if (pokemon.scene.currentBattle?.double && pokemon.getAlly().species.speciesId === Species.DONDOZO) { - if (pokemon.getAlly().getTag(BattlerTagType.COMMANDER)) { + if (!pokemon.getAlly().isFainted() && pokemon.getAlly().getTag(BattlerTagType.COMMANDER)) { return false; } diff --git a/src/data/battler-tags.ts b/src/data/battler-tags.ts index c8822ee9725..64d47b6b27d 100644 --- a/src/data/battler-tags.ts +++ b/src/data/battler-tags.ts @@ -2085,7 +2085,9 @@ export class CommanderTag extends BattlerTag { /** Triggers an {@linkcode PokemonAnimType | animation} of the tagged Pokemon "spitting out" Tatsugiri */ override onRemove(pokemon: Pokemon): void { - pokemon.scene.triggerPokemonBattleAnim(pokemon, PokemonAnimType.COMMANDER_REMOVE); + if (this.getSourcePokemon(pokemon.scene)?.isActive(true)) { + pokemon.scene.triggerPokemonBattleAnim(pokemon, PokemonAnimType.COMMANDER_REMOVE); + } } } diff --git a/src/data/move.ts b/src/data/move.ts index 1bffa5c7128..6fefc97c412 100644 --- a/src/data/move.ts +++ b/src/data/move.ts @@ -707,6 +707,10 @@ export default class Move implements Localizable { getTargetBenefitScore(user: Pokemon, target: Pokemon, move: Move): integer { let score = 0; + if (target.getAlly()?.getTag(BattlerTagType.COMMANDER)?.getSourcePokemon(target.scene) === target) { + return 20 * (target.isPlayer() === user.isPlayer() ? -1 : 1); // always -20 with how the AI handles this score + } + for (const attr of this.attrs) { // conditionals to check if the move is self targeting (if so then you are applying the move to yourself, not the target) score += attr.getTargetBenefitScore(user, !attr.selfTarget ? target : user, move) * (target !== user && attr.selfTarget ? -1 : 1); diff --git a/src/phases/command-phase.ts b/src/phases/command-phase.ts index 6adcb8a6f50..ca83e6b540b 100644 --- a/src/phases/command-phase.ts +++ b/src/phases/command-phase.ts @@ -43,12 +43,12 @@ export class CommandPhase extends FieldPhase { } } - if (this.scene.currentBattle.turnCommands[this.fieldIndex]?.skip) { - return this.end(); - } - // If the Pokemon has applied Commander's effects to its ally, skip this command if (this.scene.currentBattle?.double && this.getPokemon().getAlly()?.getTag(BattlerTagType.COMMANDER)?.getSourcePokemon(this.scene) === this.getPokemon()) { + this.scene.currentBattle.turnCommands[this.fieldIndex] = { command: Command.FIGHT, move: { move: Moves.NONE, targets: []}, skip: true }; + } + + if (this.scene.currentBattle.turnCommands[this.fieldIndex]?.skip) { return this.end(); } diff --git a/src/test/abilities/commander.test.ts b/src/test/abilities/commander.test.ts new file mode 100644 index 00000000000..4d327c2697d --- /dev/null +++ b/src/test/abilities/commander.test.ts @@ -0,0 +1,196 @@ +import { BattlerIndex } from "#app/battle"; +import { BattlerTagType } from "#enums/battler-tag-type"; +import { PokemonAnimType } from "#enums/pokemon-anim-type"; +import { EffectiveStat, Stat } from "#enums/stat"; +import { StatusEffect } from "#enums/status-effect"; +import { WeatherType } from "#enums/weather-type"; +import { MoveResult } from "#app/field/pokemon"; +import { Abilities } from "#enums/abilities"; +import { Moves } from "#enums/moves"; +import { Species } from "#enums/species"; +import GameManager from "#test/utils/gameManager"; +import Phaser from "phaser"; +import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; + +describe("Abilities - Commander", () => { + let phaserGame: Phaser.Game; + let game: GameManager; + + beforeAll(() => { + phaserGame = new Phaser.Game({ + type: Phaser.HEADLESS, + }); + }); + + afterEach(() => { + game.phaseInterceptor.restoreOg(); + }); + + beforeEach(() => { + game = new GameManager(phaserGame); + game.override + .startingLevel(100) + .enemyLevel(100) + .moveset([ Moves.LIQUIDATION, Moves.MEMENTO, Moves.SPLASH, Moves.FLIP_TURN ]) + .ability(Abilities.COMMANDER) + .battleType("double") + .disableCrits() + .enemySpecies(Species.SNORLAX) + .enemyAbility(Abilities.BALL_FETCH) + .enemyMoveset(Moves.TACKLE); + + vi.spyOn(game.scene, "triggerPokemonBattleAnim").mockReturnValue(true); + }); + + it("causes the source to jump into Dondozo's mouth, granting a stat boost and hiding the source", async () => { + await game.classicMode.startBattle([ Species.TATSUGIRI, Species.DONDOZO ]); + + const [ tatsugiri, dondozo ] = game.scene.getPlayerField(); + + const affectedStats: EffectiveStat[] = [ Stat.ATK, Stat.DEF, Stat.SPATK, Stat.SPDEF, Stat.SPD ]; + + expect(game.scene.triggerPokemonBattleAnim).toHaveBeenLastCalledWith(tatsugiri, PokemonAnimType.COMMANDER_APPLY); + expect(dondozo.getTag(BattlerTagType.COMMANDER)).toBeDefined(); + affectedStats.forEach((stat) => expect(dondozo.getStatStage(stat)).toBe(2)); + + game.move.select(Moves.SPLASH, 1); + + expect(game.scene.currentBattle.turnCommands[0]?.skip).toBeTruthy(); + + // Force both enemies to target the Tatsugiri + await game.forceEnemyMove(Moves.TACKLE, BattlerIndex.PLAYER); + await game.forceEnemyMove(Moves.TACKLE, BattlerIndex.PLAYER); + + await game.phaseInterceptor.to("BerryPhase", false); + game.scene.getEnemyField().forEach(enemy => expect(enemy.getLastXMoves(1)[0].result).toBe(MoveResult.MISS)); + expect(tatsugiri.isFullHp()).toBeTruthy(); + }); + + it("should activate when a Dondozo switches in and cancel the source's move", async () => { + game.override.enemyMoveset(Moves.SPLASH); + + await game.classicMode.startBattle([ Species.TATSUGIRI, Species.MAGIKARP, Species.DONDOZO ]); + + const tatsugiri = game.scene.getPlayerField()[0]; + + game.move.select(Moves.LIQUIDATION, 0, BattlerIndex.ENEMY); + game.doSwitchPokemon(2); + + await game.phaseInterceptor.to("MovePhase", false); + expect(game.scene.triggerPokemonBattleAnim).toHaveBeenCalledWith(tatsugiri, PokemonAnimType.COMMANDER_APPLY); + + const dondozo = game.scene.getPlayerField()[1]; + expect(dondozo.getTag(BattlerTagType.COMMANDER)).toBeDefined(); + + await game.phaseInterceptor.to("BerryPhase", false); + expect(tatsugiri.getMoveHistory()).toHaveLength(0); + expect(game.scene.getEnemyField()[0].isFullHp()).toBeTruthy(); + }); + + it("source should reenter the field when Dondozo faints", async () => { + await game.classicMode.startBattle([ Species.TATSUGIRI, Species.DONDOZO ]); + + const [ tatsugiri, dondozo ] = game.scene.getPlayerField(); + + expect(game.scene.triggerPokemonBattleAnim).toHaveBeenLastCalledWith(tatsugiri, PokemonAnimType.COMMANDER_APPLY); + expect(dondozo.getTag(BattlerTagType.COMMANDER)).toBeDefined(); + + game.move.select(Moves.MEMENTO, 1, BattlerIndex.ENEMY); + + expect(game.scene.currentBattle.turnCommands[0]?.skip).toBeTruthy(); + + await game.forceEnemyMove(Moves.TACKLE, BattlerIndex.PLAYER); + await game.forceEnemyMove(Moves.TACKLE, BattlerIndex.PLAYER); + + await game.setTurnOrder([ BattlerIndex.PLAYER_2, BattlerIndex.ENEMY, BattlerIndex.ENEMY_2, BattlerIndex.PLAYER ]); + + await game.phaseInterceptor.to("FaintPhase", false); + expect(dondozo.getTag(BattlerTagType.COMMANDER)).toBeUndefined(); + expect(game.scene.triggerPokemonBattleAnim).toHaveBeenLastCalledWith(dondozo, PokemonAnimType.COMMANDER_REMOVE); + + await game.phaseInterceptor.to("BerryPhase", false); + expect(tatsugiri.isFullHp()).toBeFalsy(); + }); + + it("source should still take damage from Poison while hidden", async () => { + game.override + .statusEffect(StatusEffect.POISON) + .enemyMoveset(Moves.SPLASH); + + await game.classicMode.startBattle([ Species.TATSUGIRI, Species.DONDOZO ]); + + const [ tatsugiri, dondozo ] = game.scene.getPlayerField(); + + expect(game.scene.triggerPokemonBattleAnim).toHaveBeenLastCalledWith(tatsugiri, PokemonAnimType.COMMANDER_APPLY); + expect(dondozo.getTag(BattlerTagType.COMMANDER)).toBeDefined(); + + game.move.select(Moves.SPLASH, 1); + + expect(game.scene.currentBattle.turnCommands[0]?.skip).toBeTruthy(); + + await game.phaseInterceptor.to("TurnEndPhase"); + expect(tatsugiri.isFullHp()).toBeFalsy(); + }); + + it("source should still take damage from Salt Cure while hidden", async () => { + game.override.enemyMoveset(Moves.SPLASH); + + await game.classicMode.startBattle([ Species.TATSUGIRI, Species.DONDOZO ]); + + const [ tatsugiri, dondozo ] = game.scene.getPlayerField(); + + expect(game.scene.triggerPokemonBattleAnim).toHaveBeenLastCalledWith(tatsugiri, PokemonAnimType.COMMANDER_APPLY); + expect(dondozo.getTag(BattlerTagType.COMMANDER)).toBeDefined(); + + tatsugiri.addTag(BattlerTagType.SALT_CURED, 0, Moves.NONE, game.scene.getField()[BattlerIndex.ENEMY].id); + + game.move.select(Moves.SPLASH, 1); + + expect(game.scene.currentBattle.turnCommands[0]?.skip).toBeTruthy(); + + await game.phaseInterceptor.to("TurnEndPhase"); + expect(tatsugiri.isFullHp()).toBeFalsy(); + }); + + it("source should still take damage from Sandstorm while hidden", async () => { + game.override + .weather(WeatherType.SANDSTORM) + .enemyMoveset(Moves.SPLASH); + + await game.classicMode.startBattle([ Species.TATSUGIRI, Species.DONDOZO ]); + + const [ tatsugiri, dondozo ] = game.scene.getPlayerField(); + + expect(game.scene.triggerPokemonBattleAnim).toHaveBeenLastCalledWith(tatsugiri, PokemonAnimType.COMMANDER_APPLY); + expect(dondozo.getTag(BattlerTagType.COMMANDER)).toBeDefined(); + + game.move.select(Moves.SPLASH, 1); + + expect(game.scene.currentBattle.turnCommands[0]?.skip).toBeTruthy(); + + await game.phaseInterceptor.to("TurnEndPhase"); + expect(tatsugiri.isFullHp()).toBeFalsy(); + }); + + it("should make Dondozo immune to being forced out", async () => { + game.override.enemyMoveset([ Moves.SPLASH, Moves.WHIRLWIND ]); + + await game.classicMode.startBattle([ Species.TATSUGIRI, Species.DONDOZO ]); + + const [ tatsugiri, dondozo ] = game.scene.getPlayerField(); + + expect(game.scene.triggerPokemonBattleAnim).toHaveBeenLastCalledWith(tatsugiri, PokemonAnimType.COMMANDER_APPLY); + expect(dondozo.getTag(BattlerTagType.COMMANDER)).toBeDefined(); + + game.move.select(Moves.SPLASH, 1); + + expect(game.scene.currentBattle.turnCommands[0]?.skip).toBeTruthy(); + + await game.forceEnemyMove(Moves.WHIRLWIND, BattlerIndex.PLAYER_2); + await game.forceEnemyMove(Moves.SPLASH); + + // Test may time out here if Whirlwind forced out a Pokemon + await game.phaseInterceptor.to("TurnEndPhase"); + expect(dondozo.isActive(true)).toBeTruthy(); + }); +});