src/timed-event-manager.ts
Rama beta · réplica de lectura del repositorio original.
import { globalScene } from "#app/global-scene";
import { speciesDataRegistry } from "#app/global-species-data-registry";
import { SHINY_CATCH_RATE_MULTIPLIER } from "#balance/rates";
import { CLASSIC_CANDY_FRIENDSHIP_MULTIPLIER } from "#balance/starters";
import { timedEvents } from "#balance/timed-events";
import type { PokemonSpeciesFilter } from "#data/pokemon-species";
import type { BiomeId } from "#enums/biome-id";
import type { MysteryEncounterTier } from "#enums/mystery-encounter-tier";
import type { MysteryEncounterType } from "#enums/mystery-encounter-type";
import type { SpeciesId } from "#enums/species-id";
import type { TrainerType } from "#enums/trainer-type";
import type { ModifierTypeKeys } from "#modifiers/modifier-type";
import type { BiomeLinks } from "#types/biomes";
import type { EventEncounter, EventMysteryEncounterTier, EventWeatherPools, TimedEvent } from "#types/game-events";
import { randSeedShuffle } from "#utils/common";
import i18next from "i18next";
export class TimedEventManager {
/**
* Whether the timed event manager is disabled.
* Used to disable events in testing.
*/
private disabled: boolean;
private cachedReplacementMap: Map<number, { speciesId: SpeciesId; formIndex: number }> | null = null;
isActive(event: TimedEvent) {
if (this.disabled) {
return false;
}
const now = new Date();
return event.startDate < now && now < event.endDate;
}
/**
* For getting the active event
* @returns The first active {@linkcode TimedEvent} or `undefined` if there are no active events
*/
activeEvent(): TimedEvent | undefined {
return timedEvents.find((te: TimedEvent) => this.isActive(te));
}
isEventActive(): boolean {
return timedEvents.some((te: TimedEvent) => this.isActive(te));
}
/**
* Check whether the current {@linkcode TimedEvent} is active and for April Fools.
* @returns Whether the April Fools event is currently active.
*/
isAprilFoolsActive(): boolean {
const now = new Date();
return now.getUTCDate() === 1 && now.getUTCMonth() === 3;
}
activeEventHasBanner(): boolean {
return this.activeEvent()?.bannerKey != null;
}
/**
* Get the multiplier for shiny encounters during a shiny {@linkcode TimedEvent}
* @returns the shiny encounter multiplier
*/
getShinyEncounterMultiplier(): number {
return this.activeEvent()?.shinyEncounterMultiplier ?? 1;
}
/**
* Get the multiplier for shiny catches during a shiny {@linkcode TimedEvent}
* @returns the shiny catch multiplier
*/
getShinyCatchMultiplier(): number {
return this.activeEvent()?.shinyCatchMultiplier ?? SHINY_CATCH_RATE_MULTIPLIER;
}
getEventBannerFilename(): string {
return this.activeEvent()?.bannerKey ?? "";
}
public getLogoKey(): string {
return this.activeEvent()?.logo ?? "logo";
}
getEventBannerLangs(): string[] {
return [...(this.activeEvent()?.availableLangs ?? [])];
}
getEventEncounters(): EventEncounter[] {
return [...(this.activeEvent()?.eventEncounters ?? [])];
}
getAllValidEventEncounters(
allowSubLegendary = true,
allowLegendary = true,
allowMythical = true,
speciesFilter: PokemonSpeciesFilter,
): EventEncounter[] {
return this.getEventEncounters().filter(enc => {
const species = speciesDataRegistry.getSpecies(enc.species);
return (
(allowSubLegendary || !species.subLegendary)
&& (allowLegendary || !species.legendary)
&& (allowMythical || !species.mythical)
&& speciesFilter(species)
);
});
}
/**
* For events that change the classic candy friendship multiplier
* @returns The classic friendship multiplier of the active {@linkcode TimedEvent}, or the default {@linkcode CLASSIC_CANDY_FRIENDSHIP_MULTIPLIER}
*/
getClassicFriendshipMultiplier(): number {
return this.activeEvent()?.classicFriendshipMultiplier ?? CLASSIC_CANDY_FRIENDSHIP_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 this.activeEvent()?.upgradeUnlockedVouchers ?? false;
}
/**
* Get the amount of extra candy to add when catching a Pokemon.
* @returns The amount of extra candy to add when catching a Pokemon
*/
public getExtraCatchCandy(): number {
return this.activeEvent()?.extraCatchCandy ?? 0;
}
/**
* For events where Delibirdy gives extra items
* @returns list of ids of {@linkcode ModifierType}s that Delibirdy hands out as a bonus
*/
getDelibirdyBuff(): string[] {
return [...(this.activeEvent()?.delibirdyBuff ?? [])];
}
/**
* For events where there's a set weather for town biome (other biomes are hard)
* @returns Event weathers for town
*/
getWeather(): EventWeatherPools | undefined {
return this.activeEvent()?.weather;
}
getAllMysteryEncounterChanges(): EventMysteryEncounterTier[] {
const ret: EventMysteryEncounterTier[] = [];
for (const te of timedEvents) {
if (this.isActive(te) && te.mysteryEncounterTierChanges != null) {
ret.push(...te.mysteryEncounterTierChanges);
}
}
return ret;
}
getEventMysteryEncountersDisabled(): MysteryEncounterType[] {
const ret: MysteryEncounterType[] = [];
const metChanges = this.activeEvent()?.mysteryEncounterTierChanges ?? [];
for (const metc of metChanges) {
if (metc.disable) {
ret.push(metc.mysteryEncounter);
}
}
return ret;
}
getMysteryEncounterTierForEvent(
encounterType: MysteryEncounterType,
normal: MysteryEncounterTier,
): MysteryEncounterTier {
const metChanges = this.activeEvent()?.mysteryEncounterTierChanges ?? [];
for (const metc of metChanges) {
if (metc.mysteryEncounter === encounterType) {
return metc.tier ?? normal;
}
}
return normal;
}
getEventLuckBoost(): number {
return this.activeEvent()?.luckBoost ?? 0;
}
getEventLuckBoostedSpecies(): SpeciesId[] {
return [...(this.activeEvent()?.luckBoostedSpecies ?? [])];
}
areFusionsBoosted(): boolean {
return this.activeEvent()?.boostFusions ?? false;
}
public isSootheBellBoosted(): boolean {
return this.activeEvent()?.boostSootheBell ?? false;
}
/**
* Gets all the modifier types associated with a certain wave during an event
* @see EventWaveReward
* @param wave the wave to check for associated rewards
* @returns array of strings of the event modifier reward types
*/
getFixedBattleEventRewards(wave: number): ModifierTypeKeys[] {
return (
this.activeEvent()
?.classicWaveRewards?.filter(cwr => cwr.wave === wave)
.map(cwr => cwr.type) ?? []
);
}
/**
* Get the extra shiny chance for trainers due to event
*/
getClassicTrainerShinyChance(): number {
return this.activeEvent()?.trainerShinyChance ?? 0;
}
getEventBgmReplacement(bgm: string): string {
const eventMusicReplacements = this.activeEvent()?.music ?? [];
for (const emr of eventMusicReplacements) {
if (emr[0] === bgm) {
console.log(`it is ${this.activeEvent()?.name} so instead of ${emr[0]} we play ${emr[1]}`);
return emr[1];
}
}
return bgm;
}
/**
* Get the event sprite replacement for a given species and form, if it exists. If the active event has `fillRandom` enabled, \
* will return a random replacement for any species/form pair that isn't explicitly listed in the `pokemonReplacements` array.
* @param species - The species ID of the pokemon to check for a sprite replacement
* @param formIndex - The form index of the pokemon to check for a sprite replacement. Defaults to 0
* @returns An object containing the species ID and form index of the replacement sprite, or null if no replacement exists.
*/
public getEventPokemonSpriteReplacement(
species: SpeciesId,
formIndex = 0,
): {
speciesId: SpeciesId;
formIndex: number;
} | null {
const event = this.activeEvent();
if (!event) {
return null;
}
const sprites = event?.sprites;
if (!sprites) {
return null;
}
const eventSpriteReplacements = sprites.pokemonReplacements ?? [];
const fillRandom = sprites.fillRandom ?? false;
for (const esr of eventSpriteReplacements) {
const [sourceSpeciesIdStr, sourceFormIndexStr] = esr[0].split("/");
if (sourceSpeciesIdStr === species.toString() && (sourceFormIndexStr ?? "0") === formIndex.toString()) {
const [targetSpeciesIdStr, targetFormIndexStr] = esr[1].split("/");
return { speciesId: Number(targetSpeciesIdStr) as SpeciesId, formIndex: Number(targetFormIndexStr ?? "0") };
}
}
if (fillRandom) {
// Multiply by 100000 to avoid collisions
const key = species * 100_000 + formIndex;
this.fillRandomPokemonSpriteReplacements();
return this.cachedReplacementMap!.get(key) ?? null;
}
return null;
}
/**
* Assign each species/form pair a random other species/form pair for sprite replacement.
*/
private fillRandomPokemonSpriteReplacements(): void {
if (this.cachedReplacementMap) {
return;
}
this.cachedReplacementMap = new Map();
const allPairs: { speciesId: SpeciesId; formIndex: number }[] = [];
for (const species of speciesDataRegistry.getAllSpecies()) {
const formCount = species.forms.length || 1;
for (let f = 0; f < formCount; f++) {
allPairs.push({ speciesId: species.speciesId, formIndex: f });
}
}
globalScene.executeWithSeedOffset(
() => {
const shuffled = randSeedShuffle([...allPairs]);
for (let i = 0; i < allPairs.length; i++) {
const sourceKey = allPairs[i].speciesId * 100_000 + allPairs[i].formIndex;
this.cachedReplacementMap!.set(sourceKey, shuffled[i]);
}
},
0,
this.activeEvent()!.name,
);
}
/**
* Get the event trainer sprite replacement for a given trainer type, if it exists.
* @param trainerType - The trainer type to check for a sprite replacement
* @returns The trainer type of the replacement sprite, or null if no replacement exists.
*/
public getEventTrainerSpriteReplacement(trainerType: TrainerType): string | null {
const event = this.activeEvent();
if (!event) {
return null;
}
const trainerReplacements = event.sprites?.trainerReplacements ?? [];
for (const tr of trainerReplacements) {
if (tr[0] === trainerType) {
return tr[1];
}
}
return null;
}
/**
* Get the event image replacement for a given image key, if it exists.
* @param imageKey - The image key to check for a replacement
* @returns The replacement image **FILENAME** if it exists, otherwise null.
* @remarks
* The image has to be in the `events` folder for it to work.
*/
public getEventImageReplacement(imageKey: string): string | null {
const event = this.activeEvent();
if (!event) {
return null;
}
const imageReplacements = event.sprites?.imageReplacements ?? [];
for (const ir of imageReplacements) {
if (ir[0] === imageKey) {
return ir[1];
}
}
return null;
}
/**
* Return the key replacement for the given i18n key if it exists in the active event, otherwise return the original key.
* @param key The i18n key to check for a replacement
* @returns The replacement key if it exists, otherwise the original key
*/
public getEventTextReplacement(key: string): string {
const event = this.activeEvent();
if (!event || !event.textReplacements) {
return key;
}
for (const [source, target] of event.textReplacements) {
if (key === source && i18next.exists(target)) {
return target;
}
}
return key;
}
/**
* Check if the current active event has any text replacements. \
* This is used to determine whether the i18next proxy should be loaded.
* @returns Whether the active event has text replacements
*/
public hasEventTextReplacement(): boolean {
const event = this.activeEvent();
if (!event) {
return false;
}
return event.textReplacements != null && event.textReplacements.length > 0;
}
public getEventDailyStartingItems(): readonly ModifierTypeKeys[] {
return this.activeEvent()?.dailyRunStartingItems ?? [];
}
/**
* Get the biomelinks for a specific biome in the active event.
* @param biomeId - the biomeId to get the links for
* @returns the {@linkcode BiomeLinks} for the specified biome.
*/
public getEventBiomeLinks(biomeId: BiomeId): BiomeLinks {
const biomes = this.activeEvent()?.biomes ?? null;
return biomes?.[biomeId] ?? [];
}
/**
* Disable the timed event manager. Used for testing.
*/
public disable(): void {
this.disabled = true;
}
// todo: add option to enable to allow for testing timed events
}