From 497551311888691239042d172e80efeedf6d1c8d Mon Sep 17 00:00:00 2001 From: AJ Fontaine Date: Tue, 7 Jan 2025 19:05:07 -0500 Subject: [PATCH] Refactor timed event changes --- .../encounters/berries-abound-encounter.ts | 7 +- .../encounters/delibirdy-encounter.ts | 2 +- .../encounters/fight-or-flight-encounter.ts | 7 +- .../encounters/uncommon-breed-encounter.ts | 7 +- src/field/pokemon.ts | 7 +- src/phases/trainer-victory-phase.ts | 6 +- src/test/utils/mocks/mockTimedEventManager.ts | 5 +- src/timed-event-manager.ts | 118 +++++++++++++----- 8 files changed, 109 insertions(+), 50 deletions(-) diff --git a/src/data/mystery-encounters/encounters/berries-abound-encounter.ts b/src/data/mystery-encounters/encounters/berries-abound-encounter.ts index eca358e51f3..6ab9890533a 100644 --- a/src/data/mystery-encounters/encounters/berries-abound-encounter.ts +++ b/src/data/mystery-encounters/encounters/berries-abound-encounter.ts @@ -60,9 +60,10 @@ export const BerriesAboundEncounter: MysteryEncounter = // Calculate boss mon const level = getEncounterPokemonLevelForWave(scene, STANDARD_ENCOUNTER_BOOSTED_LEVEL_MODIFIER); let bossSpecies: PokemonSpecies; - if (scene.eventManager.isEventActive() && scene.eventManager.activeEvent()?.uncommonBreedEncounters && randSeedInt(2) === 1) { - const eventEncounter = randSeedItem(scene.eventManager.activeEvent()!.uncommonBreedEncounters!); - const levelSpecies = getPokemonSpecies(eventEncounter.species).getWildSpeciesForLevel(level, eventEncounter.allowEvolution ?? false, true, scene.gameMode); + const eventEncounters = scene.eventManager.getEventEncounters(); + if (eventEncounters.length > 0 && randSeedInt(2) === 1) { + const eventEncounter = randSeedItem(eventEncounters); + const levelSpecies = getPokemonSpecies(eventEncounter.species).getWildSpeciesForLevel(level, !(eventEncounter.blockEvolution ?? true), true, scene.gameMode); bossSpecies = getPokemonSpecies( levelSpecies ); } else { bossSpecies = scene.arena.randomSpecies(scene.currentBattle.waveIndex, level, 0, getPartyLuckValue(scene.getPlayerParty()), true); diff --git a/src/data/mystery-encounters/encounters/delibirdy-encounter.ts b/src/data/mystery-encounters/encounters/delibirdy-encounter.ts index 99668c76143..e4431fc6611 100644 --- a/src/data/mystery-encounters/encounters/delibirdy-encounter.ts +++ b/src/data/mystery-encounters/encounters/delibirdy-encounter.ts @@ -37,7 +37,7 @@ const OPTION_3_DISALLOWED_MODIFIERS = [ const DELIBIRDY_MONEY_PRICE_MULTIPLIER = 2; const doEventReward = (scene: BattleScene) => { - const event_buff = scene.eventManager.activeEvent()?.delibirdyBuff ?? []; + const event_buff = scene.eventManager.getDelibirdyBuff(); if (event_buff.length > 0) { const candidates = event_buff.filter((c => { const mtype = generateModifierType(scene, 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 e238fd51e66..296a0b5decb 100644 --- a/src/data/mystery-encounters/encounters/fight-or-flight-encounter.ts +++ b/src/data/mystery-encounters/encounters/fight-or-flight-encounter.ts @@ -58,9 +58,10 @@ export const FightOrFlightEncounter: MysteryEncounter = // Calculate boss mon const level = getEncounterPokemonLevelForWave(scene, STANDARD_ENCOUNTER_BOOSTED_LEVEL_MODIFIER); let bossSpecies: PokemonSpecies; - if (scene.eventManager.isEventActive() && scene.eventManager.activeEvent()?.uncommonBreedEncounters && randSeedInt(2) === 1) { - const eventEncounter = randSeedItem(scene.eventManager.activeEvent()!.uncommonBreedEncounters!); - const levelSpecies = getPokemonSpecies(eventEncounter.species).getWildSpeciesForLevel(level, eventEncounter.allowEvolution ?? false, true, scene.gameMode); + const eventEncounters = scene.eventManager.getEventEncounters(); + if (eventEncounters.length > 0 && randSeedInt(2) === 1) { + const eventEncounter = randSeedItem(eventEncounters); + const levelSpecies = getPokemonSpecies(eventEncounter.species).getWildSpeciesForLevel(level, !(eventEncounter.blockEvolution ?? true), true, scene.gameMode); bossSpecies = getPokemonSpecies( levelSpecies ); } else { bossSpecies = scene.arena.randomSpecies(scene.currentBattle.waveIndex, level, 0, getPartyLuckValue(scene.getPlayerParty()), true); diff --git a/src/data/mystery-encounters/encounters/uncommon-breed-encounter.ts b/src/data/mystery-encounters/encounters/uncommon-breed-encounter.ts index ebea34253d1..d60fa2b6716 100644 --- a/src/data/mystery-encounters/encounters/uncommon-breed-encounter.ts +++ b/src/data/mystery-encounters/encounters/uncommon-breed-encounter.ts @@ -53,9 +53,10 @@ export const UncommonBreedEncounter: MysteryEncounter = // Level equal to 2 below highest party member const level = getHighestLevelPlayerPokemon(scene, false, true).level - 2; let species: PokemonSpecies; - if (scene.eventManager.isEventActive() && scene.eventManager.activeEvent()?.uncommonBreedEncounters && randSeedInt(2) === 1) { - const eventEncounter = randSeedItem(scene.eventManager.activeEvent()!.uncommonBreedEncounters!); - const levelSpecies = getPokemonSpecies(eventEncounter.species).getWildSpeciesForLevel(level, eventEncounter.allowEvolution ?? false, true, scene.gameMode); + const eventEncounters = scene.eventManager.getEventEncounters(); + if (eventEncounters.length > 0 && randSeedInt(2) === 1) { + const eventEncounter = randSeedItem(eventEncounters); + const levelSpecies = getPokemonSpecies(eventEncounter.species).getWildSpeciesForLevel(level, !(eventEncounter.blockEvolution ?? true), true, scene.gameMode); species = getPokemonSpecies( levelSpecies ); } else { species = scene.arena.randomSpecies(scene.currentBattle.waveIndex, level, 0, getPartyLuckValue(scene.getPlayerParty()), true); diff --git a/src/field/pokemon.ts b/src/field/pokemon.ts index 30856ab416a..f032832fd28 100644 --- a/src/field/pokemon.ts +++ b/src/field/pokemon.ts @@ -5,7 +5,7 @@ import { variantData } from "#app/data/variant"; import BattleInfo, { PlayerBattleInfo, EnemyBattleInfo } from "#app/ui/battle-info"; import Move, { 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 { default as PokemonSpecies, PokemonSpeciesForm, 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 { Constructor, isNullOrUndefined, randSeedInt, type nil } from "#app/utils"; import * as Utils from "#app/utils"; @@ -4311,10 +4311,7 @@ export class PlayerPokemon extends Pokemon { ].filter(d => !!d); const amount = new Utils.NumberHolder(friendship); this.scene.applyModifier(PokemonFriendshipBoosterModifier, true, this, amount); - let candyFriendshipMultiplier = CLASSIC_CANDY_FRIENDSHIP_MULTIPLIER; - if (this.scene.eventManager.isEventActive()) { - candyFriendshipMultiplier *= this.scene.eventManager.getFriendshipMultiplier(); - } + const candyFriendshipMultiplier = this.scene.eventManager.getClassicFriendshipMultiplier(); const starterAmount = new Utils.NumberHolder(Math.floor(amount.value * (this.scene.gameMode.isClassic ? candyFriendshipMultiplier : 1) / (fusionStarterSpeciesId ? 2 : 1))); // Add friendship to this PlayerPokemon diff --git a/src/phases/trainer-victory-phase.ts b/src/phases/trainer-victory-phase.ts index d797e4360ac..df41c3cea85 100644 --- a/src/phases/trainer-victory-phase.ts +++ b/src/phases/trainer-victory-phase.ts @@ -39,7 +39,11 @@ export class TrainerVictoryPhase extends BattlePhase { // Validate Voucher for boss trainers if (vouchers.hasOwnProperty(TrainerType[trainerType])) { if (!this.scene.validateVoucher(vouchers[TrainerType[trainerType]]) && this.scene.currentBattle.trainer?.config.isBoss) { - this.scene.unshiftPhase(new ModifierRewardPhase(this.scene, [ modifierTypes.VOUCHER, modifierTypes.VOUCHER, modifierTypes.VOUCHER_PLUS, modifierTypes.VOUCHER_PREMIUM ][vouchers[TrainerType[trainerType]].voucherType])); + if (this.scene.eventManager.getUpgradeUnlockedVouchers()) { + this.scene.unshiftPhase(new ModifierRewardPhase(this.scene, [ modifierTypes.VOUCHER_PLUS, modifierTypes.VOUCHER_PLUS, modifierTypes.VOUCHER_PLUS, modifierTypes.VOUCHER_PREMIUM ][vouchers[TrainerType[trainerType]].voucherType])); + } else { + this.scene.unshiftPhase(new ModifierRewardPhase(this.scene, [ 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 926da91b352..e9e523fd515 100644 --- a/src/timed-event-manager.ts +++ b/src/timed-event-manager.ts @@ -1,10 +1,11 @@ import BattleScene from "#app/battle-scene"; import { TextStyle, addTextObject } from "#app/ui/text"; -import { nil } from "#app/utils"; +import { isNullOrUndefined, nil } from "#app/utils"; import i18next from "i18next"; import { Species } from "#enums/species"; import { WeatherPoolEntry } from "#app/data/weather"; import { WeatherType } from "#enums/weather-type"; +import { CLASSIC_CANDY_FRIENDSHIP_MULTIPLIER } from "./data/balance/starters"; export enum EventType { SHINY, @@ -21,14 +22,15 @@ interface EventBanner { interface EventEncounter { species: Species; - allowEvolution?: boolean; + blockEvolution?: boolean; } interface TimedEvent extends EventBanner { name: string; eventType: EventType; shinyMultiplier?: number; - friendshipMultiplier?: number; + classicFriendshipMultiplier?: number; + upgradeUnlockedVouchers?: boolean; startDate: Date; endDate: Date; uncommonBreedEncounters?: EventEncounter[]; @@ -41,32 +43,32 @@ 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 }, + { 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" ], @@ -97,16 +99,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 +112,68 @@ 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.uncommonBreedEncounters)) { + ret.push(...te.uncommonBreedEncounters); + } + }); + 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; + } + } export class TimedEventDisplay extends Phaser.GameObjects.Container {