pokerogue/src/data/mystery-encounters/encounters/global-trade-system-encounter.ts
flx-sta 95386861bb
[Qol][Refactor] i18n lazy-loading (#4327)
* move: locales files to `/public` (from `/src`)

* install: i18next-http-backend module

* implement: i18next language lazy-loading

* remove: all `config.ts` files (for locales)

* disable: enConfig import in i18next.d.ts

* remove: console.log from utils.camelCaseToKebabCase()

* remove localization tests

we don't need to test if i18next is working.
This is the job of i18next itself

* mock i18next for tests

* fix: tests that have to use the i18next key now

instead of the english translation

* fix: absolute-avarice-encounter test

* fix: loading mystery-encounter translations

with lazy-load

* fix: 2 mystery encounter translation loading

* replace: i18next mocks any vi.fn() calls

* fix: new namespace usage in ME tests

now using "mysteryEncounters/..."

* fix: delibirdy encounter not being language specific

the encounter was checking if the modifier name includes `Berry` which is only true for english. Instead it has to check if the modifier is an instance of BerryModifier

* fix: the-expert-pokemon-breeder

the new i18n pattern requires a different namespacing which has been adopted

* fix: GTS encounter tests

* add: `MockText.on()`

* fix: berries abound test

* chore: apply review suggestion

from @DayKev

* update i18next.d.ts

* chore: fix i18next.d.ts

* fix: `dialogue-misc` switchup between `en` and `ja`

* move: `SpeciesFormKey` into enum

there was an issue with circular dependencies

* replace: `#app/enums/` with `#enums/` for `SpeciesFormKey` imports

* re-sync locales from `beta`

* rename: `ca_ES` -> `ca-ES`

* rename: `pt_BR` -> `pt-BR`

* rename: `zh_CN` -> `zh-CN`

* rename: `zh_TW` -> `zh-TW`

* fix loading Species-Form-Key in poemon-evo.

* update: i18next `supporterLngs` ...

and remove `nonExplicitSupportedLngs`

* fix: `${namespace}.` -> `${namespace}:`

thanks @MokaStitcher
2024-10-01 21:55:16 +01:00

860 lines
37 KiB
TypeScript

import { leaveEncounterWithoutBattle, selectPokemonForOption, setEncounterRewards } from "#app/data/mystery-encounters/utils/encounter-phase-utils";
import { TrainerSlot, } from "#app/data/trainer-config";
import { ModifierTier } from "#app/modifier/modifier-tier";
import { getPlayerModifierTypeOptions, ModifierPoolType, ModifierTypeOption, regenerateModifierPoolThresholds } from "#app/modifier/modifier-type";
import { MysteryEncounterType } from "#enums/mystery-encounter-type";
import BattleScene from "#app/battle-scene";
import MysteryEncounter, { MysteryEncounterBuilder } from "#app/data/mystery-encounters/mystery-encounter";
import { MysteryEncounterTier } from "#enums/mystery-encounter-tier";
import { Species } from "#enums/species";
import PokemonSpecies, { 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 * as Utils from "#app/utils";
import { IntegerHolder, isNullOrUndefined, randInt, randSeedInt, randSeedShuffle } from "#app/utils";
import Pokemon, { EnemyPokemon, PlayerPokemon, PokemonMove } from "#app/field/pokemon";
import { HiddenAbilityRateBoosterModifier, PokemonFormChangeItemModifier, PokemonHeldItemModifier, ShinyRateBoosterModifier, SpeciesStatBoosterModifier } from "#app/modifier/modifier";
import { 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, PokeballType } from "#app/data/pokeball";
import { getEncounterText, showEncounterText } from "#app/data/mystery-encounters/utils/encounter-dialogue-utils";
import { trainerNamePools } from "#app/data/trainer-names";
import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/game-mode";
import { addPokemonDataToDexAndValidateAchievements } from "#app/data/mystery-encounters/utils/encounter-pokemon-utils";
/** 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: [Species.RATTATA, Species.PIDGEY, Species.WEEDLE],
2: [Species.SENTRET, Species.HOOTHOOT, Species.LEDYBA],
3: [Species.POOCHYENA, Species.ZIGZAGOON, Species.TAILLOW],
4: [Species.BIDOOF, Species.STARLY, Species.KRICKETOT],
5: [Species.PATRAT, Species.PURRLOIN, Species.PIDOVE],
6: [Species.BUNNELBY, Species.LITLEO, Species.SCATTERBUG],
7: [Species.PIKIPEK, Species.YUNGOOS, Species.ROCKRUFF],
8: [Species.SKWOVET, Species.WOOLOO, Species.ROOKIDEE],
9: [Species.LECHONK, Species.FIDOUGH, Species.TAROUNTULA]
};
/** Exclude Paradox mons as they aren't considered legendary/mythical */
const EXCLUDED_TRADE_SPECIES = [
Species.GREAT_TUSK,
Species.SCREAM_TAIL,
Species.BRUTE_BONNET,
Species.FLUTTER_MANE,
Species.SLITHER_WING,
Species.SANDY_SHOCKS,
Species.ROARING_MOON,
Species.WALKING_WAKE,
Species.GOUGING_FIRE,
Species.RAGING_BOLT,
Species.IRON_TREADS,
Species.IRON_BUNDLE,
Species.IRON_HANDS,
Species.IRON_JUGULIS,
Species.IRON_MOTH,
Species.IRON_THORNS,
Species.IRON_VALIANT,
Species.IRON_LEAVES,
Species.IRON_BOULDER,
Species.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`,
}
])
.withTitle(`${namespace}:title`)
.withDescription(`${namespace}:description`)
.withQuery(`${namespace}:query`)
.withOnInit((scene: BattleScene) => {
const encounter = scene.currentBattle.mysteryEncounter!;
// Load bgm
let bgmKey: string;
if (scene.musicPreference === 0) {
bgmKey = "mystery_encounter_gen_5_gts";
scene.loadBgm(bgmKey, `${bgmKey}.mp3`);
} else {
// Mixed option
bgmKey = "mystery_encounter_gen_6_gts";
scene.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(scene);
encounter.misc = {
tradeOptionsMap,
bgmKey
};
return true;
})
.withOnVisualsStart((scene: BattleScene) => {
scene.fadeAndSwitchBgm(scene.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 (scene: BattleScene): Promise<boolean> => {
const encounter = scene.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(scene, `${line1}\n${line2}`, 0, 0, false);
},
};
return option;
});
};
return selectPokemonForOption(scene, onPokemonSelected);
})
.withOptionPhase(async (scene: BattleScene) => {
const encounter = scene.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
scene.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 = scene.addPlayerPokemon(receivedPokemonData.species, receivedPokemonData.level, dataSource.abilityIndex, dataSource.formIndex, dataSource.gender, dataSource.shiny, dataSource.variant, dataSource.ivs, dataSource.nature, dataSource);
scene.getParty().push(newPlayerPokemon);
await newPlayerPokemon.loadAssets();
for (const mod of modifiers) {
mod.pokemonId = newPlayerPokemon.id;
scene.addModifier(mod, true, false, false, true);
}
// Show the trade animation
await showTradeBackground(scene);
await doPokemonTradeSequence(scene, tradedPokemon, newPlayerPokemon);
await showEncounterText(scene, `${namespace}:trade_received`, null, 0, true, 4000);
scene.playBgm(encounter.misc.bgmKey);
await addPokemonDataToDexAndValidateAchievements(scene, newPlayerPokemon);
await hideTradeBackground(scene);
tradedPokemon.destroy();
leaveEncounterWithoutBattle(scene, true);
})
.build()
)
.withOption(
MysteryEncounterOptionBuilder
.newOptionWithMode(MysteryEncounterOptionMode.DEFAULT)
.withHasDexProgress(true)
.withDialogue({
buttonLabel: `${namespace}:option.2.label`,
buttonTooltip: `${namespace}:option.2.tooltip`,
})
.withPreOptionPhase(async (scene: BattleScene): Promise<boolean> => {
const encounter = scene.currentBattle.mysteryEncounter!;
const onPokemonSelected = (pokemon: PlayerPokemon) => {
// Randomly generate a Wonder Trade pokemon
const randomTradeOption = generateTradeOption(scene.getParty().map(p => p.species));
const tradePokemon = new EnemyPokemon(scene, randomTradeOption, pokemon.level, TrainerSlot.NONE, false);
// Extra shiny roll at 1/128 odds (boosted by events and charms)
if (!tradePokemon.shiny) {
const shinyThreshold = new Utils.IntegerHolder(WONDER_TRADE_SHINY_CHANCE);
if (scene.eventManager.isEventActive()) {
shinyThreshold.value *= scene.eventManager.getShinyMultiplier();
}
scene.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 IntegerHolder(64);
scene.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(scene, onPokemonSelected);
})
.withOptionPhase(async (scene: BattleScene) => {
const encounter = scene.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
scene.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 = scene.addPlayerPokemon(receivedPokemonData.species, receivedPokemonData.level, dataSource.abilityIndex, dataSource.formIndex, dataSource.gender, dataSource.shiny, dataSource.variant, dataSource.ivs, dataSource.nature, dataSource);
scene.getParty().push(newPlayerPokemon);
await newPlayerPokemon.loadAssets();
for (const mod of modifiers) {
mod.pokemonId = newPlayerPokemon.id;
scene.addModifier(mod, true, false, false, true);
}
// Show the trade animation
await showTradeBackground(scene);
await doPokemonTradeSequence(scene, tradedPokemon, newPlayerPokemon);
await showEncounterText(scene, `${namespace}:trade_received`, null, 0, true, 4000);
scene.playBgm(encounter.misc.bgmKey);
await addPokemonDataToDexAndValidateAchievements(scene, newPlayerPokemon);
await hideTradeBackground(scene);
tradedPokemon.destroy();
leaveEncounterWithoutBattle(scene, 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 (scene: BattleScene): Promise<boolean> => {
const encounter = scene.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;
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(scene, `${namespace}:option.3.invalid_selection`) ?? null;
}
return null;
};
return selectPokemonForOption(scene, onPokemonSelected, undefined, selectableFilter);
})
.withOptionPhase(async (scene: BattleScene) => {
const encounter = scene.currentBattle.mysteryEncounter!;
const modifier = encounter.misc.chosenModifier;
// Check tier of the traded item, the received item will be one tier up
const type = modifier.type.withTierFromPool();
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(scene.getParty(), ModifierPoolType.PLAYER, 0);
let item: ModifierTypeOption | null = null;
// TMs excluded from possible rewards
while (!item || item.type.id.includes("TM_")) {
item = getPlayerModifierTypeOptions(1, scene.getParty(), [], { guaranteedModifierTiers: [tier], allowLuckUpgrades: false })[0];
}
encounter.setDialogueToken("itemName", item.type.name);
setEncounterRewards(scene, { guaranteedModifierTypeOptions: [item], fillRemaining: false });
// Remove the chosen modifier if its stacks go to 0
modifier.stackCount -= 1;
if (modifier.stackCount === 0) {
scene.removeModifier(modifier);
}
scene.updateModifiers(true, true);
// Generate a trainer name
const traderName = generateRandomTraderName();
encounter.setDialogueToken("tradeTrainerName", traderName.trim());
await showEncounterText(scene, `${namespace}:item_trade_selected`);
leaveEncounterWithoutBattle(scene);
})
.build()
)
.withSimpleOption(
{
buttonLabel: `${namespace}:option.4.label`,
buttonTooltip: `${namespace}:option.4.tooltip`,
selected: [
{
text: `${namespace}:option.4.selected`,
},
],
},
async (scene: BattleScene) => {
// Leave encounter with no rewards or exp
leaveEncounterWithoutBattle(scene, true);
return true;
}
)
.build();
function getPokemonTradeOptions(scene: BattleScene): 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[] = scene.getParty().map(p => p.species);
scene.getParty().forEach(pokemon => {
// 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(scene, pokemonSpecies, 5, TrainerSlot.NONE, false);
});
tradeOptionsMap.set(pokemon.id, tradeOptions);
} else {
const originalBst = pokemon.calculateBaseStats().reduce((a, b) => a + b, 0);
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(scene, 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(scene: BattleScene) {
return new Promise<void>(resolve => {
const tradeContainer = scene.add.container(0, -scene.game.canvas.height / 6);
tradeContainer.setName("Trade Background");
const flyByStaticBg = scene.add.rectangle(0, 0, scene.game.canvas.width / 6, scene.game.canvas.height / 6, 0);
flyByStaticBg.setName("Black Background");
flyByStaticBg.setOrigin(0, 0);
flyByStaticBg.setVisible(false);
tradeContainer.add(flyByStaticBg);
const tradeBaseBg = scene.add.image(0, 0, "default_bg");
tradeBaseBg.setName("Trade Background Image");
tradeBaseBg.setOrigin(0, 0);
tradeContainer.add(tradeBaseBg);
scene.fieldUI.add(tradeContainer);
scene.fieldUI.bringToTop(tradeContainer);
tradeContainer.setVisible(true);
tradeContainer.alpha = 0;
scene.tweens.add({
targets: tradeContainer,
alpha: 1,
duration: 500,
ease: "Sine.easeInOut",
onComplete: () => {
resolve();
}
});
});
}
function hideTradeBackground(scene: BattleScene) {
return new Promise<void>(resolve => {
const transformationContainer = scene.fieldUI.getByName("Trade Background");
scene.tweens.add({
targets: transformationContainer,
alpha: 0,
duration: 1000,
ease: "Sine.easeInOut",
onComplete: () => {
scene.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 scene
* @param tradedPokemon
* @param receivedPokemon
*/
function doPokemonTradeSequence(scene: BattleScene, tradedPokemon: PlayerPokemon, receivedPokemon: PlayerPokemon) {
return new Promise<void>(resolve => {
const tradeContainer = scene.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 = scene.addPokemonSprite(tradedPokemon, tradeBaseBg.displayWidth / 2, tradeBaseBg.displayHeight / 2, "pkmn__sub");
ret.setPipeline(scene.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 => {
sprite.play(tradedPokemon.getSpriteKey(true));
sprite.setPipeline(scene.spritePipeline, { tone: [ 0.0, 0.0, 0.0, 0.0 ], hasShadow: false, teraColor: getTypeRgb(tradedPokemon.getTeraType()) });
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 => {
sprite.play(receivedPokemon.getSpriteKey(true));
sprite.setPipeline(scene.spritePipeline, { tone: [ 0.0, 0.0, 0.0, 0.0 ], hasShadow: false, teraColor: getTypeRgb(tradedPokemon.getTeraType()) });
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 = scene.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 = scene.add.sprite(tradeBaseBg.displayWidth / 2, tradeBaseBg.displayHeight / 2, "pb", receivedPbAtlasKey);
receivedPokeball.setVisible(false);
tradeContainer.add(receivedPokeball);
scene.tweens.add({
targets: tradedPokemonSprite,
alpha: 1,
ease: "Cubic.easeInOut",
duration: 500,
onComplete: async () => {
scene.fadeOutBgm(1000, false);
await showEncounterText(scene, `${namespace}:pokemon_trade_selected`);
tradedPokemon.cry();
scene.playBgm("evolution");
await showEncounterText(scene, `${namespace}:pokemon_trade_goodbye`);
tradedPokeball.setAlpha(0);
tradedPokeball.setVisible(true);
scene.tweens.add({
targets: tradedPokeball,
alpha: 1,
ease: "Cubic.easeInOut",
duration: 250,
onComplete: () => {
tradedPokeball.setTexture("pb", `${tradedPbAtlasKey}_opening`);
scene.time.delayedCall(17, () => tradedPokeball.setTexture("pb", `${tradedPbAtlasKey}_open`));
scene.playSound("se/pb_rel");
tradedPokemonTintSprite.setVisible(true);
// TODO: need to add particles to fieldUI instead of field
// addPokeballOpenParticles(scene, tradedPokemon.x, tradedPokemon.y, tradedPokemon.pokeball);
scene.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);
scene.playSound("se/pb_catch");
scene.time.delayedCall(17, () => tradedPokeball.setTexture("pb", `${tradedPbAtlasKey}`));
scene.tweens.add({
targets: tradedPokeball,
y: "+=10",
duration: 200,
delay: 250,
ease: "Cubic.easeIn",
onComplete: () => {
scene.playSound("se/pb_bounce_1");
scene.tweens.add({
targets: tradedPokeball,
y: "-=100",
duration: 200,
delay: 1000,
ease: "Cubic.easeInOut",
onStart: () => {
scene.playSound("se/pb_throw");
},
onComplete: async () => {
await doPokemonTradeFlyBySequence(scene, tradedPokemonSprite, receivedPokemonSprite);
await doTradeReceivedSequence(scene, receivedPokemon, receivedPokemonSprite, receivedPokemonTintSprite, receivedPokeball, receivedPbAtlasKey);
resolve();
}
});
}
});
}
});
}
});
}
});
});
}
function doPokemonTradeFlyBySequence(scene: BattleScene, tradedPokemonSprite: Phaser.GameObjects.Sprite, receivedPokemonSprite: Phaser.GameObjects.Sprite) {
return new Promise<void>(resolve => {
const tradeContainer = scene.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
scene.tweens.add({
targets: tradeBaseBg,
alpha: 0,
ease: "Cubic.easeInOut",
duration: FADE_DELAY,
onComplete: () => {
scene.tweens.add({
targets: [receivedPokemonSprite, tradedPokemonSprite],
y: tradeBaseBg.displayWidth / 2 - 100,
ease: "Cubic.easeInOut",
duration: BASE_ANIM_DURATION * 3,
onComplete: () => {
scene.tweens.add({
targets: receivedPokemonSprite,
x: tradeBaseBg.displayWidth / 4,
ease: "Cubic.easeInOut",
duration: BASE_ANIM_DURATION / 2,
delay: ANIM_DELAY
});
scene.tweens.add({
targets: tradedPokemonSprite,
x: tradeBaseBg.displayWidth * 3 / 4,
ease: "Cubic.easeInOut",
duration: BASE_ANIM_DURATION / 2,
delay: ANIM_DELAY,
onComplete: () => {
scene.tweens.add({
targets: receivedPokemonSprite,
y: "+=200",
ease: "Cubic.easeInOut",
duration: BASE_ANIM_DURATION * 2,
delay: ANIM_DELAY,
});
scene.tweens.add({
targets: tradedPokemonSprite,
y: "-=200",
ease: "Cubic.easeInOut",
duration: BASE_ANIM_DURATION * 2,
delay: ANIM_DELAY,
onComplete: () => {
scene.tweens.add({
targets: tradeBaseBg,
alpha: 1,
ease: "Cubic.easeInOut",
duration: FADE_DELAY,
onComplete: () => {
resolve();
}
});
}
});
}
});
}
});
}
});
});
}
function doTradeReceivedSequence(scene: BattleScene, receivedPokemon: PlayerPokemon, receivedPokemonSprite: Phaser.GameObjects.Sprite, receivedPokemonTintSprite: Phaser.GameObjects.Sprite, receivedPokeballSprite: Phaser.GameObjects.Sprite, receivedPbAtlasKey: string) {
return new Promise<void>(resolve => {
const tradeContainer = scene.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;
const BASE_ANIM_DURATION = 1000;
// Pokeball falls to the screen
scene.playSound("se/pb_throw");
scene.tweens.add({
targets: receivedPokeballSprite,
y: "+=100",
ease: "Cubic.easeInOut",
duration: BASE_ANIM_DURATION,
onComplete: () => {
scene.playSound("se/pb_bounce_1");
scene.time.delayedCall(100, () => scene.playSound("se/pb_bounce_1"));
scene.time.delayedCall(2000, () => {
scene.playSound("se/pb_rel");
scene.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`);
scene.time.delayedCall(17, () => receivedPokeballSprite.setTexture("pb", `${receivedPbAtlasKey}_open`));
scene.tweens.add({
targets: receivedPokemonSprite,
duration: 250,
ease: "Sine.easeOut",
scale: 1
});
scene.tweens.add({
targets: receivedPokemonTintSprite,
duration: 250,
ease: "Sine.easeOut",
scale: 1,
alpha: 0,
onComplete: () => {
receivedPokeballSprite.destroy();
scene.time.delayedCall(2000, () => resolve());
}
});
});
}
});
});
}
function generateRandomTraderName() {
const length = Object.keys(trainerNamePools).length;
// +1 avoids TrainerType.UNKNOWN
let trainerTypePool = trainerNamePools[randInt(length) + 1];
while (!trainerTypePool) {
trainerTypePool = trainerNamePools[randInt(length) + 1];
}
// Some trainers have 2 gendered pools, some do not
const genderedPool = trainerTypePool[randInt(trainerTypePool.length)];
const trainerNameString = genderedPool instanceof Array ? genderedPool[randInt(genderedPool.length)] : genderedPool;
// 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)];
}