diff --git a/src/battle-scene.ts b/src/battle-scene.ts index 6ac6793138d..5a89ab1987f 100644 --- a/src/battle-scene.ts +++ b/src/battle-scene.ts @@ -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): 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)) { diff --git a/src/data/mystery-encounters/encounters/dancing-lessons-encounter.ts b/src/data/mystery-encounters/encounters/dancing-lessons-encounter.ts index b3429041cc1..314255fe383 100644 --- a/src/data/mystery-encounters/encounters/dancing-lessons-encounter.ts +++ b/src/data/mystery-encounters/encounters/dancing-lessons-encounter.ts @@ -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(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(); + } + }); + }); +} diff --git a/src/data/mystery-encounters/encounters/delibirdy-encounter.ts b/src/data/mystery-encounters/encounters/delibirdy-encounter.ts index 493f5364e0b..c4227f08f90 100644 --- a/src/data/mystery-encounters/encounters/delibirdy-encounter.ts +++ b/src/data/mystery-encounters/encounters/delibirdy-encounter.ts @@ -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`, diff --git a/src/data/mystery-encounters/encounters/field-trip-encounter.ts b/src/data/mystery-encounters/encounters/field-trip-encounter.ts index 794caf36f93..ce6e3382158 100644 --- a/src/data/mystery-encounters/encounters/field-trip-encounter.ts +++ b/src/data/mystery-encounters/encounters/field-trip-encounter.ts @@ -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 => { 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 => { 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 => { 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, + }; +} diff --git a/src/data/mystery-encounters/encounters/the-strong-stuff-encounter.ts b/src/data/mystery-encounters/encounters/the-strong-stuff-encounter.ts index 006ca4535cb..e7a4f42f65a 100644 --- a/src/data/mystery-encounters/encounters/the-strong-stuff-encounter.ts +++ b/src/data/mystery-encounters/encounters/the-strong-stuff-encounter.ts @@ -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 }, diff --git a/src/data/mystery-encounters/mystery-encounter.ts b/src/data/mystery-encounters/mystery-encounter.ts index bd3ff77cbb3..4d6fb4310ee 100644 --- a/src/data/mystery-encounters/mystery-encounter.ts +++ b/src/data/mystery-encounters/mystery-encounter.ts @@ -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[]; /** * 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 = []; } /** diff --git a/src/data/mystery-encounters/utils/encounter-phase-utils.ts b/src/data/mystery-encounters/utils/encounter-phase-utils.ts index d1877482857..197a75d3b4b 100644 --- a/src/data/mystery-encounters/utils/encounter-phase-utils.ts +++ b/src/data/mystery-encounters/utils/encounter-phase-utils.ts @@ -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; }; diff --git a/src/data/mystery-encounters/utils/encounter-pokemon-utils.ts b/src/data/mystery-encounters/utils/encounter-pokemon-utils.ts index 2a408c9742f..f73c25ddaf7 100644 --- a/src/data/mystery-encounters/utils/encounter-pokemon-utils.ts +++ b/src/data/mystery-encounters/utils/encounter-pokemon-utils.ts @@ -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 { - scene.unshiftPhase(new VictoryPhase(scene, BattlerIndex.ENEMY, true)); + scene.unshiftPhase(new VictoryPhase(scene, pokemon.id, true)); const speciesForm = !pokemon.fusionSpecies ? pokemon.getSpeciesForm() : pokemon.getFusionSpeciesForm(); diff --git a/src/locales/en/mystery-encounters/field-trip-dialogue.json b/src/locales/en/mystery-encounters/field-trip-dialogue.json index d688fe7af97..61900d56cd7 100644 --- a/src/locales/en/mystery-encounters/field-trip-dialogue.json +++ b/src/locales/en/mystery-encounters/field-trip-dialogue.json @@ -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" } \ No newline at end of file diff --git a/src/phases/encounter-phase.ts b/src/phases/encounter-phase.ts index 1a4e4fdf979..61613c9adc5 100644 --- a/src/phases/encounter-phase.ts +++ b/src/phases/encounter-phase.ts @@ -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(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(); } diff --git a/src/phases/party-exp-phase.ts b/src/phases/party-exp-phase.ts index b5d85b187c1..9f7295ea825 100644 --- a/src/phases/party-exp-phase.ts +++ b/src/phases/party-exp-phase.ts @@ -3,17 +3,21 @@ import { Phase } from "#app/phase"; export class PartyExpPhase extends Phase { expValue: number; + useWaveIndexMultiplier?: boolean; + pokemonParticipantIds?: Set; - constructor(scene: BattleScene, expValue: number) { + constructor(scene: BattleScene, expValue: number, useWaveIndexMultiplier?: boolean, pokemonParticipantIds?: Set) { 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(); } diff --git a/src/phases/victory-phase.ts b/src/phases/victory-phase.ts index 85bdca71171..16f057f0faa 100644 --- a/src/phases/victory-phase.ts +++ b/src/phases/victory-phase.ts @@ -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); diff --git a/src/test/mystery-encounter/encounters/field-trip-encounter.test.ts b/src/test/mystery-encounter/encounters/field-trip-encounter.test.ts new file mode 100644 index 00000000000..2f13105047d --- /dev/null +++ b/src/test/mystery-encounter/encounters/field-trip-encounter.test.ts @@ -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.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(); + }); + }); +}); diff --git a/src/test/mystery-encounter/encounters/the-strong-stuff-encounter.test.ts b/src/test/mystery-encounter/encounters/the-strong-stuff-encounter.test.ts index 0600005aa52..cd3902fd6b1 100644 --- a/src/test/mystery-encounter/encounters/the-strong-stuff-encounter.test.ts +++ b/src/test/mystery-encounter/encounters/the-strong-stuff-encounter.test.ts @@ -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);