Merge pull request #117 from AsdarDevelops/delibirdy-avarice

Delibird-y Encounter
This commit is contained in:
ImperialSympathizer 2024-07-25 21:39:26 -04:00 committed by GitHub
commit cf92c572c9
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
18 changed files with 811 additions and 42 deletions

View File

@ -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<boolean> => {
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<boolean> => {
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<boolean> => {
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();

View File

@ -80,7 +80,8 @@ export const FieryFalloutEncounter: IMysteryEncounter =
repeat: true, repeat: true,
hidden: true, hidden: true,
hasShadow: true, hasShadow: true,
x: -20 x: -20,
startFrame: 20
}, },
{ {
spriteKey: volcaronaSpecies.getSpriteId(true ), spriteKey: volcaronaSpecies.getSpriteId(true ),

View File

@ -106,7 +106,7 @@ export const PokemonSalesmanEncounter: IMysteryEncounter =
}) })
.withOption( .withOption(
new MysteryEncounterOptionBuilder() new MysteryEncounterOptionBuilder()
.withOptionMode(MysteryEncounterOptionMode.DEFAULT_OR_SPECIAL) .withOptionMode(MysteryEncounterOptionMode.DISABLED_OR_DEFAULT)
.withHasDexProgress(true) .withHasDexProgress(true)
.withSceneMoneyRequirement(null, MAX_POKEMON_PRICE_MULTIPLIER) // Wave scaling money multiplier of 2 .withSceneMoneyRequirement(null, MAX_POKEMON_PRICE_MULTIPLIER) // Wave scaling money multiplier of 2
.withDialogue({ .withDialogue({

View File

@ -1,6 +1,6 @@
import { OptionTextDisplay } from "#app/data/mystery-encounters/mystery-encounter-dialogue"; import { OptionTextDisplay } from "#app/data/mystery-encounters/mystery-encounter-dialogue";
import { Moves } from "#app/enums/moves"; 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 BattleScene from "#app/battle-scene";
import * as Utils from "#app/utils"; import * as Utils from "#app/utils";
import { Type } from "../type"; import { Type } from "../type";
@ -57,6 +57,10 @@ export default class MysteryEncounterOption implements MysteryEncounterOption {
this.meetsPrimaryRequirementAndPrimaryPokemonSelected(scene); 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) { meetsPrimaryRequirementAndPrimaryPokemonSelected?(scene: BattleScene) {
if (!this.primaryPokemonRequirements) { if (!this.primaryPokemonRequirements) {
return true; return true;

View File

@ -1,5 +1,5 @@
import { PlayerPokemon } from "#app/field/pokemon"; 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 BattleScene from "#app/battle-scene";
import { isNullOrUndefined } from "#app/utils"; import { isNullOrUndefined } from "#app/utils";
import { Abilities } from "#enums/abilities"; import { Abilities } from "#enums/abilities";
@ -744,20 +744,20 @@ export class CanEvolveWithItemRequirement extends EncounterPokemonRequirement {
} }
export class HeldItemRequirement extends EncounterPokemonRequirement { export class HeldItemRequirement extends EncounterPokemonRequirement {
requiredHeldItemModifier: PokemonHeldItemModifierType[]; requiredHeldItemModifiers: string[];
minNumberOfPokemon: number; minNumberOfPokemon: number;
invertQuery: boolean; invertQuery: boolean;
constructor(heldItem: PokemonHeldItemModifierType | PokemonHeldItemModifierType[], minNumberOfPokemon: number = 1, invertQuery: boolean = false) { constructor(heldItem: string | string[], minNumberOfPokemon: number = 1, invertQuery: boolean = false) {
super(); super();
this.minNumberOfPokemon = minNumberOfPokemon; this.minNumberOfPokemon = minNumberOfPokemon;
this.invertQuery = invertQuery; this.invertQuery = invertQuery;
this.requiredHeldItemModifier = Array.isArray(heldItem) ? heldItem : [heldItem]; this.requiredHeldItemModifiers = Array.isArray(heldItem) ? heldItem : [heldItem];
} }
meetsRequirement(scene: BattleScene): boolean { meetsRequirement(scene: BattleScene): boolean {
const partyPokemon = scene.getParty(); const partyPokemon = scene.getParty();
if (isNullOrUndefined(partyPokemon) || this?.requiredHeldItemModifier?.length < 0) { if (isNullOrUndefined(partyPokemon) || this?.requiredHeldItemModifiers?.length < 0) {
return false; return false;
} }
return this.queryParty(partyPokemon).length >= this.minNumberOfPokemon; return this.queryParty(partyPokemon).length >= this.minNumberOfPokemon;
@ -765,19 +765,26 @@ export class HeldItemRequirement extends EncounterPokemonRequirement {
queryParty(partyPokemon: PlayerPokemon[]): PlayerPokemon[] { queryParty(partyPokemon: PlayerPokemon[]): PlayerPokemon[] {
if (!this.invertQuery) { 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 { } else {
// for an inverted query, we only want to get the pokemon that don't have ANY of the listed heldItems // for an inverted query, we only want to get the pokemon that have any held items that are NOT in requiredHeldItemModifiers
return partyPokemon.filter((pokemon) => this.requiredHeldItemModifier.filter((heldItem) => pokemon.getHeldItems().filter((it) => it.type.id === heldItem.id).length === 0).length === 0); // 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] { getDialogueToken(scene: BattleScene, pokemon?: PlayerPokemon): [string, string] {
const requiredItems = this.requiredHeldItemModifier.filter((a) => { const requiredItems = pokemon.getHeldItems().filter((it) => {
pokemon.getHeldItems().filter((it) => it.type.id === a.id).length > 0; return this.requiredHeldItemModifiers.some(heldItem => it.constructor.name === heldItem);
}); });
if (requiredItems.length > 0) { if (requiredItems.length > 0) {
return ["heldItem", requiredItems[0].name]; return ["heldItem", requiredItems[0].type.name];
} }
return null; return null;
} }

View File

@ -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 { TheStrongStuffEncounter } from "#app/data/mystery-encounters/encounters/the-strong-stuff-encounter";
import { PokemonSalesmanEncounter } from "#app/data/mystery-encounters/encounters/pokemon-salesman-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 { 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 * <number of missed spawns>) / 256 // Spawn chance: (BASE_MYSTERY_ENCOUNTER_SPAWN_WEIGHT + WIGHT_INCREMENT_ON_SPAWN_MISS * <number of missed spawns>) / 256
export const BASE_MYSTERY_ENCOUNTER_SPAWN_WEIGHT = 1; export const BASE_MYSTERY_ENCOUNTER_SPAWN_WEIGHT = 1;
@ -150,7 +151,8 @@ const anyBiomeEncounters: MysteryEncounterType[] = [
MysteryEncounterType.FIGHT_OR_FLIGHT, MysteryEncounterType.FIGHT_OR_FLIGHT,
MysteryEncounterType.DARK_DEAL, MysteryEncounterType.DARK_DEAL,
MysteryEncounterType.MYSTERIOUS_CHEST, MysteryEncounterType.MYSTERIOUS_CHEST,
MysteryEncounterType.TRAINING_SESSION MysteryEncounterType.TRAINING_SESSION,
MysteryEncounterType.DELIBIRDY,
]; ];
/** /**
@ -163,15 +165,20 @@ const anyBiomeEncounters: MysteryEncounterType[] = [
export const mysteryEncountersByBiome = new Map<Biome, MysteryEncounterType[]>([ export const mysteryEncountersByBiome = new Map<Biome, MysteryEncounterType[]>([
[Biome.TOWN, []], [Biome.TOWN, []],
[Biome.PLAINS, [ [Biome.PLAINS, [
MysteryEncounterType.SLUMBERING_SNORLAX MysteryEncounterType.SLUMBERING_SNORLAX,
MysteryEncounterType.ABSOLUTE_AVARICE
]], ]],
[Biome.GRASS, [ [Biome.GRASS, [
MysteryEncounterType.SLUMBERING_SNORLAX, MysteryEncounterType.SLUMBERING_SNORLAX,
MysteryEncounterType.ABSOLUTE_AVARICE
]],
[Biome.TALL_GRASS, [
MysteryEncounterType.ABSOLUTE_AVARICE
]], ]],
[Biome.TALL_GRASS, []],
[Biome.METROPOLIS, []], [Biome.METROPOLIS, []],
[Biome.FOREST, [ [Biome.FOREST, [
MysteryEncounterType.SAFARI_ZONE MysteryEncounterType.SAFARI_ZONE,
MysteryEncounterType.ABSOLUTE_AVARICE
]], ]],
[Biome.SEA, [ [Biome.SEA, [
@ -230,6 +237,8 @@ export function initMysteryEncounters() {
allMysteryEncounters[MysteryEncounterType.THE_STRONG_STUFF] = TheStrongStuffEncounter; allMysteryEncounters[MysteryEncounterType.THE_STRONG_STUFF] = TheStrongStuffEncounter;
allMysteryEncounters[MysteryEncounterType.POKEMON_SALESMAN] = PokemonSalesmanEncounter; allMysteryEncounters[MysteryEncounterType.POKEMON_SALESMAN] = PokemonSalesmanEncounter;
allMysteryEncounters[MysteryEncounterType.OFFER_YOU_CANT_REFUSE] = OfferYouCantRefuseEncounter; allMysteryEncounters[MysteryEncounterType.OFFER_YOU_CANT_REFUSE] = OfferYouCantRefuseEncounter;
allMysteryEncounters[MysteryEncounterType.DELIBIRDY] = DelibirdyEncounter;
// allMysteryEncounters[MysteryEncounterType.ABSOLUTE_AVARICE] = Abs;
// Add extreme encounters to biome map // Add extreme encounters to biome map
extremeBiomeEncounters.forEach(encounter => { extremeBiomeEncounters.forEach(encounter => {

View File

@ -13,5 +13,7 @@ export enum MysteryEncounterType {
FIERY_FALLOUT, FIERY_FALLOUT,
THE_STRONG_STUFF, THE_STRONG_STUFF,
POKEMON_SALESMAN, POKEMON_SALESMAN,
OFFER_YOU_CANT_REFUSE OFFER_YOU_CANT_REFUSE,
DELIBIRDY,
ABSOLUTE_AVARICE
} }

View File

@ -39,6 +39,8 @@ export class MysteryEncounterSpriteConfig {
disableAnimation?: boolean = false; disableAnimation?: boolean = false;
/** Repeat the animation. Defaults to `false` */ /** Repeat the animation. Defaults to `false` */
repeat?: boolean = 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 at start of encounter. Defaults to `false` */
hidden?: boolean = false; hidden?: boolean = false;
/** Tint color. `0` - `1`. Higher means darker tint. */ /** Tint color. `0` - `1`. Higher means darker tint. */
@ -279,7 +281,7 @@ export default class MysteryEncounterIntroVisuals extends Phaser.GameObjects.Con
const trainerAnimConfig = { const trainerAnimConfig = {
key: config.spriteKey, key: config.spriteKey,
repeat: config?.repeat ? -1 : 0, repeat: config?.repeat ? -1 : 0,
startFrame: 0 startFrame: config?.startFrame ?? 0
}; };
this.tryPlaySprite(sprites[i], tintSprites[i], trainerAnimConfig); this.tryPlaySprite(sprites[i], tintSprites[i], trainerAnimConfig);

View File

@ -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 { theStrongStuffDialogue } from "#app/locales/en/mystery-encounters/the-strong-stuff-dialogue";
import { pokemonSalesmanDialogue } from "#app/locales/en/mystery-encounters/pokemon-salesman-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 { 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: * Patterns that can be used:
@ -50,5 +51,6 @@ export const mysteryEncounter = {
fieryFallout: fieryFalloutDialogue, fieryFallout: fieryFalloutDialogue,
theStrongStuff: theStrongStuffDialogue, theStrongStuff: theStrongStuffDialogue,
pokemonSalesman: pokemonSalesmanDialogue, pokemonSalesman: pokemonSalesmanDialogue,
offerYouCantRefuse: offerYouCantRefuseDialogue offerYouCantRefuse: offerYouCantRefuseDialogue,
delibirdy: delibirdyDialogue,
} as const; } as const;

View File

@ -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!`
};

View File

@ -12,14 +12,10 @@ export const shadyVitaminDealerDialogue = {
1: { 1: {
label: "The Cheap Deal", label: "The Cheap Deal",
tooltip: "(-) Pay {{option1Money, money}}\n(-) Side Effects?\n(+) Chosen Pokémon Gains 2 Random Vitamins", 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: { 2: {
label: "The Pricey Deal", label: "The Pricey Deal",
tooltip: "(-) Pay {{option2Money, money}}\n(-) Side Effects?\n(+) Chosen Pokémon Gains 2 Random Vitamins", 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: { 3: {
label: "Leave", label: "Leave",

View File

@ -6,15 +6,21 @@ import { Mode } from "#app/ui/ui";
import GameManager from "../utils/gameManager"; import GameManager from "../utils/gameManager";
import MessageUiHandler from "#app/ui/message-ui-handler"; import MessageUiHandler from "#app/ui/message-ui-handler";
import { Status, StatusEffect } from "#app/data/status-effect"; 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 * Runs a MysteryEncounter to either the start of a battle, or to the MysteryEncounterRewardsPhase, depending on the option selected
* @param game * @param game
* @param optionNo - human number, not index * @param optionNo - human number, not index
* @param secondaryOptionSelect -
* @param isBattle - if selecting option should lead to battle, set to true * @param isBattle - if selecting option should lead to battle, set to true
*/ */
export async function runMysteryEncounterToEnd(game: GameManager, optionNo: number, isBattle: boolean = false) { export async function runMysteryEncounterToEnd(game: GameManager, optionNo: number, secondaryOptionSelect: { pokemonNo: number, optionNo: number } = null, isBattle: boolean = false) {
await runSelectMysteryEncounterOption(game, optionNo, isBattle); vi.spyOn(EncounterPhaseUtils, "selectPokemonForOption");
await runSelectMysteryEncounterOption(game, optionNo, secondaryOptionSelect);
// run the selected options phase // run the selected options phase
game.onNextPrompt("MysteryEncounterOptionSelectedPhase", Mode.MESSAGE, () => { 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.) // Handle any eventual queued messages (e.g. weather phase, etc.)
game.onNextPrompt("MessagePhase", Mode.MESSAGE, () => { game.onNextPrompt("MessagePhase", Mode.MESSAGE, () => {
const uiHandler = game.scene.ui.getHandler<MessageUiHandler>(); const uiHandler = game.scene.ui.getHandler<MessageUiHandler>();
@ -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 uiHandler.unblockInput(); // input are blocked by 1s to prevent accidental input. Tests need to handle that
switch (optionNo) { switch (optionNo) {
default:
case 1: case 1:
// no movement needed. Default cursor position // no movement needed. Default cursor position
break; break;
@ -89,6 +96,42 @@ export async function runSelectMysteryEncounterOption(game: GameManager, optionN
} }
uiHandler.processInput(Button.ACTION); 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);
}
} }
/** /**

View File

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

View File

@ -152,7 +152,7 @@ describe("Fiery Fallout - Mystery Encounter", () => {
const phaseSpy = vi.spyOn(scene, "pushPhase"); const phaseSpy = vi.spyOn(scene, "pushPhase");
await game.runToMysteryEncounter(MysteryEncounterType.FIERY_FALLOUT, defaultParty); await game.runToMysteryEncounter(MysteryEncounterType.FIERY_FALLOUT, defaultParty);
await runMysteryEncounterToEnd(game, 1, true); await runMysteryEncounterToEnd(game, 1, null, true);
const enemyField = scene.getEnemyField(); const enemyField = scene.getEnemyField();
expect(scene.getCurrentPhase().constructor.name).toBe(CommandPhase.name); 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 () => { it("should give charcoal to lead pokemon", async () => {
await game.runToMysteryEncounter(MysteryEncounterType.FIERY_FALLOUT, defaultParty); await game.runToMysteryEncounter(MysteryEncounterType.FIERY_FALLOUT, defaultParty);
await runMysteryEncounterToEnd(game, 1, true); await runMysteryEncounterToEnd(game, 1, null, true);
await skipBattleRunMysteryEncounterRewardsPhase(game); await skipBattleRunMysteryEncounterRewardsPhase(game);
await game.phaseInterceptor.to(SelectModifierPhase, false); await game.phaseInterceptor.to(SelectModifierPhase, false);
expect(scene.getCurrentPhase().constructor.name).toBe(SelectModifierPhase.name); expect(scene.getCurrentPhase().constructor.name).toBe(SelectModifierPhase.name);

View File

@ -151,12 +151,17 @@ describe("Lost at Sea - Mystery Encounter", () => {
const encounterPhase = scene.getCurrentPhase(); const encounterPhase = scene.getCurrentPhase();
expect(encounterPhase.constructor.name).toBe(MysteryEncounterPhase.name); 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, 1);
expect(scene.getCurrentPhase().constructor.name).toBe(MysteryEncounterPhase.name); 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(); const encounterPhase = scene.getCurrentPhase();
expect(encounterPhase.constructor.name).toBe(MysteryEncounterPhase.name); 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(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();
}); });
}); });

View File

@ -15,6 +15,7 @@ import { MysteryEncounterTier } from "#enums/mystery-encounter-tier";
import { initSceneWithoutEncounterPhase } from "#test/utils/gameManagerUtils"; import { initSceneWithoutEncounterPhase } from "#test/utils/gameManagerUtils";
import { getPokemonSpecies } from "#app/data/pokemon-species"; import { getPokemonSpecies } from "#app/data/pokemon-species";
import { Moves } from "#enums/moves"; import { Moves } from "#enums/moves";
import { ShinyRateBoosterModifier } from "#app/modifier/modifier";
const namespace = "mysteryEncounter:offerYouCantRefuse"; const namespace = "mysteryEncounter:offerYouCantRefuse";
/** Gyarados for Indimidate */ /** Gyarados for Indimidate */
@ -144,6 +145,16 @@ describe("An Offer You Can't Refuse - Mystery Encounter", () => {
expect(scene.money).toBe(initialMoney + price); 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 () => { it("Should remove the Pokemon from the party", async () => {
await game.runToMysteryEncounter(MysteryEncounterType.OFFER_YOU_CANT_REFUSE, defaultParty); await game.runToMysteryEncounter(MysteryEncounterType.OFFER_YOU_CANT_REFUSE, defaultParty);

View File

@ -5,7 +5,7 @@ import { Species } from "#app/enums/species";
import GameManager from "#app/test/utils/gameManager"; import GameManager from "#app/test/utils/gameManager";
import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
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 { runMysteryEncounterToEnd } from "#test/mystery-encounter/encounterTestUtils"; import { runMysteryEncounterToEnd, runSelectMysteryEncounterOption } from "#test/mystery-encounter/encounterTestUtils";
import BattleScene from "#app/battle-scene"; import BattleScene from "#app/battle-scene";
import { PlayerPokemon } from "#app/field/pokemon"; import { PlayerPokemon } from "#app/field/pokemon";
import { HUMAN_TRANSITABLE_BIOMES } from "#app/data/mystery-encounters/mystery-encounters"; 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 { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode";
import { MysteryEncounterTier } from "#enums/mystery-encounter-tier"; import { MysteryEncounterTier } from "#enums/mystery-encounter-tier";
import { initSceneWithoutEncounterPhase } from "#test/utils/gameManagerUtils"; import { initSceneWithoutEncounterPhase } from "#test/utils/gameManagerUtils";
import { MysteryEncounterPhase } from "#app/phases/mystery-encounter-phases";
const namespace = "mysteryEncounter:pokemonSalesman"; const namespace = "mysteryEncounter:pokemonSalesman";
const defaultParty = [Species.LAPRAS, Species.GENGAR, Species.ABRA]; const defaultParty = [Species.LAPRAS, Species.GENGAR, Species.ABRA];
@ -108,12 +109,20 @@ describe("The Pokemon Salesman - Mystery Encounter", () => {
expect(onInitResult).toBe(true); 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", () => { describe("Option 1 - Purchase the pokemon", () => {
it("should have the correct properties", () => { it("should have the correct properties", () => {
const option1 = PokemonSalesmanEncounter.options[0]; const option = PokemonSalesmanEncounter.options[0];
expect(option1.optionMode).toBe(MysteryEncounterOptionMode.DEFAULT_OR_SPECIAL); expect(option.optionMode).toBe(MysteryEncounterOptionMode.DISABLED_OR_DEFAULT);
expect(option1.dialogue).toBeDefined(); expect(option.dialogue).toBeDefined();
expect(option1.dialogue).toStrictEqual({ expect(option.dialogue).toStrictEqual({
buttonLabel: `${namespace}:option:1:label`, buttonLabel: `${namespace}:option:1:label`,
buttonTooltip: `${namespace}:option:1:tooltip`, buttonTooltip: `${namespace}:option:1:tooltip`,
selected: [ selected: [
@ -139,6 +148,7 @@ describe("The Pokemon Salesman - Mystery Encounter", () => {
}); });
it("Should add the Pokemon to the party", async () => { it("Should add the Pokemon to the party", async () => {
scene.money = 20000;
await game.runToMysteryEncounter(MysteryEncounterType.POKEMON_SALESMAN, defaultParty); await game.runToMysteryEncounter(MysteryEncounterType.POKEMON_SALESMAN, defaultParty);
const initialPartySize = scene.getParty().length; const initialPartySize = scene.getParty().length;
@ -150,7 +160,28 @@ describe("The Pokemon Salesman - Mystery Encounter", () => {
expect(scene.getParty().find(p => p.name === pokemonName) instanceof PlayerPokemon).toBeTruthy(); 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, 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 () => { it("should leave encounter without battle", async () => {
scene.money = 20000;
const leaveEncounterWithoutBattleSpy = vi.spyOn(EncounterPhaseUtils, "leaveEncounterWithoutBattle"); const leaveEncounterWithoutBattleSpy = vi.spyOn(EncounterPhaseUtils, "leaveEncounterWithoutBattle");
await game.runToMysteryEncounter(MysteryEncounterType.POKEMON_SALESMAN, defaultParty); await game.runToMysteryEncounter(MysteryEncounterType.POKEMON_SALESMAN, defaultParty);

View File

@ -196,7 +196,7 @@ describe("The Strong Stuff - Mystery Encounter", () => {
const phaseSpy = vi.spyOn(scene, "pushPhase"); const phaseSpy = vi.spyOn(scene, "pushPhase");
await game.runToMysteryEncounter(MysteryEncounterType.THE_STRONG_STUFF, defaultParty); await game.runToMysteryEncounter(MysteryEncounterType.THE_STRONG_STUFF, defaultParty);
await runMysteryEncounterToEnd(game, 2, true); await runMysteryEncounterToEnd(game, 2, null, true);
const enemyField = scene.getEnemyField(); const enemyField = scene.getEnemyField();
expect(scene.getCurrentPhase().constructor.name).toBe(CommandPhase.name); 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 () => { it("should have Soul Dew in rewards", async () => {
await game.runToMysteryEncounter(MysteryEncounterType.THE_STRONG_STUFF, defaultParty); await game.runToMysteryEncounter(MysteryEncounterType.THE_STRONG_STUFF, defaultParty);
await runMysteryEncounterToEnd(game, 2, true); await runMysteryEncounterToEnd(game, 2, null, true);
await skipBattleRunMysteryEncounterRewardsPhase(game); await skipBattleRunMysteryEncounterRewardsPhase(game);
await game.phaseInterceptor.to(SelectModifierPhase, false); await game.phaseInterceptor.to(SelectModifierPhase, false);
expect(scene.getCurrentPhase().constructor.name).toBe(SelectModifierPhase.name); expect(scene.getCurrentPhase().constructor.name).toBe(SelectModifierPhase.name);