[Move] Fully Implement Gulp Missile (#3438)

* Fix Dive + Gulp Missile interaction

* Fix animation + remove tests for inaccurate behavior

* Fix strict-null issue in new test
This commit is contained in:
innerthunder 2024-08-08 11:23:11 -07:00 committed by GitHub
parent 15106b83fb
commit 7f5e873457
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 23 additions and 26 deletions

View File

@ -6,7 +6,7 @@ import { BattleStat, getBattleStatName } from "./battle-stat";
import { MovePhase, PokemonHealPhase, ShowAbilityPhase, StatChangePhase } from "../phases"; import { MovePhase, PokemonHealPhase, ShowAbilityPhase, StatChangePhase } from "../phases";
import { getPokemonNameWithAffix } from "../messages"; import { getPokemonNameWithAffix } from "../messages";
import { Weather, WeatherType } from "./weather"; import { Weather, WeatherType } from "./weather";
import { BattlerTag, GroundedTag, GulpMissileTag } from "./battler-tags"; import { BattlerTag, GroundedTag, GulpMissileTag, SemiInvulnerableTag } from "./battler-tags";
import { StatusEffect, getNonVolatileStatusEffects, getStatusEffectDescriptor, getStatusEffectHealText } from "./status-effect"; import { StatusEffect, getNonVolatileStatusEffects, getStatusEffectDescriptor, getStatusEffectHealText } from "./status-effect";
import { Gender } from "./gender"; 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 } from "./move"; 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 } from "./move";
@ -517,7 +517,7 @@ export class PostDefendGulpMissileAbAttr extends PostDefendAbAttr {
*/ */
applyPostDefend(pokemon: Pokemon, passive: boolean, attacker: Pokemon, move: Move, hitResult: HitResult, args: any[]): boolean | Promise<boolean> { applyPostDefend(pokemon: Pokemon, passive: boolean, attacker: Pokemon, move: Move, hitResult: HitResult, args: any[]): boolean | Promise<boolean> {
const battlerTag = pokemon.getTag(GulpMissileTag); const battlerTag = pokemon.getTag(GulpMissileTag);
if (!battlerTag || move.category === MoveCategory.STATUS) { if (!battlerTag || move.category === MoveCategory.STATUS || pokemon.getTag(SemiInvulnerableTag)) {
return false; return false;
} }
@ -5138,9 +5138,7 @@ export function initAbilities() {
.attr(NoFusionAbilityAbAttr) .attr(NoFusionAbilityAbAttr)
.attr(UncopiableAbilityAbAttr) .attr(UncopiableAbilityAbAttr)
.attr(UnswappableAbilityAbAttr) .attr(UnswappableAbilityAbAttr)
.attr(PostDefendGulpMissileAbAttr) .attr(PostDefendGulpMissileAbAttr),
// Does not transform when Surf/Dive misses/is protected
.partial(),
new Ability(Abilities.STALWART, 8) new Ability(Abilities.STALWART, 8)
.attr(BlockRedirectAbAttr), .attr(BlockRedirectAbAttr),
new Ability(Abilities.STEAM_ENGINE, 8) new Ability(Abilities.STEAM_ENGINE, 8)

View File

@ -4356,7 +4356,7 @@ export class AddBattlerTagAttr extends MoveEffectAttr {
*/ */
export class GulpMissileTagAttr extends MoveEffectAttr { export class GulpMissileTagAttr extends MoveEffectAttr {
constructor() { constructor() {
super(true, MoveEffectTrigger.POST_APPLY); super(true);
} }
/** /**
@ -6954,7 +6954,7 @@ export function initMoves() {
.makesContact(false) .makesContact(false)
.partial(), .partial(),
new AttackMove(Moves.DIVE, Type.WATER, MoveCategory.PHYSICAL, 80, 100, 10, -1, 0, 3) new AttackMove(Moves.DIVE, Type.WATER, MoveCategory.PHYSICAL, 80, 100, 10, -1, 0, 3)
.attr(ChargeAttr, ChargeAnim.DIVE_CHARGING, i18next.t("moveTriggers:hidUnderwater", {pokemonName: "{USER}"}), BattlerTagType.UNDERWATER) .attr(ChargeAttr, ChargeAnim.DIVE_CHARGING, i18next.t("moveTriggers:hidUnderwater", {pokemonName: "{USER}"}), BattlerTagType.UNDERWATER, true)
.attr(GulpMissileTagAttr) .attr(GulpMissileTagAttr)
.ignoresVirtual(), .ignoresVirtual(),
new AttackMove(Moves.ARM_THRUST, Type.FIGHTING, MoveCategory.PHYSICAL, 15, 100, 20, -1, 0, 3) new AttackMove(Moves.ARM_THRUST, Type.FIGHTING, MoveCategory.PHYSICAL, 15, 100, 20, -1, 0, 3)

View File

@ -11,6 +11,7 @@ import { BattleSpec } from "#enums/battle-spec";
import { BattlePhase, MovePhase, PokemonHealPhase } from "./phases"; import { BattlePhase, MovePhase, PokemonHealPhase } from "./phases";
import { getTypeRgb } from "./data/type"; import { getTypeRgb } from "./data/type";
import { getPokemonNameWithAffix } from "./messages"; import { getPokemonNameWithAffix } from "./messages";
import { SemiInvulnerableTag } from "./data/battler-tags";
export class FormChangePhase extends EvolutionPhase { export class FormChangePhase extends EvolutionPhase {
private formChange: SpeciesFormChange; private formChange: SpeciesFormChange;
@ -194,7 +195,7 @@ export class QuietFormChangePhase extends BattlePhase {
const preName = getPokemonNameWithAffix(this.pokemon); const preName = getPokemonNameWithAffix(this.pokemon);
if (!this.pokemon.isOnField()) { if (!this.pokemon.isOnField() || this.pokemon.getTag(SemiInvulnerableTag)) {
this.pokemon.changeForm(this.formChange).then(() => { this.pokemon.changeForm(this.formChange).then(() => {
this.scene.ui.showText(getSpeciesFormChangeMessage(this.pokemon, this.formChange, preName), null, () => this.end(), 1500); this.scene.ui.showText(getSpeciesFormChangeMessage(this.pokemon, this.formChange, preName), null, () => this.end(), 1500);
}); });

View File

@ -1,5 +1,6 @@
import { BattlerTagType } from "#app/enums/battler-tag-type.js"; import { BattlerTagType } from "#app/enums/battler-tag-type.js";
import { import {
BerryPhase,
MoveEndPhase, MoveEndPhase,
TurnEndPhase, TurnEndPhase,
TurnStartPhase, TurnStartPhase,
@ -14,7 +15,6 @@ import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vite
import { SPLASH_ONLY } from "../utils/testUtils"; import { SPLASH_ONLY } from "../utils/testUtils";
import { BattleStat } from "#app/data/battle-stat.js"; import { BattleStat } from "#app/data/battle-stat.js";
import { StatusEffect } from "#app/enums/status-effect.js"; import { StatusEffect } from "#app/enums/status-effect.js";
import { GulpMissileTag } from "#app/data/battler-tags.js";
import Pokemon from "#app/field/pokemon.js"; import Pokemon from "#app/field/pokemon.js";
describe("Abilities - Gulp Missile", () => { describe("Abilities - Gulp Missile", () => {
@ -84,6 +84,17 @@ describe("Abilities - Gulp Missile", () => {
expect(cramorant.formIndex).toBe(GORGING_FORM); expect(cramorant.formIndex).toBe(GORGING_FORM);
}); });
it("changes form during Dive's charge turn", async () => {
await game.startBattle([Species.CRAMORANT]);
const cramorant = game.scene.getPlayerPokemon()!;
game.doAttack(getMovePosition(game.scene, 0, Moves.DIVE));
await game.phaseInterceptor.to(MoveEndPhase);
expect(cramorant.getTag(BattlerTagType.GULP_MISSILE_ARROKUDA)).toBeDefined();
expect(cramorant.formIndex).toBe(GULPING_FORM);
});
it("deals ¼ of the attacker's maximum HP when hit by a damaging attack", async () => { it("deals ¼ of the attacker's maximum HP when hit by a damaging attack", async () => {
game.override.enemyMoveset(Array(4).fill(Moves.TACKLE)); game.override.enemyMoveset(Array(4).fill(Moves.TACKLE));
await game.startBattle([Species.CRAMORANT]); await game.startBattle([Species.CRAMORANT]);
@ -165,29 +176,16 @@ describe("Abilities - Gulp Missile", () => {
}); });
it("does not activate the ability when underwater", async () => { it("does not activate the ability when underwater", async () => {
game.override game.override.enemyMoveset(Array(4).fill(Moves.SURF));
.enemyMoveset(Array(4).fill(Moves.SURF))
.enemySpecies(Species.REGIELEKI)
.enemyAbility(Abilities.BALL_FETCH)
.enemyLevel(5);
await game.startBattle([Species.CRAMORANT]); await game.startBattle([Species.CRAMORANT]);
const cramorant = game.scene.getPlayerPokemon()!; const cramorant = game.scene.getPlayerPokemon()!;
game.doAttack(getMovePosition(game.scene, 0, Moves.DIVE)); game.doAttack(getMovePosition(game.scene, 0, Moves.DIVE));
await game.toNextTurn(); await game.phaseInterceptor.to(BerryPhase, false);
// Turn 2 underwater, enemy moves first expect(cramorant.getTag(BattlerTagType.GULP_MISSILE_ARROKUDA)).toBeDefined();
game.doAttack(getMovePosition(game.scene, 0, Moves.DIVE)); expect(cramorant.formIndex).toBe(GULPING_FORM);
await game.phaseInterceptor.to(MoveEndPhase);
expect(cramorant.formIndex).toBe(NORMAL_FORM);
expect(cramorant.getTag(GulpMissileTag)).toBeUndefined();
// Turn 2 Cramorant comes out and changes form
await game.phaseInterceptor.to(TurnEndPhase);
expect(cramorant.formIndex).not.toBe(NORMAL_FORM);
expect(cramorant.getTag(GulpMissileTag)).toBeDefined();
}); });
it("prevents effect damage but inflicts secondary effect on attacker with Magic Guard", async () => { it("prevents effect damage but inflicts secondary effect on attacker with Magic Guard", async () => {