From b6e90931acd2dddba8dee83dbb9bd59d4bc6c8bc Mon Sep 17 00:00:00 2001 From: ImperialSympathizer Date: Thu, 29 Aug 2024 12:46:23 -0400 Subject: [PATCH] Adds Uncommon Breed ME and misc. ME bug fixes --- src/data/battle-anims.ts | 6 +- .../encounters/absolute-avarice-encounter.ts | 4 +- .../encounters/dancing-lessons-encounter.ts | 12 +- .../encounters/fight-or-flight-encounter.ts | 15 +- .../encounters/safari-zone-encounter.ts | 16 +- .../encounters/trash-to-treasure-encounter.ts | 8 +- .../encounters/uncommon-breed-encounter.ts | 265 ++++++++++++++++++ .../mystery-encounters/mystery-encounters.ts | 5 +- .../utils/encounter-phase-utils.ts | 2 +- src/enums/mystery-encounter-type.ts | 3 +- src/locales/en/mystery-encounter.ts | 114 ++++---- .../fight-or-flight-dialogue.json | 3 +- .../uncommon-breed-dialogue.json | 26 ++ src/overrides.ts | 4 +- src/phases/mystery-encounter-phases.ts | 33 ++- .../uncommon-breed-encounter.test.ts | 240 ++++++++++++++++ 16 files changed, 667 insertions(+), 89 deletions(-) create mode 100644 src/data/mystery-encounters/encounters/uncommon-breed-encounter.ts create mode 100644 src/locales/en/mystery-encounters/uncommon-breed-dialogue.json create mode 100644 src/test/mystery-encounter/encounters/uncommon-breed-encounter.test.ts diff --git a/src/data/battle-anims.ts b/src/data/battle-anims.ts index ebd37c33bae..acb5320b301 100644 --- a/src/data/battle-anims.ts +++ b/src/data/battle-anims.ts @@ -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); diff --git a/src/data/mystery-encounters/encounters/absolute-avarice-encounter.ts b/src/data/mystery-encounters/encounters/absolute-avarice-encounter.ts index dae320434f5..59469ecd0b5 100644 --- a/src/data/mystery-encounters/encounters/absolute-avarice-encounter.ts +++ b/src/data/mystery-encounters/encounters/absolute-avarice-encounter.ts @@ -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: [ diff --git a/src/data/mystery-encounters/encounters/dancing-lessons-encounter.ts b/src/data/mystery-encounters/encounters/dancing-lessons-encounter.ts index 76cfea4da1a..f73b2d0265e 100644 --- a/src/data/mystery-encounters/encounters/dancing-lessons-encounter.ts +++ b/src/data/mystery-encounters/encounters/dancing-lessons-encounter.ts @@ -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, diff --git a/src/data/mystery-encounters/encounters/fight-or-flight-encounter.ts b/src/data/mystery-encounters/encounters/fight-or-flight-encounter.ts index 3cc84497dd3..5d410b6e3d7 100644 --- a/src/data/mystery-encounters/encounters/fight-or-flight-encounter.ts +++ b/src/data/mystery-encounters/encounters/fight-or-flight-encounter.ts @@ -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]); diff --git a/src/data/mystery-encounters/encounters/safari-zone-encounter.ts b/src/data/mystery-encounters/encounters/safari-zone-encounter.ts index 420352dc32d..c07d952579c 100644 --- a/src/data/mystery-encounters/encounters/safari-zone-encounter.ts +++ b/src/data/mystery-encounters/encounters/safari-zone-encounter.ts @@ -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 { - 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 { // 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 { - 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); 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 41c4991877c..6724b144f62 100644 --- a/src/data/mystery-encounters/encounters/trash-to-treasure-encounter.ts +++ b/src/data/mystery-encounters/encounters/trash-to-treasure-encounter.ts @@ -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"); }); } diff --git a/src/data/mystery-encounters/encounters/uncommon-breed-encounter.ts b/src/data/mystery-encounters/encounters/uncommon-breed-encounter.ts new file mode 100644 index 00000000000..7563e2d756e --- /dev/null +++ b/src/data/mystery-encounters/encounters/uncommon-breed-encounter.ts @@ -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(); diff --git a/src/data/mystery-encounters/mystery-encounters.ts b/src/data/mystery-encounters/mystery-encounters.ts index 595d9b578d4..1a842568e85 100644 --- a/src/data/mystery-encounters/mystery-encounters.ts +++ b/src/data/mystery-encounters/mystery-encounters.ts @@ -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 * ) / 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 => { diff --git a/src/data/mystery-encounters/utils/encounter-phase-utils.ts b/src/data/mystery-encounters/utils/encounter-phase-utils.ts index fb793a6f45a..7fc21cb4653 100644 --- a/src/data/mystery-encounters/utils/encounter-phase-utils.ts +++ b/src/data/mystery-encounters/utils/encounter-phase-utils.ts @@ -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} */ diff --git a/src/enums/mystery-encounter-type.ts b/src/enums/mystery-encounter-type.ts index d48bab3076b..1cb314c4ef7 100644 --- a/src/enums/mystery-encounter-type.ts +++ b/src/enums/mystery-encounter-type.ts @@ -26,5 +26,6 @@ export enum MysteryEncounterType { THE_WINSTRATE_CHALLENGE, TELEPORTING_HIJINKS, BUG_TYPE_SUPERFAN, - FUN_AND_GAMES + FUN_AND_GAMES, + UNCOMMON_BREED } diff --git a/src/locales/en/mystery-encounter.ts b/src/locales/en/mystery-encounter.ts index 95f472d5b0c..0bc6c277077 100644 --- a/src/locales/en/mystery-encounter.ts +++ b/src/locales/en/mystery-encounter.ts @@ -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; diff --git a/src/locales/en/mystery-encounters/fight-or-flight-dialogue.json b/src/locales/en/mystery-encounters/fight-or-flight-dialogue.json index 2a82287327c..24453746abe 100644 --- a/src/locales/en/mystery-encounters/fight-or-flight-dialogue.json +++ b/src/locales/en/mystery-encounters/fight-or-flight-dialogue.json @@ -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", diff --git a/src/locales/en/mystery-encounters/uncommon-breed-dialogue.json b/src/locales/en/mystery-encounters/uncommon-breed-dialogue.json new file mode 100644 index 00000000000..a1fef9a0001 --- /dev/null +++ b/src/locales/en/mystery-encounters/uncommon-breed-dialogue.json @@ -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!" + } + } +} \ No newline at end of file diff --git a/src/overrides.ts b/src/overrides.ts index 2f73e03e2b0..b4b166de336 100644 --- a/src/overrides.ts +++ b/src/overrides.ts @@ -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 diff --git a/src/phases/mystery-encounter-phases.ts b/src/phases/mystery-encounter-phases.ts index 6ba1f468440..e22f93d3206 100644 --- a/src/phases/mystery-encounter-phases.ts +++ b/src/phases/mystery-encounter-phases.ts @@ -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(); } diff --git a/src/test/mystery-encounter/encounters/uncommon-breed-encounter.test.ts b/src/test/mystery-encounter/encounters/uncommon-breed-encounter.test.ts new file mode 100644 index 00000000000..e488eaa816f --- /dev/null +++ b/src/test/mystery-encounter/encounters/uncommon-breed-encounter.test.ts @@ -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.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(); + }); + }); +});