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 { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode";
import { import {
CombinationPokemonRequirement, CombinationPokemonRequirement,
HeldItemRequirement, HoldingItemRequirement,
TypeRequirement, TypeRequirement,
} from "#app/data/mystery-encounters/mystery-encounter-requirements"; } from "#app/data/mystery-encounters/mystery-encounter-requirements";
import { PokemonType } from "#enums/pokemon-type"; import { PokemonType } from "#enums/pokemon-type";
@ -181,7 +181,7 @@ export const BugTypeSuperfanEncounter: MysteryEncounter = MysteryEncounterBuilde
.withPrimaryPokemonRequirement( .withPrimaryPokemonRequirement(
CombinationPokemonRequirement.Some( CombinationPokemonRequirement.Some(
// Must have at least 1 Bug type on team, OR have a bug item somewhere on the team // 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), new TypeRequirement(PokemonType.BUG, false, 1),
), ),
) )
@ -403,7 +403,7 @@ export const BugTypeSuperfanEncounter: MysteryEncounter = MysteryEncounterBuilde
.withPrimaryPokemonRequirement( .withPrimaryPokemonRequirement(
CombinationPokemonRequirement.Some( CombinationPokemonRequirement.Some(
// Meets one or both of the below reqs // Meets one or both of the below reqs
new HeldItemRequirement(REQUIRED_ITEMS, 1), new HoldingItemRequirement(REQUIRED_ITEMS, 1),
), ),
) )
.withDialogue({ .withDialogue({
@ -426,9 +426,9 @@ export const BugTypeSuperfanEncounter: MysteryEncounter = MysteryEncounterBuilde
const onPokemonSelected = (pokemon: PlayerPokemon) => { const onPokemonSelected = (pokemon: PlayerPokemon) => {
// Get Pokemon held items and filter for valid ones // Get Pokemon held items and filter for valid ones
const validItems = pokemon.heldItemManager.getTransferableHeldItems().filter(item => { const validItems = pokemon.heldItemManager
item in REQUIRED_ITEMS; .getTransferableHeldItems()
}); .filter(item => REQUIRED_ITEMS.some(i => i === item));
return validItems.map((item: HeldItemId) => { return validItems.map((item: HeldItemId) => {
const option: OptionSelectItem = { const option: OptionSelectItem = {
@ -449,9 +449,7 @@ export const BugTypeSuperfanEncounter: MysteryEncounter = MysteryEncounterBuilde
const selectableFilter = (pokemon: Pokemon) => { const selectableFilter = (pokemon: Pokemon) => {
// If pokemon has valid item, it can be selected // If pokemon has valid item, it can be selected
const hasValidItem = pokemon.getHeldItems().some(item => { const hasValidItem = pokemon.getHeldItems().some(item => REQUIRED_ITEMS.some(i => i === item));
item in REQUIRED_ITEMS;
});
if (!hasValidItem) { if (!hasValidItem) {
return getEncounterText(`${namespace}:option.3.invalid_selection`) ?? null; return getEncounterText(`${namespace}:option.3.invalid_selection`) ?? null;
} }
@ -463,10 +461,10 @@ export const BugTypeSuperfanEncounter: MysteryEncounter = MysteryEncounterBuilde
}) })
.withOptionPhase(async () => { .withOptionPhase(async () => {
const encounter = globalScene.currentBattle.mysteryEncounter!; const encounter = globalScene.currentBattle.mysteryEncounter!;
const modifier = encounter.misc.chosenModifier; const lostItem = encounter.misc.chosenItem;
const chosenPokemon: PlayerPokemon = encounter.misc.chosenPokemon; const chosenPokemon: PlayerPokemon = encounter.misc.chosenPokemon;
chosenPokemon.loseHeldItem(modifier, false); chosenPokemon.loseHeldItem(lostItem, false);
globalScene.updateItems(true); globalScene.updateItems(true);
const bugNet = generateModifierTypeOption(modifierTypes.MYSTERY_ENCOUNTER_GOLDEN_BUG_NET)!; 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 { MysteryEncounterOptionBuilder } from "#app/data/mystery-encounters/mystery-encounter-option";
import { import {
CombinationPokemonRequirement, CombinationPokemonRequirement,
HeldItemRequirement, HoldingItemRequirement,
MoneyRequirement, MoneyRequirement,
} from "#app/data/mystery-encounters/mystery-encounter-requirements"; } from "#app/data/mystery-encounters/mystery-encounter-requirements";
import { getEncounterText, showEncounterText } from "#app/data/mystery-encounters/utils/encounter-dialogue-utils"; import { getEncounterText, showEncounterText } from "#app/data/mystery-encounters/utils/encounter-dialogue-utils";
@ -85,8 +85,8 @@ export const DelibirdyEncounter: MysteryEncounter = MysteryEncounterBuilder.with
.withPrimaryPokemonRequirement( .withPrimaryPokemonRequirement(
CombinationPokemonRequirement.Some( CombinationPokemonRequirement.Some(
// Must also have either option 2 or 3 available to spawn // Must also have either option 2 or 3 available to spawn
new HeldItemRequirement(OPTION_2_ALLOWED_MODIFIERS), new HoldingItemRequirement(OPTION_2_ALLOWED_MODIFIERS),
new HeldItemRequirement(OPTION_3_DISALLOWED_MODIFIERS, 1, true), new HoldingItemRequirement(OPTION_3_DISALLOWED_MODIFIERS, 1, true),
), ),
) )
.withIntroSpriteConfigs([ .withIntroSpriteConfigs([
@ -180,7 +180,7 @@ export const DelibirdyEncounter: MysteryEncounter = MysteryEncounterBuilder.with
) )
.withOption( .withOption(
MysteryEncounterOptionBuilder.newOptionWithMode(MysteryEncounterOptionMode.DISABLED_OR_DEFAULT) MysteryEncounterOptionBuilder.newOptionWithMode(MysteryEncounterOptionMode.DISABLED_OR_DEFAULT)
.withPrimaryPokemonRequirement(new HeldItemRequirement(OPTION_2_ALLOWED_MODIFIERS)) .withPrimaryPokemonRequirement(new HoldingItemRequirement(OPTION_2_ALLOWED_MODIFIERS))
.withDialogue({ .withDialogue({
buttonLabel: `${namespace}:option.2.label`, buttonLabel: `${namespace}:option.2.label`,
buttonTooltip: `${namespace}:option.2.tooltip`, buttonTooltip: `${namespace}:option.2.tooltip`,
@ -264,7 +264,7 @@ export const DelibirdyEncounter: MysteryEncounter = MysteryEncounterBuilder.with
) )
.withOption( .withOption(
MysteryEncounterOptionBuilder.newOptionWithMode(MysteryEncounterOptionMode.DISABLED_OR_DEFAULT) 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({ .withDialogue({
buttonLabel: `${namespace}:option.3.label`, buttonLabel: `${namespace}:option.3.label`,
buttonTooltip: `${namespace}:option.3.tooltip`, 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 type { MysteryEncounterType } from "#enums/mystery-encounter-type";
import { SpeciesId } from "#enums/species-id"; import { SpeciesId } from "#enums/species-id";
import { TimeOfDay } from "#enums/time-of-day"; 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"; import { allHeldItems } from "#app/data/data-lists";
export interface EncounterRequirement { 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)[]; requiredHeldItems: (HeldItemId | HeldItemCategoryId)[];
minNumberOfPokemon: number; minNumberOfPokemon: number;
invertQuery: boolean; invertQuery: boolean;
@ -830,23 +830,21 @@ export class HeldItemRequirement extends EncounterPokemonRequirement {
if (!this.invertQuery) { if (!this.invertQuery) {
return partyPokemon.filter(pokemon => return partyPokemon.filter(pokemon =>
this.requiredHeldItems.some(heldItem => { this.requiredHeldItems.some(heldItem => {
return ( return this.requireTransferable
pokemon.heldItemManager.hasItem(heldItem) && ? pokemon.heldItemManager.hasTransferableItem(heldItem)
(!this.requireTransferable || allHeldItems[heldItem].isTransferable) : 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 // 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 // E.g. functions as a blacklist
return partyPokemon.filter( return partyPokemon.filter(pokemon =>
pokemon => pokemon.getHeldItems().some(item => {
pokemon.getHeldItems().filter(item => { return (
return ( !this.requiredHeldItems.some(heldItem => item === heldItem || getHeldItemCategory(item) === heldItem) &&
!this.requiredHeldItems.some(heldItem => item === heldItem) && (!this.requireTransferable || allHeldItems[item].isTransferable)
(!this.requireTransferable || allHeldItems[item].isTransferable) );
); }),
}).length > 0,
); );
} }
@ -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 { export class LevelRequirement extends EncounterPokemonRequirement {
requiredLevelRange: [number, number]; requiredLevelRange: [number, number];
minNumberOfPokemon: number; minNumberOfPokemon: number;

View File

@ -102,6 +102,15 @@ export class PokemonItemManager {
return itemType in this.heldItems; 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 { getStack(itemType: HeldItemId): number {
const item = this.heldItems[itemType]; const item = this.heldItems[itemType];
return item ? item.stack : 0; 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 { initSceneWithoutEncounterPhase } from "#test/testUtils/gameManagerUtils";
import { TrainerType } from "#enums/trainer-type"; import { TrainerType } from "#enums/trainer-type";
import { MysteryEncounterPhase, MysteryEncounterRewardsPhase } from "#app/phases/mystery-encounter-phases"; import { MysteryEncounterPhase, MysteryEncounterRewardsPhase } from "#app/phases/mystery-encounter-phases";
import { ContactHeldItemTransferChanceModifier } from "#app/modifier/modifier";
import { CommandPhase } from "#app/phases/command-phase"; import { CommandPhase } from "#app/phases/command-phase";
import { BugTypeSuperfanEncounter } from "#app/data/mystery-encounters/encounters/bug-type-superfan-encounter"; 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 * as encounterPhaseUtils from "#app/data/mystery-encounters/utils/encounter-phase-utils";
import { SelectModifierPhase } from "#app/phases/select-modifier-phase"; import { SelectModifierPhase } from "#app/phases/select-modifier-phase";
import ModifierSelectUiHandler from "#app/ui/modifier-select-ui-handler"; import ModifierSelectUiHandler from "#app/ui/modifier-select-ui-handler";
import { HeldItemId } from "#enums/held-item-id";
const namespace = "mysteryEncounters/bugTypeSuperfan"; const namespace = "mysteryEncounters/bugTypeSuperfan";
const defaultParty = [SpeciesId.LAPRAS, SpeciesId.GENGAR, SpeciesId.WEEDLE]; 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 () => { 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.runToMysteryEncounter(MysteryEncounterType.BUG_TYPE_SUPERFAN, defaultParty);
await game.phaseInterceptor.to(MysteryEncounterPhase, false); await game.phaseInterceptor.to(MysteryEncounterPhase, false);
game.scene.modifiers = []; game.scene.trainerItems.clearItems();
const encounterPhase = scene.phaseManager.getCurrentPhase(); const encounterPhase = scene.phaseManager.getCurrentPhase();
expect(encounterPhase?.constructor.name).toBe(MysteryEncounterPhase.name); expect(encounterPhase?.constructor.name).toBe(MysteryEncounterPhase.name);
const mysteryEncounterPhase = encounterPhase as MysteryEncounterPhase; 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 () => { 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]); await game.runToMysteryEncounter(MysteryEncounterType.BUG_TYPE_SUPERFAN, [SpeciesId.BUTTERFREE]);
const gripClawCountBefore = const gripClawCountBefore = scene.getPlayerParty()[0].heldItemManager.getStack(HeldItemId.GRIP_CLAW);
scene.findModifier(m => m instanceof ContactHeldItemTransferChanceModifier)?.stackCount ?? 0;
await runMysteryEncounterToEnd(game, 3, { pokemonNo: 1, optionNo: 1 }); 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[0].modifierTypeOption.type.id).toBe("MYSTERY_ENCOUNTER_GOLDEN_BUG_NET");
expect(modifierSelectHandler.options[1].modifierTypeOption.type.id).toBe("REVIVER_SEED"); expect(modifierSelectHandler.options[1].modifierTypeOption.type.id).toBe("REVIVER_SEED");
const gripClawCountAfter = const gripClawCountAfter = scene.getPlayerParty()[0].heldItemManager.getStack(HeldItemId.GRIP_CLAW);
scene.findModifier(m => m instanceof ContactHeldItemTransferChanceModifier)?.stackCount ?? 0;
expect(gripClawCountBefore - 1).toBe(gripClawCountAfter); expect(gripClawCountBefore - 1).toBe(gripClawCountAfter);
}); });
it("should leave encounter without battle", async () => { 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"); const leaveEncounterWithoutBattleSpy = vi.spyOn(encounterPhaseUtils, "leaveEncounterWithoutBattle");
await game.runToMysteryEncounter(MysteryEncounterType.BUG_TYPE_SUPERFAN, [SpeciesId.BUTTERFREE]); await game.runToMysteryEncounter(MysteryEncounterType.BUG_TYPE_SUPERFAN, [SpeciesId.BUTTERFREE]);