diff --git a/src/battle-scene.ts b/src/battle-scene.ts index a017b8eb49e..a031ee0bca2 100644 --- a/src/battle-scene.ts +++ b/src/battle-scene.ts @@ -66,10 +66,12 @@ import { Species } from "#enums/species"; import { UiTheme } from "#enums/ui-theme"; import { TimedEventManager } from "#app/timed-event-manager.js"; import i18next from "i18next"; -import IMysteryEncounter, { MysteryEncounterTier, MysteryEncounterVariant } from "./data/mystery-encounters/mystery-encounter"; +import IMysteryEncounter from "./data/mystery-encounters/mystery-encounter"; import { allMysteryEncounters, AVERAGE_ENCOUNTERS_PER_RUN_TARGET, BASE_MYSTERY_ENCOUNTER_SPAWN_WEIGHT, mysteryEncountersByBiome, WEIGHT_INCREMENT_ON_SPAWN_MISS } from "./data/mystery-encounters/mystery-encounters"; import { MysteryEncounterData } from "#app/data/mystery-encounters/mystery-encounter-data"; import { MysteryEncounterType } from "#enums/mystery-encounter-type"; +import { MysteryEncounterMode } from "#enums/mystery-encounter-mode"; +import { MysteryEncounterTier } from "#enums/mystery-encounter-tier"; export const bypassLogin = import.meta.env.VITE_BYPASS_LOGIN === "1"; @@ -1184,7 +1186,7 @@ export default class BattleScene extends SceneBase { this.arena.removeAllTags(); // If last battle was mystery encounter and no battle occurred, skip return phases - if (lastBattle?.mysteryEncounter?.encounterVariant !== MysteryEncounterVariant.NO_BATTLE) { + if (lastBattle?.mysteryEncounter?.encounterMode !== MysteryEncounterMode.NO_BATTLE) { playerField.forEach((_, p) => this.unshiftPhase(new ReturnPhase(this, p))); for (const pokemon of this.getParty()) { diff --git a/src/battle.ts b/src/battle.ts index 2c033ce38bf..b30b820e628 100644 --- a/src/battle.ts +++ b/src/battle.ts @@ -14,7 +14,8 @@ import { PlayerGender } from "#enums/player-gender"; import { Species } from "#enums/species"; import { TrainerType } from "#enums/trainer-type"; import i18next from "#app/plugins/i18n"; -import IMysteryEncounter, { MysteryEncounterVariant } from "./data/mystery-encounters/mystery-encounter"; +import IMysteryEncounter from "./data/mystery-encounters/mystery-encounter"; +import { MysteryEncounterMode } from "#enums/mystery-encounter-mode"; export enum BattleType { WILD, @@ -205,7 +206,7 @@ export default class Battle { getBgmOverride(scene: BattleScene): string { const battlers = this.enemyParty.slice(0, this.getBattlerCount()); - if (this.battleType === BattleType.TRAINER || this.mysteryEncounter?.encounterVariant === MysteryEncounterVariant.TRAINER_BATTLE) { + if (this.battleType === BattleType.TRAINER || this.mysteryEncounter?.encounterMode === MysteryEncounterMode.TRAINER_BATTLE) { if (!this.started && this.trainer.config.encounterBgm && this.trainer.getEncounterMessages().length) { return `encounter_${this.trainer.getEncounterBgm()}`; } diff --git a/src/data/mystery-encounters/encounters/dark-deal-encounter.ts b/src/data/mystery-encounters/encounters/dark-deal-encounter.ts index 16c125806b2..1e73ffd11c0 100644 --- a/src/data/mystery-encounters/encounters/dark-deal-encounter.ts +++ b/src/data/mystery-encounters/encounters/dark-deal-encounter.ts @@ -3,13 +3,15 @@ import { ModifierRewardPhase } from "#app/phases"; import { isNullOrUndefined, randSeedInt } from "#app/utils"; import { MysteryEncounterType } from "#enums/mystery-encounter-type"; import { Species } from "#enums/species"; -import BattleScene from "../../../battle-scene"; +import BattleScene from "#app/battle-scene"; import { modifierTypes } from "#app/modifier/modifier-type"; -import { getPokemonSpecies } from "../../pokemon-species"; -import IMysteryEncounter, { MysteryEncounterBuilder, MysteryEncounterTier, } from "../mystery-encounter"; -import { EncounterOptionMode, MysteryEncounterOptionBuilder } from "../mystery-encounter-option"; +import { getPokemonSpecies } from "#app/data/pokemon-species"; +import IMysteryEncounter, { MysteryEncounterBuilder } from "../mystery-encounter"; +import { MysteryEncounterOptionBuilder } from "../mystery-encounter-option"; import { EnemyPartyConfig, EnemyPokemonConfig, initBattleWithEnemyConfig, leaveEncounterWithoutBattle, } from "../utils/encounter-phase-utils"; import { getRandomPlayerPokemon, getRandomSpeciesByStarterTier } from "#app/data/mystery-encounters/utils/encounter-pokemon-utils"; +import { MysteryEncounterTier } from "#enums/mystery-encounter-tier"; +import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode"; /** i18n namespace for encounter */ const namespace = "mysteryEncounter:darkDeal"; @@ -105,7 +107,7 @@ export const DarkDealEncounter: IMysteryEncounter = .withQuery(`${namespace}:query`) .withOption( new MysteryEncounterOptionBuilder() - .withOptionMode(EncounterOptionMode.DEFAULT) + .withOptionMode(MysteryEncounterOptionMode.DEFAULT) .withDialogue({ buttonLabel: `${namespace}:option:1:label`, buttonTooltip: `${namespace}:option:1:tooltip`, diff --git a/src/data/mystery-encounters/encounters/department-store-sale-encounter.ts b/src/data/mystery-encounters/encounters/department-store-sale-encounter.ts index 131dc68aa14..610b836ef9e 100644 --- a/src/data/mystery-encounters/encounters/department-store-sale-encounter.ts +++ b/src/data/mystery-encounters/encounters/department-store-sale-encounter.ts @@ -6,11 +6,11 @@ import { modifierTypes } from "#app/modifier/modifier-type"; import { randSeedInt } from "#app/utils"; import { MysteryEncounterType } from "#enums/mystery-encounter-type"; import { Species } from "#enums/species"; -import BattleScene from "../../../battle-scene"; +import BattleScene from "#app/battle-scene"; import IMysteryEncounter, { MysteryEncounterBuilder, - MysteryEncounterTier, } from "../mystery-encounter"; +import { MysteryEncounterTier } from "#enums/mystery-encounter-tier"; /** i18n namespace for encounter */ const namespace = "mysteryEncounter:departmentStoreSale"; diff --git a/src/data/mystery-encounters/encounters/field-trip-encounter.ts b/src/data/mystery-encounters/encounters/field-trip-encounter.ts index 525e2b91f8b..9f912be1a35 100644 --- a/src/data/mystery-encounters/encounters/field-trip-encounter.ts +++ b/src/data/mystery-encounters/encounters/field-trip-encounter.ts @@ -1,22 +1,15 @@ import { MoveCategory } from "#app/data/move"; -import { EncounterOptionMode, MysteryEncounterOptionBuilder } from "#app/data/mystery-encounters/mystery-encounter-option"; -import { - generateModifierTypeOption, - leaveEncounterWithoutBattle, - selectPokemonForOption, - setEncounterExp, - setEncounterRewards, -} from "#app/data/mystery-encounters/utils/encounter-phase-utils"; +import { MysteryEncounterOptionBuilder } from "#app/data/mystery-encounters/mystery-encounter-option"; +import { generateModifierTypeOption, leaveEncounterWithoutBattle, selectPokemonForOption, setEncounterExp, setEncounterRewards } from "#app/data/mystery-encounters/utils/encounter-phase-utils"; import { TempBattleStat } from "#app/data/temp-battle-stat"; import { PlayerPokemon, PokemonMove } from "#app/field/pokemon"; import { modifierTypes } from "#app/modifier/modifier-type"; import { OptionSelectItem } from "#app/ui/abstact-option-select-ui-handler"; import { MysteryEncounterType } from "#enums/mystery-encounter-type"; -import BattleScene from "../../../battle-scene"; -import IMysteryEncounter, { - MysteryEncounterBuilder, - MysteryEncounterTier, -} from "../mystery-encounter"; +import BattleScene from "#app/battle-scene"; +import IMysteryEncounter, { MysteryEncounterBuilder } from "../mystery-encounter"; +import { MysteryEncounterTier } from "#enums/mystery-encounter-tier"; +import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode"; /** i18n namespace for the encounter */ const namespace = "mysteryEncounter:fieldTrip"; @@ -62,7 +55,7 @@ export const FieldTripEncounter: IMysteryEncounter = .withQuery(`${namespace}:query`) .withOption( new MysteryEncounterOptionBuilder() - .withOptionMode(EncounterOptionMode.DEFAULT) + .withOptionMode(MysteryEncounterOptionMode.DEFAULT) .withDialogue({ buttonLabel: `${namespace}:option:1:label`, buttonTooltip: `${namespace}:option:1:tooltip`, @@ -88,7 +81,7 @@ export const FieldTripEncounter: IMysteryEncounter = encounter.options[0].dialogue.selected = [ { text: `${namespace}:option:incorrect`, - speaker: `${namespace}:option:speaker`, + speaker: `${namespace}:speaker`, }, { text: `${namespace}:option:lesson_learned`, @@ -148,7 +141,7 @@ export const FieldTripEncounter: IMysteryEncounter = ) .withOption( new MysteryEncounterOptionBuilder() - .withOptionMode(EncounterOptionMode.DEFAULT) + .withOptionMode(MysteryEncounterOptionMode.DEFAULT) .withDialogue({ buttonLabel: `${namespace}:option:2:label`, buttonTooltip: `${namespace}:option:2:tooltip`, @@ -174,7 +167,7 @@ export const FieldTripEncounter: IMysteryEncounter = encounter.options[1].dialogue.selected = [ { text: `${namespace}:option:incorrect`, - speaker: `${namespace}:option:speaker`, + speaker: `${namespace}:speaker`, }, { text: `${namespace}:option:lesson_learned`, @@ -240,7 +233,7 @@ export const FieldTripEncounter: IMysteryEncounter = ) .withOption( new MysteryEncounterOptionBuilder() - .withOptionMode(EncounterOptionMode.DEFAULT) + .withOptionMode(MysteryEncounterOptionMode.DEFAULT) .withDialogue({ buttonLabel: `${namespace}:option:3:label`, buttonTooltip: `${namespace}:option:3:tooltip`, @@ -266,7 +259,7 @@ export const FieldTripEncounter: IMysteryEncounter = encounter.options[2].dialogue.selected = [ { text: `${namespace}:option:incorrect`, - speaker: `${namespace}:option:speaker`, + speaker: `${namespace}:speaker`, }, { text: `${namespace}:option:lesson_learned`, diff --git a/src/data/mystery-encounters/encounters/fiery-fallout-encounter.ts b/src/data/mystery-encounters/encounters/fiery-fallout-encounter.ts index d61a0c1ff4e..478a604869b 100644 --- a/src/data/mystery-encounters/encounters/fiery-fallout-encounter.ts +++ b/src/data/mystery-encounters/encounters/fiery-fallout-encounter.ts @@ -1,9 +1,9 @@ -import { EncounterOptionMode, MysteryEncounterOptionBuilder } from "#app/data/mystery-encounters/mystery-encounter-option"; +import { MysteryEncounterOptionBuilder } from "#app/data/mystery-encounters/mystery-encounter-option"; import { EnemyPartyConfig, generateModifierTypeOption, initBattleWithEnemyConfig, initCustomMovesForEncounter, leaveEncounterWithoutBattle, setEncounterExp, setEncounterRewards, transitionMysteryEncounterIntroVisuals } from "#app/data/mystery-encounters/utils/encounter-phase-utils"; import { modifierTypes, } from "#app/modifier/modifier-type"; import { MysteryEncounterType } from "#enums/mystery-encounter-type"; -import BattleScene from "../../../battle-scene"; -import IMysteryEncounter, { MysteryEncounterBuilder, MysteryEncounterTier, } from "../mystery-encounter"; +import BattleScene from "#app/battle-scene"; +import IMysteryEncounter, { MysteryEncounterBuilder } from "../mystery-encounter"; import { TypeRequirement } from "../mystery-encounter-requirements"; import { Species } from "#enums/species"; import { getPokemonSpecies } from "#app/data/pokemon-species"; @@ -18,6 +18,8 @@ import { isNullOrUndefined, randSeedInt } from "#app/utils"; import { StatusEffect } from "#app/data/status-effect"; import { queueEncounterMessage } from "#app/data/mystery-encounters/utils/encounter-dialogue-utils"; import { applyDamageToPokemon } from "#app/data/mystery-encounters/utils/encounter-pokemon-utils"; +import { MysteryEncounterTier } from "#enums/mystery-encounter-tier"; +import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode"; /** the i18n namespace for the encounter */ const namespace = "mysteryEncounter:fieryFallout"; @@ -202,7 +204,7 @@ export const FieryFalloutEncounter: IMysteryEncounter = ) .withOption( new MysteryEncounterOptionBuilder() - .withOptionMode(EncounterOptionMode.DISABLED_OR_SPECIAL) + .withOptionMode(MysteryEncounterOptionMode.DISABLED_OR_SPECIAL) .withPrimaryPokemonRequirement(new TypeRequirement(Type.FIRE, true, 1)) // Will set option3PrimaryName dialogue token automatically .withSecondaryPokemonRequirement(new TypeRequirement(Type.FIRE, true, 1)) // Will set option3SecondaryName dialogue token automatically .withDialogue({ 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 ad693d457d9..f4460de47d7 100644 --- a/src/data/mystery-encounters/encounters/fight-or-flight-encounter.ts +++ b/src/data/mystery-encounters/encounters/fight-or-flight-encounter.ts @@ -1,5 +1,5 @@ import { BattleStat } from "#app/data/battle-stat"; -import { EncounterOptionMode, MysteryEncounterOptionBuilder } from "#app/data/mystery-encounters/mystery-encounter-option"; +import { MysteryEncounterOptionBuilder } from "#app/data/mystery-encounters/mystery-encounter-option"; import { EnemyPartyConfig, initBattleWithEnemyConfig, @@ -20,14 +20,13 @@ import { StatChangePhase } from "#app/phases"; import { randSeedInt } from "#app/utils"; import { BattlerTagType } from "#enums/battler-tag-type"; import { MysteryEncounterType } from "#enums/mystery-encounter-type"; -import BattleScene from "../../../battle-scene"; -import IMysteryEncounter, { - MysteryEncounterBuilder, - MysteryEncounterTier, -} from "../mystery-encounter"; +import BattleScene from "#app/battle-scene"; +import IMysteryEncounter, { MysteryEncounterBuilder } from "../mystery-encounter"; import { MoveRequirement } from "../mystery-encounter-requirements"; import { queueEncounterMessage, showEncounterText } from "#app/data/mystery-encounters/utils/encounter-dialogue-utils"; import { getPokemonNameWithAffix } from "#app/messages"; +import { MysteryEncounterTier } from "#enums/mystery-encounter-tier"; +import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode"; /** the i18n namespace for the encounter */ const namespace = "mysteryEncounter:fightOrFlight"; @@ -132,7 +131,7 @@ export const FightOrFlightEncounter: IMysteryEncounter = ) .withOption( new MysteryEncounterOptionBuilder() - .withOptionMode(EncounterOptionMode.DEFAULT_OR_SPECIAL) + .withOptionMode(MysteryEncounterOptionMode.DEFAULT_OR_SPECIAL) .withPrimaryPokemonRequirement(new MoveRequirement(STEALING_MOVES)) // Will set option2PrimaryName and option2PrimaryMove dialogue tokens automatically .withDialogue({ buttonLabel: `${namespace}:option:2:label`, 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 7becc65a160..a4e47311f43 100644 --- a/src/data/mystery-encounters/encounters/lost-at-sea-encounter.ts +++ b/src/data/mystery-encounters/encounters/lost-at-sea-encounter.ts @@ -2,11 +2,13 @@ import { getPokemonSpecies } from "#app/data/pokemon-species.js"; import { Moves } from "#app/enums/moves"; import { Species } from "#app/enums/species.js"; import { MysteryEncounterType } from "#enums/mystery-encounter-type"; -import BattleScene from "../../../battle-scene"; -import MysteryEncounter, { MysteryEncounterBuilder, MysteryEncounterTier } from "../mystery-encounter"; -import { EncounterOptionMode, MysteryEncounterOptionBuilder } from "../mystery-encounter-option"; +import BattleScene from "#app/battle-scene"; +import MysteryEncounter, { MysteryEncounterBuilder } from "../mystery-encounter"; +import { MysteryEncounterOptionBuilder } from "../mystery-encounter-option"; import { leaveEncounterWithoutBattle, setEncounterExp } from "../utils/encounter-phase-utils"; import { applyDamageToPokemon } from "#app/data/mystery-encounters/utils/encounter-pokemon-utils"; +import { MysteryEncounterTier } from "#enums/mystery-encounter-tier"; +import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode"; const OPTION_1_REQUIRED_MOVE = Moves.SURF; const OPTION_2_REQUIRED_MOVE = Moves.FLY; @@ -53,7 +55,7 @@ export const LostAtSeaEncounter: MysteryEncounter = MysteryEncounterBuilder.with // Option 1: Use a (non fainted) pokemon that can learn Surf to guide you back/ new MysteryEncounterOptionBuilder() .withPokemonCanLearnMoveRequirement(OPTION_1_REQUIRED_MOVE) - .withOptionMode(EncounterOptionMode.DISABLED_OR_DEFAULT) + .withOptionMode(MysteryEncounterOptionMode.DISABLED_OR_DEFAULT) .withDialogue({ buttonLabel: `${namespace}:option:1:label`, disabledButtonLabel: `${namespace}:option:1:label_disabled`, @@ -72,7 +74,7 @@ export const LostAtSeaEncounter: MysteryEncounter = MysteryEncounterBuilder.with //Option 2: Use a (non fainted) pokemon that can learn fly to guide you back. new MysteryEncounterOptionBuilder() .withPokemonCanLearnMoveRequirement(OPTION_2_REQUIRED_MOVE) - .withOptionMode(EncounterOptionMode.DISABLED_OR_DEFAULT) + .withOptionMode(MysteryEncounterOptionMode.DISABLED_OR_DEFAULT) .withDialogue({ buttonLabel: `${namespace}:option:2:label`, disabledButtonLabel: `${namespace}:option:2:label_disabled`, diff --git a/src/data/mystery-encounters/encounters/mysterious-challengers-encounter.ts b/src/data/mystery-encounters/encounters/mysterious-challengers-encounter.ts index 76bbc58a9d9..ab77f0c4646 100644 --- a/src/data/mystery-encounters/encounters/mysterious-challengers-encounter.ts +++ b/src/data/mystery-encounters/encounters/mysterious-challengers-encounter.ts @@ -13,12 +13,10 @@ import { ModifierTier } from "#app/modifier/modifier-tier"; import { modifierTypes } from "#app/modifier/modifier-type"; import { MysteryEncounterType } from "#enums/mystery-encounter-type"; import { PartyMemberStrength } from "#enums/party-member-strength"; -import BattleScene from "../../../battle-scene"; -import * as Utils from "../../../utils"; -import IMysteryEncounter, { - MysteryEncounterBuilder, - MysteryEncounterTier, -} from "../mystery-encounter"; +import BattleScene from "#app/battle-scene"; +import * as Utils from "#app/utils"; +import IMysteryEncounter, { MysteryEncounterBuilder } from "../mystery-encounter"; +import { MysteryEncounterTier } from "#enums/mystery-encounter-tier"; /** the i18n namespace for the encounter */ const namespace = "mysteryEncounter:mysteriousChallengers"; diff --git a/src/data/mystery-encounters/encounters/mysterious-chest-encounter.ts b/src/data/mystery-encounters/encounters/mysterious-chest-encounter.ts index 13c5b9cc1e4..e418655bbf5 100644 --- a/src/data/mystery-encounters/encounters/mysterious-chest-encounter.ts +++ b/src/data/mystery-encounters/encounters/mysterious-chest-encounter.ts @@ -4,9 +4,11 @@ import { getHighestLevelPlayerPokemon, koPlayerPokemon } from "#app/data/mystery import { ModifierTier } from "#app/modifier/modifier-tier"; import { randSeedInt } from "#app/utils.js"; 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 BattleScene from "#app/battle-scene"; +import IMysteryEncounter, { MysteryEncounterBuilder } from "../mystery-encounter"; +import { MysteryEncounterOptionBuilder } from "../mystery-encounter-option"; +import { MysteryEncounterTier } from "#enums/mystery-encounter-tier"; +import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode"; /** i18n namespace for encounter */ const namespace = "mysteryEncounter:mysteriousChest"; @@ -42,7 +44,7 @@ export const MysteriousChestEncounter: IMysteryEncounter = .withQuery(`${namespace}:query`) .withOption( new MysteryEncounterOptionBuilder() - .withOptionMode(EncounterOptionMode.DEFAULT) + .withOptionMode(MysteryEncounterOptionMode.DEFAULT) .withDialogue({ buttonLabel: `${namespace}:option:1:label`, buttonTooltip: `${namespace}:option:1:tooltip`, diff --git a/src/data/mystery-encounters/encounters/offer-you-cant-refuse-encounter.ts b/src/data/mystery-encounters/encounters/offer-you-cant-refuse-encounter.ts index 694acfbdb94..80510430be3 100644 --- a/src/data/mystery-encounters/encounters/offer-you-cant-refuse-encounter.ts +++ b/src/data/mystery-encounters/encounters/offer-you-cant-refuse-encounter.ts @@ -2,14 +2,16 @@ import { leaveEncounterWithoutBattle, setEncounterExp, updatePlayerMoney, } from import { modifierTypes } from "#app/modifier/modifier-type"; import { MysteryEncounterType } from "#enums/mystery-encounter-type"; import { Species } from "#enums/species"; -import BattleScene from "../../../battle-scene"; -import IMysteryEncounter, { MysteryEncounterBuilder, MysteryEncounterTier, } from "../mystery-encounter"; -import { EncounterOptionMode, MysteryEncounterOptionBuilder } from "../mystery-encounter-option"; -import { MoveRequirement } from "../mystery-encounter-requirements"; +import BattleScene from "#app/battle-scene"; +import IMysteryEncounter, { MysteryEncounterBuilder } from "../mystery-encounter"; +import { MysteryEncounterOptionBuilder } from "../mystery-encounter-option"; +import { AbilityRequirement, CombinationPokemonRequirement, MoveRequirement } from "../mystery-encounter-requirements"; import { getHighestStatTotalPlayerPokemon } from "#app/data/mystery-encounters/utils/encounter-pokemon-utils"; import { ModifierRewardPhase } from "#app/phases"; -import { EXTORTION_MOVES } from "#app/data/mystery-encounters/requirements/requirement-groups"; +import { EXTORTION_ABILITIES, EXTORTION_MOVES } from "#app/data/mystery-encounters/requirements/requirement-groups"; import { getPokemonSpecies } from "#app/data/pokemon-species"; +import { MysteryEncounterTier } from "#enums/mystery-encounter-tier"; +import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode"; /** the i18n namespace for this encounter */ const namespace = "mysteryEncounter:offerYouCantRefuse"; @@ -56,22 +58,36 @@ export const OfferYouCantRefuseEncounter: IMysteryEncounter = .withDescription(`${namespace}:description`) .withQuery(`${namespace}:query`) .withOnInit((scene: BattleScene) => { + const encounter = scene.currentBattle.mysteryEncounter; const pokemon = getHighestStatTotalPlayerPokemon(scene, false); const price = scene.getWaveMoneyAmount(10); - scene.currentBattle.mysteryEncounter.setDialogueToken("strongestPokemon", pokemon.name); - scene.currentBattle.mysteryEncounter.setDialogueToken("price", price.toString()); + encounter.setDialogueToken("strongestPokemon", pokemon.name); + encounter.setDialogueToken("price", price.toString()); // Store pokemon and price - scene.currentBattle.mysteryEncounter.misc = { + encounter.misc = { pokemon: pokemon, price: price }; + + // If player meets the combo OR requirements for option 2, populate the token + const opt2Req = encounter.options[1].primaryPokemonRequirements[0]; + if (opt2Req.meetsRequirement(scene)) { + const abilityToken = encounter.dialogueTokens["option2PrimaryAbility"]; + const moveToken = encounter.dialogueTokens["option2PrimaryMove"]; + if (abilityToken) { + encounter.setDialogueToken("moveOrAbility", abilityToken); + } else if (moveToken) { + encounter.setDialogueToken("moveOrAbility", moveToken); + } + } + return true; }) .withOption( new MysteryEncounterOptionBuilder() - .withOptionMode(EncounterOptionMode.DEFAULT) + .withOptionMode(MysteryEncounterOptionMode.DEFAULT) .withDialogue({ buttonLabel: `${namespace}:option:1:label`, buttonTooltip: `${namespace}:option:1:tooltip`, @@ -98,14 +114,18 @@ export const OfferYouCantRefuseEncounter: IMysteryEncounter = ) .withOption( new MysteryEncounterOptionBuilder() - .withOptionMode(EncounterOptionMode.DISABLED_OR_DEFAULT) - .withPrimaryPokemonRequirement(new MoveRequirement(EXTORTION_MOVES)) + .withOptionMode(MysteryEncounterOptionMode.DISABLED_OR_SPECIAL) + .withPrimaryPokemonRequirement(new CombinationPokemonRequirement( + new MoveRequirement(EXTORTION_MOVES), + new AbilityRequirement(EXTORTION_ABILITIES)) + ) .withDialogue({ buttonLabel: `${namespace}:option:2:label`, buttonTooltip: `${namespace}:option:2:tooltip`, disabledButtonTooltip: `${namespace}:option:2:tooltip_disabled`, selected: [ { + speaker: `${namespace}:speaker`, text: `${namespace}:option:2:selected`, }, ], @@ -129,7 +149,7 @@ export const OfferYouCantRefuseEncounter: IMysteryEncounter = selected: [ { speaker: `${namespace}:speaker`, - text: `${namespace}:option:2:selected`, + text: `${namespace}:option:3:selected`, }, ], }, diff --git a/src/data/mystery-encounters/encounters/pokemon-salesman-encounter.ts b/src/data/mystery-encounters/encounters/pokemon-salesman-encounter.ts index 9c8ee2ac91c..76077bf748b 100644 --- a/src/data/mystery-encounters/encounters/pokemon-salesman-encounter.ts +++ b/src/data/mystery-encounters/encounters/pokemon-salesman-encounter.ts @@ -1,17 +1,19 @@ import { leaveEncounterWithoutBattle, transitionMysteryEncounterIntroVisuals, updatePlayerMoney, } from "#app/data/mystery-encounters/utils/encounter-phase-utils"; import { isNullOrUndefined, randSeedInt } from "#app/utils"; import { MysteryEncounterType } from "#enums/mystery-encounter-type"; -import BattleScene from "../../../battle-scene"; -import IMysteryEncounter, { MysteryEncounterBuilder, MysteryEncounterTier, } from "../mystery-encounter"; +import BattleScene from "#app/battle-scene"; +import IMysteryEncounter, { MysteryEncounterBuilder } from "../mystery-encounter"; import { MoneyRequirement } from "../mystery-encounter-requirements"; import { catchPokemon, getRandomSpeciesByStarterTier } from "#app/data/mystery-encounters/utils/encounter-pokemon-utils"; import { getPokemonSpecies, speciesStarters } from "#app/data/pokemon-species"; import { Species } from "#enums/species"; import { PokeballType } from "#app/data/pokeball"; import { EnemyPokemon, PlayerPokemon } from "#app/field/pokemon"; -import { EncounterOptionMode, MysteryEncounterOptionBuilder } from "#app/data/mystery-encounters/mystery-encounter-option"; +import { MysteryEncounterOptionBuilder } from "#app/data/mystery-encounters/mystery-encounter-option"; import { showEncounterDialogue } from "#app/data/mystery-encounters/utils/encounter-dialogue-utils"; import PokemonData from "#app/system/pokemon-data"; +import { MysteryEncounterTier } from "#enums/mystery-encounter-tier"; +import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode"; /** the i18n namespace for this encounter */ const namespace = "mysteryEncounter:pokemonSalesman"; @@ -104,7 +106,7 @@ export const PokemonSalesmanEncounter: IMysteryEncounter = }) .withOption( new MysteryEncounterOptionBuilder() - .withOptionMode(EncounterOptionMode.DEFAULT_OR_SPECIAL) + .withOptionMode(MysteryEncounterOptionMode.DEFAULT_OR_SPECIAL) .withHasDexProgress(true) .withSceneMoneyRequirement(null, MAX_POKEMON_PRICE_MULTIPLIER) // Wave scaling money multiplier of 2 .withDialogue({ diff --git a/src/data/mystery-encounters/encounters/safari-zone-encounter.ts b/src/data/mystery-encounters/encounters/safari-zone-encounter.ts index 147ce2a5e2c..f9d4e487ab3 100644 --- a/src/data/mystery-encounters/encounters/safari-zone-encounter.ts +++ b/src/data/mystery-encounters/encounters/safari-zone-encounter.ts @@ -1,8 +1,8 @@ import { initSubsequentOptionSelect, leaveEncounterWithoutBattle, updatePlayerMoney, } from "#app/data/mystery-encounters/utils/encounter-phase-utils"; import { MysteryEncounterType } from "#enums/mystery-encounter-type"; -import BattleScene from "../../../battle-scene"; -import IMysteryEncounter, { MysteryEncounterBuilder, MysteryEncounterTier, MysteryEncounterVariant } from "../mystery-encounter"; -import MysteryEncounterOption, { EncounterOptionMode, MysteryEncounterOptionBuilder } from "#app/data/mystery-encounters/mystery-encounter-option"; +import BattleScene from "#app/battle-scene"; +import IMysteryEncounter, { MysteryEncounterBuilder } from "../mystery-encounter"; +import MysteryEncounterOption, { MysteryEncounterOptionBuilder } from "#app/data/mystery-encounters/mystery-encounter-option"; import { TrainerSlot } from "#app/data/trainer-config"; import { ScanIvsPhase, SummonPhase, VictoryPhase } from "#app/phases"; import { HiddenAbilityRateBoosterModifier, IvScannerModifier } from "#app/modifier/modifier"; @@ -15,6 +15,9 @@ import { MoneyRequirement } from "#app/data/mystery-encounters/mystery-encounter import { doPlayerFlee, doPokemonFlee, getRandomSpeciesByStarterTier, trainerThrowPokeball } from "#app/data/mystery-encounters/utils/encounter-pokemon-utils"; import { getEncounterText, showEncounterText } from "#app/data/mystery-encounters/utils/encounter-dialogue-utils"; import { getPokemonNameWithAffix } from "#app/messages"; +import { MysteryEncounterTier } from "#enums/mystery-encounter-tier"; +import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode"; +import { MysteryEncounterMode } from "#enums/mystery-encounter-mode"; /** the i18n namespace for the encounter */ const namespace = "mysteryEncounter:safariZone"; @@ -50,7 +53,7 @@ export const SafariZoneEncounter: IMysteryEncounter = .withDescription(`${namespace}:description`) .withQuery(`${namespace}:query`) .withOption(new MysteryEncounterOptionBuilder() - .withOptionMode(EncounterOptionMode.DISABLED_OR_DEFAULT) + .withOptionMode(MysteryEncounterOptionMode.DISABLED_OR_DEFAULT) .withSceneRequirement(new MoneyRequirement(0, 2.75)) // Cost equal to 1 Max Revive .withDialogue({ buttonLabel: `${namespace}:option:1:label`, @@ -64,7 +67,7 @@ export const SafariZoneEncounter: IMysteryEncounter = .withOptionPhase(async (scene: BattleScene) => { // Start safari encounter const encounter = scene.currentBattle.mysteryEncounter; - encounter.encounterVariant = MysteryEncounterVariant.CONTINUOUS_ENCOUNTER; + encounter.encounterMode = MysteryEncounterMode.CONTINUOUS_ENCOUNTER; encounter.misc = { safariPokemonRemaining: 3 }; @@ -116,7 +119,7 @@ export const SafariZoneEncounter: IMysteryEncounter = */ const safariZoneGameOptions: MysteryEncounterOption[] = [ new MysteryEncounterOptionBuilder() - .withOptionMode(EncounterOptionMode.DEFAULT) + .withOptionMode(MysteryEncounterOptionMode.DEFAULT) .withDialogue({ buttonLabel: `${namespace}:safari:1:label`, buttonTooltip: `${namespace}:safari:1:tooltip`, @@ -150,7 +153,7 @@ const safariZoneGameOptions: MysteryEncounterOption[] = [ }) .build(), new MysteryEncounterOptionBuilder() - .withOptionMode(EncounterOptionMode.DEFAULT) + .withOptionMode(MysteryEncounterOptionMode.DEFAULT) .withDialogue({ buttonLabel: `${namespace}:safari:2:label`, buttonTooltip: `${namespace}:safari:2:tooltip`, @@ -180,7 +183,7 @@ const safariZoneGameOptions: MysteryEncounterOption[] = [ }) .build(), new MysteryEncounterOptionBuilder() - .withOptionMode(EncounterOptionMode.DEFAULT) + .withOptionMode(MysteryEncounterOptionMode.DEFAULT) .withDialogue({ buttonLabel: `${namespace}:safari:3:label`, buttonTooltip: `${namespace}:safari:3:tooltip`, @@ -209,7 +212,7 @@ const safariZoneGameOptions: MysteryEncounterOption[] = [ }) .build(), new MysteryEncounterOptionBuilder() - .withOptionMode(EncounterOptionMode.DEFAULT) + .withOptionMode(MysteryEncounterOptionMode.DEFAULT) .withDialogue({ buttonLabel: `${namespace}:safari:4:label`, buttonTooltip: `${namespace}:safari:4:tooltip`, 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 6b208cabb61..19248d7a5bd 100644 --- a/src/data/mystery-encounters/encounters/shady-vitamin-dealer-encounter.ts +++ b/src/data/mystery-encounters/encounters/shady-vitamin-dealer-encounter.ts @@ -5,12 +5,14 @@ import { modifierTypes } from "#app/modifier/modifier-type"; import { randSeedInt } from "#app/utils"; import { MysteryEncounterType } from "#enums/mystery-encounter-type"; import { Species } from "#enums/species"; -import BattleScene from "../../../battle-scene"; -import IMysteryEncounter, { MysteryEncounterBuilder, MysteryEncounterTier, } from "../mystery-encounter"; -import { EncounterOptionMode, MysteryEncounterOptionBuilder } from "../mystery-encounter-option"; +import BattleScene from "#app/battle-scene"; +import IMysteryEncounter, { MysteryEncounterBuilder } from "../mystery-encounter"; +import { MysteryEncounterOptionBuilder } from "../mystery-encounter-option"; import { MoneyRequirement } from "../mystery-encounter-requirements"; import { getEncounterText, queueEncounterMessage } from "#app/data/mystery-encounters/utils/encounter-dialogue-utils"; import { applyDamageToPokemon } from "#app/data/mystery-encounters/utils/encounter-pokemon-utils"; +import { MysteryEncounterTier } from "#enums/mystery-encounter-tier"; +import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode"; /** the i18n namespace for this encounter */ const namespace = "mysteryEncounter:shadyVitaminDealer"; @@ -59,7 +61,7 @@ export const ShadyVitaminDealerEncounter: IMysteryEncounter = .withQuery(`${namespace}:query`) .withOption( new MysteryEncounterOptionBuilder() - .withOptionMode(EncounterOptionMode.DISABLED_OR_DEFAULT) + .withOptionMode(MysteryEncounterOptionMode.DISABLED_OR_DEFAULT) .withSceneMoneyRequirement(0, 2) // Wave scaling money multiplier of 2 .withDialogue({ buttonLabel: `${namespace}:option:1:label`, @@ -144,7 +146,7 @@ export const ShadyVitaminDealerEncounter: IMysteryEncounter = ) .withOption( new MysteryEncounterOptionBuilder() - .withOptionMode(EncounterOptionMode.DISABLED_OR_DEFAULT) + .withOptionMode(MysteryEncounterOptionMode.DISABLED_OR_DEFAULT) .withSceneMoneyRequirement(0, 5) // Wave scaling money multiplier of 5 .withDialogue({ buttonLabel: `${namespace}:option:2:label`, diff --git a/src/data/mystery-encounters/encounters/slumbering-snorlax-encounter.ts b/src/data/mystery-encounters/encounters/slumbering-snorlax-encounter.ts index 269b4ab8c21..316416e3809 100644 --- a/src/data/mystery-encounters/encounters/slumbering-snorlax-encounter.ts +++ b/src/data/mystery-encounters/encounters/slumbering-snorlax-encounter.ts @@ -4,8 +4,8 @@ import { MysteryEncounterType } from "#enums/mystery-encounter-type"; import { Species } from "#enums/species"; import BattleScene from "#app/battle-scene"; import { StatusEffect } from "#app/data/status-effect"; -import IMysteryEncounter, { MysteryEncounterBuilder, MysteryEncounterTier, } from "../mystery-encounter"; -import { EncounterOptionMode, MysteryEncounterOptionBuilder } from "../mystery-encounter-option"; +import IMysteryEncounter, { MysteryEncounterBuilder } from "../mystery-encounter"; +import { MysteryEncounterOptionBuilder } from "../mystery-encounter-option"; import { MoveRequirement } from "../mystery-encounter-requirements"; import { EnemyPartyConfig, EnemyPokemonConfig, initBattleWithEnemyConfig, initCustomMovesForEncounter, leaveEncounterWithoutBattle, setEncounterExp, setEncounterRewards, } from "../utils/encounter-phase-utils"; import { queueEncounterMessage } from "#app/data/mystery-encounters/utils/encounter-dialogue-utils"; @@ -14,6 +14,8 @@ import { BattlerIndex } from "#app/battle"; import { PokemonMove } from "#app/field/pokemon"; import { getPokemonSpecies } from "#app/data/pokemon-species"; import { PartyHealPhase } from "#app/phases"; +import { MysteryEncounterTier } from "#enums/mystery-encounter-tier"; +import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode"; /** i18n namespace for the encounter */ const namespace = "mysteryEncounter:slumberingSnorlax"; @@ -121,7 +123,7 @@ export const SlumberingSnorlaxEncounter: IMysteryEncounter = ) .withOption( new MysteryEncounterOptionBuilder() - .withOptionMode(EncounterOptionMode.DISABLED_OR_SPECIAL) + .withOptionMode(MysteryEncounterOptionMode.DISABLED_OR_SPECIAL) .withPrimaryPokemonRequirement(new MoveRequirement(STEALING_MOVES)) .withDialogue({ buttonLabel: `${namespace}:option:3:label`, 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 05c34ba6973..34967112a72 100644 --- a/src/data/mystery-encounters/encounters/the-strong-stuff-encounter.ts +++ b/src/data/mystery-encounters/encounters/the-strong-stuff-encounter.ts @@ -1,8 +1,8 @@ import { EnemyPartyConfig, generateModifierTypeOption, initBattleWithEnemyConfig, initCustomMovesForEncounter, leaveEncounterWithoutBattle, setEncounterRewards, transitionMysteryEncounterIntroVisuals } from "#app/data/mystery-encounters/utils/encounter-phase-utils"; import { modifierTypes, PokemonHeldItemModifierType, } from "#app/modifier/modifier-type"; import { MysteryEncounterType } from "#enums/mystery-encounter-type"; -import BattleScene from "../../../battle-scene"; -import IMysteryEncounter, { MysteryEncounterBuilder, MysteryEncounterTier, } from "../mystery-encounter"; +import BattleScene from "#app/battle-scene"; +import IMysteryEncounter, { MysteryEncounterBuilder } from "../mystery-encounter"; import { getPokemonSpecies } from "#app/data/pokemon-species"; import { Species } from "#enums/species"; import { Nature } from "#app/data/nature"; @@ -15,6 +15,7 @@ import { StatChangePhase } from "#app/phases"; import { BattleStat } from "#app/data/battle-stat"; import { BattlerTagType } from "#enums/battler-tag-type"; import { BerryType } from "#enums/berry-type"; +import { MysteryEncounterTier } from "#enums/mystery-encounter-tier"; /** the i18n namespace for the encounter */ const namespace = "mysteryEncounter:theStrongStuff"; diff --git a/src/data/mystery-encounters/encounters/training-session-encounter.ts b/src/data/mystery-encounters/encounters/training-session-encounter.ts index a3f18395fe2..02bee131a02 100644 --- a/src/data/mystery-encounters/encounters/training-session-encounter.ts +++ b/src/data/mystery-encounters/encounters/training-session-encounter.ts @@ -13,10 +13,12 @@ import { OptionSelectItem } from "#app/ui/abstact-option-select-ui-handler"; import { randSeedShuffle } from "#app/utils"; import { BattlerTagType } from "#enums/battler-tag-type"; import { MysteryEncounterType } from "#enums/mystery-encounter-type"; -import BattleScene from "../../../battle-scene"; -import IMysteryEncounter, { MysteryEncounterBuilder, MysteryEncounterTier, } from "../mystery-encounter"; -import { EncounterOptionMode, MysteryEncounterOptionBuilder } from "../mystery-encounter-option"; +import BattleScene from "#app/battle-scene"; +import IMysteryEncounter, { MysteryEncounterBuilder } from "../mystery-encounter"; +import { MysteryEncounterOptionBuilder } from "../mystery-encounter-option"; import { getEncounterText, queueEncounterMessage } from "#app/data/mystery-encounters/utils/encounter-dialogue-utils"; +import { MysteryEncounterTier } from "#enums/mystery-encounter-tier"; +import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode"; /** The i18n namespace for the encounter */ const namespace = "mysteryEncounter:trainingSession"; @@ -52,7 +54,7 @@ export const TrainingSessionEncounter: IMysteryEncounter = .withQuery(`${namespace}:query`) .withOption( new MysteryEncounterOptionBuilder() - .withOptionMode(EncounterOptionMode.DEFAULT) + .withOptionMode(MysteryEncounterOptionMode.DEFAULT) .withHasDexProgress(true) .withDialogue({ buttonLabel: `${namespace}:option:1:label`, @@ -196,7 +198,7 @@ export const TrainingSessionEncounter: IMysteryEncounter = ) .withOption( new MysteryEncounterOptionBuilder() - .withOptionMode(EncounterOptionMode.DEFAULT) + .withOptionMode(MysteryEncounterOptionMode.DEFAULT) .withHasDexProgress(true) .withDialogue({ buttonLabel: `${namespace}:option:2:label`, @@ -289,7 +291,7 @@ export const TrainingSessionEncounter: IMysteryEncounter = ) .withOption( new MysteryEncounterOptionBuilder() - .withOptionMode(EncounterOptionMode.DEFAULT) + .withOptionMode(MysteryEncounterOptionMode.DEFAULT) .withHasDexProgress(true) .withDialogue({ buttonLabel: `${namespace}:option:3:label`, diff --git a/src/data/mystery-encounters/mystery-encounter-data.ts b/src/data/mystery-encounters/mystery-encounter-data.ts index 7162450bfc5..81b4f74c04c 100644 --- a/src/data/mystery-encounters/mystery-encounter-data.ts +++ b/src/data/mystery-encounters/mystery-encounter-data.ts @@ -1,7 +1,7 @@ -import { MysteryEncounterTier } from "#app/data/mystery-encounters/mystery-encounter"; import { MysteryEncounterType } from "#enums/mystery-encounter-type"; import { BASE_MYSTERY_ENCOUNTER_SPAWN_WEIGHT } from "#app/data/mystery-encounters/mystery-encounters"; import { isNullOrUndefined } from "#app/utils"; +import { MysteryEncounterTier } from "#enums/mystery-encounter-tier"; export class MysteryEncounterData { encounteredEvents: [MysteryEncounterType, MysteryEncounterTier][] = []; diff --git a/src/data/mystery-encounters/mystery-encounter-option.ts b/src/data/mystery-encounters/mystery-encounter-option.ts index 8b9b26e2f95..e2736327ddb 100644 --- a/src/data/mystery-encounters/mystery-encounter-option.ts +++ b/src/data/mystery-encounters/mystery-encounter-option.ts @@ -1,29 +1,19 @@ import { OptionTextDisplay } from "#app/data/mystery-encounters/mystery-encounter-dialogue"; import { Moves } from "#app/enums/moves"; import { PlayerPokemon } from "#app/field/pokemon"; -import BattleScene from "../../battle-scene"; -import * as Utils from "../../utils"; +import BattleScene from "#app/battle-scene"; +import * as Utils from "#app/utils"; import { Type } from "../type"; import { EncounterPokemonRequirement, EncounterSceneRequirement, MoneyRequirement, TypeRequirement } from "./mystery-encounter-requirements"; import { CanLearnMoveRequirement, CanLearnMoveRequirementOptions } from "./requirements/can-learn-move-requirement"; -import { isNullOrUndefined } from "../../utils"; - -export enum EncounterOptionMode { - /** Default style */ - DEFAULT, - /** Disabled on requirements not met, default style on requirements met */ - DISABLED_OR_DEFAULT, - /** Default style on requirements not met, special style on requirements met */ - DEFAULT_OR_SPECIAL, - /** Disabled on requirements not met, special style on requirements met */ - DISABLED_OR_SPECIAL -} +import { isNullOrUndefined } from "#app/utils"; +import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode"; export type OptionPhaseCallback = (scene: BattleScene) => Promise; export default interface MysteryEncounterOption { - optionMode: EncounterOptionMode; + optionMode: MysteryEncounterOptionMode; hasDexProgress?: boolean; @@ -114,8 +104,8 @@ export default class MysteryEncounterOption implements MysteryEncounterOption { return false; } } else { - // this means we CAN have the same pokemon be a primary and secondary pokemon, so just choose any qualifying one randomly. - this.primaryPokemon = qualified[Utils.randSeedInt(qualified.length, 0)]; + // Just pick the first qualifying Pokemon + this.primaryPokemon = qualified[0]; return true; } } @@ -145,7 +135,7 @@ export default class MysteryEncounterOption implements MysteryEncounterOption { export class MysteryEncounterOptionBuilder implements Partial { - optionMode?: EncounterOptionMode; + optionMode?: MysteryEncounterOptionMode; requirements?: EncounterSceneRequirement[] = []; primaryPokemonRequirements?: EncounterPokemonRequirement[] = []; secondaryPokemonRequirements ?: EncounterPokemonRequirement[] = []; @@ -156,7 +146,7 @@ export class MysteryEncounterOptionBuilder implements Partial { + withOptionMode(optionMode: MysteryEncounterOptionMode): this & Pick { return Object.assign(this, { optionMode }); } @@ -165,6 +155,10 @@ export class MysteryEncounterOptionBuilder implements Partial> { + if (requirement instanceof EncounterPokemonRequirement) { + Error("Incorrectly added pokemon requirement as scene requirement."); + } + this.requirements.push(requirement); return Object.assign(this, { requirements: this.requirements }); } @@ -190,6 +184,10 @@ export class MysteryEncounterOptionBuilder implements Partial> { + if (requirement instanceof EncounterSceneRequirement) { + Error("Incorrectly added scene requirement as pokemon requirement."); + } + this.primaryPokemonRequirements.push(requirement); return Object.assign(this, { primaryPokemonRequirements: this.primaryPokemonRequirements }); } @@ -218,7 +216,11 @@ export class MysteryEncounterOptionBuilder implements Partial> { + withSecondaryPokemonRequirement(requirement: EncounterPokemonRequirement, excludePrimaryFromSecondaryRequirements: boolean = true): this & Required> { + if (requirement instanceof EncounterSceneRequirement) { + Error("Incorrectly added scene requirement as pokemon requirement."); + } + this.secondaryPokemonRequirements.push(requirement); this.excludePrimaryFromSecondaryRequirements = excludePrimaryFromSecondaryRequirements; return Object.assign(this, { secondaryPokemonRequirements: this.secondaryPokemonRequirements }); diff --git a/src/data/mystery-encounters/mystery-encounter-requirements.ts b/src/data/mystery-encounters/mystery-encounter-requirements.ts index 6f0b408e1d9..863ed9afc30 100644 --- a/src/data/mystery-encounters/mystery-encounter-requirements.ts +++ b/src/data/mystery-encounters/mystery-encounter-requirements.ts @@ -1,6 +1,6 @@ import { PlayerPokemon } from "#app/field/pokemon"; import { ModifierType, PokemonHeldItemModifierType } from "#app/modifier/modifier-type"; -import BattleScene from "../../battle-scene"; +import BattleScene from "#app/battle-scene"; import { isNullOrUndefined } from "#app/utils"; import { Abilities } from "#enums/abilities"; import { Moves } from "#enums/moves"; @@ -21,12 +21,35 @@ export interface EncounterRequirement { } export abstract class EncounterSceneRequirement implements EncounterRequirement { + abstract meetsRequirement(scene: BattleScene): boolean; + abstract getDialogueToken(scene: BattleScene, pokemon?: PlayerPokemon): [string, string]; +} + +export class CombinationSceneRequirement extends EncounterSceneRequirement { + orRequirements: EncounterSceneRequirement[]; + + constructor(... orRequirements: EncounterSceneRequirement[]) { + super(); + this.orRequirements = orRequirements; + } + meetsRequirement(scene: BattleScene): boolean { - throw new Error("Method not implemented."); + for (const req of this.orRequirements) { + if (req.meetsRequirement(scene)) { + return true; + } + } + return false; } getDialogueToken(scene: BattleScene, pokemon?: PlayerPokemon): [string, string] { - return ["", ""]; + for (const req of this.orRequirements) { + if (req.meetsRequirement(scene)) { + return req.getDialogueToken(scene, pokemon); + } + } + + return null; } } @@ -40,12 +63,49 @@ export abstract class EncounterPokemonRequirement implements EncounterRequiremen * Returns all party members that are compatible with this requirement. For non pokemon related requirements, the entire party is returned. * @param partyPokemon */ + abstract queryParty(partyPokemon: PlayerPokemon[]): PlayerPokemon[]; + + abstract getDialogueToken(scene: BattleScene, pokemon?: PlayerPokemon): [string, string]; +} + +export class CombinationPokemonRequirement extends EncounterPokemonRequirement { + orRequirements: EncounterPokemonRequirement[]; + + constructor(...orRequirements: EncounterPokemonRequirement[]) { + super(); + this.invertQuery = false; + this.minNumberOfPokemon = 1; + this.orRequirements = orRequirements; + } + + meetsRequirement(scene: BattleScene): boolean { + for (const req of this.orRequirements) { + if (req.meetsRequirement(scene)) { + return true; + } + } + return false; + } + queryParty(partyPokemon: PlayerPokemon[]): PlayerPokemon[] { + for (const req of this.orRequirements) { + const result = req.queryParty(partyPokemon); + if (result?.length > 0) { + return result; + } + } + return []; } getDialogueToken(scene: BattleScene, pokemon?: PlayerPokemon): [string, string] { - return ["", ""]; + for (const req of this.orRequirements) { + if (req.meetsRequirement(scene)) { + return req.getDialogueToken(scene, pokemon); + } + } + + return null; } } @@ -56,7 +116,7 @@ export class PreviousEncounterRequirement extends EncounterSceneRequirement { * Used for specifying an encounter that must be seen before this encounter can spawn * @param previousEncounterRequirement */ - constructor(previousEncounterRequirement) { + constructor(previousEncounterRequirement: MysteryEncounterType) { super(); this.previousEncounterRequirement = previousEncounterRequirement; } @@ -497,19 +557,16 @@ export class AbilityRequirement extends EncounterPokemonRequirement { queryParty(partyPokemon: PlayerPokemon[]): PlayerPokemon[] { if (!this.invertQuery) { - return partyPokemon.filter((pokemon) => this.requiredAbilities.filter((abilities) => pokemon.hasAbility(abilities)).length > 0); + return partyPokemon.filter((pokemon) => this.requiredAbilities.some((ability) => pokemon.getAbility().id === ability)); } else { // for an inverted query, we only want to get the pokemon that don't have ANY of the listed abilitiess - return partyPokemon.filter((pokemon) => this.requiredAbilities.filter((abilities) => pokemon.hasAbility(abilities)).length === 0); + return partyPokemon.filter((pokemon) => this.requiredAbilities.filter((ability) => pokemon.getAbility().id === ability).length === 0); } } getDialogueToken(scene: BattleScene, pokemon?: PlayerPokemon): [string, string] { - const reqAbilities = this.requiredAbilities.filter((a) => { - pokemon.hasAbility(a); - }); - if (reqAbilities.length > 0) { - return ["ability", Abilities[reqAbilities[0]]]; + if (this.requiredAbilities.some(a => pokemon.getAbility().id === a)) { + return ["ability", pokemon.getAbility().name]; } return null; } diff --git a/src/data/mystery-encounters/mystery-encounter.ts b/src/data/mystery-encounters/mystery-encounter.ts index aa9d4ef0997..895d6bd2da0 100644 --- a/src/data/mystery-encounters/mystery-encounter.ts +++ b/src/data/mystery-encounters/mystery-encounter.ts @@ -2,14 +2,14 @@ import { EnemyPartyConfig } from "#app/data/mystery-encounters/utils/encounter-p import Pokemon, { PlayerPokemon, PokemonMove } from "#app/field/pokemon"; import { isNullOrUndefined } from "#app/utils"; import { MysteryEncounterType } from "#enums/mystery-encounter-type"; -import BattleScene from "../../battle-scene"; -import MysteryEncounterIntroVisuals, { MysteryEncounterSpriteConfig } from "../../field/mystery-encounter-intro"; -import * as Utils from "../../utils"; +import BattleScene from "#app/battle-scene"; +import MysteryEncounterIntroVisuals, { MysteryEncounterSpriteConfig } from "#app/field/mystery-encounter-intro"; +import * as Utils from "#app/utils"; import { StatusEffect } from "../status-effect"; import MysteryEncounterDialogue, { OptionTextDisplay } from "./mystery-encounter-dialogue"; -import MysteryEncounterOption, { EncounterOptionMode, MysteryEncounterOptionBuilder, OptionPhaseCallback } from "./mystery-encounter-option"; +import MysteryEncounterOption, { MysteryEncounterOptionBuilder, OptionPhaseCallback } from "./mystery-encounter-option"; import { EncounterPokemonRequirement, EncounterSceneRequirement, @@ -20,27 +20,9 @@ import { } from "./mystery-encounter-requirements"; import { BattlerIndex } from "#app/battle"; import { EncounterAnim } from "#app/data/battle-anims"; - -export enum MysteryEncounterVariant { - DEFAULT, - TRAINER_BATTLE, - WILD_BATTLE, - BOSS_BATTLE, - NO_BATTLE, - /** For spawning new encounter queries instead of continuing to next wave */ - CONTINUOUS_ENCOUNTER -} - -/** - * Enum values are base spawn weights of each tier - */ -export enum MysteryEncounterTier { - COMMON = 64, - GREAT = 40, - ULTRA = 21, - ROGUE = 3, - MASTER = 0 // Not currently used -} +import { MysteryEncounterTier } from "#enums/mystery-encounter-tier"; +import { MysteryEncounterMode } from "#enums/mystery-encounter-mode"; +import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode"; export interface StartOfBattleEffect { sourcePokemon?: Pokemon; @@ -130,7 +112,7 @@ export default interface IMysteryEncounter { * For example, if there is no battle as part of the encounter/selected option, should be set to NO_BATTLE * Defaults to DEFAULT */ - encounterVariant?: MysteryEncounterVariant; + encounterMode?: MysteryEncounterMode; /** * Flag for checking if it's the first time a shop is being shown for an encounter. * Defaults to true so that the first shop does not override the specified rewards. @@ -181,7 +163,7 @@ export default class IMysteryEncounter implements IMysteryEncounter { this.dialogue = this.dialogue ?? {}; // Default max is 1 for ROGUE encounters, 3 for others this.maxAllowedEncounters = this.maxAllowedEncounters ?? this.encounterTier === MysteryEncounterTier.ROGUE ? 1 : 3; - this.encounterVariant = MysteryEncounterVariant.DEFAULT; + this.encounterMode = MysteryEncounterMode.DEFAULT; this.requirements = this.requirements ? this.requirements : []; this.hideBattleIntroMessage = !isNullOrUndefined(this.hideBattleIntroMessage) ? this.hideBattleIntroMessage : false; this.autoHideIntroVisuals = !isNullOrUndefined(this.autoHideIntroVisuals) ? this.autoHideIntroVisuals : true; @@ -314,7 +296,9 @@ export default class IMysteryEncounter implements IMysteryEncounter { if (this.requirements?.length > 0) { for (const req of this.requirements) { const dialogueToken = req.getDialogueToken(scene); - this.setDialogueToken(...dialogueToken); + if (dialogueToken?.length === 2) { + this.setDialogueToken(...dialogueToken); + } } } if (this.primaryPokemon?.length > 0) { @@ -322,7 +306,9 @@ export default class IMysteryEncounter implements IMysteryEncounter { for (const req of this.primaryPokemonRequirements) { if (!req.invertQuery) { const value = req.getDialogueToken(scene, this.primaryPokemon); - this.setDialogueToken("primary" + this.capitalizeFirstLetter(value[0]), value[1]); + if (value?.length === 2) { + this.setDialogueToken("primary" + this.capitalizeFirstLetter(value[0]), value[1]); + } } } } @@ -331,6 +317,9 @@ export default class IMysteryEncounter implements IMysteryEncounter { for (const req of this.secondaryPokemonRequirements) { if (!req.invertQuery) { const value = req.getDialogueToken(scene, this.secondaryPokemon[0]); + if (value?.length === 2) { + this.setDialogueToken("primary" + this.capitalizeFirstLetter(value[0]), value[1]); + } this.setDialogueToken("secondary" + this.capitalizeFirstLetter(value[0]), value[1]); } } @@ -344,7 +333,9 @@ export default class IMysteryEncounter implements IMysteryEncounter { if (opt.requirements?.length > 0) { for (const req of opt.requirements) { const dialogueToken = req.getDialogueToken(scene); - this.setDialogueToken("option" + j + this.capitalizeFirstLetter(dialogueToken[0]), dialogueToken[1]); + if (dialogueToken?.length === 2) { + this.setDialogueToken("option" + j + this.capitalizeFirstLetter(dialogueToken[0]), dialogueToken[1]); + } } } if (opt.primaryPokemonRequirements?.length > 0 && opt.primaryPokemon?.length > 0) { @@ -352,7 +343,9 @@ export default class IMysteryEncounter implements IMysteryEncounter { for (const req of opt.primaryPokemonRequirements) { if (!req.invertQuery) { const value = req.getDialogueToken(scene, opt.primaryPokemon); - this.setDialogueToken("option" + j + "Primary" + this.capitalizeFirstLetter(value[0]), value[1]); + if (value?.length === 2) { + this.setDialogueToken("option" + j + "Primary" + this.capitalizeFirstLetter(value[0]), value[1]); + } } } } @@ -361,7 +354,9 @@ export default class IMysteryEncounter implements IMysteryEncounter { for (const req of opt.secondaryPokemonRequirements) { if (!req.invertQuery) { const value = req.getDialogueToken(scene, opt.secondaryPokemon[0]); - this.setDialogueToken("option" + j + "Secondary" + this.capitalizeFirstLetter(value[0]), value[1]); + if (value?.length === 2) { + this.setDialogueToken("option" + j + "Secondary" + this.capitalizeFirstLetter(value[0]), value[1]); + } } } } @@ -373,7 +368,7 @@ export default class IMysteryEncounter implements IMysteryEncounter { } /** - * If an encounter uses {@link MysteryEncounterVariant.CONTINUOUS_ENCOUNTER}, + * If an encounter uses {@link MysteryEncounterMode.CONTINUOUS_ENCOUNTER}, * should rely on this value for seed offset instead of wave index. * * This offset is incremented for each new {@link MysteryEncounterPhase} that occurs, @@ -466,7 +461,7 @@ export class MysteryEncounterBuilder implements Partial { * @returns */ withSimpleOption(dialogue: OptionTextDisplay, callback: OptionPhaseCallback): this & Pick { - return this.withOption(new MysteryEncounterOptionBuilder().withOptionMode(EncounterOptionMode.DEFAULT).withDialogue(dialogue).withOptionPhase(callback).build()); + return this.withOption(new MysteryEncounterOptionBuilder().withOptionMode(MysteryEncounterOptionMode.DEFAULT).withDialogue(dialogue).withOptionPhase(callback).build()); } /** @@ -481,7 +476,7 @@ export class MysteryEncounterBuilder implements Partial { */ withSimpleDexProgressOption(dialogue: OptionTextDisplay, callback: OptionPhaseCallback): this & Pick { return this.withOption(new MysteryEncounterOptionBuilder() - .withOptionMode(EncounterOptionMode.DEFAULT) + .withOptionMode(MysteryEncounterOptionMode.DEFAULT) .withHasDexProgress(true) .withDialogue(dialogue) .withOptionPhase(callback).build()); @@ -592,6 +587,10 @@ export class MysteryEncounterBuilder implements Partial { * @returns */ withPrimaryPokemonRequirement(requirement: EncounterPokemonRequirement): this & Required> { + if (requirement instanceof EncounterSceneRequirement) { + Error("Incorrectly added scene requirement as pokemon requirement."); + } + this.primaryPokemonRequirements.push(requirement); return Object.assign(this, { primaryPokemonRequirements: this.primaryPokemonRequirements }); } @@ -624,6 +623,10 @@ export class MysteryEncounterBuilder implements Partial { // ex. if your only grass type pokemon, a snivy, is chosen as primary, if the support pokemon requires a grass type, the event won't trigger because // it's already been withSecondaryPokemonRequirement(requirement: EncounterPokemonRequirement, excludePrimaryFromSecondaryRequirements: boolean = false): this & Required> { + if (requirement instanceof EncounterSceneRequirement) { + Error("Incorrectly added scene requirement as pokemon requirement."); + } + this.secondaryPokemonRequirements.push(requirement); this.excludePrimaryFromSupportRequirements = excludePrimaryFromSecondaryRequirements; return Object.assign(this, { excludePrimaryFromSecondaryRequirements: this.excludePrimaryFromSupportRequirements, secondaryPokemonRequirements: this.secondaryPokemonRequirements }); diff --git a/src/data/mystery-encounters/requirements/requirement-groups.ts b/src/data/mystery-encounters/requirements/requirement-groups.ts index 4e9eb7ad4e5..cf23ace2efd 100644 --- a/src/data/mystery-encounters/requirements/requirement-groups.ts +++ b/src/data/mystery-encounters/requirements/requirement-groups.ts @@ -61,7 +61,7 @@ export const EXTORTION_MOVES = [ Moves.BEAT_UP, Moves.COIL, Moves.WRING_OUT, - Moves.STRING_SHOT + Moves.STRING_SHOT, ]; export const EXTORTION_ABILITIES = [ @@ -69,5 +69,5 @@ export const EXTORTION_ABILITIES = [ Abilities.ARENA_TRAP, Abilities.SHADOW_TAG, Abilities.SUCTION_CUPS, - Abilities.STICKY_HOLD, + Abilities.STICKY_HOLD ]; diff --git a/src/data/mystery-encounters/utils/encounter-phase-utils.ts b/src/data/mystery-encounters/utils/encounter-phase-utils.ts index 9178747500f..0064d6a105f 100644 --- a/src/data/mystery-encounters/utils/encounter-phase-utils.ts +++ b/src/data/mystery-encounters/utils/encounter-phase-utils.ts @@ -18,17 +18,17 @@ import { BattlerTagType } from "#enums/battler-tag-type"; import { Biome } from "#enums/biome"; import { TrainerType } from "#enums/trainer-type"; import i18next from "i18next"; -import BattleScene from "../../../battle-scene"; -import Trainer, { TrainerVariant } from "../../../field/trainer"; -import * as Utils from "../../../utils"; -import PokemonSpecies from "../../pokemon-species"; -import { Status, StatusEffect } from "../../status-effect"; -import { TrainerConfig, trainerConfigs, TrainerSlot } from "../../trainer-config"; -import { MysteryEncounterVariant } from "../mystery-encounter"; +import BattleScene from "#app/battle-scene"; +import Trainer, { TrainerVariant } from "#app/field/trainer"; +import * as Utils from "#app/utils"; import { Gender } from "#app/data/gender"; import { Nature } from "#app/data/nature"; import { Moves } from "#enums/moves"; import { initMoveAnim, loadMoveAnimAssets } from "#app/data/battle-anims"; +import { MysteryEncounterMode } from "#enums/mystery-encounter-mode"; +import { Status, StatusEffect } from "#app/data/status-effect"; +import { TrainerConfig, trainerConfigs, TrainerSlot } from "#app/data/trainer-config"; +import PokemonSpecies from "#app/data/pokemon-species"; /** * Animates exclamation sprite over trainer's head at start of encounter @@ -107,7 +107,7 @@ export async function initBattleWithEnemyConfig(scene: BattleScene, partyConfig: const trainerType = partyConfig?.trainerType; let trainerConfig = partyConfig?.trainerConfig; if (trainerType || trainerConfig) { - scene.currentBattle.mysteryEncounter.encounterVariant = MysteryEncounterVariant.TRAINER_BATTLE; + scene.currentBattle.mysteryEncounter.encounterMode = MysteryEncounterMode.TRAINER_BATTLE; if (scene.currentBattle.trainer) { scene.currentBattle.trainer.setVisible(false); scene.currentBattle.trainer.destroy(); @@ -128,7 +128,7 @@ export async function initBattleWithEnemyConfig(scene: BattleScene, partyConfig: battle.enemyLevels = scene.currentBattle.trainer.getPartyLevels(scene.currentBattle.waveIndex); } else { // Wild - scene.currentBattle.mysteryEncounter.encounterVariant = MysteryEncounterVariant.WILD_BATTLE; + scene.currentBattle.mysteryEncounter.encounterMode = MysteryEncounterMode.WILD_BATTLE; battle.enemyLevels = new Array(partyConfig?.pokemonConfigs?.length > 0 ? partyConfig?.pokemonConfigs?.length : doubleBattle ? 2 : 1).fill(null).map(() => scene.currentBattle.getLevelForWave()); } @@ -160,7 +160,7 @@ export async function initBattleWithEnemyConfig(scene: BattleScene, partyConfig: enemySpecies = config.species; isBoss = config.isBoss; if (isBoss) { - scene.currentBattle.mysteryEncounter.encounterVariant = MysteryEncounterVariant.BOSS_BATTLE; + scene.currentBattle.mysteryEncounter.encounterMode = MysteryEncounterMode.BOSS_BATTLE; } } else { enemySpecies = scene.randomSpecies(battle.waveIndex, level, true); @@ -508,7 +508,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.encounterVariant === MysteryEncounterVariant.TRAINER_BATTLE) { + if (scene.currentBattle.mysteryEncounter.encounterMode === MysteryEncounterMode.TRAINER_BATTLE) { expValue = Math.floor(expValue * 1.5); } for (const partyMember of nonFaintedPartyMembers) { @@ -609,7 +609,7 @@ export function initSubsequentOptionSelect(scene: BattleScene, optionSelectSetti * @param addHealPhase - when true, will add a shop phase to end of encounter with 0 rewards but healing items are available */ export function leaveEncounterWithoutBattle(scene: BattleScene, addHealPhase: boolean = false) { - scene.currentBattle.mysteryEncounter.encounterVariant = MysteryEncounterVariant.NO_BATTLE; + scene.currentBattle.mysteryEncounter.encounterMode = MysteryEncounterMode.NO_BATTLE; scene.clearPhaseQueue(); scene.clearPhaseQueueSplice(); handleMysteryEncounterVictory(scene, addHealPhase); @@ -626,14 +626,14 @@ export function handleMysteryEncounterVictory(scene: BattleScene, addHealPhase: // If in repeated encounter variant, do nothing // Variant must eventually be swapped in order to handle "true" end of the encounter - if (scene.currentBattle.mysteryEncounter.encounterVariant === MysteryEncounterVariant.CONTINUOUS_ENCOUNTER) { + if (scene.currentBattle.mysteryEncounter.encounterMode === MysteryEncounterMode.CONTINUOUS_ENCOUNTER) { return; - } else if (scene.currentBattle.mysteryEncounter.encounterVariant === MysteryEncounterVariant.NO_BATTLE) { + } else if (scene.currentBattle.mysteryEncounter.encounterMode === MysteryEncounterMode.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))) { + } else if (!scene.getEnemyParty().find(p => scene.currentBattle.mysteryEncounter.encounterMode !== MysteryEncounterMode.TRAINER_BATTLE ? p.isOnField() : !p?.isFainted(true))) { scene.pushPhase(new BattleEndPhase(scene)); - if (scene.currentBattle.mysteryEncounter.encounterVariant === MysteryEncounterVariant.TRAINER_BATTLE) { + if (scene.currentBattle.mysteryEncounter.encounterMode === MysteryEncounterMode.TRAINER_BATTLE) { scene.pushPhase(new TrainerVictoryPhase(scene)); } if (scene.gameMode.isEndless || !scene.gameMode.isWaveFinal(scene.currentBattle.waveIndex)) { @@ -693,7 +693,7 @@ export function transitionMysteryEncounterIntroVisuals(scene: BattleScene, hide: */ export function handleMysteryEncounterBattleStartEffects(scene: BattleScene) { const encounter = scene.currentBattle?.mysteryEncounter; - if (scene.currentBattle.battleType === BattleType.MYSTERY_ENCOUNTER && encounter.encounterVariant !== MysteryEncounterVariant.NO_BATTLE && !encounter.startOfBattleEffectsComplete) { + if (scene.currentBattle.battleType === BattleType.MYSTERY_ENCOUNTER && encounter.encounterMode !== MysteryEncounterMode.NO_BATTLE && !encounter.startOfBattleEffectsComplete) { const effects = encounter.startOfBattleEffects; effects.forEach(effect => { let source; diff --git a/src/enums/mystery-encounter-mode.ts b/src/enums/mystery-encounter-mode.ts new file mode 100644 index 00000000000..03094040ce0 --- /dev/null +++ b/src/enums/mystery-encounter-mode.ts @@ -0,0 +1,9 @@ +export enum MysteryEncounterMode { + DEFAULT, + TRAINER_BATTLE, + WILD_BATTLE, + BOSS_BATTLE, + NO_BATTLE, + /** For spawning new encounter queries instead of continuing to next wave */ + CONTINUOUS_ENCOUNTER +} diff --git a/src/enums/mystery-encounter-option-mode.ts b/src/enums/mystery-encounter-option-mode.ts new file mode 100644 index 00000000000..a994c30581b --- /dev/null +++ b/src/enums/mystery-encounter-option-mode.ts @@ -0,0 +1,10 @@ +export enum MysteryEncounterOptionMode { + /** Default style */ + DEFAULT, + /** Disabled on requirements not met, default style on requirements met */ + DISABLED_OR_DEFAULT, + /** Default style on requirements not met, special style on requirements met */ + DEFAULT_OR_SPECIAL, + /** Disabled on requirements not met, special style on requirements met */ + DISABLED_OR_SPECIAL +} diff --git a/src/enums/mystery-encounter-tier.ts b/src/enums/mystery-encounter-tier.ts new file mode 100644 index 00000000000..b3924b2ff9d --- /dev/null +++ b/src/enums/mystery-encounter-tier.ts @@ -0,0 +1,10 @@ +/** + * Enum values are base spawn weights of each tier + */ +export enum MysteryEncounterTier { + COMMON = 64, + GREAT = 40, + ULTRA = 21, + ROGUE = 3, + MASTER = 0 // Not currently used +} diff --git a/src/locales/en/mystery-encounters/offer-you-cant-refuse-dialogue.ts b/src/locales/en/mystery-encounters/offer-you-cant-refuse-dialogue.ts index dda5810b0a4..7cee39b3ae8 100644 --- a/src/locales/en/mystery-encounters/offer-you-cant-refuse-dialogue.ts +++ b/src/locales/en/mystery-encounters/offer-you-cant-refuse-dialogue.ts @@ -1,10 +1,10 @@ export const offerYouCantRefuseDialogue = { intro: "You're stopped by a rich looking boy.", - speaker: "Rich Kid", + speaker: "Rich Boy", intro_dialogue: `Good day to you. $I can't help but notice that your\n{{strongestPokemon}} looks positively divine! $I've always wanted to have a pet like that! - $I'd pay you handsomely, and\nI'll even give you this old bauble!`, + $I'd pay you handsomely,\nand also give you this old bauble!`, title: "An Offer You Can't Refuse", description: "You're being offered a @[TOOLTIP_TITLE]{Shiny Charm} and {{price, money}} for your {{strongestPokemon}}!\n\nIt's is an extremely good deal, but can you really bear to part with such a strong team member?", query: "What will you do?", @@ -18,11 +18,10 @@ export const offerYouCantRefuseDialogue = { }, 2: { label: "Extort the Kid", - tooltip: "(+) Gain {{price, money}}", + tooltip: "(+) {{option2PrimaryName}} uses {{moveOrAbility}}\n(+) Gain {{price, money}}", tooltip_disabled: "Your Pokémon need to have certain moves or abilities to choose this", selected: `My word, we're being robbed, Liepard! - $How wonderful! - $Now I'll have an amazing\nstory for the yacht club!`, + $You'll be hearing from my lawyers for this!`, }, 3: { label: "Leave", diff --git a/src/overrides.ts b/src/overrides.ts index 32a2988641f..b530032364d 100644 --- a/src/overrides.ts +++ b/src/overrides.ts @@ -9,7 +9,7 @@ import { PokeballType } from "./data/pokeball"; import { Gender } from "./data/gender"; import { StatusEffect } from "./data/status-effect"; import { modifierTypes, SpeciesStatBoosterItem } from "./modifier/modifier-type"; -import { VariantTier } from "./enums/variant-tiers"; +import { VariantTier } from "#enums/variant-tiers"; import { EggTier } from "#enums/egg-type"; import { Abilities } from "#enums/abilities"; import { BerryType } from "#enums/berry-type"; @@ -18,7 +18,7 @@ import { Moves } from "#enums/moves"; import { Species } from "#enums/species"; import { TimeOfDay } from "#enums/time-of-day"; import { MysteryEncounterType } from "#enums/mystery-encounter-type"; // eslint-disable-line @typescript-eslint/no-unused-vars -import { MysteryEncounterTier } from "#app/data/mystery-encounters/mystery-encounter"; // eslint-disable-line @typescript-eslint/no-unused-vars +import { MysteryEncounterTier } from "#enums/mystery-encounter-tier"; // eslint-disable-line @typescript-eslint/no-unused-vars /** * Overrides for testing different in game situations @@ -117,9 +117,9 @@ export const EGG_GACHA_PULL_COUNT_OVERRIDE: number = 0; */ // 1 to 256, set to null to ignore -export const MYSTERY_ENCOUNTER_RATE_OVERRIDE: number = 256; +export const MYSTERY_ENCOUNTER_RATE_OVERRIDE: number = null; export const MYSTERY_ENCOUNTER_TIER_OVERRIDE: MysteryEncounterTier = null; -export const MYSTERY_ENCOUNTER_OVERRIDE: MysteryEncounterType = MysteryEncounterType.OFFER_YOU_CANT_REFUSE; +export const MYSTERY_ENCOUNTER_OVERRIDE: MysteryEncounterType = null; /** * MODIFIER / ITEM OVERRIDES diff --git a/src/phases.ts b/src/phases.ts index 0ceff19f059..982cc04031e 100644 --- a/src/phases.ts +++ b/src/phases.ts @@ -65,11 +65,11 @@ import { Moves } from "#enums/moves"; import { PlayerGender } from "#enums/player-gender"; import { Species } from "#enums/species"; import { TrainerType } from "#enums/trainer-type"; -import { MysteryEncounterVariant } from "#app/data/mystery-encounters/mystery-encounter"; import { MysteryEncounterPhase } from "#app/phases/mystery-encounter-phases"; import { doTrainerExclamation, handleMysteryEncounterBattleStartEffects, handleMysteryEncounterVictory } from "#app/data/mystery-encounters/utils/encounter-phase-utils"; import ModifierSelectUiHandler, { SHOP_OPTIONS_ROW_LIMIT } from "#app/ui/modifier-select-ui-handler"; import { getEncounterText } from "#app/data/mystery-encounters/utils/encounter-dialogue-utils"; +import { MysteryEncounterMode } from "#enums/mystery-encounter-mode"; const { t } = i18next; @@ -866,7 +866,7 @@ export class EncounterPhase extends BattlePhase { } if (!this.loaded) { - this.scene.gameData.setPokemonSeen(enemyPokemon, true, battle.battleType === BattleType.TRAINER || battle?.mysteryEncounter?.encounterVariant === MysteryEncounterVariant.TRAINER_BATTLE); + this.scene.gameData.setPokemonSeen(enemyPokemon, true, battle.battleType === BattleType.TRAINER || battle?.mysteryEncounter?.encounterMode === MysteryEncounterMode.TRAINER_BATTLE); } if (enemyPokemon.species.speciesId === Species.ETERNATUS) { @@ -1550,7 +1550,7 @@ export class SummonPhase extends PartyMemberPokemonPhase { onComplete: () => this.scene.trainer.setVisible(false) }); this.scene.time.delayedCall(750, () => this.summon()); - } else if (this.scene.currentBattle.battleType === BattleType.TRAINER || this.scene?.currentBattle?.mysteryEncounter?.encounterVariant === MysteryEncounterVariant.TRAINER_BATTLE) { + } else if (this.scene.currentBattle.battleType === BattleType.TRAINER || this.scene?.currentBattle?.mysteryEncounter?.encounterMode === MysteryEncounterMode.TRAINER_BATTLE) { const trainerName = this.scene.currentBattle.trainer.getName(!(this.fieldIndex % 2) ? TrainerSlot.TRAINER : TrainerSlot.TRAINER_PARTNER); const pokemonName = this.getPokemon().name; const message = i18next.t("battle:trainerSendOut", { trainerName, pokemonName }); diff --git a/src/phases/mystery-encounter-phases.ts b/src/phases/mystery-encounter-phases.ts index d55bcaad636..1213472c35d 100644 --- a/src/phases/mystery-encounter-phases.ts +++ b/src/phases/mystery-encounter-phases.ts @@ -5,7 +5,6 @@ import { Mode } from "../ui/ui"; import { transitionMysteryEncounterIntroVisuals, OptionSelectSettings } from "../data/mystery-encounters/utils/encounter-phase-utils"; import { CheckSwitchPhase, NewBattlePhase, ReturnPhase, ScanIvsPhase, SelectModifierPhase, SummonPhase, ToggleDoublePositionPhase } from "../phases"; import MysteryEncounterOption, { OptionPhaseCallback } from "../data/mystery-encounters/mystery-encounter-option"; -import { MysteryEncounterVariant } from "../data/mystery-encounters/mystery-encounter"; import { getCharVariantFromDialogue } from "../data/dialogue"; import { TrainerSlot } from "../data/trainer-config"; import { BattleSpec } from "#enums/battle-spec"; @@ -15,6 +14,7 @@ import * as Utils from "../utils"; import { isNullOrUndefined } from "../utils"; import { getEncounterText } from "#app/data/mystery-encounters/utils/encounter-dialogue-utils"; import { BattlerTagLapseType } from "#app/data/battler-tags"; +import { MysteryEncounterMode } from "#enums/mystery-encounter-mode"; /** * Will handle (in order): @@ -210,13 +210,13 @@ export class MysteryEncounterBattlePhase extends Phase { getBattleMessage(scene: BattleScene): string { const enemyField = scene.getEnemyField(); - const encounterVariant = scene.currentBattle.mysteryEncounter.encounterVariant; + const encounterMode = scene.currentBattle.mysteryEncounter.encounterMode; if (scene.currentBattle.battleSpec === BattleSpec.FINAL_BOSS) { return i18next.t("battle:bossAppeared", { bossName: enemyField[0].name }); } - if (encounterVariant === MysteryEncounterVariant.TRAINER_BATTLE) { + if (encounterMode === MysteryEncounterMode.TRAINER_BATTLE) { if (scene.currentBattle.double) { return i18next.t("battle:trainerAppearedDouble", { trainerName: scene.currentBattle.trainer.getName(TrainerSlot.NONE, true) }); @@ -231,10 +231,10 @@ export class MysteryEncounterBattlePhase extends Phase { } doMysteryEncounterBattle(scene: BattleScene) { - const encounterVariant = scene.currentBattle.mysteryEncounter.encounterVariant; - if (encounterVariant === MysteryEncounterVariant.WILD_BATTLE || encounterVariant === MysteryEncounterVariant.BOSS_BATTLE) { + const encounterMode = scene.currentBattle.mysteryEncounter.encounterMode; + if (encounterMode === MysteryEncounterMode.WILD_BATTLE || encounterMode === MysteryEncounterMode.BOSS_BATTLE) { // Summons the wild/boss Pokemon - if (encounterVariant === MysteryEncounterVariant.BOSS_BATTLE) { + if (encounterMode === MysteryEncounterMode.BOSS_BATTLE) { scene.playBgm(undefined); } const availablePartyMembers = scene.getEnemyParty().filter(p => !p.isFainted()).length; @@ -248,7 +248,7 @@ export class MysteryEncounterBattlePhase extends Phase { } else { this.endBattleSetup(scene); } - } else if (encounterVariant === MysteryEncounterVariant.TRAINER_BATTLE) { + } else if (encounterMode === MysteryEncounterMode.TRAINER_BATTLE) { this.showEnemyTrainer(); const doSummon = () => { scene.currentBattle.started = true; @@ -296,11 +296,11 @@ export class MysteryEncounterBattlePhase extends Phase { endBattleSetup(scene: BattleScene) { const enemyField = scene.getEnemyField(); - const encounterVariant = scene.currentBattle.mysteryEncounter.encounterVariant; + const encounterMode = scene.currentBattle.mysteryEncounter.encounterMode; // PostSummon and ShinySparkle phases are handled by SummonPhase - if (encounterVariant !== MysteryEncounterVariant.TRAINER_BATTLE) { + if (encounterMode !== MysteryEncounterMode.TRAINER_BATTLE) { const ivScannerModifier = this.scene.findModifier(m => m instanceof IvScannerModifier); if (ivScannerModifier) { enemyField.map(p => this.scene.pushPhase(new ScanIvsPhase(this.scene, p.getBattlerIndex(), Math.min(ivScannerModifier.getStackCount() * 2, 6)))); @@ -327,7 +327,7 @@ export class MysteryEncounterBattlePhase extends Phase { scene.pushPhase(new ToggleDoublePositionPhase(scene, false)); } - if (encounterVariant !== MysteryEncounterVariant.TRAINER_BATTLE && !this.disableSwitch) { + if (encounterMode !== MysteryEncounterMode.TRAINER_BATTLE && !this.disableSwitch) { const minPartySize = scene.currentBattle.double ? 2 : 1; if (availablePartyMembers.length > minPartySize) { scene.pushPhase(new CheckSwitchPhase(scene, 0, scene.currentBattle.double)); diff --git a/src/test/mystery-encounter/encounterTestUtils.ts b/src/test/mystery-encounter/encounterTestUtils.ts index 88233032ccd..24afc428e68 100644 --- a/src/test/mystery-encounter/encounterTestUtils.ts +++ b/src/test/mystery-encounter/encounterTestUtils.ts @@ -13,6 +13,53 @@ import { Status, StatusEffect } from "#app/data/status-effect"; * @param optionNo - human number, not index * @param isBattle - if selecting option should lead to battle, set to true */ +export async function runMysteryEncounterToEnd(game: GameManager, optionNo: number, isBattle: boolean = false) { + await runSelectMysteryEncounterOption(game, optionNo, isBattle); + + // run the selected options phase + game.onNextPrompt("MysteryEncounterOptionSelectedPhase", Mode.MESSAGE, () => { + const uiHandler = game.scene.ui.getHandler(); + uiHandler.processInput(Button.ACTION); + }); + + // If a battle is started, fast forward to end of the battle + game.onNextPrompt("CommandPhase", Mode.COMMAND, () => { + game.scene.clearPhaseQueue(); + game.scene.clearPhaseQueueSplice(); + game.scene.unshiftPhase(new VictoryPhase(game.scene, 0)); + game.endPhase(); + }); + + // Handle end of battle trainer messages + game.onNextPrompt("TrainerVictoryPhase", Mode.MESSAGE, () => { + const uiHandler = game.scene.ui.getHandler(); + uiHandler.processInput(Button.ACTION); + }); + + // Handle egg hatch dialogue + game.onNextPrompt("EggLapsePhase", Mode.MESSAGE, () => { + const uiHandler = game.scene.ui.getHandler(); + uiHandler.processInput(Button.ACTION); + }); + + // Handle modifier reward dialogue + game.onNextPrompt("ModifierRewardPhase", Mode.MESSAGE, () => { + const uiHandler = game.scene.ui.getHandler(); + uiHandler.processInput(Button.ACTION); + }); + + game.onNextPrompt("SelectModifier", Mode.MODIFIER_SELECT, () => { + const uiHandler = game.scene.ui.getHandler(); + uiHandler.processInput(Button.ACTION); + }); + + if (isBattle) { + await game.phaseInterceptor.to(CommandPhase); + } else { + await game.phaseInterceptor.to(MysteryEncounterRewardsPhase); + } +} + export async function runSelectMysteryEncounterOption(game: GameManager, optionNo: number, isBattle: boolean = false) { // Handle any eventual queued messages (e.g. weather phase, etc.) game.onNextPrompt("MessagePhase", Mode.MESSAGE, () => { @@ -53,38 +100,6 @@ export async function runSelectMysteryEncounterOption(game: GameManager, optionN } uiHandler.processInput(Button.ACTION); - - // run the selected options phase - game.onNextPrompt("MysteryEncounterOptionSelectedPhase", Mode.MESSAGE, () => { - const uiHandler = game.scene.ui.getHandler(); - uiHandler.processInput(Button.ACTION); - }); - - // If a battle is started, fast forward to end of the battle - game.onNextPrompt("CommandPhase", Mode.COMMAND, () => { - game.scene.clearPhaseQueue(); - game.scene.clearPhaseQueueSplice(); - game.scene.unshiftPhase(new VictoryPhase(game.scene, 0)); - game.endPhase(); - }); - - // Handle end of battle trainer messages - game.onNextPrompt("TrainerVictoryPhase", Mode.MESSAGE, () => { - const uiHandler = game.scene.ui.getHandler(); - uiHandler.processInput(Button.ACTION); - }); - - // Handle egg hatch dialogue - game.onNextPrompt("EggLapsePhase", Mode.MESSAGE, () => { - const uiHandler = game.scene.ui.getHandler(); - uiHandler.processInput(Button.ACTION); - }); - - if (isBattle) { - await game.phaseInterceptor.to(CommandPhase); - } else { - await game.phaseInterceptor.to(MysteryEncounterRewardsPhase); - } } /** diff --git a/src/test/mystery-encounter/encounters/department-store-sale-encounter.test.ts b/src/test/mystery-encounter/encounters/department-store-sale-encounter.test.ts index cbc5ab7950e..095d8a109d7 100644 --- a/src/test/mystery-encounter/encounters/department-store-sale-encounter.test.ts +++ b/src/test/mystery-encounter/encounters/department-store-sale-encounter.test.ts @@ -5,15 +5,15 @@ import { Species } from "#app/enums/species"; import GameManager from "#app/test/utils/gameManager"; import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; import * as EncounterPhaseUtils from "#app/data/mystery-encounters/utils/encounter-phase-utils"; -import { EncounterOptionMode } from "#app/data/mystery-encounters/mystery-encounter-option"; -import { runSelectMysteryEncounterOption } from "#test/mystery-encounter/encounterTestUtils"; +import { runMysteryEncounterToEnd } from "#test/mystery-encounter/encounterTestUtils"; import { SelectModifierPhase } from "#app/phases"; import BattleScene from "#app/battle-scene"; -import { MysteryEncounterTier } from "#app/data/mystery-encounters/mystery-encounter"; import { Mode } from "#app/ui/ui"; import ModifierSelectUiHandler from "#app/ui/modifier-select-ui-handler"; import { DepartmentStoreSaleEncounter } from "#app/data/mystery-encounters/encounters/department-store-sale-encounter"; import { CIVILIZATION_ENCOUNTER_BIOMES } from "#app/data/mystery-encounters/mystery-encounters"; +import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode"; +import { MysteryEncounterTier } from "#enums/mystery-encounter-tier"; const namespace = "mysteryEncounter:departmentStoreSale"; const defaultParty = [Species.LAPRAS, Species.GENGAR, Species.ABRA]; @@ -98,7 +98,7 @@ describe("Department Store Sale - Mystery Encounter", () => { describe("Option 1 - TM Shop", () => { it("should have the correct properties", () => { const option = DepartmentStoreSaleEncounter.options[0]; - expect(option.optionMode).toBe(EncounterOptionMode.DEFAULT); + expect(option.optionMode).toBe(MysteryEncounterOptionMode.DEFAULT); expect(option.dialogue).toBeDefined(); expect(option.dialogue).toStrictEqual({ buttonLabel: `${namespace}:option:1:label`, @@ -108,7 +108,7 @@ describe("Department Store Sale - Mystery Encounter", () => { it("should have shop with only TMs", async () => { await game.runToMysteryEncounter(MysteryEncounterType.DEPARTMENT_STORE_SALE, defaultParty); - await runSelectMysteryEncounterOption(game, 1); + await runMysteryEncounterToEnd(game, 1); expect(scene.getCurrentPhase().constructor.name).toBe(SelectModifierPhase.name); await game.phaseInterceptor.run(SelectModifierPhase); @@ -124,7 +124,7 @@ describe("Department Store Sale - Mystery Encounter", () => { const leaveEncounterWithoutBattleSpy = vi.spyOn(EncounterPhaseUtils, "leaveEncounterWithoutBattle"); await game.runToMysteryEncounter(MysteryEncounterType.DEPARTMENT_STORE_SALE, defaultParty); - await runSelectMysteryEncounterOption(game, 1); + await runMysteryEncounterToEnd(game, 1); expect(leaveEncounterWithoutBattleSpy).toBeCalled(); }); @@ -133,7 +133,7 @@ describe("Department Store Sale - Mystery Encounter", () => { describe("Option 2 - Vitamin Shop", () => { it("should have the correct properties", () => { const option = DepartmentStoreSaleEncounter.options[1]; - expect(option.optionMode).toBe(EncounterOptionMode.DEFAULT); + expect(option.optionMode).toBe(MysteryEncounterOptionMode.DEFAULT); expect(option.dialogue).toBeDefined(); expect(option.dialogue).toStrictEqual({ buttonLabel: `${namespace}:option:2:label`, @@ -143,7 +143,7 @@ describe("Department Store Sale - Mystery Encounter", () => { it("should have shop with only Vitamins", async () => { await game.runToMysteryEncounter(MysteryEncounterType.DEPARTMENT_STORE_SALE, defaultParty); - await runSelectMysteryEncounterOption(game, 2); + await runMysteryEncounterToEnd(game, 2); expect(scene.getCurrentPhase().constructor.name).toBe(SelectModifierPhase.name); await game.phaseInterceptor.run(SelectModifierPhase); @@ -160,7 +160,7 @@ describe("Department Store Sale - Mystery Encounter", () => { const leaveEncounterWithoutBattleSpy = vi.spyOn(EncounterPhaseUtils, "leaveEncounterWithoutBattle"); await game.runToMysteryEncounter(MysteryEncounterType.DEPARTMENT_STORE_SALE, defaultParty); - await runSelectMysteryEncounterOption(game, 2); + await runMysteryEncounterToEnd(game, 2); expect(leaveEncounterWithoutBattleSpy).toBeCalled(); }); @@ -169,7 +169,7 @@ describe("Department Store Sale - Mystery Encounter", () => { describe("Option 3 - X Item Shop", () => { it("should have the correct properties", () => { const option = DepartmentStoreSaleEncounter.options[2]; - expect(option.optionMode).toBe(EncounterOptionMode.DEFAULT); + expect(option.optionMode).toBe(MysteryEncounterOptionMode.DEFAULT); expect(option.dialogue).toBeDefined(); expect(option.dialogue).toStrictEqual({ buttonLabel: `${namespace}:option:3:label`, @@ -179,7 +179,7 @@ describe("Department Store Sale - Mystery Encounter", () => { it("should have shop with only X Items", async () => { await game.runToMysteryEncounter(MysteryEncounterType.DEPARTMENT_STORE_SALE, defaultParty); - await runSelectMysteryEncounterOption(game, 3); + await runMysteryEncounterToEnd(game, 3); expect(scene.getCurrentPhase().constructor.name).toBe(SelectModifierPhase.name); await game.phaseInterceptor.run(SelectModifierPhase); @@ -196,7 +196,7 @@ describe("Department Store Sale - Mystery Encounter", () => { const leaveEncounterWithoutBattleSpy = vi.spyOn(EncounterPhaseUtils, "leaveEncounterWithoutBattle"); await game.runToMysteryEncounter(MysteryEncounterType.DEPARTMENT_STORE_SALE, defaultParty); - await runSelectMysteryEncounterOption(game, 3); + await runMysteryEncounterToEnd(game, 3); expect(leaveEncounterWithoutBattleSpy).toBeCalled(); }); @@ -205,7 +205,7 @@ describe("Department Store Sale - Mystery Encounter", () => { describe("Option 4 - Pokeball Shop", () => { it("should have the correct properties", () => { const option = DepartmentStoreSaleEncounter.options[3]; - expect(option.optionMode).toBe(EncounterOptionMode.DEFAULT); + expect(option.optionMode).toBe(MysteryEncounterOptionMode.DEFAULT); expect(option.dialogue).toBeDefined(); expect(option.dialogue).toStrictEqual({ buttonLabel: `${namespace}:option:4:label`, @@ -215,7 +215,7 @@ describe("Department Store Sale - Mystery Encounter", () => { it("should have shop with only Pokeballs", async () => { await game.runToMysteryEncounter(MysteryEncounterType.DEPARTMENT_STORE_SALE, defaultParty); - await runSelectMysteryEncounterOption(game, 4); + await runMysteryEncounterToEnd(game, 4); expect(scene.getCurrentPhase().constructor.name).toBe(SelectModifierPhase.name); await game.phaseInterceptor.run(SelectModifierPhase); @@ -231,7 +231,7 @@ describe("Department Store Sale - Mystery Encounter", () => { const leaveEncounterWithoutBattleSpy = vi.spyOn(EncounterPhaseUtils, "leaveEncounterWithoutBattle"); await game.runToMysteryEncounter(MysteryEncounterType.DEPARTMENT_STORE_SALE, defaultParty); - await runSelectMysteryEncounterOption(game, 4); + await runMysteryEncounterToEnd(game, 4); expect(leaveEncounterWithoutBattleSpy).toBeCalled(); }); 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 33845d1dfe5..076159dcd19 100644 --- a/src/test/mystery-encounter/encounters/fiery-fallout-encounter.test.ts +++ b/src/test/mystery-encounter/encounters/fiery-fallout-encounter.test.ts @@ -5,20 +5,21 @@ import { Species } from "#app/enums/species"; import GameManager from "#app/test/utils/gameManager"; import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; import { FieryFalloutEncounter } from "#app/data/mystery-encounters/encounters/fiery-fallout-encounter"; -import Battle from "#app/battle"; import { Gender } from "#app/data/gender"; import { getPokemonSpecies } from "#app/data/pokemon-species"; import * as BattleAnims from "#app/data/battle-anims"; import * as EncounterPhaseUtils from "#app/data/mystery-encounters/utils/encounter-phase-utils"; -import { EncounterOptionMode } from "#app/data/mystery-encounters/mystery-encounter-option"; -import { runSelectMysteryEncounterOption, skipBattleRunMysteryEncounterRewardsPhase } from "#test/mystery-encounter/encounterTestUtils"; +import { runMysteryEncounterToEnd, runSelectMysteryEncounterOption, skipBattleRunMysteryEncounterRewardsPhase } from "#test/mystery-encounter/encounterTestUtils"; import { CommandPhase, MovePhase, SelectModifierPhase } from "#app/phases"; import { Moves } from "#enums/moves"; import BattleScene from "#app/battle-scene"; import { PokemonHeldItemModifier } from "#app/modifier/modifier"; import { Type } from "#app/data/type"; import { Status, StatusEffect } from "#app/data/status-effect"; -import { MysteryEncounterTier } from "#app/data/mystery-encounters/mystery-encounter"; +import { MysteryEncounterPhase } from "#app/phases/mystery-encounter-phases"; +import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode"; +import { MysteryEncounterTier } from "#enums/mystery-encounter-tier"; +import { initSceneWithoutEncounterPhase } from "#test/utils/gameManagerUtils"; const namespace = "mysteryEncounter:fieryFallout"; /** Arcanine and Ninetails for 2 Fire types. Lapras, Gengar, Abra for burnable mon. */ @@ -94,7 +95,8 @@ describe("Fiery Fallout - Mystery Encounter", () => { }); it("should initialize fully ", async () => { - vi.spyOn(scene, "currentBattle", "get").mockReturnValue({ mysteryEncounter: FieryFalloutEncounter } as Battle); + initSceneWithoutEncounterPhase(scene, defaultParty); + scene.currentBattle.mysteryEncounter = FieryFalloutEncounter; const weatherSpy = vi.spyOn(scene.arena, "trySetWeather").mockReturnValue(true); const moveInitSpy = vi.spyOn(BattleAnims, "initMoveAnim"); const moveLoadSpy = vi.spyOn(BattleAnims, "loadMoveAnimAssets"); @@ -103,6 +105,7 @@ describe("Fiery Fallout - Mystery Encounter", () => { expect(FieryFalloutEncounter.onInit).toBeDefined(); + FieryFalloutEncounter.populateDialogueTokensFromRequirements(scene); const onInitResult = onInit(scene); expect(FieryFalloutEncounter.enemyPartyConfigs).toEqual([ @@ -132,7 +135,7 @@ describe("Fiery Fallout - Mystery Encounter", () => { describe("Option 1 - Fight 2 Volcarona", () => { it("should have the correct properties", () => { const option1 = FieryFalloutEncounter.options[0]; - expect(option1.optionMode).toBe(EncounterOptionMode.DEFAULT); + expect(option1.optionMode).toBe(MysteryEncounterOptionMode.DEFAULT); expect(option1.dialogue).toBeDefined(); expect(option1.dialogue).toStrictEqual({ buttonLabel: `${namespace}:option:1:label`, @@ -149,7 +152,7 @@ describe("Fiery Fallout - Mystery Encounter", () => { const phaseSpy = vi.spyOn(scene, "pushPhase"); await game.runToMysteryEncounter(MysteryEncounterType.FIERY_FALLOUT, defaultParty); - await runSelectMysteryEncounterOption(game, 1, true); + await runMysteryEncounterToEnd(game, 1, true); const enemyField = scene.getEnemyField(); expect(scene.getCurrentPhase().constructor.name).toBe(CommandPhase.name); @@ -166,7 +169,7 @@ describe("Fiery Fallout - Mystery Encounter", () => { it("should give charcoal to lead pokemon", async () => { await game.runToMysteryEncounter(MysteryEncounterType.FIERY_FALLOUT, defaultParty); - await runSelectMysteryEncounterOption(game, 1, true); + await runMysteryEncounterToEnd(game, 1, true); await skipBattleRunMysteryEncounterRewardsPhase(game); await game.phaseInterceptor.to(SelectModifierPhase, false); expect(scene.getCurrentPhase().constructor.name).toBe(SelectModifierPhase.name); @@ -182,7 +185,7 @@ describe("Fiery Fallout - Mystery Encounter", () => { describe("Option 2 - Suffer the weather", () => { it("should have the correct properties", () => { const option1 = FieryFalloutEncounter.options[1]; - expect(option1.optionMode).toBe(EncounterOptionMode.DEFAULT); + expect(option1.optionMode).toBe(MysteryEncounterOptionMode.DEFAULT); expect(option1.dialogue).toBeDefined(); expect(option1.dialogue).toStrictEqual({ buttonLabel: `${namespace}:option:2:label`, @@ -204,7 +207,7 @@ describe("Fiery Fallout - Mystery Encounter", () => { const abra = party.find((pkm) => pkm.species.speciesId === Species.ABRA); vi.spyOn(abra, "isAllowedInBattle").mockReturnValue(false); - await runSelectMysteryEncounterOption(game, 2); + await runMysteryEncounterToEnd(game, 2); const burnablePokemon = party.filter((pkm) => pkm.isAllowedInBattle() && !pkm.getTypes().includes(Type.FIRE)); const notBurnablePokemon = party.filter((pkm) => !pkm.isAllowedInBattle() || pkm.getTypes().includes(Type.FIRE)); @@ -220,7 +223,7 @@ describe("Fiery Fallout - Mystery Encounter", () => { const leaveEncounterWithoutBattleSpy = vi.spyOn(EncounterPhaseUtils, "leaveEncounterWithoutBattle"); await game.runToMysteryEncounter(MysteryEncounterType.FIERY_FALLOUT, defaultParty); - await runSelectMysteryEncounterOption(game, 2); + await runMysteryEncounterToEnd(game, 2); expect(leaveEncounterWithoutBattleSpy).toBeCalled(); }); @@ -229,7 +232,7 @@ describe("Fiery Fallout - Mystery Encounter", () => { describe("Option 3 - use FIRE types", () => { it("should have the correct properties", () => { const option1 = FieryFalloutEncounter.options[2]; - expect(option1.optionMode).toBe(EncounterOptionMode.DISABLED_OR_SPECIAL); + expect(option1.optionMode).toBe(MysteryEncounterOptionMode.DISABLED_OR_SPECIAL); expect(option1.dialogue).toBeDefined(); expect(option1.dialogue).toStrictEqual({ buttonLabel: `${namespace}:option:3:label`, @@ -245,7 +248,7 @@ describe("Fiery Fallout - Mystery Encounter", () => { it("should give charcoal to lead pokemon", async () => { await game.runToMysteryEncounter(MysteryEncounterType.FIERY_FALLOUT, defaultParty); - await runSelectMysteryEncounterOption(game, 3); + await runMysteryEncounterToEnd(game, 3); // await skipBattleRunMysteryEncounterRewardsPhase(game); await game.phaseInterceptor.to(SelectModifierPhase, false); expect(scene.getCurrentPhase().constructor.name).toBe(SelectModifierPhase.name); @@ -261,9 +264,23 @@ describe("Fiery Fallout - Mystery Encounter", () => { const leaveEncounterWithoutBattleSpy = vi.spyOn(EncounterPhaseUtils, "leaveEncounterWithoutBattle"); await game.runToMysteryEncounter(MysteryEncounterType.FIERY_FALLOUT, defaultParty); - await runSelectMysteryEncounterOption(game, 3); + await runMysteryEncounterToEnd(game, 3); expect(leaveEncounterWithoutBattleSpy).toBeCalled(); }); + + it("should be disabled if not enough FIRE types are in party", async () => { + await game.runToMysteryEncounter(MysteryEncounterType.FIERY_FALLOUT, [Species.MAGIKARP, Species.ARCANINE]); + await game.phaseInterceptor.to(MysteryEncounterPhase, false); + + const encounterPhase = scene.getCurrentPhase(); + expect(encounterPhase.constructor.name).toBe(MysteryEncounterPhase.name); + const continueEncounterSpy = vi.spyOn((encounterPhase as MysteryEncounterPhase), "continueEncounter"); + + await runSelectMysteryEncounterOption(game, 3); + + expect(scene.getCurrentPhase().constructor.name).toBe(MysteryEncounterPhase.name); + expect(continueEncounterSpy).not.toHaveBeenCalled(); + }); }); }); 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 6d36f47a8c1..aa8d95b39bd 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 @@ -1,6 +1,4 @@ -import Battle from "#app/battle"; import { LostAtSeaEncounter } from "#app/data/mystery-encounters/encounters/lost-at-sea-encounter"; -import { EncounterOptionMode } from "#app/data/mystery-encounters/mystery-encounter-option"; import * as MysteryEncounters from "#app/data/mystery-encounters/mystery-encounters"; import * as EncounterPhaseUtils from "#app/data/mystery-encounters/utils/encounter-phase-utils"; import { getPokemonSpecies } from "#app/data/pokemon-species.js"; @@ -10,8 +8,11 @@ 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 { runSelectMysteryEncounterOption } from "../encounterTestUtils"; -import { MysteryEncounterTier } from "#app/data/mystery-encounters/mystery-encounter"; +import { runMysteryEncounterToEnd } from "../encounterTestUtils"; +import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode"; +import { MysteryEncounterTier } from "#enums/mystery-encounter-tier"; +import { initSceneWithoutEncounterPhase } from "#test/utils/gameManagerUtils"; +import BattleScene from "#app/battle-scene"; const namespace = "mysteryEncounter:lostAtSea"; /** Blastoise for surf. Pidgeot for fly. Abra for none. */ @@ -22,6 +23,7 @@ const defaultWave = 33; describe("Lost at Sea - Mystery Encounter", () => { let phaserGame: Phaser.Game; let game: GameManager; + let scene: BattleScene; beforeAll(() => { phaserGame = new Phaser.Game({ type: Phaser.HEADLESS }); @@ -29,6 +31,7 @@ describe("Lost at Sea - Mystery Encounter", () => { beforeEach(async () => { game = new GameManager(phaserGame); + scene = game.scene; game.override.mysteryEncounterChance(100); game.override.startingWave(defaultWave); game.override.startingBiome(defaultBiome); @@ -83,14 +86,16 @@ describe("Lost at Sea - Mystery Encounter", () => { expect(game.scene.currentBattle.mysteryEncounter).toBeUndefined(); }); - it("should set the correct dialog tokens during initialization", () => { - vi.spyOn(game.scene, "currentBattle", "get").mockReturnValue({ mysteryEncounter: LostAtSeaEncounter } as Battle); + it("should initialize fully", () => { + initSceneWithoutEncounterPhase(scene, defaultParty); + scene.currentBattle.mysteryEncounter = LostAtSeaEncounter; const { onInit } = LostAtSeaEncounter; expect(LostAtSeaEncounter.onInit).toBeDefined(); - const onInitResult = onInit(game.scene); + LostAtSeaEncounter.populateDialogueTokensFromRequirements(scene); + const onInitResult = onInit(scene); expect(LostAtSeaEncounter.dialogueTokens?.damagePercentage).toBe("25"); expect(LostAtSeaEncounter.dialogueTokens?.option1RequiredMove).toBe(Moves[Moves.SURF]); @@ -101,7 +106,7 @@ describe("Lost at Sea - Mystery Encounter", () => { describe("Option 1 - Surf", () => { it("should have the correct properties", () => { const option1 = LostAtSeaEncounter.options[0]; - expect(option1.optionMode).toBe(EncounterOptionMode.DISABLED_OR_DEFAULT); + expect(option1.optionMode).toBe(MysteryEncounterOptionMode.DISABLED_OR_DEFAULT); expect(option1.dialogue).toBeDefined(); expect(option1.dialogue).toStrictEqual({ buttonLabel: `${namespace}:option:1:label`, @@ -124,7 +129,7 @@ describe("Lost at Sea - Mystery Encounter", () => { const blastoise = party.find((pkm) => pkm.species.speciesId === Species.PIDGEOT); const expBefore = blastoise.exp; - await runSelectMysteryEncounterOption(game, 2); + await runMysteryEncounterToEnd(game, 2); expect(blastoise.exp).toBe(expBefore + Math.floor(laprasSpecies.baseExp * defaultWave / 5 + 1)); }); @@ -134,7 +139,7 @@ describe("Lost at Sea - Mystery Encounter", () => { const leaveEncounterWithoutBattleSpy = vi.spyOn(EncounterPhaseUtils, "leaveEncounterWithoutBattle"); await game.runToMysteryEncounter(MysteryEncounterType.LOST_AT_SEA, defaultParty); - await runSelectMysteryEncounterOption(game, 1); + await runMysteryEncounterToEnd(game, 1); expect(leaveEncounterWithoutBattleSpy).toBeCalled(); }); @@ -148,7 +153,7 @@ describe("Lost at Sea - Mystery Encounter", () => { it("should have the correct properties", () => { const option2 = LostAtSeaEncounter.options[1]; - expect(option2.optionMode).toBe(EncounterOptionMode.DISABLED_OR_DEFAULT); + expect(option2.optionMode).toBe(MysteryEncounterOptionMode.DISABLED_OR_DEFAULT); expect(option2.dialogue).toBeDefined(); expect(option2.dialogue).toStrictEqual({ buttonLabel: `${namespace}:option:2:label`, @@ -173,7 +178,7 @@ describe("Lost at Sea - Mystery Encounter", () => { const pidgeot = party.find((pkm) => pkm.species.speciesId === Species.PIDGEOT); const expBefore = pidgeot.exp; - await runSelectMysteryEncounterOption(game, 2); + await runMysteryEncounterToEnd(game, 2); expect(pidgeot.exp).toBe(expBefore + Math.floor(laprasBaseExp * defaultWave / 5 + 1)); }); @@ -183,7 +188,7 @@ describe("Lost at Sea - Mystery Encounter", () => { const leaveEncounterWithoutBattleSpy = vi.spyOn(EncounterPhaseUtils, "leaveEncounterWithoutBattle"); await game.runToMysteryEncounter(MysteryEncounterType.LOST_AT_SEA, defaultParty); - await runSelectMysteryEncounterOption(game, 2); + await runMysteryEncounterToEnd(game, 2); expect(leaveEncounterWithoutBattleSpy).toBeCalled(); }); @@ -197,7 +202,7 @@ describe("Lost at Sea - Mystery Encounter", () => { it("should have the correct properties", () => { const option3 = LostAtSeaEncounter.options[2]; - expect(option3.optionMode).toBe(EncounterOptionMode.DEFAULT); + expect(option3.optionMode).toBe(MysteryEncounterOptionMode.DEFAULT); expect(option3.dialogue).toBeDefined(); expect(option3.dialogue).toStrictEqual({ buttonLabel: `${namespace}:option:3:label`, @@ -219,7 +224,7 @@ describe("Lost at Sea - Mystery Encounter", () => { const abra = party.find((pkm) => pkm.species.speciesId === Species.ABRA); vi.spyOn(abra, "isAllowedInBattle").mockReturnValue(false); - await runSelectMysteryEncounterOption(game, 3); + await runMysteryEncounterToEnd(game, 3); const allowedPkm = party.filter((pkm) => pkm.isAllowedInBattle()); const notAllowedPkm = party.filter((pkm) => !pkm.isAllowedInBattle()); @@ -235,7 +240,7 @@ describe("Lost at Sea - Mystery Encounter", () => { const leaveEncounterWithoutBattleSpy = vi.spyOn(EncounterPhaseUtils, "leaveEncounterWithoutBattle"); await game.runToMysteryEncounter(MysteryEncounterType.LOST_AT_SEA, defaultParty); - await runSelectMysteryEncounterOption(game, 3); + await runMysteryEncounterToEnd(game, 3); expect(leaveEncounterWithoutBattleSpy).toBeCalled(); }); diff --git a/src/test/mystery-encounter/encounters/offer-you-cant-refuse-encounter.test.ts b/src/test/mystery-encounter/encounters/offer-you-cant-refuse-encounter.test.ts new file mode 100644 index 00000000000..c44b399a39a --- /dev/null +++ b/src/test/mystery-encounter/encounters/offer-you-cant-refuse-encounter.test.ts @@ -0,0 +1,244 @@ +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 * as EncounterPhaseUtils from "#app/data/mystery-encounters/utils/encounter-phase-utils"; +import { runMysteryEncounterToEnd } from "#test/mystery-encounter/encounterTestUtils"; +import BattleScene from "#app/battle-scene"; +import { PlayerPokemon, PokemonMove } from "#app/field/pokemon"; +import { OfferYouCantRefuseEncounter } from "#app/data/mystery-encounters/encounters/offer-you-cant-refuse-encounter"; +import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode"; +import { MysteryEncounterTier } from "#enums/mystery-encounter-tier"; +import { initSceneWithoutEncounterPhase } from "#test/utils/gameManagerUtils"; +import { getPokemonSpecies } from "#app/data/pokemon-species"; +import { Moves } from "#enums/moves"; + +const namespace = "mysteryEncounter:offerYouCantRefuse"; +/** Gyarados for Indimidate */ +const defaultParty = [Species.GYARADOS, Species.GENGAR, Species.ABRA]; +const defaultBiome = Biome.CAVE; +const defaultWave = 45; + +describe("An Offer You Can't Refuse - 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.mysteryEncounterTier(MysteryEncounterTier.COMMON); + game.override.startingWave(defaultWave); + game.override.startingBiome(defaultBiome); + + const biomeMap = new Map([ + [Biome.VOLCANO, [MysteryEncounterType.MYSTERIOUS_CHALLENGERS]], + ]); + HUMAN_TRANSITABLE_BIOMES.forEach(biome => { + biomeMap.set(biome, [MysteryEncounterType.OFFER_YOU_CANT_REFUSE]); + }); + 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.OFFER_YOU_CANT_REFUSE, defaultParty); + + expect(OfferYouCantRefuseEncounter.encounterType).toBe(MysteryEncounterType.OFFER_YOU_CANT_REFUSE); + expect(OfferYouCantRefuseEncounter.encounterTier).toBe(MysteryEncounterTier.GREAT); + expect(OfferYouCantRefuseEncounter.dialogue).toBeDefined(); + expect(OfferYouCantRefuseEncounter.dialogue.intro).toStrictEqual([ + { text: `${namespace}:intro` }, + { speaker: `${namespace}:speaker`, text: `${namespace}:intro_dialogue` } + ]); + expect(OfferYouCantRefuseEncounter.dialogue.encounterOptionsDialogue.title).toBe(`${namespace}:title`); + expect(OfferYouCantRefuseEncounter.dialogue.encounterOptionsDialogue.description).toBe(`${namespace}:description`); + expect(OfferYouCantRefuseEncounter.dialogue.encounterOptionsDialogue.query).toBe(`${namespace}:query`); + expect(OfferYouCantRefuseEncounter.options.length).toBe(3); + }); + + it("should not spawn outside of HUMAN_TRANSITABLE_BIOMES", async () => { + game.override.startingBiome(Biome.VOLCANO); + await game.runToMysteryEncounter(); + + expect(scene.currentBattle?.mysteryEncounter?.encounterType).not.toBe(MysteryEncounterType.OFFER_YOU_CANT_REFUSE); + }); + + it("should not run below wave 10", async () => { + game.override.startingWave(9); + + await game.runToMysteryEncounter(); + + expect(scene.currentBattle?.mysteryEncounter?.encounterType).not.toBe(MysteryEncounterType.OFFER_YOU_CANT_REFUSE); + }); + + 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 = OfferYouCantRefuseEncounter; + + const { onInit } = OfferYouCantRefuseEncounter; + + expect(OfferYouCantRefuseEncounter.onInit).toBeDefined(); + + OfferYouCantRefuseEncounter.populateDialogueTokensFromRequirements(scene); + const onInitResult = onInit(scene); + + expect(OfferYouCantRefuseEncounter.dialogueTokens?.strongestPokemon).toBeDefined(); + expect(OfferYouCantRefuseEncounter.dialogueTokens?.price).toBeDefined(); + expect(OfferYouCantRefuseEncounter.dialogueTokens?.option2PrimaryAbility).toBe("Intimidate"); + expect(OfferYouCantRefuseEncounter.dialogueTokens?.moveOrAbility).toBe("Intimidate"); + expect(OfferYouCantRefuseEncounter.misc.pokemon instanceof PlayerPokemon).toBeTruthy(); + expect(OfferYouCantRefuseEncounter.misc?.price?.toString()).toBe(OfferYouCantRefuseEncounter.dialogueTokens?.price); + expect(onInitResult).toBe(true); + }); + + describe("Option 1 - Sell your Pokemon for money and a Shiny Charm", () => { + it("should have the correct properties", () => { + const option = OfferYouCantRefuseEncounter.options[0]; + expect(option.optionMode).toBe(MysteryEncounterOptionMode.DEFAULT); + expect(option.dialogue).toBeDefined(); + expect(option.dialogue).toStrictEqual({ + buttonLabel: `${namespace}:option:1:label`, + buttonTooltip: `${namespace}:option:1:tooltip`, + selected: [ + { + speaker: `${namespace}:speaker`, + text: `${namespace}:option:1:selected`, + }, + ], + }); + }); + + it("Should update the player's money properly", async () => { + const initialMoney = 20000; + scene.money = initialMoney; + const updateMoneySpy = vi.spyOn(EncounterPhaseUtils, "updatePlayerMoney"); + + await game.runToMysteryEncounter(MysteryEncounterType.OFFER_YOU_CANT_REFUSE, defaultParty); + await runMysteryEncounterToEnd(game, 1); + + const price = scene.currentBattle.mysteryEncounter.misc.price; + + expect(updateMoneySpy).toHaveBeenCalledWith(scene, price); + expect(scene.money).toBe(initialMoney + price); + }); + + it("Should remove the Pokemon from the party", async () => { + await game.runToMysteryEncounter(MysteryEncounterType.OFFER_YOU_CANT_REFUSE, defaultParty); + + const initialPartySize = scene.getParty().length; + const pokemonName = scene.currentBattle.mysteryEncounter.misc.pokemon.name; + + await runMysteryEncounterToEnd(game, 1); + + expect(scene.getParty().length).toBe(initialPartySize - 1); + expect(scene.getParty().find(p => p.name === pokemonName)).toBeUndefined(); + }); + + it("should leave encounter without battle", async () => { + const leaveEncounterWithoutBattleSpy = vi.spyOn(EncounterPhaseUtils, "leaveEncounterWithoutBattle"); + + await game.runToMysteryEncounter(MysteryEncounterType.OFFER_YOU_CANT_REFUSE, defaultParty); + await runMysteryEncounterToEnd(game, 1); + + expect(leaveEncounterWithoutBattleSpy).toBeCalled(); + }); + }); + + describe("Option 2 - Extort the Kid", () => { + it("should have the correct properties", () => { + const option = OfferYouCantRefuseEncounter.options[1]; + expect(option.optionMode).toBe(MysteryEncounterOptionMode.DISABLED_OR_SPECIAL); + expect(option.dialogue).toBeDefined(); + expect(option.dialogue).toStrictEqual({ + buttonLabel: `${namespace}:option:2:label`, + buttonTooltip: `${namespace}:option:2:tooltip`, + disabledButtonTooltip: `${namespace}:option:2:tooltip_disabled`, + selected: [ + { + speaker: `${namespace}:speaker`, + text: `${namespace}:option:2:selected`, + }, + ], + }); + }); + + it("should award EXP to a pokemon with an ability in EXTORTION_ABILITIES", async () => { + await game.runToMysteryEncounter(MysteryEncounterType.OFFER_YOU_CANT_REFUSE, defaultParty); + const party = scene.getParty(); + const gyarados = party.find((pkm) => pkm.species.speciesId === Species.GYARADOS); + const expBefore = gyarados.exp; + + await runMysteryEncounterToEnd(game, 2); + + expect(gyarados.exp).toBe(expBefore + Math.floor(getPokemonSpecies(Species.LIEPARD).baseExp * defaultWave / 5 + 1)); + }); + + it("should award EXP to a pokemon with a move in EXTORTION_MOVES", async () => { + await game.runToMysteryEncounter(MysteryEncounterType.OFFER_YOU_CANT_REFUSE, [Species.ABRA]); + const party = scene.getParty(); + const abra = party.find((pkm) => pkm.species.speciesId === Species.ABRA); + abra.moveset = [new PokemonMove(Moves.BEAT_UP)]; + const expBefore = abra.exp; + + await runMysteryEncounterToEnd(game, 2); + + expect(abra.exp).toBe(expBefore + Math.floor(getPokemonSpecies(Species.LIEPARD).baseExp * defaultWave / 5 + 1)); + }); + + it("Should update the player's money properly", async () => { + const initialMoney = 20000; + scene.money = initialMoney; + const updateMoneySpy = vi.spyOn(EncounterPhaseUtils, "updatePlayerMoney"); + + await game.runToMysteryEncounter(MysteryEncounterType.OFFER_YOU_CANT_REFUSE, defaultParty); + await runMysteryEncounterToEnd(game, 2); + + const price = scene.currentBattle.mysteryEncounter.misc.price; + + expect(updateMoneySpy).toHaveBeenCalledWith(scene, price); + expect(scene.money).toBe(initialMoney + price); + }); + + it("should leave encounter without battle", async () => { + const leaveEncounterWithoutBattleSpy = vi.spyOn(EncounterPhaseUtils, "leaveEncounterWithoutBattle"); + + await game.runToMysteryEncounter(MysteryEncounterType.OFFER_YOU_CANT_REFUSE, defaultParty); + await runMysteryEncounterToEnd(game, 2); + + expect(leaveEncounterWithoutBattleSpy).toBeCalled(); + }); + }); + + describe("Option 3 - Leave", () => { + it("should leave encounter without battle", async () => { + const leaveEncounterWithoutBattleSpy = vi.spyOn(EncounterPhaseUtils, "leaveEncounterWithoutBattle"); + + await game.runToMysteryEncounter(MysteryEncounterType.OFFER_YOU_CANT_REFUSE, defaultParty); + await runMysteryEncounterToEnd(game, 3); + + expect(leaveEncounterWithoutBattleSpy).toBeCalled(); + }); + }); +}); diff --git a/src/test/mystery-encounter/encounters/pokemon-salesman-encounter.test.ts b/src/test/mystery-encounter/encounters/pokemon-salesman-encounter.test.ts index c3045b599eb..10b739b47b2 100644 --- a/src/test/mystery-encounter/encounters/pokemon-salesman-encounter.test.ts +++ b/src/test/mystery-encounter/encounters/pokemon-salesman-encounter.test.ts @@ -4,15 +4,15 @@ 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 Battle from "#app/battle"; import * as EncounterPhaseUtils from "#app/data/mystery-encounters/utils/encounter-phase-utils"; -import { EncounterOptionMode } from "#app/data/mystery-encounters/mystery-encounter-option"; -import { runSelectMysteryEncounterOption } from "#test/mystery-encounter/encounterTestUtils"; +import { runMysteryEncounterToEnd } from "#test/mystery-encounter/encounterTestUtils"; import BattleScene from "#app/battle-scene"; -import { MysteryEncounterTier } from "#app/data/mystery-encounters/mystery-encounter"; import { PlayerPokemon } from "#app/field/pokemon"; import { HUMAN_TRANSITABLE_BIOMES } from "#app/data/mystery-encounters/mystery-encounters"; import { PokemonSalesmanEncounter } from "#app/data/mystery-encounters/encounters/pokemon-salesman-encounter"; +import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode"; +import { MysteryEncounterTier } from "#enums/mystery-encounter-tier"; +import { initSceneWithoutEncounterPhase } from "#test/utils/gameManagerUtils"; const namespace = "mysteryEncounter:pokemonSalesman"; const defaultParty = [Species.LAPRAS, Species.GENGAR, Species.ABRA]; @@ -59,7 +59,7 @@ describe("The Pokemon Salesman - Mystery Encounter", () => { expect(PokemonSalesmanEncounter.dialogue).toBeDefined(); expect(PokemonSalesmanEncounter.dialogue.intro).toStrictEqual([ { text: `${namespace}:intro` }, - { speaker: "mysteryEncounter:pokemonSalesman:speaker", text: "mysteryEncounter:pokemonSalesman:intro_dialogue" } + { speaker: `${namespace}:speaker`, text: `${namespace}:intro_dialogue` } ]); expect(PokemonSalesmanEncounter.dialogue.encounterOptionsDialogue.title).toBe(`${namespace}:title`); expect(PokemonSalesmanEncounter.dialogue.encounterOptionsDialogue.description).toBe(`${namespace}:description`); @@ -91,12 +91,14 @@ describe("The Pokemon Salesman - Mystery Encounter", () => { }); it("should initialize fully ", async () => { - vi.spyOn(scene, "currentBattle", "get").mockReturnValue({ mysteryEncounter: PokemonSalesmanEncounter } as Battle); + initSceneWithoutEncounterPhase(scene, defaultParty); + scene.currentBattle.mysteryEncounter = PokemonSalesmanEncounter; const { onInit } = PokemonSalesmanEncounter; expect(PokemonSalesmanEncounter.onInit).toBeDefined(); + PokemonSalesmanEncounter.populateDialogueTokensFromRequirements(scene); const onInitResult = onInit(scene); expect(PokemonSalesmanEncounter.dialogueTokens?.purchasePokemon).toBeDefined(); @@ -109,7 +111,7 @@ describe("The Pokemon Salesman - Mystery Encounter", () => { describe("Option 1 - Purchase the pokemon", () => { it("should have the correct properties", () => { const option1 = PokemonSalesmanEncounter.options[0]; - expect(option1.optionMode).toBe(EncounterOptionMode.DEFAULT_OR_SPECIAL); + expect(option1.optionMode).toBe(MysteryEncounterOptionMode.DEFAULT_OR_SPECIAL); expect(option1.dialogue).toBeDefined(); expect(option1.dialogue).toStrictEqual({ buttonLabel: `${namespace}:option:1:label`, @@ -128,7 +130,7 @@ describe("The Pokemon Salesman - Mystery Encounter", () => { const updateMoneySpy = vi.spyOn(EncounterPhaseUtils, "updatePlayerMoney"); await game.runToMysteryEncounter(MysteryEncounterType.POKEMON_SALESMAN, defaultParty); - await runSelectMysteryEncounterOption(game, 1); + await runMysteryEncounterToEnd(game, 1); const price = scene.currentBattle.mysteryEncounter.misc.price; @@ -142,7 +144,7 @@ describe("The Pokemon Salesman - Mystery Encounter", () => { const initialPartySize = scene.getParty().length; const pokemonName = scene.currentBattle.mysteryEncounter.misc.pokemon.name; - await runSelectMysteryEncounterOption(game, 1); + await runMysteryEncounterToEnd(game, 1); expect(scene.getParty().length).toBe(initialPartySize + 1); expect(scene.getParty().find(p => p.name === pokemonName) instanceof PlayerPokemon).toBeTruthy(); @@ -152,7 +154,7 @@ describe("The Pokemon Salesman - Mystery Encounter", () => { const leaveEncounterWithoutBattleSpy = vi.spyOn(EncounterPhaseUtils, "leaveEncounterWithoutBattle"); await game.runToMysteryEncounter(MysteryEncounterType.POKEMON_SALESMAN, defaultParty); - await runSelectMysteryEncounterOption(game, 1); + await runMysteryEncounterToEnd(game, 1); expect(leaveEncounterWithoutBattleSpy).toBeCalled(); }); @@ -163,7 +165,7 @@ describe("The Pokemon Salesman - Mystery Encounter", () => { const leaveEncounterWithoutBattleSpy = vi.spyOn(EncounterPhaseUtils, "leaveEncounterWithoutBattle"); await game.runToMysteryEncounter(MysteryEncounterType.POKEMON_SALESMAN, defaultParty); - await runSelectMysteryEncounterOption(game, 2); + await runMysteryEncounterToEnd(game, 2); expect(leaveEncounterWithoutBattleSpy).toBeCalled(); }); diff --git a/src/test/mystery-encounter/encounters/the-strong-stuff-encounter.test.ts b/src/test/mystery-encounter/encounters/the-strong-stuff-encounter.test.ts index b57683d3165..d0fd2b92989 100644 --- a/src/test/mystery-encounter/encounters/the-strong-stuff-encounter.test.ts +++ b/src/test/mystery-encounter/encounters/the-strong-stuff-encounter.test.ts @@ -4,17 +4,14 @@ 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 Battle from "#app/battle"; import { getPokemonSpecies } from "#app/data/pokemon-species"; import * as BattleAnims from "#app/data/battle-anims"; import * as EncounterPhaseUtils from "#app/data/mystery-encounters/utils/encounter-phase-utils"; -import { EncounterOptionMode } from "#app/data/mystery-encounters/mystery-encounter-option"; -import { runSelectMysteryEncounterOption, skipBattleRunMysteryEncounterRewardsPhase } from "#test/mystery-encounter/encounterTestUtils"; +import { runMysteryEncounterToEnd, skipBattleRunMysteryEncounterRewardsPhase } from "#test/mystery-encounter/encounterTestUtils"; import { CommandPhase, MovePhase, SelectModifierPhase } from "#app/phases"; import { Moves } from "#enums/moves"; import BattleScene from "#app/battle-scene"; import * as Modifiers from "#app/modifier/modifier"; -import { MysteryEncounterTier } from "#app/data/mystery-encounters/mystery-encounter"; import { TheStrongStuffEncounter } from "#app/data/mystery-encounters/encounters/the-strong-stuff-encounter"; import { Nature } from "#app/data/nature"; import { BerryType } from "#enums/berry-type"; @@ -23,6 +20,9 @@ import { PokemonMove } from "#app/field/pokemon"; import { Mode } from "#app/ui/ui"; import ModifierSelectUiHandler from "#app/ui/modifier-select-ui-handler"; import { PokemonBaseStatTotalModifier } from "#app/modifier/modifier"; +import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode"; +import { MysteryEncounterTier } from "#enums/mystery-encounter-tier"; +import { initSceneWithoutEncounterPhase } from "#test/utils/gameManagerUtils"; const namespace = "mysteryEncounter:theStrongStuff"; const defaultParty = [Species.LAPRAS, Species.GENGAR, Species.ABRA]; @@ -97,7 +97,8 @@ describe("The Strong Stuff - Mystery Encounter", () => { }); it("should initialize fully ", async () => { - vi.spyOn(scene, "currentBattle", "get").mockReturnValue({ mysteryEncounter: TheStrongStuffEncounter } as Battle); + initSceneWithoutEncounterPhase(scene, defaultParty); + scene.currentBattle.mysteryEncounter = TheStrongStuffEncounter; const moveInitSpy = vi.spyOn(BattleAnims, "loadMoveAnimAssets"); const moveLoadSpy = vi.spyOn(BattleAnims, "loadMoveAnimAssets"); @@ -105,6 +106,7 @@ describe("The Strong Stuff - Mystery Encounter", () => { expect(TheStrongStuffEncounter.onInit).toBeDefined(); + TheStrongStuffEncounter.populateDialogueTokensFromRequirements(scene); const onInitResult = onInit(scene); expect(TheStrongStuffEncounter.enemyPartyConfigs).toEqual([ @@ -134,7 +136,7 @@ describe("The Strong Stuff - Mystery Encounter", () => { describe("Option 1 - Power Swap BSTs", () => { it("should have the correct properties", () => { const option1 = TheStrongStuffEncounter.options[0]; - expect(option1.optionMode).toBe(EncounterOptionMode.DEFAULT); + expect(option1.optionMode).toBe(MysteryEncounterOptionMode.DEFAULT); expect(option1.dialogue).toBeDefined(); expect(option1.dialogue).toStrictEqual({ buttonLabel: `${namespace}:option:1:label`, @@ -151,7 +153,7 @@ describe("The Strong Stuff - Mystery Encounter", () => { await game.runToMysteryEncounter(MysteryEncounterType.THE_STRONG_STUFF, defaultParty); const bstsPrior = scene.getParty().map(p => p.getSpeciesForm().getBaseStatTotal()); - await runSelectMysteryEncounterOption(game, 1); + await runMysteryEncounterToEnd(game, 1); const bstsAfter = scene.getParty().map(p => { const baseStats = p.getSpeciesForm().baseStats.slice(0); @@ -168,7 +170,7 @@ describe("The Strong Stuff - Mystery Encounter", () => { const leaveEncounterWithoutBattleSpy = vi.spyOn(EncounterPhaseUtils, "leaveEncounterWithoutBattle"); await game.runToMysteryEncounter(MysteryEncounterType.THE_STRONG_STUFF, defaultParty); - await runSelectMysteryEncounterOption(game, 1); + await runMysteryEncounterToEnd(game, 1); expect(leaveEncounterWithoutBattleSpy).toBeCalled(); }); @@ -177,7 +179,7 @@ describe("The Strong Stuff - Mystery Encounter", () => { describe("Option 2 - battle the Shuckle", () => { it("should have the correct properties", () => { const option1 = TheStrongStuffEncounter.options[1]; - expect(option1.optionMode).toBe(EncounterOptionMode.DEFAULT); + expect(option1.optionMode).toBe(MysteryEncounterOptionMode.DEFAULT); expect(option1.dialogue).toBeDefined(); expect(option1.dialogue).toStrictEqual({ buttonLabel: `${namespace}:option:2:label`, @@ -194,7 +196,7 @@ describe("The Strong Stuff - Mystery Encounter", () => { const phaseSpy = vi.spyOn(scene, "pushPhase"); await game.runToMysteryEncounter(MysteryEncounterType.THE_STRONG_STUFF, defaultParty); - await runSelectMysteryEncounterOption(game, 2, true); + await runMysteryEncounterToEnd(game, 2, true); const enemyField = scene.getEnemyField(); expect(scene.getCurrentPhase().constructor.name).toBe(CommandPhase.name); @@ -218,7 +220,7 @@ describe("The Strong Stuff - Mystery Encounter", () => { it("should have Soul Dew in rewards", async () => { await game.runToMysteryEncounter(MysteryEncounterType.THE_STRONG_STUFF, defaultParty); - await runSelectMysteryEncounterOption(game, 2, true); + await runMysteryEncounterToEnd(game, 2, true); await skipBattleRunMysteryEncounterRewardsPhase(game); await game.phaseInterceptor.to(SelectModifierPhase, false); expect(scene.getCurrentPhase().constructor.name).toBe(SelectModifierPhase.name); diff --git a/src/test/phases/mystery-encounter-phase.test.ts b/src/test/phases/mystery-encounter-phase.test.ts index 5c7c9958c80..74ed7ff190d 100644 --- a/src/test/phases/mystery-encounter-phase.test.ts +++ b/src/test/phases/mystery-encounter-phase.test.ts @@ -7,8 +7,8 @@ import {Mode} from "#app/ui/ui"; import {Button} from "#enums/buttons"; import MysteryEncounterUiHandler from "#app/ui/mystery-encounter-ui-handler"; import {MysteryEncounterType} from "#enums/mystery-encounter-type"; -import {MysteryEncounterTier} from "#app/data/mystery-encounters/mystery-encounter"; import MessageUiHandler from "#app/ui/message-ui-handler"; +import { MysteryEncounterTier } from "#enums/mystery-encounter-tier"; describe("Mystery Encounter Phases", () => { let phaserGame: Phaser.Game; diff --git a/src/test/utils/overridesHelper.ts b/src/test/utils/overridesHelper.ts index d2011bf4581..400bad716a7 100644 --- a/src/test/utils/overridesHelper.ts +++ b/src/test/utils/overridesHelper.ts @@ -5,9 +5,9 @@ import { MockInstance, vi } from "vitest"; import GameManager from "#test/utils/gameManager"; import { MysteryEncounterType } from "#enums/mystery-encounter-type"; import * as overrides from "#app/overrides"; -import { MysteryEncounterTier } from "#app/data/mystery-encounters/mystery-encounter"; import * as GameMode from "#app/game-mode"; import { GameModes, getGameMode } from "#app/game-mode"; +import { MysteryEncounterTier } from "#enums/mystery-encounter-tier"; /** * Helper to handle overrides in tests diff --git a/src/test/utils/phaseInterceptor.ts b/src/test/utils/phaseInterceptor.ts index 7114d63afa0..4e1f2acf586 100644 --- a/src/test/utils/phaseInterceptor.ts +++ b/src/test/utils/phaseInterceptor.ts @@ -11,13 +11,15 @@ import { LearnMovePhase, LoginPhase, MessagePhase, + ModifierRewardPhase, MoveEffectPhase, MoveEndPhase, MovePhase, NewBattlePhase, NextEncounterPhase, PostSummonPhase, - SelectGenderPhase, SelectModifierPhase, + SelectGenderPhase, + SelectModifierPhase, SelectStarterPhase, SelectTargetPhase, ShinySparklePhase, @@ -105,6 +107,7 @@ export default class PhaseInterceptor { [MysteryEncounterRewardsPhase, this.startPhase], [PostMysteryEncounterPhase, this.startPhase], [LearnMovePhase, this.startPhase], + [ModifierRewardPhase, this.startPhase], // [CommonAnimPhase, this.startPhase] ]; diff --git a/src/ui/mystery-encounter-ui-handler.ts b/src/ui/mystery-encounter-ui-handler.ts index e6bcbfd697c..3d89afcd784 100644 --- a/src/ui/mystery-encounter-ui-handler.ts +++ b/src/ui/mystery-encounter-ui-handler.ts @@ -6,14 +6,15 @@ import { Button } from "#enums/buttons"; import { addWindow, WindowVariant } from "./ui-theme"; import { MysteryEncounterPhase } from "../phases/mystery-encounter-phases"; import { PartyUiMode } from "./party-ui-handler"; -import MysteryEncounterOption, { EncounterOptionMode } from "../data/mystery-encounters/mystery-encounter-option"; +import MysteryEncounterOption from "../data/mystery-encounters/mystery-encounter-option"; import * as Utils from "../utils"; import { isNullOrUndefined } from "../utils"; import { getPokeballAtlasKey } from "../data/pokeball"; import { OptionSelectSettings } from "#app/data/mystery-encounters/utils/encounter-phase-utils"; import { getEncounterText } from "#app/data/mystery-encounters/utils/encounter-dialogue-utils"; -import { MysteryEncounterTier } from "#app/data/mystery-encounters/mystery-encounter"; import i18next from "i18next"; +import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode"; +import { MysteryEncounterTier } from "#enums/mystery-encounter-tier"; export default class MysteryEncounterUiHandler extends UiHandler { private cursorContainer: Phaser.GameObjects.Container; @@ -146,7 +147,7 @@ export default class MysteryEncounterUiHandler extends UiHandler { this.unblockInput(); }, 300); }); - } else if (this.blockInput || (!this.optionsMeetsReqs[cursor] && (selected.optionMode === EncounterOptionMode.DISABLED_OR_DEFAULT || selected.optionMode === EncounterOptionMode.DISABLED_OR_SPECIAL))) { + } else if (this.blockInput || (!this.optionsMeetsReqs[cursor] && (selected.optionMode === MysteryEncounterOptionMode.DISABLED_OR_DEFAULT || selected.optionMode === MysteryEncounterOptionMode.DISABLED_OR_SPECIAL))) { success = false; } else { if ((this.scene.getCurrentPhase() as MysteryEncounterPhase).handleOptionSelect(selected, cursor)) { @@ -290,7 +291,7 @@ export default class MysteryEncounterUiHandler extends UiHandler { this.blockInput = false; for (let i = 0; i < this.optionsContainer.length - 1; i++) { const optionMode = this.encounterOptions[i].optionMode; - if (!this.optionsMeetsReqs[i] && (optionMode === EncounterOptionMode.DISABLED_OR_DEFAULT || optionMode === EncounterOptionMode.DISABLED_OR_SPECIAL)) { + if (!this.optionsMeetsReqs[i] && (optionMode === MysteryEncounterOptionMode.DISABLED_OR_DEFAULT || optionMode === MysteryEncounterOptionMode.DISABLED_OR_SPECIAL)) { continue; } (this.optionsContainer.getAt(i) as Phaser.GameObjects.Text).setAlpha(1); @@ -363,7 +364,7 @@ export default class MysteryEncounterUiHandler extends UiHandler { const optionDialogue = option.dialogue; const label = !this.optionsMeetsReqs[i] && optionDialogue.disabledButtonLabel ? optionDialogue.disabledButtonLabel : optionDialogue.buttonLabel; let text: string; - if (option.hasRequirements() && this.optionsMeetsReqs[i] && (option.optionMode === EncounterOptionMode.DEFAULT_OR_SPECIAL || option.optionMode === EncounterOptionMode.DISABLED_OR_SPECIAL)) { + if (option.hasRequirements() && this.optionsMeetsReqs[i] && (option.optionMode === MysteryEncounterOptionMode.DEFAULT_OR_SPECIAL || option.optionMode === MysteryEncounterOptionMode.DISABLED_OR_SPECIAL)) { // Options with special requirements that are met are automatically colored green text = getEncounterText(this.scene, label, TextStyle.SUMMARY_GREEN); } else { @@ -374,7 +375,7 @@ export default class MysteryEncounterUiHandler extends UiHandler { optionText.setText(text); } - if (!this.optionsMeetsReqs[i] && (option.optionMode === EncounterOptionMode.DISABLED_OR_DEFAULT || option.optionMode === EncounterOptionMode.DISABLED_OR_SPECIAL)) { + if (!this.optionsMeetsReqs[i] && (option.optionMode === MysteryEncounterOptionMode.DISABLED_OR_DEFAULT || option.optionMode === MysteryEncounterOptionMode.DISABLED_OR_SPECIAL)) { optionText.setAlpha(0.5); } if (this.blockInput) { @@ -468,7 +469,7 @@ export default class MysteryEncounterUiHandler extends UiHandler { let text: string; 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) { + if (!this.optionsMeetsReqs[cursor] && (cursorOption.optionMode === MysteryEncounterOptionMode.DISABLED_OR_DEFAULT || cursorOption.optionMode === MysteryEncounterOptionMode.DISABLED_OR_SPECIAL) && optionDialogue.disabledButtonTooltip) { text = getEncounterText(this.scene, optionDialogue.disabledButtonTooltip, TextStyle.TOOLTIP_CONTENT); } else { text = getEncounterText(this.scene, optionDialogue.buttonTooltip, TextStyle.TOOLTIP_CONTENT);