pokerogue/test/abilities/cud_chew.test.ts
Bertie690 6d90649b92
[Refactor/Bug/Ability] Reworked BattleData, fixed Rage Fist, Harvest, Belch + Implemented Cud Chew (#5655)
* Grabbed reverted changes from stuff

* Added version migrator for rage fist data + deepMergeSpriteData tests

* fixed formattign

* Fied a few

* Fixed constructor (maybe), moved deepCopy and deepMergeSpriteData to own file

`common.ts` is hella bloated so seems legit

* Moved empty moveset verification mapping thing to upgrade script bc i wanted to

* Fixed tests

* test added

* Fixed summondata being cleared inside summonPhase, removed `summonDataPrimer`

like seriously how come no-one checked this

* Fixed test

I forgot that we outsped and oneshot

* Fixed test

* huhjjjjjb

* Hopefully fixed bug

my sanity and homework are paying the price for this lol

* added commented out console.log statement

uncomment to see new berry data

* Fixed migrate script, re-added deprecated attributes out of necessity

* Fixed failing test by not trying to mock rng

* Fixed test

* Fixed tests

* Update ability.ts

Co-authored-by: NightKev <34855794+DayKev@users.noreply.github.com>

* Update ability.ts

Co-authored-by: NightKev <34855794+DayKev@users.noreply.github.com>

* Update overrides.ts

Co-authored-by: NightKev <34855794+DayKev@users.noreply.github.com>

* Update berry-phase.ts

Co-authored-by: NightKev <34855794+DayKev@users.noreply.github.com>

* Update encounter-phase.ts

Co-authored-by: NightKev <34855794+DayKev@users.noreply.github.com>

* Update game-data.ts

Co-authored-by: NightKev <34855794+DayKev@users.noreply.github.com>

* Update move-phase.ts

Co-authored-by: NightKev <34855794+DayKev@users.noreply.github.com>

* Added utility function `randSeedFloat`

basically just `Phaser.math.RND.realInRange(0, 1)`

* Applied review comments, cleaned up code a bit

* Removed unnecessary null checks for turnData and co.

I explicitly made them initialized by default for this very reason

* Added tests for Last Resort regarding moveHistory

* Update pokemon.ts

Co-authored-by: NightKev <34855794+DayKev@users.noreply.github.com>

* Update pokemon.ts

Co-authored-by: NightKev <34855794+DayKev@users.noreply.github.com>

* Update pokemon.ts

Co-authored-by: NightKev <34855794+DayKev@users.noreply.github.com>

* Update pokemon.ts

Co-authored-by: NightKev <34855794+DayKev@users.noreply.github.com>

* Update pokemon.ts

Co-authored-by: NightKev <34855794+DayKev@users.noreply.github.com>

* Update pokemon.ts

Co-authored-by: NightKev <34855794+DayKev@users.noreply.github.com>

* Update pokemon.ts

Co-authored-by: NightKev <34855794+DayKev@users.noreply.github.com>

* Update pokemon.ts

Co-authored-by: NightKev <34855794+DayKev@users.noreply.github.com>

* Update battle-scene.ts

Co-authored-by: Sirz Benjie <142067137+SirzBenjie@users.noreply.github.com>

* Update the-winstrate-challenge-encounter.ts

Co-authored-by: NightKev <34855794+DayKev@users.noreply.github.com>

* Update pokemon.ts

Co-authored-by: NightKev <34855794+DayKev@users.noreply.github.com>

* Update pokemon.ts

Co-authored-by: NightKev <34855794+DayKev@users.noreply.github.com>

* Update pokemon.ts

Co-authored-by: NightKev <34855794+DayKev@users.noreply.github.com>

* Update ability.ts

Co-authored-by: NightKev <34855794+DayKev@users.noreply.github.com>

* Update move.ts

Co-authored-by: NightKev <34855794+DayKev@users.noreply.github.com>

* Update move.ts

Co-authored-by: NightKev <34855794+DayKev@users.noreply.github.com>

* Update move.ts

Co-authored-by: NightKev <34855794+DayKev@users.noreply.github.com>

* Update battle-anims.ts

Co-authored-by: NightKev <34855794+DayKev@users.noreply.github.com>

* Update pokemon.ts comments

* Fixed a few outstanding issues with documentation

* Updated switch summon phase comment

* Re-added BattleSummonData as TempSummonData

* Hppefully fixed -1 sprite scale glitch

* Fixed comment

* Reveted `pokemon-forms.ts`

* Fuxed constructor

* fixed -1 bug

* Revert "Added utility function `randSeedFloat`"

This reverts commit 4c3447c851731c989fc591feea0094b6bbde7fd2.

---------

Co-authored-by: NightKev <34855794+DayKev@users.noreply.github.com>
Co-authored-by: Sirz Benjie <142067137+SirzBenjie@users.noreply.github.com>
2025-05-02 00:06:07 -05:00

323 lines
12 KiB
TypeScript

import { RepeatBerryNextTurnAbAttr } from "#app/data/abilities/ability";
import Pokemon from "#app/field/pokemon";
import { globalScene } from "#app/global-scene";
import { getPokemonNameWithAffix } from "#app/messages";
import { Abilities } from "#enums/abilities";
import { BerryType } from "#enums/berry-type";
import { Moves } from "#enums/moves";
import { Species } from "#enums/species";
import { Stat } from "#enums/stat";
import GameManager from "#test/testUtils/gameManager";
import i18next from "i18next";
import Phaser from "phaser";
import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
describe("Abilities - Cud Chew", () => {
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.BUG_BITE, Moves.SPLASH, Moves.HYPER_VOICE, Moves.STUFF_CHEEKS])
.startingHeldItems([{ name: "BERRY", type: BerryType.SITRUS, count: 1 }])
.ability(Abilities.CUD_CHEW)
.battleStyle("single")
.disableCrits()
.enemySpecies(Species.MAGIKARP)
.enemyAbility(Abilities.BALL_FETCH)
.enemyMoveset(Moves.SPLASH);
});
describe("tracks berries eaten", () => {
it("stores inside summonData at end of turn", async () => {
await game.classicMode.startBattle([Species.FARIGIRAF]);
const farigiraf = game.scene.getPlayerPokemon()!;
farigiraf.hp = 1; // needed to allow sitrus procs
game.move.select(Moves.SPLASH);
await game.phaseInterceptor.to("BerryPhase");
// berries tracked in turnData; not moved to battleData yet
expect(farigiraf.summonData.berriesEatenLast).toEqual([]);
expect(farigiraf.turnData.berriesEaten).toEqual([BerryType.SITRUS]);
await game.phaseInterceptor.to("TurnEndPhase");
// berries stored in battleData; not yet cleared from turnData
expect(farigiraf.summonData.berriesEatenLast).toEqual([BerryType.SITRUS]);
expect(farigiraf.turnData.berriesEaten).toEqual([BerryType.SITRUS]);
await game.toNextTurn();
// turnData cleared on turn start
expect(farigiraf.summonData.berriesEatenLast).toEqual([BerryType.SITRUS]);
expect(farigiraf.turnData.berriesEaten).toEqual([]);
});
it("shows ability popup for eating berry, even if berry is useless", async () => {
const abDisplaySpy = vi.spyOn(globalScene, "queueAbilityDisplay");
game.override.enemyMoveset([Moves.SPLASH, Moves.HEAL_PULSE]);
await game.classicMode.startBattle([Species.FARIGIRAF]);
const farigiraf = game.scene.getPlayerPokemon()!;
// Dip below half to eat berry
farigiraf.hp = farigiraf.getMaxHp() / 2 - 1;
game.move.select(Moves.SPLASH);
await game.forceEnemyMove(Moves.SPLASH);
await game.phaseInterceptor.to("TurnEndPhase");
// doesn't trigger since cud chew hasn't eaten berry yet
expect(farigiraf.summonData.berriesEatenLast).toContain(BerryType.SITRUS);
expect(abDisplaySpy).not.toHaveBeenCalledWith(farigiraf);
await game.toNextTurn();
// get heal pulsed back to full before the cud chew proc
game.move.select(Moves.SPLASH);
await game.forceEnemyMove(Moves.HEAL_PULSE);
await game.phaseInterceptor.to("TurnEndPhase");
// globalScene.queueAbilityDisplay should be called twice:
// once to show cud chew text before regurgitating berries,
// once to hide ability text after finishing.
expect(abDisplaySpy).toBeCalledTimes(2);
expect(abDisplaySpy.mock.calls[0][0]).toBe(farigiraf);
expect(abDisplaySpy.mock.calls[0][2]).toBe(true);
expect(abDisplaySpy.mock.calls[1][0]).toBe(farigiraf);
expect(abDisplaySpy.mock.calls[1][2]).toBe(false);
// should display messgae
expect(game.textInterceptor.getLatestMessage()).toBe(
i18next.t("battle:hpIsFull", {
pokemonName: getPokemonNameWithAffix(farigiraf),
}),
);
// not called again at turn end
expect(abDisplaySpy).toBeCalledTimes(2);
});
it("can store multiple berries across 2 turns with teatime", async () => {
// always eat first berry for stuff cheeks & company
vi.spyOn(Pokemon.prototype, "randSeedInt").mockReturnValue(0);
game.override
.startingHeldItems([
{ name: "BERRY", type: BerryType.PETAYA, count: 3 },
{ name: "BERRY", type: BerryType.LIECHI, count: 3 },
])
.enemyMoveset(Moves.TEATIME);
await game.classicMode.startBattle([Species.FARIGIRAF]);
const farigiraf = game.scene.getPlayerPokemon()!;
farigiraf.hp = 1; // needed to allow berry procs
game.move.select(Moves.STUFF_CHEEKS);
await game.toNextTurn();
// Ate 2 petayas from moves + 1 of each at turn end; all 4 get tallied on turn end
expect(farigiraf.summonData.berriesEatenLast).toEqual([
BerryType.PETAYA,
BerryType.PETAYA,
BerryType.PETAYA,
BerryType.LIECHI,
]);
expect(farigiraf.turnData.berriesEaten).toEqual([]);
game.move.select(Moves.SPLASH);
await game.toNextTurn();
// previous berries eaten and deleted from summon data as remaining eaten berries move to replace them
expect(farigiraf.summonData.berriesEatenLast).toEqual([BerryType.LIECHI, BerryType.LIECHI]);
expect(farigiraf.turnData.berriesEaten).toEqual([]);
expect(farigiraf.getStatStage(Stat.SPATK)).toBe(6); // 3+0+3
expect(farigiraf.getStatStage(Stat.ATK)).toBe(4); // 1+2+1
});
it("should reset both arrays on switch", async () => {
await game.classicMode.startBattle([Species.FARIGIRAF, Species.GIRAFARIG]);
const farigiraf = game.scene.getPlayerPokemon()!;
farigiraf.hp = 1;
// eat berry turn 1, switch out turn 2
game.move.select(Moves.SPLASH);
await game.toNextTurn();
const turn1Hp = farigiraf.hp;
game.doSwitchPokemon(1);
await game.toNextTurn();
// summonData got cleared due to switch, turnData got cleared due to turn end
expect(farigiraf.summonData.berriesEatenLast).toEqual([]);
expect(farigiraf.turnData.berriesEaten).toEqual([]);
expect(farigiraf.hp).toEqual(turn1Hp);
game.doSwitchPokemon(1);
await game.toNextTurn();
// TurnData gets cleared while switching in
expect(farigiraf.summonData.berriesEatenLast).toEqual([]);
expect(farigiraf.turnData.berriesEaten).toEqual([]);
expect(farigiraf.hp).toEqual(turn1Hp);
});
it("clears array if disabled", async () => {
game.override.enemyAbility(Abilities.NEUTRALIZING_GAS);
await game.classicMode.startBattle([Species.FARIGIRAF]);
const farigiraf = game.scene.getPlayerPokemon()!;
farigiraf.hp = 1;
game.move.select(Moves.SPLASH);
await game.phaseInterceptor.to("BerryPhase");
expect(farigiraf.summonData.berriesEatenLast).toEqual([]);
expect(farigiraf.turnData.berriesEaten).toEqual([BerryType.SITRUS]);
await game.toNextTurn();
// both arrays empty since neut gas disabled both the mid-turn and post-turn effects
expect(farigiraf.summonData.berriesEatenLast).toEqual([]);
expect(farigiraf.turnData.berriesEaten).toEqual([]);
});
});
describe("regurgiates berries", () => {
it("re-triggers effects on eater without pushing to array", async () => {
const apply = vi.spyOn(RepeatBerryNextTurnAbAttr.prototype, "apply");
await game.classicMode.startBattle([Species.FARIGIRAF]);
const farigiraf = game.scene.getPlayerPokemon()!;
farigiraf.hp = 1;
game.move.select(Moves.SPLASH);
await game.toNextTurn();
// ate 1 sitrus the turn prior, spitball pending
expect(farigiraf.summonData.berriesEatenLast).toEqual([BerryType.SITRUS]);
expect(farigiraf.turnData.berriesEaten).toEqual([]);
expect(apply.mock.lastCall).toBeUndefined();
const turn1Hp = farigiraf.hp;
game.move.select(Moves.SPLASH);
await game.phaseInterceptor.to("TurnEndPhase");
// healed back up to half without adding any more to array
expect(farigiraf.hp).toBeGreaterThan(turn1Hp);
expect(farigiraf.summonData.berriesEatenLast).toEqual([]);
expect(farigiraf.turnData.berriesEaten).toEqual([]);
});
it("bypasses unnerve", async () => {
game.override.enemyAbility(Abilities.UNNERVE);
await game.classicMode.startBattle([Species.FARIGIRAF]);
const farigiraf = game.scene.getPlayerPokemon()!;
farigiraf.hp = 1;
game.move.select(Moves.SPLASH);
await game.toNextTurn();
game.move.select(Moves.SPLASH);
await game.phaseInterceptor.to("TurnEndPhase");
// Turn end proc set the berriesEatenLast array back to being empty
expect(farigiraf.summonData.berriesEatenLast).toEqual([]);
expect(farigiraf.turnData.berriesEaten).toEqual([]);
expect(farigiraf.hp).toBeGreaterThanOrEqual(farigiraf.hp / 2);
});
it("doesn't trigger on non-eating removal", async () => {
game.override.enemyMoveset(Moves.INCINERATE);
await game.classicMode.startBattle([Species.FARIGIRAF]);
const farigiraf = game.scene.getPlayerPokemon()!;
farigiraf.hp = farigiraf.getMaxHp() / 4;
game.move.select(Moves.SPLASH);
await game.toNextTurn();
// no berries eaten due to getting cooked
expect(farigiraf.summonData.berriesEatenLast).toEqual([]);
expect(farigiraf.turnData.berriesEaten).toEqual([]);
expect(farigiraf.hp).toBeLessThan(farigiraf.getMaxHp() / 4);
});
it("works with pluck", async () => {
game.override
.enemySpecies(Species.BLAZIKEN)
.enemyHeldItems([{ name: "BERRY", type: BerryType.PETAYA, count: 1 }])
.startingHeldItems([]);
await game.classicMode.startBattle([Species.FARIGIRAF]);
const farigiraf = game.scene.getPlayerPokemon()!;
game.move.select(Moves.BUG_BITE);
await game.toNextTurn();
game.move.select(Moves.SPLASH);
await game.toNextTurn();
// berry effect triggered twice - once for bug bite, once for cud chew
expect(farigiraf.getStatStage(Stat.SPATK)).toBe(2);
});
it("works with Ripen", async () => {
game.override.passiveAbility(Abilities.RIPEN);
await game.classicMode.startBattle([Species.FARIGIRAF]);
const farigiraf = game.scene.getPlayerPokemon()!;
farigiraf.hp = 1;
game.move.select(Moves.SPLASH);
await game.toNextTurn();
game.move.select(Moves.SPLASH);
await game.toNextTurn();
// Rounding errors only ever cost a maximum of 4 hp
expect(farigiraf.getInverseHp()).toBeLessThanOrEqual(3);
});
it("is preserved on reload/wave clear", async () => {
game.override.enemyLevel(1);
await game.classicMode.startBattle([Species.FARIGIRAF]);
const farigiraf = game.scene.getPlayerPokemon()!;
farigiraf.hp = 1;
game.move.select(Moves.HYPER_VOICE);
await game.toNextWave();
// berry went yummy yummy in big fat giraffe tummy
expect(farigiraf.summonData.berriesEatenLast).toEqual([BerryType.SITRUS]);
expect(farigiraf.hp).toBeGreaterThan(1);
// reload and the berry should still be there
await game.reload.reloadSession();
const farigirafReloaded = game.scene.getPlayerPokemon()!;
expect(farigirafReloaded.summonData.berriesEatenLast).toEqual([BerryType.SITRUS]);
const wave1Hp = farigirafReloaded.hp;
// blow up next wave and we should proc the repeat eating
game.move.select(Moves.HYPER_VOICE);
await game.toNextWave();
expect(farigirafReloaded.hp).toBeGreaterThan(wave1Hp);
});
});
});