Merge pull request #128 from AsdarDevelops/clowning-around

Clowning around
This commit is contained in:
ImperialSympathizer 2024-08-08 17:53:56 -04:00 committed by GitHub
commit ed12d18205
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
42 changed files with 3021 additions and 153 deletions

File diff suppressed because it is too large Load Diff

View File

@ -1,41 +0,0 @@
{
"textures": [
{
"image": "encounter_radar.png",
"format": "RGBA8888",
"size": {
"w": 17,
"h": 16
},
"scale": 1,
"frames": [
{
"filename": "0001.png",
"rotated": false,
"trimmed": false,
"sourceSize": {
"w": 15,
"h": 14
},
"spriteSourceSize": {
"x": 0,
"y": 0,
"w": 15,
"h": 14
},
"frame": {
"x": 1,
"y": 1,
"w": 15,
"h": 14
}
}
]
}
],
"meta": {
"app": "https://www.codeandweb.com/texturepacker",
"version": "3.0",
"smartupdate": "$TexturePacker:SmartUpdate:eb3445f19546ab36edb2909c89b8aa86:c8de156a28ef70ee4ddf70cffe1ba3ba:e7008b81ccf0cb0325145a809afa6165$"
}
}

View File

@ -1,41 +0,0 @@
{
"textures": [
{
"image": "exclaim.png",
"format": "RGBA8888",
"size": {
"w": 32,
"h": 32
},
"scale": 1,
"frames": [
{
"filename": "0001.png",
"rotated": false,
"trimmed": false,
"sourceSize": {
"w": 32,
"h": 32
},
"spriteSourceSize": {
"x": 0,
"y": 0,
"w": 32,
"h": 32
},
"frame": {
"x": 0,
"y": 0,
"w": 32,
"h": 32
}
}
]
}
],
"meta": {
"app": "https://www.codeandweb.com/texturepacker",
"version": "3.0",
"smartupdate": "$TexturePacker:SmartUpdate:895f0a79b89fa0fb44167f4584fd9a22:357b46953b7e17c6b2f43a62d52855d8:cc1ed0e4f90aaa9dcf1b39a0af1283b0$"
}
}

View File

