From c363d2b93c10c263480ecd8085cdeff261da4f13 Mon Sep 17 00:00:00 2001 From: Sirz Benjie <142067137+SirzBenjie@users.noreply.github.com> Date: Tue, 4 Feb 2025 22:28:49 -0600 Subject: [PATCH] [Ability] Fully Implement Good as Gold (#5245) * Fix good as gold * Update good as gold tests with Kev's feedback --- src/data/ability.ts | 5 +- src/test/abilities/good_as_gold.test.ts | 143 ++++++++++++++++++++++++ 2 files changed, 145 insertions(+), 3 deletions(-) create mode 100644 src/test/abilities/good_as_gold.test.ts diff --git a/src/data/ability.ts b/src/data/ability.ts index 5020123622e..4c9c44c6f35 100644 --- a/src/data/ability.ts +++ b/src/data/ability.ts @@ -6289,9 +6289,8 @@ export function initAbilities() { .attr(NoTransformAbilityAbAttr) .partial(), // While setting the tag, the getbattlestat should ignore all modifiers to stats except stat stages new Ability(Abilities.GOOD_AS_GOLD, 9) - .attr(MoveImmunityAbAttr, (pokemon, attacker, move) => pokemon !== attacker && move.category === MoveCategory.STATUS) - .ignorable() - .partial(), // Lots of weird interactions with moves and abilities such as negating status moves that target the field + .attr(MoveImmunityAbAttr, (pokemon, attacker, move) => pokemon !== attacker && move.category === MoveCategory.STATUS && ![ MoveTarget.ENEMY_SIDE, MoveTarget.BOTH_SIDES, MoveTarget.USER_SIDE ].includes(move.moveTarget)) + .ignorable(), new Ability(Abilities.VESSEL_OF_RUIN, 9) .attr(FieldMultiplyStatAbAttr, Stat.SPATK, 0.75) .attr(PostSummonMessageAbAttr, (user) => i18next.t("abilityTriggers:postSummonVesselOfRuin", { pokemonNameWithAffix: getPokemonNameWithAffix(user), statName: i18next.t(getStatKey(Stat.SPATK)) })) diff --git a/src/test/abilities/good_as_gold.test.ts b/src/test/abilities/good_as_gold.test.ts new file mode 100644 index 00000000000..ecda1a0e031 --- /dev/null +++ b/src/test/abilities/good_as_gold.test.ts @@ -0,0 +1,143 @@ +import { BattlerIndex } from "#app/battle"; +import { allAbilities } from "#app/data/ability"; +import { ArenaTagSide } from "#app/data/arena-tag"; +import { ArenaTagType } from "#app/enums/arena-tag-type"; +import { BattlerTagType } from "#app/enums/battler-tag-type"; +import { Stat } from "#app/enums/stat"; +import { StatusEffect } from "#app/enums/status-effect"; +import { WeatherType } from "#app/enums/weather-type"; +import { Abilities } from "#enums/abilities"; +import { Moves } from "#enums/moves"; +import { Species } from "#enums/species"; +import GameManager from "#test/utils/gameManager"; +import Phaser from "phaser"; +import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; + +describe("Abilities - Good As Gold", () => { + let phaserGame: Phaser.Game; + let game: GameManager; + + beforeAll(() => { + phaserGame = new Phaser.Game({ + type: Phaser.HEADLESS, + }); + }); + + afterEach(() => { + game.phaseInterceptor.restoreOg(); + }); + + beforeEach(() => { + game = new GameManager(phaserGame); + game.override + .moveset([ Moves.SPLASH ]) + .ability(Abilities.GOOD_AS_GOLD) + .battleType("single") + .disableCrits() + .enemySpecies(Species.MAGIKARP) + .enemyAbility(Abilities.BALL_FETCH) + .enemyMoveset(Moves.SPLASH); + }); + + it("should block normal status moves", async () => { + game.override.enemyMoveset( [ Moves.GROWL ] ); + await game.classicMode.startBattle([ Species.MAGIKARP ]); + + const player = game.scene.getPlayerPokemon()!; + + game.move.select(Moves.SPLASH, 0); + + await game.phaseInterceptor.to("BerryPhase"); + + expect(player.battleData.abilitiesApplied[0]).toBe(Abilities.GOOD_AS_GOLD); + expect(player.getStatStage(Stat.ATK)).toBe(0); + }); + + it("should block memento and prevent the user from fainting", async () => { + game.override.enemyMoveset( [ Moves.MEMENTO ] ); + await game.classicMode.startBattle([ Species.MAGIKARP ]); + game.move.select(Moves.MEMENTO); + await game.phaseInterceptor.to("BerryPhase"); + expect(game.scene.getPlayerPokemon()!.isFainted()).toBe(false); + expect(game.scene.getEnemyPokemon()?.getStatStage(Stat.ATK)).toBe(0); + }); + + it("should not block any status moves that target the field, one side, or all pokemon", async () => { + game.override.battleType("double"); + game.override.enemyMoveset( [ Moves.STEALTH_ROCK, Moves.HAZE ] ); + game.override.moveset([ Moves.SWORDS_DANCE, Moves.SAFEGUARD ]); + await game.classicMode.startBattle([ Species.MAGIKARP, Species.FEEBAS ]); + const [ good_as_gold, ball_fetch ] = game.scene.getPlayerField(); + + // Force second pokemon to have ball fetch to isolate to a single mon. + vi.spyOn(ball_fetch, "getAbility").mockReturnValue(allAbilities[Abilities.BALL_FETCH]); + + game.move.select(Moves.SWORDS_DANCE, 0); + game.move.select(Moves.SAFEGUARD, 1); + await game.forceEnemyMove(Moves.STEALTH_ROCK); + await game.forceEnemyMove(Moves.HAZE); + await game.setTurnOrder( [ BattlerIndex.PLAYER, BattlerIndex.PLAYER_2, BattlerIndex.ENEMY, BattlerIndex.ENEMY_2 ] ); + await game.phaseInterceptor.to("BerryPhase"); + expect(good_as_gold.getAbility().id).toBe(Abilities.GOOD_AS_GOLD); + expect(good_as_gold.getStatStage(Stat.ATK)).toBe(0); + expect(game.scene.arena.getTagOnSide(ArenaTagType.STEALTH_ROCK, ArenaTagSide.PLAYER)).toBeDefined(); + expect(game.scene.arena.getTagOnSide(ArenaTagType.SAFEGUARD, ArenaTagSide.PLAYER)).toBeDefined(); + }); + + it("should not block field targeted effects in singles", async () => { + game.override.battleType("single"); + game.override.enemyMoveset( [ Moves.SPIKES ] ); + await game.classicMode.startBattle([ Species.MAGIKARP ]); + + game.move.select(Moves.SPLASH, 0); + await game.phaseInterceptor.to("BerryPhase"); + + expect(game.scene.arena.getTagOnSide(ArenaTagType.SPIKES, ArenaTagSide.PLAYER)).toBeDefined(); + }); + + it("should block the ally's helping hand", async () => { + game.override.battleType("double"); + game.override.moveset([ Moves.HELPING_HAND, Moves.TACKLE ]); + await game.classicMode.startBattle([ Species.MAGIKARP, Species.FEEBAS ]); + + game.move.select(Moves.HELPING_HAND, 0); + game.move.select(Moves.TACKLE, 1); + await game.phaseInterceptor.to("MoveEndPhase", true); + + expect(game.scene.getPlayerField()[1].getTag(BattlerTagType.HELPING_HAND)).toBeUndefined(); + }); + + it("should block the ally's heal bell, but only if the good as gold user is on the field", async () => { + game.override.battleType("double"); + game.override.moveset([ Moves.HEAL_BELL, Moves.SPLASH ]); + game.override.statusEffect(StatusEffect.BURN); + await game.classicMode.startBattle([ Species.MAGIKARP, Species.FEEBAS, Species.ABRA ]); + const [ good_as_gold, ball_fetch ] = game.scene.getPlayerField(); + + // Force second pokemon to have ball fetch to isolate to a single mon. + vi.spyOn(ball_fetch, "getAbility").mockReturnValue(allAbilities[Abilities.BALL_FETCH]); + + // turn 1 + game.move.select(Moves.SPLASH, 0); + game.move.select(Moves.HEAL_BELL, 1); + await game.toNextTurn(); + expect(good_as_gold.status?.effect).toBe(StatusEffect.BURN); + + game.doSwitchPokemon(2); + game.move.select(Moves.HEAL_BELL, 0); + await game.toNextTurn(); + expect(good_as_gold.status?.effect).toBeUndefined(); + }); + + it("should not block field targeted effects like rain dance", async () => { + game.override.battleType("single"); + game.override.enemyMoveset( [ Moves.RAIN_DANCE ] ); + game.override.weather(WeatherType.NONE); + await game.classicMode.startBattle([ Species.MAGIKARP ]); + + game.move.select(Moves.SPLASH, 0); + await game.phaseInterceptor.to("BerryPhase"); + + expect(game.scene.arena.weather?.weatherType).toBe(WeatherType.RAIN); + }); +});