mirror of
https://github.com/pagefaultgames/pokerogue.git
synced 2025-02-22 12:16:26 +00:00
412 lines
13 KiB
TypeScript
412 lines
13 KiB
TypeScript
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,
|
|
LUCK
|
|
}
|
|
|
|
interface EventBanner {
|
|
bannerKey?: string;
|
|
xOffset?: number;
|
|
yOffset?: number;
|
|
scale?: number;
|
|
availableLangs?: string[];
|
|
}
|
|
|
|
interface EventEncounter {
|
|
species: Species;
|
|
blockEvolution?: boolean;
|
|
}
|
|
|
|
interface EventMysteryEncounterTier {
|
|
mysteryEncounter: MysteryEncounterType;
|
|
tier?: MysteryEncounterTier;
|
|
disable?: boolean;
|
|
}
|
|
|
|
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[];
|
|
}
|
|
|
|
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 }
|
|
]
|
|
},
|
|
{
|
|
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
|
|
]
|
|
}
|
|
];
|
|
|
|
export class TimedEventManager {
|
|
constructor() {}
|
|
|
|
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("bannerFilename"));
|
|
return activeEvents.length > 0;
|
|
}
|
|
|
|
getShinyMultiplier(): number {
|
|
let multiplier = 1;
|
|
const shinyEvents = timedEvents.filter((te) => te.eventType === EventType.SHINY && this.isActive(te));
|
|
shinyEvents.forEach((se) => {
|
|
multiplier *= se.shinyMultiplier ?? 1;
|
|
});
|
|
|
|
return multiplier;
|
|
}
|
|
|
|
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 {
|
|
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 && 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(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));
|
|
}
|
|
}
|
|
}
|