@ -1430,7 +1430,7 @@ export default class BattleScene extends SceneBase {
const wave = waveIndex || this.currentBattle?.waveIndex || 0; const wave = waveIndex || this.currentBattle?.waveIndex || 0;
this.waveSeed = Utils.shiftCharCodes(this.seed, wave); this.waveSeed = Utils.shiftCharCodes(this.seed, wave);
Phaser.Math.RND.sow([ this.waveSeed ]); Phaser.Math.RND.sow([ this.waveSeed ]);
// console.log("Wave Seed:", this.waveSeed, wave); console.log("Wave Seed:", this.waveSeed, wave);
this.rngCounter = 0; this.rngCounter = 0;
} }
@ -2760,7 +2760,7 @@ export default class BattleScene extends SceneBase {
const previousEncounter = this.mysteryEncounterData.encounteredEvents?.length > 0 ? this.mysteryEncounterData.encounteredEvents[this.mysteryEncounterData.encounteredEvents.length - 1][0] : null; const previousEncounter = this.mysteryEncounterData.encounteredEvents?.length > 0 ? this.mysteryEncounterData.encounteredEvents[this.mysteryEncounterData.encounteredEvents.length - 1][0] : null;
const biomeMysteryEncounters = mysteryEncountersByBiome.get(this.arena.biomeType) ?? []; const biomeMysteryEncounters = mysteryEncountersByBiome.get(this.arena.biomeType) ?? [];
// If no valid encounters exist at tier, checks next tier down, continuing until there are some encounters available // If no valid encounters exist at tier, checks next tier down, continuing until there are some encounters available
while (availableEncounters.length === 0 && tier >= 0) { while (availableEncounters.length === 0 && tier !== null) {
availableEncounters = biomeMysteryEncounters availableEncounters = biomeMysteryEncounters
.filter((encounterType) => { .filter((encounterType) => {
const encounterCandidate = allMysteryEncounters[encounterType]; const encounterCandidate = allMysteryEncounters[encounterType];
@ -2784,7 +2784,16 @@ export default class BattleScene extends SceneBase {
return true; return true;
}) })
.map((m) => (allMysteryEncounters[m])); .map((m) => (allMysteryEncounters[m]));
tier--; // Decrement tier
if (tier === MysteryEncounterTier.ROGUE) {
tier = MysteryEncounterTier.ULTRA;
} else if (tier === MysteryEncounterTier.ULTRA) {
tier = MysteryEncounterTier.GREAT;
} else if (tier === MysteryEncounterTier.GREAT) {
tier = MysteryEncounterTier.COMMON;
} else {
tier = null; // Ends loop
}
} }
// If absolutely no encounters are available, spawn 0th encounter // If absolutely no encounters are available, spawn 0th encounter

View File

@ -110,7 +110,8 @@ export enum CommonAnim {
*/ */
export enum EncounterAnim { export enum EncounterAnim {
MAGMA_BG, MAGMA_BG,
MAGMA_SPOUT MAGMA_SPOUT,
SMOKESCREEN
} }
export class AnimConfig { export class AnimConfig {
@ -533,16 +534,14 @@ export function initMoveAnim(scene: BattleScene, move: Moves): Promise<void> {
export async function initEncounterAnims(scene: BattleScene, encounterAnim: EncounterAnim | EncounterAnim[]): Promise<void> { export async function initEncounterAnims(scene: BattleScene, encounterAnim: EncounterAnim | EncounterAnim[]): Promise<void> {
const anims = Array.isArray(encounterAnim) ? encounterAnim : [encounterAnim]; const anims = Array.isArray(encounterAnim) ? encounterAnim : [encounterAnim];
const encounterAnimNames = Utils.getEnumKeys(EncounterAnim); const encounterAnimNames = Utils.getEnumKeys(EncounterAnim);
const encounterAnimIds = Utils.getEnumValues(EncounterAnim);
const encounterAnimFetches = []; const encounterAnimFetches = [];
for (const anim of anims) { for (const anim of anims) {
if (encounterAnims.has(anim) && !isNullOrUndefined(encounterAnims.get(anim))) { if (encounterAnims.has(anim) && !isNullOrUndefined(encounterAnims.get(anim))) {
continue; continue;
} }
const encounterAnimId = encounterAnimIds[anim];
encounterAnimFetches.push(scene.cachedFetch(`./battle-anims/encounter-${encounterAnimNames[anim].toLowerCase().replace(/\_/g, "-")}.json`) encounterAnimFetches.push(scene.cachedFetch(`./battle-anims/encounter-${encounterAnimNames[anim].toLowerCase().replace(/\_/g, "-")}.json`)
.then(response => response.json()) .then(response => response.json())
.then(cas => encounterAnims.set(encounterAnimId, new AnimConfig(cas)))); .then(cas => encounterAnims.set(anim, new AnimConfig(cas))));
} }
await Promise.allSettled(encounterAnimFetches); await Promise.allSettled(encounterAnimFetches);
} }

View File

@ -62,7 +62,7 @@ export const AnOfferYouCantRefuseEncounter: IMysteryEncounter =
const pokemon = getHighestStatTotalPlayerPokemon(scene, false); const pokemon = getHighestStatTotalPlayerPokemon(scene, false);
const price = scene.getWaveMoneyAmount(10); const price = scene.getWaveMoneyAmount(10);
encounter.setDialogueToken("strongestPokemon", pokemon.name); encounter.setDialogueToken("strongestPokemon", pokemon.getNameToRender());
encounter.setDialogueToken("price", price.toString()); encounter.setDialogueToken("price", price.toString());
// Store pokemon and price // Store pokemon and price

View File

@ -0,0 +1,497 @@
import { EnemyPartyConfig, generateModifierTypeOption, initBattleWithEnemyConfig, leaveEncounterWithoutBattle, loadCustomMovesForEncounter, selectPokemonForOption, setEncounterRewards, transitionMysteryEncounterIntroVisuals, } from "#app/data/mystery-encounters/utils/encounter-phase-utils";
import { trainerConfigs, TrainerPartyCompoundTemplate, TrainerPartyTemplate, } from "#app/data/trainer-config";
import { ModifierTier } from "#app/modifier/modifier-tier";
import { modifierTypes, PokemonHeldItemModifierType } from "#app/modifier/modifier-type";
import { MysteryEncounterType } from "#enums/mystery-encounter-type";
import { PartyMemberStrength } from "#enums/party-member-strength";
import BattleScene from "#app/battle-scene";
import IMysteryEncounter, { MysteryEncounterBuilder } from "../mystery-encounter";
import { MysteryEncounterTier } from "#enums/mystery-encounter-tier";
import { Species } from "#enums/species";
import { TrainerType } from "#enums/trainer-type";
import { getPokemonSpecies } from "#app/data/pokemon-species";
import { Abilities } from "#enums/abilities";
import { applyModifierTypeToPlayerPokemon } from "#app/data/mystery-encounters/utils/encounter-pokemon-utils";
import { Type } from "#app/data/type";
import { MysteryEncounterOptionBuilder } from "#app/data/mystery-encounters/mystery-encounter-option";
import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode";
import { randSeedInt, randSeedShuffle } from "#app/utils";
import { showEncounterDialogue, showEncounterText } from "#app/data/mystery-encounters/utils/encounter-dialogue-utils";
import { Mode } from "#app/ui/ui";
import i18next from "i18next";
import { OptionSelectConfig } from "#app/ui/abstact-option-select-ui-handler";
import { PlayerPokemon, PokemonMove } from "#app/field/pokemon";
import { Ability } from "#app/data/ability";
import { BerryModifier } from "#app/modifier/modifier";
import { BerryType } from "#enums/berry-type";
import { BattlerIndex } from "#app/battle";
import { Moves } from "#enums/moves";
import { EncounterAnim, EncounterBattleAnim } from "#app/data/battle-anims";
import { MoveCategory } from "#app/data/move";
import { MysteryEncounterPokemonData } from "#app/data/mystery-encounters/mystery-encounter-pokemon-data";
/** the i18n namespace for the encounter */
const namespace = "mysteryEncounter:clowningAround";
const RANDOM_ABILITY_POOL = [
Abilities.STURDY,
Abilities.PICKUP,
Abilities.INTIMIDATE,
Abilities.GUTS,
Abilities.DROUGHT,
Abilities.DRIZZLE,
Abilities.SNOW_WARNING,
Abilities.SAND_STREAM,
Abilities.ELECTRIC_SURGE,
Abilities.PSYCHIC_SURGE,
Abilities.GRASSY_SURGE,
Abilities.MISTY_SURGE,
Abilities.MAGICIAN,
Abilities.SHEER_FORCE,
Abilities.PRANKSTER
];
/**
* Clowning Around encounter.
* @see {@link https://github.com/AsdarDevelops/PokeRogue-Events/issues/69 | GitHub Issue #69}
* @see For biome requirements check {@linkcode mysteryEncountersByBiome}
*/
export const ClowningAroundEncounter: IMysteryEncounter =
MysteryEncounterBuilder.withEncounterType(MysteryEncounterType.CLOWNING_AROUND)
.withEncounterTier(MysteryEncounterTier.ULTRA)
.withSceneWaveRangeRequirement(80, 180)
.withAnimations(EncounterAnim.SMOKESCREEN)
.withAutoHideIntroVisuals(false)
.withIntroSpriteConfigs([
{
spriteKey: Species.MR_MIME.toString(),
fileRoot: "pokemon",
hasShadow: true,
repeat: true,
x: -25,
tint: 0.3,
y: -3,
yShadow: -3
},
{
spriteKey: Species.BLACEPHALON.toString(),
fileRoot: "pokemon/exp",
hasShadow: true,
repeat: true,
x: 25,
tint: 0.3,
y: -3,
yShadow: -3
},
{
spriteKey: "harlequin",
fileRoot: "trainer",
hasShadow: true,
x: 0,
y: 2,
yShadow: 2
},
])
.withIntroDialogue([
{
text: `${namespace}.intro`,
},
{
text: `${namespace}.intro_dialogue`,
speaker: `${namespace}.speaker`
},
])
.withOnInit((scene: BattleScene) => {
const encounter = scene.currentBattle.mysteryEncounter;
const clownTrainerType = TrainerType.HARLEQUIN;
const clownConfig = trainerConfigs[clownTrainerType].copy();
const clownPartyTemplate = new TrainerPartyCompoundTemplate(
new TrainerPartyTemplate(1, PartyMemberStrength.STRONG),
new TrainerPartyTemplate(1, PartyMemberStrength.STRONGER));
clownConfig.setPartyTemplates(clownPartyTemplate);
clownConfig.setDoubleOnly();
clownConfig.partyTemplateFunc = null; // Overrides party template func if it exists
// Generate random ability for Blacephalon from pool
const ability = RANDOM_ABILITY_POOL[randSeedInt(RANDOM_ABILITY_POOL.length)];
encounter.setDialogueToken("ability", new Ability(ability, 3).name);
encounter.misc = { ability };
encounter.enemyPartyConfigs.push({
trainerConfig: clownConfig,
pokemonConfigs: [ // Overrides first 2 pokemon to be Mr. Mime and Blacephalon
{
species: getPokemonSpecies(Species.MR_MIME),
isBoss: true,
moveSet: [Moves.TEETER_DANCE, Moves.ALLY_SWITCH, Moves.DAZZLING_GLEAM, Moves.PSYCHIC]
},
{ // Blacephalon has the random ability from pool, and 2 entirely random types to fit with the theme of the encounter
species: getPokemonSpecies(Species.BLACEPHALON),
ability: ability,
mysteryEncounterData: new MysteryEncounterPokemonData(null, null, null, [randSeedInt(18), randSeedInt(18)]),
isBoss: true,
moveSet: [Moves.TRICK, Moves.HYPNOSIS, Moves.SHADOW_BALL, Moves.MIND_BLOWN]
},
],
doubleBattle: true
});
// Load animations/sfx for start of fight moves
loadCustomMovesForEncounter(scene, [Moves.ROLE_PLAY, Moves.TAUNT]);
return true;
})
.withTitle(`${namespace}.title`)
.withDescription(`${namespace}.description`)
.withQuery(`${namespace}.query`)
.withOption(
new MysteryEncounterOptionBuilder()
.withOptionMode(MysteryEncounterOptionMode.DEFAULT)
.withDialogue({
buttonLabel: `${namespace}.option.1.label`,
buttonTooltip: `${namespace}.option.1.tooltip`,
selected: [
{
text: `${namespace}.option.1.selected`,
speaker: `${namespace}.speaker`
},
],
})
.withOptionPhase(async (scene: BattleScene) => {
const encounter = scene.currentBattle.mysteryEncounter;
// Spawn battle
const config: EnemyPartyConfig = encounter.enemyPartyConfigs[0];
setEncounterRewards(scene, { fillRemaining: true });
// TODO: when Magic Room and Wonder Room are implemented, add those to start of battle
encounter.startOfBattleEffects.push(
{ // Mr. Mime copies the Blacephalon's random ability
sourceBattlerIndex: BattlerIndex.ENEMY,
targets: [BattlerIndex.ENEMY_2],
move: new PokemonMove(Moves.ROLE_PLAY),
ignorePp: true
},
{
sourceBattlerIndex: BattlerIndex.ENEMY_2,
targets: [BattlerIndex.PLAYER],
move: new PokemonMove(Moves.TAUNT),
ignorePp: true
},
{
sourceBattlerIndex: BattlerIndex.ENEMY_2,
targets: [BattlerIndex.PLAYER_2],
move: new PokemonMove(Moves.TAUNT),
ignorePp: true
});
await transitionMysteryEncounterIntroVisuals(scene);
await initBattleWithEnemyConfig(scene, config);
})
.withPostOptionPhase(async (scene: BattleScene): Promise<boolean> => {
// After the battle, offer the player the opportunity to permanently swap ability
const abilityWasSwapped = await handleSwapAbility(scene);
if (abilityWasSwapped) {
await showEncounterText(scene, `${namespace}.option.1.ability_gained`);
}
// Play animations once ability swap is complete
// Trainer sprite that is shown at end of battle is not the same as mystery encounter intro visuals
scene.tweens.add({
targets: scene.currentBattle.trainer,
x: "+=16",
y: "-=16",
alpha: 0,
ease: "Sine.easeInOut",
duration: 250
});
const background = new EncounterBattleAnim(EncounterAnim.SMOKESCREEN, scene.getPlayerPokemon(), scene.getPlayerPokemon());
background.playWithoutTargets(scene, 230, 40, 2);
return true;
})
.build()
)
.withOption(
new MysteryEncounterOptionBuilder()
.withOptionMode(MysteryEncounterOptionMode.DEFAULT)
.withDialogue({
buttonLabel: `${namespace}.option.2.label`,
buttonTooltip: `${namespace}.option.2.tooltip`,
selected: [
{
text: `${namespace}.option.2.selected`,
speaker: `${namespace}.speaker`
},
{
text: `${namespace}.option.2.selected_2`,
},
{
text: `${namespace}.option.2.selected_3`,
speaker: `${namespace}.speaker`
},
],
})
.withPreOptionPhase(async (scene: BattleScene) => {
// Swap player's items on pokemon with the most items
// Item comparisons look at whichever Pokemon has the greatest number of TRANSFERABLE, non-berry items
// So Vitamins, form change items, etc. are not included
const encounter = scene.currentBattle.mysteryEncounter;
const party = scene.getParty();
let mostHeldItemsPokemon = party[0];
let count = mostHeldItemsPokemon.getHeldItems()
.filter(m => m.isTransferrable && !(m instanceof BerryModifier))
.reduce((v, m) => v + m.stackCount, 0);
party.forEach(pokemon => {
const nextCount = pokemon.getHeldItems()
.filter(m => m.isTransferrable && !(m instanceof BerryModifier))
.reduce((v, m) => v + m.stackCount, 0);
if (nextCount > count) {
mostHeldItemsPokemon = pokemon;
count = nextCount;
}
});
encounter.setDialogueToken("switchPokemon", mostHeldItemsPokemon.getNameToRender());
const items = mostHeldItemsPokemon.getHeldItems();
// Shuffles Berries (if they have any)
let numBerries = 0;
items.filter(m => m instanceof BerryModifier)
.forEach(m => {
numBerries += m.stackCount;
scene.removeModifier(m);
});
generateItemsOfTier(scene, mostHeldItemsPokemon, numBerries, "Berries");
// Shuffle Transferable held items in the same tier (only shuffles Ultra and Rogue atm)
let numUltra = 0;
let numRogue = 0;
items.filter(m => m.isTransferrable && !(m instanceof BerryModifier))
.forEach(m => {
const type = m.type.withTierFromPool();
const tier = type.tier ?? ModifierTier.ULTRA;
if (type.id === "LUCKY_EGG" || tier === ModifierTier.ULTRA) {
numUltra += m.stackCount;
scene.removeModifier(m);
} else if (type.id === "GOLDEN_EGG" || tier === ModifierTier.ROGUE) {
numRogue += m.stackCount;
scene.removeModifier(m);
}
});
generateItemsOfTier(scene, mostHeldItemsPokemon, numUltra, ModifierTier.ULTRA);
generateItemsOfTier(scene, mostHeldItemsPokemon, numRogue, ModifierTier.ROGUE);
})
.withOptionPhase(async (scene: BattleScene) => {
leaveEncounterWithoutBattle(scene, true);
})
.withPostOptionPhase(async (scene: BattleScene) => {
// Play animations
const background = new EncounterBattleAnim(EncounterAnim.SMOKESCREEN, scene.getPlayerPokemon(), scene.getPlayerPokemon());
background.playWithoutTargets(scene, 230, 40, 2);
await transitionMysteryEncounterIntroVisuals(scene);
})
.build()
)
.withOption(
new MysteryEncounterOptionBuilder()
.withOptionMode(MysteryEncounterOptionMode.DEFAULT)
.withDialogue({
buttonLabel: `${namespace}.option.3.label`,
buttonTooltip: `${namespace}.option.3.tooltip`,
selected: [
{
text: `${namespace}.option.3.selected`,
speaker: `${namespace}.speaker`
},
{
text: `${namespace}.option.3.selected_2`,
},
{
text: `${namespace}.option.3.selected_3`,
speaker: `${namespace}.speaker`
},
],
})
.withPreOptionPhase(async (scene: BattleScene) => {
// Swap player's types on all party pokemon
// If a Pokemon had a single type prior, they will still have a single type after
for (const pokemon of scene.getParty()) {
const originalTypes = pokemon.getTypes(false, false, true);
// If the Pokemon has non-status moves that don't match the Pokemon's type, prioritizes those as the new type
// Makes the "randomness" of the shuffle slightly less punishing
let priorityTypes = pokemon.moveset
.filter(move => !originalTypes.includes(move.getMove().type) && move.getMove().category !== MoveCategory.STATUS)
.map(move => move.getMove().type);
if (priorityTypes?.length > 0) {
priorityTypes = [...new Set(priorityTypes)];
randSeedShuffle(priorityTypes);
}
let newTypes;
if (!originalTypes || originalTypes.length < 1) {
newTypes = priorityTypes?.length > 0 ? [priorityTypes.pop()] : [(randSeedInt(18) as Type)];
} else {
newTypes = originalTypes.map(m => {
if (priorityTypes?.length > 0) {
const ret = priorityTypes.pop();
randSeedShuffle(priorityTypes);
return ret;
}
return randSeedInt(18) as Type;
});
}
if (!pokemon.mysteryEncounterData) {
pokemon.mysteryEncounterData = new MysteryEncounterPokemonData(null, null, null, newTypes);
} else {
pokemon.mysteryEncounterData.types = newTypes;
}
}
})
.withOptionPhase(async (scene: BattleScene) => {
leaveEncounterWithoutBattle(scene, true);
})
.withPostOptionPhase(async (scene: BattleScene) => {
// Play animations
const background = new EncounterBattleAnim(EncounterAnim.SMOKESCREEN, scene.getPlayerPokemon(), scene.getPlayerPokemon());
background.playWithoutTargets(scene, 230, 40, 2);
await transitionMysteryEncounterIntroVisuals(scene);
})
.build()
)
.withOutroDialogue([
{
text: `${namespace}.outro`,
},
])
.build();
async function handleSwapAbility(scene: BattleScene) {
return new Promise<boolean>(async resolve => {
await showEncounterDialogue(scene, `${namespace}.option.1.apply_ability_dialogue`, `${namespace}.speaker`);
await showEncounterText(scene, `${namespace}.option.1.apply_ability_message`);
scene.ui.setMode(Mode.MESSAGE).then(() => {
displayYesNoOptions(scene, resolve);
});
});
}
function displayYesNoOptions(scene: BattleScene, resolve) {
showEncounterText(scene, `${namespace}.option.1.ability_prompt`, 500, false);
const fullOptions = [
{
label: i18next.t("menu:yes"),
handler: () => {
onYesAbilitySwap(scene, resolve);
return true;
}
},
{
label: i18next.t("menu:no"),
handler: () => {
resolve(false);
return true;
}
}
];
const config: OptionSelectConfig = {
options: fullOptions,
maxOptions: 7,
yOffset: 0
};
scene.ui.setModeWithoutClear(Mode.OPTION_SELECT, config, null, true);
}
function onYesAbilitySwap(scene: BattleScene, resolve) {
const onPokemonSelected = (pokemon: PlayerPokemon) => {
// Do ability swap
if (!pokemon.mysteryEncounterData) {
pokemon.mysteryEncounterData = new MysteryEncounterPokemonData(null, Abilities.AERILATE);
}
pokemon.mysteryEncounterData.ability = scene.currentBattle.mysteryEncounter.misc.ability;
scene.currentBattle.mysteryEncounter.setDialogueToken("chosenPokemon", pokemon.getNameToRender());
scene.ui.setMode(Mode.MESSAGE).then(() => resolve(true));
};
const onPokemonNotSelected = () => {
scene.ui.setMode(Mode.MESSAGE).then(() => {
displayYesNoOptions(scene, resolve);
});
};
selectPokemonForOption(scene, onPokemonSelected, onPokemonNotSelected);
}
function generateItemsOfTier(scene: BattleScene, pokemon: PlayerPokemon, numItems: integer, tier: ModifierTier | "Berries") {
// These pools have to be defined at runtime so that modifierTypes exist
// Pools have instances of the modifier type equal to the max stacks that modifier can be applied to any one pokemon
// This is to prevent "over-generating" a random item of a certain type during item swaps
const ultraPool = [
[modifierTypes.REVIVER_SEED, 1],
[modifierTypes.GOLDEN_PUNCH, 5],
[modifierTypes.ATTACK_TYPE_BOOSTER, 99],
[modifierTypes.QUICK_CLAW, 3],
[modifierTypes.WIDE_LENS, 3],
[modifierTypes.WHITE_HERB, 2]
];
const roguePool = [
[modifierTypes.LEFTOVERS, 4],
[modifierTypes.SHELL_BELL, 4],
[modifierTypes.SOUL_DEW, 10],
[modifierTypes.SOOTHE_BELL, 3],
[modifierTypes.SCOPE_LENS, 5],
[modifierTypes.BATON, 1],
[modifierTypes.FOCUS_BAND, 5],
[modifierTypes.KINGS_ROCK, 3],
[modifierTypes.GRIP_CLAW, 5]
];
const berryPool = [
[BerryType.APICOT, 3],
[BerryType.ENIGMA, 2],
[BerryType.GANLON, 3],
[BerryType.LANSAT, 3],
[BerryType.LEPPA, 2],
[BerryType.LIECHI, 3],
[BerryType.LUM, 2],
[BerryType.PETAYA, 3],
[BerryType.SALAC, 2],
[BerryType.SITRUS, 2],
[BerryType.STARF, 3]
];
let pool: any[];
if (tier === "Berries") {
pool = berryPool;
} else {
pool = tier === ModifierTier.ULTRA ? ultraPool : roguePool;
}
for (let i = 0; i < numItems; i++) {
const randIndex = randSeedInt(pool.length);
const newItemType = pool[randIndex];
let newMod;
if (tier === "Berries") {
newMod = generateModifierTypeOption(scene, modifierTypes.BERRY, [newItemType[0]]).type as PokemonHeldItemModifierType;
} else {
newMod = generateModifierTypeOption(scene, newItemType[0]).type as PokemonHeldItemModifierType;
}
applyModifierTypeToPlayerPokemon(scene, pokemon, newMod);
// Decrement max stacks and remove from pool if at max
newItemType[1]--;
if (newItemType[1] <= 0) {
pool.splice(randIndex, 1);
}
}
}

View File

@ -127,7 +127,7 @@ export const DarkDealEncounter: IMysteryEncounter =
const removedPokemon = getRandomPlayerPokemon(scene, false, true); const removedPokemon = getRandomPlayerPokemon(scene, false, true);
scene.removePokemonFromPlayerParty(removedPokemon); scene.removePokemonFromPlayerParty(removedPokemon);
scene.currentBattle.mysteryEncounter.setDialogueToken("pokeName", removedPokemon.name); scene.currentBattle.mysteryEncounter.setDialogueToken("pokeName", removedPokemon.getNameToRender());
// Store removed pokemon types // Store removed pokemon types
scene.currentBattle.mysteryEncounter.misc = [ scene.currentBattle.mysteryEncounter.misc = [

View File

@ -95,7 +95,7 @@ export const FieldTripEncounter: IMysteryEncounter =
]; ];
setEncounterExp(scene, scene.getParty().map((p) => p.id), 50); setEncounterExp(scene, scene.getParty().map((p) => p.id), 50);
} else { } else {
encounter.setDialogueToken("pokeName", pokemon.name); encounter.setDialogueToken("pokeName", pokemon.getNameToRender());
encounter.setDialogueToken("move", move.getName()); encounter.setDialogueToken("move", move.getName());
encounter.options[0].dialogue.selected = [ encounter.options[0].dialogue.selected = [
{ {
@ -187,7 +187,7 @@ export const FieldTripEncounter: IMysteryEncounter =
]; ];
setEncounterExp(scene, scene.getParty().map((p) => p.id), 50); setEncounterExp(scene, scene.getParty().map((p) => p.id), 50);
} else { } else {
encounter.setDialogueToken("pokeName", pokemon.name); encounter.setDialogueToken("pokeName", pokemon.getNameToRender());
encounter.setDialogueToken("move", move.getName()); encounter.setDialogueToken("move", move.getName());
encounter.options[1].dialogue.selected = [ encounter.options[1].dialogue.selected = [
{ {
@ -273,7 +273,7 @@ export const FieldTripEncounter: IMysteryEncounter =
]; ];
setEncounterExp(scene, scene.getParty().map((p) => p.id), 50); setEncounterExp(scene, scene.getParty().map((p) => p.id), 50);
} else { } else {
encounter.setDialogueToken("pokeName", pokemon.name); encounter.setDialogueToken("pokeName", pokemon.getNameToRender());
encounter.setDialogueToken("move", move.getName()); encounter.setDialogueToken("move", move.getName());
encounter.options[2].dialogue.selected = [ encounter.options[2].dialogue.selected = [
{ {

View File

@ -191,7 +191,7 @@ export const FieryFalloutEncounter: IMysteryEncounter =
const chosenPokemon = burnable[roll]; const chosenPokemon = burnable[roll];
if (chosenPokemon.trySetStatus(StatusEffect.BURN)) { if (chosenPokemon.trySetStatus(StatusEffect.BURN)) {
// Burn applied // Burn applied
encounter.setDialogueToken("burnedPokemon", chosenPokemon.name); encounter.setDialogueToken("burnedPokemon", chosenPokemon.getNameToRender());
queueEncounterMessage(scene, `${namespace}.option.2.target_burned`); queueEncounterMessage(scene, `${namespace}.option.2.target_burned`);
} }
} }
@ -245,7 +245,7 @@ function giveLeadPokemonCharcoal(scene: BattleScene) {
if (leadPokemon) { if (leadPokemon) {
const charcoal = generateModifierTypeOption(scene, modifierTypes.ATTACK_TYPE_BOOSTER, [Type.FIRE]).type as AttackTypeBoosterModifierType; const charcoal = generateModifierTypeOption(scene, modifierTypes.ATTACK_TYPE_BOOSTER, [Type.FIRE]).type as AttackTypeBoosterModifierType;
applyModifierTypeToPlayerPokemon(scene, leadPokemon, charcoal); applyModifierTypeToPlayerPokemon(scene, leadPokemon, charcoal);
scene.currentBattle.mysteryEncounter.setDialogueToken("leadPokemon", leadPokemon.name); scene.currentBattle.mysteryEncounter.setDialogueToken("leadPokemon", leadPokemon.getNameToRender());
queueEncounterMessage(scene, `${namespace}.found_charcoal`); queueEncounterMessage(scene, `${namespace}.found_charcoal`);
} }
} }

View File

@ -113,7 +113,7 @@ export const MysteriousChestEncounter: IMysteryEncounter =
); );
koPlayerPokemon(scene, highestLevelPokemon); koPlayerPokemon(scene, highestLevelPokemon);
scene.currentBattle.mysteryEncounter.setDialogueToken("pokeName", highestLevelPokemon.name); scene.currentBattle.mysteryEncounter.setDialogueToken("pokeName", highestLevelPokemon.getNameToRender());
// Show which Pokemon was KOed, then leave encounter with no rewards // Show which Pokemon was KOed, then leave encounter with no rewards
// Does this synchronously so that game over doesn't happen over result message // Does this synchronously so that game over doesn't happen over result message
await showEncounterText(scene, `${namespace}.option.1.bad`).then(() => { await showEncounterText(scene, `${namespace}.option.1.bad`).then(() => {

View File

@ -92,7 +92,7 @@ export const ThePokemonSalesmanEncounter: IMysteryEncounter =
encounter.options[0].dialogue.buttonTooltip = `${namespace}.option.1.tooltip_shiny`; encounter.options[0].dialogue.buttonTooltip = `${namespace}.option.1.tooltip_shiny`;
} }
const price = scene.getWaveMoneyAmount(priceMultiplier); const price = scene.getWaveMoneyAmount(priceMultiplier);
encounter.setDialogueToken("purchasePokemon", pokemon.name); encounter.setDialogueToken("purchasePokemon", pokemon.getNameToRender());
encounter.setDialogueToken("price", price.toString()); encounter.setDialogueToken("price", price.toString());
encounter.misc = { encounter.misc = {
price: price, price: price,

View File

@ -16,6 +16,7 @@ import { BattleStat } from "#app/data/battle-stat";
import { BattlerTagType } from "#enums/battler-tag-type"; import { BattlerTagType } from "#enums/battler-tag-type";
import { BerryType } from "#enums/berry-type"; import { BerryType } from "#enums/berry-type";
import { MysteryEncounterTier } from "#enums/mystery-encounter-tier"; import { MysteryEncounterTier } from "#enums/mystery-encounter-tier";
import { MysteryEncounterPokemonData } from "#app/data/mystery-encounters/mystery-encounter-pokemon-data";
/** the i18n namespace for the encounter */ /** the i18n namespace for the encounter */
const namespace = "mysteryEncounter:theStrongStuff"; const namespace = "mysteryEncounter:theStrongStuff";
@ -70,7 +71,7 @@ export const TheStrongStuffEncounter: IMysteryEncounter =
species: getPokemonSpecies(Species.SHUCKLE), species: getPokemonSpecies(Species.SHUCKLE),
isBoss: true, isBoss: true,
bossSegments: 5, bossSegments: 5,
spriteScale: 1.5, mysteryEncounterData: new MysteryEncounterPokemonData(1.5),
nature: Nature.BOLD, nature: Nature.BOLD,
moveSet: [Moves.INFESTATION, Moves.SALT_CURE, Moves.GASTRO_ACID, Moves.HEAL_ORDER], moveSet: [Moves.INFESTATION, Moves.SALT_CURE, Moves.GASTRO_ACID, Moves.HEAL_ORDER],
modifierTypes: [ modifierTypes: [
@ -147,7 +148,7 @@ export const TheStrongStuffEncounter: IMysteryEncounter =
modifyPlayerPokemonBST(pokemon, 10); modifyPlayerPokemonBST(pokemon, 10);
} }
encounter.setDialogueToken("highBstPokemon", highestBst.name); encounter.setDialogueToken("highBstPokemon", highestBst.getNameToRender());
await showEncounterText(scene, `${namespace}.option.1.selected_2`, null, true); await showEncounterText(scene, `${namespace}.option.1.selected_2`, null, true);
setEncounterRewards(scene, { fillRemaining: true }); setEncounterRewards(scene, { fillRemaining: true });

View File

@ -0,0 +1,16 @@
import { Abilities } from "#enums/abilities";
import { Type } from "#app/data/type";
export class MysteryEncounterPokemonData {
public spriteScale: number;
public ability: Abilities;
public passive: Abilities;
public types: Type[] = [];
constructor(spriteScale?: number, ability?: Abilities, passive?: Abilities, types?: Type[]) {
this.spriteScale = spriteScale;
this.ability = ability;
this.passive = passive;
this.types = types;
}
}

View File

@ -305,7 +305,7 @@ export default class IMysteryEncounter implements IMysteryEncounter {
} }
} }
if (this.primaryPokemon?.length > 0) { if (this.primaryPokemon?.length > 0) {
this.setDialogueToken("primaryName", this.primaryPokemon.name); this.setDialogueToken("primaryName", this.primaryPokemon.getNameToRender());
for (const req of this.primaryPokemonRequirements) { for (const req of this.primaryPokemonRequirements) {
if (!req.invertQuery) { if (!req.invertQuery) {
const value = req.getDialogueToken(scene, this.primaryPokemon); const value = req.getDialogueToken(scene, this.primaryPokemon);
@ -316,7 +316,7 @@ export default class IMysteryEncounter implements IMysteryEncounter {
} }
} }
if (this.secondaryPokemonRequirements?.length > 0 && this.secondaryPokemon?.length > 0) { if (this.secondaryPokemonRequirements?.length > 0 && this.secondaryPokemon?.length > 0) {
this.setDialogueToken("secondaryName", this.secondaryPokemon[0].name); this.setDialogueToken("secondaryName", this.secondaryPokemon[0].getNameToRender());
for (const req of this.secondaryPokemonRequirements) { for (const req of this.secondaryPokemonRequirements) {
if (!req.invertQuery) { if (!req.invertQuery) {
const value = req.getDialogueToken(scene, this.secondaryPokemon[0]); const value = req.getDialogueToken(scene, this.secondaryPokemon[0]);
@ -342,7 +342,7 @@ export default class IMysteryEncounter implements IMysteryEncounter {
} }
} }
if (opt.primaryPokemonRequirements?.length > 0 && opt.primaryPokemon?.length > 0) { if (opt.primaryPokemonRequirements?.length > 0 && opt.primaryPokemon?.length > 0) {
this.setDialogueToken("option" + j + "PrimaryName", opt.primaryPokemon.name); this.setDialogueToken("option" + j + "PrimaryName", opt.primaryPokemon.getNameToRender());
for (const req of opt.primaryPokemonRequirements) { for (const req of opt.primaryPokemonRequirements) {
if (!req.invertQuery) { if (!req.invertQuery) {
const value = req.getDialogueToken(scene, opt.primaryPokemon); const value = req.getDialogueToken(scene, opt.primaryPokemon);
@ -353,7 +353,7 @@ export default class IMysteryEncounter implements IMysteryEncounter {
} }
} }
if (opt.secondaryPokemonRequirements?.length > 0 && opt.secondaryPokemon?.length > 0) { if (opt.secondaryPokemonRequirements?.length > 0 && opt.secondaryPokemon?.length > 0) {
this.setDialogueToken("option" + j + "SecondaryName", opt.secondaryPokemon[0].name); this.setDialogueToken("option" + j + "SecondaryName", opt.secondaryPokemon[0].getNameToRender());
for (const req of opt.secondaryPokemonRequirements) { for (const req of opt.secondaryPokemonRequirements) {
if (!req.invertQuery) { if (!req.invertQuery) {
const value = req.getDialogueToken(scene, opt.secondaryPokemon[0]); const value = req.getDialogueToken(scene, opt.secondaryPokemon[0]);

View File

@ -21,6 +21,7 @@ import { AbsoluteAvariceEncounter } from "#app/data/mystery-encounters/encounter
import { ATrainersTestEncounter } from "#app/data/mystery-encounters/encounters/a-trainers-test-encounter"; import { ATrainersTestEncounter } from "#app/data/mystery-encounters/encounters/a-trainers-test-encounter";
import { TrashToTreasureEncounter } from "#app/data/mystery-encounters/encounters/trash-to-treasure-encounter"; import { TrashToTreasureEncounter } from "#app/data/mystery-encounters/encounters/trash-to-treasure-encounter";
import { BerriesAboundEncounter } from "#app/data/mystery-encounters/encounters/berries-abound-encounter"; import { BerriesAboundEncounter } from "#app/data/mystery-encounters/encounters/berries-abound-encounter";
import { ClowningAroundEncounter } from "#app/data/mystery-encounters/encounters/clowning-around-encounter";
// Spawn chance: (BASE_MYSTERY_ENCOUNTER_SPAWN_WEIGHT + WIGHT_INCREMENT_ON_SPAWN_MISS * <number of missed spawns>) / 256 // Spawn chance: (BASE_MYSTERY_ENCOUNTER_SPAWN_WEIGHT + WIGHT_INCREMENT_ON_SPAWN_MISS * <number of missed spawns>) / 256
export const BASE_MYSTERY_ENCOUNTER_SPAWN_WEIGHT = 1; export const BASE_MYSTERY_ENCOUNTER_SPAWN_WEIGHT = 1;
@ -141,7 +142,7 @@ const humanTransitableBiomeEncounters: MysteryEncounterType[] = [
MysteryEncounterType.MYSTERIOUS_CHALLENGERS, MysteryEncounterType.MYSTERIOUS_CHALLENGERS,
MysteryEncounterType.SHADY_VITAMIN_DEALER, MysteryEncounterType.SHADY_VITAMIN_DEALER,
MysteryEncounterType.THE_POKEMON_SALESMAN, MysteryEncounterType.THE_POKEMON_SALESMAN,
MysteryEncounterType.AN_OFFER_YOU_CANT_REFUSE MysteryEncounterType.AN_OFFER_YOU_CANT_REFUSE,
]; ];
const civilizationBiomeEncounters: MysteryEncounterType[] = [ const civilizationBiomeEncounters: MysteryEncounterType[] = [
@ -159,7 +160,8 @@ const anyBiomeEncounters: MysteryEncounterType[] = [
MysteryEncounterType.DELIBIRDY, MysteryEncounterType.DELIBIRDY,
MysteryEncounterType.A_TRAINERS_TEST, MysteryEncounterType.A_TRAINERS_TEST,
MysteryEncounterType.TRASH_TO_TREASURE, MysteryEncounterType.TRASH_TO_TREASURE,
MysteryEncounterType.BERRIES_ABOUND MysteryEncounterType.BERRIES_ABOUND,
MysteryEncounterType.CLOWNING_AROUND
]; ];
/** /**
@ -249,6 +251,7 @@ export function initMysteryEncounters() {
allMysteryEncounters[MysteryEncounterType.A_TRAINERS_TEST] = ATrainersTestEncounter; allMysteryEncounters[MysteryEncounterType.A_TRAINERS_TEST] = ATrainersTestEncounter;
allMysteryEncounters[MysteryEncounterType.TRASH_TO_TREASURE] = TrashToTreasureEncounter; allMysteryEncounters[MysteryEncounterType.TRASH_TO_TREASURE] = TrashToTreasureEncounter;
allMysteryEncounters[MysteryEncounterType.BERRIES_ABOUND] = BerriesAboundEncounter; allMysteryEncounters[MysteryEncounterType.BERRIES_ABOUND] = BerriesAboundEncounter;
allMysteryEncounters[MysteryEncounterType.CLOWNING_AROUND] = ClowningAroundEncounter;
// Add extreme encounters to biome map // Add extreme encounters to biome map
extremeBiomeEncounters.forEach(encounter => { extremeBiomeEncounters.forEach(encounter => {

View File

@ -12,6 +12,7 @@ import PokemonData from "#app/system/pokemon-data";
import { OptionSelectConfig, OptionSelectItem } from "#app/ui/abstact-option-select-ui-handler"; import { OptionSelectConfig, OptionSelectItem } from "#app/ui/abstact-option-select-ui-handler";
import { PartyOption, PartyUiMode } from "#app/ui/party-ui-handler"; import { PartyOption, PartyUiMode } from "#app/ui/party-ui-handler";
import { Mode } from "#app/ui/ui"; import { Mode } from "#app/ui/ui";
import * as Utils from "#app/utils";
import { isNullOrUndefined } from "#app/utils"; import { isNullOrUndefined } from "#app/utils";
import { BattlerTagType } from "#enums/battler-tag-type"; import { BattlerTagType } from "#enums/battler-tag-type";
import { Biome } from "#enums/biome"; import { Biome } from "#enums/biome";
@ -19,7 +20,6 @@ import { TrainerType } from "#enums/trainer-type";
import i18next from "i18next"; import i18next from "i18next";
import BattleScene from "#app/battle-scene"; import BattleScene from "#app/battle-scene";
import Trainer, { TrainerVariant } from "#app/field/trainer"; import Trainer, { TrainerVariant } from "#app/field/trainer";
import * as Utils from "#app/utils";
import { Gender } from "#app/data/gender"; import { Gender } from "#app/data/gender";
import { Nature } from "#app/data/nature"; import { Nature } from "#app/data/nature";
import { Moves } from "#enums/moves"; import { Moves } from "#enums/moves";
@ -30,13 +30,15 @@ import { TrainerConfig, trainerConfigs, TrainerSlot } from "#app/data/trainer-co
import PokemonSpecies from "#app/data/pokemon-species"; import PokemonSpecies from "#app/data/pokemon-species";
import Overrides from "#app/overrides"; import Overrides from "#app/overrides";
import { Egg, IEggOptions } from "#app/data/egg"; import { Egg, IEggOptions } from "#app/data/egg";
import { Abilities } from "#enums/abilities";
import { MysteryEncounterPokemonData } from "#app/data/mystery-encounters/mystery-encounter-pokemon-data";
/** /**
* Animates exclamation sprite over trainer's head at start of encounter * Animates exclamation sprite over trainer's head at start of encounter
* @param scene * @param scene
*/ */
export function doTrainerExclamation(scene: BattleScene) { export function doTrainerExclamation(scene: BattleScene) {
const exclamationSprite = scene.addFieldSprite(0, 0, "exclaim"); const exclamationSprite = scene.add.sprite(0, 0, "exclaim");
exclamationSprite.setName("exclamation"); exclamationSprite.setName("exclamation");
scene.field.add(exclamationSprite); scene.field.add(exclamationSprite);
scene.field.moveTo(exclamationSprite, scene.field.getAll().length - 1); scene.field.moveTo(exclamationSprite, scene.field.getAll().length - 1);
@ -63,7 +65,7 @@ export interface EnemyPokemonConfig {
isBoss: boolean; isBoss: boolean;
bossSegments?: number; bossSegments?: number;
bossSegmentModifier?: number; // Additive to the determined segment number bossSegmentModifier?: number; // Additive to the determined segment number
spriteScale?: number; mysteryEncounterData?: MysteryEncounterPokemonData;
formIndex?: number; formIndex?: number;
level?: number; level?: number;
gender?: Gender; gender?: Gender;
@ -71,6 +73,8 @@ export interface EnemyPokemonConfig {
moveSet?: Moves[]; moveSet?: Moves[];
nature?: Nature; nature?: Nature;
ivs?: [integer, integer, integer, integer, integer, integer]; ivs?: [integer, integer, integer, integer, integer, integer];
ability?: Abilities;
shiny?: boolean;
/** Can set just the status, or pass a timer on the status turns */ /** Can set just the status, or pass a timer on the status turns */
status?: StatusEffect | [StatusEffect, number]; status?: StatusEffect | [StatusEffect, number];
mysteryEncounterBattleEffects?: (pokemon: Pokemon) => void; mysteryEncounterBattleEffects?: (pokemon: Pokemon) => void;
@ -152,7 +156,17 @@ export async function initBattleWithEnemyConfig(scene: BattleScene, partyConfig:
let isBoss = false; let isBoss = false;
if (!loaded) { if (!loaded) {
if (trainerType || trainerConfig) { if (trainerType || trainerConfig) {
// Allows overriding a trainer's pokemon to use specific species/data
if (e < partyConfig?.pokemonConfigs?.length) {
const config = partyConfig?.pokemonConfigs?.[e];
level = config.level ? config.level : level;
dataSource = config.dataSource;
enemySpecies = config.species;
isBoss = config.isBoss;
battle.enemyParty[e] = scene.addEnemyPokemon(enemySpecies, level, TrainerSlot.TRAINER, isBoss, dataSource);
} else {
battle.enemyParty[e] = battle.trainer.genPartyMember(e); battle.enemyParty[e] = battle.trainer.genPartyMember(e);
}
} else { } else {
if (e < partyConfig?.pokemonConfigs?.length) { if (e < partyConfig?.pokemonConfigs?.length) {
const config = partyConfig?.pokemonConfigs?.[e]; const config = partyConfig?.pokemonConfigs?.[e];
@ -200,11 +214,14 @@ export async function initBattleWithEnemyConfig(scene: BattleScene, partyConfig:
enemyPokemon.formIndex = config.formIndex; enemyPokemon.formIndex = config.formIndex;
} }
// Set scale // Set shiny
if (!isNullOrUndefined(config.spriteScale)) { if (!isNullOrUndefined(config.shiny)) {
enemyPokemon.mysteryEncounterData = { enemyPokemon.shiny = config.shiny;
spriteScale: config.spriteScale }
};
// Set custom mystery encounter data fields (such as sprite scale, custom abilities, types, etc.)
if (!isNullOrUndefined(config.mysteryEncounterData)) {
enemyPokemon.mysteryEncounterData = config.mysteryEncounterData;
} }
// Set Boss // Set Boss
@ -242,6 +259,11 @@ export async function initBattleWithEnemyConfig(scene: BattleScene, partyConfig:
// Set summon data fields // Set summon data fields
// Set ability
if (!isNullOrUndefined(config.ability)) {
enemyPokemon.summonData.ability = config.ability;
}
// Set gender // Set gender
if (!isNullOrUndefined(config.gender)) { if (!isNullOrUndefined(config.gender)) {
enemyPokemon.gender = config.gender; enemyPokemon.gender = config.gender;
@ -364,14 +386,16 @@ export function generateModifierTypeOption(scene: BattleScene, modifier: () => M
*/ */
export function selectPokemonForOption(scene: BattleScene, onPokemonSelected: (pokemon: PlayerPokemon) => void | OptionSelectItem[], onPokemonNotSelected?: () => void, selectablePokemonFilter?: (pokemon: PlayerPokemon) => string): Promise<boolean> { export function selectPokemonForOption(scene: BattleScene, onPokemonSelected: (pokemon: PlayerPokemon) => void | OptionSelectItem[], onPokemonNotSelected?: () => void, selectablePokemonFilter?: (pokemon: PlayerPokemon) => string): Promise<boolean> {
return new Promise(resolve => { return new Promise(resolve => {
const modeToSetOnExit = scene.ui.getMode();
// Open party screen to choose pokemon to train // Open party screen to choose pokemon to train
scene.ui.setMode(Mode.PARTY, PartyUiMode.SELECT, -1, (slotIndex: integer, option: PartyOption) => { scene.ui.setMode(Mode.PARTY, PartyUiMode.SELECT, -1, (slotIndex: integer, option: PartyOption) => {
if (slotIndex < scene.getParty().length) { if (slotIndex < scene.getParty().length) {
scene.ui.setMode(Mode.MYSTERY_ENCOUNTER).then(() => { scene.ui.setMode(modeToSetOnExit).then(() => {
const pokemon = scene.getParty()[slotIndex]; const pokemon = scene.getParty()[slotIndex];
const secondaryOptions = onPokemonSelected(pokemon); const secondaryOptions = onPokemonSelected(pokemon);
if (!secondaryOptions) { if (!secondaryOptions) {
scene.currentBattle.mysteryEncounter.setDialogueToken("selectedPokemon", pokemon.name); scene.currentBattle.mysteryEncounter.setDialogueToken("selectedPokemon", pokemon.getNameToRender());
resolve(true); resolve(true);
return; return;
} }
@ -385,7 +409,7 @@ export function selectPokemonForOption(scene: BattleScene, onPokemonSelected: (p
const onSelect = option.handler; const onSelect = option.handler;
option.handler = () => { option.handler = () => {
onSelect(); onSelect();
scene.currentBattle.mysteryEncounter.setDialogueToken("selectedPokemon", pokemon.name); scene.currentBattle.mysteryEncounter.setDialogueToken("selectedPokemon", pokemon.getNameToRender());
resolve(true); resolve(true);
return true; return true;
}; };
@ -421,7 +445,7 @@ export function selectPokemonForOption(scene: BattleScene, onPokemonSelected: (p
}); });
}); });
} else { } else {
scene.ui.setMode(Mode.MYSTERY_ENCOUNTER).then(() => { scene.ui.setMode(modeToSetOnExit).then(() => {
if (onPokemonNotSelected) { if (onPokemonNotSelected) {
onPokemonNotSelected(); onPokemonNotSelected();
} }

View File

@ -20,10 +20,6 @@ import { getPokemonNameWithAffix } from "#app/messages";
import { modifierTypes, PokemonHeldItemModifierType } from "#app/modifier/modifier-type"; import { modifierTypes, PokemonHeldItemModifierType } from "#app/modifier/modifier-type";
import { Gender } from "#app/data/gender"; import { Gender } from "#app/data/gender";
export interface MysteryEncounterPokemonData {
spriteScale?: number
}
export function getSpriteKeysFromSpecies(species: Species, female?: boolean, formIndex?: integer, shiny?: boolean, variant?: integer): { spriteKey: string, fileRoot: string } { export function getSpriteKeysFromSpecies(species: Species, female?: boolean, formIndex?: integer, shiny?: boolean, variant?: integer): { spriteKey: string, fileRoot: string } {
const spriteKey = getPokemonSpecies(species).getSpriteKey(female ?? false, formIndex ?? 0, shiny ?? false, variant ?? 0); const spriteKey = getPokemonSpecies(species).getSpriteKey(female ?? false, formIndex ?? 0, shiny ?? false, variant ?? 0);
const fileRoot = getPokemonSpecies(species).getSpriteAtlasPath(female ?? false, formIndex ?? 0, shiny ?? false, variant ?? 0); const fileRoot = getPokemonSpecies(species).getSpriteAtlasPath(female ?? false, formIndex ?? 0, shiny ?? false, variant ?? 0);
@ -447,7 +443,7 @@ function failCatch(scene: BattleScene, pokemon: EnemyPokemon, originalY: number,
scene.currentBattle.lastUsedPokeball = pokeballType; scene.currentBattle.lastUsedPokeball = pokeballType;
removePb(scene, pokeball); removePb(scene, pokeball);
scene.ui.showText(i18next.t("battle:pokemonBrokeFree", { pokemonName: pokemon.name }), null, () => resolve(), null, true); scene.ui.showText(i18next.t("battle:pokemonBrokeFree", { pokemonName: pokemon.getNameToRender() }), null, () => resolve(), null, true);
}); });
} }
@ -516,7 +512,7 @@ export async function catchPokemon(scene: BattleScene, pokemon: EnemyPokemon, po
Promise.all([pokemon.hideInfo(), scene.gameData.setPokemonCaught(pokemon)]).then(() => { Promise.all([pokemon.hideInfo(), scene.gameData.setPokemonCaught(pokemon)]).then(() => {
if (scene.getParty().length === 6) { if (scene.getParty().length === 6) {
const promptRelease = () => { const promptRelease = () => {
scene.ui.showText(i18next.t("battle:partyFull", { pokemonName: pokemon.name }), null, () => { scene.ui.showText(i18next.t("battle:partyFull", { pokemonName: pokemon.getNameToRender() }), null, () => {
scene.pokemonInfoContainer.makeRoomForConfirmUi(); scene.pokemonInfoContainer.makeRoomForConfirmUi();
scene.ui.setMode(Mode.CONFIRM, () => { scene.ui.setMode(Mode.CONFIRM, () => {
scene.ui.setMode(Mode.PARTY, PartyUiMode.RELEASE, 0, (slotIndex: integer, _option: PartyOption) => { scene.ui.setMode(Mode.PARTY, PartyUiMode.RELEASE, 0, (slotIndex: integer, _option: PartyOption) => {
@ -544,7 +540,7 @@ export async function catchPokemon(scene: BattleScene, pokemon: EnemyPokemon, po
}; };
if (showCatchObtainMessage) { if (showCatchObtainMessage) {
scene.ui.showText(i18next.t(isObtain ? "battle:pokemonObtained" : "battle:pokemonCaught", { pokemonName: pokemon.name }), null, doPokemonCatchMenu, 0, true); scene.ui.showText(i18next.t(isObtain ? "battle:pokemonObtained" : "battle:pokemonCaught", { pokemonName: pokemon.getNameToRender() }), null, doPokemonCatchMenu, 0, true);
} else { } else {
doPokemonCatchMenu(); doPokemonCatchMenu();
} }
@ -581,7 +577,7 @@ export async function doPokemonFlee(scene: BattleScene, pokemon: EnemyPokemon):
onComplete: () => { onComplete: () => {
pokemon.setVisible(false); pokemon.setVisible(false);
scene.field.remove(pokemon, true); scene.field.remove(pokemon, true);
showEncounterText(scene, i18next.t("battle:pokemonFled", { pokemonName: pokemon.name }), 600, false) showEncounterText(scene, i18next.t("battle:pokemonFled", { pokemonName: pokemon.getNameToRender() }), 600, false)
.then(() => { .then(() => {
resolve(); resolve();
}); });
@ -604,7 +600,7 @@ export function doPlayerFlee(scene: BattleScene, pokemon: EnemyPokemon): Promise
onComplete: () => { onComplete: () => {
pokemon.setVisible(false); pokemon.setVisible(false);
scene.field.remove(pokemon, true); scene.field.remove(pokemon, true);
showEncounterText(scene, i18next.t("battle:playerFled", { pokemonName: pokemon.name }), 600, false) showEncounterText(scene, i18next.t("battle:playerFled", { pokemonName: pokemon.getNameToRender() }), 600, false)
.then(() => { .then(() => {
resolve(); resolve();
}); });

View File

@ -64,5 +64,5 @@ export enum BattlerTagType {
STOCKPILING = "STOCKPILING", STOCKPILING = "STOCKPILING",
RECEIVE_DOUBLE_DAMAGE = "RECEIVE_DOUBLE_DAMAGE", RECEIVE_DOUBLE_DAMAGE = "RECEIVE_DOUBLE_DAMAGE",
ALWAYS_GET_HIT = "ALWAYS_GET_HIT", ALWAYS_GET_HIT = "ALWAYS_GET_HIT",
MYSTERY_ENCOUNTER_POST_SUMMON = "MYSTERY_ENCOUNTER_POST_SUMMON" // Provides effects on post-summon for MEs MYSTERY_ENCOUNTER_POST_SUMMON = "MYSTERY_ENCOUNTER_POST_SUMMON", // Provides effects on post-summon for MEs
} }

View File

@ -18,5 +18,6 @@ export enum MysteryEncounterType {
ABSOLUTE_AVARICE, ABSOLUTE_AVARICE,
A_TRAINERS_TEST, A_TRAINERS_TEST,
TRASH_TO_TREASURE, TRASH_TO_TREASURE,
BERRIES_ABOUND BERRIES_ABOUND,
CLOWNING_AROUND
} }

View File

@ -190,9 +190,6 @@ export default class MysteryEncounterIntroVisuals extends Phaser.GameObjects.Con
} }
}); });
// Load dex progress icon
this.scene.loadAtlas("encounter_radar", "mystery-encounters");
this.scene.load.once(Phaser.Loader.Events.COMPLETE, () => { this.scene.load.once(Phaser.Loader.Events.COMPLETE, () => {
this.spriteConfigs.every((config) => { this.spriteConfigs.every((config) => {
if (config.isItem) { if (config.isItem) {

View File

@ -51,7 +51,7 @@ import { Biome } from "#enums/biome";
import { Moves } from "#enums/moves"; import { Moves } from "#enums/moves";
import { Species } from "#enums/species"; import { Species } from "#enums/species";
import { getPokemonNameWithAffix } from "#app/messages.js"; import { getPokemonNameWithAffix } from "#app/messages.js";
import { MysteryEncounterPokemonData } from "#app/data/mystery-encounters/utils/encounter-pokemon-utils"; import { MysteryEncounterPokemonData } from "#app/data/mystery-encounters/mystery-encounter-pokemon-data";
export enum FieldPosition { export enum FieldPosition {
CENTER, CENTER,
@ -187,6 +187,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
this.fusionVariant = dataSource.fusionVariant || 0; this.fusionVariant = dataSource.fusionVariant || 0;
this.fusionGender = dataSource.fusionGender; this.fusionGender = dataSource.fusionGender;
this.fusionLuck = dataSource.fusionLuck; this.fusionLuck = dataSource.fusionLuck;
this.mysteryEncounterData = dataSource.mysteryEncounterData;
} else { } else {
this.id = Utils.randSeedInt(4294967296); this.id = Utils.randSeedInt(4294967296);
this.ivs = ivs || Utils.getIvsFromId(this.id); this.ivs = ivs || Utils.getIvsFromId(this.id);
@ -233,6 +234,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
} }
this.luck = (this.shiny ? this.variant + 1 : 0) + (this.fusionShiny ? this.fusionVariant + 1 : 0); this.luck = (this.shiny ? this.variant + 1 : 0) + (this.fusionShiny ? this.fusionVariant + 1 : 0);
this.fusionLuck = this.luck; this.fusionLuck = this.luck;
this.mysteryEncounterData = new MysteryEncounterPokemonData();
} }
this.generateName(); this.generateName();
@ -927,7 +929,10 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
} }
if (!types.length || !includeTeraType) { if (!types.length || !includeTeraType) {
if (!ignoreOverride && this.summonData?.types) { if (this.mysteryEncounterData?.types?.length > 0) {
// "Permanent" override for a Pokemon's normal types, currently only used by Mystery Encounters
this.mysteryEncounterData.types.forEach(t => types.push(t));
} else if (!ignoreOverride && this.summonData?.types) {
this.summonData.types.forEach(t => types.push(t)); this.summonData.types.forEach(t => types.push(t));
} else { } else {
const speciesForm = this.getSpeciesForm(ignoreOverride); const speciesForm = this.getSpeciesForm(ignoreOverride);
@ -994,6 +999,9 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
if (Overrides.OPP_ABILITY_OVERRIDE && !this.isPlayer()) { if (Overrides.OPP_ABILITY_OVERRIDE && !this.isPlayer()) {
return allAbilities[Overrides.OPP_ABILITY_OVERRIDE]; return allAbilities[Overrides.OPP_ABILITY_OVERRIDE];
} }
if (this.mysteryEncounterData?.ability) {
return allAbilities[this.mysteryEncounterData.ability];
}
if (this.isFusion()) { if (this.isFusion()) {
return allAbilities[this.getFusionSpeciesForm(ignoreOverride).getAbility(this.fusionAbilityIndex)]; return allAbilities[this.getFusionSpeciesForm(ignoreOverride).getAbility(this.fusionAbilityIndex)];
} }
@ -1018,6 +1026,9 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
if (Overrides.OPP_PASSIVE_ABILITY_OVERRIDE && !this.isPlayer()) { if (Overrides.OPP_PASSIVE_ABILITY_OVERRIDE && !this.isPlayer()) {
return allAbilities[Overrides.OPP_PASSIVE_ABILITY_OVERRIDE]; return allAbilities[Overrides.OPP_PASSIVE_ABILITY_OVERRIDE];
} }
if (this.mysteryEncounterData?.passive) {
return allAbilities[this.mysteryEncounterData.passive];
}
let starterSpeciesId = this.species.speciesId; let starterSpeciesId = this.species.speciesId;
while (pokemonPrevolutions.hasOwnProperty(starterSpeciesId)) { while (pokemonPrevolutions.hasOwnProperty(starterSpeciesId)) {
@ -4059,6 +4070,7 @@ export class PokemonSummonData {
public speciesForm: PokemonSpeciesForm; public speciesForm: PokemonSpeciesForm;
public fusionSpeciesForm: PokemonSpeciesForm; public fusionSpeciesForm: PokemonSpeciesForm;
public ability: Abilities = Abilities.NONE; public ability: Abilities = Abilities.NONE;
public passiveAbility: Abilities = Abilities.NONE;
public gender: Gender; public gender: Gender;
public fusionGender: Gender; public fusionGender: Gender;
public stats: integer[]; public stats: integer[];

View File

@ -275,6 +275,9 @@ export class LoadingScene extends SceneBase {
} }
} }
// Load Mystery Encounter dex progress icon
this.loadImage("encounter_radar", "mystery-encounters");
this.loadAtlas("dualshock", "inputs"); this.loadAtlas("dualshock", "inputs");
this.loadAtlas("xbox", "inputs"); this.loadAtlas("xbox", "inputs");
this.loadAtlas("keyboard", "inputs"); this.loadAtlas("keyboard", "inputs");

View File

@ -18,6 +18,7 @@ import { absoluteAvariceDialogue } from "#app/locales/en/mystery-encounters/abso
import { aTrainersTestDialogue } from "#app/locales/en/mystery-encounters/a-trainers-test-dialogue"; import { aTrainersTestDialogue } from "#app/locales/en/mystery-encounters/a-trainers-test-dialogue";
import { trashToTreasureDialogue } from "#app/locales/en/mystery-encounters/trash-to-treasure-dialogue"; import { trashToTreasureDialogue } from "#app/locales/en/mystery-encounters/trash-to-treasure-dialogue";
import { berriesAboundDialogue } from "#app/locales/en/mystery-encounters/berries-abound-dialogue"; import { berriesAboundDialogue } from "#app/locales/en/mystery-encounters/berries-abound-dialogue";
import { clowningAroundDialogue } from "#app/locales/en/mystery-encounters/clowning-around-dialogue";
/** /**
* Patterns that can be used: * Patterns that can be used:
@ -60,5 +61,6 @@ export const mysteryEncounter = {
absoluteAvarice: absoluteAvariceDialogue, absoluteAvarice: absoluteAvariceDialogue,
aTrainersTest: aTrainersTestDialogue, aTrainersTest: aTrainersTestDialogue,
trashToTreasure: trashToTreasureDialogue, trashToTreasure: trashToTreasureDialogue,
berriesAbound: berriesAboundDialogue berriesAbound: berriesAboundDialogue,
clowningAround: clowningAroundDialogue
} as const; } as const;

View File

@ -0,0 +1,34 @@
export const clowningAroundDialogue = {
intro: "It's...@d{64} a clown?",
speaker: "Clown",
intro_dialogue: "Bumbling buffoon, brace for a brilliant battle!\nYoull be beaten by this brawling busker!",
title: "Clowning Around",
description: "Something is off about this encounter. The clown seems eager to goad you into a battle, but to what end?\n\nThe Blacephalon is especially strange, like it has @[TOOLTIP_TITLE]{weird types and ability.}",
query: "What will you do?",
option: {
1: {
label: "Battle the Clown",
tooltip: "(-) Strange Battle\n(?) Affects Pokémon Abilities",
selected: "Your pitiful Pokémon are poised for a pathetic performance!",
apply_ability_dialogue: "A sensational showcase!\nYour savvy suits a sensational skill as spoils!",
apply_ability_message: "The clown is offering to permanently Skill Swap one of your Pokémon's ability to {{ability}}!",
ability_prompt: "Would you like to permanently teach a Pokémon the {{ability}} ability?",
ability_gained: "@s{level_up_fanfare}{{chosenPokemon}} gained the {{ability}} ability!"
},
2: {
label: "Remain Unprovoked",
tooltip: "(-) Upsets the Clown\n(?) Affects Pokémon Items",
selected: "Dismal dodger, you deny a delightful duel?\nFeel my fury!",
selected_2: "The clown's Blacephalon uses Trick!\nAll of your {{switchPokemon}}'s items were randomly swapped!",
selected_3: "Flustered fool, fall for my flawless deception!",
},
3: {
label: "Return the Insults",
tooltip: "(-) Upsets the Clown\n(?) Affects Pokémon Types",
selected: "Dismal dodger, you deny a delightful duel?\nFeel my fury!",
selected_2: "The clown's Blacephalon uses a strange move!\nAll of your team's types were randomly swapped!",
selected_3: "Flustered fool, fall for my flawless deception!",
},
},
outro: "The clown and his cohorts\ndisappear in a puff of smoke."
};

View File

@ -915,7 +915,7 @@ export class EncounterPhase extends BattlePhase {
// Load Mystery Encounter Exclamation bubble and sfx // Load Mystery Encounter Exclamation bubble and sfx
loadEnemyAssets.push(new Promise<void>(resolve => { loadEnemyAssets.push(new Promise<void>(resolve => {
this.scene.loadSe("GEN8- Exclaim", "battle_anims", "GEN8- Exclaim.wav"); this.scene.loadSe("GEN8- Exclaim", "battle_anims", "GEN8- Exclaim.wav");
this.scene.loadAtlas("exclaim", "mystery-encounters"); this.scene.loadImage("exclaim", "mystery-encounters");
this.scene.load.once(Phaser.Loader.Events.COMPLETE, () => resolve()); this.scene.load.once(Phaser.Loader.Events.COMPLETE, () => resolve());
if (!this.scene.load.isLoading()) { if (!this.scene.load.isLoading()) {
this.scene.load.start(); this.scene.load.start();
@ -1369,7 +1369,7 @@ export class PostSummonPhase extends PokemonPhase {
} }
this.scene.arena.applyTags(ArenaTrapTag, pokemon); this.scene.arena.applyTags(ArenaTrapTag, pokemon);
// If this is fight or flight mystery encounter and is enemy pokemon summon phase, add enraged tag // If this is mystery encounter and has post summon phase tag, apply post summon effects
if (pokemon.findTags(t => t instanceof MysteryEncounterPostSummonTag)) { if (pokemon.findTags(t => t instanceof MysteryEncounterPostSummonTag)) {
pokemon.lapseTag(BattlerTagType.MYSTERY_ENCOUNTER_POST_SUMMON); pokemon.lapseTag(BattlerTagType.MYSTERY_ENCOUNTER_POST_SUMMON);
} }

View File

@ -12,6 +12,7 @@ import { loadBattlerTag } from "../data/battler-tags";
import { Biome } from "#enums/biome"; import { Biome } from "#enums/biome";
import { Moves } from "#enums/moves"; import { Moves } from "#enums/moves";
import { Species } from "#enums/species"; import { Species } from "#enums/species";
import { MysteryEncounterPokemonData } from "#app/data/mystery-encounters/mystery-encounter-pokemon-data";
export default class PokemonData { export default class PokemonData {
public id: integer; public id: integer;
@ -54,6 +55,7 @@ export default class PokemonData {
public bossSegments?: integer; public bossSegments?: integer;
public summonData: PokemonSummonData; public summonData: PokemonSummonData;
public mysteryEncounterData: MysteryEncounterPokemonData;
constructor(source: Pokemon | any, forHistory: boolean = false) { constructor(source: Pokemon | any, forHistory: boolean = false) {
const sourcePokemon = source instanceof Pokemon ? source : null; const sourcePokemon = source instanceof Pokemon ? source : null;
@ -108,6 +110,7 @@ export default class PokemonData {
this.status = sourcePokemon.status; this.status = sourcePokemon.status;
if (this.player) { if (this.player) {
this.summonData = sourcePokemon.summonData; this.summonData = sourcePokemon.summonData;
this.mysteryEncounterData = sourcePokemon.mysteryEncounterData;
} }
} }
} else { } else {
@ -137,6 +140,14 @@ export default class PokemonData {
this.summonData.tags = []; this.summonData.tags = [];
} }
} }
this.mysteryEncounterData = new MysteryEncounterPokemonData();
if (!forHistory && source.mysteryEncounterData) {
this.mysteryEncounterData.spriteScale = source.mysteryEncounterData.spriteScale;
this.mysteryEncounterData.ability = source.mysteryEncounterData.ability;
this.mysteryEncounterData.passive = source.mysteryEncounterData.passive;
this.mysteryEncounterData.types = source.mysteryEncounterData.types;
}
} }
} }

View File

@ -33,9 +33,9 @@ describe("A Trainer's Test - Mystery Encounter", () => {
game = new GameManager(phaserGame); game = new GameManager(phaserGame);
scene = game.scene; scene = game.scene;
game.override.mysteryEncounterChance(100); game.override.mysteryEncounterChance(100);
game.override.mysteryEncounterTier(MysteryEncounterTier.COMMON);
game.override.startingWave(defaultWave); game.override.startingWave(defaultWave);
game.override.startingBiome(defaultBiome); game.override.startingBiome(defaultBiome);
game.override.disableTrainerWaves(true);
const biomeMap = new Map<Biome, MysteryEncounterType[]>([ const biomeMap = new Map<Biome, MysteryEncounterType[]>([
[Biome.VOLCANO, [MysteryEncounterType.MYSTERIOUS_CHALLENGERS]], [Biome.VOLCANO, [MysteryEncounterType.MYSTERIOUS_CHALLENGERS]],

View File

@ -33,9 +33,9 @@ describe("Absolute Avarice - Mystery Encounter", () => {
game = new GameManager(phaserGame); game = new GameManager(phaserGame);
scene = game.scene; scene = game.scene;
game.override.mysteryEncounterChance(100); game.override.mysteryEncounterChance(100);
game.override.mysteryEncounterTier(MysteryEncounterTier.COMMON);
game.override.startingWave(defaultWave); game.override.startingWave(defaultWave);
game.override.startingBiome(defaultBiome); game.override.startingBiome(defaultBiome);
game.override.disableTrainerWaves(true);
vi.spyOn(MysteryEncounters, "mysteryEncountersByBiome", "get").mockReturnValue( vi.spyOn(MysteryEncounters, "mysteryEncountersByBiome", "get").mockReturnValue(
new Map<Biome, MysteryEncounterType[]>([ new Map<Biome, MysteryEncounterType[]>([
@ -81,6 +81,7 @@ describe("Absolute Avarice - Mystery Encounter", () => {
}); });
it("should not spawn outside of proper biomes", async () => { it("should not spawn outside of proper biomes", async () => {
game.override.mysteryEncounterTier(MysteryEncounterTier.GREAT);
game.override.startingBiome(Biome.VOLCANO); game.override.startingBiome(Biome.VOLCANO);
await game.runToMysteryEncounter(); await game.runToMysteryEncounter();
@ -96,6 +97,7 @@ describe("Absolute Avarice - Mystery Encounter", () => {
}); });
it("should spawn if player has enough berries", async () => { it("should spawn if player has enough berries", async () => {
game.override.mysteryEncounterTier(MysteryEncounterTier.GREAT);
game.override.starterHeldItems([{name: "BERRY", count: 2, type: BerryType.SITRUS}, {name: "BERRY", count: 3, type: BerryType.GANLON}]); game.override.starterHeldItems([{name: "BERRY", count: 2, type: BerryType.SITRUS}, {name: "BERRY", count: 3, type: BerryType.GANLON}]);
await game.runToMysteryEncounter(); await game.runToMysteryEncounter();
@ -106,7 +108,7 @@ describe("Absolute Avarice - Mystery Encounter", () => {
it("should remove all player's berries at the start of the encounter", async () => { it("should remove all player's berries at the start of the encounter", async () => {
game.override.starterHeldItems([{name: "BERRY", count: 2, type: BerryType.SITRUS}, {name: "BERRY", count: 3, type: BerryType.GANLON}]); game.override.starterHeldItems([{name: "BERRY", count: 2, type: BerryType.SITRUS}, {name: "BERRY", count: 3, type: BerryType.GANLON}]);
await game.runToMysteryEncounter(); await game.runToMysteryEncounter(MysteryEncounterType.ABSOLUTE_AVARICE, defaultParty);
expect(scene.currentBattle?.mysteryEncounter?.encounterType).toBe(MysteryEncounterType.ABSOLUTE_AVARICE); expect(scene.currentBattle?.mysteryEncounter?.encounterType).toBe(MysteryEncounterType.ABSOLUTE_AVARICE);
expect(scene.modifiers?.length).toBe(0); expect(scene.modifiers?.length).toBe(0);

View File

@ -36,9 +36,9 @@ describe("An Offer You Can't Refuse - Mystery Encounter", () => {
game = new GameManager(phaserGame); game = new GameManager(phaserGame);
scene = game.scene; scene = game.scene;
game.override.mysteryEncounterChance(100); game.override.mysteryEncounterChance(100);
game.override.mysteryEncounterTier(MysteryEncounterTier.COMMON);
game.override.startingWave(defaultWave); game.override.startingWave(defaultWave);
game.override.startingBiome(defaultBiome); game.override.startingBiome(defaultBiome);
game.override.disableTrainerWaves(true);
const biomeMap = new Map<Biome, MysteryEncounterType[]>([ const biomeMap = new Map<Biome, MysteryEncounterType[]>([
[Biome.VOLCANO, [MysteryEncounterType.MYSTERIOUS_CHALLENGERS]], [Biome.VOLCANO, [MysteryEncounterType.MYSTERIOUS_CHALLENGERS]],
@ -72,6 +72,7 @@ describe("An Offer You Can't Refuse - Mystery Encounter", () => {
}); });
it("should not spawn outside of HUMAN_TRANSITABLE_BIOMES", async () => { it("should not spawn outside of HUMAN_TRANSITABLE_BIOMES", async () => {
game.override.mysteryEncounterTier(MysteryEncounterTier.GREAT);
game.override.startingBiome(Biome.VOLCANO); game.override.startingBiome(Biome.VOLCANO);
await game.runToMysteryEncounter(); await game.runToMysteryEncounter();

View File

@ -39,9 +39,9 @@ describe("Berries Abound - Mystery Encounter", () => {
game = new GameManager(phaserGame); game = new GameManager(phaserGame);
scene = game.scene; scene = game.scene;
game.override.mysteryEncounterChance(100); game.override.mysteryEncounterChance(100);
game.override.mysteryEncounterTier(MysteryEncounterTier.COMMON);
game.override.startingWave(defaultWave); game.override.startingWave(defaultWave);
game.override.startingBiome(defaultBiome); game.override.startingBiome(defaultBiome);
game.override.disableTrainerWaves(true);
vi.spyOn(MysteryEncounters, "mysteryEncountersByBiome", "get").mockReturnValue( vi.spyOn(MysteryEncounters, "mysteryEncountersByBiome", "get").mockReturnValue(
new Map<Biome, MysteryEncounterType[]>([ new Map<Biome, MysteryEncounterType[]>([

View File

@ -0,0 +1,374 @@
import * as MysteryEncounters from "#app/data/mystery-encounters/mystery-encounters";
import { Biome } from "#app/enums/biome";
import { MysteryEncounterType } from "#app/enums/mystery-encounter-type";
import { Species } from "#app/enums/species";
import GameManager from "#app/test/utils/gameManager";
import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
import { getPokemonSpecies } from "#app/data/pokemon-species";
import * as BattleAnims from "#app/data/battle-anims";
import * as EncounterPhaseUtils from "#app/data/mystery-encounters/utils/encounter-phase-utils";
import { generateModifierTypeOption } from "#app/data/mystery-encounters/utils/encounter-phase-utils";
import { runMysteryEncounterToEnd, skipBattleRunMysteryEncounterRewardsPhase } from "#test/mystery-encounter/encounterTestUtils";
import { CommandPhase, MovePhase, NewBattlePhase, SelectModifierPhase } from "#app/phases";
import { Moves } from "#enums/moves";
import BattleScene from "#app/battle-scene";
import Pokemon, { PokemonMove } from "#app/field/pokemon";
import { Mode } from "#app/ui/ui";
import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode";
import { MysteryEncounterTier } from "#enums/mystery-encounter-tier";
import { initSceneWithoutEncounterPhase } from "#test/utils/gameManagerUtils";
import { ModifierTier } from "#app/modifier/modifier-tier";
import { ClowningAroundEncounter } from "#app/data/mystery-encounters/encounters/clowning-around-encounter";
import { TrainerType } from "#enums/trainer-type";
import { Abilities } from "#enums/abilities";
import { PostMysteryEncounterPhase } from "#app/phases/mystery-encounter-phases";
import { Button } from "#enums/buttons";
import PartyUiHandler from "#app/ui/party-ui-handler";
import OptionSelectUiHandler from "#app/ui/settings/option-select-ui-handler";
import { modifierTypes, PokemonHeldItemModifierType } from "#app/modifier/modifier-type";
import { BerryType } from "#enums/berry-type";
import { PokemonHeldItemModifier } from "#app/modifier/modifier";
import { Type } from "#app/data/type";
const namespace = "mysteryEncounter:clowningAround";
const defaultParty = [Species.LAPRAS, Species.GENGAR, Species.ABRA];
const defaultBiome = Biome.CAVE;
const defaultWave = 45;
describe("Clowning Around - Mystery Encounter", () => {
let phaserGame: Phaser.Game;
let game: GameManager;
let scene: BattleScene;
beforeAll(() => {
phaserGame = new Phaser.Game({ type: Phaser.HEADLESS });
});
beforeEach(async () => {
game = new GameManager(phaserGame);
scene = game.scene;
game.override.mysteryEncounterChance(100);
game.override.startingWave(defaultWave);
game.override.startingBiome(defaultBiome);
game.override.disableTrainerWaves(true);
vi.spyOn(MysteryEncounters, "mysteryEncountersByBiome", "get").mockReturnValue(
new Map<Biome, MysteryEncounterType[]>([
[Biome.CAVE, [MysteryEncounterType.CLOWNING_AROUND]],
])
);
});
afterEach(() => {
game.phaseInterceptor.restoreOg();
vi.clearAllMocks();
vi.resetAllMocks();
});
it("should have the correct properties", async () => {
await game.runToMysteryEncounter(MysteryEncounterType.CLOWNING_AROUND, defaultParty);
expect(ClowningAroundEncounter.encounterType).toBe(MysteryEncounterType.CLOWNING_AROUND);
expect(ClowningAroundEncounter.encounterTier).toBe(MysteryEncounterTier.ULTRA);
expect(ClowningAroundEncounter.dialogue).toBeDefined();
expect(ClowningAroundEncounter.dialogue.intro).toStrictEqual([
{ text: `${namespace}.intro` },
{
speaker: `${namespace}.speaker`,
text: `${namespace}.intro_dialogue`,
},
]);
expect(ClowningAroundEncounter.dialogue.encounterOptionsDialogue.title).toBe(`${namespace}.title`);
expect(ClowningAroundEncounter.dialogue.encounterOptionsDialogue.description).toBe(`${namespace}.description`);
expect(ClowningAroundEncounter.dialogue.encounterOptionsDialogue.query).toBe(`${namespace}.query`);
expect(ClowningAroundEncounter.options.length).toBe(3);
});
it("should not run below wave 80", async () => {
game.override.startingWave(79);
await game.runToMysteryEncounter();
expect(scene.currentBattle?.mysteryEncounter?.encounterType).not.toBe(MysteryEncounterType.CLOWNING_AROUND);
});
it("should not run above wave 179", async () => {
game.override.startingWave(181);
await game.runToMysteryEncounter();
expect(scene.currentBattle.mysteryEncounter).toBeUndefined();
});
it("should initialize fully", async () => {
initSceneWithoutEncounterPhase(scene, defaultParty);
scene.currentBattle.mysteryEncounter = ClowningAroundEncounter;
const moveInitSpy = vi.spyOn(BattleAnims, "initMoveAnim");
const moveLoadSpy = vi.spyOn(BattleAnims, "loadMoveAnimAssets");
const { onInit } = ClowningAroundEncounter;
expect(ClowningAroundEncounter.onInit).toBeDefined();
ClowningAroundEncounter.populateDialogueTokensFromRequirements(scene);
const onInitResult = onInit(scene);
const config = ClowningAroundEncounter.enemyPartyConfigs[0];
expect(config.doubleBattle).toBe(true);
expect(config.trainerConfig.trainerType).toBe(TrainerType.HARLEQUIN);
expect(config.pokemonConfigs[0]).toEqual({
species: getPokemonSpecies(Species.MR_MIME),
isBoss: true,
moveSet: [Moves.TEETER_DANCE, Moves.ALLY_SWITCH, Moves.DAZZLING_GLEAM, Moves.PSYCHIC]
});
expect(config.pokemonConfigs[1]).toEqual({
species: getPokemonSpecies(Species.BLACEPHALON),
ability: expect.any(Number),
mysteryEncounterData: expect.anything(),
isBoss: true,
moveSet: [Moves.TRICK, Moves.HYPNOSIS, Moves.SHADOW_BALL, Moves.MIND_BLOWN]
});
expect(config.pokemonConfigs[1].mysteryEncounterData.types.length).toBe(2);
expect([
Abilities.STURDY,
Abilities.PICKUP,
Abilities.INTIMIDATE,
Abilities.GUTS,
Abilities.DROUGHT,
Abilities.DRIZZLE,
Abilities.SNOW_WARNING,
Abilities.SAND_STREAM,
Abilities.ELECTRIC_SURGE,
Abilities.PSYCHIC_SURGE,
Abilities.GRASSY_SURGE,
Abilities.MISTY_SURGE,
Abilities.MAGICIAN,
Abilities.SHEER_FORCE,
Abilities.PRANKSTER
]).toContain(config.pokemonConfigs[1].ability);
expect(ClowningAroundEncounter.misc.ability).toBe(config.pokemonConfigs[1].ability);
await vi.waitFor(() => expect(moveInitSpy).toHaveBeenCalled());
await vi.waitFor(() => expect(moveLoadSpy).toHaveBeenCalled());
expect(onInitResult).toBe(true);
});
describe("Option 1 - Battle the Clown", () => {
it("should have the correct properties", () => {
const option = ClowningAroundEncounter.options[0];
expect(option.optionMode).toBe(MysteryEncounterOptionMode.DEFAULT);
expect(option.dialogue).toBeDefined();
expect(option.dialogue).toStrictEqual({
buttonLabel: `${namespace}.option.1.label`,
buttonTooltip: `${namespace}.option.1.tooltip`,
selected: [
{
speaker: `${namespace}.speaker`,
text: `${namespace}.option.1.selected`,
},
],
});
});
it("should start double battle against the clown", async () => {
const phaseSpy = vi.spyOn(scene, "pushPhase");
await game.runToMysteryEncounter(MysteryEncounterType.CLOWNING_AROUND, defaultParty);
await runMysteryEncounterToEnd(game, 1, null, true);
const enemyField = scene.getEnemyField();
expect(scene.getCurrentPhase().constructor.name).toBe(CommandPhase.name);
expect(enemyField.length).toBe(2);
expect(enemyField[0].species.speciesId).toBe(Species.MR_MIME);
expect(enemyField[0].moveset).toEqual([new PokemonMove(Moves.TEETER_DANCE), new PokemonMove(Moves.ALLY_SWITCH), new PokemonMove(Moves.DAZZLING_GLEAM), new PokemonMove(Moves.PSYCHIC)]);
expect(enemyField[1].species.speciesId).toBe(Species.BLACEPHALON);
expect(enemyField[1].moveset).toEqual([new PokemonMove(Moves.TRICK), new PokemonMove(Moves.HYPNOSIS), new PokemonMove(Moves.SHADOW_BALL), new PokemonMove(Moves.MIND_BLOWN)]);
// Should have used moves pre-battle
const movePhases = phaseSpy.mock.calls.filter(p => p[0] instanceof MovePhase).map(p => p[0]);
expect(movePhases.length).toBe(3);
expect(movePhases.filter(p => (p as MovePhase).move.moveId === Moves.ROLE_PLAY).length).toBe(1);
expect(movePhases.filter(p => (p as MovePhase).move.moveId === Moves.TAUNT).length).toBe(2);
});
it("should let the player gain the ability after battle completion", async () => {
await game.runToMysteryEncounter(MysteryEncounterType.CLOWNING_AROUND, defaultParty);
await runMysteryEncounterToEnd(game, 1, null, true);
await skipBattleRunMysteryEncounterRewardsPhase(game);
await game.phaseInterceptor.to(SelectModifierPhase, false);
expect(scene.getCurrentPhase().constructor.name).toBe(SelectModifierPhase.name);
await game.phaseInterceptor.run(SelectModifierPhase);
const abilityToTrain = scene.currentBattle.mysteryEncounter.misc.ability;
game.onNextPrompt("PostMysteryEncounterPhase", Mode.MESSAGE, () => {
game.scene.ui.getHandler().processInput(Button.ACTION);
});
// Run to ability train option selection
const optionSelectUiHandler = game.scene.ui.handlers[Mode.OPTION_SELECT] as OptionSelectUiHandler;
vi.spyOn(optionSelectUiHandler, "show");
const partyUiHandler = game.scene.ui.handlers[Mode.PARTY] as PartyUiHandler;
vi.spyOn(partyUiHandler, "show");
game.endPhase();
await game.phaseInterceptor.to(PostMysteryEncounterPhase);
expect(scene.getCurrentPhase().constructor.name).toBe(PostMysteryEncounterPhase.name);
// Wait for Yes/No confirmation to appear
await vi.waitFor(() => expect(optionSelectUiHandler.show).toHaveBeenCalled());
// Select "Yes" on train ability
optionSelectUiHandler.processInput(Button.ACTION);
// Select first pokemon in party to train
await vi.waitFor(() => expect(partyUiHandler.show).toHaveBeenCalled());
partyUiHandler.processInput(Button.ACTION);
// Click "Select" on Pokemon
partyUiHandler.processInput(Button.ACTION);
// Stop next battle before it runs
await game.phaseInterceptor.to(NewBattlePhase, false);
const leadPokemon = scene.getParty()[0];
expect(leadPokemon.mysteryEncounterData.ability).toBe(abilityToTrain);
});
});
describe("Option 2 - Remain Unprovoked", () => {
it("should have the correct properties", () => {
const option = ClowningAroundEncounter.options[1];
expect(option.optionMode).toBe(MysteryEncounterOptionMode.DEFAULT);
expect(option.dialogue).toBeDefined();
expect(option.dialogue).toStrictEqual({
buttonLabel: `${namespace}.option.2.label`,
buttonTooltip: `${namespace}.option.2.tooltip`,
selected: [
{
speaker: `${namespace}.speaker`,
text: `${namespace}.option.2.selected`,
},
{
text: `${namespace}.option.2.selected_2`,
},
{
speaker: `${namespace}.speaker`,
text: `${namespace}.option.2.selected_3`,
},
],
});
});
it("should randomize held items of the Pokemon with the most items, and not the held items of other pokemon", async () => {
await game.runToMysteryEncounter(MysteryEncounterType.CLOWNING_AROUND, defaultParty);
// 2 Sitrus Berries on lead
scene.modifiers = [];
let itemType = generateModifierTypeOption(scene, modifierTypes.BERRY, [BerryType.SITRUS]).type as PokemonHeldItemModifierType;
await addItemToPokemon(scene, scene.getParty()[0], 2, itemType);
// 2 Ganlon Berries on lead
itemType = generateModifierTypeOption(scene, modifierTypes.BERRY, [BerryType.GANLON]).type as PokemonHeldItemModifierType;
await addItemToPokemon(scene, scene.getParty()[0], 2, itemType);
// 5 Golden Punch on lead (ultra)
itemType = generateModifierTypeOption(scene, modifierTypes.GOLDEN_PUNCH).type as PokemonHeldItemModifierType;
await addItemToPokemon(scene, scene.getParty()[0], 5, itemType);
// 5 Lucky Egg on lead (ultra)
itemType = generateModifierTypeOption(scene, modifierTypes.LUCKY_EGG).type as PokemonHeldItemModifierType;
await addItemToPokemon(scene, scene.getParty()[0], 5, itemType);
// 5 Soul Dew on lead (rogue)
itemType = generateModifierTypeOption(scene, modifierTypes.SOUL_DEW).type as PokemonHeldItemModifierType;
await addItemToPokemon(scene, scene.getParty()[0], 5, itemType);
// 2 Golden Egg on lead (rogue)
itemType = generateModifierTypeOption(scene, modifierTypes.GOLDEN_EGG).type as PokemonHeldItemModifierType;
await addItemToPokemon(scene, scene.getParty()[0], 2, itemType);
// 5 Soul Dew on second party pokemon (these should not change)
itemType = generateModifierTypeOption(scene, modifierTypes.SOUL_DEW).type as PokemonHeldItemModifierType;
await addItemToPokemon(scene, scene.getParty()[1], 5, itemType);
await runMysteryEncounterToEnd(game, 2);
const leadItemsAfter = scene.getParty()[0].getHeldItems();
const ultraCountAfter = leadItemsAfter
.filter(m => m.type.tier === ModifierTier.ULTRA)
.reduce((a, b) => a + b.stackCount, 0);
const rogueCountAfter = leadItemsAfter
.filter(m => m.type.tier === ModifierTier.ROGUE)
.reduce((a, b) => a + b.stackCount, 0);
expect(ultraCountAfter).toBe(10);
expect(rogueCountAfter).toBe(7);
const secondItemsAfter = scene.getParty()[1].getHeldItems();
expect(secondItemsAfter.length).toBe(1);
expect(secondItemsAfter[0].type.id).toBe("SOUL_DEW");
expect(secondItemsAfter[0].stackCount).toBe(5);
});
it("should leave encounter without battle", async () => {
const leaveEncounterWithoutBattleSpy = vi.spyOn(EncounterPhaseUtils, "leaveEncounterWithoutBattle");
await game.runToMysteryEncounter(MysteryEncounterType.CLOWNING_AROUND, defaultParty);
await runMysteryEncounterToEnd(game, 2);
expect(leaveEncounterWithoutBattleSpy).toBeCalled();
});
});
describe("Option 3 - Return the Insults", () => {
it("should have the correct properties", () => {
const option = ClowningAroundEncounter.options[2];
expect(option.optionMode).toBe(MysteryEncounterOptionMode.DEFAULT);
expect(option.dialogue).toBeDefined();
expect(option.dialogue).toStrictEqual({
buttonLabel: `${namespace}.option.3.label`,
buttonTooltip: `${namespace}.option.3.tooltip`,
selected: [
{
speaker: `${namespace}.speaker`,
text: `${namespace}.option.3.selected`,
},
{
text: `${namespace}.option.3.selected_2`,
},
{
speaker: `${namespace}.speaker`,
text: `${namespace}.option.3.selected_3`,
},
],
});
});
it("should randomize the pokemon types of the party", async () => {
await game.runToMysteryEncounter(MysteryEncounterType.CLOWNING_AROUND, defaultParty);
// Same type moves on lead
scene.getParty()[0].moveset = [new PokemonMove(Moves.ICE_BEAM), new PokemonMove(Moves.SURF)];
// Different type moves on second
scene.getParty()[1].moveset = [new PokemonMove(Moves.GRASS_KNOT), new PokemonMove(Moves.ELECTRO_BALL)];
// No moves on third
scene.getParty()[2].moveset = [];
await runMysteryEncounterToEnd(game, 3);
const leadTypesAfter = scene.getParty()[0].mysteryEncounterData.types;
const secondaryTypesAfter = scene.getParty()[1].mysteryEncounterData.types;
const thirdTypesAfter = scene.getParty()[2].mysteryEncounterData.types;
expect(leadTypesAfter.length).toBe(2);
expect(leadTypesAfter).not.toBe([Type.ICE, Type.WATER]);
expect(secondaryTypesAfter.length).toBe(2);
expect(secondaryTypesAfter.includes(Type.GRASS)).toBeTruthy();
expect(secondaryTypesAfter.includes(Type.ELECTRIC)).toBeTruthy();
expect(thirdTypesAfter.length).toBe(1);
});
it("should leave encounter without battle", async () => {
const leaveEncounterWithoutBattleSpy = vi.spyOn(EncounterPhaseUtils, "leaveEncounterWithoutBattle");
await game.runToMysteryEncounter(MysteryEncounterType.CLOWNING_AROUND, defaultParty);
await runMysteryEncounterToEnd(game, 3);
expect(leaveEncounterWithoutBattleSpy).toBeCalled();
});
});
});
async function addItemToPokemon(scene: BattleScene, pokemon: Pokemon, stackCount: integer, itemType: PokemonHeldItemModifierType) {
const itemMod = itemType.newModifier(pokemon) as PokemonHeldItemModifier;
itemMod.stackCount = stackCount;
await scene.addModifier(itemMod, true, false, false, true);
await scene.updateModifiers(true);
}

View File

@ -35,9 +35,9 @@ describe("Delibird-y - Mystery Encounter", () => {
game = new GameManager(phaserGame); game = new GameManager(phaserGame);
scene = game.scene; scene = game.scene;
game.override.mysteryEncounterChance(100); game.override.mysteryEncounterChance(100);
game.override.mysteryEncounterTier(MysteryEncounterTier.COMMON);
game.override.startingWave(defaultWave); game.override.startingWave(defaultWave);
game.override.startingBiome(defaultBiome); game.override.startingBiome(defaultBiome);
game.override.disableTrainerWaves(true);
vi.spyOn(MysteryEncounters, "mysteryEncountersByBiome", "get").mockReturnValue( vi.spyOn(MysteryEncounters, "mysteryEncountersByBiome", "get").mockReturnValue(
new Map<Biome, MysteryEncounterType[]>([ new Map<Biome, MysteryEncounterType[]>([

View File

@ -33,7 +33,6 @@ describe("Department Store Sale - Mystery Encounter", () => {
game = new GameManager(phaserGame); game = new GameManager(phaserGame);
scene = game.scene; scene = game.scene;
game.override.mysteryEncounterChance(100); game.override.mysteryEncounterChance(100);
game.override.mysteryEncounterTier(MysteryEncounterTier.COMMON);
game.override.startingWave(defaultWave); game.override.startingWave(defaultWave);
game.override.startingBiome(defaultBiome); game.override.startingBiome(defaultBiome);
game.override.disableTrainerWaves(true); game.override.disableTrainerWaves(true);
@ -73,6 +72,7 @@ describe("Department Store Sale - Mystery Encounter", () => {
}); });
it("should not spawn outside of CIVILIZATION_ENCOUNTER_BIOMES", async () => { it("should not spawn outside of CIVILIZATION_ENCOUNTER_BIOMES", async () => {
game.override.mysteryEncounterTier(MysteryEncounterTier.COMMON);
game.override.startingBiome(Biome.VOLCANO); game.override.startingBiome(Biome.VOLCANO);
await game.runToMysteryEncounter(); await game.runToMysteryEncounter();

View File

@ -40,9 +40,9 @@ describe("Fiery Fallout - Mystery Encounter", () => {
game = new GameManager(phaserGame); game = new GameManager(phaserGame);
scene = game.scene; scene = game.scene;
game.override.mysteryEncounterChance(100); game.override.mysteryEncounterChance(100);
game.override.mysteryEncounterTier(MysteryEncounterTier.COMMON);
game.override.startingWave(defaultWave); game.override.startingWave(defaultWave);
game.override.startingBiome(defaultBiome); game.override.startingBiome(defaultBiome);
game.override.disableTrainerWaves(true);
vi.spyOn(MysteryEncounters, "mysteryEncountersByBiome", "get").mockReturnValue( vi.spyOn(MysteryEncounters, "mysteryEncountersByBiome", "get").mockReturnValue(
new Map<Biome, MysteryEncounterType[]>([ new Map<Biome, MysteryEncounterType[]>([

View File

@ -38,9 +38,9 @@ describe("Fight or Flight - Mystery Encounter", () => {
game = new GameManager(phaserGame); game = new GameManager(phaserGame);
scene = game.scene; scene = game.scene;
game.override.mysteryEncounterChance(100); game.override.mysteryEncounterChance(100);
game.override.mysteryEncounterTier(MysteryEncounterTier.COMMON);
game.override.startingWave(defaultWave); game.override.startingWave(defaultWave);
game.override.startingBiome(defaultBiome); game.override.startingBiome(defaultBiome);
game.override.disableTrainerWaves(true);
vi.spyOn(MysteryEncounters, "mysteryEncountersByBiome", "get").mockReturnValue( vi.spyOn(MysteryEncounters, "mysteryEncountersByBiome", "get").mockReturnValue(
new Map<Biome, MysteryEncounterType[]>([ new Map<Biome, MysteryEncounterType[]>([

View File

@ -36,6 +36,7 @@ describe("Lost at Sea - Mystery Encounter", () => {
game.override.mysteryEncounterChance(100); game.override.mysteryEncounterChance(100);
game.override.startingWave(defaultWave); game.override.startingWave(defaultWave);
game.override.startingBiome(defaultBiome); game.override.startingBiome(defaultBiome);
game.override.disableTrainerWaves(true);
vi.spyOn(MysteryEncounters, "mysteryEncountersByBiome", "get").mockReturnValue( vi.spyOn(MysteryEncounters, "mysteryEncountersByBiome", "get").mockReturnValue(
new Map<Biome, MysteryEncounterType[]>([ new Map<Biome, MysteryEncounterType[]>([
@ -65,6 +66,7 @@ describe("Lost at Sea - Mystery Encounter", () => {
}); });
it("should not spawn outside of sea biome", async () => { it("should not spawn outside of sea biome", async () => {
game.override.mysteryEncounterTier(MysteryEncounterTier.COMMON);
game.override.startingBiome(Biome.MOUNTAIN); game.override.startingBiome(Biome.MOUNTAIN);
await game.runToMysteryEncounter(); await game.runToMysteryEncounter();

View File

@ -0,0 +1,269 @@
import * as MysteryEncounters from "#app/data/mystery-encounters/mystery-encounters";
import { HUMAN_TRANSITABLE_BIOMES } from "#app/data/mystery-encounters/mystery-encounters";
import { Biome } from "#app/enums/biome";
import { MysteryEncounterType } from "#app/enums/mystery-encounter-type";
import { Species } from "#app/enums/species";
import GameManager from "#app/test/utils/gameManager";
import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
import { runMysteryEncounterToEnd, skipBattleRunMysteryEncounterRewardsPhase } from "#test/mystery-encounter/encounterTestUtils";
import { CommandPhase, SelectModifierPhase } from "#app/phases";
import BattleScene from "#app/battle-scene";
import { Mode } from "#app/ui/ui";
import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode";
import { MysteryEncounterTier } from "#enums/mystery-encounter-tier";
import { initSceneWithoutEncounterPhase } from "#test/utils/gameManagerUtils";
import { ModifierTier } from "#app/modifier/modifier-tier";
import { MysteriousChallengersEncounter } from "#app/data/mystery-encounters/encounters/mysterious-challengers-encounter";
import { TrainerConfig, TrainerPartyCompoundTemplate, TrainerPartyTemplate } from "#app/data/trainer-config";
import { PartyMemberStrength } from "#enums/party-member-strength";
import { MysteryEncounterMode } from "#enums/mystery-encounter-mode";
import ModifierSelectUiHandler from "#app/ui/modifier-select-ui-handler";
import IMysteryEncounter from "#app/data/mystery-encounters/mystery-encounter";
const namespace = "mysteryEncounter:mysteriousChallengers";
const defaultParty = [Species.LAPRAS, Species.GENGAR, Species.ABRA];
const defaultBiome = Biome.CAVE;
const defaultWave = 45;
describe("Mysterious Challengers - Mystery Encounter", () => {
let phaserGame: Phaser.Game;
let game: GameManager;
let scene: BattleScene;
beforeAll(() => {
phaserGame = new Phaser.Game({ type: Phaser.HEADLESS });
});
beforeEach(async () => {
game = new GameManager(phaserGame);
scene = game.scene;
game.override.mysteryEncounterChance(100);
game.override.startingWave(defaultWave);
game.override.startingBiome(defaultBiome);
game.override.disableTrainerWaves(true);
const biomeMap = new Map<Biome, MysteryEncounterType[]>([
[Biome.VOLCANO, [MysteryEncounterType.FIGHT_OR_FLIGHT]],
]);
HUMAN_TRANSITABLE_BIOMES.forEach(biome => {
biomeMap.set(biome, [MysteryEncounterType.MYSTERIOUS_CHALLENGERS]);
});
vi.spyOn(MysteryEncounters, "mysteryEncountersByBiome", "get").mockReturnValue(biomeMap);
});
afterEach(() => {
game.phaseInterceptor.restoreOg();
vi.clearAllMocks();
vi.resetAllMocks();
});
it("should have the correct properties", async () => {
await game.runToMysteryEncounter(MysteryEncounterType.MYSTERIOUS_CHALLENGERS, defaultParty);
expect(MysteriousChallengersEncounter.encounterType).toBe(MysteryEncounterType.MYSTERIOUS_CHALLENGERS);
expect(MysteriousChallengersEncounter.encounterTier).toBe(MysteryEncounterTier.GREAT);
expect(MysteriousChallengersEncounter.dialogue).toBeDefined();
expect(MysteriousChallengersEncounter.dialogue.intro).toStrictEqual([{ text: `${namespace}.intro` }]);
expect(MysteriousChallengersEncounter.dialogue.encounterOptionsDialogue.title).toBe(`${namespace}.title`);
expect(MysteriousChallengersEncounter.dialogue.encounterOptionsDialogue.description).toBe(`${namespace}.description`);
expect(MysteriousChallengersEncounter.dialogue.encounterOptionsDialogue.query).toBe(`${namespace}.query`);
expect(MysteriousChallengersEncounter.options.length).toBe(3);
});
it("should not spawn outside of HUMAN_TRANSITABLE_BIOMES", async () => {
game.override.mysteryEncounterTier(MysteryEncounterTier.GREAT);
game.override.startingBiome(Biome.VOLCANO);
await game.runToMysteryEncounter();
expect(scene.currentBattle?.mysteryEncounter?.encounterType).not.toBe(MysteryEncounterType.MYSTERIOUS_CHALLENGERS);
});
it("should not run below wave 10", async () => {
game.override.startingWave(9);
await game.runToMysteryEncounter();
expect(scene.currentBattle?.mysteryEncounter?.encounterType).not.toBe(MysteryEncounterType.MYSTERIOUS_CHALLENGERS);
});
it("should not run above wave 179", async () => {
game.override.startingWave(181);
await game.runToMysteryEncounter();
expect(scene.currentBattle.mysteryEncounter).toBeUndefined();
});
it("should initialize fully", async () => {
initSceneWithoutEncounterPhase(scene, defaultParty);
scene.currentBattle.mysteryEncounter = new IMysteryEncounter(MysteriousChallengersEncounter);
const encounter = scene.currentBattle.mysteryEncounter;
scene.currentBattle.waveIndex = defaultWave;
const { onInit } = encounter;
expect(encounter.onInit).toBeDefined();
encounter.populateDialogueTokensFromRequirements(scene);
const onInitResult = onInit(scene);
expect(encounter.enemyPartyConfigs).toBeDefined();
expect(encounter.enemyPartyConfigs.length).toBe(3);
expect(encounter.enemyPartyConfigs).toEqual([
{
trainerConfig: expect.any(TrainerConfig),
female: expect.any(Boolean),
},
{
trainerConfig: expect.any(TrainerConfig),
levelAdditiveMultiplier: 0.5,
female: expect.any(Boolean),
},
{
trainerConfig: expect.any(TrainerConfig),
levelAdditiveMultiplier: 1,
female: expect.any(Boolean),
}
]);
expect(encounter.enemyPartyConfigs[1].trainerConfig.partyTemplates[0]).toEqual(new TrainerPartyCompoundTemplate(
new TrainerPartyTemplate(1, PartyMemberStrength.STRONGER, false, true),
new TrainerPartyTemplate(3, PartyMemberStrength.AVERAGE, false, true)
));
expect(encounter.enemyPartyConfigs[2].trainerConfig.partyTemplates[0]).toEqual(new TrainerPartyCompoundTemplate(
new TrainerPartyTemplate(2, PartyMemberStrength.AVERAGE),
new TrainerPartyTemplate(3, PartyMemberStrength.STRONG),
new TrainerPartyTemplate(1, PartyMemberStrength.STRONGER))
);
expect(encounter.spriteConfigs).toBeDefined();
expect(encounter.spriteConfigs.length).toBe(3);
expect(onInitResult).toBe(true);
});
describe("Option 1 - Normal Battle", () => {
it("should have the correct properties", () => {
const option = MysteriousChallengersEncounter.options[0];
expect(option.optionMode).toBe(MysteryEncounterOptionMode.DEFAULT);
expect(option.dialogue).toBeDefined();
expect(option.dialogue).toStrictEqual({
buttonLabel: `${namespace}.option.1.label`,
buttonTooltip: `${namespace}.option.1.tooltip`,
selected: [
{
text: `${namespace}.option.selected`,
},
],
});
});
it("should start battle against the trainer", async () => {
await game.runToMysteryEncounter(MysteryEncounterType.MYSTERIOUS_CHALLENGERS, defaultParty);
await runMysteryEncounterToEnd(game, 1, null, true);
expect(scene.getCurrentPhase().constructor.name).toBe(CommandPhase.name);
expect(scene.currentBattle.trainer).toBeDefined();
expect(scene.currentBattle.mysteryEncounter.encounterMode).toBe(MysteryEncounterMode.TRAINER_BATTLE);
});
it("should have normal trainer rewards after battle", async () => {
await game.runToMysteryEncounter(MysteryEncounterType.MYSTERIOUS_CHALLENGERS, defaultParty);
await runMysteryEncounterToEnd(game, 1, null, true);
await skipBattleRunMysteryEncounterRewardsPhase(game);
await game.phaseInterceptor.to(SelectModifierPhase, false);
expect(scene.getCurrentPhase().constructor.name).toBe(SelectModifierPhase.name);
await game.phaseInterceptor.run(SelectModifierPhase);
expect(scene.ui.getMode()).to.equal(Mode.MODIFIER_SELECT);
const modifierSelectHandler = scene.ui.handlers.find(h => h instanceof ModifierSelectUiHandler) as ModifierSelectUiHandler;
expect(modifierSelectHandler.options.length).toEqual(3);
expect(modifierSelectHandler.options[0].modifierTypeOption.type.id).toContain("TM_COMMON");
expect(modifierSelectHandler.options[1].modifierTypeOption.type.id).toContain("TM_GREAT");
expect(modifierSelectHandler.options[2].modifierTypeOption.type.id).toContain("MEMORY_MUSHROOM");
});
});
describe("Option 2 - Hard Battle", () => {
it("should have the correct properties", () => {
const option = MysteriousChallengersEncounter.options[1];
expect(option.optionMode).toBe(MysteryEncounterOptionMode.DEFAULT);
expect(option.dialogue).toBeDefined();
expect(option.dialogue).toStrictEqual({
buttonLabel: `${namespace}.option.2.label`,
buttonTooltip: `${namespace}.option.2.tooltip`,
selected: [
{
text: `${namespace}.option.selected`,
},
],
});
});
it("should start battle against the trainer", async () => {
await game.runToMysteryEncounter(MysteryEncounterType.MYSTERIOUS_CHALLENGERS, defaultParty);
await runMysteryEncounterToEnd(game, 2, null, true);
expect(scene.getCurrentPhase().constructor.name).toBe(CommandPhase.name);
expect(scene.currentBattle.trainer).toBeDefined();
expect(scene.currentBattle.mysteryEncounter.encounterMode).toBe(MysteryEncounterMode.TRAINER_BATTLE);
});
it("should have hard trainer rewards after battle", async () => {
await game.runToMysteryEncounter(MysteryEncounterType.MYSTERIOUS_CHALLENGERS, defaultParty);
await runMysteryEncounterToEnd(game, 2, null, true);
await skipBattleRunMysteryEncounterRewardsPhase(game);
await game.phaseInterceptor.to(SelectModifierPhase, false);
expect(scene.getCurrentPhase().constructor.name).toBe(SelectModifierPhase.name);
await game.phaseInterceptor.run(SelectModifierPhase);
expect(scene.ui.getMode()).to.equal(Mode.MODIFIER_SELECT);
const modifierSelectHandler = scene.ui.handlers.find(h => h instanceof ModifierSelectUiHandler) as ModifierSelectUiHandler;
expect(modifierSelectHandler.options.length).toEqual(4);
expect(modifierSelectHandler.options[0].modifierTypeOption.type.tier - modifierSelectHandler.options[0].modifierTypeOption.upgradeCount).toBe(ModifierTier.ULTRA);
expect(modifierSelectHandler.options[1].modifierTypeOption.type.tier - modifierSelectHandler.options[1].modifierTypeOption.upgradeCount).toBe(ModifierTier.ULTRA);
expect(modifierSelectHandler.options[2].modifierTypeOption.type.tier - modifierSelectHandler.options[2].modifierTypeOption.upgradeCount).toBe(ModifierTier.GREAT);
expect(modifierSelectHandler.options[3].modifierTypeOption.type.tier - modifierSelectHandler.options[3].modifierTypeOption.upgradeCount).toBe(ModifierTier.GREAT);
});
});
describe("Option 3 - Brutal Battle", () => {
it("should have the correct properties", () => {
const option = MysteriousChallengersEncounter.options[2];
expect(option.optionMode).toBe(MysteryEncounterOptionMode.DEFAULT);
expect(option.dialogue).toBeDefined();
expect(option.dialogue).toStrictEqual({
buttonLabel: `${namespace}.option.3.label`,
buttonTooltip: `${namespace}.option.3.tooltip`,
selected: [
{
text: `${namespace}.option.selected`,
},
],
});
});
it("should start battle against the trainer", async () => {
await game.runToMysteryEncounter(MysteryEncounterType.MYSTERIOUS_CHALLENGERS, defaultParty);
await runMysteryEncounterToEnd(game, 3, null, true);
expect(scene.getCurrentPhase().constructor.name).toBe(CommandPhase.name);
expect(scene.currentBattle.trainer).toBeDefined();
expect(scene.currentBattle.mysteryEncounter.encounterMode).toBe(MysteryEncounterMode.TRAINER_BATTLE);
});
it("should have brutal trainer rewards after battle", async () => {
await game.runToMysteryEncounter(MysteryEncounterType.MYSTERIOUS_CHALLENGERS, defaultParty);
await runMysteryEncounterToEnd(game, 3, null, true);
await skipBattleRunMysteryEncounterRewardsPhase(game);
await game.phaseInterceptor.to(SelectModifierPhase, false);
expect(scene.getCurrentPhase().constructor.name).toBe(SelectModifierPhase.name);
await game.phaseInterceptor.run(SelectModifierPhase);
expect(scene.ui.getMode()).to.equal(Mode.MODIFIER_SELECT);
const modifierSelectHandler = scene.ui.handlers.find(h => h instanceof ModifierSelectUiHandler) as ModifierSelectUiHandler;
expect(modifierSelectHandler.options.length).toEqual(4);
expect(modifierSelectHandler.options[0].modifierTypeOption.type.tier - modifierSelectHandler.options[0].modifierTypeOption.upgradeCount).toBe(ModifierTier.ROGUE);
expect(modifierSelectHandler.options[1].modifierTypeOption.type.tier - modifierSelectHandler.options[1].modifierTypeOption.upgradeCount).toBe(ModifierTier.ROGUE);
expect(modifierSelectHandler.options[2].modifierTypeOption.type.tier - modifierSelectHandler.options[2].modifierTypeOption.upgradeCount).toBe(ModifierTier.ULTRA);
expect(modifierSelectHandler.options[3].modifierTypeOption.type.tier - modifierSelectHandler.options[3].modifierTypeOption.upgradeCount).toBe(ModifierTier.GREAT);
});
});
});

View File

@ -33,9 +33,9 @@ describe("The Pokemon Salesman - Mystery Encounter", () => {
game = new GameManager(phaserGame); game = new GameManager(phaserGame);
scene = game.scene; scene = game.scene;
game.override.mysteryEncounterChance(100); game.override.mysteryEncounterChance(100);
game.override.mysteryEncounterTier(MysteryEncounterTier.COMMON);
game.override.startingWave(defaultWave); game.override.startingWave(defaultWave);
game.override.startingBiome(defaultBiome); game.override.startingBiome(defaultBiome);
game.override.disableTrainerWaves(true);
const biomeMap = new Map<Biome, MysteryEncounterType[]>([ const biomeMap = new Map<Biome, MysteryEncounterType[]>([
[Biome.VOLCANO, [MysteryEncounterType.MYSTERIOUS_CHALLENGERS]], [Biome.VOLCANO, [MysteryEncounterType.MYSTERIOUS_CHALLENGERS]],
@ -69,6 +69,7 @@ describe("The Pokemon Salesman - Mystery Encounter", () => {
}); });
it("should not spawn outside of HUMAN_TRANSITABLE_BIOMES", async () => { it("should not spawn outside of HUMAN_TRANSITABLE_BIOMES", async () => {
game.override.mysteryEncounterTier(MysteryEncounterTier.ULTRA);
game.override.startingBiome(Biome.VOLCANO); game.override.startingBiome(Biome.VOLCANO);
await game.runToMysteryEncounter(); await game.runToMysteryEncounter();

View File

@ -23,6 +23,7 @@ import { PokemonBaseStatTotalModifier } from "#app/modifier/modifier";
import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode"; import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode";
import { MysteryEncounterTier } from "#enums/mystery-encounter-tier"; import { MysteryEncounterTier } from "#enums/mystery-encounter-tier";
import { initSceneWithoutEncounterPhase } from "#test/utils/gameManagerUtils"; import { initSceneWithoutEncounterPhase } from "#test/utils/gameManagerUtils";
import { MysteryEncounterPokemonData } from "#app/data/mystery-encounters/mystery-encounter-pokemon-data";
const namespace = "mysteryEncounter:theStrongStuff"; const namespace = "mysteryEncounter:theStrongStuff";
const defaultParty = [Species.LAPRAS, Species.GENGAR, Species.ABRA]; const defaultParty = [Species.LAPRAS, Species.GENGAR, Species.ABRA];
@ -42,9 +43,9 @@ describe("The Strong Stuff - Mystery Encounter", () => {
game = new GameManager(phaserGame); game = new GameManager(phaserGame);
scene = game.scene; scene = game.scene;
game.override.mysteryEncounterChance(100); game.override.mysteryEncounterChance(100);
game.override.mysteryEncounterTier(MysteryEncounterTier.COMMON);
game.override.startingWave(defaultWave); game.override.startingWave(defaultWave);
game.override.startingBiome(defaultBiome); game.override.startingBiome(defaultBiome);
game.override.disableTrainerWaves(true);
vi.spyOn(MysteryEncounters, "mysteryEncountersByBiome", "get").mockReturnValue( vi.spyOn(MysteryEncounters, "mysteryEncountersByBiome", "get").mockReturnValue(
new Map<Biome, MysteryEncounterType[]>([ new Map<Biome, MysteryEncounterType[]>([
@ -74,6 +75,7 @@ describe("The Strong Stuff - Mystery Encounter", () => {
}); });
it("should not spawn outside of CAVE biome", async () => { it("should not spawn outside of CAVE biome", async () => {
game.override.mysteryEncounterTier(MysteryEncounterTier.COMMON);
game.override.startingBiome(Biome.MOUNTAIN); game.override.startingBiome(Biome.MOUNTAIN);
await game.runToMysteryEncounter(); await game.runToMysteryEncounter();
@ -118,7 +120,7 @@ describe("The Strong Stuff - Mystery Encounter", () => {
species: getPokemonSpecies(Species.SHUCKLE), species: getPokemonSpecies(Species.SHUCKLE),
isBoss: true, isBoss: true,
bossSegments: 5, bossSegments: 5,
spriteScale: 1.5, mysteryEncounterData: new MysteryEncounterPokemonData(1.5),
nature: Nature.BOLD, nature: Nature.BOLD,
moveSet: [Moves.INFESTATION, Moves.SALT_CURE, Moves.GASTRO_ACID, Moves.HEAL_ORDER], moveSet: [Moves.INFESTATION, Moves.SALT_CURE, Moves.GASTRO_ACID, Moves.HEAL_ORDER],
modifierTypes: expect.any(Array), modifierTypes: expect.any(Array),

View File

@ -39,9 +39,9 @@ describe("Trash to Treasure - Mystery Encounter", () => {
game = new GameManager(phaserGame); game = new GameManager(phaserGame);
scene = game.scene; scene = game.scene;
game.override.mysteryEncounterChance(100); game.override.mysteryEncounterChance(100);
game.override.mysteryEncounterTier(MysteryEncounterTier.COMMON);
game.override.startingWave(defaultWave); game.override.startingWave(defaultWave);
game.override.startingBiome(defaultBiome); game.override.startingBiome(defaultBiome);
game.override.disableTrainerWaves(true);
vi.spyOn(MysteryEncounters, "mysteryEncountersByBiome", "get").mockReturnValue( vi.spyOn(MysteryEncounters, "mysteryEncountersByBiome", "get").mockReturnValue(
new Map<Biome, MysteryEncounterType[]>([ new Map<Biome, MysteryEncounterType[]>([

View File

@ -81,8 +81,8 @@ export default class MysteryEncounterUiHandler extends UiHandler {
this.rarityBall.setScale(0.75); this.rarityBall.setScale(0.75);
this.descriptionContainer.add(this.rarityBall); this.descriptionContainer.add(this.rarityBall);
const dexProgressIndicator = this.scene.add.sprite(12, 9, "encounter_radar"); const dexProgressIndicator = this.scene.add.sprite(12, 10, "encounter_radar");
dexProgressIndicator.setScale(0.85); dexProgressIndicator.setScale(0.80);
this.dexProgressContainer.add(dexProgressIndicator); this.dexProgressContainer.add(dexProgressIndicator);
this.dexProgressContainer.setInteractive(new Phaser.Geom.Rectangle(0, 0, 24, 28), Phaser.Geom.Rectangle.Contains); this.dexProgressContainer.setInteractive(new Phaser.Geom.Rectangle(0, 0, 24, 28), Phaser.Geom.Rectangle.Contains);
this.dexProgressContainer.on("pointerover", () => { this.dexProgressContainer.on("pointerover", () => {