diff --git a/src/data/abilities/ability.ts b/src/data/abilities/ability.ts index bde7a69d789..53c790e407e 100644 --- a/src/data/abilities/ability.ts +++ b/src/data/abilities/ability.ts @@ -4058,11 +4058,11 @@ export class PostTurnRestoreBerryAbAttr extends PostTurnAbAttr { } override canApplyPostTurn(pokemon: Pokemon, passive: boolean, simulated: boolean, args: any[]): boolean { - // check if we have at least 1 recoverable berry + // check if we have at least 1 recoverable berry (at least 1 berry in berriesEaten is not capped) const cappedBerries = new Set( globalScene.getModifiers(BerryModifier, pokemon.isPlayer()).filter( (bm) => bm.pokemonId === pokemon.id && bm.getCountUnderMax() < 1 - ).map((bm) => bm.berryType) + ).map(bm => bm.berryType) ); const hasBerryUnderCap = pokemon.battleData.berriesEaten.some( @@ -4156,7 +4156,7 @@ export class RepeatBerryNextTurnAbAttr extends PostTurnAbAttr { /** * Cause this {@linkcode Pokemon} to regurgitate and eat all berries * inside its `berriesEatenLast` array. - * @param pokemon - The pokemon having the tummy ache + * @param pokemon - The {@linkcode Pokemon} having a bad tummy ache * @param _passive - N/A * @param _simulated - N/A * @param _cancelled - N/A @@ -4175,19 +4175,22 @@ export class RepeatBerryNextTurnAbAttr extends PostTurnAbAttr { const bMod = new BerryModifier(new BerryModifierType(berryType), pokemon.id, berryType, 1); globalScene.eventTarget.dispatchEvent(new BerryUsedEvent(bMod)); // trigger message } + + // uncomment to make cheek pouch work with cud chew + // applyAbAttrs(HealFromBerryUseAbAttr, pokemon, new BooleanHolder(false)); } /** - * @returns `true` if the pokemon ate anything this turn (we move it into `battleData`) + * @returns always `true` */ override canApplyPostTurn(pokemon: Pokemon, _passive: boolean, _simulated: boolean, _args: any[]): boolean { this.showAbility = false; // don't show popup for turn end berry moving (should ideally be hidden) - return !!pokemon.turnData.berriesEaten.length; + return true; } /** - * Move this {@linkcode Pokemon}'s `berriesEaten` array inside `PokemonTurnData` - * into its `summonData`. + * Move this {@linkcode Pokemon}'s `berriesEaten` array from `PokemonTurnData` + * into `PokemonSummonData`. * @param pokemon The {@linkcode Pokemon} having a nice snack * @param _passive N/A * @param _simulated N/A diff --git a/src/field/pokemon.ts b/src/field/pokemon.ts index f60528e8e7c..0e88209c5a0 100644 --- a/src/field/pokemon.ts +++ b/src/field/pokemon.ts @@ -406,7 +406,6 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { dataSource?.exp || getLevelTotalExp(this.level, species.growthRate); this.levelExp = dataSource?.levelExp || 0; - // TODO?: Maybe instead of using such a giant if statement, maybe some optional chaining/null coaclescing would look better if (dataSource) { this.id = dataSource.id; this.hp = dataSource.hp; @@ -454,8 +453,6 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { this.customPokemonData = new CustomPokemonData( dataSource.customPokemonData, ); - this.summonData = dataSource.summonData; - this.battleData = dataSource.battleData; this.teraType = dataSource.teraType; this.isTerastallized = dataSource.isTerastallized; this.stellarTypesBoosted = dataSource.stellarTypesBoosted ?? []; @@ -524,6 +521,9 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { this.stellarTypesBoosted = []; } + this.summonData = new PokemonSummonData(dataSource?.summonData); + this.battleData = new PokemonBattleData(dataSource?.battleData); + this.generateName(); if (!species.isObtainable()) { diff --git a/test/abilities/cud_chew.test.ts b/test/abilities/cud_chew.test.ts index 532c72f6e66..a06c91f9316 100644 --- a/test/abilities/cud_chew.test.ts +++ b/test/abilities/cud_chew.test.ts @@ -1,13 +1,14 @@ import { RepeatBerryNextTurnAbAttr } from "#app/data/abilities/ability"; -import { getBerryEffectFunc } from "#app/data/berry"; 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"; @@ -23,7 +24,6 @@ describe("Abilities - Cud Chew", () => { afterEach(() => { game.phaseInterceptor.restoreOg(); - vi.resetAllMocks(); }); beforeEach(() => { @@ -40,7 +40,7 @@ describe("Abilities - Cud Chew", () => { }); describe("tracks berries eaten", () => { - it("stores inside battledata at end of turn", async () => { + it("stores inside summonData at end of turn", async () => { await game.classicMode.startBattle([Species.FARIGIRAF]); const farigiraf = game.scene.getPlayerPokemon()!; @@ -66,32 +66,44 @@ describe("Abilities - Cud Chew", () => { expect(farigiraf.turnData.berriesEaten).toEqual([]); }); - it("shouldn't show ability popup for end-of-turn storage", async () => { + 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()!; - farigiraf.hp = 1; // needed to allow sitrus procs + // 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.phaseInterceptor.to("BerryPhase"); + await game.forceEnemyMove(Moves.HEAL_PULSE); + await game.phaseInterceptor.to("TurnEndPhase"); - // globalScene.queueAbilityDisplay should be called twice: once to show cud chew before regurgitating berries, - // once to hide after finishing application + // 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); - await game.phaseInterceptor.to("TurnEndPhase"); + // 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); @@ -114,7 +126,7 @@ describe("Abilities - Cud Chew", () => { game.move.select(Moves.STUFF_CHEEKS); await game.toNextTurn(); - // Ate 2 petayas from moves + 1 of each at turn end + // Ate 2 petayas from moves + 1 of each at turn end; all 4 get moved on turn end expect(farigiraf.summonData.berriesEatenLast).toEqual([ BerryType.PETAYA, BerryType.PETAYA, @@ -123,23 +135,14 @@ describe("Abilities - Cud Chew", () => { ]); expect(farigiraf.turnData.berriesEaten).toEqual([]); - game.move.select(Moves.STUFF_CHEEKS); + game.move.select(Moves.SPLASH); await game.toNextTurn(); - // previous berries moved into summon data; newly eaten berries move into turn data - expect(farigiraf.summonData.berriesEatenLast).toEqual([ - BerryType.PETAYA, - BerryType.PETAYA, - BerryType.PETAYA, - BerryType.LIECHI, - ]); - expect(farigiraf.turnData.berriesEaten).toEqual([BerryType.PETAYA, BerryType.LIECHI, BerryType.LIECHI]); - expect(farigiraf.getStatStage(Stat.ATK)).toBe(4); // 1 --> 2+1 - - await game.toNextTurn(); - - // 1st array overridden after turn end - expect(farigiraf.summonData.berriesEatenLast).toEqual([BerryType.PETAYA, BerryType.LIECHI, BerryType.LIECHI]); + // 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("resets array on switch", async () => { @@ -246,24 +249,26 @@ describe("Abilities - Cud Chew", () => { expect(farigiraf.hp).toBeLessThan(farigiraf.getMaxHp() / 4); }); - it("works with pluck even if berry is useless", async () => { - const bSpy = vi.fn(getBerryEffectFunc); + it("works with pluck", async () => { game.override .enemySpecies(Species.BLAZIKEN) - .enemyHeldItems([{ name: "BERRY", type: BerryType.SITRUS, count: 1 }]) + .enemyHeldItems([{ name: "BERRY", type: BerryType.PETAYA, count: 1 }]) .startingHeldItems([]); await game.classicMode.startBattle([Species.FARIGIRAF]); - game.move.select(Moves.BUG_BITE); - await game.toNextTurn(); + const farigiraf = game.scene.getPlayerPokemon()!; + game.move.select(Moves.BUG_BITE); await game.toNextTurn(); - expect(bSpy).toBeCalledTimes(2); + 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 () => { - const bSpy = vi.fn(getBerryEffectFunc); game.override.passiveAbility(Abilities.RIPEN); await game.classicMode.startBattle([Species.FARIGIRAF]); @@ -277,7 +282,6 @@ describe("Abilities - Cud Chew", () => { // Rounding errors only ever cost a maximum of 4 hp expect(farigiraf.getInverseHp()).toBeLessThanOrEqual(3); - expect(bSpy).toHaveBeenCalledTimes(2); }); it("is preserved on reload/wave clear", async () => { diff --git a/test/abilities/harvest.test.ts b/test/abilities/harvest.test.ts index 38a37f43ea6..63221ec871f 100644 --- a/test/abilities/harvest.test.ts +++ b/test/abilities/harvest.test.ts @@ -36,7 +36,6 @@ describe("Abilities - Harvest", () => { afterEach(() => { game.phaseInterceptor.restoreOg(); - vi.resetAllMocks(); }); beforeEach(() => { @@ -146,15 +145,26 @@ describe("Abilities - Harvest", () => { expect(regielekiReloaded.battleData.berriesEaten).toEqual([BerryType.PETAYA]); }); - it("cannot restore capped berries", async () => { + it("cannot restore capped berries, even if an ally has one under cap", async () => { const initBerries: ModifierOverride[] = [ { name: "BERRY", type: BerryType.LUM, count: 2 }, { name: "BERRY", type: BerryType.STARF, count: 2 }, ]; game.override.startingHeldItems(initBerries); - await game.classicMode.startBattle([Species.FEEBAS]); - const player = game.scene.getPlayerPokemon()!; - player.battleData.berriesEaten = [BerryType.LUM, BerryType.STARF]; + await game.classicMode.startBattle([Species.FEEBAS, Species.BELLOSSOM]); + + const [feebas, bellossom] = game.scene.getPlayerParty(); + feebas.battleData.berriesEaten = [BerryType.LUM, BerryType.STARF]; + + // get rid of bellossom's modifiers and add a sitrus + await game.scene.removePartyMemberModifiers(1); + const newMod = game.scene + .getModifiers(BerryModifier, true) + .find(b => b.berryType === BerryType.SITRUS) + ?.clone()!; + expect(newMod).toBeDefined(); + newMod.pokemonId = bellossom.id; + game.scene.addModifier(newMod, true); game.move.select(Moves.SPLASH); await game.forceEnemyMove(Moves.SPLASH); @@ -166,7 +176,9 @@ describe("Abilities - Harvest", () => { vi.spyOn(Phaser.Math.RND, "integerInRange").mockReturnValue(0); await game.phaseInterceptor.to("TurnEndPhase"); + // recovered a starf, expectBerriesContaining({ name: "BERRY", type: BerryType.STARF, count: 3 }); + expect(game.scene.getModifiers(BerryModifier, true).filter(b => b.pokemonId === bellossom.id)).toHaveLength(0); }); it("does nothing if all berries are capped", async () => { diff --git a/test/moves/transform.test.ts b/test/moves/transform.test.ts index 5bcb7c7ed4c..8bfe7df688b 100644 --- a/test/moves/transform.test.ts +++ b/test/moves/transform.test.ts @@ -4,7 +4,7 @@ import GameManager from "#test/testUtils/gameManager"; import { Species } from "#enums/species"; import { TurnEndPhase } from "#app/phases/turn-end-phase"; import { Moves } from "#enums/moves"; -import { Stat, BATTLE_STATS, EFFECTIVE_STATS } from "#enums/stat"; +import { Stat, EFFECTIVE_STATS } from "#enums/stat"; import { Abilities } from "#enums/abilities"; import { BattlerIndex } from "#app/battle"; @@ -49,30 +49,18 @@ describe("Moves - Transform", () => { expect(player.getAbility()).toBe(enemy.getAbility()); expect(player.getGender()).toBe(enemy.getGender()); + // copies all stats except hp expect(player.getStat(Stat.HP, false)).not.toBe(enemy.getStat(Stat.HP)); for (const s of EFFECTIVE_STATS) { expect(player.getStat(s, false)).toBe(enemy.getStat(s, false)); } - for (const s of BATTLE_STATS) { - expect(player.getStatStage(s)).toBe(enemy.getStatStage(s)); - } + expect(player.getStatStages()).toEqual(enemy.getStatStages()); - const playerMoveset = player.getMoveset(); - const enemyMoveset = enemy.getMoveset(); + // move IDs are equal + expect(player.getMoveset().map(m => m.moveId)).toEqual(enemy.getMoveset().map(m => m.moveId)); - expect(playerMoveset.length).toBe(enemyMoveset.length); - for (let i = 0; i < playerMoveset.length && i < enemyMoveset.length; i++) { - expect(playerMoveset[i]?.moveId).toBe(enemyMoveset[i]?.moveId); - } - - const playerTypes = player.getTypes(); - const enemyTypes = enemy.getTypes(); - - expect(playerTypes.length).toBe(enemyTypes.length); - for (let i = 0; i < playerTypes.length && i < enemyTypes.length; i++) { - expect(playerTypes[i]).toBe(enemyTypes[i]); - } + expect(player.getTypes()).toEqual(enemy.getTypes()); }); it("should copy in-battle overridden stats", async () => { diff --git a/test/testUtils/helpers/reloadHelper.ts b/test/testUtils/helpers/reloadHelper.ts index 4867a146aaf..33d9afecefc 100644 --- a/test/testUtils/helpers/reloadHelper.ts +++ b/test/testUtils/helpers/reloadHelper.ts @@ -73,6 +73,6 @@ export class ReloadHelper extends GameManagerHelper { } await this.game.phaseInterceptor.to(CommandPhase); - console.log("==================[New Turn]=================="); + console.log("==================[New Turn (Reloaded)]=================="); } }