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 pub(crate) fight_clock: crate::fight::FightClock,
42 pub(super) ended_fight_id: Option<uuid::Uuid>,
47 pub(super) compute_fields_duration: opentelemetry::metrics::Histogram<f64>,
48 pub(super) last_currency_source: Option<String>,
50}
51
52impl OverlordLogic {
53 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 OverlordEvent::CreateAbilityPreset { .. } => self.handle_noop(state),
527 OverlordEvent::EditAbilityPreset { .. } => self.handle_noop(state),
528
529 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 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 OverlordEvent::LevelUpClass { .. } => self.handle_noop(state),
545 OverlordEvent::Respec { .. } => self.handle_noop(state),
546
547 OverlordEvent::LinkGuestAccount { .. } => self.handle_noop(state),
549 OverlordEvent::SetUsername { .. } => self.handle_noop(state),
550 OverlordEvent::SetCharacterBlocked { .. } => self.handle_noop(state),
551
552 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 OverlordEvent::CurrencyIncrease { currencies, .. } => {
568 self.handle_currency_increase(currencies, state)
569 }
570 OverlordEvent::CurrencyDecrease { currencies, .. } => {
571 self.handle_currency_decrease(currencies, state)
572 }
573 OverlordEvent::BuySkins { .. } => self.handle_noop(state),
575 OverlordEvent::EquipAndUnequipSkins { .. } => self.handle_noop(state),
576
577 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 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 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 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 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 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 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 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 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 pub fn set_last_currency_source(&mut self, source: Option<CurrencySource>) {
773 self.last_currency_source = source.map(|s| format!("{s:?}"));
774 }
775
776 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 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 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 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 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 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 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 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 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 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}