diff --git a/public/images/events/halloween2024-event-de.png b/public/images/events/halloween2024-event-de.png new file mode 100644 index 00000000000..a56053c73cc Binary files /dev/null and b/public/images/events/halloween2024-event-de.png differ diff --git a/public/images/events/halloween2024-event-en.png b/public/images/events/halloween2024-event-en.png new file mode 100644 index 00000000000..3886fa796ae Binary files /dev/null and b/public/images/events/halloween2024-event-en.png differ diff --git a/public/images/events/halloween2024-event-es.png b/public/images/events/halloween2024-event-es.png new file mode 100644 index 00000000000..dc9bac86cd0 Binary files /dev/null and b/public/images/events/halloween2024-event-es.png differ diff --git a/public/images/events/halloween2024-event-fr.png b/public/images/events/halloween2024-event-fr.png new file mode 100644 index 00000000000..21df18c7471 Binary files /dev/null and b/public/images/events/halloween2024-event-fr.png differ diff --git a/public/images/events/halloween2024-event-it.png b/public/images/events/halloween2024-event-it.png new file mode 100644 index 00000000000..ce2316b2ce5 Binary files /dev/null and b/public/images/events/halloween2024-event-it.png differ diff --git a/public/images/events/halloween2024-event-ja.png b/public/images/events/halloween2024-event-ja.png new file mode 100644 index 00000000000..363f3b33b0d Binary files /dev/null and b/public/images/events/halloween2024-event-ja.png differ diff --git a/public/images/events/halloween2024-event-ko.png b/public/images/events/halloween2024-event-ko.png new file mode 100644 index 00000000000..3f52526a296 Binary files /dev/null and b/public/images/events/halloween2024-event-ko.png differ diff --git a/public/images/events/halloween2024-event-pt-BR.png b/public/images/events/halloween2024-event-pt-BR.png new file mode 100644 index 00000000000..764a9c1daa9 Binary files /dev/null and b/public/images/events/halloween2024-event-pt-BR.png differ diff --git a/public/images/events/halloween2024-event-zh-CN.png b/public/images/events/halloween2024-event-zh-CN.png new file mode 100644 index 00000000000..c2096a05d98 Binary files /dev/null and b/public/images/events/halloween2024-event-zh-CN.png differ diff --git a/src/data/trainer-config.ts b/src/data/trainer-config.ts index bc69b611075..18bcec1f928 100644 --- a/src/data/trainer-config.ts +++ b/src/data/trainer-config.ts @@ -203,6 +203,7 @@ export class TrainerConfig { public modifierRewardFuncs: ModifierTypeFunc[] = []; public partyTemplates: TrainerPartyTemplate[]; public partyTemplateFunc: PartyTemplateFunc; + public eventRewardFuncs: ModifierTypeFunc[] = []; public partyMemberFuncs: PartyMemberFuncs = {}; public speciesPools: TrainerTierPools; public speciesFilter: PokemonSpeciesFilter; @@ -546,6 +547,17 @@ export class TrainerConfig { return this; } + setEventModifierRewardFuncs(...modifierTypeFuncs: (() => ModifierTypeFunc)[]): TrainerConfig { + this.eventRewardFuncs = modifierTypeFuncs.map(func => () => { + const modifierTypeFunc = func(); + const modifierType = modifierTypeFunc(); + modifierType.withIdFromFunc(modifierTypeFunc); + return modifierType; + }); + return this; + } + + setModifierRewardFuncs(...modifierTypeFuncs: (() => ModifierTypeFunc)[]): TrainerConfig { this.modifierRewardFuncs = modifierTypeFuncs.map(func => () => { const modifierTypeFunc = func(); @@ -1828,10 +1840,12 @@ export const trainerConfigs: TrainerConfigs = { [TrainerType.RIVAL]: new TrainerConfig((t = TrainerType.RIVAL)).setName("Finn").setHasGenders("Ivy").setHasCharSprite().setTitle("Rival").setStaticParty().setEncounterBgm(TrainerType.RIVAL).setBattleBgm("battle_rival").setMixedBattleBgm("battle_rival").setPartyTemplates(trainerPartyTemplates.RIVAL) .setModifierRewardFuncs(() => modifierTypes.SUPER_EXP_CHARM, () => modifierTypes.EXP_SHARE) + .setEventModifierRewardFuncs(() => modifierTypes.SHINY_CHARM, () => modifierTypes.ABILITY_CHARM) .setPartyMemberFunc(0, getRandomPartyMemberFunc([ Species.BULBASAUR, Species.CHARMANDER, Species.SQUIRTLE, Species.CHIKORITA, Species.CYNDAQUIL, Species.TOTODILE, Species.TREECKO, Species.TORCHIC, Species.MUDKIP, Species.TURTWIG, Species.CHIMCHAR, Species.PIPLUP, Species.SNIVY, Species.TEPIG, Species.OSHAWOTT, Species.CHESPIN, Species.FENNEKIN, Species.FROAKIE, Species.ROWLET, Species.LITTEN, Species.POPPLIO, Species.GROOKEY, Species.SCORBUNNY, Species.SOBBLE, Species.SPRIGATITO, Species.FUECOCO, Species.QUAXLY ], TrainerSlot.TRAINER, true)) .setPartyMemberFunc(1, getRandomPartyMemberFunc([ Species.PIDGEY, Species.HOOTHOOT, Species.TAILLOW, Species.STARLY, Species.PIDOVE, Species.FLETCHLING, Species.PIKIPEK, Species.ROOKIDEE, Species.WATTREL ], TrainerSlot.TRAINER, true)), [TrainerType.RIVAL_2]: new TrainerConfig(++t).setName("Finn").setHasGenders("Ivy").setHasCharSprite().setTitle("Rival").setStaticParty().setMoneyMultiplier(1.25).setEncounterBgm(TrainerType.RIVAL).setBattleBgm("battle_rival").setMixedBattleBgm("battle_rival").setPartyTemplates(trainerPartyTemplates.RIVAL_2) .setModifierRewardFuncs(() => modifierTypes.EXP_SHARE) + .setEventModifierRewardFuncs(() => modifierTypes.SHINY_CHARM) .setPartyMemberFunc(0, getRandomPartyMemberFunc([ Species.IVYSAUR, Species.CHARMELEON, Species.WARTORTLE, Species.BAYLEEF, Species.QUILAVA, Species.CROCONAW, Species.GROVYLE, Species.COMBUSKEN, Species.MARSHTOMP, Species.GROTLE, Species.MONFERNO, Species.PRINPLUP, Species.SERVINE, Species.PIGNITE, Species.DEWOTT, Species.QUILLADIN, Species.BRAIXEN, Species.FROGADIER, Species.DARTRIX, Species.TORRACAT, Species.BRIONNE, Species.THWACKEY, Species.RABOOT, Species.DRIZZILE, Species.FLORAGATO, Species.CROCALOR, Species.QUAXWELL ], TrainerSlot.TRAINER, true)) .setPartyMemberFunc(1, getRandomPartyMemberFunc([ Species.PIDGEOTTO, Species.HOOTHOOT, Species.TAILLOW, Species.STARAVIA, Species.TRANQUILL, Species.FLETCHINDER, Species.TRUMBEAK, Species.CORVISQUIRE, Species.WATTREL ], TrainerSlot.TRAINER, true)) .setPartyMemberFunc(2, getSpeciesFilterRandomPartyMemberFunc((species: PokemonSpecies) => !pokemonEvolutions.hasOwnProperty(species.speciesId) && !pokemonPrevolutions.hasOwnProperty(species.speciesId) && species.baseTotal >= 450)), diff --git a/src/field/pokemon.ts b/src/field/pokemon.ts index a708b2067a7..4cb8390d4f4 100644 --- a/src/field/pokemon.ts +++ b/src/field/pokemon.ts @@ -4114,7 +4114,11 @@ export class PlayerPokemon extends Pokemon { fusionStarterSpeciesId ? this.scene.gameData.starterData[fusionStarterSpeciesId] : null ].filter(d => !!d); const amount = new Utils.IntegerHolder(friendship); - const starterAmount = new Utils.IntegerHolder(Math.floor(friendship * (this.scene.gameMode.isClassic && friendship > 0 ? CLASSIC_CANDY_FRIENDSHIP_MULTIPLIER : 1) / (fusionStarterSpeciesId ? 2 : 1))); + let candyFriendshipMultiplier = CLASSIC_CANDY_FRIENDSHIP_MULTIPLIER; + if (this.scene.eventManager.isEventActive()) { + candyFriendshipMultiplier *= this.scene.eventManager.getFriendshipMultiplier(); + } + const starterAmount = new Utils.IntegerHolder(Math.floor(friendship * (this.scene.gameMode.isClassic && friendship > 0 ? candyFriendshipMultiplier : 1) / (fusionStarterSpeciesId ? 2 : 1))); if (amount.value > 0) { this.scene.applyModifier(PokemonFriendshipBoosterModifier, true, this, amount); this.scene.applyModifier(PokemonFriendshipBoosterModifier, true, this, starterAmount); diff --git a/src/loading-scene.ts b/src/loading-scene.ts index 26936bcdaad..578b9aba4fc 100644 --- a/src/loading-scene.ts +++ b/src/loading-scene.ts @@ -246,9 +246,9 @@ export class LoadingScene extends SceneBase { } const availableLangs = [ "en", "de", "it", "fr", "ja", "ko", "es", "pt-BR", "zh-CN" ]; if (lang && availableLangs.includes(lang)) { - this.loadImage("egg-update_" + lang, "events"); + this.loadImage("halloween2024-event-" + lang, "events"); } else { - this.loadImage("egg-update_en", "events"); + this.loadImage("halloween2024-event-en", "events"); } this.loadAtlas("statuses", ""); diff --git a/src/modifier/modifier-type.ts b/src/modifier/modifier-type.ts index f4b59b9d882..cf2d74483f9 100644 --- a/src/modifier/modifier-type.ts +++ b/src/modifier/modifier-type.ts @@ -1301,13 +1301,12 @@ function lureWeightFunc(maxBattles: number, weight: number): WeightedModifierTyp return !(party[0].scene.gameMode.isClassic && party[0].scene.currentBattle.waveIndex === 199) && (lures.length === 0 || lures.filter(m => m.getMaxBattles() === maxBattles && m.getBattleCount() >= maxBattles * 0.6).length === 0) ? weight : 0; }; } - class WeightedModifierType { public modifierType: ModifierType; public weight: integer | WeightedModifierTypeWeightFunc; - public maxWeight: integer; + public maxWeight: integer | WeightedModifierTypeWeightFunc; - constructor(modifierTypeFunc: ModifierTypeFunc, weight: integer | WeightedModifierTypeWeightFunc, maxWeight?: integer) { + constructor(modifierTypeFunc: ModifierTypeFunc, weight: integer | WeightedModifierTypeWeightFunc, maxWeight?: integer | WeightedModifierTypeWeightFunc) { this.modifierType = modifierTypeFunc(); this.modifierType.id = Object.keys(modifierTypes).find(k => modifierTypes[k] === modifierTypeFunc)!; // TODO: is this bang correct? this.weight = weight; @@ -1694,7 +1693,10 @@ const modifierPool: ModifierPool = { new WeightedModifierType(modifierTypes.EVOLUTION_ITEM, (party: Pokemon[]) => { return Math.min(Math.ceil(party[0].scene.currentBattle.waveIndex / 15), 8); }, 8), - new WeightedModifierType(modifierTypes.MAP, (party: Pokemon[]) => party[0].scene.gameMode.isClassic && party[0].scene.currentBattle.waveIndex < 180 ? 1 : 0, 1), + new WeightedModifierType(modifierTypes.MAP, + (party: Pokemon[]) => party[0].scene.gameMode.isClassic && party[0].scene.currentBattle.waveIndex < 180 ? party[0].scene.eventManager.isEventActive() ? 2 : 1 : 0, + (party: Pokemon[]) => party[0].scene.eventManager.isEventActive() ? 2 : 1), + new WeightedModifierType(modifierTypes.SOOTHE_BELL, (party: Pokemon[]) => party[0].scene.eventManager.isEventActive() ? 3 : 0), new WeightedModifierType(modifierTypes.TM_GREAT, 3), new WeightedModifierType(modifierTypes.MEMORY_MUSHROOM, (party: Pokemon[]) => { if (!party.find(p => p.getLearnableLevelMoves().length)) { @@ -1762,7 +1764,7 @@ const modifierPool: ModifierPool = { new WeightedModifierType(modifierTypes.CANDY_JAR, skipInLastClassicWaveOrDefault(5)), new WeightedModifierType(modifierTypes.ATTACK_TYPE_BOOSTER, 9), new WeightedModifierType(modifierTypes.TM_ULTRA, 11), - new WeightedModifierType(modifierTypes.RARER_CANDY, 4), + new WeightedModifierType(modifierTypes.RARER_CANDY, (party: Pokemon[]) => party[0].scene.eventManager.isEventActive() ? 6 : 4), new WeightedModifierType(modifierTypes.GOLDEN_PUNCH, skipInLastClassicWaveOrDefault(2)), new WeightedModifierType(modifierTypes.IV_SCANNER, skipInLastClassicWaveOrDefault(4)), new WeightedModifierType(modifierTypes.EXP_CHARM, skipInLastClassicWaveOrDefault(8)), @@ -1785,7 +1787,7 @@ const modifierPool: ModifierPool = { new WeightedModifierType(modifierTypes.BATON, 2), new WeightedModifierType(modifierTypes.SOUL_DEW, 7), //new WeightedModifierType(modifierTypes.OVAL_CHARM, 6), - new WeightedModifierType(modifierTypes.SOOTHE_BELL, 4), + new WeightedModifierType(modifierTypes.SOOTHE_BELL, (party: Pokemon[]) => party[0].scene.eventManager.isEventActive() ? 0 : 4), new WeightedModifierType(modifierTypes.ABILITY_CHARM, skipInClassicAfterWave(189, 6)), new WeightedModifierType(modifierTypes.FOCUS_BAND, 5), new WeightedModifierType(modifierTypes.KINGS_ROCK, 3), diff --git a/src/phases/trainer-victory-phase.ts b/src/phases/trainer-victory-phase.ts index fd48b91b1a1..dc1b962f47e 100644 --- a/src/phases/trainer-victory-phase.ts +++ b/src/phases/trainer-victory-phase.ts @@ -27,6 +27,12 @@ export class TrainerVictoryPhase extends BattlePhase { this.scene.unshiftPhase(new ModifierRewardPhase(this.scene, modifierRewardFunc)); } + if (this.scene.eventManager.isEventActive()) { + for (const rewardFunc of this.scene.currentBattle.trainer?.config.eventRewardFuncs!) { + this.scene.unshiftPhase(new ModifierRewardPhase(this.scene, rewardFunc)); + } + } + const trainerType = this.scene.currentBattle.trainer?.config.trainerType!; // TODO: is this bang correct? if (vouchers.hasOwnProperty(TrainerType[trainerType])) { if (!this.scene.validateVoucher(vouchers[TrainerType[trainerType]]) && this.scene.currentBattle.trainer?.config.isBoss) { diff --git a/src/timed-event-manager.ts b/src/timed-event-manager.ts index 513d21bd2f3..953ac0566f6 100644 --- a/src/timed-event-manager.ts +++ b/src/timed-event-manager.ts @@ -5,13 +5,13 @@ import i18next from "i18next"; export enum EventType { SHINY, - GENERIC + NO_TIMER_DISPLAY } interface EventBanner { bannerKey?: string; - xPosition?: number; - yPosition?: number; + xOffset?: number; + yOffset?: number; scale?: number; availableLangs?: string[]; } @@ -20,19 +20,20 @@ interface TimedEvent extends EventBanner { name: string; eventType: EventType; shinyMultiplier?: number; + friendshipMultiplier?: number; startDate: Date; endDate: Date; } const timedEvents: TimedEvent[] = [ { - name: "Egg Skip Update", - eventType: EventType.GENERIC, - startDate: new Date(Date.UTC(2024, 8, 8, 0)), - endDate: new Date(Date.UTC(2024, 8, 12, 0)), - bannerKey: "egg-update", - xPosition: 19, - yPosition: 120, + name: "Halloween Update", + eventType: EventType.SHINY, + shinyMultiplier: 2, + friendshipMultiplier: 2, + startDate: new Date(Date.UTC(2024, 9, 25, 0)), + endDate: new Date(Date.UTC(2024, 10, 4, 0)), + bannerKey: "halloween2024-event-", scale: 0.21, availableLangs: [ "en", "de", "it", "fr", "ja", "ko", "es", "pt-BR", "zh-CN" ] } @@ -61,6 +62,16 @@ 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)); @@ -80,42 +91,63 @@ export class TimedEventDisplay extends Phaser.GameObjects.Container { private event: TimedEvent | nil; private eventTimerText: Phaser.GameObjects.Text; private banner: Phaser.GameObjects.Image; - private bannerShadow: Phaser.GameObjects.Rectangle; + private availableWidth: number; private eventTimer: NodeJS.Timeout | null; constructor(scene: BattleScene, x: number, y: number, event?: TimedEvent) { super(scene, x, y); + this.availableWidth = scene.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; + key += lang; } else { - key += "_en"; + key += "en"; } } console.log(this.event.bannerKey); - this.banner = new Phaser.GameObjects.Image(this.scene, this.event.xPosition ?? 29, this.event.yPosition ?? 64, key); + const padding = 5; + const showTimer = this.event.eventType !== EventType.NO_TIMER_DISPLAY; + const yPosition = this.scene.game.canvas.height / 6 - padding - (showTimer ? 10 : 0) - (this.event.yOffset ?? 0); + this.banner = new Phaser.GameObjects.Image(this.scene, this.availableWidth / 2, yPosition - padding, key); this.banner.setName("img-event-banner"); - this.banner.setOrigin(0.08, -0.35); + this.banner.setOrigin(0.5, 1); this.banner.setScale(this.event.scale ?? 0.18); - if (this.event.eventType !== EventType.GENERIC) { + if (showTimer) { this.eventTimerText = addTextObject( this.scene, - this.banner.x + 8, - this.banner.y + 100, + 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, 0); + this.eventTimerText.setOrigin(0.5, 0); this.add(this.eventTimerText); } @@ -161,7 +193,7 @@ export class TimedEventDisplay extends Phaser.GameObjects.Container { } updateCountdown() { - if (this.event && this.event.eventType !== EventType.GENERIC) { + if (this.event && this.event.eventType !== EventType.NO_TIMER_DISPLAY) { this.eventTimerText.setText(this.timeToGo(this.event.endDate)); } } diff --git a/src/ui/title-ui-handler.ts b/src/ui/title-ui-handler.ts index f7972af2cc2..3bfba71ef08 100644 --- a/src/ui/title-ui-handler.ts +++ b/src/ui/title-ui-handler.ts @@ -103,6 +103,7 @@ export default class TitleUiHandler extends OptionSelectUiHandler { const ui = this.getUi(); if (this.scene.eventManager.isEventActive()) { + this.eventDisplay.setWidth(this.scene.scaledCanvas.width - this.optionSelectBg.width - this.optionSelectBg.x); this.eventDisplay.show(); }