Fixed bug-type-superfan encounter; split up HoldingItemRequirement from HeldItemRequirement

This commit is contained in:
Wlowscha 2025-07-12 12:56:58 +02:00
parent dbfbc24c8a
commit e312a4b8f2
No known key found for this signature in database
GPG Key ID: 3C8F1AD330565D04
5 changed files with 109 additions and 39 deletions

View File

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

View File

@ -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`,

View File

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

View File

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

View File

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