Merge branch 'mystery-battle-events' of https://github.com/InnocentGameDev/PokeRogue-Events into pokemon-salesman

This commit is contained in:
ImperialSympathizer 2024-07-12 11:27:02 -04:00
commit fb9a4d3dd3
38 changed files with 2412 additions and 1931 deletions

View File

@ -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;
}

View File

@ -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;

View File

@ -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"
}
]
};

View File

@ -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"
}
]
}
};

View File

@ -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"
}
]
}
]
}
};

View File

@ -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"
}
]
}
]
}
};

View File

@ -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"
}
]
};

View File

@ -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"
}
]
}
]
}
};

View File

@ -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"
}
]
}
};

View File

@ -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"
}
]
}
};

View File

@ -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"
}
]
};

View File

@ -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();

View File

@ -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();

View File

@ -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();

View File

@ -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();

View File

@ -1,37 +1,72 @@
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)
/** 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
hasShadow: true,
},
{
spriteKey: "teacher",
fileRoot: "mystery-encounters",
hasShadow: true
hasShadow: true,
},
{
spriteKey: "preschooler_f",
fileRoot: "trainer",
hasShadow: true
hasShadow: true,
},
])
.withIntroDialogue([
{
text: `${namespace}_intro_message`,
},
{
text: `${namespace}_intro_dialogue`,
speaker: `${namespace}_speaker`,
},
])
.withHideIntroVisuals(false)
.withSceneWaveRangeRequirement(10, 180)
.withOption(new MysteryEncounterOptionBuilder()
.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<boolean> => {
const encounter = scene.currentBattle.mysteryEncounter;
const onPokemonSelected = (pokemon: PlayerPokemon) => {
@ -41,34 +76,39 @@ export const FieldTripEncounter: MysteryEncounter = MysteryEncounterBuilder
label: move.getName(),
handler: () => {
// Pokemon and move selected
const correctMove = move.getMove().category === MoveCategory.PHYSICAL;
const correctMove =
move.getMove().category === MoveCategory.PHYSICAL;
encounter.setDialogueToken("moveCategory", "Physical");
if (!correctMove) {
encounter.dialogue.encounterOptionsDialogue.options[0].selected = [
encounter.options[0].dialogue.selected = [
{
text: "mysteryEncounter:field_trip_option_incorrect",
speaker: "mysteryEncounter:field_trip_speaker"
text: `${namespace}_option_incorrect`,
speaker: `${namespace}_speaker`,
},
{
text: "mysteryEncounter:field_trip_lesson_learned",
}
text: `${namespace}_lesson_learned`,
},
];
setEncounterExp(scene, scene.getParty().map(p => p.id), 50);
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 = [
encounter.options[0].dialogue.selected = [
{
text: "mysteryEncounter:field_trip_option_selected"
}
text: `${namespace}_option_selected`,
},
];
setEncounterExp(scene, [pokemon.id], 100);
}
encounter.misc = {
correctMove: correctMove
correctMove: correctMove,
};
return true;
}
},
};
return option;
});
@ -80,20 +120,46 @@ export const FieldTripEncounter: MysteryEncounter = MysteryEncounterBuilder
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)
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()
.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<boolean> => {
const encounter = scene.currentBattle.mysteryEncounter;
const onPokemonSelected = (pokemon: PlayerPokemon) => {
@ -103,34 +169,39 @@ export const FieldTripEncounter: MysteryEncounter = MysteryEncounterBuilder
label: move.getName(),
handler: () => {
// Pokemon and move selected
const correctMove = move.getMove().category === MoveCategory.SPECIAL;
const correctMove =
move.getMove().category === MoveCategory.SPECIAL;
encounter.setDialogueToken("moveCategory", "Special");
if (!correctMove) {
encounter.dialogue.encounterOptionsDialogue.options[1].selected = [
encounter.options[1].dialogue.selected = [
{
text: "mysteryEncounter:field_trip_option_incorrect",
speaker: "mysteryEncounter:field_trip_speaker"
text: `${namespace}_option_incorrect`,
speaker: `${namespace}_speaker`,
},
{
text: "mysteryEncounter:field_trip_lesson_learned",
}
text: `${namespace}_lesson_learned`,
},
];
setEncounterExp(scene, scene.getParty().map(p => p.id), 50);
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 = [
encounter.options[1].dialogue.selected = [
{
text: "mysteryEncounter:field_trip_option_selected"
}
text: `${namespace}_option_selected`,
},
];
setEncounterExp(scene, [pokemon.id], 100);
}
encounter.misc = {
correctMove: correctMove
correctMove: correctMove,
};
return true;
}
},
};
return option;
});
@ -142,20 +213,46 @@ export const FieldTripEncounter: MysteryEncounter = MysteryEncounterBuilder
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)
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()
.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<boolean> => {
const encounter = scene.currentBattle.mysteryEncounter;
const onPokemonSelected = (pokemon: PlayerPokemon) => {
@ -165,34 +262,39 @@ export const FieldTripEncounter: MysteryEncounter = MysteryEncounterBuilder
label: move.getName(),
handler: () => {
// Pokemon and move selected
const correctMove = move.getMove().category === MoveCategory.STATUS;
const correctMove =
move.getMove().category === MoveCategory.STATUS;
encounter.setDialogueToken("moveCategory", "Status");
if (!correctMove) {
encounter.dialogue.encounterOptionsDialogue.options[2].selected = [
encounter.options[2].dialogue.selected = [
{
text: "mysteryEncounter:field_trip_option_incorrect",
speaker: "mysteryEncounter:field_trip_speaker"
text: `${namespace}_option_incorrect`,
speaker: `${namespace}_speaker`,
},
{
text: "mysteryEncounter:field_trip_lesson_learned",
}
text: `${namespace}_lesson_learned`,
},
];
setEncounterExp(scene, scene.getParty().map(p => p.id), 50);
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 = [
encounter.options[2].dialogue.selected = [
{
text: "mysteryEncounter:field_trip_option_selected"
}
text: `${namespace}_option_selected`,
},
];
setEncounterExp(scene, [pokemon.id], 100);
}
encounter.misc = {
correctMove: correctMove
correctMove: correctMove,
};
return true;
}
},
};
return option;
});
@ -204,13 +306,24 @@ export const FieldTripEncounter: MysteryEncounter = MysteryEncounterBuilder
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.TEMP_STAT_BOOSTER,
[TempBattleStat.ACC]
),
generateModifierTypeOption(
scene,
modifierTypes.TEMP_STAT_BOOSTER,
[TempBattleStat.SPD]
),
generateModifierTypeOption(scene, modifierTypes.GREAT_BALL),
generateModifierTypeOption(scene, modifierTypes.IV_SCANNER)
generateModifierTypeOption(scene, modifierTypes.IV_SCANNER),
];
setEncounterRewards(scene, { guaranteedModifierTypeOptions: modifiers, fillRemaining: false });
setEncounterRewards(scene, {
guaranteedModifierTypeOptions: modifiers,
fillRemaining: false,
});
}
leaveEncounterWithoutBattle(scene, !encounter.misc.correctMove);

View File

@ -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();

View File

@ -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();

View File

@ -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();

View File

@ -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();

View File

@ -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<6F>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();

View File

@ -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<6F>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();

View File

@ -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<boolean> => {
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<boolean> => {
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();

View File

@ -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<boolean> => {
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<boolean> => {
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();

View File

@ -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();

View File

@ -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();

View File

@ -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<boolean> => {
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<boolean> => {
// 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<boolean> => {
// 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() {}
}

View File

@ -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<boolean> => {
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<boolean> => {
// 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<boolean> => {
// 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() {
}
}

View File

@ -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;
}

View File

@ -142,6 +142,7 @@ export class MysteryEncounterOptionBuilder implements Partial<MysteryEncounterOp
onPreOptionPhase?: OptionPhaseCallback;
onOptionPhase?: OptionPhaseCallback;
onPostOptionPhase?: OptionPhaseCallback;
dialogue?: OptionTextDisplay;
withSceneRequirement(requirement: EncounterSceneRequirement): this & Required<Pick<MysteryEncounterOption, "requirements">> {
this.requirements.push(requirement);
@ -183,4 +184,9 @@ export class MysteryEncounterOptionBuilder implements Partial<MysteryEncounterOp
this.isDisabledOnRequirementsNotMet = disabled;
return Object.assign(this, { isDisabledOnRequirementsNotMet: this.isDisabledOnRequirementsNotMet });
}
withDialogue(dialogue: OptionTextDisplay) {
this.dialogue = dialogue;
return this;
}
}

View File

@ -7,7 +7,7 @@ import MysteryEncounterIntroVisuals, { MysteryEncounterSpriteConfig } from "../.
import * as Utils from "../../utils";
import { StatusEffect } from "../status-effect";
import MysteryEncounterDialogue, {
allMysteryEncounterDialogue
OptionTextDisplay
} from "./mystery-encounter-dialogue";
import MysteryEncounterOption, { MysteryEncounterOptionBuilder, OptionPhaseCallback } from "./mystery-encounter-option";
import {
@ -35,7 +35,7 @@ export enum MysteryEncounterTier {
MASTER // Not currently used
}
export default interface MysteryEncounter {
export default interface IMysteryEncounter {
/**
* Required params
*/
@ -130,23 +130,18 @@ export default interface MysteryEncounter {
* These objects will be saved as part of session data any time the player is on a floor with an encounter
* Unless you know what you're doing, you should use MysteryEncounterBuilder to create an instance for this class
*/
export default class MysteryEncounter implements MysteryEncounter {
constructor(encounter: MysteryEncounter) {
export default class IMysteryEncounter implements IMysteryEncounter {
constructor(encounter: IMysteryEncounter) {
if (!isNullOrUndefined(encounter)) {
Object.assign(this, encounter);
}
this.encounterTier = this.encounterTier ? this.encounterTier : MysteryEncounterTier.COMMON;
this.dialogue = allMysteryEncounterDialogue[this.encounterType];
this.dialogue = {};
this.encounterVariant = MysteryEncounterVariant.DEFAULT;
this.requirements = this.requirements ? this.requirements : [];
this.hideBattleIntroMessage = !isNullOrUndefined(this.hideBattleIntroMessage) ? this.hideBattleIntroMessage : false;
this.hideIntroVisuals = !isNullOrUndefined(this.hideIntroVisuals) ? this.hideIntroVisuals : true;
// Populate options with respective dialogue
if (this.dialogue) {
this.options.forEach((o, i) => o.dialogue = this.dialogue.encounterOptionsDialogue.options[i]);
}
// Reset any dirty flags or encounter data
this.lockEncounterRewardTiers = true;
this.dialogueTokens = new Map<string, [RegExp, string]>;
@ -344,7 +339,7 @@ export default class MysteryEncounter implements MysteryEncounter {
}
}
export class MysteryEncounterBuilder implements Partial<MysteryEncounter> {
export class MysteryEncounterBuilder implements Partial<IMysteryEncounter> {
encounterType?: MysteryEncounterType;
options?: [MysteryEncounterOption, MysteryEncounterOption, ...MysteryEncounterOption[]] = [null, null];
spriteConfigs?: MysteryEncounterSpriteConfig[];
@ -372,7 +367,7 @@ export class MysteryEncounterBuilder implements Partial<MysteryEncounter> {
* @param encounterType
* @returns this
*/
static withEncounterType(encounterType: MysteryEncounterType): MysteryEncounterBuilder & Pick<MysteryEncounter, "encounterType"> {
static withEncounterType(encounterType: MysteryEncounterType): MysteryEncounterBuilder & Pick<IMysteryEncounter, "encounterType"> {
return Object.assign(new MysteryEncounterBuilder(), { encounterType: encounterType });
}
@ -382,7 +377,7 @@ export class MysteryEncounterBuilder implements Partial<MysteryEncounter> {
* @param option - MysteryEncounterOption to add, can use MysteryEncounterOptionBuilder to create instance
* @returns
*/
withOption(option: MysteryEncounterOption): this & Pick<MysteryEncounter, "options"> {
withOption(option: MysteryEncounterOption): this & Pick<IMysteryEncounter, "options"> {
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<MysteryEncounter> {
* 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<MysteryEncounter> {
* @param spriteConfigs
* @returns
*/
withIntroSpriteConfigs(spriteConfigs: MysteryEncounterSpriteConfig[]): this & Pick<MysteryEncounter, "spriteConfigs"> {
withIntroSpriteConfigs(spriteConfigs: MysteryEncounterSpriteConfig[]): this & Pick<IMysteryEncounter, "spriteConfigs"> {
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<MysteryEncounter> {
* @param encounterTier
* @returns
*/
withEncounterTier(encounterTier: MysteryEncounterTier): this & Required<Pick<MysteryEncounter, "encounterTier">> {
withEncounterTier(encounterTier: MysteryEncounterTier): this & Required<Pick<IMysteryEncounter, "encounterTier">> {
return Object.assign(this, { encounterTier: encounterTier });
}
@ -441,7 +446,7 @@ export class MysteryEncounterBuilder implements Partial<MysteryEncounter> {
* @param requirement
* @returns
*/
withSceneRequirement(requirement: EncounterSceneRequirement): this & Required<Pick<MysteryEncounter, "requirements">> {
withSceneRequirement(requirement: EncounterSceneRequirement): this & Required<Pick<IMysteryEncounter, "requirements">> {
if (requirement instanceof EncounterPokemonRequirement) {
Error("Incorrectly added pokemon requirement as scene requirement.");
}
@ -471,23 +476,45 @@ export class MysteryEncounterBuilder implements Partial<MysteryEncounter> {
return this.withSceneRequirement(new PartySizeRequirement([min, max ?? min]));
}
withPrimaryPokemonRequirement(requirement: EncounterPokemonRequirement): this & Required<Pick<MysteryEncounter, "primaryPokemonRequirements">> {
/**
* Add a primary pokemon requirement
*
* @param requirement {@linkcode EncounterPokemonRequirement}
* @returns
*/
withPrimaryPokemonRequirement(requirement: EncounterPokemonRequirement): this & Required<Pick<IMysteryEncounter, "primaryPokemonRequirements">> {
this.primaryPokemonRequirements.push(requirement);
return Object.assign(this, { primaryPokemonRequirements: this.primaryPokemonRequirements });
}
withPrimaryPokemonStatusEffectRequirement(statusEffect: StatusEffect | StatusEffect[], minNumberOfPokemon: number = 1, invertQuery: boolean = false): this & Required<Pick<MysteryEncounter, "primaryPokemonRequirements">> {
/**
* 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<Pick<IMysteryEncounter, "primaryPokemonRequirements">> {
return this.withPrimaryPokemonRequirement(new StatusEffectRequirement(statusEffect, minNumberOfPokemon, invertQuery));
}
withPrimaryPokemonHealthRatioRequirement(requiredHealthRange: [number, number], minNumberOfPokemon: number = 1, invertQuery: boolean = false): this & Required<Pick<MysteryEncounter, "primaryPokemonRequirements">> {
/**
* 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<Pick<IMysteryEncounter, "primaryPokemonRequirements">> {
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<Pick<MysteryEncounter, "secondaryPokemonRequirements">> {
withSecondaryPokemonRequirement(requirement: EncounterPokemonRequirement, excludePrimaryFromSecondaryRequirements: boolean = false): this & Required<Pick<IMysteryEncounter, "secondaryPokemonRequirements">> {
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<MysteryEncounter> {
* @param doEncounterRewards - synchronous callback function to perform during rewards phase of the encounter
* @returns
*/
withRewards(doEncounterRewards: (scene: BattleScene) => boolean): this & Required<Pick<MysteryEncounter, "doEncounterRewards">> {
withRewards(doEncounterRewards: (scene: BattleScene) => boolean): this & Required<Pick<IMysteryEncounter, "doEncounterRewards">> {
return Object.assign(this, { doEncounterRewards: doEncounterRewards });
}
@ -517,7 +544,7 @@ export class MysteryEncounterBuilder implements Partial<MysteryEncounter> {
* @param doEncounterExp - synchronous callback function to perform during rewards phase of the encounter
* @returns
*/
withExp(doEncounterExp: (scene: BattleScene) => boolean): this & Required<Pick<MysteryEncounter, "doEncounterExp">> {
withExp(doEncounterExp: (scene: BattleScene) => boolean): this & Required<Pick<IMysteryEncounter, "doEncounterExp">> {
return Object.assign(this, { doEncounterExp: doEncounterExp });
}
@ -528,7 +555,7 @@ export class MysteryEncounterBuilder implements Partial<MysteryEncounter> {
* @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<Pick<MysteryEncounter, "onInit">> {
withOnInit(onInit: (scene: BattleScene) => boolean): this & Required<Pick<IMysteryEncounter, "onInit">> {
return Object.assign(this, { onInit: onInit });
}
@ -537,7 +564,7 @@ export class MysteryEncounterBuilder implements Partial<MysteryEncounter> {
* @param enemyPartyConfig
* @returns
*/
withEnemyPartyConfig(enemyPartyConfig: EnemyPartyConfig): this & Required<Pick<MysteryEncounter, "enemyPartyConfigs">> {
withEnemyPartyConfig(enemyPartyConfig: EnemyPartyConfig): this & Required<Pick<IMysteryEncounter, "enemyPartyConfigs">> {
this.enemyPartyConfigs.push(enemyPartyConfig);
return Object.assign(this, { enemyPartyConfigs: this.enemyPartyConfigs });
}
@ -548,7 +575,7 @@ export class MysteryEncounterBuilder implements Partial<MysteryEncounter> {
* @param catchAllowed - if true, allows enemy pokemon to be caught during the encounter
* @returns
*/
withCatchAllowed(catchAllowed: boolean): this & Required<Pick<MysteryEncounter, "catchAllowed">> {
withCatchAllowed(catchAllowed: boolean): this & Required<Pick<IMysteryEncounter, "catchAllowed">> {
return Object.assign(this, { catchAllowed: catchAllowed });
}
@ -556,7 +583,7 @@ export class MysteryEncounterBuilder implements Partial<MysteryEncounter> {
* @param hideBattleIntroMessage - if true, will not show the trainerAppeared/wildAppeared/bossAppeared message for an encounter
* @returns
*/
withHideWildIntroMessage(hideBattleIntroMessage: boolean): this & Required<Pick<MysteryEncounter, "hideBattleIntroMessage">> {
withHideWildIntroMessage(hideBattleIntroMessage: boolean): this & Required<Pick<IMysteryEncounter, "hideBattleIntroMessage">> {
return Object.assign(this, { hideBattleIntroMessage: hideBattleIntroMessage });
}
@ -564,11 +591,88 @@ export class MysteryEncounterBuilder implements Partial<MysteryEncounter> {
* @param hideIntroVisuals - if false, will not hide the intro visuals that are displayed at the beginning of encounter
* @returns
*/
withHideIntroVisuals(hideIntroVisuals: boolean): this & Required<Pick<MysteryEncounter, "hideIntroVisuals">> {
withHideIntroVisuals(hideIntroVisuals: boolean): this & Required<Pick<IMysteryEncounter, "hideIntroVisuals">> {
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);
}
}

View File

@ -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 * <number of missed spawns>) / 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[] = [];

View File

@ -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

View File

@ -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();
}

View File

@ -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") {

View File

@ -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");

View File

@ -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;

View File

@ -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 (+)/(-)