From 8685ec3c3c12113a409bf834b88af0034f6a76a5 Mon Sep 17 00:00:00 2001 From: AJ Fontaine <36677462+Fontbane@users.noreply.github.com> Date: Fri, 24 Jan 2025 17:59:24 -0500 Subject: [PATCH] [Misc] Rework some aspects of timed events (#5099) * Refactor timed event changes * Use getWeather function * Add mystery encounter tier change/disabling to timed events * Event luck boost, event encounter helper function * Events without shiny boost shouldn't give shiny charm * globalScene -> this in battle scene class * Change event pools --- src/battle-scene.ts | 5 +- .../encounters/berries-abound-encounter.ts | 20 +- .../encounters/delibirdy-encounter.ts | 2 +- .../encounters/fight-or-flight-encounter.ts | 20 +- .../encounters/uncommon-breed-encounter.ts | 23 +- .../utils/encounter-phase-utils.ts | 42 ++- src/data/weather.ts | 4 +- src/field/pokemon.ts | 43 ++- src/modifier/modifier-type.ts | 5 +- src/phases/trainer-victory-phase.ts | 8 +- src/test/utils/mocks/mockTimedEventManager.ts | 5 +- src/timed-event-manager.ts | 247 +++++++++++++++--- 12 files changed, 319 insertions(+), 105 deletions(-) diff --git a/src/battle-scene.ts b/src/battle-scene.ts index 39c09a31ceb..f0aeb68e277 100644 --- a/src/battle-scene.ts +++ b/src/battle-scene.ts @@ -3390,7 +3390,8 @@ export default class BattleScene extends SceneBase { const previousEncounter = this.mysteryEncounterSaveData.encounteredEvents.length > 0 ? this.mysteryEncounterSaveData.encounteredEvents[this.mysteryEncounterSaveData.encounteredEvents.length - 1].type : null; - const biomeMysteryEncounters = mysteryEncountersByBiome.get(this.arena.biomeType) ?? []; + const disabledEncounters = this.eventManager.getEventMysteryEncountersDisabled(); + const biomeMysteryEncounters = mysteryEncountersByBiome.get(this.arena.biomeType)?.filter(enc => !disabledEncounters.includes(enc)) ?? []; // If no valid encounters exist at tier, checks next tier down, continuing until there are some encounters available while (availableEncounters.length === 0 && tier !== null) { availableEncounters = biomeMysteryEncounters @@ -3399,7 +3400,7 @@ export default class BattleScene extends SceneBase { if (!encounterCandidate) { return false; } - if (encounterCandidate.encounterTier !== tier) { + if (this.eventManager.getMysteryEncounterTierForEvent(encounterType, encounterCandidate.encounterTier) !== tier) { return false; } const disallowedGameModes = encounterCandidate.disallowedGameModes; diff --git a/src/data/mystery-encounters/encounters/berries-abound-encounter.ts b/src/data/mystery-encounters/encounters/berries-abound-encounter.ts index 20179f4c618..550d9bd4f13 100644 --- a/src/data/mystery-encounters/encounters/berries-abound-encounter.ts +++ b/src/data/mystery-encounters/encounters/berries-abound-encounter.ts @@ -4,6 +4,7 @@ import type { import { generateModifierType, generateModifierTypeOption, + getRandomEncounterSpecies, initBattleWithEnemyConfig, leaveEncounterWithoutBattle, setEncounterExp, @@ -11,17 +12,15 @@ import { } from "#app/data/mystery-encounters/utils/encounter-phase-utils"; import type { PlayerPokemon } from "#app/field/pokemon"; import type Pokemon from "#app/field/pokemon"; -import { EnemyPokemon } from "#app/field/pokemon"; import type { BerryModifierType, ModifierTypeOption } from "#app/modifier/modifier-type"; import { - getPartyLuckValue, ModifierPoolType, modifierTypes, regenerateModifierPoolThresholds, } from "#app/modifier/modifier-type"; -import { randSeedInt, randSeedItem } from "#app/utils"; +import { randSeedInt } from "#app/utils"; import { BattlerTagType } from "#enums/battler-tag-type"; import { MysteryEncounterType } from "#enums/mystery-encounter-type"; import { globalScene } from "#app/global-scene"; @@ -31,7 +30,6 @@ import { queueEncounterMessage, showEncounterText } from "#app/data/mystery-enco import { getPokemonNameWithAffix } from "#app/messages"; import { MysteryEncounterTier } from "#enums/mystery-encounter-tier"; import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode"; -import { TrainerSlot } from "#app/data/trainer-config"; import { applyModifierTypeToPlayerPokemon, getEncounterPokemonLevelForWave, getHighestStatPlayerPokemon, getSpriteKeysFromPokemon, STANDARD_ENCOUNTER_BOOSTED_LEVEL_MODIFIER } from "#app/data/mystery-encounters/utils/encounter-pokemon-utils"; import PokemonData from "#app/system/pokemon-data"; import { BerryModifier } from "#app/modifier/modifier"; @@ -40,8 +38,6 @@ import { BerryType } from "#enums/berry-type"; import { PERMANENT_STATS, Stat } from "#enums/stat"; import { StatStageChangePhase } from "#app/phases/stat-stage-change-phase"; import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/game-mode"; -import type PokemonSpecies from "#app/data/pokemon-species"; -import { getPokemonSpecies } from "#app/data/pokemon-species"; /** the i18n namespace for the encounter */ const namespace = "mysteryEncounters/berriesAbound"; @@ -69,20 +65,12 @@ export const BerriesAboundEncounter: MysteryEncounter = // Calculate boss mon const level = getEncounterPokemonLevelForWave(STANDARD_ENCOUNTER_BOOSTED_LEVEL_MODIFIER); - let bossSpecies: PokemonSpecies; - if (globalScene.eventManager.isEventActive() && globalScene.eventManager.activeEvent()?.uncommonBreedEncounters && randSeedInt(2) === 1) { - const eventEncounter = randSeedItem(globalScene.eventManager.activeEvent()!.uncommonBreedEncounters!); - const levelSpecies = getPokemonSpecies(eventEncounter.species).getWildSpeciesForLevel(level, eventEncounter.allowEvolution ?? false, true, globalScene.gameMode); - bossSpecies = getPokemonSpecies( levelSpecies ); - } else { - bossSpecies = globalScene.arena.randomSpecies(globalScene.currentBattle.waveIndex, level, 0, getPartyLuckValue(globalScene.getPlayerParty()), true); - } - const bossPokemon = new EnemyPokemon(bossSpecies, level, TrainerSlot.NONE, true); + const bossPokemon = getRandomEncounterSpecies(level, true); encounter.setDialogueToken("enemyPokemon", getPokemonNameWithAffix(bossPokemon)); const config: EnemyPartyConfig = { pokemonConfigs: [{ level: level, - species: bossSpecies, + species: bossPokemon.species, dataSource: new PokemonData(bossPokemon), isBoss: true }], diff --git a/src/data/mystery-encounters/encounters/delibirdy-encounter.ts b/src/data/mystery-encounters/encounters/delibirdy-encounter.ts index d710dffab8c..f382f130540 100644 --- a/src/data/mystery-encounters/encounters/delibirdy-encounter.ts +++ b/src/data/mystery-encounters/encounters/delibirdy-encounter.ts @@ -41,7 +41,7 @@ const OPTION_3_DISALLOWED_MODIFIERS = [ const DELIBIRDY_MONEY_PRICE_MULTIPLIER = 2; const doEventReward = () => { - const event_buff = globalScene.eventManager.activeEvent()?.delibirdyBuff ?? []; + const event_buff = globalScene.eventManager.getDelibirdyBuff(); if (event_buff.length > 0) { const candidates = event_buff.filter((c => { const mtype = generateModifierType(modifierTypes[c]); 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 fc6f163010b..1667a15e7c9 100644 --- a/src/data/mystery-encounters/encounters/fight-or-flight-encounter.ts +++ b/src/data/mystery-encounters/encounters/fight-or-flight-encounter.ts @@ -2,6 +2,7 @@ import { MysteryEncounterOptionBuilder } from "#app/data/mystery-encounters/myst import type { EnemyPartyConfig } from "#app/data/mystery-encounters/utils/encounter-phase-utils"; import { + getRandomEncounterSpecies, initBattleWithEnemyConfig, leaveEncounterWithoutBattle, setEncounterExp, @@ -9,12 +10,10 @@ import { } from "#app/data/mystery-encounters/utils/encounter-phase-utils"; import { STEALING_MOVES } from "#app/data/mystery-encounters/requirements/requirement-groups"; import type Pokemon from "#app/field/pokemon"; -import { EnemyPokemon } from "#app/field/pokemon"; import { ModifierTier } from "#app/modifier/modifier-tier"; import type { ModifierTypeOption } from "#app/modifier/modifier-type"; import { - getPartyLuckValue, getPlayerModifierTypeOptions, ModifierPoolType, regenerateModifierPoolThresholds, @@ -26,16 +25,13 @@ import { MysteryEncounterBuilder } from "#app/data/mystery-encounters/mystery-en import { MoveRequirement } from "#app/data/mystery-encounters/mystery-encounter-requirements"; import { MysteryEncounterTier } from "#enums/mystery-encounter-tier"; import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode"; -import { TrainerSlot } from "#app/data/trainer-config"; import { getEncounterPokemonLevelForWave, getSpriteKeysFromPokemon, STANDARD_ENCOUNTER_BOOSTED_LEVEL_MODIFIER } from "#app/data/mystery-encounters/utils/encounter-pokemon-utils"; import PokemonData from "#app/system/pokemon-data"; import { BattlerTagType } from "#enums/battler-tag-type"; import { queueEncounterMessage } from "#app/data/mystery-encounters/utils/encounter-dialogue-utils"; -import { randSeedInt, randSeedItem } from "#app/utils"; +import { randSeedInt } from "#app/utils"; import { StatStageChangePhase } from "#app/phases/stat-stage-change-phase"; import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/game-mode"; -import type PokemonSpecies from "#app/data/pokemon-species"; -import { getPokemonSpecies } from "#app/data/pokemon-species"; /** the i18n namespace for the encounter */ const namespace = "mysteryEncounters/fightOrFlight"; @@ -63,20 +59,12 @@ export const FightOrFlightEncounter: MysteryEncounter = // Calculate boss mon const level = getEncounterPokemonLevelForWave(STANDARD_ENCOUNTER_BOOSTED_LEVEL_MODIFIER); - let bossSpecies: PokemonSpecies; - if (globalScene.eventManager.isEventActive() && globalScene.eventManager.activeEvent()?.uncommonBreedEncounters && randSeedInt(2) === 1) { - const eventEncounter = randSeedItem(globalScene.eventManager.activeEvent()!.uncommonBreedEncounters!); - const levelSpecies = getPokemonSpecies(eventEncounter.species).getWildSpeciesForLevel(level, eventEncounter.allowEvolution ?? false, true, globalScene.gameMode); - bossSpecies = getPokemonSpecies( levelSpecies ); - } else { - bossSpecies = globalScene.arena.randomSpecies(globalScene.currentBattle.waveIndex, level, 0, getPartyLuckValue(globalScene.getPlayerParty()), true); - } - const bossPokemon = new EnemyPokemon(bossSpecies, level, TrainerSlot.NONE, true); + const bossPokemon = getRandomEncounterSpecies(level, true); encounter.setDialogueToken("enemyPokemon", bossPokemon.getNameToRender()); const config: EnemyPartyConfig = { pokemonConfigs: [{ level: level, - species: bossSpecies, + species: bossPokemon.species, dataSource: new PokemonData(bossPokemon), isBoss: true, tags: [ BattlerTagType.MYSTERY_ENCOUNTER_POST_SUMMON ], diff --git a/src/data/mystery-encounters/encounters/uncommon-breed-encounter.ts b/src/data/mystery-encounters/encounters/uncommon-breed-encounter.ts index 39297ab95b6..6e6381888f1 100644 --- a/src/data/mystery-encounters/encounters/uncommon-breed-encounter.ts +++ b/src/data/mystery-encounters/encounters/uncommon-breed-encounter.ts @@ -1,10 +1,10 @@ import { MysteryEncounterOptionBuilder } from "#app/data/mystery-encounters/mystery-encounter-option"; import type { EnemyPartyConfig } from "#app/data/mystery-encounters/utils/encounter-phase-utils"; -import { initBattleWithEnemyConfig, leaveEncounterWithoutBattle, setEncounterExp, setEncounterRewards } from "#app/data/mystery-encounters/utils/encounter-phase-utils"; +import { getRandomEncounterSpecies, initBattleWithEnemyConfig, leaveEncounterWithoutBattle, setEncounterExp, setEncounterRewards } from "#app/data/mystery-encounters/utils/encounter-phase-utils"; import { CHARMING_MOVES } from "#app/data/mystery-encounters/requirements/requirement-groups"; import type Pokemon from "#app/field/pokemon"; -import { EnemyPokemon, PokemonMove } from "#app/field/pokemon"; -import { getPartyLuckValue } from "#app/modifier/modifier-type"; +import type { EnemyPokemon } from "#app/field/pokemon"; +import { PokemonMove } from "#app/field/pokemon"; import { MysteryEncounterType } from "#enums/mystery-encounter-type"; import { globalScene } from "#app/global-scene"; import type MysteryEncounter from "#app/data/mystery-encounters/mystery-encounter"; @@ -12,10 +12,9 @@ import { MysteryEncounterBuilder } from "#app/data/mystery-encounters/mystery-en import { MoveRequirement, PersistentModifierRequirement } from "#app/data/mystery-encounters/mystery-encounter-requirements"; import { MysteryEncounterTier } from "#enums/mystery-encounter-tier"; import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode"; -import { TrainerSlot } from "#app/data/trainer-config"; import { catchPokemon, getHighestLevelPlayerPokemon, getSpriteKeysFromPokemon } from "#app/data/mystery-encounters/utils/encounter-pokemon-utils"; import PokemonData from "#app/system/pokemon-data"; -import { isNullOrUndefined, randSeedInt, randSeedItem } from "#app/utils"; +import { isNullOrUndefined, randSeedInt } from "#app/utils"; import type { Moves } from "#enums/moves"; import { BattlerIndex } from "#app/battle"; import { SelfStatusMove } from "#app/data/move"; @@ -26,8 +25,6 @@ import { BerryModifier } from "#app/modifier/modifier"; import { StatStageChangePhase } from "#app/phases/stat-stage-change-phase"; import { Stat } from "#enums/stat"; import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/game-mode"; -import type PokemonSpecies from "#app/data/pokemon-species"; -import { getPokemonSpecies } from "#app/data/pokemon-species"; /** the i18n namespace for the encounter */ const namespace = "mysteryEncounters/uncommonBreed"; @@ -56,15 +53,7 @@ export const UncommonBreedEncounter: MysteryEncounter = // Calculate boss mon // Level equal to 2 below highest party member const level = getHighestLevelPlayerPokemon(false, true).level - 2; - let species: PokemonSpecies; - if (globalScene.eventManager.isEventActive() && globalScene.eventManager.activeEvent()?.uncommonBreedEncounters && randSeedInt(2) === 1) { - const eventEncounter = randSeedItem(globalScene.eventManager.activeEvent()!.uncommonBreedEncounters!); - const levelSpecies = getPokemonSpecies(eventEncounter.species).getWildSpeciesForLevel(level, eventEncounter.allowEvolution ?? false, true, globalScene.gameMode); - species = getPokemonSpecies( levelSpecies ); - } else { - species = globalScene.arena.randomSpecies(globalScene.currentBattle.waveIndex, level, 0, getPartyLuckValue(globalScene.getPlayerParty()), true); - } - const pokemon = new EnemyPokemon(species, level, TrainerSlot.NONE, true); + const pokemon = getRandomEncounterSpecies(level, true, true); // Pokemon will always have one of its egg moves in its moveset const eggMoves = pokemon.getEggMoves(); @@ -92,7 +81,7 @@ export const UncommonBreedEncounter: MysteryEncounter = const config: EnemyPartyConfig = { pokemonConfigs: [{ level: level, - species: species, + species: pokemon.species, dataSource: new PokemonData(pokemon), isBoss: false, tags: [ BattlerTagType.MYSTERY_ENCOUNTER_POST_SUMMON ], diff --git a/src/data/mystery-encounters/utils/encounter-phase-utils.ts b/src/data/mystery-encounters/utils/encounter-phase-utils.ts index 8768fb06b37..a144b1ac5c5 100644 --- a/src/data/mystery-encounters/utils/encounter-phase-utils.ts +++ b/src/data/mystery-encounters/utils/encounter-phase-utils.ts @@ -6,9 +6,9 @@ import { AVERAGE_ENCOUNTERS_PER_RUN_TARGET, WEIGHT_INCREMENT_ON_SPAWN_MISS } fro import { showEncounterText } from "#app/data/mystery-encounters/utils/encounter-dialogue-utils"; import type { AiType, PlayerPokemon } from "#app/field/pokemon"; import type Pokemon from "#app/field/pokemon"; -import { FieldPosition, PokemonMove, PokemonSummonData } from "#app/field/pokemon"; +import { EnemyPokemon, FieldPosition, PokemonMove, PokemonSummonData } from "#app/field/pokemon"; import type { CustomModifierSettings, ModifierType } from "#app/modifier/modifier-type"; -import { ModifierPoolType, ModifierTypeGenerator, ModifierTypeOption, modifierTypes, regenerateModifierPoolThresholds } from "#app/modifier/modifier-type"; +import { getPartyLuckValue, ModifierPoolType, ModifierTypeGenerator, ModifierTypeOption, modifierTypes, regenerateModifierPoolThresholds } from "#app/modifier/modifier-type"; import { MysteryEncounterBattlePhase, MysteryEncounterBattleStartCleanupPhase, MysteryEncounterPhase, MysteryEncounterRewardsPhase } from "#app/phases/mystery-encounter-phases"; import type PokemonData from "#app/system/pokemon-data"; import type { OptionSelectConfig, OptionSelectItem } from "#app/ui/abstact-option-select-ui-handler"; @@ -16,7 +16,7 @@ import type { PartyOption, PokemonSelectFilter } from "#app/ui/party-ui-handler" import { PartyUiMode } from "#app/ui/party-ui-handler"; import { Mode } from "#app/ui/ui"; import * as Utils from "#app/utils"; -import { isNullOrUndefined } from "#app/utils"; +import { isNullOrUndefined, randSeedInt, randSeedItem } from "#app/utils"; import type { BattlerTagType } from "#enums/battler-tag-type"; import { Biome } from "#enums/biome"; import type { TrainerType } from "#enums/trainer-type"; @@ -45,6 +45,7 @@ import { PartyExpPhase } from "#app/phases/party-exp-phase"; import type { Variant } from "#app/data/variant"; import { StatusEffect } from "#enums/status-effect"; import { globalScene } from "#app/global-scene"; +import { getPokemonSpecies } from "#app/data/pokemon-species"; /** * Animates exclamation sprite over trainer's head at start of encounter @@ -874,6 +875,41 @@ export function handleMysteryEncounterTurnStartEffects(): boolean { return false; } +/** + * Helper function for encounters such as {@linkcode UncommonBreedEncounter} which call for a random species including event encounters. + * If the mon is from the event encounter list, it will do an extra shiny roll. + * @param level the level of the mon, which differs between MEs + * @param isBoss whether the mon should be a Boss + * @param rerollHidden whether the mon should get an extra roll for Hidden Ability + * @returns {@linkcode EnemyPokemon} for the requested encounter + */ +export function getRandomEncounterSpecies(level: number, isBoss: boolean = false, rerollHidden: boolean = false): EnemyPokemon { + let bossSpecies: PokemonSpecies; + let isEventEncounter = false; + const eventEncounters = globalScene.eventManager.getEventEncounters(); + + if (eventEncounters.length > 0 && randSeedInt(2) === 1) { + const eventEncounter = randSeedItem(eventEncounters); + const levelSpecies = getPokemonSpecies(eventEncounter.species).getWildSpeciesForLevel(level, !isNullOrUndefined(eventEncounter.blockEvolution), isBoss, globalScene.gameMode); + isEventEncounter = true; + bossSpecies = getPokemonSpecies(levelSpecies); + } else { + bossSpecies = globalScene.arena.randomSpecies(globalScene.currentBattle.waveIndex, level, 0, getPartyLuckValue(globalScene.getPlayerParty()), isBoss); + } + const ret = new EnemyPokemon(bossSpecies, level, TrainerSlot.NONE, isBoss); + + //Reroll shiny for event encounters + if (isEventEncounter && !ret.shiny) { + ret.trySetShinySeed(); + } + //Reroll hidden ability + if (rerollHidden && ret.abilityIndex !== 2 && ret.species.abilityHidden) { + ret.tryRerollHiddenAbilitySeed(); + } + + return ret; +} + /** * TODO: remove once encounter spawn rate is finalized * Just a helper function to calculate aggregate stats for MEs in a Classic run diff --git a/src/data/weather.ts b/src/data/weather.ts index d971f726543..7bba698f9b4 100644 --- a/src/data/weather.ts +++ b/src/data/weather.ts @@ -375,8 +375,8 @@ export function getRandomWeatherType(arena: Arena): WeatherType { break; } - if (arena.biomeType === Biome.TOWN && globalScene.eventManager.isEventActive() && (globalScene.eventManager.activeEvent()?.weather?.length ?? 0) > 0) { - globalScene.eventManager.activeEvent()?.weather?.map(w => weatherPool.push(w)); + if (arena.biomeType === Biome.TOWN && globalScene.eventManager.isEventActive()) { + globalScene.eventManager.getWeather()?.map(w => weatherPool.push(w)); } if (weatherPool.length > 1) { diff --git a/src/field/pokemon.ts b/src/field/pokemon.ts index a9c270ec09e..731d5c8fbe7 100644 --- a/src/field/pokemon.ts +++ b/src/field/pokemon.ts @@ -10,7 +10,7 @@ import type Move from "#app/data/move"; import { HighCritAttr, HitsTagAttr, applyMoveAttrs, FixedDamageAttr, VariableAtkAttr, allMoves, MoveCategory, TypelessAttr, CritOnlyAttr, getMoveTargets, OneHitKOAttr, VariableMoveTypeAttr, VariableDefAttr, AttackMove, ModifiedDamageAttr, VariableMoveTypeMultiplierAttr, IgnoreOpponentStatStagesAttr, SacrificialAttr, VariableMoveCategoryAttr, CounterDamageAttr, StatStageChangeAttr, RechargeAttr, IgnoreWeatherTypeDebuffAttr, BypassBurnDamageReductionAttr, SacrificialAttrOnHit, OneHitKOAccuracyAttr, RespectAttackTypeImmunityAttr, MoveTarget, CombinedPledgeStabBoostAttr, VariableMoveTypeChartAttr } from "#app/data/move"; import type { PokemonSpeciesForm } from "#app/data/pokemon-species"; import { default as PokemonSpecies, getFusedSpeciesName, getPokemonSpecies, getPokemonSpeciesForm } from "#app/data/pokemon-species"; -import { CLASSIC_CANDY_FRIENDSHIP_MULTIPLIER, getStarterValueFriendshipCap, speciesStarterCosts } from "#app/data/balance/starters"; +import { getStarterValueFriendshipCap, speciesStarterCosts } from "#app/data/balance/starters"; import { starterPassiveAbilities } from "#app/data/balance/passives"; import type { Constructor } from "#app/utils"; import { isNullOrUndefined, randSeedInt, type nil } from "#app/utils"; @@ -1955,7 +1955,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { * @param thresholdOverride number that is divided by 2^16 (65536) to get the shiny chance, overrides {@linkcode shinyThreshold} if set (bypassing shiny rate modifiers such as Shiny Charm) * @returns true if the Pokemon has been set as a shiny, false otherwise */ - trySetShiny(thresholdOverride?: integer): boolean { + trySetShiny(thresholdOverride?: number): boolean { // Shiny Pokemon should not spawn in the end biome in endless if (globalScene.gameMode.isEndless && globalScene.arena.biomeType === Biome.END) { return false; @@ -1967,7 +1967,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { const E = globalScene.gameData.trainerId ^ globalScene.gameData.secretId; const F = rand1 ^ rand2; - const shinyThreshold = new Utils.IntegerHolder(BASE_SHINY_CHANCE); + const shinyThreshold = new Utils.NumberHolder(BASE_SHINY_CHANCE); if (thresholdOverride === undefined) { if (globalScene.eventManager.isEventActive()) { shinyThreshold.value *= globalScene.eventManager.getShinyMultiplier(); @@ -2058,6 +2058,38 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { } } + /** + * Function that tries to set a Pokemon to have its hidden ability based on seed, if it exists. + * For manual use only, usually to roll a Pokemon's hidden ability chance a second time. + * + * The base hidden ability odds are {@linkcode BASE_HIDDEN_ABILITY_CHANCE} / `65536` + * @param thresholdOverride number that is divided by `2^16` (`65536`) to get the HA chance, overrides {@linkcode haThreshold} if set (bypassing HA rate modifiers such as Ability Charm) + * @param applyModifiersToOverride If {@linkcode thresholdOverride} is set and this is true, will apply Ability Charm to {@linkcode thresholdOverride} + * @returns `true` if the Pokemon has been set to have its hidden ability, `false` otherwise + */ + public tryRerollHiddenAbilitySeed(thresholdOverride?: number, applyModifiersToOverride?: boolean): boolean { + if (!this.species.abilityHidden) { + return false; + } + const haThreshold = new Utils.NumberHolder(BASE_HIDDEN_ABILITY_CHANCE); + if (thresholdOverride === undefined || applyModifiersToOverride) { + if (thresholdOverride !== undefined && applyModifiersToOverride) { + haThreshold.value = thresholdOverride; + } + if (!this.hasTrainer()) { + globalScene.applyModifiers(HiddenAbilityRateBoosterModifier, true, haThreshold); + } + } else { + haThreshold.value = thresholdOverride; + } + + if (randSeedInt(65536) < haThreshold.value) { + this.abilityIndex = 2; + } + + return this.abilityIndex === 2; + } + public generateFusionSpecies(forStarter?: boolean): void { const hiddenAbilityChance = new Utils.NumberHolder(BASE_HIDDEN_ABILITY_CHANCE); if (!this.hasTrainer()) { @@ -4328,10 +4360,7 @@ export class PlayerPokemon extends Pokemon { ].filter(d => !!d); const amount = new Utils.NumberHolder(friendship); globalScene.applyModifier(PokemonFriendshipBoosterModifier, true, this, amount); - let candyFriendshipMultiplier = CLASSIC_CANDY_FRIENDSHIP_MULTIPLIER; - if (globalScene.eventManager.isEventActive()) { - candyFriendshipMultiplier *= globalScene.eventManager.getFriendshipMultiplier(); - } + const candyFriendshipMultiplier = globalScene.eventManager.getClassicFriendshipMultiplier(); const starterAmount = new Utils.NumberHolder(Math.floor(amount.value * (globalScene.gameMode.isClassic ? candyFriendshipMultiplier : 1) / (fusionStarterSpeciesId ? 2 : 1))); // Add friendship to this PlayerPokemon diff --git a/src/modifier/modifier-type.ts b/src/modifier/modifier-type.ts index 9c844596abc..f531e96d641 100644 --- a/src/modifier/modifier-type.ts +++ b/src/modifier/modifier-type.ts @@ -2537,9 +2537,10 @@ export function getPartyLuckValue(party: Pokemon[]): integer { }, 0, globalScene.seed); return DailyLuck.value; } - const luck = Phaser.Math.Clamp(party.map(p => p.isAllowedInBattle() ? p.getLuck() : 0) + const eventSpecies = globalScene.eventManager.getEventLuckBoostedSpecies(); + const luck = Phaser.Math.Clamp(party.map(p => p.isAllowedInBattle() ? p.getLuck() + (eventSpecies.includes(p.species.speciesId) ? 1 : 0) : 0) .reduce((total: integer, value: integer) => total += value, 0), 0, 14); - return luck ?? 0; + return Math.min(globalScene.eventManager.getEventLuckBoost() + (luck ?? 0), 14); } export function getLuckString(luckValue: integer): string { diff --git a/src/phases/trainer-victory-phase.ts b/src/phases/trainer-victory-phase.ts index 3fa7209751a..9722e9c4197 100644 --- a/src/phases/trainer-victory-phase.ts +++ b/src/phases/trainer-victory-phase.ts @@ -29,7 +29,7 @@ export class TrainerVictoryPhase extends BattlePhase { globalScene.unshiftPhase(new ModifierRewardPhase(modifierRewardFunc)); } - if (globalScene.eventManager.isEventActive()) { + if (globalScene.eventManager.getShinyMultiplier() > 1) { //If a shiny boosting event is active for (const rewardFunc of globalScene.currentBattle.trainer?.config.eventRewardFuncs!) { globalScene.unshiftPhase(new ModifierRewardPhase(rewardFunc)); } @@ -39,7 +39,11 @@ export class TrainerVictoryPhase extends BattlePhase { // Validate Voucher for boss trainers if (vouchers.hasOwnProperty(TrainerType[trainerType])) { if (!globalScene.validateVoucher(vouchers[TrainerType[trainerType]]) && globalScene.currentBattle.trainer?.config.isBoss) { - globalScene.unshiftPhase(new ModifierRewardPhase([ modifierTypes.VOUCHER, modifierTypes.VOUCHER, modifierTypes.VOUCHER_PLUS, modifierTypes.VOUCHER_PREMIUM ][vouchers[TrainerType[trainerType]].voucherType])); + if (globalScene.eventManager.getUpgradeUnlockedVouchers()) { + globalScene.unshiftPhase(new ModifierRewardPhase([ modifierTypes.VOUCHER_PLUS, modifierTypes.VOUCHER_PLUS, modifierTypes.VOUCHER_PLUS, modifierTypes.VOUCHER_PREMIUM ][vouchers[TrainerType[trainerType]].voucherType])); + } else { + globalScene.unshiftPhase(new ModifierRewardPhase([ modifierTypes.VOUCHER, modifierTypes.VOUCHER, modifierTypes.VOUCHER_PLUS, modifierTypes.VOUCHER_PREMIUM ][vouchers[TrainerType[trainerType]].voucherType])); + } } } // Breeders in Space achievement diff --git a/src/test/utils/mocks/mockTimedEventManager.ts b/src/test/utils/mocks/mockTimedEventManager.ts index b44729996a7..10f32fd4c8b 100644 --- a/src/test/utils/mocks/mockTimedEventManager.ts +++ b/src/test/utils/mocks/mockTimedEventManager.ts @@ -1,3 +1,4 @@ +import { CLASSIC_CANDY_FRIENDSHIP_MULTIPLIER } from "#app/data/balance/starters"; import { TimedEventManager } from "#app/timed-event-manager"; /** Mock TimedEventManager so that ongoing events don't impact tests */ @@ -8,8 +9,8 @@ export class MockTimedEventManager extends TimedEventManager { override isEventActive(): boolean { return false; } - override getFriendshipMultiplier(): number { - return 1; + override getClassicFriendshipMultiplier(): number { + return CLASSIC_CANDY_FRIENDSHIP_MULTIPLIER; } override getShinyMultiplier(): number { return 1; diff --git a/src/timed-event-manager.ts b/src/timed-event-manager.ts index 216900e8f58..8633460d98c 100644 --- a/src/timed-event-manager.ts +++ b/src/timed-event-manager.ts @@ -1,14 +1,19 @@ import { globalScene } from "#app/global-scene"; import { TextStyle, addTextObject } from "#app/ui/text"; import type { nil } from "#app/utils"; +import { isNullOrUndefined } from "#app/utils"; import i18next from "i18next"; import { Species } from "#enums/species"; import type { WeatherPoolEntry } from "#app/data/weather"; import { WeatherType } from "#enums/weather-type"; +import { CLASSIC_CANDY_FRIENDSHIP_MULTIPLIER } from "./data/balance/starters"; +import { MysteryEncounterType } from "./enums/mystery-encounter-type"; +import { MysteryEncounterTier } from "./enums/mystery-encounter-tier"; export enum EventType { SHINY, - NO_TIMER_DISPLAY + NO_TIMER_DISPLAY, + LUCK } interface EventBanner { @@ -21,19 +26,29 @@ interface EventBanner { interface EventEncounter { species: Species; - allowEvolution?: boolean; + blockEvolution?: boolean; +} + +interface EventMysteryEncounterTier { + mysteryEncounter: MysteryEncounterType; + tier?: MysteryEncounterTier; + disable?: boolean; } interface TimedEvent extends EventBanner { name: string; eventType: EventType; shinyMultiplier?: number; - friendshipMultiplier?: number; + classicFriendshipMultiplier?: number; + luckBoost?: number; + upgradeUnlockedVouchers?: boolean; startDate: Date; endDate: Date; - uncommonBreedEncounters?: EventEncounter[]; + eventEncounters?: EventEncounter[]; delibirdyBuff?: string[]; weather?: WeatherPoolEntry[]; + mysteryEncounterTierChanges?: EventMysteryEncounterTier[]; + luckBoostedSpecies?: Species[]; } const timedEvents: TimedEvent[] = [ @@ -41,36 +56,94 @@ const timedEvents: TimedEvent[] = [ name: "Winter Holiday Update", eventType: EventType.SHINY, shinyMultiplier: 2, - friendshipMultiplier: 1, + upgradeUnlockedVouchers: true, startDate: new Date(Date.UTC(2024, 11, 21, 0)), endDate: new Date(Date.UTC(2025, 0, 4, 0)), bannerKey: "winter_holidays2024-event-", scale: 0.21, availableLangs: [ "en", "de", "it", "fr", "ja", "ko", "es-ES", "pt-BR", "zh-CN" ], - uncommonBreedEncounters: [ - { species: Species.GIMMIGHOUL }, + eventEncounters: [ + { species: Species.GIMMIGHOUL, blockEvolution: true }, { species: Species.DELIBIRD }, - { species: Species.STANTLER, allowEvolution: true }, - { species: Species.CYNDAQUIL, allowEvolution: true }, - { species: Species.PIPLUP, allowEvolution: true }, - { species: Species.CHESPIN, allowEvolution: true }, - { species: Species.BALTOY, allowEvolution: true }, - { species: Species.SNOVER, allowEvolution: true }, - { species: Species.CHINGLING, allowEvolution: true }, - { species: Species.LITWICK, allowEvolution: true }, - { species: Species.CUBCHOO, allowEvolution: true }, - { species: Species.SWIRLIX, allowEvolution: true }, - { species: Species.AMAURA, allowEvolution: true }, - { species: Species.MUDBRAY, allowEvolution: true }, - { species: Species.ROLYCOLY, allowEvolution: true }, - { species: Species.MILCERY, allowEvolution: true }, - { species: Species.SMOLIV, allowEvolution: true }, - { species: Species.ALOLA_VULPIX, allowEvolution: true }, - { species: Species.GALAR_DARUMAKA, allowEvolution: true }, + { species: Species.STANTLER }, + { species: Species.CYNDAQUIL }, + { species: Species.PIPLUP }, + { species: Species.CHESPIN }, + { species: Species.BALTOY }, + { species: Species.SNOVER }, + { species: Species.CHINGLING }, + { species: Species.LITWICK }, + { species: Species.CUBCHOO }, + { species: Species.SWIRLIX }, + { species: Species.AMAURA }, + { species: Species.MUDBRAY }, + { species: Species.ROLYCOLY }, + { species: Species.MILCERY }, + { species: Species.SMOLIV }, + { species: Species.ALOLA_VULPIX }, + { species: Species.GALAR_DARUMAKA }, { species: Species.IRON_BUNDLE } ], delibirdyBuff: [ "CATCHING_CHARM", "SHINY_CHARM", "ABILITY_CHARM", "EXP_CHARM", "SUPER_EXP_CHARM", "HEALING_CHARM" ], - weather: [{ weatherType: WeatherType.SNOW, weight: 1 }] + weather: [{ weatherType: WeatherType.SNOW, weight: 1 }], + mysteryEncounterTierChanges: [ + { mysteryEncounter: MysteryEncounterType.DELIBIRDY, tier: MysteryEncounterTier.COMMON }, + { mysteryEncounter: MysteryEncounterType.PART_TIMER, disable: true }, + { mysteryEncounter: MysteryEncounterType.AN_OFFER_YOU_CANT_REFUSE, disable: true }, + { mysteryEncounter: MysteryEncounterType.FIELD_TRIP, disable: true }, + { mysteryEncounter: MysteryEncounterType.DEPARTMENT_STORE_SALE, disable: true } + ] + }, + { + name: "Year of the Snake", + eventType: EventType.LUCK, + luckBoost: 1, + startDate: new Date(Date.UTC(2025, 0, 29, 0)), + endDate: new Date(Date.UTC(2025, 1, 3, 0)), + bannerKey: "yearofthesnakeevent-", + scale: 0.21, + availableLangs: [], + eventEncounters: [ + { species: Species.EKANS }, + { species: Species.ONIX }, + { species: Species.DRATINI }, + { species: Species.CLEFFA }, + { species: Species.UMBREON }, + { species: Species.DUNSPARCE }, + { species: Species.TEDDIURSA }, + { species: Species.SEVIPER }, + { species: Species.LUNATONE }, + { species: Species.CHINGLING }, + { species: Species.SNIVY }, + { species: Species.DARUMAKA }, + { species: Species.DRAMPA }, + { species: Species.SILICOBRA }, + { species: Species.BLOODMOON_URSALUNA } + ], + luckBoostedSpecies: [ + Species.EKANS, Species.ARBOK, + Species.ONIX, Species.STEELIX, + Species.DRATINI, Species.DRAGONAIR, Species.DRAGONITE, + Species.CLEFFA, Species.CLEFAIRY, Species.CLEFABLE, + Species.UMBREON, + Species.DUNSPARCE, Species.DUDUNSPARCE, + Species.TEDDIURSA, Species.URSARING, Species.URSALUNA, + Species.SEVIPER, + Species.LUNATONE, + Species.RAYQUAZA, + Species.CHINGLING, Species.CHIMECHO, + Species.CRESSELIA, + Species.DARKRAI, + Species.SNIVY, Species.SERVINE, Species.SERPERIOR, + Species.DARUMAKA, Species.DARMANITAN, + Species.ZYGARDE, + Species.DRAMPA, + Species.LUNALA, + Species.BLACEPHALON, + Species.SILICOBRA, Species.SANDACONDA, + Species.ROARING_MOON, + Species.BLOODMOON_URSALUNA + ] } ]; @@ -97,16 +170,6 @@ export class TimedEventManager { return activeEvents.length > 0; } - getFriendshipMultiplier(): number { - let multiplier = 1; - const friendshipEvents = timedEvents.filter((te) => this.isActive(te)); - friendshipEvents.forEach((fe) => { - multiplier *= fe.friendshipMultiplier ?? 1; - }); - - return multiplier; - } - getShinyMultiplier(): number { let multiplier = 1; const shinyEvents = timedEvents.filter((te) => te.eventType === EventType.SHINY && this.isActive(te)); @@ -120,6 +183,120 @@ export class TimedEventManager { getEventBannerFilename(): string { return timedEvents.find((te: TimedEvent) => this.isActive(te))?.bannerKey ?? ""; } + + getEventEncounters(): EventEncounter[] { + const ret: EventEncounter[] = []; + timedEvents.filter((te) => this.isActive(te)).map((te) => { + if (!isNullOrUndefined(te.eventEncounters)) { + ret.push(...te.eventEncounters); + } + }); + return ret; + } + + /** + * For events that change the classic candy friendship multiplier + * @returns The highest classic friendship multiplier among the active events, or the default CLASSIC_CANDY_FRIENDSHIP_MULTIPLIER + */ + getClassicFriendshipMultiplier(): number { + let multiplier = CLASSIC_CANDY_FRIENDSHIP_MULTIPLIER; + const classicFriendshipEvents = timedEvents.filter((te) => this.isActive(te)); + classicFriendshipEvents.forEach((fe) => { + if (!isNullOrUndefined(fe.classicFriendshipMultiplier) && fe.classicFriendshipMultiplier > multiplier) { + multiplier = fe.classicFriendshipMultiplier; + } + }); + return multiplier; + } + + /** + * For events where defeated bosses (Gym Leaders, E4 etc) give out Voucher Plus even if they were defeated before + * @returns Whether vouchers should be upgraded + */ + getUpgradeUnlockedVouchers(): boolean { + return timedEvents.some((te) => this.isActive(te) && (te.upgradeUnlockedVouchers ?? false)); + } + + /** + * For events where Delibirdy gives extra items + * @returns list of ids of {@linkcode ModifierType}s that Delibirdy hands out as a bonus + */ + getDelibirdyBuff(): string[] { + const ret: string[] = []; + timedEvents.filter((te) => this.isActive(te)).map((te) => { + if (!isNullOrUndefined(te.delibirdyBuff)) { + ret.push(...te.delibirdyBuff); + } + }); + return ret; + } + + /** + * For events where there's a set weather for town biome (other biomes are hard) + * @returns Event weathers for town + */ + getWeather(): WeatherPoolEntry[] { + const ret: WeatherPoolEntry[] = []; + timedEvents.filter((te) => this.isActive(te)).map((te) => { + if (!isNullOrUndefined(te.weather)) { + ret.push(...te.weather); + } + }); + return ret; + } + + getAllMysteryEncounterChanges(): EventMysteryEncounterTier[] { + const ret: EventMysteryEncounterTier[] = []; + timedEvents.filter((te) => this.isActive(te)).map((te) => { + if (!isNullOrUndefined(te.mysteryEncounterTierChanges)) { + ret.push(...te.mysteryEncounterTierChanges); + } + }); + return ret; + } + + getEventMysteryEncountersDisabled(): MysteryEncounterType[] { + const ret: MysteryEncounterType[] = []; + timedEvents.filter((te) => this.isActive(te) && !isNullOrUndefined(te.mysteryEncounterTierChanges)).map((te) => { + te.mysteryEncounterTierChanges?.map((metc) => { + if (metc.disable) { + ret.push(metc.mysteryEncounter); + } + }); + }); + return ret; + } + + getMysteryEncounterTierForEvent(encounterType: MysteryEncounterType, normal: MysteryEncounterTier): MysteryEncounterTier { + let ret = normal; + timedEvents.filter((te) => this.isActive(te) && !isNullOrUndefined(te.mysteryEncounterTierChanges)).map((te) => { + te.mysteryEncounterTierChanges?.map((metc) => { + if (metc.mysteryEncounter === encounterType) { + ret = metc.tier ?? normal; + } + }); + }); + return ret; + } + + getEventLuckBoost(): number { + let ret = 0; + const luckEvents = timedEvents.filter((te) => this.isActive(te) && !isNullOrUndefined(te.luckBoost)); + luckEvents.forEach((le) => { + ret += le.luckBoost!; + }); + return ret; + } + + getEventLuckBoostedSpecies(): Species[] { + const ret: Species[] = []; + timedEvents.filter((te) => this.isActive(te)).map((te) => { + if (!isNullOrUndefined(te.luckBoostedSpecies)) { + ret.push(...te.luckBoostedSpecies.filter(s => !ret.includes(s))); + } + }); + return ret; + } } export class TimedEventDisplay extends Phaser.GameObjects.Container {