[Ability] Refactor Gulp Missile and make it trigger when Cramorant faints (#4428)
* reimplement gulp missile * cleanup + docs * more cleanup * add override * update paths
This commit is contained in:
parent
d620b5c7fa
commit
4c327e9e63
|
@ -4,7 +4,7 @@ import { Constructor } from "#app/utils";
|
|||
import * as Utils from "../utils";
|
||||
import { getPokemonNameWithAffix } from "../messages";
|
||||
import { Weather, WeatherType } from "./weather";
|
||||
import { BattlerTag, GroundedTag, GulpMissileTag, SemiInvulnerableTag } from "./battler-tags";
|
||||
import { BattlerTag, GroundedTag } from "./battler-tags";
|
||||
import { StatusEffect, getNonVolatileStatusEffects, getStatusEffectDescriptor, getStatusEffectHealText } from "./status-effect";
|
||||
import { Gender } from "./gender";
|
||||
import Move, { AttackMove, MoveCategory, MoveFlags, MoveTarget, FlinchAttr, OneHitKOAttr, HitHealAttr, allMoves, StatusMove, SelfStatusMove, VariablePowerAttr, applyMoveAttrs, IncrementMovePriorityAttr, VariableMoveTypeAttr, RandomMovesetMoveAttr, RandomMoveAttr, NaturePowerAttr, CopyMoveAttr, MoveAttr, MultiHitAttr, ChargeAttr, SacrificialAttr, SacrificialAttrOnHit, NeutralDamageAgainstFlyingTypeMultiplierAttr, FixedDamageAttr } from "./move";
|
||||
|
@ -536,53 +536,6 @@ export class PostDefendAbAttr extends AbAttr {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Applies the effects of Gulp Missile when the user is hit by an attack.
|
||||
* @extends PostDefendAbAttr
|
||||
*/
|
||||
export class PostDefendGulpMissileAbAttr extends PostDefendAbAttr {
|
||||
constructor() {
|
||||
super(true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Damages the attacker and triggers the secondary effect based on the form or the BattlerTagType.
|
||||
* @param {Pokemon} pokemon - The defending Pokemon.
|
||||
* @param passive - n/a
|
||||
* @param {Pokemon} attacker - The attacking Pokemon.
|
||||
* @param {Move} move - The move being used.
|
||||
* @param {HitResult} hitResult - n/a
|
||||
* @param {any[]} args - n/a
|
||||
* @returns Whether the effects of the ability are applied.
|
||||
*/
|
||||
applyPostDefend(pokemon: Pokemon, passive: boolean, simulated: boolean, attacker: Pokemon, move: Move, hitResult: HitResult, args: any[]): boolean | Promise<boolean> {
|
||||
const battlerTag = pokemon.getTag(GulpMissileTag);
|
||||
if (!battlerTag || move.category === MoveCategory.STATUS || pokemon.getTag(SemiInvulnerableTag)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (simulated) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const cancelled = new Utils.BooleanHolder(false);
|
||||
applyAbAttrs(BlockNonDirectDamageAbAttr, attacker, cancelled);
|
||||
|
||||
if (!cancelled.value) {
|
||||
attacker.damageAndUpdate(Math.max(1, Math.floor(attacker.getMaxHp() / 4)), HitResult.OTHER);
|
||||
}
|
||||
|
||||
if (battlerTag.tagType === BattlerTagType.GULP_MISSILE_ARROKUDA) {
|
||||
pokemon.scene.unshiftPhase(new StatStageChangePhase(pokemon.scene, attacker.getBattlerIndex(), false, [ Stat.DEF ], -1));
|
||||
} else {
|
||||
attacker.trySetStatus(StatusEffect.PARALYSIS, true, pokemon);
|
||||
}
|
||||
|
||||
pokemon.removeTag(battlerTag.tagType);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
export class FieldPriorityMoveImmunityAbAttr extends PreDefendAbAttr {
|
||||
applyPreDefend(pokemon: Pokemon, passive: boolean, simulated: boolean, attacker: Pokemon, move: Move, cancelled: Utils.BooleanHolder, args: any[]): boolean {
|
||||
const attackPriority = new Utils.IntegerHolder(move.priority);
|
||||
|
@ -5668,13 +5621,19 @@ export function initAbilities() {
|
|||
new Ability(Abilities.MIRROR_ARMOR, 8)
|
||||
.ignorable()
|
||||
.unimplemented(),
|
||||
/**
|
||||
* Right now, the logic is attached to Surf and Dive moves. Ideally, the post-defend/hit should be an
|
||||
* ability attribute but the current implementation of move effects for BattlerTag does not support this- in the case
|
||||
* where Cramorant is fainted.
|
||||
* @see {@linkcode GulpMissileTagAttr} and {@linkcode GulpMissileTag} for Gulp Missile implementation
|
||||
*/
|
||||
new Ability(Abilities.GULP_MISSILE, 8)
|
||||
.attr(UnsuppressableAbilityAbAttr)
|
||||
.attr(NoTransformAbilityAbAttr)
|
||||
.attr(NoFusionAbilityAbAttr)
|
||||
.attr(UncopiableAbilityAbAttr)
|
||||
.attr(UnswappableAbilityAbAttr)
|
||||
.attr(PostDefendGulpMissileAbAttr),
|
||||
.bypassFaint(),
|
||||
new Ability(Abilities.STALWART, 8)
|
||||
.attr(BlockRedirectAbAttr),
|
||||
new Ability(Abilities.STEAM_ENGINE, 8)
|
||||
|
|
|
@ -2123,7 +2123,36 @@ export class StockpilingTag extends BattlerTag {
|
|||
*/
|
||||
export class GulpMissileTag extends BattlerTag {
|
||||
constructor(tagType: BattlerTagType, sourceMove: Moves) {
|
||||
super(tagType, BattlerTagLapseType.CUSTOM, 0, sourceMove);
|
||||
super(tagType, BattlerTagLapseType.HIT, 0, sourceMove);
|
||||
}
|
||||
|
||||
override lapse(pokemon: Pokemon, lapseType: BattlerTagLapseType): boolean {
|
||||
if (pokemon.getTag(BattlerTagType.UNDERWATER)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const moveEffectPhase = pokemon.scene.getCurrentPhase();
|
||||
if (moveEffectPhase instanceof MoveEffectPhase) {
|
||||
const attacker = moveEffectPhase.getUserPokemon();
|
||||
|
||||
if (!attacker) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const cancelled = new Utils.BooleanHolder(false);
|
||||
applyAbAttrs(BlockNonDirectDamageAbAttr, attacker, cancelled);
|
||||
|
||||
if (!cancelled.value) {
|
||||
attacker.damageAndUpdate(Math.max(1, Math.floor(attacker.getMaxHp() / 4)), HitResult.OTHER);
|
||||
}
|
||||
|
||||
if (this.tagType === BattlerTagType.GULP_MISSILE_ARROKUDA) {
|
||||
pokemon.scene.unshiftPhase(new StatStageChangePhase(pokemon.scene, attacker.getBattlerIndex(), false, [ Stat.DEF ], -1));
|
||||
} else {
|
||||
attacker.trySetStatus(StatusEffect.PARALYSIS, true, pokemon);
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -1,11 +1,7 @@
|
|||
import { BattlerTagType } from "#app/enums/battler-tag-type";
|
||||
import { StatusEffect } from "#app/enums/status-effect";
|
||||
import { BattlerTagType } from "#enums/battler-tag-type";
|
||||
import { StatusEffect } from "#enums/status-effect";
|
||||
import Pokemon from "#app/field/pokemon";
|
||||
import { BerryPhase } from "#app/phases/berry-phase";
|
||||
import { MoveEndPhase } from "#app/phases/move-end-phase";
|
||||
import { TurnEndPhase } from "#app/phases/turn-end-phase";
|
||||
import { TurnStartPhase } from "#app/phases/turn-start-phase";
|
||||
import GameManager from "#app/test/utils/gameManager";
|
||||
import GameManager from "#test/utils/gameManager";
|
||||
import { Abilities } from "#enums/abilities";
|
||||
import { Moves } from "#enums/moves";
|
||||
import { Species } from "#enums/species";
|
||||
|
@ -53,13 +49,13 @@ describe("Abilities - Gulp Missile", () => {
|
|||
});
|
||||
|
||||
it("changes to Gulping Form if HP is over half when Surf or Dive is used", async () => {
|
||||
await game.startBattle([Species.CRAMORANT]);
|
||||
await game.classicMode.startBattle([Species.CRAMORANT]);
|
||||
const cramorant = game.scene.getPlayerPokemon()!;
|
||||
|
||||
game.move.select(Moves.DIVE);
|
||||
await game.toNextTurn();
|
||||
game.move.select(Moves.DIVE);
|
||||
await game.phaseInterceptor.to(MoveEndPhase);
|
||||
await game.phaseInterceptor.to("MoveEndPhase");
|
||||
|
||||
expect(cramorant.getHpRatio()).toBeGreaterThanOrEqual(.5);
|
||||
expect(cramorant.getTag(BattlerTagType.GULP_MISSILE_ARROKUDA)).toBeDefined();
|
||||
|
@ -67,21 +63,21 @@ describe("Abilities - Gulp Missile", () => {
|
|||
});
|
||||
|
||||
it("changes to Gorging Form if HP is under half when Surf or Dive is used", async () => {
|
||||
await game.startBattle([Species.CRAMORANT]);
|
||||
await game.classicMode.startBattle([Species.CRAMORANT]);
|
||||
const cramorant = game.scene.getPlayerPokemon()!;
|
||||
|
||||
vi.spyOn(cramorant, "getHpRatio").mockReturnValue(.49);
|
||||
expect(cramorant.getHpRatio()).toBe(.49);
|
||||
|
||||
game.move.select(Moves.SURF);
|
||||
await game.phaseInterceptor.to(MoveEndPhase);
|
||||
await game.phaseInterceptor.to("MoveEndPhase");
|
||||
|
||||
expect(cramorant.getTag(BattlerTagType.GULP_MISSILE_PIKACHU)).toBeDefined();
|
||||
expect(cramorant.formIndex).toBe(GORGING_FORM);
|
||||
});
|
||||
|
||||
it("changes to base form when switched out after Surf or Dive is used", async () => {
|
||||
await game.startBattle([Species.CRAMORANT, Species.MAGIKARP]);
|
||||
await game.classicMode.startBattle([Species.CRAMORANT, Species.MAGIKARP]);
|
||||
const cramorant = game.scene.getPlayerPokemon()!;
|
||||
|
||||
game.move.select(Moves.SURF);
|
||||
|
@ -96,51 +92,51 @@ describe("Abilities - Gulp Missile", () => {
|
|||
});
|
||||
|
||||
it("changes form during Dive's charge turn", async () => {
|
||||
await game.startBattle([Species.CRAMORANT]);
|
||||
await game.classicMode.startBattle([Species.CRAMORANT]);
|
||||
const cramorant = game.scene.getPlayerPokemon()!;
|
||||
|
||||
game.move.select(Moves.DIVE);
|
||||
await game.phaseInterceptor.to(MoveEndPhase);
|
||||
await game.phaseInterceptor.to("MoveEndPhase");
|
||||
|
||||
expect(cramorant.getTag(BattlerTagType.GULP_MISSILE_ARROKUDA)).toBeDefined();
|
||||
expect(cramorant.formIndex).toBe(GULPING_FORM);
|
||||
});
|
||||
|
||||
it("deals 1/4 of the attacker's maximum HP when hit by a damaging attack", async () => {
|
||||
game.override.enemyMoveset([Moves.TACKLE]);
|
||||
await game.startBattle([Species.CRAMORANT]);
|
||||
game.override.enemyMoveset(Moves.TACKLE);
|
||||
await game.classicMode.startBattle([Species.CRAMORANT]);
|
||||
|
||||
const enemy = game.scene.getEnemyPokemon()!;
|
||||
vi.spyOn(enemy, "damageAndUpdate");
|
||||
|
||||
game.move.select(Moves.SURF);
|
||||
await game.phaseInterceptor.to(TurnEndPhase);
|
||||
await game.phaseInterceptor.to("TurnEndPhase");
|
||||
|
||||
expect(enemy.damageAndUpdate).toHaveReturnedWith(getEffectDamage(enemy));
|
||||
});
|
||||
|
||||
it("does not have any effect when hit by non-damaging attack", async () => {
|
||||
game.override.enemyMoveset([Moves.TAIL_WHIP]);
|
||||
await game.startBattle([Species.CRAMORANT]);
|
||||
game.override.enemyMoveset(Moves.TAIL_WHIP);
|
||||
await game.classicMode.startBattle([Species.CRAMORANT]);
|
||||
|
||||
const cramorant = game.scene.getPlayerPokemon()!;
|
||||
vi.spyOn(cramorant, "getHpRatio").mockReturnValue(.55);
|
||||
|
||||
game.move.select(Moves.SURF);
|
||||
await game.phaseInterceptor.to(MoveEndPhase);
|
||||
await game.phaseInterceptor.to("MoveEndPhase");
|
||||
|
||||
expect(cramorant.getTag(BattlerTagType.GULP_MISSILE_ARROKUDA)).toBeDefined();
|
||||
expect(cramorant.formIndex).toBe(GULPING_FORM);
|
||||
|
||||
await game.phaseInterceptor.to(TurnEndPhase);
|
||||
await game.phaseInterceptor.to("TurnEndPhase");
|
||||
|
||||
expect(cramorant.getTag(BattlerTagType.GULP_MISSILE_ARROKUDA)).toBeDefined();
|
||||
expect(cramorant.formIndex).toBe(GULPING_FORM);
|
||||
});
|
||||
|
||||
it("lowers attacker's DEF stat stage by 1 when hit in Gulping form", async () => {
|
||||
game.override.enemyMoveset([Moves.TACKLE]);
|
||||
await game.startBattle([Species.CRAMORANT]);
|
||||
game.override.enemyMoveset(Moves.TACKLE);
|
||||
await game.classicMode.startBattle([Species.CRAMORANT]);
|
||||
|
||||
const cramorant = game.scene.getPlayerPokemon()!;
|
||||
const enemy = game.scene.getEnemyPokemon()!;
|
||||
|
@ -149,12 +145,12 @@ describe("Abilities - Gulp Missile", () => {
|
|||
vi.spyOn(cramorant, "getHpRatio").mockReturnValue(.55);
|
||||
|
||||
game.move.select(Moves.SURF);
|
||||
await game.phaseInterceptor.to(MoveEndPhase);
|
||||
await game.phaseInterceptor.to("MoveEndPhase");
|
||||
|
||||
expect(cramorant.getTag(BattlerTagType.GULP_MISSILE_ARROKUDA)).toBeDefined();
|
||||
expect(cramorant.formIndex).toBe(GULPING_FORM);
|
||||
|
||||
await game.phaseInterceptor.to(TurnEndPhase);
|
||||
await game.phaseInterceptor.to("TurnEndPhase");
|
||||
|
||||
expect(enemy.damageAndUpdate).toHaveReturnedWith(getEffectDamage(enemy));
|
||||
expect(enemy.getStatStage(Stat.DEF)).toBe(-1);
|
||||
|
@ -163,8 +159,8 @@ describe("Abilities - Gulp Missile", () => {
|
|||
});
|
||||
|
||||
it("paralyzes the enemy when hit in Gorging form", async () => {
|
||||
game.override.enemyMoveset([Moves.TACKLE]);
|
||||
await game.startBattle([Species.CRAMORANT]);
|
||||
game.override.enemyMoveset(Moves.TACKLE);
|
||||
await game.classicMode.startBattle([Species.CRAMORANT]);
|
||||
|
||||
const cramorant = game.scene.getPlayerPokemon()!;
|
||||
const enemy = game.scene.getEnemyPokemon()!;
|
||||
|
@ -173,12 +169,12 @@ describe("Abilities - Gulp Missile", () => {
|
|||
vi.spyOn(cramorant, "getHpRatio").mockReturnValue(.45);
|
||||
|
||||
game.move.select(Moves.SURF);
|
||||
await game.phaseInterceptor.to(MoveEndPhase);
|
||||
await game.phaseInterceptor.to("MoveEndPhase");
|
||||
|
||||
expect(cramorant.getTag(BattlerTagType.GULP_MISSILE_PIKACHU)).toBeDefined();
|
||||
expect(cramorant.formIndex).toBe(GORGING_FORM);
|
||||
|
||||
await game.phaseInterceptor.to(TurnEndPhase);
|
||||
await game.phaseInterceptor.to("TurnEndPhase");
|
||||
|
||||
expect(enemy.damageAndUpdate).toHaveReturnedWith(getEffectDamage(enemy));
|
||||
expect(enemy.status?.effect).toBe(StatusEffect.PARALYSIS);
|
||||
|
@ -187,21 +183,21 @@ describe("Abilities - Gulp Missile", () => {
|
|||
});
|
||||
|
||||
it("does not activate the ability when underwater", async () => {
|
||||
game.override.enemyMoveset([Moves.SURF]);
|
||||
await game.startBattle([Species.CRAMORANT]);
|
||||
game.override.enemyMoveset(Moves.SURF);
|
||||
await game.classicMode.startBattle([Species.CRAMORANT]);
|
||||
|
||||
const cramorant = game.scene.getPlayerPokemon()!;
|
||||
|
||||
game.move.select(Moves.DIVE);
|
||||
await game.phaseInterceptor.to(BerryPhase, false);
|
||||
await game.phaseInterceptor.to("BerryPhase", false);
|
||||
|
||||
expect(cramorant.getTag(BattlerTagType.GULP_MISSILE_ARROKUDA)).toBeDefined();
|
||||
expect(cramorant.formIndex).toBe(GULPING_FORM);
|
||||
});
|
||||
|
||||
it("prevents effect damage but inflicts secondary effect on attacker with Magic Guard", async () => {
|
||||
game.override.enemyMoveset([Moves.TACKLE]).enemyAbility(Abilities.MAGIC_GUARD);
|
||||
await game.startBattle([Species.CRAMORANT]);
|
||||
game.override.enemyMoveset(Moves.TACKLE).enemyAbility(Abilities.MAGIC_GUARD);
|
||||
await game.classicMode.startBattle([Species.CRAMORANT]);
|
||||
|
||||
const cramorant = game.scene.getPlayerPokemon()!;
|
||||
const enemy = game.scene.getEnemyPokemon()!;
|
||||
|
@ -209,13 +205,13 @@ describe("Abilities - Gulp Missile", () => {
|
|||
vi.spyOn(cramorant, "getHpRatio").mockReturnValue(.55);
|
||||
|
||||
game.move.select(Moves.SURF);
|
||||
await game.phaseInterceptor.to(MoveEndPhase);
|
||||
await game.phaseInterceptor.to("MoveEndPhase");
|
||||
const enemyHpPreEffect = enemy.hp;
|
||||
|
||||
expect(cramorant.getTag(BattlerTagType.GULP_MISSILE_ARROKUDA)).toBeDefined();
|
||||
expect(cramorant.formIndex).toBe(GULPING_FORM);
|
||||
|
||||
await game.phaseInterceptor.to(TurnEndPhase);
|
||||
await game.phaseInterceptor.to("TurnEndPhase");
|
||||
|
||||
expect(enemy.hp).toBe(enemyHpPreEffect);
|
||||
expect(enemy.getStatStage(Stat.DEF)).toBe(-1);
|
||||
|
@ -223,20 +219,36 @@ describe("Abilities - Gulp Missile", () => {
|
|||
expect(cramorant.formIndex).toBe(NORMAL_FORM);
|
||||
});
|
||||
|
||||
it("activates on faint", async () => {
|
||||
game.override.enemyMoveset(Moves.THUNDERBOLT);
|
||||
await game.classicMode.startBattle([Species.CRAMORANT]);
|
||||
|
||||
const cramorant = game.scene.getPlayerPokemon()!;
|
||||
|
||||
game.move.select(Moves.SURF);
|
||||
await game.phaseInterceptor.to("FaintPhase");
|
||||
|
||||
expect(cramorant.hp).toBe(0);
|
||||
expect(cramorant.getTag(BattlerTagType.GULP_MISSILE_ARROKUDA)).toBeUndefined();
|
||||
expect(cramorant.formIndex).toBe(NORMAL_FORM);
|
||||
expect(game.scene.getEnemyPokemon()!.getStatStage(Stat.DEF)).toBe(-1);
|
||||
});
|
||||
|
||||
|
||||
it("cannot be suppressed", async () => {
|
||||
game.override.enemyMoveset([Moves.GASTRO_ACID]);
|
||||
await game.startBattle([Species.CRAMORANT]);
|
||||
game.override.enemyMoveset(Moves.GASTRO_ACID);
|
||||
await game.classicMode.startBattle([Species.CRAMORANT]);
|
||||
|
||||
const cramorant = game.scene.getPlayerPokemon()!;
|
||||
vi.spyOn(cramorant, "getHpRatio").mockReturnValue(.55);
|
||||
|
||||
game.move.select(Moves.SURF);
|
||||
await game.phaseInterceptor.to(MoveEndPhase);
|
||||
await game.phaseInterceptor.to("MoveEndPhase");
|
||||
|
||||
expect(cramorant.getTag(BattlerTagType.GULP_MISSILE_ARROKUDA)).toBeDefined();
|
||||
expect(cramorant.formIndex).toBe(GULPING_FORM);
|
||||
|
||||
await game.phaseInterceptor.to(TurnEndPhase);
|
||||
await game.phaseInterceptor.to("TurnEndPhase");
|
||||
|
||||
expect(cramorant.hasAbility(Abilities.GULP_MISSILE)).toBe(true);
|
||||
expect(cramorant.getTag(BattlerTagType.GULP_MISSILE_ARROKUDA)).toBeDefined();
|
||||
|
@ -244,19 +256,19 @@ describe("Abilities - Gulp Missile", () => {
|
|||
});
|
||||
|
||||
it("cannot be swapped with another ability", async () => {
|
||||
game.override.enemyMoveset([Moves.SKILL_SWAP]);
|
||||
await game.startBattle([Species.CRAMORANT]);
|
||||
game.override.enemyMoveset(Moves.SKILL_SWAP);
|
||||
await game.classicMode.startBattle([Species.CRAMORANT]);
|
||||
|
||||
const cramorant = game.scene.getPlayerPokemon()!;
|
||||
vi.spyOn(cramorant, "getHpRatio").mockReturnValue(.55);
|
||||
|
||||
game.move.select(Moves.SURF);
|
||||
await game.phaseInterceptor.to(MoveEndPhase);
|
||||
await game.phaseInterceptor.to("MoveEndPhase");
|
||||
|
||||
expect(cramorant.getTag(BattlerTagType.GULP_MISSILE_ARROKUDA)).toBeDefined();
|
||||
expect(cramorant.formIndex).toBe(GULPING_FORM);
|
||||
|
||||
await game.phaseInterceptor.to(TurnEndPhase);
|
||||
await game.phaseInterceptor.to("TurnEndPhase");
|
||||
|
||||
expect(cramorant.hasAbility(Abilities.GULP_MISSILE)).toBe(true);
|
||||
expect(cramorant.getTag(BattlerTagType.GULP_MISSILE_ARROKUDA)).toBeDefined();
|
||||
|
@ -266,9 +278,9 @@ describe("Abilities - Gulp Missile", () => {
|
|||
it("cannot be copied", async () => {
|
||||
game.override.enemyAbility(Abilities.TRACE);
|
||||
|
||||
await game.startBattle([Species.CRAMORANT]);
|
||||
await game.classicMode.startBattle([Species.CRAMORANT]);
|
||||
game.move.select(Moves.SPLASH);
|
||||
await game.phaseInterceptor.to(TurnStartPhase);
|
||||
await game.phaseInterceptor.to("TurnStartPhase");
|
||||
|
||||
expect(game.scene.getEnemyPokemon()?.hasAbility(Abilities.GULP_MISSILE)).toBe(false);
|
||||
});
|
||||
|
|
Loading…
Reference in New Issue