[Ability] Implement Gorilla Tactics (#4051)

* fully implement gorilla tactics

* fix atk increase

* update oversight

* add showAbility param

* fix postmerge

* fix postmerge

* update tests
This commit is contained in:
Adrian T. 2024-09-10 03:41:53 +08:00 committed by GitHub
parent 9317093044
commit a82d64b5b5
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 204 additions and 7 deletions

View File

@ -1595,8 +1595,8 @@ export class PostAttackAbAttr extends AbAttr {
private attackCondition: PokemonAttackCondition; private attackCondition: PokemonAttackCondition;
/** The default attackCondition requires that the selected move is a damaging move */ /** The default attackCondition requires that the selected move is a damaging move */
constructor(attackCondition: PokemonAttackCondition = (user, target, move) => (move.category !== MoveCategory.STATUS)) { constructor(attackCondition: PokemonAttackCondition = (user, target, move) => (move.category !== MoveCategory.STATUS), showAbility: boolean = true) {
super(); super(showAbility);
this.attackCondition = attackCondition; this.attackCondition = attackCondition;
} }
@ -1624,6 +1624,40 @@ export class PostAttackAbAttr extends AbAttr {
} }
} }
/**
* Ability attribute for Gorilla Tactics
* @extends PostAttackAbAttr
*/
export class GorillaTacticsAbAttr extends PostAttackAbAttr {
constructor() {
super((user, target, move) => true, false);
}
/**
*
* @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
* @returns `true` if the ability is applied
*/
applyPostAttackAfterMoveTypeCheck(pokemon: Pokemon, passive: boolean, simulated: boolean, defender: Pokemon, move: Move, hitResult: HitResult | null, args: any[]): boolean | Promise<boolean> {
if (simulated) {
return simulated;
}
if (pokemon.getTag(BattlerTagType.GORILLA_TACTICS)) {
return false;
}
pokemon.addTag(BattlerTagType.GORILLA_TACTICS);
return true;
}
}
export class PostAttackStealHeldItemAbAttr extends PostAttackAbAttr { export class PostAttackStealHeldItemAbAttr extends PostAttackAbAttr {
private stealCondition: PokemonAttackCondition | null; private stealCondition: PokemonAttackCondition | null;
@ -5597,7 +5631,7 @@ export function initAbilities() {
.bypassFaint() .bypassFaint()
.partial(), .partial(),
new Ability(Abilities.GORILLA_TACTICS, 8) new Ability(Abilities.GORILLA_TACTICS, 8)
.unimplemented(), .attr(GorillaTacticsAbAttr),
new Ability(Abilities.NEUTRALIZING_GAS, 8) new Ability(Abilities.NEUTRALIZING_GAS, 8)
.attr(SuppressFieldAbilitiesAbAttr) .attr(SuppressFieldAbilitiesAbAttr)
.attr(UncopiableAbilityAbAttr) .attr(UncopiableAbilityAbAttr)

View File

@ -119,7 +119,9 @@ export abstract class MoveRestrictionBattlerTag extends BattlerTag {
const move = phase.move; const move = phase.move;
if (this.isMoveRestricted(move.moveId)) { if (this.isMoveRestricted(move.moveId)) {
pokemon.scene.queueMessage(this.interruptedText(pokemon, move.moveId)); if (this.interruptedText(pokemon, move.moveId)) {
pokemon.scene.queueMessage(this.interruptedText(pokemon, move.moveId));
}
phase.cancel(); phase.cancel();
} }
@ -155,7 +157,9 @@ export abstract class MoveRestrictionBattlerTag extends BattlerTag {
* @param {Moves} move {@linkcode Moves} ID of the move being interrupted * @param {Moves} move {@linkcode Moves} ID of the move being interrupted
* @returns {string} text to display when the move is interrupted * @returns {string} text to display when the move is interrupted
*/ */
abstract interruptedText(pokemon: Pokemon, move: Moves): string; interruptedText(pokemon: Pokemon, move: Moves): string {
return "";
}
} }
/** /**
@ -221,7 +225,7 @@ export class DisabledTag extends MoveRestrictionBattlerTag {
/** /**
* @override * @override
* *
* Ensures that move history exists on `pokemon` and has a valid move. If so, sets the {@link moveId} and shows a message. * Ensures that move history exists on `pokemon` and has a valid move. If so, sets the {@linkcode moveId} and shows a message.
* Otherwise the move ID will not get assigned and this tag will get removed next turn. * Otherwise the move ID will not get assigned and this tag will get removed next turn.
*/ */
override onAdd(pokemon: Pokemon): void { override onAdd(pokemon: Pokemon): void {
@ -250,7 +254,12 @@ export class DisabledTag extends MoveRestrictionBattlerTag {
return i18next.t("battle:moveDisabled", { moveName: allMoves[move].name }); return i18next.t("battle:moveDisabled", { moveName: allMoves[move].name });
} }
/** @override */ /**
* @override
* @param {Pokemon} pokemon {@linkcode Pokemon} attempting to use the restricted move
* @param {Moves} move {@linkcode Moves} ID of the move being interrupted
* @returns {string} text to display when the move is interrupted
*/
override interruptedText(pokemon: Pokemon, move: Moves): string { override interruptedText(pokemon: Pokemon, move: Moves): string {
return i18next.t("battle:disableInterruptedMove", { pokemonNameWithAffix: getPokemonNameWithAffix(pokemon), moveName: allMoves[move].name }); return i18next.t("battle:disableInterruptedMove", { pokemonNameWithAffix: getPokemonNameWithAffix(pokemon), moveName: allMoves[move].name });
} }
@ -262,6 +271,72 @@ 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 = Moves.NONE;
constructor() {
super(BattlerTagType.GORILLA_TACTICS, BattlerTagLapseType.CUSTOM, 0);
}
/** @override */
override isMoveRestricted(move: Moves): boolean {
return move !== this.moveId;
}
/**
* @override
* @param {Pokemon} pokemon the {@linkcode Pokemon} to check if the tag can be added
* @returns `true` if the pokemon has a valid move and no existing {@linkcode GorillaTacticsTag}; `false` otherwise
*/
override canAdd(pokemon: Pokemon): boolean {
return (this.getLastValidMove(pokemon) !== undefined) && !pokemon.getTag(GorillaTacticsTag);
}
/**
* Ensures that move history exists on {@linkcode Pokemon} and has a valid move.
* If so, sets the {@linkcode moveId} and increases the user's Attack by 50%.
* @override
* @param {Pokemon} pokemon the {@linkcode Pokemon} to add the tag to
*/
override onAdd(pokemon: Pokemon): void {
const lastValidMove = this.getLastValidMove(pokemon);
if (!lastValidMove) {
return;
}
this.moveId = lastValidMove;
pokemon.setStat(Stat.ATK, pokemon.getStat(Stat.ATK, false) * 1.5, false);
}
/**
*
* @override
* @param {Pokemon} pokemon n/a
* @param {Moves} move {@linkcode Moves} ID of the move being denied
* @returns {string} text to display when the move is denied
*/
override selectionDeniedText(pokemon: Pokemon, move: Moves): string {
return i18next.t("battle:canOnlyUseMove", { moveName: allMoves[this.moveId].name, pokemonName: getPokemonNameWithAffix(pokemon) });
}
/**
* Gets the last valid move from the pokemon's move history.
* @param {Pokemon} pokemon {@linkcode Pokemon} to get the last valid move from
* @returns {Moves | undefined} the last valid move from the pokemon's move history
*/
getLastValidMove(pokemon: Pokemon): Moves | undefined {
const move = pokemon.getLastXMoves()
.find(m => m.move !== Moves.NONE && m.move !== Moves.STRUGGLE && !m.virtual);
return move?.move;
}
}
/** /**
* BattlerTag that represents the "recharge" effects of moves like Hyper Beam. * BattlerTag that represents the "recharge" effects of moves like Hyper Beam.
*/ */
@ -2203,6 +2278,8 @@ export function getBattlerTag(tagType: BattlerTagType, turnCount: number, source
return new TarShotTag(); return new TarShotTag();
case BattlerTagType.THROAT_CHOPPED: case BattlerTagType.THROAT_CHOPPED:
return new ThroatChoppedTag(); return new ThroatChoppedTag();
case BattlerTagType.GORILLA_TACTICS:
return new GorillaTacticsTag();
case BattlerTagType.NONE: case BattlerTagType.NONE:
default: default:
return new BattlerTag(tagType, BattlerTagLapseType.CUSTOM, turnCount, sourceMove, sourceId); return new BattlerTag(tagType, BattlerTagLapseType.CUSTOM, turnCount, sourceMove, sourceId);

View File

@ -73,6 +73,7 @@ export enum BattlerTagType {
SHELL_TRAP = "SHELL_TRAP", SHELL_TRAP = "SHELL_TRAP",
DRAGON_CHEER = "DRAGON_CHEER", DRAGON_CHEER = "DRAGON_CHEER",
NO_RETREAT = "NO_RETREAT", NO_RETREAT = "NO_RETREAT",
GORILLA_TACTICS = "GORILLA_TACTICS",
THROAT_CHOPPED = "THROAT_CHOPPED", THROAT_CHOPPED = "THROAT_CHOPPED",
TAR_SHOT = "TAR_SHOT", TAR_SHOT = "TAR_SHOT",
} }

View File

@ -44,6 +44,7 @@
"moveNotImplemented": "{{moveName}} is not yet implemented and cannot be selected.", "moveNotImplemented": "{{moveName}} is not yet implemented and cannot be selected.",
"moveNoPP": "There's no PP left for\nthis move!", "moveNoPP": "There's no PP left for\nthis move!",
"moveDisabled": "{{moveName}} is disabled!", "moveDisabled": "{{moveName}} is disabled!",
"canOnlyUseMove": "{{pokemonName}} can only use {{moveName}}!",
"moveCannotBeSelected": "{{moveName}} cannot be selected!", "moveCannotBeSelected": "{{moveName}} cannot be selected!",
"disableInterruptedMove": "{{pokemonNameWithAffix}}'s {{moveName}}\nis disabled!", "disableInterruptedMove": "{{pokemonNameWithAffix}}'s {{moveName}}\nis disabled!",
"throatChopInterruptedMove": "The effects of Throat Chop prevent\n{{pokemonName}} from using certain moves!", "throatChopInterruptedMove": "The effects of Throat Chop prevent\n{{pokemonName}} from using certain moves!",

View File

@ -0,0 +1,84 @@
import { BattlerIndex } from "#app/battle";
import { Moves } from "#app/enums/moves";
import { Species } from "#app/enums/species";
import { Stat } from "#app/enums/stat";
import { Abilities } from "#enums/abilities";
import GameManager from "#test/utils/gameManager";
import Phaser from "phaser";
import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest";
describe("Abilities - Gorilla Tactics", () => {
let phaserGame: Phaser.Game;
let game: GameManager;
const TIMEOUT = 20 * 1000;
beforeAll(() => {
phaserGame = new Phaser.Game({
type: Phaser.HEADLESS,
});
});
afterEach(() => {
game.phaseInterceptor.restoreOg();
});
beforeEach(() => {
game = new GameManager(phaserGame);
game.override
.battleType("single")
.enemyAbility(Abilities.BALL_FETCH)
.enemyMoveset([Moves.SPLASH, Moves.DISABLE])
.enemySpecies(Species.MAGIKARP)
.enemyLevel(30)
.moveset([Moves.SPLASH, Moves.TACKLE])
.ability(Abilities.GORILLA_TACTICS);
});
it("boosts the Pokémon's Attack by 50%, but limits the Pokémon to using only one move", async () => {
await game.classicMode.startBattle([Species.GALAR_DARMANITAN]);
const darmanitan = game.scene.getPlayerPokemon()!;
const initialAtkStat = darmanitan.getStat(Stat.ATK);
game.move.select(Moves.SPLASH);
await game.forceEnemyMove(Moves.SPLASH);
await game.phaseInterceptor.to("TurnEndPhase");
expect(darmanitan.getStat(Stat.ATK, false)).toBeCloseTo(initialAtkStat * 1.5);
// Other moves should be restricted
expect(darmanitan.isMoveRestricted(Moves.TACKLE)).toBe(true);
expect(darmanitan.isMoveRestricted(Moves.SPLASH)).toBe(false);
}, TIMEOUT);
it("should struggle if the only usable move is disabled", async () => {
await game.classicMode.startBattle([Species.GALAR_DARMANITAN]);
const darmanitan = game.scene.getPlayerPokemon()!;
const enemy = game.scene.getEnemyPokemon()!;
game.move.select(Moves.SPLASH);
await game.phaseInterceptor.to("TurnEndPhase");
// Turn where Tackle is interrupted by Disable
await game.toNextTurn();
game.move.select(Moves.SPLASH);
await game.forceEnemyMove(Moves.DISABLE);
await game.setTurnOrder([BattlerIndex.ENEMY, BattlerIndex.PLAYER]);
await game.phaseInterceptor.to("TurnEndPhase");
expect(enemy.hp).toBe(enemy.getMaxHp());
// Turn where Struggle is used
await game.toNextTurn();
game.move.select(Moves.TACKLE);
await game.setTurnOrder([BattlerIndex.PLAYER, BattlerIndex.ENEMY]);
await game.phaseInterceptor.to("MoveEndPhase");
expect(darmanitan.hp).toBeLessThan(darmanitan.getMaxHp());
}, TIMEOUT);
});