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);
+  });
+});