pokerogue/src/data/mystery-encounters/encounters/global-trade-system-encounter.ts
Sirz Benjie 1029afcdbf
[Refactor] Remove circular dependencies (part 4) (#5964)
* 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>
2025-06-11 19:42:04 -07:00

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)];
}