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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 OverlordEvent::CreateAbilityPreset { .. } => self.handle_noop(state),
454 OverlordEvent::EditAbilityPreset { .. } => self.handle_noop(state),
455
456 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 OverlordEvent::ClaimBundleStepGeneric { .. } => self.handle_noop(state),
466 OverlordEvent::AddBundleGroup { bundle_ids } => {
467 self.handle_add_bundle_group(bundle_ids, state)
468 }
469
470 OverlordEvent::ChangeClass { .. } => self.handle_noop(state),
472
473 OverlordEvent::LinkGuestAccount { .. } => self.handle_noop(state),
475 OverlordEvent::SetUsername { .. } => self.handle_noop(state),
476 OverlordEvent::SetCharacterBlocked { .. } => self.handle_noop(state),
477
478 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 OverlordEvent::CurrencyIncrease { currencies, .. } => {
494 self.handle_currency_increase(currencies, state)
495 }
496 OverlordEvent::CurrencyDecrease { currencies, .. } => {
497 self.handle_currency_decrease(currencies, state)
498 }
499 OverlordEvent::BuySkins { .. } => self.handle_noop(state),
501 OverlordEvent::EquipAndUnequipSkins { .. } => self.handle_noop(state),
502
503 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 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 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 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 OverlordEvent::TutorialShown { .. } => self.handle_noop(state),
547 OverlordEvent::TutorialStepCompleted { step_number } => {
548 self.handle_tutorial_step_completed(*step_number, state)
549 }
550
551 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 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 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 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 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 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}