From e312a4b8f2de54f1db4e2017c651da32e59a81b8 Mon Sep 17 00:00:00 2001 From: Wlowscha <54003515+Wlowscha@users.noreply.github.com> Date: Sat, 12 Jul 2025 12:56:58 +0200 Subject: [PATCH] Fixed bug-type-superfan encounter; split up HoldingItemRequirement from HeldItemRequirement --- .../encounters/bug-type-superfan-encounter.ts | 20 ++-- .../encounters/delibirdy-encounter.ts | 10 +- .../mystery-encounter-requirements.ts | 93 ++++++++++++++++--- src/field/pokemon-held-item-manager.ts | 9 ++ .../bug-type-superfan-encounter.test.ts | 16 ++-- 5 files changed, 109 insertions(+), 39 deletions(-) diff --git a/src/data/mystery-encounters/encounters/bug-type-superfan-encounter.ts b/src/data/mystery-encounters/encounters/bug-type-superfan-encounter.ts index 9bc3e293cbe..be5fba3a616 100644 --- a/src/data/mystery-encounters/encounters/bug-type-superfan-encounter.ts +++ b/src/data/mystery-encounters/encounters/bug-type-superfan-encounter.ts @@ -31,7 +31,7 @@ import { MysteryEncounterOptionBuilder } from "#app/data/mystery-encounters/myst import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode"; import { CombinationPokemonRequirement, - HeldItemRequirement, + HoldingItemRequirement, TypeRequirement, } from "#app/data/mystery-encounters/mystery-encounter-requirements"; import { PokemonType } from "#enums/pokemon-type"; @@ -181,7 +181,7 @@ export const BugTypeSuperfanEncounter: MysteryEncounter = MysteryEncounterBuilde .withPrimaryPokemonRequirement( CombinationPokemonRequirement.Some( // Must have at least 1 Bug type on team, OR have a bug item somewhere on the team - new HeldItemRequirement(REQUIRED_ITEMS, 1), + new HoldingItemRequirement(REQUIRED_ITEMS, 1), new TypeRequirement(PokemonType.BUG, false, 1), ), ) @@ -403,7 +403,7 @@ export const BugTypeSuperfanEncounter: MysteryEncounter = MysteryEncounterBuilde .withPrimaryPokemonRequirement( CombinationPokemonRequirement.Some( // Meets one or both of the below reqs - new HeldItemRequirement(REQUIRED_ITEMS, 1), + new HoldingItemRequirement(REQUIRED_ITEMS, 1), ), ) .withDialogue({ @@ -426,9 +426,9 @@ export const BugTypeSuperfanEncounter: MysteryEncounter = MysteryEncounterBuilde const onPokemonSelected = (pokemon: PlayerPokemon) => { // Get Pokemon held items and filter for valid ones - const validItems = pokemon.heldItemManager.getTransferableHeldItems().filter(item => { - item in REQUIRED_ITEMS; - }); + const validItems = pokemon.heldItemManager + .getTransferableHeldItems() + .filter(item => REQUIRED_ITEMS.some(i => i === item)); return validItems.map((item: HeldItemId) => { const option: OptionSelectItem = { @@ -449,9 +449,7 @@ export const BugTypeSuperfanEncounter: MysteryEncounter = MysteryEncounterBuilde const selectableFilter = (pokemon: Pokemon) => { // If pokemon has valid item, it can be selected - const hasValidItem = pokemon.getHeldItems().some(item => { - item in REQUIRED_ITEMS; - }); + const hasValidItem = pokemon.getHeldItems().some(item => REQUIRED_ITEMS.some(i => i === item)); if (!hasValidItem) { return getEncounterText(`${namespace}:option.3.invalid_selection`) ?? null; } @@ -463,10 +461,10 @@ export const BugTypeSuperfanEncounter: MysteryEncounter = MysteryEncounterBuilde }) .withOptionPhase(async () => { const encounter = globalScene.currentBattle.mysteryEncounter!; - const modifier = encounter.misc.chosenModifier; + const lostItem = encounter.misc.chosenItem; const chosenPokemon: PlayerPokemon = encounter.misc.chosenPokemon; - chosenPokemon.loseHeldItem(modifier, false); + chosenPokemon.loseHeldItem(lostItem, false); globalScene.updateItems(true); const bugNet = generateModifierTypeOption(modifierTypes.MYSTERY_ENCOUNTER_GOLDEN_BUG_NET)!; diff --git a/src/data/mystery-encounters/encounters/delibirdy-encounter.ts b/src/data/mystery-encounters/encounters/delibirdy-encounter.ts index a516e3265bc..4e9063ae429 100644 --- a/src/data/mystery-encounters/encounters/delibirdy-encounter.ts +++ b/src/data/mystery-encounters/encounters/delibirdy-encounter.ts @@ -4,7 +4,7 @@ import { MysteryEncounterBuilder } from "#app/data/mystery-encounters/mystery-en import { MysteryEncounterOptionBuilder } from "#app/data/mystery-encounters/mystery-encounter-option"; import { CombinationPokemonRequirement, - HeldItemRequirement, + HoldingItemRequirement, MoneyRequirement, } from "#app/data/mystery-encounters/mystery-encounter-requirements"; import { getEncounterText, showEncounterText } from "#app/data/mystery-encounters/utils/encounter-dialogue-utils"; @@ -85,8 +85,8 @@ export const DelibirdyEncounter: MysteryEncounter = MysteryEncounterBuilder.with .withPrimaryPokemonRequirement( CombinationPokemonRequirement.Some( // Must also have either option 2 or 3 available to spawn - new HeldItemRequirement(OPTION_2_ALLOWED_MODIFIERS), - new HeldItemRequirement(OPTION_3_DISALLOWED_MODIFIERS, 1, true), + new HoldingItemRequirement(OPTION_2_ALLOWED_MODIFIERS), + new HoldingItemRequirement(OPTION_3_DISALLOWED_MODIFIERS, 1, true), ), ) .withIntroSpriteConfigs([ @@ -180,7 +180,7 @@ export const DelibirdyEncounter: MysteryEncounter = MysteryEncounterBuilder.with ) .withOption( MysteryEncounterOptionBuilder.newOptionWithMode(MysteryEncounterOptionMode.DISABLED_OR_DEFAULT) - .withPrimaryPokemonRequirement(new HeldItemRequirement(OPTION_2_ALLOWED_MODIFIERS)) + .withPrimaryPokemonRequirement(new HoldingItemRequirement(OPTION_2_ALLOWED_MODIFIERS)) .withDialogue({ buttonLabel: `${namespace}:option.2.label`, buttonTooltip: `${namespace}:option.2.tooltip`, @@ -264,7 +264,7 @@ export const DelibirdyEncounter: MysteryEncounter = MysteryEncounterBuilder.with ) .withOption( MysteryEncounterOptionBuilder.newOptionWithMode(MysteryEncounterOptionMode.DISABLED_OR_DEFAULT) - .withPrimaryPokemonRequirement(new HeldItemRequirement(OPTION_3_DISALLOWED_MODIFIERS, 1, true)) + .withPrimaryPokemonRequirement(new HoldingItemRequirement(OPTION_3_DISALLOWED_MODIFIERS, 1, true)) .withDialogue({ buttonLabel: `${namespace}:option.3.label`, buttonTooltip: `${namespace}:option.3.tooltip`, diff --git a/src/data/mystery-encounters/mystery-encounter-requirements.ts b/src/data/mystery-encounters/mystery-encounter-requirements.ts index 9ea4d55b1c6..fc829b29f02 100644 --- a/src/data/mystery-encounters/mystery-encounter-requirements.ts +++ b/src/data/mystery-encounters/mystery-encounter-requirements.ts @@ -14,7 +14,7 @@ import { MoveId } from "#enums/move-id"; import type { MysteryEncounterType } from "#enums/mystery-encounter-type"; import { SpeciesId } from "#enums/species-id"; import { TimeOfDay } from "#enums/time-of-day"; -import type { HeldItemCategoryId, HeldItemId } from "#enums/held-item-id"; +import { getHeldItemCategory, type HeldItemCategoryId, type HeldItemId } from "#enums/held-item-id"; import { allHeldItems } from "#app/data/data-lists"; export interface EncounterRequirement { @@ -799,7 +799,7 @@ export class CanFormChangeWithItemRequirement extends EncounterPokemonRequiremen } } -export class HeldItemRequirement extends EncounterPokemonRequirement { +export class HoldingItemRequirement extends EncounterPokemonRequirement { requiredHeldItems: (HeldItemId | HeldItemCategoryId)[]; minNumberOfPokemon: number; invertQuery: boolean; @@ -830,23 +830,21 @@ export class HeldItemRequirement extends EncounterPokemonRequirement { if (!this.invertQuery) { return partyPokemon.filter(pokemon => this.requiredHeldItems.some(heldItem => { - return ( - pokemon.heldItemManager.hasItem(heldItem) && - (!this.requireTransferable || allHeldItems[heldItem].isTransferable) - ); + return this.requireTransferable + ? pokemon.heldItemManager.hasTransferableItem(heldItem) + : pokemon.heldItemManager.hasItem(heldItem); }), ); } // for an inverted query, we only want to get the pokemon that have any held items that are NOT in requiredHeldItemModifiers // E.g. functions as a blacklist - return partyPokemon.filter( - pokemon => - pokemon.getHeldItems().filter(item => { - return ( - !this.requiredHeldItems.some(heldItem => item === heldItem) && - (!this.requireTransferable || allHeldItems[item].isTransferable) - ); - }).length > 0, + return partyPokemon.filter(pokemon => + pokemon.getHeldItems().some(item => { + return ( + !this.requiredHeldItems.some(heldItem => item === heldItem || getHeldItemCategory(item) === heldItem) && + (!this.requireTransferable || allHeldItems[item].isTransferable) + ); + }), ); } @@ -864,6 +862,73 @@ export class HeldItemRequirement extends EncounterPokemonRequirement { } } +export class HeldItemRequirement extends EncounterSceneRequirement { + requiredHeldItems: (HeldItemId | HeldItemCategoryId)[]; + minNumberOfItems: number; + invertQuery: boolean; + requireTransferable: boolean; + + constructor( + heldItem: HeldItemId | HeldItemCategoryId | (HeldItemId | HeldItemCategoryId)[], + minNumberOfItems = 1, + invertQuery = false, + requireTransferable = true, + ) { + super(); + this.minNumberOfItems = minNumberOfItems; + this.invertQuery = invertQuery; + this.requiredHeldItems = coerceArray(heldItem); + this.requireTransferable = requireTransferable; + } + + override meetsRequirement(): boolean { + const partyPokemon = globalScene.getPlayerParty(); + if (isNullOrUndefined(partyPokemon)) { + return false; + } + return this.queryPartyForItems(partyPokemon) >= this.minNumberOfItems; + } + + queryPartyForItems(partyPokemon: PlayerPokemon[]): number { + if (!this.invertQuery) { + return partyPokemon.reduce((count, pokemon) => { + const matchingItems = this.requiredHeldItems.filter(heldItem => { + return this.requireTransferable + ? pokemon.heldItemManager.hasTransferableItem(heldItem) + : pokemon.heldItemManager.hasItem(heldItem); + }); + return count + matchingItems.length; + }, 0); + } + // for an inverted query, we only want to get the pokemon that have any held items that are NOT in requiredHeldItemModifiers + // E.g. functions as a blacklist + return partyPokemon.reduce((count, pokemon) => { + const matchingItems = pokemon.getHeldItems().filter(item => { + const notRequired = !this.requiredHeldItems.some( + heldItem => item === heldItem || getHeldItemCategory(item) === heldItem, + ); + const transferableOk = !this.requireTransferable || allHeldItems[item].isTransferable; + return notRequired && transferableOk; + }); + + return count + matchingItems.length; + }, 0); + } + + override getDialogueToken(pokemon?: PlayerPokemon): [string, string] { + const requiredItems = pokemon?.getHeldItems().filter(item => { + return ( + this.requiredHeldItems.some(heldItem => item === heldItem) && + (!this.requireTransferable || allHeldItems[item].isTransferable) + ); + }); + if (requiredItems && requiredItems.length > 0) { + return ["heldItem", allHeldItems[requiredItems[0]].name]; + } + return ["heldItem", ""]; + } +} + export class LevelRequirement extends EncounterPokemonRequirement { requiredLevelRange: [number, number]; minNumberOfPokemon: number; diff --git a/src/field/pokemon-held-item-manager.ts b/src/field/pokemon-held-item-manager.ts index d5bbeea4389..6185b5fc2c6 100644 --- a/src/field/pokemon-held-item-manager.ts +++ b/src/field/pokemon-held-item-manager.ts @@ -102,6 +102,15 @@ export class PokemonItemManager { return itemType in this.heldItems; } + hasTransferableItem(itemType: HeldItemId | HeldItemCategoryId): boolean { + if (isCategoryId(itemType)) { + return getTypedKeys(this.heldItems).some( + id => isItemInCategory(id, itemType as HeldItemCategoryId) && allHeldItems[id].isTransferable, + ); + } + return itemType in this.heldItems && allHeldItems[itemType].isTransferable; + } + getStack(itemType: HeldItemId): number { const item = this.heldItems[itemType]; return item ? item.stack : 0; diff --git a/test/mystery-encounter/encounters/bug-type-superfan-encounter.test.ts b/test/mystery-encounter/encounters/bug-type-superfan-encounter.test.ts index 4da8ff7f643..5e0af7b642f 100644 --- a/test/mystery-encounter/encounters/bug-type-superfan-encounter.test.ts +++ b/test/mystery-encounter/encounters/bug-type-superfan-encounter.test.ts @@ -18,12 +18,12 @@ import { MysteryEncounterTier } from "#enums/mystery-encounter-tier"; import { initSceneWithoutEncounterPhase } from "#test/testUtils/gameManagerUtils"; import { TrainerType } from "#enums/trainer-type"; import { MysteryEncounterPhase, MysteryEncounterRewardsPhase } from "#app/phases/mystery-encounter-phases"; -import { ContactHeldItemTransferChanceModifier } from "#app/modifier/modifier"; import { CommandPhase } from "#app/phases/command-phase"; import { BugTypeSuperfanEncounter } from "#app/data/mystery-encounters/encounters/bug-type-superfan-encounter"; import * as encounterPhaseUtils from "#app/data/mystery-encounters/utils/encounter-phase-utils"; import { SelectModifierPhase } from "#app/phases/select-modifier-phase"; import ModifierSelectUiHandler from "#app/ui/modifier-select-ui-handler"; +import { HeldItemId } from "#enums/held-item-id"; const namespace = "mysteryEncounters/bugTypeSuperfan"; const defaultParty = [SpeciesId.LAPRAS, SpeciesId.GENGAR, SpeciesId.WEEDLE]; @@ -528,11 +528,11 @@ describe("Bug-Type Superfan - Mystery Encounter", () => { }); it("should NOT be selectable if the player doesn't have any Bug items", async () => { - game.scene.modifiers = []; + game.scene.trainerItems.clearItems(); await game.runToMysteryEncounter(MysteryEncounterType.BUG_TYPE_SUPERFAN, defaultParty); await game.phaseInterceptor.to(MysteryEncounterPhase, false); - game.scene.modifiers = []; + game.scene.trainerItems.clearItems(); const encounterPhase = scene.phaseManager.getCurrentPhase(); expect(encounterPhase?.constructor.name).toBe(MysteryEncounterPhase.name); const mysteryEncounterPhase = encounterPhase as MysteryEncounterPhase; @@ -549,11 +549,10 @@ describe("Bug-Type Superfan - Mystery Encounter", () => { }); it("should remove the gifted item and proceed to rewards screen", async () => { - game.override.startingHeldItems([{ name: "GRIP_CLAW", count: 1 }]); + game.override.startingHeldItems([{ entry: HeldItemId.GRIP_CLAW, count: 1 }]); await game.runToMysteryEncounter(MysteryEncounterType.BUG_TYPE_SUPERFAN, [SpeciesId.BUTTERFREE]); - const gripClawCountBefore = - scene.findModifier(m => m instanceof ContactHeldItemTransferChanceModifier)?.stackCount ?? 0; + const gripClawCountBefore = scene.getPlayerParty()[0].heldItemManager.getStack(HeldItemId.GRIP_CLAW); await runMysteryEncounterToEnd(game, 3, { pokemonNo: 1, optionNo: 1 }); @@ -568,13 +567,12 @@ describe("Bug-Type Superfan - Mystery Encounter", () => { expect(modifierSelectHandler.options[0].modifierTypeOption.type.id).toBe("MYSTERY_ENCOUNTER_GOLDEN_BUG_NET"); expect(modifierSelectHandler.options[1].modifierTypeOption.type.id).toBe("REVIVER_SEED"); - const gripClawCountAfter = - scene.findModifier(m => m instanceof ContactHeldItemTransferChanceModifier)?.stackCount ?? 0; + const gripClawCountAfter = scene.getPlayerParty()[0].heldItemManager.getStack(HeldItemId.GRIP_CLAW); expect(gripClawCountBefore - 1).toBe(gripClawCountAfter); }); it("should leave encounter without battle", async () => { - game.override.startingHeldItems([{ name: "GRIP_CLAW", count: 1 }]); + game.override.startingHeldItems([{ entry: HeldItemId.GRIP_CLAW, count: 1 }]); const leaveEncounterWithoutBattleSpy = vi.spyOn(encounterPhaseUtils, "leaveEncounterWithoutBattle"); await game.runToMysteryEncounter(MysteryEncounterType.BUG_TYPE_SUPERFAN, [SpeciesId.BUTTERFREE]);