pokerogue/src/timed-event-manager.ts
damocleas fa86ea3214
[Misc] Spring Stuff (#5742)
* Update timed-event-manager.ts

* spr25 images

* Update the-pokemon-salesman-encounter.ts rolls
2025-05-01 22:54:59 -04:00

695 lines
21 KiB
TypeScript

import { globalScene } from "#app/global-scene";
import { TextStyle, addTextObject } from "#app/ui/text";
import type { nil } from "#app/utils/common";
import { isNullOrUndefined } from "#app/utils/common";
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";
import { Challenges } from "#enums/challenges";
export enum EventType {
SHINY,
NO_TIMER_DISPLAY,
LUCK,
}
interface EventBanner {
bannerKey?: string;
xOffset?: number;
yOffset?: number;
scale?: number;
availableLangs?: string[];
}
interface EventEncounter {
species: Species;
blockEvolution?: boolean;
formIndex?: number;
}
interface EventMysteryEncounterTier {
mysteryEncounter: MysteryEncounterType;
tier?: MysteryEncounterTier;
disable?: boolean;
}
interface EventWaveReward {
wave: number;
type: string;
}
type EventMusicReplacement = [string, string];
interface EventChallenge {
challenge: Challenges;
value: number;
}
interface TimedEvent extends EventBanner {
name: string;
eventType: EventType;
shinyMultiplier?: number;
classicFriendshipMultiplier?: number;
luckBoost?: number;
upgradeUnlockedVouchers?: boolean;
startDate: Date;
endDate: Date;
eventEncounters?: EventEncounter[];
delibirdyBuff?: string[];
weather?: WeatherPoolEntry[];
mysteryEncounterTierChanges?: EventMysteryEncounterTier[];
luckBoostedSpecies?: Species[];
boostFusions?: boolean; //MODIFIER REWORK PLEASE
classicWaveRewards?: EventWaveReward[]; // Rival battle rewards
trainerShinyChance?: number; // Odds over 65536 of trainer mon generating as shiny
music?: EventMusicReplacement[];
dailyRunChallenges?: EventChallenge[];
}
const timedEvents: TimedEvent[] = [
{
name: "Winter Holiday Update",
eventType: EventType.SHINY,
shinyMultiplier: 2,
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"],
eventEncounters: [
{ species: Species.GIMMIGHOUL, blockEvolution: true },
{ species: Species.DELIBIRD },
{ 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 }],
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,
},
],
classicWaveRewards: [
{ wave: 8, type: "SHINY_CHARM" },
{ wave: 8, type: "ABILITY_CHARM" },
{ wave: 8, type: "CATCHING_CHARM" },
{ wave: 25, type: "SHINY_CHARM" },
],
},
{
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: ["en", "de", "it", "fr", "ja", "ko", "es-ES", "pt-BR", "zh-CN"],
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,
],
classicWaveRewards: [
{ wave: 8, type: "SHINY_CHARM" },
{ wave: 8, type: "ABILITY_CHARM" },
{ wave: 8, type: "CATCHING_CHARM" },
{ wave: 25, type: "SHINY_CHARM" },
],
},
{
name: "Valentine",
eventType: EventType.SHINY,
startDate: new Date(Date.UTC(2025, 1, 10)),
endDate: new Date(Date.UTC(2025, 1, 21)),
boostFusions: true,
shinyMultiplier: 2,
bannerKey: "valentines2025event",
scale: 0.21,
availableLangs: ["en", "de", "it", "fr", "ja", "ko", "es-ES", "pt-BR", "zh-CN"],
eventEncounters: [
{ species: Species.NIDORAN_F },
{ species: Species.NIDORAN_M },
{ species: Species.IGGLYBUFF },
{ species: Species.SMOOCHUM },
{ species: Species.VOLBEAT },
{ species: Species.ILLUMISE },
{ species: Species.ROSELIA },
{ species: Species.LUVDISC },
{ species: Species.WOOBAT },
{ species: Species.FRILLISH },
{ species: Species.ALOMOMOLA },
{ species: Species.FURFROU, formIndex: 1 }, // Heart Trim
{ species: Species.ESPURR },
{ species: Species.SPRITZEE },
{ species: Species.SWIRLIX },
{ species: Species.APPLIN },
{ species: Species.MILCERY },
{ species: Species.INDEEDEE },
{ species: Species.TANDEMAUS },
{ species: Species.ENAMORUS },
],
luckBoostedSpecies: [Species.LUVDISC],
classicWaveRewards: [
{ wave: 8, type: "SHINY_CHARM" },
{ wave: 8, type: "ABILITY_CHARM" },
{ wave: 8, type: "CATCHING_CHARM" },
{ wave: 25, type: "SHINY_CHARM" },
],
},
{
name: "PKMNDAY2025",
eventType: EventType.LUCK,
startDate: new Date(Date.UTC(2025, 1, 27)),
endDate: new Date(Date.UTC(2025, 2, 4)),
classicFriendshipMultiplier: 4,
bannerKey: "pkmnday2025event",
scale: 0.21,
availableLangs: ["en", "de", "it", "fr", "ja", "ko", "es-ES", "pt-BR", "zh-CN"],
eventEncounters: [
{ species: Species.PIKACHU, formIndex: 1, blockEvolution: true }, // Partner Form
{ species: Species.EEVEE, formIndex: 1, blockEvolution: true }, // Partner Form
{ species: Species.CHIKORITA },
{ species: Species.TOTODILE },
{ species: Species.TEPIG },
],
luckBoostedSpecies: [
Species.PICHU,
Species.PIKACHU,
Species.RAICHU,
Species.ALOLA_RAICHU,
Species.PSYDUCK,
Species.GOLDUCK,
Species.EEVEE,
Species.FLAREON,
Species.JOLTEON,
Species.VAPOREON,
Species.ESPEON,
Species.UMBREON,
Species.LEAFEON,
Species.GLACEON,
Species.SYLVEON,
Species.CHIKORITA,
Species.BAYLEEF,
Species.MEGANIUM,
Species.TOTODILE,
Species.CROCONAW,
Species.FERALIGATR,
Species.TEPIG,
Species.PIGNITE,
Species.EMBOAR,
Species.ZYGARDE,
Species.ETERNAL_FLOETTE,
],
classicWaveRewards: [
{ wave: 8, type: "SHINY_CHARM" },
{ wave: 8, type: "ABILITY_CHARM" },
{ wave: 8, type: "CATCHING_CHARM" },
{ wave: 25, type: "SHINY_CHARM" },
],
},
{
name: "April Fools 2025",
eventType: EventType.LUCK,
startDate: new Date(Date.UTC(2025, 2, 31)),
endDate: new Date(Date.UTC(2025, 3, 3)),
bannerKey: "aprf25",
scale: 0.21,
availableLangs: ["en", "de", "it", "fr", "ja", "ko", "es-ES", "es-MX", "pt-BR", "zh-CN"],
trainerShinyChance: 13107, // 13107/65536 = 1/5
music: [
["title", "title_afd"],
["battle_rival_3", "battle_rival_3_afd"],
],
dailyRunChallenges: [
{
challenge: Challenges.INVERSE_BATTLE,
value: 1,
},
],
},
{
name: "Shining Spring",
eventType: EventType.SHINY,
startDate: new Date(Date.UTC(2025, 4, 2)),
endDate: new Date(Date.UTC(2025, 4, 12)),
bannerKey: "spr25event",
scale: 0.21,
availableLangs: ["en", "de", "it", "fr", "ja", "ko", "es-ES", "es-MX", "pt-BR", "zh-CN"],
shinyMultiplier: 2,
upgradeUnlockedVouchers: true,
eventEncounters: [
{ species: Species.HOPPIP },
{ species: Species.CELEBI },
{ species: Species.VOLBEAT },
{ species: Species.ILLUMISE },
{ species: Species.SPOINK },
{ species: Species.LILEEP },
{ species: Species.SHINX },
{ species: Species.PACHIRISU },
{ species: Species.CHERUBI },
{ species: Species.MUNCHLAX },
{ species: Species.TEPIG },
{ species: Species.PANSAGE },
{ species: Species.PANSEAR },
{ species: Species.PANPOUR },
{ species: Species.DARUMAKA },
{ species: Species.ARCHEN },
{ species: Species.DEERLING, formIndex: 0 }, // Spring Deerling
{ species: Species.CLAUNCHER },
{ species: Species.WISHIWASHI },
{ species: Species.MUDBRAY },
{ species: Species.DRAMPA },
{ species: Species.JANGMO_O },
{ species: Species.APPLIN },
],
classicWaveRewards: [
{ wave: 8, type: "SHINY_CHARM" },
{ wave: 8, type: "ABILITY_CHARM" },
{ wave: 8, type: "CATCHING_CHARM" },
{ wave: 25, type: "SHINY_CHARM" },
],
}
];
export class TimedEventManager {
isActive(event: TimedEvent) {
return event.startDate < new Date() && new Date() < event.endDate;
}
activeEvent(): TimedEvent | undefined {
return timedEvents.find((te: TimedEvent) => this.isActive(te));
}
isEventActive(): boolean {
return timedEvents.some((te: TimedEvent) => this.isActive(te));
}
activeEventHasBanner(): boolean {
const activeEvents = timedEvents.filter(te => this.isActive(te) && te.hasOwnProperty("bannerKey"));
return activeEvents.length > 0;
}
getShinyMultiplier(): number {
let multiplier = 1;
const shinyEvents = timedEvents.filter(te => te.eventType === EventType.SHINY && this.isActive(te));
for (const se of shinyEvents) {
multiplier *= se.shinyMultiplier ?? 1;
}
return multiplier;
}
getEventBannerFilename(): string {
return timedEvents.find((te: TimedEvent) => this.isActive(te))?.bannerKey ?? "";
}
getEventBannerLangs(): string[] {
const ret: string[] = [];
ret.push(...timedEvents.find(te => this.isActive(te) && !isNullOrUndefined(te.availableLangs))?.availableLangs!);
return ret;
}
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));
for (const fe of classicFriendshipEvents) {
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));
for (const le of luckEvents) {
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;
}
areFusionsBoosted(): boolean {
return timedEvents.some(te => this.isActive(te) && te.boostFusions);
}
/**
* 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): string[] {
const ret: string[] = [];
timedEvents
.filter(te => this.isActive(te) && !isNullOrUndefined(te.classicWaveRewards))
.map(te => {
ret.push(...te.classicWaveRewards!.filter(cwr => cwr.wave === wave).map(cwr => cwr.type));
});
return ret;
}
// Gets the extra shiny chance for trainers due to event (odds/65536)
getClassicTrainerShinyChance(): number {
let ret = 0;
const tsEvents = timedEvents.filter(te => this.isActive(te) && !isNullOrUndefined(te.trainerShinyChance));
tsEvents.map(t => (ret += t.trainerShinyChance!));
return ret;
}
getEventBgmReplacement(bgm: string): string {
let ret = bgm;
timedEvents.map(te => {
if (this.isActive(te) && !isNullOrUndefined(te.music)) {
te.music.map(mr => {
if (mr[0] === bgm) {
console.log(`it is ${te.name} so instead of ${mr[0]} we play ${mr[1]}`);
ret = mr[1];
}
});
}
});
return ret;
}
/**
* Activates any challenges on {@linkcode globalScene.gameMode} for the currently active event
*/
startEventChallenges(): void {
const challenges = this.activeEvent()?.dailyRunChallenges;
challenges?.forEach((eventChal: EventChallenge) =>
globalScene.gameMode.setChallengeValue(eventChal.challenge, eventChal.value),
);
}
}
export class TimedEventDisplay extends Phaser.GameObjects.Container {
private event: TimedEvent | nil;
private eventTimerText: Phaser.GameObjects.Text;
private banner: Phaser.GameObjects.Image;
private availableWidth: number;
private eventTimer: NodeJS.Timeout | null;
constructor(x: number, y: number, event?: TimedEvent) {
super(globalScene, x, y);
this.availableWidth = globalScene.scaledCanvas.width;
this.event = event;
this.setVisible(false);
}
/**
* Set the width that can be used to display the event timer and banner. By default
* these elements get centered horizontally in that space, in the bottom left of the screen
*/
setWidth(width: number) {
if (width !== this.availableWidth) {
this.availableWidth = width;
const xPosition = this.availableWidth / 2 + (this.event?.xOffset ?? 0);
if (this.banner) {
this.banner.x = xPosition;
}
if (this.eventTimerText) {
this.eventTimerText.x = xPosition;
}
}
}
setup() {
const lang = i18next.resolvedLanguage;
if (this.event?.bannerKey) {
let key = this.event.bannerKey;
if (lang && this.event.availableLangs && this.event.availableLangs.length > 0) {
if (this.event.availableLangs.includes(lang)) {
key += "-" + lang;
} else {
key += "-en";
}
}
console.log(key);
console.log(this.event.bannerKey);
const padding = 5;
const showTimer = this.event.eventType !== EventType.NO_TIMER_DISPLAY;
const yPosition = globalScene.game.canvas.height / 6 - padding - (showTimer ? 10 : 0) - (this.event.yOffset ?? 0);
this.banner = new Phaser.GameObjects.Image(globalScene, this.availableWidth / 2, yPosition - padding, key);
this.banner.setName("img-event-banner");
this.banner.setOrigin(0.5, 1);
this.banner.setScale(this.event.scale ?? 0.18);
if (showTimer) {
this.eventTimerText = addTextObject(
this.banner.x,
this.banner.y + 2,
this.timeToGo(this.event.endDate),
TextStyle.WINDOW,
);
this.eventTimerText.setName("text-event-timer");
this.eventTimerText.setScale(0.15);
this.eventTimerText.setOrigin(0.5, 0);
this.add(this.eventTimerText);
}
this.add(this.banner);
}
}
show() {
this.setVisible(true);
this.updateCountdown();
this.eventTimer = setInterval(() => {
this.updateCountdown();
}, 1000);
}
clear() {
this.setVisible(false);
this.eventTimer && clearInterval(this.eventTimer);
this.eventTimer = null;
}
private timeToGo(date: Date) {
// Utility to add leading zero
function z(n) {
return (n < 10 ? "0" : "") + n;
}
const now = new Date();
let diff = Math.abs(date.getTime() - now.getTime());
// Allow for previous times
diff = Math.abs(diff);
// Get time components
const days = (diff / 8.64e7) | 0;
const hours = ((diff % 8.64e7) / 3.6e6) | 0;
const mins = ((diff % 3.6e6) / 6e4) | 0;
const secs = Math.round((diff % 6e4) / 1e3);
// Return formatted string
return i18next.t("menu:eventTimer", {
days: z(days),
hours: z(hours),
mins: z(mins),
secs: z(secs),
});
}
updateCountdown() {
if (this.event && this.event.eventType !== EventType.NO_TIMER_DISPLAY) {
this.eventTimerText.setText(this.timeToGo(this.event.endDate));
}
}
}