diff --git a/public/images/mystery-encounters/encounter_radar.json b/public/images/mystery-encounters/encounter_radar.json new file mode 100644 index 00000000000..82f16af59f6 --- /dev/null +++ b/public/images/mystery-encounters/encounter_radar.json @@ -0,0 +1,41 @@ +{ + "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$" + } +} diff --git a/public/images/mystery-encounters/encounter_radar.png b/public/images/mystery-encounters/encounter_radar.png new file mode 100644 index 00000000000..deb9426c269 Binary files /dev/null and b/public/images/mystery-encounters/encounter_radar.png differ diff --git a/src/battle-scene.ts b/src/battle-scene.ts index 4c0f31b051e..a017b8eb49e 100644 --- a/src/battle-scene.ts +++ b/src/battle-scene.ts @@ -67,7 +67,7 @@ import { UiTheme } from "#enums/ui-theme"; import { TimedEventManager } from "#app/timed-event-manager.js"; import i18next from "i18next"; import IMysteryEncounter, { MysteryEncounterTier, MysteryEncounterVariant } from "./data/mystery-encounters/mystery-encounter"; -import { allMysteryEncounters, AVERAGE_ENCOUNTERS_PER_RUN_TARGET, BASE_MYSTERY_ENCOUNTER_SPAWN_WEIGHT, mysteryEncountersByBiome, WIGHT_INCREMENT_ON_SPAWN_MISS } from "./data/mystery-encounters/mystery-encounters"; +import { allMysteryEncounters, AVERAGE_ENCOUNTERS_PER_RUN_TARGET, BASE_MYSTERY_ENCOUNTER_SPAWN_WEIGHT, mysteryEncountersByBiome, WEIGHT_INCREMENT_ON_SPAWN_MISS } from "./data/mystery-encounters/mystery-encounters"; import { MysteryEncounterData } from "#app/data/mystery-encounters/mystery-encounter-data"; import { MysteryEncounterType } from "#enums/mystery-encounter-type"; @@ -1074,7 +1074,7 @@ export default class BattleScene extends SceneBase { // let testStartingWeight = 0; // while (testStartingWeight < 3) { // calculateMEAggregateStats(this, testStartingWeight); - // testStartingWeight += 1; + // testStartingWeight += 2; // } // Check for mystery encounter // Can only occur in place of a standard wild battle, waves 10-180 @@ -1098,7 +1098,7 @@ export default class BattleScene extends SceneBase { // Reset base spawn weight this.mysteryEncounterData.encounterSpawnChance = BASE_MYSTERY_ENCOUNTER_SPAWN_WEIGHT; } else { - this.mysteryEncounterData.encounterSpawnChance = sessionEncounterRate + WIGHT_INCREMENT_ON_SPAWN_MISS; + this.mysteryEncounterData.encounterSpawnChance = sessionEncounterRate + WEIGHT_INCREMENT_ON_SPAWN_MISS; } } } diff --git a/src/data/mystery-encounters/encounters/field-trip-encounter.ts b/src/data/mystery-encounters/encounters/field-trip-encounter.ts index 21db15ac7e0..525e2b91f8b 100644 --- a/src/data/mystery-encounters/encounters/field-trip-encounter.ts +++ b/src/data/mystery-encounters/encounters/field-trip-encounter.ts @@ -87,11 +87,17 @@ export const FieldTripEncounter: IMysteryEncounter = if (!correctMove) { encounter.options[0].dialogue.selected = [ { - text: `${namespace}:incorrect`, - speaker: `${namespace}:speaker`, + text: `${namespace}:option:incorrect`, + speaker: `${namespace}:option:speaker`, }, { - text: `${namespace}:lesson_learned`, + text: `${namespace}:option:lesson_learned`, + }, + ]; + encounter.dialogue.outro = [ + { + text: `${namespace}:outro_bad`, + speaker: `${namespace}:speaker`, }, ]; setEncounterExp(scene, scene.getParty().map((p) => p.id), 50); @@ -103,6 +109,12 @@ export const FieldTripEncounter: IMysteryEncounter = text: `${namespace}:option:selected`, }, ]; + encounter.dialogue.outro = [ + { + text: `${namespace}:outro_good`, + speaker: `${namespace}:speaker`, + }, + ]; setEncounterExp(scene, [pokemon.id], 100); } encounter.misc = { @@ -161,11 +173,23 @@ export const FieldTripEncounter: IMysteryEncounter = if (!correctMove) { encounter.options[1].dialogue.selected = [ { - text: `${namespace}:incorrect`, - speaker: `${namespace}:speaker`, + text: `${namespace}:option:incorrect`, + speaker: `${namespace}:option:speaker`, }, { - text: `${namespace}:lesson_learned`, + 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); @@ -177,6 +201,12 @@ export const FieldTripEncounter: IMysteryEncounter = text: `${namespace}:option:selected`, }, ]; + encounter.dialogue.outro = [ + { + text: `${namespace}:outro_good`, + speaker: `${namespace}:speaker`, + }, + ]; setEncounterExp(scene, [pokemon.id], 100); } encounter.misc = { @@ -235,18 +265,20 @@ export const FieldTripEncounter: IMysteryEncounter = if (!correctMove) { encounter.options[2].dialogue.selected = [ { - text: `${namespace}:incorrect`, - speaker: `${namespace}:speaker`, + text: `${namespace}:option:incorrect`, + speaker: `${namespace}:option:speaker`, }, { - text: `${namespace}:lesson_learned`, + text: `${namespace}:option:lesson_learned`, }, ]; - setEncounterExp( - scene, - scene.getParty().map((p) => p.id), - 50 - ); + encounter.dialogue.outro = [ + { + text: `${namespace}:outro_bad`, + speaker: `${namespace}:speaker`, + }, + ]; + setEncounterExp(scene, scene.getParty().map((p) => p.id), 50); } else { encounter.setDialogueToken("pokeName", pokemon.name); encounter.setDialogueToken("move", move.getName()); @@ -255,6 +287,12 @@ export const FieldTripEncounter: IMysteryEncounter = text: `${namespace}:option:selected`, }, ]; + encounter.dialogue.outro = [ + { + text: `${namespace}:outro_good`, + speaker: `${namespace}:speaker`, + }, + ]; setEncounterExp(scene, [pokemon.id], 100); } encounter.misc = { diff --git a/src/data/mystery-encounters/encounters/fight-or-flight-encounter.ts b/src/data/mystery-encounters/encounters/fight-or-flight-encounter.ts index 7675b204b50..5e7d3704fc0 100644 --- a/src/data/mystery-encounters/encounters/fight-or-flight-encounter.ts +++ b/src/data/mystery-encounters/encounters/fight-or-flight-encounter.ts @@ -150,7 +150,7 @@ export const FightOrFlightEncounter: IMysteryEncounter = const primaryPokemon = encounter.options[1].primaryPokemon; if (primaryPokemon) { // Use primaryPokemon to execute the thievery - await showEncounterText(scene, `${namespace}:option:2:steal_result`); + await showEncounterText(scene, `${namespace}:option:2:special_result`); leaveEncounterWithoutBattle(scene); return; } diff --git a/src/data/mystery-encounters/encounters/mysterious-challengers-encounter.ts b/src/data/mystery-encounters/encounters/mysterious-challengers-encounter.ts index 382dc1f212e..f18d7655936 100644 --- a/src/data/mystery-encounters/encounters/mysterious-challengers-encounter.ts +++ b/src/data/mystery-encounters/encounters/mysterious-challengers-encounter.ts @@ -169,7 +169,7 @@ export const MysteriousChallengersEncounter: IMysteryEncounter = // Spawn hard fight with ULTRA/GREAT reward (can improve with luck) const config: EnemyPartyConfig = encounter.enemyPartyConfigs[1]; - setEncounterRewards(scene, { guaranteedModifierTiers: [ModifierTier.ULTRA, ModifierTier.GREAT, ModifierTier.GREAT], fillRemaining: true }); + setEncounterRewards(scene, { guaranteedModifierTiers: [ModifierTier.ULTRA, ModifierTier.ULTRA, ModifierTier.GREAT, ModifierTier.GREAT], fillRemaining: true }); // Seed offsets to remove possibility of different trainers having exact same teams let ret; @@ -197,7 +197,7 @@ export const MysteriousChallengersEncounter: IMysteryEncounter = // To avoid player level snowballing from picking this option encounter.expMultiplier = 0.9; - setEncounterRewards(scene, { guaranteedModifierTiers: [ModifierTier.ROGUE, ModifierTier.ULTRA, ModifierTier.GREAT], fillRemaining: true }); + setEncounterRewards(scene, { guaranteedModifierTiers: [ModifierTier.ROGUE, ModifierTier.ROGUE, ModifierTier.ULTRA, ModifierTier.GREAT], fillRemaining: true }); // Seed offsets to remove possibility of different trainers having exact same teams let ret; diff --git a/src/data/mystery-encounters/encounters/mysterious-chest-encounter.ts b/src/data/mystery-encounters/encounters/mysterious-chest-encounter.ts index 9c4377b22e5..13c5b9cc1e4 100644 --- a/src/data/mystery-encounters/encounters/mysterious-chest-encounter.ts +++ b/src/data/mystery-encounters/encounters/mysterious-chest-encounter.ts @@ -34,8 +34,8 @@ export const MysteriousChestEncounter: IMysteryEncounter = ]) .withIntroDialogue([ { - text: "${namespace}:intro", - }, + text: `${namespace}:intro`, + } ]) .withTitle(`${namespace}:title`) .withDescription(`${namespace}:description`) diff --git a/src/data/mystery-encounters/encounters/pokemon-salesman-encounter.ts b/src/data/mystery-encounters/encounters/pokemon-salesman-encounter.ts index f572f6cf76b..8ba7b08feaa 100644 --- a/src/data/mystery-encounters/encounters/pokemon-salesman-encounter.ts +++ b/src/data/mystery-encounters/encounters/pokemon-salesman-encounter.ts @@ -1,21 +1,23 @@ -import { generateModifierTypeOption, leaveEncounterWithoutBattle, selectPokemonForOption, setEncounterExp, updatePlayerMoney, } from "#app/data/mystery-encounters/utils/encounter-phase-utils"; -import { StatusEffect } from "#app/data/status-effect"; -import Pokemon, { PlayerPokemon } from "#app/field/pokemon"; -import { modifierTypes } from "#app/modifier/modifier-type"; +import { leaveEncounterWithoutBattle, transitionMysteryEncounterIntroVisuals, updatePlayerMoney, } from "#app/data/mystery-encounters/utils/encounter-phase-utils"; import { isNullOrUndefined, randSeedInt } from "#app/utils"; import { MysteryEncounterType } from "#enums/mystery-encounter-type"; import BattleScene from "../../../battle-scene"; import IMysteryEncounter, { MysteryEncounterBuilder, MysteryEncounterTier, } from "../mystery-encounter"; -import { EncounterOptionMode, MysteryEncounterOptionBuilder } from "../mystery-encounter-option"; import { MoneyRequirement } from "../mystery-encounter-requirements"; -import { getEncounterText, queueEncounterMessage } from "#app/data/mystery-encounters/utils/encounter-dialogue-utils"; -import { applyDamageToPokemon, getRandomSpeciesByStarterTier } from "#app/data/mystery-encounters/utils/encounter-pokemon-utils"; +import { catchPokemon, getRandomSpeciesByStarterTier } from "#app/data/mystery-encounters/utils/encounter-pokemon-utils"; import { getPokemonSpecies, speciesStarters } from "#app/data/pokemon-species"; import { Species } from "#enums/species"; +import { PokeballType } from "#app/data/pokeball"; +import { EnemyPokemon, PlayerPokemon } from "#app/field/pokemon"; +import { EncounterOptionMode, MysteryEncounterOptionBuilder } from "#app/data/mystery-encounters/mystery-encounter-option"; +import { showEncounterDialogue } from "#app/data/mystery-encounters/utils/encounter-dialogue-utils"; +import PokemonData from "#app/system/pokemon-data"; /** the i18n namespace for this encounter */ const namespace = "mysteryEncounter:pokemonSalesman"; +const MAX_POKEMON_PRICE_MULTIPLIER = 6; + /** * Pokemon Salesman encounter. * @see {@link https://github.com/AsdarDevelops/PokeRogue-Events/issues/36 | GitHub Issue #36} @@ -25,7 +27,8 @@ export const PokemonSalesmanEncounter: IMysteryEncounter = MysteryEncounterBuilder.withEncounterType(MysteryEncounterType.POKEMON_SALESMAN) .withEncounterTier(MysteryEncounterTier.ULTRA) .withSceneWaveRangeRequirement(10, 180) - .withSceneRequirement(new MoneyRequirement(null, 8)) // Some costs may not be as significant, this is the max you'd pay + .withSceneRequirement(new MoneyRequirement(null, MAX_POKEMON_PRICE_MULTIPLIER)) // Some costs may not be as significant, this is the max you'd pay + .withAutoHideIntroVisuals(false) .withIntroSpriteConfigs([ { spriteKey: "pokemon_salesman", @@ -56,8 +59,7 @@ export const PokemonSalesmanEncounter: IMysteryEncounter = species = getPokemonSpecies(getRandomSpeciesByStarterTier([0, 5])); } - - let pokemon; + let pokemon: PlayerPokemon; if (isNullOrUndefined(species.abilityHidden) || randSeedInt(100) === 0) { // If no HA mon found or you roll 1%, give shiny Magikarp species = getPokemonSpecies(Species.MAGIKARP); @@ -68,219 +70,82 @@ export const PokemonSalesmanEncounter: IMysteryEncounter = pokemon = scene.addPlayerPokemon(species, 5, hiddenIndex, species.formIndex); } + const spriteKey = pokemon.getSpriteId(); + const spriteRoot = pokemon.getSpriteAtlasPath(); + encounter.spriteConfigs.push({ + spriteKey: spriteKey, + fileRoot: spriteRoot, + hasShadow: true, + repeat: true, + isPokemon: true + }); + const starterTier = speciesStarters[species.speciesId]; - // Prices by starter tier: 8/6.4/4.8/4/4 - let priceMultiplier = 8 * (Math.max(starterTier, 2.5) / 5); + // Prices decrease by starter tier less than 5, but only reduces cost by half at max + let priceMultiplier = MAX_POKEMON_PRICE_MULTIPLIER * (Math.max(starterTier, 2.5) / 5); if (pokemon.shiny) { - // Always max price for shiny, and add special message to intro - priceMultiplier = 8; - encounter.setDialogueToken("specialShinyText", `$t(${namespace}:shiny)`); - } else { - encounter.setDialogueToken("specialShinyText", ""); + // Always max price for shiny (flip HA back to normal), and add special messaging + priceMultiplier = MAX_POKEMON_PRICE_MULTIPLIER; + pokemon.abilityIndex = 0; + encounter.dialogue.encounterOptionsDialogue.description = `${namespace}:description_shiny`; + encounter.options[0].dialogue.buttonTooltip = `${namespace}:option:1:tooltip_shiny`; } + const price = scene.getWaveMoneyAmount(priceMultiplier); encounter.setDialogueToken("purchasePokemon", pokemon.name); - encounter.setDialogueToken("price", pokemon.name); + encounter.setDialogueToken("price", price.toString()); encounter.misc = { - money: scene.getWaveMoneyAmount(priceMultiplier), - pokemon: pokemon, - // shiny: pokemon.shiny + money: price, + pokemon: pokemon }; pokemon.calculateStats(); return true; }) - .withSimpleOption({ - buttonLabel: `${namespace}:option:1:label`, - buttonTooltip: `${namespace}:option:1:tooltip`, - selected: [ - { - text: `${namespace}:option:selected`, - }, - ], - }, - async (scene: BattleScene) => { - // Choose Cheap Option - const encounter = scene.currentBattle.mysteryEncounter; - const cost = encounter.misc.money; - // const purchasedPokemon = encounter.misc.pokemon; - - // Update money - updatePlayerMoney(scene, -cost); - - leaveEncounterWithoutBattle(scene); - }) .withOption( new MysteryEncounterOptionBuilder() - .withOptionMode(EncounterOptionMode.DEFAULT) - .withSceneMoneyRequirement(0, 2) // Wave scaling money multiplier of 2 + .withOptionMode(EncounterOptionMode.DEFAULT_OR_SPECIAL) + .withHasDexProgress(true) + .withSceneMoneyRequirement(null, MAX_POKEMON_PRICE_MULTIPLIER) // Wave scaling money multiplier of 2 .withDialogue({ buttonLabel: `${namespace}:option:1:label`, buttonTooltip: `${namespace}:option:1:tooltip`, selected: [ { - text: `${namespace}:option:selected`, - }, + text: `${namespace}:option:1:selected_message`, + } ], }) - .withPreOptionPhase(async (scene: BattleScene): Promise => { - const encounter = scene.currentBattle.mysteryEncounter; - const onPokemonSelected = (pokemon: PlayerPokemon) => { - // Update money - updatePlayerMoney(scene, -(encounter.options[0].requirements[0] as MoneyRequirement).requiredMoney); - // Calculate modifiers and dialogue tokens - const modifiers = [ - generateModifierTypeOption(scene, modifierTypes.BASE_STAT_BOOSTER).type, - generateModifierTypeOption(scene, modifierTypes.BASE_STAT_BOOSTER).type, - ]; - encounter.setDialogueToken("boost1", modifiers[0].name); - encounter.setDialogueToken("boost2", modifiers[1].name); - encounter.misc = { - chosenPokemon: pokemon, - modifiers: modifiers, - }; - }; - - // Only Pokemon that can gain benefits are above 1/3rd HP with no status - const selectableFilter = (pokemon: Pokemon) => { - // If pokemon meets primary pokemon reqs, it can be selected - const meetsReqs = encounter.pokemonMeetsPrimaryRequirements(scene, pokemon); - if (!meetsReqs) { - return getEncounterText(scene, `${namespace}:invalid_selection`); - } - - return null; - }; - - return selectPokemonForOption(scene, onPokemonSelected, null, selectableFilter); - }) .withOptionPhase(async (scene: BattleScene) => { - // Choose Cheap Option const encounter = scene.currentBattle.mysteryEncounter; - const chosenPokemon = encounter.misc.chosenPokemon; - const modifiers = encounter.misc.modifiers; + const cost = encounter.misc.money; + const purchasedPokemon = encounter.misc.pokemon as PlayerPokemon; - for (const modType of modifiers) { - const modifier = modType.newModifier(chosenPokemon); - await scene.addModifier(modifier, true, false, false, true); - } - scene.updateModifiers(true); + // Update money + updatePlayerMoney(scene, -cost, true, false); - leaveEncounterWithoutBattle(scene); - }) - .withPostOptionPhase(async (scene: BattleScene) => { - // Damage and status applied after dealer leaves (to make thematic sense) - const encounter = scene.currentBattle.mysteryEncounter; - const chosenPokemon = encounter.misc.chosenPokemon; + // Show dialogue + await showEncounterDialogue(scene, `${namespace}:option:1:selected_dialogue`, `${namespace}:speaker`); + await transitionMysteryEncounterIntroVisuals(scene); - // Pokemon takes 1/3 max HP damage - applyDamageToPokemon(scene, chosenPokemon, Math.floor(chosenPokemon.getMaxHp() / 3)); + // "Catch" purchased pokemon + const data = new PokemonData(purchasedPokemon); + data.player = false; + await catchPokemon(scene, data.toPokemon(scene) as EnemyPokemon, null, PokeballType.POKEBALL, true); - // Roll for poison (80%) - if (randSeedInt(10) < 8) { - if (chosenPokemon.trySetStatus(StatusEffect.TOXIC)) { - // Toxic applied - queueEncounterMessage(scene, `${namespace}:bad_poison`); - } else { - // Pokemon immune or something else prevents status - queueEncounterMessage(scene, `${namespace}:damage_only`); - } - } else { - queueEncounterMessage(scene, `${namespace}:damage_only`); - } - - setEncounterExp(scene, [chosenPokemon.id], 100); - - chosenPokemon.updateInfo(); - }) - .build() - ) - .withOption( - new MysteryEncounterOptionBuilder() - .withOptionMode(EncounterOptionMode.DISABLED_OR_DEFAULT) - .withSceneMoneyRequirement(0, 5) // Wave scaling money multiplier of 5 - .withDialogue({ - buttonLabel: `${namespace}:option:2:label`, - buttonTooltip: `${namespace}:option:2:tooltip`, - selected: [ - { - text: `${namespace}:option:selected`, - }, - ], - }) - .withPreOptionPhase(async (scene: BattleScene): Promise => { - const encounter = scene.currentBattle.mysteryEncounter; - const onPokemonSelected = (pokemon: PlayerPokemon) => { - // Update money - updatePlayerMoney(scene, -(encounter.options[1].requirements[0] as MoneyRequirement).requiredMoney); - // Calculate modifiers and dialogue tokens - const modifiers = [ - generateModifierTypeOption(scene, modifierTypes.BASE_STAT_BOOSTER).type, - generateModifierTypeOption(scene, modifierTypes.BASE_STAT_BOOSTER).type, - ]; - encounter.setDialogueToken("boost1", modifiers[0].name); - encounter.setDialogueToken("boost2", modifiers[1].name); - encounter.misc = { - chosenPokemon: pokemon, - modifiers: modifiers, - }; - }; - - // Only Pokemon that can gain benefits are above 1/3rd HP with no status - const selectableFilter = (pokemon: Pokemon) => { - // If pokemon meets primary pokemon reqs, it can be selected - const meetsReqs = encounter.pokemonMeetsPrimaryRequirements(scene, pokemon); - if (!meetsReqs) { - return getEncounterText(scene, `${namespace}:invalid_selection`); - } - - return null; - }; - - return selectPokemonForOption(scene, onPokemonSelected, null, selectableFilter); - }) - .withOptionPhase(async (scene: BattleScene) => { - // Choose Expensive Option - const encounter = scene.currentBattle.mysteryEncounter; - const chosenPokemon = encounter.misc.chosenPokemon; - const modifiers = encounter.misc.modifiers; - - for (const modType of modifiers) { - const modifier = modType.newModifier(chosenPokemon); - await scene.addModifier(modifier, true, false, false, true); - } - scene.updateModifiers(true); - - leaveEncounterWithoutBattle(scene); - }) - .withPostOptionPhase(async (scene: BattleScene) => { - // Status applied after dealer leaves (to make thematic sense) - const encounter = scene.currentBattle.mysteryEncounter; - const chosenPokemon = encounter.misc.chosenPokemon; - - // Roll for poison (20%) - if (randSeedInt(10) < 2) { - if (chosenPokemon.trySetStatus(StatusEffect.POISON)) { - // Poison applied - queueEncounterMessage(scene, `${namespace}:poison`); - } else { - // Pokemon immune or something else prevents status - queueEncounterMessage(scene, `${namespace}:no_bad_effects`); - } - } else { - queueEncounterMessage(scene, `${namespace}:no_bad_effects`); - } - - setEncounterExp(scene, [chosenPokemon.id], 100); - - chosenPokemon.updateInfo(); + leaveEncounterWithoutBattle(scene, true); }) .build() ) .withSimpleOption( { - buttonLabel: `${namespace}:option:3:label`, - buttonTooltip: `${namespace}:option:3:tooltip`, + buttonLabel: `${namespace}:option:2:label`, + buttonTooltip: `${namespace}:option:2:tooltip`, + selected: [ + { + text: `${namespace}:option:2:selected`, + }, + ], }, async (scene: BattleScene) => { // Leave encounter with no rewards or exp diff --git a/src/data/mystery-encounters/encounters/training-session-encounter.ts b/src/data/mystery-encounters/encounters/training-session-encounter.ts index 2c09b0a01c7..a3f18395fe2 100644 --- a/src/data/mystery-encounters/encounters/training-session-encounter.ts +++ b/src/data/mystery-encounters/encounters/training-session-encounter.ts @@ -3,7 +3,7 @@ import { EnemyPartyConfig, initBattleWithEnemyConfig, selectPokemonForOption, se import { getNatureName, Nature } from "#app/data/nature"; import { speciesStarters } from "#app/data/pokemon-species"; import { Stat } from "#app/data/pokemon-stat"; -import { PlayerPokemon } from "#app/field/pokemon"; +import Pokemon, { PlayerPokemon } from "#app/field/pokemon"; import { pokemonInfo } from "#app/locales/en/pokemon-info"; import { PokemonHeldItemModifier } from "#app/modifier/modifier"; import { PokemonHeldItemModifierType } from "#app/modifier/modifier-type"; @@ -16,7 +16,7 @@ import { MysteryEncounterType } from "#enums/mystery-encounter-type"; import BattleScene from "../../../battle-scene"; import IMysteryEncounter, { MysteryEncounterBuilder, MysteryEncounterTier, } from "../mystery-encounter"; import { EncounterOptionMode, MysteryEncounterOptionBuilder } from "../mystery-encounter-option"; -import { queueEncounterMessage } from "#app/data/mystery-encounters/utils/encounter-dialogue-utils"; +import { getEncounterText, queueEncounterMessage } from "#app/data/mystery-encounters/utils/encounter-dialogue-utils"; /** The i18n namespace for the encounter */ const namespace = "mysteryEncounter:trainingSession"; @@ -27,11 +27,10 @@ const namespace = "mysteryEncounter:trainingSession"; * @see For biome requirements check {@linkcode mysteryEncountersByBiome} */ export const TrainingSessionEncounter: IMysteryEncounter = - MysteryEncounterBuilder.withEncounterType( - MysteryEncounterType.TRAINING_SESSION - ) + MysteryEncounterBuilder.withEncounterType(MysteryEncounterType.TRAINING_SESSION) .withEncounterTier(MysteryEncounterTier.ULTRA) .withSceneWaveRangeRequirement(10, 180) // waves 10 to 180 + .withScenePartySizeRequirement(2, 6, true) // Must have at least 2 unfainted pokemon in party .withHideWildIntroMessage(true) .withIntroSpriteConfigs([ { @@ -46,7 +45,7 @@ export const TrainingSessionEncounter: IMysteryEncounter = .withIntroDialogue([ { text: `${namespace}:intro`, - }, + } ]) .withTitle(`${namespace}:title`) .withDescription(`${namespace}:description`) @@ -54,6 +53,7 @@ export const TrainingSessionEncounter: IMysteryEncounter = .withOption( new MysteryEncounterOptionBuilder() .withOptionMode(EncounterOptionMode.DEFAULT) + .withHasDexProgress(true) .withDialogue({ buttonLabel: `${namespace}:option:1:label`, buttonTooltip: `${namespace}:option:1:tooltip`, @@ -71,7 +71,17 @@ export const TrainingSessionEncounter: IMysteryEncounter = }; }; - return selectPokemonForOption(scene, onPokemonSelected); + // Only Pokemon that are not KOed/legal can be trained + const selectableFilter = (pokemon: Pokemon) => { + const meetsReqs = pokemon.isAllowedInBattle(); + if (!meetsReqs) { + return getEncounterText(scene, `${namespace}:invalid_selection`); + } + + return null; + }; + + return selectPokemonForOption(scene, onPokemonSelected, null, selectableFilter); }) .withOptionPhase(async (scene: BattleScene) => { const encounter = scene.currentBattle.mysteryEncounter; @@ -187,6 +197,7 @@ export const TrainingSessionEncounter: IMysteryEncounter = .withOption( new MysteryEncounterOptionBuilder() .withOptionMode(EncounterOptionMode.DEFAULT) + .withHasDexProgress(true) .withDialogue({ buttonLabel: `${namespace}:option:2:label`, buttonTooltip: `${namespace}:option:2:tooltip`, @@ -220,7 +231,17 @@ export const TrainingSessionEncounter: IMysteryEncounter = }); }; - return selectPokemonForOption(scene, onPokemonSelected); + // Only Pokemon that are not KOed/legal can be trained + const selectableFilter = (pokemon: Pokemon) => { + const meetsReqs = pokemon.isAllowedInBattle(); + if (!meetsReqs) { + return getEncounterText(scene, `${namespace}:invalid_selection`); + } + + return null; + }; + + return selectPokemonForOption(scene, onPokemonSelected, null, selectableFilter); }) .withOptionPhase(async (scene: BattleScene) => { const encounter = scene.currentBattle.mysteryEncounter; @@ -269,6 +290,7 @@ export const TrainingSessionEncounter: IMysteryEncounter = .withOption( new MysteryEncounterOptionBuilder() .withOptionMode(EncounterOptionMode.DEFAULT) + .withHasDexProgress(true) .withDialogue({ buttonLabel: `${namespace}:option:3:label`, buttonTooltip: `${namespace}:option:3:tooltip`, @@ -311,7 +333,17 @@ export const TrainingSessionEncounter: IMysteryEncounter = }); }; - return selectPokemonForOption(scene, onPokemonSelected); + // Only Pokemon that are not KOed/legal can be trained + const selectableFilter = (pokemon: Pokemon) => { + const meetsReqs = pokemon.isAllowedInBattle(); + if (!meetsReqs) { + return getEncounterText(scene, `${namespace}:invalid_selection`); + } + + return null; + }; + + return selectPokemonForOption(scene, onPokemonSelected, null, selectableFilter); }) .withOptionPhase(async (scene: BattleScene) => { const encounter = scene.currentBattle.mysteryEncounter; @@ -393,23 +425,12 @@ export const TrainingSessionEncounter: IMysteryEncounter = ) .build(); -function getEnemyConfig( - scene: BattleScene, - playerPokemon: PlayerPokemon, - segments: number, - modifiers: ModifiersHolder -): EnemyPartyConfig { +function getEnemyConfig(scene: BattleScene, playerPokemon: PlayerPokemon,segments: number,modifiers: ModifiersHolder): EnemyPartyConfig { playerPokemon.resetSummonData(); // Passes modifiers by reference - modifiers.value = scene.findModifiers( - (m) => - m instanceof PokemonHeldItemModifier && - (m as PokemonHeldItemModifier).pokemonId === playerPokemon.id - ) as PokemonHeldItemModifier[]; - const modifierTypes = modifiers.value.map( - (mod) => mod.type - ) as PokemonHeldItemModifierType[]; + modifiers.value = scene.findModifiers((m) => m instanceof PokemonHeldItemModifier && (m as PokemonHeldItemModifier).pokemonId === playerPokemon.id) as PokemonHeldItemModifier[]; + const modifierTypes = modifiers.value.map((mod) => mod.type) as PokemonHeldItemModifierType[]; const data = new PokemonData(playerPokemon); return { diff --git a/src/data/mystery-encounters/mystery-encounter-option.ts b/src/data/mystery-encounters/mystery-encounter-option.ts index c7feb2688c3..8b9b26e2f95 100644 --- a/src/data/mystery-encounters/mystery-encounter-option.ts +++ b/src/data/mystery-encounters/mystery-encounter-option.ts @@ -6,6 +6,7 @@ import * as Utils from "../../utils"; import { Type } from "../type"; import { EncounterPokemonRequirement, EncounterSceneRequirement, MoneyRequirement, TypeRequirement } from "./mystery-encounter-requirements"; import { CanLearnMoveRequirement, CanLearnMoveRequirementOptions } from "./requirements/can-learn-move-requirement"; +import { isNullOrUndefined } from "../../utils"; export enum EncounterOptionMode { /** Default style */ @@ -23,6 +24,9 @@ export type OptionPhaseCallback = (scene: BattleScene) => Promise> { + return Object.assign(this, { hasDexProgress: hasDexProgress }); + } + withSceneRequirement(requirement: EncounterSceneRequirement): this & Required> { this.requirements.push(requirement); return Object.assign(this, { requirements: this.requirements }); diff --git a/src/data/mystery-encounters/mystery-encounter-requirements.ts b/src/data/mystery-encounters/mystery-encounter-requirements.ts index ac1d49c919e..6f0b408e1d9 100644 --- a/src/data/mystery-encounters/mystery-encounter-requirements.ts +++ b/src/data/mystery-encounters/mystery-encounter-requirements.ts @@ -144,20 +144,23 @@ export class WeatherRequirement extends EncounterSceneRequirement { export class PartySizeRequirement extends EncounterSceneRequirement { partySizeRange: [number, number]; + excludeFainted: boolean; /** * Used for specifying a party size requirement * If min and max are equivalent, will check for exact size * @param partySizeRange - [min, max] + * @param excludeFainted */ - constructor(partySizeRange: [number, number]) { + constructor(partySizeRange: [number, number], excludeFainted: boolean) { super(); this.partySizeRange = partySizeRange; + this.excludeFainted = excludeFainted; } meetsRequirement(scene: BattleScene): boolean { if (!isNullOrUndefined(this?.partySizeRange) && this.partySizeRange?.[0] <= this.partySizeRange?.[1]) { - const partySize = scene.getParty().length; + const partySize = this.excludeFainted ? scene.getParty().filter(p => p.isAllowedInBattle()).length : scene.getParty().length; if (partySize >= 0 && (this?.partySizeRange?.[0] >= 0 && this.partySizeRange?.[0] > partySize) || (this?.partySizeRange?.[1] >= 0 && this.partySizeRange?.[1] < partySize)) { return false; } diff --git a/src/data/mystery-encounters/mystery-encounter.ts b/src/data/mystery-encounters/mystery-encounter.ts index b18707968a3..aa9d4ef0997 100644 --- a/src/data/mystery-encounters/mystery-encounter.ts +++ b/src/data/mystery-encounters/mystery-encounter.ts @@ -403,7 +403,7 @@ export class MysteryEncounterBuilder implements Partial { options?: [MysteryEncounterOption, MysteryEncounterOption, ...MysteryEncounterOption[]] = [null, null]; spriteConfigs?: MysteryEncounterSpriteConfig[]; - dialogue?: MysteryEncounterDialogue; + dialogue?: MysteryEncounterDialogue = {}; encounterTier?: MysteryEncounterTier; encounterAnimations?: EncounterAnim[]; requirements?: EncounterSceneRequirement[] = []; @@ -460,6 +460,7 @@ export class MysteryEncounterBuilder implements Partial { * There should be at least 2 options defined and no more than 4. * If complex use {@linkcode MysteryEncounterBuilder.withOption} * + * @param hasDexProgress - * @param dialogue - {@linkcode OptionTextDisplay} * @param callback - {@linkcode OptionPhaseCallback} * @returns @@ -468,6 +469,24 @@ export class MysteryEncounterBuilder implements Partial { return this.withOption(new MysteryEncounterOptionBuilder().withOptionMode(EncounterOptionMode.DEFAULT).withDialogue(dialogue).withOptionPhase(callback).build()); } + /** + * Defines an option + phasefor the encounter. + * Use for easy/streamlined options. + * There should be at least 2 options defined and no more than 4. + * If complex use {@linkcode MysteryEncounterBuilder.withOption} + * + * @param dialogue - {@linkcode OptionTextDisplay} + * @param callback - {@linkcode OptionPhaseCallback} + * @returns + */ + withSimpleDexProgressOption(dialogue: OptionTextDisplay, callback: OptionPhaseCallback): this & Pick { + return this.withOption(new MysteryEncounterOptionBuilder() + .withOptionMode(EncounterOptionMode.DEFAULT) + .withHasDexProgress(true) + .withDialogue(dialogue) + .withOptionPhase(callback).build()); + } + /** * Defines the sprites that will be shown on the enemy field when the encounter spawns * Can be one or more sprites, recommended not to exceed 4 @@ -478,7 +497,7 @@ export class MysteryEncounterBuilder implements Partial { return Object.assign(this, { spriteConfigs: spriteConfigs }); } - withIntroDialogue(dialogue: MysteryEncounterDialogue["intro"] = []) { + withIntroDialogue(dialogue: MysteryEncounterDialogue["intro"] = []): this { this.dialogue = {...this.dialogue, intro: dialogue }; return this; } @@ -550,7 +569,7 @@ export class MysteryEncounterBuilder implements Partial { * @param max optional max wave. If not given, defaults to min => exact wave * @returns */ - withSceneWaveRangeRequirement(min: number, max?: number) { + withSceneWaveRangeRequirement(min: number, max?: number): this & Required> { return this.withSceneRequirement(new WaveRangeRequirement([min, max ?? min])); } @@ -559,10 +578,11 @@ export class MysteryEncounterBuilder implements Partial { * * @param min min wave (or exact size if only min is given) * @param max optional max size. If not given, defaults to min => exact wave + * @param excludeFainted - if true, only counts unfainted mons * @returns */ - withScenePartySizeRequirement(min: number, max?: number) { - return this.withSceneRequirement(new PartySizeRequirement([min, max ?? min])); + withScenePartySizeRequirement(min: number, max?: number, excludeFainted?: boolean): this & Required> { + return this.withSceneRequirement(new PartySizeRequirement([min, max ?? min], excludeFainted)); } /** @@ -700,7 +720,7 @@ export class MysteryEncounterBuilder implements Partial { * @param title - title of the encounter * @returns */ - withTitle(title: string) { + withTitle(title: string): this { const encounterOptionsDialogue = this.dialogue.encounterOptionsDialogue ?? {}; this.dialogue = { @@ -720,7 +740,7 @@ export class MysteryEncounterBuilder implements Partial { * @param description - description of the encounter * @returns */ - withDescription(description: string) { + withDescription(description: string): this { const encounterOptionsDialogue = this.dialogue.encounterOptionsDialogue ?? {}; this.dialogue = { @@ -740,7 +760,7 @@ export class MysteryEncounterBuilder implements Partial { * @param query - query to use for the encounter * @returns */ - withQuery(query: string) { + withQuery(query: string): this { const encounterOptionsDialogue = this.dialogue.encounterOptionsDialogue ?? {}; this.dialogue = { @@ -760,7 +780,7 @@ export class MysteryEncounterBuilder implements Partial { * @param dialogue - outro dialogue/s * @returns */ - withOutroDialogue(dialogue: MysteryEncounterDialogue["outro"] = []) { + withOutroDialogue(dialogue: MysteryEncounterDialogue["outro"] = []): this { this.dialogue = {...this.dialogue, outro: dialogue }; return this; } @@ -768,10 +788,9 @@ export class MysteryEncounterBuilder implements Partial { /** * Builds the mystery encounter * - * @param this - MysteryEncounter * @returns */ - build(this: IMysteryEncounter) { + build(this: IMysteryEncounter): IMysteryEncounter { return new IMysteryEncounter(this); } } diff --git a/src/data/mystery-encounters/mystery-encounters.ts b/src/data/mystery-encounters/mystery-encounters.ts index fb2bc9a2ad8..9c26f54a128 100644 --- a/src/data/mystery-encounters/mystery-encounters.ts +++ b/src/data/mystery-encounters/mystery-encounters.ts @@ -18,7 +18,7 @@ import { PokemonSalesmanEncounter } from "#app/data/mystery-encounters/encounter // Spawn chance: (BASE_MYSTERY_ENCOUNTER_SPAWN_WEIGHT + WIGHT_INCREMENT_ON_SPAWN_MISS * ) / 256 export const BASE_MYSTERY_ENCOUNTER_SPAWN_WEIGHT = 1; -export const WIGHT_INCREMENT_ON_SPAWN_MISS = 5; +export const WEIGHT_INCREMENT_ON_SPAWN_MISS = 5; export const AVERAGE_ENCOUNTERS_PER_RUN_TARGET = 15; export const EXTREME_ENCOUNTER_BIOMES = [ diff --git a/src/data/mystery-encounters/utils/encounter-dialogue-utils.ts b/src/data/mystery-encounters/utils/encounter-dialogue-utils.ts index 1042984533d..382b2c7dde8 100644 --- a/src/data/mystery-encounters/utils/encounter-dialogue-utils.ts +++ b/src/data/mystery-encounters/utils/encounter-dialogue-utils.ts @@ -63,10 +63,12 @@ export function showEncounterText(scene: BattleScene, contentKey: string, callba * @param scene * @param textContentKey * @param speakerContentKey - * @param callback + * @param callbackDelay */ -export function showEncounterDialogue(scene: BattleScene, textContentKey: string, speakerContentKey: string, callback?: Function) { - const text: string = getEncounterText(scene, textContentKey); - const speaker: string = getEncounterText(scene, speakerContentKey); - scene.ui.showDialogue(text, speaker, null, callback, 0, 0); +export function showEncounterDialogue(scene: BattleScene, textContentKey: string, speakerContentKey: string, callbackDelay: number = 0): Promise { + return new Promise(resolve => { + const text: string = getEncounterText(scene, textContentKey); + const speaker: string = getEncounterText(scene, speakerContentKey); + scene.ui.showDialogue(text, speaker, null, () => resolve(), callbackDelay); + }); } diff --git a/src/data/mystery-encounters/utils/encounter-phase-utils.ts b/src/data/mystery-encounters/utils/encounter-phase-utils.ts index 40763358b89..37dcca3e412 100644 --- a/src/data/mystery-encounters/utils/encounter-phase-utils.ts +++ b/src/data/mystery-encounters/utils/encounter-phase-utils.ts @@ -1,7 +1,7 @@ import { BattlerIndex, BattleType } from "#app/battle"; import { biomeLinks } from "#app/data/biomes"; import MysteryEncounterOption from "#app/data/mystery-encounters/mystery-encounter-option"; -import { WIGHT_INCREMENT_ON_SPAWN_MISS } from "#app/data/mystery-encounters/mystery-encounters"; +import { 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 } from "#app/field/pokemon"; import { ExpBalanceModifier, ExpShareModifier, MultipleParticipantExpBonusModifier, PokemonExpBoosterModifier } from "#app/modifier/modifier"; @@ -317,17 +317,19 @@ export function initCustomMovesForEncounter(scene: BattleScene, moves: Moves | M * @param changeValue * @param playSound */ -export function updatePlayerMoney(scene: BattleScene, changeValue: number, playSound: boolean = true) { - scene.money += changeValue; +export function updatePlayerMoney(scene: BattleScene, changeValue: number, playSound: boolean = true, showMessage: boolean = true) { + scene.money = Math.min(Math.max(scene.money + changeValue, 0), Number.MAX_SAFE_INTEGER); scene.updateMoneyText(); scene.animateMoneyChanged(false); if (playSound) { scene.playSound("buy"); } - if (changeValue < 0) { - scene.queueMessage(i18next.t("mysteryEncounter:paid_money", { amount: -changeValue }), null, true); - } else { - scene.queueMessage(i18next.t("mysteryEncounter:receive_money", { amount: changeValue }), null, true); + if (showMessage) { + if (changeValue < 0) { + scene.queueMessage(i18next.t("mysteryEncounter:paid_money", { amount: -changeValue }), null, true); + } else { + scene.queueMessage(i18next.t("mysteryEncounter:receive_money", { amount: changeValue }), null, true); + } } } @@ -399,6 +401,7 @@ export function selectPokemonForOption(scene: BattleScene, onPokemonSelected: (p }).concat({ label: i18next.t("menu:cancel"), handler: () => { + scene.ui.clearText(); scene.ui.setMode(Mode.MYSTERY_ENCOUNTER); resolve(false); return true; @@ -730,12 +733,18 @@ export function calculateMEAggregateStats(scene: BattleScene, baseSpawnWeight: n const numRuns = 1000; let run = 0; const targetEncountersPerRun = 15; // AVERAGE_ENCOUNTERS_PER_RUN_TARGET + const biomes = Object.keys(Biome).filter(key => isNaN(Number(key))); + const alwaysPickTheseBiomes = [Biome.ISLAND, Biome.ABYSS, Biome.WASTELAND, Biome.FAIRY_CAVE, Biome.TEMPLE, Biome.LABORATORY, Biome.SPACE, Biome.WASTELAND]; - const calculateNumEncounters = (): number[] => { + const calculateNumEncounters = (): any[] => { let encounterRate = baseSpawnWeight; // BASE_MYSTERY_ENCOUNTER_SPAWN_WEIGHT const numEncounters = [0, 0, 0, 0]; + const encountersByBiome = new Map(biomes.map(b => [b, 0])); + const validMEfloorsByBiome = new Map(biomes.map(b => [b, 0])); let currentBiome = Biome.TOWN; let currentArena = scene.newArena(currentBiome); + scene.setSeed(Utils.randomString(24)); + scene.resetSeed(); for (let i = 10; i < 180; i++) { // Boss if (i % 10 === 0) { @@ -748,10 +757,17 @@ export function calculateMEAggregateStats(scene: BattleScene, baseSpawnWeight: n let biomes: Biome[]; scene.executeWithSeedOffset(() => { biomes = (biomeLinks[currentBiome] as (Biome | [Biome, integer])[]) - .filter(b => !Array.isArray(b) || !Utils.randSeedInt(b[1])) + .filter(b => { + return !Array.isArray(b) || !Utils.randSeedInt(b[1]); + }) .map(b => !Array.isArray(b) ? b : b[0]); - }, i); - currentBiome = biomes[Utils.randSeedInt(biomes.length)]; + }, i * 100); + const specialBiomes = biomes.filter(b => alwaysPickTheseBiomes.includes(b)); + if (specialBiomes.length > 0) { + currentBiome = specialBiomes[Utils.randSeedInt(specialBiomes.length)]; + } else { + currentBiome = biomes[Utils.randSeedInt(biomes.length)]; + } } else if (biomeLinks.hasOwnProperty(currentBiome)) { currentBiome = (biomeLinks[currentBiome] as Biome); } else { @@ -778,6 +794,7 @@ export function calculateMEAggregateStats(scene: BattleScene, baseSpawnWeight: n // Otherwise, roll encounter const roll = Utils.randSeedInt(256); + validMEfloorsByBiome.set(Biome[currentBiome], validMEfloorsByBiome.get(Biome[currentBiome]) + 1); // If total number of encounters is lower than expected for the run, slightly favor a new encounter // Do the reverse as well @@ -803,31 +820,63 @@ export function calculateMEAggregateStats(scene: BattleScene, baseSpawnWeight: n const rareThreshold = totalWeight - tierWeights[0] - tierWeights[1] - tierWeights[2]; // 64 - 32 - 16 - 10 = 6 tierValue > commonThreshold ? ++numEncounters[0] : tierValue > uncommonThreshold ? ++numEncounters[1] : tierValue > rareThreshold ? ++numEncounters[2] : ++numEncounters[3]; + encountersByBiome.set(Biome[currentBiome], encountersByBiome.get(Biome[currentBiome]) + 1); } else { - encounterRate += WIGHT_INCREMENT_ON_SPAWN_MISS; + encounterRate += WEIGHT_INCREMENT_ON_SPAWN_MISS; } } - return numEncounters; + return [numEncounters, encountersByBiome, validMEfloorsByBiome]; }; - const runs = []; + const encounterRuns: number[][] = []; + const encountersByBiomeRuns: Map[] = []; + const validFloorsByBiome: Map[] = []; while (run < numRuns) { scene.executeWithSeedOffset(() => { - const numEncounters = calculateNumEncounters(); - runs.push(numEncounters); + const [numEncounters, encountersByBiome, validMEfloorsByBiome] = calculateNumEncounters(); + encounterRuns.push(numEncounters); + encountersByBiomeRuns.push(encountersByBiome); + validFloorsByBiome.push(validMEfloorsByBiome); }, 1000 * run); run++; } - const n = runs.length; - const totalEncountersInRun = runs.map(run => run.reduce((a, b) => a + b)); + const n = encounterRuns.length; + const totalEncountersInRun = encounterRuns.map(run => run.reduce((a, b) => a + b)); const totalMean = totalEncountersInRun.reduce((a, b) => a + b) / n; const totalStd = Math.sqrt(totalEncountersInRun.map(x => Math.pow(x - totalMean, 2)).reduce((a, b) => a + b) / n); - const commonMean = runs.reduce((a, b) => a + b[0], 0) / n; - const uncommonMean = runs.reduce((a, b) => a + b[1], 0) / n; - const rareMean = runs.reduce((a, b) => a + b[2], 0) / n; - const superRareMean = runs.reduce((a, b) => a + b[3], 0) / n; + const commonMean = encounterRuns.reduce((a, b) => a + b[0], 0) / n; + const uncommonMean = encounterRuns.reduce((a, b) => a + b[1], 0) / n; + const rareMean = encounterRuns.reduce((a, b) => a + b[2], 0) / n; + const superRareMean = encounterRuns.reduce((a, b) => a + b[3], 0) / n; - console.log(`Starting weight: ${baseSpawnWeight}\nAverage MEs per run: ${totalMean}\nStandard Deviation: ${totalStd}\nAvg Commons: ${commonMean}\nAvg Uncommons: ${uncommonMean}\nAvg Rares: ${rareMean}\nAvg Super Rares: ${superRareMean}`); + const encountersPerRunPerBiome = encountersByBiomeRuns.reduce((a, b) => { + for (const biome of a.keys()) { + a.set(biome, a.get(biome) + b.get(biome)); + } + return a; + }); + const meanEncountersPerRunPerBiome: Map = new Map(); + encountersPerRunPerBiome.forEach((value, key) => { + meanEncountersPerRunPerBiome.set(key, value / n); + }); + + const validMEFloorsPerRunPerBiome = validFloorsByBiome.reduce((a, b) => { + for (const biome of a.keys()) { + a.set(biome, a.get(biome) + b.get(biome)); + } + return a; + }); + const meanMEFloorsPerRunPerBiome: Map = new Map(); + validMEFloorsPerRunPerBiome.forEach((value, key) => { + meanMEFloorsPerRunPerBiome.set(key, value / n); + }); + + let stats = `Starting weight: ${baseSpawnWeight}\nAverage MEs per run: ${totalMean}\nStandard Deviation: ${totalStd}\nAvg Commons: ${commonMean}\nAvg Greats: ${uncommonMean}\nAvg Ultras: ${rareMean}\nAvg Rogues: ${superRareMean}\n`; + + const meanEncountersPerRunPerBiomeSorted = [...meanEncountersPerRunPerBiome.entries()].sort((e1, e2) => e2[1] - e1[1]); + meanEncountersPerRunPerBiomeSorted.forEach(value => stats = stats + `${value[0]}: avg valid floors ${meanMEFloorsPerRunPerBiome.get(value[0])}, avg MEs ${value[1]},\n`); + + console.log(stats); } diff --git a/src/data/mystery-encounters/utils/encounter-pokemon-utils.ts b/src/data/mystery-encounters/utils/encounter-pokemon-utils.ts index 3510fe37e09..43e25793f86 100644 --- a/src/data/mystery-encounters/utils/encounter-pokemon-utils.ts +++ b/src/data/mystery-encounters/utils/encounter-pokemon-utils.ts @@ -386,7 +386,7 @@ function failCatch(scene: BattleScene, pokemon: EnemyPokemon, originalY: number, }); } -export async function catchPokemon(scene: BattleScene, pokemon: EnemyPokemon, pokeball: Phaser.GameObjects.Sprite, pokeballType: PokeballType): Promise { +export async function catchPokemon(scene: BattleScene, pokemon: EnemyPokemon, pokeball: Phaser.GameObjects.Sprite, pokeballType: PokeballType, isObtain: boolean = false): Promise { scene.unshiftPhase(new VictoryPhase(scene, BattlerIndex.ENEMY)); const speciesForm = !pokemon.fusionSpecies ? pokemon.getSpeciesForm() : pokemon.getFusionSpeciesForm(); @@ -412,14 +412,16 @@ export async function catchPokemon(scene: BattleScene, pokemon: EnemyPokemon, po scene.gameData.updateSpeciesDexIvs(pokemon.species.getRootSpeciesId(true), pokemon.ivs); return new Promise(resolve => { - scene.ui.showText(i18next.t("battle:pokemonCaught", { pokemonName: pokemon.name }), null, () => { + scene.ui.showText(i18next.t(isObtain ? "battle:pokemonObtained" : "battle:pokemonCaught", { pokemonName: pokemon.name }), null, () => { const end = () => { scene.pokemonInfoContainer.hide(); removePb(scene, pokeball); resolve(); }; const removePokemon = () => { - scene.field.remove(pokemon, true); + if (pokemon) { + scene.field.remove(pokemon, true); + } }; const addToParty = () => { const newPokemon = pokemon.addToParty(pokeballType); @@ -470,14 +472,18 @@ export async function catchPokemon(scene: BattleScene, pokemon: EnemyPokemon, po } function removePb(scene: BattleScene, pokeball: Phaser.GameObjects.Sprite) { - scene.tweens.add({ - targets: pokeball, - duration: 250, - delay: 250, - ease: "Sine.easeIn", - alpha: 0, - onComplete: () => pokeball.destroy() - }); + if (pokeball) { + scene.tweens.add({ + targets: pokeball, + duration: 250, + delay: 250, + ease: "Sine.easeIn", + alpha: 0, + onComplete: () => { + pokeball.destroy(); + } + }); + } } export async function doPokemonFlee(scene: BattleScene, pokemon: EnemyPokemon): Promise { diff --git a/src/field/mystery-encounter-intro.ts b/src/field/mystery-encounter-intro.ts index 84f6488c439..620265c0edb 100644 --- a/src/field/mystery-encounter-intro.ts +++ b/src/field/mystery-encounter-intro.ts @@ -17,6 +17,13 @@ type KnownFileRoot = | "mystery-encounters" | "pokeball" | "pokemon" + | "pokemon/back" + | "pokemon/exp" + | "pokemon/female" + | "pokemon/icons" + | "pokemon/input" + | "pokemon/shiny" + | "pokemon/variant" | "statuses" | "trainer" | "ui"; @@ -25,7 +32,7 @@ export class MysteryEncounterSpriteConfig { /** The sprite key (which is the image file name). e.g. "ace_trainer_f" */ spriteKey: string; /** Refer to [/public/images](../../public/images) directorty for all folder names */ - fileRoot: KnownFileRoot & string; + fileRoot: KnownFileRoot & string | string; /** Enable shadow. Defaults to `false` */ hasShadow?: boolean = false; /** Disable animation. Defaults to `false` */ @@ -44,6 +51,8 @@ export class MysteryEncounterSpriteConfig { yShadow?: number; /** Sprite scale. `0` - `n` */ scale?: number; + /** If you are using a Pokemon sprite, set to `true`. This will ensure variant, form, gender, shiny sprites are loaded properly */ + isPokemon?: boolean; /** If you are using an item sprite, set to `true` */ isItem?: boolean; /** The sprites alpha. `0` - `1` The lower the number, the more transparent */ @@ -155,10 +164,12 @@ export default class MysteryEncounterIntroVisuals extends Phaser.GameObjects.Con } this.spriteConfigs.forEach((config) => { - if (!config.isItem) { - this.scene.loadAtlas(config.spriteKey, config.fileRoot); - } else { + if (config.isPokemon) { + this.scene.loadPokemonAtlas(config.spriteKey, config.fileRoot); + } else if (config.isItem) { this.scene.loadAtlas("items", ""); + } else { + this.scene.loadAtlas(config.spriteKey, config.fileRoot); } }); diff --git a/src/loading-scene.ts b/src/loading-scene.ts index 1ce8ed87156..3b62e1dc983 100644 --- a/src/loading-scene.ts +++ b/src/loading-scene.ts @@ -273,6 +273,8 @@ export class LoadingScene extends SceneBase { this.loadAtlas("xbox", "inputs"); this.loadAtlas("keyboard", "inputs"); + this.loadAtlas("encounter_radar", "mystery-encounters"); + this.loadSe("select"); this.loadSe("menu_open"); this.loadSe("hit"); diff --git a/src/locales/en/battle.ts b/src/locales/en/battle.ts index 6deaf4496a0..d4dfb9ba525 100644 --- a/src/locales/en/battle.ts +++ b/src/locales/en/battle.ts @@ -16,6 +16,7 @@ export const battle: SimpleTranslationEntries = { "moneyWon": "You got\n₽{{moneyAmount}} for winning!", "moneyPickedUp": "You picked up ₽{{moneyAmount}}!", "pokemonCaught": "{{pokemonName}} was caught!", + "pokemonObtained": "You got {{pokemonName}}!", "pokemonBrokeFree": "Oh no!\nThe Pokémon broke free!", "pokemonFled": "The wild {{pokemonName}} fled!", "playerFled": "You fled from the {{pokemonName}}!", diff --git a/src/locales/en/mystery-encounter.ts b/src/locales/en/mystery-encounter.ts index a280ae1cc39..f58f18dd65a 100644 --- a/src/locales/en/mystery-encounter.ts +++ b/src/locales/en/mystery-encounter.ts @@ -33,6 +33,7 @@ export const mysteryEncounter = { // General use content "paid_money": "You paid ₽{{amount, number}}.", "receive_money": "You received ₽{{amount, number}}!", + "affects_pokedex": "Affects Pokédex Data", mysteriousChallengers: mysteriousChallengersDialogue, mysteriousChest: mysteriousChestDialogue, diff --git a/src/locales/en/mystery-encounters/pokemon-salesman-dialogue.ts b/src/locales/en/mystery-encounters/pokemon-salesman-dialogue.ts index 7645f96d696..d35e3947a42 100644 --- a/src/locales/en/mystery-encounters/pokemon-salesman-dialogue.ts +++ b/src/locales/en/mystery-encounters/pokemon-salesman-dialogue.ts @@ -1,19 +1,20 @@ export const pokemonSalesmanDialogue = { intro: "A chipper elderly man approaches you.", speaker: "Gentleman", - intro_dialogue: "Hello there! Have I got a deal just for YOU!{{specialShinyText}}", + intro_dialogue: "Hello there! Have I got a deal just for YOU!", title: "The Pokémon Salesman", - description: "\"This {{purchasePokemon}} is extremely unique, and carries an ability not normally found on its species! I'll let you have this swell {{purchasePokemon}} for just {{money, money}}!\"\n\n\"What do you say?\"", + description: "\"This {{purchasePokemon}} is extremely unique and carries an ability not normally found in its species! I'll let you have this swell {{purchasePokemon}} for just {{price, money}}!\"\n\n\"What do you say?\"", + description_shiny: "\"This {{purchasePokemon}} is extremely unique and has a pigment not normally found in its species! I'll let you have this swell {{purchasePokemon}} for just {{price, money}}!\"\n\n\"What do you say?\"", query: "What will you do?", - shiny: "$I have SUPER amazing Pokémon that\nanyone would be dying to get!", option: { 1: { label: "Accept", - tooltip: "(-) Pay {{money, money}}\n(+) Gain a {{purchasePokemon}} with its Hidden Ability (also saved in Pokédex data)", - selected_dialogue: `Excellent choice! - $I can see you've a keen eye for business.`, + tooltip: "(-) Pay {{price, money}}\n(+) Gain a {{purchasePokemon}} with its Hidden Ability", + tooltip_shiny: "(-) Pay {{price, money}}\n(+) Gain a shiny {{purchasePokemon}}", selected_message: "You paid an outrageous sum and bought the {{purchasePokemon}}.", - selected_dialogue_2: "Oh, yeah...@d{64} Returns not accepted, got that?" + selected_dialogue: `Excellent choice! + $I can see you've a keen eye for business. + $Oh, yeah...@d{64} Returns not accepted, got that?`, }, 2: { label: "Refuse", diff --git a/src/locales/en/mystery-encounters/training-session-dialogue.ts b/src/locales/en/mystery-encounters/training-session-dialogue.ts index 4e2ce979935..66e9d7c9498 100644 --- a/src/locales/en/mystery-encounters/training-session-dialogue.ts +++ b/src/locales/en/mystery-encounters/training-session-dialogue.ts @@ -3,6 +3,7 @@ export const trainingSessionDialogue = { title: "Training Session", description: "These supplies look like they could be used to train a member of your party! There are a few ways you could train your Pokémon, by battling against it with the rest of your team.", query: "How should you train?", + invalid_selection: "Pokémon must be healthy enough.", option: { 1: { label: "Light Training", diff --git a/src/overrides.ts b/src/overrides.ts index e19a5bf20dd..e2d3c73f76d 100644 --- a/src/overrides.ts +++ b/src/overrides.ts @@ -117,9 +117,9 @@ export const EGG_GACHA_PULL_COUNT_OVERRIDE: number = 0; */ // 1 to 256, set to null to ignore -export const MYSTERY_ENCOUNTER_RATE_OVERRIDE: number = null; +export const MYSTERY_ENCOUNTER_RATE_OVERRIDE: number = 256; export const MYSTERY_ENCOUNTER_TIER_OVERRIDE: MysteryEncounterTier = null; -export const MYSTERY_ENCOUNTER_OVERRIDE: MysteryEncounterType = null; +export const MYSTERY_ENCOUNTER_OVERRIDE: MysteryEncounterType = MysteryEncounterType.POKEMON_SALESMAN; /** * MODIFIER / ITEM OVERRIDES diff --git a/src/ui/mystery-encounter-ui-handler.ts b/src/ui/mystery-encounter-ui-handler.ts index 72cce41d55f..e6bcbfd697c 100644 --- a/src/ui/mystery-encounter-ui-handler.ts +++ b/src/ui/mystery-encounter-ui-handler.ts @@ -13,6 +13,7 @@ import { getPokeballAtlasKey } from "../data/pokeball"; import { OptionSelectSettings } from "#app/data/mystery-encounters/utils/encounter-phase-utils"; import { getEncounterText } from "#app/data/mystery-encounters/utils/encounter-dialogue-utils"; import { MysteryEncounterTier } from "#app/data/mystery-encounters/mystery-encounter"; +import i18next from "i18next"; export default class MysteryEncounterUiHandler extends UiHandler { private cursorContainer: Phaser.GameObjects.Container; @@ -29,6 +30,10 @@ export default class MysteryEncounterUiHandler extends UiHandler { private descriptionScrollTween: Phaser.Tweens.Tween; private rarityBall: Phaser.GameObjects.Sprite; + private dexProgressWindow: Phaser.GameObjects.NineSlice; + private dexProgressContainer: Phaser.GameObjects.Container; + private showDexProgress: boolean = false; + private overrideSettings: OptionSelectSettings; private encounterOptions: MysteryEncounterOption[] = []; private optionsMeetsReqs: boolean[]; @@ -50,6 +55,9 @@ export default class MysteryEncounterUiHandler extends UiHandler { this.optionsContainer = this.scene.add.container(12, -38.7); this.optionsContainer.setVisible(false); ui.add(this.optionsContainer); + this.dexProgressContainer = this.scene.add.container(214, -43); + this.dexProgressContainer.setVisible(false); + ui.add(this.dexProgressContainer); this.descriptionContainer = this.scene.add.container(0, -152); this.descriptionContainer.setVisible(false); ui.add(this.descriptionContainer); @@ -65,9 +73,23 @@ export default class MysteryEncounterUiHandler extends UiHandler { this.tooltipWindow = addWindow(this.scene, 0, 0, 110, 48, false, false, 0, 0, WindowVariant.THIN); this.tooltipContainer.add(this.tooltipWindow); + this.dexProgressWindow = addWindow(this.scene, 0, 0, 24, 28, false, false, 0, 0, WindowVariant.THIN); + this.dexProgressContainer.add(this.dexProgressWindow); + this.rarityBall = this.scene.add.sprite(141, 9, "pb"); this.rarityBall.setScale(0.75); this.descriptionContainer.add(this.rarityBall); + + const dexProgressIndicator = this.scene.add.sprite(12, 9, "encounter_radar"); + dexProgressIndicator.setScale(0.85); + this.dexProgressContainer.add(dexProgressIndicator); + this.dexProgressContainer.setInteractive(new Phaser.Geom.Rectangle(0, 0, 24, 28), Phaser.Geom.Rectangle.Contains); + this.dexProgressContainer.on("pointerover", () => { + (this.scene as BattleScene).ui.showTooltip(null, i18next.t("mysteryEncounter:affects_pokedex"), true); + }); + this.dexProgressContainer.on("pointerout", () => { + (this.scene as BattleScene).ui.hideTooltip(); + }); } show(args: any[]): boolean { @@ -81,6 +103,7 @@ export default class MysteryEncounterUiHandler extends UiHandler { this.cursorContainer.setVisible(true); this.descriptionContainer.setVisible(showDescriptionContainer); this.optionsContainer.setVisible(true); + this.dexProgressContainer.setVisible(true); this.displayEncounterOptions(slideInDescription); const cursor = this.getCursor(); if (cursor === (this?.optionsContainer?.length || 0) - 1) { @@ -317,7 +340,7 @@ export default class MysteryEncounterUiHandler extends UiHandler { const queryText: string = getEncounterText(this.scene, mysteryEncounter.dialogue.encounterOptionsDialogue.query, TextStyle.TOOLTIP_CONTENT); // Clear options container (except cursor) - this.optionsContainer.removeAll(); + this.optionsContainer.removeAll(true); // Options Window for (let i = 0; i < this.encounterOptions.length; i++) { @@ -437,6 +460,8 @@ export default class MysteryEncounterUiHandler extends UiHandler { if (isNullOrUndefined(cursor) || cursor > this.optionsContainer.length - 2) { // Ignore hovers on view party button + // Hide dex progress if visible + this.showHideDexProgress(false); return; } @@ -487,6 +512,13 @@ export default class MysteryEncounterUiHandler extends UiHandler { }); } } + + // Dex progress indicator + if (cursorOption.hasDexProgress && !this.showDexProgress) { + this.showHideDexProgress(true); + } else if (!cursorOption.hasDexProgress) { + this.showHideDexProgress(false); + } } clear(): void { @@ -494,6 +526,7 @@ export default class MysteryEncounterUiHandler extends UiHandler { this.overrideSettings = null; this.optionsContainer.setVisible(false); this.optionsContainer.removeAll(true); + this.dexProgressContainer.setVisible(false); this.descriptionContainer.setVisible(false); this.tooltipContainer.setVisible(false); // Keeps container background and pokeball @@ -508,4 +541,30 @@ export default class MysteryEncounterUiHandler extends UiHandler { } this.cursorObj = null; } + + /** + * + * @param show - if true does show, if false does hide + */ + showHideDexProgress(show: boolean) { + if (show && !this.showDexProgress) { + this.showDexProgress = true; + this.scene.tweens.killTweensOf(this.dexProgressContainer); + this.scene.tweens.add({ + targets: this.dexProgressContainer, + y: -63, + ease: "Sine.easeInOut", + duration: 750 + }); + } else if (!show && this.showDexProgress) { + this.showDexProgress = false; + this.scene.tweens.killTweensOf(this.dexProgressContainer); + this.scene.tweens.add({ + targets: this.dexProgressContainer, + y: -43, + ease: "Sine.easeInOut", + duration: 750, + }); + } + } }