diff --git a/src/data/abilities/ability.ts b/src/data/abilities/ability.ts index c9df6922193..d2a2da13373 100644 --- a/src/data/abilities/ability.ts +++ b/src/data/abilities/ability.ts @@ -70,14 +70,14 @@ import { CommonAnim } from "../battle-anims"; import { getBerryEffectFunc } from "#app/data/berry"; import { BerryUsedEvent } from "#app/events/battle-scene"; import { noAbilityTypeOverrideMoves } from "#app/data/moves/invalid-moves"; -import { MoveUseMode } from "#enums/move-use-mode"; +import { isVirtual, MoveUseMode } from "#enums/move-use-mode"; // Type imports import type { StatStageChangePhase } from "#app/phases/stat-stage-change-phase"; import type { BattleStat, EffectiveStat } from "#enums/stat"; import type { BerryType } from "#enums/berry-type"; import type Pokemon from "#app/field/pokemon"; -import type { EnemyPokemon, PokemonMove } from "#app/field/pokemon"; +import type { EnemyPokemon, PokemonMove, TurnMove } from "#app/field/pokemon"; import type { Weather } from "#app/data/weather"; import type { BattlerTag } from "#app/data/battler-tags"; import type { @@ -2536,48 +2536,32 @@ export class AllyStatMultiplierAbAttr extends AbAttr { } /** - * Ability attribute for Gorilla Tactics - * @extends PostAttackAbAttr + * Takes effect whenever a move succesfully executes, such as gorilla tactics' move-locking. + * (More specifically, whenever a move is pushed to the move history) */ -export class GorillaTacticsAbAttr extends PostAttackAbAttr { - constructor() { - super((_user, _target, _move) => true, false); +export class ExecutedMoveAbAttr extends AbAttr { + canApplyExecutedMove(_pokemon: Pokemon, _simulated: boolean): boolean { + return true; } - override canApplyPostAttack( - pokemon: Pokemon, - passive: boolean, - simulated: boolean, - defender: Pokemon, - move: Move, - hitResult: HitResult | null, - args: any[], - ): boolean { - return ( - (super.canApplyPostAttack(pokemon, passive, simulated, defender, move, hitResult, args) && simulated) || - !pokemon.getTag(BattlerTagType.GORILLA_TACTICS) - ); + applyExecutedMove(_pokemon: Pokemon, _simulated: boolean): void {} +} + +/** + * Ability attribute for {@linkcode AbilityId.GORILLA_TACTICS | Gorilla Tactics} + * to lock the user into its first selected move. + */ +export class GorillaTacticsAbAttr extends ExecutedMoveAbAttr { + constructor(showAbility = false) { + super(showAbility); } - /** - * - * @param {Pokemon} pokemon the {@linkcode Pokemon} with this ability - * @param _passive n/a - * @param simulated whether the ability is being simulated - * @param _defender n/a - * @param _move n/a - * @param _hitResult n/a - * @param _args n/a - */ - override applyPostAttack( - pokemon: Pokemon, - _passive: boolean, - simulated: boolean, - _defender: Pokemon, - _move: Move, - _hitResult: HitResult | null, - _args: any[], - ): void { + override canApplyExecutedMove(pokemon: Pokemon, _simulated: boolean): boolean { + // Gorilla Tactics does not trigger on called/reflected moves, only the calling move. + return !pokemon.canAddTag(BattlerTagType.GORILLA_TACTICS); + } + + override applyExecutedMove(pokemon: Pokemon, simulated: boolean): void { if (!simulated) { pokemon.addTag(BattlerTagType.GORILLA_TACTICS); } @@ -7800,6 +7784,22 @@ export function applyPreAttackAbAttrs( ); } +export function applyExecutedMoveAbAttrs( + attrType: Constructor, + pokemon: Pokemon, + simulated: boolean = false, + ...args: any[] +): void { + applyAbAttrsInternal( + attrType, + pokemon, + attr => attr.applyExecutedMove(pokemon, simulated), + attr => attr.canApplyExecutedMove(pokemon, simulated), + args, + simulated, + ); +} + export function applyPostAttackAbAttrs( attrType: Constructor, pokemon: Pokemon, diff --git a/src/data/battler-tags.ts b/src/data/battler-tags.ts index 4f9077a0fc0..192acecc6c9 100644 --- a/src/data/battler-tags.ts +++ b/src/data/battler-tags.ts @@ -362,7 +362,6 @@ export class DisabledTag extends MoveRestrictionBattlerTag { /** * Tag used by Gorilla Tactics to restrict the user to using only one move. - * @extends MoveRestrictionBattlerTag */ export class GorillaTacticsTag extends MoveRestrictionBattlerTag { private moveId = MoveId.NONE; @@ -371,7 +370,6 @@ export class GorillaTacticsTag extends MoveRestrictionBattlerTag { super(BattlerTagType.GORILLA_TACTICS, BattlerTagLapseType.CUSTOM, 0); } - /** @override */ override isMoveRestricted(move: MoveId): boolean { return move !== this.moveId; } @@ -386,8 +384,7 @@ export class GorillaTacticsTag extends MoveRestrictionBattlerTag { const lastSelectedMove = pokemon.getLastNonVirtualMove(); return ( !isNullOrUndefined(lastSelectedMove) && - lastSelectedMove.move !== MoveId.STRUGGLE && - !pokemon.getTag(GorillaTacticsTag) + lastSelectedMove.move !== MoveId.STRUGGLE ); } @@ -408,19 +405,17 @@ export class GorillaTacticsTag extends MoveRestrictionBattlerTag { * @override * @param source Gorilla Tactics' {@linkcode BattlerTag} information */ - public override loadTag(source: BattlerTag | any): void { + override loadTag(source: BattlerTag | any): void { super.loadTag(source); this.moveId = source.moveId; } /** - * - * @override - * @param {Pokemon} pokemon n/a - * @param {MoveId} _move {@linkcode MoveId} ID of the move being denied - * @returns {string} text to display when the move is denied + * Return the text displayed when a move is restricted. + * @param pokemon - The {@linkcode Pokemon} with this tag. + * @returns A string containing the text to display when the move is denied */ - override selectionDeniedText(pokemon: Pokemon, _move: MoveId): string { + override selectionDeniedText(pokemon: Pokemon): string { return i18next.t("battle:canOnlyUseMove", { moveName: allMoves[this.moveId].name, pokemonName: getPokemonNameWithAffix(pokemon), diff --git a/src/phases/move-effect-phase.ts b/src/phases/move-effect-phase.ts index 13319c043d1..6fffc730a0f 100644 --- a/src/phases/move-effect-phase.ts +++ b/src/phases/move-effect-phase.ts @@ -3,10 +3,12 @@ import { globalScene } from "#app/global-scene"; import { AddSecondStrikeAbAttr, AlwaysHitAbAttr, + applyExecutedMoveAbAttrs, applyPostAttackAbAttrs, applyPostDamageAbAttrs, applyPostDefendAbAttrs, applyPreAttackAbAttrs, + ExecutedMoveAbAttr, IgnoreMoveEffectsAbAttr, MaxMultiHitAbAttr, PostAttackAbAttr, @@ -389,6 +391,7 @@ export class MoveEffectPhase extends PokemonPhase { // Add to the move history entry if (this.firstHit) { user.pushMoveHistory(this.moveHistoryEntry); + applyExecutedMoveAbAttrs(ExecutedMoveAbAttr, user); } try { diff --git a/test/abilities/gorilla_tactics.test.ts b/test/abilities/gorilla_tactics.test.ts index 0ac6e6add64..04dff0bcd37 100644 --- a/test/abilities/gorilla_tactics.test.ts +++ b/test/abilities/gorilla_tactics.test.ts @@ -7,6 +7,8 @@ import { Stat } from "#app/enums/stat"; import GameManager from "#test/testUtils/gameManager"; import Phaser from "phaser"; import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; +import { MoveResult } from "#app/field/pokemon"; +import { MoveUseMode } from "#enums/move-use-mode"; describe("Abilities - Gorilla Tactics", () => { let phaserGame: Phaser.Game; @@ -40,7 +42,7 @@ describe("Abilities - Gorilla Tactics", () => { game.move.select(MoveId.SPLASH); await game.move.forceEnemyMove(MoveId.SPLASH); - await game.toEndOfTurn() + await game.toEndOfTurn(); expect(darmanitan.getStat(Stat.ATK, false)).toBeCloseTo(initialAtkStat * 1.5); // Other moves should be restricted @@ -69,6 +71,7 @@ describe("Abilities - Gorilla Tactics", () => { // Third turn, Struggle is used game.move.select(MoveId.TACKLE); + await game.move.forceEnemyMove(MoveId.SPLASH); //prevent protect from being used by the enemy await game.setTurnOrder([BattlerIndex.PLAYER, BattlerIndex.ENEMY]); await game.phaseInterceptor.to("MoveEndPhase"); @@ -90,5 +93,37 @@ describe("Abilities - Gorilla Tactics", () => { // Gorilla Tactics should bypass dancer and instruct expect(darmanitan.isMoveRestricted(MoveId.TACKLE)).toBe(true); expect(darmanitan.isMoveRestricted(MoveId.METRONOME)).toBe(false); + expect(darmanitan.getLastXMoves(-1)).toEqual([ + expect.objectContaining({ move: MoveId.TACKLE, result: MoveResult.SUCCESS, useMode: MoveUseMode.FOLLOW_UP }), + expect.objectContaining({ move: MoveId.METRONOME, result: MoveResult.SUCCESS, useMode: MoveUseMode.NORMAL }), + ]); + }); + + it("should activate when the opponenet protects", async () => { + await game.classicMode.startBattle([SpeciesId.GALAR_DARMANITAN]); + + const darmanitan = game.field.getPlayerPokemon(); + + game.move.select(MoveId.TACKLE); + await game.move.selectEnemyMove(MoveId.PROTECT); + + await game.toEndOfTurn(); + expect(darmanitan.isMoveRestricted(MoveId.SPLASH)).toBe(true); + expect(darmanitan.isMoveRestricted(MoveId.TACKLE)).toBe(false); + }); + + it("should activate when a move is succesfully executed but misses", async () => { + await game.classicMode.startBattle([SpeciesId.GALAR_DARMANITAN]); + + const darmanitan = game.field.getPlayerPokemon(); + + game.move.select(MoveId.TACKLE); + await game.move.selectEnemyMove(MoveId.SPLASH); + await game.setTurnOrder([BattlerIndex.PLAYER, BattlerIndex.ENEMY]); + await game.move.forceMiss(); + await game.toEndOfTurn(); + + expect(darmanitan.isMoveRestricted(MoveId.SPLASH)).toBe(true); + expect(darmanitan.isMoveRestricted(MoveId.TACKLE)).toBe(false); }); });