diff --git a/src/data/mystery-encounters/encounters/safari-zone-encounter.ts b/src/data/mystery-encounters/encounters/safari-zone-encounter.ts index e5c5bd39b4d..28c60a0eb10 100644 --- a/src/data/mystery-encounters/encounters/safari-zone-encounter.ts +++ b/src/data/mystery-encounters/encounters/safari-zone-encounter.ts @@ -64,7 +64,7 @@ export const SafariZoneEncounter: IMysteryEncounter = .withOptionPhase(async (scene: BattleScene) => { // Start safari encounter const encounter = scene.currentBattle.mysteryEncounter; - encounter.encounterVariant = MysteryEncounterVariant.REPEATED_ENCOUNTER; + encounter.encounterVariant = MysteryEncounterVariant.CONTINUOUS_ENCOUNTER; encounter.misc = { safariPokemonRemaining: 3 }; diff --git a/src/data/mystery-encounters/mystery-encounter.ts b/src/data/mystery-encounters/mystery-encounter.ts index 6136e338854..026c58bb0c3 100644 --- a/src/data/mystery-encounters/mystery-encounter.ts +++ b/src/data/mystery-encounters/mystery-encounter.ts @@ -28,7 +28,7 @@ export enum MysteryEncounterVariant { BOSS_BATTLE, NO_BATTLE, /** For spawning new encounter queries instead of continuing to next wave */ - REPEATED_ENCOUNTER + CONTINUOUS_ENCOUNTER } /** @@ -115,11 +115,6 @@ export default interface IMysteryEncounter { * You probably shouldn't do anything with this unless you have a very specific need */ introVisuals?: MysteryEncounterIntroVisuals; - /** - * Used for keeping RNG consistent on session resets, but increments when cycling through multiple "Encounters" on the same wave - * You should never need to modify this - */ - seedOffset?: any; /** * Flags @@ -172,6 +167,12 @@ export default interface IMysteryEncounter { * Unless you know what you're doing, you should use MysteryEncounterBuilder to create an instance for this class */ export default class IMysteryEncounter implements IMysteryEncounter { + /** + * Used for keeping RNG consistent on session resets, but increments when cycling through multiple "Encounters" on the same wave + * You should only need to interact via getter/update methods + */ + private seedOffset?: any; + constructor(encounter: IMysteryEncounter) { if (!isNullOrUndefined(encounter)) { Object.assign(this, encounter); @@ -371,6 +372,20 @@ export default class IMysteryEncounter implements IMysteryEncounter { this.dialogueTokens[key] = value; } + getSeedOffset?() { + return this.seedOffset; + } + + /** + * Maintains seed offset for RNG consistency + * Increments if the same MysteryEncounter has multiple option select cycles + * @param scene + */ + updateSeedOffset?(scene: BattleScene) { + const currentOffset = this.seedOffset ?? scene.currentBattle.waveIndex * 1000; + this.seedOffset = currentOffset + 512; + } + private capitalizeFirstLetter?(str: string) { return str.charAt(0).toUpperCase() + str.slice(1); } @@ -442,7 +457,7 @@ export class MysteryEncounterBuilder implements Partial { * @param callback - {@linkcode OptionPhaseCallback} * @returns */ - withSimpleOption(dialogue: OptionTextDisplay, callback: OptionPhaseCallback): this { + withSimpleOption(dialogue: OptionTextDisplay, callback: OptionPhaseCallback): this & Pick { return this.withOption(new MysteryEncounterOptionBuilder().withOptionMode(EncounterOptionMode.DEFAULT).withDialogue(dialogue).withOptionPhase(callback).build()); } diff --git a/src/data/mystery-encounters/utils/encounter-phase-utils.ts b/src/data/mystery-encounters/utils/encounter-phase-utils.ts index 9c569775f38..8408dbfdd2c 100644 --- a/src/data/mystery-encounters/utils/encounter-phase-utils.ts +++ b/src/data/mystery-encounters/utils/encounter-phase-utils.ts @@ -30,6 +30,10 @@ import { Gender } from "#app/data/gender"; import { Moves } from "#enums/moves"; import { initMoveAnim, loadMoveAnimAssets } from "#app/data/battle-anims"; +/** + * Animates exclamation sprite over trainer's head at start of encounter + * @param scene + */ export function doTrainerExclamation(scene: BattleScene) { const exclamationSprite = scene.addFieldSprite(0, 0, "exclaim"); exclamationSprite.setName("exclamation"); @@ -476,6 +480,7 @@ export function setEncounterExp(scene: BattleScene, participantId: integer | int const nonFaintedPartyMembers = party.filter(p => p.hp); const expPartyMembers = nonFaintedPartyMembers.filter(p => p.level < scene.getMaxExpLevel()); const partyMemberExp = []; + // EXP value calculation is based off Pokemon.getExpValue let expValue = Math.floor(baseExpValue * (useWaveIndex ? scene.currentBattle.waveIndex : 1) / 5 + 1); if (participantIds?.length > 0) { @@ -597,7 +602,7 @@ export function handleMysteryEncounterVictory(scene: BattleScene, addHealPhase: // If in repeated encounter variant, do nothing // Variant must eventually be swapped in order to handle "true" end of the encounter - if (scene.currentBattle.mysteryEncounter.encounterVariant === MysteryEncounterVariant.REPEATED_ENCOUNTER) { + if (scene.currentBattle.mysteryEncounter.encounterVariant === MysteryEncounterVariant.CONTINUOUS_ENCOUNTER) { return; } else if (scene.currentBattle.mysteryEncounter.encounterVariant === MysteryEncounterVariant.NO_BATTLE) { scene.pushPhase(new EggLapsePhase(scene)); @@ -657,7 +662,12 @@ export function transitionMysteryEncounterIntroVisuals(scene: BattleScene, hide: }); } -export function handleEncounterStartOfBattleEffects(scene: BattleScene) { +/** + * Will queue moves for any pokemon to use before the first CommandPhase of a battle + * Mostly useful for allowing MysteryEncounter enemies to "cheat" and use moves before the first turn + * @param scene + */ +export function handleMysteryEncounterBattleStartEffects(scene: BattleScene) { const encounter = scene.currentBattle?.mysteryEncounter; if (scene.currentBattle.battleType === BattleType.MYSTERY_ENCOUNTER && encounter.encounterVariant !== MysteryEncounterVariant.NO_BATTLE && !encounter.startOfBattleEffectsComplete) { const effects = encounter.startOfBattleEffects; diff --git a/src/field/mystery-encounter-intro.ts b/src/field/mystery-encounter-intro.ts index fb894869adf..9ae83f49fab 100644 --- a/src/field/mystery-encounter-intro.ts +++ b/src/field/mystery-encounter-intro.ts @@ -349,6 +349,10 @@ export default class MysteryEncounterIntroVisuals extends Phaser.GameObjects.Con }); } + /** + * Sets container and all child sprites to visible + * @param value - true for visible, false for hidden + */ setVisible(value: boolean): this { this.getSprites().forEach(sprite => { sprite.setVisible(value); diff --git a/src/field/pokemon.ts b/src/field/pokemon.ts index 65fd982cbdc..b0974bd0aa8 100644 --- a/src/field/pokemon.ts +++ b/src/field/pokemon.ts @@ -101,6 +101,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { public battleSummonData: PokemonBattleSummonData; public turnData: PokemonTurnData; + /** Used by Mystery Encounters to execute pokemon-specific logic (such as stat boosts) at start of battle */ public mysteryEncounterBattleEffects: (pokemon: Pokemon) => void = null; public fieldPosition: FieldPosition; diff --git a/src/phases.ts b/src/phases.ts index 22a7b8bdd98..87e8cde9f5d 100644 --- a/src/phases.ts +++ b/src/phases.ts @@ -67,7 +67,7 @@ import { Species } from "#enums/species"; import { TrainerType } from "#enums/trainer-type"; import { MysteryEncounterVariant } from "#app/data/mystery-encounters/mystery-encounter"; import { MysteryEncounterPhase } from "#app/phases/mystery-encounter-phases"; -import { doTrainerExclamation, handleEncounterStartOfBattleEffects, handleMysteryEncounterVictory } from "#app/data/mystery-encounters/utils/encounter-phase-utils"; +import { doTrainerExclamation, handleMysteryEncounterBattleStartEffects, handleMysteryEncounterVictory } from "#app/data/mystery-encounters/utils/encounter-phase-utils"; import ModifierSelectUiHandler, { SHOP_OPTIONS_ROW_LIMIT } from "#app/ui/modifier-select-ui-handler"; import { getEncounterText } from "#app/data/mystery-encounters/utils/encounter-dialogue-utils"; @@ -2053,8 +2053,7 @@ export class TurnInitPhase extends FieldPhase { //this.scene.pushPhase(new MoveAnimTestPhase(this.scene)); this.scene.eventTarget.dispatchEvent(new TurnInitEvent()); - // Start of battle effects for Mystery Encounters - handleEncounterStartOfBattleEffects(this.scene); + handleMysteryEncounterBattleStartEffects(this.scene); this.scene.getField().forEach((pokemon, i) => { if (pokemon?.isActive()) { diff --git a/src/phases/mystery-encounter-phases.ts b/src/phases/mystery-encounter-phases.ts index 25f6b140140..11ff4766695 100644 --- a/src/phases/mystery-encounter-phases.ts +++ b/src/phases/mystery-encounter-phases.ts @@ -28,6 +28,8 @@ import { BattlerTagLapseType } from "#app/data/battler-tags"; export class MysteryEncounterPhase extends Phase { optionSelectSettings: OptionSelectSettings; + private FIRST_DIALOGUE_PROMPT_DELAY = 300; + /** * * @param scene @@ -46,9 +48,7 @@ export class MysteryEncounterPhase extends Phase { this.scene.clearPhaseQueue(); this.scene.clearPhaseQueueSplice(); - // Generates seed offset for RNG consistency, but incremented if the same MysteryEncounter has multiple option select cycles - const offset = this.scene.currentBattle.mysteryEncounter.seedOffset ?? this.scene.currentBattle.waveIndex * 1000; - this.scene.currentBattle.mysteryEncounter.seedOffset = offset + 512; + this.scene.currentBattle.mysteryEncounter.updateSeedOffset(this.scene); if (!this.optionSelectSettings) { // Sets flag that ME was encountered, only if this is not a followup option select phase @@ -79,7 +79,7 @@ export class MysteryEncounterPhase extends Phase { this.continueEncounter(); } }); - }, this.scene.currentBattle.mysteryEncounter.seedOffset); + }, this.scene.currentBattle.mysteryEncounter.getSeedOffset()); } else { this.continueEncounter(); } @@ -109,9 +109,9 @@ export class MysteryEncounterPhase extends Phase { } if (title) { - this.scene.ui.showDialogue(text, title, null, nextAction, 0, i === 0 ? 300 : 0); + this.scene.ui.showDialogue(text, title, null, nextAction, 0, i === 0 ? this.FIRST_DIALOGUE_PROMPT_DELAY : 0); } else { - this.scene.ui.showText(text, null, nextAction, i === 0 ? 300 : 0, true); + this.scene.ui.showText(text, null, nextAction, i === 0 ? this.FIRST_DIALOGUE_PROMPT_DELAY : 0, true); } i++; }; @@ -154,14 +154,14 @@ export class MysteryEncounterOptionSelectedPhase extends Phase { this.onOptionSelect(this.scene).finally(() => { this.end(); }); - }, this.scene.currentBattle.mysteryEncounter.seedOffset); + }, this.scene.currentBattle.mysteryEncounter.getSeedOffset()); }); } else { this.scene.executeWithSeedOffset(() => { this.onOptionSelect(this.scene).finally(() => { this.end(); }); - }, this.scene.currentBattle.mysteryEncounter.seedOffset); + }, this.scene.currentBattle.mysteryEncounter.getSeedOffset()); } } } @@ -279,7 +279,7 @@ export class MysteryEncounterBattlePhase extends Phase { } else { const trainer = this.scene.currentBattle.trainer; let message: string; - scene.executeWithSeedOffset(() => message = Utils.randSeedItem(encounterMessages), this.scene.currentBattle.mysteryEncounter.seedOffset); + scene.executeWithSeedOffset(() => message = Utils.randSeedItem(encounterMessages), this.scene.currentBattle.mysteryEncounter.getSeedOffset()); const showDialogueAndSummon = () => { scene.ui.showDialogue(message, trainer.getName(TrainerSlot.NONE, true), null, () => { @@ -438,7 +438,7 @@ export class PostMysteryEncounterPhase extends Phase { this.continueEncounter(); } }); - }, this.scene.currentBattle.mysteryEncounter.seedOffset); + }, this.scene.currentBattle.mysteryEncounter.getSeedOffset()); } else { this.continueEncounter(); } diff --git a/src/test/mystery-encounter/encounterTestUtils.ts b/src/test/mystery-encounter/encounterTestUtils.ts index 218f2cdb971..88233032ccd 100644 --- a/src/test/mystery-encounter/encounterTestUtils.ts +++ b/src/test/mystery-encounter/encounterTestUtils.ts @@ -7,6 +7,12 @@ import GameManager from "../utils/gameManager"; import MessageUiHandler from "#app/ui/message-ui-handler"; import { Status, StatusEffect } from "#app/data/status-effect"; +/** + * Runs a MysteryEncounter to either the start of a battle, or to the MysteryEncounterRewardsPhase, depending on the option selected + * @param game + * @param optionNo - human number, not index + * @param isBattle - if selecting option should lead to battle, set to true + */ export async function runSelectMysteryEncounterOption(game: GameManager, optionNo: number, isBattle: boolean = false) { // Handle any eventual queued messages (e.g. weather phase, etc.) game.onNextPrompt("MessagePhase", Mode.MESSAGE, () => { @@ -81,6 +87,10 @@ export async function runSelectMysteryEncounterOption(game: GameManager, optionN } } +/** + * For any MysteryEncounter that has a battle, can call this to skip battle and proceed to MysteryEncounterRewardsPhase + * @param game + */ export async function skipBattleRunMysteryEncounterRewardsPhase(game: GameManager) { game.scene.clearPhaseQueue(); game.scene.clearPhaseQueueSplice(); diff --git a/src/test/mystery-encounter/encounters/fiery-fallout-encounter.test.ts b/src/test/mystery-encounter/encounters/fiery-fallout-encounter.test.ts index 9bcb3a36d2c..20d0426e02d 100644 --- a/src/test/mystery-encounter/encounters/fiery-fallout-encounter.test.ts +++ b/src/test/mystery-encounter/encounters/fiery-fallout-encounter.test.ts @@ -74,7 +74,7 @@ describe("Fiery Fallout - Mystery Encounter", () => { game.override.startingBiome(Biome.MOUNTAIN); await game.runToMysteryEncounter(); - expect(scene.currentBattle.mysteryEncounter.encounterType).not.toBe(MysteryEncounterType.LOST_AT_SEA); + expect(scene.currentBattle.mysteryEncounter.encounterType).not.toBe(MysteryEncounterType.FIERY_FALLOUT); }); it("should not run below wave 41", async () => { diff --git a/src/test/utils/TextInterceptor.ts b/src/test/utils/TextInterceptor.ts index a49f41f6be0..c209ece7e04 100644 --- a/src/test/utils/TextInterceptor.ts +++ b/src/test/utils/TextInterceptor.ts @@ -1,3 +1,6 @@ +/** + * Class will intercept any text or dialogue message calls and log them for test purposes + */ export default class TextInterceptor { private scene; public logs = [];