overlord_event_system/async_handler/
handler.rs

1use crate::{
2    attributes, cases::try_finalize_item, event::OverlordEvent,
3    gacha::item_case::generate_item_from_template, game_config_helpers::GameConfigLookup,
4    script::ScriptRunner, state::OverlordState,
5};
6
7use configs::game_config::GameConfig;
8
9use analytics::constants::METRICS_TARGET;
10
11use essences::{
12    ad_usage::{AdPlacement, AdUsageData},
13    bundles::{BundleAbility, BundleElement, BundleRawStep},
14    character_state::CharacterState,
15    currency::{
16        CurrencyConsumer, CurrencySource, CurrencyUnit, check_can_decrease_currencies,
17        decrease_currencies, from_es_currencies, increase_currencies,
18    },
19    entity::EntityState,
20    fighting::ActiveFight,
21    items::Item,
22    mail::Mail,
23    offers::OfferTemplate,
24    quest::{QuestGroupType, QuestInstance},
25};
26
27use event_system::event::EventRhaiEnum;
28pub use event_system::{
29    event::{EventPluginized, EventStruct},
30    plugin::{cron::CronMark, delayed::DelayedMark},
31    script::types::ConditionalProgress,
32    system::{AsyncEventHandler, EventHandleResult},
33};
34use rand::TryRngCore;
35use rand::{
36    SeedableRng,
37    rngs::{OsRng, StdRng},
38};
39
40#[derive(Debug)]
41pub struct OverlordAsyncEventHandler {
42    pub(super) game_config: configs::SharedGameConfig,
43    pub(super) script_runner: ScriptRunner<OverlordEvent, OverlordState>,
44    #[allow(dead_code)]
45    pub(super) frontend: bool,
46    pub(super) start_fight_tick: u64,
47    pub(super) compute_fields_duration: opentelemetry::metrics::Histogram<f64>,
48    /// Tracks the source/consumer for currency change metrics logging.
49    pub(super) last_currency_source: Option<String>,
50}
51
52impl AsyncEventHandler<OverlordEvent, OverlordState> for OverlordAsyncEventHandler {
53    #[tracing::instrument(
54        skip_all,
55        fields(
56            character_id = %state.character_state.character.id,
57            current_power = %state.character_state.character.power,
58            event_type = %event,
59            sampling_label = tracing::field::Empty,
60        ),
61        target = METRICS_TARGET,
62    )]
63    fn handle_event(
64        &mut self,
65        event: &OverlordEvent,
66        state: OverlordState,
67        rand_gen: rand::rngs::StdRng,
68        current_tick: u64,
69    ) -> EventHandleResult<OverlordEvent, OverlordState> {
70        self.last_currency_source = match event {
71            OverlordEvent::CurrencyIncrease {
72                currency_source, ..
73            } => Some(format!("{currency_source:?}")),
74            OverlordEvent::CurrencyDecrease {
75                currency_consumer, ..
76            } => Some(format!("{currency_consumer:?}")),
77            other => Some(other.to_string()),
78        };
79        let game_config = self.game_config.get();
80        let mut events: Vec<EventPluginized<OverlordEvent, OverlordState>> = Vec::new();
81        if let Some(fight) = &state.active_fight {
82            for entity in &fight.entities {
83                for effect_id in &entity.effect_ids {
84                    let Ok(effect) = game_config.require_effect(*effect_id) else {
85                        tracing::error!("Couldn't find effect in config with id = {effect_id}");
86                        return EventHandleResult::fail(state);
87                    };
88                    if let Some(events_subscribe) = &effect.events_subscribe
89                        && events_subscribe.contains(&event.to_string())
90                    {
91                        events.push(EventPluginized::now(OverlordEvent::CastEffectFromEvent {
92                            entity_id: entity.id,
93                            effect_id: effect.id,
94                            caller_event: Box::new(event.clone()),
95                        }));
96                    }
97                }
98            }
99        }
100
101        let mut result = match &event {
102            // Items
103            OverlordEvent::OpenItemCase { batch_size } => {
104                self.handle_open_item_case(*batch_size, state)
105            }
106            OverlordEvent::AutoChestOpenItemCase { batch_size } => {
107                self.handle_auto_chest_open_item_case(*batch_size, state)
108            }
109            OverlordEvent::PlayerEquipItem { item_id } => {
110                self.handle_player_equip_item(*item_id, state)
111            }
112            OverlordEvent::SellItem { item_id } => self.handle_sell_item(*item_id, state),
113            OverlordEvent::ItemSold { .. } => self.handle_noop(state),
114            OverlordEvent::PlayerNewItems { items } => self.handle_player_new_items(items, state),
115            OverlordEvent::UpgradeItemCase {} => self.handle_noop(state),
116            OverlordEvent::ItemCaseUpgraded {} => self.handle_noop(state),
117            OverlordEvent::SpeedupUpgradeItemCase {} => self.handle_noop(state),
118            OverlordEvent::SkipUpgradeItemCase {} => self.handle_noop(state),
119            OverlordEvent::ClaimUpgradeItemCase {} => self.handle_noop(state),
120
121            OverlordEvent::EnableAutoSell {} => self.handle_enable_auto_sell(state),
122            OverlordEvent::DisableAutoSell {} => self.handle_disable_auto_sell(state),
123
124            OverlordEvent::SetGearOverrideEnabled { item_type, enabled } => {
125                self.handle_set_gear_override_enabled(*item_type, *enabled, state)
126            }
127
128            OverlordEvent::EnableCaseUpgradePopUp {} => {
129                self.handle_enable_case_upgrade_pop_up(state)
130            }
131            OverlordEvent::DisableCaseUpgradePopUp {} => {
132                self.handle_disable_case_upgrade_pop_up(state)
133            }
134
135            // Abilities
136            OverlordEvent::OpenAbilityCase { .. } => self.handle_noop(state),
137            OverlordEvent::SetAbilityGachaWishlist { .. } => self.handle_noop(state),
138            OverlordEvent::UpgradeAbilitySlot { .. } => self.handle_noop(state),
139            OverlordEvent::AbilityCaseOpened { .. } => self.handle_noop(state),
140            OverlordEvent::NewAbilities { .. } => self.handle_noop(state),
141            OverlordEvent::UpgradeAbilityCase {} => self.handle_noop(state),
142            OverlordEvent::FastEquipAbilities {} => self.handle_noop(state),
143            OverlordEvent::EquipAbility {
144                slot_id,
145                ability_id,
146            } => self.handle_equip_ability(*slot_id, *ability_id, current_tick, state),
147            OverlordEvent::UnequipAbility { slot_id } => {
148                self.handle_unequip_ability(*slot_id, state)
149            }
150            OverlordEvent::EquipAbilities { equipped_abilities } => {
151                self.handle_equip_abilities(equipped_abilities.clone(), current_tick, state)
152            }
153            OverlordEvent::UpgradeAbility { .. } => self.handle_noop(state),
154            OverlordEvent::UpgradeAllAbilities {} => self.handle_noop(state),
155            OverlordEvent::UpgradedAbilities { .. } => self.handle_noop(state),
156
157            // Fight management
158            OverlordEvent::StartGame {} => self.handle_noop(state),
159            OverlordEvent::PrepareFight { prepare_fight_type } => {
160                self.handle_prepare_fight(prepare_fight_type.clone(), rand_gen, state)
161            }
162            OverlordEvent::StartFight { fight_id } => {
163                self.handle_start_fight(event.clone(), *fight_id, rand_gen, current_tick, state)
164            }
165            OverlordEvent::EndFight {
166                fight_id,
167                is_win,
168                pvp_state,
169            } => self.handle_end_fight(*fight_id, *is_win, pvp_state, state),
170            OverlordEvent::StageCleared {} => self.handle_noop(state),
171            OverlordEvent::RaidDungeon { .. } => self.handle_noop(state),
172
173            // Moving
174            OverlordEvent::StartMove {
175                entity_id,
176                to,
177                duration_ticks,
178            } => self.handle_start_move(*entity_id, to.clone(), *duration_ticks, state),
179            OverlordEvent::EndMove { entity_id } => self.handle_end_move(*entity_id, state),
180
181            // Fighting
182            OverlordEvent::SpawnEntity {
183                id,
184                entity_template_id,
185                position,
186                entity_team,
187                has_big_hp_bar,
188                entity_attributes,
189            } => self.handle_spawn_entity(
190                *id,
191                *entity_template_id,
192                position.clone(),
193                entity_team.clone(),
194                *has_big_hp_bar,
195                entity_attributes.clone(),
196                current_tick,
197                state,
198            ),
199            OverlordEvent::FightProgress {} => self.handle_fight_progress(current_tick, state),
200            OverlordEvent::StartCastAbility {
201                by_entity_id,
202                ability_id,
203                ..
204            } => self.handle_start_cast_ability(
205                event.clone(),
206                *by_entity_id,
207                *ability_id,
208                rand_gen,
209                current_tick,
210                state,
211            ),
212            OverlordEvent::StartedCastAbility { .. } => self.handle_noop(state),
213            OverlordEvent::CastAbility {
214                by_entity_id,
215                to_entity_id,
216                ability_id,
217                ..
218            } => self.handle_cast_ability(
219                event.clone(),
220                *by_entity_id,
221                *to_entity_id,
222                *ability_id,
223                rand_gen,
224                current_tick,
225                state,
226            ),
227            OverlordEvent::StartCastProjectile {
228                by_entity_id,
229                to_entity_id,
230                projectile_id,
231                level,
232                delay: _,
233            } => self.handle_start_cast_projectile(
234                event.clone(),
235                *by_entity_id,
236                *to_entity_id,
237                *projectile_id,
238                *level,
239                current_tick,
240                state,
241            ),
242            OverlordEvent::StartedCastProjectile { .. } => self.handle_noop(state),
243            OverlordEvent::CastProjectile {
244                by_entity_id,
245                to_entity_id,
246                projectile_id,
247                level,
248                projectile_data,
249            } => self.handle_cast_projectile(
250                event.clone(),
251                *by_entity_id,
252                *to_entity_id,
253                *projectile_id,
254                *level,
255                projectile_data,
256                rand_gen,
257                current_tick,
258                state,
259            ),
260            OverlordEvent::Damage {
261                entity_id,
262                damage,
263                damage_data: _,
264            } => self.handle_damage(*entity_id, *damage, current_tick, rand_gen, state),
265            OverlordEvent::Heal { entity_id, heal } => self.handle_heal(*entity_id, *heal, state),
266            OverlordEvent::CounterAttack { .. } => self.handle_noop(state),
267            OverlordEvent::WaveCleared {} => self.handle_noop(state),
268            OverlordEvent::Multicast { .. } => self.handle_noop(state),
269            OverlordEvent::Evasion { .. } => self.handle_noop(state),
270            OverlordEvent::PlayerDeath {} => self.handle_player_death(state),
271            OverlordEvent::EntityDeath { entity_id, reward } => {
272                self.handle_entity_death(*entity_id, reward.clone(), rand_gen, state)
273            }
274            OverlordEvent::EntityIncrAttribute {
275                entity_id,
276                attribute,
277                delta,
278            } => self.handle_entity_incr_attribute(*entity_id, attribute, *delta, state),
279            OverlordEvent::EntityApplyEffect {
280                entity_id,
281                effect_id,
282            } => self.handle_entity_apply_effect(*entity_id, *effect_id, current_tick, state),
283            OverlordEvent::CastEffect {
284                entity_id,
285                effect_id,
286            } => {
287                self.handle_cast_effect(*entity_id, *effect_id, None, rand_gen, current_tick, state)
288            }
289            OverlordEvent::CastEffectFromEvent {
290                entity_id,
291                effect_id,
292                caller_event,
293            } => self.handle_cast_effect(
294                *entity_id,
295                *effect_id,
296                Some(caller_event.to_owned()),
297                rand_gen,
298                current_tick,
299                state,
300            ),
301            OverlordEvent::FightCustomEvent { .. } => self.handle_noop(state),
302            OverlordEvent::FightVisualEvent { .. } => self.handle_noop(state),
303            OverlordEvent::NewCharacterLevel { level } => {
304                self.handle_new_character_level(*level, state)
305            }
306
307            // Vassal links
308            OverlordEvent::ClaimSuzerainReward {} => self.handle_noop(state),
309            OverlordEvent::ClaimVassalReward { .. } => self.handle_noop(state),
310            OverlordEvent::NewSuzerain { new_suzerain } => {
311                self.handle_new_suzerain(new_suzerain.clone(), state)
312            }
313            OverlordEvent::RemoveVassal { vassal_id } => {
314                self.handle_remove_vassal(*vassal_id, state)
315            }
316
317            // Quests
318            OverlordEvent::ClaimQuest { quest_id } => {
319                self.handle_claim_quest(*quest_id, rand_gen, state)
320            }
321            OverlordEvent::NewQuests { quest_ids } => {
322                self.handle_new_quests(quest_ids.clone(), state)
323            }
324            OverlordEvent::UpdateActiveLoopTaskId { quest_id } => {
325                self.handle_update_active_loop_task_id(*quest_id, state)
326            }
327
328            OverlordEvent::ClaimQuestProgressionReward { quest_group_type } => {
329                self.handle_claim_quest_progression_reward(quest_group_type.to_owned(), state)
330            }
331
332            OverlordEvent::ResetRepeatingQuests { quest_ids } => {
333                self.handle_reset_repeating_quests(quest_ids.clone(), state)
334            }
335
336            // Vassal Tasks
337            OverlordEvent::GiveTask { .. } => self.handle_noop(state),
338            OverlordEvent::NewTask { new_task } => self.handle_new_task(new_task.clone(), state),
339            OverlordEvent::AcceptTask { task_id, is_good } => {
340                self.handle_accept_task(*task_id, *is_good, state)
341            }
342            OverlordEvent::TaskAccepted {
343                task_id,
344                started_good,
345                started_at,
346                finish_at,
347            } => self.handle_task_accepted(*task_id, *started_good, *started_at, *finish_at, state),
348            OverlordEvent::HitHands { task_id } => self.handle_hit_hands(*task_id, state),
349            OverlordEvent::HandsHitted { task_id } => self.handle_hands_hitted(*task_id, state),
350            OverlordEvent::ClaimTaskReward { task_id } => {
351                self.handle_claim_task_reward(*task_id, state)
352            }
353            OverlordEvent::TaskFinished { task_id } => self.handle_finish_task(*task_id, state),
354
355            // Resist Task
356            OverlordEvent::GiveResistTask { new_resist_task } => {
357                self.handle_give_resist_task(new_resist_task.clone(), state)
358            }
359            OverlordEvent::AcceptResistTask {} => self.handle_noop(state),
360            OverlordEvent::ResistTaskAccepted { new_resist_task } => {
361                self.handle_resist_task_accepted(new_resist_task.clone(), state)
362            }
363            OverlordEvent::CatchResistTask { task_id } => {
364                self.handle_catch_resist_task(*task_id, state)
365            }
366            OverlordEvent::ResistTaskCatched { task_id } => {
367                self.handle_resist_task_catched(*task_id, state)
368            }
369            OverlordEvent::ResistTaskFinished { task_id } => {
370                self.handle_resist_task_finished(*task_id, state)
371            }
372            OverlordEvent::ClaimResistTaskReward { task_id } => {
373                self.handle_claim_resist_task_reward(*task_id, state)
374            }
375
376            // Gifts
377            OverlordEvent::SendGift {
378                receiver_id,
379                config_gift_id,
380            } => self.handle_send_gift(*receiver_id, *config_gift_id, state),
381            OverlordEvent::NewGift { new_gift } => self.handle_new_gift(new_gift.clone(), state),
382            OverlordEvent::AcceptGift { gift_id } => self.handle_accept_gift(*gift_id, state),
383
384            // PVP
385            OverlordEvent::StartVassalPVPSync { .. } => self.handle_noop(state),
386            OverlordEvent::StartArenaPVPSync { .. } => self.handle_noop(state),
387            OverlordEvent::StartArenaRematchSync { .. } => self.handle_noop(state),
388            OverlordEvent::RefreshArenaMatchmaking {} => self.handle_noop(state),
389            OverlordEvent::BuyArenaTicket {} => self.handle_noop(state),
390
391            // Referral Rewards
392            OverlordEvent::ClaimReferralLvlUpReward { level } => {
393                self.handle_claim_referral_lvlup_reward(*level, state)
394            }
395            OverlordEvent::ClaimReferralDailyReward {} => {
396                self.handle_claim_referral_daily_reward(state)
397            }
398            OverlordEvent::PatronQuestCompleted { quest_id } => {
399                self.handle_patron_quest_completed(*quest_id, state)
400            }
401            OverlordEvent::HiddenQuestCompleted { quest_id } => {
402                self.handle_hidden_quest_completed(*quest_id, rand_gen, state)
403            }
404            OverlordEvent::QuestCompleted { .. } => self.handle_noop(state),
405            OverlordEvent::ReferralDailyRewardStatusUpdate {
406                referral_daily_reward_status,
407            } => self
408                .handle_referral_daily_reward_status_update(*referral_daily_reward_status, state),
409
410            // AutoChest
411            OverlordEvent::EnableAutoChest {} => self.handle_enable_auto_chest(state),
412            OverlordEvent::DisableAutoChest {} => self.handle_disable_auto_chest(state),
413
414            OverlordEvent::EnableAutoChestFilter { filter_id } => {
415                self.handle_enable_auto_chest_filter(*filter_id, state)
416            }
417            OverlordEvent::DisableAutoChestFilter {} => {
418                self.handle_disable_auto_chest_filter(state)
419            }
420
421            OverlordEvent::EnableAutoChestPowerCompare {} => {
422                self.handle_enable_auto_chest_power_compare(state)
423            }
424            OverlordEvent::DisableAutoChestPowerCompare {} => {
425                self.handle_disable_auto_chest_power_compare(state)
426            }
427
428            OverlordEvent::UpdateAutoChestBatchSize { batch_size } => {
429                self.handle_update_auto_chest_batch_size(*batch_size, state)
430            }
431
432            OverlordEvent::NewAutoChestFilter { filter } => {
433                self.handle_new_auto_chest_filter(filter.clone(), state)
434            }
435
436            OverlordEvent::UpdateAutoChestFilter { updated_filter } => {
437                self.handle_update_auto_chest_filter(updated_filter.clone(), state)
438            }
439
440            OverlordEvent::RemoveAutoChestFilter { filter_id } => {
441                self.handle_remove_auto_chest_filter(*filter_id, state)
442            }
443
444            // Technical
445            OverlordEvent::SetCustomValue { key, value } => {
446                self.handle_set_custom_value(key.clone(), *value, state)
447            }
448            OverlordEvent::SetConnectionStore { key, value } => {
449                self.handle_set_connection_store(key.clone(), *value, state)
450            }
451
452            // Ability Presets
453            OverlordEvent::CreateAbilityPreset { .. } => self.handle_noop(state),
454            OverlordEvent::EditAbilityPreset { .. } => self.handle_noop(state),
455
456            // AfkReward
457            OverlordEvent::ClaimAfkReward {} => self.handle_noop(state),
458            OverlordEvent::AfkRewardClaimed {} => self.handle_noop(state),
459            OverlordEvent::AfkRewardsGatingUnlocked {} => {
460                self.handle_afk_rewards_gating_unlocked(state)
461            }
462            OverlordEvent::ClaimAfkInstantRewardGems {} => self.handle_noop(state),
463
464            // Bundles
465            OverlordEvent::ClaimBundleStepGeneric { .. } => self.handle_noop(state),
466            OverlordEvent::AddBundleGroup { bundle_ids } => {
467                self.handle_add_bundle_group(bundle_ids, state)
468            }
469
470            // Classes
471            OverlordEvent::ChangeClass { .. } => self.handle_noop(state),
472
473            // User Account
474            OverlordEvent::LinkGuestAccount { .. } => self.handle_noop(state),
475            OverlordEvent::SetUsername { .. } => self.handle_noop(state),
476            OverlordEvent::SetCharacterBlocked { .. } => self.handle_noop(state),
477
478            // Cheat
479            OverlordEvent::SetMaxHp {
480                entity_id,
481                new_max_hp,
482                new_hp,
483            } => self.handle_set_max_hp(*entity_id, *new_max_hp, *new_hp, state),
484
485            OverlordEvent::RunCheat { cheat } => {
486                self.handle_run_cheat(cheat, current_tick, rand_gen, state)
487            }
488
489            OverlordEvent::Error { .. } => self.handle_noop(state),
490            OverlordEvent::CustomRhai { .. } => self.handle_noop(state),
491
492            // Currencies — the event itself performs the state mutation
493            OverlordEvent::CurrencyIncrease { currencies, .. } => {
494                self.handle_currency_increase(currencies, state)
495            }
496            OverlordEvent::CurrencyDecrease { currencies, .. } => {
497                self.handle_currency_decrease(currencies, state)
498            }
499            // Skins
500            OverlordEvent::BuySkins { .. } => self.handle_noop(state),
501            OverlordEvent::EquipAndUnequipSkins { .. } => self.handle_noop(state),
502
503            // Mails
504            OverlordEvent::ClaimMail { .. } => self.handle_noop(state),
505            OverlordEvent::ClaimAllMails {} => self.handle_noop(state),
506            OverlordEvent::ClaimAllQuests { .. } => self.handle_noop(state),
507            OverlordEvent::MakeRead { .. } => self.handle_noop(state),
508            OverlordEvent::MakeAllRead {} => self.handle_noop(state),
509            OverlordEvent::DeleteMail { .. } => self.handle_noop(state),
510            OverlordEvent::DeleteAllMails {} => self.handle_noop(state),
511            OverlordEvent::NewMail { new_mail } => self.handle_new_mail(new_mail.clone(), state),
512            // Offers
513            OverlordEvent::NewOffer { .. } => self.handle_noop(state),
514            OverlordEvent::BuyOffer { .. } => self.handle_noop(state),
515            OverlordEvent::ResetOffers { new_offers } => {
516                self.handle_reset_offers(new_offers, state)
517            }
518            OverlordEvent::OfferPurchaseCompleted { .. } => self.handle_noop(state),
519            OverlordEvent::OfferPurchaseFailed { .. } => self.handle_noop(state),
520            OverlordEvent::PurchasesBanned {} => self.handle_purchases_banned(state),
521
522            // Pets
523            OverlordEvent::EquipPet { slot_id, pet_id } => {
524                self.handle_equip_pet(*slot_id, *pet_id, current_tick, state)
525            }
526            OverlordEvent::UnequipPet { slot_id } => {
527                self.handle_unequip_pet(*slot_id, current_tick, state)
528            }
529            OverlordEvent::FastEquipPets {} => self.handle_noop(state),
530            OverlordEvent::EquipPets { equipped_pets } => {
531                self.handle_equip_pets(equipped_pets.clone(), current_tick, state)
532            }
533            OverlordEvent::UpgradePet { .. } => self.handle_noop(state),
534            OverlordEvent::UpgradeAllPets {} => self.handle_noop(state),
535            OverlordEvent::UpgradedPets { .. } => self.handle_noop(state),
536            OverlordEvent::UpgradePetSlot { .. } => self.handle_noop(state),
537
538            // Pet Gacha
539            OverlordEvent::OpenPetCase { .. } => self.handle_noop(state),
540            OverlordEvent::SetPetGachaWishlist { .. } => self.handle_noop(state),
541            OverlordEvent::PetCaseOpened { .. } => self.handle_noop(state),
542            OverlordEvent::NewPets { .. } => self.handle_noop(state),
543            OverlordEvent::UpgradePetCase {} => self.handle_noop(state),
544
545            // Tutorial
546            OverlordEvent::TutorialShown { .. } => self.handle_noop(state),
547            OverlordEvent::TutorialStepCompleted { step_number } => {
548                self.handle_tutorial_step_completed(*step_number, state)
549            }
550
551            // Party
552            OverlordEvent::AddCharacterToParty { .. } => self.handle_noop(state),
553            OverlordEvent::RemoveCharacterFromParty {} => self.handle_noop(state),
554            OverlordEvent::RefreshPartyPlayers {} => self.handle_noop(state),
555            OverlordEvent::RefreshPartyMemberState {} => self.handle_noop(state),
556
557            // Talent Tree
558            OverlordEvent::StartTalentResearch { .. } => self.handle_noop(state),
559            OverlordEvent::TalentResearchStarted { .. } => self.handle_noop(state),
560            OverlordEvent::SpeedupTalentResearch {} => self.handle_noop(state),
561            OverlordEvent::SkipTalentResearch {} => self.handle_noop(state),
562            OverlordEvent::ClaimTalentResearch {} => self.handle_claim_talent_research(state),
563
564            // Statue
565            OverlordEvent::StatueRoll { .. } => self.handle_noop(state),
566            OverlordEvent::StatueActivateSet { .. } => self.handle_noop(state),
567            OverlordEvent::StatueAddSet {} => self.handle_noop(state),
568            OverlordEvent::StatueRenameSet { .. } => self.handle_noop(state),
569            OverlordEvent::StatueLockSlot { .. } => self.handle_noop(state),
570            OverlordEvent::StatueRollNewSlot { .. } => self.handle_noop(state),
571            OverlordEvent::UserRating { .. } => self.handle_noop(state),
572            OverlordEvent::WatchAd { .. } => self.handle_noop(state),
573            OverlordEvent::ShowBird { .. } => self.handle_show_bird(state),
574            OverlordEvent::BirdShown {} => self.handle_bird_shown(state),
575            OverlordEvent::ResetAdUsage { placements } => {
576                self.handle_reset_ad_usage(placements, state)
577            }
578            OverlordEvent::ResetInstantRewardGemsPressCount {} => {
579                self.handle_reset_instant_reward_gems_press_count(state)
580            }
581        };
582
583        if result.success() {
584            let (state, events) = result.state_and_events_mut();
585            self.update_quests_progress(state, events, event);
586            self.try_give_new_offers(state, events, event);
587        }
588
589        result.events_mut().append(&mut events);
590        result
591    }
592
593    fn compute_fields(
594        &self,
595        state: &mut OverlordState,
596        prev_state: &OverlordState,
597    ) -> Vec<OverlordEvent> {
598        let _span = tracing::info_span!("compute_fields").entered();
599        let start = std::time::Instant::now();
600        let game_config = self.game_config.get();
601        let mut events = Vec::new();
602        self.log_currency_change_metrics(state, prev_state);
603
604        if state.character_state.character.character_experience
605            != prev_state.character_state.character.character_experience
606        {
607            events.append(&mut self.compute_character_level(state));
608        }
609
610        if state.character_state != prev_state.character_state {
611            match attributes::calculate_player_entity_stats_with_zeroes(
612                &EntityState::Character(&state.character_state),
613                &game_config,
614                &self.script_runner,
615            ) {
616                Ok(attributes) => {
617                    state.character_state.player_attributes = attributes.attributes.clone();
618                    state.character_state.player_attributes.remove_zeroes();
619
620                    if let Some(fight) = &mut state.active_fight
621                        && let Some(entity) = fight
622                            .entities
623                            .iter_mut()
624                            .find(|ent| ent.id == fight.player_id)
625                    {
626                        entity.max_hp = attributes.max_hp;
627                        // This rewrites old attributes with new ones, keeping custon rhai attributes
628                        entity
629                            .attributes
630                            .0
631                            .extend(attributes.attributes.0.iter().map(|(k, v)| (k.clone(), *v)));
632                        entity.attributes.remove_zeroes();
633                    }
634                }
635                Err(err) => {
636                    tracing::error!("Failed to fill player entity attributes: {:?}", err);
637                }
638            }
639
640            if let Ok(power) = self
641                .script_runner
642                .calculate_character_power(&state.character_state, &game_config)
643            {
644                state.character_state.character.power = power;
645            }
646        }
647
648        self.compute_fields_duration
649            .record(start.elapsed().as_secs_f64(), &[]);
650
651        events
652    }
653}
654
655impl OverlordAsyncEventHandler {
656    pub fn new(game_config: configs::SharedGameConfig, frontend: bool) -> Self {
657        let game_config_for_runner = game_config.get();
658        let script_runner = ScriptRunner::new(&game_config_for_runner);
659        let meter = opentelemetry::global::meter("compute_fields");
660        Self {
661            game_config,
662            script_runner,
663            frontend,
664            start_fight_tick: 0,
665            last_currency_source: None,
666            compute_fields_duration: meter
667                .f64_histogram("compute_fields_duration_seconds")
668                .with_boundaries(vec![
669                    0.0001, 0.0005, 0.001, 0.002, 0.005, 0.01, 0.02, 0.05, 0.1,
670                ])
671                .build(),
672        }
673    }
674
675    fn handle_set_custom_value(
676        &self,
677        key: String,
678        value: i64,
679        mut state: OverlordState,
680    ) -> EventHandleResult<OverlordEvent, OverlordState> {
681        state
682            .character_state
683            .character
684            .custom_values
685            .insert(&key, value);
686        EventHandleResult::ok(state)
687    }
688
689    fn handle_set_connection_store(
690        &self,
691        key: String,
692        value: i64,
693        mut state: OverlordState,
694    ) -> EventHandleResult<OverlordEvent, OverlordState> {
695        state.connection_store.insert(key, value);
696        EventHandleResult::ok(state)
697    }
698
699    fn handle_new_character_level(
700        &self,
701        level: i64,
702        mut state: OverlordState,
703    ) -> EventHandleResult<OverlordEvent, OverlordState> {
704        state.character_state.character.character_level = level;
705
706        EventHandleResult::ok(state)
707    }
708
709    fn handle_purchases_banned(
710        &self,
711        mut state: OverlordState,
712    ) -> EventHandleResult<OverlordEvent, OverlordState> {
713        state.character_state.character.purchases_banned = true;
714        EventHandleResult::ok(state)
715    }
716
717    fn handle_new_mail(
718        &self,
719        new_mail: Mail,
720        mut state: OverlordState,
721    ) -> EventHandleResult<OverlordEvent, OverlordState> {
722        if !state.incoming_mails.contains(&new_mail) {
723            state.incoming_mails.push(new_mail);
724        }
725
726        EventHandleResult::ok(state)
727    }
728
729    fn handle_noop(&self, state: OverlordState) -> EventHandleResult<OverlordEvent, OverlordState> {
730        EventHandleResult::ok(state)
731    }
732
733    fn handle_tutorial_step_completed(
734        &self,
735        step_number: i16,
736        mut state: OverlordState,
737    ) -> EventHandleResult<OverlordEvent, OverlordState> {
738        if !state
739            .character_state
740            .character
741            .completed_tutorials
742            .contains(&step_number)
743        {
744            state
745                .character_state
746                .character
747                .completed_tutorials
748                .push(step_number);
749        } else {
750            tracing::error!("Provided step {step_number} is already in state");
751        }
752        EventHandleResult::ok(state)
753    }
754}
755
756impl OverlordAsyncEventHandler {
757    fn log_currency_change_metrics(&self, state: &OverlordState, prev_state: &OverlordState) {
758        let character_id = state.character_state.character.id;
759        let current_currencies = &state.character_state.currencies;
760        let prev_currencies = &prev_state.character_state.currencies;
761        let source = self.last_currency_source.as_deref().unwrap_or("unknown");
762
763        for current_currency in current_currencies {
764            let amount_before = prev_currencies
765                .iter()
766                .find(|unit| unit.currency_id == current_currency.currency_id)
767                .map_or(0, |unit| unit.amount);
768            let delta = current_currency.amount - amount_before;
769            if delta > 0 {
770                tracing::info!(
771                    target: METRICS_TARGET,
772                    character_id = %character_id,
773                    event_type = "increase_currency",
774                    currency_id = %current_currency.currency_id,
775                    amount = delta,
776                    amount_before,
777                    source = %source,
778                    "Add currency",
779                );
780            } else if delta < 0 {
781                tracing::info!(
782                    target: METRICS_TARGET,
783                    character_id = %character_id,
784                    event_type = "decrease_currency",
785                    currency_id = %current_currency.currency_id,
786                    amount = -delta,
787                    amount_before,
788                    source = %source,
789                    "Decrease currency",
790                );
791            }
792        }
793
794        for prev_currency in prev_currencies {
795            if current_currencies
796                .iter()
797                .any(|unit| unit.currency_id == prev_currency.currency_id)
798            {
799                continue;
800            }
801
802            let delta = -prev_currency.amount;
803            if delta > 0 {
804                tracing::info!(
805                    target: METRICS_TARGET,
806                    character_id = %character_id,
807                    event_type = "increase_currency",
808                    currency_id = %prev_currency.currency_id,
809                    amount = delta,
810                    amount_before = prev_currency.amount,
811                    source = %source,
812                    "Add currency",
813                );
814            } else if delta < 0 {
815                tracing::info!(
816                    target: METRICS_TARGET,
817                    character_id = %character_id,
818                    event_type = "decrease_currency",
819                    currency_id = %prev_currency.currency_id,
820                    amount = -delta,
821                    amount_before = prev_currency.amount,
822                    source = %source,
823                    "Decrease currency",
824                );
825            }
826        }
827    }
828
829    pub fn compute_character_level(&self, state: &mut OverlordState) -> Vec<OverlordEvent> {
830        let game_config = self.game_config.get();
831        let current_level = game_config
832            .require_character_level(state.character_state.character.character_level)
833            .unwrap_or_else(|err| panic!("{err:?}"));
834
835        let new_level = game_config
836            .character_levels
837            .iter()
838            .fold(current_level, |mut acc, x| {
839                if x.level > acc.level
840                    && state.character_state.character.character_experience >= x.required_experience
841                {
842                    acc = x;
843                }
844                acc
845            });
846
847        if new_level.level != current_level.level {
848            vec![OverlordEvent::NewCharacterLevel {
849                level: new_level.level,
850            }]
851        } else {
852            Vec::new()
853        }
854    }
855
856    fn try_give_new_offers(
857        &self,
858        state: &mut OverlordState,
859        events: &mut Vec<EventPluginized<OverlordEvent, OverlordState>>,
860        trigger_event: &OverlordEvent,
861    ) {
862        for offer in &self.game_config.get().offers_templates {
863            if !offer.enabled {
864                continue;
865            }
866
867            if offer.limit_of_buys.is_some_and(|limit| {
868                state
869                    .offers_info
870                    .offer_buy_counts
871                    .get(&offer.id)
872                    .copied()
873                    .unwrap_or(0)
874                    >= limit as u32
875            }) {
876                continue;
877            }
878
879            if state
880                .offers_info
881                .active_offers
882                .iter()
883                .any(|x| x.template_id == offer.id)
884            {
885                continue;
886            }
887
888            if !offer.events_subscribe.contains(&trigger_event.to_string()) {
889                continue;
890            }
891
892            let should_give = match self.should_give_new_offer(
893                &state.character_state,
894                offer,
895                &offer.trigger_script,
896                trigger_event.clone(),
897            ) {
898                Ok(progress) => progress,
899                Err(e) => {
900                    tracing::error!(
901                        "Failed determining for offer id: {}\n Error: {e:?}",
902                        offer.id
903                    );
904                    continue;
905                }
906            };
907
908            if should_give {
909                events.push(EventPluginized::now(OverlordEvent::NewOffer {
910                    offer_template_id: offer.id,
911                }));
912            }
913        }
914    }
915
916    fn should_give_new_offer(
917        &self,
918        character_state: &CharacterState,
919        offer: &OfferTemplate,
920        script: &str,
921        trigger_event: OverlordEvent,
922    ) -> anyhow::Result<bool> {
923        self.script_runner.run_expression::<bool>(
924            |mut scope_setter| {
925                trigger_event.add_event_to_scope(&mut scope_setter, "Event");
926                scope_setter.set_const("CharacterState", character_state.clone());
927                scope_setter.set_const("Offer", offer.clone());
928                scope_setter
929            },
930            script,
931        )
932    }
933
934    fn update_quests_progress(
935        &self,
936        state: &mut OverlordState,
937        events: &mut Vec<EventPluginized<OverlordEvent, OverlordState>>,
938        trigger_event: &OverlordEvent,
939    ) {
940        let game_config = self.game_config.get();
941        let all_active_quests = state.quest_groups.get_not_claimed_quests_mut();
942        let active_fight = &state.active_fight;
943
944        for active_quest in all_active_quests {
945            let Some(quest_template) = game_config.quest(active_quest.id) else {
946                continue;
947            };
948
949            if state.patron.is_none()
950                && (quest_template.quest_group_type == QuestGroupType::PatronLifetime
951                    || quest_template.quest_group_type == QuestGroupType::PatronDaily)
952            {
953                continue;
954            }
955
956            if !quest_template
957                .events_subscribe
958                .contains(&trigger_event.to_string())
959            {
960                continue;
961            }
962
963            if quest_template.quest_group_type == QuestGroupType::LoopTask
964                && !quest_template.progress_if_inactive
965                && let Some(active_loop_task_id) =
966                    state.character_state.character.active_loop_task_id
967                && active_loop_task_id != quest_template.id
968            {
969                continue;
970            }
971
972            let was_completed = active_quest.is_completed(quest_template.progress_target);
973
974            if !active_quest.is_completed(quest_template.progress_target) {
975                let progress = match self.get_quest_progress(
976                    &state.character_state,
977                    active_fight,
978                    active_quest,
979                    &quest_template.progress_script,
980                    trigger_event.clone(),
981                ) {
982                    Ok(progress) => progress,
983                    Err(e) => {
984                        tracing::error!(
985                            "Failed updating quest progress for quest id: {}\n Error: {e:?}",
986                            active_quest.id
987                        );
988                        continue;
989                    }
990                };
991                active_quest.current = progress;
992            }
993
994            if !was_completed {
995                if (quest_template.quest_group_type == QuestGroupType::PatronLifetime
996                    || quest_template.quest_group_type == QuestGroupType::PatronDaily)
997                    && active_quest.is_completed(quest_template.progress_target)
998                {
999                    events.push(EventPluginized::now(OverlordEvent::PatronQuestCompleted {
1000                        quest_id: active_quest.id,
1001                    }));
1002                }
1003
1004                if (quest_template.quest_group_type != QuestGroupType::Hidden)
1005                    && active_quest.is_completed(quest_template.progress_target)
1006                {
1007                    events.push(EventPluginized::now(OverlordEvent::QuestCompleted {
1008                        quest_id: active_quest.id,
1009                    }));
1010                }
1011            }
1012
1013            if (quest_template.quest_group_type == QuestGroupType::Hidden)
1014                && active_quest.is_completed(quest_template.progress_target)
1015            {
1016                events.push(EventPluginized::now(OverlordEvent::HiddenQuestCompleted {
1017                    quest_id: active_quest.id,
1018                }));
1019            }
1020        }
1021    }
1022
1023    pub fn get_quest_progress(
1024        &self,
1025        character_state: &CharacterState,
1026        active_fight: &Option<ActiveFight>,
1027        quest: &QuestInstance,
1028        script: &str,
1029        trigger_event: OverlordEvent,
1030    ) -> anyhow::Result<i64> {
1031        self.script_runner.run_expression::<i64>(
1032            |mut scope_setter| {
1033                trigger_event.add_event_to_scope(&mut scope_setter, "Event");
1034                scope_setter.set_const("CharacterState", character_state.clone());
1035                if let Some(fight) = active_fight.as_ref() {
1036                    scope_setter.set_const("ActiveFight", fight.clone());
1037                }
1038                scope_setter.set_const("Quest", quest.clone());
1039                scope_setter
1040            },
1041            script,
1042        )
1043    }
1044
1045    fn handle_currency_increase(
1046        &self,
1047        currencies: &[CurrencyUnit],
1048        mut state: OverlordState,
1049    ) -> EventHandleResult<OverlordEvent, OverlordState> {
1050        increase_currencies(&mut state.character_state.currencies, currencies);
1051        EventHandleResult::ok(state)
1052    }
1053
1054    fn handle_currency_decrease(
1055        &self,
1056        currencies: &[CurrencyUnit],
1057        mut state: OverlordState,
1058    ) -> EventHandleResult<OverlordEvent, OverlordState> {
1059        let non_zero: Vec<_> = currencies
1060            .iter()
1061            .filter(|c| c.amount > 0)
1062            .cloned()
1063            .collect();
1064        if let Err(e) = decrease_currencies(&mut state.character_state.currencies, &non_zero) {
1065            tracing::error!("CurrencyDecrease failed: {e}");
1066            return EventHandleResult::fail(state);
1067        }
1068        EventHandleResult::ok(state)
1069    }
1070
1071    /// Creates a `CurrencyIncrease` event. The actual state mutation happens when the
1072    /// event is processed by the handler — callers don't need to mutate state manually.
1073    pub fn currency_increase(
1074        currencies: &[CurrencyUnit],
1075        currency_source: CurrencySource,
1076    ) -> EventPluginized<OverlordEvent, OverlordState> {
1077        EventPluginized::now(OverlordEvent::CurrencyIncrease {
1078            currencies: currencies.to_owned(),
1079            currency_source,
1080        })
1081    }
1082
1083    /// Creates a `CurrencyDecrease` event after validating sufficient balance.
1084    /// Returns `None` if the player doesn't have enough currency.
1085    /// The actual state mutation happens when the event is processed by the handler.
1086    pub fn currency_decrease(
1087        state: &OverlordState,
1088        currencies: &[CurrencyUnit],
1089        currency_consumer: CurrencyConsumer,
1090    ) -> Option<EventPluginized<OverlordEvent, OverlordState>> {
1091        if !check_can_decrease_currencies(&state.character_state.currencies, currencies) {
1092            tracing::error!(
1093                "currency_decrease({currency_consumer:?}): not enough currency, \
1094                 required={currencies:?}, available={:?}",
1095                state.character_state.currencies
1096            );
1097            return None;
1098        }
1099        Some(EventPluginized::now(OverlordEvent::CurrencyDecrease {
1100            currencies: currencies.to_owned(),
1101            currency_consumer,
1102        }))
1103    }
1104
1105    pub fn process_currency(
1106        &self,
1107        raw_item: &BundleRawStep,
1108        script_runner: &ScriptRunner<OverlordEvent, OverlordState>,
1109    ) -> BundleElement {
1110        let currency_unit = from_es_currencies(
1111            &script_runner.run_currencies_calculate(|scope| scope, &raw_item.script),
1112        );
1113
1114        BundleElement::Currencies(currency_unit)
1115    }
1116
1117    pub fn process_ability(
1118        &self,
1119        raw_item: &BundleRawStep,
1120        script_runner: &ScriptRunner<OverlordEvent, OverlordState>,
1121        config: &GameConfig,
1122    ) -> BundleElement {
1123        let ability_shards =
1124            script_runner.run_ability_shards_calculate(|scope| scope, &raw_item.script);
1125
1126        let abilities = ability_shards
1127            .iter()
1128            .filter_map(|ability_shard| {
1129                let Some(template) = config.ability_template(ability_shard.ability_id).cloned()
1130                else {
1131                    tracing::error!(
1132                        "Failed to get ability with ability_id={}",
1133                        ability_shard.ability_id
1134                    );
1135                    return None;
1136                };
1137
1138                Some(BundleAbility {
1139                    template,
1140                    shards_amount: ability_shard.shards_amount,
1141                })
1142            })
1143            .collect();
1144
1145        BundleElement::Abilities(abilities)
1146    }
1147
1148    pub fn process_item(
1149        &self,
1150        raw_item: &BundleRawStep,
1151        character_level: i64,
1152        script_runner: &ScriptRunner<OverlordEvent, OverlordState>,
1153        config: &GameConfig,
1154    ) -> anyhow::Result<BundleElement> {
1155        let mut rng = StdRng::seed_from_u64(OsRng.try_next_u64()?);
1156
1157        let item_ids = script_runner.run_item_ids_script(|scope| scope, &raw_item.script);
1158
1159        let items: Vec<Item> = item_ids
1160            .iter()
1161            .filter_map(|&item_id| {
1162                let Some(template) = config.item_template(item_id) else {
1163                    tracing::error!("Failed to get ability with item_id={}", item_id);
1164                    return None;
1165                };
1166
1167                let Some(rarity) = config.item_rarity(template.rarity_id).cloned() else {
1168                    tracing::error!("Failed to get item rarity with id={}", template.rarity_id);
1169                    return None;
1170                };
1171
1172                Some(generate_item_from_template(
1173                    template,
1174                    rarity,
1175                    character_level,
1176                    config,
1177                    &mut rng,
1178                ))
1179            })
1180            .collect();
1181
1182        let finalized_items = items
1183            .into_iter()
1184            .filter_map(
1185                |mut item| match try_finalize_item(&mut item, config, script_runner) {
1186                    Ok(()) => Some(item),
1187                    Err(e) => {
1188                        tracing::error!("Failed to finalize item: {}", e);
1189                        None
1190                    }
1191                },
1192            )
1193            .collect();
1194
1195        Ok(BundleElement::Items(finalized_items))
1196    }
1197
1198    fn handle_reset_ad_usage(
1199        &mut self,
1200        placements: &[AdPlacement],
1201        mut state: OverlordState,
1202    ) -> EventHandleResult<OverlordEvent, OverlordState> {
1203        let now = ::time::utc_now();
1204        for placement in placements {
1205            state.character_state.ad_usage.insert(
1206                *placement,
1207                AdUsageData {
1208                    daily_count: 0,
1209                    last_reset_at: now,
1210                },
1211            );
1212        }
1213
1214        EventHandleResult::ok(state)
1215    }
1216
1217    fn handle_reset_instant_reward_gems_press_count(
1218        &mut self,
1219        mut state: OverlordState,
1220    ) -> EventHandleResult<OverlordEvent, OverlordState> {
1221        state
1222            .character_state
1223            .character
1224            .instant_reward_gems_press_count = 0;
1225        EventHandleResult::ok(state)
1226    }
1227
1228    fn handle_show_bird(
1229        &mut self,
1230        mut state: OverlordState,
1231    ) -> EventHandleResult<OverlordEvent, OverlordState> {
1232        let bird_config = &self.game_config.get().ads_settings.bird_ad;
1233        let cooldown_until = ::time::utc_now()
1234            + chrono::TimeDelta::seconds(bird_config.post_show_cooldown_sec as i64);
1235        state.character_state.character.bird_cooldown_until = Some(cooldown_until);
1236        EventHandleResult::ok(state)
1237    }
1238
1239    fn handle_bird_shown(
1240        &mut self,
1241        mut state: OverlordState,
1242    ) -> EventHandleResult<OverlordEvent, OverlordState> {
1243        let bird_config = &self.game_config.get().ads_settings.bird_ad;
1244        let cooldown_until =
1245            ::time::utc_now() + chrono::TimeDelta::seconds(bird_config.cooldown_sec as i64);
1246        state.character_state.character.bird_cooldown_until = Some(cooldown_until);
1247        EventHandleResult::ok(state)
1248    }
1249}