From b5bd2dd0581a4c6077a48944061b084e793bbe8f Mon Sep 17 00:00:00 2001 From: ImperialSympathizer Date: Thu, 25 Jul 2024 20:40:06 -0400 Subject: [PATCH] add delibird-y encounter --- .../encounters/delibirdy-encounter.ts | 247 ++++++++++++ .../encounters/fiery-fallout-encounter.ts | 3 +- .../mystery-encounter-option.ts | 6 +- .../mystery-encounter-requirements.ts | 29 +- .../mystery-encounters/mystery-encounters.ts | 17 +- src/enums/mystery-encounter-type.ts | 4 +- src/field/mystery-encounter-intro.ts | 4 +- src/locales/en/mystery-encounter.ts | 4 +- .../mystery-encounters/delibirdy-dialogue.ts | 31 ++ .../shady-vitamin-dealer-dialogue.ts | 4 - .../mystery-encounter/encounterTestUtils.ts | 49 ++- .../encounters/delibirdy-encounter.test.ts | 373 ++++++++++++++++++ .../fiery-fallout-encounter.test.ts | 4 +- .../encounters/lost-at-sea-encounter.test.ts | 20 +- .../offer-you-cant-refuse-encounter.test.ts | 11 + .../pokemon-salesman-encounter.test.ts | 39 +- .../the-strong-stuff-encounter.test.ts | 4 +- 17 files changed, 808 insertions(+), 41 deletions(-) create mode 100644 src/data/mystery-encounters/encounters/delibirdy-encounter.ts create mode 100644 src/locales/en/mystery-encounters/delibirdy-dialogue.ts create mode 100644 src/test/mystery-encounter/encounters/delibirdy-encounter.test.ts diff --git a/src/data/mystery-encounters/encounters/delibirdy-encounter.ts b/src/data/mystery-encounters/encounters/delibirdy-encounter.ts new file mode 100644 index 00000000000..c58f9775a04 --- /dev/null +++ b/src/data/mystery-encounters/encounters/delibirdy-encounter.ts @@ -0,0 +1,247 @@ +import { leaveEncounterWithoutBattle, selectPokemonForOption, updatePlayerMoney, } from "#app/data/mystery-encounters/utils/encounter-phase-utils"; +import Pokemon, { PlayerPokemon } from "#app/field/pokemon"; +import { modifierTypes } from "#app/modifier/modifier-type"; +import { MysteryEncounterType } from "#enums/mystery-encounter-type"; +import { Species } from "#enums/species"; +import BattleScene from "#app/battle-scene"; +import IMysteryEncounter, { MysteryEncounterBuilder } from "../mystery-encounter"; +import { MysteryEncounterOptionBuilder } from "../mystery-encounter-option"; +import { HeldItemRequirement, MoneyRequirement } from "../mystery-encounter-requirements"; +import { getEncounterText } from "#app/data/mystery-encounters/utils/encounter-dialogue-utils"; +import { MysteryEncounterTier } from "#enums/mystery-encounter-tier"; +import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode"; +import { BerryModifier, PokemonBaseStatModifier, PokemonBaseStatTotalModifier, PokemonHeldItemModifier, PokemonInstantReviveModifier, TerastallizeModifier } from "#app/modifier/modifier"; +import { ModifierRewardPhase } from "#app/phases"; +import { OptionSelectItem } from "#app/ui/abstact-option-select-ui-handler"; + +/** the i18n namespace for this encounter */ +const namespace = "mysteryEncounter:delibirdy"; + +/** Berries only */ +const OPTION_2_ALLOWED_MODIFIERS = [BerryModifier.name, PokemonInstantReviveModifier.name]; + +/** Disallowed items are berries, Reviver Seeds, and Vitamins (form change items and fusion items are not PokemonHeldItemModifiers) */ +const OPTION_3_DISALLOWED_MODIFIERS = [ + BerryModifier.name, + PokemonInstantReviveModifier.name, + TerastallizeModifier.name, + PokemonBaseStatModifier.name, + PokemonBaseStatTotalModifier.name +]; + +/** + * Delibird-y encounter. + * @see {@link https://github.com/AsdarDevelops/PokeRogue-Events/issues/57 | GitHub Issue #57} + * @see For biome requirements check {@linkcode mysteryEncountersByBiome} + */ +export const DelibirdyEncounter: IMysteryEncounter = + MysteryEncounterBuilder.withEncounterType(MysteryEncounterType.DELIBIRDY) + .withEncounterTier(MysteryEncounterTier.GREAT) + .withSceneWaveRangeRequirement(10, 180) + .withSceneRequirement(new MoneyRequirement(0, 2.75)) // Must have enough money for it to spawn at the very least + .withIntroSpriteConfigs([ + { + spriteKey: Species.DELIBIRD.toString(), + fileRoot: "pokemon", + hasShadow: true, + repeat: true, + startFrame: 38, + scale: 0.94 + }, + { + spriteKey: Species.DELIBIRD.toString(), + fileRoot: "pokemon", + hasShadow: true, + repeat: true, + scale: 1.06 + }, + { + spriteKey: Species.DELIBIRD.toString(), + fileRoot: "pokemon", + hasShadow: true, + repeat: true, + startFrame: 65, + x: 1, + y: 5, + yShadow: 5 + }, + ]) + .withIntroDialogue([ + { + text: `${namespace}:intro`, + } + ]) + .withTitle(`${namespace}:title`) + .withDescription(`${namespace}:description`) + .withQuery(`${namespace}:query`) + .withOutroDialogue([ + { + text: `${namespace}:outro`, + } + ]) + .withOption( + new MysteryEncounterOptionBuilder() + .withOptionMode(MysteryEncounterOptionMode.DISABLED_OR_DEFAULT) + .withSceneMoneyRequirement(0, 2.75) + .withDialogue({ + buttonLabel: `${namespace}:option:1:label`, + buttonTooltip: `${namespace}:option:1:tooltip`, + selected: [ + { + text: `${namespace}:option:1:selected`, + }, + ], + }) + .withPreOptionPhase(async (scene: BattleScene): Promise => { + const encounter = scene.currentBattle.mysteryEncounter; + updatePlayerMoney(scene, -(encounter.options[0].requirements[0] as MoneyRequirement).requiredMoney, true, false); + return true; + }) + .withOptionPhase(async (scene: BattleScene) => { + // Give the player an Ability Charm + scene.unshiftPhase(new ModifierRewardPhase(scene, modifierTypes.ABILITY_CHARM)); + leaveEncounterWithoutBattle(scene, true); + }) + .build() + ) + .withOption( + new MysteryEncounterOptionBuilder() + .withOptionMode(MysteryEncounterOptionMode.DISABLED_OR_DEFAULT) + .withPrimaryPokemonRequirement(new HeldItemRequirement(OPTION_2_ALLOWED_MODIFIERS)) + .withDialogue({ + buttonLabel: `${namespace}:option:2:label`, + buttonTooltip: `${namespace}:option:2:tooltip`, + secondOptionPrompt: `${namespace}:option:2:select_prompt`, + selected: [ + { + text: `${namespace}:option:2:selected`, + }, + ], + }) + .withPreOptionPhase(async (scene: BattleScene): Promise => { + const encounter = scene.currentBattle.mysteryEncounter; + const onPokemonSelected = (pokemon: PlayerPokemon) => { + // Get Pokemon held items and filter for valid ones + const validItems = pokemon.getHeldItems().filter((it) => { + return OPTION_2_ALLOWED_MODIFIERS.some(heldItem => it.constructor.name === heldItem); + }); + + return validItems.map((modifier: PokemonHeldItemModifier) => { + const option: OptionSelectItem = { + label: modifier.type.name, + handler: () => { + // Pokemon and item selected + encounter.setDialogueToken("chosenItem", modifier.type.name); + encounter.misc = { + chosenPokemon: pokemon, + chosenModifier: modifier, + }; + return true; + }, + }; + return option; + }); + }; + + // Only Pokemon that can gain benefits are above 1/3rd HP with no status + const selectableFilter = (pokemon: Pokemon) => { + // If pokemon meets primary pokemon reqs, it can be selected + const meetsReqs = encounter.options[1].pokemonMeetsPrimaryRequirements(scene, pokemon); + if (!meetsReqs) { + return getEncounterText(scene, `${namespace}:invalid_selection`); + } + + return null; + }; + + return selectPokemonForOption(scene, onPokemonSelected, null, selectableFilter); + }) + .withOptionPhase(async (scene: BattleScene) => { + const encounter = scene.currentBattle.mysteryEncounter; + const modifier = encounter.misc.chosenModifier; + // Give the player a Candy Jar if they gave a Berry, and a Healing Charm for Reviver Seed + if (modifier.type.name.includes("Berry")) { + scene.unshiftPhase(new ModifierRewardPhase(scene, modifierTypes.CANDY_JAR)); + } else { + scene.unshiftPhase(new ModifierRewardPhase(scene, modifierTypes.HEALING_CHARM)); + } + + // Remove the modifier if its stacks go to 0 + modifier.stackCount -= 1; + if (modifier.stackCount === 0) { + scene.removeModifier(modifier); + } + + leaveEncounterWithoutBattle(scene, true); + }) + .build() + ) + .withOption( + new MysteryEncounterOptionBuilder() + .withOptionMode(MysteryEncounterOptionMode.DISABLED_OR_DEFAULT) + .withPrimaryPokemonRequirement(new HeldItemRequirement(OPTION_3_DISALLOWED_MODIFIERS, 1, true)) + .withDialogue({ + buttonLabel: `${namespace}:option:3:label`, + buttonTooltip: `${namespace}:option:3:tooltip`, + secondOptionPrompt: `${namespace}:option:3:select_prompt`, + selected: [ + { + text: `${namespace}:option:3:selected`, + }, + ], + }) + .withPreOptionPhase(async (scene: BattleScene): Promise => { + const encounter = scene.currentBattle.mysteryEncounter; + const onPokemonSelected = (pokemon: PlayerPokemon) => { + // Get Pokemon held items and filter for valid ones + const validItems = pokemon.getHeldItems().filter((it) => { + return !OPTION_3_DISALLOWED_MODIFIERS.some(heldItem => it.constructor.name === heldItem); + }); + + return validItems.map((modifier: PokemonHeldItemModifier) => { + const option: OptionSelectItem = { + label: modifier.type.name, + handler: () => { + // Pokemon and item selected + encounter.setDialogueToken("chosenItem", modifier.type.name); + encounter.misc = { + chosenPokemon: pokemon, + chosenModifier: modifier, + }; + return true; + }, + }; + return option; + }); + }; + + // Only Pokemon that can gain benefits are above 1/3rd HP with no status + const selectableFilter = (pokemon: Pokemon) => { + // If pokemon meets primary pokemon reqs, it can be selected + const meetsReqs = encounter.options[2].pokemonMeetsPrimaryRequirements(scene, pokemon); + if (!meetsReqs) { + return getEncounterText(scene, `${namespace}:invalid_selection`); + } + + return null; + }; + + return selectPokemonForOption(scene, onPokemonSelected, null, selectableFilter); + }) + .withOptionPhase(async (scene: BattleScene) => { + const encounter = scene.currentBattle.mysteryEncounter; + const modifier = encounter.misc.chosenModifier; + // Give the player a Berry Pouch + scene.unshiftPhase(new ModifierRewardPhase(scene, modifierTypes.BERRY_POUCH)); + + // Remove the modifier if its stacks go to 0 + modifier.stackCount -= 1; + if (modifier.stackCount === 0) { + scene.removeModifier(modifier); + } + + leaveEncounterWithoutBattle(scene, true); + }) + .build() + ) + .build(); diff --git a/src/data/mystery-encounters/encounters/fiery-fallout-encounter.ts b/src/data/mystery-encounters/encounters/fiery-fallout-encounter.ts index 478a604869b..2da9ade0ff0 100644 --- a/src/data/mystery-encounters/encounters/fiery-fallout-encounter.ts +++ b/src/data/mystery-encounters/encounters/fiery-fallout-encounter.ts @@ -80,7 +80,8 @@ export const FieryFalloutEncounter: IMysteryEncounter = repeat: true, hidden: true, hasShadow: true, - x: -20 + x: -20, + startFrame: 20 }, { spriteKey: volcaronaSpecies.getSpriteId(true ), diff --git a/src/data/mystery-encounters/mystery-encounter-option.ts b/src/data/mystery-encounters/mystery-encounter-option.ts index e2736327ddb..6e0e2b83de1 100644 --- a/src/data/mystery-encounters/mystery-encounter-option.ts +++ b/src/data/mystery-encounters/mystery-encounter-option.ts @@ -1,6 +1,6 @@ import { OptionTextDisplay } from "#app/data/mystery-encounters/mystery-encounter-dialogue"; import { Moves } from "#app/enums/moves"; -import { PlayerPokemon } from "#app/field/pokemon"; +import Pokemon, { PlayerPokemon } from "#app/field/pokemon"; import BattleScene from "#app/battle-scene"; import * as Utils from "#app/utils"; import { Type } from "../type"; @@ -57,6 +57,10 @@ export default class MysteryEncounterOption implements MysteryEncounterOption { this.meetsPrimaryRequirementAndPrimaryPokemonSelected(scene); } + pokemonMeetsPrimaryRequirements?(scene: BattleScene, pokemon: Pokemon) { + return !this.primaryPokemonRequirements.some(req => !req.queryParty(scene.getParty()).map(p => p.id).includes(pokemon.id)); + } + meetsPrimaryRequirementAndPrimaryPokemonSelected?(scene: BattleScene) { if (!this.primaryPokemonRequirements) { return true; diff --git a/src/data/mystery-encounters/mystery-encounter-requirements.ts b/src/data/mystery-encounters/mystery-encounter-requirements.ts index 863ed9afc30..b2b0cee5f38 100644 --- a/src/data/mystery-encounters/mystery-encounter-requirements.ts +++ b/src/data/mystery-encounters/mystery-encounter-requirements.ts @@ -1,5 +1,5 @@ import { PlayerPokemon } from "#app/field/pokemon"; -import { ModifierType, PokemonHeldItemModifierType } from "#app/modifier/modifier-type"; +import { ModifierType } from "#app/modifier/modifier-type"; import BattleScene from "#app/battle-scene"; import { isNullOrUndefined } from "#app/utils"; import { Abilities } from "#enums/abilities"; @@ -744,20 +744,20 @@ export class CanEvolveWithItemRequirement extends EncounterPokemonRequirement { } export class HeldItemRequirement extends EncounterPokemonRequirement { - requiredHeldItemModifier: PokemonHeldItemModifierType[]; + requiredHeldItemModifiers: string[]; minNumberOfPokemon: number; invertQuery: boolean; - constructor(heldItem: PokemonHeldItemModifierType | PokemonHeldItemModifierType[], minNumberOfPokemon: number = 1, invertQuery: boolean = false) { + constructor(heldItem: string | string[], minNumberOfPokemon: number = 1, invertQuery: boolean = false) { super(); this.minNumberOfPokemon = minNumberOfPokemon; this.invertQuery = invertQuery; - this.requiredHeldItemModifier = Array.isArray(heldItem) ? heldItem : [heldItem]; + this.requiredHeldItemModifiers = Array.isArray(heldItem) ? heldItem : [heldItem]; } meetsRequirement(scene: BattleScene): boolean { const partyPokemon = scene.getParty(); - if (isNullOrUndefined(partyPokemon) || this?.requiredHeldItemModifier?.length < 0) { + if (isNullOrUndefined(partyPokemon) || this?.requiredHeldItemModifiers?.length < 0) { return false; } return this.queryParty(partyPokemon).length >= this.minNumberOfPokemon; @@ -765,19 +765,26 @@ export class HeldItemRequirement extends EncounterPokemonRequirement { queryParty(partyPokemon: PlayerPokemon[]): PlayerPokemon[] { if (!this.invertQuery) { - return partyPokemon.filter((pokemon) => this.requiredHeldItemModifier.filter((heldItem) => pokemon.getHeldItems().filter((it) => it.type.id === heldItem.id).length > 0).length > 0); + return partyPokemon.filter((pokemon) => this.requiredHeldItemModifiers.some((heldItem) => { + return pokemon.getHeldItems().some((it) => { + return it.constructor.name === heldItem; + }); + })); } else { - // for an inverted query, we only want to get the pokemon that don't have ANY of the listed heldItems - return partyPokemon.filter((pokemon) => this.requiredHeldItemModifier.filter((heldItem) => pokemon.getHeldItems().filter((it) => it.type.id === heldItem.id).length === 0).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.filter((pokemon) => pokemon.getHeldItems().filter((it) => { + return !this.requiredHeldItemModifiers.some(heldItem => it.constructor.name === heldItem); + }).length > 0); } } getDialogueToken(scene: BattleScene, pokemon?: PlayerPokemon): [string, string] { - const requiredItems = this.requiredHeldItemModifier.filter((a) => { - pokemon.getHeldItems().filter((it) => it.type.id === a.id).length > 0; + const requiredItems = pokemon.getHeldItems().filter((it) => { + return this.requiredHeldItemModifiers.some(heldItem => it.constructor.name === heldItem); }); if (requiredItems.length > 0) { - return ["heldItem", requiredItems[0].name]; + return ["heldItem", requiredItems[0].type.name]; } return null; } diff --git a/src/data/mystery-encounters/mystery-encounters.ts b/src/data/mystery-encounters/mystery-encounters.ts index dacce43919e..1f7ad880fa1 100644 --- a/src/data/mystery-encounters/mystery-encounters.ts +++ b/src/data/mystery-encounters/mystery-encounters.ts @@ -16,6 +16,7 @@ import { FieryFalloutEncounter } from "#app/data/mystery-encounters/encounters/f import { TheStrongStuffEncounter } from "#app/data/mystery-encounters/encounters/the-strong-stuff-encounter"; import { PokemonSalesmanEncounter } from "#app/data/mystery-encounters/encounters/pokemon-salesman-encounter"; import { OfferYouCantRefuseEncounter } from "#app/data/mystery-encounters/encounters/offer-you-cant-refuse-encounter"; +import { DelibirdyEncounter } from "#app/data/mystery-encounters/encounters/delibirdy-encounter"; // Spawn chance: (BASE_MYSTERY_ENCOUNTER_SPAWN_WEIGHT + WIGHT_INCREMENT_ON_SPAWN_MISS * ) / 256 export const BASE_MYSTERY_ENCOUNTER_SPAWN_WEIGHT = 1; @@ -150,7 +151,8 @@ const anyBiomeEncounters: MysteryEncounterType[] = [ MysteryEncounterType.FIGHT_OR_FLIGHT, MysteryEncounterType.DARK_DEAL, MysteryEncounterType.MYSTERIOUS_CHEST, - MysteryEncounterType.TRAINING_SESSION + MysteryEncounterType.TRAINING_SESSION, + MysteryEncounterType.DELIBIRDY, ]; /** @@ -163,15 +165,20 @@ const anyBiomeEncounters: MysteryEncounterType[] = [ export const mysteryEncountersByBiome = new Map([ [Biome.TOWN, []], [Biome.PLAINS, [ - MysteryEncounterType.SLUMBERING_SNORLAX + MysteryEncounterType.SLUMBERING_SNORLAX, + MysteryEncounterType.ABSOLUTE_AVARICE ]], [Biome.GRASS, [ MysteryEncounterType.SLUMBERING_SNORLAX, + MysteryEncounterType.ABSOLUTE_AVARICE + ]], + [Biome.TALL_GRASS, [ + MysteryEncounterType.ABSOLUTE_AVARICE ]], - [Biome.TALL_GRASS, []], [Biome.METROPOLIS, []], [Biome.FOREST, [ - MysteryEncounterType.SAFARI_ZONE + MysteryEncounterType.SAFARI_ZONE, + MysteryEncounterType.ABSOLUTE_AVARICE ]], [Biome.SEA, [ @@ -230,6 +237,8 @@ export function initMysteryEncounters() { allMysteryEncounters[MysteryEncounterType.THE_STRONG_STUFF] = TheStrongStuffEncounter; allMysteryEncounters[MysteryEncounterType.POKEMON_SALESMAN] = PokemonSalesmanEncounter; allMysteryEncounters[MysteryEncounterType.OFFER_YOU_CANT_REFUSE] = OfferYouCantRefuseEncounter; + allMysteryEncounters[MysteryEncounterType.DELIBIRDY] = DelibirdyEncounter; + // allMysteryEncounters[MysteryEncounterType.ABSOLUTE_AVARICE] = Abs; // Add extreme encounters to biome map extremeBiomeEncounters.forEach(encounter => { diff --git a/src/enums/mystery-encounter-type.ts b/src/enums/mystery-encounter-type.ts index 8ec7d4967d3..95283e63b36 100644 --- a/src/enums/mystery-encounter-type.ts +++ b/src/enums/mystery-encounter-type.ts @@ -13,5 +13,7 @@ export enum MysteryEncounterType { FIERY_FALLOUT, THE_STRONG_STUFF, POKEMON_SALESMAN, - OFFER_YOU_CANT_REFUSE + OFFER_YOU_CANT_REFUSE, + DELIBIRDY, + ABSOLUTE_AVARICE } diff --git a/src/field/mystery-encounter-intro.ts b/src/field/mystery-encounter-intro.ts index d97c267b095..59cdb90fc30 100644 --- a/src/field/mystery-encounter-intro.ts +++ b/src/field/mystery-encounter-intro.ts @@ -39,6 +39,8 @@ export class MysteryEncounterSpriteConfig { disableAnimation?: boolean = false; /** Repeat the animation. Defaults to `false` */ repeat?: boolean = false; + /** What frame of the animation to start on. Defaults to 0 */ + startFrame?: number = 0; /** Hidden at start of encounter. Defaults to `false` */ hidden?: boolean = false; /** Tint color. `0` - `1`. Higher means darker tint. */ @@ -279,7 +281,7 @@ export default class MysteryEncounterIntroVisuals extends Phaser.GameObjects.Con const trainerAnimConfig = { key: config.spriteKey, repeat: config?.repeat ? -1 : 0, - startFrame: 0 + startFrame: config?.startFrame ?? 0 }; this.tryPlaySprite(sprites[i], tintSprites[i], trainerAnimConfig); diff --git a/src/locales/en/mystery-encounter.ts b/src/locales/en/mystery-encounter.ts index 2454859884b..59931894e6a 100644 --- a/src/locales/en/mystery-encounter.ts +++ b/src/locales/en/mystery-encounter.ts @@ -13,6 +13,7 @@ import { trainingSessionDialogue } from "#app/locales/en/mystery-encounters/trai import { theStrongStuffDialogue } from "#app/locales/en/mystery-encounters/the-strong-stuff-dialogue"; import { pokemonSalesmanDialogue } from "#app/locales/en/mystery-encounters/pokemon-salesman-dialogue"; import { offerYouCantRefuseDialogue } from "#app/locales/en/mystery-encounters/offer-you-cant-refuse-dialogue"; +import { delibirdyDialogue } from "#app/locales/en/mystery-encounters/delibirdy-dialogue"; /** * Patterns that can be used: @@ -50,5 +51,6 @@ export const mysteryEncounter = { fieryFallout: fieryFalloutDialogue, theStrongStuff: theStrongStuffDialogue, pokemonSalesman: pokemonSalesmanDialogue, - offerYouCantRefuse: offerYouCantRefuseDialogue + offerYouCantRefuse: offerYouCantRefuseDialogue, + delibirdy: delibirdyDialogue, } as const; diff --git a/src/locales/en/mystery-encounters/delibirdy-dialogue.ts b/src/locales/en/mystery-encounters/delibirdy-dialogue.ts new file mode 100644 index 00000000000..a77261573a3 --- /dev/null +++ b/src/locales/en/mystery-encounters/delibirdy-dialogue.ts @@ -0,0 +1,31 @@ +export const delibirdyDialogue = { + intro: "A pack of Delibird have appeared!", + title: "Delibird-y", + description: "The Delibirds are looking at you expectantly, as if they want something. Perhaps giving them an item or some money would satisfy them?", + query: "What will you give them?", + invalid_selection: "Pokémon doesn't have that kind of item.", + option: { + 1: { + label: "Give Money", + tooltip: "(-) Give the Delibirds {{money, money}}\n(+) Receive a Gift Item", + selected: `You toss the money to the Delibirds,\nwho chatter amongst themselves excitedly. + $They turn back to you and happily give you a present!`, + }, + 2: { + label: "Give Food", + tooltip: "(-) Give the Delibirds a Berry or Reviver Seed\n(+) Receive a Gift Item", + select_prompt: "Select an item to give.", + selected: `You toss the {{chosenItem}} to the Delibirds,\nwho chatter amongst themselves excitedly. + $They turn back to you and happily give you a present!`, + }, + 3: { + label: "Give an Item", + tooltip: "(-) Give the Delibirds a Held Item\n(+) Receive a Gift Item", + select_prompt: "Select an item to give.", + selected: `You toss the {{chosenItem}} to the Delibirds,\nwho chatter amongst themselves excitedly. + $They turn back to you and happily give you a present!`, + }, + }, + outro: `The Delibird pack happily waddles off into the distance. + $What a curious little exchange!` +}; diff --git a/src/locales/en/mystery-encounters/shady-vitamin-dealer-dialogue.ts b/src/locales/en/mystery-encounters/shady-vitamin-dealer-dialogue.ts index 1f2d62751ea..5570cc9b7ad 100644 --- a/src/locales/en/mystery-encounters/shady-vitamin-dealer-dialogue.ts +++ b/src/locales/en/mystery-encounters/shady-vitamin-dealer-dialogue.ts @@ -12,14 +12,10 @@ export const shadyVitaminDealerDialogue = { 1: { label: "The Cheap Deal", tooltip: "(-) Pay {{option1Money, money}}\n(-) Side Effects?\n(+) Chosen Pokémon Gains 2 Random Vitamins", - selected: `{{option1PrimaryName}} swims ahead, guiding you back on track. - \${{option1PrimaryName}} seems to also have gotten stronger in this time of need!`, }, 2: { label: "The Pricey Deal", tooltip: "(-) Pay {{option2Money, money}}\n(-) Side Effects?\n(+) Chosen Pokémon Gains 2 Random Vitamins", - selected: `{{option2PrimaryName}} flies ahead of your boat, guiding you back on track. - \${{option2PrimaryName}} seems to also have gotten stronger in this time of need!`, }, 3: { label: "Leave", diff --git a/src/test/mystery-encounter/encounterTestUtils.ts b/src/test/mystery-encounter/encounterTestUtils.ts index 57b636ac793..aa0551e78e2 100644 --- a/src/test/mystery-encounter/encounterTestUtils.ts +++ b/src/test/mystery-encounter/encounterTestUtils.ts @@ -6,15 +6,21 @@ import { Mode } from "#app/ui/ui"; import GameManager from "../utils/gameManager"; import MessageUiHandler from "#app/ui/message-ui-handler"; import { Status, StatusEffect } from "#app/data/status-effect"; +import { expect, vi } from "vitest"; +import * as EncounterPhaseUtils from "#app/data/mystery-encounters/utils/encounter-phase-utils"; +import PartyUiHandler from "#app/ui/party-ui-handler"; +import OptionSelectUiHandler from "#app/ui/settings/option-select-ui-handler"; /** * Runs a MysteryEncounter to either the start of a battle, or to the MysteryEncounterRewardsPhase, depending on the option selected * @param game * @param optionNo - human number, not index + * @param secondaryOptionSelect - * @param isBattle - if selecting option should lead to battle, set to true */ -export async function runMysteryEncounterToEnd(game: GameManager, optionNo: number, isBattle: boolean = false) { - await runSelectMysteryEncounterOption(game, optionNo, isBattle); +export async function runMysteryEncounterToEnd(game: GameManager, optionNo: number, secondaryOptionSelect: { pokemonNo: number, optionNo: number } = null, isBattle: boolean = false) { + vi.spyOn(EncounterPhaseUtils, "selectPokemonForOption"); + await runSelectMysteryEncounterOption(game, optionNo, secondaryOptionSelect); // run the selected options phase game.onNextPrompt("MysteryEncounterOptionSelectedPhase", Mode.MESSAGE, () => { @@ -49,7 +55,7 @@ export async function runMysteryEncounterToEnd(game: GameManager, optionNo: numb } } -export async function runSelectMysteryEncounterOption(game: GameManager, optionNo: number, isBattle: boolean = false) { +export async function runSelectMysteryEncounterOption(game: GameManager, optionNo: number, secondaryOptionSelect: { pokemonNo: number, optionNo: number } = null) { // Handle any eventual queued messages (e.g. weather phase, etc.) game.onNextPrompt("MessagePhase", Mode.MESSAGE, () => { const uiHandler = game.scene.ui.getHandler(); @@ -73,6 +79,7 @@ export async function runSelectMysteryEncounterOption(game: GameManager, optionN uiHandler.unblockInput(); // input are blocked by 1s to prevent accidental input. Tests need to handle that switch (optionNo) { + default: case 1: // no movement needed. Default cursor position break; @@ -89,6 +96,42 @@ export async function runSelectMysteryEncounterOption(game: GameManager, optionN } uiHandler.processInput(Button.ACTION); + + if (!isNaN(secondaryOptionSelect?.pokemonNo)) { + await handleSecondaryOptionSelect(game, secondaryOptionSelect.pokemonNo, secondaryOptionSelect.optionNo); + } +} + +async function handleSecondaryOptionSelect(game: GameManager, pokemonNo: number, optionNo: number) { + // Handle secondary option selections + const partyUiHandler = game.scene.ui.handlers[Mode.PARTY] as PartyUiHandler; + vi.spyOn(partyUiHandler, "show"); + await vi.waitFor(() => expect(partyUiHandler.show).toHaveBeenCalled()); + + for (let i = 1; i < pokemonNo; i++) { + partyUiHandler.processInput(Button.DOWN); + } + + // Open options on Pokemon + partyUiHandler.processInput(Button.ACTION); + // Click "Select" on Pokemon options + partyUiHandler.processInput(Button.ACTION); + + // If there is a second choice to make after selecting a Pokemon + if (!isNaN(optionNo)) { + // Wait for Summary menu to close and second options to spawn + const secondOptionUiHandler = game.scene.ui.handlers[Mode.OPTION_SELECT] as OptionSelectUiHandler; + vi.spyOn(secondOptionUiHandler, "show"); + await vi.waitFor(() => expect(secondOptionUiHandler.show).toHaveBeenCalled()); + + // Navigate down to the correct option + for (let i = 1; i < optionNo; i++) { + secondOptionUiHandler.processInput(Button.DOWN); + } + + // Select the option + secondOptionUiHandler.processInput(Button.ACTION); + } } /** diff --git a/src/test/mystery-encounter/encounters/delibirdy-encounter.test.ts b/src/test/mystery-encounter/encounters/delibirdy-encounter.test.ts new file mode 100644 index 00000000000..ee6ce0f6705 --- /dev/null +++ b/src/test/mystery-encounter/encounters/delibirdy-encounter.test.ts @@ -0,0 +1,373 @@ +import { Biome } from "#app/enums/biome"; +import { MysteryEncounterType } from "#app/enums/mystery-encounter-type"; +import { Species } from "#app/enums/species"; +import GameManager from "#app/test/utils/gameManager"; +import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; +import * as EncounterPhaseUtils from "#app/data/mystery-encounters/utils/encounter-phase-utils"; +import { runMysteryEncounterToEnd, runSelectMysteryEncounterOption } from "#test/mystery-encounter/encounterTestUtils"; +import BattleScene from "#app/battle-scene"; +import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode"; +import { MysteryEncounterTier } from "#enums/mystery-encounter-tier"; +import { DelibirdyEncounter } from "#app/data/mystery-encounters/encounters/delibirdy-encounter"; +import * as MysteryEncounters from "#app/data/mystery-encounters/mystery-encounters"; +import { MoneyRequirement } from "#app/data/mystery-encounters/mystery-encounter-requirements"; +import { BerryModifier, HealingBoosterModifier, HiddenAbilityRateBoosterModifier, LevelIncrementBoosterModifier, PokemonInstantReviveModifier, PokemonNatureWeightModifier, PreserveBerryModifier } from "#app/modifier/modifier"; +import { MysteryEncounterPhase } from "#app/phases/mystery-encounter-phases"; +import { generateModifierTypeOption } from "#app/data/mystery-encounters/utils/encounter-phase-utils"; +import { modifierTypes } from "#app/modifier/modifier-type"; +import { BerryType } from "#enums/berry-type"; + +const namespace = "mysteryEncounter:delibirdy"; +const defaultParty = [Species.LAPRAS, Species.GENGAR, Species.ABRA]; +const defaultBiome = Biome.CAVE; +const defaultWave = 45; + +describe("Delibird-y - Mystery Encounter", () => { + let phaserGame: Phaser.Game; + let game: GameManager; + let scene: BattleScene; + + beforeAll(() => { + phaserGame = new Phaser.Game({ type: Phaser.HEADLESS }); + }); + + beforeEach(async () => { + game = new GameManager(phaserGame); + scene = game.scene; + game.override.mysteryEncounterChance(100); + game.override.mysteryEncounterTier(MysteryEncounterTier.COMMON); + game.override.startingWave(defaultWave); + game.override.startingBiome(defaultBiome); + + vi.spyOn(MysteryEncounters, "mysteryEncountersByBiome", "get").mockReturnValue( + new Map([ + [Biome.CAVE, [MysteryEncounterType.DELIBIRDY]], + ]) + ); + }); + + afterEach(() => { + game.phaseInterceptor.restoreOg(); + vi.clearAllMocks(); + vi.resetAllMocks(); + }); + + it("should have the correct properties", async () => { + await game.runToMysteryEncounter(MysteryEncounterType.DELIBIRDY, defaultParty); + + expect(DelibirdyEncounter.encounterType).toBe(MysteryEncounterType.DELIBIRDY); + expect(DelibirdyEncounter.encounterTier).toBe(MysteryEncounterTier.GREAT); + expect(DelibirdyEncounter.dialogue).toBeDefined(); + expect(DelibirdyEncounter.dialogue.intro).toStrictEqual([{ text: `${namespace}:intro` }]); + expect(DelibirdyEncounter.dialogue.outro).toStrictEqual([{ text: `${namespace}:outro` }]); + expect(DelibirdyEncounter.dialogue.encounterOptionsDialogue.title).toBe(`${namespace}:title`); + expect(DelibirdyEncounter.dialogue.encounterOptionsDialogue.description).toBe(`${namespace}:description`); + expect(DelibirdyEncounter.dialogue.encounterOptionsDialogue.query).toBe(`${namespace}:query`); + expect(DelibirdyEncounter.options.length).toBe(3); + }); + + it("should not run below wave 10", async () => { + game.override.startingWave(9); + + await game.runToMysteryEncounter(); + + expect(scene.currentBattle?.mysteryEncounter?.encounterType).not.toBe(MysteryEncounterType.DELIBIRDY); + }); + + it("should not run above wave 179", async () => { + game.override.startingWave(181); + + await game.runToMysteryEncounter(); + + expect(scene.currentBattle.mysteryEncounter).toBeUndefined(); + }); + + it("should not spawn if player does not have enough money", async () => { + scene.money = 0; + + await game.runToMysteryEncounter(); + + expect(scene.currentBattle?.mysteryEncounter?.encounterType).not.toBe(MysteryEncounterType.DELIBIRDY); + }); + + describe("Option 1 - Give them money", () => { + it("should have the correct properties", () => { + const option1 = DelibirdyEncounter.options[0]; + expect(option1.optionMode).toBe(MysteryEncounterOptionMode.DISABLED_OR_DEFAULT); + expect(option1.dialogue).toBeDefined(); + expect(option1.dialogue).toStrictEqual({ + buttonLabel: `${namespace}:option:1:label`, + buttonTooltip: `${namespace}:option:1:tooltip`, + selected: [ + { + text: `${namespace}:option:1:selected`, + }, + ], + }); + }); + + it("Should update the player's money properly", async () => { + const initialMoney = 20000; + scene.money = initialMoney; + const updateMoneySpy = vi.spyOn(EncounterPhaseUtils, "updatePlayerMoney"); + + await game.runToMysteryEncounter(MysteryEncounterType.DELIBIRDY, defaultParty); + await runMysteryEncounterToEnd(game, 1); + + const price = (scene.currentBattle.mysteryEncounter.options[0].requirements[0] as MoneyRequirement).requiredMoney; + + expect(updateMoneySpy).toHaveBeenCalledWith(scene, -price, true, false); + expect(scene.money).toBe(initialMoney - price); + }); + + it("Should give the player a Hidden Ability Charm", async () => { + scene.money = 200000; + await game.runToMysteryEncounter(MysteryEncounterType.DELIBIRDY, defaultParty); + await runMysteryEncounterToEnd(game, 1); + + const itemModifier = scene.findModifier(m => m instanceof HiddenAbilityRateBoosterModifier) as HiddenAbilityRateBoosterModifier; + + expect(itemModifier).toBeDefined(); + expect(itemModifier.stackCount).toBe(1); + }); + + it("should be disabled if player does not have enough money", async () => { + scene.money = 0; + await game.runToMysteryEncounter(MysteryEncounterType.DELIBIRDY, defaultParty); + await game.phaseInterceptor.to(MysteryEncounterPhase, false); + + const encounterPhase = scene.getCurrentPhase(); + expect(encounterPhase.constructor.name).toBe(MysteryEncounterPhase.name); + const mysteryEncounterPhase = encounterPhase as MysteryEncounterPhase; + vi.spyOn(mysteryEncounterPhase, "continueEncounter"); + vi.spyOn(mysteryEncounterPhase, "handleOptionSelect"); + vi.spyOn(scene.ui, "playError"); + + await runSelectMysteryEncounterOption(game, 1); + + expect(scene.getCurrentPhase().constructor.name).toBe(MysteryEncounterPhase.name); + expect(scene.ui.playError).not.toHaveBeenCalled(); // No error sfx, option is disabled + expect(mysteryEncounterPhase.handleOptionSelect).not.toHaveBeenCalled(); + expect(mysteryEncounterPhase.continueEncounter).not.toHaveBeenCalled(); + }); + + it("should leave encounter without battle", async () => { + scene.money = 200000; + const leaveEncounterWithoutBattleSpy = vi.spyOn(EncounterPhaseUtils, "leaveEncounterWithoutBattle"); + + await game.runToMysteryEncounter(MysteryEncounterType.DELIBIRDY, defaultParty); + await runMysteryEncounterToEnd(game, 1); + + expect(leaveEncounterWithoutBattleSpy).toBeCalled(); + }); + }); + + describe("Option 2 - Give Food", () => { + it("should have the correct properties", () => { + const option = DelibirdyEncounter.options[1]; + expect(option.optionMode).toBe(MysteryEncounterOptionMode.DISABLED_OR_DEFAULT); + expect(option.dialogue).toBeDefined(); + expect(option.dialogue).toStrictEqual({ + buttonLabel: `${namespace}:option:2:label`, + buttonTooltip: `${namespace}:option:2:tooltip`, + secondOptionPrompt: `${namespace}:option:2:select_prompt`, + selected: [ + { + text: `${namespace}:option:2:selected`, + }, + ], + }); + }); + + it("Should decrease Berry stacks and give the player a Candy Jar", async () => { + await game.runToMysteryEncounter(MysteryEncounterType.DELIBIRDY, defaultParty); + + // Set 2 Sitrus berries on party lead + scene.modifiers = []; + const sitrus = generateModifierTypeOption(scene, modifierTypes.BERRY, [BerryType.SITRUS]).type; + const sitrusMod = sitrus.newModifier(scene.getParty()[0]) as BerryModifier; + sitrusMod.stackCount = 2; + await scene.addModifier(sitrusMod, true, false, false, true); + await scene.updateModifiers(true); + + await runMysteryEncounterToEnd(game, 2, { pokemonNo: 1, optionNo: 1}); + + const sitrusAfter = scene.findModifier(m => m instanceof BerryModifier); + const candyJarAfter = scene.findModifier(m => m instanceof LevelIncrementBoosterModifier); + + expect(sitrusAfter.stackCount).toBe(1); + expect(candyJarAfter).toBeDefined(); + expect(candyJarAfter.stackCount).toBe(1); + }); + + it("Should remove Reviver Seed and give the player a Healing Charm", async () => { + await game.runToMysteryEncounter(MysteryEncounterType.DELIBIRDY, defaultParty); + + // Set 1 Reviver Seed on party lead + scene.modifiers = []; + const revSeed = generateModifierTypeOption(scene, modifierTypes.REVIVER_SEED).type; + const modifier = revSeed.newModifier(scene.getParty()[0]) as PokemonInstantReviveModifier; + modifier.stackCount = 1; + await scene.addModifier(modifier, true, false, false, true); + await scene.updateModifiers(true); + + await runMysteryEncounterToEnd(game, 2, { pokemonNo: 1, optionNo: 1}); + + const reviverSeedAfter = scene.findModifier(m => m instanceof PokemonInstantReviveModifier); + const healingCharmAfter = scene.findModifier(m => m instanceof HealingBoosterModifier); + + expect(reviverSeedAfter).toBeUndefined(); + expect(healingCharmAfter).toBeDefined(); + expect(healingCharmAfter.stackCount).toBe(1); + }); + + it("should be disabled if player does not have any proper items", async () => { + await game.runToMysteryEncounter(MysteryEncounterType.DELIBIRDY, defaultParty); + + // Set 1 Soul Dew on party lead + scene.modifiers = []; + const soulDew = generateModifierTypeOption(scene, modifierTypes.SOUL_DEW).type; + const modifier = soulDew.newModifier(scene.getParty()[0]); + await scene.addModifier(modifier, true, false, false, true); + await scene.updateModifiers(true); + + await game.phaseInterceptor.to(MysteryEncounterPhase, false); + + const encounterPhase = scene.getCurrentPhase(); + expect(encounterPhase.constructor.name).toBe(MysteryEncounterPhase.name); + const mysteryEncounterPhase = encounterPhase as MysteryEncounterPhase; + vi.spyOn(mysteryEncounterPhase, "continueEncounter"); + vi.spyOn(mysteryEncounterPhase, "handleOptionSelect"); + vi.spyOn(scene.ui, "playError"); + + await runSelectMysteryEncounterOption(game, 2); + + expect(scene.getCurrentPhase().constructor.name).toBe(MysteryEncounterPhase.name); + expect(scene.ui.playError).not.toHaveBeenCalled(); // No error sfx, option is disabled + expect(mysteryEncounterPhase.handleOptionSelect).not.toHaveBeenCalled(); + expect(mysteryEncounterPhase.continueEncounter).not.toHaveBeenCalled(); + }); + + it("should leave encounter without battle", async () => { + const leaveEncounterWithoutBattleSpy = vi.spyOn(EncounterPhaseUtils, "leaveEncounterWithoutBattle"); + + await game.runToMysteryEncounter(MysteryEncounterType.DELIBIRDY, defaultParty); + + // Set 1 Reviver Seed on party lead + const revSeed = generateModifierTypeOption(scene, modifierTypes.REVIVER_SEED).type; + const modifier = revSeed.newModifier(scene.getParty()[0]) as PokemonInstantReviveModifier; + modifier.stackCount = 1; + await scene.addModifier(modifier, true, false, false, true); + await scene.updateModifiers(true); + + await runMysteryEncounterToEnd(game, 2, { pokemonNo: 1, optionNo: 1}); + + expect(leaveEncounterWithoutBattleSpy).toBeCalled(); + }); + }); + + describe("Option 3 - Give Item", () => { + it("should have the correct properties", () => { + const option = DelibirdyEncounter.options[2]; + expect(option.optionMode).toBe(MysteryEncounterOptionMode.DISABLED_OR_DEFAULT); + expect(option.dialogue).toBeDefined(); + expect(option.dialogue).toStrictEqual({ + buttonLabel: `${namespace}:option:3:label`, + buttonTooltip: `${namespace}:option:3:tooltip`, + secondOptionPrompt: `${namespace}:option:3:select_prompt`, + selected: [ + { + text: `${namespace}:option:3:selected`, + }, + ], + }); + }); + + it("Should decrease held item stacks and give the player a Berry Pouch", async () => { + await game.runToMysteryEncounter(MysteryEncounterType.DELIBIRDY, defaultParty); + + // Set 2 Soul Dew on party lead + scene.modifiers = []; + const soulDew = generateModifierTypeOption(scene, modifierTypes.SOUL_DEW).type; + const modifier = soulDew.newModifier(scene.getParty()[0]) as PokemonNatureWeightModifier; + modifier.stackCount = 2; + await scene.addModifier(modifier, true, false, false, true); + await scene.updateModifiers(true); + + await runMysteryEncounterToEnd(game, 3, { pokemonNo: 1, optionNo: 1}); + + const soulDewAfter = scene.findModifier(m => m instanceof PokemonNatureWeightModifier); + const berryPouchAfter = scene.findModifier(m => m instanceof PreserveBerryModifier); + + expect(soulDewAfter.stackCount).toBe(1); + expect(berryPouchAfter).toBeDefined(); + expect(berryPouchAfter.stackCount).toBe(1); + }); + + it("Should remove held item and give the player a Berry Pouch", async () => { + await game.runToMysteryEncounter(MysteryEncounterType.DELIBIRDY, defaultParty); + + // Set 1 Soul Dew on party lead + scene.modifiers = []; + const soulDew = generateModifierTypeOption(scene, modifierTypes.SOUL_DEW).type; + const modifier = soulDew.newModifier(scene.getParty()[0]) as PokemonNatureWeightModifier; + modifier.stackCount = 1; + await scene.addModifier(modifier, true, false, false, true); + await scene.updateModifiers(true); + + await runMysteryEncounterToEnd(game, 3, { pokemonNo: 1, optionNo: 1}); + + const soulDewAfter = scene.findModifier(m => m instanceof PokemonNatureWeightModifier); + const berryPouchAfter = scene.findModifier(m => m instanceof PreserveBerryModifier); + + expect(soulDewAfter).toBeUndefined(); + expect(berryPouchAfter).toBeDefined(); + expect(berryPouchAfter.stackCount).toBe(1); + }); + + it("should be disabled if player does not have any proper items", async () => { + await game.runToMysteryEncounter(MysteryEncounterType.DELIBIRDY, defaultParty); + + // Set 1 Reviver Seed on party lead + scene.modifiers = []; + const revSeed = generateModifierTypeOption(scene, modifierTypes.REVIVER_SEED).type; + const modifier = revSeed.newModifier(scene.getParty()[0]); + await scene.addModifier(modifier, true, false, false, true); + await scene.updateModifiers(true); + + await game.phaseInterceptor.to(MysteryEncounterPhase, false); + + const encounterPhase = scene.getCurrentPhase(); + expect(encounterPhase.constructor.name).toBe(MysteryEncounterPhase.name); + const mysteryEncounterPhase = encounterPhase as MysteryEncounterPhase; + vi.spyOn(mysteryEncounterPhase, "continueEncounter"); + vi.spyOn(mysteryEncounterPhase, "handleOptionSelect"); + vi.spyOn(scene.ui, "playError"); + + await runSelectMysteryEncounterOption(game, 3); + + expect(scene.getCurrentPhase().constructor.name).toBe(MysteryEncounterPhase.name); + expect(scene.ui.playError).not.toHaveBeenCalled(); // No error sfx, option is disabled + expect(mysteryEncounterPhase.handleOptionSelect).not.toHaveBeenCalled(); + expect(mysteryEncounterPhase.continueEncounter).not.toHaveBeenCalled(); + }); + + it("should leave encounter without battle", async () => { + const leaveEncounterWithoutBattleSpy = vi.spyOn(EncounterPhaseUtils, "leaveEncounterWithoutBattle"); + + await game.runToMysteryEncounter(MysteryEncounterType.DELIBIRDY, defaultParty); + + // Set 1 Soul Dew on party lead + scene.modifiers = []; + const soulDew = generateModifierTypeOption(scene, modifierTypes.SOUL_DEW).type; + const modifier = soulDew.newModifier(scene.getParty()[0]) as PokemonNatureWeightModifier; + modifier.stackCount = 1; + await scene.addModifier(modifier, true, false, false, true); + await scene.updateModifiers(true); + + await runMysteryEncounterToEnd(game, 3, { pokemonNo: 1, optionNo: 1}); + + expect(leaveEncounterWithoutBattleSpy).toBeCalled(); + }); + }); +}); diff --git a/src/test/mystery-encounter/encounters/fiery-fallout-encounter.test.ts b/src/test/mystery-encounter/encounters/fiery-fallout-encounter.test.ts index 076159dcd19..43f23b261e6 100644 --- a/src/test/mystery-encounter/encounters/fiery-fallout-encounter.test.ts +++ b/src/test/mystery-encounter/encounters/fiery-fallout-encounter.test.ts @@ -152,7 +152,7 @@ describe("Fiery Fallout - Mystery Encounter", () => { const phaseSpy = vi.spyOn(scene, "pushPhase"); await game.runToMysteryEncounter(MysteryEncounterType.FIERY_FALLOUT, defaultParty); - await runMysteryEncounterToEnd(game, 1, true); + await runMysteryEncounterToEnd(game, 1, null, true); const enemyField = scene.getEnemyField(); expect(scene.getCurrentPhase().constructor.name).toBe(CommandPhase.name); @@ -169,7 +169,7 @@ describe("Fiery Fallout - Mystery Encounter", () => { it("should give charcoal to lead pokemon", async () => { await game.runToMysteryEncounter(MysteryEncounterType.FIERY_FALLOUT, defaultParty); - await runMysteryEncounterToEnd(game, 1, true); + await runMysteryEncounterToEnd(game, 1, null, true); await skipBattleRunMysteryEncounterRewardsPhase(game); await game.phaseInterceptor.to(SelectModifierPhase, false); expect(scene.getCurrentPhase().constructor.name).toBe(SelectModifierPhase.name); diff --git a/src/test/mystery-encounter/encounters/lost-at-sea-encounter.test.ts b/src/test/mystery-encounter/encounters/lost-at-sea-encounter.test.ts index 600cbdbd751..e1318c79c8a 100644 --- a/src/test/mystery-encounter/encounters/lost-at-sea-encounter.test.ts +++ b/src/test/mystery-encounter/encounters/lost-at-sea-encounter.test.ts @@ -151,12 +151,17 @@ describe("Lost at Sea - Mystery Encounter", () => { const encounterPhase = scene.getCurrentPhase(); expect(encounterPhase.constructor.name).toBe(MysteryEncounterPhase.name); - const continueEncounterSpy = vi.spyOn((encounterPhase as MysteryEncounterPhase), "continueEncounter"); + const mysteryEncounterPhase = encounterPhase as MysteryEncounterPhase; + vi.spyOn(mysteryEncounterPhase, "continueEncounter"); + vi.spyOn(mysteryEncounterPhase, "handleOptionSelect"); + vi.spyOn(scene.ui, "playError"); await runSelectMysteryEncounterOption(game, 1); expect(scene.getCurrentPhase().constructor.name).toBe(MysteryEncounterPhase.name); - expect(continueEncounterSpy).not.toHaveBeenCalled(); + expect(scene.ui.playError).not.toHaveBeenCalled(); // No error sfx, option is disabled + expect(mysteryEncounterPhase.handleOptionSelect).not.toHaveBeenCalled(); + expect(mysteryEncounterPhase.continueEncounter).not.toHaveBeenCalled(); }); }); @@ -210,12 +215,17 @@ describe("Lost at Sea - Mystery Encounter", () => { const encounterPhase = scene.getCurrentPhase(); expect(encounterPhase.constructor.name).toBe(MysteryEncounterPhase.name); - const continueEncounterSpy = vi.spyOn((encounterPhase as MysteryEncounterPhase), "continueEncounter"); + const mysteryEncounterPhase = encounterPhase as MysteryEncounterPhase; + vi.spyOn(mysteryEncounterPhase, "continueEncounter"); + vi.spyOn(mysteryEncounterPhase, "handleOptionSelect"); + vi.spyOn(scene.ui, "playError"); - await runSelectMysteryEncounterOption(game, 1); + await runSelectMysteryEncounterOption(game, 2); expect(scene.getCurrentPhase().constructor.name).toBe(MysteryEncounterPhase.name); - expect(continueEncounterSpy).not.toHaveBeenCalled(); + expect(scene.ui.playError).not.toHaveBeenCalled(); // No error sfx, option is disabled + expect(mysteryEncounterPhase.handleOptionSelect).not.toHaveBeenCalled(); + expect(mysteryEncounterPhase.continueEncounter).not.toHaveBeenCalled(); }); }); diff --git a/src/test/mystery-encounter/encounters/offer-you-cant-refuse-encounter.test.ts b/src/test/mystery-encounter/encounters/offer-you-cant-refuse-encounter.test.ts index c44b399a39a..0290d32f242 100644 --- a/src/test/mystery-encounter/encounters/offer-you-cant-refuse-encounter.test.ts +++ b/src/test/mystery-encounter/encounters/offer-you-cant-refuse-encounter.test.ts @@ -15,6 +15,7 @@ import { MysteryEncounterTier } from "#enums/mystery-encounter-tier"; import { initSceneWithoutEncounterPhase } from "#test/utils/gameManagerUtils"; import { getPokemonSpecies } from "#app/data/pokemon-species"; import { Moves } from "#enums/moves"; +import { ShinyRateBoosterModifier } from "#app/modifier/modifier"; const namespace = "mysteryEncounter:offerYouCantRefuse"; /** Gyarados for Indimidate */ @@ -144,6 +145,16 @@ describe("An Offer You Can't Refuse - Mystery Encounter", () => { expect(scene.money).toBe(initialMoney + price); }); + it("Should give the player a Shiny Charm", async () => { + await game.runToMysteryEncounter(MysteryEncounterType.OFFER_YOU_CANT_REFUSE, defaultParty); + await runMysteryEncounterToEnd(game, 1); + + const itemModifier = scene.findModifier(m => m instanceof ShinyRateBoosterModifier) as ShinyRateBoosterModifier; + + expect(itemModifier).toBeDefined(); + expect(itemModifier.stackCount).toBe(1); + }); + it("Should remove the Pokemon from the party", async () => { await game.runToMysteryEncounter(MysteryEncounterType.OFFER_YOU_CANT_REFUSE, defaultParty); diff --git a/src/test/mystery-encounter/encounters/pokemon-salesman-encounter.test.ts b/src/test/mystery-encounter/encounters/pokemon-salesman-encounter.test.ts index 10b739b47b2..29d17917bcd 100644 --- a/src/test/mystery-encounter/encounters/pokemon-salesman-encounter.test.ts +++ b/src/test/mystery-encounter/encounters/pokemon-salesman-encounter.test.ts @@ -5,7 +5,7 @@ import { Species } from "#app/enums/species"; import GameManager from "#app/test/utils/gameManager"; import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; import * as EncounterPhaseUtils from "#app/data/mystery-encounters/utils/encounter-phase-utils"; -import { runMysteryEncounterToEnd } from "#test/mystery-encounter/encounterTestUtils"; +import { runMysteryEncounterToEnd, runSelectMysteryEncounterOption } from "#test/mystery-encounter/encounterTestUtils"; import BattleScene from "#app/battle-scene"; import { PlayerPokemon } from "#app/field/pokemon"; import { HUMAN_TRANSITABLE_BIOMES } from "#app/data/mystery-encounters/mystery-encounters"; @@ -13,6 +13,7 @@ import { PokemonSalesmanEncounter } from "#app/data/mystery-encounters/encounter import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode"; import { MysteryEncounterTier } from "#enums/mystery-encounter-tier"; import { initSceneWithoutEncounterPhase } from "#test/utils/gameManagerUtils"; +import { MysteryEncounterPhase } from "#app/phases/mystery-encounter-phases"; const namespace = "mysteryEncounter:pokemonSalesman"; const defaultParty = [Species.LAPRAS, Species.GENGAR, Species.ABRA]; @@ -108,12 +109,20 @@ describe("The Pokemon Salesman - Mystery Encounter", () => { expect(onInitResult).toBe(true); }); + it("should not spawn if player does not have enough money", async () => { + scene.money = 0; + + await game.runToMysteryEncounter(); + + expect(scene.currentBattle?.mysteryEncounter?.encounterType).not.toBe(MysteryEncounterType.POKEMON_SALESMAN); + }); + describe("Option 1 - Purchase the pokemon", () => { it("should have the correct properties", () => { - const option1 = PokemonSalesmanEncounter.options[0]; - expect(option1.optionMode).toBe(MysteryEncounterOptionMode.DEFAULT_OR_SPECIAL); - expect(option1.dialogue).toBeDefined(); - expect(option1.dialogue).toStrictEqual({ + const option = PokemonSalesmanEncounter.options[0]; + expect(option.optionMode).toBe(MysteryEncounterOptionMode.DISABLED_OR_DEFAULT); + expect(option.dialogue).toBeDefined(); + expect(option.dialogue).toStrictEqual({ buttonLabel: `${namespace}:option:1:label`, buttonTooltip: `${namespace}:option:1:tooltip`, selected: [ @@ -150,6 +159,26 @@ describe("The Pokemon Salesman - Mystery Encounter", () => { expect(scene.getParty().find(p => p.name === pokemonName) instanceof PlayerPokemon).toBeTruthy(); }); + it("should be disabled if player does not have enough money", async () => { + scene.money = 0; + await game.runToMysteryEncounter(MysteryEncounterType.POKEMON_SALESMAN, [Species.ARCANINE]); + await game.phaseInterceptor.to(MysteryEncounterPhase, false); + + const encounterPhase = scene.getCurrentPhase(); + expect(encounterPhase.constructor.name).toBe(MysteryEncounterPhase.name); + const mysteryEncounterPhase = encounterPhase as MysteryEncounterPhase; + vi.spyOn(mysteryEncounterPhase, "continueEncounter"); + vi.spyOn(mysteryEncounterPhase, "handleOptionSelect"); + vi.spyOn(scene.ui, "playError"); + + await runSelectMysteryEncounterOption(game, 1); + + expect(scene.getCurrentPhase().constructor.name).toBe(MysteryEncounterPhase.name); + expect(scene.ui.playError).not.toHaveBeenCalled(); // No error sfx, option is disabled + expect(mysteryEncounterPhase.handleOptionSelect).not.toHaveBeenCalled(); + expect(mysteryEncounterPhase.continueEncounter).not.toHaveBeenCalled(); + }); + it("should leave encounter without battle", async () => { const leaveEncounterWithoutBattleSpy = vi.spyOn(EncounterPhaseUtils, "leaveEncounterWithoutBattle"); diff --git a/src/test/mystery-encounter/encounters/the-strong-stuff-encounter.test.ts b/src/test/mystery-encounter/encounters/the-strong-stuff-encounter.test.ts index d0fd2b92989..91b9d509cbf 100644 --- a/src/test/mystery-encounter/encounters/the-strong-stuff-encounter.test.ts +++ b/src/test/mystery-encounter/encounters/the-strong-stuff-encounter.test.ts @@ -196,7 +196,7 @@ describe("The Strong Stuff - Mystery Encounter", () => { const phaseSpy = vi.spyOn(scene, "pushPhase"); await game.runToMysteryEncounter(MysteryEncounterType.THE_STRONG_STUFF, defaultParty); - await runMysteryEncounterToEnd(game, 2, true); + await runMysteryEncounterToEnd(game, 2, null, true); const enemyField = scene.getEnemyField(); expect(scene.getCurrentPhase().constructor.name).toBe(CommandPhase.name); @@ -220,7 +220,7 @@ describe("The Strong Stuff - Mystery Encounter", () => { it("should have Soul Dew in rewards", async () => { await game.runToMysteryEncounter(MysteryEncounterType.THE_STRONG_STUFF, defaultParty); - await runMysteryEncounterToEnd(game, 2, true); + await runMysteryEncounterToEnd(game, 2, null, true); await skipBattleRunMysteryEncounterRewardsPhase(game); await game.phaseInterceptor.to(SelectModifierPhase, false); expect(scene.getCurrentPhase().constructor.name).toBe(SelectModifierPhase.name);