pokerogue/src/data/mystery-encounters/encounters/weird-dream-encounter.ts
2025-06-19 22:29:37 -04:00

857 lines
31 KiB
TypeScript

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<boolean> {
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<void>(resolve => {
const allTransformationPromises: Promise<void>[] = [];
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<number | null> {
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;
}
}
}