balance changes and bug fixes to MEs

This commit is contained in:
ImperialSympathizer 2024-09-09 09:11:49 -04:00
parent 0db39f9a1d
commit 709e1b3148
14 changed files with 369 additions and 252 deletions

View File

@ -2920,8 +2920,8 @@ export default class BattleScene extends SceneBase {
this.shiftPhase();
}
applyPartyExp(expValue: number): void {
const participantIds = this.currentBattle.playerParticipantIds;
applyPartyExp(expValue: number, pokemonDefeated: boolean, useWaveIndexMultiplier?: boolean, pokemonParticipantIds?: Set<number>): void {
const participantIds = pokemonParticipantIds ?? this.currentBattle.playerParticipantIds;
const party = this.getParty();
const expShareModifier = this.findModifier(m => m instanceof ExpShareModifier) as ExpShareModifier;
const expBalanceModifier = this.findModifier(m => m instanceof ExpBalanceModifier) as ExpBalanceModifier;
@ -2929,8 +2929,12 @@ export default class BattleScene extends SceneBase {
const nonFaintedPartyMembers = party.filter(p => p.hp);
const expPartyMembers = nonFaintedPartyMembers.filter(p => p.level < this.getMaxExpLevel());
const partyMemberExp: number[] = [];
// EXP value calculation is based off Pokemon.getExpValue
if (useWaveIndexMultiplier) {
expValue = Math.floor(expValue * this.currentBattle.waveIndex / 5 + 1);
}
if (participantIds.size) {
if (participantIds.size > 0) {
if (this.currentBattle.battleType === BattleType.TRAINER || this.currentBattle.mysteryEncounter?.encounterMode === MysteryEncounterMode.TRAINER_BATTLE) {
expValue = Math.floor(expValue * 1.5);
} else if (this.currentBattle.battleType === BattleType.MYSTERY_ENCOUNTER && this.currentBattle.mysteryEncounter) {
@ -2939,7 +2943,7 @@ export default class BattleScene extends SceneBase {
for (const partyMember of nonFaintedPartyMembers) {
const pId = partyMember.id;
const participated = participantIds.has(pId);
if (participated) {
if (participated && pokemonDefeated) {
partyMember.addFriendship(2);
const machoBraceModifier = partyMember.getHeldItems().find(m => m instanceof PokemonIncrementingStatModifier);
if (machoBraceModifier && machoBraceModifier.stackCount < machoBraceModifier.getMaxStackCount(this)) {

View File

@ -1,4 +1,4 @@
import { EnemyPartyConfig, initBattleWithEnemyConfig, leaveEncounterWithoutBattle, selectPokemonForOption, setEncounterRewards, transitionMysteryEncounterIntroVisuals, } from "#app/data/mystery-encounters/utils/encounter-phase-utils";
import { EnemyPartyConfig, initBattleWithEnemyConfig, leaveEncounterWithoutBattle, selectPokemonForOption, setEncounterRewards } from "#app/data/mystery-encounters/utils/encounter-phase-utils";
import Pokemon, { EnemyPokemon, PlayerPokemon, PokemonMove } from "#app/field/pokemon";
import { MysteryEncounterType } from "#enums/mystery-encounter-type";
import { Species } from "#enums/species";
@ -132,13 +132,16 @@ export const DancingLessonsEncounter: MysteryEncounter =
const oricorioData = new PokemonData(enemyPokemon);
const oricorio = scene.addEnemyPokemon(species, scene.currentBattle.enemyLevels![0], TrainerSlot.NONE, false, oricorioData);
oricorio.setVisible(false);
oricorio.loadAssets().then(() => oricorio.setVisible(true));
// Adds a real Pokemon sprite to the field (required for the animation)
scene.getEnemyParty().forEach(enemyPokemon => enemyPokemon.destroy());
scene.getEnemyParty().forEach(enemyPokemon => {
scene.field.remove(enemyPokemon, true);
});
scene.currentBattle.enemyParty = [oricorio];
scene.field.add(oricorio);
// Spawns on offscreen field
oricorio.x -= 300;
encounter.loadAssets.push(oricorio.loadAssets());
const config: EnemyPartyConfig = {
levelAdditiveMultiplier: 1,
@ -177,8 +180,6 @@ export const DancingLessonsEncounter: MysteryEncounter =
// Pick battle
const encounter = scene.currentBattle.mysteryEncounter!;
transitionMysteryEncounterIntroVisuals(scene, true, true, 500);
encounter.startOfBattleEffects.push({
sourceBattlerIndex: BattlerIndex.ENEMY,
targets: [BattlerIndex.PLAYER],
@ -186,6 +187,7 @@ export const DancingLessonsEncounter: MysteryEncounter =
ignorePp: true
});
await hideOricorioPokemon(scene);
setEncounterRewards(scene, { guaranteedModifierTypeFuncs: [modifierTypes.BATON], fillRemaining: true });
await initBattleWithEnemyConfig(scene, encounter.enemyPartyConfigs[0]);
})
@ -220,6 +222,7 @@ export const DancingLessonsEncounter: MysteryEncounter =
})
.withOptionPhase(async (scene: BattleScene) => {
// Learn its Dance
hideOricorioPokemon(scene);
leaveEncounterWithoutBattle(scene, true);
})
.build()
@ -291,10 +294,28 @@ export const DancingLessonsEncounter: MysteryEncounter =
}
}
transitionMysteryEncounterIntroVisuals(scene, true, true, 500);
hideOricorioPokemon(scene);
await catchPokemon(scene, oricorio, null, PokeballType.POKEBALL, false);
leaveEncounterWithoutBattle(scene, true);
})
.build()
)
.build();
function hideOricorioPokemon(scene: BattleScene) {
return new Promise<void>(resolve => {
const oricorioSprite = scene.getEnemyParty()[0];
scene.tweens.add({
targets: oricorioSprite,
x: "+=16",
y: "-=16",
alpha: 0,
ease: "Sine.easeInOut",
duration: 750,
onComplete: () => {
scene.field.remove(oricorioSprite, true);
resolve();
}
});
});
}

View File

@ -40,7 +40,7 @@ export const DelibirdyEncounter: MysteryEncounter =
MysteryEncounterBuilder.withEncounterType(MysteryEncounterType.DELIBIRDY)
.withEncounterTier(MysteryEncounterTier.GREAT)
.withSceneWaveRangeRequirement(10, 180)
.withSceneRequirement(new MoneyRequirement(0, 2.75)) // Must have enough money for it to spawn at the very least
.withSceneRequirement(new MoneyRequirement(0, 2)) // Must have enough money for it to spawn at the very least
.withPrimaryPokemonRequirement(new CombinationPokemonRequirement( // Must also have either option 2 or 3 available to spawn
new HeldItemRequirement(OPTION_2_ALLOWED_MODIFIERS),
new HeldItemRequirement(OPTION_3_DISALLOWED_MODIFIERS, 1, true)
@ -91,7 +91,7 @@ export const DelibirdyEncounter: MysteryEncounter =
.withOption(
MysteryEncounterOptionBuilder
.newOptionWithMode(MysteryEncounterOptionMode.DISABLED_OR_DEFAULT)
.withSceneMoneyRequirement(0, 2.75) // Must have money to spawn
.withSceneMoneyRequirement(0, 2) // Must have money to spawn
.withDialogue({
buttonLabel: `${namespace}.option.1.label`,
buttonTooltip: `${namespace}.option.1.tooltip`,

View File

@ -10,6 +10,7 @@ import MysteryEncounter, { MysteryEncounterBuilder } from "../mystery-encounter"
import { MysteryEncounterTier } from "#enums/mystery-encounter-tier";
import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode";
import { Stat } from "#enums/stat";
import i18next from "i18next";
/** i18n namespace for the encounter */
const namespace = "mysteryEncounter:fieldTrip";
@ -60,11 +61,6 @@ export const FieldTripEncounter: MysteryEncounter =
buttonLabel: `${namespace}.option.1.label`,
buttonTooltip: `${namespace}.option.1.tooltip`,
secondOptionPrompt: `${namespace}.second_option_prompt`,
selected: [
{
text: `${namespace}.option.selected`,
},
],
})
.withPreOptionPhase(async (scene: BattleScene): Promise<boolean> => {
const encounter = scene.currentBattle.mysteryEncounter!;
@ -75,44 +71,8 @@ export const FieldTripEncounter: MysteryEncounter =
label: move.getName(),
handler: () => {
// Pokemon and move selected
const correctMove = move.getMove().category === MoveCategory.PHYSICAL;
encounter.setDialogueToken("moveCategory", "Physical");
if (!correctMove) {
encounter.options[0].dialogue!.selected = [
{
text: `${namespace}.option.incorrect`,
speaker: `${namespace}.speaker`,
},
{
text: `${namespace}.option.lesson_learned`,
},
];
encounter.dialogue.outro = [
{
text: `${namespace}.outro_bad`,
speaker: `${namespace}.speaker`,
},
];
setEncounterExp(scene, scene.getParty().map((p) => p.id), 50);
} else {
encounter.setDialogueToken("pokeName", pokemon.getNameToRender());
encounter.setDialogueToken("move", move.getName());
encounter.options[0].dialogue!.selected = [
{
text: `${namespace}.option.selected`,
},
];
encounter.dialogue.outro = [
{
text: `${namespace}.outro_good`,
speaker: `${namespace}.speaker`,
},
];
setEncounterExp(scene, [pokemon.id], 100);
}
encounter.misc = {
correctMove: correctMove,
};
encounter.setDialogueToken("moveCategory", i18next.t(`${namespace}.physical`));
pokemonAndMoveChosen(scene, pokemon, move, MoveCategory.PHYSICAL);
return true;
},
};
@ -146,11 +106,6 @@ export const FieldTripEncounter: MysteryEncounter =
buttonLabel: `${namespace}.option.2.label`,
buttonTooltip: `${namespace}.option.2.tooltip`,
secondOptionPrompt: `${namespace}.second_option_prompt`,
selected: [
{
text: `${namespace}.option.selected`,
},
],
})
.withPreOptionPhase(async (scene: BattleScene): Promise<boolean> => {
const encounter = scene.currentBattle.mysteryEncounter!;
@ -161,50 +116,8 @@ export const FieldTripEncounter: MysteryEncounter =
label: move.getName(),
handler: () => {
// Pokemon and move selected
const correctMove = move.getMove().category === MoveCategory.SPECIAL;
encounter.setDialogueToken("moveCategory", "Special");
if (!correctMove) {
encounter.options[1].dialogue!.selected = [
{
text: `${namespace}.option.incorrect`,
speaker: `${namespace}.speaker`,
},
{
text: `${namespace}.option.lesson_learned`,
},
];
encounter.dialogue.outro = [
{
text: `${namespace}.outro_bad`,
speaker: `${namespace}.speaker`,
},
];
encounter.dialogue.outro = [
{
text: `${namespace}.outro_bad`,
speaker: `${namespace}.speaker`,
},
];
setEncounterExp(scene, scene.getParty().map((p) => p.id), 50);
} else {
encounter.setDialogueToken("pokeName", pokemon.getNameToRender());
encounter.setDialogueToken("move", move.getName());
encounter.options[1].dialogue!.selected = [
{
text: `${namespace}.option.selected`,
},
];
encounter.dialogue.outro = [
{
text: `${namespace}.outro_good`,
speaker: `${namespace}.speaker`,
},
];
setEncounterExp(scene, [pokemon.id], 100);
}
encounter.misc = {
correctMove: correctMove,
};
encounter.setDialogueToken("moveCategory", i18next.t(`${namespace}.special`));
pokemonAndMoveChosen(scene, pokemon, move, MoveCategory.SPECIAL);
return true;
},
};
@ -238,11 +151,6 @@ export const FieldTripEncounter: MysteryEncounter =
buttonLabel: `${namespace}.option.3.label`,
buttonTooltip: `${namespace}.option.3.tooltip`,
secondOptionPrompt: `${namespace}.second_option_prompt`,
selected: [
{
text: `${namespace}.option.selected`,
},
],
})
.withPreOptionPhase(async (scene: BattleScene): Promise<boolean> => {
const encounter = scene.currentBattle.mysteryEncounter!;
@ -253,44 +161,8 @@ export const FieldTripEncounter: MysteryEncounter =
label: move.getName(),
handler: () => {
// Pokemon and move selected
const correctMove = move.getMove().category === MoveCategory.STATUS;
encounter.setDialogueToken("moveCategory", "Status");
if (!correctMove) {
encounter.options[2].dialogue!.selected = [
{
text: `${namespace}.option.incorrect`,
speaker: `${namespace}.speaker`,
},
{
text: `${namespace}.option.lesson_learned`,
},
];
encounter.dialogue.outro = [
{
text: `${namespace}.outro_bad`,
speaker: `${namespace}.speaker`,
},
];
setEncounterExp(scene, scene.getParty().map((p) => p.id), 50);
} else {
encounter.setDialogueToken("pokeName", pokemon.getNameToRender());
encounter.setDialogueToken("move", move.getName());
encounter.options[2].dialogue!.selected = [
{
text: `${namespace}.option.selected`,
},
];
encounter.dialogue.outro = [
{
text: `${namespace}.outro_good`,
speaker: `${namespace}.speaker`,
},
];
setEncounterExp(scene, [pokemon.id], 100);
}
encounter.misc = {
correctMove: correctMove,
};
encounter.setDialogueToken("moveCategory", i18next.t(`${namespace}.status`));
pokemonAndMoveChosen(scene, pokemon, move, MoveCategory.STATUS);
return true;
},
};
@ -318,3 +190,42 @@ export const FieldTripEncounter: MysteryEncounter =
.build()
)
.build();
function pokemonAndMoveChosen(scene: BattleScene, pokemon: PlayerPokemon, move: PokemonMove, correctMoveCategory: MoveCategory) {
const encounter = scene.currentBattle.mysteryEncounter!;
const correctMove = move.getMove().category === correctMoveCategory;
if (!correctMove) {
encounter.selectedOption!.dialogue!.selected = [
{
text: `${namespace}.option.selected`,
},
{
text: `${namespace}.incorrect`,
speaker: `${namespace}.speaker`,
},
{
text: `${namespace}.incorrect_exp`,
},
];
setEncounterExp(scene, scene.getParty().map((p) => p.id), 50);
} else {
encounter.setDialogueToken("pokeName", pokemon.getNameToRender());
encounter.setDialogueToken("move", move.getName());
encounter.selectedOption!.dialogue!.selected = [
{
text: `${namespace}.option.selected`,
},
{
text: `${namespace}.correct`,
speaker: `${namespace}.speaker`,
},
{
text: `${namespace}.correct_exp`,
},
];
setEncounterExp(scene, [pokemon.id], 100);
}
encounter.misc = {
correctMove: correctMove,
};
}

View File

@ -83,6 +83,9 @@ export const TheStrongStuffEncounter: MysteryEncounter =
{
modifier: generateModifierType(scene, modifierTypes.BERRY, [BerryType.SITRUS]) as PokemonHeldItemModifierType
},
{
modifier: generateModifierType(scene, modifierTypes.BERRY, [BerryType.ENIGMA]) as PokemonHeldItemModifierType
},
{
modifier: generateModifierType(scene, modifierTypes.BERRY, [BerryType.APICOT]) as PokemonHeldItemModifierType
},

View File

@ -224,6 +224,10 @@ export default class MysteryEncounter implements IMysteryEncounter {
* Defaults to 1
*/
expMultiplier: number;
/**
* Can add any asset load promises here during onInit() to make sure the scene awaits the loads properly
*/
loadAssets: Promise<void>[];
/**
* Generic property to set any custom data required for the encounter
* Extremely useful for carrying state/data between onPreOptionPhase/onOptionPhase/onPostOptionPhase
@ -260,6 +264,7 @@ export default class MysteryEncounter implements IMysteryEncounter {
this.introVisuals = undefined;
this.misc = null;
this.expMultiplier = 1;
this.loadAssets = [];
}
/**

View File

@ -4,7 +4,6 @@ import MysteryEncounterOption from "#app/data/mystery-encounters/mystery-encount
import { AVERAGE_ENCOUNTERS_PER_RUN_TARGET, WEIGHT_INCREMENT_ON_SPAWN_MISS } from "#app/data/mystery-encounters/mystery-encounters";
import { showEncounterText } from "#app/data/mystery-encounters/utils/encounter-dialogue-utils";
import Pokemon, { FieldPosition, PlayerPokemon, PokemonMove, PokemonSummonData } from "#app/field/pokemon";
import { ExpBalanceModifier, ExpShareModifier, MultipleParticipantExpBonusModifier, PokemonExpBoosterModifier } from "#app/modifier/modifier";
import { CustomModifierSettings, ModifierPoolType, ModifierType, ModifierTypeGenerator, ModifierTypeOption, modifierTypes, regenerateModifierPoolThresholds } from "#app/modifier/modifier-type";
import { MysteryEncounterBattlePhase, MysteryEncounterBattleStartCleanupPhase, MysteryEncounterPhase, MysteryEncounterRewardsPhase } from "#app/phases/mystery-encounter-phases";
import PokemonData from "#app/system/pokemon-data";
@ -27,7 +26,6 @@ import { MysteryEncounterMode } from "#enums/mystery-encounter-mode";
import { Status, StatusEffect } from "#app/data/status-effect";
import { TrainerConfig, trainerConfigs, TrainerSlot } from "#app/data/trainer-config";
import PokemonSpecies from "#app/data/pokemon-species";
import Overrides from "#app/overrides";
import { Egg, IEggOptions } from "#app/data/egg";
import { MysteryEncounterPokemonData } from "#app/data/mystery-encounters/mystery-encounter-pokemon-data";
import HeldModifierConfig from "#app/interfaces/held-modifier-config";
@ -36,9 +34,8 @@ import { EggLapsePhase } from "#app/phases/egg-lapse-phase";
import { TrainerVictoryPhase } from "#app/phases/trainer-victory-phase";
import { BattleEndPhase } from "#app/phases/battle-end-phase";
import { GameOverPhase } from "#app/phases/game-over-phase";
import { ExpPhase } from "#app/phases/exp-phase";
import { ShowPartyExpBarPhase } from "#app/phases/show-party-exp-bar-phase";
import { SelectModifierPhase } from "#app/phases/select-modifier-phase";
import { PartyExpPhase } from "#app/phases/party-exp-phase";
/**
* Animates exclamation sprite over trainer's head at start of encounter
@ -146,7 +143,9 @@ export async function initBattleWithEnemyConfig(scene: BattleScene, partyConfig:
battle.enemyLevels = new Array(numEnemies).fill(null).map(() => scene.currentBattle.getLevelForWave());
}
scene.getEnemyParty().forEach(enemyPokemon => enemyPokemon.destroy());
scene.getEnemyParty().forEach(enemyPokemon => {
scene.field.remove(enemyPokemon, true);
});
battle.enemyParty = [];
battle.double = doubleBattle;
@ -635,90 +634,11 @@ export function setEncounterRewards(scene: BattleScene, customShopRewards?: Cust
* https://bulbapedia.bulbagarden.net/wiki/List_of_Pok%C3%A9mon_by_effort_value_yield_(Generation_IX)
* @param useWaveIndex - set to false when directly passing the the full exp value instead of baseExpValue
*/
export function setEncounterExp(scene: BattleScene, participantId: integer | integer[], baseExpValue: number, useWaveIndex: boolean = true) {
export function setEncounterExp(scene: BattleScene, participantId: number | number[], baseExpValue: number, useWaveIndex: boolean = true) {
const participantIds = Array.isArray(participantId) ? participantId : [participantId];
scene.currentBattle.mysteryEncounter!.doEncounterExp = (scene: BattleScene) => {
const party = scene.getParty();
const expShareModifier = scene.findModifier(m => m instanceof ExpShareModifier) as ExpShareModifier;
const expBalanceModifier = scene.findModifier(m => m instanceof ExpBalanceModifier) as ExpBalanceModifier;
const multipleParticipantExpBonusModifier = scene.findModifier(m => m instanceof MultipleParticipantExpBonusModifier) as MultipleParticipantExpBonusModifier;
const nonFaintedPartyMembers = party.filter(p => p.hp);
const expPartyMembers = nonFaintedPartyMembers.filter(p => p.level < scene.getMaxExpLevel());
const partyMemberExp: number[] = [];
// EXP value calculation is based off Pokemon.getExpValue
let expValue = Math.floor(baseExpValue * (useWaveIndex ? scene.currentBattle.waveIndex : 1) / 5 + 1);
if (participantIds?.length > 0) {
if (scene.currentBattle.mysteryEncounter!.encounterMode === MysteryEncounterMode.TRAINER_BATTLE) {
expValue = Math.floor(expValue * 1.5);
}
for (const partyMember of nonFaintedPartyMembers) {
const pId = partyMember.id;
const participated = participantIds.includes(pId);
if (participated) {
partyMember.addFriendship(2);
}
if (!expPartyMembers.includes(partyMember)) {
continue;
}
if (!participated && !expShareModifier) {
partyMemberExp.push(0);
continue;
}
let expMultiplier = 0;
if (participated) {
expMultiplier += (1 / participantIds.length);
if (participantIds.length > 1 && multipleParticipantExpBonusModifier) {
expMultiplier += multipleParticipantExpBonusModifier.getStackCount() * 0.2;
}
} else if (expShareModifier) {
expMultiplier += (expShareModifier.getStackCount() * 0.2) / participantIds.length;
}
if (partyMember.pokerus) {
expMultiplier *= 1.5;
}
if (Overrides.XP_MULTIPLIER_OVERRIDE !== null) {
expMultiplier = Overrides.XP_MULTIPLIER_OVERRIDE;
}
const pokemonExp = new Utils.NumberHolder(expValue * expMultiplier);
scene.applyModifiers(PokemonExpBoosterModifier, true, partyMember, pokemonExp);
partyMemberExp.push(Math.floor(pokemonExp.value));
}
if (expBalanceModifier) {
let totalLevel = 0;
let totalExp = 0;
expPartyMembers.forEach((expPartyMember, epm) => {
totalExp += partyMemberExp[epm];
totalLevel += expPartyMember.level;
});
const medianLevel = Math.floor(totalLevel / expPartyMembers.length);
const recipientExpPartyMemberIndexes: number[] = [];
expPartyMembers.forEach((expPartyMember, epm) => {
if (expPartyMember.level <= medianLevel) {
recipientExpPartyMemberIndexes.push(epm);
}
});
const splitExp = Math.floor(totalExp / recipientExpPartyMemberIndexes.length);
expPartyMembers.forEach((_partyMember, pm) => {
partyMemberExp[pm] = Phaser.Math.Linear(partyMemberExp[pm], recipientExpPartyMemberIndexes.indexOf(pm) > -1 ? splitExp : 0, 0.2 * expBalanceModifier.getStackCount());
});
}
for (let pm = 0; pm < expPartyMembers.length; pm++) {
const exp = partyMemberExp[pm];
if (exp) {
const partyMemberIndex = party.indexOf(expPartyMembers[pm]);
scene.unshiftPhase(expPartyMembers[pm].isOnField() ? new ExpPhase(scene, partyMemberIndex, exp) : new ShowPartyExpBarPhase(scene, partyMemberIndex, exp));
}
}
}
scene.unshiftPhase(new PartyExpPhase(scene, baseExpValue, useWaveIndex, new Set(participantIds)));
return true;
};

View File

@ -7,7 +7,6 @@ import { doPokeballBounceAnim, getPokeballAtlasKey, getPokeballCatchMultiplier,
import { PlayerGender } from "#enums/player-gender";
import { addPokeballCaptureStars, addPokeballOpenParticles } from "#app/field/anims";
import { getStatusEffectCatchRateMultiplier, StatusEffect } from "#app/data/status-effect";
import { BattlerIndex } from "#app/battle";
import { achvs } from "#app/system/achv";
import { Mode } from "#app/ui/ui";
import { PartyOption, PartyUiMode } from "#app/ui/party-ui-handler";
@ -482,7 +481,7 @@ function failCatch(scene: BattleScene, pokemon: EnemyPokemon, originalY: number,
* @param isObtain
*/
export async function catchPokemon(scene: BattleScene, pokemon: EnemyPokemon, pokeball: Phaser.GameObjects.Sprite | null, pokeballType: PokeballType, showCatchObtainMessage: boolean = true, isObtain: boolean = false): Promise<void> {
scene.unshiftPhase(new VictoryPhase(scene, BattlerIndex.ENEMY, true));
scene.unshiftPhase(new VictoryPhase(scene, pokemon.id, true));
const speciesForm = !pokemon.fusionSpecies ? pokemon.getSpeciesForm() : pokemon.getFusionSpeciesForm();

View File

@ -18,11 +18,14 @@
"label": "A Status Move",
"tooltip": "(+) Status Item Rewards"
},
"selected": "{{pokeName}} shows off an awesome display of {{move}}!",
"incorrect": "...$That isn't a {{moveCategory}} move!$I'm sorry, but I can't give you anything.",
"lesson_learned": "Looks like you learned a valuable lesson?$Your Pokémon also gained some knowledge."
"selected": "{{pokeName}} shows off an awesome display of {{move}}!"
},
"second_option_prompt": "Choose a move for your Pokémon to use.",
"outro_good": "Thank you so much for your kindness!\nI hope the items I had were helpful!",
"outro_bad": "Come along children, we'll\nfind a better demonstration elsewhere."
"incorrect": "...$That isn't a {{moveCategory}} move!\nI'm sorry, but I can't give you anything.$Come along children, we'll\nfind a better demonstration elsewhere.",
"incorrect_exp": "Looks like you learned a valuable lesson?$Your Pokémon also gained some experience.",
"correct": "Thank you so much for your kindness!\nI hope these items might be of use to you!",
"correct_exp": "{{pokeName}} also gained some valuable experience!",
"status": "Status",
"physical": "Physical",
"special": "Special"
}

View File

@ -87,7 +87,11 @@ export class EncounterPhase extends BattlePhase {
let totalBst = 0;
battle.enemyLevels?.forEach((level, e) => {
battle.enemyLevels?.every((level, e) => {
if (battle.battleType === BattleType.MYSTERY_ENCOUNTER) {
// Skip enemy loading for MEs, those are loaded elsewhere
return false;
}
if (!this.loaded) {
if (battle.battleType === BattleType.TRAINER) {
battle.enemyParty[e] = battle.trainer?.genPartyMember(e)!; // TODO:: is the bang correct here?
@ -138,6 +142,7 @@ export class EncounterPhase extends BattlePhase {
loadEnemyAssets.push(enemyPokemon.loadAssets());
console.log(getPokemonNameWithAffix(enemyPokemon), enemyPokemon.species.speciesId, enemyPokemon.stats);
return true;
});
if (this.scene.getParty().filter(p => p.isShiny()).length === 6) {
@ -151,7 +156,12 @@ export class EncounterPhase extends BattlePhase {
const newEncounter = this.scene.getMysteryEncounter(mysteryEncounter);
battle.mysteryEncounter = newEncounter;
}
loadEnemyAssets.push(battle.mysteryEncounter.introVisuals!.loadAssets().then(() => battle.mysteryEncounter!.introVisuals!.initSprite()));
if (battle.mysteryEncounter.introVisuals) {
loadEnemyAssets.push(battle.mysteryEncounter.introVisuals.loadAssets().then(() => battle.mysteryEncounter!.introVisuals!.initSprite()));
}
if (battle.mysteryEncounter.loadAssets.length > 0) {
loadEnemyAssets.push(...battle.mysteryEncounter.loadAssets);
}
// Load Mystery Encounter Exclamation bubble and sfx
loadEnemyAssets.push(new Promise<void>(resolve => {
this.scene.loadSe("GEN8- Exclaim", "battle_anims", "GEN8- Exclaim.wav");
@ -176,7 +186,10 @@ export class EncounterPhase extends BattlePhase {
}
Promise.all(loadEnemyAssets).then(() => {
battle.enemyParty.forEach((enemyPokemon, e) => {
battle.enemyParty.every((enemyPokemon, e) => {
if (battle.battleType === BattleType.MYSTERY_ENCOUNTER) {
return false;
}
if (e < (battle.double ? 2 : 1)) {
if (battle.battleType === BattleType.WILD) {
this.scene.field.add(enemyPokemon);
@ -189,16 +202,15 @@ export class EncounterPhase extends BattlePhase {
} else if (battle.battleType === BattleType.TRAINER) {
enemyPokemon.setVisible(false);
this.scene.currentBattle.trainer?.tint(0, 0.5);
} else if (battle.battleType === BattleType.MYSTERY_ENCOUNTER) {
// TODO: this may not be necessary, but leaving as placeholder
}
if (battle.double) {
enemyPokemon.setFieldPosition(e ? FieldPosition.RIGHT : FieldPosition.LEFT);
}
}
return true;
});
if (!this.loaded) {
if (!this.loaded && battle.battleType !== BattleType.MYSTERY_ENCOUNTER) {
regenerateModifierPoolThresholds(this.scene.getEnemyField(), battle.battleType === BattleType.TRAINER ? ModifierPoolType.TRAINER : ModifierPoolType.WILD);
this.scene.generateEnemyModifiers();
}

View File

@ -3,17 +3,21 @@ import { Phase } from "#app/phase";
export class PartyExpPhase extends Phase {
expValue: number;
useWaveIndexMultiplier?: boolean;
pokemonParticipantIds?: Set<number>;
constructor(scene: BattleScene, expValue: number) {
constructor(scene: BattleScene, expValue: number, useWaveIndexMultiplier?: boolean, pokemonParticipantIds?: Set<number>) {
super(scene);
this.expValue = expValue;
this.useWaveIndexMultiplier = useWaveIndexMultiplier;
this.pokemonParticipantIds = pokemonParticipantIds;
}
start() {
super.start();
this.scene.applyPartyExp(this.expValue);
this.scene.applyPartyExp(this.expValue, false, this.useWaveIndexMultiplier, this.pokemonParticipantIds);
this.end();
}

View File

@ -16,7 +16,7 @@ export class VictoryPhase extends PokemonPhase {
/** If true, indicates that the phase is intended for EXP purposes only, and not to continue a battle to next phase */
isExpOnly: boolean;
constructor(scene: BattleScene, battlerIndex: BattlerIndex, isExpOnly: boolean = false) {
constructor(scene: BattleScene, battlerIndex: BattlerIndex | integer, isExpOnly: boolean = false) {
super(scene, battlerIndex);
this.isExpOnly = isExpOnly;
@ -28,7 +28,7 @@ export class VictoryPhase extends PokemonPhase {
this.scene.gameData.gameStats.pokemonDefeated++;
const expValue = this.getPokemon().getExpValue();
this.scene.applyPartyExp(expValue);
this.scene.applyPartyExp(expValue, true);
if (this.scene.currentBattle.battleType === BattleType.MYSTERY_ENCOUNTER) {
handleMysteryEncounterVictory(this.scene, false, this.isExpOnly);

View File

@ -0,0 +1,234 @@
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 * as EncounterPhaseUtils from "#app/data/mystery-encounters/utils/encounter-phase-utils";
import { runMysteryEncounterToEnd } from "#test/mystery-encounter/encounter-test-utils";
import BattleScene from "#app/battle-scene";
import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode";
import { MysteryEncounterTier } from "#enums/mystery-encounter-tier";
import * as MysteryEncounters from "#app/data/mystery-encounters/mystery-encounters";
import { FieldTripEncounter } from "#app/data/mystery-encounters/encounters/field-trip-encounter";
import { Moves } from "#enums/moves";
import { SelectModifierPhase } from "#app/phases/select-modifier-phase";
import { Mode } from "#app/ui/ui";
import ModifierSelectUiHandler from "#app/ui/modifier-select-ui-handler";
const namespace = "mysteryEncounter:fieldTrip";
const defaultParty = [Species.LAPRAS, Species.GENGAR, Species.ABRA];
const defaultBiome = Biome.CAVE;
const defaultWave = 45;
describe("Field Trip - 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();
game.override.moveset([Moves.TACKLE, Moves.UPROAR, Moves.SWORDS_DANCE]);
vi.spyOn(MysteryEncounters, "mysteryEncountersByBiome", "get").mockReturnValue(
new Map<Biome, MysteryEncounterType[]>([
[Biome.CAVE, [MysteryEncounterType.FIELD_TRIP]],
])
);
});
afterEach(() => {
game.phaseInterceptor.restoreOg();
vi.clearAllMocks();
vi.resetAllMocks();
});
it("should have the correct properties", async () => {
await game.runToMysteryEncounter(MysteryEncounterType.FIELD_TRIP, defaultParty);
expect(FieldTripEncounter.encounterType).toBe(MysteryEncounterType.FIELD_TRIP);
expect(FieldTripEncounter.encounterTier).toBe(MysteryEncounterTier.COMMON);
expect(FieldTripEncounter.dialogue).toBeDefined();
expect(FieldTripEncounter.dialogue.intro).toStrictEqual([
{
text: `${namespace}.intro`
},
{
speaker: `${namespace}.speaker`,
text: `${namespace}.intro_dialogue`
}
]);
expect(FieldTripEncounter.dialogue.encounterOptionsDialogue?.title).toBe(`${namespace}.title`);
expect(FieldTripEncounter.dialogue.encounterOptionsDialogue?.description).toBe(`${namespace}.description`);
expect(FieldTripEncounter.dialogue.encounterOptionsDialogue?.query).toBe(`${namespace}.query`);
expect(FieldTripEncounter.options.length).toBe(3);
});
it("should not run below wave 10", async () => {
game.override.startingWave(9);
await game.runToMysteryEncounter();
expect(scene.currentBattle?.mysteryEncounter?.encounterType).not.toBe(MysteryEncounterType.FIELD_TRIP);
});
it("should not run above wave 179", async () => {
game.override.startingWave(181);
await game.runToMysteryEncounter();
expect(scene.currentBattle.mysteryEncounter).toBeUndefined();
});
describe("Option 1 - Show off a physical move", () => {
it("should have the correct properties", () => {
const option = FieldTripEncounter.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`,
secondOptionPrompt: `${namespace}.second_option_prompt`,
});
});
it("Should give no reward on incorrect option", async () => {
await game.runToMysteryEncounter(MysteryEncounterType.FIELD_TRIP, defaultParty);
await runMysteryEncounterToEnd(game, 1, { pokemonNo: 1, optionNo: 2 });
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(0);
});
it("Should give proper rewards on correct Physical move option", async () => {
await game.runToMysteryEncounter(MysteryEncounterType.FIELD_TRIP, defaultParty);
await runMysteryEncounterToEnd(game, 1, { pokemonNo: 1, optionNo: 1 });
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.name).toBe("X Attack");
expect(modifierSelectHandler.options[1].modifierTypeOption.type.name).toBe("X Defense");
expect(modifierSelectHandler.options[2].modifierTypeOption.type.name).toBe("X Speed");
expect(modifierSelectHandler.options[3].modifierTypeOption.type.name).toBe("Dire Hit");
});
it("should leave encounter without battle", async () => {
const leaveEncounterWithoutBattleSpy = vi.spyOn(EncounterPhaseUtils, "leaveEncounterWithoutBattle");
await game.runToMysteryEncounter(MysteryEncounterType.FIELD_TRIP, defaultParty);
await runMysteryEncounterToEnd(game, 1, { pokemonNo: 1, optionNo: 1 });
expect(leaveEncounterWithoutBattleSpy).toBeCalled();
});
});
describe("Option 2 - Give Food", () => {
it("should have the correct properties", () => {
const option = FieldTripEncounter.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`,
secondOptionPrompt: `${namespace}.second_option_prompt`,
});
});
it("Should give no reward on incorrect option", async () => {
await game.runToMysteryEncounter(MysteryEncounterType.FIELD_TRIP, defaultParty);
await runMysteryEncounterToEnd(game, 2, { pokemonNo: 1, optionNo: 1 });
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(0);
});
it("Should give proper rewards on correct Special move option", async () => {
await game.runToMysteryEncounter(MysteryEncounterType.FIELD_TRIP, defaultParty);
await runMysteryEncounterToEnd(game, 2, { pokemonNo: 1, optionNo: 2 });
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.name).toBe("X Sp. Atk");
expect(modifierSelectHandler.options[1].modifierTypeOption.type.name).toBe("X Sp. Def");
expect(modifierSelectHandler.options[2].modifierTypeOption.type.name).toBe("X Speed");
expect(modifierSelectHandler.options[3].modifierTypeOption.type.name).toBe("Dire Hit");
});
it("should leave encounter without battle", async () => {
const leaveEncounterWithoutBattleSpy = vi.spyOn(EncounterPhaseUtils, "leaveEncounterWithoutBattle");
await game.runToMysteryEncounter(MysteryEncounterType.FIELD_TRIP, defaultParty);
await runMysteryEncounterToEnd(game, 2, { pokemonNo: 1, optionNo: 2 });
expect(leaveEncounterWithoutBattleSpy).toBeCalled();
});
});
describe("Option 3 - Give Item", () => {
it("should have the correct properties", () => {
const option = FieldTripEncounter.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`,
secondOptionPrompt: `${namespace}.second_option_prompt`,
});
});
it("Should give no reward on incorrect option", async () => {
await game.runToMysteryEncounter(MysteryEncounterType.FIELD_TRIP, defaultParty);
await runMysteryEncounterToEnd(game, 3, { pokemonNo: 1, optionNo: 1 });
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(0);
});
it("Should give proper rewards on correct Special move option", async () => {
await game.runToMysteryEncounter(MysteryEncounterType.FIELD_TRIP, defaultParty);
await runMysteryEncounterToEnd(game, 3, { pokemonNo: 1, optionNo: 3 });
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.name).toBe("X Accuracy");
expect(modifierSelectHandler.options[1].modifierTypeOption.type.name).toBe("X Speed");
expect(modifierSelectHandler.options[2].modifierTypeOption.type.name).toBe("5x Great Ball");
expect(modifierSelectHandler.options[3].modifierTypeOption.type.name).toBe("IV Scanner");
});
it("should leave encounter without battle", async () => {
const leaveEncounterWithoutBattleSpy = vi.spyOn(EncounterPhaseUtils, "leaveEncounterWithoutBattle");
await game.runToMysteryEncounter(MysteryEncounterType.FIELD_TRIP, defaultParty);
await runMysteryEncounterToEnd(game, 2, { pokemonNo: 1, optionNo: 3 });
expect(leaveEncounterWithoutBattleSpy).toBeCalled();
});
});
});

View File

@ -208,8 +208,9 @@ describe("The Strong Stuff - Mystery Encounter", () => {
expect(enemyField[0].species.speciesId).toBe(Species.SHUCKLE);
expect(enemyField[0].summonData.statStages).toEqual([0, 2, 0, 2, 0, 0, 0]);
const shuckleItems = enemyField[0].getHeldItems();
expect(shuckleItems.length).toBe(4);
expect(shuckleItems.length).toBe(5);
expect(shuckleItems.find(m => m instanceof BerryModifier && m.berryType === BerryType.SITRUS)?.stackCount).toBe(1);
expect(shuckleItems.find(m => m instanceof BerryModifier && m.berryType === BerryType.ENIGMA)?.stackCount).toBe(1);
expect(shuckleItems.find(m => m instanceof BerryModifier && m.berryType === BerryType.GANLON)?.stackCount).toBe(1);
expect(shuckleItems.find(m => m instanceof BerryModifier && m.berryType === BerryType.APICOT)?.stackCount).toBe(1);
expect(shuckleItems.find(m => m instanceof BerryModifier && m.berryType === BerryType.LUM)?.stackCount).toBe(2);