diff --git a/src/battle-scene.ts b/src/battle-scene.ts index 2edec910f7b..3e9176430a5 100644 --- a/src/battle-scene.ts +++ b/src/battle-scene.ts @@ -67,7 +67,7 @@ import { Species } from "#enums/species"; import { UiTheme } from "#enums/ui-theme"; import { TimedEventManager } from "#app/timed-event-manager.js"; import i18next from "i18next"; -import MysteryEncounter, { MysteryEncounterTier, MysteryEncounterVariant } from "./data/mystery-encounters/mystery-encounter"; +import IMysteryEncounter, { MysteryEncounterTier, MysteryEncounterVariant } from "./data/mystery-encounters/mystery-encounter"; import { mysteryEncountersByBiome, allMysteryEncounters, BASE_MYSTERY_ENCOUNTER_SPAWN_WEIGHT, AVERAGE_ENCOUNTERS_PER_RUN_TARGET, WIGHT_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"; @@ -216,7 +216,7 @@ export default class BattleScene extends SceneBase { public pokemonInfoContainer: PokemonInfoContainer; private party: PlayerPokemon[]; public mysteryEncounterData: MysteryEncounterData = new MysteryEncounterData(null); - public lastMysteryEncounter: MysteryEncounter; + public lastMysteryEncounter: IMysteryEncounter; /** Combined Biome and Wave count text */ private biomeWaveText: Phaser.GameObjects.Text; private moneyText: Phaser.GameObjects.Text; @@ -1026,7 +1026,7 @@ export default class BattleScene extends SceneBase { } } - newBattle(waveIndex?: integer, battleType?: BattleType, trainerData?: TrainerData, double?: boolean, mysteryEncounter?: MysteryEncounter): Battle { + newBattle(waveIndex?: integer, battleType?: BattleType, trainerData?: TrainerData, double?: boolean, mysteryEncounter?: IMysteryEncounter): Battle { const _startingWave = Overrides.STARTING_WAVE_OVERRIDE || startingWave; const newWaveIndex = waveIndex || ((this.currentBattle?.waveIndex || (_startingWave - 1)) + 1); let newDouble: boolean; @@ -2621,9 +2621,9 @@ export default class BattleScene extends SceneBase { * @param override - used to load session encounter when restarting game, etc. * @returns */ - getMysteryEncounter(override: MysteryEncounter): MysteryEncounter { + getMysteryEncounter(override: IMysteryEncounter): IMysteryEncounter { // Loading override or session encounter - let encounter: MysteryEncounter; + let encounter: IMysteryEncounter; if (!isNullOrUndefined(Overrides.MYSTERY_ENCOUNTER_OVERRIDE) && allMysteryEncounters.hasOwnProperty(Overrides.MYSTERY_ENCOUNTER_OVERRIDE)) { encounter = allMysteryEncounters[Overrides.MYSTERY_ENCOUNTER_OVERRIDE]; } else { @@ -2645,8 +2645,8 @@ export default class BattleScene extends SceneBase { } if (encounter) { - encounter = new MysteryEncounter(encounter); - encounter.meetsRequirements(this); + encounter = new IMysteryEncounter(encounter); + encounter.populateDialogueTokensFromRequirements(this); return encounter; } @@ -2674,7 +2674,7 @@ export default class BattleScene extends SceneBase { tier = Overrides.MYSTERY_ENCOUNTER_TIER_OVERRIDE; } - let availableEncounters = []; + let availableEncounters: IMysteryEncounter[] = []; // New encounter will never be the same as the most recent encounter const previousEncounter = this.mysteryEncounterData.encounteredEvents?.length > 0 ? this.mysteryEncounterData.encounteredEvents[this.mysteryEncounterData.encounteredEvents.length - 1][0] : null; const biomeMysteryEncounters = mysteryEncountersByBiome.get(this.arena.biomeType); @@ -2695,7 +2695,7 @@ export default class BattleScene extends SceneBase { } encounter = availableEncounters[Utils.randSeedInt(availableEncounters.length)]; // New encounter object to not dirty flags - encounter = new MysteryEncounter(encounter); + encounter = new IMysteryEncounter(encounter); encounter.populateDialogueTokensFromRequirements(this); return encounter; } diff --git a/src/battle.ts b/src/battle.ts index 30543933f49..ab47eac7993 100644 --- a/src/battle.ts +++ b/src/battle.ts @@ -14,7 +14,7 @@ import { PlayerGender } from "#enums/player-gender"; import { Species } from "#enums/species"; import { TrainerType } from "#enums/trainer-type"; import i18next from "#app/plugins/i18n"; -import MysteryEncounter, { MysteryEncounterVariant } from "./data/mystery-encounters/mystery-encounter"; +import IMysteryEncounter, { MysteryEncounterVariant } from "./data/mystery-encounters/mystery-encounter"; export enum BattleType { WILD, @@ -69,7 +69,7 @@ export default class Battle { public lastUsedPokeball: PokeballType; public playerFaints: number; // The amount of times pokemon on the players side have fainted public enemyFaints: number; // The amount of times pokemon on the enemies side have fainted - public mysteryEncounter: MysteryEncounter; + public mysteryEncounter: IMysteryEncounter; private rngCounter: integer = 0; diff --git a/src/data/mystery-encounters/dialogue/dark-deal-dialogue.ts b/src/data/mystery-encounters/dialogue/dark-deal-dialogue.ts deleted file mode 100644 index 25b31f79a50..00000000000 --- a/src/data/mystery-encounters/dialogue/dark-deal-dialogue.ts +++ /dev/null @@ -1,48 +0,0 @@ -import MysteryEncounterDialogue from "#app/data/mystery-encounters/mystery-encounter-dialogue"; - -export const DarkDealDialogue: MysteryEncounterDialogue = { - intro: [ - { - text: "mysteryEncounter:dark_deal_intro_message" - }, - { - speaker: "mysteryEncounter:dark_deal_speaker", - text: "mysteryEncounter:dark_deal_intro_dialogue" - } - ], - encounterOptionsDialogue: { - title: "mysteryEncounter:dark_deal_title", - description: "mysteryEncounter:dark_deal_description", - query: "mysteryEncounter:dark_deal_query", - options: [ - { - buttonLabel: "mysteryEncounter:dark_deal_option_1_label", - buttonTooltip: "mysteryEncounter:dark_deal_option_1_tooltip", - selected: [ - { - speaker: "mysteryEncounter:dark_deal_speaker", - text: "mysteryEncounter:dark_deal_option_1_selected" - }, - { - text: "mysteryEncounter:dark_deal_option_1_selected_message" - } - ] - }, - { - buttonLabel: "mysteryEncounter:dark_deal_option_2_label", - buttonTooltip: "mysteryEncounter:dark_deal_option_2_tooltip", - selected: [ - { - speaker: "mysteryEncounter:dark_deal_speaker", - text: "mysteryEncounter:dark_deal_option_2_selected" - } - ] - } - ] - }, - outro: [ - { - text: "mysteryEncounter:dark_deal_outro" - } - ] -}; diff --git a/src/data/mystery-encounters/dialogue/department-store-sale-dialogue.ts b/src/data/mystery-encounters/dialogue/department-store-sale-dialogue.ts deleted file mode 100644 index 87cd8dbda55..00000000000 --- a/src/data/mystery-encounters/dialogue/department-store-sale-dialogue.ts +++ /dev/null @@ -1,36 +0,0 @@ -import MysteryEncounterDialogue from "#app/data/mystery-encounters/mystery-encounter-dialogue"; - -export const DepartmentStoreSaleDialogue: MysteryEncounterDialogue = { - intro: [ - { - text: "mysteryEncounter:department_store_sale_intro_message" - }, - { - text: "mysteryEncounter:department_store_sale_intro_dialogue", - speaker: "mysteryEncounter:department_store_sale_speaker" - } - ], - encounterOptionsDialogue: { - title: "mysteryEncounter:department_store_sale_title", - description: "mysteryEncounter:department_store_sale_description", - query: "mysteryEncounter:department_store_sale_query", - options: [ - { - buttonLabel: "mysteryEncounter:department_store_sale_option_1_label", - buttonTooltip: "mysteryEncounter:department_store_sale_option_1_tooltip" - }, - { - buttonLabel: "mysteryEncounter:department_store_sale_option_2_label", - buttonTooltip: "mysteryEncounter:department_store_sale_option_2_tooltip" - }, - { - buttonLabel: "mysteryEncounter:department_store_sale_option_3_label", - buttonTooltip: "mysteryEncounter:department_store_sale_option_3_tooltip" - }, - { - buttonLabel: "mysteryEncounter:department_store_sale_option_4_label", - buttonTooltip: "mysteryEncounter:department_store_sale_option_4_tooltip" - } - ] - } -}; diff --git a/src/data/mystery-encounters/dialogue/field-trip-dialogue.ts b/src/data/mystery-encounters/dialogue/field-trip-dialogue.ts deleted file mode 100644 index dca3f48f5bf..00000000000 --- a/src/data/mystery-encounters/dialogue/field-trip-dialogue.ts +++ /dev/null @@ -1,50 +0,0 @@ -import MysteryEncounterDialogue from "#app/data/mystery-encounters/mystery-encounter-dialogue"; - -export const FieldTripDialogue: MysteryEncounterDialogue = { - intro: [ - { - text: "mysteryEncounter:field_trip_intro_message" - }, - { - text: "mysteryEncounter:field_trip_intro_dialogue", - speaker: "mysteryEncounter:field_trip_speaker" - } - ], - encounterOptionsDialogue: { - title: "mysteryEncounter:field_trip_title", - description: "mysteryEncounter:field_trip_description", - query: "mysteryEncounter:field_trip_query", - options: [ - { - buttonLabel: "mysteryEncounter:field_trip_option_1_label", - buttonTooltip: "mysteryEncounter:field_trip_option_1_tooltip", - secondOptionPrompt: "mysteryEncounter:field_trip_second_option_prompt", - selected: [ - { - text: "mysteryEncounter:field_trip_option_selected" - } - ] - }, - { - buttonLabel: "mysteryEncounter:field_trip_option_2_label", - buttonTooltip: "mysteryEncounter:field_trip_option_2_tooltip", - secondOptionPrompt: "mysteryEncounter:field_trip_second_option_prompt", - selected: [ - { - text: "mysteryEncounter:field_trip_option_selected" - } - ] - }, - { - buttonLabel: "mysteryEncounter:field_trip_option_3_label", - buttonTooltip: "mysteryEncounter:field_trip_option_3_tooltip", - secondOptionPrompt: "mysteryEncounter:field_trip_second_option_prompt", - selected: [ - { - text: "mysteryEncounter:field_trip_option_selected" - } - ] - } - ] - } -}; diff --git a/src/data/mystery-encounters/dialogue/fight-or-flight-dialogue.ts b/src/data/mystery-encounters/dialogue/fight-or-flight-dialogue.ts deleted file mode 100644 index 93d46548d7f..00000000000 --- a/src/data/mystery-encounters/dialogue/fight-or-flight-dialogue.ts +++ /dev/null @@ -1,38 +0,0 @@ -import MysteryEncounterDialogue from "#app/data/mystery-encounters/mystery-encounter-dialogue"; - -export const FightOrFlightDialogue: MysteryEncounterDialogue = { - intro: [ - { - text: "mysteryEncounter:fight_or_flight_intro_message" - } - ], - encounterOptionsDialogue: { - title: "mysteryEncounter:fight_or_flight_title", - description: "mysteryEncounter:fight_or_flight_description", - query: "mysteryEncounter:fight_or_flight_query", - options: [ - { - buttonLabel: "mysteryEncounter:fight_or_flight_option_1_label", - buttonTooltip: "mysteryEncounter:fight_or_flight_option_1_tooltip", - selected: [ - { - text: "mysteryEncounter:fight_or_flight_option_1_selected_message" - } - ] - }, - { - buttonLabel: "mysteryEncounter:fight_or_flight_option_2_label", - buttonTooltip: "mysteryEncounter:fight_or_flight_option_2_tooltip" - }, - { - buttonLabel: "mysteryEncounter:fight_or_flight_option_3_label", - buttonTooltip: "mysteryEncounter:fight_or_flight_option_3_tooltip", - selected: [ - { - text: "mysteryEncounter:fight_or_flight_option_3_selected" - } - ] - } - ] - } -}; diff --git a/src/data/mystery-encounters/dialogue/mysterious-challengers-dialogue.ts b/src/data/mystery-encounters/dialogue/mysterious-challengers-dialogue.ts deleted file mode 100644 index 065cfceb1b7..00000000000 --- a/src/data/mystery-encounters/dialogue/mysterious-challengers-dialogue.ts +++ /dev/null @@ -1,57 +0,0 @@ -import MysteryEncounterDialogue from "#app/data/mystery-encounters/mystery-encounter-dialogue"; - -export const MysteriousChallengersDialogue: MysteryEncounterDialogue = { - intro: [ - { - text: "mysteryEncounter:mysterious_challengers_intro_message" - } - ], - encounterOptionsDialogue: { - title: "mysteryEncounter:mysterious_challengers_title", - description: "mysteryEncounter:mysterious_challengers_description", - query: "mysteryEncounter:mysterious_challengers_query", - options: [ - { - buttonLabel: "mysteryEncounter:mysterious_challengers_option_1_label", - buttonTooltip: "mysteryEncounter:mysterious_challengers_option_1_tooltip", - selected: [ - { - text: "mysteryEncounter:mysterious_challengers_option_selected_message" - } - ] - }, - { - buttonLabel: "mysteryEncounter:mysterious_challengers_option_2_label", - buttonTooltip: "mysteryEncounter:mysterious_challengers_option_2_tooltip", - selected: [ - { - text: "mysteryEncounter:mysterious_challengers_option_selected_message" - } - ] - }, - { - buttonLabel: "mysteryEncounter:mysterious_challengers_option_3_label", - buttonTooltip: "mysteryEncounter:mysterious_challengers_option_3_tooltip", - selected: [ - { - text: "mysteryEncounter:mysterious_challengers_option_selected_message" - } - ] - }, - { - buttonLabel: "mysteryEncounter:mysterious_challengers_option_4_label", - buttonTooltip: "mysteryEncounter:mysterious_challengers_option_4_tooltip", - selected: [ - { - text: "mysteryEncounter:mysterious_challengers_option_4_selected_message" - } - ] - } - ] - }, - outro: [ - { - text: "mysteryEncounter:mysterious_challengers_outro_win" - } - ] -}; diff --git a/src/data/mystery-encounters/dialogue/mysterious-chest-dialogue.ts b/src/data/mystery-encounters/dialogue/mysterious-chest-dialogue.ts deleted file mode 100644 index ba8dafc6826..00000000000 --- a/src/data/mystery-encounters/dialogue/mysterious-chest-dialogue.ts +++ /dev/null @@ -1,34 +0,0 @@ -import MysteryEncounterDialogue from "#app/data/mystery-encounters/mystery-encounter-dialogue"; - -export const MysteriousChestDialogue: MysteryEncounterDialogue = { - intro: [ - { - text: "mysteryEncounter:mysterious_chest_intro_message" - } - ], - encounterOptionsDialogue: { - title: "mysteryEncounter:mysterious_chest_title", - description: "mysteryEncounter:mysterious_chest_description", - query: "mysteryEncounter:mysterious_chest_query", - options: [ - { - buttonLabel: "mysteryEncounter:mysterious_chest_option_1_label", - buttonTooltip: "mysteryEncounter:mysterious_chest_option_1_tooltip", - selected: [ - { - text: "mysteryEncounter:mysterious_chest_option_1_selected_message" - } - ] - }, - { - buttonLabel: "mysteryEncounter:mysterious_chest_option_2_label", - buttonTooltip: "mysteryEncounter:mysterious_chest_option_2_tooltip", - selected: [ - { - text: "mysteryEncounter:mysterious_chest_option_2_selected_message" - } - ] - } - ] - } -}; diff --git a/src/data/mystery-encounters/dialogue/shady-vitamin-dealer.ts b/src/data/mystery-encounters/dialogue/shady-vitamin-dealer.ts deleted file mode 100644 index 52b9741caf8..00000000000 --- a/src/data/mystery-encounters/dialogue/shady-vitamin-dealer.ts +++ /dev/null @@ -1,42 +0,0 @@ -import MysteryEncounterDialogue from "#app/data/mystery-encounters/mystery-encounter-dialogue"; - -export const ShadyVitaminDealerDialogue: MysteryEncounterDialogue = { - intro: [ - { - text: "mysteryEncounter:shady_vitamin_dealer_intro_message" - }, - { - text: "mysteryEncounter:shady_vitamin_dealer_intro_dialogue", - speaker: "mysteryEncounter:shady_vitamin_dealer_speaker" - } - ], - encounterOptionsDialogue: { - title: "mysteryEncounter:shady_vitamin_dealer_title", - description: "mysteryEncounter:shady_vitamin_dealer_description", - query: "mysteryEncounter:shady_vitamin_dealer_query", - options: [ - { - buttonLabel: "mysteryEncounter:shady_vitamin_dealer_option_1_label", - buttonTooltip: "mysteryEncounter:shady_vitamin_dealer_option_1_tooltip", - selected: [ - { - text: "mysteryEncounter:shady_vitamin_dealer_option_selected" - } - ] - }, - { - buttonLabel: "mysteryEncounter:shady_vitamin_dealer_option_2_label", - buttonTooltip: "mysteryEncounter:shady_vitamin_dealer_option_2_tooltip", - selected: [ - { - text: "mysteryEncounter:shady_vitamin_dealer_option_selected" - } - ] - }, - { - buttonLabel: "mysteryEncounter:shady_vitamin_dealer_option_3_label", - buttonTooltip: "mysteryEncounter:shady_vitamin_dealer_option_3_tooltip" - } - ] - } -}; diff --git a/src/data/mystery-encounters/dialogue/sleeping-snorlax-dialogue.ts b/src/data/mystery-encounters/dialogue/sleeping-snorlax-dialogue.ts deleted file mode 100644 index c10835f728e..00000000000 --- a/src/data/mystery-encounters/dialogue/sleeping-snorlax-dialogue.ts +++ /dev/null @@ -1,39 +0,0 @@ -import MysteryEncounterDialogue from "#app/data/mystery-encounters/mystery-encounter-dialogue"; - -export const SleepingSnorlaxDialogue: MysteryEncounterDialogue = { - intro: [ - { - text: "mysteryEncounter:sleeping_snorlax_intro_message" - } - ], - encounterOptionsDialogue: { - title: "mysteryEncounter:sleeping_snorlax_title", - description: "mysteryEncounter:sleeping_snorlax_description", - query: "mysteryEncounter:sleeping_snorlax_query", - options: [ - { - buttonLabel: "mysteryEncounter:sleeping_snorlax_option_1_label", - buttonTooltip: "mysteryEncounter:sleeping_snorlax_option_1_tooltip", - selected: [ - { - text: "mysteryEncounter:sleeping_snorlax_option_1_selected_message" - } - ] - }, - { - buttonLabel: "mysteryEncounter:sleeping_snorlax_option_2_label", - buttonTooltip: "mysteryEncounter:sleeping_snorlax_option_2_tooltip", - selected: [ - { - text: "mysteryEncounter:sleeping_snorlax_option_2_selected_message" - } - ] - }, - { - buttonLabel: "mysteryEncounter:sleeping_snorlax_option_3_label", - buttonTooltip: "mysteryEncounter:sleeping_snorlax_option_3_tooltip", - disabledTooltip: "mysteryEncounter:sleeping_snorlax_option_3_disabled_tooltip" - } - ] - } -}; diff --git a/src/data/mystery-encounters/dialogue/training-session-dialogue.ts b/src/data/mystery-encounters/dialogue/training-session-dialogue.ts deleted file mode 100644 index 87f30ca14c2..00000000000 --- a/src/data/mystery-encounters/dialogue/training-session-dialogue.ts +++ /dev/null @@ -1,50 +0,0 @@ -import MysteryEncounterDialogue from "#app/data/mystery-encounters/mystery-encounter-dialogue"; - -export const TrainingSessionDialogue: MysteryEncounterDialogue = { - intro: [ - { - text: "mysteryEncounter:training_session_intro_message" - } - ], - encounterOptionsDialogue: { - title: "mysteryEncounter:training_session_title", - description: "mysteryEncounter:training_session_description", - query: "mysteryEncounter:training_session_query", - options: [ - { - buttonLabel: "mysteryEncounter:training_session_option_1_label", - buttonTooltip: "mysteryEncounter:training_session_option_1_tooltip", - selected: [ - { - text: "mysteryEncounter:training_session_option_selected_message" - } - ] - }, - { - buttonLabel: "mysteryEncounter:training_session_option_2_label", - buttonTooltip: "mysteryEncounter:training_session_option_2_tooltip", - secondOptionPrompt: "mysteryEncounter:training_session_option_2_select_prompt", - selected: [ - { - text: "mysteryEncounter:training_session_option_selected_message" - } - ] - }, - { - buttonLabel: "mysteryEncounter:training_session_option_3_label", - buttonTooltip: "mysteryEncounter:training_session_option_3_tooltip", - secondOptionPrompt: "mysteryEncounter:training_session_option_3_select_prompt", - selected: [ - { - text: "mysteryEncounter:training_session_option_selected_message" - } - ] - } - ] - }, - outro: [ - { - text: "mysteryEncounter:training_session_outro_win" - } - ] -}; diff --git a/src/data/mystery-encounters/encounters/dark-deal-encounter.ts b/src/data/mystery-encounters/encounters/dark-deal-encounter.ts new file mode 100644 index 00000000000..9d755ad9493 --- /dev/null +++ b/src/data/mystery-encounters/encounters/dark-deal-encounter.ts @@ -0,0 +1,207 @@ +import { Type } from "#app/data/type"; +import { ModifierRewardPhase } from "#app/phases"; +import { isNullOrUndefined, randSeedInt } from "#app/utils"; +import { MysteryEncounterType } from "#enums/mystery-encounter-type"; +import { Species } from "#enums/species"; +import BattleScene from "../../../battle-scene"; +import { AddPokeballModifierType } from "../../../modifier/modifier-type"; +import { PokeballType } from "../../pokeball"; +import { getPokemonSpecies } from "../../pokemon-species"; +import IMysteryEncounter, { + MysteryEncounterBuilder, + MysteryEncounterTier, +} from "../mystery-encounter"; +import { MysteryEncounterOptionBuilder } from "../mystery-encounter-option"; +import { + EnemyPartyConfig, + EnemyPokemonConfig, + getRandomPlayerPokemon, + getRandomSpeciesByStarterTier, + initBattleWithEnemyConfig, + leaveEncounterWithoutBattle, +} from "../mystery-encounter-utils"; + +/** i18n namespace for encounter */ +const namespace = "mysteryEncounter:dark_deal"; + +// Exclude Ultra Beasts, Paradox, Necrozma, Eternatus, and egg-locked mythicals +const excludedBosses = [ + Species.NECROZMA, + Species.ETERNATUS, + Species.NIHILEGO, + Species.BUZZWOLE, + Species.PHEROMOSA, + Species.XURKITREE, + Species.CELESTEELA, + Species.KARTANA, + Species.GUZZLORD, + Species.POIPOLE, + Species.NAGANADEL, + Species.STAKATAKA, + Species.BLACEPHALON, + Species.GREAT_TUSK, + Species.SCREAM_TAIL, + Species.BRUTE_BONNET, + Species.FLUTTER_MANE, + Species.SLITHER_WING, + Species.SANDY_SHOCKS, + Species.ROARING_MOON, + Species.KORAIDON, + Species.WALKING_WAKE, + Species.GOUGING_FIRE, + Species.RAGING_BOLT, + Species.IRON_TREADS, + Species.IRON_BUNDLE, + Species.IRON_HANDS, + Species.IRON_JUGULIS, + Species.IRON_MOTH, + Species.IRON_THORNS, + Species.IRON_VALIANT, + Species.MIRAIDON, + Species.IRON_LEAVES, + Species.IRON_BOULDER, + Species.IRON_CROWN, + Species.MEW, + Species.CELEBI, + Species.DEOXYS, + Species.JIRACHI, + Species.PHIONE, + Species.MANAPHY, + Species.ARCEUS, + Species.VICTINI, + Species.MELTAN, + Species.PECHARUNT, +]; + +export const DarkDealEncounter: IMysteryEncounter = + MysteryEncounterBuilder.withEncounterType(MysteryEncounterType.DARK_DEAL) + .withEncounterTier(MysteryEncounterTier.ROGUE) + .withIntroSpriteConfigs([ + { + spriteKey: "mad_scientist_m", + fileRoot: "mystery-encounters", + hasShadow: true, + }, + { + spriteKey: "dark_deal_porygon", + fileRoot: "mystery-encounters", + hasShadow: true, + repeat: true, + }, + ]) + .withIntroDialogue([ + { + text: `${namespace}_intro_message`, + }, + { + speaker: `${namespace}_speaker`, + text: `${namespace}_intro_dialogue`, + }, + ]) + .withSceneWaveRangeRequirement(30, 180) // waves 30 to 180 + .withScenePartySizeRequirement(2, 6) // Must have at least 2 pokemon in party + .withCatchAllowed(true) + .withTitle(`${namespace}_title`) + .withDescription(`${namespace}_description`) + .withQuery(`${namespace}_query`) + .withOption( + new MysteryEncounterOptionBuilder() + .withDialogue({ + buttonLabel: `${namespace}_option_1_label`, + buttonTooltip: `${namespace}_option_1_tooltip`, + selected: [ + { + speaker: `${namespace}_speaker`, + text: `${namespace}_option_1_selected`, + }, + { + text: `${namespace}_option_1_selected_message`, + }, + ], + }) + .withPreOptionPhase(async (scene: BattleScene) => { + // Removes random pokemon (including fainted) from party and adds name to dialogue data tokens + // Will never return last battle able mon and instead pick fainted/unable to battle + const removedPokemon = getRandomPlayerPokemon(scene, false, true); + scene.removePokemonFromPlayerParty(removedPokemon); + + scene.currentBattle.mysteryEncounter.setDialogueToken( + "pokeName", + removedPokemon.name + ); + + // Store removed pokemon types + scene.currentBattle.mysteryEncounter.misc = [ + removedPokemon.species.type1, + ]; + if (removedPokemon.species.type2) { + scene.currentBattle.mysteryEncounter.misc.push( + removedPokemon.species.type2 + ); + } + }) + .withOptionPhase(async (scene: BattleScene) => { + // Give the player 5 Rogue Balls + scene.unshiftPhase( + new ModifierRewardPhase( + scene, + () => + new AddPokeballModifierType("rb", PokeballType.ROGUE_BALL, 5) + ) + ); + + // Start encounter with random legendary (7-10 starter strength) that has level additive + const bossTypes = scene.currentBattle.mysteryEncounter.misc as Type[]; + // Starter egg tier, 35/50/10/5 %odds for tiers 6/7/8/9+ + const roll = randSeedInt(100); + const starterTier: number | [number, number] = + roll > 65 ? 6 : roll > 15 ? 7 : roll > 5 ? 8 : [9, 10]; + const bossSpecies = getPokemonSpecies( + getRandomSpeciesByStarterTier( + starterTier, + excludedBosses, + bossTypes + ) + ); + const pokemonConfig: EnemyPokemonConfig = { + species: bossSpecies, + isBoss: true, + }; + if ( + !isNullOrUndefined(bossSpecies.forms) && + bossSpecies.forms.length > 0 + ) { + pokemonConfig.formIndex = 0; + } + const config: EnemyPartyConfig = { + levelAdditiveMultiplier: 0.75, + pokemonConfigs: [pokemonConfig], + }; + return initBattleWithEnemyConfig(scene, config); + }) + .build() + ) + .withSimpleOption( + { + buttonLabel: `${namespace}_option_2_label`, + buttonTooltip: `${namespace}_option_2_tooltip`, + selected: [ + { + speaker: `${namespace}_speaker`, + text: `${namespace}_option_2_selected`, + }, + ], + }, + async (scene: BattleScene) => { + // Leave encounter with no rewards or exp + + leaveEncounterWithoutBattle(scene, true); + return true; + } + ) + .withOutroDialogue([ + { + text: `${namespace}_outro` + } + ]) + .build(); diff --git a/src/data/mystery-encounters/encounters/dark-deal.ts b/src/data/mystery-encounters/encounters/dark-deal.ts deleted file mode 100644 index 1985ffcf8dd..00000000000 --- a/src/data/mystery-encounters/encounters/dark-deal.ts +++ /dev/null @@ -1,133 +0,0 @@ -import { Type } from "#app/data/type"; -import { ModifierRewardPhase } from "#app/phases"; -import { isNullOrUndefined, randSeedInt } from "#app/utils"; -import { MysteryEncounterType } from "#enums/mystery-encounter-type"; -import { Species } from "#enums/species"; -import BattleScene from "../../../battle-scene"; -import { AddPokeballModifierType } from "../../../modifier/modifier-type"; -import { PokeballType } from "../../pokeball"; -import { getPokemonSpecies } from "../../pokemon-species"; -import MysteryEncounter, { MysteryEncounterBuilder, MysteryEncounterTier } from "../mystery-encounter"; -import { MysteryEncounterOptionBuilder } from "../mystery-encounter-option"; -import { - EnemyPartyConfig, EnemyPokemonConfig, - getRandomPlayerPokemon, - getRandomSpeciesByStarterTier, - initBattleWithEnemyConfig, - leaveEncounterWithoutBattle -} from "../mystery-encounter-utils"; - -// Exclude Ultra Beasts, Paradox, Necrozma, Eternatus, and egg-locked mythicals -const excludedBosses = [ - Species.NECROZMA, - Species.ETERNATUS, - Species.NIHILEGO, - Species.BUZZWOLE, - Species.PHEROMOSA, - Species.XURKITREE, - Species.CELESTEELA, - Species.KARTANA, - Species.GUZZLORD, - Species.POIPOLE, - Species.NAGANADEL, - Species.STAKATAKA, - Species.BLACEPHALON, - Species.GREAT_TUSK, - Species.SCREAM_TAIL, - Species.BRUTE_BONNET, - Species.FLUTTER_MANE, - Species.SLITHER_WING, - Species.SANDY_SHOCKS, - Species.ROARING_MOON, - Species.KORAIDON, - Species.WALKING_WAKE, - Species.GOUGING_FIRE, - Species.RAGING_BOLT, - Species.IRON_TREADS, - Species.IRON_BUNDLE, - Species.IRON_HANDS, - Species.IRON_JUGULIS, - Species.IRON_MOTH, - Species.IRON_THORNS, - Species.IRON_VALIANT, - Species.MIRAIDON, - Species.IRON_LEAVES, - Species.IRON_BOULDER, - Species.IRON_CROWN, - Species.MEW, - Species.CELEBI, - Species.DEOXYS, - Species.JIRACHI, - Species.PHIONE, - Species.MANAPHY, - Species.ARCEUS, - Species.VICTINI, - Species.MELTAN, - Species.PECHARUNT -]; - -export const DarkDealEncounter: MysteryEncounter = MysteryEncounterBuilder - .withEncounterType(MysteryEncounterType.DARK_DEAL) - .withEncounterTier(MysteryEncounterTier.ROGUE) - .withIntroSpriteConfigs([ - { - spriteKey: "mad_scientist_m", - fileRoot: "mystery-encounters", - hasShadow: true - }, - { - spriteKey: "dark_deal_porygon", - fileRoot: "mystery-encounters", - hasShadow: true, - repeat: true - } - ]) - .withSceneWaveRangeRequirement(30, 180) // waves 30 to 180 - .withScenePartySizeRequirement(2, 6) // Must have at least 2 pokemon in party - .withCatchAllowed(true) - .withOption(new MysteryEncounterOptionBuilder() - .withPreOptionPhase(async (scene: BattleScene) => { - // Removes random pokemon (including fainted) from party and adds name to dialogue data tokens - // Will never return last battle able mon and instead pick fainted/unable to battle - const removedPokemon = getRandomPlayerPokemon(scene, false, true); - scene.removePokemonFromPlayerParty(removedPokemon); - - scene.currentBattle.mysteryEncounter.setDialogueToken("pokeName", removedPokemon.name); - - // Store removed pokemon types - scene.currentBattle.mysteryEncounter.misc = [removedPokemon.species.type1]; - if (removedPokemon.species.type2) { - scene.currentBattle.mysteryEncounter.misc.push(removedPokemon.species.type2); - } - }) - .withOptionPhase(async (scene: BattleScene) => { - // Give the player 5 Rogue Balls - scene.unshiftPhase(new ModifierRewardPhase(scene, () => new AddPokeballModifierType("rb", PokeballType.ROGUE_BALL, 5))); - - // Start encounter with random legendary (7-10 starter strength) that has level additive - const bossTypes = scene.currentBattle.mysteryEncounter.misc as Type[]; - // Starter egg tier, 35/50/10/5 %odds for tiers 6/7/8/9+ - const roll = randSeedInt(100); - const starterTier: number | [number, number] = roll > 65 ? 6 : roll > 15 ? 7 : roll > 5 ? 8 : [9, 10]; - const bossSpecies = getPokemonSpecies(getRandomSpeciesByStarterTier(starterTier, excludedBosses, bossTypes)); - const pokemonConfig: EnemyPokemonConfig = { - species: bossSpecies, - isBoss: true - }; - if (!isNullOrUndefined(bossSpecies.forms) && bossSpecies.forms.length > 0) { - pokemonConfig.formIndex = 0; - } - const config: EnemyPartyConfig = { - levelAdditiveMultiplier: 0.75, - pokemonConfigs: [pokemonConfig] - }; - return initBattleWithEnemyConfig(scene, config); - }) - .build() - ) - .withOptionPhase(async (scene: BattleScene) => { - // Leave encounter with no rewards or exp - leaveEncounterWithoutBattle(scene, true); - return true; - }) - .build(); diff --git a/src/data/mystery-encounters/encounters/department-store-sale-encounter.ts b/src/data/mystery-encounters/encounters/department-store-sale-encounter.ts new file mode 100644 index 00000000000..d821a952864 --- /dev/null +++ b/src/data/mystery-encounters/encounters/department-store-sale-encounter.ts @@ -0,0 +1,166 @@ +import { + leaveEncounterWithoutBattle, + setEncounterRewards, +} from "#app/data/mystery-encounters/mystery-encounter-utils"; +import { modifierTypes } from "#app/modifier/modifier-type"; +import { randSeedInt } from "#app/utils"; +import { MysteryEncounterType } from "#enums/mystery-encounter-type"; +import { Species } from "#enums/species"; +import BattleScene from "../../../battle-scene"; +import IMysteryEncounter, { + MysteryEncounterBuilder, + MysteryEncounterTier, +} from "../mystery-encounter"; + +/** i18n namespace for encounter */ +const namespace = "mysteryEncounter:department_store_sale"; + +export const DepartmentStoreSaleEncounter: IMysteryEncounter = + MysteryEncounterBuilder.withEncounterType( + MysteryEncounterType.DEPARTMENT_STORE_SALE + ) + .withEncounterTier(MysteryEncounterTier.COMMON) + .withSceneWaveRangeRequirement(10, 100) + .withIntroSpriteConfigs([ + { + spriteKey: "b2w2_lady", + fileRoot: "mystery-encounters", + hasShadow: true, + x: -20, + }, + { + spriteKey: Species.FURFROU.toString(), + fileRoot: "pokemon", + hasShadow: true, + repeat: true, + x: 30, + }, + ]) + .withIntroDialogue([ + { + text: `${namespace}_intro_message`, + }, + { + text: `${namespace}_intro_dialogue`, + speaker: `${namespace}_speaker`, + }, + ]) + // .withHideIntroVisuals(false) + .withTitle(`${namespace}_title`) + .withDescription(`${namespace}_description`) + .withQuery(`${namespace}_query`) + .withSimpleOption( + { + buttonLabel: `${namespace}_option_1_label`, + buttonTooltip: `${namespace}_option_1_tooltip`, + }, + async (scene: BattleScene) => { + // Choose TMs + const modifiers = []; + let i = 0; + while (i < 4) { + // 2/2/1 weight on TM rarity + const roll = randSeedInt(5); + if (roll < 2) { + modifiers.push(modifierTypes.TM_COMMON); + } else if (roll < 4) { + modifiers.push(modifierTypes.TM_GREAT); + } else { + modifiers.push(modifierTypes.TM_ULTRA); + } + i++; + } + + setEncounterRewards(scene, { + guaranteedModifierTypeFuncs: modifiers, + fillRemaining: false, + }); + leaveEncounterWithoutBattle(scene); + } + ) + .withSimpleOption( + { + buttonLabel: `${namespace}_option_2_label`, + buttonTooltip: `${namespace}_option_2_tooltip`, + }, + async (scene: BattleScene) => { + // Choose Vitamins + const modifiers = []; + let i = 0; + while (i < 3) { + // 2/1 weight on base stat booster vs PP Up + const roll = randSeedInt(3); + if (roll === 0) { + modifiers.push(modifierTypes.PP_UP); + } else { + modifiers.push(modifierTypes.BASE_STAT_BOOSTER); + } + i++; + } + + setEncounterRewards(scene, { + guaranteedModifierTypeFuncs: modifiers, + fillRemaining: false, + }); + leaveEncounterWithoutBattle(scene); + } + ) + .withSimpleOption( + { + buttonLabel: `${namespace}_option_3_label`, + buttonTooltip: `${namespace}_option_3_tooltip`, + }, + async (scene: BattleScene) => { + // Choose X Items + const modifiers = []; + let i = 0; + while (i < 5) { + // 4/1 weight on base stat booster vs Dire Hit + const roll = randSeedInt(5); + if (roll === 0) { + modifiers.push(modifierTypes.DIRE_HIT); + } else { + modifiers.push(modifierTypes.TEMP_STAT_BOOSTER); + } + i++; + } + + setEncounterRewards(scene, { + guaranteedModifierTypeFuncs: modifiers, + fillRemaining: false, + }); + leaveEncounterWithoutBattle(scene); + } + ) + .withSimpleOption( + { + buttonLabel: `${namespace}_option_4_label`, + buttonTooltip: `${namespace}_option_4_tooltip`, + }, + async (scene: BattleScene) => { + // Choose Pokeballs + const modifiers = []; + let i = 0; + while (i < 4) { + // 10/30/20/5 weight on pokeballs + const roll = randSeedInt(65); + if (roll < 10) { + modifiers.push(modifierTypes.POKEBALL); + } else if (roll < 40) { + modifiers.push(modifierTypes.GREAT_BALL); + } else if (roll < 60) { + modifiers.push(modifierTypes.ULTRA_BALL); + } else { + modifiers.push(modifierTypes.ROGUE_BALL); + } + i++; + } + + setEncounterRewards(scene, { + guaranteedModifierTypeFuncs: modifiers, + fillRemaining: false, + }); + leaveEncounterWithoutBattle(scene); + } + ) + .build(); diff --git a/src/data/mystery-encounters/encounters/department-store-sale.ts b/src/data/mystery-encounters/encounters/department-store-sale.ts deleted file mode 100644 index 19269a774f3..00000000000 --- a/src/data/mystery-encounters/encounters/department-store-sale.ts +++ /dev/null @@ -1,110 +0,0 @@ -import { - leaveEncounterWithoutBattle, - setEncounterRewards, -} from "#app/data/mystery-encounters/mystery-encounter-utils"; -import { modifierTypes } from "#app/modifier/modifier-type"; -import { randSeedInt } from "#app/utils"; -import { MysteryEncounterType } from "#enums/mystery-encounter-type"; -import { Species } from "#enums/species"; -import BattleScene from "../../../battle-scene"; -import MysteryEncounter, { MysteryEncounterBuilder, MysteryEncounterTier } from "../mystery-encounter"; - -export const DepartmentStoreSaleEncounter: MysteryEncounter = MysteryEncounterBuilder - .withEncounterType(MysteryEncounterType.DEPARTMENT_STORE_SALE) - .withEncounterTier(MysteryEncounterTier.COMMON) - .withIntroSpriteConfigs([ - { - spriteKey: "b2w2_lady", - fileRoot: "mystery-encounters", - hasShadow: true, - x: -20 - }, - { - spriteKey: Species.FURFROU.toString(), - fileRoot: "pokemon", - hasShadow: true, - repeat: true, - x: 30 - } - ]) - // .withHideIntroVisuals(false) - .withSceneWaveRangeRequirement(10, 100) - .withOptionPhase(async (scene: BattleScene) => { - // Choose TMs - const modifiers = []; - let i = 0; - while (i < 4) { - // 2/2/1 weight on TM rarity - const roll = randSeedInt(5); - if (roll < 2) { - modifiers.push(modifierTypes.TM_COMMON); - } else if (roll < 4) { - modifiers.push(modifierTypes.TM_GREAT); - } else { - modifiers.push(modifierTypes.TM_ULTRA); - } - i++; - } - - setEncounterRewards(scene, { guaranteedModifierTypeFuncs: modifiers, fillRemaining: false }); - leaveEncounterWithoutBattle(scene); - }) - .withOptionPhase(async (scene: BattleScene) => { - // Choose Vitamins - const modifiers = []; - let i = 0; - while (i < 3) { - // 2/1 weight on base stat booster vs PP Up - const roll = randSeedInt(3); - if (roll === 0) { - modifiers.push(modifierTypes.PP_UP); - } else { - modifiers.push(modifierTypes.BASE_STAT_BOOSTER); - } - i++; - } - - setEncounterRewards(scene, { guaranteedModifierTypeFuncs: modifiers, fillRemaining: false }); - leaveEncounterWithoutBattle(scene); - }) - .withOptionPhase(async (scene: BattleScene) => { - // Choose X Items - const modifiers = []; - let i = 0; - while (i < 5) { - // 4/1 weight on base stat booster vs Dire Hit - const roll = randSeedInt(5); - if (roll === 0) { - modifiers.push(modifierTypes.DIRE_HIT); - } else { - modifiers.push(modifierTypes.TEMP_STAT_BOOSTER); - } - i++; - } - - setEncounterRewards(scene, { guaranteedModifierTypeFuncs: modifiers, fillRemaining: false }); - leaveEncounterWithoutBattle(scene); - }) - .withOptionPhase(async (scene: BattleScene) => { - // Choose Pokeballs - const modifiers = []; - let i = 0; - while (i < 4) { - // 10/30/20/5 weight on pokeballs - const roll = randSeedInt(65); - if (roll < 10) { - modifiers.push(modifierTypes.POKEBALL); - } else if (roll < 40) { - modifiers.push(modifierTypes.GREAT_BALL); - } else if (roll < 60) { - modifiers.push(modifierTypes.ULTRA_BALL); - } else { - modifiers.push(modifierTypes.ROGUE_BALL); - } - i++; - } - - setEncounterRewards(scene, { guaranteedModifierTypeFuncs: modifiers, fillRemaining: false }); - leaveEncounterWithoutBattle(scene); - }) - .build(); diff --git a/src/data/mystery-encounters/encounters/field-trip-encounter.ts b/src/data/mystery-encounters/encounters/field-trip-encounter.ts index 410df127b6e..9235504e332 100644 --- a/src/data/mystery-encounters/encounters/field-trip-encounter.ts +++ b/src/data/mystery-encounters/encounters/field-trip-encounter.ts @@ -1,220 +1,333 @@ -import { generateModifierTypeOption, leaveEncounterWithoutBattle, selectPokemonForOption, setEncounterExp, setEncounterRewards, } from "#app/data/mystery-encounters/mystery-encounter-utils"; +import { MoveCategory } from "#app/data/move"; +import { MysteryEncounterOptionBuilder } from "#app/data/mystery-encounters/mystery-encounter-option"; +import { + generateModifierTypeOption, + leaveEncounterWithoutBattle, + selectPokemonForOption, + setEncounterExp, + setEncounterRewards, +} from "#app/data/mystery-encounters/mystery-encounter-utils"; +import { TempBattleStat } from "#app/data/temp-battle-stat"; +import { PlayerPokemon, PokemonMove } from "#app/field/pokemon"; import { modifierTypes } from "#app/modifier/modifier-type"; +import { OptionSelectItem } from "#app/ui/abstact-option-select-ui-handler"; import { MysteryEncounterType } from "#enums/mystery-encounter-type"; import BattleScene from "../../../battle-scene"; -import MysteryEncounter, { MysteryEncounterBuilder, MysteryEncounterTier } from "../mystery-encounter"; -import { MysteryEncounterOptionBuilder } from "#app/data/mystery-encounters/mystery-encounter-option"; -import { PlayerPokemon, PokemonMove } from "#app/field/pokemon"; -import { OptionSelectItem } from "#app/ui/abstact-option-select-ui-handler"; -import { MoveCategory } from "#app/data/move"; -import { TempBattleStat } from "#app/data/temp-battle-stat"; +import IMysteryEncounter, { + MysteryEncounterBuilder, + MysteryEncounterTier, +} from "../mystery-encounter"; -export const FieldTripEncounter: MysteryEncounter = MysteryEncounterBuilder - .withEncounterType(MysteryEncounterType.FIELD_TRIP) - .withEncounterTier(MysteryEncounterTier.COMMON) - .withIntroSpriteConfigs([ - { - spriteKey: "preschooler_m", - fileRoot: "trainer", - hasShadow: true - }, - { - spriteKey: "teacher", - fileRoot: "mystery-encounters", - hasShadow: true - }, - { - spriteKey: "preschooler_f", - fileRoot: "trainer", - hasShadow: true - }, - ]) - .withHideIntroVisuals(false) - .withSceneWaveRangeRequirement(10, 180) - .withOption(new MysteryEncounterOptionBuilder() - .withPreOptionPhase(async (scene: BattleScene): Promise => { - const encounter = scene.currentBattle.mysteryEncounter; - const onPokemonSelected = (pokemon: PlayerPokemon) => { - // Return the options for Pokemon move valid for this option - return pokemon.moveset.map((move: PokemonMove) => { - const option: OptionSelectItem = { - label: move.getName(), - handler: () => { - // Pokemon and move selected - const correctMove = move.getMove().category === MoveCategory.PHYSICAL; - encounter.setDialogueToken("moveCategory", "Physical"); - if (!correctMove) { - encounter.dialogue.encounterOptionsDialogue.options[0].selected = [ - { - text: "mysteryEncounter:field_trip_option_incorrect", - speaker: "mysteryEncounter:field_trip_speaker" - }, - { - text: "mysteryEncounter:field_trip_lesson_learned", +/** i18n namespace for the encounter */ +const namespace = "mysteryEncounter:field_trip"; + +export const FieldTripEncounter: IMysteryEncounter = + MysteryEncounterBuilder.withEncounterType(MysteryEncounterType.FIELD_TRIP) + .withEncounterTier(MysteryEncounterTier.COMMON) + .withSceneWaveRangeRequirement(10, 180) + .withIntroSpriteConfigs([ + { + spriteKey: "preschooler_m", + fileRoot: "trainer", + hasShadow: true, + }, + { + spriteKey: "teacher", + fileRoot: "mystery-encounters", + hasShadow: true, + }, + { + spriteKey: "preschooler_f", + fileRoot: "trainer", + hasShadow: true, + }, + ]) + .withIntroDialogue([ + { + text: `${namespace}_intro_message`, + }, + { + text: `${namespace}_intro_dialogue`, + speaker: `${namespace}_speaker`, + }, + ]) + .withHideIntroVisuals(false) + .withTitle(`${namespace}_title`) + .withDescription(`${namespace}_description`) + .withQuery(`${namespace}_query`) + .withOption( + new MysteryEncounterOptionBuilder() + .withDialogue({ + 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; + const onPokemonSelected = (pokemon: PlayerPokemon) => { + // Return the options for Pokemon move valid for this option + return pokemon.moveset.map((move: PokemonMove) => { + const option: OptionSelectItem = { + 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}_lesson_learned`, + }, + ]; + setEncounterExp( + scene, + scene.getParty().map((p) => p.id), + 50 + ); + } else { + encounter.setDialogueToken("pokeName", pokemon.name); + encounter.setDialogueToken("move", move.getName()); + encounter.options[0].dialogue.selected = [ + { + text: `${namespace}_option_selected`, + }, + ]; + setEncounterExp(scene, [pokemon.id], 100); } - ]; - setEncounterExp(scene, scene.getParty().map(p => p.id), 50); - } else { - encounter.setDialogueToken("pokeName", pokemon.name); - encounter.setDialogueToken("move", move.getName()); - encounter.dialogue.encounterOptionsDialogue.options[0].selected = [ - { - text: "mysteryEncounter:field_trip_option_selected" - } - ]; - setEncounterExp(scene, [pokemon.id], 100); - } - encounter.misc = { - correctMove: correctMove + encounter.misc = { + correctMove: correctMove, + }; + return true; + }, }; - return true; - } + return option; + }); }; - return option; - }); - }; - return selectPokemonForOption(scene, onPokemonSelected); - }) - .withOptionPhase(async (scene: BattleScene) => { - const encounter = scene.currentBattle.mysteryEncounter; - if (encounter.misc.correctMove) { - const modifiers = [ - generateModifierTypeOption(scene, modifierTypes.TEMP_STAT_BOOSTER, [TempBattleStat.ATK]), - generateModifierTypeOption(scene, modifierTypes.TEMP_STAT_BOOSTER, [TempBattleStat.DEF]), - generateModifierTypeOption(scene, modifierTypes.TEMP_STAT_BOOSTER, [TempBattleStat.SPD]), - generateModifierTypeOption(scene, modifierTypes.DIRE_HIT) - ]; + return selectPokemonForOption(scene, onPokemonSelected); + }) + .withOptionPhase(async (scene: BattleScene) => { + const encounter = scene.currentBattle.mysteryEncounter; + if (encounter.misc.correctMove) { + const modifiers = [ + generateModifierTypeOption( + scene, + modifierTypes.TEMP_STAT_BOOSTER, + [TempBattleStat.ATK] + ), + generateModifierTypeOption( + scene, + modifierTypes.TEMP_STAT_BOOSTER, + [TempBattleStat.DEF] + ), + generateModifierTypeOption( + scene, + modifierTypes.TEMP_STAT_BOOSTER, + [TempBattleStat.SPD] + ), + generateModifierTypeOption(scene, modifierTypes.DIRE_HIT), + ]; - setEncounterRewards(scene, { guaranteedModifierTypeOptions: modifiers, fillRemaining: false }); - } + setEncounterRewards(scene, { + guaranteedModifierTypeOptions: modifiers, + fillRemaining: false, + }); + } - leaveEncounterWithoutBattle(scene, !encounter.misc.correctMove); - }) - .build() - ) - .withOption(new MysteryEncounterOptionBuilder() - .withPreOptionPhase(async (scene: BattleScene): Promise => { - const encounter = scene.currentBattle.mysteryEncounter; - const onPokemonSelected = (pokemon: PlayerPokemon) => { - // Return the options for Pokemon move valid for this option - return pokemon.moveset.map((move: PokemonMove) => { - const option: OptionSelectItem = { - label: move.getName(), - handler: () => { - // Pokemon and move selected - const correctMove = move.getMove().category === MoveCategory.SPECIAL; - encounter.setDialogueToken("moveCategory", "Special"); - if (!correctMove) { - encounter.dialogue.encounterOptionsDialogue.options[1].selected = [ - { - text: "mysteryEncounter:field_trip_option_incorrect", - speaker: "mysteryEncounter:field_trip_speaker" - }, - { - text: "mysteryEncounter:field_trip_lesson_learned", + leaveEncounterWithoutBattle(scene, !encounter.misc.correctMove); + }) + .build() + ) + .withOption( + new MysteryEncounterOptionBuilder() + .withDialogue({ + 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; + const onPokemonSelected = (pokemon: PlayerPokemon) => { + // Return the options for Pokemon move valid for this option + return pokemon.moveset.map((move: PokemonMove) => { + const option: OptionSelectItem = { + 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}_lesson_learned`, + }, + ]; + setEncounterExp( + scene, + scene.getParty().map((p) => p.id), + 50 + ); + } else { + encounter.setDialogueToken("pokeName", pokemon.name); + encounter.setDialogueToken("move", move.getName()); + encounter.options[1].dialogue.selected = [ + { + text: `${namespace}_option_selected`, + }, + ]; + setEncounterExp(scene, [pokemon.id], 100); } - ]; - setEncounterExp(scene, scene.getParty().map(p => p.id), 50); - } else { - encounter.setDialogueToken("pokeName", pokemon.name); - encounter.setDialogueToken("move", move.getName()); - encounter.dialogue.encounterOptionsDialogue.options[1].selected = [ - { - text: "mysteryEncounter:field_trip_option_selected" - } - ]; - setEncounterExp(scene, [pokemon.id], 100); - } - encounter.misc = { - correctMove: correctMove + encounter.misc = { + correctMove: correctMove, + }; + return true; + }, }; - return true; - } + return option; + }); }; - return option; - }); - }; - return selectPokemonForOption(scene, onPokemonSelected); - }) - .withOptionPhase(async (scene: BattleScene) => { - const encounter = scene.currentBattle.mysteryEncounter; - if (encounter.misc.correctMove) { - const modifiers = [ - generateModifierTypeOption(scene, modifierTypes.TEMP_STAT_BOOSTER, [TempBattleStat.SPATK]), - generateModifierTypeOption(scene, modifierTypes.TEMP_STAT_BOOSTER, [TempBattleStat.SPDEF]), - generateModifierTypeOption(scene, modifierTypes.TEMP_STAT_BOOSTER, [TempBattleStat.SPD]), - generateModifierTypeOption(scene, modifierTypes.DIRE_HIT) - ]; + return selectPokemonForOption(scene, onPokemonSelected); + }) + .withOptionPhase(async (scene: BattleScene) => { + const encounter = scene.currentBattle.mysteryEncounter; + if (encounter.misc.correctMove) { + const modifiers = [ + generateModifierTypeOption( + scene, + modifierTypes.TEMP_STAT_BOOSTER, + [TempBattleStat.SPATK] + ), + generateModifierTypeOption( + scene, + modifierTypes.TEMP_STAT_BOOSTER, + [TempBattleStat.SPDEF] + ), + generateModifierTypeOption( + scene, + modifierTypes.TEMP_STAT_BOOSTER, + [TempBattleStat.SPD] + ), + generateModifierTypeOption(scene, modifierTypes.DIRE_HIT), + ]; - setEncounterRewards(scene, { guaranteedModifierTypeOptions: modifiers, fillRemaining: false }); - } + setEncounterRewards(scene, { + guaranteedModifierTypeOptions: modifiers, + fillRemaining: false, + }); + } - leaveEncounterWithoutBattle(scene, !encounter.misc.correctMove); - }) - .build() - ) - .withOption(new MysteryEncounterOptionBuilder() - .withPreOptionPhase(async (scene: BattleScene): Promise => { - const encounter = scene.currentBattle.mysteryEncounter; - const onPokemonSelected = (pokemon: PlayerPokemon) => { - // Return the options for Pokemon move valid for this option - return pokemon.moveset.map((move: PokemonMove) => { - const option: OptionSelectItem = { - label: move.getName(), - handler: () => { - // Pokemon and move selected - const correctMove = move.getMove().category === MoveCategory.STATUS; - encounter.setDialogueToken("moveCategory", "Status"); - if (!correctMove) { - encounter.dialogue.encounterOptionsDialogue.options[2].selected = [ - { - text: "mysteryEncounter:field_trip_option_incorrect", - speaker: "mysteryEncounter:field_trip_speaker" - }, - { - text: "mysteryEncounter:field_trip_lesson_learned", + leaveEncounterWithoutBattle(scene, !encounter.misc.correctMove); + }) + .build() + ) + .withOption( + new MysteryEncounterOptionBuilder() + .withDialogue({ + 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; + const onPokemonSelected = (pokemon: PlayerPokemon) => { + // Return the options for Pokemon move valid for this option + return pokemon.moveset.map((move: PokemonMove) => { + const option: OptionSelectItem = { + 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}_lesson_learned`, + }, + ]; + setEncounterExp( + scene, + scene.getParty().map((p) => p.id), + 50 + ); + } else { + encounter.setDialogueToken("pokeName", pokemon.name); + encounter.setDialogueToken("move", move.getName()); + encounter.options[2].dialogue.selected = [ + { + text: `${namespace}_option_selected`, + }, + ]; + setEncounterExp(scene, [pokemon.id], 100); } - ]; - setEncounterExp(scene, scene.getParty().map(p => p.id), 50); - } else { - encounter.setDialogueToken("pokeName", pokemon.name); - encounter.setDialogueToken("move", move.getName()); - encounter.dialogue.encounterOptionsDialogue.options[2].selected = [ - { - text: "mysteryEncounter:field_trip_option_selected" - } - ]; - setEncounterExp(scene, [pokemon.id], 100); - } - encounter.misc = { - correctMove: correctMove + encounter.misc = { + correctMove: correctMove, + }; + return true; + }, }; - return true; - } + return option; + }); }; - return option; - }); - }; - return selectPokemonForOption(scene, onPokemonSelected); - }) - .withOptionPhase(async (scene: BattleScene) => { - const encounter = scene.currentBattle.mysteryEncounter; - if (encounter.misc.correctMove) { - const modifiers = [ - generateModifierTypeOption(scene, modifierTypes.TEMP_STAT_BOOSTER, [TempBattleStat.ACC]), - generateModifierTypeOption(scene, modifierTypes.TEMP_STAT_BOOSTER, [TempBattleStat.SPD]), - generateModifierTypeOption(scene, modifierTypes.GREAT_BALL), - generateModifierTypeOption(scene, modifierTypes.IV_SCANNER) - ]; + return selectPokemonForOption(scene, onPokemonSelected); + }) + .withOptionPhase(async (scene: BattleScene) => { + const encounter = scene.currentBattle.mysteryEncounter; + if (encounter.misc.correctMove) { + const modifiers = [ + generateModifierTypeOption( + scene, + modifierTypes.TEMP_STAT_BOOSTER, + [TempBattleStat.ACC] + ), + generateModifierTypeOption( + scene, + modifierTypes.TEMP_STAT_BOOSTER, + [TempBattleStat.SPD] + ), + generateModifierTypeOption(scene, modifierTypes.GREAT_BALL), + generateModifierTypeOption(scene, modifierTypes.IV_SCANNER), + ]; - setEncounterRewards(scene, { guaranteedModifierTypeOptions: modifiers, fillRemaining: false }); - } + setEncounterRewards(scene, { + guaranteedModifierTypeOptions: modifiers, + fillRemaining: false, + }); + } - leaveEncounterWithoutBattle(scene, !encounter.misc.correctMove); - }) - .build() - ) - .build(); + leaveEncounterWithoutBattle(scene, !encounter.misc.correctMove); + }) + .build() + ) + .build(); diff --git a/src/data/mystery-encounters/encounters/fight-or-flight-encounter.ts b/src/data/mystery-encounters/encounters/fight-or-flight-encounter.ts new file mode 100644 index 00000000000..c98d7cd485f --- /dev/null +++ b/src/data/mystery-encounters/encounters/fight-or-flight-encounter.ts @@ -0,0 +1,235 @@ +import { BattleStat } from "#app/data/battle-stat"; +import { MysteryEncounterOptionBuilder } from "#app/data/mystery-encounters/mystery-encounter-option"; +import { + EnemyPartyConfig, + initBattleWithEnemyConfig, + leaveEncounterWithoutBattle, + queueEncounterMessage, + setEncounterRewards, + showEncounterText, +} from "#app/data/mystery-encounters/mystery-encounter-utils"; +import { STEALING_MOVES } from "#app/data/mystery-encounters/requirements/requirement-groups"; +import Pokemon from "#app/field/pokemon"; +import { ModifierTier } from "#app/modifier/modifier-tier"; +import { + getPartyLuckValue, + getPlayerModifierTypeOptions, + ModifierPoolType, + ModifierTypeOption, + regenerateModifierPoolThresholds, +} from "#app/modifier/modifier-type"; +import { StatChangePhase } from "#app/phases"; +import { randSeedInt } from "#app/utils"; +import { BattlerTagType } from "#enums/battler-tag-type"; +import { MysteryEncounterType } from "#enums/mystery-encounter-type"; +import BattleScene from "../../../battle-scene"; +import IMysteryEncounter, { + MysteryEncounterBuilder, + MysteryEncounterTier, +} from "../mystery-encounter"; +import { MoveRequirement } from "../mystery-encounter-requirements"; + +/** the i18n namespace for the encounter */ +const namespace = "mysteryEncounter:fight_or_flight"; + +export const FightOrFlightEncounter: IMysteryEncounter = + MysteryEncounterBuilder.withEncounterType( + MysteryEncounterType.FIGHT_OR_FLIGHT + ) + .withEncounterTier(MysteryEncounterTier.COMMON) + .withSceneWaveRangeRequirement(10, 180) // waves 10 to 180 + .withCatchAllowed(true) + .withHideWildIntroMessage(true) + .withIntroSpriteConfigs([]) // Set in onInit() + .withIntroDialogue([ + { + text: `${namespace}_intro_message`, + }, + ]) + .withOnInit((scene: BattleScene) => { + const encounter = scene.currentBattle.mysteryEncounter; + + // Calculate boss mon + const bossSpecies = scene.arena.randomSpecies( + scene.currentBattle.waveIndex, + scene.currentBattle.waveIndex, + 0, + getPartyLuckValue(scene.getParty()), + true + ); + const config: EnemyPartyConfig = { + levelAdditiveMultiplier: 1, + pokemonConfigs: [{ species: bossSpecies, isBoss: true }], + }; + encounter.enemyPartyConfigs = [config]; + + // Calculate item + // 10-60 GREAT, 60-110 ULTRA, 110-160 ROGUE, 160-180 MASTER + const tier = + scene.currentBattle.waveIndex > 160 + ? ModifierTier.MASTER + : scene.currentBattle.waveIndex > 110 + ? ModifierTier.ROGUE + : scene.currentBattle.waveIndex > 60 + ? ModifierTier.ULTRA + : ModifierTier.GREAT; + regenerateModifierPoolThresholds( + scene.getParty(), + ModifierPoolType.PLAYER, + 0 + ); // refresh player item pool + const item = getPlayerModifierTypeOptions(1, scene.getParty(), [], { + guaranteedModifierTiers: [tier], + })[0]; + encounter.setDialogueToken("itemName", item.type.name); + encounter.misc = item; + + encounter.spriteConfigs = [ + { + spriteKey: item.type.iconImage, + fileRoot: "items", + hasShadow: false, + x: 35, + y: -5, + scale: 0.75, + isItem: true, + }, + { + spriteKey: bossSpecies.speciesId.toString(), + fileRoot: "pokemon", + hasShadow: true, + tint: 0.25, + x: -5, + repeat: true, + }, + ]; + + // If player has a stealing move, they succeed automatically + encounter.options[1].meetsRequirements(scene); + const primaryPokemon = encounter.options[1].primaryPokemon; + if (primaryPokemon) { + // Use primaryPokemon to execute the thievery + encounter.options[1].dialogue.buttonTooltip = `${namespace}_option_2_steal_tooltip`; + } else { + encounter.options[1].dialogue.buttonTooltip = `${namespace}_option_2_tooltip`; + } + + return true; + }) + .withTitle(`${namespace}_title`) + .withDescription(`${namespace}_description`) + .withQuery(`${namespace}_query`) + .withSimpleOption( + { + buttonLabel: `${namespace}_option_1_label`, + buttonTooltip: `${namespace}_option_1_tooltip`, + selected: [ + { + text: `${namespace}_option_1_selected_message`, + }, + ], + }, + async (scene: BattleScene) => { + // Pick battle + const item = scene.currentBattle.mysteryEncounter + .misc as ModifierTypeOption; + setEncounterRewards(scene, { + guaranteedModifierTypeOptions: [item], + fillRemaining: false, + }); + await initBattleWithEnemyConfig( + scene, + scene.currentBattle.mysteryEncounter.enemyPartyConfigs[0] + ); + } + ) + .withOption( + new MysteryEncounterOptionBuilder() + .withPrimaryPokemonRequirement(new MoveRequirement(STEALING_MOVES)) // Will set option2PrimaryName and option2PrimaryMove dialogue tokens automatically + .withDisabledOnRequirementsNotMet(false) + .withDialogue({ + buttonLabel: `${namespace}_option_2_label`, + buttonTooltip: `${namespace}_option_2_tooltip`, + }) + .withOptionPhase(async (scene: BattleScene) => { + // Pick steal + const encounter = scene.currentBattle.mysteryEncounter; + const item = scene.currentBattle.mysteryEncounter + .misc as ModifierTypeOption; + setEncounterRewards(scene, { + guaranteedModifierTypeOptions: [item], + fillRemaining: false, + }); + + // If player has a stealing move, they succeed automatically + const primaryPokemon = encounter.options[1].primaryPokemon; + if (primaryPokemon) { + // Use primaryPokemon to execute the thievery + await showEncounterText( + scene, + `${namespace}_option_2_steal_result` + ); + leaveEncounterWithoutBattle(scene); + return; + } + + const roll = randSeedInt(16); + if (roll > 6) { + // Noticed and attacked by boss, gets +1 to all stats at start of fight (62.5%) + const config = + scene.currentBattle.mysteryEncounter.enemyPartyConfigs[0]; + config.pokemonConfigs[0].tags = [ + BattlerTagType.MYSTERY_ENCOUNTER_POST_SUMMON, + ]; + config.pokemonConfigs[0].mysteryEncounterBattleEffects = ( + pokemon: Pokemon + ) => { + pokemon.scene.currentBattle.mysteryEncounter.setDialogueToken( + "enemyPokemon", + pokemon.name + ); + queueEncounterMessage(pokemon.scene, `${namespace}_boss_enraged`); + pokemon.scene.unshiftPhase( + new StatChangePhase( + pokemon.scene, + pokemon.getBattlerIndex(), + true, + [ + BattleStat.ATK, + BattleStat.DEF, + BattleStat.SPATK, + BattleStat.SPDEF, + BattleStat.SPD, + ], + 1 + ) + ); + }; + await showEncounterText(scene, `${namespace}_option_2_bad_result`); + await initBattleWithEnemyConfig(scene, config); + } else { + // Steal item (37.5%) + // Display result message then proceed to rewards + await showEncounterText(scene, `${namespace}_option_2_good_result`); + leaveEncounterWithoutBattle(scene); + } + }) + .build() + ) + .withSimpleOption( + { + buttonLabel: `${namespace}_option_3_label`, + buttonTooltip: `${namespace}_option_3_tooltip`, + selected: [ + { + text: `${namespace}_option_3_selected`, + }, + ], + }, + async (scene: BattleScene) => { + // Leave encounter with no rewards or exp + leaveEncounterWithoutBattle(scene, true); + return true; + } + ) + .build(); diff --git a/src/data/mystery-encounters/encounters/fight-or-flight.ts b/src/data/mystery-encounters/encounters/fight-or-flight.ts deleted file mode 100644 index 8a82439772c..00000000000 --- a/src/data/mystery-encounters/encounters/fight-or-flight.ts +++ /dev/null @@ -1,135 +0,0 @@ -import { BattleStat } from "#app/data/battle-stat"; -import { - EnemyPartyConfig, - initBattleWithEnemyConfig, - leaveEncounterWithoutBattle, queueEncounterMessage, - setEncounterRewards, - showEncounterText -} from "#app/data/mystery-encounters/mystery-encounter-utils"; -import Pokemon from "#app/field/pokemon"; -import { ModifierTier } from "#app/modifier/modifier-tier"; -import { - getPartyLuckValue, - getPlayerModifierTypeOptions, - ModifierPoolType, - ModifierTypeOption, - regenerateModifierPoolThresholds -} from "#app/modifier/modifier-type"; -import { StatChangePhase } from "#app/phases"; -import { randSeedInt } from "#app/utils"; -import { BattlerTagType } from "#enums/battler-tag-type"; -import { MysteryEncounterType } from "#enums/mystery-encounter-type"; -import BattleScene from "../../../battle-scene"; -import MysteryEncounter, { MysteryEncounterBuilder, MysteryEncounterTier } from "../mystery-encounter"; -import { MoveRequirement } from "../mystery-encounter-requirements"; -import { MysteryEncounterOptionBuilder } from "#app/data/mystery-encounters/mystery-encounter-option"; -import { STEALING_MOVES } from "#app/data/mystery-encounters/requirements/requirement-groups"; - -export const FightOrFlightEncounter: MysteryEncounter = MysteryEncounterBuilder - .withEncounterType(MysteryEncounterType.FIGHT_OR_FLIGHT) - .withEncounterTier(MysteryEncounterTier.COMMON) - .withIntroSpriteConfigs([]) // Set in onInit() - .withSceneWaveRangeRequirement(10, 180) // waves 10 to 180 - .withCatchAllowed(true) - .withHideWildIntroMessage(true) - .withOnInit((scene: BattleScene) => { - const encounter = scene.currentBattle.mysteryEncounter; - - // Calculate boss mon - const bossSpecies = scene.arena.randomSpecies(scene.currentBattle.waveIndex, scene.currentBattle.waveIndex, 0, getPartyLuckValue(scene.getParty()), true); - const config: EnemyPartyConfig = { - levelAdditiveMultiplier: 1, - pokemonConfigs: [{ species: bossSpecies, isBoss: true }] - }; - encounter.enemyPartyConfigs = [config]; - - // Calculate item - // 10-60 GREAT, 60-110 ULTRA, 110-160 ROGUE, 160-180 MASTER - const tier = scene.currentBattle.waveIndex > 160 ? ModifierTier.MASTER : scene.currentBattle.waveIndex > 110 ? ModifierTier.ROGUE : scene.currentBattle.waveIndex > 60 ? ModifierTier.ULTRA : ModifierTier.GREAT; - regenerateModifierPoolThresholds(scene.getParty(), ModifierPoolType.PLAYER, 0); // refresh player item pool - const item = getPlayerModifierTypeOptions(1, scene.getParty(), [], { guaranteedModifierTiers: [tier] })[0]; - encounter.setDialogueToken("itemName", item.type.name); - encounter.misc = item; - - encounter.spriteConfigs = [ - { - spriteKey: item.type.iconImage, - fileRoot: "items", - hasShadow: false, - x: 35, - y: -5, - scale: 0.75, - isItem: true - }, - { - spriteKey: bossSpecies.speciesId.toString(), - fileRoot: "pokemon", - hasShadow: true, - tint: 0.25, - x: -5, - repeat: true - } - ]; - - // If player has a stealing move, they succeed automatically - encounter.options[1].meetsRequirements(scene); - const primaryPokemon = encounter.options[1].primaryPokemon; - if (primaryPokemon) { - // Use primaryPokemon to execute the thievery - encounter.dialogue.encounterOptionsDialogue.options[1].buttonTooltip = "mysteryEncounter:fight_or_flight_option_2_steal_tooltip"; - } else { - encounter.dialogue.encounterOptionsDialogue.options[1].buttonTooltip = "mysteryEncounter:fight_or_flight_option_2_tooltip"; - } - - return true; - }) - .withOptionPhase(async (scene: BattleScene) => { - // Pick battle - const item = scene.currentBattle.mysteryEncounter.misc as ModifierTypeOption; - setEncounterRewards(scene, { guaranteedModifierTypeOptions: [item], fillRemaining: false }); - await initBattleWithEnemyConfig(scene, scene.currentBattle.mysteryEncounter.enemyPartyConfigs[0]); - }) - .withOption(new MysteryEncounterOptionBuilder() - .withPrimaryPokemonRequirement(new MoveRequirement(STEALING_MOVES)) // Will set option2PrimaryName and option2PrimaryMove dialogue tokens automatically - .withDisabledOnRequirementsNotMet(false) - .withOptionPhase(async (scene: BattleScene) => { - // Pick steal - const encounter = scene.currentBattle.mysteryEncounter; - const item = scene.currentBattle.mysteryEncounter.misc as ModifierTypeOption; - setEncounterRewards(scene, { guaranteedModifierTypeOptions: [item], fillRemaining: false }); - - // If player has a stealing move, they succeed automatically - const primaryPokemon = encounter.options[1].primaryPokemon; - if (primaryPokemon) { - // Use primaryPokemon to execute the thievery - await showEncounterText(scene, "mysteryEncounter:fight_or_flight_option_2_steal_result"); - leaveEncounterWithoutBattle(scene); - return; - } - - const roll = randSeedInt(16); - if (roll > 6) { - // Noticed and attacked by boss, gets +1 to all stats at start of fight (62.5%) - const config = scene.currentBattle.mysteryEncounter.enemyPartyConfigs[0]; - config.pokemonConfigs[0].tags = [BattlerTagType.MYSTERY_ENCOUNTER_POST_SUMMON]; - config.pokemonConfigs[0].mysteryEncounterBattleEffects = (pokemon: Pokemon) => { - pokemon.scene.currentBattle.mysteryEncounter.setDialogueToken("enemyPokemon", pokemon.name); - queueEncounterMessage(pokemon.scene, "mysteryEncounter:fight_or_flight_boss_enraged"); - pokemon.scene.unshiftPhase(new StatChangePhase(pokemon.scene, pokemon.getBattlerIndex(), true, [BattleStat.ATK, BattleStat.DEF, BattleStat.SPATK, BattleStat.SPDEF, BattleStat.SPD], 1)); - }; - await showEncounterText(scene, "mysteryEncounter:fight_or_flight_option_2_bad_result"); - await initBattleWithEnemyConfig(scene, config); - } else { - // Steal item (37.5%) - // Display result message then proceed to rewards - await showEncounterText(scene, "mysteryEncounter:fight_or_flight_option_2_good_result"); - leaveEncounterWithoutBattle(scene); - } - }) - .build()) - .withOptionPhase(async (scene: BattleScene) => { - // Leave encounter with no rewards or exp - leaveEncounterWithoutBattle(scene, true); - return true; - }) - .build(); diff --git a/src/data/mystery-encounters/encounters/mysterious-challengers-encounter.ts b/src/data/mystery-encounters/encounters/mysterious-challengers-encounter.ts new file mode 100644 index 00000000000..1276d6e2226 --- /dev/null +++ b/src/data/mystery-encounters/encounters/mysterious-challengers-encounter.ts @@ -0,0 +1,244 @@ +import { + EnemyPartyConfig, + initBattleWithEnemyConfig, + setEncounterRewards, +} from "#app/data/mystery-encounters/mystery-encounter-utils"; +import { + trainerConfigs, + TrainerPartyCompoundTemplate, + TrainerPartyTemplate, + trainerPartyTemplates, +} from "#app/data/trainer-config"; +import { ModifierTier } from "#app/modifier/modifier-tier"; +import { modifierTypes } from "#app/modifier/modifier-type"; +import { MysteryEncounterType } from "#enums/mystery-encounter-type"; +import { PartyMemberStrength } from "#enums/party-member-strength"; +import BattleScene from "../../../battle-scene"; +import * as Utils from "../../../utils"; +import IMysteryEncounter, { + MysteryEncounterBuilder, + MysteryEncounterTier, +} from "../mystery-encounter"; + +/** the i18n namespace for the encounter */ +const namespace = "mysteryEncounter:mysterious_challengers"; + +export const MysteriousChallengersEncounter: IMysteryEncounter = + MysteryEncounterBuilder.withEncounterType( + MysteryEncounterType.MYSTERIOUS_CHALLENGERS + ) + .withEncounterTier(MysteryEncounterTier.GREAT) + .withSceneWaveRangeRequirement(10, 180) // waves 10 to 180 + .withIntroSpriteConfigs([]) // These are set in onInit() + .withIntroDialogue([ + { + text: `${namespace}_intro_message`, + }, + ]) + .withOnInit((scene: BattleScene) => { + const encounter = scene.currentBattle.mysteryEncounter; + // Calculates what trainers are available for battle in the encounter + + // Normal difficulty trainer is randomly pulled from biome + const normalTrainerType = scene.arena.randomTrainerType( + scene.currentBattle.waveIndex + ); + const normalConfig = trainerConfigs[normalTrainerType].copy(); + let female = false; + if (normalConfig.hasGenders) { + female = !!Utils.randSeedInt(2); + } + const normalSpriteKey = normalConfig.getSpriteKey( + female, + normalConfig.doubleOnly + ); + encounter.enemyPartyConfigs.push({ + trainerConfig: normalConfig, + female: female, + }); + + // Hard difficulty trainer is another random trainer, but with AVERAGE_BALANCED config + // Number of mons is based off wave: 1-20 is 2, 20-40 is 3, etc. capping at 6 after wave 100 + const hardTrainerType = scene.arena.randomTrainerType( + scene.currentBattle.waveIndex + ); + const hardTemplate = new TrainerPartyCompoundTemplate( + new TrainerPartyTemplate(1, PartyMemberStrength.STRONGER, false, true), + new TrainerPartyTemplate( + Math.min(Math.ceil(scene.currentBattle.waveIndex / 20), 5), + PartyMemberStrength.AVERAGE, + false, + true + ) + ); + const hardConfig = trainerConfigs[hardTrainerType].copy(); + hardConfig.setPartyTemplates(hardTemplate); + female = false; + if (hardConfig.hasGenders) { + female = !!Utils.randSeedInt(2); + } + const hardSpriteKey = hardConfig.getSpriteKey( + female, + hardConfig.doubleOnly + ); + encounter.enemyPartyConfigs.push({ + trainerConfig: hardConfig, + levelAdditiveMultiplier: 0.5, + female: female, + }); + + // Brutal trainer is pulled from pool of boss trainers (gym leaders) for the biome + // They are given an E4 template team, so will be stronger than usual boss encounter and always have 6 mons + const brutalTrainerType = scene.arena.randomTrainerType( + scene.currentBattle.waveIndex, + true + ); + const e4Template = trainerPartyTemplates.ELITE_FOUR; + const brutalConfig = trainerConfigs[brutalTrainerType].copy(); + brutalConfig.setPartyTemplates(e4Template); + brutalConfig.partyTemplateFunc = null; // Overrides gym leader party template func + female = false; + if (brutalConfig.hasGenders) { + female = !!Utils.randSeedInt(2); + } + const brutalSpriteKey = brutalConfig.getSpriteKey( + female, + brutalConfig.doubleOnly + ); + encounter.enemyPartyConfigs.push({ + trainerConfig: brutalConfig, + levelAdditiveMultiplier: 1.1, + female: female, + }); + + encounter.spriteConfigs = [ + { + spriteKey: normalSpriteKey, + fileRoot: "trainer", + hasShadow: true, + tint: 1, + }, + { + spriteKey: hardSpriteKey, + fileRoot: "trainer", + hasShadow: true, + tint: 1, + }, + { + spriteKey: brutalSpriteKey, + fileRoot: "trainer", + hasShadow: true, + tint: 1, + }, + ]; + + return true; + }) + .withTitle(`${namespace}_title`) + .withDescription(`${namespace}_description`) + .withQuery(`${namespace}_query`) + .withSimpleOption( + { + buttonLabel: `${namespace}_option_1_label`, + buttonTooltip: `${namespace}_option_1_tooltip`, + selected: [ + { + text: `${namespace}_option_selected_message`, + }, + ], + }, + async (scene: BattleScene) => { + const encounter = scene.currentBattle.mysteryEncounter; + // Spawn standard trainer battle with memory mushroom reward + const config: EnemyPartyConfig = encounter.enemyPartyConfigs[0]; + + setEncounterRewards(scene, { + guaranteedModifierTypeFuncs: [ + modifierTypes.TM_COMMON, + modifierTypes.TM_GREAT, + modifierTypes.MEMORY_MUSHROOM, + ], + fillRemaining: true, + }); + + // Seed offsets to remove possibility of different trainers having exact same teams + let ret; + scene.executeWithSeedOffset(() => { + ret = initBattleWithEnemyConfig(scene, config); + }, scene.currentBattle.waveIndex * 10); + return ret; + } + ) + .withSimpleOption( + { + buttonLabel: `${namespace}_option_2_label`, + buttonTooltip: `${namespace}_option_2_tooltip`, + selected: [ + { + text: `${namespace}_option_selected_message`, + }, + ], + }, + async (scene: BattleScene) => { + const encounter = scene.currentBattle.mysteryEncounter; + // 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, + }); + + // Seed offsets to remove possibility of different trainers having exact same teams + let ret; + scene.executeWithSeedOffset(() => { + ret = initBattleWithEnemyConfig(scene, config); + }, scene.currentBattle.waveIndex * 100); + return ret; + } + ) + .withSimpleOption( + { + buttonLabel: `${namespace}_option_3_label`, + buttonTooltip: `${namespace}_option_3_tooltip`, + selected: [ + { + text: `${namespace}_option_selected_message`, + }, + ], + }, + async (scene: BattleScene) => { + const encounter = scene.currentBattle.mysteryEncounter; + // Spawn brutal fight with ROGUE/ULTRA/GREAT reward (can improve with luck) + const config: EnemyPartyConfig = encounter.enemyPartyConfigs[2]; + + // To avoid player level snowballing from picking this option + encounter.expMultiplier = 0.9; + + setEncounterRewards(scene, { + guaranteedModifierTiers: [ + ModifierTier.ROGUE, + ModifierTier.ULTRA, + ModifierTier.GREAT, + ], + fillRemaining: true, + }); + + // Seed offsets to remove possibility of different trainers having exact same teams + let ret; + scene.executeWithSeedOffset(() => { + ret = initBattleWithEnemyConfig(scene, config); + }, scene.currentBattle.waveIndex * 1000); + return ret; + } + ) + .withOutroDialogue([ + { + text: `${namespace}_outro_win`, + }, + ]) + .build(); diff --git a/src/data/mystery-encounters/encounters/mysterious-challengers.ts b/src/data/mystery-encounters/encounters/mysterious-challengers.ts deleted file mode 100644 index 880c7be4ca2..00000000000 --- a/src/data/mystery-encounters/encounters/mysterious-challengers.ts +++ /dev/null @@ -1,143 +0,0 @@ -import { EnemyPartyConfig, initBattleWithEnemyConfig, setEncounterRewards } from "#app/data/mystery-encounters/mystery-encounter-utils"; -import { - trainerConfigs, - TrainerPartyCompoundTemplate, - TrainerPartyTemplate, - trainerPartyTemplates -} from "#app/data/trainer-config"; -import { ModifierTier } from "#app/modifier/modifier-tier"; -import { modifierTypes } from "#app/modifier/modifier-type"; -import { MysteryEncounterType } from "#enums/mystery-encounter-type"; -import { PartyMemberStrength } from "#enums/party-member-strength"; -import BattleScene from "../../../battle-scene"; -import * as Utils from "../../../utils"; -import MysteryEncounter, { MysteryEncounterBuilder, MysteryEncounterTier } from "../mystery-encounter"; - -export const MysteriousChallengersEncounter: MysteryEncounter = MysteryEncounterBuilder - .withEncounterType(MysteryEncounterType.MYSTERIOUS_CHALLENGERS) - .withEncounterTier(MysteryEncounterTier.GREAT) - .withIntroSpriteConfigs([]) // These are set in onInit() - .withSceneWaveRangeRequirement(10, 180) // waves 10 to 180 - .withOnInit((scene: BattleScene) => { - const encounter = scene.currentBattle.mysteryEncounter; - // Calculates what trainers are available for battle in the encounter - - // Normal difficulty trainer is randomly pulled from biome - const normalTrainerType = scene.arena.randomTrainerType(scene.currentBattle.waveIndex); - const normalConfig = trainerConfigs[normalTrainerType].copy(); - let female = false; - if (normalConfig.hasGenders) { - female = !!(Utils.randSeedInt(2)); - } - const normalSpriteKey = normalConfig.getSpriteKey(female, normalConfig.doubleOnly); - encounter.enemyPartyConfigs.push({ - trainerConfig: normalConfig, - female: female - }); - - // Hard difficulty trainer is another random trainer, but with AVERAGE_BALANCED config - // Number of mons is based off wave: 1-20 is 2, 20-40 is 3, etc. capping at 6 after wave 100 - const hardTrainerType = scene.arena.randomTrainerType(scene.currentBattle.waveIndex); - const hardTemplate = new TrainerPartyCompoundTemplate( - new TrainerPartyTemplate(1, PartyMemberStrength.STRONGER, false, true), - new TrainerPartyTemplate(Math.min(Math.ceil(scene.currentBattle.waveIndex / 20), 5), PartyMemberStrength.AVERAGE, false, true)); - const hardConfig = trainerConfigs[hardTrainerType].copy(); - hardConfig.setPartyTemplates(hardTemplate); - female = false; - if (hardConfig.hasGenders) { - female = !!(Utils.randSeedInt(2)); - } - const hardSpriteKey = hardConfig.getSpriteKey(female, hardConfig.doubleOnly); - encounter.enemyPartyConfigs.push({ - trainerConfig: hardConfig, - levelAdditiveMultiplier: 0.5, - female: female, - }); - - // Brutal trainer is pulled from pool of boss trainers (gym leaders) for the biome - // They are given an E4 template team, so will be stronger than usual boss encounter and always have 6 mons - const brutalTrainerType = scene.arena.randomTrainerType(scene.currentBattle.waveIndex, true); - const e4Template = trainerPartyTemplates.ELITE_FOUR; - const brutalConfig = trainerConfigs[brutalTrainerType].copy(); - brutalConfig.setPartyTemplates(e4Template); - brutalConfig.partyTemplateFunc = null; // Overrides gym leader party template func - female = false; - if (brutalConfig.hasGenders) { - female = !!(Utils.randSeedInt(2)); - } - const brutalSpriteKey = brutalConfig.getSpriteKey(female, brutalConfig.doubleOnly); - encounter.enemyPartyConfigs.push({ - trainerConfig: brutalConfig, - levelAdditiveMultiplier: 1.1, - female: female - }); - - encounter.spriteConfigs = [ - { - spriteKey: normalSpriteKey, - fileRoot: "trainer", - hasShadow: true, - tint: 1 - }, - { - spriteKey: hardSpriteKey, - fileRoot: "trainer", - hasShadow: true, - tint: 1 - }, - { - spriteKey: brutalSpriteKey, - fileRoot: "trainer", - hasShadow: true, - tint: 1 - } - ]; - - return true; - }) - .withOptionPhase(async (scene: BattleScene) => { - const encounter = scene.currentBattle.mysteryEncounter; - // Spawn standard trainer battle with memory mushroom reward - const config: EnemyPartyConfig = encounter.enemyPartyConfigs[0]; - - setEncounterRewards(scene, { guaranteedModifierTypeFuncs: [modifierTypes.TM_COMMON, modifierTypes.TM_GREAT, modifierTypes.MEMORY_MUSHROOM], fillRemaining: true }); - - // Seed offsets to remove possibility of different trainers having exact same teams - let ret; - scene.executeWithSeedOffset(() => { - ret = initBattleWithEnemyConfig(scene, config); - }, scene.currentBattle.waveIndex * 10); - return ret; - }) - .withOptionPhase(async (scene: BattleScene) => { - const encounter = scene.currentBattle.mysteryEncounter; - // 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 }); - - // Seed offsets to remove possibility of different trainers having exact same teams - let ret; - scene.executeWithSeedOffset(() => { - ret = initBattleWithEnemyConfig(scene, config); - }, scene.currentBattle.waveIndex * 100); - return ret; - }) - .withOptionPhase(async (scene: BattleScene) => { - const encounter = scene.currentBattle.mysteryEncounter; - // Spawn brutal fight with ROGUE/ULTRA/GREAT reward (can improve with luck) - const config: EnemyPartyConfig = encounter.enemyPartyConfigs[2]; - - // To avoid player level snowballing from picking this option - encounter.expMultiplier = 0.9; - - setEncounterRewards(scene, { guaranteedModifierTiers: [ModifierTier.ROGUE, ModifierTier.ULTRA, ModifierTier.GREAT], fillRemaining: true }); - - // Seed offsets to remove possibility of different trainers having exact same teams - let ret; - scene.executeWithSeedOffset(() => { - ret = initBattleWithEnemyConfig(scene, config); - }, scene.currentBattle.waveIndex * 1000); - return ret; - }) - .build(); diff --git a/src/data/mystery-encounters/encounters/mysterious-chest-encounter.ts b/src/data/mystery-encounters/encounters/mysterious-chest-encounter.ts new file mode 100644 index 00000000000..8d1f1c2eeb1 --- /dev/null +++ b/src/data/mystery-encounters/encounters/mysterious-chest-encounter.ts @@ -0,0 +1,168 @@ +import { + getHighestLevelPlayerPokemon, + koPlayerPokemon, + leaveEncounterWithoutBattle, + queueEncounterMessage, + setEncounterRewards, + showEncounterText, +} from "#app/data/mystery-encounters/mystery-encounter-utils"; +import { ModifierTier } from "#app/modifier/modifier-tier"; +import { GameOverPhase } from "#app/phases"; +import { randSeedInt } from "#app/utils"; +import { MysteryEncounterType } from "#enums/mystery-encounter-type"; +import BattleScene from "../../../battle-scene"; +import IMysteryEncounter, { + MysteryEncounterBuilder, + MysteryEncounterTier, +} from "../mystery-encounter"; +import { MysteryEncounterOptionBuilder } from "../mystery-encounter-option"; + +export const MysteriousChestEncounter: IMysteryEncounter = + MysteryEncounterBuilder.withEncounterType( + MysteryEncounterType.MYSTERIOUS_CHEST + ) + .withEncounterTier(MysteryEncounterTier.COMMON) + .withSceneWaveRangeRequirement(10, 180) // waves 2 to 180 + .withHideIntroVisuals(false) + .withIntroSpriteConfigs([ + { + spriteKey: "chest_blue", + fileRoot: "mystery-encounters", + hasShadow: true, + x: 4, + y: 8, + disableAnimation: true, // Re-enabled after option select + }, + ]) + .withIntroDialogue([ + { + text: "mysteryEncounter:mysterious_chest_intro_message", + }, + ]) + .withTitle("mysteryEncounter:mysterious_chest_title") + .withDescription("mysteryEncounter:mysterious_chest_description") + .withQuery("mysteryEncounter:mysterious_chest_query") + .withOption( + new MysteryEncounterOptionBuilder() + .withDialogue({ + buttonLabel: "mysteryEncounter:mysterious_chest_option_1_label", + buttonTooltip: "mysteryEncounter:mysterious_chest_option_1_tooltip", + selected: [ + { + text: "mysteryEncounter:mysterious_chest_option_1_selected_message", + }, + ], + }) + .withPreOptionPhase(async (scene: BattleScene) => { + // Play animation + const introVisuals = + scene.currentBattle.mysteryEncounter.introVisuals; + introVisuals.spriteConfigs[0].disableAnimation = false; + introVisuals.playAnim(); + }) + .withOptionPhase(async (scene: BattleScene) => { + // Open the chest + const roll = randSeedInt(100); + if (roll > 60) { + // Choose between 2 COMMON / 2 GREAT tier items (40%) + setEncounterRewards(scene, { + guaranteedModifierTiers: [ + ModifierTier.COMMON, + ModifierTier.COMMON, + ModifierTier.GREAT, + ModifierTier.GREAT, + ], + }); + // Display result message then proceed to rewards + queueEncounterMessage( + scene, + "mysteryEncounter:mysterious_chest_option_1_normal_result" + ); + leaveEncounterWithoutBattle(scene); + } else if (roll > 40) { + // Choose between 3 ULTRA tier items (20%) + setEncounterRewards(scene, { + guaranteedModifierTiers: [ + ModifierTier.ULTRA, + ModifierTier.ULTRA, + ModifierTier.ULTRA, + ], + }); + // Display result message then proceed to rewards + queueEncounterMessage( + scene, + "mysteryEncounter:mysterious_chest_option_1_good_result" + ); + leaveEncounterWithoutBattle(scene); + } else if (roll > 36) { + // Choose between 2 ROGUE tier items (4%) + setEncounterRewards(scene, { + guaranteedModifierTiers: [ModifierTier.ROGUE, ModifierTier.ROGUE], + }); + // Display result message then proceed to rewards + queueEncounterMessage( + scene, + "mysteryEncounter:mysterious_chest_option_1_great_result" + ); + leaveEncounterWithoutBattle(scene); + } else if (roll > 35) { + // Choose 1 MASTER tier item (1%) + setEncounterRewards(scene, { + guaranteedModifierTiers: [ModifierTier.MASTER], + }); + // Display result message then proceed to rewards + queueEncounterMessage( + scene, + "mysteryEncounter:mysterious_chest_option_1_amazing_result" + ); + leaveEncounterWithoutBattle(scene); + } else { + // Your highest level unfainted Pok�mon gets OHKO. Progress with no rewards (35%) + const highestLevelPokemon = getHighestLevelPlayerPokemon( + scene, + true + ); + koPlayerPokemon(highestLevelPokemon); + + scene.currentBattle.mysteryEncounter.setDialogueToken( + "pokeName", + highestLevelPokemon.name + ); + // Show which Pokemon was KOed, then leave encounter with no rewards + // Does this synchronously so that game over doesn't happen over result message + await showEncounterText( + scene, + "mysteryEncounter:mysterious_chest_option_1_bad_result" + ).then(() => { + if ( + scene.getParty().filter((p) => p.isAllowedInBattle()).length === + 0 + ) { + // All pokemon fainted, game over + scene.clearPhaseQueue(); + scene.unshiftPhase(new GameOverPhase(scene)); + } else { + leaveEncounterWithoutBattle(scene); + } + }); + } + }) + .build() + ) + .withSimpleOption( + { + buttonLabel: "mysteryEncounter:mysterious_chest_option_2_label", + buttonTooltip: "mysteryEncounter:mysterious_chest_option_2_tooltip", + selected: [ + { + text: "mysteryEncounter:mysterious_chest_option_2_selected_message", + }, + ], + }, + async (scene: BattleScene) => { + // Leave encounter with no rewards or exp + leaveEncounterWithoutBattle(scene, true); + return true; + } + ) + .build(); diff --git a/src/data/mystery-encounters/encounters/mysterious-chest.ts b/src/data/mystery-encounters/encounters/mysterious-chest.ts deleted file mode 100644 index 5e572131562..00000000000 --- a/src/data/mystery-encounters/encounters/mysterious-chest.ts +++ /dev/null @@ -1,93 +0,0 @@ -import { - getHighestLevelPlayerPokemon, - koPlayerPokemon, - leaveEncounterWithoutBattle, - queueEncounterMessage, - setEncounterRewards, - showEncounterText -} from "#app/data/mystery-encounters/mystery-encounter-utils"; -import { ModifierTier } from "#app/modifier/modifier-tier"; -import { GameOverPhase } from "#app/phases"; -import { randSeedInt } from "#app/utils"; -import { MysteryEncounterType } from "#enums/mystery-encounter-type"; -import BattleScene from "../../../battle-scene"; -import MysteryEncounter, { MysteryEncounterBuilder, MysteryEncounterTier } from "../mystery-encounter"; -import { MysteryEncounterOptionBuilder } from "../mystery-encounter-option"; - -export const MysteriousChestEncounter: MysteryEncounter = MysteryEncounterBuilder - .withEncounterType(MysteryEncounterType.MYSTERIOUS_CHEST) - .withEncounterTier(MysteryEncounterTier.COMMON) - .withIntroSpriteConfigs([ - { - spriteKey: "chest_blue", - fileRoot: "mystery-encounters", - hasShadow: true, - x: 4, - y: 8, - disableAnimation: true // Re-enabled after option select - } - ]) - .withHideIntroVisuals(false) - .withSceneWaveRangeRequirement(10, 180) // waves 2 to 180 - .withOption(new MysteryEncounterOptionBuilder() - .withPreOptionPhase(async (scene: BattleScene) => { - // Play animation - const introVisuals = scene.currentBattle.mysteryEncounter.introVisuals; - introVisuals.spriteConfigs[0].disableAnimation = false; - introVisuals.playAnim(); - }) - .withOptionPhase(async (scene: BattleScene) => { - // Open the chest - const roll = randSeedInt(100); - if (roll > 60) { - // Choose between 2 COMMON / 2 GREAT tier items (40%) - setEncounterRewards(scene, { guaranteedModifierTiers: [ModifierTier.COMMON, ModifierTier.COMMON, ModifierTier.GREAT, ModifierTier.GREAT] }); - // Display result message then proceed to rewards - queueEncounterMessage(scene, "mysteryEncounter:mysterious_chest_option_1_normal_result"); - leaveEncounterWithoutBattle(scene); - } else if (roll > 40) { - // Choose between 3 ULTRA tier items (20%) - setEncounterRewards(scene, { guaranteedModifierTiers: [ModifierTier.ULTRA, ModifierTier.ULTRA, ModifierTier.ULTRA] }); - // Display result message then proceed to rewards - queueEncounterMessage(scene, "mysteryEncounter:mysterious_chest_option_1_good_result"); - leaveEncounterWithoutBattle(scene); - } else if (roll > 36) { - // Choose between 2 ROGUE tier items (4%) - setEncounterRewards(scene, { guaranteedModifierTiers: [ModifierTier.ROGUE, ModifierTier.ROGUE] }); - // Display result message then proceed to rewards - queueEncounterMessage(scene, "mysteryEncounter:mysterious_chest_option_1_great_result"); - leaveEncounterWithoutBattle(scene); - } else if (roll > 35) { - // Choose 1 MASTER tier item (1%) - setEncounterRewards(scene, { guaranteedModifierTiers: [ModifierTier.MASTER] }); - // Display result message then proceed to rewards - queueEncounterMessage(scene, "mysteryEncounter:mysterious_chest_option_1_amazing_result"); - leaveEncounterWithoutBattle(scene); - } else { - // Your highest level unfainted Pok�mon gets OHKO. Progress with no rewards (35%) - const highestLevelPokemon = getHighestLevelPlayerPokemon(scene, true); - koPlayerPokemon(highestLevelPokemon); - - scene.currentBattle.mysteryEncounter.setDialogueToken("pokeName", highestLevelPokemon.name); - // Show which Pokemon was KOed, then leave encounter with no rewards - // Does this synchronously so that game over doesn't happen over result message - await showEncounterText(scene, "mysteryEncounter:mysterious_chest_option_1_bad_result") - .then(() => { - if (scene.getParty().filter(p => p.isAllowedInBattle()).length === 0) { - // All pokemon fainted, game over - scene.clearPhaseQueue(); - scene.unshiftPhase(new GameOverPhase(scene)); - } else { - leaveEncounterWithoutBattle(scene); - } - }); - } - }) - .build() - ) - .withOptionPhase(async (scene: BattleScene) => { - // Leave encounter with no rewards or exp - leaveEncounterWithoutBattle(scene, true); - return true; - }) - .build(); diff --git a/src/data/mystery-encounters/encounters/shady-vitamin-dealer-encounter.ts b/src/data/mystery-encounters/encounters/shady-vitamin-dealer-encounter.ts new file mode 100644 index 00000000000..14dddf91189 --- /dev/null +++ b/src/data/mystery-encounters/encounters/shady-vitamin-dealer-encounter.ts @@ -0,0 +1,269 @@ +import { + generateModifierTypeOption, + leaveEncounterWithoutBattle, + queueEncounterMessage, + selectPokemonForOption, + setEncounterExp, + updatePlayerMoney, +} from "#app/data/mystery-encounters/mystery-encounter-utils"; +import { StatusEffect } from "#app/data/status-effect"; +import Pokemon, { PlayerPokemon } from "#app/field/pokemon"; +import { modifierTypes } from "#app/modifier/modifier-type"; +import { randSeedInt } from "#app/utils"; +import { MysteryEncounterType } from "#enums/mystery-encounter-type"; +import { Species } from "#enums/species"; +import i18next from "i18next"; +import BattleScene from "../../../battle-scene"; +import IMysteryEncounter, { + MysteryEncounterBuilder, + MysteryEncounterTier, +} from "../mystery-encounter"; +import { MysteryEncounterOptionBuilder } from "../mystery-encounter-option"; +import { MoneyRequirement } from "../mystery-encounter-requirements"; + +/** the i18n namespace for this encounter */ +const namespace = "mysteryEncounter:shady_vitamin_dealer"; + +export const ShadyVitaminDealerEncounter: IMysteryEncounter = + MysteryEncounterBuilder.withEncounterType( + MysteryEncounterType.SHADY_VITAMIN_DEALER + ) + .withEncounterTier(MysteryEncounterTier.COMMON) + .withSceneWaveRangeRequirement(10, 180) + .withPrimaryPokemonStatusEffectRequirement([StatusEffect.NONE]) // Pokemon must not have status + .withPrimaryPokemonHealthRatioRequirement([0.34, 1]) // Pokemon must have above 1/3rd HP + .withIntroSpriteConfigs([ + { + spriteKey: Species.KROOKODILE.toString(), + fileRoot: "pokemon", + hasShadow: true, + repeat: true, + x: 10, + y: -1, + }, + { + spriteKey: "b2w2_veteran_m", + fileRoot: "mystery-encounters", + hasShadow: true, + x: -10, + y: 2, + }, + ]) + .withIntroDialogue([ + { + text: `${namespace}_intro_message`, + }, + { + text: `${namespace}_intro_dialogue`, + speaker: `${namespace}_speaker`, + }, + ]) + .withTitle(`${namespace}_title`) + .withDescription(`${namespace}_description`) + .withQuery(`${namespace}_query`) + .withOption( + new MysteryEncounterOptionBuilder() + .withSceneMoneyRequirement(0, 2) // Wave scaling money multiplier of 2 + .withDialogue({ + buttonLabel: `${namespace}_option_1_label`, + buttonTooltip: `${namespace}_option_1_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[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 i18next.t(`${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; + + 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) => { + // Damage and status applied after dealer leaves (to make thematic sense) + const encounter = scene.currentBattle.mysteryEncounter; + const chosenPokemon = encounter.misc.chosenPokemon; + + // Pokemon takes 1/3 max HP damage + const damage = Math.round(chosenPokemon.getMaxHp() / 3); + chosenPokemon.hp = Math.max(chosenPokemon.hp - damage, 0); + + // 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() + .withDialogue({ + buttonLabel: `${namespace}_option_2_label`, + buttonTooltip: `${namespace}_option_2_tooltip`, + selected: [ + { + text: `${namespace}_option_selected`, + }, + ], + }) + .withSceneMoneyRequirement(0, 5) // Wave scaling money multiplier of 5 + .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 i18next.t(`${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(); + }) + .build() + ) + .withSimpleOption( + { + buttonLabel: `${namespace}_option_3_label`, + buttonTooltip: `${namespace}_option_3_tooltip`, + }, + async (scene: BattleScene) => { + // Leave encounter with no rewards or exp + leaveEncounterWithoutBattle(scene, true); + return true; + } + ) + .build(); diff --git a/src/data/mystery-encounters/encounters/shady-vitamin-dealer.ts b/src/data/mystery-encounters/encounters/shady-vitamin-dealer.ts deleted file mode 100644 index 21e1b199e04..00000000000 --- a/src/data/mystery-encounters/encounters/shady-vitamin-dealer.ts +++ /dev/null @@ -1,194 +0,0 @@ -import { - generateModifierTypeOption, - leaveEncounterWithoutBattle, - queueEncounterMessage, - selectPokemonForOption, setEncounterExp, - updatePlayerMoney, -} from "#app/data/mystery-encounters/mystery-encounter-utils"; -import { StatusEffect } from "#app/data/status-effect"; -import Pokemon, { PlayerPokemon } from "#app/field/pokemon"; -import { modifierTypes } from "#app/modifier/modifier-type"; -import { randSeedInt } from "#app/utils"; -import { MysteryEncounterType } from "#enums/mystery-encounter-type"; -import { Species } from "#enums/species"; -import BattleScene from "../../../battle-scene"; -import MysteryEncounter, { MysteryEncounterBuilder, MysteryEncounterTier } from "../mystery-encounter"; -import { MysteryEncounterOptionBuilder } from "../mystery-encounter-option"; -import { - MoneyRequirement -} from "../mystery-encounter-requirements"; -import i18next from "i18next"; - -export const ShadyVitaminDealerEncounter: MysteryEncounter = MysteryEncounterBuilder - .withEncounterType(MysteryEncounterType.SHADY_VITAMIN_DEALER) - .withEncounterTier(MysteryEncounterTier.COMMON) - .withIntroSpriteConfigs([ - { - spriteKey: Species.KROOKODILE.toString(), - fileRoot: "pokemon", - hasShadow: true, - repeat: true, - x: 10, - y: -1 - }, - { - spriteKey: "b2w2_veteran_m", - fileRoot: "mystery-encounters", - hasShadow: true, - x: -10, - y: 2 - } - ]) - .withSceneWaveRangeRequirement(10, 180) - .withPrimaryPokemonStatusEffectRequirement([StatusEffect.NONE]) // Pokemon must not have status - .withPrimaryPokemonHealthRatioRequirement([0.34, 1]) // Pokemon must have above 1/3rd HP - .withOption(new MysteryEncounterOptionBuilder() - .withSceneMoneyRequirement(0, 2) // Wave scaling money multiplier of 2 - .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 i18next.t("mysteryEncounter:shady_vitamin_dealer_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; - - 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) => { - // Damage and status applied after dealer leaves (to make thematic sense) - const encounter = scene.currentBattle.mysteryEncounter; - const chosenPokemon = encounter.misc.chosenPokemon; - - // Pokemon takes 1/3 max HP damage - const damage = Math.round(chosenPokemon.getMaxHp() / 3); - chosenPokemon.hp = Math.max(chosenPokemon.hp - damage, 0); - - // Roll for poison (80%) - if (randSeedInt(10) < 8) { - if (chosenPokemon.trySetStatus(StatusEffect.TOXIC)) { - // Toxic applied - queueEncounterMessage(scene, "mysteryEncounter:shady_vitamin_dealer_bad_poison"); - } else { - // Pokemon immune or something else prevents status - queueEncounterMessage(scene, "mysteryEncounter:shady_vitamin_dealer_damage_only"); - } - } else { - queueEncounterMessage(scene, "mysteryEncounter:shady_vitamin_dealer_damage_only"); - } - - setEncounterExp(scene, [chosenPokemon.id], 100); - - chosenPokemon.updateInfo(); - }) - .build()) - .withOption(new MysteryEncounterOptionBuilder() - .withSceneMoneyRequirement(0, 5) // Wave scaling money multiplier of 5 - .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 i18next.t("mysteryEncounter:shady_vitamin_dealer_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, "mysteryEncounter:shady_vitamin_dealer_poison"); - } else { - // Pokemon immune or something else prevents status - queueEncounterMessage(scene, "mysteryEncounter:shady_vitamin_dealer_no_bad_effects"); - } - } else { - queueEncounterMessage(scene, "mysteryEncounter:shady_vitamin_dealer_no_bad_effects"); - } - - setEncounterExp(scene, [chosenPokemon.id], 100); - - chosenPokemon.updateInfo(); - }) - .build()) - .withOptionPhase(async (scene: BattleScene) => { - // Leave encounter with no rewards or exp - leaveEncounterWithoutBattle(scene, true); - return true; - }) - .build(); diff --git a/src/data/mystery-encounters/encounters/sleeping-snorlax-encounter.ts b/src/data/mystery-encounters/encounters/sleeping-snorlax-encounter.ts new file mode 100644 index 00000000000..b0130c34419 --- /dev/null +++ b/src/data/mystery-encounters/encounters/sleeping-snorlax-encounter.ts @@ -0,0 +1,175 @@ +import { STEALING_MOVES } from "#app/data/mystery-encounters/requirements/requirement-groups"; +import { modifierTypes } from "#app/modifier/modifier-type"; +import { BerryType } from "#enums/berry-type"; +import { MysteryEncounterType } from "#enums/mystery-encounter-type"; +import { Species } from "#enums/species"; +import BattleScene from "../../../battle-scene"; +import * as Utils from "../../../utils"; +import { getPokemonSpecies } from "../../pokemon-species"; +import { Status, StatusEffect } from "../../status-effect"; +import IMysteryEncounter, { + MysteryEncounterBuilder, + MysteryEncounterTier, +} from "../mystery-encounter"; +import { MysteryEncounterOptionBuilder } from "../mystery-encounter-option"; +import { MoveRequirement } from "../mystery-encounter-requirements"; +import { + EnemyPartyConfig, + EnemyPokemonConfig, + generateModifierTypeOption, + initBattleWithEnemyConfig, + leaveEncounterWithoutBattle, + queueEncounterMessage, + setEncounterExp, + setEncounterRewards, +} from "../mystery-encounter-utils"; + +/** i18n namespace for the encounter */ +const namespace = "mysteryEncounter:sleeping_snorlax"; + +export const SleepingSnorlaxEncounter: IMysteryEncounter = + MysteryEncounterBuilder.withEncounterType( + MysteryEncounterType.SLEEPING_SNORLAX + ) + .withEncounterTier(MysteryEncounterTier.ULTRA) + .withSceneWaveRangeRequirement(10, 180) // waves 10 to 180 + .withCatchAllowed(true) + .withHideWildIntroMessage(true) + .withIntroSpriteConfigs([ + { + spriteKey: Species.SNORLAX.toString(), + fileRoot: "pokemon", + hasShadow: true, + tint: 0.25, + scale: 1.5, + repeat: true, + y: 5, + }, + ]) + .withIntroDialogue([ + { + text: `${namespace}_intro_message`, + }, + ]) + .withOnInit((scene: BattleScene) => { + const encounter = scene.currentBattle.mysteryEncounter; + console.log(encounter); + + // Calculate boss mon + const bossSpecies = getPokemonSpecies(Species.SNORLAX); + const pokemonConfig: EnemyPokemonConfig = { + species: bossSpecies, + isBoss: true, + status: StatusEffect.SLEEP, + }; + const config: EnemyPartyConfig = { + levelAdditiveMultiplier: 2, + pokemonConfigs: [pokemonConfig], + }; + encounter.enemyPartyConfigs = [config]; + return true; + }) + .withTitle(`${namespace}_title`) + .withDescription(`${namespace}_description`) + .withQuery(`${namespace}_query`) + .withSimpleOption( + { + buttonLabel: `${namespace}_option_1_label`, + buttonTooltip: `${namespace}_option_1_tooltip`, + selected: [ + { + text: `${namespace}_option_1_selected_message`, + }, + ], + }, + async (scene: BattleScene) => { + // Pick battle + // TODO: do we want special rewards for this? + // setCustomEncounterRewards(scene, { guaranteedModifierTypeFuncs: [modifierTypes.LEFTOVERS], fillRemaining: true}); + await initBattleWithEnemyConfig( + scene, + scene.currentBattle.mysteryEncounter.enemyPartyConfigs[0] + ); + } + ) + .withSimpleOption( + { + buttonLabel: `${namespace}_option_2_label`, + buttonTooltip: `${namespace}_option_2_tooltip`, + selected: [ + { + text: `${namespace}_option_2_selected_message`, + }, + ], + }, + async (scene: BattleScene) => { + const instance = scene.currentBattle.mysteryEncounter; + let roll: integer; + scene.executeWithSeedOffset(() => { + roll = Utils.randSeedInt(16, 0); + }, scene.currentBattle.waveIndex); + + // Half Snorlax exp to entire party + setEncounterExp( + scene, + scene.getParty().map((p) => p.id), + 98 + ); + + if (roll > 4) { + // Fall asleep and get a sitrus berry (75%) + const p = instance.primaryPokemon; + p.status = new Status(StatusEffect.SLEEP, 0, 3); + p.updateInfo(true); + // const sitrus = (modifierTypes.BERRY?.() as ModifierTypeGenerator).generateType(scene.getParty(), [BerryType.SITRUS]); + const sitrus = generateModifierTypeOption( + scene, + modifierTypes.BERRY, + [BerryType.SITRUS] + ); + + setEncounterRewards(scene, { + guaranteedModifierTypeOptions: [sitrus], + fillRemaining: false, + }); + queueEncounterMessage(scene, `${namespace}_option_2_bad_result`); + leaveEncounterWithoutBattle(scene); + } else { + // Heal to full (25%) + for (const pokemon of scene.getParty()) { + pokemon.hp = pokemon.getMaxHp(); + pokemon.resetStatus(); + for (const move of pokemon.moveset) { + move.ppUsed = 0; + } + pokemon.updateInfo(true); + } + + queueEncounterMessage(scene, `${namespace}_option_2_good_result`); + leaveEncounterWithoutBattle(scene); + } + } + ) + .withOption( + new MysteryEncounterOptionBuilder() + .withPrimaryPokemonRequirement(new MoveRequirement(STEALING_MOVES)) + .withDialogue({ + buttonLabel: `${namespace}_option_3_label`, + buttonTooltip: `${namespace}_option_3_tooltip`, + disabledTooltip: `${namespace}_option_3_disabled_tooltip`, + }) + .withOptionPhase(async (scene: BattleScene) => { + // Steal the Snorlax's Leftovers + const instance = scene.currentBattle.mysteryEncounter; + setEncounterRewards(scene, { + guaranteedModifierTypeFuncs: [modifierTypes.LEFTOVERS], + fillRemaining: false, + }); + queueEncounterMessage(scene, `${namespace}_option_3_good_result`); + // Snorlax exp to Pokemon that did the stealing + setEncounterExp(scene, [instance.primaryPokemon.id], 189); + leaveEncounterWithoutBattle(scene); + }) + .build() + ) + .build(); diff --git a/src/data/mystery-encounters/encounters/sleeping-snorlax.ts b/src/data/mystery-encounters/encounters/sleeping-snorlax.ts deleted file mode 100644 index 444ce5a6581..00000000000 --- a/src/data/mystery-encounters/encounters/sleeping-snorlax.ts +++ /dev/null @@ -1,113 +0,0 @@ -import { - modifierTypes -} from "#app/modifier/modifier-type"; -import { BerryType } from "#enums/berry-type"; -import { MysteryEncounterType } from "#enums/mystery-encounter-type"; -import { Species } from "#enums/species"; -import BattleScene from "../../../battle-scene"; -import * as Utils from "../../../utils"; -import { getPokemonSpecies } from "../../pokemon-species"; -import { Status, StatusEffect } from "../../status-effect"; -import MysteryEncounter, { MysteryEncounterBuilder, MysteryEncounterTier } from "../mystery-encounter"; -import { MysteryEncounterOptionBuilder } from "../mystery-encounter-option"; -import { MoveRequirement } from "../mystery-encounter-requirements"; -import { - EnemyPartyConfig, - EnemyPokemonConfig, generateModifierTypeOption, - initBattleWithEnemyConfig, - leaveEncounterWithoutBattle, queueEncounterMessage, setEncounterExp, - setEncounterRewards -} from "../mystery-encounter-utils"; -import { STEALING_MOVES } from "#app/data/mystery-encounters/requirements/requirement-groups"; - -export const SleepingSnorlaxEncounter: MysteryEncounter = MysteryEncounterBuilder - .withEncounterType(MysteryEncounterType.SLEEPING_SNORLAX) - .withEncounterTier(MysteryEncounterTier.ULTRA) - .withIntroSpriteConfigs([ - { - spriteKey: Species.SNORLAX.toString(), - fileRoot: "pokemon", - hasShadow: true, - tint: 0.25, - scale: 1.5, - repeat: true, - y: 5 - } - ]) - .withSceneWaveRangeRequirement(10, 180) // waves 10 to 180 - .withCatchAllowed(true) - .withHideWildIntroMessage(true) - .withOnInit((scene: BattleScene) => { - const encounter = scene.currentBattle.mysteryEncounter; - console.log(encounter); - - // Calculate boss mon - const bossSpecies = getPokemonSpecies(Species.SNORLAX); - const pokemonConfig: EnemyPokemonConfig = { - species: bossSpecies, - isBoss: true, - status: StatusEffect.SLEEP - }; - const config: EnemyPartyConfig = { - levelAdditiveMultiplier: 2, - pokemonConfigs: [pokemonConfig] - }; - encounter.enemyPartyConfigs = [config]; - return true; - }) - .withOptionPhase(async (scene: BattleScene) => { - // Pick battle - // TODO: do we want special rewards for this? - // setCustomEncounterRewards(scene, { guaranteedModifierTypeFuncs: [modifierTypes.LEFTOVERS], fillRemaining: true}); - await initBattleWithEnemyConfig(scene, scene.currentBattle.mysteryEncounter.enemyPartyConfigs[0]); - }) - .withOptionPhase(async (scene: BattleScene) => { - const instance = scene.currentBattle.mysteryEncounter; - let roll: integer; - scene.executeWithSeedOffset(() => { - roll = Utils.randSeedInt(16, 0); - }, scene.currentBattle.waveIndex); - - // Half Snorlax exp to entire party - setEncounterExp(scene, scene.getParty().map(p => p.id), 98); - - if (roll > 4) { - // Fall asleep and get a sitrus berry (75%) - const p = instance.primaryPokemon; - p.status = new Status(StatusEffect.SLEEP, 0, 3); - p.updateInfo(true); - // const sitrus = (modifierTypes.BERRY?.() as ModifierTypeGenerator).generateType(scene.getParty(), [BerryType.SITRUS]); - const sitrus = generateModifierTypeOption(scene, modifierTypes.BERRY, [BerryType.SITRUS]); - - setEncounterRewards(scene, { guaranteedModifierTypeOptions: [sitrus], fillRemaining: false }); - queueEncounterMessage(scene, "mysteryEncounter:sleeping_snorlax_option_2_bad_result"); - leaveEncounterWithoutBattle(scene); - } else { - // Heal to full (25%) - for (const pokemon of scene.getParty()) { - pokemon.hp = pokemon.getMaxHp(); - pokemon.resetStatus(); - for (const move of pokemon.moveset) { - move.ppUsed = 0; - } - pokemon.updateInfo(true); - } - - queueEncounterMessage(scene, "mysteryEncounter:sleeping_snorlax_option_2_good_result"); - leaveEncounterWithoutBattle(scene); - } - }) - .withOption(new MysteryEncounterOptionBuilder() - .withPrimaryPokemonRequirement(new MoveRequirement(STEALING_MOVES)) - .withOptionPhase(async (scene: BattleScene) => { - // Steal the Snorlax's Leftovers - const instance = scene.currentBattle.mysteryEncounter; - setEncounterRewards(scene, { guaranteedModifierTypeFuncs: [modifierTypes.LEFTOVERS], fillRemaining: false }); - queueEncounterMessage(scene, "mysteryEncounter:sleeping_snorlax_option_3_good_result"); - // Snorlax exp to Pokemon that did the stealing - setEncounterExp(scene, [instance.primaryPokemon.id], 189); - leaveEncounterWithoutBattle(scene); - }) - .build() - ) - .build(); diff --git a/src/data/mystery-encounters/encounters/training-session-encounter.ts b/src/data/mystery-encounters/encounters/training-session-encounter.ts new file mode 100644 index 00000000000..c5f0ee2e46f --- /dev/null +++ b/src/data/mystery-encounters/encounters/training-session-encounter.ts @@ -0,0 +1,444 @@ +import { Ability, allAbilities } from "#app/data/ability"; +import { + EnemyPartyConfig, + getEncounterText, + initBattleWithEnemyConfig, + selectPokemonForOption, + setEncounterRewards, +} from "#app/data/mystery-encounters/mystery-encounter-utils"; +import { Nature, getNatureName } 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 { pokemonInfo } from "#app/locales/en/pokemon-info"; +import { PokemonHeldItemModifier } from "#app/modifier/modifier"; +import { PokemonHeldItemModifierType } from "#app/modifier/modifier-type"; +import { AbilityAttr } from "#app/system/game-data"; +import PokemonData from "#app/system/pokemon-data"; +import { OptionSelectItem } from "#app/ui/abstact-option-select-ui-handler"; +import { randSeedShuffle } from "#app/utils"; +import { BattlerTagType } from "#enums/battler-tag-type"; +import { MysteryEncounterType } from "#enums/mystery-encounter-type"; +import BattleScene from "../../../battle-scene"; +import IMysteryEncounter, { + MysteryEncounterBuilder, + MysteryEncounterTier, +} from "../mystery-encounter"; +import { MysteryEncounterOptionBuilder } from "../mystery-encounter-option"; + +/** The i18n namespace for the encounter */ +const namespace = "mysteryEncounter:training_session"; + +export const TrainingSessionEncounter: IMysteryEncounter = + MysteryEncounterBuilder.withEncounterType( + MysteryEncounterType.TRAINING_SESSION + ) + .withEncounterTier(MysteryEncounterTier.ULTRA) + .withSceneWaveRangeRequirement(10, 180) // waves 10 to 180 + .withHideWildIntroMessage(true) + .withIntroSpriteConfigs([ + { + spriteKey: "training_gear", + fileRoot: "mystery-encounters", + hasShadow: true, + y: 3, + }, + ]) + .withIntroDialogue([ + { + text: `${namespace}_intro_message`, + }, + ]) + .withTitle(`${namespace}_title`) + .withDescription(`${namespace}_description`) + .withQuery(`${namespace}_query`) + .withOption( + new MysteryEncounterOptionBuilder() + .withDialogue({ + buttonLabel: `${namespace}_option_1_label`, + buttonTooltip: `${namespace}_option_1_tooltip`, + selected: [ + { + text: `${namespace}_option_selected_message`, + }, + ], + }) + .withPreOptionPhase(async (scene: BattleScene): Promise => { + const encounter = scene.currentBattle.mysteryEncounter; + const onPokemonSelected = (pokemon: PlayerPokemon) => { + encounter.misc = { + playerPokemon: pokemon, + }; + }; + + return selectPokemonForOption(scene, onPokemonSelected); + }) + .withOptionPhase(async (scene: BattleScene) => { + const encounter = scene.currentBattle.mysteryEncounter; + const playerPokemon: PlayerPokemon = encounter.misc.playerPokemon; + + // Spawn light training session with chosen pokemon + // Every 50 waves, add +1 boss segment, capping at 5 + const segments = Math.min( + 2 + Math.floor(scene.currentBattle.waveIndex / 50), + 5 + ); + const modifiers = new ModifiersHolder(); + const config = getEnemyConfig( + scene, + playerPokemon, + segments, + modifiers + ); + scene.removePokemonFromPlayerParty(playerPokemon, false); + + const getIvName = (index: number) => { + switch (index) { + case Stat.HP: + return pokemonInfo.Stat["HPshortened"]; + case Stat.ATK: + return pokemonInfo.Stat["ATKshortened"]; + case Stat.DEF: + return pokemonInfo.Stat["DEFshortened"]; + case Stat.SPATK: + return pokemonInfo.Stat["SPATKshortened"]; + case Stat.SPDEF: + return pokemonInfo.Stat["SPDEFshortened"]; + case Stat.SPD: + return pokemonInfo.Stat["SPDshortened"]; + } + }; + + const onBeforeRewardsPhase = () => { + encounter.setDialogueToken("stat1", "-"); + encounter.setDialogueToken("stat2", "-"); + // Add the pokemon back to party with IV boost + const ivIndexes = []; + playerPokemon.ivs.forEach((iv, index) => { + if (iv < 31) { + ivIndexes.push({ iv: iv, index: index }); + } + }); + + // Improves 2 random non-maxed IVs + // +10 if IV is < 10, +5 if between 10-20, and +3 if > 20 + // A 0-4 starting IV will cap in 6 encounters (assuming you always rolled that IV) + // 5-14 starting IV caps in 5 encounters + // 15-19 starting IV caps in 4 encounters + // 20-24 starting IV caps in 3 encounters + // 25-27 starting IV caps in 2 encounters + let improvedCount = 0; + while (ivIndexes.length > 0 && improvedCount < 2) { + randSeedShuffle(ivIndexes); + const ivToChange = ivIndexes.pop(); + let newVal = ivToChange.iv; + if (improvedCount === 0) { + encounter.setDialogueToken( + "stat1", + getIvName(ivToChange.index) + ); + } else { + encounter.setDialogueToken( + "stat2", + getIvName(ivToChange.index) + ); + } + + // Corrects required encounter breakpoints to be continuous for all IV values + if (ivToChange.iv <= 21 && ivToChange.iv - (1 % 5) === 0) { + newVal += 1; + } + + newVal += ivToChange.iv <= 10 ? 10 : ivToChange.iv <= 20 ? 5 : 3; + newVal = Math.min(newVal, 31); + playerPokemon.ivs[ivToChange.index] = newVal; + improvedCount++; + } + + if (improvedCount > 0) { + playerPokemon.calculateStats(); + scene.gameData.updateSpeciesDexIvs( + playerPokemon.species.getRootSpeciesId(true), + playerPokemon.ivs + ); + scene.gameData.setPokemonCaught(playerPokemon, false); + } + + // Add pokemon and mods back + scene.getParty().push(playerPokemon); + for (const mod of modifiers.value) { + scene.addModifier(mod, true, false, false, true); + } + scene.updateModifiers(true); + scene.queueMessage( + getEncounterText(scene, `${namespace}_battle_finished_1`), + null, + true + ); + }; + + setEncounterRewards( + scene, + { fillRemaining: true }, + null, + onBeforeRewardsPhase + ); + + return initBattleWithEnemyConfig(scene, config); + }) + .build() + ) + .withOption( + new MysteryEncounterOptionBuilder() + .withDialogue({ + buttonLabel: `${namespace}_option_2_label`, + buttonTooltip: `${namespace}_option_2_tooltip`, + secondOptionPrompt: `${namespace}_option_2_select_prompt`, + selected: [ + { + text: `${namespace}_option_selected_message`, + }, + ], + }) + .withPreOptionPhase(async (scene: BattleScene): Promise => { + // Open menu for selecting pokemon and Nature + const encounter = scene.currentBattle.mysteryEncounter; + const natures = new Array(25).fill(null).map((val, i) => i as Nature); + const onPokemonSelected = (pokemon: PlayerPokemon) => { + // Return the options for nature selection + return natures.map((nature: Nature) => { + const option: OptionSelectItem = { + label: getNatureName(nature, true, true, true, scene.uiTheme), + handler: () => { + // Pokemon and second option selected + encounter.setDialogueToken("nature", getNatureName(nature)); + encounter.misc = { + playerPokemon: pokemon, + chosenNature: nature, + }; + return true; + }, + }; + return option; + }); + }; + + return selectPokemonForOption(scene, onPokemonSelected); + }) + .withOptionPhase(async (scene: BattleScene) => { + const encounter = scene.currentBattle.mysteryEncounter; + const playerPokemon: PlayerPokemon = encounter.misc.playerPokemon; + + // Spawn medium training session with chosen pokemon + // Every 40 waves, add +1 boss segment, capping at 6 + const segments = Math.min( + 2 + Math.floor(scene.currentBattle.waveIndex / 40), + 6 + ); + const modifiers = new ModifiersHolder(); + const config = getEnemyConfig( + scene, + playerPokemon, + segments, + modifiers + ); + scene.removePokemonFromPlayerParty(playerPokemon, false); + + const onBeforeRewardsPhase = () => { + scene.queueMessage( + getEncounterText(scene, `${namespace}_battle_finished_2`), + null, + true + ); + // Add the pokemon back to party with Nature change + playerPokemon.setNature(encounter.misc.chosenNature); + scene.gameData.setPokemonCaught(playerPokemon, false); + + // Add pokemon and mods back + scene.getParty().push(playerPokemon); + for (const mod of modifiers.value) { + scene.addModifier(mod, true, false, false, true); + } + scene.updateModifiers(true); + }; + + setEncounterRewards( + scene, + { fillRemaining: true }, + null, + onBeforeRewardsPhase + ); + + return initBattleWithEnemyConfig(scene, config); + }) + .build() + ) + .withOption( + new MysteryEncounterOptionBuilder() + .withDialogue({ + buttonLabel: `${namespace}_option_3_label`, + buttonTooltip: `${namespace}_option_3_tooltip`, + secondOptionPrompt: `${namespace}_option_3_select_prompt`, + selected: [ + { + text: `${namespace}_option_selected_message`, + }, + ], + }) + .withPreOptionPhase(async (scene: BattleScene): Promise => { + // Open menu for selecting pokemon and ability to learn + const encounter = scene.currentBattle.mysteryEncounter; + const onPokemonSelected = (pokemon: PlayerPokemon) => { + // Return the options for ability selection + const speciesForm = !!pokemon.getFusionSpeciesForm() + ? pokemon.getFusionSpeciesForm() + : pokemon.getSpeciesForm(); + const abilityCount = speciesForm.getAbilityCount(); + const abilities = new Array(abilityCount) + .fill(null) + .map((val, i) => allAbilities[speciesForm.getAbility(i)]); + return abilities.map((ability: Ability, index) => { + const option: OptionSelectItem = { + label: ability.name, + handler: () => { + // Pokemon and ability selected + encounter.setDialogueToken("ability", ability.name); + encounter.misc = { + playerPokemon: pokemon, + abilityIndex: index, + }; + return true; + }, + onHover: () => { + scene.ui.showText(ability.description); + }, + }; + return option; + }); + }; + + return selectPokemonForOption(scene, onPokemonSelected); + }) + .withOptionPhase(async (scene: BattleScene) => { + const encounter = scene.currentBattle.mysteryEncounter; + const playerPokemon: PlayerPokemon = encounter.misc.playerPokemon; + + // Spawn hard training session with chosen pokemon + // Every 30 waves, add +1 boss segment, capping at 6 + // Also starts with +1 to all stats + const segments = Math.min( + 2 + Math.floor(scene.currentBattle.waveIndex / 30), + 6 + ); + const modifiers = new ModifiersHolder(); + const config = getEnemyConfig( + scene, + playerPokemon, + segments, + modifiers + ); + config.pokemonConfigs[0].tags = [ + BattlerTagType.MYSTERY_ENCOUNTER_POST_SUMMON, + ]; + scene.removePokemonFromPlayerParty(playerPokemon, false); + + const onBeforeRewardsPhase = () => { + scene.queueMessage( + getEncounterText(scene, `${namespace}_battle_finished_3`), + null, + true + ); + // Add the pokemon back to party with ability change + const abilityIndex = encounter.misc.abilityIndex; + if (!!playerPokemon.getFusionSpeciesForm()) { + playerPokemon.fusionAbilityIndex = abilityIndex; + if ( + speciesStarters.hasOwnProperty( + playerPokemon.fusionSpecies.speciesId + ) + ) { + scene.gameData.starterData[ + playerPokemon.fusionSpecies.speciesId + ].abilityAttr |= + abilityIndex !== 1 || playerPokemon.fusionSpecies.ability2 + ? Math.pow(2, playerPokemon.fusionAbilityIndex) + : AbilityAttr.ABILITY_HIDDEN; + } + } else { + playerPokemon.abilityIndex = abilityIndex; + if ( + speciesStarters.hasOwnProperty(playerPokemon.species.speciesId) + ) { + scene.gameData.starterData[ + playerPokemon.species.speciesId + ].abilityAttr |= + abilityIndex !== 1 || playerPokemon.species.ability2 + ? Math.pow(2, playerPokemon.abilityIndex) + : AbilityAttr.ABILITY_HIDDEN; + } + } + + playerPokemon.getAbility(); + playerPokemon.calculateStats(); + scene.gameData.setPokemonCaught(playerPokemon, false); + + // Add pokemon and mods back + scene.getParty().push(playerPokemon); + for (const mod of modifiers.value) { + scene.addModifier(mod, true, false, false, true); + } + scene.updateModifiers(true); + }; + + setEncounterRewards( + scene, + { fillRemaining: true }, + null, + onBeforeRewardsPhase + ); + + return initBattleWithEnemyConfig(scene, config); + }) + .build() + ) + .build(); + +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[]; + + const data = new PokemonData(playerPokemon); + return { + pokemonConfigs: [ + { + species: playerPokemon.species, + isBoss: true, + bossSegments: segments, + formIndex: playerPokemon.formIndex, + level: playerPokemon.level, + dataSource: data, + modifierTypes: modifierTypes, + }, + ], + }; +} + +class ModifiersHolder { + public value: PokemonHeldItemModifier[] = []; + + constructor() {} +} diff --git a/src/data/mystery-encounters/encounters/training-session.ts b/src/data/mystery-encounters/encounters/training-session.ts deleted file mode 100644 index 8030cbaf850..00000000000 --- a/src/data/mystery-encounters/encounters/training-session.ts +++ /dev/null @@ -1,309 +0,0 @@ -import { Ability, allAbilities } from "#app/data/ability"; -import { - EnemyPartyConfig, - getEncounterText, - initBattleWithEnemyConfig, - selectPokemonForOption, - setEncounterRewards -} from "#app/data/mystery-encounters/mystery-encounter-utils"; -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 { pokemonInfo } from "#app/locales/en/pokemon-info"; -import { PokemonHeldItemModifier } from "#app/modifier/modifier"; -import { PokemonHeldItemModifierType } from "#app/modifier/modifier-type"; -import { AbilityAttr } from "#app/system/game-data"; -import PokemonData from "#app/system/pokemon-data"; -import { OptionSelectItem } from "#app/ui/abstact-option-select-ui-handler"; -import { randSeedShuffle } from "#app/utils"; -import { BattlerTagType } from "#enums/battler-tag-type"; -import { MysteryEncounterType } from "#enums/mystery-encounter-type"; -import BattleScene from "../../../battle-scene"; -import MysteryEncounter, { MysteryEncounterBuilder, MysteryEncounterTier } from "../mystery-encounter"; -import { MysteryEncounterOptionBuilder } from "../mystery-encounter-option"; - -export const TrainingSessionEncounter: MysteryEncounter = MysteryEncounterBuilder - .withEncounterType(MysteryEncounterType.TRAINING_SESSION) - .withEncounterTier(MysteryEncounterTier.ULTRA) - .withIntroSpriteConfigs([ - { - spriteKey: "training_gear", - fileRoot: "mystery-encounters", - hasShadow: true, - y: 3 - } - ]) - .withSceneWaveRangeRequirement(10, 180) // waves 10 to 180 - .withHideWildIntroMessage(true) - .withOption(new MysteryEncounterOptionBuilder() - .withPreOptionPhase(async (scene: BattleScene): Promise => { - const encounter = scene.currentBattle.mysteryEncounter; - const onPokemonSelected = (pokemon: PlayerPokemon) => { - encounter.misc = { - playerPokemon: pokemon - }; - }; - - return selectPokemonForOption(scene, onPokemonSelected); - }) - .withOptionPhase(async (scene: BattleScene) => { - const encounter = scene.currentBattle.mysteryEncounter; - const playerPokemon: PlayerPokemon = encounter.misc.playerPokemon; - - // Spawn light training session with chosen pokemon - // Every 50 waves, add +1 boss segment, capping at 5 - const segments = Math.min(2 + Math.floor(scene.currentBattle.waveIndex / 50), 5); - const modifiers = new ModifiersHolder(); - const config = getEnemyConfig(scene, playerPokemon, segments, modifiers); - scene.removePokemonFromPlayerParty(playerPokemon, false); - - const getIvName = (index: number) => { - switch (index) { - case Stat.HP: - return pokemonInfo.Stat["HPshortened"]; - case Stat.ATK: - return pokemonInfo.Stat["ATKshortened"]; - case Stat.DEF: - return pokemonInfo.Stat["DEFshortened"]; - case Stat.SPATK: - return pokemonInfo.Stat["SPATKshortened"]; - case Stat.SPDEF: - return pokemonInfo.Stat["SPDEFshortened"]; - case Stat.SPD: - return pokemonInfo.Stat["SPDshortened"]; - } - }; - - const onBeforeRewardsPhase = () => { - encounter.setDialogueToken("stat1", "-"); - encounter.setDialogueToken("stat2", "-"); - // Add the pokemon back to party with IV boost - const ivIndexes = []; - playerPokemon.ivs.forEach((iv, index) => { - if (iv < 31) { - ivIndexes.push({ iv: iv, index: index }); - } - }); - - // Improves 2 random non-maxed IVs - // +10 if IV is < 10, +5 if between 10-20, and +3 if > 20 - // A 0-4 starting IV will cap in 6 encounters (assuming you always rolled that IV) - // 5-14 starting IV caps in 5 encounters - // 15-19 starting IV caps in 4 encounters - // 20-24 starting IV caps in 3 encounters - // 25-27 starting IV caps in 2 encounters - let improvedCount = 0; - while (ivIndexes.length > 0 && improvedCount < 2) { - randSeedShuffle(ivIndexes); - const ivToChange = ivIndexes.pop(); - let newVal = ivToChange.iv; - if (improvedCount === 0) { - encounter.setDialogueToken("stat1", getIvName(ivToChange.index)); - } else { - encounter.setDialogueToken("stat2", getIvName(ivToChange.index)); - } - - // Corrects required encounter breakpoints to be continuous for all IV values - if (ivToChange.iv <= 21 && ivToChange.iv - 1 % 5 === 0) { - newVal += 1; - } - - newVal += ivToChange.iv <= 10 ? 10 : ivToChange.iv <= 20 ? 5 : 3; - newVal = Math.min(newVal, 31); - playerPokemon.ivs[ivToChange.index] = newVal; - improvedCount++; - } - - if (improvedCount > 0) { - playerPokemon.calculateStats(); - scene.gameData.updateSpeciesDexIvs(playerPokemon.species.getRootSpeciesId(true), playerPokemon.ivs); - scene.gameData.setPokemonCaught(playerPokemon, false); - } - - // Add pokemon and mods back - scene.getParty().push(playerPokemon); - for (const mod of modifiers.value) { - scene.addModifier(mod, true, false, false, true); - } - scene.updateModifiers(true); - scene.queueMessage(getEncounterText(scene, "mysteryEncounter:training_session_battle_finished_1"), null, true); - }; - - setEncounterRewards(scene, { fillRemaining: true }, null, onBeforeRewardsPhase); - - return initBattleWithEnemyConfig(scene, config); - }) - .build() - ) - .withOption(new MysteryEncounterOptionBuilder() - .withPreOptionPhase(async (scene: BattleScene): Promise => { - // Open menu for selecting pokemon and Nature - const encounter = scene.currentBattle.mysteryEncounter; - const natures = new Array(25).fill(null).map((val, i) => i as Nature); - const onPokemonSelected = (pokemon: PlayerPokemon) => { - // Return the options for nature selection - return natures.map((nature: Nature) => { - const option: OptionSelectItem = { - label: getNatureName(nature, true, true, true, scene.uiTheme), - handler: () => { - // Pokemon and second option selected - encounter.setDialogueToken("nature", getNatureName(nature)); - encounter.misc = { - playerPokemon: pokemon, - chosenNature: nature - }; - return true; - } - }; - return option; - }); - }; - - return selectPokemonForOption(scene, onPokemonSelected); - }) - .withOptionPhase(async (scene: BattleScene) => { - const encounter = scene.currentBattle.mysteryEncounter; - const playerPokemon: PlayerPokemon = encounter.misc.playerPokemon; - - // Spawn medium training session with chosen pokemon - // Every 40 waves, add +1 boss segment, capping at 6 - const segments = Math.min(2 + Math.floor(scene.currentBattle.waveIndex / 40), 6); - const modifiers = new ModifiersHolder(); - const config = getEnemyConfig(scene, playerPokemon, segments, modifiers); - scene.removePokemonFromPlayerParty(playerPokemon, false); - - const onBeforeRewardsPhase = () => { - scene.queueMessage(getEncounterText(scene, "mysteryEncounter:training_session_battle_finished_2"), null, true); - // Add the pokemon back to party with Nature change - playerPokemon.setNature(encounter.misc.chosenNature); - scene.gameData.setPokemonCaught(playerPokemon, false); - - // Add pokemon and mods back - scene.getParty().push(playerPokemon); - for (const mod of modifiers.value) { - scene.addModifier(mod, true, false, false, true); - } - scene.updateModifiers(true); - }; - - setEncounterRewards(scene, { fillRemaining: true }, null, onBeforeRewardsPhase); - - return initBattleWithEnemyConfig(scene, config); - }) - .build() - ) - .withOption(new MysteryEncounterOptionBuilder() - .withPreOptionPhase(async (scene: BattleScene): Promise => { - // Open menu for selecting pokemon and ability to learn - const encounter = scene.currentBattle.mysteryEncounter; - const onPokemonSelected = (pokemon: PlayerPokemon) => { - // Return the options for ability selection - const speciesForm = !!pokemon.getFusionSpeciesForm() ? pokemon.getFusionSpeciesForm() : pokemon.getSpeciesForm(); - const abilityCount = speciesForm.getAbilityCount(); - const abilities = new Array(abilityCount).fill(null).map((val, i) => allAbilities[speciesForm.getAbility(i)]); - return abilities.map((ability: Ability, index) => { - const option: OptionSelectItem = { - label: ability.name, - handler: () => { - // Pokemon and ability selected - encounter.setDialogueToken("ability", ability.name); - encounter.misc = { - playerPokemon: pokemon, - abilityIndex: index - }; - return true; - }, - onHover: () => { - scene.ui.showText(ability.description); - } - }; - return option; - }); - }; - - return selectPokemonForOption(scene, onPokemonSelected); - }) - .withOptionPhase(async (scene: BattleScene) => { - const encounter = scene.currentBattle.mysteryEncounter; - const playerPokemon: PlayerPokemon = encounter.misc.playerPokemon; - - // Spawn hard training session with chosen pokemon - // Every 30 waves, add +1 boss segment, capping at 6 - // Also starts with +1 to all stats - const segments = Math.min(2 + Math.floor(scene.currentBattle.waveIndex / 30), 6); - const modifiers = new ModifiersHolder(); - const config = getEnemyConfig(scene, playerPokemon, segments, modifiers); - config.pokemonConfigs[0].tags = [BattlerTagType.MYSTERY_ENCOUNTER_POST_SUMMON]; - scene.removePokemonFromPlayerParty(playerPokemon, false); - - const onBeforeRewardsPhase = () => { - scene.queueMessage(getEncounterText(scene, "mysteryEncounter:training_session_battle_finished_3"), null, true); - // Add the pokemon back to party with ability change - const abilityIndex = encounter.misc.abilityIndex; - if (!!playerPokemon.getFusionSpeciesForm()) { - playerPokemon.fusionAbilityIndex = abilityIndex; - if (speciesStarters.hasOwnProperty(playerPokemon.fusionSpecies.speciesId)) { - scene.gameData.starterData[playerPokemon.fusionSpecies.speciesId].abilityAttr |= abilityIndex !== 1 || playerPokemon.fusionSpecies.ability2 - ? Math.pow(2, playerPokemon.fusionAbilityIndex) - : AbilityAttr.ABILITY_HIDDEN; - } - } else { - playerPokemon.abilityIndex = abilityIndex; - if (speciesStarters.hasOwnProperty(playerPokemon.species.speciesId)) { - scene.gameData.starterData[playerPokemon.species.speciesId].abilityAttr |= abilityIndex !== 1 || playerPokemon.species.ability2 - ? Math.pow(2, playerPokemon.abilityIndex) - : AbilityAttr.ABILITY_HIDDEN; - } - } - - playerPokemon.getAbility(); - playerPokemon.calculateStats(); - scene.gameData.setPokemonCaught(playerPokemon, false); - - // Add pokemon and mods back - scene.getParty().push(playerPokemon); - for (const mod of modifiers.value) { - scene.addModifier(mod, true, false, false, true); - } - scene.updateModifiers(true); - }; - - setEncounterRewards(scene, { fillRemaining: true }, null, onBeforeRewardsPhase); - - return initBattleWithEnemyConfig(scene, config); - }) - .build() - ) - .build(); - -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[]; - - const data = new PokemonData(playerPokemon); - return { - pokemonConfigs: [ - { - species: playerPokemon.species, - isBoss: true, - bossSegments: segments, - formIndex: playerPokemon.formIndex, - level: playerPokemon.level, - dataSource: data, - modifierTypes: modifierTypes - } - ] - }; -} - -class ModifiersHolder { - public value: PokemonHeldItemModifier[] = []; - - constructor() { - } -} diff --git a/src/data/mystery-encounters/mystery-encounter-dialogue.ts b/src/data/mystery-encounters/mystery-encounter-dialogue.ts index 5129f3bf23e..d96aeb87ba6 100644 --- a/src/data/mystery-encounters/mystery-encounter-dialogue.ts +++ b/src/data/mystery-encounters/mystery-encounter-dialogue.ts @@ -1,14 +1,4 @@ -import { MysteryEncounterType } from "#enums/mystery-encounter-type"; -import { MysteriousChallengersDialogue } from "#app/data/mystery-encounters/dialogue/mysterious-challengers-dialogue"; -import { MysteriousChestDialogue } from "#app/data/mystery-encounters/dialogue/mysterious-chest-dialogue"; -import { DarkDealDialogue } from "#app/data/mystery-encounters/dialogue/dark-deal-dialogue"; -import { FightOrFlightDialogue } from "#app/data/mystery-encounters/dialogue/fight-or-flight-dialogue"; -import { TrainingSessionDialogue } from "#app/data/mystery-encounters/dialogue/training-session-dialogue"; -import { SleepingSnorlaxDialogue } from "./dialogue/sleeping-snorlax-dialogue"; -import { DepartmentStoreSaleDialogue } from "#app/data/mystery-encounters/dialogue/department-store-sale-dialogue"; -import { ShadyVitaminDealerDialogue } from "#app/data/mystery-encounters/dialogue/shady-vitamin-dealer"; import { TextStyle } from "#app/ui/text"; -import { FieldTripDialogue } from "#app/data/mystery-encounters/dialogue/field-trip-dialogue"; export class TextDisplay { speaker?: TemplateStringsArray | `mysteryEncounter:${string}`; @@ -26,15 +16,15 @@ export class OptionTextDisplay { } export class EncounterOptionsDialogue { - title: TemplateStringsArray | `mysteryEncounter:${string}`; - description: TemplateStringsArray | `mysteryEncounter:${string}`; + title?: TemplateStringsArray | `mysteryEncounter:${string}`; + description?: TemplateStringsArray | `mysteryEncounter:${string}`; query?: TemplateStringsArray | `mysteryEncounter:${string}`; - options: [OptionTextDisplay, OptionTextDisplay, ...OptionTextDisplay[]]; // Options array with minimum 2 options + options?: [...OptionTextDisplay[]]; // Options array with minimum 2 options } export default class MysteryEncounterDialogue { intro?: TextDisplay[]; - encounterOptionsDialogue: EncounterOptionsDialogue; + encounterOptionsDialogue?: EncounterOptionsDialogue; outro?: TextDisplay[]; } @@ -81,17 +71,3 @@ export default class MysteryEncounterDialogue { } * */ - -export const allMysteryEncounterDialogue: { [encounterType: number]: MysteryEncounterDialogue } = {}; - -export function initMysteryEncounterDialogue() { - allMysteryEncounterDialogue[MysteryEncounterType.MYSTERIOUS_CHALLENGERS] = MysteriousChallengersDialogue; - allMysteryEncounterDialogue[MysteryEncounterType.MYSTERIOUS_CHEST] = MysteriousChestDialogue; - allMysteryEncounterDialogue[MysteryEncounterType.DARK_DEAL] = DarkDealDialogue; - allMysteryEncounterDialogue[MysteryEncounterType.FIGHT_OR_FLIGHT] = FightOrFlightDialogue; - allMysteryEncounterDialogue[MysteryEncounterType.TRAINING_SESSION] = TrainingSessionDialogue; - allMysteryEncounterDialogue[MysteryEncounterType.SLEEPING_SNORLAX] = SleepingSnorlaxDialogue; - allMysteryEncounterDialogue[MysteryEncounterType.DEPARTMENT_STORE_SALE] = DepartmentStoreSaleDialogue; - allMysteryEncounterDialogue[MysteryEncounterType.SHADY_VITAMIN_DEALER] = ShadyVitaminDealerDialogue; - allMysteryEncounterDialogue[MysteryEncounterType.FIELD_TRIP] = FieldTripDialogue; -} diff --git a/src/data/mystery-encounters/mystery-encounter-option.ts b/src/data/mystery-encounters/mystery-encounter-option.ts index 7731468a977..1ab6a4d59ef 100644 --- a/src/data/mystery-encounters/mystery-encounter-option.ts +++ b/src/data/mystery-encounters/mystery-encounter-option.ts @@ -142,6 +142,7 @@ export class MysteryEncounterOptionBuilder implements Partial> { this.requirements.push(requirement); @@ -183,4 +184,9 @@ export class MysteryEncounterOptionBuilder implements Partial o.dialogue = this.dialogue.encounterOptionsDialogue.options[i]); - } - // Reset any dirty flags or encounter data this.lockEncounterRewardTiers = true; this.dialogueTokens = new Map; @@ -344,7 +339,7 @@ export default class MysteryEncounter implements MysteryEncounter { } } -export class MysteryEncounterBuilder implements Partial { +export class MysteryEncounterBuilder implements Partial { encounterType?: MysteryEncounterType; options?: [MysteryEncounterOption, MysteryEncounterOption, ...MysteryEncounterOption[]] = [null, null]; spriteConfigs?: MysteryEncounterSpriteConfig[]; @@ -372,7 +367,7 @@ export class MysteryEncounterBuilder implements Partial { * @param encounterType * @returns this */ - static withEncounterType(encounterType: MysteryEncounterType): MysteryEncounterBuilder & Pick { + static withEncounterType(encounterType: MysteryEncounterType): MysteryEncounterBuilder & Pick { return Object.assign(new MysteryEncounterBuilder(), { encounterType: encounterType }); } @@ -382,7 +377,7 @@ export class MysteryEncounterBuilder implements Partial { * @param option - MysteryEncounterOption to add, can use MysteryEncounterOptionBuilder to create instance * @returns */ - withOption(option: MysteryEncounterOption): this & Pick { + withOption(option: MysteryEncounterOption): this & Pick { if (this.options[0] === null) { return Object.assign(this, { options: [option, this.options[0]] }); } else if (this.options[1] === null) { @@ -397,11 +392,12 @@ export class MysteryEncounterBuilder implements Partial { * Adds a streamlined option phase. * Only use if no pre-/post-options or condtions necessary. * - * @param callback - OptionPhaseCallback + * @param dialogue - {@linkcode OptionTextDisplay} + * @param callback - {@linkcode OptionPhaseCallback} * @returns */ - withOptionPhase(callback: OptionPhaseCallback) { - return this.withOption(new MysteryEncounterOptionBuilder().withOptionPhase(callback).build()); + withSimpleOption(dialogue: OptionTextDisplay, callback: OptionPhaseCallback) { + return this.withOption(new MysteryEncounterOptionBuilder().withDialogue(dialogue).withOptionPhase(callback).build()); } /** @@ -410,10 +406,19 @@ export class MysteryEncounterBuilder implements Partial { * @param spriteConfigs * @returns */ - withIntroSpriteConfigs(spriteConfigs: MysteryEncounterSpriteConfig[]): this & Pick { + withIntroSpriteConfigs(spriteConfigs: MysteryEncounterSpriteConfig[]): this & Pick { return Object.assign(this, { spriteConfigs: spriteConfigs }); } + withIntroDialogue(dialogue: MysteryEncounterDialogue["intro"] = []) { + this.dialogue = {...this.dialogue, intro: dialogue }; + return this; + } + + withIntro({spriteConfigs, dialogue} : {spriteConfigs: MysteryEncounterSpriteConfig[], dialogue?: MysteryEncounterDialogue["intro"]}) { + return this.withIntroSpriteConfigs(spriteConfigs).withIntroDialogue(dialogue); + } + /** * OPTIONAL */ @@ -430,7 +435,7 @@ export class MysteryEncounterBuilder implements Partial { * @param encounterTier * @returns */ - withEncounterTier(encounterTier: MysteryEncounterTier): this & Required> { + withEncounterTier(encounterTier: MysteryEncounterTier): this & Required> { return Object.assign(this, { encounterTier: encounterTier }); } @@ -441,7 +446,7 @@ export class MysteryEncounterBuilder implements Partial { * @param requirement * @returns */ - withSceneRequirement(requirement: EncounterSceneRequirement): this & Required> { + withSceneRequirement(requirement: EncounterSceneRequirement): this & Required> { if (requirement instanceof EncounterPokemonRequirement) { Error("Incorrectly added pokemon requirement as scene requirement."); } @@ -471,23 +476,45 @@ export class MysteryEncounterBuilder implements Partial { return this.withSceneRequirement(new PartySizeRequirement([min, max ?? min])); } - withPrimaryPokemonRequirement(requirement: EncounterPokemonRequirement): this & Required> { + /** + * Add a primary pokemon requirement + * + * @param requirement {@linkcode EncounterPokemonRequirement} + * @returns + */ + withPrimaryPokemonRequirement(requirement: EncounterPokemonRequirement): this & Required> { this.primaryPokemonRequirements.push(requirement); return Object.assign(this, { primaryPokemonRequirements: this.primaryPokemonRequirements }); } - withPrimaryPokemonStatusEffectRequirement(statusEffect: StatusEffect | StatusEffect[], minNumberOfPokemon: number = 1, invertQuery: boolean = false): this & Required> { + /** + * Add a primary pokemon status effect requirement + * + * @param statusEffect the status effect/s to check + * @param minNumberOfPokemon minimum number of pokemon to have the effect + * @param invertQuery if true will invert the query + * @returns + */ + withPrimaryPokemonStatusEffectRequirement(statusEffect: StatusEffect | StatusEffect[], minNumberOfPokemon: number = 1, invertQuery: boolean = false): this & Required> { return this.withPrimaryPokemonRequirement(new StatusEffectRequirement(statusEffect, minNumberOfPokemon, invertQuery)); } - withPrimaryPokemonHealthRatioRequirement(requiredHealthRange: [number, number], minNumberOfPokemon: number = 1, invertQuery: boolean = false): this & Required> { + /** + * Add a primary pokemon health ratio requirement + * + * @param requiredHealthRange the health range to check + * @param minNumberOfPokemon minimum number of pokemon to have the health range + * @param invertQuery if true will invert the query + * @returns + */ + withPrimaryPokemonHealthRatioRequirement(requiredHealthRange: [number, number], minNumberOfPokemon: number = 1, invertQuery: boolean = false): this & Required> { return this.withPrimaryPokemonRequirement(new HealthRatioRequirement(requiredHealthRange, minNumberOfPokemon, invertQuery)); } // TODO: Maybe add an optional parameter for excluding primary pokemon from the support cast? // ex. if your only grass type pokemon, a snivy, is chosen as primary, if the support pokemon requires a grass type, the event won't trigger because // it's already been - withSecondaryPokemonRequirement(requirement: EncounterPokemonRequirement, excludePrimaryFromSecondaryRequirements: boolean = false): this & Required> { + withSecondaryPokemonRequirement(requirement: EncounterPokemonRequirement, excludePrimaryFromSecondaryRequirements: boolean = false): this & Required> { this.secondaryPokemonRequirements.push(requirement); this.excludePrimaryFromSupportRequirements = excludePrimaryFromSecondaryRequirements; return Object.assign(this, { excludePrimaryFromSecondaryRequirements: this.excludePrimaryFromSupportRequirements, secondaryPokemonRequirements: this.secondaryPokemonRequirements }); @@ -503,7 +530,7 @@ export class MysteryEncounterBuilder implements Partial { * @param doEncounterRewards - synchronous callback function to perform during rewards phase of the encounter * @returns */ - withRewards(doEncounterRewards: (scene: BattleScene) => boolean): this & Required> { + withRewards(doEncounterRewards: (scene: BattleScene) => boolean): this & Required> { return Object.assign(this, { doEncounterRewards: doEncounterRewards }); } @@ -517,7 +544,7 @@ export class MysteryEncounterBuilder implements Partial { * @param doEncounterExp - synchronous callback function to perform during rewards phase of the encounter * @returns */ - withExp(doEncounterExp: (scene: BattleScene) => boolean): this & Required> { + withExp(doEncounterExp: (scene: BattleScene) => boolean): this & Required> { return Object.assign(this, { doEncounterExp: doEncounterExp }); } @@ -528,7 +555,7 @@ export class MysteryEncounterBuilder implements Partial { * @param onInit - synchronous callback function to perform as soon as the encounter is selected for the next phase * @returns */ - withOnInit(onInit: (scene: BattleScene) => boolean): this & Required> { + withOnInit(onInit: (scene: BattleScene) => boolean): this & Required> { return Object.assign(this, { onInit: onInit }); } @@ -537,7 +564,7 @@ export class MysteryEncounterBuilder implements Partial { * @param enemyPartyConfig * @returns */ - withEnemyPartyConfig(enemyPartyConfig: EnemyPartyConfig): this & Required> { + withEnemyPartyConfig(enemyPartyConfig: EnemyPartyConfig): this & Required> { this.enemyPartyConfigs.push(enemyPartyConfig); return Object.assign(this, { enemyPartyConfigs: this.enemyPartyConfigs }); } @@ -548,7 +575,7 @@ export class MysteryEncounterBuilder implements Partial { * @param catchAllowed - if true, allows enemy pokemon to be caught during the encounter * @returns */ - withCatchAllowed(catchAllowed: boolean): this & Required> { + withCatchAllowed(catchAllowed: boolean): this & Required> { return Object.assign(this, { catchAllowed: catchAllowed }); } @@ -556,7 +583,7 @@ export class MysteryEncounterBuilder implements Partial { * @param hideBattleIntroMessage - if true, will not show the trainerAppeared/wildAppeared/bossAppeared message for an encounter * @returns */ - withHideWildIntroMessage(hideBattleIntroMessage: boolean): this & Required> { + withHideWildIntroMessage(hideBattleIntroMessage: boolean): this & Required> { return Object.assign(this, { hideBattleIntroMessage: hideBattleIntroMessage }); } @@ -564,11 +591,88 @@ export class MysteryEncounterBuilder implements Partial { * @param hideIntroVisuals - if false, will not hide the intro visuals that are displayed at the beginning of encounter * @returns */ - withHideIntroVisuals(hideIntroVisuals: boolean): this & Required> { + withHideIntroVisuals(hideIntroVisuals: boolean): this & Required> { return Object.assign(this, { hideIntroVisuals: hideIntroVisuals }); } - build(this: MysteryEncounter) { - return new MysteryEncounter(this); + /** + * Add a title for the encounter + * + * @param title - title of the encounter + * @returns + */ + withTitle(title: TemplateStringsArray | `mysteryEncounter:${string}`) { + const encounterOptionsDialogue = this.dialogue.encounterOptionsDialogue ?? {}; + + this.dialogue = { + ...this.dialogue, + encounterOptionsDialogue: { + ...encounterOptionsDialogue, + title, + } + }; + + return this; + } + + /** + * Add a description of the encounter + * + * @param description - description of the encounter + * @returns + */ + withDescription(description: TemplateStringsArray | `mysteryEncounter:${string}`) { + const encounterOptionsDialogue = this.dialogue.encounterOptionsDialogue ?? {}; + + this.dialogue = { + ...this.dialogue, + encounterOptionsDialogue: { + ...encounterOptionsDialogue, + description, + } + }; + + return this; + } + + /** + * Add a query for the encounter + * + * @param query - query to use for the encounter + * @returns + */ + withQuery(query: TemplateStringsArray | `mysteryEncounter:${string}`) { + const encounterOptionsDialogue = this.dialogue.encounterOptionsDialogue ?? {}; + + this.dialogue = { + ...this.dialogue, + encounterOptionsDialogue: { + ...encounterOptionsDialogue, + query, + } + }; + + return this; + } + + /** + * Add outro dialogue/s for the encounter + * + * @param dialogue - outro dialogue/s + * @returns + */ + withOutroDialogue(dialogue: MysteryEncounterDialogue["outro"] = []) { + this.dialogue = {...this.dialogue, outro: dialogue }; + return this; + } + + /** + * Builds the mystery encounter + * + * @param this - MysteryEncounter + * @returns + */ + build(this: IMysteryEncounter) { + return new IMysteryEncounter(this); } } diff --git a/src/data/mystery-encounters/mystery-encounters.ts b/src/data/mystery-encounters/mystery-encounters.ts index 1651430e59d..7a78c6edb4c 100644 --- a/src/data/mystery-encounters/mystery-encounters.ts +++ b/src/data/mystery-encounters/mystery-encounters.ts @@ -1,15 +1,15 @@ -import MysteryEncounter from "./mystery-encounter"; -import { DarkDealEncounter } from "./encounters/dark-deal"; -import { MysteriousChallengersEncounter } from "./encounters/mysterious-challengers"; -import { MysteriousChestEncounter } from "./encounters/mysterious-chest"; -import { FightOrFlightEncounter } from "#app/data/mystery-encounters/encounters/fight-or-flight"; -import { TrainingSessionEncounter } from "#app/data/mystery-encounters/encounters/training-session"; +import IMysteryEncounter from "./mystery-encounter"; +import { DarkDealEncounter } from "./encounters/dark-deal-encounter"; +import { MysteriousChallengersEncounter } from "./encounters/mysterious-challengers-encounter"; +import { MysteriousChestEncounter } from "./encounters/mysterious-chest-encounter"; +import { FightOrFlightEncounter } from "./encounters/fight-or-flight-encounter"; +import { TrainingSessionEncounter } from "./encounters/training-session-encounter"; import { Biome } from "#enums/biome"; -import { SleepingSnorlaxEncounter } from "./encounters/sleeping-snorlax"; +import { SleepingSnorlaxEncounter } from "./encounters/sleeping-snorlax-encounter"; import { MysteryEncounterType } from "#enums/mystery-encounter-type"; -import { DepartmentStoreSaleEncounter } from "#app/data/mystery-encounters/encounters/department-store-sale"; -import { ShadyVitaminDealerEncounter } from "#app/data/mystery-encounters/encounters/shady-vitamin-dealer"; -import { FieldTripEncounter } from "#app/data/mystery-encounters/encounters/field-trip-encounter"; +import { DepartmentStoreSaleEncounter } from "./encounters/department-store-sale-encounter"; +import { ShadyVitaminDealerEncounter } from "./encounters/shady-vitamin-dealer-encounter"; +import { FieldTripEncounter } from "./encounters/field-trip-encounter"; // Spawn chance: (BASE_MYSTERY_ENCOUNTER_SPAWN_WEIGHT + WIGHT_INCREMENT_ON_SPAWN_MISS * ) / 256 export const BASE_MYSTERY_ENCOUNTER_SPAWN_WEIGHT = 1; @@ -117,7 +117,7 @@ export const CIVILIZATION_ENCOUNTER_BIOMES = [ Biome.ISLAND ]; -export const allMysteryEncounters: { [encounterType: number]: MysteryEncounter } = {}; +export const allMysteryEncounters: { [encounterType: number]: IMysteryEncounter } = {}; const extremeBiomeEncounters: MysteryEncounterType[] = []; diff --git a/src/field/mystery-encounter-intro.ts b/src/field/mystery-encounter-intro.ts index adb700b4854..1b0fb3bca01 100644 --- a/src/field/mystery-encounter-intro.ts +++ b/src/field/mystery-encounter-intro.ts @@ -1,6 +1,6 @@ import { GameObjects } from "phaser"; import BattleScene from "../battle-scene"; -import MysteryEncounter from "../data/mystery-encounters/mystery-encounter"; +import IMysteryEncounter from "../data/mystery-encounters/mystery-encounter"; export class MysteryEncounterSpriteConfig { spriteKey: string; // e.g. "ace_trainer_f" @@ -21,10 +21,10 @@ export class MysteryEncounterSpriteConfig { * Note: intro visuals are not "Trainers" or any other specific game object, though they may contain trainer sprites */ export default class MysteryEncounterIntroVisuals extends Phaser.GameObjects.Container { - public encounter: MysteryEncounter; + public encounter: IMysteryEncounter; public spriteConfigs: MysteryEncounterSpriteConfig[]; - constructor(scene: BattleScene, encounter: MysteryEncounter) { + constructor(scene: BattleScene, encounter: IMysteryEncounter) { super(scene, -72, 76); this.encounter = encounter; // Shallow copy configs to allow visual config updates at runtime without dirtying master copy of Encounter diff --git a/src/loading-scene.ts b/src/loading-scene.ts index 096986ebcc8..1ce8ed87156 100644 --- a/src/loading-scene.ts +++ b/src/loading-scene.ts @@ -22,7 +22,6 @@ import { initStatsKeys } from "./ui/game-stats-ui-handler"; import { initVouchers } from "./system/voucher"; import { Biome } from "#enums/biome"; import { TrainerType } from "#enums/trainer-type"; -import {initMysteryEncounterDialogue} from "#app/data/mystery-encounters/mystery-encounter-dialogue"; import {initMysteryEncounters} from "#app/data/mystery-encounters/mystery-encounters"; export class LoadingScene extends SceneBase { @@ -346,7 +345,6 @@ export class LoadingScene extends SceneBase { initMoves(); initAbilities(); initChallenges(); - initMysteryEncounterDialogue(); initMysteryEncounters(); } diff --git a/src/system/game-data.ts b/src/system/game-data.ts index 871e2458bbd..606d63d70fe 100644 --- a/src/system/game-data.ts +++ b/src/system/game-data.ts @@ -41,7 +41,7 @@ import { Moves } from "#enums/moves"; import { PlayerGender } from "#enums/player-gender"; import { Species } from "#enums/species"; import { MysteryEncounterData } from "../data/mystery-encounters/mystery-encounter-data"; -import MysteryEncounter from "../data/mystery-encounters/mystery-encounter"; +import IMysteryEncounter from "../data/mystery-encounters/mystery-encounter"; export const defaultStarterSpecies: Species[] = [ Species.BULBASAUR, Species.CHARMANDER, Species.SQUIRTLE, @@ -124,7 +124,7 @@ export interface SessionSaveData { gameVersion: string; timestamp: integer; challenges: ChallengeData[]; - mysteryEncounter: MysteryEncounter; + mysteryEncounter: IMysteryEncounter; mysteryEncounterData: MysteryEncounterData; } @@ -1155,7 +1155,7 @@ export class GameData { } if (k === "mysteryEncounter") { - return new MysteryEncounter(v); + return new IMysteryEncounter(v); } if (k === "mysteryEncounterData") { diff --git a/src/test/mystery-encounter/mystery-encounter-utils.test.ts b/src/test/mystery-encounter/mystery-encounter-utils.test.ts index e558fe7a95c..50853c05a2e 100644 --- a/src/test/mystery-encounter/mystery-encounter-utils.test.ts +++ b/src/test/mystery-encounter/mystery-encounter-utils.test.ts @@ -10,7 +10,7 @@ import { initSceneWithoutEncounterPhase } from "#test/utils/gameManagerUtils"; import { Species } from "#enums/species"; import BattleScene from "#app/battle-scene"; import { StatusEffect } from "#app/data/status-effect"; -import MysteryEncounter from "#app/data/mystery-encounters/mystery-encounter"; +import IMysteryEncounter from "#app/data/mystery-encounters/mystery-encounter"; import { MessagePhase } from "#app/phases"; import { getPokemonSpecies, speciesStarters } from "#app/data/pokemon-species"; import { Type } from "#app/data/type"; @@ -273,7 +273,7 @@ describe("Mystery Encounter Utils", () => { describe("getTextWithEncounterDialogueTokens", () => { it("injects dialogue tokens and color styling", () => { - scene.currentBattle.mysteryEncounter = new MysteryEncounter(null); + scene.currentBattle.mysteryEncounter = new IMysteryEncounter(null); scene.currentBattle.mysteryEncounter.setDialogueToken("test", "value"); const result = getEncounterText(scene, "mysteryEncounter:unit_test_dialogue"); @@ -281,7 +281,7 @@ describe("Mystery Encounter Utils", () => { }); it("can perform nested dialogue token injection", () => { - scene.currentBattle.mysteryEncounter = new MysteryEncounter(null); + scene.currentBattle.mysteryEncounter = new IMysteryEncounter(null); scene.currentBattle.mysteryEncounter.setDialogueToken("test", "value"); scene.currentBattle.mysteryEncounter.setDialogueToken("testvalue", "new"); @@ -292,7 +292,7 @@ describe("Mystery Encounter Utils", () => { describe("queueEncounterMessage", () => { it("queues a message with encounter dialogue tokens", async () => { - scene.currentBattle.mysteryEncounter = new MysteryEncounter(null); + scene.currentBattle.mysteryEncounter = new IMysteryEncounter(null); scene.currentBattle.mysteryEncounter.setDialogueToken("test", "value"); const spy = vi.spyOn(game.scene, "queueMessage"); const phaseSpy = vi.spyOn(game.scene, "unshiftPhase"); @@ -305,7 +305,7 @@ describe("Mystery Encounter Utils", () => { describe("showEncounterText", () => { it("showText with dialogue tokens", async () => { - scene.currentBattle.mysteryEncounter = new MysteryEncounter(null); + scene.currentBattle.mysteryEncounter = new IMysteryEncounter(null); scene.currentBattle.mysteryEncounter.setDialogueToken("test", "value"); const spy = vi.spyOn(game.scene.ui, "showText"); @@ -316,7 +316,7 @@ describe("Mystery Encounter Utils", () => { describe("showEncounterDialogue", () => { it("showText with dialogue tokens", async () => { - scene.currentBattle.mysteryEncounter = new MysteryEncounter(null); + scene.currentBattle.mysteryEncounter = new IMysteryEncounter(null); scene.currentBattle.mysteryEncounter.setDialogueToken("test", "value"); const spy = vi.spyOn(game.scene.ui, "showDialogue"); diff --git a/src/test/vitest.setup.ts b/src/test/vitest.setup.ts index b8b655e3e96..37c533a33db 100644 --- a/src/test/vitest.setup.ts +++ b/src/test/vitest.setup.ts @@ -13,7 +13,6 @@ import { initVouchers } from "#app/system/voucher"; import { initAchievements } from "#app/system/achv"; import { initStatsKeys } from "#app/ui/game-stats-ui-handler"; import { initMysteryEncounters } from "#app/data/mystery-encounters/mystery-encounters"; -import { initMysteryEncounterDialogue } from "#app/data/mystery-encounters/mystery-encounter-dialogue"; import { beforeAll, beforeEach, vi } from "vitest"; import * as overrides from "#app/overrides"; @@ -28,7 +27,6 @@ initSpecies(); initMoves(); initAbilities(); initLoggedInUser(); -initMysteryEncounterDialogue(); initMysteryEncounters(); global.testFailed = false; diff --git a/src/ui/mystery-encounter-ui-handler.ts b/src/ui/mystery-encounter-ui-handler.ts index cad678acf87..9eccfd5d0e8 100644 --- a/src/ui/mystery-encounter-ui-handler.ts +++ b/src/ui/mystery-encounter-ui-handler.ts @@ -307,6 +307,8 @@ export default class MysteryEncounterUiHandler extends UiHandler { // Options Window for (let i = 0; i < this.filteredEncounterOptions.length; i++) { + const option = this.filteredEncounterOptions[i]; + let optionText; switch (this.filteredEncounterOptions.length) { case 2: @@ -319,11 +321,11 @@ export default class MysteryEncounterUiHandler extends UiHandler { optionText = addBBCodeTextObject(this.scene, i % 2 === 0 ? 0 : 100, i < 2 ? 0 : 16, "-", TextStyle.WINDOW, { wordWrap: { width: 558 }, fontSize: "80px", lineSpacing: -8 }); break; } - this.optionsMeetsReqs.push(this.filteredEncounterOptions[i].meetsRequirements(this.scene)); - const optionDialogue = mysteryEncounter.dialogue.encounterOptionsDialogue.options[i]; - let text; - if (this.filteredEncounterOptions[i].hasRequirements() && this.optionsMeetsReqs[i]) { + this.optionsMeetsReqs.push(option.meetsRequirements(this.scene)); + const optionDialogue = option.dialogue; + let text: string; + if (option.hasRequirements() && this.optionsMeetsReqs[i]) { // Options with special requirements that are met are automatically colored green // In cases where isDisabledOnRequirementsNotMet = false and requirements are not met, option will not be auto-colored text = getEncounterText(this.scene, optionDialogue.buttonLabel, TextStyle.SUMMARY_GREEN); @@ -335,7 +337,7 @@ export default class MysteryEncounterUiHandler extends UiHandler { optionText.setText(text); } - if (!this.optionsMeetsReqs[i] && this.filteredEncounterOptions[i].isDisabledOnRequirementsNotMet) { + if (!this.optionsMeetsReqs[i] && option.isDisabledOnRequirementsNotMet) { optionText.setAlpha(0.5); } if (this.blockInput) { @@ -420,13 +422,13 @@ export default class MysteryEncounterUiHandler extends UiHandler { return; } - const mysteryEncounter = this.scene.currentBattle.mysteryEncounter; - let text; - const option = mysteryEncounter.dialogue.encounterOptionsDialogue.options[cursor]; - if (!this.optionsMeetsReqs[cursor] && this.filteredEncounterOptions[cursor].isDisabledOnRequirementsNotMet && option.disabledTooltip) { - text = getEncounterText(this.scene, option.disabledTooltip, TextStyle.TOOLTIP_CONTENT); + let text: string; + const cursorOption = this.filteredEncounterOptions[cursor]; + const optionDialogue = cursorOption.dialogue; + if (!this.optionsMeetsReqs[cursor] && cursorOption.isDisabledOnRequirementsNotMet && optionDialogue.disabledTooltip) { + text = getEncounterText(this.scene, optionDialogue.disabledTooltip, TextStyle.TOOLTIP_CONTENT); } else { - text = getEncounterText(this.scene, option.buttonTooltip, TextStyle.TOOLTIP_CONTENT); + text = getEncounterText(this.scene, optionDialogue.buttonTooltip, TextStyle.TOOLTIP_CONTENT); } // Auto-color options green/blue for good/bad by looking for (+)/(-)