From da0aea0d1e6213c04c4d3c2c6345111d101898ce Mon Sep 17 00:00:00 2001 From: ImperialSympathizer Date: Sun, 14 Jul 2024 17:37:03 -0400 Subject: [PATCH] safari encounter and sprite offset positioning updates --- .../images/mystery-encounters/chest_blue.json | 169 ++-- .../images/mystery-encounters/chest_blue.png | Bin 2723 -> 2671 bytes .../encounters/mysterious-chest-encounter.ts | 52 +- .../encounters/safari-zone-encounter.ts | 913 +++++++++++++++--- .../shady-vitamin-dealer-encounter.ts | 10 +- .../mystery-encounter-utils.ts | 24 +- .../mystery-encounters/mystery-encounter.ts | 15 +- .../mystery-encounters/mystery-encounters.ts | 2 + src/field/mystery-encounter-intro.ts | 11 +- src/locales/en/battle.ts | 3 + src/locales/en/mystery-encounter.ts | 38 +- src/phases.ts | 6 +- src/phases/mystery-encounter-phase.ts | 39 +- src/pipelines/sprite.ts | 9 +- src/ui/mystery-encounter-ui-handler.ts | 45 +- 15 files changed, 1048 insertions(+), 288 deletions(-) diff --git a/public/images/mystery-encounters/chest_blue.json b/public/images/mystery-encounters/chest_blue.json index 88aadda845a..9a386802e03 100644 --- a/public/images/mystery-encounters/chest_blue.json +++ b/public/images/mystery-encounters/chest_blue.json @@ -4,177 +4,198 @@ "image": "chest_blue.png", "format": "RGBA8888", "size": { - "w": 300, - "h": 75 + "w": 58, + "h": 528 }, "scale": 1, "frames": [ { - "filename": "0001.png", + "filename": "0000.png", "rotated": false, - "trimmed": false, + "trimmed": true, "sourceSize": { "w": 75, "h": 75 }, "spriteSourceSize": { - "x": 0, - "y": 0, + "x": 14, + "y": 30, + "w": 48, + "h": 41 + }, + "frame": { + "x": 1, + "y": 1, + "w": 48, + "h": 41 + } + }, + { + "filename": "0001.png", + "rotated": false, + "trimmed": true, + "sourceSize": { "w": 75, "h": 75 }, + "spriteSourceSize": { + "x": 14, + "y": 34, + "w": 49, + "h": 37 + }, "frame": { - "x": -15, - "y": 0, - "w": 75, - "h": 75 + "x": 1, + "y": 44, + "w": 49, + "h": 37 } }, { "filename": "0002.png", "rotated": false, - "trimmed": false, + "trimmed": true, "sourceSize": { "w": 75, "h": 75 }, "spriteSourceSize": { - "x": 0, - "y": 0, - "w": 75, - "h": 75 + "x": 14, + "y": 30, + "w": 48, + "h": 41 }, "frame": { - "x": -15, - "y": 0, - "w": 75, - "h": 75 + "x": 1, + "y": 83, + "w": 48, + "h": 41 } }, { "filename": "0003.png", "rotated": false, - "trimmed": false, + "trimmed": true, "sourceSize": { "w": 75, "h": 75 }, "spriteSourceSize": { - "x": 0, - "y": 0, - "w": 75, - "h": 75 + "x": 14, + "y": 23, + "w": 48, + "h": 48 }, "frame": { - "x": 57, - "y": 0, - "w": 75, - "h": 75 + "x": 1, + "y": 126, + "w": 48, + "h": 48 } }, { "filename": "0004.png", "rotated": false, - "trimmed": false, + "trimmed": true, "sourceSize": { "w": 75, "h": 75 }, "spriteSourceSize": { - "x": 0, - "y": 0, - "w": 75, - "h": 75 + "x": 13, + "y": 4, + "w": 55, + "h": 67 }, "frame": { - "x": 57, - "y": 0, - "w": 75, - "h": 75 + "x": 1, + "y": 176, + "w": 55, + "h": 67 } }, { "filename": "0005.png", "rotated": false, - "trimmed": false, + "trimmed": true, "sourceSize": { "w": 75, "h": 75 }, "spriteSourceSize": { - "x": 0, - "y": 0, - "w": 75, - "h": 75 + "x": 15, + "y": 2, + "w": 56, + "h": 69 }, "frame": { - "x": 129, - "y": 0, - "w": 75, - "h": 75 + "x": 1, + "y": 245, + "w": 56, + "h": 69 } }, { "filename": "0006.png", "rotated": false, - "trimmed": false, + "trimmed": true, "sourceSize": { "w": 75, "h": 75 }, "spriteSourceSize": { - "x": 0, - "y": 0, - "w": 75, - "h": 75 + "x": 15, + "y": 2, + "w": 56, + "h": 69 }, "frame": { - "x": 129, - "y": 0, - "w": 75, - "h": 75 + "x": 1, + "y": 316, + "w": 56, + "h": 69 } }, { "filename": "0007.png", "rotated": false, - "trimmed": false, + "trimmed": true, "sourceSize": { "w": 75, "h": 75 }, "spriteSourceSize": { - "x": 0, - "y": 0, - "w": 75, - "h": 75 + "x": 13, + "y": 2, + "w": 56, + "h": 69 }, "frame": { - "x": 201, - "y": 0, - "w": 75, - "h": 75 + "x": 1, + "y": 387, + "w": 56, + "h": 69 } }, { "filename": "0008.png", "rotated": false, - "trimmed": false, + "trimmed": true, "sourceSize": { "w": 75, "h": 75 }, "spriteSourceSize": { - "x": 0, - "y": 0, - "w": 75, - "h": 75 + "x": 13, + "y": 2, + "w": 56, + "h": 69 }, "frame": { - "x": 201, - "y": 0, - "w": 75, - "h": 75 + "x": 1, + "y": 458, + "w": 56, + "h": 69 } } ] @@ -183,6 +204,6 @@ "meta": { "app": "https://www.codeandweb.com/texturepacker", "version": "3.0", - "smartupdate": "$TexturePacker:SmartUpdate:895f0a79b89fa0fb44167f4584fd9a22:357b46953b7e17c6b2f43a62d52855d8:cc1ed0e4f90aaa9dcf1b39a0af1283b0$" + "smartupdate": "$TexturePacker:SmartUpdate:5f36000f6160ee6f397afe5a6fd60b73:cf6f4b08e23400447813583c322eb6c7:f4f3c064e6c93b8d1290f93bee927f60$" } } diff --git a/public/images/mystery-encounters/chest_blue.png b/public/images/mystery-encounters/chest_blue.png index 0097346e845a2a21d52fb840704f7566b2f23921..ac1039544e3249789b3ece452203dfdde1f6933a 100644 GIT binary patch literal 2671 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 diff --git a/src/data/mystery-encounters/encounters/mysterious-chest-encounter.ts b/src/data/mystery-encounters/encounters/mysterious-chest-encounter.ts index 49672ee636c..ae6c14f725e 100644 --- a/src/data/mystery-encounters/encounters/mysterious-chest-encounter.ts +++ b/src/data/mystery-encounters/encounters/mysterious-chest-encounter.ts @@ -30,7 +30,8 @@ export const MysteriousChestEncounter: IMysteryEncounter = fileRoot: "mystery-encounters", hasShadow: true, x: 4, - y: 8, + y: 10, + yShadowOffset: 3, disableAnimation: true, // Re-enabled after option select }, ]) @@ -75,10 +76,7 @@ export const MysteriousChestEncounter: IMysteryEncounter = ], }); // Display result message then proceed to rewards - queueEncounterMessage( - scene, - "mysteryEncounter:mysterious_chest_option_1_normal_result" - ); + queueEncounterMessage(scene, "mysteryEncounter:mysterious_chest_option_1_normal_result"); leaveEncounterWithoutBattle(scene); } else if (roll > 40) { // Choose between 3 ULTRA tier items (20%) @@ -90,10 +88,7 @@ export const MysteriousChestEncounter: IMysteryEncounter = ], }); // Display result message then proceed to rewards - queueEncounterMessage( - scene, - "mysteryEncounter:mysterious_chest_option_1_good_result" - ); + queueEncounterMessage(scene, "mysteryEncounter:mysterious_chest_option_1_good_result"); leaveEncounterWithoutBattle(scene); } else if (roll > 36) { // Choose between 2 ROGUE tier items (4%) @@ -101,10 +96,7 @@ export const MysteriousChestEncounter: IMysteryEncounter = guaranteedModifierTiers: [ModifierTier.ROGUE, ModifierTier.ROGUE], }); // Display result message then proceed to rewards - queueEncounterMessage( - scene, - "mysteryEncounter:mysterious_chest_option_1_great_result" - ); + queueEncounterMessage(scene, "mysteryEncounter:mysterious_chest_option_1_great_result"); leaveEncounterWithoutBattle(scene); } else if (roll > 35) { // Choose 1 MASTER tier item (1%) @@ -112,10 +104,7 @@ export const MysteriousChestEncounter: IMysteryEncounter = guaranteedModifierTiers: [ModifierTier.MASTER], }); // Display result message then proceed to rewards - queueEncounterMessage( - scene, - "mysteryEncounter:mysterious_chest_option_1_amazing_result" - ); + queueEncounterMessage(scene, "mysteryEncounter:mysterious_chest_option_1_amazing_result"); leaveEncounterWithoutBattle(scene); } else { // Your highest level unfainted Pok�mon gets OHKO. Progress with no rewards (35%) @@ -125,27 +114,22 @@ export const MysteriousChestEncounter: IMysteryEncounter = ); koPlayerPokemon(highestLevelPokemon); - scene.currentBattle.mysteryEncounter.setDialogueToken( - "pokeName", - highestLevelPokemon.name - ); + scene.currentBattle.mysteryEncounter.setDialogueToken("pokeName", highestLevelPokemon.name); // Show which Pokemon was KOed, then leave encounter with no rewards // Does this synchronously so that game over doesn't happen over result message - await showEncounterText( - scene, - "mysteryEncounter:mysterious_chest_option_1_bad_result" - ).then(() => { - if ( - scene.getParty().filter((p) => p.isAllowedInBattle()).length === + await showEncounterText(scene, "mysteryEncounter:mysterious_chest_option_1_bad_result") + .then(() => { + if ( + scene.getParty().filter((p) => p.isAllowedInBattle()).length === 0 - ) { + ) { // All pokemon fainted, game over - scene.clearPhaseQueue(); - scene.unshiftPhase(new GameOverPhase(scene)); - } else { - leaveEncounterWithoutBattle(scene); - } - }); + scene.clearPhaseQueue(); + scene.unshiftPhase(new GameOverPhase(scene)); + } else { + leaveEncounterWithoutBattle(scene); + } + }); } }) .build() diff --git a/src/data/mystery-encounters/encounters/safari-zone-encounter.ts b/src/data/mystery-encounters/encounters/safari-zone-encounter.ts index e2dd7849a49..000c5f24f8d 100644 --- a/src/data/mystery-encounters/encounters/safari-zone-encounter.ts +++ b/src/data/mystery-encounters/encounters/safari-zone-encounter.ts @@ -1,31 +1,167 @@ -import { - getHighestLevelPlayerPokemon, - koPlayerPokemon, - leaveEncounterWithoutBattle, - queueEncounterMessage, - setEncounterRewards, - showEncounterText, -} from "#app/data/mystery-encounters/mystery-encounter-utils"; -import { ModifierTier } from "#app/modifier/modifier-tier"; -import { GameOverPhase } from "#app/phases"; -import { randSeedInt } from "#app/utils"; +import { getRandomSpeciesByStarterTier, initFollowupOptionSelect, leaveEncounterWithoutBattle, updatePlayerMoney, } from "#app/data/mystery-encounters/mystery-encounter-utils"; import { MysteryEncounterType } from "#enums/mystery-encounter-type"; import BattleScene from "../../../battle-scene"; -import IMysteryEncounter, { - MysteryEncounterBuilder, - MysteryEncounterTier, -} from "../mystery-encounter"; -import { EncounterOptionMode, MysteryEncounterOptionBuilder } from "../mystery-encounter-option"; +import IMysteryEncounter, { MysteryEncounterBuilder, MysteryEncounterTier, MysteryEncounterVariant } from "../mystery-encounter"; +import MysteryEncounterOption, { EncounterOptionMode, MysteryEncounterOptionBuilder } from "#app/data/mystery-encounters/mystery-encounter-option"; +import { TrainerSlot } from "#app/data/trainer-config"; +import { ScanIvsPhase, SummonPhase, VictoryPhase } from "#app/phases"; +import i18next from "i18next"; +import { HiddenAbilityRateBoosterModifier, IvScannerModifier, PokemonHeldItemModifier } from "#app/modifier/modifier"; +import { EnemyPokemon } from "#app/field/pokemon"; +import { doPokeballBounceAnim, getPokeballAtlasKey, getPokeballTintColor, PokeballType } from "#app/data/pokeball"; +import { StatusEffect } from "#app/data/status-effect"; +import { addPokeballCaptureStars, addPokeballOpenParticles } from "#app/field/anims"; +import { achvs } from "#app/system/achv"; +import { Mode } from "#app/ui/ui"; +import { PartyOption, PartyUiMode } from "#app/ui/party-ui-handler"; +import { BattlerIndex } from "#app/battle"; +import { PlayerGender } from "#enums/player-gender"; +import { IntegerHolder, randSeedInt } from "#app/utils"; +import { getPokemonSpecies } from "#app/data/pokemon-species"; +import { MoneyRequirement } from "#app/data/mystery-encounters/mystery-encounter-requirements"; /** the i18n namespace for the encounter */ const namespace = "mysteryEncounter:safari_zone"; +/** + * SAFARI ZONE OPTIONS + * + * Catch and flee rate **multipliers** are calculated in the same way stat changes are (they range from -6/+6) + * https://bulbapedia.bulbagarden.net/wiki/Catch_rate#Great_Marsh_and_Johto_Safari_Zone + * + * Catch Rate calculation: + * catchRate = speciesCatchRate [1 to 255] * catchStageMultiplier [2/8 to 8/2] * ballCatchRate [1.5] + * + * Flee calculation: + * The harder a species is to catch, the higher its flee rate is + * (Caps at 50% base chance to flee for the hardest to catch Pokemon, before factoring in flee stage) + * fleeRate = ((255^2 - speciesCatchRate^2) / 255 / 2) [0 to 127.5] * fleeStageMultiplier [2/8 to 8/2] + * Flee chance = fleeRate / 255 + */ +const safariZoneOptions: MysteryEncounterOption[] = [ + new MysteryEncounterOptionBuilder() + .withOptionMode(EncounterOptionMode.DEFAULT) + .withDialogue({ + buttonLabel: `${namespace}_pokeball_option_label`, + buttonTooltip: `${namespace}_pokeball_option_tooltip`, + selected: [ + { + text: `${namespace}_pokeball_option_selected`, + }, + ], + }) + .withOptionPhase(async (scene: BattleScene) => { + // Throw a ball option + const pokemon = scene.currentBattle.mysteryEncounter.misc.pokemon; + const catchResult = await throwPokeball(scene, pokemon); + + if (catchResult) { + // You caught pokemon + scene.unshiftPhase(new VictoryPhase(scene, 0)); + // Check how many safari pokemon left + if (scene.currentBattle.mysteryEncounter.misc.safariPokemonRemaining > 0) { + await summonSafariPokemon(scene); + initFollowupOptionSelect(scene, { overrideOptions: safariZoneOptions, startingCursorIndex: 0, hideDescription: true }); + } else { + // End safari mode + leaveEncounterWithoutBattle(scene, true); + } + } else { + // Pokemon failed to catch, end turn + await doEndTurn(scene, 0); + } + return true; + }) + .build(), + new MysteryEncounterOptionBuilder() + .withOptionMode(EncounterOptionMode.DEFAULT) + .withDialogue({ + buttonLabel: `${namespace}_bait_option_label`, + buttonTooltip: `${namespace}_bait_option_tooltip`, + selected: [ + { + text: `${namespace}_bait_option_selected`, + }, + ], + }) + .withOptionPhase(async (scene: BattleScene) => { + // Throw bait option + const pokemon = scene.currentBattle.mysteryEncounter.misc.pokemon; + await throwBait(scene, pokemon); + + // 100% chance to increase catch stage +2 + tryChangeCatchStage(scene, 2); + // 80% chance to increase flee stage +1 + const fleeChangeResult = tryChangeFleeStage(scene, 1, 8); + if (!fleeChangeResult) { + scene.queueMessage(i18next.t(`${namespace}_pokemon_busy_eating`, { pokemonName: pokemon.name }), 0, null, 500); + } else { + scene.queueMessage(i18next.t(`${namespace}_pokemon_eating`, { pokemonName: pokemon.name }), 0, null, 500); + } + // TODO: throw bait with eat animation + // TODO: play bug bite sfx, maybe spike cannon? + + await doEndTurn(scene, 1); + return true; + }) + .build(), + new MysteryEncounterOptionBuilder() + .withOptionMode(EncounterOptionMode.DEFAULT) + .withDialogue({ + buttonLabel: `${namespace}_mud_option_label`, + buttonTooltip: `${namespace}_mud_option_tooltip`, + selected: [ + { + text: `${namespace}_mud_option_selected`, + }, + ], + }) + .withOptionPhase(async (scene: BattleScene) => { + // Throw mud option + const pokemon = scene.currentBattle.mysteryEncounter.misc.pokemon; + await throwMud(scene, pokemon); + // 100% chance to decrease flee stage -2 + tryChangeFleeStage(scene, -2); + // 80% chance to decrease catch stage -1 + const catchChangeResult = tryChangeCatchStage(scene, -1, 8); + if (!catchChangeResult) { + scene.queueMessage(i18next.t(`${namespace}_pokemon_beside_itself_angry`, { pokemonName: pokemon.name }), 0, null, 500); + } else { + scene.queueMessage(i18next.t(`${namespace}_pokemon_angry`, { pokemonName: pokemon.name }), 0, null, 500); + } + + await doEndTurn(scene, 2); + return true; + }) + .build(), + new MysteryEncounterOptionBuilder() + .withOptionMode(EncounterOptionMode.DEFAULT) + .withDialogue({ + buttonLabel: `${namespace}_flee_option_label`, + buttonTooltip: `${namespace}_flee_option_tooltip`, + }) + .withOptionPhase(async (scene: BattleScene) => { + // Flee option + const pokemon = scene.currentBattle.mysteryEncounter.misc.pokemon; + await doPlayerFlee(scene, pokemon); + // Check how many safari pokemon left + if (scene.currentBattle.mysteryEncounter.misc.safariPokemonRemaining > 0) { + await summonSafariPokemon(scene); + initFollowupOptionSelect(scene, { overrideOptions: safariZoneOptions, startingCursorIndex: 3, hideDescription: true }); + } else { + // End safari mode + leaveEncounterWithoutBattle(scene, true); + } + return true; + }) + .build() +]; + export const SafariZoneEncounter: IMysteryEncounter = - MysteryEncounterBuilder.withEncounterType( - MysteryEncounterType.SAFARI_ZONE - ) + MysteryEncounterBuilder.withEncounterType(MysteryEncounterType.SAFARI_ZONE) .withEncounterTier(MysteryEncounterTier.GREAT) .withSceneWaveRangeRequirement(10, 180) // waves 2 to 180 + .withSceneRequirement(new MoneyRequirement(0, 2.75)) // Cost equal to 1 Max Revive .withHideIntroVisuals(false) .withIntroSpriteConfigs([ { @@ -33,133 +169,57 @@ export const SafariZoneEncounter: IMysteryEncounter = fileRoot: "mystery-encounters", hasShadow: true, x: 4, - y: 8, + y: 10, + yShadowOffset: 3, disableAnimation: true, // Re-enabled after option select }, ]) .withIntroDialogue([ { - text: `mysteryEncounter:${namespace}_intro_message`, + text: `${namespace}_intro_message`, }, ]) - .withTitle(`mysteryEncounter:${namespace}_title`) - .withDescription(`mysteryEncounter:${namespace}_description`) - .withQuery(`mysteryEncounter:${namespace}_query`) - .withOption( - new MysteryEncounterOptionBuilder() - .withOptionMode(EncounterOptionMode.DEFAULT) - .withDialogue({ - buttonLabel: "mysteryEncounter:${namespace}_option_1_label", - buttonTooltip: "mysteryEncounter:${namespace}_option_1_tooltip", - selected: [ - { - text: "mysteryEncounter:${namespace}_option_1_selected_message", - }, - ], - }) - .withPreOptionPhase(async (scene: BattleScene) => { - // Play animation - const introVisuals = - scene.currentBattle.mysteryEncounter.introVisuals; - introVisuals.spriteConfigs[0].disableAnimation = false; - introVisuals.playAnim(); - }) - .withOptionPhase(async (scene: BattleScene) => { - // Open the chest - const roll = randSeedInt(100); - if (roll > 60) { - // Choose between 2 COMMON / 2 GREAT tier items (40%) - setEncounterRewards(scene, { - guaranteedModifierTiers: [ - ModifierTier.COMMON, - ModifierTier.COMMON, - ModifierTier.GREAT, - ModifierTier.GREAT, - ], - }); - // Display result message then proceed to rewards - queueEncounterMessage( - scene, - "mysteryEncounter:${namespace}_option_1_normal_result" - ); - leaveEncounterWithoutBattle(scene); - } else if (roll > 40) { - // Choose between 3 ULTRA tier items (20%) - setEncounterRewards(scene, { - guaranteedModifierTiers: [ - ModifierTier.ULTRA, - ModifierTier.ULTRA, - ModifierTier.ULTRA, - ], - }); - // Display result message then proceed to rewards - queueEncounterMessage( - scene, - "mysteryEncounter:${namespace}_option_1_good_result" - ); - leaveEncounterWithoutBattle(scene); - } else if (roll > 36) { - // Choose between 2 ROGUE tier items (4%) - setEncounterRewards(scene, { - guaranteedModifierTiers: [ModifierTier.ROGUE, ModifierTier.ROGUE], - }); - // Display result message then proceed to rewards - queueEncounterMessage( - scene, - "mysteryEncounter:${namespace}_option_1_great_result" - ); - leaveEncounterWithoutBattle(scene); - } else if (roll > 35) { - // Choose 1 MASTER tier item (1%) - setEncounterRewards(scene, { - guaranteedModifierTiers: [ModifierTier.MASTER], - }); - // Display result message then proceed to rewards - queueEncounterMessage( - scene, - "mysteryEncounter:${namespace}_option_1_amazing_result" - ); - leaveEncounterWithoutBattle(scene); - } else { - // Your highest level unfainted Pok�mon gets OHKO. Progress with no rewards (35%) - const highestLevelPokemon = getHighestLevelPlayerPokemon( - scene, - true - ); - koPlayerPokemon(highestLevelPokemon); - - scene.currentBattle.mysteryEncounter.setDialogueToken( - "pokeName", - highestLevelPokemon.name - ); - // Show which Pokemon was KOed, then leave encounter with no rewards - // Does this synchronously so that game over doesn't happen over result message - await showEncounterText( - scene, - "mysteryEncounter:${namespace}_option_1_bad_result" - ).then(() => { - if ( - scene.getParty().filter((p) => p.isAllowedInBattle()).length === - 0 - ) { - // All pokemon fainted, game over - scene.clearPhaseQueue(); - scene.unshiftPhase(new GameOverPhase(scene)); - } else { - leaveEncounterWithoutBattle(scene); - } - }); - } - }) - .build() + .withTitle(`${namespace}_title`) + .withDescription(`${namespace}_description`) + .withQuery(`${namespace}_query`) + .withOption(new MysteryEncounterOptionBuilder() + .withOptionMode(EncounterOptionMode.DISABLED_OR_DEFAULT) + // TODO: update + .withSceneRequirement(new MoneyRequirement(0, 2.75)) // Cost equal to 1 Max Revive + .withDialogue({ + buttonLabel: `${namespace}_option_1_label`, + buttonTooltip: `${namespace}_option_1_tooltip`, + selected: [ + { + text: `${namespace}_option_1_selected_message`, + }, + ], + }) + .withOptionPhase(async (scene: BattleScene) => { + // Start safari encounter + const encounter = scene.currentBattle.mysteryEncounter; + encounter.encounterVariant = MysteryEncounterVariant.SAFARI_BATTLE; + encounter.misc = { + safariPokemonRemaining: 3 + }; + updatePlayerMoney(scene, -(encounter.options[0].requirements[0] as MoneyRequirement).requiredMoney); + scene.loadSe("PRSFX- Bug Bite", "battle_anims"); + scene.loadSe("PRSFX- Sludge Bomb2", "battle_anims"); + scene.loadSe("PRSFX- Taunt2", "battle_anims"); + await hideMysteryEncounterIntroVisuals(scene); + await summonSafariPokemon(scene); + initFollowupOptionSelect(scene, { overrideOptions: safariZoneOptions, hideDescription: true }); + return true; + }) + .build() ) .withSimpleOption( { - buttonLabel: "mysteryEncounter:${namespace}_option_2_label", - buttonTooltip: "mysteryEncounter:${namespace}_option_2_tooltip", + buttonLabel: `${namespace}_option_2_label`, + buttonTooltip: `${namespace}_option_2_tooltip`, selected: [ { - text: "mysteryEncounter:${namespace}_option_2_selected_message", + text: `${namespace}_option_2_selected_message`, }, ], }, @@ -170,3 +230,588 @@ export const SafariZoneEncounter: IMysteryEncounter = } ) .build(); + +function hideMysteryEncounterIntroVisuals(scene: BattleScene): Promise { + return new Promise(resolve => { + const introVisuals = scene.currentBattle.mysteryEncounter.introVisuals; + if (introVisuals) { + // Hide + scene.tweens.add({ + targets: introVisuals, + x: "+=16", + y: "-=16", + alpha: 0, + ease: "Sine.easeInOut", + duration: 750, + onComplete: () => { + scene.field.remove(introVisuals); + introVisuals.setVisible(false); + introVisuals.destroy(); + scene.currentBattle.mysteryEncounter.introVisuals = null; + resolve(true); + } + }); + } else { + resolve(true); + } + }); +} + +async function summonSafariPokemon(scene: BattleScene) { + const encounter = scene.currentBattle.mysteryEncounter; + // Message pokemon remaining + scene.queueMessage(i18next.t(`${namespace}_remaining_count`, { remainingCount: encounter.misc.safariPokemonRemaining}), null, true); + + // Generate pokemon using safariPokemonRemaining so they are always the same pokemon no matter how many turns are taken + // Safari pokemon roll twice on shiny and HA chances, but are otherwise normal + let enemySpecies; + let pokemon; + scene.executeWithSeedOffset(() => { + enemySpecies = getPokemonSpecies(getRandomSpeciesByStarterTier([0, 5])); + enemySpecies = getPokemonSpecies(enemySpecies.getWildSpeciesForLevel(scene.currentBattle.waveIndex, true, false, scene.gameMode)); + scene.currentBattle.enemyParty = []; + pokemon = scene.addEnemyPokemon(enemySpecies, scene.currentBattle.waveIndex, TrainerSlot.NONE, false); + + // Roll shiny twice + if (!pokemon.shiny) { + pokemon.trySetShiny(); + } + + // Roll HA twice + if (pokemon.species.abilityHidden) { + const hiddenIndex = pokemon.species.ability2 ? 2 : 1; + if (pokemon.abilityIndex < hiddenIndex) { + const hiddenAbilityChance = new IntegerHolder(256); + scene.applyModifiers(HiddenAbilityRateBoosterModifier, true, hiddenAbilityChance); + + const hasHiddenAbility = !randSeedInt(hiddenAbilityChance.value); + + if (hasHiddenAbility) { + pokemon.abilityIndex = hiddenIndex; + } + } + } + + pokemon.calculateStats(); + + scene.currentBattle.enemyParty[0] = pokemon; + }, scene.currentBattle.waveIndex + encounter.misc.safariPokemonRemaining); + + scene.gameData.setPokemonSeen(pokemon, true); + await pokemon.loadAssets(); + + // Reset safari catch and flee rates + encounter.misc.catchStage = 0; + encounter.misc.fleeStage = 0; + encounter.misc.pokemon = pokemon; + encounter.misc.safariPokemonRemaining -= 1; + + scene.unshiftPhase(new SummonPhase(scene, 0, false)); + + scene.ui.showText(i18next.t("battle:singleWildAppeared", { pokemonName: pokemon.name }), null, () => { + const ivScannerModifier = scene.findModifier(m => m instanceof IvScannerModifier); + if (ivScannerModifier) { + scene.pushPhase(new ScanIvsPhase(scene, pokemon.getBattlerIndex(), Math.min(ivScannerModifier.getStackCount() * 2, 6))); + } + }, 1500); +} + +async function throwPokeball(scene: BattleScene, pokemon: EnemyPokemon): Promise { + const pokeballType: PokeballType = PokeballType.POKEBALL; + const originalY: number = pokemon.y; + + const baseCatchRate = pokemon.species.catchRate; + // Catch stage ranges from -6 to +6 (like stat boost stages) + 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 + const pokeballMultiplier = 1.5; + const catchRate = Math.round(baseCatchRate * pokeballMultiplier * safariModifier); + const ballTwitchRate = Math.round(1048560 / Math.sqrt(Math.sqrt(16711680 / catchRate))); + const fpOffset = pokemon.getFieldPositionOffset(); + const catchSuccess = (ballTwitchRate / 65536) * (ballTwitchRate / 65536) * (ballTwitchRate / 65536); + console.log("Catch success rate: " + catchSuccess); + + const pokeballAtlasKey = getPokeballAtlasKey(pokeballType); + const pokeball: Phaser.GameObjects.Sprite = scene.addFieldSprite(16 + 75, 80 + 25, "pb", pokeballAtlasKey); + pokeball.setOrigin(0.5, 0.625); + scene.field.add(pokeball); + + scene.playSound("pb_throw"); + scene.time.delayedCall(300, () => { + scene.field.moveBelow(pokeball as Phaser.GameObjects.GameObject, pokemon); + }); + + return new Promise(resolve => { + scene.trainer.setTexture(`trainer_${scene.gameData.gender === PlayerGender.FEMALE ? "f" : "m"}_back_pb`); + scene.time.delayedCall(512, () => { + // Trainer throw frames + scene.trainer.setFrame("2"); + scene.time.delayedCall(256, () => { + scene.trainer.setFrame("3"); + scene.time.delayedCall(768, () => { + scene.trainer.setTexture(`trainer_${scene.gameData.gender === PlayerGender.FEMALE ? "f" : "m"}_back`); + }); + }); + + // Pokeball move and catch logic + scene.tweens.add({ + targets: pokeball, + x: { value: 236 + fpOffset[0], ease: "Linear" }, + y: { value: 16 + fpOffset[1], ease: "Cubic.easeOut" }, + duration: 500, + onComplete: () => { + pokeball.setTexture("pb", `${pokeballAtlasKey}_opening`); + scene.time.delayedCall(17, () => pokeball.setTexture("pb", `${pokeballAtlasKey}_open`)); + scene.playSound("pb_rel"); + pokemon.tint(getPokeballTintColor(pokeballType)); + + addPokeballOpenParticles(scene, pokeball.x, pokeball.y, pokeballType); + + scene.tweens.add({ + targets: pokemon, + duration: 500, + ease: "Sine.easeIn", + scale: 0.25, + y: 20, + onComplete: () => { + pokeball.setTexture("pb", `${pokeballAtlasKey}_opening`); + pokemon.setVisible(false); + scene.playSound("pb_catch"); + scene.time.delayedCall(17, () => pokeball.setTexture("pb", `${pokeballAtlasKey}`)); + + const doShake = () => { + let shakeCount = 0; + const pbX = pokeball.x; + const shakeCounter = scene.tweens.addCounter({ + from: 0, + to: 1, + repeat: 4, + yoyo: true, + ease: "Cubic.easeOut", + duration: 250, + repeatDelay: 500, + onUpdate: t => { + if (shakeCount && shakeCount < 4) { + const value = t.getValue(); + const directionMultiplier = shakeCount % 2 === 1 ? 1 : -1; + pokeball.setX(pbX + value * 4 * directionMultiplier); + pokeball.setAngle(value * 27.5 * directionMultiplier); + } + }, + onRepeat: () => { + if (!pokemon.species.isObtainable()) { + shakeCounter.stop(); + failCatch(scene, pokemon, originalY, pokeball, pokeballType).then(() => resolve(false)); + } else if (shakeCount++ < 3) { + if (randSeedInt(65536) < ballTwitchRate) { + scene.playSound("pb_move"); + } else { + shakeCounter.stop(); + failCatch(scene, pokemon, originalY, pokeball, pokeballType).then(() => resolve(false)); + } + } else { + scene.playSound("pb_lock"); + addPokeballCaptureStars(scene, pokeball); + + const pbTint = scene.add.sprite(pokeball.x, pokeball.y, "pb", "pb"); + pbTint.setOrigin(pokeball.originX, pokeball.originY); + pbTint.setTintFill(0); + pbTint.setAlpha(0); + scene.field.add(pbTint); + scene.tweens.add({ + targets: pbTint, + alpha: 0.375, + duration: 200, + easing: "Sine.easeOut", + onComplete: () => { + scene.tweens.add({ + targets: pbTint, + alpha: 0, + duration: 200, + easing: "Sine.easeIn", + onComplete: () => pbTint.destroy() + }); + } + }); + } + }, + onComplete: () => { + catchPokemon(scene, pokemon, pokeball, pokeballType).then(() => resolve(true)); + } + }); + }; + + scene.time.delayedCall(250, () => doPokeballBounceAnim(scene, pokeball, 16, 72, 350, doShake)); + } + }); + } + }); + }); + }); +} + +async function throwBait(scene: BattleScene, pokemon: EnemyPokemon): Promise { + // TODO: replace with bait + const pokeballType: PokeballType = PokeballType.POKEBALL; + const originalY: number = pokemon.y; + + const fpOffset = pokemon.getFieldPositionOffset(); + const pokeballAtlasKey = getPokeballAtlasKey(pokeballType); + const bait: Phaser.GameObjects.Sprite = scene.addFieldSprite(16 + 75, 80 + 25, "pb", pokeballAtlasKey); + bait.setOrigin(0.5, 0.625); + scene.field.add(bait); + + scene.playSound("pb_throw"); + // scene.time.delayedCall(300, () => { + // scene.field.moveBelow(pokemon, pokeball as Phaser.GameObjects.GameObject); + // }); + + return new Promise(resolve => { + scene.trainer.setTexture(`trainer_${scene.gameData.gender === PlayerGender.FEMALE ? "f" : "m"}_back_pb`); + scene.time.delayedCall(512, () => { + // Trainer throw frames + scene.trainer.setFrame("2"); + scene.time.delayedCall(256, () => { + scene.trainer.setFrame("3"); + scene.time.delayedCall(768, () => { + scene.trainer.setTexture(`trainer_${scene.gameData.gender === PlayerGender.FEMALE ? "f" : "m"}_back`); + }); + }); + + // Pokeball move and catch logic + scene.tweens.add({ + targets: bait, + x: { value: 210 + fpOffset[0], ease: "Linear" }, + y: { value: 55 + fpOffset[1], ease: "Cubic.easeOut" }, + duration: 500, + onComplete: () => { + // Bait frame 2 + bait.setTexture("pb", `${pokeballAtlasKey}_opening`); + // Bait frame 3 + scene.time.delayedCall(17, () => bait.setTexture("pb", `${pokeballAtlasKey}_open`)); + // scene.playSound("pb_rel"); + // pokemon.tint(getPokeballTintColor(pokeballType)); + + // addPokeballOpenParticles(scene, pokeball.x, pokeball.y, pokeballType); + scene.time.delayedCall(512, () => { + scene.tweens.add({ + targets: pokemon, + duration: 200, + ease: "Cubic.easeOut", + yoyo: true, + y: originalY - 30, + loop: 2, + onStart: () => { + scene.playSound("PRSFX- Bug Bite"); + }, + onLoop: () => { + scene.playSound("PRSFX- Bug Bite"); + }, + onComplete: () => { + resolve(true); + bait.destroy(); + } + }); + }); + } + }); + }); + }); +} + +async function throwMud(scene: BattleScene, pokemon: EnemyPokemon): Promise { + // TODO: replace with mud + const pokeballType: PokeballType = PokeballType.POKEBALL; + const originalY: number = pokemon.y; + + const fpOffset = pokemon.getFieldPositionOffset(); + const pokeballAtlasKey = getPokeballAtlasKey(pokeballType); + const mud: Phaser.GameObjects.Sprite = scene.addFieldSprite(16 + 75, 80 + 25, "pb", pokeballAtlasKey); + mud.setOrigin(0.5, 0.625); + scene.field.add(mud); + + scene.playSound("pb_throw"); + + return new Promise(resolve => { + scene.trainer.setTexture(`trainer_${scene.gameData.gender === PlayerGender.FEMALE ? "f" : "m"}_back_pb`); + scene.time.delayedCall(512, () => { + // Trainer throw frames + scene.trainer.setFrame("2"); + scene.time.delayedCall(256, () => { + scene.trainer.setFrame("3"); + scene.time.delayedCall(768, () => { + scene.trainer.setTexture(`trainer_${scene.gameData.gender === PlayerGender.FEMALE ? "f" : "m"}_back`); + }); + }); + + // Pokeball move and catch logic + scene.tweens.add({ + targets: mud, + x: { value: 230 + fpOffset[0], ease: "Linear" }, + y: { value: 55 + fpOffset[1], ease: "Cubic.easeOut" }, + duration: 500, + onComplete: () => { + // Bait frame 2 + mud.setTexture("pb", `${pokeballAtlasKey}_opening`); + // Bait frame 3 + scene.time.delayedCall(17, () => mud.setTexture("pb", `${pokeballAtlasKey}_open`)); + scene.playSound("PRSFX- Sludge Bomb2"); + // pokemon.tint(getPokeballTintColor(pokeballType)); + + // addPokeballOpenParticles(scene, pokeball.x, pokeball.y, pokeballType); + scene.time.delayedCall(1536, () => { + mud.destroy(); + scene.tweens.add({ + targets: pokemon, + duration: 300, + ease: "Cubic.easeOut", + yoyo: true, + y: originalY - 20, + loop: 1, + onStart: () => { + scene.playSound("PRSFX- Taunt2"); + }, + onLoop: () => { + scene.playSound("PRSFX- Taunt2"); + }, + onComplete: () => { + resolve(true); + } + }); + }); + } + }); + }); + }); +} + +async function failCatch(scene: BattleScene, pokemon: EnemyPokemon, originalY: number, pokeball: Phaser.GameObjects.Sprite, pokeballType: PokeballType) { + return new Promise(resolve => { + scene.playSound("pb_rel"); + pokemon.setY(originalY); + if (pokemon.status?.effect !== StatusEffect.SLEEP) { + pokemon.cry(pokemon.getHpRatio() > 0.25 ? undefined : { rate: 0.85 }); + } + pokemon.tint(getPokeballTintColor(pokeballType)); + pokemon.setVisible(true); + pokemon.untint(250, "Sine.easeOut"); + + const pokeballAtlasKey = getPokeballAtlasKey(pokeballType); + pokeball.setTexture("pb", `${pokeballAtlasKey}_opening`); + scene.time.delayedCall(17, () => pokeball.setTexture("pb", `${pokeballAtlasKey}_open`)); + + scene.tweens.add({ + targets: pokemon, + duration: 250, + ease: "Sine.easeOut", + scale: 1 + }); + + scene.currentBattle.lastUsedPokeball = pokeballType; + removePb(scene, pokeball); + + scene.ui.showText(i18next.t("battle:pokemonBrokeFree", { pokemonName: pokemon.name }), null, () => resolve(), null, true); + }); +} + +async function catchPokemon(scene: BattleScene, pokemon: EnemyPokemon, pokeball: Phaser.GameObjects.Sprite, pokeballType: PokeballType): Promise { + scene.unshiftPhase(new VictoryPhase(scene, BattlerIndex.ENEMY)); + + const speciesForm = !pokemon.fusionSpecies ? pokemon.getSpeciesForm() : pokemon.getFusionSpeciesForm(); + + if (speciesForm.abilityHidden && (pokemon.fusionSpecies ? pokemon.fusionAbilityIndex : pokemon.abilityIndex) === speciesForm.getAbilityCount() - 1) { + scene.validateAchv(achvs.HIDDEN_ABILITY); + } + + if (pokemon.species.subLegendary) { + scene.validateAchv(achvs.CATCH_SUB_LEGENDARY); + } + + if (pokemon.species.legendary) { + scene.validateAchv(achvs.CATCH_LEGENDARY); + } + + if (pokemon.species.mythical) { + scene.validateAchv(achvs.CATCH_MYTHICAL); + } + + scene.pokemonInfoContainer.show(pokemon, true); + + scene.gameData.updateSpeciesDexIvs(pokemon.species.getRootSpeciesId(true), pokemon.ivs); + + return new Promise(resolve => { + scene.ui.showText(i18next.t("battle:pokemonCaught", { pokemonName: pokemon.name }), null, () => { + const end = () => { + scene.pokemonInfoContainer.hide(); + removePb(scene, pokeball); + resolve(); + }; + const removePokemon = () => { + scene.field.remove(pokemon, true); + }; + const addToParty = () => { + const newPokemon = pokemon.addToParty(pokeballType); + const modifiers = scene.findModifiers(m => m instanceof PokemonHeldItemModifier, false); + if (scene.getParty().filter(p => p.isShiny()).length === 6) { + scene.validateAchv(achvs.SHINY_PARTY); + } + Promise.all(modifiers.map(m => scene.addModifier(m, true))).then(() => { + scene.updateModifiers(true); + removePokemon(); + if (newPokemon) { + newPokemon.loadAssets().then(end); + } else { + end(); + } + }); + }; + Promise.all([pokemon.hideInfo(), scene.gameData.setPokemonCaught(pokemon)]).then(() => { + if (scene.getParty().length === 6) { + const promptRelease = () => { + scene.ui.showText(i18next.t("battle:partyFull", { pokemonName: pokemon.name }), null, () => { + scene.pokemonInfoContainer.makeRoomForConfirmUi(); + scene.ui.setMode(Mode.CONFIRM, () => { + scene.ui.setMode(Mode.PARTY, PartyUiMode.RELEASE, 0, (slotIndex: integer, _option: PartyOption) => { + scene.ui.setMode(Mode.MESSAGE).then(() => { + if (slotIndex < 6) { + addToParty(); + } else { + promptRelease(); + } + }); + }); + }, () => { + scene.ui.setMode(Mode.MESSAGE).then(() => { + removePokemon(); + end(); + }); + }); + }); + }; + promptRelease(); + } else { + addToParty(); + } + }); + }, 0, true); + }); +} + +function removePb(scene: BattleScene, pokeball: Phaser.GameObjects.Sprite) { + scene.tweens.add({ + targets: pokeball, + duration: 250, + delay: 250, + ease: "Sine.easeIn", + alpha: 0, + onComplete: () => pokeball.destroy() + }); +} + +function isPokemonFlee(pokemon: EnemyPokemon, fleeStage: number): boolean { + const speciesCatchRate = pokemon.species.catchRate; + const fleeModifier = (2 + Math.min(Math.max(fleeStage, 0), 6)) / (2 - Math.max(Math.min(fleeStage, 0), -6)); + const fleeRate = (255 * 255 - speciesCatchRate * speciesCatchRate) / 255 / 2 * fleeModifier; + console.log("Flee rate: " + fleeRate); + const roll = randSeedInt(256); + console.log("Roll: " + roll); + return roll < fleeRate; +} + +async function doPokemonFlee(scene: BattleScene, pokemon: EnemyPokemon): Promise { + const fleeAnimation = new Promise(resolve => { + // Ease pokemon out + scene.tweens.add({ + targets: pokemon, + x: "+=16", + y: "-=16", + alpha: 0, + duration: 1000, + ease: "Sine.easeIn", + scale: pokemon.getSpriteScale(), + onComplete: () => { + pokemon.setVisible(false); + scene.field.remove(pokemon, true); + resolve(); + } + }); + }); + + const prompt = new Promise(resolve => { + scene.ui.showText(i18next.t("battle:pokemonFled", { pokemonName: pokemon.name }), 0, () => resolve(), 500); + }); + + await Promise.all([fleeAnimation, prompt]); +} + +async function doPlayerFlee(scene: BattleScene, pokemon: EnemyPokemon): Promise { + const fleeAnimation = new Promise(resolve => { + // Ease pokemon out + scene.tweens.add({ + targets: pokemon, + x: "+=16", + y: "-=16", + alpha: 0, + duration: 1000, + ease: "Sine.easeIn", + scale: pokemon.getSpriteScale(), + onComplete: () => { + pokemon.setVisible(false); + scene.field.remove(pokemon, true); + resolve(); + } + }); + }); + + const prompt = new Promise(resolve => { + scene.ui.showText(i18next.t("battle:playerFled", { pokemonName: pokemon.name }), 0, () => resolve(), 500); + }); + + await Promise.all([fleeAnimation, prompt]); +} + +function tryChangeFleeStage(scene: BattleScene, change: number, chance?: number): boolean { + if (chance && randSeedInt(10) >= chance) { + console.log("Failed to change flee stage"); + return false; + } + const currentFleeStage = scene.currentBattle.mysteryEncounter.misc.fleeStage ?? 0; + // console.log("currentFleeStage: " + currentFleeStage); + scene.currentBattle.mysteryEncounter.misc.fleeStage = Math.min(Math.max(currentFleeStage + change, -6), 6); + return true; +} + +function tryChangeCatchStage(scene: BattleScene, change: number, chance?: number): boolean { + if (chance && randSeedInt(10) >= chance) { + console.log("Failed to change catch stage"); + return false; + } + const currentCatchStage = scene.currentBattle.mysteryEncounter.misc.catchStage ?? 0; + // console.log("currentCatchStage: " + currentCatchStage); + scene.currentBattle.mysteryEncounter.misc.catchStage = Math.min(Math.max(currentCatchStage + change, -6), 6); + return true; +} + +async function doEndTurn(scene: BattleScene, cursorIndex: number, message?: string) { + const pokemon = scene.currentBattle.mysteryEncounter.misc.pokemon; + console.log("fleeStage: " + scene.currentBattle.mysteryEncounter.misc.fleeStage); + console.log("catchStage: " + scene.currentBattle.mysteryEncounter.misc.catchStage); + const isFlee = isPokemonFlee(pokemon, scene.currentBattle.mysteryEncounter.misc.fleeStage); + if (isFlee) { + // Pokemon flees! + await doPokemonFlee(scene, pokemon); + // Check how many safari pokemon left + if (scene.currentBattle.mysteryEncounter.misc.safariPokemonRemaining > 0) { + await summonSafariPokemon(scene); + initFollowupOptionSelect(scene, { overrideOptions: safariZoneOptions, startingCursorIndex: cursorIndex, hideDescription: true }); + } else { + // End safari mode + leaveEncounterWithoutBattle(scene, true); + } + } else { + scene.queueMessage(i18next.t(`${namespace}_pokemon_watching`, { pokemonName: pokemon.name }), 0, null, 500); + initFollowupOptionSelect(scene, { overrideOptions: safariZoneOptions, startingCursorIndex: cursorIndex, hideDescription: true }); + } +} 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 6018b651639..8bfd6daa9f9 100644 --- a/src/data/mystery-encounters/encounters/shady-vitamin-dealer-encounter.ts +++ b/src/data/mystery-encounters/encounters/shady-vitamin-dealer-encounter.ts @@ -28,15 +28,17 @@ export const ShadyVitaminDealerEncounter: IMysteryEncounter = fileRoot: "pokemon", hasShadow: true, repeat: true, - x: 10, - y: -1, + x: 12, + y: -5, + yShadowOffset: -5 }, { spriteKey: "b2w2_veteran_m", fileRoot: "mystery-encounters", hasShadow: true, - x: -10, - y: 2, + x: -12, + y: 3, + yShadowOffset: 3 }, ]) .withIntroDialogue([ diff --git a/src/data/mystery-encounters/mystery-encounter-utils.ts b/src/data/mystery-encounters/mystery-encounter-utils.ts index 5c6e7eefa8d..e0c8f73e569 100644 --- a/src/data/mystery-encounters/mystery-encounter-utils.ts +++ b/src/data/mystery-encounters/mystery-encounter-utils.ts @@ -10,7 +10,7 @@ import Trainer, { TrainerVariant } from "../../field/trainer"; import { ExpBalanceModifier, ExpShareModifier, MultipleParticipantExpBonusModifier, PokemonExpBoosterModifier } from "#app/modifier/modifier"; import { CustomModifierSettings, getModifierPoolForType, ModifierPoolType, ModifierType, ModifierTypeFunc, ModifierTypeGenerator, ModifierTypeOption, modifierTypes, PokemonHeldItemModifierType, regenerateModifierPoolThresholds } from "#app/modifier/modifier-type"; import { BattleEndPhase, EggLapsePhase, ExpPhase, ModifierRewardPhase, SelectModifierPhase, ShowPartyExpBarPhase, TrainerVictoryPhase } from "#app/phases"; -import { MysteryEncounterBattlePhase, MysteryEncounterRewardsPhase } from "#app/phases/mystery-encounter-phase"; +import { MysteryEncounterBattlePhase, MysteryEncounterPhase, MysteryEncounterRewardsPhase } from "#app/phases/mystery-encounter-phase"; import * as Utils from "../../utils"; import { isNullOrUndefined } from "#app/utils"; import { TrainerType } from "#enums/trainer-type"; @@ -27,6 +27,7 @@ import { WIGHT_INCREMENT_ON_SPAWN_MISS } from "#app/data/mystery-encounters/myst import { getTextWithColors, TextStyle } from "#app/ui/text"; import * as Overrides from "#app/overrides"; import { UiTheme } from "#enums/ui-theme"; +import { MysteryEncounterUiSettings } from "#app/ui/mystery-encounter-ui-handler"; /** * @@ -427,6 +428,11 @@ export function updatePlayerMoney(scene: BattleScene, changeValue: number, playS if (playSound) { scene.playSound("buy"); } + if (changeValue < 0) { + scene.queueMessage(i18next.t("mysteryEncounter:paid_money", { amount: -changeValue }), null, true); + } else { + scene.queueMessage(i18next.t("mysteryEncounter:receive_money", { amount: changeValue }), null, true); + } } /** @@ -459,7 +465,7 @@ export function generateModifierTypeOption(scene: BattleScene, modifier: () => M } /** - * + * This function is intended for use inside onPreOptionPhase() of an encounter option * @param scene * @param onPokemonSelected - Any logic that needs to be performed when Pokemon is chosen * If a second option needs to be selected, onPokemonSelected should return a OptionSelectItem[] object @@ -674,6 +680,16 @@ export function setEncounterExp(scene: BattleScene, participantIds: integer[], b }; } +/** + * Can be used to exit an encounter without any battles or followup + * Will skip any shops and rewards, and queue the next encounter phase as normal + * @param scene + * @param followupOptionSelectSettings + */ +export function initFollowupOptionSelect(scene: BattleScene, followupOptionSelectSettings: MysteryEncounterUiSettings) { + scene.pushPhase(new MysteryEncounterPhase(scene, followupOptionSelectSettings)); +} + /** * Can be used to exit an encounter without any battles or followup * Will skip any shops and rewards, and queue the next encounter phase as normal @@ -688,7 +704,9 @@ export function leaveEncounterWithoutBattle(scene: BattleScene, addHealPhase: bo } export function handleMysteryEncounterVictory(scene: BattleScene, addHealPhase: boolean = false) { - if (scene.currentBattle.mysteryEncounter.encounterVariant === MysteryEncounterVariant.NO_BATTLE) { + if (scene.currentBattle.mysteryEncounter.encounterVariant === MysteryEncounterVariant.SAFARI_BATTLE) { + scene.pushPhase(new MysteryEncounterRewardsPhase(scene, addHealPhase)); + } else if (scene.currentBattle.mysteryEncounter.encounterVariant === MysteryEncounterVariant.NO_BATTLE) { scene.pushPhase(new EggLapsePhase(scene)); scene.pushPhase(new MysteryEncounterRewardsPhase(scene, addHealPhase)); } else if (!scene.getEnemyParty().find(p => scene.currentBattle.mysteryEncounter.encounterVariant !== MysteryEncounterVariant.TRAINER_BATTLE ? p.isOnField() : !p?.isFainted(true))) { diff --git a/src/data/mystery-encounters/mystery-encounter.ts b/src/data/mystery-encounters/mystery-encounter.ts index e02639939b8..0be21d1520f 100644 --- a/src/data/mystery-encounters/mystery-encounter.ts +++ b/src/data/mystery-encounters/mystery-encounter.ts @@ -24,7 +24,8 @@ export enum MysteryEncounterVariant { TRAINER_BATTLE, WILD_BATTLE, BOSS_BATTLE, - NO_BATTLE + NO_BATTLE, + SAFARI_BATTLE } export enum MysteryEncounterTier { @@ -118,6 +119,18 @@ export default interface IMysteryEncounter { */ expMultiplier?: number; + /** + * When true, will never queue PostSummon phases from a SummonPhase + * Defaults to false + */ + disableAllPostSummon?: boolean; + + /** + * Used for keeping RNG consistent on session resets, but increments when cycling through multiple "Encounters" on the same wave + * You should never need to modify this + */ + seedOffset?: any; + /** * Generic property to set any custom data required for the encounter * Extremely useful for carrying state/data between onPreOptionPhase/onOptionPhase/onPostOptionPhase diff --git a/src/data/mystery-encounters/mystery-encounters.ts b/src/data/mystery-encounters/mystery-encounters.ts index c384606f5e3..a53f12a0bfa 100644 --- a/src/data/mystery-encounters/mystery-encounters.ts +++ b/src/data/mystery-encounters/mystery-encounters.ts @@ -10,6 +10,7 @@ import { MysteryEncounterType } from "#enums/mystery-encounter-type"; import { DepartmentStoreSaleEncounter } from "./encounters/department-store-sale-encounter"; import { ShadyVitaminDealerEncounter } from "./encounters/shady-vitamin-dealer-encounter"; import { FieldTripEncounter } from "./encounters/field-trip-encounter"; +import { SafariZoneEncounter } from "#app/data/mystery-encounters/encounters/safari-zone-encounter"; // Spawn chance: (BASE_MYSTERY_ENCOUNTER_SPAWN_WEIGHT + WIGHT_INCREMENT_ON_SPAWN_MISS * ) / 256 export const BASE_MYSTERY_ENCOUNTER_SPAWN_WEIGHT = 1; @@ -213,6 +214,7 @@ export function initMysteryEncounters() { allMysteryEncounters[MysteryEncounterType.DEPARTMENT_STORE_SALE] = DepartmentStoreSaleEncounter; allMysteryEncounters[MysteryEncounterType.SHADY_VITAMIN_DEALER] = ShadyVitaminDealerEncounter; allMysteryEncounters[MysteryEncounterType.FIELD_TRIP] = FieldTripEncounter; + allMysteryEncounters[MysteryEncounterType.SAFARI_ZONE] = SafariZoneEncounter; // Add extreme encounters to biome map extremeBiomeEncounters.forEach(encounter => { diff --git a/src/field/mystery-encounter-intro.ts b/src/field/mystery-encounter-intro.ts index 1b0fb3bca01..2c606ad61cc 100644 --- a/src/field/mystery-encounter-intro.ts +++ b/src/field/mystery-encounter-intro.ts @@ -11,6 +11,7 @@ export class MysteryEncounterSpriteConfig { tint?: number; x?: number; // X offset y?: number; // Y offset + yShadowOffset?: number; scale?: number; isItem?: boolean; // For item sprites, set to true } @@ -37,10 +38,10 @@ export default class MysteryEncounterIntroVisuals extends Phaser.GameObjects.Con return; } - const getSprite = (spriteKey: string, hasShadow?: boolean) => { + const getSprite = (spriteKey: string, hasShadow?: boolean, yShadowOffset?: number) => { const ret = this.scene.addFieldSprite(0, 0, spriteKey); ret.setOrigin(0.5, 1); - ret.setPipeline(this.scene.spritePipeline, { tone: [0.0, 0.0, 0.0, 0.0], hasShadow: !!hasShadow }); + ret.setPipeline(this.scene.spritePipeline, { tone: [0.0, 0.0, 0.0, 0.0], hasShadow: !!hasShadow, yShadowOffset: yShadowOffset ?? 0 }); return ret; }; @@ -62,7 +63,7 @@ export default class MysteryEncounterIntroVisuals extends Phaser.GameObjects.Con let sprite: GameObjects.Sprite; let tintSprite: GameObjects.Sprite; if (!config.isItem) { - sprite = getSprite(config.spriteKey, config.hasShadow); + sprite = getSprite(config.spriteKey, config.hasShadow, config.yShadowOffset); tintSprite = getSprite(config.spriteKey); } else { sprite = getItemSprite(config.spriteKey); @@ -83,8 +84,8 @@ export default class MysteryEncounterIntroVisuals extends Phaser.GameObjects.Con tintSprite.setPosition(origin + config.x, tintSprite.y); } if (config.y) { - sprite.setPosition(sprite.x, config.y); - tintSprite.setPosition(tintSprite.x, config.y); + sprite.setPosition(sprite.x, sprite.y + config.y); + tintSprite.setPosition(tintSprite.x, tintSprite.y + config.y); } } else { // Single sprite diff --git a/src/locales/en/battle.ts b/src/locales/en/battle.ts index b10e5507b3b..6deaf4496a0 100644 --- a/src/locales/en/battle.ts +++ b/src/locales/en/battle.ts @@ -16,6 +16,9 @@ export const battle: SimpleTranslationEntries = { "moneyWon": "You got\n₽{{moneyAmount}} for winning!", "moneyPickedUp": "You picked up ₽{{moneyAmount}}!", "pokemonCaught": "{{pokemonName}} was caught!", + "pokemonBrokeFree": "Oh no!\nThe Pokémon broke free!", + "pokemonFled": "The wild {{pokemonName}} fled!", + "playerFled": "You fled from the {{pokemonName}}!", "addedAsAStarter": "{{pokemonName}} has been\nadded as a starter!", "partyFull": "Your party is full.\nRelease a Pokémon to make room for {{pokemonName}}?", "pokemon": "Pokémon", diff --git a/src/locales/en/mystery-encounter.ts b/src/locales/en/mystery-encounter.ts index 6ba84f6142b..a13e35a4474 100644 --- a/src/locales/en/mystery-encounter.ts +++ b/src/locales/en/mystery-encounter.ts @@ -17,6 +17,10 @@ export const mysteryEncounter: SimpleTranslationEntries = { // DO NOT REMOVE "unit_test_dialogue": "{{test}}{{test}} {{test{{test}}}} {{test1}} {{test\}} {{test\\}} {{test\\\}} {test}}", + // General use content + "paid_money": "You paid ₽{{amount, number}}.", + "receive_money": "You received ₽{{amount, number}}!", + // Mystery Encounters -- Common Tier "mysterious_chest_intro_message": "You found...@d{32} a chest?", @@ -125,7 +129,7 @@ export const mysteryEncounter: SimpleTranslationEntries = { "field_trip_outro_good": "Thank you so much for your kindness!\nI hope the items I had were helpful!", "field_trip_outro_bad": "Come along children, we'll\nfind a better demonstration elsewhere.", - // Mystery Encounters -- Uncommon Tier + // Mystery Encounters -- Great Tier "mysterious_challengers_intro_message": "Mysterious challengers have appeared!", "mysterious_challengers_title": "Mysterious Challengers", @@ -140,7 +144,35 @@ export const mysteryEncounter: SimpleTranslationEntries = { "mysterious_challengers_option_selected_message": "The trainer steps forward...", "mysterious_challengers_outro_win": "The mysterious challenger was defeated!", - // Mystery Encounters -- Rare Tier + "safari_zone_intro_message": "It's a safari zone!", + "safari_zone_title": "The Safari Zone", + "safari_zone_description": "There are all kinds of rare and special Pokémon that can be found here!\nIf you choose to enter, you'll have a time limit of 3 wild encounters where you can try to catch these special Pokémon.\nBeware, though. These Pokémon may flee before you're able to catch them!", + "safari_zone_query": "Would you like to enter?", + "safari_zone_option_1_label": "Enter", + "safari_zone_option_1_tooltip": "(-) Pay {{option1Money, money}}\n@[SUMMARY_GREEN]{(?) Safari Zone}", + "safari_zone_option_2_label": "Leave", + "safari_zone_option_2_tooltip": "(-) No Rewards", + "safari_zone_option_1_selected_message": "Time to test your luck.", + "safari_zone_option_2_selected_message": "You hurry along your way,\nwith a slight feeling of regret.", + "safari_zone_pokeball_option_label": "Throw a Pokéball", + "safari_zone_pokeball_option_tooltip": "(+) Throw a Pokéball", + "safari_zone_pokeball_option_selected": "You throw a Pokéball!", + "safari_zone_bait_option_label": "Throw bait", + "safari_zone_bait_option_tooltip": "(+) Increases Capture Rate\n(-) Chance to Increase Flee Rate", + "safari_zone_bait_option_selected": "You throw some bait!", + "safari_zone_mud_option_label": "Throw mud", + "safari_zone_mud_option_tooltip": "(+) Decreases Flee Rate\n(-) Chance to Decrease Capture Rate", + "safari_zone_mud_option_selected": "You throw some mud!", + "safari_zone_flee_option_label": "Flee", + "safari_zone_flee_option_tooltip": "(?) Flee from this Pokémon", + "safari_zone_pokemon_watching": "{{pokemonName}} is watching carefully!", + "safari_zone_pokemon_eating": "{{pokemonName}} is eating!", + "safari_zone_pokemon_busy_eating": "{{pokemonName}} is busy eating!", + "safari_zone_pokemon_angry": "{{pokemonName}} is angry!", + "safari_zone_pokemon_beside_itself_angry": "{{pokemonName}} is beside itself with anger!", + "safari_zone_remaining_count": "{{remainingCount}} Pokémon remaining!", + + // Mystery Encounters -- Ultra Tier "training_session_intro_message": "You've come across some\ntraining tools and supplies.", "training_session_title": "Training Session", @@ -163,7 +195,7 @@ export const mysteryEncounter: SimpleTranslationEntries = { $Its ability was changed to {{ability}}!`, "training_session_outro_win": "That was a successful training session!", - // Mystery Encounters -- Super Rare Tier + // Mystery Encounters -- Rogue Tier "dark_deal_intro_message": "A strange man in a tattered coat\nstands in your way...", "dark_deal_speaker": "Shady Guy", diff --git a/src/phases.ts b/src/phases.ts index 4be1305c8b7..9bc61aa8454 100644 --- a/src/phases.ts +++ b/src/phases.ts @@ -1650,7 +1650,7 @@ export class SummonPhase extends PartyMemberPokemonPhase { pokemon.untint(250, "Sine.easeIn"); this.scene.updateFieldScale(); pokemon.x += 16; - pokemon.y -= 16; + pokemon.y -= 20; pokemon.alpha = 0; // Ease pokemon in @@ -1680,7 +1680,9 @@ export class SummonPhase extends PartyMemberPokemonPhase { pokemon.resetTurnData(); - if (!this.loaded || this.scene.currentBattle.battleType === BattleType.TRAINER || this.scene.currentBattle.battleType === BattleType.MYSTERY_ENCOUNTER || (this.scene.currentBattle.waveIndex % 10) === 1) { + const addPostSummonForEncounter = this.scene.currentBattle.battleType === BattleType.MYSTERY_ENCOUNTER && !this.scene.currentBattle.mysteryEncounter?.disableAllPostSummon; + + if (!this.loaded || this.scene.currentBattle.battleType === BattleType.TRAINER || (this.scene.currentBattle.waveIndex % 10) === 1 || addPostSummonForEncounter) { this.scene.triggerPokemonFormChange(pokemon, SpeciesFormChangeActiveTrigger, true); this.queuePostSummon(); } diff --git a/src/phases/mystery-encounter-phase.ts b/src/phases/mystery-encounter-phase.ts index 638f2c0b15d..96acce6adcc 100644 --- a/src/phases/mystery-encounter-phase.ts +++ b/src/phases/mystery-encounter-phase.ts @@ -15,6 +15,7 @@ import { Tutorial, handleTutorial } from "../tutorial"; import { IvScannerModifier } from "../modifier/modifier"; import * as Utils from "../utils"; import { isNullOrUndefined } from "../utils"; +import { MysteryEncounterUiSettings } from "#app/ui/mystery-encounter-ui-handler"; /** * Will handle (in order): @@ -26,8 +27,11 @@ import { isNullOrUndefined } from "../utils"; * - Queuing of the MysteryEncounterOptionSelectedPhase */ export class MysteryEncounterPhase extends Phase { - constructor(scene: BattleScene) { + followupOptionSelectSettings: MysteryEncounterUiSettings; + + constructor(scene: BattleScene, followupOptionSelectSettings?: MysteryEncounterUiSettings) { super(scene); + this.followupOptionSelectSettings = followupOptionSelectSettings; } start() { @@ -37,12 +41,18 @@ export class MysteryEncounterPhase extends Phase { this.scene.clearPhaseQueue(); this.scene.clearPhaseQueueSplice(); - // Sets flag that ME was encountered - // 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]); + // Generates seed offset for RNG consistency, but incremented if the same MysteryEncounter has multiple option select cycles + const offset = this.scene.currentBattle.mysteryEncounter.seedOffset ?? this.scene.currentBattle.waveIndex * 1000; + this.scene.currentBattle.mysteryEncounter.seedOffset = offset + 512; + + if (!this.followupOptionSelectSettings) { + // 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]); + } // Initiates encounter dialogue window and option select - this.scene.ui.setMode(Mode.MYSTERY_ENCOUNTER); + this.scene.ui.setMode(Mode.MYSTERY_ENCOUNTER, this.followupOptionSelectSettings); } handleOptionSelect(option: MysteryEncounterOption, index: number): boolean { @@ -61,24 +71,24 @@ export class MysteryEncounterPhase extends Phase { return await option.onPreOptionPhase(this.scene) .then((result) => { if (isNullOrUndefined(result) || result) { - this.continueEncounter(index); + this.continueEncounter(); } }); - }, this.scene.currentBattle.waveIndex * 1000); + }, this.scene.currentBattle.mysteryEncounter.seedOffset); } else { - this.continueEncounter(index); + this.continueEncounter(); } return true; } - continueEncounter(optionIndex: number) { + continueEncounter() { const endDialogueAndContinueEncounter = () => { this.scene.pushPhase(new MysteryEncounterOptionSelectedPhase(this.scene)); this.end(); }; - const optionSelectDialogue = this.scene.currentBattle?.mysteryEncounter?.options?.[optionIndex]?.dialogue; + const optionSelectDialogue = this.scene.currentBattle?.mysteryEncounter?.selectedOption?.dialogue; if (optionSelectDialogue?.selected?.length > 0) { // Handle intermediate dialogue (between player selection event and the onOptionSelect logic) this.scene.ui.setMode(Mode.MESSAGE); @@ -139,14 +149,14 @@ export class MysteryEncounterOptionSelectedPhase extends Phase { this.onOptionSelect(this.scene).finally(() => { this.end(); }); - }, this.scene.currentBattle.waveIndex * 1000); + }, this.scene.currentBattle.mysteryEncounter.seedOffset); }); } else { this.scene.executeWithSeedOffset(() => { this.onOptionSelect(this.scene).finally(() => { this.end(); }); - }, this.scene.currentBattle.waveIndex * 1000); + }, this.scene.currentBattle.mysteryEncounter.seedOffset); } } @@ -265,7 +275,7 @@ export class MysteryEncounterBattlePhase extends Phase { } else { const trainer = this.scene.currentBattle.trainer; let message: string; - scene.executeWithSeedOffset(() => message = Utils.randSeedItem(encounterMessages), scene.currentBattle.waveIndex * 1000); + scene.executeWithSeedOffset(() => message = Utils.randSeedItem(encounterMessages), this.scene.currentBattle.mysteryEncounter.seedOffset); const showDialogueAndSummon = () => { scene.ui.showDialogue(message, trainer.getName(TrainerSlot.NONE, true), null, () => { @@ -390,6 +400,7 @@ export class MysteryEncounterRewardsPhase extends Phase { this.scene.tryRemovePhase(p => p instanceof SelectModifierPhase); this.scene.unshiftPhase(new SelectModifierPhase(this.scene, 0, null, { fillRemaining: false, rerollMultiplier: 0 })); } + // Do not use ME's seedOffset for rewards, these should always be consistent with waveIndex (once per wave) }, this.scene.currentBattle.waveIndex * 1000); this.scene.pushPhase(new PostMysteryEncounterPhase(this.scene)); @@ -423,7 +434,7 @@ export class PostMysteryEncounterPhase extends Phase { this.continueEncounter(); } }); - }, this.scene.currentBattle.waveIndex * 1000); + }, this.scene.currentBattle.mysteryEncounter.seedOffset); } else { this.continueEncounter(); } diff --git a/src/pipelines/sprite.ts b/src/pipelines/sprite.ts index a61d321c765..e36765f0d4c 100644 --- a/src/pipelines/sprite.ts +++ b/src/pipelines/sprite.ts @@ -38,6 +38,7 @@ uniform vec2 texFrameUv; uniform vec2 size; uniform vec2 texSize; uniform float yOffset; +uniform float yShadowOffset; uniform vec4 tone; uniform ivec4 baseVariantColors[32]; uniform vec4 variantColors[32]; @@ -252,7 +253,7 @@ void main() { float width = size.x - (yOffset / 2.0); float spriteX = ((floor(outPosition.x / fieldScale) - relPosition.x) / width) + 0.5; - float spriteY = ((floor(outPosition.y / fieldScale) - relPosition.y) / size.y); + float spriteY = ((floor(outPosition.y / fieldScale) - relPosition.y - yShadowOffset) / size.y); if (yCenter == 1) { spriteY += 0.5; @@ -339,6 +340,7 @@ export default class SpritePipeline extends FieldSpritePipeline { this.set2f("size", 0, 0); this.set2f("texSize", 0, 0); this.set1f("yOffset", 0); + this.set1f("yShadowOffset", 0); this.set4fv("tone", this._tone); } @@ -351,6 +353,7 @@ export default class SpritePipeline extends FieldSpritePipeline { const tone = data["tone"] as number[]; const teraColor = data["teraColor"] as integer[] ?? [ 0, 0, 0 ]; const hasShadow = data["hasShadow"] as boolean; + const yShadowOffset = data["yShadowOffset"] as number; const ignoreFieldPos = data["ignoreFieldPos"] as boolean; const ignoreOverride = data["ignoreOverride"] as boolean; @@ -377,6 +380,7 @@ export default class SpritePipeline extends FieldSpritePipeline { this.set2f("size", sprite.frame.width, sprite.height); this.set2f("texSize", sprite.texture.source[0].width, sprite.texture.source[0].height); this.set1f("yOffset", sprite.height - sprite.frame.height * (isEntityObj ? sprite.parentContainer.scale : sprite.scale)); + this.set1f("yShadowOffset", yShadowOffset ?? 0); this.set4fv("tone", tone); this.bindTexture(this.game.textures.get("tera").source[0].glTexture, 1); @@ -448,6 +452,7 @@ export default class SpritePipeline extends FieldSpritePipeline { this.set1f("vCutoff", v1); const hasShadow = sprite.pipelineData["hasShadow"] as boolean; + const yShadowOffset = sprite.pipelineData["yShadowOffset"] as number; if (hasShadow) { const isEntityObj = sprite.parentContainer instanceof Pokemon || sprite.parentContainer instanceof Trainer || sprite.parentContainer instanceof MysteryEncounterIntroVisuals; const field = isEntityObj ? sprite.parentContainer.parentContainer : sprite.parentContainer; @@ -455,7 +460,7 @@ export default class SpritePipeline extends FieldSpritePipeline { const baseY = (isEntityObj ? sprite.parentContainer.y : sprite.y + sprite.height) * 6 / fieldScaleRatio; - const bottomPadding = Math.ceil(sprite.height * 0.05) * 6 / fieldScaleRatio; + const bottomPadding = Math.ceil(sprite.height * 0.05 + Math.max(yShadowOffset, 0)) * 6 / fieldScaleRatio; const yDelta = (baseY - y1) / field.scale; y2 = y1 = baseY + bottomPadding; const pixelHeight = (v1 - v0) / (sprite.frame.height * (isEntityObj ? sprite.parentContainer.scale : sprite.scale)); diff --git a/src/ui/mystery-encounter-ui-handler.ts b/src/ui/mystery-encounter-ui-handler.ts index e9ee0f1f6e7..800dd551dc4 100644 --- a/src/ui/mystery-encounter-ui-handler.ts +++ b/src/ui/mystery-encounter-ui-handler.ts @@ -12,6 +12,16 @@ import { isNullOrUndefined } from "../utils"; import { getPokeballAtlasKey } from "../data/pokeball"; import { getEncounterText } from "#app/data/mystery-encounters/mystery-encounter-utils"; +export class MysteryEncounterUiSettings { + hideDescription?: boolean; + slideInDescription?: boolean; + overrideTitle?: string; + overrideDescription?: string; + overrideQuery?: string; + overrideOptions?: MysteryEncounterOption[]; + startingCursorIndex?: number; +} + export default class MysteryEncounterUiHandler extends UiHandler { private cursorContainer: Phaser.GameObjects.Container; private cursorObj: Phaser.GameObjects.Image; @@ -27,7 +37,8 @@ export default class MysteryEncounterUiHandler extends UiHandler { private descriptionScrollTween: Phaser.Tweens.Tween; private rarityBall: Phaser.GameObjects.Sprite; - private filteredEncounterOptions: MysteryEncounterOption[] = []; + private overrideSettings: MysteryEncounterUiSettings; + private encounterOptions: MysteryEncounterOption[] = []; private optionsMeetsReqs: boolean[]; protected viewPartyIndex: integer = 0; @@ -70,16 +81,21 @@ export default class MysteryEncounterUiHandler extends UiHandler { show(args: any[]): boolean { super.show(args); + this.overrideSettings = args[0] as MysteryEncounterUiSettings ?? {}; + const showDescriptionContainer = isNullOrUndefined(this.overrideSettings?.hideDescription) ? true : !this.overrideSettings?.hideDescription; + const slideInDescription = isNullOrUndefined(this.overrideSettings?.slideInDescription) ? true : this.overrideSettings?.slideInDescription; + const startingCursorIndex = this.overrideSettings?.startingCursorIndex ?? 0; + this.cursorContainer.setVisible(true); - this.descriptionContainer.setVisible(true); + this.descriptionContainer.setVisible(showDescriptionContainer); this.optionsContainer.setVisible(true); - this.displayEncounterOptions(!(args[0] as boolean || false)); + this.displayEncounterOptions(slideInDescription); const cursor = this.getCursor(); if (cursor === (this?.optionsContainer?.length || 0) - 1) { // Always resets cursor on view party button if it was last there this.setCursor(cursor); } else { - this.setCursor(0); + this.setCursor(startingCursorIndex); } if (this.blockInput) { setTimeout(() => { @@ -100,12 +116,16 @@ export default class MysteryEncounterUiHandler extends UiHandler { if (button === Button.CANCEL || button === Button.ACTION) { if (button === Button.ACTION) { - const selected = this.filteredEncounterOptions[cursor]; + const selected = this.encounterOptions[cursor]; if (cursor === this.viewPartyIndex) { // Handle view party success = true; + const overrideSettings: MysteryEncounterUiSettings = { + ...this.overrideSettings, + slideInDescription: false + }; this.scene.ui.setMode(Mode.PARTY, PartyUiMode.CHECK, -1, () => { - this.scene.ui.setMode(Mode.MYSTERY_ENCOUNTER, true); + this.scene.ui.setMode(Mode.MYSTERY_ENCOUNTER, overrideSettings); setTimeout(() => { this.setCursor(this.viewPartyIndex); this.unblockInput(); @@ -253,7 +273,7 @@ export default class MysteryEncounterUiHandler extends UiHandler { if (this.blockInput) { this.blockInput = false; for (let i = 0; i < this.optionsContainer.length - 1; i++) { - const optionMode = this.filteredEncounterOptions[i].optionMode; + const optionMode = this.encounterOptions[i].optionMode; if (!this.optionsMeetsReqs[i] && (optionMode === EncounterOptionMode.DISABLED_OR_DEFAULT || optionMode === EncounterOptionMode.DISABLED_OR_SPECIAL)) { continue; } @@ -296,7 +316,7 @@ export default class MysteryEncounterUiHandler extends UiHandler { displayEncounterOptions(slideInDescription: boolean = true): void { this.getUi().clearText(); const mysteryEncounter = this.scene.currentBattle.mysteryEncounter; - this.filteredEncounterOptions = mysteryEncounter.options; + this.encounterOptions = this.overrideSettings?.overrideOptions ?? mysteryEncounter.options; this.optionsMeetsReqs = []; const titleText: string = getEncounterText(this.scene, mysteryEncounter.dialogue.encounterOptionsDialogue.title, TextStyle.TOOLTIP_TITLE); @@ -307,11 +327,11 @@ export default class MysteryEncounterUiHandler extends UiHandler { this.optionsContainer.removeAll(); // Options Window - for (let i = 0; i < this.filteredEncounterOptions.length; i++) { - const option = this.filteredEncounterOptions[i]; + for (let i = 0; i < this.encounterOptions.length; i++) { + const option = this.encounterOptions[i]; let optionText; - switch (this.filteredEncounterOptions.length) { + switch (this.encounterOptions.length) { case 2: optionText = addBBCodeTextObject(this.scene, i % 2 === 0 ? 0 : 100, 8, "-", TextStyle.WINDOW, { wordWrap: { width: 558 }, fontSize: "80px", lineSpacing: -8 }); break; @@ -424,7 +444,7 @@ export default class MysteryEncounterUiHandler extends UiHandler { } let text: string; - const cursorOption = this.filteredEncounterOptions[cursor]; + const cursorOption = this.encounterOptions[cursor]; const optionDialogue = cursorOption.dialogue; if (!this.optionsMeetsReqs[cursor] && (cursorOption.optionMode === EncounterOptionMode.DISABLED_OR_DEFAULT || cursorOption.optionMode === EncounterOptionMode.DISABLED_OR_SPECIAL) && optionDialogue.disabledButtonTooltip) { text = getEncounterText(this.scene, optionDialogue.disabledButtonTooltip, TextStyle.TOOLTIP_CONTENT); @@ -474,6 +494,7 @@ export default class MysteryEncounterUiHandler extends UiHandler { clear(): void { super.clear(); + this.overrideSettings = null; this.optionsContainer.setVisible(false); this.optionsContainer.removeAll(true); this.descriptionContainer.setVisible(false);