From c7a1b0fac568bb3f4c397ecdc7301c086756e900 Mon Sep 17 00:00:00 2001 From: Wlowscha <54003515+Wlowscha@users.noreply.github.com> Date: Sun, 13 Jul 2025 15:59:53 +0200 Subject: [PATCH] Reworked getPartyBerries function, fixed harvest test --- .../encounters/absolute-avarice-encounter.ts | 15 +-- .../encounters/uncommon-breed-encounter.ts | 13 +-- src/items/item-utility.ts | 4 +- test/abilities/harvest.test.ts | 101 +++++++++--------- 4 files changed, 68 insertions(+), 65 deletions(-) diff --git a/src/data/mystery-encounters/encounters/absolute-avarice-encounter.ts b/src/data/mystery-encounters/encounters/absolute-avarice-encounter.ts index 8a5b0821033..8a6d2daebb0 100644 --- a/src/data/mystery-encounters/encounters/absolute-avarice-encounter.ts +++ b/src/data/mystery-encounters/encounters/absolute-avarice-encounter.ts @@ -15,7 +15,7 @@ import { TrainerSlot } from "#enums/trainer-slot"; import type { MysteryEncounterSpriteConfig } from "#field/mystery-encounter-intro"; import type { Pokemon } from "#field/pokemon"; import { EnemyPokemon } from "#field/pokemon"; -import type { HeldItemConfiguration, PokemonItemMap } from "#items/held-item-data-types"; +import type { HeldItemConfiguration, HeldItemSpecs, PokemonItemMap } from "#items/held-item-data-types"; import { getPartyBerries } from "#items/item-utility"; import { PokemonMove } from "#moves/pokemon-move"; import { queueEncounterMessage } from "#mystery-encounters/encounter-dialogue-utils"; @@ -31,7 +31,7 @@ import type { MysteryEncounter } from "#mystery-encounters/mystery-encounter"; import { MysteryEncounterBuilder } from "#mystery-encounters/mystery-encounter"; import { MysteryEncounterOptionBuilder } from "#mystery-encounters/mystery-encounter-option"; import { HeldItemRequirement } from "#mystery-encounters/mystery-encounter-requirements"; -import { randInt } from "#utils/common"; +import { pickWeightedIndex, randInt } from "#utils/common"; import { getPokemonSpecies } from "#utils/pokemon-utils"; import i18next from "i18next"; @@ -231,14 +231,17 @@ export const AbsoluteAvariceEncounter: MysteryEncounter = MysteryEncounterBuilde const party = globalScene.getPlayerParty(); party.forEach(pokemon => { const stolenBerries = berryMap.filter(map => map.pokemonId === pokemon.id); - const returnedBerryCount = Math.floor(((stolenBerries.length ?? 0) * 2) / 5); + const stolenBerryCount = stolenBerries.reduce((a, b) => a + (b.item as HeldItemSpecs).stack, 0); + const returnedBerryCount = Math.floor(((stolenBerryCount ?? 0) * 2) / 5); if (returnedBerryCount > 0) { for (let i = 0; i < returnedBerryCount; i++) { // Shuffle remaining berry types and pop - Phaser.Math.RND.shuffle(stolenBerries); - const randBerryType = stolenBerries.pop(); - pokemon.heldItemManager.add(randBerryType?.item.id as HeldItemId); + const berryWeights = stolenBerries.map(b => (b.item as HeldItemSpecs).stack); + const which = pickWeightedIndex(berryWeights) ?? 0; + const randBerry = stolenBerries[which]; + pokemon.heldItemManager.add(randBerry.item.id as HeldItemId); + (randBerry.item as HeldItemSpecs).stack -= 1; } } }); diff --git a/src/data/mystery-encounters/encounters/uncommon-breed-encounter.ts b/src/data/mystery-encounters/encounters/uncommon-breed-encounter.ts index 18e5f604f44..52986bec62e 100644 --- a/src/data/mystery-encounters/encounters/uncommon-breed-encounter.ts +++ b/src/data/mystery-encounters/encounters/uncommon-breed-encounter.ts @@ -11,7 +11,7 @@ import { MysteryEncounterType } from "#enums/mystery-encounter-type"; import { PokeballType } from "#enums/pokeball"; import { Stat } from "#enums/stat"; import type { EnemyPokemon, Pokemon } from "#field/pokemon"; -import type { PokemonItemMap } from "#items/held-item-data-types"; +import type { HeldItemSpecs } from "#items/held-item-data-types"; import { getPartyBerries } from "#items/item-utility"; import { PokemonMove } from "#moves/pokemon-move"; import { queueEncounterMessage } from "#mystery-encounters/encounter-dialogue-utils"; @@ -34,7 +34,7 @@ import { MysteryEncounterOptionBuilder } from "#mystery-encounters/mystery-encou import { HeldItemRequirement, MoveRequirement } from "#mystery-encounters/mystery-encounter-requirements"; import { CHARMING_MOVES } from "#mystery-encounters/requirement-groups"; import { PokemonData } from "#system/pokemon-data"; -import { isNullOrUndefined, randSeedInt } from "#utils/common"; +import { isNullOrUndefined, pickWeightedIndex, randSeedInt } from "#utils/common"; /** the i18n namespace for the encounter */ const namespace = "mysteryEncounters/uncommonBreed"; @@ -204,17 +204,14 @@ export const UncommonBreedEncounter: MysteryEncounter = MysteryEncounterBuilder. // Give it some food // Remove 4 random berries from player's party - // Get all player berry items, remove from party, and store reference - const berryMap = getPartyBerries(); - const stolenBerryMap: PokemonItemMap[] = []; for (let i = 0; i < 4; i++) { - const index = randSeedInt(berryMap.length); + const berryWeights = berryMap.map(b => (b.item as HeldItemSpecs).stack); + const index = pickWeightedIndex(berryWeights) ?? 0; const randBerry = berryMap[index]; globalScene.getPokemonById(randBerry.pokemonId)?.heldItemManager.remove(randBerry.item.id as HeldItemId); - stolenBerryMap.push(randBerry); - berryMap.splice(index, 1); + (randBerry.item as HeldItemSpecs).stack -= 1; } await globalScene.updateItems(true); diff --git a/src/items/item-utility.ts b/src/items/item-utility.ts index 89e6b6c3233..4108ab7e636 100644 --- a/src/items/item-utility.ts +++ b/src/items/item-utility.ts @@ -65,9 +65,7 @@ export function getPartyBerries(): PokemonItemMap[] { const berries = pokemon.getHeldItems().filter(item => isItemInCategory(item, HeldItemCategoryId.BERRY)); berries.forEach(berryId => { const berryStack = pokemon.heldItemManager.getStack(berryId); - for (let i = 1; i <= berryStack; i++) { - pokemonItems.push({ item: { id: berryId, stack: 1 }, pokemonId: pokemon.id }); - } + pokemonItems.push({ item: { id: berryId, stack: berryStack }, pokemonId: pokemon.id }); }); }); return pokemonItems; diff --git a/test/abilities/harvest.test.ts b/test/abilities/harvest.test.ts index 7959c89663e..d5452835116 100644 --- a/test/abilities/harvest.test.ts +++ b/test/abilities/harvest.test.ts @@ -6,11 +6,11 @@ import { HeldItemId } from "#enums/held-item-id"; import { MoveId } from "#enums/move-id"; import { SpeciesId } from "#enums/species-id"; import { Stat } from "#enums/stat"; +import { TrainerItemId } from "#enums/trainer-item-id"; import { WeatherType } from "#enums/weather-type"; -import type { Pokemon } from "#field/pokemon"; -import type { ModifierOverride } from "#modifiers/modifier-type"; +import type { PokemonItemMap } from "#items/held-item-data-types"; +import { getPartyBerries } from "#items/item-utility"; import { GameManager } from "#test/testUtils/gameManager"; -import type { BooleanHolder } from "#utils/common"; import Phaser from "phaser"; import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; @@ -18,15 +18,9 @@ describe("Abilities - Harvest", () => { let phaserGame: Phaser.Game; let game: GameManager; - const getPlayerBerries = () => - game.scene.getModifiers(BerryModifier, true).filter(b => b.pokemonId === game.scene.getPlayerPokemon()?.id); - /** Check whether the player's Modifiers contains the specified berries and nothing else. */ - function expectBerriesContaining(...berries: ModifierOverride[]): void { - const actualBerries: ModifierOverride[] = getPlayerBerries().map( - // only grab berry type and quantity since that's literally all we care about - b => ({ name: "BERRY", type: b.berryType, count: b.getStackCount() }), - ); + function expectBerriesContaining(berries: PokemonItemMap[]): void { + const actualBerries = getPartyBerries(); expect(actualBerries).toEqual(berries); } @@ -63,11 +57,13 @@ describe("Abilities - Harvest", () => { game.move.select(MoveId.SPLASH); await game.move.selectEnemyMove(MoveId.NUZZLE); await game.phaseInterceptor.to("BerryPhase"); - expect(getPlayerBerries()).toHaveLength(0); + expect(getPartyBerries()).toHaveLength(0); expect(game.scene.getPlayerPokemon()?.battleData.berriesEaten).toHaveLength(1); await game.phaseInterceptor.to("TurnEndPhase"); - expectBerriesContaining({ name: "BERRY", type: BerryType.LUM, count: 1 }); + expectBerriesContaining([ + { item: { id: HeldItemId.LUM_BERRY, stack: 1 }, pokemonId: game.scene.getPlayerPokemon()?.id! }, + ]); expect(game.scene.getPlayerPokemon()?.battleData.berriesEaten).toEqual([]); }); @@ -76,8 +72,8 @@ describe("Abilities - Harvest", () => { // the game consider all other pokemon to *not* have their respective abilities. game.override .startingHeldItems([ - { name: "BERRY", type: BerryType.ENIGMA, count: 2 }, - { name: "BERRY", type: BerryType.LUM, count: 2 }, + { entry: HeldItemId.ENIGMA_BERRY, count: 2 }, + { entry: HeldItemId.LUM_BERRY, count: 2 }, ]) .enemyAbility(AbilityId.NEUTRALIZING_GAS); await game.classicMode.startBattle([SpeciesId.MILOTIC]); @@ -91,7 +87,7 @@ describe("Abilities - Harvest", () => { await game.toNextTurn(); expect(milotic.battleData.berriesEaten).toEqual(expect.arrayContaining([BerryType.ENIGMA, BerryType.LUM])); - expect(getPlayerBerries()).toHaveLength(2); + expect(getPartyBerries()).toHaveLength(2); // Give ourselves harvest and disable enemy neut gas, // but force our roll to fail so we don't accidentally recover anything @@ -105,7 +101,7 @@ describe("Abilities - Harvest", () => { expect(milotic.battleData.berriesEaten).toEqual( expect.arrayContaining([BerryType.ENIGMA, BerryType.LUM, BerryType.ENIGMA, BerryType.LUM]), ); - expect(getPlayerBerries()).toHaveLength(0); + expect(getPartyBerries()).toHaveLength(0); // proc a high roll and we _should_ get a berry back! game.move.select(MoveId.SPLASH); @@ -113,7 +109,7 @@ describe("Abilities - Harvest", () => { await game.toNextTurn(); expect(milotic.battleData.berriesEaten).toHaveLength(3); - expect(getPlayerBerries()).toHaveLength(1); + expect(getPartyBerries()).toHaveLength(1); }); it("remembers berries eaten array across waves", async () => { @@ -131,13 +127,13 @@ describe("Abilities - Harvest", () => { // ate 1 berry without recovering (no harvest) expect(regieleki.battleData.berriesEaten).toEqual([BerryType.PETAYA]); - expectBerriesContaining({ name: "BERRY", count: 1, type: BerryType.PETAYA }); + expectBerriesContaining([{ item: { id: HeldItemId.PETAYA_BERRY, stack: 1 }, pokemonId: regieleki.id }]); expect(regieleki.getStatStage(Stat.SPATK)).toBe(1); await game.toNextWave(); expect(regieleki.battleData.berriesEaten).toEqual([BerryType.PETAYA]); - expectBerriesContaining({ name: "BERRY", count: 1, type: BerryType.PETAYA }); + expectBerriesContaining([{ item: { id: HeldItemId.PETAYA_BERRY, stack: 1 }, pokemonId: regieleki.id }]); expect(regieleki.getStatStage(Stat.SPATK)).toBe(1); }); @@ -159,7 +155,9 @@ describe("Abilities - Harvest", () => { // ate 1 berry and recovered it expect(regieleki.battleData.berriesEaten).toEqual([]); - expect(getPlayerBerries()).toEqual([expect.objectContaining({ berryType: BerryType.PETAYA, stackCount: 1 })]); + expectBerriesContaining([ + { item: { id: HeldItemId.PETAYA_BERRY, stack: 1 }, pokemonId: game.scene.getPlayerPokemon()?.id! }, + ]); expect(game.scene.getPlayerPokemon()?.getStatStage(Stat.SPATK)).toBe(1); // heal up so harvest doesn't proc and kill enemy @@ -168,13 +166,17 @@ describe("Abilities - Harvest", () => { await game.setTurnOrder([BattlerIndex.ENEMY, BattlerIndex.PLAYER]); await game.toNextWave(); - expectBerriesContaining({ name: "BERRY", count: 1, type: BerryType.PETAYA }); + expectBerriesContaining([ + { item: { id: HeldItemId.PETAYA_BERRY, stack: 1 }, pokemonId: game.scene.getPlayerPokemon()?.id! }, + ]); expect(game.scene.getPlayerPokemon()?.getStatStage(Stat.SPATK)).toBe(1); await game.reload.reloadSession(); expect(regieleki.battleData.berriesEaten).toEqual([]); - expectBerriesContaining({ name: "BERRY", count: 1, type: BerryType.PETAYA }); + expectBerriesContaining([ + { item: { id: HeldItemId.PETAYA_BERRY, stack: 1 }, pokemonId: game.scene.getPlayerPokemon()?.id! }, + ]); expect(game.scene.getPlayerPokemon()?.getStatStage(Stat.SPATK)).toBe(1); }); @@ -199,16 +201,16 @@ describe("Abilities - Harvest", () => { await game.phaseInterceptor.to("TurnEndPhase"); // recovered a starf - expectBerriesContaining( - { name: "BERRY", type: BerryType.LUM, count: 2 }, - { name: "BERRY", type: BerryType.STARF, count: 3 }, - ); + expectBerriesContaining([ + { item: { id: HeldItemId.LUM_BERRY, stack: 2 }, pokemonId: feebas.id }, + { item: { id: HeldItemId.STARF_BERRY, stack: 3 }, pokemonId: feebas.id }, + ]); }); it("does nothing if all berries are capped", async () => { - const initBerries: ModifierOverride[] = [ - { name: "BERRY", type: BerryType.LUM, count: 2 }, - { name: "BERRY", type: BerryType.STARF, count: 3 }, + const initBerries = [ + { entry: HeldItemId.LUM_BERRY, count: 2 }, + { entry: HeldItemId.STARF_BERRY, count: 3 }, ]; game.override.startingHeldItems(initBerries); await game.classicMode.startBattle([SpeciesId.FEEBAS]); @@ -220,7 +222,10 @@ describe("Abilities - Harvest", () => { await game.move.selectEnemyMove(MoveId.SPLASH); await game.phaseInterceptor.to("TurnEndPhase"); - expectBerriesContaining(...initBerries); + expectBerriesContaining([ + { item: { id: HeldItemId.LUM_BERRY, stack: 2 }, pokemonId: player.id }, + { item: { id: HeldItemId.STARF_BERRY, stack: 3 }, pokemonId: player.id }, + ]); }); describe("move/ability interactions", () => { @@ -236,7 +241,7 @@ describe("Abilities - Harvest", () => { }); it("cannot restore knocked off berries", async () => { - game.override.startingHeldItems([{ name: "BERRY", type: BerryType.STARF, count: 3 }]); + game.override.startingHeldItems([{ entry: HeldItemId.STARF_BERRY, count: 3 }]); await game.classicMode.startBattle([SpeciesId.FEEBAS]); game.move.select(MoveId.SPLASH); @@ -255,7 +260,9 @@ describe("Abilities - Harvest", () => { await game.phaseInterceptor.to("TurnEndPhase"); expect(game.scene.getPlayerPokemon()?.battleData.berriesEaten).toEqual([]); - expectBerriesContaining(...initBerries); + expectBerriesContaining([ + { item: { id: HeldItemId.STARF_BERRY, stack: 1 }, pokemonId: game.scene.getPlayerPokemon()!.id }, + ]); }); it("cannot restore Plucked berries for either side", async () => { @@ -272,21 +279,13 @@ describe("Abilities - Harvest", () => { // pluck triggers harvest for neither side expect(game.scene.getPlayerPokemon()?.battleData.berriesEaten).toEqual([]); expect(game.scene.getEnemyPokemon()?.battleData.berriesEaten).toEqual([]); - expect(getPlayerBerries()).toEqual([]); + expect(getPartyBerries()).toEqual([]); }); it("cannot restore berries preserved via Berry Pouch", async () => { - // mock berry pouch to have a 100% success rate - vi.spyOn(PreserveBerryModifier.prototype, "apply").mockImplementation( - (_pokemon: Pokemon, doPreserve: BooleanHolder): boolean => { - doPreserve.value = false; - return true; - }, - ); - game.override .startingHeldItems([{ entry: HeldItemId.PETAYA_BERRY }]) - .startingModifier([{ name: "BERRY_POUCH", count: 1 }]); + .startingTrainerItems([{ entry: TrainerItemId.BERRY_POUCH, count: 5850 }]); await game.classicMode.startBattle([SpeciesId.FEEBAS]); game.move.select(MoveId.SPLASH); @@ -294,11 +293,13 @@ describe("Abilities - Harvest", () => { // won't trigger harvest since we didn't lose the berry (it just doesn't ever add it to the array) expect(game.scene.getPlayerPokemon()?.battleData.berriesEaten).toEqual([]); - expectBerriesContaining(...initBerries); + expectBerriesContaining([ + { item: { id: HeldItemId.PETAYA_BERRY, stack: 1 }, pokemonId: game.scene.getPlayerPokemon()!.id }, + ]); }); it("can restore stolen berries", async () => { - const initBerries: ModifierOverride[] = [{ name: "BERRY", type: BerryType.SITRUS, count: 1 }]; + const initBerries = [{ entry: HeldItemId.SITRUS_BERRY }]; game.override.enemyHeldItems(initBerries).passiveAbility(AbilityId.MAGICIAN).hasPassiveAbility(true); await game.classicMode.startBattle([SpeciesId.MEOWSCARADA]); @@ -315,7 +316,9 @@ describe("Abilities - Harvest", () => { await game.phaseInterceptor.to("TurnEndPhase"); expect(player.battleData.berriesEaten).toEqual([]); - expectBerriesContaining(...initBerries); + expectBerriesContaining([ + { item: { id: HeldItemId.SITRUS_BERRY, stack: 1 }, pokemonId: game.scene.getPlayerPokemon()!.id }, + ]); }); // TODO: Enable once fling actually works...??? @@ -327,7 +330,7 @@ describe("Abilities - Harvest", () => { await game.phaseInterceptor.to("TurnEndPhase"); expect(game.scene.getPlayerPokemon()?.battleData.berriesEaten).toBe([]); - expect(getPlayerBerries()).toEqual([]); + expect(getPartyBerries()).toEqual([]); }); // TODO: Enable once Nat Gift gets implemented...??? @@ -339,7 +342,9 @@ describe("Abilities - Harvest", () => { await game.phaseInterceptor.to("TurnEndPhase"); expect(game.scene.getPlayerPokemon()?.battleData.berriesEaten).toHaveLength(0); - expectBerriesContaining(...initBerries); + expectBerriesContaining([ + { item: { id: HeldItemId.STARF_BERRY, stack: 1 }, pokemonId: game.scene.getPlayerPokemon()!.id }, + ]); }); }); });