diff --git a/public/images/mystery-encounters/teacher.json b/public/images/mystery-encounters/teacher.json new file mode 100644 index 00000000000..881386d7228 --- /dev/null +++ b/public/images/mystery-encounters/teacher.json @@ -0,0 +1,41 @@ +{ + "textures": [ + { + "image": "teacher.png", + "format": "RGBA8888", + "size": { + "w": 43, + "h": 74 + }, + "scale": 1, + "frames": [ + { + "filename": "Spr_HGSS_Teacher.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 80, + "h": 80 + }, + "spriteSourceSize": { + "x": 19, + "y": 8, + "w": 41, + "h": 72 + }, + "frame": { + "x": 1, + "y": 1, + "w": 41, + "h": 72 + } + } + ] + } + ], + "meta": { + "app": "https://www.codeandweb.com/texturepacker", + "version": "3.0", + "smartupdate": "$TexturePacker:SmartUpdate:506e5a4ce79c134a7b4af90a90aef244:1b81d3d84bf12cedc419805eaff82548:59bc5dd000b5e72588320b473e31c312$" + } +} diff --git a/public/images/mystery-encounters/teacher.png b/public/images/mystery-encounters/teacher.png new file mode 100644 index 00000000000..b4332bc0032 Binary files /dev/null and b/public/images/mystery-encounters/teacher.png differ diff --git a/src/data/mystery-encounters/dialogue/field-trip-dialogue.ts b/src/data/mystery-encounters/dialogue/field-trip-dialogue.ts new file mode 100644 index 00000000000..dca3f48f5bf --- /dev/null +++ b/src/data/mystery-encounters/dialogue/field-trip-dialogue.ts @@ -0,0 +1,50 @@ +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/shady-vitamin-dealer.ts b/src/data/mystery-encounters/dialogue/shady-vitamin-dealer.ts index cb0c5ac130e..52b9741caf8 100644 --- a/src/data/mystery-encounters/dialogue/shady-vitamin-dealer.ts +++ b/src/data/mystery-encounters/dialogue/shady-vitamin-dealer.ts @@ -21,7 +21,7 @@ export const ShadyVitaminDealerDialogue: MysteryEncounterDialogue = { selected: [ { text: "mysteryEncounter:shady_vitamin_dealer_option_selected" - }, + } ] }, { @@ -30,7 +30,7 @@ export const ShadyVitaminDealerDialogue: MysteryEncounterDialogue = { selected: [ { text: "mysteryEncounter:shady_vitamin_dealer_option_selected" - }, + } ] }, { diff --git a/src/data/mystery-encounters/encounters/department-store-sale.ts b/src/data/mystery-encounters/encounters/department-store-sale.ts index 1d44c2b3af8..19269a774f3 100644 --- a/src/data/mystery-encounters/encounters/department-store-sale.ts +++ b/src/data/mystery-encounters/encounters/department-store-sale.ts @@ -1,5 +1,5 @@ import { - leaveEncounterWithoutBattle, setEncounterExp, + leaveEncounterWithoutBattle, setEncounterRewards, } from "#app/data/mystery-encounters/mystery-encounter-utils"; import { modifierTypes } from "#app/modifier/modifier-type"; @@ -46,7 +46,6 @@ export const DepartmentStoreSaleEncounter: MysteryEncounter = MysteryEncounterBu i++; } - setEncounterExp(scene, scene.getParty().map(p => p.id), 300); setEncounterRewards(scene, { guaranteedModifierTypeFuncs: modifiers, fillRemaining: false }); leaveEncounterWithoutBattle(scene); }) diff --git a/src/data/mystery-encounters/encounters/field-trip-encounter.ts b/src/data/mystery-encounters/encounters/field-trip-encounter.ts new file mode 100644 index 00000000000..410df127b6e --- /dev/null +++ b/src/data/mystery-encounters/encounters/field-trip-encounter.ts @@ -0,0 +1,220 @@ +import { generateModifierTypeOption, leaveEncounterWithoutBattle, selectPokemonForOption, setEncounterExp, setEncounterRewards, } from "#app/data/mystery-encounters/mystery-encounter-utils"; +import { modifierTypes } from "#app/modifier/modifier-type"; +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"; + +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", + } + ]; + 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 + }; + return true; + } + }; + 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) + ]; + + 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", + } + ]; + 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 + }; + return true; + } + }; + 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) + ]; + + 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", + } + ]; + 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 + }; + return true; + } + }; + 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) + ]; + + setEncounterRewards(scene, { guaranteedModifierTypeOptions: modifiers, fillRemaining: false }); + } + + leaveEncounterWithoutBattle(scene, !encounter.misc.correctMove); + }) + .build() + ) + .build(); diff --git a/src/data/mystery-encounters/encounters/shady-vitamin-dealer.ts b/src/data/mystery-encounters/encounters/shady-vitamin-dealer.ts index d58bc44daf7..21e1b199e04 100644 --- a/src/data/mystery-encounters/encounters/shady-vitamin-dealer.ts +++ b/src/data/mystery-encounters/encounters/shady-vitamin-dealer.ts @@ -1,9 +1,8 @@ import { - generateModifierType, + generateModifierTypeOption, leaveEncounterWithoutBattle, queueEncounterMessage, - selectPokemonForOption, - setEncounterRewards, + selectPokemonForOption, setEncounterExp, updatePlayerMoney, } from "#app/data/mystery-encounters/mystery-encounter-utils"; import { StatusEffect } from "#app/data/status-effect"; @@ -18,6 +17,7 @@ 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) @@ -43,7 +43,7 @@ export const ShadyVitaminDealerEncounter: MysteryEncounter = MysteryEncounterBui .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 multiplier of 2 for cost + .withSceneMoneyRequirement(0, 2) // Wave scaling money multiplier of 2 .withPreOptionPhase(async (scene: BattleScene): Promise => { const encounter = scene.currentBattle.mysteryEncounter; const onPokemonSelected = (pokemon: PlayerPokemon) => { @@ -51,8 +51,8 @@ export const ShadyVitaminDealerEncounter: MysteryEncounter = MysteryEncounterBui updatePlayerMoney(scene, -(encounter.options[0].requirements[0] as MoneyRequirement).requiredMoney); // Calculate modifiers and dialogue tokens const modifiers = [ - generateModifierType(scene, modifierTypes.BASE_STAT_BOOSTER), - generateModifierType(scene, modifierTypes.BASE_STAT_BOOSTER) + 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); @@ -62,12 +62,12 @@ export const ShadyVitaminDealerEncounter: MysteryEncounter = MysteryEncounterBui }; }; - // Only Pokemon that can gain benefits are unfainted with no status + // 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 "Pokémon must be healthy enough."; + return i18next.t("mysteryEncounter:shady_vitamin_dealer_invalid_selection"); } return null; @@ -99,7 +99,7 @@ export const ShadyVitaminDealerEncounter: MysteryEncounter = MysteryEncounterBui chosenPokemon.hp = Math.max(chosenPokemon.hp - damage, 0); // Roll for poison (80%) - if (randSeedInt(10) < 10) { + if (randSeedInt(10) < 8) { if (chosenPokemon.trySetStatus(StatusEffect.TOXIC)) { // Toxic applied queueEncounterMessage(scene, "mysteryEncounter:shady_vitamin_dealer_bad_poison"); @@ -111,32 +111,81 @@ export const ShadyVitaminDealerEncounter: MysteryEncounter = MysteryEncounterBui queueEncounterMessage(scene, "mysteryEncounter:shady_vitamin_dealer_damage_only"); } + setEncounterExp(scene, [chosenPokemon.id], 100); + chosenPokemon.updateInfo(); }) .build()) - .withOption(new MysteryEncounterOptionBuilder() - .withSceneMoneyRequirement(0, 5) // Wave scaling multiplier of 2 for cost + .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 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 { + const encounter = scene.currentBattle.mysteryEncounter; + const chosenPokemon = encounter.misc.chosenPokemon; + const modifiers = encounter.misc.modifiers; - } - i++; + for (const modType of modifiers) { + const modifier = modType.newModifier(chosenPokemon); + await scene.addModifier(modifier, true, false, false, true); } + scene.updateModifiers(true); - setEncounterRewards(scene, { guaranteedModifierTypeFuncs: modifiers, fillRemaining: false }); leaveEncounterWithoutBattle(scene); }) - .build() - ) + .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); diff --git a/src/data/mystery-encounters/encounters/sleeping-snorlax.ts b/src/data/mystery-encounters/encounters/sleeping-snorlax.ts index 1a011bb05a0..cb2029272ef 100644 --- a/src/data/mystery-encounters/encounters/sleeping-snorlax.ts +++ b/src/data/mystery-encounters/encounters/sleeping-snorlax.ts @@ -1,5 +1,4 @@ import { - ModifierTypeOption, modifierTypes } from "#app/modifier/modifier-type"; import { BerryType } from "#enums/berry-type"; @@ -15,9 +14,9 @@ import { MysteryEncounterOptionBuilder } from "../mystery-encounter-option"; import { MoveRequirement } from "../mystery-encounter-requirements"; import { EnemyPartyConfig, - EnemyPokemonConfig, generateModifierType, + EnemyPokemonConfig, generateModifierTypeOption, initBattleWithEnemyConfig, - leaveEncounterWithoutBattle, queueEncounterMessage, + leaveEncounterWithoutBattle, queueEncounterMessage, setEncounterExp, setEncounterRewards } from "../mystery-encounter-utils"; @@ -30,7 +29,9 @@ export const SleepingSnorlaxEncounter: MysteryEncounter = MysteryEncounterBuilde fileRoot: "pokemon", hasShadow: true, tint: 0.25, - repeat: true + scale: 1.5, + repeat: true, + y: 5 } ]) .withSceneWaveRangeRequirement(10, 180) // waves 10 to 180 @@ -66,16 +67,19 @@ export const SleepingSnorlaxEncounter: MysteryEncounter = MysteryEncounterBuilde scene.executeWithSeedOffset(() => { roll = Utils.randSeedInt(16, 0); }, scene.currentBattle.waveIndex); - console.log(roll); + + // 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 = generateModifierType(scene, modifierTypes.BERRY, [BerryType.SITRUS]); + const sitrus = generateModifierTypeOption(scene, modifierTypes.BERRY, [BerryType.SITRUS]); - setEncounterRewards(scene, { guaranteedModifierTypeOptions: [new ModifierTypeOption(sitrus, 0)], fillRemaining: false }); + setEncounterRewards(scene, { guaranteedModifierTypeOptions: [sitrus], fillRemaining: false }); queueEncounterMessage(scene, "mysteryEncounter:sleeping_snorlax_option_2_bad_result"); leaveEncounterWithoutBattle(scene); } else { @@ -96,9 +100,12 @@ export const SleepingSnorlaxEncounter: MysteryEncounter = MysteryEncounterBuilde .withOption(new MysteryEncounterOptionBuilder() .withPrimaryPokemonRequirement(new MoveRequirement([Moves.PLUCK, Moves.COVET, Moves.KNOCK_OFF, Moves.THIEF, Moves.TRICK, Moves.SWITCHEROO])) .withOptionPhase(async (scene: BattleScene) => { - // Leave encounter with no rewards or exp + // 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() diff --git a/src/data/mystery-encounters/mystery-encounter-dialogue.ts b/src/data/mystery-encounters/mystery-encounter-dialogue.ts index 1e2dfe85045..5129f3bf23e 100644 --- a/src/data/mystery-encounters/mystery-encounter-dialogue.ts +++ b/src/data/mystery-encounters/mystery-encounter-dialogue.ts @@ -8,6 +8,7 @@ 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}`; @@ -92,4 +93,5 @@ export function initMysteryEncounterDialogue() { 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 f1316ff64f1..7731468a977 100644 --- a/src/data/mystery-encounters/mystery-encounter-option.ts +++ b/src/data/mystery-encounters/mystery-encounter-option.ts @@ -3,6 +3,7 @@ import { PlayerPokemon } from "#app/field/pokemon"; import BattleScene from "../../battle-scene"; import * as Utils from "../../utils"; import { EncounterPokemonRequirement, EncounterSceneRequirement, MoneyRequirement } from "./mystery-encounter-requirements"; +import { isNullOrUndefined } from "../../utils"; export type OptionPhaseCallback = (scene: BattleScene) => Promise; @@ -14,6 +15,12 @@ export default interface MysteryEncounterOption { primaryPokemon?: PlayerPokemon; secondaryPokemon?: PlayerPokemon[]; excludePrimaryFromSecondaryRequirements?: boolean; + /** + * There are two modes of option requirements: + * 1 (DEFAULT): Option is completely disabled if requirements are not met (unselectable and greyed out) + * 2: Option is *NOT* disabled if requirements are not met + */ + isDisabledOnRequirementsNotMet?: boolean; /** * Dialogue object containing all the dialogue, messages, tooltips, etc. for this option @@ -33,12 +40,19 @@ export default class MysteryEncounterOption implements MysteryEncounterOption { constructor(option: MysteryEncounterOption) { Object.assign(this, option); this.requirements = this.requirements ? this.requirements : []; + this.primaryPokemonRequirements = this.primaryPokemonRequirements ? this.primaryPokemonRequirements : []; + this.secondaryPokemonRequirements = this.secondaryPokemonRequirements ? this.secondaryPokemonRequirements : []; + this.isDisabledOnRequirementsNotMet = isNullOrUndefined(this.isDisabledOnRequirementsNotMet) ? true : this.isDisabledOnRequirementsNotMet; + } + + hasRequirements?() { + return this.requirements.length > 0 || this.primaryPokemonRequirements.length > 0 || this.secondaryPokemonRequirements.length > 0; } meetsRequirements?(scene: BattleScene) { return !this.requirements.some(requirement => !requirement.meetsRequirement(scene)) && - this.meetsPrimaryRequirementAndPrimaryPokemonSelected(scene) && - this.meetsSupportingRequirementAndSupportingPokemonSelected(scene); + this.meetsSupportingRequirementAndSupportingPokemonSelected(scene) && + this.meetsPrimaryRequirementAndPrimaryPokemonSelected(scene); } meetsPrimaryRequirementAndPrimaryPokemonSelected?(scene: BattleScene) { @@ -124,6 +138,7 @@ export class MysteryEncounterOptionBuilder implements Partial> { + this.isDisabledOnRequirementsNotMet = disabled; + return Object.assign(this, { isDisabledOnRequirementsNotMet: this.isDisabledOnRequirementsNotMet }); + } } diff --git a/src/data/mystery-encounters/mystery-encounter-utils.ts b/src/data/mystery-encounters/mystery-encounter-utils.ts index 2d8d7bfbf66..d2e1a5eba2f 100644 --- a/src/data/mystery-encounters/mystery-encounter-utils.ts +++ b/src/data/mystery-encounters/mystery-encounter-utils.ts @@ -8,7 +8,7 @@ import { TrainerConfig, trainerConfigs, TrainerSlot } from "../trainer-config"; import Pokemon, { FieldPosition, PlayerPokemon } from "#app/field/pokemon"; import Trainer, { TrainerVariant } from "../../field/trainer"; import { ExpBalanceModifier, ExpShareModifier, MultipleParticipantExpBonusModifier, PokemonExpBoosterModifier } from "#app/modifier/modifier"; -import { CustomModifierSettings, getModifierPoolForType, ModifierPoolType, ModifierType, ModifierTypeFunc, ModifierTypeGenerator, modifierTypes, PokemonHeldItemModifierType, regenerateModifierPoolThresholds } from "#app/modifier/modifier-type"; +import { CustomModifierSettings, getModifierPoolForType, ModifierPoolType, ModifierType, ModifierTypeFunc, ModifierTypeGenerator, ModifierTypeOption, modifierTypes, PokemonHeldItemModifierType, regenerateModifierPoolThresholds } from "#app/modifier/modifier-type"; import { BattleEndPhase, EggLapsePhase, ExpPhase, ModifierRewardPhase, SelectModifierPhase, ShowPartyExpBarPhase, TrainerVictoryPhase } from "#app/phases"; import { MysteryEncounterBattlePhase, MysteryEncounterRewardsPhase } from "#app/phases/mystery-encounter-phase"; import * as Utils from "../../utils"; @@ -443,7 +443,7 @@ export function updatePlayerMoney(scene: BattleScene, changeValue: number, playS * @param modifier * @param pregenArgs - can specify BerryType for berries, TM for TMs, AttackBoostType for item, etc. */ -export function generateModifierType(scene: BattleScene, modifier: () => ModifierType, pregenArgs?: any[]): ModifierType { +export function generateModifierTypeOption(scene: BattleScene, modifier: () => ModifierType, pregenArgs?: any[]): ModifierTypeOption { const modifierId = Object.keys(modifierTypes).find(k => modifierTypes[k] === modifier); let result: ModifierType = modifierTypes[modifierId]?.(); @@ -463,7 +463,7 @@ export function generateModifierType(scene: BattleScene, modifier: () => Modifie }); result = result instanceof ModifierTypeGenerator ? result.generateType(scene.getParty(), pregenArgs) : result; - return result; + return new ModifierTypeOption(result, 0); } /** @@ -593,6 +593,7 @@ export function setEncounterRewards(scene: BattleScene, customShopRewards?: Cust * 290 - trio legendaries * 340 - box legendaries * 608 - Blissey (highest in game) + * https://bulbapedia.bulbagarden.net/wiki/List_of_Pok%C3%A9mon_by_effort_value_yield_(Generation_IX) * @param useWaveIndex - set to false when directly passing the the full exp value instead of baseExpValue */ export function setEncounterExp(scene: BattleScene, participantIds: integer[], baseExpValue: number, useWaveIndex: boolean = true) { diff --git a/src/data/mystery-encounters/mystery-encounter.ts b/src/data/mystery-encounters/mystery-encounter.ts index 87b80aba321..0d656988009 100644 --- a/src/data/mystery-encounters/mystery-encounter.ts +++ b/src/data/mystery-encounters/mystery-encounter.ts @@ -120,6 +120,7 @@ export default interface MysteryEncounter { /** * Generic property to set any custom data required for the encounter + * Extremely useful for carrying state/data between onPreOptionPhase/onOptionPhase/onPostOptionPhase */ misc?: any; } @@ -276,6 +277,7 @@ export default class MysteryEncounter implements MysteryEncounter { * For multiple support pokemon in the dialogue token, it will have to be overridden. */ populateDialogueTokensFromRequirements?(scene: BattleScene) { + this.meetsRequirements(scene); if (this.requirements?.length > 0) { for (const req of this.requirements) { const dialogueToken = req.getDialogueToken(scene); @@ -304,6 +306,7 @@ export default class MysteryEncounter implements MysteryEncounter { // Dialogue tokens for options for (let i = 0; i < this.options.length; i++) { const opt = this.options[i]; + opt.meetsRequirements(scene); const j = i + 1; if (opt.requirements?.length > 0) { for (const req of opt.requirements) { @@ -316,7 +319,7 @@ export default class MysteryEncounter implements MysteryEncounter { for (const req of opt.primaryPokemonRequirements) { if (!req.invertQuery) { const value = req.getDialogueToken(scene, opt.primaryPokemon); - this.setDialogueToken("option" + j + "Primary", value[1]); + this.setDialogueToken("option" + j + "Primary" + this.capitalizeFirstLetter(value[0]), value[1]); } } } @@ -325,7 +328,7 @@ export default class MysteryEncounter implements MysteryEncounter { for (const req of opt.secondaryPokemonRequirements) { if (!req.invertQuery) { const value = req.getDialogueToken(scene, opt.secondaryPokemon[0]); - this.setDialogueToken("option" + j + "Secondary", value[1]); + this.setDialogueToken("option" + j + "Secondary" + this.capitalizeFirstLetter(value[0]), value[1]); } } } diff --git a/src/data/mystery-encounters/mystery-encounters.ts b/src/data/mystery-encounters/mystery-encounters.ts index e4a748d3a23..a1f8a73192e 100644 --- a/src/data/mystery-encounters/mystery-encounters.ts +++ b/src/data/mystery-encounters/mystery-encounters.ts @@ -9,6 +9,7 @@ import { SleepingSnorlaxEncounter } from "./encounters/sleeping-snorlax"; 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"; // Spawn chance: (BASE_MYSTERY_ENCOUNTER_SPAWN_WEIGHT + WIGHT_INCREMENT_ON_SPAWN_MISS * ) / 256 export const BASE_MYSTERY_ENCOUNTER_SPAWN_WEIGHT = 1; @@ -95,6 +96,7 @@ export function initMysteryEncounters() { allMysteryEncounters[MysteryEncounterType.SLEEPING_SNORLAX] = SleepingSnorlaxEncounter; allMysteryEncounters[MysteryEncounterType.DEPARTMENT_STORE_SALE] = DepartmentStoreSaleEncounter; allMysteryEncounters[MysteryEncounterType.SHADY_VITAMIN_DEALER] = ShadyVitaminDealerEncounter; + allMysteryEncounters[MysteryEncounterType.FIELD_TRIP] = FieldTripEncounter; // Append encounters that can occur in any biome to biome map const anyBiomeEncounters: MysteryEncounterType[] = Object.keys(MysteryEncounterType).filter(e => !isNaN(Number(e))).map(k => Number(k) as MysteryEncounterType); diff --git a/src/enums/mystery-encounter-type.ts b/src/enums/mystery-encounter-type.ts index 6e2815babca..b5b0144c3be 100644 --- a/src/enums/mystery-encounter-type.ts +++ b/src/enums/mystery-encounter-type.ts @@ -6,5 +6,6 @@ export enum MysteryEncounterType { SLEEPING_SNORLAX, TRAINING_SESSION, DEPARTMENT_STORE_SALE, - SHADY_VITAMIN_DEALER + SHADY_VITAMIN_DEALER, + FIELD_TRIP } diff --git a/src/locales/en/mystery-encounter.ts b/src/locales/en/mystery-encounter.ts index ac04eed2688..59fcf6940c2 100644 --- a/src/locales/en/mystery-encounter.ts +++ b/src/locales/en/mystery-encounter.ts @@ -17,6 +17,7 @@ export const mysteryEncounter: SimpleTranslationEntries = { "unit_test_dialogue": "@ec{test}@ec{test} @ec{test@ec{test}} @ec{test1} @ec{test\} @ec{test\\} @ec{test\\\} {test}", // Mystery Encounters -- Common Tier + "mysterious_chest_intro_message": "You found...@d{32} a chest?", "mysterious_chest_title": "The Mysterious Chest", "mysterious_chest_description": "A beautifully ornamented chest stands on the ground. There must be something good inside... right?", @@ -82,6 +83,7 @@ export const mysteryEncounter: SimpleTranslationEntries = { "shady_vitamin_dealer_title": "The Vitamin Dealer", "shady_vitamin_dealer_description": "The man opens his jacket to reveal some Pokémon vitamins. The numbers he quotes seem like a really good deal. Almost too good...\nHe offers two package deals to choose from.", "shady_vitamin_dealer_query": "Which deal will choose?", + "shady_vitamin_dealer_invalid_selection": "Pokémon must be healthy enough.", "shady_vitamin_dealer_option_1_label": "The Cheap Deal", "shady_vitamin_dealer_option_1_tooltip": "(-) Pay @ec{option1Money}\n(-) Side Effects?\n(+) Chosen Pokémon Gains 2 Random Vitamins", "shady_vitamin_dealer_option_2_label": "The Pricey Deal", @@ -94,9 +96,34 @@ export const mysteryEncounter: SimpleTranslationEntries = { $Your @ec{selectedPokemon} takes some damage\nand becomes badly poisoned...`, "shady_vitamin_dealer_poison": `But the medicine had some side effects! $Your @ec{selectedPokemon} becomes poisoned...`, + "shady_vitamin_dealer_no_bad_effects": "Looks like there were no side-effects this time.", "shady_vitamin_dealer_option_3_label": "Leave", "shady_vitamin_dealer_option_3_tooltip": "(-) No Rewards", - "shady_vitamin_dealer_outro_good": "Looks like there were no side-effects this time.", + + "field_trip_intro_message": "It's a teacher and some school children!", + "field_trip_speaker": "Teacher", + "field_trip_intro_dialogue": `Hello, there! Would you be able to\nspare a minute for my students? + $I'm teaching them about Pokémon moves\nand would love to show them a demonstration. + $Would you mind showing us one of\nthe moves your Pokémon can use?`, + "field_trip_title": "Field Trip", + "field_trip_description": "A teacher is requesting a move demonstration from a Pokémon. Depending on the move you choose, she might have something useful for you in exchange.", + "field_trip_query": "Which move category will you show off?", + // "field_trip_invalid_selection": "Pokémon doesn't know that type of move.", + "field_trip_option_1_label": "A Physical Move", + "field_trip_option_1_tooltip": "(+) Physical Item Rewards", + "field_trip_option_2_label": "A Special Move", + "field_trip_option_2_tooltip": "(+) Special Item Rewards", + "field_trip_option_3_label": "A Status Move", + "field_trip_option_3_tooltip": "(+) Status Item Rewards", + "field_trip_second_option_prompt": "Choose a move for your Pokémon to use.", + "field_trip_option_selected": "@ec{pokeName} shows off an awesome display of @ec{move}!", + "field_trip_option_incorrect": `... + $That isn't a @ec{moveCategory} move! + $I'm sorry, but I can't give you anything.`, + "field_trip_lesson_learned": `Looks like you learned a valuable lesson? + $Your Pokémon also gained some knowledge.`, + "field_trip_outro_good": "Thank you so much for your kindness!\nI hope the items I had were helpful!", + "field_trip_outro_bad": "Come along children, we'll\nfind a better demonstration elsewhere.", // Mystery Encounters -- Uncommon Tier @@ -162,14 +189,14 @@ export const mysteryEncounter: SimpleTranslationEntries = { "sleeping_snorlax_intro_message": `As you walk down a narrow pathway, you see a towering silhouette blocking your path. $You get closer to see a Snorlax sleeping peacefully.\nIt seems like there's no way around it.`, "sleeping_snorlax_title": "Sleeping Snorlax", - "sleeping_snorlax_description": "You could attack it to try and get it to move, or simply wait for it to wake up.", + "sleeping_snorlax_description": "You could attack it to try and get it to move, or simply wait for it to wake up. Who knows how long that could take, though...", "sleeping_snorlax_query": "What will you do?", "sleeping_snorlax_option_1_label": "Fight it", "sleeping_snorlax_option_1_tooltip": "(-) Fight Sleeping Snorlax", "sleeping_snorlax_option_2_label": "Wait for it to move", "sleeping_snorlax_option_2_tooltip": "@[SUMMARY_BLUE]{(75%) Wait a short time}\n@[SUMMARY_BLUE]{(25%) Wait a long time}", - "sleeping_snorlax_option_3_label": "Steal", - "sleeping_snorlax_option_3_tooltip": "(+) Leftovers", + "sleeping_snorlax_option_3_label": "Steal its item", + "sleeping_snorlax_option_3_tooltip": "(+) @ec{option3PrimaryName} uses @ec{option3PrimaryMove}\n(+) Leftovers", "sleeping_snorlax_option_3_disabled_tooltip": "Your Pokémon need to know certain moves to choose this", "sleeping_snorlax_option_1_selected_message": "You approach the\nPokémon without fear.", "sleeping_snorlax_option_2_selected_message": `.@d{32}.@d{32}.@d{32} diff --git a/src/overrides.ts b/src/overrides.ts index ce706173691..e19a5bf20dd 100644 --- a/src/overrides.ts +++ b/src/overrides.ts @@ -8,18 +8,17 @@ import { PokeballCounts } from "./battle-scene"; import { PokeballType } from "./data/pokeball"; import { Gender } from "./data/gender"; import { StatusEffect } from "./data/status-effect"; -import { SpeciesStatBoosterItem, modifierTypes } from "./modifier/modifier-type"; +import { modifierTypes, SpeciesStatBoosterItem } from "./modifier/modifier-type"; import { VariantTier } from "./enums/variant-tiers"; import { EggTier } from "#enums/egg-type"; -import { allSpecies } from "./data/pokemon-species"; // eslint-disable-line @typescript-eslint/no-unused-vars import { Abilities } from "#enums/abilities"; import { BerryType } from "#enums/berry-type"; import { Biome } from "#enums/biome"; import { Moves } from "#enums/moves"; import { Species } from "#enums/species"; import { TimeOfDay } from "#enums/time-of-day"; -import {MysteryEncounterType} from "#enums/mystery-encounter-type"; // eslint-disable-line @typescript-eslint/no-unused-vars -import {MysteryEncounterTier} from "#app/data/mystery-encounters/mystery-encounter"; // eslint-disable-line @typescript-eslint/no-unused-vars +import { MysteryEncounterType } from "#enums/mystery-encounter-type"; // eslint-disable-line @typescript-eslint/no-unused-vars +import { MysteryEncounterTier } from "#app/data/mystery-encounters/mystery-encounter"; // eslint-disable-line @typescript-eslint/no-unused-vars /** * Overrides for testing different in game situations diff --git a/src/phases.ts b/src/phases.ts index 091629b31d5..6c64cc9454f 100644 --- a/src/phases.ts +++ b/src/phases.ts @@ -5363,6 +5363,7 @@ export class SelectModifierPhase extends BattlePhase { this.scene.ui.revertMode(); this.scene.ui.setMode(Mode.MESSAGE); super.end(); + break; } modifierType = typeOptions[cursor].type; break; diff --git a/src/ui/mystery-encounter-ui-handler.ts b/src/ui/mystery-encounter-ui-handler.ts index 163d79784f8..8be862b965d 100644 --- a/src/ui/mystery-encounter-ui-handler.ts +++ b/src/ui/mystery-encounter-ui-handler.ts @@ -1,16 +1,16 @@ import BattleScene from "../battle-scene"; -import {addBBCodeTextObject, getBBCodeFrag, TextStyle} from "./text"; -import {Mode} from "./ui"; +import { addBBCodeTextObject, getBBCodeFrag, TextStyle } from "./text"; +import { Mode } from "./ui"; import UiHandler from "./ui-handler"; -import {Button} from "#enums/buttons"; -import {addWindow, WindowVariant} from "./ui-theme"; -import {MysteryEncounterPhase} from "../phases/mystery-encounter-phase"; -import {PartyUiMode} from "./party-ui-handler"; +import { Button } from "#enums/buttons"; +import { addWindow, WindowVariant } from "./ui-theme"; +import { MysteryEncounterPhase } from "../phases/mystery-encounter-phase"; +import { PartyUiMode } from "./party-ui-handler"; import MysteryEncounterOption from "../data/mystery-encounters/mystery-encounter-option"; import * as Utils from "../utils"; -import {isNullOrUndefined} from "../utils"; -import {getPokeballAtlasKey} from "../data/pokeball"; -import {getEncounterText} from "#app/data/mystery-encounters/mystery-encounter-utils"; +import { isNullOrUndefined } from "../utils"; +import { getPokeballAtlasKey } from "../data/pokeball"; +import { getEncounterText } from "#app/data/mystery-encounters/mystery-encounter-utils"; export default class MysteryEncounterUiHandler extends UiHandler { private cursorContainer: Phaser.GameObjects.Container; @@ -319,13 +319,22 @@ 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; } - const option = mysteryEncounter.dialogue.encounterOptionsDialogue.options[i]; - const text = getEncounterText(this.scene, option.buttonLabel, option.style ? option.style : TextStyle.WINDOW); + 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]) { + // Options with special requirements that are met are automatically colored green + text = getEncounterText(this.scene, optionDialogue.buttonLabel, TextStyle.SUMMARY_GREEN); + } else { + text = getEncounterText(this.scene, optionDialogue.buttonLabel, optionDialogue.style ? optionDialogue.style : TextStyle.WINDOW); + } + if (text) { optionText.setText(text); } - this.optionsMeetsReqs.push(this.filteredEncounterOptions[i].meetsRequirements(this.scene)); + if (!this.optionsMeetsReqs[i]) { optionText.setAlpha(0.5);