diff --git a/public/audio/bgm/mystery_encounter_fun_and_games.mp3 b/public/audio/bgm/mystery_encounter_fun_and_games.mp3 new file mode 100644 index 00000000000..a9660d75e90 Binary files /dev/null and b/public/audio/bgm/mystery_encounter_fun_and_games.mp3 differ diff --git a/public/images/mystery-encounters/carnival_game.json b/public/images/mystery-encounters/carnival_game.json new file mode 100644 index 00000000000..0572b95990c --- /dev/null +++ b/public/images/mystery-encounters/carnival_game.json @@ -0,0 +1,41 @@ +{ + "textures": [ + { + "image": "carnival_game.png", + "format": "RGBA8888", + "size": { + "w": 38, + "h": 82 + }, + "scale": 1, + "frames": [ + { + "filename": "0001.png", + "rotated": false, + "trimmed": false, + "sourceSize": { + "w": 38, + "h": 82 + }, + "spriteSourceSize": { + "x": 0, + "y": 0, + "w": 38, + "h": 82 + }, + "frame": { + "x": 0, + "y": 0, + "w": 38, + "h": 82 + } + } + ] + } + ], + "meta": { + "app": "https://www.codeandweb.com/texturepacker", + "version": "3.0", + "smartupdate": "$TexturePacker:SmartUpdate:d40b6742392c2fe8ca0735b3f561e319:5dcda5410b12f0aa75eb0dd1fbcbe4f9:d171fb17d3017d1f655cd8dd14c252b7$" + } +} diff --git a/public/images/mystery-encounters/carnival_game.png b/public/images/mystery-encounters/carnival_game.png new file mode 100644 index 00000000000..03a3b9c9cbc Binary files /dev/null and b/public/images/mystery-encounters/carnival_game.png differ diff --git a/public/images/mystery-encounters/carnival_man.json b/public/images/mystery-encounters/carnival_man.json new file mode 100644 index 00000000000..3e77765bbce --- /dev/null +++ b/public/images/mystery-encounters/carnival_man.json @@ -0,0 +1,41 @@ +{ + "textures": [ + { + "image": "carnival_man.png", + "format": "RGBA8888", + "size": { + "w": 50, + "h": 77 + }, + "scale": 1, + "frames": [ + { + "filename": "0001.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 80, + "h": 80 + }, + "spriteSourceSize": { + "x": 15, + "y": 3, + "w": 50, + "h": 77 + }, + "frame": { + "x": 0, + "y": 0, + "w": 50, + "h": 77 + } + } + ] + } + ], + "meta": { + "app": "https://www.codeandweb.com/texturepacker", + "version": "3.0", + "smartupdate": "$TexturePacker:SmartUpdate:e80aa9a809a7cca6d05992cb82f6dbd9:ea9962edd1cdc1e503deecf2ce1863c1:55647352b6547cf03212506309f2abf5$" + } +} diff --git a/public/images/mystery-encounters/carnival_man.png b/public/images/mystery-encounters/carnival_man.png new file mode 100644 index 00000000000..05f94dbd33d Binary files /dev/null and b/public/images/mystery-encounters/carnival_man.png differ diff --git a/public/images/mystery-encounters/carnival_wobbuffet.json b/public/images/mystery-encounters/carnival_wobbuffet.json new file mode 100644 index 00000000000..c059bb35a96 --- /dev/null +++ b/public/images/mystery-encounters/carnival_wobbuffet.json @@ -0,0 +1,41 @@ +{ + "textures": [ + { + "image": "carnival_wobbuffet.png", + "format": "RGBA8888", + "size": { + "w": 45, + "h": 55 + }, + "scale": 1, + "frames": [ + { + "filename": "0001.png", + "rotated": false, + "trimmed": false, + "sourceSize": { + "w": 45, + "h": 55 + }, + "spriteSourceSize": { + "x": 0, + "y": 0, + "w": 45, + "h": 55 + }, + "frame": { + "x": 0, + "y": 0, + "w": 45, + "h": 55 + } + } + ] + } + ], + "meta": { + "app": "https://www.codeandweb.com/texturepacker", + "version": "3.0", + "smartupdate": "$TexturePacker:SmartUpdate:879de17da906ea52e5a71afacb88fcf6:90f64e8eaac4ff1e67373f60c3d98d36:a090cb3294ca1218a4f90ecb97df81d7$" + } +} diff --git a/public/images/mystery-encounters/carnival_wobbuffet.png b/public/images/mystery-encounters/carnival_wobbuffet.png new file mode 100644 index 00000000000..37e7220196a Binary files /dev/null and b/public/images/mystery-encounters/carnival_wobbuffet.png differ diff --git a/src/battle-scene.ts b/src/battle-scene.ts index 8228bdb54e6..a8cffa1d9cd 100644 --- a/src/battle-scene.ts +++ b/src/battle-scene.ts @@ -254,7 +254,7 @@ export default class BattleScene extends SceneBase { public pokemonInfoContainer: PokemonInfoContainer; private party: PlayerPokemon[]; public mysteryEncounterData: MysteryEncounterData = new MysteryEncounterData(null); - public lastMysteryEncounter: MysteryEncounter; + public lastMysteryEncounter?: MysteryEncounter; /** Combined Biome and Wave count text */ private biomeWaveText: Phaser.GameObjects.Text; private moneyText: Phaser.GameObjects.Text; @@ -1213,7 +1213,7 @@ export default class BattleScene extends SceneBase { const maxExpLevel = this.getMaxExpLevel(); this.lastEnemyTrainer = lastBattle?.trainer ?? null; - this.lastMysteryEncounter = lastBattle?.mysteryEncounter ?? null; + this.lastMysteryEncounter = lastBattle?.mysteryEncounter; this.executeWithSeedOffset(() => { this.currentBattle = new Battle(this.gameMode, newWaveIndex, newBattleType, newTrainer, newDouble); diff --git a/src/battle.ts b/src/battle.ts index 035b051326e..75077a1c4cb 100644 --- a/src/battle.ts +++ b/src/battle.ts @@ -70,7 +70,7 @@ export default class Battle { public lastUsedPokeball: PokeballType | null; 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?: MysteryEncounter; private rngCounter: integer = 0; diff --git a/src/data/battle-anims.ts b/src/data/battle-anims.ts index 4f646fa9532..ebd37c33bae 100644 --- a/src/data/battle-anims.ts +++ b/src/data/battle-anims.ts @@ -891,6 +891,8 @@ export abstract class BattleAnim { const isUser = frame.target === AnimFrameTarget.USER; if (isUser && target === user) { continue; + } else if (this.playOnEmptyField && frame.target === AnimFrameTarget.TARGET) { + continue; } const sprites = spriteCache[isUser ? AnimFrameTarget.USER : AnimFrameTarget.TARGET]; const spriteSource = isUser ? userSprite : targetSprite; @@ -1223,8 +1225,8 @@ export class CommonBattleAnim extends BattleAnim { export class MoveAnim extends BattleAnim { public move: Moves; - constructor(move: Moves, user: Pokemon, target: BattlerIndex) { - super(user, user.scene.getField()[target]); + constructor(move: Moves, user: Pokemon, target: BattlerIndex, playOnEmptyField: boolean = false) { + super(user, user.scene.getField()[target], playOnEmptyField); this.move = move; } diff --git a/src/data/mystery-encounters/encounters/a-trainers-test-encounter.ts b/src/data/mystery-encounters/encounters/a-trainers-test-encounter.ts index c6de3130ad5..9ff861fc5b1 100644 --- a/src/data/mystery-encounters/encounters/a-trainers-test-encounter.ts +++ b/src/data/mystery-encounters/encounters/a-trainers-test-encounter.ts @@ -34,7 +34,7 @@ export const ATrainersTestEncounter: MysteryEncounter = ]) .withAutoHideIntroVisuals(false) .withOnInit((scene: BattleScene) => { - const encounter = scene.currentBattle.mysteryEncounter; + const encounter = scene.currentBattle.mysteryEncounter!; // Randomly pick from 1 of the 5 stat trainers to spawn let trainerType: TrainerType; @@ -135,7 +135,7 @@ export const ATrainersTestEncounter: MysteryEncounter = buttonTooltip: `${namespace}.option.1.tooltip` }, async (scene: BattleScene) => { - const encounter = scene.currentBattle.mysteryEncounter; + const encounter = scene.currentBattle.mysteryEncounter!; // Spawn standard trainer battle with memory mushroom reward const config: EnemyPartyConfig = encounter.enemyPartyConfigs[0]; @@ -160,7 +160,7 @@ export const ATrainersTestEncounter: MysteryEncounter = buttonTooltip: `${namespace}.option.2.tooltip` }, async (scene: BattleScene) => { - const encounter = scene.currentBattle.mysteryEncounter; + const encounter = scene.currentBattle.mysteryEncounter!; // Full heal party scene.unshiftPhase(new PartyHealPhase(scene, true)); diff --git a/src/data/mystery-encounters/encounters/absolute-avarice-encounter.ts b/src/data/mystery-encounters/encounters/absolute-avarice-encounter.ts index dc554c440f7..29873486399 100644 --- a/src/data/mystery-encounters/encounters/absolute-avarice-encounter.ts +++ b/src/data/mystery-encounters/encounters/absolute-avarice-encounter.ts @@ -173,7 +173,7 @@ export const AbsoluteAvariceEncounter: MysteryEncounter = .withDescription(`${namespace}.description`) .withQuery(`${namespace}.query`) .withOnInit((scene: BattleScene) => { - const encounter = scene.currentBattle.mysteryEncounter; + const encounter = scene.currentBattle.mysteryEncounter!; scene.loadSe("PRSFX- Bug Bite", "battle_anims"); scene.loadSe("Follow Me", "battle_anims", "Follow Me.mp3"); @@ -242,7 +242,7 @@ export const AbsoluteAvariceEncounter: MysteryEncounter = }) .withOptionPhase(async (scene: BattleScene) => { // Pick battle - const encounter = scene.currentBattle.mysteryEncounter; + const encounter = scene.currentBattle.mysteryEncounter!; // Provides 1x Reviver Seed to each party member at end of battle const revSeed = generateModifierType(scene, modifierTypes.REVIVER_SEED); @@ -283,7 +283,7 @@ export const AbsoluteAvariceEncounter: MysteryEncounter = ], }) .withOptionPhase(async (scene: BattleScene) => { - const encounter = scene.currentBattle.mysteryEncounter; + const encounter = scene.currentBattle.mysteryEncounter!; const berryMap = encounter.misc.berryItemsMap; // Returns 2/5 of the berries stolen from each Pokemon @@ -349,7 +349,7 @@ function doGreedentSpriteSteal(scene: BattleScene) { const shakeDelay = 50; const slideDelay = 500; - const greedentSprites = scene.currentBattle.mysteryEncounter.introVisuals?.getSpriteAtIndex(1); + const greedentSprites = scene.currentBattle.mysteryEncounter!.introVisuals?.getSpriteAtIndex(1); scene.playSound("battle-anims/Follow Me"); scene.tweens.chain({ @@ -423,7 +423,7 @@ function doGreedentSpriteSteal(scene: BattleScene) { } function doGreedentEatBerries(scene: BattleScene) { - const greedentSprites = scene.currentBattle.mysteryEncounter.introVisuals?.getSpriteAtIndex(1); + const greedentSprites = scene.currentBattle.mysteryEncounter!.introVisuals?.getSpriteAtIndex(1); let index = 1; scene.tweens.add({ targets: greedentSprites, @@ -455,7 +455,7 @@ function doBerrySpritePile(scene: BattleScene, isEat: boolean = false) { if (isEat) { animationOrder = animationOrder.reverse(); } - const encounter = scene.currentBattle.mysteryEncounter; + const encounter = scene.currentBattle.mysteryEncounter!; animationOrder.forEach((berry, i) => { const introVisualsIndex = encounter.spriteConfigs.findIndex(config => config.spriteKey?.includes(berry)); let sprite: Phaser.GameObjects.Sprite, tintSprite: Phaser.GameObjects.Sprite; diff --git a/src/data/mystery-encounters/encounters/an-offer-you-cant-refuse-encounter.ts b/src/data/mystery-encounters/encounters/an-offer-you-cant-refuse-encounter.ts index 94c004b2617..2166913e892 100644 --- a/src/data/mystery-encounters/encounters/an-offer-you-cant-refuse-encounter.ts +++ b/src/data/mystery-encounters/encounters/an-offer-you-cant-refuse-encounter.ts @@ -58,7 +58,7 @@ export const AnOfferYouCantRefuseEncounter: MysteryEncounter = .withDescription(`${namespace}.description`) .withQuery(`${namespace}.query`) .withOnInit((scene: BattleScene) => { - const encounter = scene.currentBattle.mysteryEncounter; + const encounter = scene.currentBattle.mysteryEncounter!; const pokemon = getHighestStatTotalPlayerPokemon(scene, false); const price = scene.getWaveMoneyAmount(10); @@ -99,7 +99,7 @@ export const AnOfferYouCantRefuseEncounter: MysteryEncounter = ], }) .withPreOptionPhase(async (scene: BattleScene): Promise => { - const encounter = scene.currentBattle.mysteryEncounter; + const encounter = scene.currentBattle.mysteryEncounter!; // Update money and remove pokemon from party updatePlayerMoney(scene, encounter.misc.price); scene.removePokemonFromPlayerParty(encounter.misc.pokemon); @@ -132,7 +132,7 @@ export const AnOfferYouCantRefuseEncounter: MysteryEncounter = }) .withOptionPhase(async (scene: BattleScene) => { // Extort the rich kid for money - const encounter = scene.currentBattle.mysteryEncounter; + const encounter = scene.currentBattle.mysteryEncounter!; // Update money and remove pokemon from party updatePlayerMoney(scene, encounter.misc.price); diff --git a/src/data/mystery-encounters/encounters/berries-abound-encounter.ts b/src/data/mystery-encounters/encounters/berries-abound-encounter.ts index 68fd7b90f9c..18a77b63e34 100644 --- a/src/data/mystery-encounters/encounters/berries-abound-encounter.ts +++ b/src/data/mystery-encounters/encounters/berries-abound-encounter.ts @@ -53,7 +53,7 @@ export const BerriesAboundEncounter: MysteryEncounter = }, ]) .withOnInit((scene: BattleScene) => { - const encounter = scene.currentBattle.mysteryEncounter; + const encounter = scene.currentBattle.mysteryEncounter!; // Calculate boss mon const level = (scene.currentBattle.enemyLevels?.[0] ?? scene.currentBattle.waveIndex) + Math.max(Math.round((scene.currentBattle.waveIndex / 10)), 0); @@ -125,7 +125,7 @@ export const BerriesAboundEncounter: MysteryEncounter = }, async (scene: BattleScene) => { // Pick battle - const encounter = scene.currentBattle.mysteryEncounter; + const encounter = scene.currentBattle.mysteryEncounter!; const numBerries = encounter.misc.numBerries; const doBerryRewards = async () => { @@ -150,7 +150,7 @@ export const BerriesAboundEncounter: MysteryEncounter = } setEncounterRewards(scene, { guaranteedModifierTypeOptions: shopOptions, fillRemaining: false }, undefined, doBerryRewards); - await initBattleWithEnemyConfig(scene, scene.currentBattle.mysteryEncounter.enemyPartyConfigs[0]); + await initBattleWithEnemyConfig(scene, scene.currentBattle.mysteryEncounter!.enemyPartyConfigs[0]); } ) .withOption( @@ -162,7 +162,7 @@ export const BerriesAboundEncounter: MysteryEncounter = }) .withOptionPhase(async (scene: BattleScene) => { // Pick race for berries - const encounter = scene.currentBattle.mysteryEncounter; + const encounter = scene.currentBattle.mysteryEncounter!; const fastestPokemon = encounter.misc.fastestPokemon; const enemySpeed = encounter.misc.enemySpeed; const speedDiff = fastestPokemon.getStat(Stat.SPD) / (enemySpeed * 1.1); @@ -191,7 +191,7 @@ export const BerriesAboundEncounter: MysteryEncounter = } }; - const config = scene.currentBattle.mysteryEncounter.enemyPartyConfigs[0]; + const config = scene.currentBattle.mysteryEncounter!.enemyPartyConfigs[0]; config.pokemonConfigs![0].tags = [BattlerTagType.MYSTERY_ENCOUNTER_POST_SUMMON]; config.pokemonConfigs![0].mysteryEncounterBattleEffects = (pokemon: Pokemon) => { queueEncounterMessage(pokemon.scene, `${namespace}.option.2.boss_enraged`); diff --git a/src/data/mystery-encounters/encounters/bug-type-superfan-encounter.ts b/src/data/mystery-encounters/encounters/bug-type-superfan-encounter.ts index 227e128fe20..51fdfbc2f4d 100644 --- a/src/data/mystery-encounters/encounters/bug-type-superfan-encounter.ts +++ b/src/data/mystery-encounters/encounters/bug-type-superfan-encounter.ts @@ -204,7 +204,7 @@ export const BugTypeSuperfanEncounter: MysteryEncounter = }, ]) .withOnInit((scene: BattleScene) => { - const encounter = scene.currentBattle.mysteryEncounter; + const encounter = scene.currentBattle.mysteryEncounter!; // Calculates what trainers are available for battle in the encounter // Bug type superfan trainer config @@ -241,7 +241,7 @@ export const BugTypeSuperfanEncounter: MysteryEncounter = }, async (scene: BattleScene) => { // Select battle the bug trainer - const encounter = scene.currentBattle.mysteryEncounter; + const encounter = scene.currentBattle.mysteryEncounter!; const config: EnemyPartyConfig = encounter.enemyPartyConfigs[0]; // Init the moves available for tutor @@ -272,7 +272,7 @@ export const BugTypeSuperfanEncounter: MysteryEncounter = }) .withPreOptionPhase(async (scene: BattleScene) => { // Player shows off their bug types - const encounter = scene.currentBattle.mysteryEncounter; + const encounter = scene.currentBattle.mysteryEncounter!; // Player gets different rewards depending on the number of bug types they have const numBugTypes = scene.getParty().filter(p => p.isOfType(Type.BUG, true)).length; @@ -360,7 +360,7 @@ export const BugTypeSuperfanEncounter: MysteryEncounter = secondOptionPrompt: `${namespace}.option.3.select_prompt`, }) .withPreOptionPhase(async (scene: BattleScene): Promise => { - const encounter = scene.currentBattle.mysteryEncounter; + const encounter = scene.currentBattle.mysteryEncounter!; const onPokemonSelected = (pokemon: PlayerPokemon) => { // Get Pokemon held items and filter for valid ones const validItems = pokemon.getHeldItems().filter(item => { @@ -403,7 +403,7 @@ export const BugTypeSuperfanEncounter: MysteryEncounter = return selectPokemonForOption(scene, onPokemonSelected, undefined, selectableFilter); }) .withOptionPhase(async (scene: BattleScene) => { - const encounter = scene.currentBattle.mysteryEncounter; + const encounter = scene.currentBattle.mysteryEncounter!; const modifier = encounter.misc.chosenModifier; // Remove the modifier if its stacks go to 0 @@ -583,7 +583,7 @@ function getRandomPartyMemberFunc(speciesPool: Species[], trainerSlot: TrainerSl function doBugTypeMoveTutor(scene: BattleScene): Promise { return new Promise(async resolve => { - const moveOptions = scene.currentBattle.mysteryEncounter.misc.moveTutorOptions; + const moveOptions = scene.currentBattle.mysteryEncounter!.misc.moveTutorOptions; await showEncounterDialogue(scene, `${namespace}.battle_won`, `${namespace}.speaker`); const overlayScale = 1; diff --git a/src/data/mystery-encounters/encounters/clowning-around-encounter.ts b/src/data/mystery-encounters/encounters/clowning-around-encounter.ts index 4ea978c8c4c..e57c234a8cc 100644 --- a/src/data/mystery-encounters/encounters/clowning-around-encounter.ts +++ b/src/data/mystery-encounters/encounters/clowning-around-encounter.ts @@ -102,7 +102,7 @@ export const ClowningAroundEncounter: MysteryEncounter = }, ]) .withOnInit((scene: BattleScene) => { - const encounter = scene.currentBattle.mysteryEncounter; + const encounter = scene.currentBattle.mysteryEncounter!; const clownTrainerType = TrainerType.HARLEQUIN; const clownConfig = trainerConfigs[clownTrainerType].copy(); @@ -159,7 +159,7 @@ export const ClowningAroundEncounter: MysteryEncounter = ], }) .withOptionPhase(async (scene: BattleScene) => { - const encounter = scene.currentBattle.mysteryEncounter; + const encounter = scene.currentBattle.mysteryEncounter!; // Spawn battle const config: EnemyPartyConfig = encounter.enemyPartyConfigs[0]; @@ -236,7 +236,7 @@ export const ClowningAroundEncounter: MysteryEncounter = // Swap player's items on pokemon with the most items // Item comparisons look at whichever Pokemon has the greatest number of TRANSFERABLE, non-berry items // So Vitamins, form change items, etc. are not included - const encounter = scene.currentBattle.mysteryEncounter; + const encounter = scene.currentBattle.mysteryEncounter!; const party = scene.getParty(); let mostHeldItemsPokemon = party[0]; @@ -418,8 +418,8 @@ function onYesAbilitySwap(scene: BattleScene, resolve) { if (!pokemon.mysteryEncounterData) { pokemon.mysteryEncounterData = new MysteryEncounterPokemonData(undefined, Abilities.AERILATE); } - pokemon.mysteryEncounterData.ability = scene.currentBattle.mysteryEncounter.misc.ability; - scene.currentBattle.mysteryEncounter.setDialogueToken("chosenPokemon", pokemon.getNameToRender()); + pokemon.mysteryEncounterData.ability = scene.currentBattle.mysteryEncounter!.misc.ability; + scene.currentBattle.mysteryEncounter!.setDialogueToken("chosenPokemon", pokemon.getNameToRender()); scene.ui.setMode(Mode.MESSAGE).then(() => resolve(true)); }; diff --git a/src/data/mystery-encounters/encounters/dancing-lessons-encounter.ts b/src/data/mystery-encounters/encounters/dancing-lessons-encounter.ts index 5b6cbfa9866..3d89e4531d8 100644 --- a/src/data/mystery-encounters/encounters/dancing-lessons-encounter.ts +++ b/src/data/mystery-encounters/encounters/dancing-lessons-encounter.ts @@ -102,7 +102,7 @@ export const DancingLessonsEncounter: MysteryEncounter = .withDescription(`${namespace}.description`) .withQuery(`${namespace}.query`) .withOnInit((scene: BattleScene) => { - const encounter = scene.currentBattle.mysteryEncounter; + const encounter = scene.currentBattle.mysteryEncounter!; const species = getPokemonSpecies(Species.ORICORIO); const enemyPokemon = scene.addEnemyPokemon(species, scene.currentBattle.enemyLevels![0], TrainerSlot.NONE, false); @@ -170,7 +170,7 @@ export const DancingLessonsEncounter: MysteryEncounter = }) .withOptionPhase(async (scene: BattleScene) => { // Pick battle - const encounter = scene.currentBattle.mysteryEncounter; + const encounter = scene.currentBattle.mysteryEncounter!; transitionMysteryEncounterIntroVisuals(scene, true, true, 500); @@ -200,7 +200,7 @@ export const DancingLessonsEncounter: MysteryEncounter = }) .withPreOptionPhase(async (scene: BattleScene) => { // Learn its Dance - const encounter = scene.currentBattle.mysteryEncounter; + const encounter = scene.currentBattle.mysteryEncounter!; const onPokemonSelected = (pokemon: PlayerPokemon) => { encounter.setDialogueToken("selectedPokemon", pokemon.getNameToRender()); @@ -236,7 +236,7 @@ export const DancingLessonsEncounter: MysteryEncounter = }) .withPreOptionPhase(async (scene: BattleScene) => { // Open menu for selecting pokemon with a Dancing move - const encounter = scene.currentBattle.mysteryEncounter; + const encounter = scene.currentBattle.mysteryEncounter!; const onPokemonSelected = (pokemon: PlayerPokemon) => { // Return the options for nature selection return pokemon.moveset @@ -272,7 +272,7 @@ export const DancingLessonsEncounter: MysteryEncounter = }) .withOptionPhase(async (scene: BattleScene) => { // Show the Oricorio a dance, and recruit it - const encounter = scene.currentBattle.mysteryEncounter; + const encounter = scene.currentBattle.mysteryEncounter!; const oricorio = encounter.misc.oricorioData.toPokemon(scene); oricorio.passive = true; diff --git a/src/data/mystery-encounters/encounters/dark-deal-encounter.ts b/src/data/mystery-encounters/encounters/dark-deal-encounter.ts index b8e3ba81101..eaa3c94d129 100644 --- a/src/data/mystery-encounters/encounters/dark-deal-encounter.ts +++ b/src/data/mystery-encounters/encounters/dark-deal-encounter.ts @@ -127,14 +127,15 @@ export const DarkDealEncounter: MysteryEncounter = const removedPokemon = getRandomPlayerPokemon(scene, false, true); scene.removePokemonFromPlayerParty(removedPokemon); - scene.currentBattle.mysteryEncounter.setDialogueToken("pokeName", removedPokemon.getNameToRender()); + const encounter = scene.currentBattle.mysteryEncounter!; + encounter.setDialogueToken("pokeName", removedPokemon.getNameToRender()); // Store removed pokemon types - scene.currentBattle.mysteryEncounter.misc = [ + encounter.misc = [ removedPokemon.species.type1, ]; if (removedPokemon.species.type2) { - scene.currentBattle.mysteryEncounter.misc.push(removedPokemon.species.type2); + encounter.misc.push(removedPokemon.species.type2); } }) .withOptionPhase(async (scene: BattleScene) => { @@ -142,7 +143,7 @@ export const DarkDealEncounter: MysteryEncounter = scene.unshiftPhase(new ModifierRewardPhase(scene, modifierTypes.ROGUE_BALL)); // Start encounter with random legendary (7-10 starter strength) that has level additive - const bossTypes = scene.currentBattle.mysteryEncounter.misc as Type[]; + 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] = diff --git a/src/data/mystery-encounters/encounters/delibirdy-encounter.ts b/src/data/mystery-encounters/encounters/delibirdy-encounter.ts index ecb8cbeb925..f0faa5cefda 100644 --- a/src/data/mystery-encounters/encounters/delibirdy-encounter.ts +++ b/src/data/mystery-encounters/encounters/delibirdy-encounter.ts @@ -102,7 +102,7 @@ export const DelibirdyEncounter: MysteryEncounter = ], }) .withPreOptionPhase(async (scene: BattleScene): Promise => { - const encounter = scene.currentBattle.mysteryEncounter; + const encounter = scene.currentBattle.mysteryEncounter!; updatePlayerMoney(scene, -(encounter.options[0].requirements[0] as MoneyRequirement).requiredMoney, true, false); return true; }) @@ -140,7 +140,7 @@ export const DelibirdyEncounter: MysteryEncounter = ], }) .withPreOptionPhase(async (scene: BattleScene): Promise => { - const encounter = scene.currentBattle.mysteryEncounter; + const encounter = scene.currentBattle.mysteryEncounter!; const onPokemonSelected = (pokemon: PlayerPokemon) => { // Get Pokemon held items and filter for valid ones const validItems = pokemon.getHeldItems().filter((it) => { @@ -178,7 +178,7 @@ export const DelibirdyEncounter: MysteryEncounter = return selectPokemonForOption(scene, onPokemonSelected, undefined, selectableFilter); }) .withOptionPhase(async (scene: BattleScene) => { - const encounter = scene.currentBattle.mysteryEncounter; + const encounter = scene.currentBattle.mysteryEncounter!; const modifier = encounter.misc.chosenModifier; // Give the player a Candy Jar if they gave a Berry, and a Healing Charm for Reviver Seed @@ -235,7 +235,7 @@ export const DelibirdyEncounter: MysteryEncounter = ], }) .withPreOptionPhase(async (scene: BattleScene): Promise => { - const encounter = scene.currentBattle.mysteryEncounter; + const encounter = scene.currentBattle.mysteryEncounter!; const onPokemonSelected = (pokemon: PlayerPokemon) => { // Get Pokemon held items and filter for valid ones const validItems = pokemon.getHeldItems().filter((it) => { @@ -273,7 +273,7 @@ export const DelibirdyEncounter: MysteryEncounter = return selectPokemonForOption(scene, onPokemonSelected, undefined, selectableFilter); }) .withOptionPhase(async (scene: BattleScene) => { - const encounter = scene.currentBattle.mysteryEncounter; + const encounter = scene.currentBattle.mysteryEncounter!; const modifier = encounter.misc.chosenModifier; // Check if the player has max stacks of Berry Pouch already diff --git a/src/data/mystery-encounters/encounters/field-trip-encounter.ts b/src/data/mystery-encounters/encounters/field-trip-encounter.ts index cef0a9cd3fd..c1517948e0c 100644 --- a/src/data/mystery-encounters/encounters/field-trip-encounter.ts +++ b/src/data/mystery-encounters/encounters/field-trip-encounter.ts @@ -67,7 +67,7 @@ export const FieldTripEncounter: MysteryEncounter = ], }) .withPreOptionPhase(async (scene: BattleScene): Promise => { - const encounter = scene.currentBattle.mysteryEncounter; + const encounter = scene.currentBattle.mysteryEncounter!; const onPokemonSelected = (pokemon: PlayerPokemon) => { // Return the options for Pokemon move valid for this option return pokemon.moveset.map((move: PokemonMove) => { @@ -123,7 +123,7 @@ export const FieldTripEncounter: MysteryEncounter = return selectPokemonForOption(scene, onPokemonSelected); }) .withOptionPhase(async (scene: BattleScene) => { - const encounter = scene.currentBattle.mysteryEncounter; + const encounter = scene.currentBattle.mysteryEncounter!; if (encounter.misc.correctMove) { const modifiers = [ generateModifierTypeOption(scene, modifierTypes.TEMP_STAT_BOOSTER, [TempBattleStat.ATK])!, @@ -153,7 +153,7 @@ export const FieldTripEncounter: MysteryEncounter = ], }) .withPreOptionPhase(async (scene: BattleScene): Promise => { - const encounter = scene.currentBattle.mysteryEncounter; + const encounter = scene.currentBattle.mysteryEncounter!; const onPokemonSelected = (pokemon: PlayerPokemon) => { // Return the options for Pokemon move valid for this option return pokemon.moveset.map((move: PokemonMove) => { @@ -215,7 +215,7 @@ export const FieldTripEncounter: MysteryEncounter = return selectPokemonForOption(scene, onPokemonSelected); }) .withOptionPhase(async (scene: BattleScene) => { - const encounter = scene.currentBattle.mysteryEncounter; + const encounter = scene.currentBattle.mysteryEncounter!; if (encounter.misc.correctMove) { const modifiers = [ generateModifierTypeOption(scene, modifierTypes.TEMP_STAT_BOOSTER, [TempBattleStat.SPATK])!, @@ -245,7 +245,7 @@ export const FieldTripEncounter: MysteryEncounter = ], }) .withPreOptionPhase(async (scene: BattleScene): Promise => { - const encounter = scene.currentBattle.mysteryEncounter; + const encounter = scene.currentBattle.mysteryEncounter!; const onPokemonSelected = (pokemon: PlayerPokemon) => { // Return the options for Pokemon move valid for this option return pokemon.moveset.map((move: PokemonMove) => { @@ -301,7 +301,7 @@ export const FieldTripEncounter: MysteryEncounter = return selectPokemonForOption(scene, onPokemonSelected); }) .withOptionPhase(async (scene: BattleScene) => { - const encounter = scene.currentBattle.mysteryEncounter; + const encounter = scene.currentBattle.mysteryEncounter!; if (encounter.misc.correctMove) { const modifiers = [ generateModifierTypeOption(scene, modifierTypes.TEMP_STAT_BOOSTER, [TempBattleStat.ACC])!, diff --git a/src/data/mystery-encounters/encounters/fiery-fallout-encounter.ts b/src/data/mystery-encounters/encounters/fiery-fallout-encounter.ts index 346158ec6d5..93b1612e1ba 100644 --- a/src/data/mystery-encounters/encounters/fiery-fallout-encounter.ts +++ b/src/data/mystery-encounters/encounters/fiery-fallout-encounter.ts @@ -50,7 +50,7 @@ export const FieryFalloutEncounter: MysteryEncounter = }, ]) .withOnInit((scene: BattleScene) => { - const encounter = scene.currentBattle.mysteryEncounter; + const encounter = scene.currentBattle.mysteryEncounter!; // Calculate boss mons const volcaronaSpecies = getPokemonSpecies(Species.VOLCARONA); @@ -132,7 +132,7 @@ export const FieryFalloutEncounter: MysteryEncounter = }, async (scene: BattleScene) => { // Pick battle - const encounter = scene.currentBattle.mysteryEncounter; + const encounter = scene.currentBattle.mysteryEncounter!; setEncounterRewards(scene, { fillRemaining: true }, undefined, () => giveLeadPokemonCharcoal(scene)); encounter.startOfBattleEffects.push( @@ -160,7 +160,7 @@ export const FieryFalloutEncounter: MysteryEncounter = move: new PokemonMove(Moves.QUIVER_DANCE), ignorePp: true }); - await initBattleWithEnemyConfig(scene, scene.currentBattle.mysteryEncounter.enemyPartyConfigs[0]); + await initBattleWithEnemyConfig(scene, scene.currentBattle.mysteryEncounter!.enemyPartyConfigs[0]); } ) .withSimpleOption( @@ -175,7 +175,7 @@ export const FieryFalloutEncounter: MysteryEncounter = }, async (scene: BattleScene) => { // Damage non-fire types and burn 1 random non-fire type member - const encounter = scene.currentBattle.mysteryEncounter; + const encounter = scene.currentBattle.mysteryEncounter!; const nonFireTypes = scene.getParty().filter((p) => p.isAllowedInBattle() && !p.getTypes().includes(Type.FIRE)); for (const pkm of nonFireTypes) { @@ -220,7 +220,7 @@ export const FieryFalloutEncounter: MysteryEncounter = }) .withOptionPhase(async (scene: BattleScene) => { // Fire types help calm the Volcarona - const encounter = scene.currentBattle.mysteryEncounter; + const encounter = scene.currentBattle.mysteryEncounter!; transitionMysteryEncounterIntroVisuals(scene); setEncounterRewards(scene, { fillRemaining: true }, @@ -245,7 +245,7 @@ function giveLeadPokemonCharcoal(scene: BattleScene) { if (leadPokemon) { const charcoal = generateModifierType(scene, modifierTypes.ATTACK_TYPE_BOOSTER, [Type.FIRE]) as AttackTypeBoosterModifierType; applyModifierTypeToPlayerPokemon(scene, leadPokemon, charcoal); - scene.currentBattle.mysteryEncounter.setDialogueToken("leadPokemon", leadPokemon.getNameToRender()); + scene.currentBattle.mysteryEncounter!.setDialogueToken("leadPokemon", leadPokemon.getNameToRender()); queueEncounterMessage(scene, `${namespace}.found_charcoal`); } } diff --git a/src/data/mystery-encounters/encounters/fight-or-flight-encounter.ts b/src/data/mystery-encounters/encounters/fight-or-flight-encounter.ts index 304f81846b1..f754ef33944 100644 --- a/src/data/mystery-encounters/encounters/fight-or-flight-encounter.ts +++ b/src/data/mystery-encounters/encounters/fight-or-flight-encounter.ts @@ -46,7 +46,7 @@ export const FightOrFlightEncounter: MysteryEncounter = }, ]) .withOnInit((scene: BattleScene) => { - const encounter = scene.currentBattle.mysteryEncounter; + const encounter = scene.currentBattle.mysteryEncounter!; // Calculate boss mon const level = (scene.currentBattle.enemyLevels?.[0] ?? scene.currentBattle.waveIndex) + Math.max(Math.round((scene.currentBattle.waveIndex / 10)), 0); @@ -122,10 +122,9 @@ export const FightOrFlightEncounter: MysteryEncounter = }, async (scene: BattleScene) => { // Pick battle - const item = scene.currentBattle.mysteryEncounter - .misc as ModifierTypeOption; + const item = scene.currentBattle.mysteryEncounter!.misc as ModifierTypeOption; setEncounterRewards(scene, { guaranteedModifierTypeOptions: [item], fillRemaining: false }); - await initBattleWithEnemyConfig(scene, scene.currentBattle.mysteryEncounter.enemyPartyConfigs[0]); + await initBattleWithEnemyConfig(scene, scene.currentBattle.mysteryEncounter!.enemyPartyConfigs[0]); } ) .withOption( @@ -144,8 +143,8 @@ export const FightOrFlightEncounter: MysteryEncounter = }) .withOptionPhase(async (scene: BattleScene) => { // Pick steal - const encounter = scene.currentBattle.mysteryEncounter; - const item = scene.currentBattle.mysteryEncounter.misc as ModifierTypeOption; + const encounter = scene.currentBattle.mysteryEncounter!; + const item = scene.currentBattle.mysteryEncounter!.misc as ModifierTypeOption; setEncounterRewards(scene, { guaranteedModifierTypeOptions: [item], fillRemaining: false }); // Use primaryPokemon to execute the thievery diff --git a/src/data/mystery-encounters/encounters/fun-and-games-encounter.ts b/src/data/mystery-encounters/encounters/fun-and-games-encounter.ts new file mode 100644 index 00000000000..b80aef983c8 --- /dev/null +++ b/src/data/mystery-encounters/encounters/fun-and-games-encounter.ts @@ -0,0 +1,422 @@ +import { leaveEncounterWithoutBattle, selectPokemonForOption, setEncounterRewards, transitionMysteryEncounterIntroVisuals, updatePlayerMoney, } from "#app/data/mystery-encounters/utils/encounter-phase-utils"; +import { MysteryEncounterType } from "#enums/mystery-encounter-type"; +import BattleScene from "#app/battle-scene"; +import MysteryEncounter, { MysteryEncounterBuilder } from "../mystery-encounter"; +import { MysteryEncounterOptionBuilder } from "#app/data/mystery-encounters/mystery-encounter-option"; +import { TrainerSlot } from "#app/data/trainer-config"; +import Pokemon, { FieldPosition, PlayerPokemon } from "#app/field/pokemon"; +import { getPokemonSpecies } from "#app/data/pokemon-species"; +import { MoneyRequirement } from "#app/data/mystery-encounters/mystery-encounter-requirements"; +import { getEncounterText, queueEncounterMessage, showEncounterText } from "#app/data/mystery-encounters/utils/encounter-dialogue-utils"; +import { MysteryEncounterTier } from "#enums/mystery-encounter-tier"; +import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode"; +import { Species } from "#enums/species"; +import i18next from "i18next"; +import { getPokemonNameWithAffix } from "#app/messages"; +import { PlayerGender } from "#enums/player-gender"; +import { getPokeballAtlasKey, getPokeballTintColor } from "#app/data/pokeball"; +import { addPokeballOpenParticles } from "#app/field/anims"; +import { ShinySparklePhase } from "#app/phases/shiny-sparkle-phase"; +import { SpeciesFormChangeActiveTrigger } from "#app/data/pokemon-forms"; +import { PostSummonPhase } from "#app/phases/post-summon-phase"; +import { modifierTypes } from "#app/modifier/modifier-type"; +import { Nature } from "#enums/nature"; + +/** the i18n namespace for the encounter */ +const namespace = "mysteryEncounter:funAndGames"; + +/** + * Fun and Games! encounter. + * @see {@link https://github.com/pagefaultgames/pokerogue/issues/3819 | GitHub Issue #3819} + * @see For biome requirements check {@linkcode mysteryEncountersByBiome} + */ +export const FunAndGamesEncounter: MysteryEncounter = + MysteryEncounterBuilder.withEncounterType(MysteryEncounterType.FUN_AND_GAMES) + .withEncounterTier(MysteryEncounterTier.GREAT) + .withSceneWaveRangeRequirement(10, 180) + .withSceneRequirement(new MoneyRequirement(0, 1.5)) // Cost equal to 1 Max Potion to play + .withAutoHideIntroVisuals(false) + // Allows using move without a visible enemy pokemon + .withBattleAnimationsWithoutTargets(true) + // The Wobbuffet won't use moves + .withSkipEnemyBattleTurns(true) + // Will skip COMMAND selection menu and go straight to FIGHT (move select) menu + .withSkipToFightInput(true) + .withIntroSpriteConfigs([ + { + spriteKey: "carnival_game", + fileRoot: "mystery-encounters", + hasShadow: false, + x: 0, + y: 6, + }, + { + spriteKey: "carnival_wobbuffet", + fileRoot: "mystery-encounters", + hasShadow: true, + x: -28, + y: 6, + yShadow: 6 + }, + { + spriteKey: "carnival_man", + fileRoot: "mystery-encounters", + hasShadow: true, + x: 40, + y: 6, + yShadow: 6 + }, + ]) + .withIntroDialogue([ + { + speaker: `${namespace}.speaker`, + text: `${namespace}.intro_dialogue`, + }, + ]) + .withTitle(`${namespace}.title`) + .withDescription(`${namespace}.description`) + .withQuery(`${namespace}.query`) + .withOnInit((scene: BattleScene) => { + scene.loadBgm("mystery_encounter_fun_and_games", "mystery_encounter_fun_and_games.mp3"); + return true; + }) + .withOnVisualsStart((scene: BattleScene) => { + // Change the bgm + scene.fadeOutBgm(2000, false); + scene.time.delayedCall(2000, () => { + scene.playBgm("mystery_encounter_fun_and_games"); + }); + + return true; + }) + .withOption(MysteryEncounterOptionBuilder + .newOptionWithMode(MysteryEncounterOptionMode.DISABLED_OR_DEFAULT) + .withSceneRequirement(new MoneyRequirement(0, 1.5)) // Cost equal to 1 Max Potion + .withDialogue({ + buttonLabel: `${namespace}.option.1.label`, + buttonTooltip: `${namespace}.option.1.tooltip`, + selected: [ + { + text: `${namespace}.option.1.selected`, + }, + ], + }) + .withPreOptionPhase(async (scene: BattleScene) => { + // Select Pokemon for minigame + const encounter = scene.currentBattle.mysteryEncounter!; + const onPokemonSelected = (pokemon: PlayerPokemon) => { + encounter.misc = { + playerPokemon: pokemon, + }; + }; + + // Only Pokemon that are not KOed/legal can be selected + const selectableFilter = (pokemon: Pokemon) => { + const meetsReqs = pokemon.isAllowedInBattle(); + if (!meetsReqs) { + return getEncounterText(scene, `${namespace}.invalid_selection`) ?? null; + } + + return null; + }; + + return selectPokemonForOption(scene, onPokemonSelected, undefined, selectableFilter); + }) + .withOptionPhase(async (scene: BattleScene) => { + // Start minigame + const encounter = scene.currentBattle.mysteryEncounter!; + encounter.misc.turnsRemaining = 3; + + // Update money + const moneyCost = (encounter.options[0].requirements[0] as MoneyRequirement).requiredMoney; + updatePlayerMoney(scene, -moneyCost, true, false); + await showEncounterText(scene, i18next.t("mysteryEncounterMessages:paid_money", { amount: moneyCost })); + + // Handlers for battle events + encounter.onTurnStart = handleNextTurn; // triggered during TurnInitPhase + encounter.doContinueEncounter = handleLoseMinigame; // triggered during MysteryEncounterRewardsPhase, post VictoryPhase if the player KOs Wobbuffet + + hideShowmanIntroSprite(scene); + await summonPlayerPokemon(scene); + await showWobbuffetHealthBar(scene); + + return true; + }) + .build() + ) + .withSimpleOption( + { + buttonLabel: `${namespace}.option.2.label`, + buttonTooltip: `${namespace}.option.2.tooltip`, + selected: [ + { + text: `${namespace}.option.2.selected`, + }, + ], + }, + async (scene: BattleScene) => { + // Leave encounter with no rewards or exp + transitionMysteryEncounterIntroVisuals(scene, true, true); + leaveEncounterWithoutBattle(scene, true); + return true; + } + ) + .build(); + +async function summonPlayerPokemon(scene: BattleScene) { + return new Promise(async resolve => { + const encounter = scene.currentBattle.mysteryEncounter!; + + const playerPokemon = encounter.misc.playerPokemon; + // Swaps the chosen Pokemon and the first player's lead Pokemon in the party + const party = scene.getParty(); + const chosenIndex = party.indexOf(playerPokemon); + if (chosenIndex !== 0) { + [party[chosenIndex], party[0]] = [party[chosenIndex], party[chosenIndex]]; + } + + // Do trainer summon animation + let playerAnimationPromise: Promise | undefined; + scene.ui.showText(i18next.t("battle:playerGo", { pokemonName: getPokemonNameWithAffix(playerPokemon) })); + scene.pbTray.hide(); + scene.trainer.setTexture(`trainer_${scene.gameData.gender === PlayerGender.FEMALE ? "f" : "m"}_back_pb`); + scene.time.delayedCall(562, () => { + scene.trainer.setFrame("2"); + scene.time.delayedCall(64, () => { + scene.trainer.setFrame("3"); + }); + }); + scene.tweens.add({ + targets: scene.trainer, + x: -36, + duration: 1000, + onComplete: () => scene.trainer.setVisible(false) + }); + scene.time.delayedCall(750, () => { + playerAnimationPromise = summonPlayerPokemonAnimation(scene, playerPokemon); + }); + + // Also loads Wobbuffet data + const enemySpecies = getPokemonSpecies(Species.WOBBUFFET); + scene.currentBattle.enemyParty = []; + const wobbuffet = scene.addEnemyPokemon(enemySpecies, encounter.misc.playerPokemon.level, TrainerSlot.NONE, false); + wobbuffet.ivs = [0, 0, 0, 0, 0, 0]; + wobbuffet.setNature(Nature.MILD); + wobbuffet.setAlpha(0); + wobbuffet.setVisible(false); + wobbuffet.calculateStats(); + scene.currentBattle.enemyParty[0] = wobbuffet; + scene.gameData.setPokemonSeen(wobbuffet, true); + await wobbuffet.loadAssets(); + const id = setInterval(checkPlayerAnimationPromise, 500); + async function checkPlayerAnimationPromise() { + if (playerAnimationPromise) { + clearInterval(id); + await playerAnimationPromise; + resolve(); + } + } + }); +} + +function handleLoseMinigame(scene: BattleScene) { + return new Promise(async resolve => { + // Check Wobbuffet is still alive + const wobbuffet = scene.getEnemyPokemon(); + if (!wobbuffet || wobbuffet.isFainted(true) || wobbuffet.hp === 0) { + // Player loses + // End the battle + if (wobbuffet) { + wobbuffet.hideInfo(); + scene.field.remove(wobbuffet); + } + transitionMysteryEncounterIntroVisuals(scene, true, true); + scene.currentBattle.enemyParty = []; + scene.currentBattle.mysteryEncounter!.doContinueEncounter = undefined; + leaveEncounterWithoutBattle(scene, true); + await showEncounterText(scene, `${namespace}.ko`); + const reviveCost = scene.getWaveMoneyAmount(1.5); + updatePlayerMoney(scene, -reviveCost, true, false); + } + + resolve(); + }); +} + +function handleNextTurn(scene: BattleScene) { + const encounter = scene.currentBattle.mysteryEncounter!; + + const wobbuffet = scene.getEnemyPokemon(); + if (!wobbuffet) { + // Should never be triggered, just handling the edge case + handleLoseMinigame(scene); + return true; + } + if (encounter.misc.turnsRemaining <= 0) { + // Check Wobbuffet's health for the actual result + const healthRatio = wobbuffet.hp / wobbuffet.getMaxHp(); + let resultMessageKey: string; + let isHealPhase = false; + if (healthRatio < 0.03) { + // Grand prize + setEncounterRewards(scene, { guaranteedModifierTypeFuncs: [modifierTypes.MULTI_LENS], fillRemaining: false }); + resultMessageKey = `${namespace}.best_result`; + } else if (healthRatio < 0.15) { + // 2nd prize + setEncounterRewards(scene, { guaranteedModifierTypeFuncs: [modifierTypes.SCOPE_LENS], fillRemaining: false }); + resultMessageKey = `${namespace}.great_result`; + } else if (healthRatio < 0.33) { + // 3rd prize + setEncounterRewards(scene, { guaranteedModifierTypeFuncs: [modifierTypes.WIDE_LENS], fillRemaining: false }); + resultMessageKey = `${namespace}.good_result`; + } else { + // No prize + isHealPhase = true; + resultMessageKey = `${namespace}.bad_result`; + } + + // End the battle + wobbuffet.hideInfo(); + scene.field.remove(wobbuffet); + scene.currentBattle.enemyParty = []; + scene.currentBattle.mysteryEncounter!.doContinueEncounter = undefined; + leaveEncounterWithoutBattle(scene, isHealPhase); + // Must end the TurnInit phase prematurely so battle phases aren't added to queue + queueEncounterMessage(scene, `${namespace}.end_game`); + queueEncounterMessage(scene, resultMessageKey); + + // Skip remainder of TurnInitPhase + return true; + } else { + if (encounter.misc.turnsRemaining < 3) { + // Display charging messages on turns that aren't the initial turn + queueEncounterMessage(scene, `${namespace}.charging_continue`); + } + queueEncounterMessage(scene, `${namespace}.turn_remaining_${encounter.misc.turnsRemaining}`); + encounter.misc.turnsRemaining--; + } + + // Don't skip remainder of TurnInitPhase + return false; +} + +async function showWobbuffetHealthBar(scene: BattleScene) { + const wobbuffet = scene.getEnemyPokemon()!; + + scene.add.existing(wobbuffet); + scene.field.add(wobbuffet); + + const playerPokemon = scene.getPlayerPokemon() as Pokemon; + if (playerPokemon?.visible) { + scene.field.moveBelow(wobbuffet, playerPokemon); + } + // Show health bar and trigger cry + wobbuffet.showInfo(); + scene.time.delayedCall(1000, () => { + wobbuffet.cry(); + }); + wobbuffet.resetSummonData(); + + // Track the HP change across turns + scene.currentBattle.mysteryEncounter!.misc.wobbuffetHealth = wobbuffet.hp; +} + +function summonPlayerPokemonAnimation(scene: BattleScene, pokemon: PlayerPokemon): Promise { + return new Promise(resolve => { + const pokeball = scene.addFieldSprite(36, 80, "pb", getPokeballAtlasKey(pokemon.pokeball)); + pokeball.setVisible(false); + pokeball.setOrigin(0.5, 0.625); + scene.field.add(pokeball); + + pokemon.setFieldPosition(FieldPosition.CENTER, 0); + + const fpOffset = pokemon.getFieldPositionOffset(); + + pokeball.setVisible(true); + + scene.tweens.add({ + targets: pokeball, + duration: 650, + x: 100 + fpOffset[0] + }); + + scene.tweens.add({ + targets: pokeball, + duration: 150, + ease: "Cubic.easeOut", + y: 70 + fpOffset[1], + onComplete: () => { + scene.tweens.add({ + targets: pokeball, + duration: 500, + ease: "Cubic.easeIn", + angle: 1440, + y: 132 + fpOffset[1], + onComplete: () => { + scene.playSound("se/pb_rel"); + pokeball.destroy(); + scene.add.existing(pokemon); + scene.field.add(pokemon); + addPokeballOpenParticles(scene, pokemon.x, pokemon.y - 16, pokemon.pokeball); + scene.updateModifiers(true); + scene.updateFieldScale(); + pokemon.showInfo(); + pokemon.playAnim(); + pokemon.setVisible(true); + pokemon.getSprite().setVisible(true); + pokemon.setScale(0.5); + pokemon.tint(getPokeballTintColor(pokemon.pokeball)); + pokemon.untint(250, "Sine.easeIn"); + scene.updateFieldScale(); + scene.tweens.add({ + targets: pokemon, + duration: 250, + ease: "Sine.easeIn", + scale: pokemon.getSpriteScale(), + onComplete: () => { + pokemon.cry(pokemon.getHpRatio() > 0.25 ? undefined : { rate: 0.85 }); + pokemon.getSprite().clearTint(); + pokemon.resetSummonData(); + scene.time.delayedCall(1000, () => { + if (pokemon.isShiny()) { + scene.unshiftPhase(new ShinySparklePhase(scene, pokemon.getBattlerIndex())); + } + + pokemon.resetTurnData(); + + scene.triggerPokemonFormChange(pokemon, SpeciesFormChangeActiveTrigger, true); + scene.pushPhase(new PostSummonPhase(scene, pokemon.getBattlerIndex())); + resolve(); + }); + } + }); + } + }); + } + }); + }); +} + +function hideShowmanIntroSprite(scene: BattleScene) { + const carnivalGame = scene.currentBattle.mysteryEncounter!.introVisuals?.getSpriteAtIndex(0)[0]; + const wobbuffet = scene.currentBattle.mysteryEncounter!.introVisuals?.getSpriteAtIndex(1)[0]; + const showMan = scene.currentBattle.mysteryEncounter!.introVisuals?.getSpriteAtIndex(2)[0]; + + // Hide the showman + scene.tweens.add({ + targets: showMan, + x: "+=16", + y: "-=16", + alpha: 0, + ease: "Sine.easeInOut", + duration: 750 + }); + + // Slide the Wobbuffet and Game over slightly + scene.tweens.add({ + targets: [wobbuffet, carnivalGame], + x: "+=16", + ease: "Sine.easeInOut", + duration: 750 + }); +} diff --git a/src/data/mystery-encounters/encounters/lost-at-sea-encounter.ts b/src/data/mystery-encounters/encounters/lost-at-sea-encounter.ts index a06aa5a404b..bdeef60fe91 100644 --- a/src/data/mystery-encounters/encounters/lost-at-sea-encounter.ts +++ b/src/data/mystery-encounters/encounters/lost-at-sea-encounter.ts @@ -40,11 +40,11 @@ export const LostAtSeaEncounter: MysteryEncounter = MysteryEncounterBuilder.with ]) .withIntroDialogue([{ text: `${namespace}.intro` }]) .withOnInit((scene: BattleScene) => { - const { mysteryEncounter } = scene.currentBattle; + const encounter = scene.currentBattle.mysteryEncounter!; - mysteryEncounter.setDialogueToken("damagePercentage", String(DAMAGE_PERCENTAGE)); - mysteryEncounter.setDialogueToken("option1RequiredMove", Moves[OPTION_1_REQUIRED_MOVE]); - mysteryEncounter.setDialogueToken("option2RequiredMove", Moves[OPTION_2_REQUIRED_MOVE]); + encounter.setDialogueToken("damagePercentage", String(DAMAGE_PERCENTAGE)); + encounter.setDialogueToken("option1RequiredMove", Moves[OPTION_1_REQUIRED_MOVE]); + encounter.setDialogueToken("option2RequiredMove", Moves[OPTION_2_REQUIRED_MOVE]); return true; }) @@ -130,7 +130,7 @@ async function handlePokemonGuidingYouPhase(scene: BattleScene) { const laprasSpecies = getPokemonSpecies(Species.LAPRAS); const { mysteryEncounter } = scene.currentBattle; - if (mysteryEncounter.selectedOption?.primaryPokemon?.id) { + if (mysteryEncounter?.selectedOption?.primaryPokemon?.id) { setEncounterExp(scene, mysteryEncounter.selectedOption.primaryPokemon.id, laprasSpecies.baseExp, true); } else { console.warn("Lost at sea: No guide pokemon found but pokemon guides player. huh!?"); diff --git a/src/data/mystery-encounters/encounters/mysterious-challengers-encounter.ts b/src/data/mystery-encounters/encounters/mysterious-challengers-encounter.ts index de9132f3493..f418cdf7b1e 100644 --- a/src/data/mystery-encounters/encounters/mysterious-challengers-encounter.ts +++ b/src/data/mystery-encounters/encounters/mysterious-challengers-encounter.ts @@ -37,7 +37,7 @@ export const MysteriousChallengersEncounter: MysteryEncounter = }, ]) .withOnInit((scene: BattleScene) => { - const encounter = scene.currentBattle.mysteryEncounter; + const encounter = scene.currentBattle.mysteryEncounter!; // Calculates what trainers are available for battle in the encounter // Normal difficulty trainer is randomly pulled from biome @@ -137,7 +137,7 @@ export const MysteriousChallengersEncounter: MysteryEncounter = ], }, async (scene: BattleScene) => { - const encounter = scene.currentBattle.mysteryEncounter; + const encounter = scene.currentBattle.mysteryEncounter!; // Spawn standard trainer battle with memory mushroom reward const config: EnemyPartyConfig = encounter.enemyPartyConfigs[0]; @@ -162,7 +162,7 @@ export const MysteriousChallengersEncounter: MysteryEncounter = ], }, async (scene: BattleScene) => { - const encounter = scene.currentBattle.mysteryEncounter; + const encounter = scene.currentBattle.mysteryEncounter!; // Spawn hard fight with ULTRA/GREAT reward (can improve with luck) const config: EnemyPartyConfig = encounter.enemyPartyConfigs[1]; @@ -187,7 +187,7 @@ export const MysteriousChallengersEncounter: MysteryEncounter = ], }, async (scene: BattleScene) => { - const encounter = scene.currentBattle.mysteryEncounter; + const encounter = scene.currentBattle.mysteryEncounter!; // Spawn brutal fight with ROGUE/ULTRA/GREAT reward (can improve with luck) const config: EnemyPartyConfig = encounter.enemyPartyConfigs[2]; diff --git a/src/data/mystery-encounters/encounters/mysterious-chest-encounter.ts b/src/data/mystery-encounters/encounters/mysterious-chest-encounter.ts index 6caef1c36ec..60df1f87b0f 100644 --- a/src/data/mystery-encounters/encounters/mysterious-chest-encounter.ts +++ b/src/data/mystery-encounters/encounters/mysterious-chest-encounter.ts @@ -57,7 +57,7 @@ export const MysteriousChestEncounter: MysteryEncounter = .withPreOptionPhase(async (scene: BattleScene) => { // Play animation const introVisuals = - scene.currentBattle.mysteryEncounter.introVisuals!; + scene.currentBattle.mysteryEncounter!.introVisuals!; introVisuals.spriteConfigs[0].disableAnimation = false; introVisuals.playAnim(); }) @@ -113,7 +113,7 @@ export const MysteriousChestEncounter: MysteryEncounter = ); koPlayerPokemon(scene, highestLevelPokemon); - scene.currentBattle.mysteryEncounter.setDialogueToken("pokeName", highestLevelPokemon.getNameToRender()); + scene.currentBattle.mysteryEncounter!.setDialogueToken("pokeName", highestLevelPokemon.getNameToRender()); // 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, `${namespace}.option.1.bad`); diff --git a/src/data/mystery-encounters/encounters/part-timer-encounter.ts b/src/data/mystery-encounters/encounters/part-timer-encounter.ts index d7797f133ce..8dffeac6cb2 100644 --- a/src/data/mystery-encounters/encounters/part-timer-encounter.ts +++ b/src/data/mystery-encounters/encounters/part-timer-encounter.ts @@ -82,7 +82,7 @@ export const PartTimerEncounter: MysteryEncounter = ] }) .withPreOptionPhase(async (scene: BattleScene) => { - const encounter = scene.currentBattle.mysteryEncounter; + const encounter = scene.currentBattle.mysteryEncounter!; const onPokemonSelected = (pokemon: PlayerPokemon) => { encounter.setDialogueToken("selectedPokemon", pokemon.getNameToRender()); @@ -130,7 +130,7 @@ export const PartTimerEncounter: MysteryEncounter = // Bring visuals back in await transitionMysteryEncounterIntroVisuals(scene, false, false); - const moneyMultiplier = scene.currentBattle.mysteryEncounter.misc.moneyMultiplier; + const moneyMultiplier = scene.currentBattle.mysteryEncounter!.misc.moneyMultiplier; // Give money and do dialogue if (moneyMultiplier > 2.5) { @@ -160,7 +160,7 @@ export const PartTimerEncounter: MysteryEncounter = ] }) .withPreOptionPhase(async (scene: BattleScene) => { - const encounter = scene.currentBattle.mysteryEncounter; + const encounter = scene.currentBattle.mysteryEncounter!; const onPokemonSelected = (pokemon: PlayerPokemon) => { encounter.setDialogueToken("selectedPokemon", pokemon.getNameToRender()); @@ -211,7 +211,7 @@ export const PartTimerEncounter: MysteryEncounter = // Bring visuals back in await transitionMysteryEncounterIntroVisuals(scene, false, false); - const moneyMultiplier = scene.currentBattle.mysteryEncounter.misc.moneyMultiplier; + const moneyMultiplier = scene.currentBattle.mysteryEncounter!.misc.moneyMultiplier; // Give money and do dialogue if (moneyMultiplier > 2.5) { @@ -244,7 +244,7 @@ export const PartTimerEncounter: MysteryEncounter = ], }) .withPreOptionPhase(async (scene: BattleScene) => { - const encounter = scene.currentBattle.mysteryEncounter; + const encounter = scene.currentBattle.mysteryEncounter!; const selectedPokemon = encounter.selectedOption?.primaryPokemon!; encounter.setDialogueToken("selectedPokemon", selectedPokemon.getNameToRender()); diff --git a/src/data/mystery-encounters/encounters/safari-zone-encounter.ts b/src/data/mystery-encounters/encounters/safari-zone-encounter.ts index 43370fc189c..e2cbcade004 100644 --- a/src/data/mystery-encounters/encounters/safari-zone-encounter.ts +++ b/src/data/mystery-encounters/encounters/safari-zone-encounter.ts @@ -65,7 +65,7 @@ export const SafariZoneEncounter: MysteryEncounter = }) .withOptionPhase(async (scene: BattleScene) => { // Start safari encounter - const encounter = scene.currentBattle.mysteryEncounter; + const encounter = scene.currentBattle.mysteryEncounter!; encounter.continuousEncounter = true; encounter.misc = { safariPokemonRemaining: 3 @@ -130,7 +130,7 @@ const safariZoneGameOptions: MysteryEncounterOption[] = [ }) .withOptionPhase(async (scene: BattleScene) => { // Throw a ball option - const encounter = scene.currentBattle.mysteryEncounter; + const encounter = scene.currentBattle.mysteryEncounter!; const pokemon = encounter.misc.pokemon; const catchResult = await throwPokeball(scene, pokemon); @@ -165,7 +165,7 @@ const safariZoneGameOptions: MysteryEncounterOption[] = [ }) .withOptionPhase(async (scene: BattleScene) => { // Throw bait option - const pokemon = scene.currentBattle.mysteryEncounter.misc.pokemon; + const pokemon = scene.currentBattle.mysteryEncounter!.misc.pokemon; await throwBait(scene, pokemon); // 100% chance to increase catch stage +2 @@ -195,7 +195,7 @@ const safariZoneGameOptions: MysteryEncounterOption[] = [ }) .withOptionPhase(async (scene: BattleScene) => { // Throw mud option - const pokemon = scene.currentBattle.mysteryEncounter.misc.pokemon; + const pokemon = scene.currentBattle.mysteryEncounter!.misc.pokemon; await throwMud(scene, pokemon); // 100% chance to decrease flee stage -2 tryChangeFleeStage(scene, -2); @@ -219,7 +219,7 @@ const safariZoneGameOptions: MysteryEncounterOption[] = [ }) .withOptionPhase(async (scene: BattleScene) => { // Flee option - const encounter = scene.currentBattle.mysteryEncounter; + const encounter = scene.currentBattle.mysteryEncounter!; const pokemon = encounter.misc.pokemon; await doPlayerFlee(scene, pokemon); // Check how many safari pokemon left @@ -237,7 +237,7 @@ const safariZoneGameOptions: MysteryEncounterOption[] = [ ]; async function summonSafariPokemon(scene: BattleScene) { - const encounter = scene.currentBattle.mysteryEncounter; + const encounter = scene.currentBattle.mysteryEncounter!; // Message pokemon remaining encounter.setDialogueToken("remainingCount", encounter.misc.safariPokemonRemaining); scene.queueMessage(getEncounterText(scene, `${namespace}.safari.remaining_count`) ?? "", null, true); @@ -301,7 +301,7 @@ async function summonSafariPokemon(scene: BattleScene) { function throwPokeball(scene: BattleScene, pokemon: EnemyPokemon): Promise { const baseCatchRate = pokemon.species.catchRate; // Catch stage ranges from -6 to +6 (like stat boost stages) - const safariCatchStage = scene.currentBattle.mysteryEncounter.misc.catchStage; + const safariCatchStage = scene.currentBattle.mysteryEncounter!.misc.catchStage; // Catch modifier ranges from 2/8 (-6 stage) to 8/2 (+6) const safariModifier = (2 + Math.min(Math.max(safariCatchStage, 0), 6)) / (2 - Math.max(Math.min(safariCatchStage, 0), -6)); // Catch rate same as safari ball @@ -464,8 +464,8 @@ function tryChangeFleeStage(scene: BattleScene, change: number, chance?: number) if (chance && randSeedInt(10) >= chance) { return false; } - const currentFleeStage = scene.currentBattle.mysteryEncounter.misc.fleeStage ?? 0; - scene.currentBattle.mysteryEncounter.misc.fleeStage = Math.min(Math.max(currentFleeStage + change, -6), 6); + const currentFleeStage = scene.currentBattle.mysteryEncounter!.misc.fleeStage ?? 0; + scene.currentBattle.mysteryEncounter!.misc.fleeStage = Math.min(Math.max(currentFleeStage + change, -6), 6); return true; } @@ -473,13 +473,13 @@ function tryChangeCatchStage(scene: BattleScene, change: number, chance?: number if (chance && randSeedInt(10) >= chance) { return false; } - const currentCatchStage = scene.currentBattle.mysteryEncounter.misc.catchStage ?? 0; - scene.currentBattle.mysteryEncounter.misc.catchStage = Math.min(Math.max(currentCatchStage + change, -6), 6); + const currentCatchStage = scene.currentBattle.mysteryEncounter!.misc.catchStage ?? 0; + scene.currentBattle.mysteryEncounter!.misc.catchStage = Math.min(Math.max(currentCatchStage + change, -6), 6); return true; } async function doEndTurn(scene: BattleScene, cursorIndex: number) { - const encounter = scene.currentBattle.mysteryEncounter; + const encounter = scene.currentBattle.mysteryEncounter!; const pokemon = encounter.misc.pokemon; const isFlee = isPokemonFlee(pokemon, encounter.misc.fleeStage); if (isFlee) { diff --git a/src/data/mystery-encounters/encounters/shady-vitamin-dealer-encounter.ts b/src/data/mystery-encounters/encounters/shady-vitamin-dealer-encounter.ts index 1f553624aa7..645992ac9c7 100644 --- a/src/data/mystery-encounters/encounters/shady-vitamin-dealer-encounter.ts +++ b/src/data/mystery-encounters/encounters/shady-vitamin-dealer-encounter.ts @@ -73,7 +73,7 @@ export const ShadyVitaminDealerEncounter: MysteryEncounter = ], }) .withPreOptionPhase(async (scene: BattleScene): Promise => { - const encounter = scene.currentBattle.mysteryEncounter; + const encounter = scene.currentBattle.mysteryEncounter!; const onPokemonSelected = (pokemon: PlayerPokemon) => { // Update money updatePlayerMoney(scene, -(encounter.options[0].requirements[0] as MoneyRequirement).requiredMoney); @@ -105,7 +105,7 @@ export const ShadyVitaminDealerEncounter: MysteryEncounter = }) .withOptionPhase(async (scene: BattleScene) => { // Choose Cheap Option - const encounter = scene.currentBattle.mysteryEncounter; + const encounter = scene.currentBattle.mysteryEncounter!; const chosenPokemon = encounter.misc.chosenPokemon; const modifiers = encounter.misc.modifiers; @@ -117,7 +117,7 @@ export const ShadyVitaminDealerEncounter: MysteryEncounter = }) .withPostOptionPhase(async (scene: BattleScene) => { // Damage and status applied after dealer leaves (to make thematic sense) - const encounter = scene.currentBattle.mysteryEncounter; + const encounter = scene.currentBattle.mysteryEncounter!; const chosenPokemon = encounter.misc.chosenPokemon; // Pokemon takes 1/3 max HP damage @@ -156,7 +156,7 @@ export const ShadyVitaminDealerEncounter: MysteryEncounter = ], }) .withPreOptionPhase(async (scene: BattleScene): Promise => { - const encounter = scene.currentBattle.mysteryEncounter; + const encounter = scene.currentBattle.mysteryEncounter!; const onPokemonSelected = (pokemon: PlayerPokemon) => { // Update money updatePlayerMoney(scene, -(encounter.options[1].requirements[0] as MoneyRequirement).requiredMoney); @@ -188,7 +188,7 @@ export const ShadyVitaminDealerEncounter: MysteryEncounter = }) .withOptionPhase(async (scene: BattleScene) => { // Choose Expensive Option - const encounter = scene.currentBattle.mysteryEncounter; + const encounter = scene.currentBattle.mysteryEncounter!; const chosenPokemon = encounter.misc.chosenPokemon; const modifiers = encounter.misc.modifiers; @@ -200,7 +200,7 @@ export const ShadyVitaminDealerEncounter: MysteryEncounter = }) .withPostOptionPhase(async (scene: BattleScene) => { // Status applied after dealer leaves (to make thematic sense) - const encounter = scene.currentBattle.mysteryEncounter; + const encounter = scene.currentBattle.mysteryEncounter!; const chosenPokemon = encounter.misc.chosenPokemon; // Roll for poison (20%) diff --git a/src/data/mystery-encounters/encounters/slumbering-snorlax-encounter.ts b/src/data/mystery-encounters/encounters/slumbering-snorlax-encounter.ts index 47a85edb4f2..6fd475a9fc4 100644 --- a/src/data/mystery-encounters/encounters/slumbering-snorlax-encounter.ts +++ b/src/data/mystery-encounters/encounters/slumbering-snorlax-encounter.ts @@ -48,7 +48,7 @@ export const SlumberingSnorlaxEncounter: MysteryEncounter = }, ]) .withOnInit((scene: BattleScene) => { - const encounter = scene.currentBattle.mysteryEncounter; + const encounter = scene.currentBattle.mysteryEncounter!; console.log(encounter); // Calculate boss mon @@ -85,7 +85,7 @@ export const SlumberingSnorlaxEncounter: MysteryEncounter = }, async (scene: BattleScene) => { // Pick battle - const encounter = scene.currentBattle.mysteryEncounter; + const encounter = scene.currentBattle.mysteryEncounter!; setEncounterRewards(scene, { guaranteedModifierTypeFuncs: [modifierTypes.LEFTOVERS], fillRemaining: true}); encounter.startOfBattleEffects.push( { @@ -137,7 +137,7 @@ export const SlumberingSnorlaxEncounter: MysteryEncounter = }) .withOptionPhase(async (scene: BattleScene) => { // Steal the Snorlax's Leftovers - const instance = scene.currentBattle.mysteryEncounter; + const instance = scene.currentBattle.mysteryEncounter!; setEncounterRewards(scene, { guaranteedModifierTypeFuncs: [modifierTypes.LEFTOVERS], fillRemaining: false }); // Snorlax exp to Pokemon that did the stealing setEncounterExp(scene, instance.primaryPokemon!.id, getPokemonSpecies(Species.SNORLAX).baseExp); diff --git a/src/data/mystery-encounters/encounters/teleporting-hijinks-encounter.ts b/src/data/mystery-encounters/encounters/teleporting-hijinks-encounter.ts index a4c4d57daca..4459f177a30 100644 --- a/src/data/mystery-encounters/encounters/teleporting-hijinks-encounter.ts +++ b/src/data/mystery-encounters/encounters/teleporting-hijinks-encounter.ts @@ -57,7 +57,7 @@ export const TeleportingHijinksEncounter: MysteryEncounter = .withDescription(`${namespace}.description`) .withQuery(`${namespace}.query`) .withOnInit((scene: BattleScene) => { - const encounter = scene.currentBattle.mysteryEncounter; + const encounter = scene.currentBattle.mysteryEncounter!; const price = scene.getWaveMoneyAmount(MONEY_COST_MULTIPLIER); encounter.setDialogueToken("price", price.toString()); encounter.misc = { @@ -81,7 +81,7 @@ export const TeleportingHijinksEncounter: MysteryEncounter = }) .withPreOptionPhase(async (scene: BattleScene) => { // Update money - updatePlayerMoney(scene, -scene.currentBattle.mysteryEncounter.misc.price, true, false); + updatePlayerMoney(scene, -scene.currentBattle.mysteryEncounter!.misc.price, true, false); }) .withOptionPhase(async (scene: BattleScene) => { const config: EnemyPartyConfig = await doBiomeTransitionDialogueAndBattleInit(scene); @@ -107,7 +107,7 @@ export const TeleportingHijinksEncounter: MysteryEncounter = .withOptionPhase(async (scene: BattleScene) => { const config: EnemyPartyConfig = await doBiomeTransitionDialogueAndBattleInit(scene); setEncounterRewards(scene, { fillRemaining: true }); - setEncounterExp(scene, scene.currentBattle.mysteryEncounter.selectedOption!.primaryPokemon!.id, 100); + setEncounterExp(scene, scene.currentBattle.mysteryEncounter!.selectedOption!.primaryPokemon!.id, 100); await initBattleWithEnemyConfig(scene, config); }) .build() @@ -124,7 +124,7 @@ export const TeleportingHijinksEncounter: MysteryEncounter = }, async (scene: BattleScene) => { // Inspect the Machine - const encounter = scene.currentBattle.mysteryEncounter; + const encounter = scene.currentBattle.mysteryEncounter!; // Init enemy const level = (scene.currentBattle.enemyLevels?.[0] ?? scene.currentBattle.waveIndex) + Math.max(Math.round((scene.currentBattle.waveIndex / 10)), 0); @@ -150,7 +150,7 @@ export const TeleportingHijinksEncounter: MysteryEncounter = .build(); async function doBiomeTransitionDialogueAndBattleInit(scene: BattleScene) { - const encounter = scene.currentBattle.mysteryEncounter; + const encounter = scene.currentBattle.mysteryEncounter!; // Calculate new biome (cannot be current biome) const filteredBiomes = BIOME_CANDIDATES.filter(b => scene.arena.biomeType !== b); diff --git a/src/data/mystery-encounters/encounters/the-pokemon-salesman-encounter.ts b/src/data/mystery-encounters/encounters/the-pokemon-salesman-encounter.ts index cb0d7e486fe..ad28d5f653f 100644 --- a/src/data/mystery-encounters/encounters/the-pokemon-salesman-encounter.ts +++ b/src/data/mystery-encounters/encounters/the-pokemon-salesman-encounter.ts @@ -51,7 +51,7 @@ export const ThePokemonSalesmanEncounter: MysteryEncounter = .withDescription(`${namespace}.description`) .withQuery(`${namespace}.query`) .withOnInit((scene: BattleScene) => { - const encounter = scene.currentBattle.mysteryEncounter; + const encounter = scene.currentBattle.mysteryEncounter!; let species = getPokemonSpecies(getRandomSpeciesByStarterTier([0, 5])); const tries = 0; @@ -118,7 +118,7 @@ export const ThePokemonSalesmanEncounter: MysteryEncounter = ], }) .withOptionPhase(async (scene: BattleScene) => { - const encounter = scene.currentBattle.mysteryEncounter; + const encounter = scene.currentBattle.mysteryEncounter!; const price = encounter.misc.price; const purchasedPokemon = encounter.misc.pokemon as PlayerPokemon; diff --git a/src/data/mystery-encounters/encounters/the-strong-stuff-encounter.ts b/src/data/mystery-encounters/encounters/the-strong-stuff-encounter.ts index ccdfe98d7a9..9c20a511bdc 100644 --- a/src/data/mystery-encounters/encounters/the-strong-stuff-encounter.ts +++ b/src/data/mystery-encounters/encounters/the-strong-stuff-encounter.ts @@ -60,7 +60,7 @@ export const TheStrongStuffEncounter: MysteryEncounter = }, ]) .withOnInit((scene: BattleScene) => { - const encounter = scene.currentBattle.mysteryEncounter; + const encounter = scene.currentBattle.mysteryEncounter!; // Calculate boss mon const config: EnemyPartyConfig = { @@ -118,7 +118,7 @@ export const TheStrongStuffEncounter: MysteryEncounter = ] }, async (scene: BattleScene) => { - const encounter = scene.currentBattle.mysteryEncounter; + const encounter = scene.currentBattle.mysteryEncounter!; // Do blackout and hide intro visuals during blackout scene.time.delayedCall(750, () => { transitionMysteryEncounterIntroVisuals(scene, true, true, 50); @@ -176,7 +176,7 @@ export const TheStrongStuffEncounter: MysteryEncounter = }, async (scene: BattleScene) => { // Pick battle - const encounter = scene.currentBattle.mysteryEncounter; + const encounter = scene.currentBattle.mysteryEncounter!; setEncounterRewards(scene, { guaranteedModifierTypeFuncs: [modifierTypes.SOUL_DEW], fillRemaining: true }); encounter.startOfBattleEffects.push( { diff --git a/src/data/mystery-encounters/encounters/the-winstrate-challenge-encounter.ts b/src/data/mystery-encounters/encounters/the-winstrate-challenge-encounter.ts index 24c5af39f45..d7ffdb78dae 100644 --- a/src/data/mystery-encounters/encounters/the-winstrate-challenge-encounter.ts +++ b/src/data/mystery-encounters/encounters/the-winstrate-challenge-encounter.ts @@ -80,7 +80,7 @@ export const TheWinstrateChallengeEncounter: MysteryEncounter = ]) .withAutoHideIntroVisuals(false) .withOnInit((scene: BattleScene) => { - const encounter = scene.currentBattle.mysteryEncounter; + const encounter = scene.currentBattle.mysteryEncounter!; // Loaded back to front for pop() operations encounter.enemyPartyConfigs.push(getVitoTrainerConfig(scene)); @@ -107,8 +107,7 @@ export const TheWinstrateChallengeEncounter: MysteryEncounter = }, async (scene: BattleScene) => { // Spawn 5 trainer battles back to back with Macho Brace in rewards - // scene.currentBattle.mysteryEncounter.continuousEncounter = true; - scene.currentBattle.mysteryEncounter.doContinueEncounter = (scene: BattleScene) => { + scene.currentBattle.mysteryEncounter!.doContinueEncounter = (scene: BattleScene) => { return endTrainerBattleAndShowDialogue(scene); }; await transitionMysteryEncounterIntroVisuals(scene, true, false); @@ -136,7 +135,7 @@ export const TheWinstrateChallengeEncounter: MysteryEncounter = .build(); async function spawnNextTrainerOrEndEncounter(scene: BattleScene) { - const encounter = scene.currentBattle.mysteryEncounter; + const encounter = scene.currentBattle.mysteryEncounter!; const nextConfig = encounter.enemyPartyConfigs.pop(); if (!nextConfig) { await transitionMysteryEncounterIntroVisuals(scene, false, false); @@ -151,7 +150,7 @@ async function spawnNextTrainerOrEndEncounter(scene: BattleScene) { function endTrainerBattleAndShowDialogue(scene: BattleScene): Promise { return new Promise(async resolve => { - if (scene.currentBattle.mysteryEncounter.enemyPartyConfigs.length === 0) { + if (scene.currentBattle.mysteryEncounter!.enemyPartyConfigs.length === 0) { // Battle is over const trainer = scene.currentBattle.trainer; if (trainer) { diff --git a/src/data/mystery-encounters/encounters/training-session-encounter.ts b/src/data/mystery-encounters/encounters/training-session-encounter.ts index d82ca5c704d..4ffe60e29bd 100644 --- a/src/data/mystery-encounters/encounters/training-session-encounter.ts +++ b/src/data/mystery-encounters/encounters/training-session-encounter.ts @@ -65,7 +65,7 @@ export const TrainingSessionEncounter: MysteryEncounter = ], }) .withPreOptionPhase(async (scene: BattleScene): Promise => { - const encounter = scene.currentBattle.mysteryEncounter; + const encounter = scene.currentBattle.mysteryEncounter!; const onPokemonSelected = (pokemon: PlayerPokemon) => { encounter.misc = { playerPokemon: pokemon, @@ -85,7 +85,7 @@ export const TrainingSessionEncounter: MysteryEncounter = return selectPokemonForOption(scene, onPokemonSelected, undefined, selectableFilter); }) .withOptionPhase(async (scene: BattleScene) => { - const encounter = scene.currentBattle.mysteryEncounter; + const encounter = scene.currentBattle.mysteryEncounter!; const playerPokemon: PlayerPokemon = encounter.misc.playerPokemon; // Spawn light training session with chosen pokemon @@ -189,7 +189,7 @@ export const TrainingSessionEncounter: MysteryEncounter = }) .withPreOptionPhase(async (scene: BattleScene): Promise => { // Open menu for selecting pokemon and Nature - const encounter = scene.currentBattle.mysteryEncounter; + 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 @@ -223,7 +223,7 @@ export const TrainingSessionEncounter: MysteryEncounter = return selectPokemonForOption(scene, onPokemonSelected, undefined, selectableFilter); }) .withOptionPhase(async (scene: BattleScene) => { - const encounter = scene.currentBattle.mysteryEncounter; + const encounter = scene.currentBattle.mysteryEncounter!; const playerPokemon: PlayerPokemon = encounter.misc.playerPokemon; // Spawn medium training session with chosen pokemon @@ -277,7 +277,7 @@ export const TrainingSessionEncounter: MysteryEncounter = }) .withPreOptionPhase(async (scene: BattleScene): Promise => { // Open menu for selecting pokemon and ability to learn - const encounter = scene.currentBattle.mysteryEncounter; + const encounter = scene.currentBattle.mysteryEncounter!; const onPokemonSelected = (pokemon: PlayerPokemon) => { // Return the options for ability selection const speciesForm = !!pokemon.getFusionSpeciesForm() @@ -320,7 +320,7 @@ export const TrainingSessionEncounter: MysteryEncounter = return selectPokemonForOption(scene, onPokemonSelected, undefined, selectableFilter); }) .withOptionPhase(async (scene: BattleScene) => { - const encounter = scene.currentBattle.mysteryEncounter; + const encounter = scene.currentBattle.mysteryEncounter!; const playerPokemon: PlayerPokemon = encounter.misc.playerPokemon; // Spawn hard training session with chosen pokemon diff --git a/src/data/mystery-encounters/encounters/trash-to-treasure-encounter.ts b/src/data/mystery-encounters/encounters/trash-to-treasure-encounter.ts index 47a214b7fa2..da76922a5ef 100644 --- a/src/data/mystery-encounters/encounters/trash-to-treasure-encounter.ts +++ b/src/data/mystery-encounters/encounters/trash-to-treasure-encounter.ts @@ -54,7 +54,7 @@ export const TrashToTreasureEncounter: MysteryEncounter = .withDescription(`${namespace}.description`) .withQuery(`${namespace}.query`) .withOnInit((scene: BattleScene) => { - const encounter = scene.currentBattle.mysteryEncounter; + const encounter = scene.currentBattle.mysteryEncounter!; // Calculate boss mon const bossSpecies = getPokemonSpecies(Species.GARBODOR); @@ -124,7 +124,7 @@ export const TrashToTreasureEncounter: MysteryEncounter = await showEncounterText(scene, `${namespace}.option.2.selected_2`); transitionMysteryEncounterIntroVisuals(scene); - const encounter = scene.currentBattle.mysteryEncounter; + const encounter = scene.currentBattle.mysteryEncounter!; setEncounterRewards(scene, { guaranteedModifierTiers: [ModifierTier.ROGUE, ModifierTier.ROGUE, ModifierTier.ULTRA, ModifierTier.GREAT], fillRemaining: true }); encounter.startOfBattleEffects.push( diff --git a/src/data/mystery-encounters/encounters/weird-dream-encounter.ts b/src/data/mystery-encounters/encounters/weird-dream-encounter.ts index 8e79506b4b9..d3ddafb0194 100644 --- a/src/data/mystery-encounters/encounters/weird-dream-encounter.ts +++ b/src/data/mystery-encounters/encounters/weird-dream-encounter.ts @@ -150,7 +150,7 @@ export const WeirdDreamEncounter: MysteryEncounter = // Calculate all the newly transformed Pokemon and begin asset load const teamTransformations = getTeamTransformations(scene); const loadAssets = teamTransformations.map(t => (t.newPokemon as PlayerPokemon).loadAssets()); - scene.currentBattle.mysteryEncounter.misc = { + scene.currentBattle.mysteryEncounter!.misc = { teamTransformations, loadAssets }; @@ -161,8 +161,8 @@ export const WeirdDreamEncounter: MysteryEncounter = // Change the entire player's party // Wait for all new Pokemon assets to be loaded before showing transformation animations - await Promise.all(scene.currentBattle.mysteryEncounter.misc.loadAssets); - const transformations = scene.currentBattle.mysteryEncounter.misc.teamTransformations; + await Promise.all(scene.currentBattle.mysteryEncounter!.misc.loadAssets); + const transformations = scene.currentBattle.mysteryEncounter!.misc.teamTransformations; // If there are 1-3 transformations, do them centered back to back // Otherwise, the first 3 transformations are executed side-by-side, then any remaining 1-3 transformations occur in those same respective positions diff --git a/src/data/mystery-encounters/mystery-encounter.ts b/src/data/mystery-encounters/mystery-encounter.ts index 4bbd4bcb1b6..3a796e0c445 100644 --- a/src/data/mystery-encounters/mystery-encounter.ts +++ b/src/data/mystery-encounters/mystery-encounter.ts @@ -42,6 +42,9 @@ export interface IMysteryEncounter { catchAllowed: boolean; continuousEncounter: boolean; maxAllowedEncounters: number; + hasBattleAnimationsWithoutTargets: boolean; + skipEnemyBattleTurns: boolean; + skipToFightInput: boolean; onInit?: (scene: BattleScene) => boolean; onVisualsStart?: (scene: BattleScene) => boolean; @@ -113,7 +116,20 @@ export default class MysteryEncounter implements IMysteryEncounter { * Rogue tier encounters default to 1, others default to 3 */ maxAllowedEncounters: number; - + /** + * If true, encounter will not animate the target Pokemon as part of battle animations + * Used for encounters where it is not a "real" battle, but still uses battle animations and commands (see {@link FunAndGamesEncounter} for an example) + */ + hasBattleAnimationsWithoutTargets: boolean; + /** + * If true, will skip enemy pokemon turns during battle for the encounter + * Used for encounters where it is not a "real" battle, but still uses battle animations and commands (see {@link FunAndGamesEncounter} for an example) + */ + skipEnemyBattleTurns: boolean; + /** + * If true, will skip COMMAND input and go straight to FIGHT (move select) input menu + */ + skipToFightInput: boolean; /** * Event callback functions @@ -122,6 +138,8 @@ export default class MysteryEncounter implements IMysteryEncounter { onInit?: (scene: BattleScene) => boolean; /** Event when battlefield visuals have finished sliding in and the encounter dialogue begins */ onVisualsStart?: (scene: BattleScene) => boolean; + /** Event triggered prior to {@link CommandPhase}, during {@link TurnInitPhase} */ + onTurnStart?: (scene: BattleScene) => boolean; /** Event prior to any rewards logic in {@link MysteryEncounterRewardsPhase} */ onRewards?: (scene: BattleScene) => Promise; /** Will provide the player party EXP before rewards are displayed for that wave */ @@ -471,6 +489,9 @@ export class MysteryEncounterBuilder implements Partial { catchAllowed: boolean = false; lockEncounterRewardTiers: boolean = false; startOfBattleEffectsComplete: boolean = false; + hasBattleAnimationsWithoutTargets: boolean = false; + skipEnemyBattleTurns: boolean = false; + skipToFightInput: boolean = false; maxAllowedEncounters: number = 3; expMultiplier: number = 1; @@ -600,6 +621,35 @@ export class MysteryEncounterBuilder implements Partial { return Object.assign(this, { continuousEncounter: continuousEncounter }); } + /** + * If true, encounter will not animate the target Pokemon as part of battle animations + * Used for encounters where it is not a "real" battle, but still uses battle animations and commands (see {@link FunAndGamesEncounter} for an example) + * Default false + * @param hasBattleAnimationsWithoutTargets + */ + withBattleAnimationsWithoutTargets(hasBattleAnimationsWithoutTargets: boolean): this & Required> { + return Object.assign(this, { hasBattleAnimationsWithoutTargets }); + } + + /** + * If true, encounter will not animate the target Pokemon as part of battle animations + * Used for encounters where it is not a "real" battle, but still uses battle animations and commands (see {@link FunAndGamesEncounter} for an example) + * Default false + * @param skipEnemyBattleTurns + */ + withSkipEnemyBattleTurns(skipEnemyBattleTurns: boolean): this & Required> { + return Object.assign(this, { skipEnemyBattleTurns }); + } + + /** + * If true, will skip COMMAND input and go straight to FIGHT (move select) input menu + * Default false + * @param skipToFightInput + */ + withSkipToFightInput(skipToFightInput: boolean): this & Required> { + return Object.assign(this, { skipToFightInput }); + } + /** * Sets the maximum number of times that an encounter can spawn in a given Classic run * @param maxAllowedEncounters diff --git a/src/data/mystery-encounters/mystery-encounters.ts b/src/data/mystery-encounters/mystery-encounters.ts index 794c2772be1..595d9b578d4 100644 --- a/src/data/mystery-encounters/mystery-encounters.ts +++ b/src/data/mystery-encounters/mystery-encounters.ts @@ -28,6 +28,7 @@ import { WeirdDreamEncounter } from "#app/data/mystery-encounters/encounters/wei import { TheWinstrateChallengeEncounter } from "#app/data/mystery-encounters/encounters/the-winstrate-challenge-encounter"; import { TeleportingHijinksEncounter } from "#app/data/mystery-encounters/encounters/teleporting-hijinks-encounter"; import { BugTypeSuperfanEncounter } from "#app/data/mystery-encounters/encounters/bug-type-superfan-encounter"; +import { FunAndGamesEncounter } from "#app/data/mystery-encounters/encounters/fun-and-games-encounter"; // Spawn chance: (BASE_MYSTERY_ENCOUNTER_SPAWN_WEIGHT + WIGHT_INCREMENT_ON_SPAWN_MISS * ) / 256 export const BASE_MYSTERY_ENCOUNTER_SPAWN_WEIGHT = 1; @@ -155,7 +156,8 @@ const humanTransitableBiomeEncounters: MysteryEncounterType[] = [ const civilizationBiomeEncounters: MysteryEncounterType[] = [ MysteryEncounterType.DEPARTMENT_STORE_SALE, - MysteryEncounterType.PART_TIMER + MysteryEncounterType.PART_TIMER, + MysteryEncounterType.FUN_AND_GAMES ]; /** @@ -279,6 +281,7 @@ export function initMysteryEncounters() { allMysteryEncounters[MysteryEncounterType.THE_WINSTRATE_CHALLENGE] = TheWinstrateChallengeEncounter; allMysteryEncounters[MysteryEncounterType.TELEPORTING_HIJINKS] = TeleportingHijinksEncounter; allMysteryEncounters[MysteryEncounterType.BUG_TYPE_SUPERFAN] = BugTypeSuperfanEncounter; + allMysteryEncounters[MysteryEncounterType.FUN_AND_GAMES] = FunAndGamesEncounter; // Add extreme encounters to biome map extremeBiomeEncounters.forEach(encounter => { diff --git a/src/data/mystery-encounters/utils/encounter-phase-utils.ts b/src/data/mystery-encounters/utils/encounter-phase-utils.ts index dab7cb8c826..fb793a6f45a 100644 --- a/src/data/mystery-encounters/utils/encounter-phase-utils.ts +++ b/src/data/mystery-encounters/utils/encounter-phase-utils.ts @@ -120,7 +120,7 @@ export async function initBattleWithEnemyConfig(scene: BattleScene, partyConfig: const partyTrainerConfig = partyConfig?.trainerConfig; let trainerConfig: TrainerConfig; if (!isNullOrUndefined(trainerType) || partyTrainerConfig) { - scene.currentBattle.mysteryEncounter.encounterMode = MysteryEncounterMode.TRAINER_BATTLE; + scene.currentBattle.mysteryEncounter!.encounterMode = MysteryEncounterMode.TRAINER_BATTLE; if (scene.currentBattle.trainer) { scene.currentBattle.trainer.setVisible(false); scene.currentBattle.trainer.destroy(); @@ -141,7 +141,7 @@ export async function initBattleWithEnemyConfig(scene: BattleScene, partyConfig: battle.enemyLevels = scene.currentBattle.trainer.getPartyLevels(scene.currentBattle.waveIndex); } else { // Wild - scene.currentBattle.mysteryEncounter.encounterMode = MysteryEncounterMode.WILD_BATTLE; + scene.currentBattle.mysteryEncounter!.encounterMode = MysteryEncounterMode.WILD_BATTLE; const numEnemies = partyConfig?.pokemonConfigs && partyConfig.pokemonConfigs.length > 0 ? partyConfig?.pokemonConfigs?.length : doubleBattle ? 2 : 1; battle.enemyLevels = new Array(numEnemies).fill(null).map(() => scene.currentBattle.getLevelForWave()); } @@ -184,7 +184,7 @@ export async function initBattleWithEnemyConfig(scene: BattleScene, partyConfig: enemySpecies = config.species; isBoss = config.isBoss; if (isBoss) { - scene.currentBattle.mysteryEncounter.encounterMode = MysteryEncounterMode.BOSS_BATTLE; + scene.currentBattle.mysteryEncounter!.encounterMode = MysteryEncounterMode.BOSS_BATTLE; } } else { enemySpecies = scene.randomSpecies(battle.waveIndex, level, true); @@ -429,7 +429,7 @@ export function selectPokemonForOption(scene: BattleScene, onPokemonSelected: (p const pokemon = scene.getParty()[slotIndex]; const secondaryOptions = onPokemonSelected(pokemon); if (!secondaryOptions) { - scene.currentBattle.mysteryEncounter.setDialogueToken("selectedPokemon", pokemon.getNameToRender()); + scene.currentBattle.mysteryEncounter!.setDialogueToken("selectedPokemon", pokemon.getNameToRender()); resolve(true); return; } @@ -443,7 +443,7 @@ export function selectPokemonForOption(scene: BattleScene, onPokemonSelected: (p const onSelect = option.handler; option.handler = () => { onSelect(); - scene.currentBattle.mysteryEncounter.setDialogueToken("selectedPokemon", pokemon.getNameToRender()); + scene.currentBattle.mysteryEncounter!.setDialogueToken("selectedPokemon", pokemon.getNameToRender()); resolve(true); return true; }; @@ -595,7 +595,7 @@ export function selectOptionThenPokemon(scene: BattleScene, options: OptionSelec * @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 setEncounterRewards(scene: BattleScene, customShopRewards?: CustomModifierSettings, eggRewards?: IEggOptions[], preRewardsCallback?: Function) { - scene.currentBattle.mysteryEncounter.doEncounterRewards = (scene: BattleScene) => { + scene.currentBattle.mysteryEncounter!.doEncounterRewards = (scene: BattleScene) => { if (preRewardsCallback) { preRewardsCallback(); } @@ -638,7 +638,7 @@ export function setEncounterRewards(scene: BattleScene, customShopRewards?: Cust export function setEncounterExp(scene: BattleScene, participantId: integer | integer[], baseExpValue: number, useWaveIndex: boolean = true) { const participantIds = Array.isArray(participantId) ? participantId : [participantId]; - scene.currentBattle.mysteryEncounter.doEncounterExp = (scene: BattleScene) => { + 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; @@ -650,7 +650,7 @@ export function setEncounterExp(scene: BattleScene, participantId: integer | int let expValue = Math.floor(baseExpValue * (useWaveIndex ? scene.currentBattle.waveIndex : 1) / 5 + 1); if (participantIds?.length > 0) { - if (scene.currentBattle.mysteryEncounter.encounterMode === MysteryEncounterMode.TRAINER_BATTLE) { + if (scene.currentBattle.mysteryEncounter!.encounterMode === MysteryEncounterMode.TRAINER_BATTLE) { expValue = Math.floor(expValue * 1.5); } for (const partyMember of nonFaintedPartyMembers) { @@ -752,7 +752,7 @@ export function initSubsequentOptionSelect(scene: BattleScene, optionSelectSetti * @param encounterMode - Can set custom encounter mode if necessary (may be required for forcing Pokemon to return before next phase) */ export function leaveEncounterWithoutBattle(scene: BattleScene, addHealPhase: boolean = false, encounterMode: MysteryEncounterMode = MysteryEncounterMode.NO_BATTLE) { - scene.currentBattle.mysteryEncounter.encounterMode = encounterMode; + scene.currentBattle.mysteryEncounter!.encounterMode = encounterMode; scene.clearPhaseQueue(); scene.clearPhaseQueueSplice(); handleMysteryEncounterVictory(scene, addHealPhase); @@ -775,7 +775,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 - const encounter = scene.currentBattle.mysteryEncounter; + const encounter = scene.currentBattle.mysteryEncounter!; if (encounter.continuousEncounter || doNotContinue) { return; } else if (encounter.encounterMode === MysteryEncounterMode.NO_BATTLE) { @@ -805,7 +805,7 @@ export function handleMysteryEncounterVictory(scene: BattleScene, addHealPhase: */ export function transitionMysteryEncounterIntroVisuals(scene: BattleScene, hide: boolean = true, destroy: boolean = true, duration: number = 750): Promise { return new Promise(resolve => { - const introVisuals = scene.currentBattle.mysteryEncounter.introVisuals; + const introVisuals = scene.currentBattle.mysteryEncounter!.introVisuals; const enemyPokemon = scene.getEnemyField(); if (enemyPokemon) { scene.currentBattle.enemyParty = []; @@ -835,7 +835,7 @@ export function transitionMysteryEncounterIntroVisuals(scene: BattleScene, hide: scene.field.remove(pokemon, true); }); - scene.currentBattle.mysteryEncounter.introVisuals = undefined; + scene.currentBattle.mysteryEncounter!.introVisuals = undefined; } resolve(true); } @@ -852,8 +852,8 @@ export function transitionMysteryEncounterIntroVisuals(scene: BattleScene, hide: * @param scene */ export function handleMysteryEncounterBattleStartEffects(scene: BattleScene) { - const encounter = scene.currentBattle?.mysteryEncounter; - if (scene.currentBattle.battleType === BattleType.MYSTERY_ENCOUNTER && encounter.encounterMode !== MysteryEncounterMode.NO_BATTLE && !encounter.startOfBattleEffectsComplete) { + const encounter = scene.currentBattle.mysteryEncounter; + if (scene.currentBattle.battleType === BattleType.MYSTERY_ENCOUNTER && encounter && encounter.encounterMode !== MysteryEncounterMode.NO_BATTLE && !encounter.startOfBattleEffectsComplete) { const effects = encounter.startOfBattleEffects; effects.forEach(effect => { let source; @@ -884,6 +884,21 @@ export function handleMysteryEncounterBattleStartEffects(scene: BattleScene) { } } +/** + * Can queue extra phases or logic during {@link TurnInitPhase} + * Mostly useful for allowing MysteryEncounter enemies to "cheat" and use moves before the first turn + * @param scene + * @return boolean - if true, will skip the remainder of the {@link TurnInitPhase} + */ +export function handleMysteryEncounterTurnStartEffects(scene: BattleScene): boolean { + const encounter = scene.currentBattle.mysteryEncounter; + if (scene.currentBattle.battleType === BattleType.MYSTERY_ENCOUNTER && encounter && encounter.onTurnStart) { + return encounter.onTurnStart(scene); + } + + return false; +} + /** * TODO: remove once encounter spawn rate is finalized * Just a helper function to calculate aggregate stats for MEs in a Classic run diff --git a/src/enums/mystery-encounter-type.ts b/src/enums/mystery-encounter-type.ts index 26a6b591048..d48bab3076b 100644 --- a/src/enums/mystery-encounter-type.ts +++ b/src/enums/mystery-encounter-type.ts @@ -25,5 +25,6 @@ export enum MysteryEncounterType { WEIRD_DREAM, THE_WINSTRATE_CHALLENGE, TELEPORTING_HIJINKS, - BUG_TYPE_SUPERFAN + BUG_TYPE_SUPERFAN, + FUN_AND_GAMES } diff --git a/src/locales/en/mystery-encounter.ts b/src/locales/en/mystery-encounter.ts index 8ed2d597523..95f472d5b0c 100644 --- a/src/locales/en/mystery-encounter.ts +++ b/src/locales/en/mystery-encounter.ts @@ -25,6 +25,7 @@ import weirdDreamDialogue from "#app/locales/en/mystery-encounters/weird-dream-d import theWinstrateChallengeDialogue from "#app/locales/en/mystery-encounters/the-winstrate-challenge-dialogue.json"; import teleportingHijinksDialogue from "#app/locales/en/mystery-encounters/teleporting-hijinks-dialogue.json"; import bugTypeSuperfanDialogue from "#app/locales/en/mystery-encounters/bug-type-superfan-dialogue.json"; +import funAndGamesDialogue from "#app/locales/en/mystery-encounters/fun-and-games-dialogue.json"; /** * Injection patterns that can be used: @@ -71,5 +72,6 @@ export const mysteryEncounter = { weirdDream: weirdDreamDialogue, theWinstrateChallenge: theWinstrateChallengeDialogue, teleportingHijinks: teleportingHijinksDialogue, - bugTypeSuperfan: bugTypeSuperfanDialogue + bugTypeSuperfan: bugTypeSuperfanDialogue, + funAndGames: funAndGamesDialogue } as const; diff --git a/src/locales/en/mystery-encounters/fun-and-games-dialogue.json b/src/locales/en/mystery-encounters/fun-and-games-dialogue.json new file mode 100644 index 00000000000..6ad1497336a --- /dev/null +++ b/src/locales/en/mystery-encounters/fun-and-games-dialogue.json @@ -0,0 +1,30 @@ +{ + "intro_dialogue": "Step right up, folks! Try your luck\non the brand new Wobbuffet Whack-o-matic!", + "speaker": "Showman", + "title": "Fun And Games!", + "description": "You've encountered a traveling show with a prize game! You will have @[TOOLTIP_TITLE]{3 turns} to bring the Wobbuffet as close to @[TOOLTIP_TITLE]{1 HP} as possible @[TOOLTIP_TITLE]{without KOing it} so it can wind up a huge Counter on the bell-ringing machine.\nBut be careful! If you KO the Wobbuffet, you'll have to pay for the cost of reviving it!", + "query": "Would you like to play?", + "option": { + "1": { + "label": "Play the Game", + "tooltip": "(-) Pay {{option1Money, money}}\n(+) Play Wobbuffet Whack-o-matic", + "selected": "Time to test your luck!" + }, + "2": { + "label": "Leave", + "tooltip": "(-) No Rewards", + "selected": "You hurry along your way,\nwith a slight feeling of regret." + } + }, + "ko": "Oh no! The Wobbuffet fainted!$You lose the game and\nhave to pay for the revive cost...", + "charging_continue": "The Wubboffet keeps charging its counter-swing!", + "turn_remaining_3": "Three turns remaining!", + "turn_remaining_2": "Two turns remaining!", + "turn_remaining_1": "One turn remaining!", + "end_game": "Time's up!$The Wobbuffet winds up to counter-swing and@d{16}.@d{16}.@d{16}.", + "best_result": "The Wobbuffet smacks the button so hard\nthe bell breaks off the top!$You win the grand prize!", + "great_result": "The Wobbuffet smacks the button, nearly hitting the bell!$So close!\nYou earn the second tier prize!", + "good_result": "The Wobbuffet hits the button hard enough to go midway up the scale!$You earn the third tier prize!", + "bad_result": "The Wobbuffet barely taps the button and nothing happens...$Oh no!\nYou don't win anything!", + "outro": "That was a fun little game!" +} \ No newline at end of file diff --git a/src/phases/command-phase.ts b/src/phases/command-phase.ts index b81164be4f1..8862f626a0a 100644 --- a/src/phases/command-phase.ts +++ b/src/phases/command-phase.ts @@ -72,7 +72,12 @@ export class CommandPhase extends FieldPhase { } } } else { - this.scene.ui.setMode(Mode.COMMAND, this.fieldIndex); + if (this.scene.currentBattle.battleType === BattleType.MYSTERY_ENCOUNTER && this.scene.currentBattle.mysteryEncounter?.skipToFightInput) { + this.scene.ui.clearText(); + this.scene.ui.setMode(Mode.FIGHT, this.fieldIndex); + } else { + this.scene.ui.setMode(Mode.COMMAND, this.fieldIndex); + } } } @@ -138,7 +143,7 @@ export class CommandPhase extends FieldPhase { this.scene.ui.showText("", 0); this.scene.ui.setMode(Mode.COMMAND, this.fieldIndex); }, null, true); - } else if (this.scene.currentBattle.battleType === BattleType.MYSTERY_ENCOUNTER && !this.scene.currentBattle.mysteryEncounter.catchAllowed) { + } else if (this.scene.currentBattle.battleType === BattleType.MYSTERY_ENCOUNTER && !this.scene.currentBattle.mysteryEncounter!.catchAllowed) { this.scene.ui.setMode(Mode.COMMAND, this.fieldIndex); this.scene.ui.setMode(Mode.MESSAGE); this.scene.ui.showText(i18next.t("battle:noPokeballMysteryEncounter"), null, () => { diff --git a/src/phases/encounter-phase.ts b/src/phases/encounter-phase.ts index ec4a4d8fb62..48d3e7d5e23 100644 --- a/src/phases/encounter-phase.ts +++ b/src/phases/encounter-phase.ts @@ -150,7 +150,7 @@ export class EncounterPhase extends BattlePhase { const newEncounter = this.scene.getMysteryEncounter(mysteryEncounter); battle.mysteryEncounter = newEncounter; } - loadEnemyAssets.push(battle.mysteryEncounter.introVisuals!.loadAssets().then(() => battle.mysteryEncounter.introVisuals!.initSprite())); + loadEnemyAssets.push(battle.mysteryEncounter.introVisuals!.loadAssets().then(() => battle.mysteryEncounter!.introVisuals!.initSprite())); // Load Mystery Encounter Exclamation bubble and sfx loadEnemyAssets.push(new Promise(resolve => { this.scene.loadSe("GEN8- Exclaim", "battle_anims", "GEN8- Exclaim.wav"); @@ -349,12 +349,13 @@ export class EncounterPhase extends BattlePhase { showDialogueAndSummon(); } } - } else if (this.scene.currentBattle.battleType === BattleType.MYSTERY_ENCOUNTER) { - const introVisuals = this.scene.currentBattle.mysteryEncounter.introVisuals!; + } else if (this.scene.currentBattle.battleType === BattleType.MYSTERY_ENCOUNTER && this.scene.currentBattle.mysteryEncounter) { + const encounter = this.scene.currentBattle.mysteryEncounter; + const introVisuals = encounter.introVisuals!; introVisuals.playAnim(); - if (this.scene.currentBattle.mysteryEncounter.onVisualsStart) { - this.scene.currentBattle.mysteryEncounter.onVisualsStart(this.scene); + if (encounter.onVisualsStart) { + encounter.onVisualsStart(this.scene); } const doEncounter = () => { @@ -367,7 +368,7 @@ export class EncounterPhase extends BattlePhase { }; if (showEncounterMessage) { - const introDialogue = this.scene.currentBattle.mysteryEncounter.dialogue.intro; + const introDialogue = encounter.dialogue.intro; if (!introDialogue) { doShowEncounterOptions(); } else { diff --git a/src/phases/enemy-command-phase.ts b/src/phases/enemy-command-phase.ts index 0b62fcbe813..f91116c0e9b 100644 --- a/src/phases/enemy-command-phase.ts +++ b/src/phases/enemy-command-phase.ts @@ -17,11 +17,15 @@ import { FieldPhase } from "./field-phase"; */ export class EnemyCommandPhase extends FieldPhase { protected fieldIndex: integer; + protected skipTurn: boolean = false; constructor(scene: BattleScene, fieldIndex: integer) { super(scene); this.fieldIndex = fieldIndex; + if (this.scene.currentBattle.mysteryEncounter?.skipEnemyBattleTurns) { + this.skipTurn = true; + } } start() { @@ -63,7 +67,7 @@ export class EnemyCommandPhase extends FieldPhase { const index = trainer.getNextSummonIndex(enemyPokemon.trainerSlot, partyMemberScores); battle.turnCommands[this.fieldIndex + BattlerIndex.ENEMY] = - { command: Command.POKEMON, cursor: index, args: [false] }; + { command: Command.POKEMON, cursor: index, args: [false], skip: this.skipTurn }; battle.enemySwitchCounter++; @@ -77,7 +81,7 @@ export class EnemyCommandPhase extends FieldPhase { const nextMove = enemyPokemon.getNextMove(); this.scene.currentBattle.turnCommands[this.fieldIndex + BattlerIndex.ENEMY] = - { command: Command.FIGHT, move: nextMove }; + { command: Command.FIGHT, move: nextMove, skip: this.skipTurn }; this.scene.currentBattle.enemySwitchCounter = Math.max(this.scene.currentBattle.enemySwitchCounter - 1, 0); diff --git a/src/phases/move-effect-phase.ts b/src/phases/move-effect-phase.ts index f100a763219..a6f1ec8adff 100644 --- a/src/phases/move-effect-phase.ts +++ b/src/phases/move-effect-phase.ts @@ -119,8 +119,9 @@ export class MoveEffectPhase extends PokemonPhase { /** All move effect attributes are chained together in this array to be applied asynchronously. */ const applyAttrs: Promise[] = []; + const playOnEmptyField = this.scene.currentBattle?.mysteryEncounter?.hasBattleAnimationsWithoutTargets ?? false; // Move animation only needs one target - new MoveAnim(move.id as Moves, user, this.getTarget()?.getBattlerIndex()!).play(this.scene, () => { // TODO: is the bang correct here? + new MoveAnim(move.id as Moves, user, this.getTarget()?.getBattlerIndex()!, playOnEmptyField).play(this.scene, () => { // TODO: is the bang correct here? /** Has the move successfully hit a target (for damage) yet? */ let hasHit: boolean = false; for (const target of targets) { diff --git a/src/phases/mystery-encounter-phases.ts b/src/phases/mystery-encounter-phases.ts index 92850b3b38e..6ba1f468440 100644 --- a/src/phases/mystery-encounter-phases.ts +++ b/src/phases/mystery-encounter-phases.ts @@ -54,12 +54,13 @@ export class MysteryEncounterPhase extends Phase { this.scene.clearPhaseQueue(); this.scene.clearPhaseQueueSplice(); - this.scene.currentBattle.mysteryEncounter.updateSeedOffset(this.scene); + const encounter = this.scene.currentBattle.mysteryEncounter!; + encounter.updateSeedOffset(this.scene); if (!this.optionSelectSettings) { // Sets flag that ME was encountered, only if this is not a followup option select phase // Can be used in later MEs to check for requirements to spawn, etc. - this.scene.mysteryEncounterData.encounteredEvents.push([this.scene.currentBattle.mysteryEncounter.encounterType, this.scene.currentBattle.mysteryEncounter.encounterTier]); + this.scene.mysteryEncounterData.encounteredEvents.push([encounter.encounterType, encounter.encounterTier]); } // Initiates encounter dialogue window and option select @@ -68,14 +69,14 @@ export class MysteryEncounterPhase extends Phase { handleOptionSelect(option: MysteryEncounterOption, index: number): boolean { // Set option selected flag - this.scene.currentBattle.mysteryEncounter.selectedOption = option; + this.scene.currentBattle.mysteryEncounter!.selectedOption = option; if (!option.onOptionPhase) { return false; } // Populate dialogue tokens for option requirements - this.scene.currentBattle.mysteryEncounter.populateDialogueTokensFromRequirements(this.scene); + this.scene.currentBattle.mysteryEncounter!.populateDialogueTokensFromRequirements(this.scene); if (option.onPreOptionPhase) { this.scene.executeWithSeedOffset(async () => { @@ -85,7 +86,7 @@ export class MysteryEncounterPhase extends Phase { this.continueEncounter(); } }); - }, this.scene.currentBattle.mysteryEncounter.getSeedOffset()); + }, this.scene.currentBattle.mysteryEncounter?.getSeedOffset()); } else { this.continueEncounter(); } @@ -149,25 +150,25 @@ export class MysteryEncounterOptionSelectedPhase extends Phase { constructor(scene: BattleScene) { super(scene); - this.onOptionSelect = this.scene.currentBattle.mysteryEncounter.selectedOption!.onOptionPhase; + this.onOptionSelect = this.scene.currentBattle.mysteryEncounter!.selectedOption!.onOptionPhase; } start() { super.start(); - if (this.scene.currentBattle.mysteryEncounter.autoHideIntroVisuals) { + if (this.scene.currentBattle.mysteryEncounter?.autoHideIntroVisuals) { transitionMysteryEncounterIntroVisuals(this.scene).then(() => { this.scene.executeWithSeedOffset(() => { this.onOptionSelect(this.scene).finally(() => { this.end(); }); - }, this.scene.currentBattle.mysteryEncounter.getSeedOffset()); + }, this.scene.currentBattle.mysteryEncounter?.getSeedOffset()); }); } else { this.scene.executeWithSeedOffset(() => { this.onOptionSelect(this.scene).finally(() => { this.end(); }); - }, this.scene.currentBattle.mysteryEncounter.getSeedOffset()); + }, this.scene.currentBattle.mysteryEncounter?.getSeedOffset()); } } } @@ -222,7 +223,7 @@ export class MysteryEncounterBattlePhase extends Phase { getBattleMessage(scene: BattleScene): string { const enemyField = scene.getEnemyField(); - const encounterMode = scene.currentBattle.mysteryEncounter.encounterMode; + const encounterMode = scene.currentBattle.mysteryEncounter!.encounterMode; if (scene.currentBattle.battleSpec === BattleSpec.FINAL_BOSS) { return i18next.t("battle:bossAppeared", { bossName: enemyField[0].name }); @@ -243,7 +244,7 @@ export class MysteryEncounterBattlePhase extends Phase { } doMysteryEncounterBattle(scene: BattleScene) { - const encounterMode = scene.currentBattle.mysteryEncounter.encounterMode; + const encounterMode = scene.currentBattle.mysteryEncounter!.encounterMode; if (encounterMode === MysteryEncounterMode.WILD_BATTLE || encounterMode === MysteryEncounterMode.BOSS_BATTLE) { // Summons the wild/boss Pokemon if (encounterMode === MysteryEncounterMode.BOSS_BATTLE) { @@ -255,7 +256,7 @@ export class MysteryEncounterBattlePhase extends Phase { scene.unshiftPhase(new SummonPhase(scene, 1, false)); } - if (!scene.currentBattle.mysteryEncounter.hideBattleIntroMessage) { + if (!scene.currentBattle.mysteryEncounter?.hideBattleIntroMessage) { scene.ui.showText(this.getBattleMessage(scene), null, () => this.endBattleSetup(scene), 0); } else { this.endBattleSetup(scene); @@ -276,7 +277,7 @@ export class MysteryEncounterBattlePhase extends Phase { } this.endBattleSetup(scene); }; - if (!scene.currentBattle.mysteryEncounter.hideBattleIntroMessage) { + if (!scene.currentBattle.mysteryEncounter?.hideBattleIntroMessage) { scene.ui.showText(this.getBattleMessage(scene), null, doTrainerSummon, 1000, true); } else { doTrainerSummon(); @@ -290,7 +291,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.getSeedOffset()); + scene.executeWithSeedOffset(() => message = Utils.randSeedItem(encounterMessages), this.scene.currentBattle.mysteryEncounter?.getSeedOffset()); message = message!; // tell TS compiler it's defined now const showDialogueAndSummon = () => { scene.ui.showDialogue(message, trainer?.getName(TrainerSlot.NONE, true), null, () => { @@ -308,7 +309,7 @@ export class MysteryEncounterBattlePhase extends Phase { endBattleSetup(scene: BattleScene) { const enemyField = scene.getEnemyField(); - const encounterMode = scene.currentBattle.mysteryEncounter.encounterMode; + const encounterMode = scene.currentBattle.mysteryEncounter!.encounterMode; // PostSummon and ShinySparkle phases are handled by SummonPhase @@ -410,7 +411,7 @@ export class MysteryEncounterRewardsPhase extends Phase { start() { super.start(); - const encounter = this.scene.currentBattle.mysteryEncounter; + const encounter = this.scene.currentBattle.mysteryEncounter!; if (encounter.doContinueEncounter) { encounter.doContinueEncounter(this.scene).then(() => { @@ -431,7 +432,7 @@ export class MysteryEncounterRewardsPhase extends Phase { } doEncounterRewardsAndContinue() { - const encounter = this.scene.currentBattle.mysteryEncounter; + const encounter = this.scene.currentBattle.mysteryEncounter!; if (encounter.doEncounterExp) { encounter.doEncounterExp(this.scene); @@ -462,7 +463,7 @@ export class PostMysteryEncounterPhase extends Phase { constructor(scene: BattleScene) { super(scene); - this.onPostOptionSelect = this.scene.currentBattle.mysteryEncounter.selectedOption?.onPostOptionPhase; + this.onPostOptionSelect = this.scene.currentBattle.mysteryEncounter?.selectedOption?.onPostOptionPhase; } start() { @@ -476,7 +477,7 @@ export class PostMysteryEncounterPhase extends Phase { this.continueEncounter(); } }); - }, this.scene.currentBattle.mysteryEncounter.getSeedOffset()); + }, this.scene.currentBattle.mysteryEncounter?.getSeedOffset()); } else { this.continueEncounter(); } diff --git a/src/phases/next-encounter-phase.ts b/src/phases/next-encounter-phase.ts index 547b1b19651..d1b4d2e93d1 100644 --- a/src/phases/next-encounter-phase.ts +++ b/src/phases/next-encounter-phase.ts @@ -24,11 +24,11 @@ export class NextEncounterPhase extends EncounterPhase { const enemyField = this.scene.getEnemyField(); const moveTargets: any[] = [this.scene.arenaEnemy, this.scene.arenaNextEnemy, this.scene.currentBattle.trainer, enemyField, this.scene.lastEnemyTrainer]; - const lastEncounterVisuals = this.scene?.lastMysteryEncounter?.introVisuals; + const lastEncounterVisuals = this.scene.lastMysteryEncounter?.introVisuals; if (lastEncounterVisuals) { moveTargets.push(lastEncounterVisuals); } - const nextEncounterVisuals = this.scene.currentBattle?.mysteryEncounter?.introVisuals; + const nextEncounterVisuals = this.scene.currentBattle.mysteryEncounter?.introVisuals; if (nextEncounterVisuals) { const enterFromRight = nextEncounterVisuals.enterFromRight; if (enterFromRight) { @@ -58,7 +58,7 @@ export class NextEncounterPhase extends EncounterPhase { } if (lastEncounterVisuals) { this.scene.field.remove(lastEncounterVisuals, true); - this.scene.lastMysteryEncounter.introVisuals = undefined; + this.scene.lastMysteryEncounter!.introVisuals = undefined; } if (!this.tryOverrideForBattleSpec()) { diff --git a/src/phases/turn-init-phase.ts b/src/phases/turn-init-phase.ts index a8d1b16bc2f..23c7985ad2c 100644 --- a/src/phases/turn-init-phase.ts +++ b/src/phases/turn-init-phase.ts @@ -9,7 +9,7 @@ import { CommandPhase } from "./command-phase"; import { EnemyCommandPhase } from "./enemy-command-phase"; import { GameOverPhase } from "./game-over-phase"; import { TurnStartPhase } from "./turn-start-phase"; -import { handleMysteryEncounterBattleStartEffects } from "#app/data/mystery-encounters/utils/encounter-phase-utils"; +import { handleMysteryEncounterBattleStartEffects, handleMysteryEncounterTurnStartEffects } from "#app/data/mystery-encounters/utils/encounter-phase-utils"; export class TurnInitPhase extends FieldPhase { constructor(scene: BattleScene) { @@ -49,6 +49,12 @@ export class TurnInitPhase extends FieldPhase { handleMysteryEncounterBattleStartEffects(this.scene); + // If true, will skip remainder of current phase (and not queue CommandPhases etc.) + if (handleMysteryEncounterTurnStartEffects(this.scene)) { + this.end(); + return; + } + this.scene.getField().forEach((pokemon, i) => { if (pokemon?.isActive()) { if (pokemon.isPlayer()) { diff --git a/src/phases/victory-phase.ts b/src/phases/victory-phase.ts index a4d8c096c33..761a43fcb57 100644 --- a/src/phases/victory-phase.ts +++ b/src/phases/victory-phase.ts @@ -46,7 +46,7 @@ export class VictoryPhase extends PokemonPhase { let expValue = this.getPokemon().getExpValue(); if (this.scene.currentBattle.battleType === BattleType.TRAINER) { expValue = Math.floor(expValue * 1.5); - } else if (this.scene.currentBattle.battleType === BattleType.MYSTERY_ENCOUNTER) { + } else if (this.scene.currentBattle.battleType === BattleType.MYSTERY_ENCOUNTER && this.scene.currentBattle.mysteryEncounter) { expValue = Math.floor(expValue * this.scene.currentBattle.mysteryEncounter.expMultiplier); } for (const partyMember of nonFaintedPartyMembers) { diff --git a/src/test/mystery-encounter/encounters/an-offer-you-cant-refuse-encounter.test.ts b/src/test/mystery-encounter/encounters/an-offer-you-cant-refuse-encounter.test.ts index 19aa04b7a7a..a106ef7615f 100644 --- a/src/test/mystery-encounter/encounters/an-offer-you-cant-refuse-encounter.test.ts +++ b/src/test/mystery-encounter/encounters/an-offer-you-cant-refuse-encounter.test.ts @@ -140,7 +140,7 @@ describe("An Offer You Can't Refuse - Mystery Encounter", () => { await game.runToMysteryEncounter(MysteryEncounterType.AN_OFFER_YOU_CANT_REFUSE, defaultParty); await runMysteryEncounterToEnd(game, 1); - const price = scene.currentBattle.mysteryEncounter.misc.price; + const price = scene.currentBattle.mysteryEncounter!.misc.price; expect(updateMoneySpy).toHaveBeenCalledWith(scene, price); expect(scene.money).toBe(initialMoney + price); @@ -160,7 +160,7 @@ describe("An Offer You Can't Refuse - Mystery Encounter", () => { await game.runToMysteryEncounter(MysteryEncounterType.AN_OFFER_YOU_CANT_REFUSE, defaultParty); const initialPartySize = scene.getParty().length; - const pokemonName = scene.currentBattle.mysteryEncounter.misc.pokemon.name; + const pokemonName = scene.currentBattle.mysteryEncounter!.misc.pokemon.name; await runMysteryEncounterToEnd(game, 1); @@ -227,7 +227,7 @@ describe("An Offer You Can't Refuse - Mystery Encounter", () => { await game.runToMysteryEncounter(MysteryEncounterType.AN_OFFER_YOU_CANT_REFUSE, defaultParty); await runMysteryEncounterToEnd(game, 2); - const price = scene.currentBattle.mysteryEncounter.misc.price; + const price = scene.currentBattle.mysteryEncounter!.misc.price; expect(updateMoneySpy).toHaveBeenCalledWith(scene, price); expect(scene.money).toBe(initialMoney + price); diff --git a/src/test/mystery-encounter/encounters/berries-abound-encounter.test.ts b/src/test/mystery-encounter/encounters/berries-abound-encounter.test.ts index 3e772c9fb4c..ccc8b85311d 100644 --- a/src/test/mystery-encounter/encounters/berries-abound-encounter.test.ts +++ b/src/test/mystery-encounter/encounters/berries-abound-encounter.test.ts @@ -119,7 +119,7 @@ describe("Berries Abound - Mystery Encounter", () => { it("should start a fight against the boss", async () => { await game.runToMysteryEncounter(MysteryEncounterType.BERRIES_ABOUND, defaultParty); - const config = game.scene.currentBattle.mysteryEncounter.enemyPartyConfigs[0]; + const config = game.scene.currentBattle.mysteryEncounter!.enemyPartyConfigs[0]; const speciesToSpawn = config.pokemonConfigs?.[0].species.speciesId; await runMysteryEncounterToEnd(game, 1, undefined, true); @@ -133,7 +133,7 @@ describe("Berries Abound - Mystery Encounter", () => { it("should reward the player with X berries based on wave", { retry: 5 }, async () => { await game.runToMysteryEncounter(MysteryEncounterType.BERRIES_ABOUND, defaultParty); - const numBerries = game.scene.currentBattle.mysteryEncounter.misc.numBerries; + const numBerries = game.scene.currentBattle.mysteryEncounter!.misc.numBerries; scene.modifiers = []; await runMysteryEncounterToEnd(game, 1, undefined, true); @@ -179,7 +179,7 @@ describe("Berries Abound - Mystery Encounter", () => { const encounterTextSpy = vi.spyOn(EncounterDialogueUtils, "showEncounterText"); await game.runToMysteryEncounter(MysteryEncounterType.BERRIES_ABOUND, defaultParty); - const config = game.scene.currentBattle.mysteryEncounter.enemyPartyConfigs[0]; + const config = game.scene.currentBattle.mysteryEncounter!.enemyPartyConfigs[0]; const speciesToSpawn = config.pokemonConfigs?.[0].species.speciesId; // Setting enemy's level arbitrarily high to outspeed config.pokemonConfigs![0].dataSource!.level = 1000; diff --git a/src/test/mystery-encounter/encounters/clowning-around-encounter.test.ts b/src/test/mystery-encounter/encounters/clowning-around-encounter.test.ts index 273bb903f15..2b48ecc126b 100644 --- a/src/test/mystery-encounter/encounters/clowning-around-encounter.test.ts +++ b/src/test/mystery-encounter/encounters/clowning-around-encounter.test.ts @@ -199,7 +199,7 @@ describe("Clowning Around - Mystery Encounter", () => { await game.phaseInterceptor.to(SelectModifierPhase, false); expect(scene.getCurrentPhase()?.constructor.name).toBe(SelectModifierPhase.name); await game.phaseInterceptor.run(SelectModifierPhase); - const abilityToTrain = scene.currentBattle.mysteryEncounter.misc.ability; + const abilityToTrain = scene.currentBattle.mysteryEncounter?.misc.ability; game.onNextPrompt("PostMysteryEncounterPhase", Mode.MESSAGE, () => { game.scene.ui.getHandler().processInput(Button.ACTION); diff --git a/src/test/mystery-encounter/encounters/delibirdy-encounter.test.ts b/src/test/mystery-encounter/encounters/delibirdy-encounter.test.ts index 44701f71de8..969188dca06 100644 --- a/src/test/mystery-encounter/encounters/delibirdy-encounter.test.ts +++ b/src/test/mystery-encounter/encounters/delibirdy-encounter.test.ts @@ -114,7 +114,7 @@ describe("Delibird-y - Mystery Encounter", () => { await game.runToMysteryEncounter(MysteryEncounterType.DELIBIRDY, defaultParty); await runMysteryEncounterToEnd(game, 1); - const price = (scene.currentBattle.mysteryEncounter.options[0].requirements[0] as MoneyRequirement).requiredMoney; + const price = (scene.currentBattle.mysteryEncounter?.options[0].requirements[0] as MoneyRequirement).requiredMoney; expect(updateMoneySpy).toHaveBeenCalledWith(scene, -price, true, false); expect(scene.money).toBe(initialMoney - price); 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 376c05297b7..a0db6b3a448 100644 --- a/src/test/mystery-encounter/encounters/fiery-fallout-encounter.test.ts +++ b/src/test/mystery-encounter/encounters/fiery-fallout-encounter.test.ts @@ -213,7 +213,7 @@ describe("Fiery Fallout - Mystery Encounter", () => { const burnablePokemon = party.filter((pkm) => pkm.isAllowedInBattle() && !pkm.getTypes().includes(Type.FIRE)); const notBurnablePokemon = party.filter((pkm) => !pkm.isAllowedInBattle() || pkm.getTypes().includes(Type.FIRE)); - expect(scene.currentBattle.mysteryEncounter.dialogueTokens["burnedPokemon"]).toBe("Gengar"); + expect(scene.currentBattle.mysteryEncounter?.dialogueTokens["burnedPokemon"]).toBe("Gengar"); burnablePokemon.forEach((pkm) => { expect(pkm.hp, `${pkm.name} should have received 20% damage: ${pkm.hp} / ${pkm.getMaxHp()} HP`).toBe(pkm.getMaxHp() - Math.floor(pkm.getMaxHp() * 0.2)); }); diff --git a/src/test/mystery-encounter/encounters/fight-or-flight-encounter.test.ts b/src/test/mystery-encounter/encounters/fight-or-flight-encounter.test.ts index 918e8a60c1e..735dcc709bf 100644 --- a/src/test/mystery-encounter/encounters/fight-or-flight-encounter.test.ts +++ b/src/test/mystery-encounter/encounters/fight-or-flight-encounter.test.ts @@ -120,7 +120,7 @@ describe("Fight or Flight - Mystery Encounter", () => { it("should start a fight against the boss", async () => { await game.runToMysteryEncounter(MysteryEncounterType.FIGHT_OR_FLIGHT, defaultParty); - const config = game.scene.currentBattle.mysteryEncounter.enemyPartyConfigs[0]; + const config = game.scene.currentBattle.mysteryEncounter!.enemyPartyConfigs[0]; const speciesToSpawn = config.pokemonConfigs?.[0].species.speciesId; await runMysteryEncounterToEnd(game, 1, undefined, true); @@ -134,7 +134,7 @@ describe("Fight or Flight - Mystery Encounter", () => { it("should reward the player with the item based on wave", async () => { await game.runToMysteryEncounter(MysteryEncounterType.FIGHT_OR_FLIGHT, defaultParty); - const item = game.scene.currentBattle.mysteryEncounter.misc; + const item = game.scene.currentBattle.mysteryEncounter?.misc; await runMysteryEncounterToEnd(game, 1, undefined, true); await skipBattleRunMysteryEncounterRewardsPhase(game); @@ -193,7 +193,7 @@ describe("Fight or Flight - Mystery Encounter", () => { // Mock moveset scene.getParty()[0].moveset = [new PokemonMove(Moves.KNOCK_OFF)]; - const item = game.scene.currentBattle.mysteryEncounter.misc; + const item = game.scene.currentBattle.mysteryEncounter!.misc; await runMysteryEncounterToEnd(game, 2); await game.phaseInterceptor.to(SelectModifierPhase, false); diff --git a/src/test/mystery-encounter/encounters/fun-and-games-encounter.test.ts b/src/test/mystery-encounter/encounters/fun-and-games-encounter.test.ts new file mode 100644 index 00000000000..70250350af4 --- /dev/null +++ b/src/test/mystery-encounter/encounters/fun-and-games-encounter.test.ts @@ -0,0 +1,304 @@ +import * as MysteryEncounters from "#app/data/mystery-encounters/mystery-encounters"; +import { HUMAN_TRANSITABLE_BIOMES } from "#app/data/mystery-encounters/mystery-encounters"; +import { Biome } from "#app/enums/biome"; +import { MysteryEncounterType } from "#app/enums/mystery-encounter-type"; +import { Species } from "#app/enums/species"; +import GameManager from "#app/test/utils/gameManager"; +import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; +import { runMysteryEncounterToEnd, runSelectMysteryEncounterOption } from "#test/mystery-encounter/encounter-test-utils"; +import BattleScene from "#app/battle-scene"; +import { Mode } from "#app/ui/ui"; +import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode"; +import { MysteryEncounterTier } from "#enums/mystery-encounter-tier"; +import { initSceneWithoutEncounterPhase } from "#test/utils/gameManagerUtils"; +import ModifierSelectUiHandler from "#app/ui/modifier-select-ui-handler"; +import MysteryEncounter from "#app/data/mystery-encounters/mystery-encounter"; +import { Nature } from "#enums/nature"; +import { MysteryEncounterPhase } from "#app/phases/mystery-encounter-phases"; +import { CommandPhase } from "#app/phases/command-phase"; +import { SelectModifierPhase } from "#app/phases/select-modifier-phase"; +import { FunAndGamesEncounter } from "#app/data/mystery-encounters/encounters/fun-and-games-encounter"; +import { Moves } from "#enums/moves"; +import { Command } from "#app/ui/command-ui-handler"; +import * as EncounterPhaseUtils from "#app/data/mystery-encounters/utils/encounter-phase-utils"; + +const namespace = "mysteryEncounter:funAndGames"; +const defaultParty = [Species.LAPRAS, Species.GENGAR, Species.ABRA]; +const defaultBiome = Biome.CAVE; +const defaultWave = 45; + +describe("Fun And Games! - Mystery Encounter", () => { + let phaserGame: Phaser.Game; + let game: GameManager; + let scene: BattleScene; + + beforeAll(() => { + phaserGame = new Phaser.Game({ type: Phaser.HEADLESS }); + }); + + beforeEach(async () => { + game = new GameManager(phaserGame); + scene = game.scene; + game.override.mysteryEncounterChance(100); + game.override.startingWave(defaultWave); + game.override.startingBiome(defaultBiome); + game.override.disableTrainerWaves(); + + const biomeMap = new Map([ + [Biome.VOLCANO, [MysteryEncounterType.FIGHT_OR_FLIGHT]], + ]); + HUMAN_TRANSITABLE_BIOMES.forEach(biome => { + biomeMap.set(biome, [MysteryEncounterType.FUN_AND_GAMES]); + }); + vi.spyOn(MysteryEncounters, "mysteryEncountersByBiome", "get").mockReturnValue(biomeMap); + }); + + afterEach(() => { + game.phaseInterceptor.restoreOg(); + vi.clearAllMocks(); + vi.resetAllMocks(); + }); + + it("should have the correct properties", async () => { + await game.runToMysteryEncounter(MysteryEncounterType.FUN_AND_GAMES, defaultParty); + + expect(FunAndGamesEncounter.encounterType).toBe(MysteryEncounterType.FUN_AND_GAMES); + expect(FunAndGamesEncounter.encounterTier).toBe(MysteryEncounterTier.GREAT); + expect(FunAndGamesEncounter.dialogue).toBeDefined(); + expect(FunAndGamesEncounter.dialogue.intro).toStrictEqual([ + { + speaker: `${namespace}.speaker`, + text: `${namespace}.intro_dialogue`, + } + ]); + expect(FunAndGamesEncounter.dialogue.encounterOptionsDialogue?.title).toBe(`${namespace}.title`); + expect(FunAndGamesEncounter.dialogue.encounterOptionsDialogue?.description).toBe(`${namespace}.description`); + expect(FunAndGamesEncounter.dialogue.encounterOptionsDialogue?.query).toBe(`${namespace}.query`); + expect(FunAndGamesEncounter.options.length).toBe(2); + }); + + it("should not spawn outside of CIVILIZATIONN biomes", async () => { + game.override.mysteryEncounterTier(MysteryEncounterTier.GREAT); + game.override.startingBiome(Biome.VOLCANO); + await game.runToMysteryEncounter(); + + expect(scene.currentBattle?.mysteryEncounter?.encounterType).not.toBe(MysteryEncounterType.FUN_AND_GAMES); + }); + + it("should not run below wave 10", async () => { + game.override.startingWave(9); + + await game.runToMysteryEncounter(); + + expect(scene.currentBattle?.mysteryEncounter?.encounterType).not.toBe(MysteryEncounterType.FUN_AND_GAMES); + }); + + it("should not run above wave 179", async () => { + game.override.startingWave(181); + + await game.runToMysteryEncounter(); + + expect(scene.currentBattle.mysteryEncounter).toBeUndefined(); + }); + + it("should initialize fully", async () => { + initSceneWithoutEncounterPhase(scene, defaultParty); + scene.currentBattle.mysteryEncounter = new MysteryEncounter(FunAndGamesEncounter); + const encounter = scene.currentBattle.mysteryEncounter!; + scene.currentBattle.waveIndex = defaultWave; + + const { onInit } = encounter; + + expect(encounter.onInit).toBeDefined(); + + const onInitResult = onInit!(scene); + expect(onInitResult).toBe(true); + }); + + describe("Option 1 - Play the Wobbuffet game", () => { + it("should have the correct properties", () => { + const option = FunAndGamesEncounter.options[0]; + expect(option.optionMode).toBe(MysteryEncounterOptionMode.DISABLED_OR_DEFAULT); + expect(option.dialogue).toBeDefined(); + expect(option.dialogue).toStrictEqual({ + buttonLabel: `${namespace}.option.1.label`, + buttonTooltip: `${namespace}.option.1.tooltip`, + selected: [ + { + text: `${namespace}.option.1.selected`, + }, + ], + }); + }); + + it("should NOT be selectable if the player doesn't have enough money", async () => { + game.scene.money = 0; + await game.runToMysteryEncounter(MysteryEncounterType.FUN_AND_GAMES, defaultParty); + await game.phaseInterceptor.to(MysteryEncounterPhase, false); + + const encounterPhase = scene.getCurrentPhase(); + expect(encounterPhase?.constructor.name).toBe(MysteryEncounterPhase.name); + const mysteryEncounterPhase = encounterPhase as MysteryEncounterPhase; + vi.spyOn(mysteryEncounterPhase, "continueEncounter"); + vi.spyOn(mysteryEncounterPhase, "handleOptionSelect"); + vi.spyOn(scene.ui, "playError"); + + await runSelectMysteryEncounterOption(game, 1); + + expect(scene.getCurrentPhase()?.constructor.name).toBe(MysteryEncounterPhase.name); + expect(scene.ui.playError).not.toHaveBeenCalled(); // No error sfx, option is disabled + expect(mysteryEncounterPhase.handleOptionSelect).not.toHaveBeenCalled(); + expect(mysteryEncounterPhase.continueEncounter).not.toHaveBeenCalled(); + }); + + it("should get 3 turns to attack the Wobbuffet for a reward", async () => { + scene.money = 20000; + game.override.moveset([Moves.TACKLE]); + await game.runToMysteryEncounter(MysteryEncounterType.FUN_AND_GAMES, defaultParty); + await runMysteryEncounterToEnd(game, 1, { pokemonNo: 1 }, true); + + expect(scene.getCurrentPhase()?.constructor.name).toBe(CommandPhase.name); + expect(scene.getEnemyPokemon()?.species.speciesId).toBe(Species.WOBBUFFET); + expect(scene.getEnemyPokemon()?.ivs).toEqual([0, 0, 0, 0, 0, 0]); + expect(scene.getEnemyPokemon()?.nature).toBe(Nature.MILD); + + game.onNextPrompt("MessagePhase", Mode.MESSAGE, () => { + game.endPhase(); + }); + + // Turn 1 + (game.scene.getCurrentPhase() as CommandPhase).handleCommand(Command.FIGHT, 0, false); + await game.phaseInterceptor.to(CommandPhase); + + // Turn 2 + (game.scene.getCurrentPhase() as CommandPhase).handleCommand(Command.FIGHT, 0, false); + await game.phaseInterceptor.to(CommandPhase); + + // Turn 3 + (game.scene.getCurrentPhase() as CommandPhase).handleCommand(Command.FIGHT, 0, false); + await game.phaseInterceptor.to(SelectModifierPhase, false); + + // Rewards + expect(scene.getCurrentPhase()?.constructor.name).toBe(SelectModifierPhase.name); + }); + + it("should have no items in rewards if Wubboffet doesn't take enough damage", async () => { + scene.money = 20000; + await game.runToMysteryEncounter(MysteryEncounterType.FUN_AND_GAMES, defaultParty); + await runMysteryEncounterToEnd(game, 1, { pokemonNo: 1 }, true); + + expect(scene.getCurrentPhase()?.constructor.name).toBe(CommandPhase.name); + game.onNextPrompt("MessagePhase", Mode.MESSAGE, () => { + game.endPhase(); + }); + + // Skip minigame + scene.currentBattle.mysteryEncounter!.misc.turnsRemaining = 0; + (game.scene.getCurrentPhase() as CommandPhase).handleCommand(Command.FIGHT, 0, false); + await game.phaseInterceptor.to(SelectModifierPhase, false); + + // Rewards + expect(scene.getCurrentPhase()?.constructor.name).toBe(SelectModifierPhase.name); + await game.phaseInterceptor.run(SelectModifierPhase); + + expect(scene.ui.getMode()).to.equal(Mode.MODIFIER_SELECT); + const modifierSelectHandler = scene.ui.handlers.find(h => h instanceof ModifierSelectUiHandler) as ModifierSelectUiHandler; + expect(modifierSelectHandler.options.length).toEqual(0); + }); + + it("should have Wide Lens item in rewards if Wubboffet is at 15-33% HP remaining", async () => { + scene.money = 20000; + game.override.moveset([Moves.SPLASH]); + await game.runToMysteryEncounter(MysteryEncounterType.FUN_AND_GAMES, defaultParty); + await runMysteryEncounterToEnd(game, 1, { pokemonNo: 1 }, true); + + expect(scene.getCurrentPhase()?.constructor.name).toBe(CommandPhase.name); + game.onNextPrompt("MessagePhase", Mode.MESSAGE, () => { + game.endPhase(); + }); + + // Skip minigame + const wobbuffet = scene.getEnemyPokemon()!; + wobbuffet.hp = Math.floor(0.2 * wobbuffet.getMaxHp()); + scene.currentBattle.mysteryEncounter!.misc.turnsRemaining = 0; + (game.scene.getCurrentPhase() as CommandPhase).handleCommand(Command.FIGHT, 0, false); + await game.phaseInterceptor.to(SelectModifierPhase, false); + + // Rewards + expect(scene.getCurrentPhase()?.constructor.name).toBe(SelectModifierPhase.name); + await game.phaseInterceptor.run(SelectModifierPhase); + + expect(scene.ui.getMode()).to.equal(Mode.MODIFIER_SELECT); + const modifierSelectHandler = scene.ui.handlers.find(h => h instanceof ModifierSelectUiHandler) as ModifierSelectUiHandler; + expect(modifierSelectHandler.options.length).toEqual(1); + expect(modifierSelectHandler.options[0].modifierTypeOption.type.id).toEqual("WIDE_LENS"); + }); + + it("should have Scope Lens item in rewards if Wubboffet is at 3-15% HP remaining", async () => { + scene.money = 20000; + game.override.moveset([Moves.SPLASH]); + await game.runToMysteryEncounter(MysteryEncounterType.FUN_AND_GAMES, defaultParty); + await runMysteryEncounterToEnd(game, 1, { pokemonNo: 1 }, true); + + expect(scene.getCurrentPhase()?.constructor.name).toBe(CommandPhase.name); + game.onNextPrompt("MessagePhase", Mode.MESSAGE, () => { + game.endPhase(); + }); + + // Skip minigame + const wobbuffet = scene.getEnemyPokemon()!; + wobbuffet.hp = Math.floor(0.1 * wobbuffet.getMaxHp()); + scene.currentBattle.mysteryEncounter!.misc.turnsRemaining = 0; + (game.scene.getCurrentPhase() as CommandPhase).handleCommand(Command.FIGHT, 0, false); + await game.phaseInterceptor.to(SelectModifierPhase, false); + + // Rewards + expect(scene.getCurrentPhase()?.constructor.name).toBe(SelectModifierPhase.name); + await game.phaseInterceptor.run(SelectModifierPhase); + + expect(scene.ui.getMode()).to.equal(Mode.MODIFIER_SELECT); + const modifierSelectHandler = scene.ui.handlers.find(h => h instanceof ModifierSelectUiHandler) as ModifierSelectUiHandler; + expect(modifierSelectHandler.options.length).toEqual(1); + expect(modifierSelectHandler.options[0].modifierTypeOption.type.id).toEqual("SCOPE_LENS"); + }); + + it("should have Multi Lens item in rewards if Wubboffet is at <3% HP remaining", async () => { + scene.money = 20000; + game.override.moveset([Moves.SPLASH]); + await game.runToMysteryEncounter(MysteryEncounterType.FUN_AND_GAMES, defaultParty); + await runMysteryEncounterToEnd(game, 1, { pokemonNo: 1 }, true); + + expect(scene.getCurrentPhase()?.constructor.name).toBe(CommandPhase.name); + game.onNextPrompt("MessagePhase", Mode.MESSAGE, () => { + game.endPhase(); + }); + + // Skip minigame + const wobbuffet = scene.getEnemyPokemon()!; + wobbuffet.hp = 1; + scene.currentBattle.mysteryEncounter!.misc.turnsRemaining = 0; + (game.scene.getCurrentPhase() as CommandPhase).handleCommand(Command.FIGHT, 0, false); + await game.phaseInterceptor.to(SelectModifierPhase, false); + + // Rewards + expect(scene.getCurrentPhase()?.constructor.name).toBe(SelectModifierPhase.name); + await game.phaseInterceptor.run(SelectModifierPhase); + + expect(scene.ui.getMode()).to.equal(Mode.MODIFIER_SELECT); + const modifierSelectHandler = scene.ui.handlers.find(h => h instanceof ModifierSelectUiHandler) as ModifierSelectUiHandler; + expect(modifierSelectHandler.options.length).toEqual(1); + expect(modifierSelectHandler.options[0].modifierTypeOption.type.id).toEqual("MULTI_LENS"); + }); + }); + + describe("Option 2 - Leave", () => { + it("should leave encounter without battle", async () => { + const leaveEncounterWithoutBattleSpy = vi.spyOn(EncounterPhaseUtils, "leaveEncounterWithoutBattle"); + + await game.runToMysteryEncounter(MysteryEncounterType.FUN_AND_GAMES, defaultParty); + await runMysteryEncounterToEnd(game, 2); + + expect(leaveEncounterWithoutBattleSpy).toBeCalled(); + }); + }); +}); diff --git a/src/test/mystery-encounter/encounters/lost-at-sea-encounter.test.ts b/src/test/mystery-encounter/encounters/lost-at-sea-encounter.test.ts index f2ea020ca88..f8ad13eb046 100644 --- a/src/test/mystery-encounter/encounters/lost-at-sea-encounter.test.ts +++ b/src/test/mystery-encounter/encounters/lost-at-sea-encounter.test.ts @@ -70,7 +70,7 @@ describe("Lost at Sea - Mystery Encounter", () => { game.override.startingBiome(Biome.MOUNTAIN); await game.runToMysteryEncounter(); - expect(game.scene.currentBattle.mysteryEncounter.encounterType).not.toBe(MysteryEncounterType.LOST_AT_SEA); + expect(game.scene.currentBattle.mysteryEncounter?.encounterType).not.toBe(MysteryEncounterType.LOST_AT_SEA); }); it("should not run below wave 11", async () => { diff --git a/src/test/mystery-encounter/encounters/mysterious-challengers-encounter.test.ts b/src/test/mystery-encounter/encounters/mysterious-challengers-encounter.test.ts index 996149046b1..7f5e88a68c2 100644 --- a/src/test/mystery-encounter/encounters/mysterious-challengers-encounter.test.ts +++ b/src/test/mystery-encounter/encounters/mysterious-challengers-encounter.test.ts @@ -98,7 +98,7 @@ describe("Mysterious Challengers - Mystery Encounter", () => { it("should initialize fully", async () => { initSceneWithoutEncounterPhase(scene, defaultParty); scene.currentBattle.mysteryEncounter = new MysteryEncounter(MysteriousChallengersEncounter); - const encounter = scene.currentBattle.mysteryEncounter; + const encounter = scene.currentBattle.mysteryEncounter!; scene.currentBattle.waveIndex = defaultWave; const { onInit } = encounter; @@ -162,7 +162,7 @@ describe("Mysterious Challengers - Mystery Encounter", () => { expect(scene.getCurrentPhase()?.constructor.name).toBe(CommandPhase.name); expect(scene.currentBattle.trainer).toBeDefined(); - expect(scene.currentBattle.mysteryEncounter.encounterMode).toBe(MysteryEncounterMode.TRAINER_BATTLE); + expect(scene.currentBattle.mysteryEncounter?.encounterMode).toBe(MysteryEncounterMode.TRAINER_BATTLE); }); it("should have normal trainer rewards after battle", async () => { @@ -204,7 +204,7 @@ describe("Mysterious Challengers - Mystery Encounter", () => { expect(scene.getCurrentPhase()?.constructor.name).toBe(CommandPhase.name); expect(scene.currentBattle.trainer).toBeDefined(); - expect(scene.currentBattle.mysteryEncounter.encounterMode).toBe(MysteryEncounterMode.TRAINER_BATTLE); + expect(scene.currentBattle.mysteryEncounter?.encounterMode).toBe(MysteryEncounterMode.TRAINER_BATTLE); }); it("should have hard trainer rewards after battle", async () => { @@ -247,7 +247,7 @@ describe("Mysterious Challengers - Mystery Encounter", () => { expect(scene.getCurrentPhase()?.constructor.name).toBe(CommandPhase.name); expect(scene.currentBattle.trainer).toBeDefined(); - expect(scene.currentBattle.mysteryEncounter.encounterMode).toBe(MysteryEncounterMode.TRAINER_BATTLE); + expect(scene.currentBattle.mysteryEncounter?.encounterMode).toBe(MysteryEncounterMode.TRAINER_BATTLE); }); it("should have brutal trainer rewards after battle", async () => { diff --git a/src/test/mystery-encounter/encounters/the-pokemon-salesman-encounter.test.ts b/src/test/mystery-encounter/encounters/the-pokemon-salesman-encounter.test.ts index 35fbd0b51af..9bc5f508e68 100644 --- a/src/test/mystery-encounter/encounters/the-pokemon-salesman-encounter.test.ts +++ b/src/test/mystery-encounter/encounters/the-pokemon-salesman-encounter.test.ts @@ -142,7 +142,7 @@ describe("The Pokemon Salesman - Mystery Encounter", () => { await game.runToMysteryEncounter(MysteryEncounterType.THE_POKEMON_SALESMAN, defaultParty); await runMysteryEncounterToEnd(game, 1); - const price = scene.currentBattle.mysteryEncounter.misc.price; + const price = scene.currentBattle.mysteryEncounter!.misc.price; expect(updateMoneySpy).toHaveBeenCalledWith(scene, -price, true, false); expect(scene.money).toBe(initialMoney - price); @@ -153,7 +153,7 @@ describe("The Pokemon Salesman - Mystery Encounter", () => { await game.runToMysteryEncounter(MysteryEncounterType.THE_POKEMON_SALESMAN, defaultParty); const initialPartySize = scene.getParty().length; - const pokemonName = scene.currentBattle.mysteryEncounter.misc.pokemon.name; + const pokemonName = scene.currentBattle.mysteryEncounter!.misc.pokemon.name; await runMysteryEncounterToEnd(game, 1); diff --git a/src/test/mystery-encounter/encounters/the-winstrate-challenge-encounter.test.ts b/src/test/mystery-encounter/encounters/the-winstrate-challenge-encounter.test.ts index e21bb08f165..7b3b6137054 100644 --- a/src/test/mystery-encounter/encounters/the-winstrate-challenge-encounter.test.ts +++ b/src/test/mystery-encounter/encounters/the-winstrate-challenge-encounter.test.ts @@ -109,7 +109,7 @@ describe("The Winstrate Challenge - Mystery Encounter", () => { it("should initialize fully", async () => { initSceneWithoutEncounterPhase(scene, defaultParty); scene.currentBattle.mysteryEncounter = new MysteryEncounter(TheWinstrateChallengeEncounter); - const encounter = scene.currentBattle.mysteryEncounter; + const encounter = scene.currentBattle.mysteryEncounter!; scene.currentBattle.waveIndex = defaultWave; const { onInit } = encounter; @@ -281,32 +281,32 @@ describe("The Winstrate Challenge - Mystery Encounter", () => { expect(scene.getCurrentPhase()?.constructor.name).toBe(CommandPhase.name); expect(scene.currentBattle.trainer).toBeDefined(); expect(scene.currentBattle.trainer!.config.trainerType).toBe(TrainerType.VICTOR); - expect(scene.currentBattle.mysteryEncounter.enemyPartyConfigs.length).toBe(4); - expect(scene.currentBattle.mysteryEncounter.encounterMode).toBe(MysteryEncounterMode.TRAINER_BATTLE); + expect(scene.currentBattle.mysteryEncounter?.enemyPartyConfigs.length).toBe(4); + expect(scene.currentBattle.mysteryEncounter?.encounterMode).toBe(MysteryEncounterMode.TRAINER_BATTLE); await skipBattleToNextBattle(game); expect(scene.currentBattle.trainer).toBeDefined(); expect(scene.currentBattle.trainer!.config.trainerType).toBe(TrainerType.VICTORIA); - expect(scene.currentBattle.mysteryEncounter.enemyPartyConfigs.length).toBe(3); - expect(scene.currentBattle.mysteryEncounter.encounterMode).toBe(MysteryEncounterMode.TRAINER_BATTLE); + expect(scene.currentBattle.mysteryEncounter?.enemyPartyConfigs.length).toBe(3); + expect(scene.currentBattle.mysteryEncounter?.encounterMode).toBe(MysteryEncounterMode.TRAINER_BATTLE); await skipBattleToNextBattle(game); expect(scene.currentBattle.trainer).toBeDefined(); expect(scene.currentBattle.trainer!.config.trainerType).toBe(TrainerType.VIVI); - expect(scene.currentBattle.mysteryEncounter.enemyPartyConfigs.length).toBe(2); - expect(scene.currentBattle.mysteryEncounter.encounterMode).toBe(MysteryEncounterMode.TRAINER_BATTLE); + expect(scene.currentBattle.mysteryEncounter?.enemyPartyConfigs.length).toBe(2); + expect(scene.currentBattle.mysteryEncounter?.encounterMode).toBe(MysteryEncounterMode.TRAINER_BATTLE); await skipBattleToNextBattle(game); expect(scene.currentBattle.trainer).toBeDefined(); expect(scene.currentBattle.trainer!.config.trainerType).toBe(TrainerType.VICKY); - expect(scene.currentBattle.mysteryEncounter.enemyPartyConfigs.length).toBe(1); - expect(scene.currentBattle.mysteryEncounter.encounterMode).toBe(MysteryEncounterMode.TRAINER_BATTLE); + expect(scene.currentBattle.mysteryEncounter?.enemyPartyConfigs.length).toBe(1); + expect(scene.currentBattle.mysteryEncounter?.encounterMode).toBe(MysteryEncounterMode.TRAINER_BATTLE); await skipBattleToNextBattle(game); expect(scene.currentBattle.trainer).toBeDefined(); expect(scene.currentBattle.trainer!.config.trainerType).toBe(TrainerType.VITO); - expect(scene.currentBattle.mysteryEncounter.enemyPartyConfigs.length).toBe(0); - expect(scene.currentBattle.mysteryEncounter.encounterMode).toBe(MysteryEncounterMode.TRAINER_BATTLE); + expect(scene.currentBattle.mysteryEncounter?.enemyPartyConfigs.length).toBe(0); + expect(scene.currentBattle.mysteryEncounter?.encounterMode).toBe(MysteryEncounterMode.TRAINER_BATTLE); // Should have Macho Brace in the rewards await skipBattleToNextBattle(game, true); diff --git a/src/test/mystery-encounter/mystery-encounter-utils.test.ts b/src/test/mystery-encounter/mystery-encounter-utils.test.ts index baeef631e53..d15a34afa59 100644 --- a/src/test/mystery-encounter/mystery-encounter-utils.test.ts +++ b/src/test/mystery-encounter/mystery-encounter-utils.test.ts @@ -286,7 +286,7 @@ describe("Mystery Encounter Utils", () => { scene.currentBattle.mysteryEncounter.setDialogueToken("test", "value"); const spy = vi.spyOn(game.scene.ui, "showText"); - showEncounterText(scene, "mysteryEncounter:unit_test_dialogue"); + await showEncounterText(scene, "mysteryEncounter:unit_test_dialogue"); expect(spy).toHaveBeenCalledWith("valuevalue {{testvalue}} {{test1}} {{test}} {{test\\}} {{test\\}} {test}}", null, expect.any(Function), 0, true); }); }); @@ -297,7 +297,7 @@ describe("Mystery Encounter Utils", () => { scene.currentBattle.mysteryEncounter.setDialogueToken("test", "value"); const spy = vi.spyOn(game.scene.ui, "showDialogue"); - showEncounterDialogue(scene, "mysteryEncounter:unit_test_dialogue", "mysteryEncounter:unit_test_dialogue"); + await showEncounterDialogue(scene, "mysteryEncounter:unit_test_dialogue", "mysteryEncounter:unit_test_dialogue"); expect(spy).toHaveBeenCalledWith("valuevalue {{testvalue}} {{test1}} {{test}} {{test\\}} {{test\\}} {test}}", "valuevalue {{testvalue}} {{test1}} {{test}} {{test\\}} {{test\\}} {test}}", null, expect.any(Function), 0); }); }); diff --git a/src/ui/fight-ui-handler.ts b/src/ui/fight-ui-handler.ts index 0beaddbb517..cf3f3cc4e8a 100644 --- a/src/ui/fight-ui-handler.ts +++ b/src/ui/fight-ui-handler.ts @@ -10,6 +10,7 @@ import i18next from "i18next"; import {Button} from "#enums/buttons"; import Pokemon, { PokemonMove } from "#app/field/pokemon.js"; import { CommandPhase } from "#app/phases/command-phase.js"; +import { BattleType } from "#app/battle"; export default class FightUiHandler extends UiHandler { public static readonly MOVES_CONTAINER_NAME = "moves"; @@ -116,8 +117,11 @@ export default class FightUiHandler extends UiHandler { ui.playError(); } } else { - ui.setMode(Mode.COMMAND, this.fieldIndex); - success = true; + // Cannot back out of fight menu if skipToFightInput is enabled + if (this.scene.currentBattle.battleType !== BattleType.MYSTERY_ENCOUNTER || !this.scene.currentBattle.mysteryEncounter?.skipToFightInput) { + ui.setMode(Mode.COMMAND, this.fieldIndex); + success = true; + } } } else { switch (button) { diff --git a/src/ui/mystery-encounter-ui-handler.ts b/src/ui/mystery-encounter-ui-handler.ts index a93167c8a96..9847fe5ff44 100644 --- a/src/ui/mystery-encounter-ui-handler.ts +++ b/src/ui/mystery-encounter-ui-handler.ts @@ -326,7 +326,7 @@ export default class MysteryEncounterUiHandler extends UiHandler { displayEncounterOptions(slideInDescription: boolean = true): void { this.getUi().clearText(); - const mysteryEncounter = this.scene.currentBattle.mysteryEncounter; + const mysteryEncounter = this.scene.currentBattle.mysteryEncounter!; this.encounterOptions = this.overrideSettings?.overrideOptions ?? mysteryEncounter.options; this.optionsMeetsReqs = [];