mirror of
https://github.com/pagefaultgames/pokerogue.git
synced 2025-07-02 06:22:15 +02:00
* Add abilityAttr.is methods * [WIP] move modifier stuff around * Untangle circular deps from modifiers * Move unlockables to own file * Untangle all circular deps outside of MEs * Move constants in MEs to their own files * Re-add missing import to battle.ts * Add necessary overload for getTag * Add missing type import in weather.ts * Init modifier types and pools in loading-scene * Remove stray commented code * Apply kev's suggestions from code review Co-authored-by: NightKev <34855794+DayKev@users.noreply.github.com> --------- Co-authored-by: NightKev <34855794+DayKev@users.noreply.github.com>
1005 lines
39 KiB
TypeScript
1005 lines
39 KiB
TypeScript
import {
|
|
leaveEncounterWithoutBattle,
|
|
selectPokemonForOption,
|
|
setEncounterRewards,
|
|
} from "#app/data/mystery-encounters/utils/encounter-phase-utils";
|
|
import { TrainerSlot } from "#enums/trainer-slot";
|
|
import { ModifierTier } from "#enums/modifier-tier";
|
|
import { MusicPreference } from "#app/system/settings/settings";
|
|
import type { ModifierTypeOption } from "#app/modifier/modifier-type";
|
|
import {
|
|
getPlayerModifierTypeOptions,
|
|
regenerateModifierPoolThresholds,
|
|
} from "#app/modifier/modifier-type";
|
|
import { ModifierPoolType } from "#enums/modifier-pool-type";
|
|
import { MysteryEncounterType } from "#enums/mystery-encounter-type";
|
|
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 { MysteryEncounterTier } from "#enums/mystery-encounter-tier";
|
|
import { SpeciesId } from "#enums/species-id";
|
|
import type PokemonSpecies from "#app/data/pokemon-species";
|
|
import { allSpecies, getPokemonSpecies } from "#app/data/pokemon-species";
|
|
import { getTypeRgb } from "#app/data/type";
|
|
import { MysteryEncounterOptionBuilder } from "#app/data/mystery-encounters/mystery-encounter-option";
|
|
import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode";
|
|
import {
|
|
NumberHolder,
|
|
isNullOrUndefined,
|
|
randInt,
|
|
randSeedInt,
|
|
randSeedShuffle,
|
|
randSeedItem,
|
|
} from "#app/utils/common";
|
|
import type { PlayerPokemon } from "#app/field/pokemon";
|
|
import type Pokemon from "#app/field/pokemon";
|
|
import { EnemyPokemon } from "#app/field/pokemon";
|
|
import { PokemonMove } from "#app/data/moves/pokemon-move";
|
|
import type { PokemonHeldItemModifier } from "#app/modifier/modifier";
|
|
import {
|
|
HiddenAbilityRateBoosterModifier,
|
|
PokemonFormChangeItemModifier,
|
|
ShinyRateBoosterModifier,
|
|
SpeciesStatBoosterModifier,
|
|
} from "#app/modifier/modifier";
|
|
import type { OptionSelectItem } from "#app/ui/abstact-option-select-ui-handler";
|
|
import PokemonData from "#app/system/pokemon-data";
|
|
import i18next from "i18next";
|
|
import { Gender, getGenderSymbol } from "#app/data/gender";
|
|
import { getNatureName } from "#app/data/nature";
|
|
import { getPokeballAtlasKey, getPokeballTintColor } from "#app/data/pokeball";
|
|
import { getEncounterText, showEncounterText } from "#app/data/mystery-encounters/utils/encounter-dialogue-utils";
|
|
import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/constants";
|
|
import { addPokemonDataToDexAndValidateAchievements } from "#app/data/mystery-encounters/utils/encounter-pokemon-utils";
|
|
import type { PokeballType } from "#enums/pokeball";
|
|
import { doShinySparkleAnim } from "#app/field/anims";
|
|
import { TrainerType } from "#enums/trainer-type";
|
|
import { timedEventManager } from "#app/global-event-manager";
|
|
|
|
/** the i18n namespace for the encounter */
|
|
const namespace = "mysteryEncounters/globalTradeSystem";
|
|
|
|
/** Base shiny chance of 512/65536 -> 1/128 odds, affected by events and Shiny Charms. Cannot exceed 1/16 odds. */
|
|
const WONDER_TRADE_SHINY_CHANCE = 512;
|
|
/** Max shiny chance of 4096/65536 -> 1/16 odds. */
|
|
const MAX_WONDER_TRADE_SHINY_CHANCE = 4096;
|
|
|
|
const LEGENDARY_TRADE_POOLS = {
|
|
1: [SpeciesId.RATTATA, SpeciesId.PIDGEY, SpeciesId.WEEDLE],
|
|
2: [SpeciesId.SENTRET, SpeciesId.HOOTHOOT, SpeciesId.LEDYBA],
|
|
3: [SpeciesId.POOCHYENA, SpeciesId.ZIGZAGOON, SpeciesId.TAILLOW],
|
|
4: [SpeciesId.BIDOOF, SpeciesId.STARLY, SpeciesId.KRICKETOT],
|
|
5: [SpeciesId.PATRAT, SpeciesId.PURRLOIN, SpeciesId.PIDOVE],
|
|
6: [SpeciesId.BUNNELBY, SpeciesId.LITLEO, SpeciesId.SCATTERBUG],
|
|
7: [SpeciesId.PIKIPEK, SpeciesId.YUNGOOS, SpeciesId.ROCKRUFF],
|
|
8: [SpeciesId.SKWOVET, SpeciesId.WOOLOO, SpeciesId.ROOKIDEE],
|
|
9: [SpeciesId.LECHONK, SpeciesId.FIDOUGH, SpeciesId.TAROUNTULA],
|
|
};
|
|
|
|
/** Exclude Paradox mons as they aren't considered legendary/mythical */
|
|
const EXCLUDED_TRADE_SPECIES = [
|
|
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,
|
|
];
|
|
|
|
/**
|
|
* Global Trade System encounter.
|
|
* @see {@link https://github.com/pagefaultgames/pokerogue/issues/3812 | GitHub Issue #3812}
|
|
* @see For biome requirements check {@linkcode mysteryEncountersByBiome}
|
|
*/
|
|
export const GlobalTradeSystemEncounter: MysteryEncounter = MysteryEncounterBuilder.withEncounterType(
|
|
MysteryEncounterType.GLOBAL_TRADE_SYSTEM,
|
|
)
|
|
.withEncounterTier(MysteryEncounterTier.COMMON)
|
|
.withSceneWaveRangeRequirement(...CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES)
|
|
.withAutoHideIntroVisuals(false)
|
|
.withIntroSpriteConfigs([
|
|
{
|
|
spriteKey: "global_trade_system",
|
|
fileRoot: "mystery-encounters",
|
|
hasShadow: true,
|
|
disableAnimation: true,
|
|
x: 3,
|
|
y: 5,
|
|
yShadow: 1,
|
|
},
|
|
])
|
|
.withIntroDialogue([
|
|
{
|
|
text: `${namespace}:intro`,
|
|
},
|
|
])
|
|
.setLocalizationKey(`${namespace}`)
|
|
.withTitle(`${namespace}:title`)
|
|
.withDescription(`${namespace}:description`)
|
|
.withQuery(`${namespace}:query`)
|
|
.withOnInit(() => {
|
|
const encounter = globalScene.currentBattle.mysteryEncounter!;
|
|
|
|
// Load bgm
|
|
let bgmKey: string;
|
|
if (globalScene.musicPreference === MusicPreference.GENFIVE) {
|
|
bgmKey = "mystery_encounter_gen_5_gts";
|
|
globalScene.loadBgm(bgmKey, `${bgmKey}.mp3`);
|
|
} else {
|
|
// Mixed option
|
|
bgmKey = "mystery_encounter_gen_6_gts";
|
|
globalScene.loadBgm(bgmKey, `${bgmKey}.mp3`);
|
|
}
|
|
|
|
// Load possible trade options
|
|
// Maps current party member's id to 3 EnemyPokemon objects
|
|
// None of the trade options can be the same species
|
|
const tradeOptionsMap: Map<number, EnemyPokemon[]> = getPokemonTradeOptions();
|
|
encounter.misc = {
|
|
tradeOptionsMap,
|
|
bgmKey,
|
|
};
|
|
|
|
return true;
|
|
})
|
|
.withOnVisualsStart(() => {
|
|
globalScene.fadeAndSwitchBgm(globalScene.currentBattle.mysteryEncounter!.misc.bgmKey);
|
|
return true;
|
|
})
|
|
.withOption(
|
|
MysteryEncounterOptionBuilder.newOptionWithMode(MysteryEncounterOptionMode.DEFAULT)
|
|
.withHasDexProgress(true)
|
|
.withDialogue({
|
|
buttonLabel: `${namespace}:option.1.label`,
|
|
buttonTooltip: `${namespace}:option.1.tooltip`,
|
|
secondOptionPrompt: `${namespace}:option.1.trade_options_prompt`,
|
|
})
|
|
.withPreOptionPhase(async (): Promise<boolean> => {
|
|
const encounter = globalScene.currentBattle.mysteryEncounter!;
|
|
const onPokemonSelected = (pokemon: PlayerPokemon) => {
|
|
// Get the trade species options for the selected pokemon
|
|
const tradeOptionsMap: Map<number, EnemyPokemon[]> = encounter.misc.tradeOptionsMap;
|
|
const tradeOptions = tradeOptionsMap.get(pokemon.id);
|
|
if (!tradeOptions) {
|
|
return [];
|
|
}
|
|
|
|
return tradeOptions.map((tradePokemon: EnemyPokemon) => {
|
|
const option: OptionSelectItem = {
|
|
label: tradePokemon.getNameToRender(),
|
|
handler: () => {
|
|
// Pokemon trade selected
|
|
encounter.setDialogueToken("tradedPokemon", pokemon.getNameToRender());
|
|
encounter.setDialogueToken("received", tradePokemon.getNameToRender());
|
|
encounter.misc.tradedPokemon = pokemon;
|
|
encounter.misc.receivedPokemon = tradePokemon;
|
|
return true;
|
|
},
|
|
onHover: () => {
|
|
const formName =
|
|
tradePokemon.species.forms && tradePokemon.species.forms.length > tradePokemon.formIndex
|
|
? tradePokemon.species.forms[tradePokemon.formIndex].formName
|
|
: null;
|
|
const line1 = `${i18next.t("pokemonInfoContainer:ability")} ${tradePokemon.getAbility().name}${
|
|
tradePokemon.getGender() !== Gender.GENDERLESS
|
|
? ` | ${i18next.t("pokemonInfoContainer:gender")} ${getGenderSymbol(tradePokemon.getGender())}`
|
|
: ""
|
|
}`;
|
|
const line2 =
|
|
i18next.t("pokemonInfoContainer:nature") +
|
|
" " +
|
|
getNatureName(tradePokemon.getNature()) +
|
|
(formName ? ` | ${i18next.t("pokemonInfoContainer:form")} ${formName}` : "");
|
|
showEncounterText(`${line1}\n${line2}`, 0, 0, false);
|
|
},
|
|
};
|
|
return option;
|
|
});
|
|
};
|
|
|
|
return selectPokemonForOption(onPokemonSelected);
|
|
})
|
|
.withOptionPhase(async () => {
|
|
const encounter = globalScene.currentBattle.mysteryEncounter!;
|
|
const tradedPokemon: PlayerPokemon = encounter.misc.tradedPokemon;
|
|
const receivedPokemonData: EnemyPokemon = encounter.misc.receivedPokemon;
|
|
const modifiers = tradedPokemon
|
|
.getHeldItems()
|
|
.filter(m => !(m instanceof PokemonFormChangeItemModifier) && !(m instanceof SpeciesStatBoosterModifier));
|
|
|
|
// Generate a trainer name
|
|
const traderName = generateRandomTraderName();
|
|
encounter.setDialogueToken("tradeTrainerName", traderName.trim());
|
|
|
|
// Remove the original party member from party
|
|
globalScene.removePokemonFromPlayerParty(tradedPokemon, false);
|
|
|
|
// Set data properly, then generate the new Pokemon's assets
|
|
receivedPokemonData.passive = tradedPokemon.passive;
|
|
// Pokeball to Ultra ball, randomly
|
|
receivedPokemonData.pokeball = randInt(4) as PokeballType;
|
|
const dataSource = new PokemonData(receivedPokemonData);
|
|
const newPlayerPokemon = globalScene.addPlayerPokemon(
|
|
receivedPokemonData.species,
|
|
receivedPokemonData.level,
|
|
dataSource.abilityIndex,
|
|
dataSource.formIndex,
|
|
dataSource.gender,
|
|
dataSource.shiny,
|
|
dataSource.variant,
|
|
dataSource.ivs,
|
|
dataSource.nature,
|
|
dataSource,
|
|
);
|
|
globalScene.getPlayerParty().push(newPlayerPokemon);
|
|
await newPlayerPokemon.loadAssets();
|
|
|
|
for (const mod of modifiers) {
|
|
mod.pokemonId = newPlayerPokemon.id;
|
|
globalScene.addModifier(mod, true, false, false, true);
|
|
}
|
|
|
|
// Show the trade animation
|
|
await showTradeBackground();
|
|
await doPokemonTradeSequence(tradedPokemon, newPlayerPokemon);
|
|
await showEncounterText(`${namespace}:trade_received`, null, 0, true, 4000);
|
|
globalScene.playBgm(encounter.misc.bgmKey);
|
|
await addPokemonDataToDexAndValidateAchievements(newPlayerPokemon);
|
|
await hideTradeBackground();
|
|
tradedPokemon.destroy();
|
|
|
|
leaveEncounterWithoutBattle(true);
|
|
})
|
|
.build(),
|
|
)
|
|
.withOption(
|
|
MysteryEncounterOptionBuilder.newOptionWithMode(MysteryEncounterOptionMode.DEFAULT)
|
|
.withHasDexProgress(true)
|
|
.withDialogue({
|
|
buttonLabel: `${namespace}:option.2.label`,
|
|
buttonTooltip: `${namespace}:option.2.tooltip`,
|
|
})
|
|
.withPreOptionPhase(async (): Promise<boolean> => {
|
|
const encounter = globalScene.currentBattle.mysteryEncounter!;
|
|
const onPokemonSelected = (pokemon: PlayerPokemon) => {
|
|
// Randomly generate a Wonder Trade pokemon
|
|
const randomTradeOption = generateTradeOption(globalScene.getPlayerParty().map(p => p.species));
|
|
const tradePokemon = new EnemyPokemon(randomTradeOption, pokemon.level, TrainerSlot.NONE, false);
|
|
// Extra shiny roll at 1/128 odds (boosted by events and charms)
|
|
if (!tradePokemon.shiny) {
|
|
const shinyThreshold = new NumberHolder(WONDER_TRADE_SHINY_CHANCE);
|
|
if (timedEventManager.isEventActive()) {
|
|
shinyThreshold.value *= timedEventManager.getShinyMultiplier();
|
|
}
|
|
globalScene.applyModifiers(ShinyRateBoosterModifier, true, shinyThreshold);
|
|
|
|
// Base shiny chance of 512/65536 -> 1/128, affected by events and Shiny Charms
|
|
// Maximum shiny chance of 4096/65536 -> 1/16, cannot improve further after that
|
|
const shinyChance = Math.min(shinyThreshold.value, MAX_WONDER_TRADE_SHINY_CHANCE);
|
|
|
|
tradePokemon.trySetShinySeed(shinyChance, false);
|
|
}
|
|
|
|
// Extra HA roll at base 1/64 odds (boosted by events and charms)
|
|
const hiddenIndex = tradePokemon.species.ability2 ? 2 : 1;
|
|
if (tradePokemon.species.abilityHidden) {
|
|
if (tradePokemon.abilityIndex < hiddenIndex) {
|
|
const hiddenAbilityChance = new NumberHolder(64);
|
|
globalScene.applyModifiers(HiddenAbilityRateBoosterModifier, true, hiddenAbilityChance);
|
|
|
|
const hasHiddenAbility = !randSeedInt(hiddenAbilityChance.value);
|
|
|
|
if (hasHiddenAbility) {
|
|
tradePokemon.abilityIndex = hiddenIndex;
|
|
}
|
|
}
|
|
}
|
|
|
|
// If Pokemon is still not shiny or with HA, give the Pokemon a random Common egg move in its moveset
|
|
if (!tradePokemon.shiny && (!tradePokemon.species.abilityHidden || tradePokemon.abilityIndex < hiddenIndex)) {
|
|
const eggMoves = tradePokemon.getEggMoves();
|
|
if (eggMoves) {
|
|
// Cannot gen the rare egg move, only 1 of the first 3 common moves
|
|
const eggMove = eggMoves[randSeedInt(3)];
|
|
if (!tradePokemon.moveset.some(m => m.moveId === eggMove)) {
|
|
if (tradePokemon.moveset.length < 4) {
|
|
tradePokemon.moveset.push(new PokemonMove(eggMove));
|
|
} else {
|
|
const eggMoveIndex = randSeedInt(4);
|
|
tradePokemon.moveset[eggMoveIndex] = new PokemonMove(eggMove);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
encounter.setDialogueToken("tradedPokemon", pokemon.getNameToRender());
|
|
encounter.setDialogueToken("received", tradePokemon.getNameToRender());
|
|
encounter.misc.tradedPokemon = pokemon;
|
|
encounter.misc.receivedPokemon = tradePokemon;
|
|
};
|
|
|
|
return selectPokemonForOption(onPokemonSelected);
|
|
})
|
|
.withOptionPhase(async () => {
|
|
const encounter = globalScene.currentBattle.mysteryEncounter!;
|
|
const tradedPokemon: PlayerPokemon = encounter.misc.tradedPokemon;
|
|
const receivedPokemonData: EnemyPokemon = encounter.misc.receivedPokemon;
|
|
const modifiers = tradedPokemon
|
|
.getHeldItems()
|
|
.filter(m => !(m instanceof PokemonFormChangeItemModifier) && !(m instanceof SpeciesStatBoosterModifier));
|
|
|
|
// Generate a trainer name
|
|
const traderName = generateRandomTraderName();
|
|
encounter.setDialogueToken("tradeTrainerName", traderName.trim());
|
|
|
|
// Remove the original party member from party
|
|
globalScene.removePokemonFromPlayerParty(tradedPokemon, false);
|
|
|
|
// Set data properly, then generate the new Pokemon's assets
|
|
receivedPokemonData.passive = tradedPokemon.passive;
|
|
receivedPokemonData.pokeball = randInt(4) as PokeballType;
|
|
const dataSource = new PokemonData(receivedPokemonData);
|
|
const newPlayerPokemon = globalScene.addPlayerPokemon(
|
|
receivedPokemonData.species,
|
|
receivedPokemonData.level,
|
|
dataSource.abilityIndex,
|
|
dataSource.formIndex,
|
|
dataSource.gender,
|
|
dataSource.shiny,
|
|
dataSource.variant,
|
|
dataSource.ivs,
|
|
dataSource.nature,
|
|
dataSource,
|
|
);
|
|
globalScene.getPlayerParty().push(newPlayerPokemon);
|
|
await newPlayerPokemon.loadAssets();
|
|
|
|
for (const mod of modifiers) {
|
|
mod.pokemonId = newPlayerPokemon.id;
|
|
globalScene.addModifier(mod, true, false, false, true);
|
|
}
|
|
|
|
// Show the trade animation
|
|
await showTradeBackground();
|
|
await doPokemonTradeSequence(tradedPokemon, newPlayerPokemon);
|
|
await showEncounterText(`${namespace}:trade_received`, null, 0, true, 4000);
|
|
globalScene.playBgm(encounter.misc.bgmKey);
|
|
await addPokemonDataToDexAndValidateAchievements(newPlayerPokemon);
|
|
await hideTradeBackground();
|
|
tradedPokemon.destroy();
|
|
|
|
leaveEncounterWithoutBattle(true);
|
|
})
|
|
.build(),
|
|
)
|
|
.withOption(
|
|
MysteryEncounterOptionBuilder.newOptionWithMode(MysteryEncounterOptionMode.DEFAULT)
|
|
.withDialogue({
|
|
buttonLabel: `${namespace}:option.3.label`,
|
|
buttonTooltip: `${namespace}:option.3.tooltip`,
|
|
secondOptionPrompt: `${namespace}:option.3.trade_options_prompt`,
|
|
})
|
|
.withPreOptionPhase(async (): Promise<boolean> => {
|
|
const encounter = globalScene.currentBattle.mysteryEncounter!;
|
|
const onPokemonSelected = (pokemon: PlayerPokemon) => {
|
|
// Get Pokemon held items and filter for valid ones
|
|
const validItems = pokemon.getHeldItems().filter(it => {
|
|
return it.isTransferable;
|
|
});
|
|
|
|
return validItems.map((modifier: PokemonHeldItemModifier) => {
|
|
const option: OptionSelectItem = {
|
|
label: modifier.type.name,
|
|
handler: () => {
|
|
// Pokemon and item selected
|
|
encounter.setDialogueToken("chosenItem", modifier.type.name);
|
|
encounter.misc.chosenModifier = modifier;
|
|
encounter.misc.chosenPokemon = pokemon;
|
|
return true;
|
|
},
|
|
};
|
|
return option;
|
|
});
|
|
};
|
|
|
|
const selectableFilter = (pokemon: Pokemon) => {
|
|
// If pokemon has items to trade
|
|
const meetsReqs =
|
|
pokemon.getHeldItems().filter(it => {
|
|
return it.isTransferable;
|
|
}).length > 0;
|
|
if (!meetsReqs) {
|
|
return getEncounterText(`${namespace}:option.3.invalid_selection`) ?? null;
|
|
}
|
|
|
|
return null;
|
|
};
|
|
|
|
return selectPokemonForOption(onPokemonSelected, undefined, selectableFilter);
|
|
})
|
|
.withOptionPhase(async () => {
|
|
const encounter = globalScene.currentBattle.mysteryEncounter!;
|
|
const modifier = encounter.misc.chosenModifier as PokemonHeldItemModifier;
|
|
const party = globalScene.getPlayerParty();
|
|
const chosenPokemon: PlayerPokemon = encounter.misc.chosenPokemon;
|
|
|
|
// Check tier of the traded item, the received item will be one tier up
|
|
const type = modifier.type.withTierFromPool(ModifierPoolType.PLAYER, party);
|
|
let tier = type.tier ?? ModifierTier.GREAT;
|
|
// Eggs and White Herb are not in the pool
|
|
if (type.id === "WHITE_HERB") {
|
|
tier = ModifierTier.GREAT;
|
|
} else if (type.id === "LUCKY_EGG") {
|
|
tier = ModifierTier.ULTRA;
|
|
} else if (type.id === "GOLDEN_EGG") {
|
|
tier = ModifierTier.ROGUE;
|
|
}
|
|
// Increment tier by 1
|
|
if (tier < ModifierTier.MASTER) {
|
|
tier++;
|
|
}
|
|
|
|
regenerateModifierPoolThresholds(party, ModifierPoolType.PLAYER, 0);
|
|
let item: ModifierTypeOption | null = null;
|
|
// TMs excluded from possible rewards
|
|
while (!item || item.type.id.includes("TM_")) {
|
|
item = getPlayerModifierTypeOptions(1, party, [], {
|
|
guaranteedModifierTiers: [tier],
|
|
allowLuckUpgrades: false,
|
|
})[0];
|
|
}
|
|
|
|
encounter.setDialogueToken("itemName", item.type.name);
|
|
setEncounterRewards({
|
|
guaranteedModifierTypeOptions: [item],
|
|
fillRemaining: false,
|
|
});
|
|
|
|
chosenPokemon.loseHeldItem(modifier, false);
|
|
await globalScene.updateModifiers(true, true);
|
|
|
|
// Generate a trainer name
|
|
const traderName = generateRandomTraderName();
|
|
encounter.setDialogueToken("tradeTrainerName", traderName.trim());
|
|
await showEncounterText(`${namespace}:item_trade_selected`);
|
|
leaveEncounterWithoutBattle();
|
|
})
|
|
.build(),
|
|
)
|
|
.withSimpleOption(
|
|
{
|
|
buttonLabel: `${namespace}:option.4.label`,
|
|
buttonTooltip: `${namespace}:option.4.tooltip`,
|
|
selected: [
|
|
{
|
|
text: `${namespace}:option.4.selected`,
|
|
},
|
|
],
|
|
},
|
|
async () => {
|
|
// Leave encounter with no rewards or exp
|
|
leaveEncounterWithoutBattle(true);
|
|
return true;
|
|
},
|
|
)
|
|
.build();
|
|
|
|
function getPokemonTradeOptions(): Map<number, EnemyPokemon[]> {
|
|
const tradeOptionsMap: Map<number, EnemyPokemon[]> = new Map<number, EnemyPokemon[]>();
|
|
// Starts by filtering out any current party members as valid resulting species
|
|
const alreadyUsedSpecies: PokemonSpecies[] = globalScene.getPlayerParty().map(p => p.species);
|
|
|
|
for (const pokemon of globalScene.getPlayerParty()) {
|
|
// If the party member is legendary/mythical, the only trade options available are always pulled from generation-specific legendary trade pools
|
|
if (pokemon.species.legendary || pokemon.species.subLegendary || pokemon.species.mythical) {
|
|
const generation = pokemon.species.generation;
|
|
const tradeOptions: EnemyPokemon[] = LEGENDARY_TRADE_POOLS[generation].map(s => {
|
|
const pokemonSpecies = getPokemonSpecies(s);
|
|
return new EnemyPokemon(pokemonSpecies, 5, TrainerSlot.NONE, false);
|
|
});
|
|
tradeOptionsMap.set(pokemon.id, tradeOptions);
|
|
} else {
|
|
const originalBst = pokemon.getSpeciesForm().getBaseStatTotal();
|
|
|
|
const tradeOptions: PokemonSpecies[] = [];
|
|
for (let i = 0; i < 3; i++) {
|
|
const speciesTradeOption = generateTradeOption(alreadyUsedSpecies, originalBst);
|
|
alreadyUsedSpecies.push(speciesTradeOption);
|
|
tradeOptions.push(speciesTradeOption);
|
|
}
|
|
|
|
// Add trade options to map
|
|
tradeOptionsMap.set(
|
|
pokemon.id,
|
|
tradeOptions.map(s => {
|
|
return new EnemyPokemon(s, pokemon.level, TrainerSlot.NONE, false);
|
|
}),
|
|
);
|
|
}
|
|
}
|
|
|
|
return tradeOptionsMap;
|
|
}
|
|
|
|
function generateTradeOption(alreadyUsedSpecies: PokemonSpecies[], originalBst?: number): PokemonSpecies {
|
|
let newSpecies: PokemonSpecies | undefined;
|
|
let bstCap = 9999;
|
|
let bstMin = 0;
|
|
if (originalBst) {
|
|
bstCap = originalBst + 100;
|
|
bstMin = originalBst - 100;
|
|
}
|
|
while (isNullOrUndefined(newSpecies)) {
|
|
// Get all non-legendary species that fall within the Bst range requirements
|
|
let validSpecies = allSpecies.filter(s => {
|
|
const isLegendaryOrMythical = s.legendary || s.subLegendary || s.mythical;
|
|
const speciesBst = s.getBaseStatTotal();
|
|
const bstInRange = speciesBst >= bstMin && speciesBst <= bstCap;
|
|
return !isLegendaryOrMythical && bstInRange && !EXCLUDED_TRADE_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 range until at least 20 are in the pool
|
|
bstMin -= 10;
|
|
bstCap += 10;
|
|
}
|
|
}
|
|
|
|
return newSpecies!;
|
|
}
|
|
|
|
function showTradeBackground() {
|
|
return new Promise<void>(resolve => {
|
|
const tradeContainer = globalScene.add.container(0, -globalScene.game.canvas.height / 6);
|
|
tradeContainer.setName("Trade Background");
|
|
|
|
const flyByStaticBg = globalScene.add.rectangle(
|
|
0,
|
|
0,
|
|
globalScene.game.canvas.width / 6,
|
|
globalScene.game.canvas.height / 6,
|
|
0,
|
|
);
|
|
flyByStaticBg.setName("Black Background");
|
|
flyByStaticBg.setOrigin(0, 0);
|
|
flyByStaticBg.setVisible(false);
|
|
tradeContainer.add(flyByStaticBg);
|
|
|
|
const tradeBaseBg = globalScene.add.image(0, 0, "default_bg");
|
|
tradeBaseBg.setName("Trade Background Image");
|
|
tradeBaseBg.setOrigin(0, 0);
|
|
tradeContainer.add(tradeBaseBg);
|
|
|
|
globalScene.fieldUI.add(tradeContainer);
|
|
globalScene.fieldUI.bringToTop(tradeContainer);
|
|
tradeContainer.setVisible(true);
|
|
tradeContainer.alpha = 0;
|
|
|
|
globalScene.tweens.add({
|
|
targets: tradeContainer,
|
|
alpha: 1,
|
|
duration: 500,
|
|
ease: "Sine.easeInOut",
|
|
onComplete: () => {
|
|
resolve();
|
|
},
|
|
});
|
|
});
|
|
}
|
|
|
|
function hideTradeBackground() {
|
|
return new Promise<void>(resolve => {
|
|
const transformationContainer = globalScene.fieldUI.getByName("Trade Background");
|
|
|
|
globalScene.tweens.add({
|
|
targets: transformationContainer,
|
|
alpha: 0,
|
|
duration: 1000,
|
|
ease: "Sine.easeInOut",
|
|
onComplete: () => {
|
|
globalScene.fieldUI.remove(transformationContainer, true);
|
|
resolve();
|
|
},
|
|
});
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Initiates an "evolution-like" animation to transform a previousPokemon (presumably from the player's party) into a new one, not necessarily an evolution species.
|
|
* @param tradedPokemon
|
|
* @param receivedPokemon
|
|
*/
|
|
function doPokemonTradeSequence(tradedPokemon: PlayerPokemon, receivedPokemon: PlayerPokemon) {
|
|
return new Promise<void>(resolve => {
|
|
const tradeContainer = globalScene.fieldUI.getByName("Trade Background") as Phaser.GameObjects.Container;
|
|
const tradeBaseBg = tradeContainer.getByName("Trade Background Image") as Phaser.GameObjects.Image;
|
|
|
|
let tradedPokemonSprite: Phaser.GameObjects.Sprite;
|
|
let tradedPokemonTintSprite: Phaser.GameObjects.Sprite;
|
|
let receivedPokemonSprite: Phaser.GameObjects.Sprite;
|
|
let receivedPokemonTintSprite: Phaser.GameObjects.Sprite;
|
|
|
|
const getPokemonSprite = () => {
|
|
const ret = globalScene.addPokemonSprite(
|
|
tradedPokemon,
|
|
tradeBaseBg.displayWidth / 2,
|
|
tradeBaseBg.displayHeight / 2,
|
|
"pkmn__sub",
|
|
);
|
|
ret.setPipeline(globalScene.spritePipeline, {
|
|
tone: [0.0, 0.0, 0.0, 0.0],
|
|
ignoreTimeTint: true,
|
|
});
|
|
return ret;
|
|
};
|
|
|
|
tradeContainer.add((tradedPokemonSprite = getPokemonSprite()));
|
|
tradeContainer.add((tradedPokemonTintSprite = getPokemonSprite()));
|
|
tradeContainer.add((receivedPokemonSprite = getPokemonSprite()));
|
|
tradeContainer.add((receivedPokemonTintSprite = getPokemonSprite()));
|
|
|
|
tradedPokemonSprite.setAlpha(0);
|
|
tradedPokemonTintSprite.setAlpha(0);
|
|
tradedPokemonTintSprite.setTintFill(getPokeballTintColor(tradedPokemon.pokeball));
|
|
receivedPokemonSprite.setVisible(false);
|
|
receivedPokemonTintSprite.setVisible(false);
|
|
receivedPokemonTintSprite.setTintFill(getPokeballTintColor(receivedPokemon.pokeball));
|
|
|
|
[tradedPokemonSprite, tradedPokemonTintSprite].map(sprite => {
|
|
const spriteKey = tradedPokemon.getSpriteKey(true);
|
|
try {
|
|
sprite.play(spriteKey);
|
|
} catch (err: unknown) {
|
|
console.error(`Failed to play animation for ${spriteKey}`, err);
|
|
}
|
|
|
|
sprite.setPipeline(globalScene.spritePipeline, {
|
|
tone: [0.0, 0.0, 0.0, 0.0],
|
|
hasShadow: false,
|
|
teraColor: getTypeRgb(tradedPokemon.getTeraType()),
|
|
isTerastallized: tradedPokemon.isTerastallized,
|
|
});
|
|
sprite.setPipelineData("ignoreTimeTint", true);
|
|
sprite.setPipelineData("spriteKey", tradedPokemon.getSpriteKey());
|
|
sprite.setPipelineData("shiny", tradedPokemon.shiny);
|
|
sprite.setPipelineData("variant", tradedPokemon.variant);
|
|
["spriteColors", "fusionSpriteColors"].map(k => {
|
|
if (tradedPokemon.summonData.speciesForm) {
|
|
k += "Base";
|
|
}
|
|
sprite.pipelineData[k] = tradedPokemon.getSprite().pipelineData[k];
|
|
});
|
|
});
|
|
|
|
[receivedPokemonSprite, receivedPokemonTintSprite].map(sprite => {
|
|
const spriteKey = receivedPokemon.getSpriteKey(true);
|
|
try {
|
|
sprite.play(spriteKey);
|
|
} catch (err: unknown) {
|
|
console.error(`Failed to play animation for ${spriteKey}`, err);
|
|
}
|
|
|
|
sprite.setPipeline(globalScene.spritePipeline, {
|
|
tone: [0.0, 0.0, 0.0, 0.0],
|
|
hasShadow: false,
|
|
teraColor: getTypeRgb(tradedPokemon.getTeraType()),
|
|
isTerastallized: tradedPokemon.isTerastallized,
|
|
});
|
|
sprite.setPipelineData("ignoreTimeTint", true);
|
|
sprite.setPipelineData("spriteKey", receivedPokemon.getSpriteKey());
|
|
sprite.setPipelineData("shiny", receivedPokemon.shiny);
|
|
sprite.setPipelineData("variant", receivedPokemon.variant);
|
|
["spriteColors", "fusionSpriteColors"].map(k => {
|
|
if (receivedPokemon.summonData.speciesForm) {
|
|
k += "Base";
|
|
}
|
|
sprite.pipelineData[k] = receivedPokemon.getSprite().pipelineData[k];
|
|
});
|
|
});
|
|
|
|
// Traded pokemon pokeball
|
|
const tradedPbAtlasKey = getPokeballAtlasKey(tradedPokemon.pokeball);
|
|
const tradedPokeball: Phaser.GameObjects.Sprite = globalScene.add.sprite(
|
|
tradeBaseBg.displayWidth / 2,
|
|
tradeBaseBg.displayHeight / 2,
|
|
"pb",
|
|
tradedPbAtlasKey,
|
|
);
|
|
tradedPokeball.setVisible(false);
|
|
tradeContainer.add(tradedPokeball);
|
|
|
|
// Received pokemon pokeball
|
|
const receivedPbAtlasKey = getPokeballAtlasKey(receivedPokemon.pokeball);
|
|
const receivedPokeball: Phaser.GameObjects.Sprite = globalScene.add.sprite(
|
|
tradeBaseBg.displayWidth / 2,
|
|
tradeBaseBg.displayHeight / 2,
|
|
"pb",
|
|
receivedPbAtlasKey,
|
|
);
|
|
receivedPokeball.setVisible(false);
|
|
tradeContainer.add(receivedPokeball);
|
|
|
|
globalScene.tweens.add({
|
|
targets: tradedPokemonSprite,
|
|
alpha: 1,
|
|
ease: "Cubic.easeInOut",
|
|
duration: 500,
|
|
onComplete: async () => {
|
|
globalScene.fadeOutBgm(1000, false);
|
|
await showEncounterText(`${namespace}:pokemon_trade_selected`);
|
|
tradedPokemon.cry();
|
|
globalScene.playBgm("evolution");
|
|
await showEncounterText(`${namespace}:pokemon_trade_goodbye`);
|
|
|
|
tradedPokeball.setAlpha(0);
|
|
tradedPokeball.setVisible(true);
|
|
globalScene.tweens.add({
|
|
targets: tradedPokeball,
|
|
alpha: 1,
|
|
ease: "Cubic.easeInOut",
|
|
duration: 250,
|
|
onComplete: () => {
|
|
tradedPokeball.setTexture("pb", `${tradedPbAtlasKey}_opening`);
|
|
globalScene.time.delayedCall(17, () => tradedPokeball.setTexture("pb", `${tradedPbAtlasKey}_open`));
|
|
globalScene.playSound("se/pb_rel");
|
|
tradedPokemonTintSprite.setVisible(true);
|
|
|
|
// TODO: need to add particles to fieldUI instead of field
|
|
// addPokeballOpenParticles(tradedPokemon.x, tradedPokemon.y, tradedPokemon.pokeball);
|
|
|
|
globalScene.tweens.add({
|
|
targets: [tradedPokemonTintSprite, tradedPokemonSprite],
|
|
duration: 500,
|
|
ease: "Sine.easeIn",
|
|
scale: 0.25,
|
|
onComplete: () => {
|
|
tradedPokemonSprite.setVisible(false);
|
|
tradedPokeball.setTexture("pb", `${tradedPbAtlasKey}_opening`);
|
|
tradedPokemonTintSprite.setVisible(false);
|
|
globalScene.playSound("se/pb_catch");
|
|
globalScene.time.delayedCall(17, () => tradedPokeball.setTexture("pb", `${tradedPbAtlasKey}`));
|
|
|
|
globalScene.tweens.add({
|
|
targets: tradedPokeball,
|
|
y: "+=10",
|
|
duration: 200,
|
|
delay: 250,
|
|
ease: "Cubic.easeIn",
|
|
onComplete: () => {
|
|
globalScene.playSound("se/pb_bounce_1");
|
|
|
|
globalScene.tweens.add({
|
|
targets: tradedPokeball,
|
|
y: "-=100",
|
|
duration: 200,
|
|
delay: 1000,
|
|
ease: "Cubic.easeInOut",
|
|
onStart: () => {
|
|
globalScene.playSound("se/pb_throw");
|
|
},
|
|
onComplete: async () => {
|
|
await doPokemonTradeFlyBySequence(tradedPokemonSprite, receivedPokemonSprite);
|
|
await doTradeReceivedSequence(
|
|
receivedPokemon,
|
|
receivedPokemonSprite,
|
|
receivedPokemonTintSprite,
|
|
receivedPokeball,
|
|
receivedPbAtlasKey,
|
|
);
|
|
resolve();
|
|
},
|
|
});
|
|
},
|
|
});
|
|
},
|
|
});
|
|
},
|
|
});
|
|
},
|
|
});
|
|
});
|
|
}
|
|
|
|
function doPokemonTradeFlyBySequence(
|
|
tradedPokemonSprite: Phaser.GameObjects.Sprite,
|
|
receivedPokemonSprite: Phaser.GameObjects.Sprite,
|
|
) {
|
|
return new Promise<void>(resolve => {
|
|
const tradeContainer = globalScene.fieldUI.getByName("Trade Background") as Phaser.GameObjects.Container;
|
|
const tradeBaseBg = tradeContainer.getByName("Trade Background Image") as Phaser.GameObjects.Image;
|
|
const flyByStaticBg = tradeContainer.getByName("Black Background") as Phaser.GameObjects.Rectangle;
|
|
flyByStaticBg.setVisible(true);
|
|
tradeContainer.bringToTop(tradedPokemonSprite);
|
|
tradeContainer.bringToTop(receivedPokemonSprite);
|
|
|
|
tradedPokemonSprite.x = tradeBaseBg.displayWidth / 4;
|
|
tradedPokemonSprite.y = 200;
|
|
tradedPokemonSprite.scale = 1;
|
|
tradedPokemonSprite.setVisible(true);
|
|
receivedPokemonSprite.x = (tradeBaseBg.displayWidth * 3) / 4;
|
|
receivedPokemonSprite.y = -200;
|
|
receivedPokemonSprite.scale = 1;
|
|
receivedPokemonSprite.setVisible(true);
|
|
|
|
const FADE_DELAY = 300;
|
|
const ANIM_DELAY = 750;
|
|
const BASE_ANIM_DURATION = 1000;
|
|
|
|
// Fade out trade background
|
|
globalScene.tweens.add({
|
|
targets: tradeBaseBg,
|
|
alpha: 0,
|
|
ease: "Cubic.easeInOut",
|
|
duration: FADE_DELAY,
|
|
onComplete: () => {
|
|
globalScene.tweens.add({
|
|
targets: [receivedPokemonSprite, tradedPokemonSprite],
|
|
y: tradeBaseBg.displayWidth / 2 - 100,
|
|
ease: "Cubic.easeInOut",
|
|
duration: BASE_ANIM_DURATION * 3,
|
|
onComplete: () => {
|
|
globalScene.tweens.add({
|
|
targets: receivedPokemonSprite,
|
|
x: tradeBaseBg.displayWidth / 4,
|
|
ease: "Cubic.easeInOut",
|
|
duration: BASE_ANIM_DURATION / 2,
|
|
delay: ANIM_DELAY,
|
|
});
|
|
globalScene.tweens.add({
|
|
targets: tradedPokemonSprite,
|
|
x: (tradeBaseBg.displayWidth * 3) / 4,
|
|
ease: "Cubic.easeInOut",
|
|
duration: BASE_ANIM_DURATION / 2,
|
|
delay: ANIM_DELAY,
|
|
onComplete: () => {
|
|
globalScene.tweens.add({
|
|
targets: receivedPokemonSprite,
|
|
y: "+=200",
|
|
ease: "Cubic.easeInOut",
|
|
duration: BASE_ANIM_DURATION * 2,
|
|
delay: ANIM_DELAY,
|
|
});
|
|
globalScene.tweens.add({
|
|
targets: tradedPokemonSprite,
|
|
y: "-=200",
|
|
ease: "Cubic.easeInOut",
|
|
duration: BASE_ANIM_DURATION * 2,
|
|
delay: ANIM_DELAY,
|
|
onComplete: () => {
|
|
globalScene.tweens.add({
|
|
targets: tradeBaseBg,
|
|
alpha: 1,
|
|
ease: "Cubic.easeInOut",
|
|
duration: FADE_DELAY,
|
|
onComplete: () => {
|
|
resolve();
|
|
},
|
|
});
|
|
},
|
|
});
|
|
},
|
|
});
|
|
},
|
|
});
|
|
},
|
|
});
|
|
});
|
|
}
|
|
|
|
function doTradeReceivedSequence(
|
|
receivedPokemon: PlayerPokemon,
|
|
receivedPokemonSprite: Phaser.GameObjects.Sprite,
|
|
receivedPokemonTintSprite: Phaser.GameObjects.Sprite,
|
|
receivedPokeballSprite: Phaser.GameObjects.Sprite,
|
|
receivedPbAtlasKey: string,
|
|
) {
|
|
return new Promise<void>(resolve => {
|
|
const tradeContainer = globalScene.fieldUI.getByName("Trade Background") as Phaser.GameObjects.Container;
|
|
const tradeBaseBg = tradeContainer.getByName("Trade Background Image") as Phaser.GameObjects.Image;
|
|
|
|
receivedPokemonSprite.setVisible(false);
|
|
receivedPokemonSprite.x = tradeBaseBg.displayWidth / 2;
|
|
receivedPokemonSprite.y = tradeBaseBg.displayHeight / 2;
|
|
receivedPokemonTintSprite.setVisible(false);
|
|
receivedPokemonTintSprite.x = tradeBaseBg.displayWidth / 2;
|
|
receivedPokemonTintSprite.y = tradeBaseBg.displayHeight / 2;
|
|
|
|
receivedPokeballSprite.setVisible(true);
|
|
receivedPokeballSprite.x = tradeBaseBg.displayWidth / 2;
|
|
receivedPokeballSprite.y = tradeBaseBg.displayHeight / 2 - 100;
|
|
|
|
// Received pokemon sparkles
|
|
let pokemonShinySparkle: Phaser.GameObjects.Sprite;
|
|
if (receivedPokemon.shiny) {
|
|
pokemonShinySparkle = globalScene.add.sprite(receivedPokemonSprite.x, receivedPokemonSprite.y, "shiny");
|
|
pokemonShinySparkle.setVisible(false);
|
|
tradeContainer.add(pokemonShinySparkle);
|
|
}
|
|
|
|
const BASE_ANIM_DURATION = 1000;
|
|
|
|
// Pokeball falls to the screen
|
|
globalScene.playSound("se/pb_throw");
|
|
globalScene.tweens.add({
|
|
targets: receivedPokeballSprite,
|
|
y: "+=100",
|
|
ease: "Cubic.easeInOut",
|
|
duration: BASE_ANIM_DURATION,
|
|
onComplete: () => {
|
|
globalScene.playSound("se/pb_bounce_1");
|
|
globalScene.time.delayedCall(100, () => globalScene.playSound("se/pb_bounce_1"));
|
|
|
|
globalScene.time.delayedCall(2000, () => {
|
|
globalScene.playSound("se/pb_rel");
|
|
globalScene.fadeOutBgm(500, false);
|
|
receivedPokemon.cry();
|
|
receivedPokemonTintSprite.scale = 0.25;
|
|
receivedPokemonTintSprite.alpha = 1;
|
|
receivedPokemonSprite.setVisible(true);
|
|
receivedPokemonSprite.scale = 0.25;
|
|
receivedPokemonTintSprite.alpha = 1;
|
|
receivedPokemonTintSprite.setVisible(true);
|
|
receivedPokeballSprite.setTexture("pb", `${receivedPbAtlasKey}_opening`);
|
|
globalScene.time.delayedCall(17, () => receivedPokeballSprite.setTexture("pb", `${receivedPbAtlasKey}_open`));
|
|
globalScene.tweens.add({
|
|
targets: receivedPokemonSprite,
|
|
duration: 250,
|
|
ease: "Sine.easeOut",
|
|
scale: 1,
|
|
});
|
|
globalScene.tweens.add({
|
|
targets: receivedPokemonTintSprite,
|
|
duration: 250,
|
|
ease: "Sine.easeOut",
|
|
scale: 1,
|
|
alpha: 0,
|
|
onComplete: () => {
|
|
if (receivedPokemon.shiny) {
|
|
globalScene.time.delayedCall(500, () => {
|
|
doShinySparkleAnim(pokemonShinySparkle, receivedPokemon.variant);
|
|
});
|
|
}
|
|
receivedPokeballSprite.destroy();
|
|
globalScene.time.delayedCall(2000, () => resolve());
|
|
},
|
|
});
|
|
});
|
|
},
|
|
});
|
|
});
|
|
}
|
|
|
|
function generateRandomTraderName() {
|
|
const length = TrainerType.YOUNGSTER - TrainerType.ACE_TRAINER + 1;
|
|
// +1 avoids TrainerType.UNKNOWN
|
|
const classKey = `trainersCommon:${TrainerType[randInt(length) + 1]}`;
|
|
// Some trainers have 2 gendered pools, some do not
|
|
const genderKey = i18next.exists(`${classKey}.MALE`) ? (randInt(2) === 0 ? ".MALE" : ".FEMALE") : "";
|
|
const trainerNameKey = randSeedItem(Object.keys(i18next.t(`${classKey}${genderKey}`, { returnObjects: true })));
|
|
const trainerNameString = i18next.t(`${classKey}${genderKey}.${trainerNameKey}`);
|
|
// Some names have an '&' symbol and need to be trimmed to a single name instead of a double name
|
|
const trainerNames = trainerNameString.split(" & ");
|
|
return trainerNames[randInt(trainerNames.length)];
|
|
}
|