From 3b6e8a86ee549a130002211108b327bd6714a739 Mon Sep 17 00:00:00 2001 From: ImperialSympathizer Date: Thu, 1 Aug 2024 22:36:25 -0400 Subject: [PATCH] add berries abound encounter --- src/battle-scene.ts | 2 +- .../encounters/berries-abound-encounter.ts | 331 ++++++++++++++++++ .../encounters/trash-to-treasure-encounter.ts | 3 +- .../mystery-encounters/mystery-encounter.ts | 1 + .../mystery-encounters/mystery-encounters.ts | 5 +- src/enums/mystery-encounter-type.ts | 5 +- src/locales/en/mystery-encounter.ts | 4 +- .../an-offer-you-cant-refuse-dialogue.ts | 2 +- .../berries-abound-dialogue.ts | 32 ++ .../fight-or-flight-dialogue.ts | 4 +- .../the-strong-stuff-dialogue.ts | 2 +- .../trash-to-treasure-dialogue.ts | 4 +- src/modifier/modifier-type.ts | 19 +- .../berries-abound-encounter.test.ts | 273 +++++++++++++++ .../fight-or-flight-encounter.test.ts | 255 ++++++++++++++ .../trash-to-treasure-encounter.test.ts | 1 - 16 files changed, 922 insertions(+), 21 deletions(-) create mode 100644 src/data/mystery-encounters/encounters/berries-abound-encounter.ts create mode 100644 src/locales/en/mystery-encounters/berries-abound-dialogue.ts create mode 100644 src/test/mystery-encounter/encounters/berries-abound-encounter.test.ts create mode 100644 src/test/mystery-encounter/encounters/fight-or-flight-encounter.test.ts diff --git a/src/battle-scene.ts b/src/battle-scene.ts index 249ddebdd60..2b01b777a64 100644 --- a/src/battle-scene.ts +++ b/src/battle-scene.ts @@ -1430,7 +1430,7 @@ export default class BattleScene extends SceneBase { const wave = waveIndex || this.currentBattle?.waveIndex || 0; this.waveSeed = Utils.shiftCharCodes(this.seed, wave); Phaser.Math.RND.sow([ this.waveSeed ]); - console.log("Wave Seed:", this.waveSeed, wave); + // console.log("Wave Seed:", this.waveSeed, wave); this.rngCounter = 0; } diff --git a/src/data/mystery-encounters/encounters/berries-abound-encounter.ts b/src/data/mystery-encounters/encounters/berries-abound-encounter.ts new file mode 100644 index 00000000000..5f0d180ccab --- /dev/null +++ b/src/data/mystery-encounters/encounters/berries-abound-encounter.ts @@ -0,0 +1,331 @@ +import { BattleStat } from "#app/data/battle-stat"; +import { MysteryEncounterOptionBuilder } from "#app/data/mystery-encounters/mystery-encounter-option"; +import { + EnemyPartyConfig, generateModifierTypeOption, + initBattleWithEnemyConfig, + leaveEncounterWithoutBattle, setEncounterExp, + setEncounterRewards +} from "#app/data/mystery-encounters/utils/encounter-phase-utils"; +import { STEALING_MOVES } from "#app/data/mystery-encounters/requirements/requirement-groups"; +import Pokemon, { EnemyPokemon } from "#app/field/pokemon"; +import { + BerryModifierType, + getPartyLuckValue, + ModifierPoolType, + ModifierTypeOption, modifierTypes, + regenerateModifierPoolThresholds, +} from "#app/modifier/modifier-type"; +import { StatChangePhase } from "#app/phases"; +import { randSeedInt } from "#app/utils"; +import { BattlerTagType } from "#enums/battler-tag-type"; +import { MysteryEncounterType } from "#enums/mystery-encounter-type"; +import BattleScene from "#app/battle-scene"; +import IMysteryEncounter, { MysteryEncounterBuilder } from "../mystery-encounter"; +import { MoveRequirement } from "../mystery-encounter-requirements"; +import { queueEncounterMessage, showEncounterText } from "#app/data/mystery-encounters/utils/encounter-dialogue-utils"; +import { getPokemonNameWithAffix } from "#app/messages"; +import { MysteryEncounterTier } from "#enums/mystery-encounter-tier"; +import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode"; +import { TrainerSlot } from "#app/data/trainer-config"; +import { applyModifierTypeToPlayerPokemon, getSpriteKeysFromPokemon } from "#app/data/mystery-encounters/utils/encounter-pokemon-utils"; +import PokemonData from "#app/system/pokemon-data"; +import { BerryModifier } from "#app/modifier/modifier"; +import i18next from "#app/plugins/i18n"; +import { BerryType } from "#enums/berry-type"; + +/** the i18n namespace for the encounter */ +const namespace = "mysteryEncounter:berriesAbound"; + +/** + * Berries Abound encounter. + * @see {@link https://github.com/AsdarDevelops/PokeRogue-Events/issues/24 | GitHub Issue #24} + * @see For biome requirements check {@linkcode mysteryEncountersByBiome} + */ +export const BerriesAboundEncounter: IMysteryEncounter = + MysteryEncounterBuilder.withEncounterType(MysteryEncounterType.BERRIES_ABOUND) + .withEncounterTier(MysteryEncounterTier.COMMON) + .withSceneWaveRangeRequirement(10, 180) // waves 10 to 180 + .withCatchAllowed(true) + .withHideWildIntroMessage(true) + .withIntroSpriteConfigs([ + { + spriteKey: "lum_berry", + fileRoot: "items", + isItem: true, + x: 7, + y: -14, + disableAnimation: true + }, + { + spriteKey: "salac_berry", + fileRoot: "items", + isItem: true, + x: 2, + y: 4, + disableAnimation: true + }, + { + spriteKey: "lansat_berry", + fileRoot: "items", + isItem: true, + x: 32, + y: 5, + disableAnimation: true + }, + { + spriteKey: "liechi_berry", + fileRoot: "items", + isItem: true, + x: 6, + y: -5, + disableAnimation: true + }, + { + spriteKey: "sitrus_berry", + fileRoot: "items", + isItem: true, + x: 7, + y: 8, + disableAnimation: true + }, + { + spriteKey: "enigma_berry", + fileRoot: "items", + isItem: true, + x: 26, + y: -4, + disableAnimation: true + }, + { + spriteKey: "leppa_berry", + fileRoot: "items", + isItem: true, + x: 16, + y: -27, + disableAnimation: true + }, + { + spriteKey: "petaya_berry", + fileRoot: "items", + isItem: true, + x: 30, + y: -17, + disableAnimation: true + }, + { + spriteKey: "ganlon_berry", + fileRoot: "items", + isItem: true, + x: 16, + y: -11, + disableAnimation: true + }, + { + spriteKey: "apicot_berry", + fileRoot: "items", + isItem: true, + x: 14, + y: -2, + disableAnimation: true + }, + { + spriteKey: "starf_berry", + fileRoot: "items", + isItem: true, + x: 18, + y: 9, + disableAnimation: true + }, + ]) // Set in onInit() + .withIntroDialogue([ + { + text: `${namespace}.intro`, + }, + ]) + .withOnInit((scene: BattleScene) => { + const encounter = scene.currentBattle.mysteryEncounter; + + // Calculate boss mon + const bossSpecies = scene.arena.randomSpecies(scene.currentBattle.waveIndex, scene.currentBattle.waveIndex, 0, getPartyLuckValue(scene.getParty()), true); + const bossPokemon = new EnemyPokemon(scene, bossSpecies, scene.currentBattle.waveIndex, TrainerSlot.NONE, true, null); + const config: EnemyPartyConfig = { + levelAdditiveMultiplier: 1, + pokemonConfigs: [{ + species: bossSpecies, + dataSource: new PokemonData(bossPokemon), + isBoss: true + }], + }; + encounter.enemyPartyConfigs = [config]; + + // Calculate the number of extra berries that player receives + // 10-60 GREAT, 60-110 ULTRA, 110-160 ROGUE, 160-180 MASTER + const numBerries = + scene.currentBattle.waveIndex > 160 ? 7 + : scene.currentBattle.waveIndex > 110 ? 5 + : scene.currentBattle.waveIndex > 60 ? 4 : 2; + regenerateModifierPoolThresholds(scene.getParty(), ModifierPoolType.PLAYER, 0); + encounter.misc = { numBerries }; + + const { spriteKey, fileRoot } = getSpriteKeysFromPokemon(bossPokemon); + encounter.spriteConfigs.push({ + spriteKey: spriteKey, + fileRoot: fileRoot, + hasShadow: true, + tint: 0.25, + x: -5, + repeat: true, + isPokemon: true + }); + + // If player has a stealing move, they succeed automatically + encounter.options[1].meetsRequirements(scene); + const primaryPokemon = encounter.options[1].primaryPokemon; + if (primaryPokemon) { + // Use primaryPokemon to execute the thievery + encounter.options[1].dialogue.buttonTooltip = `${namespace}.option.2.tooltip_special`; + } else { + encounter.options[1].dialogue.buttonTooltip = `${namespace}.option.2.tooltip`; + } + + return true; + }) + .withTitle(`${namespace}.title`) + .withDescription(`${namespace}.description`) + .withQuery(`${namespace}.query`) + .withSimpleOption( + { + buttonLabel: `${namespace}.option.1.label`, + buttonTooltip: `${namespace}.option.1.tooltip`, + selected: [ + { + text: `${namespace}.option.1.selected`, + }, + ], + }, + async (scene: BattleScene) => { + // Pick battle + const encounter = scene.currentBattle.mysteryEncounter; + const numBerries = encounter.misc.numBerries; + + const doBerryRewards = async () => { + const berryText = numBerries + " " + i18next.t(`${namespace}.berries`); + + scene.playSound("item_fanfare"); + queueEncounterMessage(scene, i18next.t("battle:rewardGain", { modifierName: berryText })); + + // Generate a random berry and give it to the first Pokemon with room for it + for (let i = 0; i < numBerries; i++) { + await tryGiveBerry(scene); + } + }; + + const shopOptions: ModifierTypeOption[] = []; + for (let i = 0; i < 5; i++) { + // Generate shop berries + shopOptions.push(generateModifierTypeOption(scene, modifierTypes.BERRY)); + } + + setEncounterRewards(scene, { guaranteedModifierTypeOptions: shopOptions, fillRemaining: false }, null, doBerryRewards); + await initBattleWithEnemyConfig(scene, scene.currentBattle.mysteryEncounter.enemyPartyConfigs[0]); + } + ) + .withOption( + new MysteryEncounterOptionBuilder() + .withOptionMode(MysteryEncounterOptionMode.DEFAULT_OR_SPECIAL) + .withPrimaryPokemonRequirement(new MoveRequirement(STEALING_MOVES)) // Will set option2PrimaryName and option2PrimaryMove dialogue tokens automatically + .withDialogue({ + buttonLabel: `${namespace}.option.2.label`, + buttonTooltip: `${namespace}.option.2.tooltip`, + }) + .withOptionPhase(async (scene: BattleScene) => { + // Pick steal + const encounter = scene.currentBattle.mysteryEncounter; + const numBerries = encounter.misc.numBerries; + + const doBerryRewards = async () => { + const berryText = numBerries + " " + i18next.t(`${namespace}.berries`); + + scene.playSound("item_fanfare"); + queueEncounterMessage(scene, i18next.t("battle:rewardGain", { modifierName: berryText })); + + // Generate a random berry and give it to the first Pokemon with room for it + for (let i = 0; i < numBerries; i++) { + await tryGiveBerry(scene); + } + }; + + const shopOptions: ModifierTypeOption[] = []; + for (let i = 0; i < 5; i++) { + // Generate shop berries + shopOptions.push(generateModifierTypeOption(scene, modifierTypes.BERRY)); + } + + setEncounterRewards(scene, { guaranteedModifierTypeOptions: shopOptions, fillRemaining: false }, null, doBerryRewards); + + // If player has a stealing move, they succeed automatically + const primaryPokemon = encounter.options[1].primaryPokemon; + if (primaryPokemon) { + // Use primaryPokemon to execute the thievery + await showEncounterText(scene, `${namespace}.option.2.special_result`); + setEncounterExp(scene, primaryPokemon.id, encounter.enemyPartyConfigs[0].pokemonConfigs[0].species.baseExp, true); + leaveEncounterWithoutBattle(scene); + return; + } + + const roll = randSeedInt(16); + if (roll > 6) { + // Noticed and attacked by boss, gets +1 to all stats at start of fight (62.5%) + const config = scene.currentBattle.mysteryEncounter.enemyPartyConfigs[0]; + config.pokemonConfigs[0].tags = [BattlerTagType.MYSTERY_ENCOUNTER_POST_SUMMON]; + config.pokemonConfigs[0].mysteryEncounterBattleEffects = (pokemon: Pokemon) => { + pokemon.scene.currentBattle.mysteryEncounter.setDialogueToken("enemyPokemon", getPokemonNameWithAffix(pokemon)); + queueEncounterMessage(pokemon.scene, `${namespace}option.2.boss_enraged`); + pokemon.scene.unshiftPhase(new StatChangePhase(pokemon.scene, pokemon.getBattlerIndex(), true, [BattleStat.ATK, BattleStat.DEF, BattleStat.SPATK, BattleStat.SPDEF, BattleStat.SPD], 1)); + }; + await showEncounterText(scene, `${namespace}.option.2.bad_result`); + await initBattleWithEnemyConfig(scene, config); + } else { + // Steal item (37.5%) + // Display result message then proceed to rewards + await showEncounterText(scene, `${namespace}.option.2.good_result`); + leaveEncounterWithoutBattle(scene); + } + }) + .build() + ) + .withSimpleOption( + { + buttonLabel: `${namespace}.option.3.label`, + buttonTooltip: `${namespace}.option.3.tooltip`, + selected: [ + { + text: `${namespace}.option.3.selected`, + }, + ], + }, + async (scene: BattleScene) => { + // Leave encounter with no rewards or exp + leaveEncounterWithoutBattle(scene, true); + return true; + } + ) + .build(); + +async function tryGiveBerry(scene: BattleScene) { + const berryType = randSeedInt(Object.keys(BerryType).filter(s => !isNaN(Number(s))).length) as BerryType; + const berry = generateModifierTypeOption(scene, modifierTypes.BERRY, [berryType]).type as BerryModifierType; + + const party = scene.getParty(); + + // Iterate over the party until berry was successfully given + for (const pokemon of party) { + const heldBerriesOfType = scene.findModifier(m => m instanceof BerryModifier + && m.pokemonId === pokemon.id && (m as BerryModifier).berryType === berryType, true) as BerryModifier; + + if (!heldBerriesOfType || heldBerriesOfType.getStackCount() < heldBerriesOfType.getMaxStackCount(scene)) { + await applyModifierTypeToPlayerPokemon(scene, pokemon, berry); + break; + } + } +} diff --git a/src/data/mystery-encounters/encounters/trash-to-treasure-encounter.ts b/src/data/mystery-encounters/encounters/trash-to-treasure-encounter.ts index 7fa433b17af..0ea9d42ca63 100644 --- a/src/data/mystery-encounters/encounters/trash-to-treasure-encounter.ts +++ b/src/data/mystery-encounters/encounters/trash-to-treasure-encounter.ts @@ -40,7 +40,8 @@ export const TrashToTreasureEncounter: IMysteryEncounter = hasShadow: false, disableAnimation: true, scale: 1.5, - y: 8 + y: 8, + tint: 0.4 } ]) .withAutoHideIntroVisuals(false) diff --git a/src/data/mystery-encounters/mystery-encounter.ts b/src/data/mystery-encounters/mystery-encounter.ts index 8eababcfdeb..be496fe1133 100644 --- a/src/data/mystery-encounters/mystery-encounter.ts +++ b/src/data/mystery-encounters/mystery-encounter.ts @@ -162,6 +162,7 @@ export default class IMysteryEncounter implements IMysteryEncounter { } this.encounterTier = this.encounterTier ?? MysteryEncounterTier.COMMON; this.dialogue = this.dialogue ?? {}; + this.spriteConfigs = this.spriteConfigs ? [...this.spriteConfigs] : []; // Default max is 1 for ROGUE encounters, 3 for others this.maxAllowedEncounters = this.maxAllowedEncounters ?? this.encounterTier === MysteryEncounterTier.ROGUE ? 1 : 3; this.encounterMode = MysteryEncounterMode.DEFAULT; diff --git a/src/data/mystery-encounters/mystery-encounters.ts b/src/data/mystery-encounters/mystery-encounters.ts index 3a9c411830b..6d49e6d014f 100644 --- a/src/data/mystery-encounters/mystery-encounters.ts +++ b/src/data/mystery-encounters/mystery-encounters.ts @@ -20,6 +20,7 @@ import { DelibirdyEncounter } from "#app/data/mystery-encounters/encounters/deli import { AbsoluteAvariceEncounter } from "#app/data/mystery-encounters/encounters/absolute-avarice-encounter"; import { ATrainersTestEncounter } from "#app/data/mystery-encounters/encounters/a-trainers-test-encounter"; import { TrashToTreasureEncounter } from "#app/data/mystery-encounters/encounters/trash-to-treasure-encounter"; +import { BerriesAboundEncounter } from "#app/data/mystery-encounters/encounters/berries-abound-encounter"; // Spawn chance: (BASE_MYSTERY_ENCOUNTER_SPAWN_WEIGHT + WIGHT_INCREMENT_ON_SPAWN_MISS * ) / 256 export const BASE_MYSTERY_ENCOUNTER_SPAWN_WEIGHT = 1; @@ -157,7 +158,8 @@ const anyBiomeEncounters: MysteryEncounterType[] = [ MysteryEncounterType.TRAINING_SESSION, MysteryEncounterType.DELIBIRDY, MysteryEncounterType.A_TRAINERS_TEST, - MysteryEncounterType.TRASH_TO_TREASURE + MysteryEncounterType.TRASH_TO_TREASURE, + MysteryEncounterType.BERRIES_ABOUND ]; /** @@ -246,6 +248,7 @@ export function initMysteryEncounters() { allMysteryEncounters[MysteryEncounterType.ABSOLUTE_AVARICE] = AbsoluteAvariceEncounter; allMysteryEncounters[MysteryEncounterType.A_TRAINERS_TEST] = ATrainersTestEncounter; allMysteryEncounters[MysteryEncounterType.TRASH_TO_TREASURE] = TrashToTreasureEncounter; + allMysteryEncounters[MysteryEncounterType.BERRIES_ABOUND] = BerriesAboundEncounter; // Add extreme encounters to biome map extremeBiomeEncounters.forEach(encounter => { diff --git a/src/enums/mystery-encounter-type.ts b/src/enums/mystery-encounter-type.ts index de4ab055e89..6cf2c1b4fb2 100644 --- a/src/enums/mystery-encounter-type.ts +++ b/src/enums/mystery-encounter-type.ts @@ -9,7 +9,7 @@ export enum MysteryEncounterType { SHADY_VITAMIN_DEALER, FIELD_TRIP, SAFARI_ZONE, - LOST_AT_SEA, //might be generalized later on + LOST_AT_SEA, // Might be generalized later on FIERY_FALLOUT, THE_STRONG_STUFF, THE_POKEMON_SALESMAN, @@ -17,5 +17,6 @@ export enum MysteryEncounterType { DELIBIRDY, ABSOLUTE_AVARICE, A_TRAINERS_TEST, - TRASH_TO_TREASURE + TRASH_TO_TREASURE, + BERRIES_ABOUND } diff --git a/src/locales/en/mystery-encounter.ts b/src/locales/en/mystery-encounter.ts index 6a47b84c590..099802a5f53 100644 --- a/src/locales/en/mystery-encounter.ts +++ b/src/locales/en/mystery-encounter.ts @@ -17,6 +17,7 @@ import { delibirdyDialogue } from "#app/locales/en/mystery-encounters/delibirdy- import { absoluteAvariceDialogue } from "#app/locales/en/mystery-encounters/absolute-avarice-dialogue"; import { aTrainersTestDialogue } from "#app/locales/en/mystery-encounters/a-trainers-test-dialogue"; import { trashToTreasureDialogue } from "#app/locales/en/mystery-encounters/trash-to-treasure-dialogue"; +import { berriesAboundDialogue } from "#app/locales/en/mystery-encounters/berries-abound-dialogue"; /** * Patterns that can be used: @@ -58,5 +59,6 @@ export const mysteryEncounter = { delibirdy: delibirdyDialogue, absoluteAvarice: absoluteAvariceDialogue, aTrainersTest: aTrainersTestDialogue, - trashToTreasure: trashToTreasureDialogue + trashToTreasure: trashToTreasureDialogue, + berriesAbound: berriesAboundDialogue } as const; diff --git a/src/locales/en/mystery-encounters/an-offer-you-cant-refuse-dialogue.ts b/src/locales/en/mystery-encounters/an-offer-you-cant-refuse-dialogue.ts index 00ca2f5f309..1148b5c1425 100644 --- a/src/locales/en/mystery-encounters/an-offer-you-cant-refuse-dialogue.ts +++ b/src/locales/en/mystery-encounters/an-offer-you-cant-refuse-dialogue.ts @@ -6,7 +6,7 @@ export const anOfferYouCantRefuseDialogue = { $I've always wanted to have a pet like that! $I'd pay you handsomely,\nand also give you this old bauble!`, title: "An Offer You Can't Refuse", - description: "You're being offered a @[TOOLTIP_TITLE]{Shiny Charm} and {{price, money}} for your {{strongestPokemon}}!\n\nIt's is an extremely good deal, but can you really bear to part with such a strong team member?", + description: "You're being offered a @[TOOLTIP_TITLE]{Shiny Charm} and {{price, money}} for your {{strongestPokemon}}!\n\nIt's an extremely good deal, but can you really bear to part with such a strong team member?", query: "What will you do?", option: { 1: { diff --git a/src/locales/en/mystery-encounters/berries-abound-dialogue.ts b/src/locales/en/mystery-encounters/berries-abound-dialogue.ts new file mode 100644 index 00000000000..803da26406d --- /dev/null +++ b/src/locales/en/mystery-encounters/berries-abound-dialogue.ts @@ -0,0 +1,32 @@ +export const berriesAboundDialogue = { + intro: "There's a huge berry bush\nnear that Pokémon!", + title: "Berries Abound", + description: "It looks like there's a strong Pokémon guarding a berry bush. Battling is the straightforward approach, but this Pokémon looks strong. You could also try to sneak around, though the Pokémon might catch you.", + query: "What will you do?", + berries: "Berries!", + option: { + 1: { + label: "Battle the Pokémon", + tooltip: "(-) Hard Battle\n(+) New Item", + selected: "You approach the\nPokémon without fear.", + }, + 2: { + label: "Steal the Berries", + tooltip: "@[SUMMARY_GREEN]{(35%) Steal Item}\n@[SUMMARY_BLUE]{(65%) Harder Battle}", + tooltip_special: "(+) {{option2PrimaryName}} uses {{option2PrimaryMove}}", + good_result: `.@d{32}.@d{32}.@d{32} + $You manage to sneak your way\npast and grab the berries!`, + special_result: `.@d{32}.@d{32}.@d{32} + $Your {{option2PrimaryName}} helps you out and uses {{option2PrimaryMove}}! + $You nabbed the berries!`, + bad_result: `.@d{32}.@d{32}.@d{32} + $The Pokémon catches you\nas you try to sneak around!`, + boss_enraged: "The opposing {{enemyPokemon}} has become enraged!" + }, + 3: { + label: "Leave", + tooltip: "(-) No Rewards", + selected: "You leave the strong Pokémon\nwith its prize and continue on.", + }, + } +}; diff --git a/src/locales/en/mystery-encounters/fight-or-flight-dialogue.ts b/src/locales/en/mystery-encounters/fight-or-flight-dialogue.ts index a7b8b03aab5..1eb4a984b21 100644 --- a/src/locales/en/mystery-encounters/fight-or-flight-dialogue.ts +++ b/src/locales/en/mystery-encounters/fight-or-flight-dialogue.ts @@ -10,14 +10,14 @@ export const fightOrFlightDialogue = { selected: "You approach the\nPokémon without fear.", }, 2: { - label: "Steal the item", + label: "Steal the Item", tooltip: "@[SUMMARY_GREEN]{(35%) Steal Item}\n@[SUMMARY_BLUE]{(65%) Harder Battle}", tooltip_special: "(+) {{option2PrimaryName}} uses {{option2PrimaryMove}}", good_result: `.@d{32}.@d{32}.@d{32} $You manage to sneak your way\npast and grab the item!`, special_result: `.@d{32}.@d{32}.@d{32} $Your {{option2PrimaryName}} helps you out and uses {{option2PrimaryMove}}! - $ You nabbed the item!`, + $You nabbed the item!`, bad_result: `.@d{32}.@d{32}.@d{32} $The Pokémon catches you\nas you try to sneak around!`, boss_enraged: "The opposing {{enemyPokemon}} has become enraged!" diff --git a/src/locales/en/mystery-encounters/the-strong-stuff-dialogue.ts b/src/locales/en/mystery-encounters/the-strong-stuff-dialogue.ts index c1a35c4f68a..baaca049f08 100644 --- a/src/locales/en/mystery-encounters/the-strong-stuff-dialogue.ts +++ b/src/locales/en/mystery-encounters/the-strong-stuff-dialogue.ts @@ -10,7 +10,7 @@ export const theStrongStuffDialogue = { selected: "You black out.", selected_2: `@f{150}When you awaken, the Shuckle is gone\nand juice stash completely drained. $Your {{highBstPokemon}} feels a\nterrible lethargy come over it! - $It's base stats were reduced by 20 in each stat! + $Its base stats were reduced by 20 in each stat! $Your remaining Pokémon feel an incredible vigor, though! $Their base stats are increased by 10 in each stat!` }, diff --git a/src/locales/en/mystery-encounters/trash-to-treasure-dialogue.ts b/src/locales/en/mystery-encounters/trash-to-treasure-dialogue.ts index a9e369270ad..9242058b128 100644 --- a/src/locales/en/mystery-encounters/trash-to-treasure-dialogue.ts +++ b/src/locales/en/mystery-encounters/trash-to-treasure-dialogue.ts @@ -6,7 +6,7 @@ export const trashToTreasureDialogue = { option: { 1: { label: "Dig for Valuables", - tooltip: "(-) Become Covered in Filth\n(+) Gain Amazing Items", + tooltip: "(-) Lose Healing Items in Shops\n(+) Gain Amazing Items", selected: `You wade through the garbage pile, becoming mired in filth. $There's no way any respectable shopkeepers\nwill sell you anything in your grimy state! $You'll just have to make do without shop healing items. @@ -16,7 +16,7 @@ export const trashToTreasureDialogue = { label: "Investigate Further", tooltip: "(?) Find the Source of the Garbage", selected: "You wander around the heap, searching for any indication as to how this might have appeared here...", - selected_2: "Suddenly, the garbage shifts! It wasn't just garbage, it's a Pokémon!" + selected_2: "Suddenly, the garbage shifts! It wasn't just garbage, it was a Pokémon!" }, }, }; diff --git a/src/modifier/modifier-type.ts b/src/modifier/modifier-type.ts index 3e52f080c94..f430a0a118b 100644 --- a/src/modifier/modifier-type.ts +++ b/src/modifier/modifier-type.ts @@ -127,15 +127,18 @@ export class ModifierType { */ 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 + + for (const weightedModifiers of Object.values(modifierPool)) { + for (const mod of weightedModifiers) { + if (mod.modifierType.id === this.id) { + this.tier = mod.modifierType.tier; + return this; } - }); - return !this.tier; // Early lookup return if tier found - }); + } + } + + // Fallback to COMMON tier if no tier found + this.tier = ModifierTier.COMMON; return this; } diff --git a/src/test/mystery-encounter/encounters/berries-abound-encounter.test.ts b/src/test/mystery-encounter/encounters/berries-abound-encounter.test.ts new file mode 100644 index 00000000000..9bd17702f74 --- /dev/null +++ b/src/test/mystery-encounter/encounters/berries-abound-encounter.test.ts @@ -0,0 +1,273 @@ +import * as MysteryEncounters from "#app/data/mystery-encounters/mystery-encounters"; +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 { runMysteryEncounterToEnd, skipBattleRunMysteryEncounterRewardsPhase } from "#test/mystery-encounter/encounterTestUtils"; +import { CommandPhase, SelectModifierPhase } from "#app/phases"; +import { Moves } from "#enums/moves"; +import BattleScene from "#app/battle-scene"; +import { PokemonMove } from "#app/field/pokemon"; +import { Mode } from "#app/ui/ui"; +import ModifierSelectUiHandler from "#app/ui/modifier-select-ui-handler"; +import { BerryModifier } from "#app/modifier/modifier"; +import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode"; +import { MysteryEncounterTier } from "#enums/mystery-encounter-tier"; +import { initSceneWithoutEncounterPhase } from "#test/utils/gameManagerUtils"; +import { BerriesAboundEncounter } from "#app/data/mystery-encounters/encounters/berries-abound-encounter"; +import * as Utils from "utils"; +import { isNullOrUndefined } from "utils"; +import * as EncounterPhaseUtils from "#app/data/mystery-encounters/utils/encounter-phase-utils"; +import * as EncounterDialogueUtils from "#app/data/mystery-encounters/utils/encounter-dialogue-utils"; + +const namespace = "mysteryEncounter:berriesAbound"; +const defaultParty = [Species.LAPRAS, Species.GENGAR, Species.ABRA]; +const defaultBiome = Biome.CAVE; +const defaultWave = 45; + +describe("Berries Abound - Mystery Encounter", () => { + let phaserGame: Phaser.Game; + let game: GameManager; + let scene: BattleScene; + + beforeAll(() => { + phaserGame = new Phaser.Game({ type: Phaser.HEADLESS }); + }); + + beforeEach(async () => { + game = new GameManager(phaserGame); + scene = game.scene; + game.override.mysteryEncounterChance(100); + game.override.mysteryEncounterTier(MysteryEncounterTier.COMMON); + game.override.startingWave(defaultWave); + game.override.startingBiome(defaultBiome); + + vi.spyOn(MysteryEncounters, "mysteryEncountersByBiome", "get").mockReturnValue( + new Map([ + [Biome.CAVE, [MysteryEncounterType.BERRIES_ABOUND]], + ]) + ); + }); + + afterEach(() => { + game.phaseInterceptor.restoreOg(); + vi.clearAllMocks(); + vi.resetAllMocks(); + }); + + it("should have the correct properties", async () => { + await game.runToMysteryEncounter(MysteryEncounterType.BERRIES_ABOUND, defaultParty); + + expect(BerriesAboundEncounter.encounterType).toBe(MysteryEncounterType.BERRIES_ABOUND); + expect(BerriesAboundEncounter.encounterTier).toBe(MysteryEncounterTier.COMMON); + expect(BerriesAboundEncounter.dialogue).toBeDefined(); + expect(BerriesAboundEncounter.dialogue.intro).toStrictEqual([{ text: `${namespace}.intro` }]); + expect(BerriesAboundEncounter.dialogue.encounterOptionsDialogue.title).toBe(`${namespace}.title`); + expect(BerriesAboundEncounter.dialogue.encounterOptionsDialogue.description).toBe(`${namespace}.description`); + expect(BerriesAboundEncounter.dialogue.encounterOptionsDialogue.query).toBe(`${namespace}.query`); + expect(BerriesAboundEncounter.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.BERRIES_ABOUND); + }); + + it("should not run above wave 179", async () => { + game.override.startingWave(181); + + await game.runToMysteryEncounter(); + + expect(scene.currentBattle.mysteryEncounter).toBeUndefined(); + }); + + it("should initialize fully", async () => { + initSceneWithoutEncounterPhase(scene, defaultParty); + scene.currentBattle.mysteryEncounter = BerriesAboundEncounter; + + const { onInit } = BerriesAboundEncounter; + + expect(BerriesAboundEncounter.onInit).toBeDefined(); + + BerriesAboundEncounter.populateDialogueTokensFromRequirements(scene); + const onInitResult = onInit(scene); + + const config = BerriesAboundEncounter.enemyPartyConfigs[0]; + expect(config).toBeDefined(); + expect(config.levelAdditiveMultiplier).toBe(1); + expect(config.pokemonConfigs[0].isBoss).toBe(true); + expect(onInitResult).toBe(true); + }); + + describe("Option 1 - Fight", () => { + it("should have the correct properties", () => { + const option1 = BerriesAboundEncounter.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 a fight against the boss", async () => { + await game.runToMysteryEncounter(MysteryEncounterType.BERRIES_ABOUND, defaultParty); + + const config = game.scene.currentBattle.mysteryEncounter.enemyPartyConfigs[0]; + const speciesToSpawn = config.pokemonConfigs[0].species.speciesId; + + 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(speciesToSpawn); + }); + + it("should reward the player with X berries based on wave", async () => { + await game.runToMysteryEncounter(MysteryEncounterType.BERRIES_ABOUND, defaultParty); + + const numBerries = game.scene.currentBattle.mysteryEncounter.misc.numBerries; + + await runMysteryEncounterToEnd(game, 1, null, true); + await skipBattleRunMysteryEncounterRewardsPhase(game); + await game.phaseInterceptor.to(SelectModifierPhase, false); + expect(scene.getCurrentPhase().constructor.name).toBe(SelectModifierPhase.name); + + const berriesAfter = scene.findModifiers(m => m instanceof BerryModifier) as BerryModifier[]; + const berriesAfterCount = berriesAfter.reduce((a, b) => a + b.stackCount, 0); + + expect(numBerries).toBe(berriesAfterCount); + }); + + it("should spawn a shop with 5 berries", async () => { + await game.runToMysteryEncounter(MysteryEncounterType.BERRIES_ABOUND, defaultParty); + await runMysteryEncounterToEnd(game, 1, null, true); + await skipBattleRunMysteryEncounterRewardsPhase(game); + await game.phaseInterceptor.to(SelectModifierPhase, false); + expect(scene.getCurrentPhase().constructor.name).toBe(SelectModifierPhase.name); + await game.phaseInterceptor.run(SelectModifierPhase); + + expect(scene.ui.getMode()).to.equal(Mode.MODIFIER_SELECT); + const modifierSelectHandler = scene.ui.handlers.find(h => h instanceof ModifierSelectUiHandler) as ModifierSelectUiHandler; + expect(modifierSelectHandler.options.length).toEqual(5); + for (const option of modifierSelectHandler.options) { + expect(option.modifierTypeOption.type.id).toContain("BERRY"); + } + }); + }); + + describe("Option 2 - Attempt to Steal", () => { + it("should have the correct properties", () => { + const option1 = BerriesAboundEncounter.options[1]; + expect(option1.optionMode).toBe(MysteryEncounterOptionMode.DEFAULT_OR_SPECIAL); + expect(option1.dialogue).toBeDefined(); + expect(option1.dialogue).toStrictEqual({ + buttonLabel: `${namespace}.option.2.label`, + buttonTooltip: `${namespace}.option.2.tooltip`, + }); + }); + + it("should start battle on failing to steal", async () => { + await game.runToMysteryEncounter(MysteryEncounterType.BERRIES_ABOUND, defaultParty); + + const config = game.scene.currentBattle.mysteryEncounter.enemyPartyConfigs[0]; + const speciesToSpawn = config.pokemonConfigs[0].species.speciesId; + + const realFn = Utils.randSeedInt; + vi.spyOn(Utils, "randSeedInt").mockImplementation((range, min) => { + if (range === 16 && isNullOrUndefined(min)) { + // Mock the steal roll + return 12; + } else { + return realFn(range, min); + } + }); + + await runMysteryEncounterToEnd(game, 2, 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(speciesToSpawn); + + // Should be enraged + expect(enemyField[0].summonData.battleStats).toEqual([1, 1, 1, 1, 1, 0, 0]); + }); + + it("Should skip battle when succeed on steal", async () => { + const leaveEncounterWithoutBattleSpy = vi.spyOn(EncounterPhaseUtils, "leaveEncounterWithoutBattle"); + + await game.runToMysteryEncounter(MysteryEncounterType.BERRIES_ABOUND, defaultParty); + + const realFn = Utils.randSeedInt; + vi.spyOn(Utils, "randSeedInt").mockImplementation((range, min) => { + if (range === 16 && isNullOrUndefined(min)) { + // Mock the steal roll + return 6; + } else { + return realFn(range, min); + } + }); + + await runMysteryEncounterToEnd(game, 2); + await game.phaseInterceptor.to(SelectModifierPhase, false); + expect(scene.getCurrentPhase().constructor.name).toBe(SelectModifierPhase.name); + await game.phaseInterceptor.run(SelectModifierPhase); + + expect(scene.ui.getMode()).to.equal(Mode.MODIFIER_SELECT); + const modifierSelectHandler = scene.ui.handlers.find(h => h instanceof ModifierSelectUiHandler) as ModifierSelectUiHandler; + expect(modifierSelectHandler.options.length).toEqual(5); + for (const option of modifierSelectHandler.options) { + expect(option.modifierTypeOption.type.id).toContain("BERRY"); + } + + expect(leaveEncounterWithoutBattleSpy).toBeCalled(); + }); + + it("Should skip fight when special requirements are met", async () => { + const leaveEncounterWithoutBattleSpy = vi.spyOn(EncounterPhaseUtils, "leaveEncounterWithoutBattle"); + const encounterTextSpy = vi.spyOn(EncounterDialogueUtils, "showEncounterText"); + + await game.runToMysteryEncounter(MysteryEncounterType.BERRIES_ABOUND, defaultParty); + + // Mock moveset + scene.getParty()[0].moveset = [new PokemonMove(Moves.KNOCK_OFF)]; + + await runMysteryEncounterToEnd(game, 2); + await game.phaseInterceptor.to(SelectModifierPhase, false); + expect(scene.getCurrentPhase().constructor.name).toBe(SelectModifierPhase.name); + await game.phaseInterceptor.run(SelectModifierPhase); + expect(scene.ui.getMode()).to.equal(Mode.MODIFIER_SELECT); + + const modifierSelectHandler = scene.ui.handlers.find(h => h instanceof ModifierSelectUiHandler) as ModifierSelectUiHandler; + expect(modifierSelectHandler.options.length).toEqual(5); + for (const option of modifierSelectHandler.options) { + expect(option.modifierTypeOption.type.id).toContain("BERRY"); + } + + expect(encounterTextSpy).toHaveBeenCalledWith(expect.any(BattleScene), `${namespace}.option.2.special_result`); + expect(leaveEncounterWithoutBattleSpy).toBeCalled(); + }); + }); + + describe("Option 3 - Leave", () => { + it("should leave encounter without battle", async () => { + const leaveEncounterWithoutBattleSpy = vi.spyOn(EncounterPhaseUtils, "leaveEncounterWithoutBattle"); + + await game.runToMysteryEncounter(MysteryEncounterType.BERRIES_ABOUND, defaultParty); + await runMysteryEncounterToEnd(game, 3); + + expect(leaveEncounterWithoutBattleSpy).toBeCalled(); + }); + }); +}); diff --git a/src/test/mystery-encounter/encounters/fight-or-flight-encounter.test.ts b/src/test/mystery-encounter/encounters/fight-or-flight-encounter.test.ts new file mode 100644 index 00000000000..70978c90834 --- /dev/null +++ b/src/test/mystery-encounter/encounters/fight-or-flight-encounter.test.ts @@ -0,0 +1,255 @@ +import * as MysteryEncounters from "#app/data/mystery-encounters/mystery-encounters"; +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 { runMysteryEncounterToEnd, skipBattleRunMysteryEncounterRewardsPhase } from "#test/mystery-encounter/encounterTestUtils"; +import { CommandPhase, SelectModifierPhase } from "#app/phases"; +import { Moves } from "#enums/moves"; +import BattleScene from "#app/battle-scene"; +import { PokemonMove } from "#app/field/pokemon"; +import { Mode } from "#app/ui/ui"; +import ModifierSelectUiHandler from "#app/ui/modifier-select-ui-handler"; +import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode"; +import { MysteryEncounterTier } from "#enums/mystery-encounter-tier"; +import { initSceneWithoutEncounterPhase } from "#test/utils/gameManagerUtils"; +import * as Utils from "utils"; +import { isNullOrUndefined } from "utils"; +import * as EncounterPhaseUtils from "#app/data/mystery-encounters/utils/encounter-phase-utils"; +import * as EncounterDialogueUtils from "#app/data/mystery-encounters/utils/encounter-dialogue-utils"; +import { FightOrFlightEncounter } from "#app/data/mystery-encounters/encounters/fight-or-flight-encounter"; + +const namespace = "mysteryEncounter:fightOrFlight"; +const defaultParty = [Species.LAPRAS, Species.GENGAR, Species.ABRA]; +const defaultBiome = Biome.CAVE; +const defaultWave = 45; + +describe("Fight or Flight - Mystery Encounter", () => { + let phaserGame: Phaser.Game; + let game: GameManager; + let scene: BattleScene; + + beforeAll(() => { + phaserGame = new Phaser.Game({ type: Phaser.HEADLESS }); + }); + + beforeEach(async () => { + game = new GameManager(phaserGame); + scene = game.scene; + game.override.mysteryEncounterChance(100); + game.override.mysteryEncounterTier(MysteryEncounterTier.COMMON); + game.override.startingWave(defaultWave); + game.override.startingBiome(defaultBiome); + + vi.spyOn(MysteryEncounters, "mysteryEncountersByBiome", "get").mockReturnValue( + new Map([ + [Biome.CAVE, [MysteryEncounterType.FIGHT_OR_FLIGHT]], + ]) + ); + }); + + afterEach(() => { + game.phaseInterceptor.restoreOg(); + vi.clearAllMocks(); + vi.resetAllMocks(); + }); + + it("should have the correct properties", async () => { + await game.runToMysteryEncounter(MysteryEncounterType.FIGHT_OR_FLIGHT, defaultParty); + + expect(FightOrFlightEncounter.encounterType).toBe(MysteryEncounterType.FIGHT_OR_FLIGHT); + expect(FightOrFlightEncounter.encounterTier).toBe(MysteryEncounterTier.COMMON); + expect(FightOrFlightEncounter.dialogue).toBeDefined(); + expect(FightOrFlightEncounter.dialogue.intro).toStrictEqual([{ text: `${namespace}.intro` }]); + expect(FightOrFlightEncounter.dialogue.encounterOptionsDialogue.title).toBe(`${namespace}.title`); + expect(FightOrFlightEncounter.dialogue.encounterOptionsDialogue.description).toBe(`${namespace}.description`); + expect(FightOrFlightEncounter.dialogue.encounterOptionsDialogue.query).toBe(`${namespace}.query`); + expect(FightOrFlightEncounter.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.FIGHT_OR_FLIGHT); + }); + + it("should not run above wave 179", async () => { + game.override.startingWave(181); + + await game.runToMysteryEncounter(); + + expect(scene.currentBattle.mysteryEncounter).toBeUndefined(); + }); + + it("should initialize fully", async () => { + initSceneWithoutEncounterPhase(scene, defaultParty); + scene.currentBattle.mysteryEncounter = FightOrFlightEncounter; + + const { onInit } = FightOrFlightEncounter; + + expect(FightOrFlightEncounter.onInit).toBeDefined(); + + FightOrFlightEncounter.populateDialogueTokensFromRequirements(scene); + const onInitResult = onInit(scene); + + const config = FightOrFlightEncounter.enemyPartyConfigs[0]; + expect(config).toBeDefined(); + expect(config.levelAdditiveMultiplier).toBe(1); + expect(config.pokemonConfigs[0].isBoss).toBe(true); + expect(onInitResult).toBe(true); + }); + + describe("Option 1 - Fight", () => { + it("should have the correct properties", () => { + const option1 = FightOrFlightEncounter.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 a fight against the boss", async () => { + await game.runToMysteryEncounter(MysteryEncounterType.FIGHT_OR_FLIGHT, defaultParty); + + const config = game.scene.currentBattle.mysteryEncounter.enemyPartyConfigs[0]; + const speciesToSpawn = config.pokemonConfigs[0].species.speciesId; + + 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(speciesToSpawn); + }); + + it("should reward the player with the item based on wave", async () => { + await game.runToMysteryEncounter(MysteryEncounterType.FIGHT_OR_FLIGHT, defaultParty); + + const item = game.scene.currentBattle.mysteryEncounter.misc; + + await runMysteryEncounterToEnd(game, 1, null, true); + await skipBattleRunMysteryEncounterRewardsPhase(game); + await game.phaseInterceptor.to(SelectModifierPhase, false); + expect(scene.getCurrentPhase().constructor.name).toBe(SelectModifierPhase.name); + await game.phaseInterceptor.run(SelectModifierPhase); + expect(scene.ui.getMode()).to.equal(Mode.MODIFIER_SELECT); + + const modifierSelectHandler = scene.ui.handlers.find(h => h instanceof ModifierSelectUiHandler) as ModifierSelectUiHandler; + expect(modifierSelectHandler.options.length).toEqual(1); + expect(item.type.name).toBe(modifierSelectHandler.options[0].modifierTypeOption.type.name); + }); + }); + + describe("Option 2 - Attempt to Steal", () => { + it("should have the correct properties", () => { + const option1 = FightOrFlightEncounter.options[1]; + expect(option1.optionMode).toBe(MysteryEncounterOptionMode.DEFAULT_OR_SPECIAL); + expect(option1.dialogue).toBeDefined(); + expect(option1.dialogue).toStrictEqual({ + buttonLabel: `${namespace}.option.2.label`, + buttonTooltip: `${namespace}.option.2.tooltip`, + }); + }); + + it("should start battle on failing to steal", async () => { + await game.runToMysteryEncounter(MysteryEncounterType.FIGHT_OR_FLIGHT, defaultParty); + + const config = game.scene.currentBattle.mysteryEncounter.enemyPartyConfigs[0]; + const speciesToSpawn = config.pokemonConfigs[0].species.speciesId; + + const realFn = Utils.randSeedInt; + vi.spyOn(Utils, "randSeedInt").mockImplementation((range, min) => { + if (range === 16 && isNullOrUndefined(min)) { + // Mock the steal roll + return 12; + } else { + return realFn(range, min); + } + }); + + await runMysteryEncounterToEnd(game, 2, 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(speciesToSpawn); + + // Should be enraged + expect(enemyField[0].summonData.battleStats).toEqual([1, 1, 1, 1, 1, 0, 0]); + }); + + it("Should skip battle when succeed on steal", async () => { + const leaveEncounterWithoutBattleSpy = vi.spyOn(EncounterPhaseUtils, "leaveEncounterWithoutBattle"); + + await game.runToMysteryEncounter(MysteryEncounterType.FIGHT_OR_FLIGHT, defaultParty); + + const item = game.scene.currentBattle.mysteryEncounter.misc; + const realFn = Utils.randSeedInt; + vi.spyOn(Utils, "randSeedInt").mockImplementation((range, min) => { + if (range === 16 && isNullOrUndefined(min)) { + // Mock the steal roll + return 6; + } else { + return realFn(range, min); + } + }); + + await runMysteryEncounterToEnd(game, 2); + await game.phaseInterceptor.to(SelectModifierPhase, false); + expect(scene.getCurrentPhase().constructor.name).toBe(SelectModifierPhase.name); + await game.phaseInterceptor.run(SelectModifierPhase); + expect(scene.ui.getMode()).to.equal(Mode.MODIFIER_SELECT); + + const modifierSelectHandler = scene.ui.handlers.find(h => h instanceof ModifierSelectUiHandler) as ModifierSelectUiHandler; + expect(modifierSelectHandler.options.length).toEqual(1); + expect(item.type.name).toBe(modifierSelectHandler.options[0].modifierTypeOption.type.name); + + expect(leaveEncounterWithoutBattleSpy).toBeCalled(); + }); + + it("Should skip fight when special requirements are met", async () => { + const leaveEncounterWithoutBattleSpy = vi.spyOn(EncounterPhaseUtils, "leaveEncounterWithoutBattle"); + const encounterTextSpy = vi.spyOn(EncounterDialogueUtils, "showEncounterText"); + + await game.runToMysteryEncounter(MysteryEncounterType.FIGHT_OR_FLIGHT, defaultParty); + + // Mock moveset + scene.getParty()[0].moveset = [new PokemonMove(Moves.KNOCK_OFF)]; + const item = game.scene.currentBattle.mysteryEncounter.misc; + + await runMysteryEncounterToEnd(game, 2); + await game.phaseInterceptor.to(SelectModifierPhase, false); + expect(scene.getCurrentPhase().constructor.name).toBe(SelectModifierPhase.name); + await game.phaseInterceptor.run(SelectModifierPhase); + expect(scene.ui.getMode()).to.equal(Mode.MODIFIER_SELECT); + + const modifierSelectHandler = scene.ui.handlers.find(h => h instanceof ModifierSelectUiHandler) as ModifierSelectUiHandler; + expect(modifierSelectHandler.options.length).toEqual(1); + expect(item.type.name).toBe(modifierSelectHandler.options[0].modifierTypeOption.type.name); + + expect(encounterTextSpy).toHaveBeenCalledWith(expect.any(BattleScene), `${namespace}.option.2.special_result`); + expect(leaveEncounterWithoutBattleSpy).toBeCalled(); + }); + }); + + describe("Option 3 - Leave", () => { + it("should leave encounter without battle", async () => { + const leaveEncounterWithoutBattleSpy = vi.spyOn(EncounterPhaseUtils, "leaveEncounterWithoutBattle"); + + await game.runToMysteryEncounter(MysteryEncounterType.FIGHT_OR_FLIGHT, defaultParty); + await runMysteryEncounterToEnd(game, 3); + + expect(leaveEncounterWithoutBattleSpy).toBeCalled(); + }); + }); +}); diff --git a/src/test/mystery-encounter/encounters/trash-to-treasure-encounter.test.ts b/src/test/mystery-encounter/encounters/trash-to-treasure-encounter.test.ts index 9cd1d477a22..df3a3471548 100644 --- a/src/test/mystery-encounter/encounters/trash-to-treasure-encounter.test.ts +++ b/src/test/mystery-encounter/encounters/trash-to-treasure-encounter.test.ts @@ -46,7 +46,6 @@ describe("Trash to Treasure - Mystery Encounter", () => { vi.spyOn(MysteryEncounters, "mysteryEncountersByBiome", "get").mockReturnValue( new Map([ [Biome.CAVE, [MysteryEncounterType.TRASH_TO_TREASURE]], - [Biome.MOUNTAIN, [MysteryEncounterType.MYSTERIOUS_CHALLENGERS]], ]) ); });