overlord_event_system/logic/
handler.rs

1use crate::{
2    BehaviorRegistry, attributes, event::OverlordEvent, game_config_helpers::GameConfigLookup,
3    state::OverlordState,
4};
5
6use std::sync::Arc;
7
8use configs::game_config::GameConfig;
9
10use analytics::constants::METRICS_TARGET;
11
12use essences::{
13    ad_usage::{AdPlacement, AdUsageData},
14    character_state::CharacterState,
15    currency::{
16        CurrencyConsumer, CurrencySource, CurrencyUnit, check_can_decrease_currencies,
17        decrease_currencies, increase_currencies,
18    },
19    entity::EntityState,
20    fighting::ActiveFight,
21    mail::Mail,
22    offers::OfferTemplate,
23    quest::{QuestGroupType, QuestInstance},
24};
25
26pub use event_system::{
27    event::{EventPluginized, EventStruct},
28    plugin::{cron::CronMark, delayed::DelayedMark},
29    system::EventHandleResult,
30};
31
32#[derive(Debug)]
33pub struct OverlordLogic {
34    pub(super) game_config: configs::SharedGameConfig,
35    pub(super) behaviors: Arc<BehaviorRegistry>,
36    pub(super) frontend: bool,
37    pub(super) start_fight_tick: u64,
38    /// Combat scheduler: delayed combat events + the FightProgress heartbeat.
39    /// Drained via `collect_due_scheduled` by whichever loop drives this
40    /// handler — the live `System` or the `FightEngine`.
41    pub(crate) fight_clock: crate::fight::FightClock,
42    /// Id of the last fight whose `EndFight` was processed. Producers can race
43    /// two `EndFight` events for one fight (max-duration timeout + an
44    /// in-flight projectile death); the end-of-fight pipeline (PvP rating,
45    /// vassal, reward bundles, chapter advance) must run exactly once.
46    pub(super) ended_fight_id: Option<uuid::Uuid>,
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 OverlordLogic {
53    // No analytics span here: the unified `OverlordEventHandler::handle_event`
54    // already opens the per-event METRICS_TARGET span with these exact fields,
55    // and a second nested one would double span volume on the 10Hz fight path.
56    /// Per-event pre-amble: stamps the combat clock so relative `schedule`
57    /// calls made anywhere in the dispatch resolve against the current tick,
58    /// and records the currency source label for the next `compute_fields`
59    /// diff. Called by `handle_event` and by the monolith's merged
60    /// (logic + persistence) arms, which bypass this dispatch.
61    pub fn pre_event(&mut self, event: &OverlordEvent, current_tick: u64) {
62        self.fight_clock.set_now(current_tick);
63        self.last_currency_source = match event {
64            OverlordEvent::CurrencyIncrease {
65                currency_source, ..
66            } => Some(format!("{currency_source:?}")),
67            OverlordEvent::CurrencyDecrease {
68                currency_consumer, ..
69            } => Some(format!("{currency_consumer:?}")),
70            other => Some(other.to_string()),
71        };
72    }
73
74    /// Effect `events_subscribe` scan: when a fight is active, effects applied
75    /// to entities can react to any event by casting. The returned events are
76    /// appended to the handling result's events (after the success hooks).
77    pub fn effect_subscription_events(
78        &self,
79        event: &OverlordEvent,
80        state: &OverlordState,
81    ) -> Vec<EventPluginized<OverlordEvent, OverlordState>> {
82        let game_config = self.game_config.get();
83        let mut events: Vec<EventPluginized<OverlordEvent, OverlordState>> = Vec::new();
84        if let Some(fight) = &state.active_fight {
85            for entity in &fight.entities {
86                for effect_id in &entity.effect_ids {
87                    // Config drift: an effect can be removed from GameConfig
88                    // while still applied to an entity in an active fight.
89                    // Skip the stale effect instead of failing every event for
90                    // the session before dispatch.
91                    let Some(effect) = game_config.effect(*effect_id) else {
92                        tracing::warn!(
93                            "Skipping unknown effect with id = {effect_id} on fight entity {}",
94                            entity.id
95                        );
96                        continue;
97                    };
98                    if let Some(events_subscribe) = &effect.events_subscribe
99                        && events_subscribe.contains(&event.to_string())
100                    {
101                        events.push(EventPluginized::now(OverlordEvent::CastEffectFromEvent {
102                            entity_id: entity.id,
103                            effect_id: effect.id,
104                            caller_event: Box::new(event.clone()),
105                        }));
106                    }
107                }
108            }
109        }
110        events
111    }
112
113    /// Cross-cutting success hooks: quest progress + offer triggers. Runs for
114    /// every successfully handled event, both here and in the monolith's
115    /// merged arms.
116    pub fn apply_success_hooks(
117        &self,
118        state: &mut OverlordState,
119        events: &mut Vec<EventPluginized<OverlordEvent, OverlordState>>,
120        event: &OverlordEvent,
121    ) {
122        self.update_quests_progress(state, events, event);
123        self.try_give_new_offers(state, events, event);
124    }
125
126    pub fn handle_event(
127        &mut self,
128        event: &OverlordEvent,
129        state: OverlordState,
130        rand_gen: rand::rngs::StdRng,
131        current_tick: u64,
132    ) -> EventHandleResult<OverlordEvent, OverlordState> {
133        self.pre_event(event, current_tick);
134        let mut events = self.effect_subscription_events(event, &state);
135
136        let mut result = match &event {
137            // Items
138            OverlordEvent::OpenItemCase { batch_size } => {
139                self.handle_open_item_case(*batch_size, state)
140            }
141            OverlordEvent::AutoChestOpenItemCase { batch_size } => {
142                self.handle_auto_chest_open_item_case(*batch_size, state)
143            }
144            OverlordEvent::PlayerEquipItem { item_id } => {
145                self.handle_player_equip_item(*item_id, state)
146            }
147            OverlordEvent::SellItem { item_id } => self.handle_sell_item(*item_id, state),
148            OverlordEvent::ItemSold { .. } => self.handle_noop(state),
149            OverlordEvent::ItemsExpired { .. } => self.handle_noop(state),
150            OverlordEvent::PlayerNewItems { items } => self.handle_player_new_items(items, state),
151            OverlordEvent::UpgradeItemCase {} => self.handle_noop(state),
152            OverlordEvent::ItemCaseUpgraded {} => self.handle_noop(state),
153            OverlordEvent::SpeedupUpgradeItemCase {} => self.handle_noop(state),
154            OverlordEvent::SkipUpgradeItemCase {} => self.handle_noop(state),
155            OverlordEvent::ClaimUpgradeItemCase {} => self.handle_noop(state),
156
157            OverlordEvent::EnableAutoSell {} => self.handle_enable_auto_sell(state),
158            OverlordEvent::DisableAutoSell {} => self.handle_disable_auto_sell(state),
159
160            OverlordEvent::SetGearOverrideEnabled { item_type, enabled } => {
161                self.handle_set_gear_override_enabled(*item_type, *enabled, state)
162            }
163
164            OverlordEvent::EnableCaseUpgradePopUp {} => {
165                self.handle_enable_case_upgrade_pop_up(state)
166            }
167            OverlordEvent::DisableCaseUpgradePopUp {} => {
168                self.handle_disable_case_upgrade_pop_up(state)
169            }
170
171            // Abilities
172            OverlordEvent::OpenAbilityCase { .. } => self.handle_noop(state),
173            OverlordEvent::SetAbilityGachaWishlist { .. } => self.handle_noop(state),
174            OverlordEvent::UpgradeAbilitySlot { .. } => self.handle_noop(state),
175            OverlordEvent::AbilityCaseOpened { .. } => self.handle_noop(state),
176            OverlordEvent::NewAbilities { .. } => self.handle_noop(state),
177            OverlordEvent::UpgradeAbilityCase {} => self.handle_noop(state),
178            OverlordEvent::FastEquipAbilities {} => self.handle_noop(state),
179            OverlordEvent::EquipAbility {
180                slot_id,
181                ability_id,
182            } => self.handle_equip_ability(*slot_id, *ability_id, current_tick, state),
183            OverlordEvent::UnequipAbility { slot_id } => {
184                self.handle_unequip_ability(*slot_id, state)
185            }
186            OverlordEvent::EquipAbilities { equipped_abilities } => {
187                self.handle_equip_abilities(equipped_abilities.clone(), current_tick, state)
188            }
189            OverlordEvent::UpgradeAbility { .. } => self.handle_noop(state),
190            OverlordEvent::UpgradeAllAbilities {} => self.handle_noop(state),
191            OverlordEvent::UpgradedAbilities { .. } => self.handle_noop(state),
192
193            // Fight management
194            OverlordEvent::StartGame {} => self.handle_noop(state),
195            OverlordEvent::PrepareFight { prepare_fight_type } => {
196                self.handle_prepare_fight(prepare_fight_type.clone(), rand_gen, state)
197            }
198            OverlordEvent::StartFight { fight_id } => {
199                self.handle_start_fight(event.clone(), *fight_id, rand_gen, current_tick, state)
200            }
201            OverlordEvent::EndFight {
202                fight_id,
203                is_win,
204                pvp_state,
205            } => self.handle_end_fight(*fight_id, *is_win, pvp_state.as_deref(), state),
206            OverlordEvent::StageCleared {} => self.handle_noop(state),
207            OverlordEvent::RaidDungeon { .. } => self.handle_noop(state),
208
209            // Moving
210            OverlordEvent::StartMove {
211                entity_id,
212                to,
213                duration_ticks,
214            } => self.handle_start_move(*entity_id, to.clone(), *duration_ticks, state),
215            OverlordEvent::EndMove { entity_id } => self.handle_end_move(*entity_id, state),
216            OverlordEvent::MoveProgress { entity_id, to } => {
217                self.handle_move_progress(*entity_id, to.clone(), state)
218            }
219
220            // Fighting
221            OverlordEvent::SpawnEntity {
222                id,
223                entity_template_id,
224                position,
225                entity_team,
226                has_big_hp_bar,
227                entity_attributes,
228            } => self.handle_spawn_entity(
229                *id,
230                *entity_template_id,
231                position.clone(),
232                entity_team.clone(),
233                *has_big_hp_bar,
234                entity_attributes.clone(),
235                current_tick,
236                state,
237            ),
238            OverlordEvent::FightProgress {} => self.handle_fight_progress(current_tick, state),
239            OverlordEvent::StartCastAbility {
240                by_entity_id,
241                ability_id,
242                ..
243            } => self.handle_start_cast_ability(
244                event.clone(),
245                *by_entity_id,
246                *ability_id,
247                rand_gen,
248                current_tick,
249                state,
250            ),
251            OverlordEvent::StartedCastAbility { .. } => self.handle_noop(state),
252            OverlordEvent::CastAbility {
253                by_entity_id,
254                to_entity_id,
255                ability_id,
256                ..
257            } => self.handle_cast_ability(
258                event.clone(),
259                *by_entity_id,
260                *to_entity_id,
261                *ability_id,
262                rand_gen,
263                current_tick,
264                state,
265            ),
266            OverlordEvent::StartCastProjectile {
267                by_entity_id,
268                to_entity_id,
269                projectile_id,
270                level,
271                delay: _,
272            } => self.handle_start_cast_projectile(
273                event.clone(),
274                *by_entity_id,
275                *to_entity_id,
276                *projectile_id,
277                *level,
278                current_tick,
279                state,
280            ),
281            OverlordEvent::StartedCastProjectile { .. } => self.handle_noop(state),
282            OverlordEvent::CastProjectile {
283                by_entity_id,
284                to_entity_id,
285                projectile_id,
286                level,
287                projectile_data,
288            } => self.handle_cast_projectile(
289                event.clone(),
290                *by_entity_id,
291                *to_entity_id,
292                *projectile_id,
293                *level,
294                projectile_data,
295                rand_gen,
296                current_tick,
297                state,
298            ),
299            OverlordEvent::Damage {
300                entity_id,
301                damage,
302                damage_data: _,
303            } => self.handle_damage(*entity_id, *damage, current_tick, rand_gen, state),
304            OverlordEvent::Heal { entity_id, heal } => self.handle_heal(*entity_id, *heal, state),
305            OverlordEvent::CounterAttack { .. } => self.handle_noop(state),
306            OverlordEvent::WaveCleared {} => self.handle_noop(state),
307            OverlordEvent::Multicast { .. } => self.handle_noop(state),
308            OverlordEvent::Evasion { .. } => self.handle_noop(state),
309            OverlordEvent::PlayerDeath {} => self.handle_player_death(state),
310            OverlordEvent::EntityDeath { entity_id, reward } => {
311                self.handle_entity_death(*entity_id, reward.clone(), rand_gen, state)
312            }
313            OverlordEvent::EntityIncrAttribute {
314                entity_id,
315                attribute,
316                delta,
317            } => self.handle_entity_incr_attribute(
318                *entity_id,
319                attribute,
320                *delta,
321                current_tick,
322                state,
323            ),
324            OverlordEvent::EntityAddAbilityCooldown {
325                entity_id,
326                ability_id,
327                delta_ticks,
328            } => self.handle_entity_add_ability_cooldown(
329                *entity_id,
330                *ability_id,
331                *delta_ticks,
332                current_tick,
333                state,
334            ),
335            OverlordEvent::EntityCancelCastWithCooldown {
336                entity_id,
337                ability_id,
338            } => self.handle_entity_cancel_cast_with_cooldown(
339                *entity_id,
340                *ability_id,
341                current_tick,
342                state,
343            ),
344            OverlordEvent::EntityStun {
345                entity_id,
346                duration_ticks,
347            } => self.handle_entity_stun(*entity_id, *duration_ticks, current_tick, state),
348            OverlordEvent::EntityApplyEffect {
349                entity_id,
350                effect_id,
351            } => self.handle_entity_apply_effect(*entity_id, *effect_id, current_tick, state),
352            OverlordEvent::CastEffect {
353                entity_id,
354                effect_id,
355            } => {
356                self.handle_cast_effect(*entity_id, *effect_id, None, rand_gen, current_tick, state)
357            }
358            OverlordEvent::CastEffectFromEvent {
359                entity_id,
360                effect_id,
361                caller_event,
362            } => self.handle_cast_effect(
363                *entity_id,
364                *effect_id,
365                Some(caller_event.to_owned()),
366                rand_gen,
367                current_tick,
368                state,
369            ),
370            OverlordEvent::FightCustomEvent { .. } => self.handle_noop(state),
371            OverlordEvent::FightVisualEvent { .. } => self.handle_noop(state),
372            OverlordEvent::NewCharacterLevel { level } => {
373                self.handle_new_character_level(*level, state)
374            }
375
376            // Vassal links
377            OverlordEvent::ClaimSuzerainReward {} => self.handle_noop(state),
378            OverlordEvent::ClaimVassalReward { .. } => self.handle_noop(state),
379            OverlordEvent::NewSuzerain { new_suzerain } => {
380                self.handle_new_suzerain(new_suzerain.as_deref().cloned(), state)
381            }
382            OverlordEvent::RemoveVassal { vassal_id } => {
383                self.handle_remove_vassal(*vassal_id, state)
384            }
385
386            // Quests
387            OverlordEvent::ClaimQuest { quest_id } => {
388                self.handle_claim_quest(*quest_id, rand_gen, state)
389            }
390            OverlordEvent::NewQuests { quest_ids } => {
391                self.handle_new_quests(quest_ids.clone(), state)
392            }
393            OverlordEvent::UpdateActiveLoopTaskId { quest_id } => {
394                self.handle_update_active_loop_task_id(*quest_id, state)
395            }
396
397            OverlordEvent::ClaimQuestProgressionReward { quest_group_type } => {
398                self.handle_claim_quest_progression_reward(quest_group_type.to_owned(), state)
399            }
400
401            OverlordEvent::ResetRepeatingQuests { quest_ids } => {
402                self.handle_reset_repeating_quests(quest_ids.clone(), state)
403            }
404
405            // Vassal Tasks
406            OverlordEvent::GiveTask { .. } => self.handle_noop(state),
407            OverlordEvent::NewTask { new_task } => {
408                self.handle_new_task((**new_task).clone(), state)
409            }
410            OverlordEvent::AcceptTask { task_id, is_good } => {
411                self.handle_accept_task(*task_id, *is_good, state)
412            }
413            OverlordEvent::TaskAccepted {
414                task_id,
415                started_good,
416                started_at,
417                finish_at,
418            } => self.handle_task_accepted(*task_id, *started_good, *started_at, *finish_at, state),
419            OverlordEvent::HitHands { task_id } => self.handle_hit_hands(*task_id, state),
420            OverlordEvent::HandsHitted { task_id } => self.handle_hands_hitted(*task_id, state),
421            OverlordEvent::ClaimTaskReward { task_id } => {
422                self.handle_claim_task_reward(*task_id, state)
423            }
424            OverlordEvent::TaskFinished { task_id } => self.handle_finish_task(*task_id, state),
425
426            // Resist Task
427            OverlordEvent::GiveResistTask { new_resist_task } => {
428                self.handle_give_resist_task((**new_resist_task).clone(), state)
429            }
430            OverlordEvent::AcceptResistTask {} => self.handle_noop(state),
431            OverlordEvent::ResistTaskAccepted { new_resist_task } => {
432                self.handle_resist_task_accepted((**new_resist_task).clone(), state)
433            }
434            OverlordEvent::CatchResistTask { task_id } => {
435                self.handle_catch_resist_task(*task_id, state)
436            }
437            OverlordEvent::ResistTaskCatched { task_id } => {
438                self.handle_resist_task_catched(*task_id, state)
439            }
440            OverlordEvent::ResistTaskFinished { task_id } => {
441                self.handle_resist_task_finished(*task_id, state)
442            }
443            OverlordEvent::ClaimResistTaskReward { task_id } => {
444                self.handle_claim_resist_task_reward(*task_id, state)
445            }
446
447            // Gifts
448            OverlordEvent::SendGift {
449                receiver_id,
450                config_gift_id,
451            } => self.handle_send_gift(*receiver_id, *config_gift_id, state),
452            OverlordEvent::NewGift { new_gift } => {
453                self.handle_new_gift((**new_gift).clone(), state)
454            }
455            OverlordEvent::AcceptGift { gift_id } => self.handle_accept_gift(*gift_id, state),
456
457            // PVP
458            OverlordEvent::StartVassalPVPSync { .. } => self.handle_noop(state),
459            OverlordEvent::StartArenaPVPSync { .. } => self.handle_noop(state),
460            OverlordEvent::StartArenaRematchSync { .. } => self.handle_noop(state),
461            OverlordEvent::RefreshArenaMatchmaking {} => self.handle_noop(state),
462            OverlordEvent::BuyArenaTicket {} => self.handle_noop(state),
463
464            // Referral Rewards
465            OverlordEvent::ClaimReferralLvlUpReward { level } => {
466                self.handle_claim_referral_lvlup_reward(*level, state)
467            }
468            OverlordEvent::ClaimReferralDailyReward {} => {
469                self.handle_claim_referral_daily_reward(state)
470            }
471            OverlordEvent::PatronQuestCompleted { quest_id } => {
472                self.handle_patron_quest_completed(*quest_id, state)
473            }
474            OverlordEvent::HiddenQuestCompleted { quest_id } => {
475                self.handle_hidden_quest_completed(*quest_id, rand_gen, state)
476            }
477            OverlordEvent::QuestCompleted { .. } => self.handle_noop(state),
478            OverlordEvent::ReferralDailyRewardStatusUpdate {
479                referral_daily_reward_status,
480            } => self
481                .handle_referral_daily_reward_status_update(*referral_daily_reward_status, state),
482
483            // AutoChest
484            OverlordEvent::EnableAutoChest {} => self.handle_enable_auto_chest(state),
485            OverlordEvent::DisableAutoChest {} => self.handle_disable_auto_chest(state),
486
487            OverlordEvent::EnableAutoChestFilter { filter_id } => {
488                self.handle_enable_auto_chest_filter(*filter_id, state)
489            }
490            OverlordEvent::DisableAutoChestFilter {} => {
491                self.handle_disable_auto_chest_filter(state)
492            }
493
494            OverlordEvent::EnableAutoChestPowerCompare {} => {
495                self.handle_enable_auto_chest_power_compare(state)
496            }
497            OverlordEvent::DisableAutoChestPowerCompare {} => {
498                self.handle_disable_auto_chest_power_compare(state)
499            }
500
501            OverlordEvent::UpdateAutoChestBatchSize { batch_size } => {
502                self.handle_update_auto_chest_batch_size(*batch_size, state)
503            }
504
505            OverlordEvent::NewAutoChestFilter { filter } => {
506                self.handle_new_auto_chest_filter((**filter).clone(), state)
507            }
508
509            OverlordEvent::UpdateAutoChestFilter { updated_filter } => {
510                self.handle_update_auto_chest_filter((**updated_filter).clone(), state)
511            }
512
513            OverlordEvent::RemoveAutoChestFilter { filter_id } => {
514                self.handle_remove_auto_chest_filter(*filter_id, state)
515            }
516
517            // Technical
518            OverlordEvent::SetCustomValue { key, value } => {
519                self.handle_set_custom_value(key.clone(), *value, state)
520            }
521            OverlordEvent::SetConnectionStore { key, value } => {
522                self.handle_set_connection_store(key.clone(), *value, state)
523            }
524
525            // Ability Presets
526            OverlordEvent::CreateAbilityPreset { .. } => self.handle_noop(state),
527            OverlordEvent::EditAbilityPreset { .. } => self.handle_noop(state),
528
529            // AfkReward
530            OverlordEvent::ClaimAfkReward {} => self.handle_noop(state),
531            OverlordEvent::AfkRewardClaimed {} => self.handle_noop(state),
532            OverlordEvent::AfkRewardsGatingUnlocked {} => {
533                self.handle_afk_rewards_gating_unlocked(state)
534            }
535            OverlordEvent::ClaimAfkInstantRewardGems {} => self.handle_noop(state),
536
537            // Bundles
538            OverlordEvent::ClaimBundleStepGeneric { .. } => self.handle_noop(state),
539            OverlordEvent::AddBundleGroup { bundle_ids, source } => {
540                self.handle_add_bundle_group(bundle_ids, *source, state)
541            }
542
543            // Classes
544            OverlordEvent::LevelUpClass { .. } => self.handle_noop(state),
545            OverlordEvent::Respec { .. } => self.handle_noop(state),
546
547            // User Account
548            OverlordEvent::LinkGuestAccount { .. } => self.handle_noop(state),
549            OverlordEvent::SetUsername { .. } => self.handle_noop(state),
550            OverlordEvent::SetCharacterBlocked { .. } => self.handle_noop(state),
551
552            // Cheat
553            OverlordEvent::SetMaxHp {
554                entity_id,
555                new_max_hp,
556                new_hp,
557            } => self.handle_set_max_hp(*entity_id, *new_max_hp, *new_hp, state),
558
559            OverlordEvent::RunCheat { cheat } => {
560                self.handle_run_cheat(cheat, current_tick, rand_gen, state)
561            }
562
563            OverlordEvent::Error { .. } => self.handle_noop(state),
564            OverlordEvent::CustomEvent { .. } => self.handle_noop(state),
565
566            // Currencies — the event itself performs the state mutation
567            OverlordEvent::CurrencyIncrease { currencies, .. } => {
568                self.handle_currency_increase(currencies, state)
569            }
570            OverlordEvent::CurrencyDecrease { currencies, .. } => {
571                self.handle_currency_decrease(currencies, state)
572            }
573            // Skins
574            OverlordEvent::BuySkins { .. } => self.handle_noop(state),
575            OverlordEvent::EquipAndUnequipSkins { .. } => self.handle_noop(state),
576
577            // Mails
578            OverlordEvent::ClaimMail { .. } => self.handle_noop(state),
579            OverlordEvent::ClaimAllMails {} => self.handle_noop(state),
580            OverlordEvent::ClaimAllQuests { .. } => self.handle_noop(state),
581            OverlordEvent::MakeRead { .. } => self.handle_noop(state),
582            OverlordEvent::MakeAllRead {} => self.handle_noop(state),
583            OverlordEvent::DeleteMail { .. } => self.handle_noop(state),
584            OverlordEvent::DeleteAllMails {} => self.handle_noop(state),
585            OverlordEvent::NewMail { new_mail } => {
586                self.handle_new_mail((**new_mail).clone(), state)
587            }
588            // Offers
589            OverlordEvent::NewOffer { .. } => self.handle_noop(state),
590            OverlordEvent::BuyOffer { .. } => self.handle_noop(state),
591            OverlordEvent::ResetOffers { new_offers } => {
592                self.handle_reset_offers(new_offers, state)
593            }
594            OverlordEvent::OfferPurchaseCompleted { .. } => self.handle_noop(state),
595            OverlordEvent::OfferPurchaseFailed { .. } => self.handle_noop(state),
596            OverlordEvent::PurchasesBanned {} => self.handle_purchases_banned(state),
597
598            // Pets
599            OverlordEvent::EquipPet { slot_id, pet_id } => {
600                self.handle_equip_pet(*slot_id, *pet_id, current_tick, state)
601            }
602            OverlordEvent::UnequipPet { slot_id } => {
603                self.handle_unequip_pet(*slot_id, current_tick, state)
604            }
605            OverlordEvent::FastEquipPets {} => self.handle_noop(state),
606            OverlordEvent::EquipPets { equipped_pets } => {
607                self.handle_equip_pets(equipped_pets.clone(), current_tick, state)
608            }
609            OverlordEvent::UpgradePet { .. } => self.handle_noop(state),
610            OverlordEvent::UpgradeAllPets {} => self.handle_noop(state),
611            OverlordEvent::UpgradedPets { .. } => self.handle_noop(state),
612            OverlordEvent::UpgradePetSlot { .. } => self.handle_noop(state),
613
614            // Pet Gacha
615            OverlordEvent::OpenPetCase { .. } => self.handle_noop(state),
616            OverlordEvent::SetPetGachaWishlist { .. } => self.handle_noop(state),
617            OverlordEvent::PetCaseOpened { .. } => self.handle_noop(state),
618            OverlordEvent::NewPets { .. } => self.handle_noop(state),
619            OverlordEvent::UpgradePetCase {} => self.handle_noop(state),
620
621            // Tutorial
622            OverlordEvent::TutorialShown { .. } => self.handle_noop(state),
623            OverlordEvent::TutorialStepCompleted { step_number } => {
624                self.handle_tutorial_step_completed(*step_number, state)
625            }
626            OverlordEvent::ClientLifecycle { .. } => self.handle_noop(state),
627
628            // Party
629            OverlordEvent::AddCharacterToParty { .. } => self.handle_noop(state),
630            OverlordEvent::RemoveCharacterFromParty {} => self.handle_noop(state),
631            OverlordEvent::RefreshPartyPlayers {} => self.handle_noop(state),
632            OverlordEvent::RefreshPartyMemberState {} => self.handle_noop(state),
633
634            // Talent Tree
635            OverlordEvent::StartTalentResearch { .. } => self.handle_noop(state),
636            OverlordEvent::TalentResearchStarted { .. } => self.handle_noop(state),
637            OverlordEvent::SpeedupTalentResearch {} => self.handle_noop(state),
638            OverlordEvent::SkipTalentResearch {} => self.handle_noop(state),
639            OverlordEvent::ClaimTalentResearch {} => self.handle_claim_talent_research(state),
640
641            // Statue
642            OverlordEvent::StatueRoll { .. } => self.handle_noop(state),
643            OverlordEvent::StatueActivateSet { .. } => self.handle_noop(state),
644            OverlordEvent::StatueAddSet {} => self.handle_noop(state),
645            OverlordEvent::StatueRenameSet { .. } => self.handle_noop(state),
646            OverlordEvent::StatueLockSlot { .. } => self.handle_noop(state),
647            OverlordEvent::StatueRollNewSlot { .. } => self.handle_noop(state),
648            // Progress Pass
649            OverlordEvent::ClaimProgressPassFreeReward { .. } => self.handle_noop(state),
650            OverlordEvent::ClaimProgressPassPaidReward { .. } => self.handle_noop(state),
651            OverlordEvent::ClaimAllProgressPassRewards { .. } => self.handle_noop(state),
652            OverlordEvent::UserRating { .. } => self.handle_noop(state),
653            OverlordEvent::ShowRateUs {} => self.handle_noop(state),
654            OverlordEvent::RateUsShown {} => self.handle_rate_us_shown(state),
655            OverlordEvent::RateUs { .. } => self.handle_noop(state),
656            OverlordEvent::WatchAd { .. } => self.handle_noop(state),
657            OverlordEvent::ShowBird { .. } => self.handle_show_bird(state),
658            OverlordEvent::BirdShown {} => self.handle_bird_shown(state),
659            OverlordEvent::ResetAdUsage { placements } => {
660                self.handle_reset_ad_usage(placements, state)
661            }
662            OverlordEvent::ResetInstantRewardGemsPressCount {} => {
663                self.handle_reset_instant_reward_gems_press_count(state)
664            }
665        };
666
667        if result.success() {
668            let (state, events) = result.state_and_events_mut();
669            self.apply_success_hooks(state, events, event);
670        }
671
672        result.events_mut().append(&mut events);
673        result
674    }
675
676    pub fn collect_due_scheduled(&mut self, current_tick: u64) -> Vec<OverlordEvent> {
677        self.fight_clock.collect_due(current_tick)
678    }
679
680    pub fn compute_fields(
681        &self,
682        state: &mut OverlordState,
683        prev_state: &OverlordState,
684    ) -> Vec<OverlordEvent> {
685        let _span = tracing::info_span!("compute_fields").entered();
686        let start = std::time::Instant::now();
687        let game_config = self.game_config.get();
688        let mut events = Vec::new();
689        self.log_currency_change_metrics(state, prev_state);
690
691        if state.character_state.character.character_experience
692            != prev_state.character_state.character.character_experience
693        {
694            events.append(&mut self.compute_character_level(state));
695        }
696
697        if state.character_state != prev_state.character_state {
698            match attributes::calculate_player_entity_stats_with_zeroes(
699                &EntityState::Character(&state.character_state),
700                &game_config,
701            ) {
702                Ok(attributes) => {
703                    state.character_state.player_attributes = attributes.attributes.clone();
704                    state.character_state.player_attributes.remove_zeroes();
705
706                    if let Some(fight) = &mut state.active_fight
707                        && let Some(entity) = fight
708                            .entities
709                            .iter_mut()
710                            .find(|ent| ent.id == fight.player_id)
711                    {
712                        entity.max_hp = attributes.max_hp;
713                        entity
714                            .attributes
715                            .0
716                            .extend(attributes.attributes.0.iter().map(|(k, v)| (k.clone(), *v)));
717                        entity.attributes.remove_zeroes();
718                    }
719                }
720                Err(err) => {
721                    tracing::error!("Failed to fill player entity attributes: {:?}", err);
722                }
723            }
724
725            match crate::behaviors::power::character_power(
726                &crate::behaviors::power::CharacterPowerCtx {
727                    character: &state.character_state,
728                    config: &game_config,
729                    lookups: self.behaviors.lookups(),
730                },
731            ) {
732                Ok(power) => state.character_state.character.power = power,
733                Err(err) => tracing::error!("Couldn't calculate player power: {err:?}"),
734            }
735        }
736
737        self.compute_fields_duration
738            .record(start.elapsed().as_secs_f64(), &[]);
739
740        events
741    }
742}
743
744impl OverlordLogic {
745    pub fn new(
746        game_config: configs::SharedGameConfig,
747        behaviors: Arc<BehaviorRegistry>,
748        frontend: bool,
749    ) -> Self {
750        let meter = opentelemetry::global::meter("compute_fields");
751        Self {
752            game_config,
753            behaviors,
754            frontend,
755            start_fight_tick: 0,
756            fight_clock: crate::fight::FightClock::default(),
757            ended_fight_id: None,
758            last_currency_source: None,
759            compute_fields_duration: meter
760                .f64_histogram("compute_fields_duration_seconds")
761                .with_boundaries(vec![
762                    0.0001, 0.0005, 0.001, 0.002, 0.005, 0.01, 0.02, 0.05, 0.1,
763                ])
764                .build(),
765        }
766    }
767
768    /// Sets the source label used by `log_currency_change_metrics` for the next
769    /// `compute_fields` diff. Use when state mutations happen outside the normal
770    /// `CurrencyIncrease` / `CurrencyDecrease` event flow (e.g., starter bundle
771    /// seeding on character creation).
772    pub fn set_last_currency_source(&mut self, source: Option<CurrencySource>) {
773        self.last_currency_source = source.map(|s| format!("{s:?}"));
774    }
775
776    /// Swaps the config this logic reads from. Locale-translated configs only
777    /// differ in localized strings, so swapping mid-session is safe — used by
778    /// the backend to switch a connection to the user's localized GameConfig
779    /// once their language is known after auth.
780    pub fn set_game_config(&mut self, game_config: configs::SharedGameConfig) {
781        self.game_config = game_config;
782    }
783
784    pub fn handle_set_custom_value(
785        &self,
786        key: String,
787        value: i64,
788        mut state: OverlordState,
789    ) -> EventHandleResult<OverlordEvent, OverlordState> {
790        state
791            .character_state
792            .character
793            .custom_values
794            .insert(&key, value);
795        EventHandleResult::ok(state)
796    }
797
798    fn handle_set_connection_store(
799        &self,
800        key: String,
801        value: i64,
802        mut state: OverlordState,
803    ) -> EventHandleResult<OverlordEvent, OverlordState> {
804        state.connection_store.insert(key, value);
805        EventHandleResult::ok(state)
806    }
807
808    pub fn handle_new_character_level(
809        &self,
810        level: i64,
811        mut state: OverlordState,
812    ) -> EventHandleResult<OverlordEvent, OverlordState> {
813        state.character_state.character.character_level = level;
814
815        EventHandleResult::ok(state)
816    }
817
818    fn handle_purchases_banned(
819        &self,
820        mut state: OverlordState,
821    ) -> EventHandleResult<OverlordEvent, OverlordState> {
822        state.character_state.character.purchases_banned = true;
823        EventHandleResult::ok(state)
824    }
825
826    fn handle_new_mail(
827        &self,
828        new_mail: Mail,
829        mut state: OverlordState,
830    ) -> EventHandleResult<OverlordEvent, OverlordState> {
831        if !state.incoming_mails.contains(&new_mail) {
832            state.incoming_mails.push(new_mail);
833        }
834
835        EventHandleResult::ok(state)
836    }
837
838    pub fn handle_noop(
839        &self,
840        state: OverlordState,
841    ) -> EventHandleResult<OverlordEvent, OverlordState> {
842        EventHandleResult::ok(state)
843    }
844
845    pub fn handle_tutorial_step_completed(
846        &self,
847        step_number: i16,
848        mut state: OverlordState,
849    ) -> EventHandleResult<OverlordEvent, OverlordState> {
850        if !state
851            .character_state
852            .character
853            .completed_tutorials
854            .contains(&step_number)
855        {
856            state
857                .character_state
858                .character
859                .completed_tutorials
860                .push(step_number);
861        } else {
862            tracing::error!("Provided step {step_number} is already in state");
863        }
864        EventHandleResult::ok(state)
865    }
866}
867
868impl OverlordLogic {
869    fn log_currency_change_metrics(&self, state: &OverlordState, prev_state: &OverlordState) {
870        let character_id = state.character_state.character.id;
871        // Chapter the player is in as this currency change lands, so a per-run
872        // report can attribute faucet/sink per chapter (the glut detector).
873        let chapter_level = state.character_state.character.current_chapter_level;
874        let current_currencies = &state.character_state.currencies;
875        let prev_currencies = &prev_state.character_state.currencies;
876        let source = self.last_currency_source.as_deref().unwrap_or("unknown");
877
878        // These currency events are emitted inline (not through the monolith's
879        // deferred-metrics span), so they need their own run-id tagging for the
880        // per-run balance report to isolate them. Enter a parent span carrying
881        // the sim run id ONLY when set — in production it is empty, so no span
882        // is entered and these events are byte-identical to before.
883        let run_id = analytics::run_id::run_id();
884        let _run_span = (!run_id.is_empty())
885            .then(|| tracing::info_span!(target: METRICS_TARGET, "sim_run", run_id = %run_id));
886        let _run_guard = _run_span.as_ref().map(|s| s.enter());
887
888        for current_currency in current_currencies {
889            let amount_before = prev_currencies
890                .iter()
891                .find(|unit| unit.currency_id == current_currency.currency_id)
892                .map_or(0, |unit| unit.amount);
893            let delta = current_currency.amount - amount_before;
894            let amount_after = current_currency.amount;
895            if delta > 0 {
896                tracing::info!(
897                    target: METRICS_TARGET,
898                    character_id = %character_id,
899                    event_type = "increase_currency",
900                    currency_id = %current_currency.currency_id,
901                    amount = delta,
902                    amount_before,
903                    amount_after,
904                    chapter_level,
905                    source = %source,
906                    "Add currency",
907                );
908            } else if delta < 0 {
909                tracing::info!(
910                    target: METRICS_TARGET,
911                    character_id = %character_id,
912                    event_type = "decrease_currency",
913                    currency_id = %current_currency.currency_id,
914                    amount = -delta,
915                    amount_before,
916                    amount_after,
917                    chapter_level,
918                    source = %source,
919                    "Decrease currency",
920                );
921            }
922        }
923
924        for prev_currency in prev_currencies {
925            if current_currencies
926                .iter()
927                .any(|unit| unit.currency_id == prev_currency.currency_id)
928            {
929                continue;
930            }
931
932            // Currency disappeared from state: emit a decrease whose
933            // post-balance is 0 (entry removed). Pre-balance is the value the
934            // prev state still carried.
935            let delta = -prev_currency.amount;
936            if delta > 0 {
937                tracing::info!(
938                    target: METRICS_TARGET,
939                    character_id = %character_id,
940                    event_type = "increase_currency",
941                    currency_id = %prev_currency.currency_id,
942                    amount = delta,
943                    amount_before = prev_currency.amount,
944                    amount_after = prev_currency.amount + delta,
945                    chapter_level,
946                    source = %source,
947                    "Add currency",
948                );
949            } else if delta < 0 {
950                tracing::info!(
951                    target: METRICS_TARGET,
952                    character_id = %character_id,
953                    event_type = "decrease_currency",
954                    currency_id = %prev_currency.currency_id,
955                    amount = -delta,
956                    amount_before = prev_currency.amount,
957                    amount_after = 0_i64,
958                    chapter_level,
959                    source = %source,
960                    "Decrease currency",
961                );
962            }
963        }
964    }
965
966    pub fn compute_character_level(&self, state: &mut OverlordState) -> Vec<OverlordEvent> {
967        let game_config = self.game_config.get();
968        let stored_level = state.character_state.character.character_level;
969        let current_level = match game_config.character_level(stored_level) {
970            Some(level) => level,
971            None => {
972                // Config drift: the persisted level is absent from the shipped
973                // config (e.g. a rollback that lowered max level). Clamp to the
974                // nearest known level instead of panicking inside the per-event
975                // compute_fields path.
976                let Some(nearest) = game_config
977                    .character_levels
978                    .iter()
979                    .min_by_key(|x| x.level.abs_diff(stored_level))
980                else {
981                    tracing::error!(
982                        "No character_levels in config; skipping level computation for level={stored_level}"
983                    );
984                    return Vec::new();
985                };
986                tracing::warn!(
987                    "No character_level with level={stored_level} in config; clamping to nearest known level {}",
988                    nearest.level
989                );
990                nearest
991            }
992        };
993
994        let new_level = game_config
995            .character_levels
996            .iter()
997            .fold(current_level, |mut acc, x| {
998                if x.level > acc.level
999                    && state.character_state.character.character_experience >= x.required_experience
1000                {
1001                    acc = x;
1002                }
1003                acc
1004            });
1005
1006        // Compare against the stored level (identical to `current_level.level`
1007        // when the lookup succeeded) so a clamped level is written back to
1008        // state via NewCharacterLevel and the drift self-heals.
1009        if new_level.level != stored_level {
1010            vec![OverlordEvent::NewCharacterLevel {
1011                level: new_level.level,
1012            }]
1013        } else {
1014            Vec::new()
1015        }
1016    }
1017
1018    fn try_give_new_offers(
1019        &self,
1020        state: &mut OverlordState,
1021        events: &mut Vec<EventPluginized<OverlordEvent, OverlordState>>,
1022        trigger_event: &OverlordEvent,
1023    ) {
1024        for offer in &self.game_config.get().offers_templates {
1025            if !offer.enabled {
1026                continue;
1027            }
1028
1029            if offer.limit_of_buys.is_some_and(|limit| {
1030                state
1031                    .offers_info
1032                    .offer_buy_counts
1033                    .get(&offer.id)
1034                    .copied()
1035                    .unwrap_or(0)
1036                    >= limit as u32
1037            }) {
1038                continue;
1039            }
1040
1041            if state
1042                .offers_info
1043                .active_offers
1044                .iter()
1045                .any(|x| x.template_id == offer.id)
1046            {
1047                continue;
1048            }
1049
1050            if !offer.events_subscribe.contains(&trigger_event.to_string()) {
1051                continue;
1052            }
1053
1054            let should_give =
1055                match self.should_give_new_offer(&state.character_state, offer, trigger_event) {
1056                    Ok(progress) => progress,
1057                    Err(e) => {
1058                        tracing::error!(
1059                            "Failed determining for offer id: {}\n Error: {e:?}",
1060                            offer.id
1061                        );
1062                        continue;
1063                    }
1064                };
1065
1066            if should_give {
1067                events.push(EventPluginized::now(OverlordEvent::NewOffer {
1068                    offer_template_id: offer.id,
1069                }));
1070            }
1071        }
1072    }
1073
1074    fn should_give_new_offer(
1075        &self,
1076        character_state: &CharacterState,
1077        offer: &OfferTemplate,
1078        trigger_event: &OverlordEvent,
1079    ) -> anyhow::Result<bool> {
1080        // By the time we get here the candidate offer is already `enabled` and
1081        // the firing event is in its `events_subscribe` list (see
1082        // `try_give_new_offers`), so a subscribing offer is auto-given when its
1083        // event fires. No deployed offer subscribes to any event, so this path
1084        // only drives the test fixture's triggered offers.
1085        crate::behaviors::quests::triggers::always_trigger(
1086            &crate::behaviors::quests::triggers::TriggerCtx {
1087                trigger_event,
1088                character: character_state,
1089                offer,
1090                config: &self.game_config.get(),
1091            },
1092        )
1093    }
1094
1095    fn update_quests_progress(
1096        &self,
1097        state: &mut OverlordState,
1098        events: &mut Vec<EventPluginized<OverlordEvent, OverlordState>>,
1099        trigger_event: &OverlordEvent,
1100    ) {
1101        let game_config = self.game_config.get();
1102        let mut progress_pass_tier_completed = false;
1103        let all_active_quests = state.quest_groups.get_not_claimed_quests_mut();
1104        let active_fight = &state.active_fight;
1105
1106        for active_quest in all_active_quests {
1107            let Some(quest_template) = game_config.quest(active_quest.id) else {
1108                continue;
1109            };
1110
1111            if state.patron.is_none()
1112                && (quest_template.quest_group_type == QuestGroupType::PatronLifetime
1113                    || quest_template.quest_group_type == QuestGroupType::PatronDaily)
1114            {
1115                continue;
1116            }
1117
1118            if !quest_template
1119                .events_subscribe
1120                .contains(&trigger_event.to_string())
1121            {
1122                continue;
1123            }
1124
1125            if quest_template.quest_group_type == QuestGroupType::LoopTask
1126                && !quest_template.progress_if_inactive
1127                && let Some(active_loop_task_id) =
1128                    state.character_state.character.active_loop_task_id
1129                && active_loop_task_id != quest_template.id
1130            {
1131                continue;
1132            }
1133
1134            let was_completed = active_quest.is_completed(quest_template.progress_target);
1135
1136            if !active_quest.is_completed(quest_template.progress_target) {
1137                let progress = match self.get_quest_progress(
1138                    &state.character_state,
1139                    active_fight,
1140                    active_quest,
1141                    quest_template.progress_behavior.as_deref(),
1142                    trigger_event,
1143                    &game_config,
1144                ) {
1145                    Ok(progress) => progress,
1146                    Err(e) => {
1147                        tracing::error!(
1148                            "Failed updating quest progress for quest id: {}\n Error: {e:?}",
1149                            active_quest.id
1150                        );
1151                        continue;
1152                    }
1153                };
1154                active_quest.current = progress;
1155            }
1156
1157            if !was_completed {
1158                if (quest_template.quest_group_type == QuestGroupType::PatronLifetime
1159                    || quest_template.quest_group_type == QuestGroupType::PatronDaily)
1160                    && active_quest.is_completed(quest_template.progress_target)
1161                {
1162                    events.push(EventPluginized::now(OverlordEvent::PatronQuestCompleted {
1163                        quest_id: active_quest.id,
1164                    }));
1165                }
1166
1167                if (quest_template.quest_group_type != QuestGroupType::Hidden)
1168                    && active_quest.is_completed(quest_template.progress_target)
1169                {
1170                    events.push(EventPluginized::now(OverlordEvent::QuestCompleted {
1171                        quest_id: active_quest.id,
1172                    }));
1173                }
1174
1175                if quest_template.quest_group_type == QuestGroupType::ProgressPass
1176                    && active_quest.is_completed(quest_template.progress_target)
1177                {
1178                    progress_pass_tier_completed = true;
1179                }
1180            }
1181
1182            if (quest_template.quest_group_type == QuestGroupType::Hidden)
1183                && active_quest.is_completed(quest_template.progress_target)
1184            {
1185                events.push(EventPluginized::now(OverlordEvent::HiddenQuestCompleted {
1186                    quest_id: active_quest.id,
1187                }));
1188            }
1189        }
1190
1191        if progress_pass_tier_completed {
1192            for tier_state in &mut state.progress_pass.tiers {
1193                let Some(tier_template) = game_config
1194                    .progress_pass
1195                    .tiers
1196                    .iter()
1197                    .find(|t| t.tier == tier_state.tier)
1198                else {
1199                    continue;
1200                };
1201                tier_state.is_unlocked = essences::progress_pass::is_progress_pass_tier_unlocked(
1202                    tier_template,
1203                    &state.quest_groups.progress_pass,
1204                    &game_config.quests,
1205                );
1206            }
1207        }
1208    }
1209
1210    pub fn get_quest_progress(
1211        &self,
1212        character_state: &CharacterState,
1213        active_fight: &Option<ActiveFight>,
1214        quest: &QuestInstance,
1215        progress_script_native: Option<&str>,
1216        trigger_event: &OverlordEvent,
1217        game_config: &GameConfig,
1218    ) -> anyhow::Result<i64> {
1219        // Native `conditional_progress` port (named by the quest's
1220        // `progress_script_native` ref). `quest` is the pre-update instance,
1221        // mirroring the `Quest` const the script saw. Missing / unregistered ref
1222        // leaves the progress unchanged.
1223        let Some(name) = progress_script_native else {
1224            return Ok(quest.current);
1225        };
1226        let Some(f) = self.behaviors.conditional_progress_fn(name) else {
1227            return Ok(quest.current);
1228        };
1229        f(
1230            &crate::behaviors::quests::progress::ConditionalProgressCtx {
1231                event: trigger_event,
1232                character_state,
1233                active_fight,
1234                quest,
1235                config: game_config,
1236                lookups: self.behaviors.lookups(),
1237            },
1238        )
1239    }
1240
1241    pub fn handle_currency_increase(
1242        &self,
1243        currencies: &[CurrencyUnit],
1244        mut state: OverlordState,
1245    ) -> EventHandleResult<OverlordEvent, OverlordState> {
1246        increase_currencies(&mut state.character_state.currencies, currencies);
1247        EventHandleResult::ok(state)
1248    }
1249
1250    pub fn handle_currency_decrease(
1251        &self,
1252        currencies: &[CurrencyUnit],
1253        mut state: OverlordState,
1254    ) -> EventHandleResult<OverlordEvent, OverlordState> {
1255        let non_zero: Vec<_> = currencies
1256            .iter()
1257            .filter(|c| c.amount > 0)
1258            .cloned()
1259            .collect();
1260        if let Err(e) = decrease_currencies(&mut state.character_state.currencies, &non_zero) {
1261            tracing::error!("CurrencyDecrease failed: {e}");
1262            return EventHandleResult::fail(state);
1263        }
1264        EventHandleResult::ok(state)
1265    }
1266
1267    /// Creates a `CurrencyIncrease` event. The actual state mutation happens when the
1268    /// event is processed by the handler — callers don't need to mutate state manually.
1269    pub fn currency_increase(
1270        currencies: &[CurrencyUnit],
1271        currency_source: CurrencySource,
1272    ) -> EventPluginized<OverlordEvent, OverlordState> {
1273        EventPluginized::now(OverlordEvent::CurrencyIncrease {
1274            currencies: currencies.to_owned(),
1275            currency_source,
1276        })
1277    }
1278
1279    /// Creates a `CurrencyDecrease` event after validating sufficient balance.
1280    /// Returns `None` if the player doesn't have enough currency.
1281    /// The actual state mutation happens when the event is processed by the handler.
1282    pub fn currency_decrease(
1283        state: &OverlordState,
1284        currencies: &[CurrencyUnit],
1285        currency_consumer: CurrencyConsumer,
1286    ) -> Option<EventPluginized<OverlordEvent, OverlordState>> {
1287        if !check_can_decrease_currencies(&state.character_state.currencies, currencies) {
1288            tracing::error!(
1289                "currency_decrease({currency_consumer:?}): not enough currency, \
1290                 required={currencies:?}, available={:?}",
1291                state.character_state.currencies
1292            );
1293            return None;
1294        }
1295        Some(EventPluginized::now(OverlordEvent::CurrencyDecrease {
1296            currencies: currencies.to_owned(),
1297            currency_consumer,
1298        }))
1299    }
1300
1301    fn handle_reset_ad_usage(
1302        &mut self,
1303        placements: &[AdPlacement],
1304        mut state: OverlordState,
1305    ) -> EventHandleResult<OverlordEvent, OverlordState> {
1306        let now = ::time::utc_now();
1307        for placement in placements {
1308            state.character_state.ad_usage.insert(
1309                *placement,
1310                AdUsageData {
1311                    daily_count: 0,
1312                    last_reset_at: now,
1313                },
1314            );
1315        }
1316
1317        EventHandleResult::ok(state)
1318    }
1319
1320    fn handle_reset_instant_reward_gems_press_count(
1321        &mut self,
1322        mut state: OverlordState,
1323    ) -> EventHandleResult<OverlordEvent, OverlordState> {
1324        state
1325            .character_state
1326            .character
1327            .instant_reward_gems_press_count = 0;
1328        EventHandleResult::ok(state)
1329    }
1330
1331    pub fn handle_show_bird(
1332        &mut self,
1333        mut state: OverlordState,
1334    ) -> EventHandleResult<OverlordEvent, OverlordState> {
1335        let bird_config = &self.game_config.get().ads_settings.bird_ad;
1336        let cooldown_until = ::time::utc_now()
1337            + chrono::TimeDelta::seconds(bird_config.post_show_cooldown_sec as i64);
1338        state.character_state.character.bird_cooldown_until = Some(cooldown_until);
1339        EventHandleResult::ok(state)
1340    }
1341
1342    pub fn handle_bird_shown(
1343        &mut self,
1344        mut state: OverlordState,
1345    ) -> EventHandleResult<OverlordEvent, OverlordState> {
1346        let bird_config = &self.game_config.get().ads_settings.bird_ad;
1347        let cooldown_until =
1348            ::time::utc_now() + chrono::TimeDelta::seconds(bird_config.cooldown_sec as i64);
1349        state.character_state.character.bird_cooldown_until = Some(cooldown_until);
1350        EventHandleResult::ok(state)
1351    }
1352
1353    pub fn handle_rate_us_shown(
1354        &mut self,
1355        mut state: OverlordState,
1356    ) -> EventHandleResult<OverlordEvent, OverlordState> {
1357        state.character_state.character.rate_us_shown = true;
1358        EventHandleResult::ok(state)
1359    }
1360}