diff --git a/src/battle-scene.ts b/src/battle-scene.ts index c9409d7cab0..169c024d329 100644 --- a/src/battle-scene.ts +++ b/src/battle-scene.ts @@ -1072,12 +1072,14 @@ export default class BattleScene extends SceneBase { this.field.add(newTrainer); } - // TODO: remove this once spawn rates are finalized + // TODO: remove these once ME spawn rates are finalized // let testStartingWeight = 0; // while (testStartingWeight < 3) { // calculateMEAggregateStats(this, testStartingWeight); // testStartingWeight += 2; // } + // calculateRareSpawnAggregateStats(this, 14); + // Check for mystery encounter // Can only occur in place of a standard wild battle, waves 10-180 if (this.gameMode.hasMysteryEncounters && newBattleType === BattleType.WILD && !this.gameMode.isBoss(newWaveIndex) && newWaveIndex < 180 && newWaveIndex > 10) { diff --git a/src/data/mystery-encounters/encounters/absolute-avarice-encounter.ts b/src/data/mystery-encounters/encounters/absolute-avarice-encounter.ts index a63fc527324..9077f0f1ab8 100644 --- a/src/data/mystery-encounters/encounters/absolute-avarice-encounter.ts +++ b/src/data/mystery-encounters/encounters/absolute-avarice-encounter.ts @@ -168,11 +168,6 @@ export const AbsoluteAvariceEncounter: IMysteryEncounter = .withTitle(`${namespace}:title`) .withDescription(`${namespace}:description`) .withQuery(`${namespace}:query`) - .withOutroDialogue([ - { - text: `${namespace}:outro`, - } - ]) .withOnInit((scene: BattleScene) => { const encounter = scene.currentBattle.mysteryEncounter; @@ -423,8 +418,6 @@ function doGreedentSpriteSteal(scene: BattleScene) { function doGreedentEatBerries(scene: BattleScene) { const greedentSprites = scene.currentBattle.mysteryEncounter.introVisuals.getSpriteAtIndex(0); - - // scene.playSound("Follow Me"); let index = 1; scene.tweens.add({ targets: greedentSprites, diff --git a/src/data/mystery-encounters/encounters/delibirdy-encounter.ts b/src/data/mystery-encounters/encounters/delibirdy-encounter.ts index c58f9775a04..dcc37cda8fe 100644 --- a/src/data/mystery-encounters/encounters/delibirdy-encounter.ts +++ b/src/data/mystery-encounters/encounters/delibirdy-encounter.ts @@ -1,18 +1,20 @@ -import { leaveEncounterWithoutBattle, selectPokemonForOption, updatePlayerMoney, } from "#app/data/mystery-encounters/utils/encounter-phase-utils"; +import { generateModifierTypeOption, 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 { modifierTypes, PokemonHeldItemModifierType } 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 { CombinationPokemonRequirement, HeldItemRequirement, MoneyRequirement } from "../mystery-encounter-requirements"; +import { getEncounterText, showEncounterText } 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 { BerryModifier, HealingBoosterModifier, HiddenAbilityRateBoosterModifier, LevelIncrementBoosterModifier, PokemonBaseStatModifier, PokemonBaseStatTotalModifier, PokemonHeldItemModifier, PokemonInstantReviveModifier, PreserveBerryModifier, TerastallizeModifier } from "#app/modifier/modifier"; import { ModifierRewardPhase } from "#app/phases"; import { OptionSelectItem } from "#app/ui/abstact-option-select-ui-handler"; +import { applyModifierTypeToPlayerPokemon } from "#app/data/mystery-encounters/utils/encounter-pokemon-utils"; +import i18next from "#app/plugins/i18n"; /** the i18n namespace for this encounter */ const namespace = "mysteryEncounter:delibirdy"; @@ -39,6 +41,10 @@ export const DelibirdyEncounter: IMysteryEncounter = .withEncounterTier(MysteryEncounterTier.GREAT) .withSceneWaveRangeRequirement(10, 180) .withSceneRequirement(new MoneyRequirement(0, 2.75)) // Must have enough money for it to spawn at the very least + .withPrimaryPokemonRequirement(new CombinationPokemonRequirement( // Must also have either option 2 or 3 available to spawn + new HeldItemRequirement(OPTION_2_ALLOWED_MODIFIERS), + new HeldItemRequirement(OPTION_3_DISALLOWED_MODIFIERS, 1, true) + )) .withIntroSpriteConfigs([ { spriteKey: Species.DELIBIRD.toString(), @@ -82,7 +88,7 @@ export const DelibirdyEncounter: IMysteryEncounter = .withOption( new MysteryEncounterOptionBuilder() .withOptionMode(MysteryEncounterOptionMode.DISABLED_OR_DEFAULT) - .withSceneMoneyRequirement(0, 2.75) + .withSceneMoneyRequirement(0, 2.75) // Must have money to spawn .withDialogue({ buttonLabel: `${namespace}:option:1:label`, buttonTooltip: `${namespace}:option:1:tooltip`, @@ -99,7 +105,19 @@ export const DelibirdyEncounter: IMysteryEncounter = }) .withOptionPhase(async (scene: BattleScene) => { // Give the player an Ability Charm - scene.unshiftPhase(new ModifierRewardPhase(scene, modifierTypes.ABILITY_CHARM)); + // Check if the player has max stacks of that item already + const existing = scene.findModifier(m => m instanceof HiddenAbilityRateBoosterModifier) as HiddenAbilityRateBoosterModifier; + + if (existing && existing.getStackCount() >= existing.getMaxStackCount(scene)) { + // At max stacks, give the first party pokemon a Shell Bell instead + const shellBell = generateModifierTypeOption(scene, modifierTypes.SHELL_BELL).type as PokemonHeldItemModifierType; + await applyModifierTypeToPlayerPokemon(scene, scene.getParty()[0], shellBell); + scene.playSound("item_fanfare"); + await showEncounterText(scene, i18next.t("battle:rewardGain", { modifierName: shellBell.name }), null, true); + } else { + scene.unshiftPhase(new ModifierRewardPhase(scene, modifierTypes.ABILITY_CHARM)); + } + leaveEncounterWithoutBattle(scene, true); }) .build() @@ -159,11 +177,34 @@ export const DelibirdyEncounter: IMysteryEncounter = .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)); + // Check if the player has max stacks of that Candy Jar already + const existing = scene.findModifier(m => m instanceof LevelIncrementBoosterModifier) as LevelIncrementBoosterModifier; + + if (existing && existing.getStackCount() >= existing.getMaxStackCount(scene)) { + // At max stacks, give the first party pokemon a Shell Bell instead + const shellBell = generateModifierTypeOption(scene, modifierTypes.SHELL_BELL).type as PokemonHeldItemModifierType; + await applyModifierTypeToPlayerPokemon(scene, scene.getParty()[0], shellBell); + scene.playSound("item_fanfare"); + await showEncounterText(scene, i18next.t("battle:rewardGain", { modifierName: shellBell.name }), null, true); + } else { + scene.unshiftPhase(new ModifierRewardPhase(scene, modifierTypes.CANDY_JAR)); + } } else { - scene.unshiftPhase(new ModifierRewardPhase(scene, modifierTypes.HEALING_CHARM)); + // Check if the player has max stacks of that Healing Charm already + const existing = scene.findModifier(m => m instanceof HealingBoosterModifier) as HealingBoosterModifier; + + if (existing && existing.getStackCount() >= existing.getMaxStackCount(scene)) { + // At max stacks, give the first party pokemon a Shell Bell instead + const shellBell = generateModifierTypeOption(scene, modifierTypes.SHELL_BELL).type as PokemonHeldItemModifierType; + await applyModifierTypeToPlayerPokemon(scene, scene.getParty()[0], shellBell); + scene.playSound("item_fanfare"); + await showEncounterText(scene, i18next.t("battle:rewardGain", { modifierName: shellBell.name }), null, true); + } else { + scene.unshiftPhase(new ModifierRewardPhase(scene, modifierTypes.HEALING_CHARM)); + } } // Remove the modifier if its stacks go to 0 @@ -231,8 +272,19 @@ export const DelibirdyEncounter: IMysteryEncounter = .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)); + + // Check if the player has max stacks of Berry Pouch already + const existing = scene.findModifier(m => m instanceof PreserveBerryModifier) as PreserveBerryModifier; + + if (existing && existing.getStackCount() >= existing.getMaxStackCount(scene)) { + // At max stacks, give the first party pokemon a Shell Bell instead + const shellBell = generateModifierTypeOption(scene, modifierTypes.SHELL_BELL).type as PokemonHeldItemModifierType; + await applyModifierTypeToPlayerPokemon(scene, scene.getParty()[0], shellBell); + scene.playSound("item_fanfare"); + await showEncounterText(scene, i18next.t("battle:rewardGain", { modifierName: shellBell.name }), null, true); + } else { + scene.unshiftPhase(new ModifierRewardPhase(scene, modifierTypes.BERRY_POUCH)); + } // Remove the modifier if its stacks go to 0 modifier.stackCount -= 1; diff --git a/src/data/mystery-encounters/utils/encounter-phase-utils.ts b/src/data/mystery-encounters/utils/encounter-phase-utils.ts index 52b67cb4a47..a0fe413339d 100644 --- a/src/data/mystery-encounters/utils/encounter-phase-utils.ts +++ b/src/data/mystery-encounters/utils/encounter-phase-utils.ts @@ -1,11 +1,11 @@ import { BattlerIndex, BattleType } from "#app/battle"; -import { biomeLinks } from "#app/data/biomes"; +import { biomeLinks, BiomePoolTier } from "#app/data/biomes"; import MysteryEncounterOption from "#app/data/mystery-encounters/mystery-encounter-option"; import { WEIGHT_INCREMENT_ON_SPAWN_MISS } from "#app/data/mystery-encounters/mystery-encounters"; import { showEncounterText } from "#app/data/mystery-encounters/utils/encounter-dialogue-utils"; import Pokemon, { FieldPosition, PlayerPokemon, PokemonMove } from "#app/field/pokemon"; import { ExpBalanceModifier, ExpShareModifier, MultipleParticipantExpBonusModifier, PokemonExpBoosterModifier } from "#app/modifier/modifier"; -import { CustomModifierSettings, getModifierPoolForType, ModifierPoolType, ModifierType, ModifierTypeFunc, ModifierTypeGenerator, ModifierTypeOption, modifierTypes, PokemonHeldItemModifierType, regenerateModifierPoolThresholds } from "#app/modifier/modifier-type"; +import { CustomModifierSettings, ModifierPoolType, ModifierType, ModifierTypeFunc, ModifierTypeGenerator, ModifierTypeOption, modifierTypes, PokemonHeldItemModifierType, regenerateModifierPoolThresholds } from "#app/modifier/modifier-type"; import * as Overrides from "#app/overrides"; import { BattleEndPhase, EggLapsePhase, ExpPhase, GameOverPhase, ModifierRewardPhase, MovePhase, SelectModifierPhase, ShowPartyExpBarPhase, TrainerVictoryPhase } from "#app/phases"; import { MysteryEncounterBattlePhase, MysteryEncounterBattleStartCleanupPhase, MysteryEncounterPhase, MysteryEncounterRewardsPhase } from "#app/phases/mystery-encounter-phases"; @@ -344,20 +344,10 @@ export function generateModifierTypeOption(scene: BattleScene, modifier: () => M const modifierId = Object.keys(modifierTypes).find(k => modifierTypes[k] === modifier); let result: ModifierType = modifierTypes[modifierId]?.(); - // Gets tier of item by checking player item pool - const modifierPool = getModifierPoolForType(ModifierPoolType.PLAYER); - Object.keys(modifierPool).every(modifierTier => { - const modType = modifierPool[modifierTier].find(m => { - if (m.modifierType.id === modifierId) { - return m; - } - }); - if (modType) { - result = modType.modifierType; - return false; - } - return true; - }); + // Populates item id and tier (order matters) + result = result + .withIdFromFunc(modifierTypes[modifierId]) + .withTierFromPool(); result = result instanceof ModifierTypeGenerator ? result.generateType(scene.getParty(), pregenArgs) : result; return new ModifierTypeOption(result, 0); @@ -881,3 +871,72 @@ export function calculateMEAggregateStats(scene: BattleScene, baseSpawnWeight: n console.log(stats); } + + +/** + * TODO: remove once encounter spawn rate is finalized + * Just a helper function to calculate aggregate stats for MEs in a Classic run + * @param scene + * @param luckValue - 0 to 14 + */ +export function calculateRareSpawnAggregateStats(scene: BattleScene, luckValue: number) { + const numRuns = 1000; + let run = 0; + + const calculateNumRareEncounters = (): any[] => { + const bossEncountersByRarity = [0, 0, 0, 0]; + scene.setSeed(Utils.randomString(24)); + scene.resetSeed(); + // There are 12 wild boss floors + for (let i = 0; i < 12; i++) { + // Roll boss tier + // luck influences encounter rarity + let luckModifier = 0; + if (!isNaN(luckValue)) { + luckModifier = luckValue * 0.5; + } + const tierValue = Utils.randSeedInt(64 - luckModifier); + const tier = tierValue >= 20 ? BiomePoolTier.BOSS : tierValue >= 6 ? BiomePoolTier.BOSS_RARE : tierValue >= 1 ? BiomePoolTier.BOSS_SUPER_RARE : BiomePoolTier.BOSS_ULTRA_RARE; + + switch (tier) { + default: + case BiomePoolTier.BOSS: + ++bossEncountersByRarity[0]; + break; + case BiomePoolTier.BOSS_RARE: + ++bossEncountersByRarity[1]; + break; + case BiomePoolTier.BOSS_SUPER_RARE: + ++bossEncountersByRarity[2]; + break; + case BiomePoolTier.BOSS_ULTRA_RARE: + ++bossEncountersByRarity[3]; + break; + } + } + + return bossEncountersByRarity; + }; + + const encounterRuns: number[][] = []; + while (run < numRuns) { + scene.executeWithSeedOffset(() => { + const bossEncountersByRarity = calculateNumRareEncounters(); + encounterRuns.push(bossEncountersByRarity); + }, 1000 * run); + run++; + } + + const n = encounterRuns.length; + // const totalEncountersInRun = encounterRuns.map(run => run.reduce((a, b) => a + b)); + // const totalMean = totalEncountersInRun.reduce((a, b) => a + b) / n; + // const totalStd = Math.sqrt(totalEncountersInRun.map(x => Math.pow(x - totalMean, 2)).reduce((a, b) => a + b) / n); + const commonMean = encounterRuns.reduce((a, b) => a + b[0], 0) / n; + const rareMean = encounterRuns.reduce((a, b) => a + b[1], 0) / n; + const superRareMean = encounterRuns.reduce((a, b) => a + b[2], 0) / n; + const ultraRareMean = encounterRuns.reduce((a, b) => a + b[3], 0) / n; + + const stats = `Avg Commons: ${commonMean}\nAvg Rare: ${rareMean}\nAvg Super Rare: ${superRareMean}\nAvg Ultra Rare: ${ultraRareMean}\n`; + + console.log(stats); +} diff --git a/src/modifier/modifier-type.ts b/src/modifier/modifier-type.ts index ae5b1ba0ee8..9a8504f16d3 100644 --- a/src/modifier/modifier-type.ts +++ b/src/modifier/modifier-type.ts @@ -109,11 +109,35 @@ export class ModifierType { return null; } + /** + * Populates item id for ModifierType instance + * @param func + */ withIdFromFunc(func: ModifierTypeFunc): ModifierType { this.id = Object.keys(modifierTypes).find(k => modifierTypes[k] === func); return this; } + /** + * Populates item tier for ModifierType instance + * Tier is a necessary field for items that appear in player shop (determines the Pokeball visual they use) + * To find the tier, this function performs a reverse lookup of the item type in modifier pools + * @param poolType - Default 'ModifierPoolType.PLAYER'. Which pool to lookup item tier from + */ + withTierFromPool(poolType: ModifierPoolType = ModifierPoolType.PLAYER): ModifierType { + const modifierPool = getModifierPoolForType(poolType); + Object.values(modifierPool).every(weightedModifiers => { + weightedModifiers.every(m => { + if (m.modifierType.id === this.id) { + this.tier = m.modifierType.tier; + return false; // Early lookup return if tier found + } + }); + return !this.tier; // Early lookup return if tier found + }); + return this; + } + newModifier(...args: any[]): Modifier { return this.newModifierFunc(this, args); } @@ -1841,6 +1865,21 @@ export function getModifierTypeFuncById(id: string): ModifierTypeFunc { return modifierTypes[id]; } +/** + * Generates modifier options for a SelectModifierPhase + * @param count - Determines the number of items to generate + * @param party - Party is required for generating proper modifier pools + * @param modifierTiers - (Optional) If specified, rolls items in the specified tiers. Commonly used for tier-locking with Lock Capsule. + * @param customModifierSettings - (Optional) If specified, can customize the item shop rewards further. + * - `guaranteedModifierTypeOptions?: ModifierTypeOption[]` - If specified, will override the first X items to be specific modifier options (these should be pre-genned). + * - `guaranteedModifierTypeFuncs?: ModifierTypeFunc[]` - If specified, will override the next X items to be auto-generated from specific modifier functions (these don't have to be pre-genned). + * - `guaranteedModifierTiers?: ModifierTier[]` - If specified, will override the next X items to be the specified tier. These can upgrade with luck. + * - `fillRemaining?: boolean` - Default 'false'. If set to true, will fill the remainder of shop items that were not overridden by the 3 options above, up to the 'count' param value. + * - Example: `count = 4`, `customModifierSettings = { guaranteedModifierTiers: [ModifierTier.GREAT], fillRemaining: true }`, + * - The first item in the shop will be `GREAT` tier, and the remaining 3 items will be generated normally. + * - If `fillRemaining = false` in the same scenario, only 1 `GREAT` tier item will appear in the shop (regardless of `count` value). + * - `rerollMultiplier?: number` - If specified, can adjust the amount of money required for a shop reroll. If set to 0, the shop will not allow rerolls at all. + */ export function getPlayerModifierTypeOptions(count: integer, party: PlayerPokemon[], modifierTiers?: ModifierTier[], customModifierSettings?: CustomModifierSettings): ModifierTypeOption[] { const options: ModifierTypeOption[] = []; const retryCount = Math.min(count * 5, 50); @@ -1849,32 +1888,21 @@ export function getPlayerModifierTypeOptions(count: integer, party: PlayerPokemo options.push(getModifierTypeOptionWithRetry(options, retryCount, party, modifierTiers?.length > i ? modifierTiers[i] : undefined)); }); } else { - // Guaranteed mods first - if (customModifierSettings?.guaranteedModifierTypeOptions?.length) { - customModifierSettings?.guaranteedModifierTypeOptions.forEach((option) => { - options.push(option); - }); + // Guaranteed mod options first + if (customModifierSettings?.guaranteedModifierTypeOptions?.length > 0) { + options.push(...customModifierSettings.guaranteedModifierTypeOptions); } - // Guaranteed mod funcs second - if (customModifierSettings?.guaranteedModifierTypeFuncs?.length) { + // Guaranteed mod functions second + if (customModifierSettings?.guaranteedModifierTypeFuncs?.length > 0) { customModifierSettings?.guaranteedModifierTypeFuncs.forEach((mod, i) => { const modifierId = Object.keys(modifierTypes).find(k => modifierTypes[k] === mod); let guaranteedMod: ModifierType = modifierTypes[modifierId]?.(); - // Gets tier of item by checking player item pool - Object.keys(modifierPool).every(modifierTier => { - const modType = modifierPool[modifierTier].find(m => { - if (m.modifierType.id === modifierId) { - return m; - } - }); - if (modType) { - guaranteedMod = modType.modifierType; - return false; - } - return true; - }); + // Populates item id and tier + guaranteedMod = guaranteedMod + .withIdFromFunc(modifierTypes[modifierId]) + .withTierFromPool(); const modType = guaranteedMod instanceof ModifierTypeGenerator ? guaranteedMod.generateType(party) : guaranteedMod; const option = new ModifierTypeOption(modType, 0); @@ -1883,7 +1911,7 @@ export function getPlayerModifierTypeOptions(count: integer, party: PlayerPokemo } // Guaranteed tiers third - if (customModifierSettings?.guaranteedModifierTiers?.length) { + if (customModifierSettings?.guaranteedModifierTiers?.length > 0) { customModifierSettings?.guaranteedModifierTiers.forEach((tier) => { options.push(getModifierTypeOptionWithRetry(options, retryCount, party, tier)); }); @@ -1900,8 +1928,12 @@ export function getPlayerModifierTypeOptions(count: integer, party: PlayerPokemo // OVERRIDE IF NECESSARY if (Overrides.ITEM_REWARD_OVERRIDE?.length) { options.forEach((mod, i) => { - // @ts-ignore: keeps throwing don't use string as index error in typedoc run - const override = modifierTypes[Overrides.ITEM_REWARD_OVERRIDE[i]]?.(); + let override = modifierTypes[Overrides.ITEM_REWARD_OVERRIDE[i]]?.(); + // Populates item id and tier + override = override + .withIdFromFunc(modifierTypes[Overrides.ITEM_REWARD_OVERRIDE[i]]) + .withTierFromPool(); + mod.type = (override instanceof ModifierTypeGenerator ? override.generateType(party) : override) || mod.type; }); } diff --git a/src/modifier/modifier.ts b/src/modifier/modifier.ts index 72aaf6cbb41..165a1227c4a 100644 --- a/src/modifier/modifier.ts +++ b/src/modifier/modifier.ts @@ -21,7 +21,7 @@ import { VoucherType } from "../system/voucher"; import { FormChangeItem, SpeciesFormChangeItemTrigger } from "../data/pokemon-forms"; import { Nature } from "#app/data/nature"; import * as Overrides from "../overrides"; -import { ModifierType, modifierTypes } from "./modifier-type"; +import { ModifierType, ModifierTypeGenerator, modifierTypes } from "./modifier-type"; import { Command } from "#app/ui/command-ui-handler.js"; import { Species } from "#enums/species"; import i18next from "i18next"; @@ -2683,8 +2683,12 @@ export function overrideModifiers(scene: BattleScene, player: boolean = true): v if (!modifierTypes.hasOwnProperty(modifierName)) { return; } // if the modifier does not exist, we skip it - const modifierType: ModifierType = modifierTypes[modifierName](); - const modifier: PersistentModifier = modifierType.withIdFromFunc(modifierTypes[modifierName]).newModifier() as PersistentModifier; + let modifierType: ModifierType = modifierTypes[modifierName](); + if (modifierType instanceof ModifierTypeGenerator) { + modifierType = (modifierType as ModifierTypeGenerator).generateType(scene.getParty(), [item.type]); + } + modifierType = modifierType.withIdFromFunc(modifierTypes[modifierName]); + const modifier: PersistentModifier = modifierType.newModifier(scene.getParty()[0]) as PersistentModifier; modifier.stackCount = qty; if (player) { scene.addModifier(modifier, true, false, false, true); diff --git a/src/overrides.ts b/src/overrides.ts index 1d006630983..d4e4a65134d 100644 --- a/src/overrides.ts +++ b/src/overrides.ts @@ -117,9 +117,9 @@ export const EGG_GACHA_PULL_COUNT_OVERRIDE: number = 0; */ // 1 to 256, set to null to ignore -export const MYSTERY_ENCOUNTER_RATE_OVERRIDE: number = 256; +export const MYSTERY_ENCOUNTER_RATE_OVERRIDE: number = null; export const MYSTERY_ENCOUNTER_TIER_OVERRIDE: MysteryEncounterTier = null; -export const MYSTERY_ENCOUNTER_OVERRIDE: MysteryEncounterType = MysteryEncounterType.ABSOLUTE_AVARICE; +export const MYSTERY_ENCOUNTER_OVERRIDE: MysteryEncounterType = null; /** * MODIFIER / ITEM OVERRIDES @@ -137,7 +137,7 @@ export const MYSTERY_ENCOUNTER_OVERRIDE: MysteryEncounterType = MysteryEncounter * - BerryType is for BERRY * - SpeciesStatBoosterItem is for SPECIES_STAT_BOOSTER */ -interface ModifierOverride { +export interface ModifierOverride { name: keyof typeof modifierTypes & string, count?: integer type?: TempBattleStat|Stat|Nature|Type|BerryType|SpeciesStatBoosterItem @@ -155,4 +155,4 @@ export const NEVER_CRIT_OVERRIDE: boolean = false; * If less items are listed than rolled, only some items will be replaced * If more items are listed than rolled, only the first X items will be shown, where X is the number of items rolled. */ -export const ITEM_REWARD_OVERRIDE: Array = []; +export const ITEM_REWARD_OVERRIDE: Array = []; diff --git a/src/test/mystery-encounter/encounterTestUtils.ts b/src/test/mystery-encounter/encounterTestUtils.ts index aa0551e78e2..a3eb505c090 100644 --- a/src/test/mystery-encounter/encounterTestUtils.ts +++ b/src/test/mystery-encounter/encounterTestUtils.ts @@ -1,6 +1,6 @@ import { Button } from "#app/enums/buttons"; import { CommandPhase, MessagePhase, VictoryPhase } from "#app/phases"; -import { MysteryEncounterPhase, MysteryEncounterRewardsPhase } from "#app/phases/mystery-encounter-phases"; +import { MysteryEncounterBattlePhase, MysteryEncounterOptionSelectedPhase, MysteryEncounterPhase, MysteryEncounterRewardsPhase } from "#app/phases/mystery-encounter-phases"; import MysteryEncounterUiHandler from "#app/ui/mystery-encounter-ui-handler"; import { Mode } from "#app/ui/ui"; import GameManager from "../utils/gameManager"; @@ -26,29 +26,39 @@ export async function runMysteryEncounterToEnd(game: GameManager, optionNo: numb game.onNextPrompt("MysteryEncounterOptionSelectedPhase", Mode.MESSAGE, () => { const uiHandler = game.scene.ui.getHandler(); uiHandler.processInput(Button.ACTION); - }); - - // If a battle is started, fast forward to end of the battle - game.onNextPrompt("CommandPhase", Mode.COMMAND, () => { - game.scene.clearPhaseQueue(); - game.scene.clearPhaseQueueSplice(); - game.scene.unshiftPhase(new VictoryPhase(game.scene, 0)); - game.endPhase(); - }); - - // Handle end of battle trainer messages - game.onNextPrompt("TrainerVictoryPhase", Mode.MESSAGE, () => { - const uiHandler = game.scene.ui.getHandler(); - uiHandler.processInput(Button.ACTION); - }); - - // Handle egg hatch dialogue - game.onNextPrompt("EggLapsePhase", Mode.MESSAGE, () => { - const uiHandler = game.scene.ui.getHandler(); - uiHandler.processInput(Button.ACTION); - }); + }, () => game.isCurrentPhase(MysteryEncounterBattlePhase) || game.isCurrentPhase(MysteryEncounterRewardsPhase)); if (isBattle) { + game.onNextPrompt("CheckSwitchPhase", Mode.CONFIRM, () => { + game.setMode(Mode.MESSAGE); + game.endPhase(); + }, () => game.isCurrentPhase(CommandPhase)); + + game.onNextPrompt("CheckSwitchPhase", Mode.MESSAGE, () => { + game.setMode(Mode.MESSAGE); + game.endPhase(); + }, () => game.isCurrentPhase(CommandPhase)); + + // If a battle is started, fast forward to end of the battle + game.onNextPrompt("CommandPhase", Mode.COMMAND, () => { + game.scene.clearPhaseQueue(); + game.scene.clearPhaseQueueSplice(); + game.scene.unshiftPhase(new VictoryPhase(game.scene, 0)); + game.endPhase(); + }); + + // Handle end of battle trainer messages + game.onNextPrompt("TrainerVictoryPhase", Mode.MESSAGE, () => { + const uiHandler = game.scene.ui.getHandler(); + uiHandler.processInput(Button.ACTION); + }); + + // Handle egg hatch dialogue + game.onNextPrompt("EggLapsePhase", Mode.MESSAGE, () => { + const uiHandler = game.scene.ui.getHandler(); + uiHandler.processInput(Button.ACTION); + }); + await game.phaseInterceptor.to(CommandPhase); } else { await game.phaseInterceptor.to(MysteryEncounterRewardsPhase); @@ -60,7 +70,7 @@ export async function runSelectMysteryEncounterOption(game: GameManager, optionN game.onNextPrompt("MessagePhase", Mode.MESSAGE, () => { const uiHandler = game.scene.ui.getHandler(); uiHandler.processInput(Button.ACTION); - }); + }, () => game.isCurrentPhase(MysteryEncounterOptionSelectedPhase)); if (game.isCurrentPhase(MessagePhase)) { await game.phaseInterceptor.run(MessagePhase); @@ -70,7 +80,7 @@ export async function runSelectMysteryEncounterOption(game: GameManager, optionN game.onNextPrompt("MysteryEncounterPhase", Mode.MESSAGE, () => { const uiHandler = game.scene.ui.getHandler(); uiHandler.processInput(Button.ACTION); - }); + }, () => game.isCurrentPhase(MysteryEncounterOptionSelectedPhase)); await game.phaseInterceptor.to(MysteryEncounterPhase, true); diff --git a/src/test/mystery-encounter/encounters/absolute-avarice-encounter.test.ts b/src/test/mystery-encounter/encounters/absolute-avarice-encounter.test.ts new file mode 100644 index 00000000000..0df5ec9403f --- /dev/null +++ b/src/test/mystery-encounter/encounters/absolute-avarice-encounter.test.ts @@ -0,0 +1,266 @@ +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, skipBattleRunMysteryEncounterRewardsPhase } 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 * as MysteryEncounters from "#app/data/mystery-encounters/mystery-encounters"; +import { BerryModifier, PokemonHeldItemModifier } from "#app/modifier/modifier"; +import { BerryType } from "#enums/berry-type"; +import { AbsoluteAvariceEncounter } from "#app/data/mystery-encounters/encounters/absolute-avarice-encounter"; +import { CommandPhase, MovePhase, SelectModifierPhase } from "#app/phases"; +import { Moves } from "#enums/moves"; + +const namespace = "mysteryEncounter:absoluteAvarice"; +const defaultParty = [Species.LAPRAS, Species.GENGAR, Species.ABRA]; +const defaultBiome = Biome.PLAINS; +const defaultWave = 45; + +describe("Absolute Avarice - 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.PLAINS, [MysteryEncounterType.ABSOLUTE_AVARICE]], + [Biome.VOLCANO, [MysteryEncounterType.MYSTERIOUS_CHALLENGERS]], + ]) + ); + }); + + afterEach(() => { + game.phaseInterceptor.restoreOg(); + vi.clearAllMocks(); + vi.resetAllMocks(); + }); + + it("should have the correct properties", async () => { + await game.runToMysteryEncounter(MysteryEncounterType.ABSOLUTE_AVARICE, defaultParty); + + expect(AbsoluteAvariceEncounter.encounterType).toBe(MysteryEncounterType.ABSOLUTE_AVARICE); + expect(AbsoluteAvariceEncounter.encounterTier).toBe(MysteryEncounterTier.GREAT); + expect(AbsoluteAvariceEncounter.dialogue).toBeDefined(); + expect(AbsoluteAvariceEncounter.dialogue.intro).toStrictEqual([{ text: `${namespace}:intro` }]); + expect(AbsoluteAvariceEncounter.dialogue.encounterOptionsDialogue.title).toBe(`${namespace}:title`); + expect(AbsoluteAvariceEncounter.dialogue.encounterOptionsDialogue.description).toBe(`${namespace}:description`); + expect(AbsoluteAvariceEncounter.dialogue.encounterOptionsDialogue.query).toBe(`${namespace}:query`); + expect(AbsoluteAvariceEncounter.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.ABSOLUTE_AVARICE); + }); + + it("should not run above wave 179", async () => { + game.override.startingWave(181); + + await game.runToMysteryEncounter(); + + expect(scene.currentBattle.mysteryEncounter).toBeUndefined(); + }); + + it("should not spawn outside of proper biomes", async () => { + game.override.startingBiome(Biome.VOLCANO); + await game.runToMysteryEncounter(); + + expect(game.scene.currentBattle.mysteryEncounter.encounterType).not.toBe(MysteryEncounterType.ABSOLUTE_AVARICE); + }); + + it("should not spawn if player does not have enough berries", async () => { + scene.modifiers = []; + + await game.runToMysteryEncounter(); + + expect(scene.currentBattle?.mysteryEncounter?.encounterType).not.toBe(MysteryEncounterType.ABSOLUTE_AVARICE); + }); + + it("should spawn if player has enough berries", async () => { + game.override.starterHeldItems([{name: "BERRY", count: 2, type: BerryType.SITRUS}, {name: "BERRY", count: 3, type: BerryType.GANLON}]); + + await game.runToMysteryEncounter(); + + expect(scene.currentBattle?.mysteryEncounter?.encounterType).toBe(MysteryEncounterType.ABSOLUTE_AVARICE); + }); + + it("should remove all player's berries at the start of the encounter", async () => { + game.override.starterHeldItems([{name: "BERRY", count: 2, type: BerryType.SITRUS}, {name: "BERRY", count: 3, type: BerryType.GANLON}]); + + await game.runToMysteryEncounter(); + + expect(scene.currentBattle?.mysteryEncounter?.encounterType).toBe(MysteryEncounterType.ABSOLUTE_AVARICE); + expect(scene.modifiers?.length).toBe(0); + }); + + describe("Option 1 - Fight the Greedent", () => { + it("should have the correct properties", () => { + const option1 = AbsoluteAvariceEncounter.options[0]; + expect(option1.optionMode).toBe(MysteryEncounterOptionMode.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 start battle against Greedent", async () => { + const phaseSpy = vi.spyOn(scene, "pushPhase"); + + await game.runToMysteryEncounter(MysteryEncounterType.ABSOLUTE_AVARICE, defaultParty); + await runMysteryEncounterToEnd(game, 1, null, true); + + const enemyField = scene.getEnemyField(); + expect(scene.getCurrentPhase().constructor.name).toBe(CommandPhase.name); + expect(enemyField.length).toBe(1); + expect(enemyField[0].species.speciesId).toBe(Species.GREEDENT); + const moveset = enemyField[0].moveset.map(m => m.moveId); + expect(moveset?.length).toBe(4); + expect(moveset).toEqual([Moves.THRASH, Moves.BODY_PRESS, Moves.STUFF_CHEEKS, Moves.SLACK_OFF]); + + const movePhases = phaseSpy.mock.calls.filter(p => p[0] instanceof MovePhase).map(p => p[0]); + expect(movePhases.length).toBe(1); + expect(movePhases.filter(p => (p as MovePhase).move.moveId === Moves.STUFF_CHEEKS).length).toBe(1); // Stuff Cheeks used before battle + }); + + it("should give reviver seed to each pokemon after battle", async () => { + await game.runToMysteryEncounter(MysteryEncounterType.ABSOLUTE_AVARICE, defaultParty); + await runMysteryEncounterToEnd(game, 1, null, true); + await skipBattleRunMysteryEncounterRewardsPhase(game); + await game.phaseInterceptor.to(SelectModifierPhase, false); + expect(scene.getCurrentPhase().constructor.name).toBe(SelectModifierPhase.name); + + for (const partyPokemon of scene.getParty()) { + const pokemonId = partyPokemon.id; + const pokemonItems = scene.findModifiers(m => m instanceof PokemonHeldItemModifier + && (m as PokemonHeldItemModifier).pokemonId === pokemonId, true) as PokemonHeldItemModifier[]; + const revSeed = pokemonItems.find(i => i.type.name === "Reviver Seed"); + expect(revSeed).toBeDefined; + expect(revSeed.stackCount).toBe(1); + } + }); + }); + + describe("Option 2 - Reason with It", () => { + it("should have the correct properties", () => { + const option = AbsoluteAvariceEncounter.options[1]; + expect(option.optionMode).toBe(MysteryEncounterOptionMode.DEFAULT); + expect(option.dialogue).toBeDefined(); + expect(option.dialogue).toStrictEqual({ + buttonLabel: `${namespace}:option:2:label`, + buttonTooltip: `${namespace}:option:2:tooltip`, + selected: [ + { + text: `${namespace}:option:2:selected`, + }, + ], + }); + }); + + it("Should return 3 (2/5ths floored) berries if 8 were stolen", async () => { + game.override.starterHeldItems([{name: "BERRY", count: 2, type: BerryType.SITRUS}, {name: "BERRY", count: 3, type: BerryType.GANLON}, {name: "BERRY", count: 3, type: BerryType.APICOT}]); + + await game.runToMysteryEncounter(MysteryEncounterType.ABSOLUTE_AVARICE, defaultParty); + + expect(scene.currentBattle?.mysteryEncounter?.encounterType).toBe(MysteryEncounterType.ABSOLUTE_AVARICE); + expect(scene.modifiers?.length).toBe(0); + + await runMysteryEncounterToEnd(game, 2); + + const berriesAfter = scene.findModifiers(m => m instanceof BerryModifier); + const berryCountAfter = berriesAfter.reduce((a, b) => a + b.stackCount, 0); + expect(berriesAfter).toBeDefined(); + expect(berryCountAfter).toBe(3); + }); + + it("Should return 2 (2/5ths floored) berries if 7 were stolen", async () => { + game.override.starterHeldItems([{name: "BERRY", count: 2, type: BerryType.SITRUS}, {name: "BERRY", count: 3, type: BerryType.GANLON}, {name: "BERRY", count: 2, type: BerryType.APICOT}]); + + await game.runToMysteryEncounter(MysteryEncounterType.ABSOLUTE_AVARICE, defaultParty); + + expect(scene.currentBattle?.mysteryEncounter?.encounterType).toBe(MysteryEncounterType.ABSOLUTE_AVARICE); + expect(scene.modifiers?.length).toBe(0); + + await runMysteryEncounterToEnd(game, 2); + + const berriesAfter = scene.findModifiers(m => m instanceof BerryModifier); + const berryCountAfter = berriesAfter.reduce((a, b) => a + b.stackCount, 0); + expect(berriesAfter).toBeDefined(); + expect(berryCountAfter).toBe(2); + }); + + it("should leave encounter without battle", async () => { + const leaveEncounterWithoutBattleSpy = vi.spyOn(EncounterPhaseUtils, "leaveEncounterWithoutBattle"); + + await game.runToMysteryEncounter(MysteryEncounterType.ABSOLUTE_AVARICE, defaultParty); + await runMysteryEncounterToEnd(game, 2); + + expect(leaveEncounterWithoutBattleSpy).toBeCalled(); + }); + }); + + describe("Option 3 - Let it have the food", () => { + it("should have the correct properties", () => { + const option = AbsoluteAvariceEncounter.options[2]; + expect(option.optionMode).toBe(MysteryEncounterOptionMode.DEFAULT); + expect(option.dialogue).toBeDefined(); + expect(option.dialogue).toStrictEqual({ + buttonLabel: `${namespace}:option:3:label`, + buttonTooltip: `${namespace}:option:3:tooltip`, + selected: [ + { + text: `${namespace}:option:3:selected`, + }, + ], + }); + }); + + it("should add Greedent to the party", async () => { + await game.runToMysteryEncounter(MysteryEncounterType.ABSOLUTE_AVARICE, defaultParty); + const partyCountBefore = scene.getParty().length; + + await runMysteryEncounterToEnd(game, 3); + const partyCountAfter = scene.getParty().length; + + expect(partyCountBefore + 1).toBe(partyCountAfter); + const greedent = scene.getParty()[scene.getParty().length - 1]; + expect(greedent.species.speciesId).toBe(Species.GREEDENT); + const moveset = greedent.moveset.map(m => m.moveId); + expect(moveset?.length).toBe(4); + expect(moveset).toEqual([Moves.THRASH, Moves.BODY_PRESS, Moves.STUFF_CHEEKS, Moves.SLACK_OFF]); + }); + + it("should leave encounter without battle", async () => { + const leaveEncounterWithoutBattleSpy = vi.spyOn(EncounterPhaseUtils, "leaveEncounterWithoutBattle"); + + await game.runToMysteryEncounter(MysteryEncounterType.ABSOLUTE_AVARICE, defaultParty); + await runMysteryEncounterToEnd(game, 3); + + expect(leaveEncounterWithoutBattleSpy).toBeCalled(); + }); + }); +}); diff --git a/src/test/mystery-encounter/encounters/delibirdy-encounter.test.ts b/src/test/mystery-encounter/encounters/delibirdy-encounter.test.ts index ee6ce0f6705..9cee29b130d 100644 --- a/src/test/mystery-encounter/encounters/delibirdy-encounter.test.ts +++ b/src/test/mystery-encounter/encounters/delibirdy-encounter.test.ts @@ -11,7 +11,7 @@ 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 { BerryModifier, HealingBoosterModifier, HiddenAbilityRateBoosterModifier, HitHealModifier, 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"; @@ -131,6 +131,28 @@ describe("Delibird-y - Mystery Encounter", () => { expect(itemModifier.stackCount).toBe(1); }); + it("Should give the player a Shell Bell if they have max stacks of Berry Pouches", async () => { + scene.money = 200000; + await game.runToMysteryEncounter(MysteryEncounterType.DELIBIRDY, defaultParty); + + // 5 Healing Charms + scene.modifiers = []; + const abilityCharm = generateModifierTypeOption(scene, modifierTypes.ABILITY_CHARM).type.newModifier() as HiddenAbilityRateBoosterModifier; + abilityCharm.stackCount = 4; + await scene.addModifier(abilityCharm, true, false, false, true); + await scene.updateModifiers(true); + + await runMysteryEncounterToEnd(game, 1); + + const abilityCharmAfter = scene.findModifier(m => m instanceof HiddenAbilityRateBoosterModifier); + const shellBellAfter = scene.findModifier(m => m instanceof HitHealModifier); + + expect(abilityCharmAfter).toBeDefined(); + expect(abilityCharmAfter.stackCount).toBe(4); + expect(shellBellAfter).toBeDefined(); + expect(shellBellAfter.stackCount).toBe(1); + }); + it("should be disabled if player does not have enough money", async () => { scene.money = 0; await game.runToMysteryEncounter(MysteryEncounterType.DELIBIRDY, defaultParty); @@ -221,6 +243,64 @@ describe("Delibird-y - Mystery Encounter", () => { expect(healingCharmAfter.stackCount).toBe(1); }); + it("Should give the player a Shell Bell if they have max stacks of Candy Jars", async () => { + await game.runToMysteryEncounter(MysteryEncounterType.DELIBIRDY, defaultParty); + + // 99 Candy Jars + scene.modifiers = []; + const candyJar = generateModifierTypeOption(scene, modifierTypes.CANDY_JAR).type.newModifier() as LevelIncrementBoosterModifier; + candyJar.stackCount = 99; + await scene.addModifier(candyJar, true, false, false, true); + const sitrus = generateModifierTypeOption(scene, modifierTypes.BERRY, [BerryType.SITRUS]).type; + + // Sitrus berries on party + 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); + const shellBellAfter = scene.findModifier(m => m instanceof HitHealModifier); + + expect(sitrusAfter.stackCount).toBe(1); + expect(candyJarAfter).toBeDefined(); + expect(candyJarAfter.stackCount).toBe(99); + expect(shellBellAfter).toBeDefined(); + expect(shellBellAfter.stackCount).toBe(1); + }); + + it("Should give the player a Shell Bell if they have max stacks of Healing Charms", async () => { + await game.runToMysteryEncounter(MysteryEncounterType.DELIBIRDY, defaultParty); + + // 5 Healing Charms + scene.modifiers = []; + const healingCharm = generateModifierTypeOption(scene, modifierTypes.HEALING_CHARM).type.newModifier() as HealingBoosterModifier; + healingCharm.stackCount = 5; + await scene.addModifier(healingCharm, true, false, false, true); + + // 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}); + + const reviverSeedAfter = scene.findModifier(m => m instanceof PokemonInstantReviveModifier); + const healingCharmAfter = scene.findModifier(m => m instanceof HealingBoosterModifier); + const shellBellAfter = scene.findModifier(m => m instanceof HitHealModifier); + + expect(reviverSeedAfter).toBeUndefined(); + expect(healingCharmAfter).toBeDefined(); + expect(healingCharmAfter.stackCount).toBe(5); + expect(shellBellAfter).toBeDefined(); + expect(shellBellAfter.stackCount).toBe(1); + }); + it("should be disabled if player does not have any proper items", async () => { await game.runToMysteryEncounter(MysteryEncounterType.DELIBIRDY, defaultParty); @@ -325,6 +405,35 @@ describe("Delibird-y - Mystery Encounter", () => { expect(berryPouchAfter.stackCount).toBe(1); }); + it("Should give the player a Shell Bell if they have max stacks of Berry Pouches", async () => { + await game.runToMysteryEncounter(MysteryEncounterType.DELIBIRDY, defaultParty); + + // 5 Healing Charms + scene.modifiers = []; + const healingCharm = generateModifierTypeOption(scene, modifierTypes.BERRY_POUCH).type.newModifier() as PreserveBerryModifier; + healingCharm.stackCount = 3; + await scene.addModifier(healingCharm, true, false, false, true); + + // Set 1 Soul Dew on party lead + 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); + const shellBellAfter = scene.findModifier(m => m instanceof HitHealModifier); + + expect(soulDewAfter).toBeUndefined(); + expect(berryPouchAfter).toBeDefined(); + expect(berryPouchAfter.stackCount).toBe(3); + expect(shellBellAfter).toBeDefined(); + expect(shellBellAfter.stackCount).toBe(1); + }); + it("should be disabled if player does not have any proper items", async () => { await game.runToMysteryEncounter(MysteryEncounterType.DELIBIRDY, defaultParty); diff --git a/src/test/utils/overridesHelper.ts b/src/test/utils/overridesHelper.ts index 400bad716a7..bd4744503e0 100644 --- a/src/test/utils/overridesHelper.ts +++ b/src/test/utils/overridesHelper.ts @@ -8,6 +8,7 @@ import * as overrides from "#app/overrides"; import * as GameMode from "#app/game-mode"; import { GameModes, getGameMode } from "#app/game-mode"; import { MysteryEncounterTier } from "#enums/mystery-encounter-tier"; +import { ModifierOverride } from "#app/overrides"; /** * Helper to handle overrides in tests @@ -117,6 +118,12 @@ export class OverridesHelper { return spy; } + starterHeldItems(modifiers: ModifierOverride[]) { + const spy = vi.spyOn(Overrides, "STARTING_MODIFIER_OVERRIDE", "get").mockReturnValue(modifiers); + this.log(`Starting modifiers set to ${modifiers.map(m => JSON.stringify(m)).join(", ")}!`); + return spy; + } + private log(...params: any[]) { console.log("Overrides:", ...params); } diff --git a/src/test/utils/phaseInterceptor.ts b/src/test/utils/phaseInterceptor.ts index 4e1f2acf586..a98b462006a 100644 --- a/src/test/utils/phaseInterceptor.ts +++ b/src/test/utils/phaseInterceptor.ts @@ -48,6 +48,14 @@ import { PostMysteryEncounterPhase } from "#app/phases/mystery-encounter-phases"; +export interface PromptHandler { + phaseTarget?; + mode?; + callback?; + expireFn?; + awaitingActionInput?; +} + export default class PhaseInterceptor { public scene; public phases = {}; @@ -56,7 +64,7 @@ export default class PhaseInterceptor { private interval; private promptInterval; private intervalRun; - private prompts; + private prompts: PromptHandler[]; private phaseFrom; private inProgress; private originalSetMode; @@ -337,6 +345,7 @@ export default class PhaseInterceptor { * @param mode - The mode of the UI. * @param callback - The callback function to execute. * @param expireFn - The function to determine if the prompt has expired. + * @param awaitingActionInput */ addToNextPrompt(phaseTarget: string, mode: Mode, callback: () => void, expireFn: () => void, awaitingActionInput: boolean = false) { this.prompts.push({