From deb1e95e3489e8e5349e921a78bbbb501ce9c1c6 Mon Sep 17 00:00:00 2001 From: ImperialSympathizer Date: Tue, 9 Jul 2024 21:26:24 -0400 Subject: [PATCH] add encounter exp utility and small cleanups/refactors --- src/battle-scene.ts | 4 +- src/data/mystery-encounter-requirements.ts | 2 +- src/data/mystery-encounter.ts | 27 +- src/data/mystery-encounters/dark-deal.ts | 2 +- .../department-store-sale.ts | 13 +- .../mystery-encounters/fight-or-flight.ts | 6 +- .../mysterious-challengers.ts | 10 +- .../mystery-encounters/mysterious-chest.ts | 10 +- .../mystery-encounter-utils.ts | 270 ++++++++++++------ .../shady-vitamin-dealer.ts | 4 +- .../mystery-encounters/sleeping-snorlax.ts | 8 +- .../mystery-encounters/training-session.ts | 18 +- src/locales/en/mystery-encounter.ts | 12 +- src/phases.ts | 6 +- src/phases/mystery-encounter-phase.ts | 15 +- .../mystery-encounter-utils.test.ts | 6 +- .../phases/mystery-encounter-phase.test.ts | 2 +- src/ui/mystery-encounter-ui-handler.ts | 18 +- src/ui/text.ts | 28 ++ 19 files changed, 297 insertions(+), 164 deletions(-) diff --git a/src/battle-scene.ts b/src/battle-scene.ts index fe3023cbc22..c8ef8aef3b2 100644 --- a/src/battle-scene.ts +++ b/src/battle-scene.ts @@ -2681,7 +2681,7 @@ export default class BattleScene extends SceneBase { const tier = val[1]; if (tier === MysteryEncounterTier.COMMON) { tierWeights[0] = tierWeights[0] - 6; - } else if (tier === MysteryEncounterTier.UNCOMMON) { + } else if (tier === MysteryEncounterTier.GREAT) { tierWeights[1] = tierWeights[1] - 4; } }); @@ -2691,7 +2691,7 @@ export default class BattleScene extends SceneBase { const commonThreshold = totalWeight - tierWeights[0]; const uncommonThreshold = totalWeight - tierWeights[0] - tierWeights[1]; const rareThreshold = totalWeight - tierWeights[0] - tierWeights[1] - tierWeights[2]; - let tier = tierValue > commonThreshold ? MysteryEncounterTier.COMMON : tierValue > uncommonThreshold ? MysteryEncounterTier.UNCOMMON : tierValue > rareThreshold ? MysteryEncounterTier.RARE : MysteryEncounterTier.SUPER_RARE; + let tier = tierValue > commonThreshold ? MysteryEncounterTier.COMMON : tierValue > uncommonThreshold ? MysteryEncounterTier.GREAT : tierValue > rareThreshold ? MysteryEncounterTier.ULTRA : MysteryEncounterTier.ROGUE; if (!isNullOrUndefined(Overrides.MYSTERY_ENCOUNTER_TIER_OVERRIDE)) { tier = Overrides.MYSTERY_ENCOUNTER_TIER_OVERRIDE; diff --git a/src/data/mystery-encounter-requirements.ts b/src/data/mystery-encounter-requirements.ts index f55319d881b..26ca5163fe5 100644 --- a/src/data/mystery-encounter-requirements.ts +++ b/src/data/mystery-encounter-requirements.ts @@ -239,7 +239,7 @@ export class MoneyRequirement extends EncounterSceneRequirement { getDialogueToken(scene: BattleScene, pokemon?: PlayerPokemon): [string, string] { const value = this?.scalingMultiplier > 0 ? scene.getWaveMoneyAmount(this.scalingMultiplier).toString() : this.requiredMoney.toString(); // Colors money text - return ["money", "@ecCol[MONEY]{₽" + value + "}"]; + return ["money", "@[MONEY]{₽" + value + "}"]; } } diff --git a/src/data/mystery-encounter.ts b/src/data/mystery-encounter.ts index d768a672632..090a9aa6ded 100644 --- a/src/data/mystery-encounter.ts +++ b/src/data/mystery-encounter.ts @@ -24,10 +24,10 @@ export enum MysteryEncounterVariant { export enum MysteryEncounterTier { COMMON, - UNCOMMON, - RARE, - SUPER_RARE, - ULTRA_RARE // Not currently used + GREAT, + ULTRA, + ROGUE, + MASTER // Not currently used } export default interface MysteryEncounter { @@ -44,6 +44,7 @@ export default interface MysteryEncounter { hideBattleIntroMessage?: boolean; hideIntroVisuals?: boolean; catchAllowed?: boolean; + doEncounterExp?: (scene: BattleScene) => boolean; doEncounterRewards?: (scene: BattleScene) => boolean; onInit?: (scene: BattleScene) => boolean; @@ -347,6 +348,7 @@ export class MysteryEncounterBuilder implements Partial { secondaryPokemonRequirements ?: EncounterPokemonRequirement[] = []; excludePrimaryFromSupportRequirements?: boolean; dialogueTokens?: Map; + doEncounterExp?: (scene: BattleScene) => boolean; doEncounterRewards?: (scene: BattleScene) => boolean; onInit?: (scene: BattleScene) => boolean; hideBattleIntroMessage?: boolean; @@ -448,8 +450,7 @@ export class MysteryEncounterBuilder implements Partial { * * NOTE: If rewards are dependent on options selected, runtime data, etc., * It may be better to programmatically set doEncounterRewards elsewhere. - * For instance, doEncounterRewards could instead be set inside the onOptionPhase() callback function for a MysteryEncounterOption - * Check other existing mystery encounters for examples on how to use this + * There is a helper function in mystery-encounter utils, setEncounterRewards(), which can be called programmatically to set rewards * @param doEncounterRewards - synchronous callback function to perform during rewards phase of the encounter * @returns */ @@ -457,6 +458,20 @@ export class MysteryEncounterBuilder implements Partial { return Object.assign(this, { doEncounterRewards: doEncounterRewards }); } + /** + * Can set custom encounter exp via this callback function + * If exp always deterministic for an encounter, this is a good way to set them + * + * NOTE: If rewards are dependent on options selected, runtime data, etc., + * It may be better to programmatically set doEncounterExp elsewhere. + * There is a helper function in mystery-encounter utils, setEncounterExp(), which can be called programmatically to set rewards + * @param doEncounterExp - synchronous callback function to perform during rewards phase of the encounter + * @returns + */ + withExp(doEncounterExp: (scene: BattleScene) => boolean): this & Required> { + return Object.assign(this, { doEncounterExp: doEncounterExp }); + } + /** * Can be used to perform init logic before intro visuals are shown and before the MysteryEncounterPhase begins * Useful for performing things like procedural generation of intro sprites, etc. diff --git a/src/data/mystery-encounters/dark-deal.ts b/src/data/mystery-encounters/dark-deal.ts index 170e9ac01b7..a092613b612 100644 --- a/src/data/mystery-encounters/dark-deal.ts +++ b/src/data/mystery-encounters/dark-deal.ts @@ -69,7 +69,7 @@ const excludedBosses = [ export const DarkDealEncounter: MysteryEncounter = new MysteryEncounterBuilder() .withEncounterType(MysteryEncounterType.DARK_DEAL) - .withEncounterTier(MysteryEncounterTier.ULTRA_RARE) + .withEncounterTier(MysteryEncounterTier.ROGUE) .withIntroSpriteConfigs([ { spriteKey: "mad_scientist_m", diff --git a/src/data/mystery-encounters/department-store-sale.ts b/src/data/mystery-encounters/department-store-sale.ts index 3837e94af60..6ed4bce5262 100644 --- a/src/data/mystery-encounters/department-store-sale.ts +++ b/src/data/mystery-encounters/department-store-sale.ts @@ -1,7 +1,7 @@ import BattleScene from "../../battle-scene"; import { - leaveEncounterWithoutBattle, - setCustomEncounterRewards, + leaveEncounterWithoutBattle, setEncounterExp, + setEncounterRewards, } from "#app/data/mystery-encounters/mystery-encounter-utils"; import MysteryEncounter, {MysteryEncounterBuilder, MysteryEncounterTier} from "../mystery-encounter"; import { MysteryEncounterType } from "#enums/mystery-encounter-type"; @@ -49,7 +49,8 @@ export const DepartmentStoreSaleEncounter: MysteryEncounter = new MysteryEncount i++; } - setCustomEncounterRewards(scene, { guaranteedModifierTypeFuncs: modifiers, fillRemaining: false}); + setEncounterExp(scene, scene.getParty().map(p => p.id), 300); + setEncounterRewards(scene, { guaranteedModifierTypeFuncs: modifiers, fillRemaining: false}); leaveEncounterWithoutBattle(scene); }) .build()) @@ -69,7 +70,7 @@ export const DepartmentStoreSaleEncounter: MysteryEncounter = new MysteryEncount i++; } - setCustomEncounterRewards(scene, { guaranteedModifierTypeFuncs: modifiers, fillRemaining: false}); + setEncounterRewards(scene, { guaranteedModifierTypeFuncs: modifiers, fillRemaining: false}); leaveEncounterWithoutBattle(scene); }) .build()) @@ -89,7 +90,7 @@ export const DepartmentStoreSaleEncounter: MysteryEncounter = new MysteryEncount i++; } - setCustomEncounterRewards(scene, { guaranteedModifierTypeFuncs: modifiers, fillRemaining: false}); + setEncounterRewards(scene, { guaranteedModifierTypeFuncs: modifiers, fillRemaining: false}); leaveEncounterWithoutBattle(scene); }) .build()) @@ -113,7 +114,7 @@ export const DepartmentStoreSaleEncounter: MysteryEncounter = new MysteryEncount i++; } - setCustomEncounterRewards(scene, { guaranteedModifierTypeFuncs: modifiers, fillRemaining: false}); + setEncounterRewards(scene, { guaranteedModifierTypeFuncs: modifiers, fillRemaining: false}); leaveEncounterWithoutBattle(scene); }) .build()) diff --git a/src/data/mystery-encounters/fight-or-flight.ts b/src/data/mystery-encounters/fight-or-flight.ts index 9ee4166662e..1deb26149ee 100644 --- a/src/data/mystery-encounters/fight-or-flight.ts +++ b/src/data/mystery-encounters/fight-or-flight.ts @@ -4,7 +4,7 @@ import { EnemyPartyConfig, initBattleWithEnemyConfig, leaveEncounterWithoutBattle, queueEncounterMessage, - setCustomEncounterRewards, + setEncounterRewards, showEncounterText } from "#app/data/mystery-encounters/mystery-encounter-utils"; import MysteryEncounter, {MysteryEncounterBuilder, MysteryEncounterTier} from "../mystery-encounter"; @@ -103,7 +103,7 @@ export const FightOrFlightEncounter: MysteryEncounter = new MysteryEncounterBuil .withOptionPhase(async (scene: BattleScene) => { // Pick battle const item = scene.currentBattle.mysteryEncounter.misc as ModifierTypeOption; - setCustomEncounterRewards(scene, { guaranteedModifierTypeOptions: [item], fillRemaining: false}); + setEncounterRewards(scene, { guaranteedModifierTypeOptions: [item], fillRemaining: false}); await initBattleWithEnemyConfig(scene, scene.currentBattle.mysteryEncounter.enemyPartyConfigs[0]); }) .build()) @@ -112,7 +112,7 @@ export const FightOrFlightEncounter: MysteryEncounter = new MysteryEncounterBuil // Pick steal const encounter = scene.currentBattle.mysteryEncounter; const item = scene.currentBattle.mysteryEncounter.misc as ModifierTypeOption; - setCustomEncounterRewards(scene, { guaranteedModifierTypeOptions: [item], fillRemaining: false}); + setEncounterRewards(scene, { guaranteedModifierTypeOptions: [item], fillRemaining: false}); // If player has a stealing move, they succeed automatically const moveRequirement = new MoveRequirement(validMovesForSteal); diff --git a/src/data/mystery-encounters/mysterious-challengers.ts b/src/data/mystery-encounters/mysterious-challengers.ts index eaaeffec9d0..6986b738b4c 100644 --- a/src/data/mystery-encounters/mysterious-challengers.ts +++ b/src/data/mystery-encounters/mysterious-challengers.ts @@ -1,7 +1,7 @@ import BattleScene from "../../battle-scene"; import { ModifierTier } from "#app/modifier/modifier-tier"; import {modifierTypes} from "#app/modifier/modifier-type"; -import { EnemyPartyConfig, initBattleWithEnemyConfig, setCustomEncounterRewards } from "#app/data/mystery-encounters/mystery-encounter-utils"; +import { EnemyPartyConfig, initBattleWithEnemyConfig, setEncounterRewards } from "#app/data/mystery-encounters/mystery-encounter-utils"; import { MysteryEncounterType } from "#enums/mystery-encounter-type"; import MysteryEncounter, {MysteryEncounterBuilder, MysteryEncounterTier} from "../mystery-encounter"; import { MysteryEncounterOptionBuilder } from "../mystery-encounter-option"; @@ -17,7 +17,7 @@ import {PartyMemberStrength} from "#enums/party-member-strength"; export const MysteriousChallengersEncounter: MysteryEncounter = new MysteryEncounterBuilder() .withEncounterType(MysteryEncounterType.MYSTERIOUS_CHALLENGERS) - .withEncounterTier(MysteryEncounterTier.UNCOMMON) + .withEncounterTier(MysteryEncounterTier.GREAT) .withIntroSpriteConfigs([]) // These are set in onInit() .withSceneRequirement(new WaveCountRequirement([10, 180])) // waves 10 to 180 .withOnInit((scene: BattleScene) => { @@ -103,7 +103,7 @@ export const MysteriousChallengersEncounter: MysteryEncounter = new MysteryEncou // Spawn standard trainer battle with memory mushroom reward const config: EnemyPartyConfig = encounter.enemyPartyConfigs[0]; - setCustomEncounterRewards(scene, { guaranteedModifierTypeFuncs: [modifierTypes.TM_COMMON, modifierTypes.TM_GREAT, modifierTypes.MEMORY_MUSHROOM], fillRemaining: true }); + 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; @@ -119,7 +119,7 @@ export const MysteriousChallengersEncounter: MysteryEncounter = new MysteryEncou // Spawn hard fight with ULTRA/GREAT reward (can improve with luck) const config: EnemyPartyConfig = encounter.enemyPartyConfigs[1]; - setCustomEncounterRewards(scene, { guaranteedModifierTiers: [ModifierTier.ULTRA, ModifierTier.GREAT, ModifierTier.GREAT], fillRemaining: true }); + 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; @@ -138,7 +138,7 @@ export const MysteriousChallengersEncounter: MysteryEncounter = new MysteryEncou // To avoid player level snowballing from picking this option encounter.expMultiplier = 0.9; - setCustomEncounterRewards(scene, { guaranteedModifierTiers: [ModifierTier.ROGUE, ModifierTier.ULTRA, ModifierTier.GREAT], fillRemaining: true }); + 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; diff --git a/src/data/mystery-encounters/mysterious-chest.ts b/src/data/mystery-encounters/mysterious-chest.ts index 25abcf1fe74..7e221424650 100644 --- a/src/data/mystery-encounters/mysterious-chest.ts +++ b/src/data/mystery-encounters/mysterious-chest.ts @@ -5,7 +5,7 @@ import { koPlayerPokemon, leaveEncounterWithoutBattle, queueEncounterMessage, - setCustomEncounterRewards, + setEncounterRewards, showEncounterText } from "#app/data/mystery-encounters/mystery-encounter-utils"; import MysteryEncounter, {MysteryEncounterBuilder, MysteryEncounterTier} from "../mystery-encounter"; @@ -42,25 +42,25 @@ export const MysteriousChestEncounter: MysteryEncounter = new MysteryEncounterBu const roll = randSeedInt(100); if (roll > 60) { // Choose between 2 COMMON / 2 GREAT tier items (40%) - setCustomEncounterRewards(scene, { guaranteedModifierTiers: [ModifierTier.COMMON, ModifierTier.COMMON, ModifierTier.GREAT, ModifierTier.GREAT]}); + 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%) - setCustomEncounterRewards(scene, { guaranteedModifierTiers: [ModifierTier.ULTRA, ModifierTier.ULTRA, ModifierTier.ULTRA]}); + 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%) - setCustomEncounterRewards(scene, { guaranteedModifierTiers: [ModifierTier.ROGUE, ModifierTier.ROGUE]}); + 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%) - setCustomEncounterRewards(scene, { guaranteedModifierTiers: [ModifierTier.MASTER]}); + setEncounterRewards(scene, { guaranteedModifierTiers: [ModifierTier.MASTER]}); // Display result message then proceed to rewards queueEncounterMessage(scene, "mysteryEncounter:mysterious_chest_option_1_amazing_result"); leaveEncounterWithoutBattle(scene); diff --git a/src/data/mystery-encounters/mystery-encounter-utils.ts b/src/data/mystery-encounters/mystery-encounter-utils.ts index dcbcc753d32..89e497d22f9 100644 --- a/src/data/mystery-encounters/mystery-encounter-utils.ts +++ b/src/data/mystery-encounters/mystery-encounter-utils.ts @@ -7,7 +7,12 @@ import {Status, StatusEffect} from "../status-effect"; import {TrainerConfig, trainerConfigs, TrainerSlot} from "../trainer-config"; import Pokemon, {FieldPosition, PlayerPokemon} from "#app/field/pokemon"; import Trainer, {TrainerVariant} from "../../field/trainer"; -import {PokemonExpBoosterModifier} from "#app/modifier/modifier"; +import { + ExpBalanceModifier, + ExpShareModifier, + MultipleParticipantExpBonusModifier, + PokemonExpBoosterModifier +} from "#app/modifier/modifier"; import { CustomModifierSettings, getModifierPoolForType, @@ -19,7 +24,14 @@ import { PokemonHeldItemModifierType, regenerateModifierPoolThresholds } from "#app/modifier/modifier-type"; -import {BattleEndPhase, EggLapsePhase, ModifierRewardPhase, TrainerVictoryPhase} from "#app/phases"; +import { + BattleEndPhase, + EggLapsePhase, + ExpPhase, + ModifierRewardPhase, + ShowPartyExpBarPhase, + TrainerVictoryPhase +} from "#app/phases"; import {MysteryEncounterBattlePhase, MysteryEncounterRewardsPhase} from "#app/phases/mystery-encounter-phase"; import * as Utils from "../../utils"; import {isNullOrUndefined} from "#app/utils"; @@ -35,7 +47,9 @@ import {Mode} from "#app/ui/ui"; import {PartyOption, PartyUiMode} from "#app/ui/party-ui-handler"; import {OptionSelectConfig, OptionSelectItem} from "#app/ui/abstact-option-select-ui-handler"; import {WIGHT_INCREMENT_ON_SPAWN_MISS} from "#app/data/mystery-encounters/mystery-encounters"; -import {getBBCodeFrag, TextStyle} from "#app/ui/text"; +import {getTextWithColors, TextStyle} from "#app/ui/text"; +import * as Overrides from "#app/overrides"; +import {UiTheme} from "#enums/ui-theme"; /** * @@ -167,17 +181,29 @@ export function koPlayerPokemon(pokemon: PlayerPokemon) { pokemon.updateInfo(); } -export function getTextWithEncounterDialogueTokensAndColor(scene: BattleScene, textKey: TemplateStringsArray | `mysteryEncounter:${string}`, primaryStyle: TextStyle = TextStyle.MESSAGE): string { +export function getEncounterText(scene: BattleScene, textKey: TemplateStringsArray | `mysteryEncounter:${string}`, primaryStyle?: TextStyle, uiTheme: UiTheme = UiTheme.DEFAULT): string { + if (isNullOrUndefined(textKey)) { + return null; + } + + let textString: string = getTextWithDialogueTokens(scene, textKey); + + // Can only color the text if a Primary Style is defined + // primaryStyle is applied to all text that does not have its own specified style + if (primaryStyle) { + textString = getTextWithColors(textString, primaryStyle, uiTheme); + } + + return textString; +} + +function getTextWithDialogueTokens(scene: BattleScene, textKey: TemplateStringsArray | `mysteryEncounter:${string}`): string { if (isNullOrUndefined(textKey)) { return null; } let textString: string = i18next.t(textKey); - // Apply primary styling before anything else, if it exists - textString = getBBCodeFrag(textString, primaryStyle) + "[/color][/shadow]"; - const primaryStyleString = [...textString.match(new RegExp(/\[color=[^\[]*\]\[shadow=[^\[]*\]/i))][0]; - // Apply dialogue tokens const dialogueTokens = scene.currentBattle?.mysteryEncounter?.dialogueTokens; if (dialogueTokens) { @@ -186,16 +212,6 @@ export function getTextWithEncounterDialogueTokensAndColor(scene: BattleScene, t }); } - // Set custom colors - // Looks for any pattern like this: @ecCol[SUMMARY_BLUE]{my text to color} - // Resulting in: "my text to color" string with TextStyle.SUMMARY_BLUE - textString = textString.replace(/@ecCol\[([^{]*)\]{([^}]*)}/gi, (substring, textStyle: string, textToColor: string) => { - return "[/color][/shadow]" + getBBCodeFrag(textToColor, TextStyle[textStyle]) + "[/color][/shadow]" + primaryStyleString; - }); - - // Remove extra style block at the end - textString = textString.replace(/\[color=[^\[]*\]\[shadow=[^\[]*\]\[\/color\]\[\/shadow\]/gi, ""); - return textString; } @@ -205,7 +221,7 @@ export function getTextWithEncounterDialogueTokensAndColor(scene: BattleScene, t * @param contentKey */ export function queueEncounterMessage(scene: BattleScene, contentKey: TemplateStringsArray | `mysteryEncounter:${string}`): void { - const text: string = getTextWithEncounterDialogueTokensAndColor(scene, contentKey, TextStyle.MESSAGE); + const text: string = getEncounterText(scene, contentKey); scene.queueMessage(text, null, true); } @@ -216,7 +232,7 @@ export function queueEncounterMessage(scene: BattleScene, contentKey: TemplateSt */ export function showEncounterText(scene: BattleScene, contentKey: TemplateStringsArray | `mysteryEncounter:${string}`): Promise { return new Promise(resolve => { - const text: string = getTextWithEncounterDialogueTokensAndColor(scene, contentKey, TextStyle.MESSAGE); + const text: string = getEncounterText(scene, contentKey); scene.ui.showText(text, null, () => resolve(), 0, true); }); } @@ -229,8 +245,8 @@ export function showEncounterText(scene: BattleScene, contentKey: TemplateString * @param callback */ export function showEncounterDialogue(scene: BattleScene, textContentKey: TemplateStringsArray | `mysteryEncounter:${string}`, speakerContentKey: TemplateStringsArray | `mysteryEncounter:${string}`, callback?: Function) { - const text: string = getTextWithEncounterDialogueTokensAndColor(scene, textContentKey, TextStyle.MESSAGE); - const speaker: string = getTextWithEncounterDialogueTokensAndColor(scene, speakerContentKey); + const text: string = getEncounterText(scene, textContentKey); + const speaker: string = getEncounterText(scene, speakerContentKey); scene.ui.showDialogue(text, speaker, null, callback, 0, 0); } @@ -246,6 +262,7 @@ export class EnemyPokemonConfig { tags?: BattlerTagType[]; mysteryEncounterBattleEffects?: (pokemon: Pokemon) => void; status?: StatusEffect; + passive?: boolean; } export class EnemyPartyConfig { @@ -341,6 +358,11 @@ export async function initBattleWithEnemyConfig(scene: BattleScene, partyConfig: const enemyPokemon = scene.getEnemyParty()[e]; + // Make sure basic data is clean + enemyPokemon.hp = enemyPokemon.getMaxHp(); + enemyPokemon.status = null; + enemyPokemon.passive = false; + if (e < (doubleBattle ? 2 : 1)) { enemyPokemon.setX(-66 + enemyPokemon.getFieldPositionOffset()[0]); enemyPokemon.resetSummonData(); @@ -353,7 +375,7 @@ export async function initBattleWithEnemyConfig(scene: BattleScene, partyConfig: if (e < partyConfig?.pokemonConfigs?.length) { const config = partyConfig?.pokemonConfigs?.[e]; - // Generate new id in case using data source + // Generate new id, reset status and HP in case using data source if (config.dataSource) { enemyPokemon.id = Utils.randSeedInt(4294967296); } @@ -372,6 +394,11 @@ export async function initBattleWithEnemyConfig(scene: BattleScene, partyConfig: enemyPokemon.setBoss(true, segments); } + // Set Passive + if (partyConfig.pokemonConfigs[e].passive) { + enemyPokemon.passive = true; + } + // Set Status if (partyConfig.pokemonConfigs[e].status) { // Default to cureturn 3 for sleep @@ -462,40 +489,6 @@ export function generateModifierType(scene: BattleScene, modifier: () => Modifie return result; } -/** - * Will initialize reward phases to follow the mystery encounter - * Can have shop displayed or skipped - * @param scene - Battle Scene - * @param customShopRewards - adds a shop phase with the specified rewards / reward tiers - * @param nonShopRewards - will add a non-shop reward phase for each specified item/modifier (can happen in addition to a shop) - * @param preRewardsCallback - can execute an arbitrary callback before the new phases if necessary (useful for updating items/party/injecting new phases before MysteryEncounterRewardsPhase) - */ -export function setCustomEncounterRewards(scene: BattleScene, customShopRewards?: CustomModifierSettings, nonShopRewards?: ModifierTypeFunc[], preRewardsCallback?: Function) { - scene.currentBattle.mysteryEncounter.doEncounterRewards = (scene: BattleScene) => { - if (preRewardsCallback) { - preRewardsCallback(); - } - - if (customShopRewards) { - scene.unshiftPhase(new SelectModifierPhase(scene, 0, null, customShopRewards)); - } else { - scene.tryRemovePhase(p => p instanceof SelectModifierPhase); - } - - if (nonShopRewards?.length > 0) { - nonShopRewards.forEach((reward) => { - scene.unshiftPhase(new ModifierRewardPhase(scene, reward)); - }); - } else { - while (!isNullOrUndefined(scene.findPhase(p => p instanceof ModifierRewardPhase))) { - scene.tryRemovePhase(p => p instanceof ModifierRewardPhase); - } - } - - return true; - }; -} - /** * * @param scene @@ -557,7 +550,7 @@ export function selectPokemonForOption(scene: BattleScene, onPokemonSelected: (p if (!textPromptKey) { displayOptions(); } else { - const secondOptionSelectPrompt = getTextWithEncounterDialogueTokensAndColor(scene, textPromptKey, TextStyle.MESSAGE); + const secondOptionSelectPrompt = getEncounterText(scene, textPromptKey, TextStyle.MESSAGE); scene.ui.showText(secondOptionSelectPrompt, null, displayOptions, null, true); } }); @@ -575,49 +568,140 @@ export function selectPokemonForOption(scene: BattleScene, onPokemonSelected: (p } /** - * Will initialize exp phases to follow the mystery encounter (in addition to any combat or other exp earned) - * Exp earned will be a simple function that linearly scales with wave index, that can be increased or decreased by the expMultiplier - * Exp Share will have no effect (so no accounting for what mon is "on the field") - * Exp Balance will still function as normal + * Will initialize reward phases to follow the mystery encounter + * Can have shop displayed or skipped * @param scene - Battle Scene - * @param expMultiplier - default is 100, can be increased or decreased as desired + * @param customShopRewards - adds a shop phase with the specified rewards / reward tiers + * @param nonShopRewards - will add a non-shop reward phase for each specified item/modifier (can happen in addition to a shop) + * @param preRewardsCallback - can execute an arbitrary callback before the new phases if necessary (useful for updating items/party/injecting new phases before MysteryEncounterRewardsPhase) */ -export function setEncounterExp(scene: BattleScene, expMultiplier: number = 100) { - //const expBalanceModifier = scene.findModifier(m => m instanceof ExpBalanceModifier) as ExpBalanceModifier; - const expVal = scene.currentBattle.waveIndex * expMultiplier; - const pokemonExp = new Utils.NumberHolder(expVal); - const partyMemberExp = []; +export function setEncounterRewards(scene: BattleScene, customShopRewards?: CustomModifierSettings, nonShopRewards?: ModifierTypeFunc[], preRewardsCallback?: Function) { + scene.currentBattle.mysteryEncounter.doEncounterRewards = (scene: BattleScene) => { + if (preRewardsCallback) { + preRewardsCallback(); + } - const party = scene.getParty(); - party.forEach(pokemon => { - scene.applyModifiers(PokemonExpBoosterModifier, true, pokemon, pokemonExp); - partyMemberExp.push(Math.floor(pokemonExp.value)); - }); + if (customShopRewards) { + scene.unshiftPhase(new SelectModifierPhase(scene, 0, null, customShopRewards)); + } else { + scene.tryRemovePhase(p => p instanceof SelectModifierPhase); + } - // TODO - //if (expBalanceModifier) { - // let totalLevel = 0; - // let totalExp = 0; - // expPartyMembers.forEach((expPartyMember, epm) => { - // totalExp += partyMemberExp[epm]; - // totalLevel += expPartyMember.level; - // }); + if (nonShopRewards?.length > 0) { + nonShopRewards.forEach((reward) => { + scene.unshiftPhase(new ModifierRewardPhase(scene, reward)); + }); + } else { + while (!isNullOrUndefined(scene.findPhase(p => p instanceof ModifierRewardPhase))) { + scene.tryRemovePhase(p => p instanceof ModifierRewardPhase); + } + } - // const medianLevel = Math.floor(totalLevel / expPartyMembers.length); + return true; + }; +} - // const recipientExpPartyMemberIndexes = []; - // expPartyMembers.forEach((expPartyMember, epm) => { - // if (expPartyMember.level <= medianLevel) { - // recipientExpPartyMemberIndexes.push(epm); - // } - // }); +/** + * Will initialize exp phases into the phase queue (these are in addition to any combat or other exp earned) + * Exp Share and Exp Balance will still function as normal + * @param scene - Battle Scene + * @param participantIds - ids of party pokemon that get full exp value. Other party members will receive Exp Share amounts + * @param baseExpValue - gives exp equivalent to a pokemon of the wave index's level. + * Guidelines: + * 36 - Sunkern (lowest in game) + * 62-64 - regional starter base evos + * 100 - Scyther + * 170 - Spiritomb + * 250 - Gengar + * 290 - trio legendaries + * 340 - box legendaries + * 608 - Blissey (highest in game) + * @param useWaveIndex - set to false when directly passing the the full exp value instead of baseExpValue + */ +export function setEncounterExp(scene: BattleScene, participantIds: integer[], baseExpValue: number, useWaveIndex: boolean = true) { + scene.currentBattle.mysteryEncounter.doEncounterExp = (scene: BattleScene) => { + const party = scene.getParty(); + const expShareModifier = scene.findModifier(m => m instanceof ExpShareModifier) as ExpShareModifier; + const expBalanceModifier = scene.findModifier(m => m instanceof ExpBalanceModifier) as ExpBalanceModifier; + const multipleParticipantExpBonusModifier = scene.findModifier(m => m instanceof MultipleParticipantExpBonusModifier) as MultipleParticipantExpBonusModifier; + const nonFaintedPartyMembers = party.filter(p => p.hp); + const expPartyMembers = nonFaintedPartyMembers.filter(p => p.level < scene.getMaxExpLevel()); + const partyMemberExp = []; + let expValue = baseExpValue * (useWaveIndex ? scene.currentBattle.waveIndex : 1); - // const splitExp = Math.floor(totalExp / recipientExpPartyMemberIndexes.length); + if (participantIds?.length > 0) { + if (scene.currentBattle.mysteryEncounter.encounterVariant === MysteryEncounterVariant.TRAINER_BATTLE) { + expValue = Math.floor(expValue * 1.5); + } + for (const partyMember of nonFaintedPartyMembers) { + const pId = partyMember.id; + const participated = participantIds.includes(pId); + if (participated) { + partyMember.addFriendship(2); + } + if (!expPartyMembers.includes(partyMember)) { + continue; + } + if (!participated && !expShareModifier) { + partyMemberExp.push(0); + continue; + } + let expMultiplier = 0; + if (participated) { + expMultiplier += (1 / participantIds.length); + if (participantIds.length > 1 && multipleParticipantExpBonusModifier) { + expMultiplier += multipleParticipantExpBonusModifier.getStackCount() * 0.2; + } + } else if (expShareModifier) { + expMultiplier += (expShareModifier.getStackCount() * 0.2) / participantIds.length; + } + if (partyMember.pokerus) { + expMultiplier *= 1.5; + } + if (Overrides.XP_MULTIPLIER_OVERRIDE !== null) { + expMultiplier = Overrides.XP_MULTIPLIER_OVERRIDE; + } + const pokemonExp = new Utils.NumberHolder(expValue * expMultiplier); + scene.applyModifiers(PokemonExpBoosterModifier, true, partyMember, pokemonExp); + partyMemberExp.push(Math.floor(pokemonExp.value)); + } - // expPartyMembers.forEach((_partyMember, pm) => { - // partyMemberExp[pm] = Phaser.Math.Linear(partyMemberExp[pm], recipientExpPartyMemberIndexes.indexOf(pm) > -1 ? splitExp : 0, 0.2 * expBalanceModifier.getStackCount()); - // }); - //} + if (expBalanceModifier) { + let totalLevel = 0; + let totalExp = 0; + expPartyMembers.forEach((expPartyMember, epm) => { + totalExp += partyMemberExp[epm]; + totalLevel += expPartyMember.level; + }); + + const medianLevel = Math.floor(totalLevel / expPartyMembers.length); + + const recipientExpPartyMemberIndexes = []; + expPartyMembers.forEach((expPartyMember, epm) => { + if (expPartyMember.level <= medianLevel) { + recipientExpPartyMemberIndexes.push(epm); + } + }); + + const splitExp = Math.floor(totalExp / recipientExpPartyMemberIndexes.length); + + expPartyMembers.forEach((_partyMember, pm) => { + partyMemberExp[pm] = Phaser.Math.Linear(partyMemberExp[pm], recipientExpPartyMemberIndexes.indexOf(pm) > -1 ? splitExp : 0, 0.2 * expBalanceModifier.getStackCount()); + }); + } + + for (let pm = 0; pm < expPartyMembers.length; pm++) { + const exp = partyMemberExp[pm]; + + if (exp) { + const partyMemberIndex = party.indexOf(expPartyMembers[pm]); + scene.unshiftPhase(expPartyMembers[pm].isOnField() ? new ExpPhase(scene, partyMemberIndex, exp) : new ShowPartyExpBarPhase(scene, partyMemberIndex, exp)); + } + } + } + + return true; + }; } /** diff --git a/src/data/mystery-encounters/shady-vitamin-dealer.ts b/src/data/mystery-encounters/shady-vitamin-dealer.ts index bd2e6094748..b6ac5eacd22 100644 --- a/src/data/mystery-encounters/shady-vitamin-dealer.ts +++ b/src/data/mystery-encounters/shady-vitamin-dealer.ts @@ -4,7 +4,7 @@ import { leaveEncounterWithoutBattle, queueEncounterMessage, selectPokemonForOption, - setCustomEncounterRewards, + setEncounterRewards, updatePlayerMoney, } from "#app/data/mystery-encounters/mystery-encounter-utils"; import MysteryEncounter, {MysteryEncounterBuilder, MysteryEncounterTier} from "../mystery-encounter"; @@ -134,7 +134,7 @@ export const ShadyVitaminDealerEncounter: MysteryEncounter = new MysteryEncounte i++; } - setCustomEncounterRewards(scene, { guaranteedModifierTypeFuncs: modifiers, fillRemaining: false}); + setEncounterRewards(scene, { guaranteedModifierTypeFuncs: modifiers, fillRemaining: false}); leaveEncounterWithoutBattle(scene); }) .build()) diff --git a/src/data/mystery-encounters/sleeping-snorlax.ts b/src/data/mystery-encounters/sleeping-snorlax.ts index bf53ad72593..410a9081db3 100644 --- a/src/data/mystery-encounters/sleeping-snorlax.ts +++ b/src/data/mystery-encounters/sleeping-snorlax.ts @@ -4,7 +4,7 @@ import { EnemyPokemonConfig, generateModifierType, initBattleWithEnemyConfig, leaveEncounterWithoutBattle, queueEncounterMessage, - setCustomEncounterRewards + setEncounterRewards } from "./mystery-encounter-utils"; import MysteryEncounter, {MysteryEncounterBuilder, MysteryEncounterTier} from "../mystery-encounter"; import * as Utils from "../../utils"; @@ -23,7 +23,7 @@ import { BerryType } from "#enums/berry-type"; export const SleepingSnorlaxEncounter: MysteryEncounter = new MysteryEncounterBuilder() .withEncounterType(MysteryEncounterType.SLEEPING_SNORLAX) - .withEncounterTier(MysteryEncounterTier.RARE) + .withEncounterTier(MysteryEncounterTier.ULTRA) .withIntroSpriteConfigs([ { spriteKey: Species.SNORLAX.toString(), @@ -78,7 +78,7 @@ export const SleepingSnorlaxEncounter: MysteryEncounter = new MysteryEncounterBu // const sitrus = (modifierTypes.BERRY?.() as ModifierTypeGenerator).generateType(scene.getParty(), [BerryType.SITRUS]); const sitrus = generateModifierType(scene, modifierTypes.BERRY, [BerryType.SITRUS]); - setCustomEncounterRewards(scene, { guaranteedModifierTypeOptions: [new ModifierTypeOption(sitrus, 0)], fillRemaining: false}); + setEncounterRewards(scene, { guaranteedModifierTypeOptions: [new ModifierTypeOption(sitrus, 0)], fillRemaining: false}); queueEncounterMessage(scene, "mysteryEncounter:sleeping_snorlax_option_2_bad_result"); leaveEncounterWithoutBattle(scene); } else { @@ -101,7 +101,7 @@ export const SleepingSnorlaxEncounter: MysteryEncounter = new MysteryEncounterBu .withPrimaryPokemonRequirement(new MoveRequirement([Moves.PLUCK, Moves.COVET, Moves.KNOCK_OFF, Moves.THIEF, Moves.TRICK, Moves.SWITCHEROO])) .withOptionPhase(async (scene: BattleScene) => { // Leave encounter with no rewards or exp - setCustomEncounterRewards(scene, { guaranteedModifierTypeFuncs: [modifierTypes.LEFTOVERS], fillRemaining: false}); + setEncounterRewards(scene, { guaranteedModifierTypeFuncs: [modifierTypes.LEFTOVERS], fillRemaining: false}); queueEncounterMessage(scene, "mysteryEncounter:sleeping_snorlax_option_3_good_result"); leaveEncounterWithoutBattle(scene); }) diff --git a/src/data/mystery-encounters/training-session.ts b/src/data/mystery-encounters/training-session.ts index c833c37c06f..ab1bacc71f9 100644 --- a/src/data/mystery-encounters/training-session.ts +++ b/src/data/mystery-encounters/training-session.ts @@ -1,10 +1,10 @@ import BattleScene from "../../battle-scene"; import { EnemyPartyConfig, - getTextWithEncounterDialogueTokensAndColor, + getEncounterText, initBattleWithEnemyConfig, selectPokemonForOption, - setCustomEncounterRewards + setEncounterRewards } from "#app/data/mystery-encounters/mystery-encounter-utils"; import {MysteryEncounterType} from "#enums/mystery-encounter-type"; import MysteryEncounter, {MysteryEncounterBuilder, MysteryEncounterTier} from "../mystery-encounter"; @@ -26,7 +26,7 @@ import {pokemonInfo} from "#app/locales/en/pokemon-info"; export const TrainingSessionEncounter: MysteryEncounter = new MysteryEncounterBuilder() .withEncounterType(MysteryEncounterType.TRAINING_SESSION) - .withEncounterTier(MysteryEncounterTier.RARE) + .withEncounterTier(MysteryEncounterTier.ULTRA) .withIntroSpriteConfigs([ { spriteKey: "training_gear", @@ -128,10 +128,10 @@ export const TrainingSessionEncounter: MysteryEncounter = new MysteryEncounterBu scene.addModifier(mod, true, false, false, true); } scene.updateModifiers(true); - scene.queueMessage(getTextWithEncounterDialogueTokensAndColor(scene, "mysteryEncounter:training_session_battle_finished_1"), null, true); + scene.queueMessage(getEncounterText(scene, "mysteryEncounter:training_session_battle_finished_1"), null, true); }; - setCustomEncounterRewards(scene, { fillRemaining: true }, null, onBeforeRewardsPhase); + setEncounterRewards(scene, { fillRemaining: true }, null, onBeforeRewardsPhase); return initBattleWithEnemyConfig(scene, config); }) @@ -174,7 +174,7 @@ export const TrainingSessionEncounter: MysteryEncounter = new MysteryEncounterBu scene.removePokemonFromPlayerParty(playerPokemon, false); const onBeforeRewardsPhase = () => { - scene.queueMessage(getTextWithEncounterDialogueTokensAndColor(scene, "mysteryEncounter:training_session_battle_finished_2"), null, true); + 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); @@ -187,7 +187,7 @@ export const TrainingSessionEncounter: MysteryEncounter = new MysteryEncounterBu scene.updateModifiers(true); }; - setCustomEncounterRewards(scene, { fillRemaining: true }, null, onBeforeRewardsPhase); + setEncounterRewards(scene, { fillRemaining: true }, null, onBeforeRewardsPhase); return initBattleWithEnemyConfig(scene, config); }) @@ -237,7 +237,7 @@ export const TrainingSessionEncounter: MysteryEncounter = new MysteryEncounterBu scene.removePokemonFromPlayerParty(playerPokemon, false); const onBeforeRewardsPhase = () => { - scene.queueMessage(getTextWithEncounterDialogueTokensAndColor(scene, "mysteryEncounter:training_session_battle_finished_3"), null, true); + 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()) { @@ -268,7 +268,7 @@ export const TrainingSessionEncounter: MysteryEncounter = new MysteryEncounterBu scene.updateModifiers(true); }; - setCustomEncounterRewards(scene, { fillRemaining: true }, null, onBeforeRewardsPhase); + setEncounterRewards(scene, { fillRemaining: true }, null, onBeforeRewardsPhase); return initBattleWithEnemyConfig(scene, config); }) diff --git a/src/locales/en/mystery-encounter.ts b/src/locales/en/mystery-encounter.ts index 9fe63b33f58..8bcf5df76d9 100644 --- a/src/locales/en/mystery-encounter.ts +++ b/src/locales/en/mystery-encounter.ts @@ -7,10 +7,10 @@ import {SimpleTranslationEntries} from "#app/interfaces/locales"; * * '@ec{}' will auto-inject the matching token value for the specified Encounter * - * '@ecCol[]{}' will auto-color the given text to a specified TextStyle (e.g. TextStyle.SUMMARY_GREEN) + * '@[]{}' will auto-color the given text to a specified TextStyle (e.g. TextStyle.SUMMARY_GREEN) * * Any '(+)' or '(-)' type of tooltip will auto-color to green/blue respectively. THIS ONLY OCCURS FOR OPTION TOOLTIPS, NOWHERE ELSE - * Other types of '(...)' tooltips will have to specify the text color manually by using '@ecCol[SUMMARY_GREEN]{}' pattern + * Other types of '(...)' tooltips will have to specify the text color manually by using '@[SUMMARY_GREEN]{}' pattern */ export const mysteryEncounter: SimpleTranslationEntries = { // DO NOT REMOVE @@ -22,7 +22,7 @@ export const mysteryEncounter: SimpleTranslationEntries = { "mysterious_chest_description": "A beautifully ornamented chest stands on the ground. There must be something good inside... right?", "mysterious_chest_query": "Will you open it?", "mysterious_chest_option_1_label": "Open it", - "mysterious_chest_option_1_tooltip": "@ecCol[SUMMARY_BLUE]{(35%) Something terrible}\n@ecCol[SUMMARY_GREEN]{(40%) Okay Rewards}\n@ecCol[SUMMARY_GREEN]{(20%) Good Rewards}\n@ecCol[SUMMARY_GREEN]{(4%) Great Rewards}\n@ecCol[SUMMARY_GREEN]{(1%) Amazing Rewards}", + "mysterious_chest_option_1_tooltip": "@[SUMMARY_BLUE]{(35%) Something terrible}\n@[SUMMARY_GREEN]{(40%) Okay Rewards}\n@[SUMMARY_GREEN]{(20%) Good Rewards}\n@[SUMMARY_GREEN]{(4%) Great Rewards}\n@[SUMMARY_GREEN]{(1%) Amazing Rewards}", "mysterious_chest_option_2_label": "It's too risky, leave", "mysterious_chest_option_2_tooltip": "(-) No Rewards", "mysterious_chest_option_1_selected_message": "You open the chest to find...", @@ -41,8 +41,8 @@ export const mysteryEncounter: SimpleTranslationEntries = { "fight_or_flight_option_1_label": "Battle the Pokémon", "fight_or_flight_option_1_tooltip": "(-) Hard Battle\n(+) New Item", "fight_or_flight_option_2_label": "Steal the item", - "fight_or_flight_option_2_tooltip": "@ecCol[SUMMARY_GREEN]{(35%) Steal Item}\n@ecCol[SUMMARY_BLUE]{(65%) Harder Battle}", - "fight_or_flight_option_2_steal_tooltip": "@ecCol[SUMMARY_GREEN]{(?) Use a Pokémon Move}", + "fight_or_flight_option_2_tooltip": "@[SUMMARY_GREEN]{(35%) Steal Item}\n@[SUMMARY_BLUE]{(65%) Harder Battle}", + "fight_or_flight_option_2_steal_tooltip": "@[SUMMARY_GREEN]{(?) Use a Pokémon Move}", "fight_or_flight_option_3_label": "Leave", "fight_or_flight_option_3_tooltip": "(-) No Rewards", "fight_or_flight_option_1_selected_message": "You approach the\nPokémon without fear.", @@ -167,7 +167,7 @@ export const mysteryEncounter: SimpleTranslationEntries = { "sleeping_snorlax_option_1_label": "Fight it", "sleeping_snorlax_option_1_tooltip": "(-) Fight Sleeping Snorlax", "sleeping_snorlax_option_2_label": "Wait for it to move", - "sleeping_snorlax_option_2_tooltip": "@ecCol[SUMMARY_BLUE]{(75%) Wait a short time}\n@ecCol[SUMMARY_BLUE]{(25%) Wait a long time}", + "sleeping_snorlax_option_2_tooltip": "@[SUMMARY_BLUE]{(75%) Wait a short time}\n@[SUMMARY_BLUE]{(25%) Wait a long time}", "sleeping_snorlax_option_3_label": "Steal", "sleeping_snorlax_option_3_tooltip": "(+) Leftovers", "sleeping_snorlax_option_3_disabled_tooltip": "Your Pokémon need to know certain moves to choose this", diff --git a/src/phases.ts b/src/phases.ts index 4b918008978..730797c3d2f 100644 --- a/src/phases.ts +++ b/src/phases.ts @@ -67,7 +67,7 @@ import { TrainerType } from "#enums/trainer-type"; import { BattlePhase } from "#app/phases/battle-phase"; import { MysteryEncounterVariant } from "#app/data/mystery-encounter"; import { MysteryEncounterPhase } from "#app/phases/mystery-encounter-phase"; -import { getTextWithEncounterDialogueTokensAndColor, handleMysteryEncounterVictory } from "#app/data/mystery-encounters/mystery-encounter-utils"; +import { getEncounterText, handleMysteryEncounterVictory } from "#app/data/mystery-encounters/mystery-encounter-utils"; import { SelectModifierPhase } from "#app/phases/select-modifier-phase"; const { t } = i18next; @@ -1081,8 +1081,8 @@ export class EncounterPhase extends BattlePhase { const showNextDialogue = () => { const nextAction = i === introDialogue.length - 1 ? doShowEncounterOptions : showNextDialogue; const dialogue = introDialogue[i]; - const title = getTextWithEncounterDialogueTokensAndColor(this.scene, dialogue.speaker); - const text = getTextWithEncounterDialogueTokensAndColor(this.scene, dialogue.text); + const title = getEncounterText(this.scene, dialogue.speaker); + const text = getEncounterText(this.scene, dialogue.text); if (title) { this.scene.ui.showDialogue(text, title, null, nextAction, 0, i === 0 ? 750 : 0); } else { diff --git a/src/phases/mystery-encounter-phase.ts b/src/phases/mystery-encounter-phase.ts index b928d98834c..f4a6d955fb6 100644 --- a/src/phases/mystery-encounter-phase.ts +++ b/src/phases/mystery-encounter-phase.ts @@ -3,7 +3,7 @@ import BattleScene from "../battle-scene"; import { Phase } from "../phase"; import { Mode } from "../ui/ui"; import { - getTextWithEncounterDialogueTokensAndColor + getEncounterText } from "../data/mystery-encounters/mystery-encounter-utils"; import { CheckSwitchPhase, NewBattlePhase, PostSummonPhase, ReturnPhase, ScanIvsPhase, SummonPhase, ToggleDoublePositionPhase } from "../phases"; import MysteryEncounterOption from "../data/mystery-encounter-option"; @@ -89,9 +89,9 @@ export class MysteryEncounterPhase extends Phase { const nextAction = i === selectedDialogue.length - 1 ? endDialogueAndContinueEncounter : showNextDialogue; const dialogue = selectedDialogue[i]; let title: string = null; - const text: string = getTextWithEncounterDialogueTokensAndColor(this.scene, dialogue.text); + const text: string = getEncounterText(this.scene, dialogue.text); if (dialogue.speaker) { - title = getTextWithEncounterDialogueTokensAndColor(this.scene, dialogue.speaker); + title = getEncounterText(this.scene, dialogue.speaker); } if (title) { @@ -377,6 +377,7 @@ export class MysteryEncounterBattlePhase extends Phase { /** * Will handle (in order): + * - Any encounter reward logic that is set within MysteryEncounter doEncounterExp * - Any encounter reward logic that is set within MysteryEncounter doEncounterRewards * - Otherwise, can add a no-reward-item shop with only Potions, etc. if addHealPhase is true * - Queuing of the PostMysteryEncounterPhase @@ -393,6 +394,10 @@ export class MysteryEncounterRewardsPhase extends Phase { super.start(); this.scene.executeWithSeedOffset(() => { + if (this.scene.currentBattle.mysteryEncounter.doEncounterExp) { + this.scene.currentBattle.mysteryEncounter.doEncounterExp(this.scene); + } + if (this.scene.currentBattle.mysteryEncounter.doEncounterRewards) { this.scene.currentBattle.mysteryEncounter.doEncounterRewards(this.scene); } else if (this.addHealPhase) { @@ -451,9 +456,9 @@ export class PostMysteryEncounterPhase extends Phase { const nextAction = i === outroDialogue.length - 1 ? endPhase : showNextDialogue; const dialogue = outroDialogue[i]; let title: string = null; - const text: string = getTextWithEncounterDialogueTokensAndColor(this.scene, dialogue.text); + const text: string = getEncounterText(this.scene, dialogue.text); if (dialogue.speaker) { - title = getTextWithEncounterDialogueTokensAndColor(this.scene, dialogue.speaker); + title = getEncounterText(this.scene, dialogue.speaker); } this.scene.ui.setMode(Mode.MESSAGE); diff --git a/src/test/mystery-encounter/mystery-encounter-utils.test.ts b/src/test/mystery-encounter/mystery-encounter-utils.test.ts index 63cbd83d975..cd03033ae3b 100644 --- a/src/test/mystery-encounter/mystery-encounter-utils.test.ts +++ b/src/test/mystery-encounter/mystery-encounter-utils.test.ts @@ -3,7 +3,7 @@ import GameManager from "#app/test/utils/gameManager"; import Phaser from "phaser"; import { getHighestLevelPlayerPokemon, getLowestLevelPlayerPokemon, - getRandomPlayerPokemon, getRandomSpeciesByStarterTier, getTextWithEncounterDialogueTokensAndColor, + getRandomPlayerPokemon, getRandomSpeciesByStarterTier, getEncounterText, koPlayerPokemon, queueEncounterMessage, showEncounterDialogue, showEncounterText, } from "#app/data/mystery-encounters/mystery-encounter-utils"; import {initSceneWithoutEncounterPhase} from "#test/utils/gameManagerUtils"; @@ -276,7 +276,7 @@ describe("Mystery Encounter Utils", () => { scene.currentBattle.mysteryEncounter = new MysteryEncounter(null); scene.currentBattle.mysteryEncounter.setDialogueToken("test", "value"); - const result = getTextWithEncounterDialogueTokensAndColor(scene, "mysteryEncounter:unit_test_dialogue"); + const result = getEncounterText(scene, "mysteryEncounter:unit_test_dialogue"); expect(result).toEqual("[color=#f8f8f8][shadow=#6b5a73]valuevalue @ec{testvalue} @ec{test1} value @ec{test\\} @ec{test\\} {test}[/color][/shadow]"); }); @@ -285,7 +285,7 @@ describe("Mystery Encounter Utils", () => { scene.currentBattle.mysteryEncounter.setDialogueToken("test", "value"); scene.currentBattle.mysteryEncounter.setDialogueToken("testvalue", "new"); - const result = getTextWithEncounterDialogueTokensAndColor(scene, "mysteryEncounter:unit_test_dialogue"); + const result = getEncounterText(scene, "mysteryEncounter:unit_test_dialogue"); expect(result).toEqual("[color=#f8f8f8][shadow=#6b5a73]valuevalue new @ec{test1} value @ec{test\\} @ec{test\\} {test}[/color][/shadow]"); }); }); diff --git a/src/test/phases/mystery-encounter-phase.test.ts b/src/test/phases/mystery-encounter-phase.test.ts index 0e9cce09f12..efe7dab810a 100644 --- a/src/test/phases/mystery-encounter-phase.test.ts +++ b/src/test/phases/mystery-encounter-phase.test.ts @@ -63,7 +63,7 @@ describe("Mystery Encounter Phases", () => { expect(game.scene.mysteryEncounterData.encounteredEvents.length).toBeGreaterThan(0); expect(game.scene.mysteryEncounterData.encounteredEvents[0][0]).toEqual(MysteryEncounterType.MYSTERIOUS_CHALLENGERS); - expect(game.scene.mysteryEncounterData.encounteredEvents[0][1]).toEqual(MysteryEncounterTier.UNCOMMON); + expect(game.scene.mysteryEncounterData.encounteredEvents[0][1]).toEqual(MysteryEncounterTier.GREAT); expect(game.scene.ui.getMode()).toBe(Mode.MYSTERY_ENCOUNTER); }); diff --git a/src/ui/mystery-encounter-ui-handler.ts b/src/ui/mystery-encounter-ui-handler.ts index 862fa155bf4..7714f280197 100644 --- a/src/ui/mystery-encounter-ui-handler.ts +++ b/src/ui/mystery-encounter-ui-handler.ts @@ -10,7 +10,7 @@ import MysteryEncounterOption from "../data/mystery-encounter-option"; import * as Utils from "../utils"; import {isNullOrUndefined} from "../utils"; import {getPokeballAtlasKey} from "../data/pokeball"; -import {getTextWithEncounterDialogueTokensAndColor} from "#app/data/mystery-encounters/mystery-encounter-utils"; +import {getEncounterText} from "#app/data/mystery-encounters/mystery-encounter-utils"; export default class MysteryEncounterUiHandler extends UiHandler { private cursorContainer: Phaser.GameObjects.Container; @@ -298,9 +298,9 @@ export default class MysteryEncounterUiHandler extends UiHandler { this.filteredEncounterOptions = mysteryEncounter.options; this.optionsMeetsReqs = []; - const titleText: string = getTextWithEncounterDialogueTokensAndColor(this.scene, mysteryEncounter.dialogue.encounterOptionsDialogue.title, TextStyle.TOOLTIP_TITLE); - const descriptionText: string = getTextWithEncounterDialogueTokensAndColor(this.scene, mysteryEncounter.dialogue.encounterOptionsDialogue.description, TextStyle.TOOLTIP_CONTENT); - const queryText: string = getTextWithEncounterDialogueTokensAndColor(this.scene, mysteryEncounter.dialogue.encounterOptionsDialogue.query, TextStyle.TOOLTIP_CONTENT); + const titleText: string = getEncounterText(this.scene, mysteryEncounter.dialogue.encounterOptionsDialogue.title, TextStyle.TOOLTIP_TITLE); + const descriptionText: string = getEncounterText(this.scene, mysteryEncounter.dialogue.encounterOptionsDialogue.description, TextStyle.TOOLTIP_CONTENT); + const queryText: string = getEncounterText(this.scene, mysteryEncounter.dialogue.encounterOptionsDialogue.query, TextStyle.TOOLTIP_CONTENT); // Clear options container (except cursor) this.optionsContainer.removeAll(); @@ -320,7 +320,7 @@ export default class MysteryEncounterUiHandler extends UiHandler { break; } const option = mysteryEncounter.dialogue.encounterOptionsDialogue.options[i]; - const text = getTextWithEncounterDialogueTokensAndColor(this.scene, option.buttonLabel, option.style ? option.style : TextStyle.WINDOW); + const text = getEncounterText(this.scene, option.buttonLabel, option.style ? option.style : TextStyle.WINDOW); if (text) { optionText.setText(text); } @@ -416,15 +416,15 @@ export default class MysteryEncounterUiHandler extends UiHandler { let text; const option = mysteryEncounter.dialogue.encounterOptionsDialogue.options[cursor]; if (!this.optionsMeetsReqs[cursor] && option.disabledTooltip) { - text = getTextWithEncounterDialogueTokensAndColor(this.scene, option.disabledTooltip, TextStyle.TOOLTIP_CONTENT); + text = getEncounterText(this.scene, option.disabledTooltip, TextStyle.TOOLTIP_CONTENT); } else { - text = getTextWithEncounterDialogueTokensAndColor(this.scene, option.buttonTooltip, TextStyle.TOOLTIP_CONTENT); + text = getEncounterText(this.scene, option.buttonTooltip, TextStyle.TOOLTIP_CONTENT); } // Auto-color options green/blue for good/bad by looking for (+)/(-) const primaryStyleString = [...text.match(new RegExp(/\[color=[^\[]*\]\[shadow=[^\[]*\]/i))][0]; - text = text.replace(/(\([^\(]*\+\)[^\(\[]*)/gi, substring => "[/color][/shadow]" + getBBCodeFrag(substring, TextStyle.SUMMARY_GREEN) + "[/color][/shadow]" + primaryStyleString); - text = text.replace(/(\([^\(]*\-\)[^\(\[]*)/gi, substring => "[/color][/shadow]" + getBBCodeFrag(substring, TextStyle.SUMMARY_BLUE) + "[/color][/shadow]" + primaryStyleString); + text = text.replace(/(\(\+\)[^\(\[]*)/gi, substring => "[/color][/shadow]" + getBBCodeFrag(substring, TextStyle.SUMMARY_GREEN) + "[/color][/shadow]" + primaryStyleString); + text = text.replace(/(\(\-\)[^\(\[]*)/gi, substring => "[/color][/shadow]" + getBBCodeFrag(substring, TextStyle.SUMMARY_BLUE) + "[/color][/shadow]" + primaryStyleString); if (text) { const tooltipTextObject = addBBCodeTextObject(this.scene, 6, 7, text, TextStyle.TOOLTIP_CONTENT, { wordWrap: { width: 600 }, fontSize: "72px" }); diff --git a/src/ui/text.ts b/src/ui/text.ts index 2a12e883deb..482e01ea29a 100644 --- a/src/ui/text.ts +++ b/src/ui/text.ts @@ -171,6 +171,34 @@ export function getBBCodeFrag(content: string, textStyle: TextStyle, uiTheme: Ui return `[color=${getTextColor(textStyle, false, uiTheme)}][shadow=${getTextColor(textStyle, true, uiTheme)}]${content}`; } +/** + * Should only be used with BBCodeText (see addBBCodeTextObject()) + * This does NOT work with UI showText() or showDialogue() methods. + * Method will do pattern match/replace and apply BBCode color/shadow styling to substrings within the content: + * @[]{} + * + * Example: passing a content string of "@[SUMMARY_BLUE]{blue text} primaryStyle text @[SUMMARY_RED]{red text}" will result in: + * - "blue text" with TextStyle.SUMMARY_BLUE applied + * - " primaryStyle text " with primaryStyle TextStyle applied + * - "red text" with TextStyle.SUMMARY_RED applied + * @param content - string with styling that need to be applied for BBCodeTextObject + * @param primaryStyle - primary style is required in order to escape BBCode styling properly. + * @param uiTheme + */ +export function getTextWithColors(content: string, primaryStyle: TextStyle, uiTheme: UiTheme = UiTheme.DEFAULT): string { + // Apply primary styling before anything else + let text = getBBCodeFrag(content, primaryStyle, uiTheme) + "[/color][/shadow]"; + const primaryStyleString = [...text.match(new RegExp(/\[color=[^\[]*\]\[shadow=[^\[]*\]/i))][0]; + + // Set custom colors + text = text.replace(/@\[([^{]*)\]{([^}]*)}/gi, (substring, textStyle: string, textToColor: string) => { + return "[/color][/shadow]" + getBBCodeFrag(textToColor, TextStyle[textStyle], uiTheme) + "[/color][/shadow]" + primaryStyleString; + }); + + // Remove extra style block at the end + return text.replace(/\[color=[^\[]*\]\[shadow=[^\[]*\]\[\/color\]\[\/shadow\]/gi, ""); +} + export function getTextColor(textStyle: TextStyle, shadow?: boolean, uiTheme: UiTheme = UiTheme.DEFAULT): string { switch (textStyle) { case TextStyle.MESSAGE: