[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;
|
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)
|
||||||
|
|
|
@ -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)) {
|
||||||
|
if (this.interruptedText(pokemon, move.moveId)) {
|
||||||
pokemon.scene.queueMessage(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);
|
||||||
|
|
|
@ -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",
|
||||||
}
|
}
|
||||||
|
|
|
@ -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!",
|
||||||
|
|
|
@ -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