Reworked getPartyBerries function, fixed harvest test

This commit is contained in:
Wlowscha 2025-07-13 15:59:53 +02:00
parent b8324e85f7
commit c7a1b0fac5
No known key found for this signature in database
GPG Key ID: 3C8F1AD330565D04
4 changed files with 68 additions and 65 deletions

View File

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

View File

@ -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);

View File

@ -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;

View File

@ -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 },
]);
});
});
});