global timed event manager

This commit is contained in:
AJ Fontaine 2025-03-24 16:04:58 -04:00
parent 7f72794d23
commit 75cdd3d8d9
12 changed files with 131 additions and 45 deletions

View File

@ -37,6 +37,7 @@ import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode
import { MysteryEncounterTier } from "#enums/mystery-encounter-tier";
import { MysteryEncounterType } from "#enums/mystery-encounter-type";
import { Species } from "#enums/species";
import { timedEventManager } from "#app/global-event-manager";
/** the i18n namespace for this encounter */
const namespace = "mysteryEncounters/delibirdy";
@ -56,7 +57,7 @@ const OPTION_3_DISALLOWED_MODIFIERS = [
const DELIBIRDY_MONEY_PRICE_MULTIPLIER = 2;
const doEventReward = () => {
const event_buff = globalScene.eventManager.getDelibirdyBuff();
const event_buff = timedEventManager.getDelibirdyBuff();
if (event_buff.length > 0) {
const candidates = event_buff.filter(c => {
const mtype = generateModifierType(modifierTypes[c]);

View File

@ -46,6 +46,7 @@ import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/game-mode";
import { addPokemonDataToDexAndValidateAchievements } from "#app/data/mystery-encounters/utils/encounter-pokemon-utils";
import type { PokeballType } from "#enums/pokeball";
import { doShinySparkleAnim } from "#app/field/anims";
import { timedEventManager } from "#app/global-event-manager";
/** the i18n namespace for the encounter */
const namespace = "mysteryEncounters/globalTradeSystem";
@ -273,8 +274,8 @@ export const GlobalTradeSystemEncounter: MysteryEncounter = MysteryEncounterBuil
// Extra shiny roll at 1/128 odds (boosted by events and charms)
if (!tradePokemon.shiny) {
const shinyThreshold = new NumberHolder(WONDER_TRADE_SHINY_CHANCE);
if (globalScene.eventManager.isEventActive()) {
shinyThreshold.value *= globalScene.eventManager.getShinyMultiplier();
if (timedEventManager.isEventActive()) {
shinyThreshold.value *= timedEventManager.getShinyMultiplier();
}
globalScene.applyModifiers(ShinyRateBoosterModifier, true, shinyThreshold);

View File

@ -64,6 +64,7 @@ import { getPokemonSpecies } from "#app/data/pokemon-species";
import { PokemonType } from "#enums/pokemon-type";
import { getNatureName } from "#app/data/nature";
import { getPokemonNameWithAffix } from "#app/messages";
import { timedEventManager } from "#app/global-event-manager";
/**
* Animates exclamation sprite over trainer's head at start of encounter
@ -1045,7 +1046,7 @@ export function handleMysteryEncounterTurnStartEffects(): boolean {
export function getRandomEncounterSpecies(level: number, isBoss = false, rerollHidden = false): EnemyPokemon {
let bossSpecies: PokemonSpecies;
let isEventEncounter = false;
const eventEncounters = globalScene.eventManager.getEventEncounters();
const eventEncounters = timedEventManager.getEventEncounters();
let formIndex: number | undefined;
if (eventEncounters.length > 0 && randSeedInt(2) === 1) {

View File

@ -24,6 +24,7 @@ import { TrainerType } from "#enums/trainer-type";
import { Gender } from "#app/data/gender";
import { signatureSpecies } from "./balance/signature-species";
import { Abilities } from "#enums/abilities";
import { timedEventManager } from "#app/global-event-manager";
/** Minimum BST for Pokemon generated onto the Elite Four's teams */
const ELITE_FOUR_MINIMUM_BST = 460;
@ -739,13 +740,13 @@ export class TrainerConfig {
// return ret;
// }
setEventModifierRewardFuncs(...modifierTypeFuncs: (() => ModifierTypeFunc)[]): TrainerConfig {
this.eventRewardFuncs = modifierTypeFuncs.map(func => () => {
const modifierTypeFunc = func();
const modifierType = modifierTypeFunc();
modifierType.withIdFromFunc(modifierTypeFunc);
return modifierType;
});
/**
* Sets eventRewardFuncs to the active event rewards for the specified wave
* @param wave Associated with {@linkcode getFixedBattleEventRewards}
* @returns this
*/
setEventModifierRewardFuncs(wave: number): TrainerConfig {
this.eventRewardFuncs = timedEventManager.getFixedBattleEventRewards(wave).map(r => modifierTypes[r]);
return this;
}
@ -4346,11 +4347,7 @@ export const trainerConfigs: TrainerConfigs = {
() => modifierTypes.SUPER_EXP_CHARM,
() => modifierTypes.EXP_SHARE,
)
.setEventModifierRewardFuncs(
() => modifierTypes.SHINY_CHARM,
() => modifierTypes.ABILITY_CHARM,
() => modifierTypes.CATCHING_CHARM,
)
.setEventModifierRewardFuncs(8)
.setPartyMemberFunc(
0,
getRandomPartyMemberFunc(
@ -4418,7 +4415,7 @@ export const trainerConfigs: TrainerConfigs = {
.setMixedBattleBgm("battle_rival")
.setPartyTemplates(trainerPartyTemplates.RIVAL_2)
.setModifierRewardFuncs(() => modifierTypes.EXP_SHARE)
.setEventModifierRewardFuncs(() => modifierTypes.SHINY_CHARM)
.setEventModifierRewardFuncs(25)
.setPartyMemberFunc(
0,
getRandomPartyMemberFunc(
@ -4727,7 +4724,7 @@ export const trainerConfigs: TrainerConfigs = {
getRandomPartyMemberFunc([Species.RAYQUAZA], TrainerSlot.TRAINER, true, p => {
p.setBoss(true, 3);
p.pokeball = PokeballType.MASTER_BALL;
p.shiny = true;
p.shiny = timedEventManager.getClassicTrainerShinyChance() === 0;
p.variant = 1;
}),
)
@ -4824,7 +4821,7 @@ export const trainerConfigs: TrainerConfigs = {
p.setBoss();
p.generateAndPopulateMoveset();
p.pokeball = PokeballType.MASTER_BALL;
p.shiny = true;
p.shiny = timedEventManager.getClassicTrainerShinyChance() === 0;
p.variant = 1;
p.formIndex = 1; // Mega Rayquaza
p.generateName();

View File

@ -11,6 +11,7 @@ import { TerrainType, getTerrainName } from "./terrain";
import i18next from "i18next";
import { globalScene } from "#app/global-scene";
import type { Arena } from "#app/field/arena";
import { timedEventManager } from "#app/global-event-manager";
export class Weather {
public weatherType: WeatherType;
@ -405,8 +406,8 @@ export function getRandomWeatherType(arena: Arena): WeatherType {
break;
}
if (arena.biomeType === Biome.TOWN && globalScene.eventManager.isEventActive()) {
globalScene.eventManager.getWeather()?.map(w => weatherPool.push(w));
if (arena.biomeType === Biome.TOWN && timedEventManager.isEventActive()) {
timedEventManager.getWeather()?.map(w => weatherPool.push(w));
}
if (weatherPool.length > 1) {

View File

@ -260,6 +260,7 @@ import {
import { Nature } from "#enums/nature";
import { StatusEffect } from "#enums/status-effect";
import { doShinySparkleAnim } from "#app/field/anims";
import { timedEventManager } from "#app/global-event-manager";
export enum LearnMoveSituation {
MISC,
@ -2977,8 +2978,12 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
const shinyThreshold = new Utils.NumberHolder(BASE_SHINY_CHANCE);
if (thresholdOverride === undefined) {
if (globalScene.eventManager.isEventActive()) {
shinyThreshold.value *= globalScene.eventManager.getShinyMultiplier();
if (timedEventManager.isEventActive()) {
const tchance = timedEventManager.getClassicTrainerShinyChance();
shinyThreshold.value *= timedEventManager.getShinyMultiplier();
if (this.hasTrainer() && tchance > 0) {
shinyThreshold.value = Math.max(tchance, shinyThreshold.value); // Choose the higher boost
}
}
if (!this.hasTrainer()) {
globalScene.applyModifiers(
@ -3019,8 +3024,8 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
if (thresholdOverride !== undefined && applyModifiersToOverride) {
shinyThreshold.value = thresholdOverride;
}
if (globalScene.eventManager.isEventActive()) {
shinyThreshold.value *= globalScene.eventManager.getShinyMultiplier();
if (timedEventManager.isEventActive()) {
shinyThreshold.value *= timedEventManager.getShinyMultiplier();
}
if (!this.hasTrainer()) {
globalScene.applyModifiers(
@ -6433,10 +6438,10 @@ export class PlayerPokemon extends Pokemon {
amount,
);
const candyFriendshipMultiplier = globalScene.gameMode.isClassic
? globalScene.eventManager.getClassicFriendshipMultiplier()
? timedEventManager.getClassicFriendshipMultiplier()
: 1;
const fusionReduction = fusionStarterSpeciesId
? globalScene.eventManager.areFusionsBoosted()
? timedEventManager.areFusionsBoosted()
? 1.5 // Divide candy gain for fusions by 1.5 during events
: 2 // 2 for fusions outside events
: 1; // 1 for non-fused mons

View File

@ -0,0 +1,3 @@
import { TimedEventManager } from "./timed-event-manager";
export const timedEventManager = new TimedEventManager();

View File

@ -20,6 +20,7 @@ import { initStatsKeys } from "#app/ui/game-stats-ui-handler";
import { initVouchers } from "#app/system/voucher";
import { Biome } from "#enums/biome";
import { initMysteryEncounters } from "#app/data/mystery-encounters/mystery-encounters";
import { timedEventManager } from "./global-event-manager";
export class LoadingScene extends SceneBase {
public static readonly KEY = "loading";
@ -250,11 +251,13 @@ export class LoadingScene extends SceneBase {
this.loadAtlas("statuses", "");
this.loadAtlas("types", "");
}
const availableLangs = ["en", "de", "it", "fr", "ja", "ko", "es-ES", "pt-BR", "zh-CN"];
if (lang && availableLangs.includes(lang)) {
this.loadImage(`pkmnday2025event-${lang}`, "events");
} else {
this.loadImage("pkmnday2025event-en", "events");
if (timedEventManager.activeEventHasBanner()) {
const availableLangs = timedEventManager.getEventBannerLangs();
if (lang && availableLangs.includes(lang)) {
this.loadImage(`${timedEventManager.getEventBannerFilename()}-${lang}`, "events");
} else {
this.loadImage(`${timedEventManager.getEventBannerFilename()}-en`, "events");
}
}
this.loadAtlas("statuses", "");

View File

@ -126,6 +126,7 @@ import type { PermanentStat, TempBattleStat } from "#enums/stat";
import { getStatKey, Stat, TEMP_BATTLE_STATS } from "#enums/stat";
import { StatusEffect } from "#enums/status-effect";
import i18next from "i18next";
import { timedEventManager } from "#app/global-event-manager";
const outputModifierData = false;
const useMaxWeightForOutput = false;
@ -2647,7 +2648,7 @@ const modifierPool: ModifierPool = {
if (globalScene.gameMode.isSplicedOnly) {
return 4;
}
if (globalScene.gameMode.isClassic && globalScene.eventManager.areFusionsBoosted()) {
if (globalScene.gameMode.isClassic && timedEventManager.areFusionsBoosted()) {
return 2;
}
}
@ -2890,7 +2891,7 @@ const modifierPool: ModifierPool = {
new WeightedModifierType(
modifierTypes.DNA_SPLICERS,
(party: Pokemon[]) =>
!(globalScene.gameMode.isClassic && globalScene.eventManager.areFusionsBoosted()) &&
!(globalScene.gameMode.isClassic && timedEventManager.areFusionsBoosted()) &&
!globalScene.gameMode.isSplicedOnly &&
party.filter(p => !p.fusionSpecies).length > 1
? 24
@ -3654,7 +3655,7 @@ export function getPartyLuckValue(party: Pokemon[]): number {
);
return DailyLuck.value;
}
const eventSpecies = globalScene.eventManager.getEventLuckBoostedSpecies();
const eventSpecies = timedEventManager.getEventLuckBoostedSpecies();
const luck = Phaser.Math.Clamp(
party
.map(p => (p.isAllowedInBattle() ? p.getLuck() + (eventSpecies.includes(p.species.speciesId) ? 1 : 0) : 0))
@ -3662,7 +3663,7 @@ export function getPartyLuckValue(party: Pokemon[]): number {
0,
14,
);
return Math.min(globalScene.eventManager.getEventLuckBoost() + (luck ?? 0), 14);
return Math.min(timedEventManager.getEventLuckBoost() + (luck ?? 0), 14);
}
export function getLuckString(luckValue: number): string {

View File

@ -11,6 +11,7 @@ import { TrainerSlot } from "#app/data/trainer-config";
import { globalScene } from "#app/global-scene";
import { Biome } from "#app/enums/biome";
import { achvs } from "#app/system/achv";
import { timedEventManager } from "#app/global-event-manager";
export class TrainerVictoryPhase extends BattlePhase {
constructor() {
@ -29,7 +30,7 @@ export class TrainerVictoryPhase extends BattlePhase {
globalScene.unshiftPhase(new ModifierRewardPhase(modifierRewardFunc));
}
if (globalScene.eventManager.isEventActive()) {
if (timedEventManager.isEventActive()) {
for (const rewardFunc of globalScene.currentBattle.trainer?.config.eventRewardFuncs!) {
globalScene.unshiftPhase(new ModifierRewardPhase(rewardFunc));
}
@ -42,7 +43,7 @@ export class TrainerVictoryPhase extends BattlePhase {
!globalScene.validateVoucher(vouchers[TrainerType[trainerType]]) &&
globalScene.currentBattle.trainer?.config.isBoss
) {
if (globalScene.eventManager.getUpgradeUnlockedVouchers()) {
if (timedEventManager.getUpgradeUnlockedVouchers()) {
globalScene.unshiftPhase(
new ModifierRewardPhase(
[

View File

@ -36,6 +36,11 @@ interface EventMysteryEncounterTier {
disable?: boolean;
}
interface EventWaveReward {
wave: number;
type: string;
}
interface TimedEvent extends EventBanner {
name: string;
eventType: EventType;
@ -51,6 +56,8 @@ interface TimedEvent extends EventBanner {
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
}
const timedEvents: TimedEvent[] = [
@ -61,7 +68,7 @@ const timedEvents: TimedEvent[] = [
upgradeUnlockedVouchers: true,
startDate: new Date(Date.UTC(2024, 11, 21, 0)),
endDate: new Date(Date.UTC(2025, 0, 4, 0)),
bannerKey: "winter_holidays2024-event-",
bannerKey: "winter_holidays2024-event",
scale: 0.21,
availableLangs: ["en", "de", "it", "fr", "ja", "ko", "es-ES", "pt-BR", "zh-CN"],
eventEncounters: [
@ -104,6 +111,12 @@ const timedEvents: TimedEvent[] = [
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",
@ -111,7 +124,7 @@ const timedEvents: TimedEvent[] = [
luckBoost: 1,
startDate: new Date(Date.UTC(2025, 0, 29, 0)),
endDate: new Date(Date.UTC(2025, 1, 3, 0)),
bannerKey: "yearofthesnakeevent-",
bannerKey: "yearofthesnakeevent",
scale: 0.21,
availableLangs: ["en", "de", "it", "fr", "ja", "ko", "es-ES", "pt-BR", "zh-CN"],
eventEncounters: [
@ -169,6 +182,12 @@ const timedEvents: TimedEvent[] = [
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",
@ -177,7 +196,7 @@ const timedEvents: TimedEvent[] = [
endDate: new Date(Date.UTC(2025, 1, 21)),
boostFusions: true,
shinyMultiplier: 2,
bannerKey: "valentines2025event-",
bannerKey: "valentines2025event",
scale: 0.21,
availableLangs: ["en", "de", "it", "fr", "ja", "ko", "es-ES", "pt-BR", "zh-CN"],
eventEncounters: [
@ -203,6 +222,12 @@ const timedEvents: TimedEvent[] = [
{ 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",
@ -210,7 +235,7 @@ const timedEvents: TimedEvent[] = [
startDate: new Date(Date.UTC(2025, 1, 27)),
endDate: new Date(Date.UTC(2025, 2, 4)),
classicFriendshipMultiplier: 4,
bannerKey: "pkmnday2025event-",
bannerKey: "pkmnday2025event",
scale: 0.21,
availableLangs: ["en", "de", "it", "fr", "ja", "ko", "es-ES", "pt-BR", "zh-CN"],
eventEncounters: [
@ -248,6 +273,22 @@ const timedEvents: TimedEvent[] = [
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: "APRF25",
eventType: EventType.NO_TIMER_DISPLAY,
startDate: new Date(Date.UTC(2025, 2, 1)),
endDate: new Date(Date.UTC(2025, 3, 3)),
// bannerKey: "aprf25-",
// scale: 0.21,
// availableLangs: ["en", "de", "it", "fr", "ja", "ko", "es-ES", "pt-BR", "zh-CN"],
trainerShinyChance: 32000, // 16384/65536 = 1/4
},
];
@ -283,6 +324,12 @@ export class TimedEventManager {
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
@ -417,6 +464,30 @@ export class TimedEventManager {
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;
}
}
export class TimedEventDisplay extends Phaser.GameObjects.Container {

View File

@ -8,6 +8,7 @@ import { TimedEventDisplay } from "#app/timed-event-manager";
import { version } from "../../package.json";
import { pokerogueApi } from "#app/plugins/api/pokerogue-api";
import { globalScene } from "#app/global-scene";
import { timedEventManager } from "#app/global-event-manager";
export default class TitleUiHandler extends OptionSelectUiHandler {
/** If the stats can not be retrieved, use this fallback value */
@ -40,8 +41,8 @@ export default class TitleUiHandler extends OptionSelectUiHandler {
logo.setOrigin(0.5, 0);
this.titleContainer.add(logo);
if (globalScene.eventManager.isEventActive()) {
this.eventDisplay = new TimedEventDisplay(0, 0, globalScene.eventManager.activeEvent());
if (timedEventManager.isEventActive()) {
this.eventDisplay = new TimedEventDisplay(0, 0, timedEventManager.activeEvent());
this.eventDisplay.setup();
this.titleContainer.add(this.eventDisplay);
}
@ -116,7 +117,7 @@ export default class TitleUiHandler extends OptionSelectUiHandler {
const ui = this.getUi();
if (globalScene.eventManager.isEventActive()) {
if (timedEventManager.isEventActive()) {
this.eventDisplay.setWidth(globalScene.scaledCanvas.width - this.optionSelectBg.width - this.optionSelectBg.x);
this.eventDisplay.show();
}