import { PokemonType } from "#enums/pokemon-type"; import { MysteryEncounterType } from "#enums/mystery-encounter-type"; import { SpeciesId } from "#enums/species-id"; import { globalScene } from "#app/global-scene"; import type MysteryEncounter from "#app/data/mystery-encounters/mystery-encounter"; import { MysteryEncounterBuilder } from "#app/data/mystery-encounters/mystery-encounter"; import { MysteryEncounterOptionBuilder } from "#app/data/mystery-encounters/mystery-encounter-option"; import type { EnemyPartyConfig, EnemyPokemonConfig } from "../utils/encounter-phase-utils"; import { generateModifierType, initBattleWithEnemyConfig, leaveEncounterWithoutBattle, setEncounterRewards, } from "../utils/encounter-phase-utils"; import { MysteryEncounterTier } from "#enums/mystery-encounter-tier"; import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode"; import type { PlayerPokemon } from "#app/field/pokemon"; import type Pokemon from "#app/field/pokemon"; import { PokemonMove } from "#app/data/moves/pokemon-move"; import { NumberHolder, isNullOrUndefined, randSeedInt, randSeedShuffle } from "#app/utils/common"; import type PokemonSpecies from "#app/data/pokemon-species"; import { getPokemonSpecies } from "#app/utils/pokemon-utils"; import { allSpecies } from "#app/data/data-lists"; import type { PokemonHeldItemModifier } from "#app/modifier/modifier"; import { HiddenAbilityRateBoosterModifier, PokemonFormChangeItemModifier } from "#app/modifier/modifier"; import { achvs } from "#app/system/achv"; import { showEncounterText } from "#app/data/mystery-encounters/utils/encounter-dialogue-utils"; import type { PokemonHeldItemModifierType } from "#app/modifier/modifier-type"; import { modifierTypes } from "#app/data/data-lists"; import i18next from "#app/plugins/i18n"; import { doPokemonTransformationSequence, TransformationScreenPosition, } from "#app/data/mystery-encounters/utils/encounter-transformation-sequence"; import { getLevelTotalExp } from "#app/data/exp"; import { Stat } from "#enums/stat"; import { Challenges } from "#enums/challenges"; import { ModifierTier } from "#enums/modifier-tier"; import { PlayerGender } from "#enums/player-gender"; import { TrainerType } from "#enums/trainer-type"; import PokemonData from "#app/system/pokemon-data"; import { Nature } from "#enums/nature"; import type HeldModifierConfig from "#app/@types/held-modifier-config"; import { trainerConfigs } from "#app/data/trainers/trainer-config"; import { TrainerPartyTemplate } from "#app/data/trainers/TrainerPartyTemplate"; import { PartyMemberStrength } from "#enums/party-member-strength"; /** i18n namespace for encounter */ const namespace = "mysteryEncounters/weirdDream"; /** Exclude Ultra Beasts, Paradox, Eternatus, and all legendary/mythical/trio pokemon that are below 570 BST */ const EXCLUDED_TRANSFORMATION_SPECIES = [ SpeciesId.ETERNATUS, /** UBs */ SpeciesId.NIHILEGO, SpeciesId.BUZZWOLE, SpeciesId.PHEROMOSA, SpeciesId.XURKITREE, SpeciesId.CELESTEELA, SpeciesId.KARTANA, SpeciesId.GUZZLORD, SpeciesId.POIPOLE, SpeciesId.NAGANADEL, SpeciesId.STAKATAKA, SpeciesId.BLACEPHALON, /** Paradox */ SpeciesId.GREAT_TUSK, SpeciesId.SCREAM_TAIL, SpeciesId.BRUTE_BONNET, SpeciesId.FLUTTER_MANE, SpeciesId.SLITHER_WING, SpeciesId.SANDY_SHOCKS, SpeciesId.ROARING_MOON, SpeciesId.WALKING_WAKE, SpeciesId.GOUGING_FIRE, SpeciesId.RAGING_BOLT, SpeciesId.IRON_TREADS, SpeciesId.IRON_BUNDLE, SpeciesId.IRON_HANDS, SpeciesId.IRON_JUGULIS, SpeciesId.IRON_MOTH, SpeciesId.IRON_THORNS, SpeciesId.IRON_VALIANT, SpeciesId.IRON_LEAVES, SpeciesId.IRON_BOULDER, SpeciesId.IRON_CROWN, /** These are banned so they don't appear in the < 570 BST pool */ SpeciesId.COSMOG, SpeciesId.MELTAN, SpeciesId.KUBFU, SpeciesId.COSMOEM, SpeciesId.POIPOLE, SpeciesId.TERAPAGOS, SpeciesId.TYPE_NULL, SpeciesId.CALYREX, SpeciesId.NAGANADEL, SpeciesId.URSHIFU, SpeciesId.OGERPON, SpeciesId.OKIDOGI, SpeciesId.MUNKIDORI, SpeciesId.FEZANDIPITI, ]; const SUPER_LEGENDARY_BST_THRESHOLD = 600; const NON_LEGENDARY_BST_THRESHOLD = 570; const OLD_GATEAU_STATS_UP = 20; /** 0-100 */ const PERCENT_LEVEL_LOSS_ON_REFUSE = 10; /** * Value ranges of the resulting species BST transformations after adding values to original species * 2 Pokemon in the party use this range */ const HIGH_BST_TRANSFORM_BASE_VALUES: [number, number] = [90, 110]; /** * Value ranges of the resulting species BST transformations after adding values to original species * All remaining Pokemon in the party use this range */ const STANDARD_BST_TRANSFORM_BASE_VALUES: [number, number] = [40, 50]; /** * Weird Dream encounter. * @see {@link https://github.com/pagefaultgames/pokerogue/issues/3822 | GitHub Issue #3822} * @see For biome requirements check {@linkcode mysteryEncountersByBiome} */ export const WeirdDreamEncounter: MysteryEncounter = MysteryEncounterBuilder.withEncounterType( MysteryEncounterType.WEIRD_DREAM, ) .withEncounterTier(MysteryEncounterTier.ROGUE) .withDisallowedChallenges(Challenges.SINGLE_TYPE, Challenges.SINGLE_GENERATION) // TODO: should reset minimum wave to 10 when there are more Rogue tiers in pool. Matching Dark Deal minimum for now. .withSceneWaveRangeRequirement(30, 140) .withIntroSpriteConfigs([ { spriteKey: "weird_dream_woman", fileRoot: "mystery-encounters", hasShadow: true, y: 11, yShadow: 6, x: 4, }, ]) .withIntroDialogue([ { text: `${namespace}:intro`, }, { speaker: `${namespace}:speaker`, text: `${namespace}:intro_dialogue`, }, ]) .setLocalizationKey(`${namespace}`) .withTitle(`${namespace}:title`) .withDescription(`${namespace}:description`) .withQuery(`${namespace}:query`) .withOnInit(() => { globalScene.loadBgm("mystery_encounter_weird_dream", "mystery_encounter_weird_dream.mp3"); // Calculate all the newly transformed Pokemon and begin asset load const teamTransformations = getTeamTransformations(); const loadAssets = teamTransformations.map(t => (t.newPokemon as PlayerPokemon).loadAssets()); globalScene.currentBattle.mysteryEncounter!.misc = { teamTransformations, loadAssets, }; return true; }) .withOnVisualsStart(() => { globalScene.fadeAndSwitchBgm("mystery_encounter_weird_dream"); return true; }) .withOption( MysteryEncounterOptionBuilder.newOptionWithMode(MysteryEncounterOptionMode.DEFAULT) .withHasDexProgress(true) .withDialogue({ buttonLabel: `${namespace}:option.1.label`, buttonTooltip: `${namespace}:option.1.tooltip`, selected: [ { text: `${namespace}:option.1.selected`, }, ], }) .withPreOptionPhase(async () => { // Play the animation as the player goes through the dialogue globalScene.time.delayedCall(1000, () => { doShowDreamBackground(); }); for (const transformation of globalScene.currentBattle.mysteryEncounter!.misc.teamTransformations) { globalScene.removePokemonFromPlayerParty(transformation.previousPokemon, false); globalScene.getPlayerParty().push(transformation.newPokemon); } }) .withOptionPhase(async () => { // Starts cutscene dialogue, but does not await so that cutscene plays as player goes through dialogue const cutsceneDialoguePromise = showEncounterText(`${namespace}:option.1.cutscene`); // Change the entire player's party // Wait for all new Pokemon assets to be loaded before showing transformation animations await Promise.all(globalScene.currentBattle.mysteryEncounter!.misc.loadAssets); const transformations = globalScene.currentBattle.mysteryEncounter!.misc.teamTransformations; // If there are 1-3 transformations, do them centered back to back // Otherwise, the first 3 transformations are executed side-by-side, then any remaining 1-3 transformations occur in those same respective positions if (transformations.length <= 3) { for (const transformation of transformations) { const pokemon1 = transformation.previousPokemon; const pokemon2 = transformation.newPokemon; await doPokemonTransformationSequence(pokemon1, pokemon2, TransformationScreenPosition.CENTER); } } else { await doSideBySideTransformations(transformations); } // Make sure player has finished cutscene dialogue await cutsceneDialoguePromise; doHideDreamBackground(); await showEncounterText(`${namespace}:option.1.dream_complete`); await doNewTeamPostProcess(transformations); setEncounterRewards({ guaranteedModifierTypeFuncs: [ modifierTypes.MEMORY_MUSHROOM, modifierTypes.ROGUE_BALL, modifierTypes.MINT, modifierTypes.MINT, modifierTypes.MINT, ], fillRemaining: false, }); leaveEncounterWithoutBattle(true); }) .build(), ) .withSimpleOption( { buttonLabel: `${namespace}:option.2.label`, buttonTooltip: `${namespace}:option.2.tooltip`, selected: [ { text: `${namespace}:option.2.selected`, }, ], }, async () => { // Battle your "future" team for some item rewards const transformations: PokemonTransformation[] = globalScene.currentBattle.mysteryEncounter!.misc.teamTransformations; // Uses the pokemon that player's party would have transformed into const enemyPokemonConfigs: EnemyPokemonConfig[] = []; for (const transformation of transformations) { const newPokemon = transformation.newPokemon; const previousPokemon = transformation.previousPokemon; await postProcessTransformedPokemon(previousPokemon, newPokemon, newPokemon.species.getRootSpeciesId(), true); const dataSource = new PokemonData(newPokemon); dataSource.player = false; // Copy held items to new pokemon const newPokemonHeldItemConfigs: HeldModifierConfig[] = []; for (const item of transformation.heldItems) { newPokemonHeldItemConfigs.push({ modifier: item.clone() as PokemonHeldItemModifier, stackCount: item.getStackCount(), isTransferable: false, }); } // Any pokemon that is below 570 BST gets +20 permanent BST to 3 stats if (shouldGetOldGateau(newPokemon)) { const stats = getOldGateauBoostedStats(newPokemon); newPokemonHeldItemConfigs.push({ modifier: generateModifierType(modifierTypes.MYSTERY_ENCOUNTER_OLD_GATEAU, [ OLD_GATEAU_STATS_UP, stats, ]) as PokemonHeldItemModifierType, stackCount: 1, isTransferable: false, }); } const enemyConfig: EnemyPokemonConfig = { species: transformation.newSpecies, isBoss: newPokemon.getSpeciesForm().getBaseStatTotal() > NON_LEGENDARY_BST_THRESHOLD, level: previousPokemon.level, dataSource: dataSource, modifierConfigs: newPokemonHeldItemConfigs, }; enemyPokemonConfigs.push(enemyConfig); } const genderIndex = globalScene.gameData.gender ?? PlayerGender.UNSET; const trainerConfig = trainerConfigs[ genderIndex === PlayerGender.FEMALE ? TrainerType.FUTURE_SELF_F : TrainerType.FUTURE_SELF_M ].clone(); trainerConfig.setPartyTemplates(new TrainerPartyTemplate(transformations.length, PartyMemberStrength.STRONG)); const enemyPartyConfig: EnemyPartyConfig = { trainerConfig: trainerConfig, pokemonConfigs: enemyPokemonConfigs, female: genderIndex === PlayerGender.FEMALE, }; const onBeforeRewards = () => { // Before battle rewards, unlock the passive on a pokemon in the player's team for the rest of the run (not permanently) // One random pokemon will get its passive unlocked const passiveDisabledPokemon = globalScene.getPlayerParty().filter(p => !p.passive); if (passiveDisabledPokemon?.length > 0) { const enablePassiveMon = passiveDisabledPokemon[randSeedInt(passiveDisabledPokemon.length)]; enablePassiveMon.passive = true; enablePassiveMon.updateInfo(true); } }; setEncounterRewards( { guaranteedModifierTiers: [ ModifierTier.ROGUE, ModifierTier.ROGUE, ModifierTier.ULTRA, ModifierTier.ULTRA, ModifierTier.GREAT, ModifierTier.GREAT, ], fillRemaining: false, }, undefined, onBeforeRewards, ); await showEncounterText(`${namespace}:option.2.selected_2`, null, undefined, true); await initBattleWithEnemyConfig(enemyPartyConfig); }, ) .withSimpleOption( { buttonLabel: `${namespace}:option.3.label`, buttonTooltip: `${namespace}:option.3.tooltip`, selected: [ { text: `${namespace}:option.3.selected`, }, ], }, async () => { // Leave, reduce party levels by 10% for (const pokemon of globalScene.getPlayerParty()) { pokemon.level = Math.max(Math.ceil(((100 - PERCENT_LEVEL_LOSS_ON_REFUSE) / 100) * pokemon.level), 1); pokemon.exp = getLevelTotalExp(pokemon.level, pokemon.species.growthRate); pokemon.levelExp = 0; pokemon.calculateStats(); pokemon.getBattleInfo().setLevel(pokemon.level); await pokemon.updateInfo(); } leaveEncounterWithoutBattle(true); return true; }, ) .build(); interface PokemonTransformation { previousPokemon: PlayerPokemon; newSpecies: PokemonSpecies; newPokemon: PlayerPokemon; heldItems: PokemonHeldItemModifier[]; } function getTeamTransformations(): PokemonTransformation[] { const party = globalScene.getPlayerParty(); // Removes all pokemon from the party const alreadyUsedSpecies: PokemonSpecies[] = party.map(p => p.species); const pokemonTransformations: PokemonTransformation[] = party.map(p => { return { previousPokemon: p, } as PokemonTransformation; }); // Only 1 Pokemon can be transformed into BST higher than 600 let hasPokemonInSuperLegendaryBstThreshold = false; // Only 1 other Pokemon can be transformed into BST between 570-600 let hasPokemonInLegendaryBstThreshold = false; // First, roll 2 of the party members to new Pokemon at a +90 to +110 BST difference // Then, roll the remainder of the party members at a +40 to +50 BST difference const numPokemon = party.length; const removedPokemon = randSeedShuffle(party.slice(0)); for (let i = 0; i < numPokemon; i++) { const removed = removedPokemon[i]; const index = pokemonTransformations.findIndex(p => p.previousPokemon.id === removed.id); pokemonTransformations[index].heldItems = removed .getHeldItems() .filter(m => !(m instanceof PokemonFormChangeItemModifier)); const bst = removed.getSpeciesForm().getBaseStatTotal(); let newBstRange: [number, number]; if (i < 2) { newBstRange = HIGH_BST_TRANSFORM_BASE_VALUES; } else { newBstRange = STANDARD_BST_TRANSFORM_BASE_VALUES; } const newSpecies = getTransformedSpecies( bst, newBstRange, hasPokemonInSuperLegendaryBstThreshold, hasPokemonInLegendaryBstThreshold, alreadyUsedSpecies, ); const newSpeciesBst = newSpecies.getBaseStatTotal(); if (newSpeciesBst > SUPER_LEGENDARY_BST_THRESHOLD) { hasPokemonInSuperLegendaryBstThreshold = true; } if (newSpeciesBst <= SUPER_LEGENDARY_BST_THRESHOLD && newSpeciesBst >= NON_LEGENDARY_BST_THRESHOLD) { hasPokemonInLegendaryBstThreshold = true; } pokemonTransformations[index].newSpecies = newSpecies; console.log("New species: " + JSON.stringify(newSpecies)); alreadyUsedSpecies.push(newSpecies); } for (const transformation of pokemonTransformations) { const newAbilityIndex = randSeedInt(transformation.newSpecies.getAbilityCount()); transformation.newPokemon = globalScene.addPlayerPokemon( transformation.newSpecies, transformation.previousPokemon.level, newAbilityIndex, undefined, ); } return pokemonTransformations; } async function doNewTeamPostProcess(transformations: PokemonTransformation[]) { let atLeastOneNewStarter = false; for (const transformation of transformations) { const previousPokemon = transformation.previousPokemon; const newPokemon = transformation.newPokemon; const speciesRootForm = newPokemon.species.getRootSpeciesId(); if (await postProcessTransformedPokemon(previousPokemon, newPokemon, speciesRootForm)) { atLeastOneNewStarter = true; } // Copy old items to new pokemon for (const item of transformation.heldItems) { item.pokemonId = newPokemon.id; globalScene.addModifier(item, false, false, false, true); } // Any pokemon that is below 570 BST gets +20 permanent BST to 3 stats if (shouldGetOldGateau(newPokemon)) { const stats = getOldGateauBoostedStats(newPokemon); const modType = modifierTypes .MYSTERY_ENCOUNTER_OLD_GATEAU() .generateType(globalScene.getPlayerParty(), [OLD_GATEAU_STATS_UP, stats]) ?.withIdFromFunc(modifierTypes.MYSTERY_ENCOUNTER_OLD_GATEAU); const modifier = modType?.newModifier(newPokemon); if (modifier) { globalScene.addModifier(modifier, false, false, false, true); } } newPokemon.calculateStats(); await newPokemon.updateInfo(); } // One random pokemon will get its passive unlocked const passiveDisabledPokemon = globalScene.getPlayerParty().filter(p => !p.passive); if (passiveDisabledPokemon?.length > 0) { const enablePassiveMon = passiveDisabledPokemon[randSeedInt(passiveDisabledPokemon.length)]; enablePassiveMon.passive = true; await enablePassiveMon.updateInfo(true); } // If at least one new starter was unlocked, play 1 fanfare if (atLeastOneNewStarter) { globalScene.playSound("level_up_fanfare"); } } /** * Applies special changes to the newly transformed pokemon, such as passing previous moves, gaining egg moves, etc. * Returns whether the transformed pokemon unlocks a new starter for the player. * @param previousPokemon * @param newPokemon * @param speciesRootForm * @param forBattle Default `false`. If false, will perform achievements and dex unlocks for the player. */ async function postProcessTransformedPokemon( previousPokemon: PlayerPokemon, newPokemon: PlayerPokemon, speciesRootForm: SpeciesId, forBattle = false, ): Promise { let isNewStarter = false; // Roll HA a second time if (newPokemon.species.abilityHidden) { const hiddenIndex = newPokemon.species.ability2 ? 2 : 1; if (newPokemon.abilityIndex < hiddenIndex) { const hiddenAbilityChance = new NumberHolder(256); globalScene.applyModifiers(HiddenAbilityRateBoosterModifier, true, hiddenAbilityChance); const hasHiddenAbility = randSeedInt(hiddenAbilityChance.value) === 0; if (hasHiddenAbility) { newPokemon.abilityIndex = hiddenIndex; } } } // Roll IVs a second time newPokemon.ivs = newPokemon.ivs.map(iv => { const newValue = randSeedInt(31); return newValue > iv ? newValue : iv; }); // Roll a neutral nature newPokemon.nature = [Nature.HARDY, Nature.DOCILE, Nature.BASHFUL, Nature.QUIRKY, Nature.SERIOUS][randSeedInt(5)]; // For pokemon at/below 570 BST or any shiny pokemon, unlock it permanently as if you had caught it if ( !forBattle && (newPokemon.getSpeciesForm().getBaseStatTotal() <= NON_LEGENDARY_BST_THRESHOLD || newPokemon.isShiny()) ) { if ( newPokemon.getSpeciesForm().abilityHidden && newPokemon.abilityIndex === newPokemon.getSpeciesForm().getAbilityCount() - 1 ) { globalScene.validateAchv(achvs.HIDDEN_ABILITY); } if (newPokemon.species.subLegendary) { globalScene.validateAchv(achvs.CATCH_SUB_LEGENDARY); } if (newPokemon.species.legendary) { globalScene.validateAchv(achvs.CATCH_LEGENDARY); } if (newPokemon.species.mythical) { globalScene.validateAchv(achvs.CATCH_MYTHICAL); } globalScene.gameData.updateSpeciesDexIvs(newPokemon.species.getRootSpeciesId(true), newPokemon.ivs); const newStarterUnlocked = await globalScene.gameData.setPokemonCaught(newPokemon, true, false, false); if (newStarterUnlocked) { isNewStarter = true; await showEncounterText( i18next.t("battle:addedAsAStarter", { pokemonName: getPokemonSpecies(speciesRootForm).getName(), }), ); } } // If the previous pokemon had pokerus, transfer to new pokemon newPokemon.pokerus = previousPokemon.pokerus; // Transfer previous Pokemon's luck value newPokemon.luck = previousPokemon.getLuck(); // If the previous pokemon had higher IVs, override to those (after updating dex IVs > prevents perfect 31s on a new unlock) newPokemon.ivs = newPokemon.ivs.map((iv, index) => { return previousPokemon.ivs[index] > iv ? previousPokemon.ivs[index] : iv; }); // For pokemon that the player owns (including ones just caught), gain a candy if (!forBattle && !!globalScene.gameData.dexData[speciesRootForm].caughtAttr) { globalScene.gameData.addStarterCandy(getPokemonSpecies(speciesRootForm), 1); } // Set the moveset of the new pokemon to be the same as previous, but with 1 egg move and 1 (attempted) STAB move of the new species newPokemon.generateAndPopulateMoveset(); // Store a copy of a "standard" generated moveset for the new pokemon, will be used later for finding a favored move const newPokemonGeneratedMoveset = newPokemon.moveset; newPokemon.moveset = previousPokemon.moveset.slice(0); const newEggMoveIndex = await addEggMoveToNewPokemonMoveset(newPokemon, speciesRootForm, forBattle); // Try to add a favored STAB move (might fail if Pokemon already knows a bunch of moves from newPokemonGeneratedMoveset) addFavoredMoveToNewPokemonMoveset(newPokemon, newPokemonGeneratedMoveset, newEggMoveIndex); // Randomize the second type of the pokemon // If the pokemon does not normally have a second type, it will gain 1 const newTypes = [PokemonType.UNKNOWN]; let newType = randSeedInt(18) as PokemonType; while (newType === newTypes[0]) { newType = randSeedInt(18) as PokemonType; } newTypes.push(newType); newPokemon.customPokemonData.types = newTypes; // Enable passive if previous had it newPokemon.passive = previousPokemon.passive; return isNewStarter; } /** * @returns `true` if a given Pokemon has valid BST to be given an Old Gateau */ function shouldGetOldGateau(pokemon: Pokemon): boolean { return pokemon.getSpeciesForm().getBaseStatTotal() < NON_LEGENDARY_BST_THRESHOLD; } /** * Get the lowest of HP/Spd, lowest of Atk/SpAtk, and lowest of Def/SpDef * @returns Array of 3 {@linkcode Stat}s to boost */ function getOldGateauBoostedStats(pokemon: Pokemon): Stat[] { const stats: Stat[] = []; const baseStats = pokemon.getSpeciesForm().baseStats.slice(0); // HP or Speed stats.push(baseStats[Stat.HP] < baseStats[Stat.SPD] ? Stat.HP : Stat.SPD); // Attack or SpAtk stats.push(baseStats[Stat.ATK] < baseStats[Stat.SPATK] ? Stat.ATK : Stat.SPATK); // Def or SpDef stats.push(baseStats[Stat.DEF] < baseStats[Stat.SPDEF] ? Stat.DEF : Stat.SPDEF); return stats; } function getTransformedSpecies( originalBst: number, bstSearchRange: [number, number], hasPokemonBstHigherThan600: boolean, hasPokemonBstBetween570And600: boolean, alreadyUsedSpecies: PokemonSpecies[], ): PokemonSpecies { let newSpecies: PokemonSpecies | undefined; while (isNullOrUndefined(newSpecies)) { const bstCap = originalBst + bstSearchRange[1]; const bstMin = Math.max(originalBst + bstSearchRange[0], 0); // Get any/all species that fall within the Bst range requirements let validSpecies = allSpecies.filter(s => { const speciesBst = s.getBaseStatTotal(); const bstInRange = speciesBst >= bstMin && speciesBst <= bstCap; // Checks that a Pokemon has not already been added in the +600 or 570-600 slots; const validBst = (!hasPokemonBstBetween570And600 || speciesBst < NON_LEGENDARY_BST_THRESHOLD || speciesBst > SUPER_LEGENDARY_BST_THRESHOLD) && (!hasPokemonBstHigherThan600 || speciesBst <= SUPER_LEGENDARY_BST_THRESHOLD); return bstInRange && validBst && !EXCLUDED_TRANSFORMATION_SPECIES.includes(s.speciesId); }); // There must be at least 20 species available before it will choose one if (validSpecies?.length > 20) { validSpecies = randSeedShuffle(validSpecies); newSpecies = validSpecies.pop(); while (isNullOrUndefined(newSpecies) || alreadyUsedSpecies.includes(newSpecies)) { newSpecies = validSpecies.pop(); } } else { // Expands search rand until a Pokemon is found bstSearchRange[0] -= 10; bstSearchRange[1] += 10; } } return newSpecies; } function doShowDreamBackground() { const transformationContainer = globalScene.add.container(0, -globalScene.game.canvas.height / 6); transformationContainer.name = "Dream Background"; // In case it takes a bit for video to load const transformationStaticBg = globalScene.add.rectangle( 0, 0, globalScene.game.canvas.width / 6, globalScene.game.canvas.height / 6, 0, ); transformationStaticBg.setName("Black Background"); transformationStaticBg.setOrigin(0, 0); transformationContainer.add(transformationStaticBg); transformationStaticBg.setVisible(true); const transformationVideoBg: Phaser.GameObjects.Video = globalScene.add.video(0, 0, "evo_bg").stop(); transformationVideoBg.setLoop(true); transformationVideoBg.setOrigin(0, 0); transformationVideoBg.setScale(0.4359673025); transformationContainer.add(transformationVideoBg); globalScene.fieldUI.add(transformationContainer); globalScene.fieldUI.bringToTop(transformationContainer); transformationVideoBg.play(); transformationContainer.setVisible(true); transformationContainer.alpha = 0; globalScene.tweens.add({ targets: transformationContainer, alpha: 1, duration: 3000, ease: "Sine.easeInOut", }); } function doHideDreamBackground() { const transformationContainer = globalScene.fieldUI.getByName("Dream Background"); globalScene.tweens.add({ targets: transformationContainer, alpha: 0, duration: 3000, ease: "Sine.easeInOut", onComplete: () => { globalScene.fieldUI.remove(transformationContainer, true); }, }); } function doSideBySideTransformations(transformations: PokemonTransformation[]) { return new Promise(resolve => { const allTransformationPromises: Promise[] = []; for (let i = 0; i < 3; i++) { const delay = i * 4000; globalScene.time.delayedCall(delay, () => { const transformation = transformations[i]; const pokemon1 = transformation.previousPokemon; const pokemon2 = transformation.newPokemon; const screenPosition = i as TransformationScreenPosition; const transformationPromise = doPokemonTransformationSequence(pokemon1, pokemon2, screenPosition).then(() => { if (transformations.length > i + 3) { const nextTransformationAtPosition = transformations[i + 3]; const nextPokemon1 = nextTransformationAtPosition.previousPokemon; const nextPokemon2 = nextTransformationAtPosition.newPokemon; allTransformationPromises.push(doPokemonTransformationSequence(nextPokemon1, nextPokemon2, screenPosition)); } }); allTransformationPromises.push(transformationPromise); }); } // Wait for all transformations to be loaded into promise array const id = setInterval(checkAllPromisesExist, 500); async function checkAllPromisesExist() { if (allTransformationPromises.length === transformations.length) { clearInterval(id); await Promise.all(allTransformationPromises); resolve(); } } }); } /** * Returns index of the new egg move within the Pokemon's moveset (not the index of the move in `speciesEggMoves`) * @param newPokemon * @param speciesRootForm */ async function addEggMoveToNewPokemonMoveset( newPokemon: PlayerPokemon, speciesRootForm: SpeciesId, forBattle = false, ): Promise { let eggMoveIndex: null | number = null; const eggMoves = newPokemon.getEggMoves()?.slice(0); if (eggMoves) { const eggMoveIndices = randSeedShuffle([0, 1, 2, 3]); let randomEggMoveIndex = eggMoveIndices.pop(); let randomEggMove = !isNullOrUndefined(randomEggMoveIndex) ? eggMoves[randomEggMoveIndex] : null; let retries = 0; while (retries < 3 && (!randomEggMove || newPokemon.moveset.some(m => m.moveId === randomEggMove))) { // If Pokemon already knows this move, roll for another egg move randomEggMoveIndex = eggMoveIndices.pop(); randomEggMove = !isNullOrUndefined(randomEggMoveIndex) ? eggMoves[randomEggMoveIndex] : null; retries++; } if (randomEggMove) { if (!newPokemon.moveset.some(m => m.moveId === randomEggMove)) { if (newPokemon.moveset.length < 4) { newPokemon.moveset.push(new PokemonMove(randomEggMove)); } else { eggMoveIndex = randSeedInt(4); newPokemon.moveset[eggMoveIndex] = new PokemonMove(randomEggMove); } } // For pokemon that the player owns (including ones just caught), unlock the egg move if ( !forBattle && !isNullOrUndefined(randomEggMoveIndex) && !!globalScene.gameData.dexData[speciesRootForm].caughtAttr ) { await globalScene.gameData.setEggMoveUnlocked(getPokemonSpecies(speciesRootForm), randomEggMoveIndex, true); } } } return eggMoveIndex; } /** * Returns index of the new egg move within the Pokemon's moveset (not the index of the move in `speciesEggMoves`) * @param newPokemon * @param newPokemonGeneratedMoveset * @param newEggMoveIndex */ function addFavoredMoveToNewPokemonMoveset( newPokemon: PlayerPokemon, newPokemonGeneratedMoveset: PokemonMove[], newEggMoveIndex: number | null, ) { let favoredMove: PokemonMove | null = null; for (const move of newPokemonGeneratedMoveset) { // Needs to match first type, second type will be replaced if (move?.getMove().type === newPokemon.getTypes()[0] && !newPokemon.moveset.some(m => m.moveId === move.moveId)) { favoredMove = move; break; } } // If was unable to find a favored move, uses first move in moveset that isn't already known (typically a high power STAB move) // Otherwise, it gains no favored move if (!favoredMove) { for (const move of newPokemonGeneratedMoveset) { // Needs to match first type, second type will be replaced if (!newPokemon.moveset.some(m => m.moveId === move.moveId)) { favoredMove = move; break; } } } // Finally, assign favored move to random index that isn't the new egg move index if (favoredMove) { if (newPokemon.moveset.length < 4) { newPokemon.moveset.push(favoredMove); } else { let favoredMoveIndex = randSeedInt(4); while (newEggMoveIndex !== null && favoredMoveIndex === newEggMoveIndex) { favoredMoveIndex = randSeedInt(4); } newPokemon.moveset[favoredMoveIndex] = favoredMove; } } }