[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:
parent
9317093044
commit
a82d64b5b5
|
@ -1595,8 +1595,8 @@ export class PostAttackAbAttr extends AbAttr {
|
|||
private attackCondition: PokemonAttackCondition;
|
||||
|
||||
/** The default attackCondition requires that the selected move is a damaging move */
|
||||
constructor(attackCondition: PokemonAttackCondition = (user, target, move) => (move.category !== MoveCategory.STATUS)) {
|
||||
super();
|
||||
constructor(attackCondition: PokemonAttackCondition = (user, target, move) => (move.category !== MoveCategory.STATUS), showAbility: boolean = true) {
|
||||
super(showAbility);
|
||||
|
||||
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 {
|
||||
private stealCondition: PokemonAttackCondition | null;
|
||||
|
||||
|
@ -5597,7 +5631,7 @@ export function initAbilities() {
|
|||
.bypassFaint()
|
||||
.partial(),
|
||||
new Ability(Abilities.GORILLA_TACTICS, 8)
|
||||
.unimplemented(),
|
||||
.attr(GorillaTacticsAbAttr),
|
||||
new Ability(Abilities.NEUTRALIZING_GAS, 8)
|
||||
.attr(SuppressFieldAbilitiesAbAttr)
|
||||
.attr(UncopiableAbilityAbAttr)
|
||||
|
|
|
@ -119,7 +119,9 @@ export abstract class MoveRestrictionBattlerTag extends BattlerTag {
|
|||
const move = phase.move;
|
||||
|
||||
if (this.isMoveRestricted(move.moveId)) {
|
||||
if (this.interruptedText(pokemon, move.moveId)) {
|
||||
pokemon.scene.queueMessage(this.interruptedText(pokemon, move.moveId));
|
||||
}
|
||||
phase.cancel();
|
||||
}
|
||||
|
||||
|
@ -155,7 +157,9 @@ export abstract class MoveRestrictionBattlerTag extends BattlerTag {
|
|||
* @param {Moves} move {@linkcode Moves} ID of the move being 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
|
||||
*
|
||||
* 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.
|
||||
*/
|
||||
override onAdd(pokemon: Pokemon): void {
|
||||
|
@ -250,7 +254,12 @@ export class DisabledTag extends MoveRestrictionBattlerTag {
|
|||
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 {
|
||||
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.
|
||||
*/
|
||||
|
@ -2203,6 +2278,8 @@ export function getBattlerTag(tagType: BattlerTagType, turnCount: number, source
|
|||
return new TarShotTag();
|
||||
case BattlerTagType.THROAT_CHOPPED:
|
||||
return new ThroatChoppedTag();
|
||||
case BattlerTagType.GORILLA_TACTICS:
|
||||
return new GorillaTacticsTag();
|
||||
case BattlerTagType.NONE:
|
||||
default:
|
||||
return new BattlerTag(tagType, BattlerTagLapseType.CUSTOM, turnCount, sourceMove, sourceId);
|
||||
|
|
|
@ -73,6 +73,7 @@ export enum BattlerTagType {
|
|||
SHELL_TRAP = "SHELL_TRAP",
|
||||
DRAGON_CHEER = "DRAGON_CHEER",
|
||||
NO_RETREAT = "NO_RETREAT",
|
||||
GORILLA_TACTICS = "GORILLA_TACTICS",
|
||||
THROAT_CHOPPED = "THROAT_CHOPPED",
|
||||
TAR_SHOT = "TAR_SHOT",
|
||||
}
|
||||
|
|
|
@ -44,6 +44,7 @@
|
|||
"moveNotImplemented": "{{moveName}} is not yet implemented and cannot be selected.",
|
||||
"moveNoPP": "There's no PP left for\nthis move!",
|
||||
"moveDisabled": "{{moveName}} is disabled!",
|
||||
"canOnlyUseMove": "{{pokemonName}} can only use {{moveName}}!",
|
||||
"moveCannotBeSelected": "{{moveName}} cannot be selected!",
|
||||
"disableInterruptedMove": "{{pokemonNameWithAffix}}'s {{moveName}}\nis disabled!",
|
||||
"throatChopInterruptedMove": "The effects of Throat Chop prevent\n{{pokemonName}} from using certain moves!",
|
||||
|
|
|
@ -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);
|
||||
});
|
Loading…
Reference in New Issue