Adds Uncommon Breed ME and misc. ME bug fixes

This commit is contained in:
ImperialSympathizer 2024-08-29 12:46:23 -04:00
parent e77c6adb4f
commit b6e90931ac
16 changed files with 667 additions and 89 deletions

View File

@ -1107,12 +1107,13 @@ export abstract class BattleAnim {
let r = anim!.frames.length;
let f = 0;
const existingFieldSprites = [...scene.field.getAll()];
let existingFieldSprites = scene.field.getAll().slice(0);
scene.tweens.addCounter({
duration: Utils.getFrameMs(3) * frameTimeMult,
repeat: anim!.frames.length,
onRepeat: () => {
existingFieldSprites = scene.field.getAll().slice(0);
const spriteFrames = anim!.frames[f];
const frameData = this.getGraphicFrameDataWithoutTarget(anim!.frames[f], targetInitialX, targetInitialY);
const u = 0;
@ -1139,7 +1140,8 @@ export abstract class BattleAnim {
const setSpritePriority = (priority: integer) => {
if (existingFieldSprites.length > priority) {
// Move to specified priority index
scene.field.moveTo(moveSprite, scene.field.getIndex(existingFieldSprites[priority]));
const index = scene.field.getIndex(existingFieldSprites[priority]);
scene.field.moveTo(moveSprite, index);
} else {
// Move to top of scene
scene.field.moveTo(moveSprite, scene.field.getAll().length - 1);

View File

@ -175,7 +175,7 @@ export const AbsoluteAvariceEncounter: MysteryEncounter =
.withOnInit((scene: BattleScene) => {
const encounter = scene.currentBattle.mysteryEncounter!;
scene.loadSe("PRSFX- Bug Bite", "battle_anims");
scene.loadSe("PRSFX- Bug Bite", "battle_anims", "PRSFX- Bug Bite.wav");
scene.loadSe("Follow Me", "battle_anims", "Follow Me.mp3");
// Get all player berry items, remove from party, and store reference
@ -351,7 +351,7 @@ function doGreedentSpriteSteal(scene: BattleScene) {
const greedentSprites = scene.currentBattle.mysteryEncounter!.introVisuals?.getSpriteAtIndex(1);
scene.playSound("battle-anims/Follow Me");
scene.playSound("battle_anims/Follow Me");
scene.tweens.chain({
targets: greedentSprites,
tweens: [

View File

@ -1,5 +1,5 @@
import { EnemyPartyConfig, initBattleWithEnemyConfig, leaveEncounterWithoutBattle, selectPokemonForOption, setEncounterRewards, transitionMysteryEncounterIntroVisuals, } from "#app/data/mystery-encounters/utils/encounter-phase-utils";
import Pokemon, { PlayerPokemon, PokemonMove } from "#app/field/pokemon";
import Pokemon, { EnemyPokemon, PlayerPokemon, PokemonMove } from "#app/field/pokemon";
import { MysteryEncounterType } from "#enums/mystery-encounter-type";
import { Species } from "#enums/species";
import BattleScene from "#app/battle-scene";
@ -88,7 +88,7 @@ export const DancingLessonsEncounter: MysteryEncounter =
.withAutoHideIntroVisuals(false)
.withCatchAllowed(true)
.withOnVisualsStart((scene: BattleScene) => {
const danceAnim = new EncounterBattleAnim(EncounterAnim.DANCE, scene.getEnemyPokemon()!, scene.getPlayerPokemon());
const danceAnim = new EncounterBattleAnim(EncounterAnim.DANCE, scene.getEnemyPokemon()!, scene.getParty()[0]);
danceAnim.play(scene);
return true;
@ -105,7 +105,8 @@ export const DancingLessonsEncounter: MysteryEncounter =
const encounter = scene.currentBattle.mysteryEncounter!;
const species = getPokemonSpecies(Species.ORICORIO);
const enemyPokemon = scene.addEnemyPokemon(species, scene.currentBattle.enemyLevels![0], TrainerSlot.NONE, false);
const level = (scene.currentBattle.enemyLevels?.[0] ?? scene.currentBattle.waveIndex) + Math.max(Math.round((scene.currentBattle.waveIndex / 10)), 0);
const enemyPokemon = new EnemyPokemon(scene, species, level, TrainerSlot.NONE, false);
if (!enemyPokemon.moveset.some(m => m && m.getMove().id === Moves.REVELATION_DANCE)) {
if (enemyPokemon.moveset.length < 4) {
enemyPokemon.moveset.push(new PokemonMove(Moves.REVELATION_DANCE));
@ -130,10 +131,11 @@ export const DancingLessonsEncounter: MysteryEncounter =
}
const oricorioData = new PokemonData(enemyPokemon);
const oricorio = scene.addEnemyPokemon(species, scene.currentBattle.enemyLevels![0], TrainerSlot.NONE, false, oricorioData);
// Adds a real Pokemon sprite to the field (required for the animation)
scene.currentBattle.enemyParty[0] = enemyPokemon;
scene.field.add(enemyPokemon);
scene.currentBattle.enemyParty[0] = oricorio;
scene.field.add(oricorio);
const config: EnemyPartyConfig = {
levelAdditiveMultiplier: 1,

View File

@ -6,7 +6,7 @@ import {
setEncounterRewards
} from "#app/data/mystery-encounters/utils/encounter-phase-utils";
import { STEALING_MOVES } from "#app/data/mystery-encounters/requirements/requirement-groups";
import { EnemyPokemon } from "#app/field/pokemon";
import Pokemon, { EnemyPokemon } from "#app/field/pokemon";
import { ModifierTier } from "#app/modifier/modifier-tier";
import {
getPartyLuckValue,
@ -24,6 +24,10 @@ import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode
import { TrainerSlot } from "#app/data/trainer-config";
import { getSpriteKeysFromPokemon } from "#app/data/mystery-encounters/utils/encounter-pokemon-utils";
import PokemonData from "#app/system/pokemon-data";
import { BattlerTagType } from "#enums/battler-tag-type";
import { queueEncounterMessage } from "#app/data/mystery-encounters/utils/encounter-dialogue-utils";
import { StatChangePhase } from "#app/phases/stat-change-phase";
import { randSeedInt } from "#app/utils";
/** the i18n namespace for the encounter */
const namespace = "mysteryEncounter:fightOrFlight";
@ -58,7 +62,13 @@ export const FightOrFlightEncounter: MysteryEncounter =
level: level,
species: bossSpecies,
dataSource: new PokemonData(bossPokemon),
isBoss: true
isBoss: true,
tags: [BattlerTagType.MYSTERY_ENCOUNTER_POST_SUMMON],
mysteryEncounterBattleEffects: (pokemon: Pokemon) => {
queueEncounterMessage(pokemon.scene, `${namespace}.option.1.stat_boost`);
// Randomly boost 1 stat 2 stages
pokemon.scene.unshiftPhase(new StatChangePhase(pokemon.scene, pokemon.getBattlerIndex(), true, [randSeedInt(8)], 2));
}
}],
};
encounter.enemyPartyConfigs = [config];
@ -122,6 +132,7 @@ export const FightOrFlightEncounter: MysteryEncounter =
},
async (scene: BattleScene) => {
// Pick battle
// Pokemon will randomly boost 1 stat by 2 stages
const item = scene.currentBattle.mysteryEncounter!.misc as ModifierTypeOption;
setEncounterRewards(scene, { guaranteedModifierTypeOptions: [item], fillRemaining: false });
await initBattleWithEnemyConfig(scene, scene.currentBattle.mysteryEncounter!.enemyPartyConfigs[0]);

View File

@ -74,9 +74,9 @@ export const SafariZoneEncounter: MysteryEncounter =
};
updatePlayerMoney(scene, -(encounter.options[0].requirements[0] as MoneyRequirement).requiredMoney);
// Load bait/mud assets
scene.loadSe("PRSFX- Bug Bite", "battle_anims");
scene.loadSe("PRSFX- Sludge Bomb2", "battle_anims");
scene.loadSe("PRSFX- Taunt2", "battle_anims");
scene.loadSe("PRSFX- Bug Bite", "battle_anims", "PRSFX- Bug Bite.wav");
scene.loadSe("PRSFX- Sludge Bomb2", "battle_anims", "PRSFX- Sludge Bomb2.wav");
scene.loadSe("PRSFX- Taunt2", "battle_anims", "PRSFX- Taunt2.wav");
scene.loadAtlas("bait", "mystery-encounters");
scene.loadAtlas("mud", "mystery-encounters");
await summonSafariPokemon(scene);
@ -353,12 +353,12 @@ async function throwBait(scene: BattleScene, pokemon: EnemyPokemon): Promise<boo
y: originalY - 5,
loop: 6,
onStart: () => {
scene.playSound("battle-anims/PRSFX- Bug Bite");
scene.playSound("battle_anims/PRSFX- Bug Bite");
bait.setFrame("0002.png");
},
onLoop: () => {
if (index % 2 === 0) {
scene.playSound("battle-anims/PRSFX- Bug Bite");
scene.playSound("battle_anims/PRSFX- Bug Bite");
}
if (index === 4) {
bait.setFrame("0003.png");
@ -409,7 +409,7 @@ async function throwMud(scene: BattleScene, pokemon: EnemyPokemon): Promise<bool
duration: 500,
onComplete: () => {
// Mud frame 2
scene.playSound("battle-anims/PRSFX- Sludge Bomb2");
scene.playSound("battle_anims/PRSFX- Sludge Bomb2");
mud.setFrame("0002.png");
// Mud splat
scene.time.delayedCall(200, () => {
@ -435,10 +435,10 @@ async function throwMud(scene: BattleScene, pokemon: EnemyPokemon): Promise<bool
y: originalY - 20,
loop: 1,
onStart: () => {
scene.playSound("battle-anims/PRSFX- Taunt2");
scene.playSound("battle_anims/PRSFX- Taunt2");
},
onLoop: () => {
scene.playSound("battle-anims/PRSFX- Taunt2");
scene.playSound("battle_anims/PRSFX- Taunt2");
},
onComplete: () => {
resolve(true);

View File

@ -209,12 +209,12 @@ async function tryApplyDigRewardItems(scene: BattleScene) {
}
async function doGarbageDig(scene: BattleScene) {
scene.playSound("battle-anims/PRSFX- Dig2");
scene.playSound("battle_anims/PRSFX- Dig2");
scene.time.delayedCall(SOUND_EFFECT_WAIT_TIME, () => {
scene.playSound("battle-anims/PRSFX- Dig2");
scene.playSound("battle-anims/PRSFX- Venom Drench", { volume: 2 });
scene.playSound("battle_anims/PRSFX- Dig2");
scene.playSound("battle_anims/PRSFX- Venom Drench", { volume: 2 });
});
scene.time.delayedCall(SOUND_EFFECT_WAIT_TIME * 2, () => {
scene.playSound("battle-anims/PRSFX- Dig2");
scene.playSound("battle_anims/PRSFX- Dig2");
});
}

View File

@ -0,0 +1,265 @@
import { MysteryEncounterOptionBuilder } from "#app/data/mystery-encounters/mystery-encounter-option";
import { EnemyPartyConfig, initBattleWithEnemyConfig, leaveEncounterWithoutBattle, setEncounterExp, setEncounterRewards } from "#app/data/mystery-encounters/utils/encounter-phase-utils";
import { CHARMING_MOVES } from "#app/data/mystery-encounters/requirements/requirement-groups";
import Pokemon, { EnemyPokemon, PokemonMove } from "#app/field/pokemon";
import { getPartyLuckValue } from "#app/modifier/modifier-type";
import { MysteryEncounterType } from "#enums/mystery-encounter-type";
import BattleScene from "#app/battle-scene";
import MysteryEncounter, { MysteryEncounterBuilder } from "../mystery-encounter";
import { MoveRequirement, PersistentModifierRequirement } from "../mystery-encounter-requirements";
import { MysteryEncounterTier } from "#enums/mystery-encounter-tier";
import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode";
import { TrainerSlot } from "#app/data/trainer-config";
import { catchPokemon, getHighestLevelPlayerPokemon, getSpriteKeysFromPokemon } from "#app/data/mystery-encounters/utils/encounter-pokemon-utils";
import PokemonData from "#app/system/pokemon-data";
import { speciesEggMoves } from "#app/data/egg-moves";
import { isNullOrUndefined, randSeedInt } from "#app/utils";
import { Moves } from "#enums/moves";
import { BattlerIndex } from "#app/battle";
import { SelfStatusMove } from "#app/data/move";
import { PokeballType } from "#enums/pokeball";
import { BattlerTagType } from "#enums/battler-tag-type";
import { queueEncounterMessage } from "#app/data/mystery-encounters/utils/encounter-dialogue-utils";
import { StatChangePhase } from "#app/phases/stat-change-phase";
import { BattleStat } from "#app/data/battle-stat";
import { BerryModifier } from "#app/modifier/modifier";
/** the i18n namespace for the encounter */
const namespace = "mysteryEncounter:uncommonBreed";
/**
* Uncommon Breed encounter.
* @see {@link https://github.com/pagefaultgames/pokerogue/issues/3811 | GitHub Issue #3811}
* @see For biome requirements check {@linkcode mysteryEncountersByBiome}
*/
export const UncommonBreedEncounter: MysteryEncounter =
MysteryEncounterBuilder.withEncounterType(MysteryEncounterType.UNCOMMON_BREED)
.withEncounterTier(MysteryEncounterTier.COMMON)
.withSceneWaveRangeRequirement(10, 180) // waves 10 to 180
.withCatchAllowed(true)
.withHideWildIntroMessage(true)
.withIntroSpriteConfigs([]) // Set in onInit()
.withIntroDialogue([
{
text: `${namespace}.intro`,
},
])
.withOnInit((scene: BattleScene) => {
const encounter = scene.currentBattle.mysteryEncounter!;
// Calculate boss mon
// Level equal to 2 below highest party member
const level = getHighestLevelPlayerPokemon(scene).level - 2;
const species = scene.arena.randomSpecies(scene.currentBattle.waveIndex, level, 0, getPartyLuckValue(scene.getParty()), true);
const pokemon = new EnemyPokemon(scene, species, level, TrainerSlot.NONE, true);
const speciesRootForm = pokemon.species.getRootSpeciesId();
encounter.misc = {
pokemon
};
// Pokemon will always have one of its egg moves in its moveset
if (speciesEggMoves.hasOwnProperty(speciesRootForm)) {
const eggMoves: Moves[] = speciesEggMoves[speciesRootForm];
const eggMoveIndex = randSeedInt(4);
const randomEggMove: Moves = eggMoves[eggMoveIndex];
encounter.misc.eggMove = randomEggMove;
if (pokemon.moveset.length < 4) {
pokemon.moveset.push(new PokemonMove(randomEggMove));
} else {
pokemon.moveset[0] = new PokemonMove(randomEggMove);
}
}
const config: EnemyPartyConfig = {
pokemonConfigs: [{
level: level,
species: species,
dataSource: new PokemonData(pokemon),
isBoss: false,
tags: [BattlerTagType.MYSTERY_ENCOUNTER_POST_SUMMON],
mysteryEncounterBattleEffects: (pokemon: Pokemon) => {
queueEncounterMessage(pokemon.scene, `${namespace}.option.1.stat_boost`);
pokemon.scene.unshiftPhase(new StatChangePhase(pokemon.scene, pokemon.getBattlerIndex(), true, [BattleStat.ATK, BattleStat.DEF, BattleStat.SPATK, BattleStat.SPDEF, BattleStat.SPD], 1));
}
}],
};
encounter.enemyPartyConfigs = [config];
const { spriteKey, fileRoot } = getSpriteKeysFromPokemon(pokemon);
encounter.spriteConfigs = [
{
spriteKey: spriteKey,
fileRoot: fileRoot,
hasShadow: true,
x: -5,
repeat: true,
isPokemon: true
},
];
encounter.setDialogueToken("enemyPokemon", pokemon.getNameToRender());
scene.loadSe("PRSFX- Spotlight2", "battle_anims", "PRSFX- Spotlight2.wav");
return true;
})
.withOnVisualsStart((scene: BattleScene) => {
// Animate the pokemon
const encounter = scene.currentBattle.mysteryEncounter!;
const pokemonSprite = encounter.introVisuals!.getSprites();
scene.tweens.add({ // Bounce at the end
targets: pokemonSprite,
duration: 300,
ease: "Cubic.easeOut",
yoyo: true,
y: "-=20",
loop: 1,
});
scene.time.delayedCall(500, () => scene.playSound("battle_anims/PRSFX- Spotlight2"));
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 eggMove = encounter.misc.eggMove;
if (!isNullOrUndefined(eggMove)) {
// Check what type of move the egg move is to determine target
const pokemonMove = new PokemonMove(eggMove);
const move = pokemonMove.getMove();
const target = move instanceof SelfStatusMove ? BattlerIndex.ENEMY : BattlerIndex.PLAYER;
encounter.startOfBattleEffects.push(
{
sourceBattlerIndex: BattlerIndex.ENEMY,
targets: [target],
move: pokemonMove,
ignorePp: true
});
}
setEncounterRewards(scene, { fillRemaining: true });
await initBattleWithEnemyConfig(scene, encounter.enemyPartyConfigs[0]);
}
)
.withOption(
MysteryEncounterOptionBuilder
.newOptionWithMode(MysteryEncounterOptionMode.DISABLED_OR_SPECIAL)
.withSceneRequirement(new PersistentModifierRequirement("BerryModifier", 4)) // Will set option2PrimaryName and option2PrimaryMove dialogue tokens automatically
.withDialogue({
buttonLabel: `${namespace}.option.2.label`,
buttonTooltip: `${namespace}.option.2.tooltip`,
disabledButtonTooltip: `${namespace}.option.2.disabled_tooltip`,
selected: [
{
text: `${namespace}.option.2.selected`
}
]
})
.withOptionPhase(async (scene: BattleScene) => {
// Give it some food
// Remove 4 random berries from player's party
// Get all player berry items, remove from party, and store reference
let berryItems: BerryModifier[] = scene.findModifiers(m => m instanceof BerryModifier) as BerryModifier[];
for (let i = 0; i < 4; i++) {
berryItems = scene.findModifiers(m => m instanceof BerryModifier) as BerryModifier[];
const randBerry = berryItems[randSeedInt(berryItems.length)];
randBerry.stackCount--;
if (randBerry.stackCount === 0) {
scene.removeModifier(randBerry);
}
scene.updateModifiers(true, true);
}
// Pokemon joins the team, with 2 egg moves
const encounter = scene.currentBattle.mysteryEncounter!;
const pokemon = encounter.misc.pokemon;
// Give 1 additional egg move
const previousEggMove = encounter.misc.eggMove;
const speciesRootForm = pokemon.species.getRootSpeciesId();
if (speciesEggMoves.hasOwnProperty(speciesRootForm)) {
const eggMoves: Moves[] = speciesEggMoves[speciesRootForm];
let randomEggMove: Moves = eggMoves[randSeedInt(4)];
while (randomEggMove === previousEggMove) {
randomEggMove = eggMoves[randSeedInt(4)];
}
if (pokemon.moveset.length < 4) {
pokemon.moveset.push(new PokemonMove(randomEggMove));
} else {
pokemon.moveset[1] = new PokemonMove(randomEggMove);
}
}
await catchPokemon(scene, pokemon, null, PokeballType.POKEBALL, false);
setEncounterRewards(scene, { fillRemaining: true });
leaveEncounterWithoutBattle(scene);
})
.build()
)
.withOption(
MysteryEncounterOptionBuilder
.newOptionWithMode(MysteryEncounterOptionMode.DISABLED_OR_SPECIAL)
.withPrimaryPokemonRequirement(new MoveRequirement(CHARMING_MOVES)) // Will set option2PrimaryName and option2PrimaryMove dialogue tokens automatically
.withDialogue({
buttonLabel: `${namespace}.option.3.label`,
buttonTooltip: `${namespace}.option.3.tooltip`,
disabledButtonTooltip: `${namespace}.option.3.disabled_tooltip`,
selected: [
{
text: `${namespace}.option.3.selected`
}
]
})
.withOptionPhase(async (scene: BattleScene) => {
// Attract the pokemon with a move
// Pokemon joins the team, with 2 egg moves and IVs rolled an additional time
const encounter = scene.currentBattle.mysteryEncounter!;
const pokemon = encounter.misc.pokemon;
// Give 1 additional egg move
const previousEggMove = encounter.misc.eggMove;
const speciesRootForm = pokemon.species.getRootSpeciesId();
if (speciesEggMoves.hasOwnProperty(speciesRootForm)) {
const eggMoves: Moves[] = speciesEggMoves[speciesRootForm];
let randomEggMove: Moves = eggMoves[randSeedInt(4)];
while (randomEggMove === previousEggMove) {
randomEggMove = eggMoves[randSeedInt(4)];
}
if (pokemon.moveset.length < 4) {
pokemon.moveset.push(new PokemonMove(randomEggMove));
} else {
pokemon.moveset[1] = new PokemonMove(randomEggMove);
}
}
// Roll IVs a second time
pokemon.ivs = pokemon.ivs.map(iv => {
const newValue = randSeedInt(31);
return newValue > iv ? newValue : iv;
});
await catchPokemon(scene, pokemon, null, PokeballType.POKEBALL, false);
if (encounter.selectedOption?.primaryPokemon?.id) {
setEncounterExp(scene, encounter.selectedOption.primaryPokemon.id, pokemon.getExpValue(), false);
}
setEncounterRewards(scene, { fillRemaining: true });
leaveEncounterWithoutBattle(scene);
})
.build()
)
.build();

View File

@ -29,6 +29,7 @@ import { TheWinstrateChallengeEncounter } from "#app/data/mystery-encounters/enc
import { TeleportingHijinksEncounter } from "#app/data/mystery-encounters/encounters/teleporting-hijinks-encounter";
import { BugTypeSuperfanEncounter } from "#app/data/mystery-encounters/encounters/bug-type-superfan-encounter";
import { FunAndGamesEncounter } from "#app/data/mystery-encounters/encounters/fun-and-games-encounter";
import { UncommonBreedEncounter } from "#app/data/mystery-encounters/encounters/uncommon-breed-encounter";
// Spawn chance: (BASE_MYSTERY_ENCOUNTER_SPAWN_WEIGHT + WIGHT_INCREMENT_ON_SPAWN_MISS * <number of missed spawns>) / 256
export const BASE_MYSTERY_ENCOUNTER_SPAWN_WEIGHT = 1;
@ -175,7 +176,8 @@ const anyBiomeEncounters: MysteryEncounterType[] = [
MysteryEncounterType.CLOWNING_AROUND,
MysteryEncounterType.WEIRD_DREAM,
MysteryEncounterType.TELEPORTING_HIJINKS,
MysteryEncounterType.BUG_TYPE_SUPERFAN
MysteryEncounterType.BUG_TYPE_SUPERFAN,
MysteryEncounterType.UNCOMMON_BREED
];
/**
@ -282,6 +284,7 @@ export function initMysteryEncounters() {
allMysteryEncounters[MysteryEncounterType.TELEPORTING_HIJINKS] = TeleportingHijinksEncounter;
allMysteryEncounters[MysteryEncounterType.BUG_TYPE_SUPERFAN] = BugTypeSuperfanEncounter;
allMysteryEncounters[MysteryEncounterType.FUN_AND_GAMES] = FunAndGamesEncounter;
allMysteryEncounters[MysteryEncounterType.UNCOMMON_BREED] = UncommonBreedEncounter;
// Add extreme encounters to biome map
extremeBiomeEncounters.forEach(encounter => {

View File

@ -886,7 +886,7 @@ export function handleMysteryEncounterBattleStartEffects(scene: BattleScene) {
/**
* Can queue extra phases or logic during {@link TurnInitPhase}
* Mostly useful for allowing MysteryEncounter enemies to "cheat" and use moves before the first turn
* Should mostly just be used for injecting custom phases into the battle system on turn start
* @param scene
* @return boolean - if true, will skip the remainder of the {@link TurnInitPhase}
*/

View File

@ -26,5 +26,6 @@ export enum MysteryEncounterType {
THE_WINSTRATE_CHALLENGE,
TELEPORTING_HIJINKS,
BUG_TYPE_SUPERFAN,
FUN_AND_GAMES
FUN_AND_GAMES,
UNCOMMON_BREED
}

View File

@ -1,31 +1,32 @@
import lostAtSeaDialogue from "./mystery-encounters/lost-at-sea-dialogue.json";
import mysteriousChestDialogue from "#app/locales/en/mystery-encounters/mysterious-chest-dialogue.json";
import mysteriousChallengersDialogue from "#app/locales/en/mystery-encounters/mysterious-challengers-dialogue.json";
import darkDealDialogue from "#app/locales/en/mystery-encounters/dark-deal-dialogue.json";
import departmentStoreSaleDialogue from "#app/locales/en/mystery-encounters/department-store-sale-dialogue.json";
import fieldTripDialogue from "#app/locales/en/mystery-encounters/field-trip-dialogue.json";
import fieryFalloutDialogue from "#app/locales/en/mystery-encounters/fiery-fallout-dialogue.json";
import fightOrFlightDialogue from "#app/locales/en/mystery-encounters/fight-or-flight-dialogue.json";
import safariZoneDialogue from "#app/locales/en/mystery-encounters/safari-zone-dialogue.json";
import shadyVitaminDealerDialogue from "#app/locales/en/mystery-encounters/shady-vitamin-dealer-dialogue.json";
import slumberingSnorlaxDialogue from "#app/locales/en/mystery-encounters/slumbering-snorlax-dialogue.json";
import trainingSessionDialogue from "#app/locales/en/mystery-encounters/training-session-dialogue.json";
import theStrongStuffDialogue from "#app/locales/en/mystery-encounters/the-strong-stuff-dialogue.json";
import thePokemonSalesmanDialogue from "#app/locales/en/mystery-encounters/the-pokemon-salesman-dialogue.json";
import anOfferYouCantRefuseDialogue from "#app/locales/en/mystery-encounters/an-offer-you-cant-refuse-dialogue.json";
import delibirdyDialogue from "#app/locales/en/mystery-encounters/delibirdy-dialogue.json";
import absoluteAvariceDialogue from "#app/locales/en/mystery-encounters/absolute-avarice-dialogue.json";
import aTrainersTestDialogue from "#app/locales/en/mystery-encounters/a-trainers-test-dialogue.json";
import trashToTreasureDialogue from "#app/locales/en/mystery-encounters/trash-to-treasure-dialogue.json";
import berriesAboundDialogue from "#app/locales/en/mystery-encounters/berries-abound-dialogue.json";
import clowningAroundDialogue from "#app/locales/en/mystery-encounters/clowning-around-dialogue.json";
import partTimerDialogue from "#app/locales/en/mystery-encounters/part-timer-dialogue.json";
import dancingLessonsDialogue from "#app/locales/en/mystery-encounters/dancing-lessons-dialogue.json";
import weirdDreamDialogue from "#app/locales/en/mystery-encounters/weird-dream-dialogue.json";
import theWinstrateChallengeDialogue from "#app/locales/en/mystery-encounters/the-winstrate-challenge-dialogue.json";
import teleportingHijinksDialogue from "#app/locales/en/mystery-encounters/teleporting-hijinks-dialogue.json";
import bugTypeSuperfanDialogue from "#app/locales/en/mystery-encounters/bug-type-superfan-dialogue.json";
import funAndGamesDialogue from "#app/locales/en/mystery-encounters/fun-and-games-dialogue.json";
import lostAtSea from "./mystery-encounters/lost-at-sea-dialogue.json";
import mysteriousChest from "#app/locales/en/mystery-encounters/mysterious-chest-dialogue.json";
import mysteriousChallengers from "#app/locales/en/mystery-encounters/mysterious-challengers-dialogue.json";
import darkDeal from "#app/locales/en/mystery-encounters/dark-deal-dialogue.json";
import departmentStoreSale from "#app/locales/en/mystery-encounters/department-store-sale-dialogue.json";
import fieldTrip from "#app/locales/en/mystery-encounters/field-trip-dialogue.json";
import fieryFallout from "#app/locales/en/mystery-encounters/fiery-fallout-dialogue.json";
import fightOrFlight from "#app/locales/en/mystery-encounters/fight-or-flight-dialogue.json";
import safariZone from "#app/locales/en/mystery-encounters/safari-zone-dialogue.json";
import shadyVitaminDealer from "#app/locales/en/mystery-encounters/shady-vitamin-dealer-dialogue.json";
import slumberingSnorlax from "#app/locales/en/mystery-encounters/slumbering-snorlax-dialogue.json";
import trainingSession from "#app/locales/en/mystery-encounters/training-session-dialogue.json";
import theStrongStuff from "#app/locales/en/mystery-encounters/the-strong-stuff-dialogue.json";
import pokemonSalesman from "#app/locales/en/mystery-encounters/the-pokemon-salesman-dialogue.json";
import offerYouCantRefuse from "#app/locales/en/mystery-encounters/an-offer-you-cant-refuse-dialogue.json";
import delibirdy from "#app/locales/en/mystery-encounters/delibirdy-dialogue.json";
import absoluteAvarice from "#app/locales/en/mystery-encounters/absolute-avarice-dialogue.json";
import aTrainersTest from "#app/locales/en/mystery-encounters/a-trainers-test-dialogue.json";
import trashToTreasure from "#app/locales/en/mystery-encounters/trash-to-treasure-dialogue.json";
import berriesAbound from "#app/locales/en/mystery-encounters/berries-abound-dialogue.json";
import clowningAround from "#app/locales/en/mystery-encounters/clowning-around-dialogue.json";
import partTimer from "#app/locales/en/mystery-encounters/part-timer-dialogue.json";
import dancingLessons from "#app/locales/en/mystery-encounters/dancing-lessons-dialogue.json";
import weirdDream from "#app/locales/en/mystery-encounters/weird-dream-dialogue.json";
import theWinstrateChallenge from "#app/locales/en/mystery-encounters/the-winstrate-challenge-dialogue.json";
import teleportingHijinks from "#app/locales/en/mystery-encounters/teleporting-hijinks-dialogue.json";
import bugTypeSuperfan from "#app/locales/en/mystery-encounters/bug-type-superfan-dialogue.json";
import funAndGames from "#app/locales/en/mystery-encounters/fun-and-games-dialogue.json";
import uncommonBreed from "#app/locales/en/mystery-encounters/uncommon-breed-dialogue.json";
/**
* Injection patterns that can be used:
@ -46,32 +47,33 @@ export const mysteryEncounter = {
// DO NOT REMOVE
"unit_test_dialogue": "{{test}}{{test}} {{test{{test}}}} {{test1}} {{test\}} {{test\\}} {{test\\\}} {test}}",
mysteriousChallengers: mysteriousChallengersDialogue,
mysteriousChest: mysteriousChestDialogue,
darkDeal: darkDealDialogue,
fightOrFlight: fightOrFlightDialogue,
slumberingSnorlax: slumberingSnorlaxDialogue,
trainingSession: trainingSessionDialogue,
departmentStoreSale: departmentStoreSaleDialogue,
shadyVitaminDealer: shadyVitaminDealerDialogue,
fieldTrip: fieldTripDialogue,
safariZone: safariZoneDialogue,
lostAtSea: lostAtSeaDialogue,
fieryFallout: fieryFalloutDialogue,
theStrongStuff: theStrongStuffDialogue,
pokemonSalesman: thePokemonSalesmanDialogue,
offerYouCantRefuse: anOfferYouCantRefuseDialogue,
delibirdy: delibirdyDialogue,
absoluteAvarice: absoluteAvariceDialogue,
aTrainersTest: aTrainersTestDialogue,
trashToTreasure: trashToTreasureDialogue,
berriesAbound: berriesAboundDialogue,
clowningAround: clowningAroundDialogue,
partTimer: partTimerDialogue,
dancingLessons: dancingLessonsDialogue,
weirdDream: weirdDreamDialogue,
theWinstrateChallenge: theWinstrateChallengeDialogue,
teleportingHijinks: teleportingHijinksDialogue,
bugTypeSuperfan: bugTypeSuperfanDialogue,
funAndGames: funAndGamesDialogue
mysteriousChallengers,
mysteriousChest,
darkDeal,
fightOrFlight,
slumberingSnorlax,
trainingSession,
departmentStoreSale,
shadyVitaminDealer,
fieldTrip,
safariZone,
lostAtSea,
fieryFallout,
theStrongStuff,
pokemonSalesman,
offerYouCantRefuse,
delibirdy,
absoluteAvarice,
aTrainersTest,
trashToTreasure,
berriesAbound,
clowningAround,
partTimer,
dancingLessons,
weirdDream,
theWinstrateChallenge,
teleportingHijinks,
bugTypeSuperfan,
funAndGames,
uncommonBreed,
} as const;

View File

@ -7,7 +7,8 @@
"1": {
"label": "Battle the Pokémon",
"tooltip": "(-) Hard Battle\n(+) New Item",
"selected": "You approach the\nPokémon without fear."
"selected": "You approach the\nPokémon without fear.",
"stat_boost": "The {{enemyPokemon}} latent strength boosted one of its stats!"
},
"2": {
"label": "Steal the Item",

View File

@ -0,0 +1,26 @@
{
"intro": "That isn't just an ordinary Pokémon!",
"title": "Uncommon Breed",
"description": "That {{enemyPokemon}} looks special compared to others of its kind. @[TOOLTIP_TITLE]{Perhaps it knows a special move?} You could battle and catch it outright, but there might also be a way to befriend it.",
"query": "What will you do?",
"option": {
"1": {
"label": "Battle the Pokémon",
"tooltip": "(-) Tricky Battle\n(+) Strong Catchable Foe",
"selected": "You approach the\n{{enemyPokemon}} without fear.",
"stat_boost": "The {{enemyPokemon}} heightened abilities boost its stats!"
},
"2": {
"label": "Give It Food",
"disabled_tooltip": "You need 4 berry items to choose this",
"tooltip": "(-) Give 4 Berries\n(+) The {{enemyPokemon}} Likes You",
"selected": "You toss the berries at the {{enemyPokemon}}!$It eats them happily!$The {{enemyPokemon}} wants to join your party!"
},
"3": {
"label": "Befriend It",
"disabled_tooltip": "Your Pokémon need to know certain moves to choose this",
"tooltip": "(+) {{option3PrimaryName}} uses {{option3PrimaryMove}}\n(+) The {{enemyPokemon}} Likes You",
"selected": "Your {{option3PrimaryName}} uses {{option3PrimaryMove}} to charm the {{enemyPokemon}}!$The {{enemyPokemon}} wants to join your party!"
}
}
}

View File

@ -133,9 +133,9 @@ class DefaultOverrides {
// -------------------------
/** 1 to 256, set to null to ignore */
readonly MYSTERY_ENCOUNTER_RATE_OVERRIDE: number | null = 256;
readonly MYSTERY_ENCOUNTER_RATE_OVERRIDE: number | null = null;
readonly MYSTERY_ENCOUNTER_TIER_OVERRIDE: MysteryEncounterTier | null = null;
readonly MYSTERY_ENCOUNTER_OVERRIDE: MysteryEncounterType | null = MysteryEncounterType.MYSTERIOUS_CHEST;
readonly MYSTERY_ENCOUNTER_OVERRIDE: MysteryEncounterType | null = null;
// -------------------------
// MODIFIER / ITEM OVERRIDES

View File

@ -22,6 +22,8 @@ import { ReturnPhase } from "#app/phases/return-phase";
import { CheckSwitchPhase } from "#app/phases/check-switch-phase";
import { SelectModifierPhase } from "#app/phases/select-modifier-phase";
import { NewBattlePhase } from "#app/phases/new-battle-phase";
import { GameOverPhase } from "#app/phases/game-over-phase";
import { SwitchPhase } from "#app/phases/switch-phase";
/**
* Will handle (in order):
@ -161,21 +163,22 @@ export class MysteryEncounterOptionSelectedPhase extends Phase {
this.onOptionSelect(this.scene).finally(() => {
this.end();
});
}, this.scene.currentBattle.mysteryEncounter?.getSeedOffset());
}, this.scene.currentBattle.mysteryEncounter?.getSeedOffset() * 500);
});
} else {
this.scene.executeWithSeedOffset(() => {
this.onOptionSelect(this.scene).finally(() => {
this.end();
});
}, this.scene.currentBattle.mysteryEncounter?.getSeedOffset());
}, this.scene.currentBattle.mysteryEncounter?.getSeedOffset() * 500);
}
}
}
/**
* Runs at the beginning of an Encounter's battle
* Will cleanup any residual flinches, Endure, etc. that are left over from startOfBattleEffects
* Will clean up any residual flinches, Endure, etc. that are left over from startOfBattleEffects
* Will also handle Game Overs, switches, etc. that could happen from handleMysteryEncounterBattleStartEffects
* See [TurnEndPhase](../phases.ts) for more details
*/
export class MysteryEncounterBattleStartCleanupPhase extends Phase {
@ -196,6 +199,28 @@ export class MysteryEncounterBattleStartCleanupPhase extends Phase {
this.scene.tryRemovePhase(p => p instanceof PostTurnStatusEffectPhase);
}
// The total number of Pokemon in the player's party that can legally fight
const legalPlayerPokemon = this.scene.getParty().filter(p => p.isAllowedInBattle());
// The total number of legal player Pokemon that aren't currently on the field
const legalPlayerPartyPokemon = legalPlayerPokemon.filter(p => !p.isActive(true));
if (!legalPlayerPokemon.length) {
this.scene.unshiftPhase(new GameOverPhase(this.scene));
}
// Check for any KOd player mons and switch
// For each fainted mon on the field, if there is a legal replacement, summon it
const playerField = this.scene.getPlayerField();
playerField.forEach((pokemon, i) => {
if (!pokemon.isAllowedInBattle() && legalPlayerPartyPokemon.length > i) {
this.scene.unshiftPhase(new SwitchPhase(this.scene, i, true, false));
}
});
// THEN, if is a double battle, and player only has 1 summoned pokemon, center pokemon on field
if (this.scene.currentBattle.double && legalPlayerPokemon.length === 1 && legalPlayerPartyPokemon.length === 0) {
this.scene.unshiftPhase(new ToggleDoublePositionPhase(this.scene, true));
}
super.end();
}
}
@ -477,7 +502,7 @@ export class PostMysteryEncounterPhase extends Phase {
this.continueEncounter();
}
});
}, this.scene.currentBattle.mysteryEncounter?.getSeedOffset());
}, this.scene.currentBattle.mysteryEncounter?.getSeedOffset() * 2000);
} else {
this.continueEncounter();
}

View File

@ -0,0 +1,240 @@
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, runSelectMysteryEncounterOption } from "#test/mystery-encounter/encounter-test-utils";
import { Moves } from "#enums/moves";
import BattleScene from "#app/battle-scene";
import { PokemonMove } from "#app/field/pokemon";
import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode";
import { MysteryEncounterTier } from "#enums/mystery-encounter-tier";
import { initSceneWithoutEncounterPhase } from "#test/utils/gameManagerUtils";
import * as EncounterPhaseUtils from "#app/data/mystery-encounters/utils/encounter-phase-utils";
import { MysteryEncounterPhase } from "#app/phases/mystery-encounter-phases";
import { CommandPhase } from "#app/phases/command-phase";
import { UncommonBreedEncounter } from "#app/data/mystery-encounters/encounters/uncommon-breed-encounter";
import { MovePhase } from "#app/phases/move-phase";
import { speciesEggMoves } from "#app/data/egg-moves";
import { getPokemonSpecies } from "#app/data/pokemon-species";
import { BerryType } from "#enums/berry-type";
const namespace = "mysteryEncounter:uncommonBreed";
const defaultParty = [Species.LAPRAS, Species.GENGAR, Species.ABRA];
const defaultBiome = Biome.CAVE;
const defaultWave = 45;
describe("Uncommon Breed - 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.startingWave(defaultWave);
game.override.startingBiome(defaultBiome);
game.override.disableTrainerWaves();
vi.spyOn(MysteryEncounters, "mysteryEncountersByBiome", "get").mockReturnValue(
new Map<Biome, MysteryEncounterType[]>([
[Biome.CAVE, [MysteryEncounterType.UNCOMMON_BREED]],
])
);
});
afterEach(() => {
game.phaseInterceptor.restoreOg();
vi.clearAllMocks();
vi.resetAllMocks();
});
it("should have the correct properties", async () => {
await game.runToMysteryEncounter(MysteryEncounterType.UNCOMMON_BREED, defaultParty);
expect(UncommonBreedEncounter.encounterType).toBe(MysteryEncounterType.UNCOMMON_BREED);
expect(UncommonBreedEncounter.encounterTier).toBe(MysteryEncounterTier.COMMON);
expect(UncommonBreedEncounter.dialogue).toBeDefined();
expect(UncommonBreedEncounter.dialogue.intro).toStrictEqual([{ text: `${namespace}.intro` }]);
expect(UncommonBreedEncounter.dialogue.encounterOptionsDialogue?.title).toBe(`${namespace}.title`);
expect(UncommonBreedEncounter.dialogue.encounterOptionsDialogue?.description).toBe(`${namespace}.description`);
expect(UncommonBreedEncounter.dialogue.encounterOptionsDialogue?.query).toBe(`${namespace}.query`);
expect(UncommonBreedEncounter.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.UNCOMMON_BREED);
});
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 = UncommonBreedEncounter;
const { onInit } = UncommonBreedEncounter;
expect(UncommonBreedEncounter.onInit).toBeDefined();
UncommonBreedEncounter.populateDialogueTokensFromRequirements(scene);
const onInitResult = onInit!(scene);
const config = UncommonBreedEncounter.enemyPartyConfigs[0];
expect(config).toBeDefined();
expect(config.pokemonConfigs?.[0].isBoss).toBe(false);
expect(onInitResult).toBe(true);
});
describe("Option 1 - Fight", () => {
it("should have the correct properties", () => {
const option = UncommonBreedEncounter.options[0];
expect(option.optionMode).toBe(MysteryEncounterOptionMode.DEFAULT);
expect(option.dialogue).toBeDefined();
expect(option.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 () => {
const phaseSpy = vi.spyOn(scene, "pushPhase");
await game.runToMysteryEncounter(MysteryEncounterType.UNCOMMON_BREED, defaultParty);
const config = game.scene.currentBattle.mysteryEncounter!.enemyPartyConfigs[0];
const speciesToSpawn = config.pokemonConfigs?.[0].species.speciesId;
await runMysteryEncounterToEnd(game, 1, undefined, 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);
expect(enemyField[0].summonData.battleStats).toEqual([1, 1, 1, 1, 1, 0, 0]);
// Should have used its egg move pre-battle
const movePhases = phaseSpy.mock.calls.filter(p => p[0] instanceof MovePhase).map(p => p[0]);
expect(movePhases.length).toBe(1);
const eggMoves: Moves[] = speciesEggMoves[getPokemonSpecies(speciesToSpawn).getRootSpeciesId()];
const usedMove = (movePhases[0] as MovePhase).move.moveId;
expect(eggMoves.includes(usedMove)).toBe(true);
});
});
describe("Option 2 - Give it Food", () => {
it("should have the correct properties", () => {
const option = UncommonBreedEncounter.options[1];
expect(option.optionMode).toBe(MysteryEncounterOptionMode.DISABLED_OR_SPECIAL);
expect(option.dialogue).toBeDefined();
expect(option.dialogue).toStrictEqual({
buttonLabel: `${namespace}.option.2.label`,
buttonTooltip: `${namespace}.option.2.tooltip`,
disabledButtonTooltip: `${namespace}.option.2.disabled_tooltip`,
selected: [
{
text: `${namespace}.option.2.selected`,
}
],
});
});
it("should NOT be selectable if the player doesn't have enough berries", async () => {
game.override.startingHeldItems([{name: "BERRY", count: 2, type: BerryType.SITRUS}]);
await game.runToMysteryEncounter(MysteryEncounterType.UNCOMMON_BREED, defaultParty);
await game.phaseInterceptor.to(MysteryEncounterPhase, false);
const encounterPhase = scene.getCurrentPhase();
expect(encounterPhase?.constructor.name).toBe(MysteryEncounterPhase.name);
const mysteryEncounterPhase = encounterPhase as MysteryEncounterPhase;
vi.spyOn(mysteryEncounterPhase, "continueEncounter");
vi.spyOn(mysteryEncounterPhase, "handleOptionSelect");
vi.spyOn(scene.ui, "playError");
await runSelectMysteryEncounterOption(game, 2);
expect(scene.getCurrentPhase()?.constructor.name).toBe(MysteryEncounterPhase.name);
expect(scene.ui.playError).not.toHaveBeenCalled(); // No error sfx, option is disabled
expect(mysteryEncounterPhase.handleOptionSelect).not.toHaveBeenCalled();
expect(mysteryEncounterPhase.continueEncounter).not.toHaveBeenCalled();
});
it("Should skip fight when player meets requirements", async () => {
game.override.startingHeldItems([{name: "BERRY", count: 2, type: BerryType.SITRUS}, {name: "BERRY", count: 3, type: BerryType.GANLON}]);
const leaveEncounterWithoutBattleSpy = vi.spyOn(EncounterPhaseUtils, "leaveEncounterWithoutBattle");
await game.runToMysteryEncounter(MysteryEncounterType.UNCOMMON_BREED, defaultParty);
await runMysteryEncounterToEnd(game, 2);
expect(leaveEncounterWithoutBattleSpy).toBeCalled();
});
});
describe("Option 3 - Use an Attracting Move", () => {
it("should have the correct properties", () => {
const option = UncommonBreedEncounter.options[2];
expect(option.optionMode).toBe(MysteryEncounterOptionMode.DISABLED_OR_SPECIAL);
expect(option.dialogue).toBeDefined();
expect(option.dialogue).toStrictEqual({
buttonLabel: `${namespace}.option.3.label`,
buttonTooltip: `${namespace}.option.3.tooltip`,
disabledButtonTooltip: `${namespace}.option.3.disabled_tooltip`,
selected: [
{
text: `${namespace}.option.3.selected`,
}
],
});
});
it("should NOT be selectable if the player doesn't have an Attracting move", async () => {
await game.runToMysteryEncounter(MysteryEncounterType.UNCOMMON_BREED, defaultParty);
scene.getParty().forEach(p => p.moveset = []);
await game.phaseInterceptor.to(MysteryEncounterPhase, false);
const encounterPhase = scene.getCurrentPhase();
expect(encounterPhase?.constructor.name).toBe(MysteryEncounterPhase.name);
const mysteryEncounterPhase = encounterPhase as MysteryEncounterPhase;
vi.spyOn(mysteryEncounterPhase, "continueEncounter");
vi.spyOn(mysteryEncounterPhase, "handleOptionSelect");
vi.spyOn(scene.ui, "playError");
await runSelectMysteryEncounterOption(game, 3);
expect(scene.getCurrentPhase()?.constructor.name).toBe(MysteryEncounterPhase.name);
expect(scene.ui.playError).not.toHaveBeenCalled(); // No error sfx, option is disabled
expect(mysteryEncounterPhase.handleOptionSelect).not.toHaveBeenCalled();
expect(mysteryEncounterPhase.continueEncounter).not.toHaveBeenCalled();
});
it("Should skip fight when player meets requirements", async () => {
const leaveEncounterWithoutBattleSpy = vi.spyOn(EncounterPhaseUtils, "leaveEncounterWithoutBattle");
await game.runToMysteryEncounter(MysteryEncounterType.UNCOMMON_BREED, defaultParty);
// Mock moveset
scene.getParty()[0].moveset = [new PokemonMove(Moves.CHARM)];
await runMysteryEncounterToEnd(game, 3);
expect(leaveEncounterWithoutBattleSpy).toBeCalled();
});
});
});