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 combat_origin::CombatEventOrigin,
16 currency::{
17 CurrencyConsumer, CurrencySource, CurrencyUnit, check_can_decrease_currencies,
18 decrease_currencies, increase_currencies,
19 },
20 entity::EntityState,
21 fighting::ActiveFight,
22 mail::Mail,
23 offers::OfferTemplate,
24 quest::{QuestGroupType, QuestInstance},
25 stones::StoneKind,
26};
27
28pub use event_system::{
29 event::{EventPluginized, EventStruct},
30 plugin::{cron::CronMark, delayed::DelayedMark},
31 system::EventHandleResult,
32};
33
34#[derive(Debug)]
38struct FightRng {
39 seed: event_system::random::Seed,
40 draw: u64,
41}
42
43#[derive(Debug)]
44pub struct OverlordLogic {
45 pub(super) game_config: configs::SharedGameConfig,
46 pub(super) behaviors: Arc<BehaviorRegistry>,
47 pub(super) frontend: bool,
48 pub(super) start_fight_tick: u64,
49 pub(crate) fight_clock: crate::fight::FightClock,
53 pending_fight_seed: Option<event_system::random::Seed>,
62 dispatch_origin: CombatEventOrigin,
66 fight_rng: Option<FightRng>,
68 current_fight_draw: Option<u64>,
72 pub(super) ended_fight_id: Option<uuid::Uuid>,
77 pub(super) fight_breakdown_acc: Option<crate::fight::BreakdownAccumulator>,
86 pub(super) last_party_refresh_at: Option<chrono::DateTime<chrono::Utc>>,
92 pub(super) compute_fields_duration: opentelemetry::metrics::Histogram<f64>,
93 pub(super) last_currency_source: Option<String>,
95}
96
97impl OverlordLogic {
98 pub fn pre_event(&mut self, event: &OverlordEvent, current_tick: u64) {
107 self.fight_clock.set_now(current_tick);
108
109 match event {
110 OverlordEvent::PrepareFight { prepare_fight_type } => {
115 self.fight_rng = match prepare_fight_type {
116 crate::event::PrepareFightType::PVPFight { .. } => self
117 .pending_fight_seed
118 .take()
119 .map(|seed| FightRng { seed, draw: 0 }),
120 _ => None,
121 };
122 }
123 OverlordEvent::EndFight { .. } => self.fight_rng = None,
124 _ => {}
125 }
126
127 self.current_fight_draw = match &mut self.fight_rng {
128 Some(fight) if Self::is_fight_scoped(event) => {
129 let key = fight.seed.with_fight_draw(fight.draw);
130 fight.draw += 1;
131 Some(key)
132 }
133 _ => None,
134 };
135
136 self.last_currency_source = match event {
137 OverlordEvent::CurrencyIncrease {
138 currency_source, ..
139 } => Some(format!("{currency_source:?}")),
140 OverlordEvent::CurrencyDecrease {
141 currency_consumer, ..
142 } => Some(format!("{currency_consumer:?}")),
143 other => Some(other.to_string()),
144 };
145
146 self.dispatch_origin = event.carried_origin().unwrap_or(CombatEventOrigin::Core);
152 self.fight_clock.set_dispatch_origin(self.dispatch_origin);
153 }
154
155 pub fn dispatch_origin(&self) -> CombatEventOrigin {
166 self.dispatch_origin
167 }
168
169 pub fn post_event(&mut self, result: &mut EventHandleResult<OverlordEvent, OverlordState>) {
186 let origin = std::mem::replace(&mut self.dispatch_origin, CombatEventOrigin::Core);
187 self.fight_clock
188 .set_dispatch_origin(CombatEventOrigin::Core);
189 if origin.is_core() {
190 return;
191 }
192 for out in result.events_mut() {
193 out.event_mut().set_origin(origin);
194 }
195 }
196
197 #[allow(clippy::too_many_arguments)]
204 pub(super) fn record_fight_breakdown(
205 &mut self,
206 fight_instance_id: uuid::Uuid,
207 fight_id: essences::fighting::FightTemplateId,
208 actor: crate::fight::BreakdownActor,
209 source: essences::fight_breakdown::CombatSource,
210 damage: u64,
211 heal: u64,
212 crit: bool,
213 ) {
214 let acc = match &mut self.fight_breakdown_acc {
215 Some(acc) if acc.fight_instance_id() == fight_instance_id => acc,
216 _ => self
217 .fight_breakdown_acc
218 .insert(crate::fight::BreakdownAccumulator::new(
219 fight_instance_id,
220 fight_id,
221 )),
222 };
223 acc.record(actor, source, damage, heal, crit);
224 }
225
226 pub fn fight_breakdown(
234 &self,
235 is_win: bool,
236 duration_ticks: u64,
237 ) -> Option<essences::fight_breakdown::FightBreakdown> {
238 self.fight_breakdown_acc
239 .as_ref()
240 .map(|acc| acc.finish(is_win, duration_ticks))
241 }
242
243 pub fn arm_fight_seed(&mut self, seed: event_system::random::Seed) {
246 self.pending_fight_seed = Some(seed);
247 }
248
249 pub fn event_rng(&self, driver_rng: rand::rngs::StdRng) -> rand::rngs::StdRng {
255 use rand::SeedableRng as _;
256
257 match self.current_fight_draw {
258 Some(key) => rand::rngs::StdRng::seed_from_u64(key),
259 None => driver_rng,
260 }
261 }
262
263 pub fn is_fight_scoped(event: &OverlordEvent) -> bool {
276 matches!(
277 event,
278 OverlordEvent::PrepareFight { .. }
279 | OverlordEvent::StartFight { .. }
280 | OverlordEvent::FightProgress {}
281 | OverlordEvent::WaveCleared { .. }
282 | OverlordEvent::SpawnEntity { .. }
283 | OverlordEvent::StartMove { .. }
284 | OverlordEvent::EndMove { .. }
285 | OverlordEvent::MoveProgress { .. }
286 | OverlordEvent::StartCastAbility { .. }
287 | OverlordEvent::StartedCastAbility { .. }
288 | OverlordEvent::CastAbility { .. }
289 | OverlordEvent::StartCastProjectile { .. }
290 | OverlordEvent::StartedCastProjectile { .. }
291 | OverlordEvent::DerivedAbilityStrike { .. }
292 | OverlordEvent::EntityIncrAttributeDelayed { .. }
293 | OverlordEvent::CastProjectile { .. }
294 | OverlordEvent::Damage { .. }
295 | OverlordEvent::GlobalFlip { .. }
296 | OverlordEvent::StoneTriggerFired { .. }
297 | OverlordEvent::Heal { .. }
298 | OverlordEvent::CounterAttack { .. }
299 | OverlordEvent::Multicast { .. }
300 | OverlordEvent::Evasion { .. }
301 | OverlordEvent::PlayerDeath {}
302 | OverlordEvent::EntityDeath { .. }
303 | OverlordEvent::EntityIncrAttribute { .. }
304 | OverlordEvent::EntityAddAbilityCooldown { .. }
305 | OverlordEvent::EntityCancelCastWithCooldown { .. }
306 | OverlordEvent::EntityStun { .. }
307 | OverlordEvent::EntityApplyEffect { .. }
308 | OverlordEvent::CastEffect { .. }
309 | OverlordEvent::CastEffectFromEvent { .. }
310 | OverlordEvent::FightCustomEvent { .. }
311 | OverlordEvent::FightVisualEvent { .. }
312 )
313 }
314
315 pub fn effect_subscription_events(
319 &self,
320 event: &OverlordEvent,
321 state: &OverlordState,
322 ) -> Vec<EventPluginized<OverlordEvent, OverlordState>> {
323 if !Self::is_fight_scoped(event) {
329 return Vec::new();
330 }
331
332 let game_config = self.game_config.get();
333 let mut events: Vec<EventPluginized<OverlordEvent, OverlordState>> = Vec::new();
334 if let Some(fight) = &state.active_fight {
335 for entity in &fight.entities {
336 for effect_id in &entity.effect_ids {
337 let Some(effect) = game_config.effect(*effect_id) else {
342 tracing::warn!(
343 "Skipping unknown effect with id = {effect_id} on fight entity {}",
344 entity.id
345 );
346 continue;
347 };
348 if let Some(events_subscribe) = &effect.events_subscribe
349 && events_subscribe.contains(&event.to_string())
350 {
351 events.push(EventPluginized::now(OverlordEvent::CastEffectFromEvent {
352 entity_id: entity.id,
353 effect_id: effect.id,
354 caller_event: Box::new(event.clone()),
355 }));
356 }
357 }
358 }
359 }
360 events
361 }
362
363 pub fn apply_success_hooks(
376 &mut self,
377 state: &mut OverlordState,
378 events: &mut Vec<EventPluginized<OverlordEvent, OverlordState>>,
379 event: &OverlordEvent,
380 ) {
381 self.update_quests_progress(state, events, event);
382 self.try_give_new_offers(state, events, event);
383 self.apply_stone_triggers(state, events, event);
384 self.apply_law_reactions(state, events, event);
389 self.apply_artifact_aspects(state, events, event);
390 self.apply_pet_facets(state, events, event);
395 }
396
397 pub fn handle_event(
398 &mut self,
399 event: &OverlordEvent,
400 state: OverlordState,
401 rand_gen: rand::rngs::StdRng,
402 current_tick: u64,
403 ) -> EventHandleResult<OverlordEvent, OverlordState> {
404 self.pre_event(event, current_tick);
405 let rand_gen = self.event_rng(rand_gen);
406 let mut events = self.effect_subscription_events(event, &state);
407
408 let mut result = match &event {
409 OverlordEvent::OpenItemCase { batch_size } => {
411 self.handle_open_item_case(*batch_size, rand_gen, state)
412 }
413 OverlordEvent::AutoChestOpenItemCase { batch_size } => {
414 self.handle_auto_chest_open_item_case(*batch_size, rand_gen, state)
415 }
416 OverlordEvent::PlinkoBallDropped { item_id, path } => {
417 self.handle_plinko_ball_dropped(*item_id, path, state)
418 }
419 OverlordEvent::PlayerEquipItem { item_id } => {
420 self.handle_player_equip_item(*item_id, state)
421 }
422 OverlordEvent::SellItem { item_id } => self.handle_sell_item(*item_id, state),
423 OverlordEvent::ItemSold { .. } => self.handle_noop(state),
424 OverlordEvent::ItemsExpired { .. } => self.handle_noop(state),
425 OverlordEvent::PlayerNewItems { items } => self.handle_player_new_items(items, state),
426 OverlordEvent::UpgradeItemCase {} => self.handle_noop(state),
427 OverlordEvent::ItemCaseUpgraded {} => self.handle_noop(state),
428 OverlordEvent::SpeedupUpgradeItemCase {} => self.handle_noop(state),
429 OverlordEvent::SkipUpgradeItemCase {} => self.handle_noop(state),
430 OverlordEvent::ClaimUpgradeItemCase {} => self.handle_noop(state),
431
432 OverlordEvent::EnableAutoSell {} => self.handle_enable_auto_sell(state),
433 OverlordEvent::DisableAutoSell {} => self.handle_disable_auto_sell(state),
434
435 OverlordEvent::SetGearOverrideEnabled { item_type, enabled } => {
436 self.handle_set_gear_override_enabled(*item_type, *enabled, state)
437 }
438
439 OverlordEvent::EnableCaseUpgradePopUp {} => {
440 self.handle_enable_case_upgrade_pop_up(state)
441 }
442 OverlordEvent::DisableCaseUpgradePopUp {} => {
443 self.handle_disable_case_upgrade_pop_up(state)
444 }
445
446 OverlordEvent::OpenAbilityCase { .. } => self.handle_noop(state),
448 OverlordEvent::SetAbilityGachaWishlist { .. } => self.handle_noop(state),
449 OverlordEvent::UpgradeAbilitySlot { .. } => self.handle_noop(state),
450 OverlordEvent::AbilityCaseOpened { .. } => self.handle_noop(state),
451 OverlordEvent::NewAbilities { .. } => self.handle_noop(state),
452 OverlordEvent::UpgradeAbilityCase {} => self.handle_noop(state),
453
454 OverlordEvent::SocketAbilityStone { .. } => self.handle_noop(state),
458 OverlordEvent::UnsocketAbilityStone { .. } => self.handle_noop(state),
459 OverlordEvent::UpgradeAbilityStone { .. } => self.handle_noop(state),
460 OverlordEvent::QuickEquipAbilityStones { .. } => self.handle_noop(state),
461 OverlordEvent::UpgradeAllAbilityStones {} => self.handle_noop(state),
462 OverlordEvent::UpgradedAbilityStones { .. } => self.handle_noop(state),
463 OverlordEvent::NewAbilityStones { .. } => self.handle_noop(state),
464 OverlordEvent::FastEquipAbilities {} => self.handle_noop(state),
465 OverlordEvent::EquipAbility {
466 slot_id,
467 ability_id,
468 } => self.handle_equip_ability(*slot_id, *ability_id, current_tick, state),
469 OverlordEvent::UnequipAbility { slot_id } => {
470 self.handle_unequip_ability(*slot_id, state)
471 }
472 OverlordEvent::EquipAbilities { equipped_abilities } => {
473 self.handle_equip_abilities(equipped_abilities.clone(), current_tick, state)
474 }
475 OverlordEvent::UpgradeAbility { .. } => self.handle_noop(state),
476 OverlordEvent::UpgradeAllAbilities {} => self.handle_noop(state),
477 OverlordEvent::UpgradedAbilities { .. } => self.handle_noop(state),
478 OverlordEvent::UpgradedStones { .. } => self.handle_noop(state),
482
483 OverlordEvent::StartGame {} => self.handle_noop(state),
485 OverlordEvent::PrepareFight { prepare_fight_type } => {
486 self.handle_prepare_fight(prepare_fight_type.clone(), rand_gen, state)
487 }
488 OverlordEvent::StartFight { fight_id } => {
489 self.handle_start_fight(event.clone(), *fight_id, rand_gen, current_tick, state)
490 }
491 OverlordEvent::EndFight {
492 fight_id,
493 is_win,
494 pvp_state,
495 } => self.handle_end_fight(*fight_id, *is_win, pvp_state.as_deref(), rand_gen, state),
496 OverlordEvent::StageCleared {} => self.handle_noop(state),
497 OverlordEvent::RaidDungeon { .. } => self.handle_noop(state),
498
499 OverlordEvent::StartMove {
501 entity_id,
502 to,
503 duration_ticks,
504 } => self.handle_start_move(*entity_id, to.clone(), *duration_ticks, state),
505 OverlordEvent::EndMove { entity_id } => self.handle_end_move(*entity_id, state),
506 OverlordEvent::MoveProgress { entity_id, to } => {
507 self.handle_move_progress(*entity_id, to.clone(), state)
508 }
509
510 OverlordEvent::SpawnEntity {
512 id,
513 entity_template_id,
514 position,
515 entity_team,
516 has_big_hp_bar,
517 entity_attributes,
518 } => self.handle_spawn_entity(
519 *id,
520 *entity_template_id,
521 position.clone(),
522 entity_team.clone(),
523 *has_big_hp_bar,
524 entity_attributes.clone(),
525 current_tick,
526 state,
527 ),
528 OverlordEvent::FightProgress {} => self.handle_fight_progress(current_tick, state),
529 OverlordEvent::StartCastAbility {
530 by_entity_id,
531 ability_id,
532 ..
533 } => self.handle_start_cast_ability(
534 event.clone(),
535 *by_entity_id,
536 *ability_id,
537 rand_gen,
538 current_tick,
539 state,
540 ),
541 OverlordEvent::StartedCastAbility { .. } => self.handle_noop(state),
542 OverlordEvent::CastAbility {
543 by_entity_id,
544 to_entity_id,
545 ability_id,
546 ..
547 } => self.handle_cast_ability(
548 event.clone(),
549 *by_entity_id,
550 *to_entity_id,
551 *ability_id,
552 rand_gen,
553 state,
554 ),
555 OverlordEvent::StartCastProjectile {
556 by_entity_id,
557 to_entity_id,
558 projectile_id,
559 level,
560 delay: _,
561 origin: _,
564 source,
565 } => self.handle_start_cast_projectile(
566 event.clone(),
567 *by_entity_id,
568 *to_entity_id,
569 *projectile_id,
570 *level,
571 *source,
572 current_tick,
573 state,
574 ),
575 OverlordEvent::StartedCastProjectile { .. } => self.handle_noop(state),
576 OverlordEvent::DerivedAbilityStrike {
577 by_entity_id,
578 to_entity_id,
579 ability_id,
580 level,
581 payload_permille,
582 source_paid_mana_x100,
583 ..
584 } => self.handle_derived_ability_strike(
585 *by_entity_id,
586 *to_entity_id,
587 *ability_id,
588 *level,
589 *payload_permille,
590 *source_paid_mana_x100,
591 rand_gen,
592 state,
593 ),
594 OverlordEvent::EntityIncrAttributeDelayed {
597 entity_id,
598 attribute,
599 delta,
600 ..
601 } => self.handle_entity_incr_attribute(
602 *entity_id,
603 attribute,
604 *delta,
605 current_tick,
606 state,
607 ),
608 OverlordEvent::CastProjectile {
609 by_entity_id,
610 to_entity_id,
611 projectile_id,
612 level,
613 projectile_data,
614 origin: _,
615 source,
616 } => self.handle_cast_projectile(
617 event.clone(),
618 *by_entity_id,
619 *to_entity_id,
620 *projectile_id,
621 *level,
622 projectile_data,
623 *source,
624 rand_gen,
625 current_tick,
626 state,
627 ),
628 OverlordEvent::Damage {
629 by_entity_id,
630 entity_id,
631 damage,
632 damage_data,
633 origin: _,
636 source,
637 } => self.handle_damage(
638 *by_entity_id,
639 *entity_id,
640 *damage,
641 damage_data,
642 *source,
643 rand_gen,
644 state,
645 ),
646 OverlordEvent::GlobalFlip {
650 entity_id,
651 from_side,
652 to_side,
653 revision,
654 ..
655 } => {
656 let mut rng = rand_gen;
657 self.handle_global_flip(
658 *entity_id, *from_side, *to_side, *revision, &mut rng, state,
659 )
660 }
661 OverlordEvent::Heal {
662 by_entity_id,
663 entity_id,
664 heal,
665 source,
666 ..
667 } => self.handle_heal(*by_entity_id, *entity_id, *heal, *source, state),
668 OverlordEvent::CounterAttack { .. } => self.handle_noop(state),
669 OverlordEvent::WaveCleared {} => self.handle_noop(state),
670 OverlordEvent::Multicast { .. } => self.handle_noop(state),
671 OverlordEvent::StoneTriggerFired { .. } => self.handle_noop(state),
673 OverlordEvent::Evasion { .. } => self.handle_noop(state),
674 OverlordEvent::PlayerDeath {} => self.handle_player_death(state),
675 OverlordEvent::EntityDeath {
676 entity_id, reward, ..
677 } => self.handle_entity_death(*entity_id, reward.clone(), rand_gen, state),
678 OverlordEvent::EntityIncrAttribute {
679 entity_id,
680 attribute,
681 delta,
682 } => self.handle_entity_incr_attribute(
683 *entity_id,
684 attribute,
685 *delta,
686 current_tick,
687 state,
688 ),
689 OverlordEvent::EntityAddAbilityCooldown {
690 entity_id,
691 ability_id,
692 delta_ticks,
693 } => self.handle_entity_add_ability_cooldown(
694 *entity_id,
695 *ability_id,
696 *delta_ticks,
697 current_tick,
698 state,
699 ),
700 OverlordEvent::EntityCancelCastWithCooldown {
701 entity_id,
702 ability_id,
703 } => self.handle_entity_cancel_cast_with_cooldown(
704 *entity_id,
705 *ability_id,
706 current_tick,
707 state,
708 ),
709 OverlordEvent::EntityStun {
710 entity_id,
711 duration_ticks,
712 } => self.handle_entity_stun(*entity_id, *duration_ticks, current_tick, state),
713 OverlordEvent::EntityApplyEffect {
714 entity_id,
715 effect_id,
716 ..
717 } => self.handle_entity_apply_effect(*entity_id, *effect_id, current_tick, state),
718 OverlordEvent::CastEffect {
719 entity_id,
720 effect_id,
721 ..
722 } => {
723 self.handle_cast_effect(*entity_id, *effect_id, None, rand_gen, current_tick, state)
724 }
725 OverlordEvent::CastEffectFromEvent {
726 entity_id,
727 effect_id,
728 caller_event,
729 } => self.handle_cast_effect(
730 *entity_id,
731 *effect_id,
732 Some(caller_event.to_owned()),
733 rand_gen,
734 current_tick,
735 state,
736 ),
737 OverlordEvent::FightCustomEvent { .. } => self.handle_noop(state),
738 OverlordEvent::FightVisualEvent { .. } => self.handle_noop(state),
739 OverlordEvent::NewCharacterLevel { level } => {
740 self.handle_new_character_level(*level, state)
741 }
742
743 OverlordEvent::ClaimSuzerainReward {} => self.handle_noop(state),
745 OverlordEvent::ClaimVassalReward { .. } => self.handle_noop(state),
746 OverlordEvent::NewSuzerain { new_suzerain } => {
747 self.handle_new_suzerain(new_suzerain.as_deref().cloned(), state)
748 }
749 OverlordEvent::RemoveVassal { vassal_id } => {
750 self.handle_remove_vassal(*vassal_id, state)
751 }
752
753 OverlordEvent::ClaimQuest { quest_id } => {
755 self.handle_claim_quest(*quest_id, rand_gen, state)
756 }
757 OverlordEvent::NewQuests { quest_ids } => {
758 self.handle_new_quests(quest_ids.clone(), state)
759 }
760 OverlordEvent::UpdateActiveLoopTaskId { quest_id } => {
761 self.handle_update_active_loop_task_id(*quest_id, state)
762 }
763
764 OverlordEvent::ClaimQuestProgressionReward { quest_group_type } => {
765 self.handle_claim_quest_progression_reward(quest_group_type.to_owned(), state)
766 }
767
768 OverlordEvent::ResetRepeatingQuests { quest_ids } => {
769 self.handle_reset_repeating_quests(quest_ids.clone(), state)
770 }
771
772 OverlordEvent::GiveTask { .. } => self.handle_noop(state),
774 OverlordEvent::NewTask { new_task } => {
775 self.handle_new_task((**new_task).clone(), state)
776 }
777 OverlordEvent::AcceptTask { task_id, is_good } => {
778 self.handle_accept_task(*task_id, *is_good, state)
779 }
780 OverlordEvent::TaskAccepted {
781 task_id,
782 started_good,
783 started_at,
784 finish_at,
785 } => self.handle_task_accepted(*task_id, *started_good, *started_at, *finish_at, state),
786 OverlordEvent::HitHands { task_id } => self.handle_hit_hands(*task_id, state),
787 OverlordEvent::HandsHitted { task_id } => self.handle_hands_hitted(*task_id, state),
788 OverlordEvent::ClaimTaskReward { task_id } => {
789 self.handle_claim_task_reward(*task_id, state)
790 }
791 OverlordEvent::TaskFinished { task_id } => self.handle_finish_task(*task_id, state),
792
793 OverlordEvent::GiveResistTask { new_resist_task } => {
795 self.handle_give_resist_task((**new_resist_task).clone(), state)
796 }
797 OverlordEvent::AcceptResistTask {} => self.handle_noop(state),
798 OverlordEvent::ResistTaskAccepted { new_resist_task } => {
799 self.handle_resist_task_accepted((**new_resist_task).clone(), state)
800 }
801 OverlordEvent::CatchResistTask { task_id } => {
802 self.handle_catch_resist_task(*task_id, state)
803 }
804 OverlordEvent::ResistTaskCatched { task_id } => {
805 self.handle_resist_task_catched(*task_id, state)
806 }
807 OverlordEvent::ResistTaskFinished { task_id } => {
808 self.handle_resist_task_finished(*task_id, state)
809 }
810 OverlordEvent::ClaimResistTaskReward { task_id } => {
811 self.handle_claim_resist_task_reward(*task_id, state)
812 }
813
814 OverlordEvent::SendGift {
816 receiver_id,
817 config_gift_id,
818 } => self.handle_send_gift(*receiver_id, *config_gift_id, state),
819 OverlordEvent::NewGift { new_gift } => {
820 self.handle_new_gift((**new_gift).clone(), state)
821 }
822 OverlordEvent::AcceptGift { gift_id } => self.handle_accept_gift(*gift_id, state),
823
824 OverlordEvent::StartVassalPVPSync { .. } => self.handle_noop(state),
826 OverlordEvent::StartArenaPVPSync { .. } => self.handle_noop(state),
827 OverlordEvent::StartArenaRematchSync { .. } => self.handle_noop(state),
828 OverlordEvent::RefreshArenaMatchmaking {} => self.handle_noop(state),
829 OverlordEvent::BuyArenaTicket {} => self.handle_noop(state),
830 OverlordEvent::SetArenaWorldSide { side } => {
831 self.handle_set_arena_world_side(*side, state)
832 }
833
834 OverlordEvent::ClaimReferralLvlUpReward { level } => {
836 self.handle_claim_referral_lvlup_reward(*level, state)
837 }
838 OverlordEvent::ClaimReferralDailyReward {} => {
839 self.handle_claim_referral_daily_reward(state)
840 }
841 OverlordEvent::PatronQuestCompleted { quest_id } => {
842 self.handle_patron_quest_completed(*quest_id, state)
843 }
844 OverlordEvent::HiddenQuestCompleted { quest_id } => {
845 self.handle_hidden_quest_completed(*quest_id, rand_gen, state)
846 }
847 OverlordEvent::QuestCompleted { .. } => self.handle_noop(state),
848 OverlordEvent::ReferralDailyRewardStatusUpdate {
849 referral_daily_reward_status,
850 } => self
851 .handle_referral_daily_reward_status_update(*referral_daily_reward_status, state),
852
853 OverlordEvent::EnableAutoChest {} => self.handle_enable_auto_chest(state),
855 OverlordEvent::DisableAutoChest {} => self.handle_disable_auto_chest(state),
856
857 OverlordEvent::EnableAutoChestFilter { filter_id } => {
858 self.handle_enable_auto_chest_filter(*filter_id, state)
859 }
860 OverlordEvent::DisableAutoChestFilter {} => {
861 self.handle_disable_auto_chest_filter(state)
862 }
863
864 OverlordEvent::EnableAutoChestPowerCompare {} => {
865 self.handle_enable_auto_chest_power_compare(state)
866 }
867 OverlordEvent::DisableAutoChestPowerCompare {} => {
868 self.handle_disable_auto_chest_power_compare(state)
869 }
870
871 OverlordEvent::UpdateAutoChestBatchSize { batch_size } => {
872 self.handle_update_auto_chest_batch_size(*batch_size, state)
873 }
874
875 OverlordEvent::NewAutoChestFilter { filter } => {
876 self.handle_new_auto_chest_filter((**filter).clone(), state)
877 }
878
879 OverlordEvent::UpdateAutoChestFilter { updated_filter } => {
880 self.handle_update_auto_chest_filter((**updated_filter).clone(), state)
881 }
882
883 OverlordEvent::RemoveAutoChestFilter { filter_id } => {
884 self.handle_remove_auto_chest_filter(*filter_id, state)
885 }
886
887 OverlordEvent::SetCustomValue { key, value } => {
889 self.handle_set_custom_value(key.clone(), *value, state)
890 }
891 OverlordEvent::SetConnectionStore { key, value } => {
892 self.handle_set_connection_store(key.clone(), *value, state)
893 }
894
895 OverlordEvent::CreateAbilityPreset { .. } => self.handle_noop(state),
897 OverlordEvent::EditAbilityPreset { .. } => self.handle_noop(state),
898
899 OverlordEvent::ClaimAfkReward {} => self.handle_noop(state),
901 OverlordEvent::AfkRewardClaimed {} => self.handle_noop(state),
902 OverlordEvent::AfkRewardsGatingUnlocked {} => {
903 self.handle_afk_rewards_gating_unlocked(state)
904 }
905 OverlordEvent::ClaimAfkInstantRewardGems {} => self.handle_noop(state),
906
907 OverlordEvent::ClaimBundleStepGeneric { .. } => self.handle_noop(state),
909 OverlordEvent::AddBundleGroup { bundle_ids, source } => {
910 self.handle_add_bundle_group(bundle_ids, *source, state)
911 }
912
913 OverlordEvent::LevelUpClass { .. } => self.handle_noop(state),
915 OverlordEvent::Respec { .. } => self.handle_noop(state),
916
917 OverlordEvent::LinkGuestAccount { .. } => self.handle_noop(state),
919 OverlordEvent::SetUsername { .. } => self.handle_noop(state),
920 OverlordEvent::SetCharacterBlocked { .. } => self.handle_noop(state),
921
922 OverlordEvent::SetMaxHp {
924 entity_id,
925 new_max_hp,
926 new_hp,
927 } => self.handle_set_max_hp(*entity_id, *new_max_hp, *new_hp, state),
928
929 OverlordEvent::RunCheat { cheat } => {
930 self.handle_run_cheat(cheat, current_tick, rand_gen, state)
931 }
932
933 OverlordEvent::Error { .. } => self.handle_noop(state),
934 OverlordEvent::CustomEvent { .. } => self.handle_noop(state),
935
936 OverlordEvent::CurrencyIncrease { currencies, .. } => {
938 self.handle_currency_increase(currencies, state)
939 }
940 OverlordEvent::CurrencyDecrease { currencies, .. } => {
941 self.handle_currency_decrease(currencies, state)
942 }
943 OverlordEvent::BuySkins { .. } => self.handle_noop(state),
945 OverlordEvent::EquipAndUnequipSkins { .. } => self.handle_noop(state),
946
947 OverlordEvent::ClaimMail { .. } => self.handle_noop(state),
949 OverlordEvent::ClaimAllMails {} => self.handle_noop(state),
950 OverlordEvent::ClaimAllQuests { .. } => self.handle_noop(state),
951 OverlordEvent::MakeRead { .. } => self.handle_noop(state),
952 OverlordEvent::MakeAllRead {} => self.handle_noop(state),
953 OverlordEvent::DeleteMail { .. } => self.handle_noop(state),
954 OverlordEvent::DeleteAllMails {} => self.handle_noop(state),
955 OverlordEvent::NewMail { new_mail } => {
956 self.handle_new_mail((**new_mail).clone(), state)
957 }
958 OverlordEvent::NewOffer { .. } => self.handle_noop(state),
960 OverlordEvent::BuyOffer { .. } => self.handle_noop(state),
961 OverlordEvent::ResetOffers { new_offers } => {
962 self.handle_reset_offers(new_offers, state)
963 }
964 OverlordEvent::OfferPurchaseCompleted { .. } => self.handle_noop(state),
965 OverlordEvent::OfferPurchaseFailed { .. } => self.handle_noop(state),
966 OverlordEvent::PurchasesBanned {} => self.handle_purchases_banned(state),
967
968 OverlordEvent::EquipPet { slot_id, pet_id } => {
970 self.handle_equip_pet(*slot_id, *pet_id, current_tick, state)
971 }
972 OverlordEvent::UnequipPet { slot_id } => {
973 self.handle_unequip_pet(*slot_id, current_tick, state)
974 }
975 OverlordEvent::FastEquipPets {} => self.handle_noop(state),
976 OverlordEvent::EquipPets { equipped_pets } => {
977 self.handle_equip_pets(equipped_pets.clone(), current_tick, state)
978 }
979 OverlordEvent::UpgradePet { .. } => self.handle_noop(state),
980 OverlordEvent::UpgradeAllPets {} => self.handle_noop(state),
981 OverlordEvent::UpgradedPets { .. } => self.handle_noop(state),
982 OverlordEvent::UpgradePetSlot { .. } => self.handle_noop(state),
983 OverlordEvent::SetPetFacetLaw {
984 role,
985 law_template_id,
986 } => self.handle_set_pet_facet_law(*role, *law_template_id, state),
987
988 OverlordEvent::OpenPetCase { .. } => self.handle_noop(state),
990 OverlordEvent::SetPetGachaWishlist { .. } => self.handle_noop(state),
991 OverlordEvent::PetCaseOpened { .. } => self.handle_noop(state),
992 OverlordEvent::NewPets { .. } => self.handle_noop(state),
993 OverlordEvent::UpgradePetCase {} => self.handle_noop(state),
994
995 OverlordEvent::TutorialShown { .. } => self.handle_noop(state),
997 OverlordEvent::TutorialStepCompleted { step_number } => {
998 self.handle_tutorial_step_completed(*step_number, state)
999 }
1000 OverlordEvent::ClientLifecycle { .. } => self.handle_noop(state),
1001 OverlordEvent::SetFightContentLoading { fight_id, loading } => {
1002 self.handle_set_fight_content_loading(*fight_id, *loading, state)
1003 }
1004
1005 OverlordEvent::AddCharacterToParty { .. } => self.handle_noop(state),
1007 OverlordEvent::RemoveCharacterFromParty {} => self.handle_noop(state),
1008 OverlordEvent::RefreshPartyPlayers {} => self.handle_noop(state),
1009 OverlordEvent::RefreshPartyMemberState {} => self.handle_noop(state),
1010
1011 OverlordEvent::StartTalentResearch { .. } => self.handle_noop(state),
1013 OverlordEvent::TalentResearchStarted { .. } => self.handle_noop(state),
1014 OverlordEvent::SpeedupTalentResearch {} => self.handle_noop(state),
1015 OverlordEvent::SkipTalentResearch {} => self.handle_noop(state),
1016 OverlordEvent::ClaimTalentResearch {} => self.handle_claim_talent_research(state),
1017
1018 OverlordEvent::StatueRoll { .. } => self.handle_noop(state),
1020 OverlordEvent::StatueActivateSet { .. } => self.handle_noop(state),
1021 OverlordEvent::StatueAddSet {} => self.handle_noop(state),
1022 OverlordEvent::StatueRenameSet { .. } => self.handle_noop(state),
1023 OverlordEvent::StatueLockSlot { .. } => self.handle_noop(state),
1024 OverlordEvent::StatueRollNewSlot { .. } => self.handle_noop(state),
1025 OverlordEvent::UpgradeCore { .. } => self.handle_noop(state),
1028 OverlordEvent::ResetCores {} => self.handle_noop(state),
1029 OverlordEvent::UpgradeLaw { .. } => self.handle_noop(state),
1030 OverlordEvent::SlotLaw { .. } => self.handle_noop(state),
1031 OverlordEvent::UnslotLaw { .. } => self.handle_noop(state),
1032 OverlordEvent::CreateLawBridge { .. } => self.handle_noop(state),
1033 OverlordEvent::RemoveLawBridge { .. } => self.handle_noop(state),
1034 OverlordEvent::NewLawCopies { .. } => self.handle_noop(state),
1035 OverlordEvent::ClaimProgressPassFreeReward { .. } => self.handle_noop(state),
1037 OverlordEvent::ClaimProgressPassPaidReward { .. } => self.handle_noop(state),
1038 OverlordEvent::ClaimAllProgressPassRewards { .. } => self.handle_noop(state),
1039 OverlordEvent::ClaimRatingRewards { .. } => self.handle_noop(state),
1040 OverlordEvent::SetRatingRewardAvailability { availability } => {
1041 self.handle_set_rating_reward_availability(availability.clone(), state)
1042 }
1043 OverlordEvent::UserRating { .. } => self.handle_noop(state),
1044 OverlordEvent::ShowRateUs {} => self.handle_noop(state),
1045 OverlordEvent::RateUsShown {} => self.handle_rate_us_shown(state),
1046 OverlordEvent::RateUs { .. } => self.handle_noop(state),
1047 OverlordEvent::WatchAd { .. } => self.handle_noop(state),
1048 OverlordEvent::ShowBird { .. } => self.handle_show_bird(state),
1049 OverlordEvent::BirdShown {} => self.handle_bird_shown(state),
1050 OverlordEvent::ResetAdUsage { placements } => {
1051 self.handle_reset_ad_usage(placements, state)
1052 }
1053 OverlordEvent::ResetInstantRewardGemsPressCount {} => {
1054 self.handle_reset_instant_reward_gems_press_count(state)
1055 }
1056 OverlordEvent::InsertStone {
1058 template_id,
1059 item_type,
1060 socket,
1061 } => self.handle_insert_stone(*template_id, *item_type, *socket, state),
1062 OverlordEvent::RemoveStone { template_id } => {
1063 self.handle_remove_stone(*template_id, state)
1064 }
1065 OverlordEvent::UpgradeStone { template_id } => {
1066 self.handle_upgrade_stone(*template_id, state)
1067 }
1068 OverlordEvent::QuickEquipStones { item_type } => {
1074 self.handle_quick_equip_stones(*item_type, state)
1075 }
1076 OverlordEvent::UpgradeAllTriggerStones {} => {
1077 self.handle_upgrade_all_stones(StoneKind::Trigger, state)
1078 }
1079 OverlordEvent::UpgradeAllEffectStones {} => {
1080 self.handle_upgrade_all_stones(StoneKind::Effect, state)
1081 }
1082 OverlordEvent::PlayerNewStones {
1083 trigger_stones,
1084 effect_stones,
1085 } => self.handle_player_new_stones(trigger_stones, effect_stones, state),
1086
1087 OverlordEvent::EquipArtifact { template_id } => {
1089 self.handle_equip_artifact(*template_id, state)
1090 }
1091 OverlordEvent::InsertArtifactStone {
1092 template_id,
1093 socket,
1094 } => self.handle_insert_artifact_stone(*template_id, *socket, state),
1095 OverlordEvent::RemoveArtifactStone { template_id } => {
1096 self.handle_remove_artifact_stone(*template_id, state)
1097 }
1098 OverlordEvent::UpgradeArtifactStone { template_id } => {
1099 self.handle_upgrade_artifact_stone(*template_id, state)
1100 }
1101 OverlordEvent::UpgradeArtifact { template_id } => {
1102 self.handle_upgrade_artifact(*template_id, state)
1103 }
1104 OverlordEvent::SetArtifactStoneLawTarget {
1105 template_id,
1106 law_template_id,
1107 } => self.handle_set_artifact_stone_law_target(*template_id, *law_template_id, state),
1108 OverlordEvent::PlayerNewArtifacts {
1109 artifacts,
1110 artifact_stones,
1111 } => self.handle_player_new_artifacts(artifacts, artifact_stones, state),
1112 OverlordEvent::FireArtifactEffect {
1113 owner_id,
1114 effect_template_id,
1115 effect_level,
1116 share_permyriad,
1117 artifact_stone_id,
1118 } => self.handle_fire_artifact_effect(
1119 *owner_id,
1120 *effect_template_id,
1121 *effect_level,
1122 *share_permyriad,
1123 *artifact_stone_id,
1124 state,
1125 ),
1126 };
1127
1128 if result.success() {
1129 let (state, events) = result.state_and_events_mut();
1130 self.apply_success_hooks(state, events, event);
1131 }
1132
1133 result.events_mut().append(&mut events);
1134 self.post_event(&mut result);
1135 result
1136 }
1137
1138 pub fn fight_clock_now(&self) -> u64 {
1142 self.fight_clock.now()
1143 }
1144
1145 pub fn collect_due_scheduled(&mut self, current_tick: u64) -> Vec<OverlordEvent> {
1146 self.fight_clock.collect_due(current_tick)
1147 }
1148
1149 pub fn compute_fields(
1150 &self,
1151 state: &mut OverlordState,
1152 prev_state: &OverlordState,
1153 ) -> Vec<OverlordEvent> {
1154 let _span = tracing::info_span!("compute_fields").entered();
1155 let start = std::time::Instant::now();
1156 let game_config = self.game_config.get();
1157 let mut events = Vec::new();
1158 self.log_currency_change_metrics(state, prev_state);
1159
1160 if state.character_state.character.character_experience
1161 != prev_state.character_state.character.character_experience
1162 {
1163 events.append(&mut self.compute_character_level(state));
1164 }
1165
1166 if state.character_state != prev_state.character_state {
1167 match attributes::calculate_player_entity_stats_with_zeroes(
1168 &EntityState::Character(&state.character_state),
1169 &game_config,
1170 ) {
1171 Ok(attributes) => {
1172 state.character_state.player_attributes = attributes.attributes.clone();
1173 state.character_state.player_attributes.remove_zeroes();
1174
1175 if state.pvp_state.is_none()
1182 && let Some(fight) = &mut state.active_fight
1183 && let Some(entity) = fight
1184 .entities
1185 .iter_mut()
1186 .find(|ent| ent.id == fight.player_id)
1187 {
1188 let law_snapshot = entity
1203 .flip_state
1204 .map(|flip| flip.active_side)
1205 .map(|active_side| {
1206 crate::entities::law_attribute_snapshot(
1207 entity,
1208 active_side,
1209 &game_config,
1210 )
1211 })
1212 .unwrap_or_default();
1213
1214 let hp_before = entity.hp;
1223
1224 if !law_snapshot.is_empty() {
1225 crate::entities::apply_law_attribute_change(
1226 entity,
1227 &law_snapshot,
1228 &Default::default(),
1229 &game_config,
1230 );
1231 }
1232
1233 entity.max_hp = attributes.max_hp;
1234 entity
1235 .attributes
1236 .0
1237 .extend(attributes.attributes.0.iter().map(|(k, v)| (k.clone(), *v)));
1238 entity.attributes.remove_zeroes();
1239
1240 if !law_snapshot.is_empty() {
1241 crate::entities::apply_law_attribute_change(
1242 entity,
1243 &Default::default(),
1244 &law_snapshot,
1245 &game_config,
1246 );
1247 entity.hp = hp_before.min(entity.max_hp);
1251 }
1252 }
1253 }
1254 Err(err) => {
1255 tracing::error!("Failed to fill player entity attributes: {:?}", err);
1256 }
1257 }
1258
1259 match crate::behaviors::power::character_power(
1260 &crate::behaviors::power::CharacterPowerCtx {
1261 character: &state.character_state,
1262 config: &game_config,
1263 lookups: self.behaviors.lookups(),
1264 },
1265 ) {
1266 Ok(power) => state.character_state.character.power = power,
1267 Err(err) => tracing::error!("Couldn't calculate player power: {err:?}"),
1268 }
1269 }
1270
1271 self.compute_fields_duration
1272 .record(start.elapsed().as_secs_f64(), &[]);
1273
1274 events
1275 }
1276}
1277
1278impl OverlordLogic {
1279 pub fn new(
1280 game_config: configs::SharedGameConfig,
1281 behaviors: Arc<BehaviorRegistry>,
1282 frontend: bool,
1283 ) -> Self {
1284 let meter = opentelemetry::global::meter("compute_fields");
1285 Self {
1286 game_config,
1287 behaviors,
1288 frontend,
1289 start_fight_tick: 0,
1290 fight_breakdown_acc: None,
1291 fight_clock: crate::fight::FightClock::default(),
1292 pending_fight_seed: None,
1293 dispatch_origin: CombatEventOrigin::Core,
1294 fight_rng: None,
1295 current_fight_draw: None,
1296 ended_fight_id: None,
1297 last_party_refresh_at: None,
1298 last_currency_source: None,
1299 compute_fields_duration: meter
1300 .f64_histogram("compute_fields_duration_seconds")
1301 .with_boundaries(vec![
1302 0.0001, 0.0005, 0.001, 0.002, 0.005, 0.01, 0.02, 0.05, 0.1,
1303 ])
1304 .build(),
1305 }
1306 }
1307
1308 pub fn set_last_currency_source(&mut self, source: Option<CurrencySource>) {
1313 self.last_currency_source = source.map(|s| format!("{s:?}"));
1314 }
1315
1316 pub fn set_game_config(&mut self, game_config: configs::SharedGameConfig) {
1321 self.game_config = game_config;
1322 }
1323
1324 pub fn handle_set_custom_value(
1325 &self,
1326 key: String,
1327 value: i64,
1328 mut state: OverlordState,
1329 ) -> EventHandleResult<OverlordEvent, OverlordState> {
1330 state
1331 .character_state
1332 .character
1333 .custom_values
1334 .insert(&key, value);
1335 EventHandleResult::ok(state)
1336 }
1337
1338 fn handle_set_connection_store(
1339 &self,
1340 key: String,
1341 value: i64,
1342 mut state: OverlordState,
1343 ) -> EventHandleResult<OverlordEvent, OverlordState> {
1344 state.connection_store.insert(key, value);
1345 EventHandleResult::ok(state)
1346 }
1347
1348 pub fn handle_new_character_level(
1349 &self,
1350 level: i64,
1351 mut state: OverlordState,
1352 ) -> EventHandleResult<OverlordEvent, OverlordState> {
1353 state.character_state.character.character_level = level;
1354
1355 EventHandleResult::ok(state)
1356 }
1357
1358 fn handle_purchases_banned(
1359 &self,
1360 mut state: OverlordState,
1361 ) -> EventHandleResult<OverlordEvent, OverlordState> {
1362 state.character_state.character.purchases_banned = true;
1363 EventHandleResult::ok(state)
1364 }
1365
1366 fn handle_new_mail(
1367 &self,
1368 new_mail: Mail,
1369 mut state: OverlordState,
1370 ) -> EventHandleResult<OverlordEvent, OverlordState> {
1371 if !state.incoming_mails.contains(&new_mail) {
1372 state.incoming_mails.push(new_mail);
1373 }
1374
1375 EventHandleResult::ok(state)
1376 }
1377
1378 pub fn handle_noop(
1379 &self,
1380 state: OverlordState,
1381 ) -> EventHandleResult<OverlordEvent, OverlordState> {
1382 EventHandleResult::ok(state)
1383 }
1384
1385 pub fn handle_set_fight_content_loading(
1386 &self,
1387 fight_id: uuid::Uuid,
1388 loading: bool,
1389 mut state: OverlordState,
1390 ) -> EventHandleResult<OverlordEvent, OverlordState> {
1391 let Some(fight) = state.active_fight.as_mut() else {
1392 tracing::warn!(
1393 %fight_id,
1394 loading,
1395 "Ignoring fight content loading state without an active fight"
1396 );
1397 return if loading {
1398 EventHandleResult::fail(state)
1399 } else {
1400 EventHandleResult::ok(state)
1401 };
1402 };
1403
1404 if fight.id != fight_id {
1405 tracing::warn!(
1406 %fight_id,
1407 active_fight_id = %fight.id,
1408 loading,
1409 "Ignoring stale fight content loading state"
1410 );
1411 return if loading {
1412 EventHandleResult::fail(state)
1413 } else {
1414 EventHandleResult::ok(state)
1415 };
1416 }
1417
1418 fight.paused = loading;
1419 tracing::info!(
1420 %fight_id,
1421 loading,
1422 "Updated fight content loading state"
1423 );
1424
1425 EventHandleResult::ok(state)
1426 }
1427
1428 pub fn handle_tutorial_step_completed(
1429 &self,
1430 step_number: i16,
1431 mut state: OverlordState,
1432 ) -> EventHandleResult<OverlordEvent, OverlordState> {
1433 if !state
1434 .character_state
1435 .character
1436 .completed_tutorials
1437 .contains(&step_number)
1438 {
1439 state
1440 .character_state
1441 .character
1442 .completed_tutorials
1443 .push(step_number);
1444 } else {
1445 tracing::error!("Provided step {step_number} is already in state");
1446 }
1447 EventHandleResult::ok(state)
1448 }
1449}
1450
1451impl OverlordLogic {
1452 fn log_currency_change_metrics(&self, state: &OverlordState, prev_state: &OverlordState) {
1453 let character_id = state.character_state.character.id;
1454 let chapter_level = state.character_state.character.current_chapter_level;
1457 let current_currencies = &state.character_state.currencies;
1458 let prev_currencies = &prev_state.character_state.currencies;
1459 let source = self.last_currency_source.as_deref().unwrap_or("unknown");
1460
1461 let run_id = analytics::run_id::run_id();
1467 let _run_span = (!run_id.is_empty())
1468 .then(|| tracing::info_span!(target: METRICS_TARGET, "sim_run", run_id = %run_id));
1469 let _run_guard = _run_span.as_ref().map(|s| s.enter());
1470
1471 for current_currency in current_currencies {
1472 let amount_before = prev_currencies
1473 .iter()
1474 .find(|unit| unit.currency_id == current_currency.currency_id)
1475 .map_or(0, |unit| unit.amount);
1476 let delta = current_currency.amount - amount_before;
1477 let amount_after = current_currency.amount;
1478 if delta > 0 {
1479 tracing::info!(
1480 target: METRICS_TARGET,
1481 character_id = %character_id,
1482 event_type = "increase_currency",
1483 currency_id = %current_currency.currency_id,
1484 amount = delta,
1485 amount_before,
1486 amount_after,
1487 chapter_level,
1488 source = %source,
1489 "Add currency",
1490 );
1491 } else if delta < 0 {
1492 tracing::info!(
1493 target: METRICS_TARGET,
1494 character_id = %character_id,
1495 event_type = "decrease_currency",
1496 currency_id = %current_currency.currency_id,
1497 amount = -delta,
1498 amount_before,
1499 amount_after,
1500 chapter_level,
1501 source = %source,
1502 "Decrease currency",
1503 );
1504 }
1505 }
1506
1507 for prev_currency in prev_currencies {
1508 if current_currencies
1509 .iter()
1510 .any(|unit| unit.currency_id == prev_currency.currency_id)
1511 {
1512 continue;
1513 }
1514
1515 let delta = -prev_currency.amount;
1519 if delta > 0 {
1520 tracing::info!(
1521 target: METRICS_TARGET,
1522 character_id = %character_id,
1523 event_type = "increase_currency",
1524 currency_id = %prev_currency.currency_id,
1525 amount = delta,
1526 amount_before = prev_currency.amount,
1527 amount_after = prev_currency.amount + delta,
1528 chapter_level,
1529 source = %source,
1530 "Add currency",
1531 );
1532 } else if delta < 0 {
1533 tracing::info!(
1534 target: METRICS_TARGET,
1535 character_id = %character_id,
1536 event_type = "decrease_currency",
1537 currency_id = %prev_currency.currency_id,
1538 amount = -delta,
1539 amount_before = prev_currency.amount,
1540 amount_after = 0_i64,
1541 chapter_level,
1542 source = %source,
1543 "Decrease currency",
1544 );
1545 }
1546 }
1547 }
1548
1549 pub fn compute_character_level(&self, state: &mut OverlordState) -> Vec<OverlordEvent> {
1550 let game_config = self.game_config.get();
1551 let stored_level = state.character_state.character.character_level;
1552 let experience = state.character_state.character.character_experience;
1553
1554 let new_level = game_config
1559 .level_for_experience(experience)
1560 .max(stored_level);
1561
1562 if new_level != stored_level {
1563 vec![OverlordEvent::NewCharacterLevel { level: new_level }]
1564 } else {
1565 Vec::new()
1566 }
1567 }
1568
1569 fn try_give_new_offers(
1570 &self,
1571 state: &mut OverlordState,
1572 events: &mut Vec<EventPluginized<OverlordEvent, OverlordState>>,
1573 trigger_event: &OverlordEvent,
1574 ) {
1575 for offer in &self.game_config.get().offers_templates {
1576 if !offer.enabled {
1577 continue;
1578 }
1579
1580 if offer.limit_of_buys.is_some_and(|limit| {
1581 state
1582 .offers_info
1583 .offer_buy_counts
1584 .get(&offer.id)
1585 .copied()
1586 .unwrap_or(0)
1587 >= limit as u32
1588 }) {
1589 continue;
1590 }
1591
1592 if state
1593 .offers_info
1594 .active_offers
1595 .iter()
1596 .any(|x| x.template_id == offer.id)
1597 {
1598 continue;
1599 }
1600
1601 if !offer.events_subscribe.contains(&trigger_event.to_string()) {
1602 continue;
1603 }
1604
1605 let should_give =
1606 match self.should_give_new_offer(&state.character_state, offer, trigger_event) {
1607 Ok(progress) => progress,
1608 Err(e) => {
1609 tracing::error!(
1610 "Failed determining for offer id: {}\n Error: {e:?}",
1611 offer.id
1612 );
1613 continue;
1614 }
1615 };
1616
1617 if should_give {
1618 events.push(EventPluginized::now(OverlordEvent::NewOffer {
1619 offer_template_id: offer.id,
1620 }));
1621 }
1622 }
1623 }
1624
1625 fn should_give_new_offer(
1626 &self,
1627 character_state: &CharacterState,
1628 offer: &OfferTemplate,
1629 trigger_event: &OverlordEvent,
1630 ) -> anyhow::Result<bool> {
1631 crate::behaviors::quests::triggers::always_trigger(
1637 &crate::behaviors::quests::triggers::TriggerCtx {
1638 trigger_event,
1639 character: character_state,
1640 offer,
1641 config: &self.game_config.get(),
1642 },
1643 )
1644 }
1645
1646 fn update_quests_progress(
1647 &self,
1648 state: &mut OverlordState,
1649 events: &mut Vec<EventPluginized<OverlordEvent, OverlordState>>,
1650 trigger_event: &OverlordEvent,
1651 ) {
1652 let game_config = self.game_config.get();
1653 let mut progress_pass_tier_completed = false;
1654 let all_active_quests = state.quest_groups.get_not_claimed_quests_mut();
1655 let active_fight = &state.active_fight;
1656
1657 for active_quest in all_active_quests {
1658 let Some(quest_template) = game_config.quest(active_quest.id) else {
1659 continue;
1660 };
1661
1662 if state.patron.is_none()
1663 && (quest_template.quest_group_type == QuestGroupType::PatronLifetime
1664 || quest_template.quest_group_type == QuestGroupType::PatronDaily)
1665 {
1666 continue;
1667 }
1668
1669 if !quest_template
1670 .events_subscribe
1671 .contains(&trigger_event.to_string())
1672 {
1673 continue;
1674 }
1675
1676 if quest_template.quest_group_type == QuestGroupType::LoopTask
1677 && !quest_template.progress_if_inactive
1678 && let Some(active_loop_task_id) =
1679 state.character_state.character.active_loop_task_id
1680 && active_loop_task_id != quest_template.id
1681 {
1682 continue;
1683 }
1684
1685 let was_completed = active_quest.is_completed(quest_template.progress_target);
1686
1687 if !active_quest.is_completed(quest_template.progress_target) {
1688 let progress = match self.get_quest_progress(
1689 &state.character_state,
1690 active_fight,
1691 active_quest,
1692 quest_template.progress_behavior.as_deref(),
1693 trigger_event,
1694 &game_config,
1695 ) {
1696 Ok(progress) => progress,
1697 Err(e) => {
1698 tracing::error!(
1699 "Failed updating quest progress for quest id: {}\n Error: {e:?}",
1700 active_quest.id
1701 );
1702 continue;
1703 }
1704 };
1705 active_quest.current = progress;
1706 }
1707
1708 if !was_completed {
1709 if (quest_template.quest_group_type == QuestGroupType::PatronLifetime
1710 || quest_template.quest_group_type == QuestGroupType::PatronDaily)
1711 && active_quest.is_completed(quest_template.progress_target)
1712 {
1713 events.push(EventPluginized::now(OverlordEvent::PatronQuestCompleted {
1714 quest_id: active_quest.id,
1715 }));
1716 }
1717
1718 if (quest_template.quest_group_type != QuestGroupType::Hidden)
1719 && active_quest.is_completed(quest_template.progress_target)
1720 {
1721 events.push(EventPluginized::now(OverlordEvent::QuestCompleted {
1722 quest_id: active_quest.id,
1723 }));
1724 }
1725
1726 if quest_template.quest_group_type == QuestGroupType::ProgressPass
1727 && active_quest.is_completed(quest_template.progress_target)
1728 {
1729 progress_pass_tier_completed = true;
1730 }
1731 }
1732
1733 if (quest_template.quest_group_type == QuestGroupType::Hidden)
1734 && active_quest.is_completed(quest_template.progress_target)
1735 {
1736 events.push(EventPluginized::now(OverlordEvent::HiddenQuestCompleted {
1737 quest_id: active_quest.id,
1738 }));
1739 }
1740 }
1741
1742 if progress_pass_tier_completed {
1743 for tier_state in &mut state.progress_pass.tiers {
1744 let Some(tier_template) = game_config
1745 .progress_pass
1746 .tiers
1747 .iter()
1748 .find(|t| t.tier == tier_state.tier)
1749 else {
1750 continue;
1751 };
1752 tier_state.is_unlocked = essences::progress_pass::is_progress_pass_tier_unlocked(
1753 tier_template,
1754 &state.quest_groups.progress_pass,
1755 &game_config.quests,
1756 );
1757 }
1758 }
1759 }
1760
1761 pub fn get_quest_progress(
1762 &self,
1763 character_state: &CharacterState,
1764 active_fight: &Option<ActiveFight>,
1765 quest: &QuestInstance,
1766 progress_script_native: Option<&str>,
1767 trigger_event: &OverlordEvent,
1768 game_config: &GameConfig,
1769 ) -> anyhow::Result<i64> {
1770 let Some(name) = progress_script_native else {
1775 return Ok(quest.current);
1776 };
1777 let Some(f) = self.behaviors.conditional_progress_fn(name) else {
1778 return Ok(quest.current);
1779 };
1780 f(
1781 &crate::behaviors::quests::progress::ConditionalProgressCtx {
1782 event: trigger_event,
1783 character_state,
1784 active_fight,
1785 quest,
1786 config: game_config,
1787 lookups: self.behaviors.lookups(),
1788 },
1789 )
1790 }
1791
1792 pub fn handle_currency_increase(
1793 &self,
1794 currencies: &[CurrencyUnit],
1795 mut state: OverlordState,
1796 ) -> EventHandleResult<OverlordEvent, OverlordState> {
1797 let banked = self.drop_pre_gate_pet_currency(currencies, &state);
1798 increase_currencies(&mut state.character_state.currencies, &banked);
1799 EventHandleResult::ok(state)
1800 }
1801
1802 fn drop_pre_gate_pet_currency(
1816 &self,
1817 currencies: &[CurrencyUnit],
1818 state: &OverlordState,
1819 ) -> Vec<CurrencyUnit> {
1820 let game_config = self.game_config.get();
1821 let chapter = state.character_state.character.current_chapter_level;
1822 if game_config.pet_slots_for_chapter_level(chapter).is_some() {
1823 return currencies.to_vec();
1824 }
1825 let pet_currency = game_config.game_settings.pet_gacha.currency_id;
1826 currencies
1827 .iter()
1828 .filter(|unit| {
1829 if unit.currency_id == pet_currency {
1830 tracing::debug!(
1831 amount = unit.amount,
1832 chapter,
1833 "dropped pre-gate pet currency"
1834 );
1835 return false;
1836 }
1837 true
1838 })
1839 .cloned()
1840 .collect()
1841 }
1842
1843 pub fn handle_currency_decrease(
1844 &self,
1845 currencies: &[CurrencyUnit],
1846 mut state: OverlordState,
1847 ) -> EventHandleResult<OverlordEvent, OverlordState> {
1848 let non_zero: Vec<_> = currencies
1849 .iter()
1850 .filter(|c| c.amount > 0)
1851 .cloned()
1852 .collect();
1853 if let Err(e) = decrease_currencies(&mut state.character_state.currencies, &non_zero) {
1854 tracing::error!("CurrencyDecrease failed: {e}");
1855 return EventHandleResult::fail(state);
1856 }
1857 EventHandleResult::ok(state)
1858 }
1859
1860 pub fn currency_increase(
1863 currencies: &[CurrencyUnit],
1864 currency_source: CurrencySource,
1865 ) -> EventPluginized<OverlordEvent, OverlordState> {
1866 EventPluginized::now(OverlordEvent::CurrencyIncrease {
1867 currencies: currencies.to_owned(),
1868 currency_source,
1869 })
1870 }
1871
1872 pub fn currency_decrease(
1876 state: &OverlordState,
1877 currencies: &[CurrencyUnit],
1878 currency_consumer: CurrencyConsumer,
1879 ) -> Option<EventPluginized<OverlordEvent, OverlordState>> {
1880 if !check_can_decrease_currencies(&state.character_state.currencies, currencies) {
1881 tracing::error!(
1882 "currency_decrease({currency_consumer:?}): not enough currency, \
1883 required={currencies:?}, available={:?}",
1884 state.character_state.currencies
1885 );
1886 return None;
1887 }
1888 Some(EventPluginized::now(OverlordEvent::CurrencyDecrease {
1889 currencies: currencies.to_owned(),
1890 currency_consumer,
1891 }))
1892 }
1893
1894 fn handle_reset_ad_usage(
1895 &mut self,
1896 placements: &[AdPlacement],
1897 mut state: OverlordState,
1898 ) -> EventHandleResult<OverlordEvent, OverlordState> {
1899 let now = ::time::utc_now();
1900 for placement in placements {
1901 state.character_state.ad_usage.insert(
1902 *placement,
1903 AdUsageData {
1904 daily_count: 0,
1905 last_reset_at: now,
1906 },
1907 );
1908 }
1909
1910 EventHandleResult::ok(state)
1911 }
1912
1913 fn handle_reset_instant_reward_gems_press_count(
1914 &mut self,
1915 mut state: OverlordState,
1916 ) -> EventHandleResult<OverlordEvent, OverlordState> {
1917 state
1918 .character_state
1919 .character
1920 .instant_reward_gems_press_count = 0;
1921 EventHandleResult::ok(state)
1922 }
1923
1924 pub fn handle_show_bird(
1925 &mut self,
1926 mut state: OverlordState,
1927 ) -> EventHandleResult<OverlordEvent, OverlordState> {
1928 let bird_config = &self.game_config.get().ads_settings.bird_ad;
1929 let cooldown_until = ::time::utc_now()
1930 + chrono::TimeDelta::seconds(bird_config.post_show_cooldown_sec as i64);
1931 state.character_state.character.bird_cooldown_until = Some(cooldown_until);
1932 EventHandleResult::ok(state)
1933 }
1934
1935 pub fn handle_bird_shown(
1936 &mut self,
1937 mut state: OverlordState,
1938 ) -> EventHandleResult<OverlordEvent, OverlordState> {
1939 let bird_config = &self.game_config.get().ads_settings.bird_ad;
1940 let cooldown_until =
1941 ::time::utc_now() + chrono::TimeDelta::seconds(bird_config.cooldown_sec as i64);
1942 state.character_state.character.bird_cooldown_until = Some(cooldown_until);
1943 EventHandleResult::ok(state)
1944 }
1945
1946 pub fn handle_rate_us_shown(
1947 &mut self,
1948 mut state: OverlordState,
1949 ) -> EventHandleResult<OverlordEvent, OverlordState> {
1950 state.character_state.character.rate_us_shown = true;
1951 EventHandleResult::ok(state)
1952 }
1953}