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, source } => {
467 self.handle_add_bundle_group(bundle_ids, *source, 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 OverlordEvent::ClientLifecycle { .. } => self.handle_noop(state),
551
552 OverlordEvent::AddCharacterToParty { .. } => self.handle_noop(state),
554 OverlordEvent::RemoveCharacterFromParty {} => self.handle_noop(state),
555 OverlordEvent::RefreshPartyPlayers {} => self.handle_noop(state),
556 OverlordEvent::RefreshPartyMemberState {} => self.handle_noop(state),
557
558 OverlordEvent::StartTalentResearch { .. } => self.handle_noop(state),
560 OverlordEvent::TalentResearchStarted { .. } => self.handle_noop(state),
561 OverlordEvent::SpeedupTalentResearch {} => self.handle_noop(state),
562 OverlordEvent::SkipTalentResearch {} => self.handle_noop(state),
563 OverlordEvent::ClaimTalentResearch {} => self.handle_claim_talent_research(state),
564
565 OverlordEvent::StatueRoll { .. } => self.handle_noop(state),
567 OverlordEvent::StatueActivateSet { .. } => self.handle_noop(state),
568 OverlordEvent::StatueAddSet {} => self.handle_noop(state),
569 OverlordEvent::StatueRenameSet { .. } => self.handle_noop(state),
570 OverlordEvent::StatueLockSlot { .. } => self.handle_noop(state),
571 OverlordEvent::StatueRollNewSlot { .. } => self.handle_noop(state),
572 OverlordEvent::UserRating { .. } => self.handle_noop(state),
573 OverlordEvent::WatchAd { .. } => self.handle_noop(state),
574 OverlordEvent::ShowBird { .. } => self.handle_show_bird(state),
575 OverlordEvent::BirdShown {} => self.handle_bird_shown(state),
576 OverlordEvent::ResetAdUsage { placements } => {
577 self.handle_reset_ad_usage(placements, state)
578 }
579 OverlordEvent::ResetInstantRewardGemsPressCount {} => {
580 self.handle_reset_instant_reward_gems_press_count(state)
581 }
582 };
583
584 if result.success() {
585 let (state, events) = result.state_and_events_mut();
586 self.update_quests_progress(state, events, event);
587 self.try_give_new_offers(state, events, event);
588 }
589
590 result.events_mut().append(&mut events);
591 result
592 }
593
594 fn compute_fields(
595 &self,
596 state: &mut OverlordState,
597 prev_state: &OverlordState,
598 ) -> Vec<OverlordEvent> {
599 let _span = tracing::info_span!("compute_fields").entered();
600 let start = std::time::Instant::now();
601 let game_config = self.game_config.get();
602 let mut events = Vec::new();
603 self.log_currency_change_metrics(state, prev_state);
604
605 if state.character_state.character.character_experience
606 != prev_state.character_state.character.character_experience
607 {
608 events.append(&mut self.compute_character_level(state));
609 }
610
611 if state.character_state != prev_state.character_state {
612 match attributes::calculate_player_entity_stats_with_zeroes(
613 &EntityState::Character(&state.character_state),
614 &game_config,
615 &self.script_runner,
616 ) {
617 Ok(attributes) => {
618 state.character_state.player_attributes = attributes.attributes.clone();
619 state.character_state.player_attributes.remove_zeroes();
620
621 if let Some(fight) = &mut state.active_fight
622 && let Some(entity) = fight
623 .entities
624 .iter_mut()
625 .find(|ent| ent.id == fight.player_id)
626 {
627 entity.max_hp = attributes.max_hp;
628 entity
630 .attributes
631 .0
632 .extend(attributes.attributes.0.iter().map(|(k, v)| (k.clone(), *v)));
633 entity.attributes.remove_zeroes();
634 }
635 }
636 Err(err) => {
637 tracing::error!("Failed to fill player entity attributes: {:?}", err);
638 }
639 }
640
641 if let Ok(power) = self
642 .script_runner
643 .calculate_character_power(&state.character_state, &game_config)
644 {
645 state.character_state.character.power = power;
646 }
647 }
648
649 self.compute_fields_duration
650 .record(start.elapsed().as_secs_f64(), &[]);
651
652 events
653 }
654}
655
656impl OverlordAsyncEventHandler {
657 pub fn new(game_config: configs::SharedGameConfig, frontend: bool) -> Self {
658 let game_config_for_runner = game_config.get();
659 let script_runner = ScriptRunner::new(&game_config_for_runner);
660 let meter = opentelemetry::global::meter("compute_fields");
661 Self {
662 game_config,
663 script_runner,
664 frontend,
665 start_fight_tick: 0,
666 last_currency_source: None,
667 compute_fields_duration: meter
668 .f64_histogram("compute_fields_duration_seconds")
669 .with_boundaries(vec![
670 0.0001, 0.0005, 0.001, 0.002, 0.005, 0.01, 0.02, 0.05, 0.1,
671 ])
672 .build(),
673 }
674 }
675
676 pub fn set_last_currency_source(&mut self, source: Option<CurrencySource>) {
681 self.last_currency_source = source.map(|s| format!("{s:?}"));
682 }
683
684 fn handle_set_custom_value(
685 &self,
686 key: String,
687 value: i64,
688 mut state: OverlordState,
689 ) -> EventHandleResult<OverlordEvent, OverlordState> {
690 state
691 .character_state
692 .character
693 .custom_values
694 .insert(&key, value);
695 EventHandleResult::ok(state)
696 }
697
698 fn handle_set_connection_store(
699 &self,
700 key: String,
701 value: i64,
702 mut state: OverlordState,
703 ) -> EventHandleResult<OverlordEvent, OverlordState> {
704 state.connection_store.insert(key, value);
705 EventHandleResult::ok(state)
706 }
707
708 fn handle_new_character_level(
709 &self,
710 level: i64,
711 mut state: OverlordState,
712 ) -> EventHandleResult<OverlordEvent, OverlordState> {
713 state.character_state.character.character_level = level;
714
715 EventHandleResult::ok(state)
716 }
717
718 fn handle_purchases_banned(
719 &self,
720 mut state: OverlordState,
721 ) -> EventHandleResult<OverlordEvent, OverlordState> {
722 state.character_state.character.purchases_banned = true;
723 EventHandleResult::ok(state)
724 }
725
726 fn handle_new_mail(
727 &self,
728 new_mail: Mail,
729 mut state: OverlordState,
730 ) -> EventHandleResult<OverlordEvent, OverlordState> {
731 if !state.incoming_mails.contains(&new_mail) {
732 state.incoming_mails.push(new_mail);
733 }
734
735 EventHandleResult::ok(state)
736 }
737
738 fn handle_noop(&self, state: OverlordState) -> EventHandleResult<OverlordEvent, OverlordState> {
739 EventHandleResult::ok(state)
740 }
741
742 fn handle_tutorial_step_completed(
743 &self,
744 step_number: i16,
745 mut state: OverlordState,
746 ) -> EventHandleResult<OverlordEvent, OverlordState> {
747 if !state
748 .character_state
749 .character
750 .completed_tutorials
751 .contains(&step_number)
752 {
753 state
754 .character_state
755 .character
756 .completed_tutorials
757 .push(step_number);
758 } else {
759 tracing::error!("Provided step {step_number} is already in state");
760 }
761 EventHandleResult::ok(state)
762 }
763}
764
765impl OverlordAsyncEventHandler {
766 fn log_currency_change_metrics(&self, state: &OverlordState, prev_state: &OverlordState) {
767 let character_id = state.character_state.character.id;
768 let current_currencies = &state.character_state.currencies;
769 let prev_currencies = &prev_state.character_state.currencies;
770 let source = self.last_currency_source.as_deref().unwrap_or("unknown");
771
772 for current_currency in current_currencies {
773 let amount_before = prev_currencies
774 .iter()
775 .find(|unit| unit.currency_id == current_currency.currency_id)
776 .map_or(0, |unit| unit.amount);
777 let delta = current_currency.amount - amount_before;
778 let amount_after = current_currency.amount;
779 if delta > 0 {
780 tracing::info!(
781 target: METRICS_TARGET,
782 character_id = %character_id,
783 event_type = "increase_currency",
784 currency_id = %current_currency.currency_id,
785 amount = delta,
786 amount_before,
787 amount_after,
788 source = %source,
789 "Add currency",
790 );
791 } else if delta < 0 {
792 tracing::info!(
793 target: METRICS_TARGET,
794 character_id = %character_id,
795 event_type = "decrease_currency",
796 currency_id = %current_currency.currency_id,
797 amount = -delta,
798 amount_before,
799 amount_after,
800 source = %source,
801 "Decrease currency",
802 );
803 }
804 }
805
806 for prev_currency in prev_currencies {
807 if current_currencies
808 .iter()
809 .any(|unit| unit.currency_id == prev_currency.currency_id)
810 {
811 continue;
812 }
813
814 let delta = -prev_currency.amount;
818 if delta > 0 {
819 tracing::info!(
820 target: METRICS_TARGET,
821 character_id = %character_id,
822 event_type = "increase_currency",
823 currency_id = %prev_currency.currency_id,
824 amount = delta,
825 amount_before = prev_currency.amount,
826 amount_after = prev_currency.amount + delta,
827 source = %source,
828 "Add currency",
829 );
830 } else if delta < 0 {
831 tracing::info!(
832 target: METRICS_TARGET,
833 character_id = %character_id,
834 event_type = "decrease_currency",
835 currency_id = %prev_currency.currency_id,
836 amount = -delta,
837 amount_before = prev_currency.amount,
838 amount_after = 0_i64,
839 source = %source,
840 "Decrease currency",
841 );
842 }
843 }
844 }
845
846 pub fn compute_character_level(&self, state: &mut OverlordState) -> Vec<OverlordEvent> {
847 let game_config = self.game_config.get();
848 let current_level = game_config
849 .require_character_level(state.character_state.character.character_level)
850 .unwrap_or_else(|err| panic!("{err:?}"));
851
852 let new_level = game_config
853 .character_levels
854 .iter()
855 .fold(current_level, |mut acc, x| {
856 if x.level > acc.level
857 && state.character_state.character.character_experience >= x.required_experience
858 {
859 acc = x;
860 }
861 acc
862 });
863
864 if new_level.level != current_level.level {
865 vec![OverlordEvent::NewCharacterLevel {
866 level: new_level.level,
867 }]
868 } else {
869 Vec::new()
870 }
871 }
872
873 fn try_give_new_offers(
874 &self,
875 state: &mut OverlordState,
876 events: &mut Vec<EventPluginized<OverlordEvent, OverlordState>>,
877 trigger_event: &OverlordEvent,
878 ) {
879 for offer in &self.game_config.get().offers_templates {
880 if !offer.enabled {
881 continue;
882 }
883
884 if offer.limit_of_buys.is_some_and(|limit| {
885 state
886 .offers_info
887 .offer_buy_counts
888 .get(&offer.id)
889 .copied()
890 .unwrap_or(0)
891 >= limit as u32
892 }) {
893 continue;
894 }
895
896 if state
897 .offers_info
898 .active_offers
899 .iter()
900 .any(|x| x.template_id == offer.id)
901 {
902 continue;
903 }
904
905 if !offer.events_subscribe.contains(&trigger_event.to_string()) {
906 continue;
907 }
908
909 let should_give = match self.should_give_new_offer(
910 &state.character_state,
911 offer,
912 &offer.trigger_script,
913 trigger_event.clone(),
914 ) {
915 Ok(progress) => progress,
916 Err(e) => {
917 tracing::error!(
918 "Failed determining for offer id: {}\n Error: {e:?}",
919 offer.id
920 );
921 continue;
922 }
923 };
924
925 if should_give {
926 events.push(EventPluginized::now(OverlordEvent::NewOffer {
927 offer_template_id: offer.id,
928 }));
929 }
930 }
931 }
932
933 fn should_give_new_offer(
934 &self,
935 character_state: &CharacterState,
936 offer: &OfferTemplate,
937 script: &str,
938 trigger_event: OverlordEvent,
939 ) -> anyhow::Result<bool> {
940 self.script_runner.run_expression::<bool>(
941 |mut scope_setter| {
942 trigger_event.add_event_to_scope(&mut scope_setter, "Event");
943 scope_setter.set_const("CharacterState", character_state.clone());
944 scope_setter.set_const("Offer", offer.clone());
945 scope_setter
946 },
947 script,
948 )
949 }
950
951 fn update_quests_progress(
952 &self,
953 state: &mut OverlordState,
954 events: &mut Vec<EventPluginized<OverlordEvent, OverlordState>>,
955 trigger_event: &OverlordEvent,
956 ) {
957 let game_config = self.game_config.get();
958 let all_active_quests = state.quest_groups.get_not_claimed_quests_mut();
959 let active_fight = &state.active_fight;
960
961 for active_quest in all_active_quests {
962 let Some(quest_template) = game_config.quest(active_quest.id) else {
963 continue;
964 };
965
966 if state.patron.is_none()
967 && (quest_template.quest_group_type == QuestGroupType::PatronLifetime
968 || quest_template.quest_group_type == QuestGroupType::PatronDaily)
969 {
970 continue;
971 }
972
973 if !quest_template
974 .events_subscribe
975 .contains(&trigger_event.to_string())
976 {
977 continue;
978 }
979
980 if quest_template.quest_group_type == QuestGroupType::LoopTask
981 && !quest_template.progress_if_inactive
982 && let Some(active_loop_task_id) =
983 state.character_state.character.active_loop_task_id
984 && active_loop_task_id != quest_template.id
985 {
986 continue;
987 }
988
989 let was_completed = active_quest.is_completed(quest_template.progress_target);
990
991 if !active_quest.is_completed(quest_template.progress_target) {
992 let progress = match self.get_quest_progress(
993 &state.character_state,
994 active_fight,
995 active_quest,
996 &quest_template.progress_script,
997 trigger_event.clone(),
998 ) {
999 Ok(progress) => progress,
1000 Err(e) => {
1001 tracing::error!(
1002 "Failed updating quest progress for quest id: {}\n Error: {e:?}",
1003 active_quest.id
1004 );
1005 continue;
1006 }
1007 };
1008 active_quest.current = progress;
1009 }
1010
1011 if !was_completed {
1012 if (quest_template.quest_group_type == QuestGroupType::PatronLifetime
1013 || quest_template.quest_group_type == QuestGroupType::PatronDaily)
1014 && active_quest.is_completed(quest_template.progress_target)
1015 {
1016 events.push(EventPluginized::now(OverlordEvent::PatronQuestCompleted {
1017 quest_id: active_quest.id,
1018 }));
1019 }
1020
1021 if (quest_template.quest_group_type != QuestGroupType::Hidden)
1022 && active_quest.is_completed(quest_template.progress_target)
1023 {
1024 events.push(EventPluginized::now(OverlordEvent::QuestCompleted {
1025 quest_id: active_quest.id,
1026 }));
1027 }
1028 }
1029
1030 if (quest_template.quest_group_type == QuestGroupType::Hidden)
1031 && active_quest.is_completed(quest_template.progress_target)
1032 {
1033 events.push(EventPluginized::now(OverlordEvent::HiddenQuestCompleted {
1034 quest_id: active_quest.id,
1035 }));
1036 }
1037 }
1038 }
1039
1040 pub fn get_quest_progress(
1041 &self,
1042 character_state: &CharacterState,
1043 active_fight: &Option<ActiveFight>,
1044 quest: &QuestInstance,
1045 script: &str,
1046 trigger_event: OverlordEvent,
1047 ) -> anyhow::Result<i64> {
1048 self.script_runner.run_expression::<i64>(
1049 |mut scope_setter| {
1050 trigger_event.add_event_to_scope(&mut scope_setter, "Event");
1051 scope_setter.set_const("CharacterState", character_state.clone());
1052 if let Some(fight) = active_fight.as_ref() {
1053 scope_setter.set_const("ActiveFight", fight.clone());
1054 }
1055 scope_setter.set_const("Quest", quest.clone());
1056 scope_setter
1057 },
1058 script,
1059 )
1060 }
1061
1062 fn handle_currency_increase(
1063 &self,
1064 currencies: &[CurrencyUnit],
1065 mut state: OverlordState,
1066 ) -> EventHandleResult<OverlordEvent, OverlordState> {
1067 increase_currencies(&mut state.character_state.currencies, currencies);
1068 EventHandleResult::ok(state)
1069 }
1070
1071 fn handle_currency_decrease(
1072 &self,
1073 currencies: &[CurrencyUnit],
1074 mut state: OverlordState,
1075 ) -> EventHandleResult<OverlordEvent, OverlordState> {
1076 let non_zero: Vec<_> = currencies
1077 .iter()
1078 .filter(|c| c.amount > 0)
1079 .cloned()
1080 .collect();
1081 if let Err(e) = decrease_currencies(&mut state.character_state.currencies, &non_zero) {
1082 tracing::error!("CurrencyDecrease failed: {e}");
1083 return EventHandleResult::fail(state);
1084 }
1085 EventHandleResult::ok(state)
1086 }
1087
1088 pub fn currency_increase(
1091 currencies: &[CurrencyUnit],
1092 currency_source: CurrencySource,
1093 ) -> EventPluginized<OverlordEvent, OverlordState> {
1094 EventPluginized::now(OverlordEvent::CurrencyIncrease {
1095 currencies: currencies.to_owned(),
1096 currency_source,
1097 })
1098 }
1099
1100 pub fn currency_decrease(
1104 state: &OverlordState,
1105 currencies: &[CurrencyUnit],
1106 currency_consumer: CurrencyConsumer,
1107 ) -> Option<EventPluginized<OverlordEvent, OverlordState>> {
1108 if !check_can_decrease_currencies(&state.character_state.currencies, currencies) {
1109 tracing::error!(
1110 "currency_decrease({currency_consumer:?}): not enough currency, \
1111 required={currencies:?}, available={:?}",
1112 state.character_state.currencies
1113 );
1114 return None;
1115 }
1116 Some(EventPluginized::now(OverlordEvent::CurrencyDecrease {
1117 currencies: currencies.to_owned(),
1118 currency_consumer,
1119 }))
1120 }
1121
1122 pub fn process_currency(
1123 &self,
1124 raw_item: &BundleRawStep,
1125 script_runner: &ScriptRunner<OverlordEvent, OverlordState>,
1126 ) -> BundleElement {
1127 let currency_unit = from_es_currencies(
1128 &script_runner.run_currencies_calculate(|scope| scope, &raw_item.script),
1129 );
1130
1131 BundleElement::Currencies(currency_unit)
1132 }
1133
1134 pub fn process_ability(
1135 &self,
1136 raw_item: &BundleRawStep,
1137 script_runner: &ScriptRunner<OverlordEvent, OverlordState>,
1138 config: &GameConfig,
1139 ) -> BundleElement {
1140 let ability_shards =
1141 script_runner.run_ability_shards_calculate(|scope| scope, &raw_item.script);
1142
1143 let abilities = ability_shards
1144 .iter()
1145 .filter_map(|ability_shard| {
1146 let Some(template) = config.ability_template(ability_shard.ability_id).cloned()
1147 else {
1148 tracing::error!(
1149 "Failed to get ability with ability_id={}",
1150 ability_shard.ability_id
1151 );
1152 return None;
1153 };
1154
1155 Some(BundleAbility {
1156 template,
1157 shards_amount: ability_shard.shards_amount,
1158 })
1159 })
1160 .collect();
1161
1162 BundleElement::Abilities(abilities)
1163 }
1164
1165 pub fn process_item(
1166 &self,
1167 raw_item: &BundleRawStep,
1168 character_level: i64,
1169 script_runner: &ScriptRunner<OverlordEvent, OverlordState>,
1170 config: &GameConfig,
1171 ) -> anyhow::Result<BundleElement> {
1172 let mut rng = StdRng::seed_from_u64(OsRng.try_next_u64()?);
1173
1174 let item_ids = script_runner.run_item_ids_script(|scope| scope, &raw_item.script);
1175
1176 let items: Vec<Item> = item_ids
1177 .iter()
1178 .filter_map(|&item_id| {
1179 let Some(template) = config.item_template(item_id) else {
1180 tracing::error!("Failed to get ability with item_id={}", item_id);
1181 return None;
1182 };
1183
1184 let Some(rarity) = config.item_rarity(template.rarity_id).cloned() else {
1185 tracing::error!("Failed to get item rarity with id={}", template.rarity_id);
1186 return None;
1187 };
1188
1189 Some(generate_item_from_template(
1190 template,
1191 rarity,
1192 character_level,
1193 config,
1194 &mut rng,
1195 ))
1196 })
1197 .collect();
1198
1199 let finalized_items = items
1200 .into_iter()
1201 .filter_map(
1202 |mut item| match try_finalize_item(&mut item, config, script_runner) {
1203 Ok(()) => Some(item),
1204 Err(e) => {
1205 tracing::error!("Failed to finalize item: {}", e);
1206 None
1207 }
1208 },
1209 )
1210 .collect();
1211
1212 Ok(BundleElement::Items(finalized_items))
1213 }
1214
1215 fn handle_reset_ad_usage(
1216 &mut self,
1217 placements: &[AdPlacement],
1218 mut state: OverlordState,
1219 ) -> EventHandleResult<OverlordEvent, OverlordState> {
1220 let now = ::time::utc_now();
1221 for placement in placements {
1222 state.character_state.ad_usage.insert(
1223 *placement,
1224 AdUsageData {
1225 daily_count: 0,
1226 last_reset_at: now,
1227 },
1228 );
1229 }
1230
1231 EventHandleResult::ok(state)
1232 }
1233
1234 fn handle_reset_instant_reward_gems_press_count(
1235 &mut self,
1236 mut state: OverlordState,
1237 ) -> EventHandleResult<OverlordEvent, OverlordState> {
1238 state
1239 .character_state
1240 .character
1241 .instant_reward_gems_press_count = 0;
1242 EventHandleResult::ok(state)
1243 }
1244
1245 fn handle_show_bird(
1246 &mut self,
1247 mut state: OverlordState,
1248 ) -> EventHandleResult<OverlordEvent, OverlordState> {
1249 let bird_config = &self.game_config.get().ads_settings.bird_ad;
1250 let cooldown_until = ::time::utc_now()
1251 + chrono::TimeDelta::seconds(bird_config.post_show_cooldown_sec as i64);
1252 state.character_state.character.bird_cooldown_until = Some(cooldown_until);
1253 EventHandleResult::ok(state)
1254 }
1255
1256 fn handle_bird_shown(
1257 &mut self,
1258 mut state: OverlordState,
1259 ) -> EventHandleResult<OverlordEvent, OverlordState> {
1260 let bird_config = &self.game_config.get().ads_settings.bird_ad;
1261 let cooldown_until =
1262 ::time::utc_now() + chrono::TimeDelta::seconds(bird_config.cooldown_sec as i64);
1263 state.character_state.character.bird_cooldown_until = Some(cooldown_until);
1264 EventHandleResult::ok(state)
1265 }
1266}