1use crate::{
2 entities,
3 event::{OverlordEvent, PrepareFightType},
4 game_config_helpers::GameConfigLookup,
5 logic::handler::OverlordLogic,
6 state::OverlordState,
7};
8
9use essences::{
10 currency::{CurrencyConsumer, CurrencyUnit, check_can_decrease_currencies},
11 dungeons::DungeonTemplateId,
12 entity::{ActionWithDeadline, Entity, EntityAction, EntityId, EntityState},
13 fighting::{
14 ActiveDungeon, ActiveFight, EntityTeam, EntityType, FightTemplate, FightTemplateId,
15 FightType,
16 },
17 game::Chapter,
18 pvp::PVPState,
19};
20
21use analytics::constants::METRICS_TARGET;
22use configs::game_config::GameConfig;
23use event_system::{event::EventPluginized, script::random::GameRng, system::EventHandleResult};
24use rand::Rng;
25
26impl OverlordLogic {
27 pub fn is_battle_active(&self, state: &OverlordState) -> bool {
28 if let Some(active_fight) = &state.active_fight {
29 let has_player = active_fight
30 .entities
31 .iter()
32 .any(|e| e.id == active_fight.player_id);
33
34 let has_enemy = active_fight
35 .entities
36 .iter()
37 .any(|e| e.team == EntityTeam::Enemy);
38
39 has_player && has_enemy
40 } else {
41 false
42 }
43 }
44
45 pub fn handle_prepare_fight(
46 &mut self,
47 prepare_fight_type: PrepareFightType,
48 rand_gen: rand::rngs::StdRng,
49 mut state: OverlordState,
50 ) -> EventHandleResult<OverlordEvent, OverlordState> {
51 let game_config = self.game_config.get();
52
53 if state.pvp_state.is_some() {
54 return EventHandleResult::fail(state);
55 }
56
57 let is_retry_boss_fight = matches!(prepare_fight_type, PrepareFightType::RetryBossFight);
58
59 match prepare_fight_type {
60 PrepareFightType::PVEFight => {
61 if !self.validate_pve_fight(&state) {
62 return EventHandleResult::fail(state);
63 }
64 }
65 PrepareFightType::PVPFight { .. } => {}
66 PrepareFightType::RetryBossFight => {
67 if !self.validate_prepare_retry_boss_fight(&state) {
68 return EventHandleResult::fail(state);
69 }
70 }
71 PrepareFightType::DungeonFight {
72 dungeon_id,
73 difficulty,
74 } => {
75 if !self.validate_dungeon_fight(&state, dungeon_id, difficulty) {
76 return EventHandleResult::fail(state);
77 }
78 }
79 PrepareFightType::ForfeitDungeonFight => {}
80 PrepareFightType::SingleFight { .. } => {}
81 };
82
83 let now = ::time::utc_now();
89 let pre_sweep_inventory = state
90 .character_state
91 .inventory
92 .iter()
93 .any(|item| item.expires_at.is_some_and(|exp| exp <= now))
94 .then(|| state.character_state.inventory.clone());
95 let expiry_events = super::items::sweep_expired_items(&mut state, now);
96
97 let saved_clock = self.fight_clock.clone();
103 self.fight_clock.clear();
104 state.active_fight = None;
105
106 let prepare_fight_events = match prepare_fight_type {
107 PrepareFightType::PVEFight => self.handle_pve_prepare_fight(&mut state, rand_gen),
108 PrepareFightType::ForfeitDungeonFight => {
109 self.handle_pve_prepare_fight(&mut state, rand_gen)
110 }
111 PrepareFightType::PVPFight {
112 fight_id,
113 pvp_state,
114 } => self.handle_pvp_prepare_fight(&mut state, fight_id, pvp_state, rand_gen),
115 PrepareFightType::RetryBossFight => {
116 match game_config
117 .require_chapter_by_level(state.character_state.character.current_chapter_level)
118 {
119 Ok(chapter) => {
120 state.character_state.character.current_fight_number =
121 (chapter.fight_ids.len() - 1) as i64;
122 self.handle_pve_prepare_fight(&mut state, rand_gen)
123 }
124 Err(_) => {
125 tracing::error!(
126 "Failed to get chapter with chapter_level={}",
127 state.character_state.character.current_chapter_level
128 );
129 None
130 }
131 }
132 }
133 PrepareFightType::DungeonFight {
134 dungeon_id,
135 difficulty,
136 } => self.handle_dungeon_prepare_fight(&mut state, dungeon_id, difficulty, rand_gen),
137 PrepareFightType::SingleFight { fight_templated_id } => {
138 self.handle_single_prepare_fight(&mut state, fight_templated_id, rand_gen)
139 }
140 };
141
142 let Some(events) = prepare_fight_events else {
143 tracing::error!("Preparing fight failed, something went wrong");
144 self.fight_clock = saved_clock;
145 if let Some(inventory) = pre_sweep_inventory {
146 state.character_state.inventory = inventory;
147 }
148 return EventHandleResult::fail(state);
149 };
150
151 if state.active_fight.is_none() {
152 tracing::error!("No active fight after preparing fight");
153 self.fight_clock = saved_clock;
154 if let Some(inventory) = pre_sweep_inventory {
155 state.character_state.inventory = inventory;
156 }
157 return EventHandleResult::fail(state);
158 };
159
160 if is_retry_boss_fight {
161 state.character_state.character.last_boss_fight_won = true;
166 }
167
168 self.fight_clock.set_heartbeat(
169 OverlordEvent::FightProgress {},
170 game_config.game_settings.fight_progress_tick,
171 );
172
173 let mut events = events;
175 events.splice(0..0, expiry_events);
176
177 EventHandleResult::ok_events(state, events)
178 }
179
180 fn validate_pve_fight(&self, state: &OverlordState) -> bool {
181 if let Some(active_fight) = &state.active_fight
182 && !active_fight.fight_ended
183 && active_fight.dungeon.is_some()
184 {
185 tracing::error!("Dungeon fight is in progress");
186 return false;
187 };
188
189 true
190 }
191
192 fn validate_prepare_retry_boss_fight(&self, state: &OverlordState) -> bool {
193 let game_config = self.game_config.get();
194
195 if state.character_state.character.last_boss_fight_won {
201 tracing::info!("The last boss fight was won, so we can't try to retry boss fight");
202 return false;
203 }
204
205 let Some(active_fight) = &state.active_fight else {
206 return true;
207 };
208
209 if active_fight.dungeon.is_some() {
210 tracing::info!("Dungeon fight is in progress");
211 return false;
212 }
213
214 let Ok(fight) = game_config.require_fight_template(active_fight.fight_id) else {
215 tracing::error!(
216 "Failed to get fight_template with id {} ",
217 active_fight.fight_id,
218 );
219 return false;
220 };
221
222 if fight.fight_type == FightType::CampaignBossFight && self.is_battle_active(state) {
223 tracing::info!("The current fight is a boss fight, so we can't retry a boss fight");
224 return false;
225 };
226
227 true
228 }
229
230 fn validate_dungeon_fight(
231 &self,
232 state: &OverlordState,
233 dungeon_id: DungeonTemplateId,
234 difficulty: i64,
235 ) -> bool {
236 let game_config = self.game_config.get();
237
238 if *state
239 .dungeons
240 .completed_difficulties
241 .get(&dungeon_id)
242 .unwrap_or(&0)
243 + 1
244 < difficulty
245 {
246 tracing::error!(
247 "Maximum available difficulty is {} ",
248 *state
249 .dungeons
250 .completed_difficulties
251 .get(&dungeon_id)
252 .unwrap_or(&0)
253 + 1
254 );
255 return false;
256 }
257
258 let Ok(dungeon) = game_config.require_dungeon_template(dungeon_id) else {
259 tracing::error!("Failed to get dungeon_template with id {} ", dungeon_id);
260 return false;
261 };
262
263 if state.character_state.character.current_chapter_level < dungeon.chapter_level_unlock {
264 tracing::error!(
265 "Current chapter level is: {}, required is {} ",
266 state.character_state.character.current_chapter_level,
267 dungeon.chapter_level_unlock
268 );
269 return false;
270 }
271
272 if difficulty > dungeon.max_difficulty_level {
273 tracing::error!(
274 "Maximum difficulty for dungeon with id: {}, is {} ",
275 dungeon_id,
276 dungeon.max_difficulty_level
277 );
278 return false;
279 }
280
281 let keys_required = vec![CurrencyUnit {
282 currency_id: dungeon.key_currency_id,
283 amount: 1,
284 }];
285
286 if !check_can_decrease_currencies(&state.character_state.currencies, &keys_required) {
287 tracing::error!("Not enough keys for running dungeon: {}", dungeon_id);
288 return false;
289 }
290
291 true
292 }
293
294 fn generate_pve_fight_entities(
295 &self,
296 entities: &mut Vec<Entity>,
297 fight: &FightTemplate,
298 state: &mut OverlordState,
299 rand_gen: &mut rand::rngs::StdRng,
300 ) -> anyhow::Result<(Entity, Option<EntityId>)> {
301 let game_config = self.game_config.get();
302
303 fight.fight_entities.iter().for_each(|entity| {
304 let created_entity = match entities::create_pve_entity(
305 uuid::Builder::from_random_bytes(rand_gen.random()).into_uuid(),
306 entity,
307 &game_config,
308 None,
309 ) {
310 Ok(entity) => entity,
311 Err(err) => {
312 tracing::error!("Failed creating entity {}", err.to_string());
313 return;
314 }
315 };
316
317 entities.push(created_entity);
318 });
319
320 let player = match entities::create_player_entity(
321 &state.character_state,
322 uuid::Builder::from_random_bytes(rand_gen.random()).into_uuid(),
323 &game_config,
324 ) {
325 Ok(entity) => entity,
326 Err(err) => {
327 anyhow::bail!("Failed creating player entity {}", err);
328 }
329 };
330
331 entities.push(player.clone());
332
333 let mut party_player_id = None;
334 if let Some(party_state) = &state.party.party_state {
335 match entities::create_party_entity(
336 party_state,
337 uuid::Builder::from_random_bytes(rand_gen.random()).into_uuid(),
338 &game_config,
339 ) {
340 Ok(party_ally) => {
341 party_player_id = Some(party_ally.id);
342 entities.push(party_ally);
343 }
344 Err(err) => {
345 tracing::error!("Failed creating party ally entity {}", err);
346 }
347 }
348 }
349
350 Ok((player, party_player_id))
351 }
352
353 fn generate_pvp_fight_entities(
354 &self,
355 entities: &mut Vec<Entity>,
356 fight: &FightTemplate,
357 pvp_state: &PVPState,
358 state: &mut OverlordState,
359 rand_gen: &mut rand::rngs::StdRng,
360 ) -> anyhow::Result<(Entity, Option<EntityId>)> {
361 let game_config = self.game_config.get();
362
363 fight.fight_entities.iter().for_each(|entity| {
364 let created_entity = match entity.entity_type {
365 EntityType::PVEEntity {
366 entity_template_id: _,
367 } => {
368 match entities::create_pve_entity(
369 uuid::Builder::from_random_bytes(rand_gen.random()).into_uuid(),
370 entity,
371 &game_config,
372 None,
373 ) {
374 Ok(entity) => entity,
375 Err(err) => {
376 tracing::error!("{}", err.to_string());
377 return;
378 }
379 }
380 }
381 EntityType::PVPEntity => {
382 match entities::create_pvp_entity(
383 &EntityState::Opponent(&pvp_state.opponent_state),
384 entity,
385 &game_config,
386 ) {
387 Ok(entity) => entity,
388 Err(err) => {
389 tracing::error!("{}", err.to_string());
390 return;
391 }
392 }
393 }
394 };
395
396 entities.push(created_entity);
397 });
398
399 let player = match entities::create_player_entity(
400 &state.character_state,
401 uuid::Builder::from_random_bytes(rand_gen.random()).into_uuid(),
402 &game_config,
403 ) {
404 Ok(entity) => entity,
405 Err(err) => {
406 anyhow::bail!("Failed creating player entity {}", err);
407 }
408 };
409
410 entities.push(player.clone());
411
412 let party_player_id = None;
414
415 Ok((player, party_player_id))
416 }
417
418 fn run_prepare_fight_script(
419 &self,
420 events: &mut Vec<EventPluginized<OverlordEvent, OverlordState>>,
421 fight: &FightTemplate,
422 active_fight: &ActiveFight,
423 state: &mut OverlordState,
424 rand_gen: rand::rngs::StdRng,
425 ) -> anyhow::Result<()> {
426 let game_config = self.game_config.get();
427 let current_chapter = state.character_state.character.current_chapter_level;
428
429 let Some(waves_cfg) = fight.prepare_fight_waves.as_ref() else {
445 return Ok(());
446 };
447
448 let wave_data = crate::mechanics::fight::wave_data_from_config(waves_cfg);
449 let fight_type_str = format!("{:?}", fight.fight_type);
450 let mut sink = crate::mechanics::fight::NativeSink::default();
451 let rng = GameRng::new(rand_gen);
452 if let Err(err) = crate::mechanics::fight::spawn_wave(
453 &mut sink,
454 &rng,
455 &game_config,
456 self.behaviors.lookups(),
457 active_fight,
458 &wave_data,
459 fight.power.map(|p| p as f64).unwrap_or(0.0),
460 current_chapter,
461 &fight_type_str,
462 ) {
463 anyhow::bail!("Prepare fight wave spawn failed with error: {err:?}");
464 }
465
466 events.append(&mut sink.events.into_iter().map(EventPluginized::now).collect());
467
468 Ok(())
469 }
470
471 fn schedule_start_fight(
474 &mut self,
475 fight: &FightTemplate,
476 fight_id: uuid::Uuid,
477 game_config: &GameConfig,
478 ) {
479 let start_fight_delay = fight
480 .start_fight_delay_ticks
481 .unwrap_or(game_config.fight_settings.start_fight_delay_ticks_default);
482
483 self.fight_clock
484 .schedule(OverlordEvent::StartFight { fight_id }, start_fight_delay);
485 }
486
487 fn handle_pve_prepare_fight(
488 &mut self,
489 state: &mut OverlordState,
490 mut rand_gen: rand::rngs::StdRng,
491 ) -> Option<Vec<EventPluginized<OverlordEvent, OverlordState>>> {
492 let game_config = self.game_config.get();
493
494 let mut events = vec![];
495 let Ok(chapter) = game_config
496 .require_chapter_by_level(state.character_state.character.current_chapter_level)
497 else {
498 tracing::error!(
499 "Failed to get chapter with chapter_level={}",
500 state.character_state.character.current_chapter_level
501 );
502 return None;
503 };
504
505 let Some(fight_id) = chapter
506 .fight_ids
507 .get(state.character_state.character.current_fight_number as usize)
508 .cloned()
509 else {
510 tracing::error!(
511 "Failed to get fight {} for chapter_level={}",
512 state.character_state.character.current_fight_number,
513 state.character_state.character.current_chapter_level
514 );
515 return None;
516 };
517
518 let Ok(fight) = game_config.require_fight_template(fight_id) else {
519 tracing::error!("Failed to get fight_template with id {} ", fight_id,);
520 return None;
521 };
522
523 let mut entities = vec![];
524
525 let (player, party_player_id) =
526 match self.generate_pve_fight_entities(&mut entities, fight, state, &mut rand_gen) {
527 Ok(result) => result,
528 Err(err) => {
529 tracing::error!("Got error, while creating PVE entities: {}", err);
530 return None;
531 }
532 };
533
534 let (pet_combat_state, leader_pet_template_id) = self.build_pet_combat_state(state);
535
536 let active_fight = ActiveFight {
537 id: uuid::Builder::from_random_bytes(rand_gen.random()).into_uuid(),
538 fight_id,
539 current_wave: 1,
540 player_id: player.id,
541 party_player_id,
542 entities,
543 max_duration_ticks: fight.max_duration_ticks,
544 fight_stopped: false,
545 fight_ended: false,
546 dungeon: None,
547 paused: false,
548 pet_combat_state,
549 leader_pet_template_id,
550 };
551
552 state.active_fight = Some(active_fight.clone());
553
554 if let Err(err) =
555 self.run_prepare_fight_script(&mut events, fight, &active_fight, state, rand_gen)
556 {
557 tracing::error!("Error running pve prepare_fight script: {err:?}");
558 return None;
559 }
560
561 self.schedule_start_fight(fight, active_fight.id, &game_config);
562
563 tracing::debug!(
564 "Starting chapter_level={}, fight_number={}, fight_id={}",
565 chapter.level,
566 state.character_state.character.current_fight_number,
567 fight_id,
568 );
569
570 Some(events)
571 }
572
573 fn handle_pvp_prepare_fight(
574 &mut self,
575 state: &mut OverlordState,
576 fight_id: FightTemplateId,
577 pvp_state: Box<PVPState>,
578 mut rand_gen: rand::rngs::StdRng,
579 ) -> Option<Vec<EventPluginized<OverlordEvent, OverlordState>>> {
580 let game_config = self.game_config.get();
581 let prepared_pvp_state = *pvp_state.clone();
586
587 let mut events = vec![];
588
589 let Ok(fight) = game_config.require_fight_template(fight_id) else {
590 tracing::error!("Failed to get fight_template with id {} ", fight_id);
591 return None;
592 };
593
594 let mut entities = vec![];
595
596 let (player, party_player_id) = match self.generate_pvp_fight_entities(
597 &mut entities,
598 fight,
599 &pvp_state,
600 state,
601 &mut rand_gen,
602 ) {
603 Ok(result) => result,
604 Err(err) => {
605 tracing::error!("Got error, while creating PVP entities: {}", err);
606 return None;
607 }
608 };
609
610 let (pet_combat_state, leader_pet_template_id) = self.build_pet_combat_state(state);
611
612 let active_fight = ActiveFight {
613 id: uuid::Builder::from_random_bytes(rand_gen.random()).into_uuid(),
614 fight_id,
615 current_wave: 1,
616 player_id: player.id,
617 party_player_id,
618 entities,
619 max_duration_ticks: fight.max_duration_ticks,
620 fight_stopped: false,
621 fight_ended: false,
622 dungeon: None,
623 paused: false,
624 pet_combat_state,
625 leader_pet_template_id,
626 };
627
628 state.active_fight = Some(active_fight.clone());
629
630 if let Err(err) =
631 self.run_prepare_fight_script(&mut events, fight, &active_fight, state, rand_gen)
632 {
633 tracing::error!("Error running pvp prepare_fight script: {err:?}");
634 return None;
635 }
636
637 state.pvp_state = Some(prepared_pvp_state);
638
639 self.schedule_start_fight(fight, active_fight.id, &game_config);
640
641 Some(events)
642 }
643
644 fn handle_dungeon_prepare_fight(
645 &mut self,
646 state: &mut OverlordState,
647 dungeon_id: DungeonTemplateId,
648 difficulty: i64,
649 mut rand_gen: rand::rngs::StdRng,
650 ) -> Option<Vec<EventPluginized<OverlordEvent, OverlordState>>> {
651 let game_config = self.game_config.get();
652
653 let Ok(dungeon) = game_config.require_dungeon_template(dungeon_id) else {
654 tracing::error!("Failed to get dungeon_template with id {} ", dungeon_id);
655 return None;
656 };
657
658 let Some(fight_template_id) = dungeon.fight_template_ids.get((difficulty - 1) as usize)
659 else {
660 tracing::error!(
661 "Failed to get fight_template from dungeon with id {}, for difficulty: {}",
662 dungeon_id,
663 difficulty
664 );
665 return None;
666 };
667
668 let Ok(fight) = game_config.require_fight_template(*fight_template_id) else {
669 tracing::error!(
670 "Failed to get fight_template with id {} ",
671 fight_template_id
672 );
673 return None;
674 };
675
676 let mut events = vec![];
677
678 let mut entities = vec![];
679
680 let (player, party_player_id) =
681 match self.generate_pve_fight_entities(&mut entities, fight, state, &mut rand_gen) {
682 Ok(result) => result,
683 Err(err) => {
684 tracing::error!("Got error, while creating entities: {}", err);
685 return None;
686 }
687 };
688
689 let (pet_combat_state, leader_pet_template_id) = self.build_pet_combat_state(state);
690
691 let active_fight = ActiveFight {
692 id: uuid::Builder::from_random_bytes(rand_gen.random()).into_uuid(),
693 fight_id: *fight_template_id,
694 current_wave: 1,
695 player_id: player.id,
696 party_player_id,
697 entities,
698 max_duration_ticks: fight.max_duration_ticks,
699 fight_stopped: false,
700 fight_ended: false,
701 dungeon: Some(ActiveDungeon {
702 id: dungeon_id,
703 difficulty,
704 }),
705 paused: false,
706 pet_combat_state,
707 leader_pet_template_id,
708 };
709
710 state.active_fight = Some(active_fight.clone());
711
712 if let Err(err) =
713 self.run_prepare_fight_script(&mut events, fight, &active_fight, state, rand_gen)
714 {
715 tracing::error!("Error running pvp prepare_fight script: {err:?}");
716 return None;
717 }
718
719 self.schedule_start_fight(fight, active_fight.id, &game_config);
720
721 Some(events)
722 }
723
724 fn handle_single_prepare_fight(
725 &mut self,
726 state: &mut OverlordState,
727 fight_template_id: FightTemplateId,
728 mut rand_gen: rand::rngs::StdRng,
729 ) -> Option<Vec<EventPluginized<OverlordEvent, OverlordState>>> {
730 let game_config = self.game_config.get();
731
732 let Ok(fight) = game_config.require_fight_template(fight_template_id) else {
733 tracing::error!(
734 "Failed to get fight_template with id {} ",
735 fight_template_id
736 );
737 return None;
738 };
739
740 let mut events = vec![];
741
742 let mut entities = vec![];
743
744 let (player, party_player_id) =
745 match self.generate_pve_fight_entities(&mut entities, fight, state, &mut rand_gen) {
746 Ok(result) => result,
747 Err(err) => {
748 tracing::error!("Got error, while creating entities: {}", err);
749 return None;
750 }
751 };
752
753 let (pet_combat_state, leader_pet_template_id) = self.build_pet_combat_state(state);
754
755 let active_fight = ActiveFight {
756 id: uuid::Builder::from_random_bytes(rand_gen.random()).into_uuid(),
757 fight_id: fight_template_id,
758 current_wave: 1,
759 player_id: player.id,
760 party_player_id,
761 entities,
762 max_duration_ticks: fight.max_duration_ticks,
763 fight_stopped: false,
764 paused: false,
765 fight_ended: false,
766 dungeon: None,
767 pet_combat_state,
768 leader_pet_template_id,
769 };
770
771 state.active_fight = Some(active_fight.clone());
772
773 if let Err(err) =
774 self.run_prepare_fight_script(&mut events, fight, &active_fight, state, rand_gen)
775 {
776 tracing::error!("Error running single prepare_fight script: {err:?}");
777 return None;
778 }
779
780 self.schedule_start_fight(fight, active_fight.id, &game_config);
781
782 Some(events)
783 }
784
785 pub fn handle_start_fight(
786 &mut self,
787 event: OverlordEvent,
788 fight_id: uuid::Uuid,
789 _rand_gen: rand::rngs::StdRng,
791 current_tick: u64,
792 mut state: OverlordState,
793 ) -> EventHandleResult<OverlordEvent, OverlordState> {
794 let game_config = self.game_config.get();
795
796 let Some(active_fight) = &mut state.active_fight else {
797 tracing::error!("No active fight for start_fight");
798 return EventHandleResult::fail(state);
799 };
800
801 if active_fight.id != fight_id {
802 tracing::error!(
803 "StartFight fight_id mismatch: expected {}, got {}",
804 active_fight.id,
805 fight_id
806 );
807 return EventHandleResult::fail(state);
808 }
809
810 let Ok(fight_template) = game_config.require_fight_template(active_fight.fight_id) else {
811 tracing::error!("No fight template with fight_id: {}", active_fight.fight_id);
812 return EventHandleResult::fail(state);
813 };
814
815 self.start_fight_tick = current_tick;
816
817 let mut events = vec![];
818
819 active_fight.entities.iter_mut().for_each(|e| {
820 e.abilities.iter_mut().for_each(|ability| {
821 e.actions_queue.push(&ActionWithDeadline {
822 action: self.make_start_cast_ability_action(e.id, ability.ability.template_id),
823 deadline_tick: current_tick,
824 })
825 })
826 });
827
828 let player_id = active_fight.player_id;
830 for pet in state.character_state.equipped_pets.supports() {
831 if let Some(ability_template) = pet
832 .passive_ability_id
833 .and_then(|id| game_config.ability_template(id))
834 {
835 let passive_ability =
836 essences::abilities::Ability::from_template(ability_template, None, None);
837 let passive_ability_id = passive_ability.template_id;
838
839 if let Some(player_entity) =
841 active_fight.entities.iter_mut().find(|e| e.id == player_id)
842 {
843 player_entity
844 .abilities
845 .push(essences::abilities::ActiveAbility {
846 ability: passive_ability,
847 deadline: None,
848 slot_id: None,
849 });
850 player_entity.actions_queue.push(&ActionWithDeadline {
851 action: EntityAction::StartCastAbility {
852 ability_id: passive_ability_id,
853 by_entity_id: player_id,
854 pet_id: Some(pet.template_id),
855 },
856 deadline_tick: current_tick,
857 });
858 }
859 }
860 }
861
862 active_fight.max_duration_ticks = fight_template.max_duration_ticks;
863
864 let active_fight_for_native = active_fight.clone();
865 let start_behavior = fight_template.start_behavior.clone();
866 let _ = event;
869
870 let Some(native_name) = start_behavior.as_deref() else {
874 tracing::error!("Fight template {} has no start_behavior", fight_template.id);
875 return EventHandleResult::fail(state);
876 };
877 let Some(native_fn) = self.behaviors.fight_start_fn(native_name) else {
878 tracing::error!("No registered fight_start native fn named {native_name}");
879 return EventHandleResult::fail(state);
880 };
881 match native_fn(&crate::behaviors::combat::fight_start::FightStartCtx {
882 fight: &active_fight_for_native,
883 state: &state,
884 lookups: self.behaviors.lookups(),
885 }) {
886 Ok(start_fight_events) => {
887 events.append(
888 &mut start_fight_events
889 .into_iter()
890 .map(EventPluginized::now)
891 .collect(),
892 );
893 }
894 Err(err) => {
895 tracing::error!("Start fight native fn failed with error: {err:?}");
896 return EventHandleResult::fail(state);
897 }
898 };
899
900 EventHandleResult::ok_events(state, events)
901 }
902
903 pub fn get_prepare_fight_delay(
904 &self,
905 is_win: bool,
906 chapter: &Chapter,
907 state: &OverlordState,
908 ) -> u64 {
909 let game_config = self.game_config.get();
910
911 let Some(fight_id) = chapter
912 .fight_ids
913 .get(state.character_state.character.current_fight_number as usize)
914 .cloned()
915 else {
916 tracing::error!(
917 "Failed to get fight {} for chapter_level={}",
918 state.character_state.character.current_fight_number,
919 state.character_state.character.current_chapter_level
920 );
921 return 0;
922 };
923
924 let Ok(fight) = game_config.require_fight_template(fight_id) else {
925 tracing::error!("Failed to get fight_template with id {} ", fight_id,);
926 return 0;
927 };
928
929 if is_win {
930 fight.prepare_fight_win_duration_ticks.unwrap_or(
931 game_config
932 .fight_settings
933 .prepare_fight_win_delay_ticks_default,
934 )
935 } else {
936 fight.prepare_fight_lose_duration_ticks.unwrap_or(
937 game_config
938 .fight_settings
939 .prepare_fight_lose_delay_ticks_default,
940 )
941 }
942 }
943
944 pub fn get_end_fight_delay(&self, fight_id: FightTemplateId) -> u64 {
945 let game_config = self.game_config.get();
946
947 let Ok(fight) = game_config.require_fight_template(fight_id) else {
948 tracing::error!("Failed to get fight_template with id {} ", fight_id,);
949 return 0;
950 };
951
952 fight
953 .end_fight_delay_ticks
954 .unwrap_or(game_config.fight_settings.end_fight_delay_ticks_default)
955 }
956
957 fn continue_or_stop_fight_loop(
961 &mut self,
962 state: &mut OverlordState,
963 events: &mut Vec<EventPluginized<OverlordEvent, OverlordState>>,
964 current_fight: &FightTemplate,
965 is_win: bool,
966 prepare_fight_delay_ticks: u64,
967 ) {
968 if (!is_win || !current_fight.stop_on_win) && (is_win || !current_fight.stop_on_lose) {
969 events.push(EventPluginized::now(
970 OverlordEvent::RefreshPartyMemberState {},
971 ));
972 self.fight_clock.schedule(
973 OverlordEvent::PrepareFight {
974 prepare_fight_type: PrepareFightType::PVEFight,
975 },
976 prepare_fight_delay_ticks,
977 );
978 } else if let Some(fight) = state.active_fight.as_mut() {
979 fight.fight_stopped = true;
980 }
981 }
982
983 fn end_pvp_fight(
984 &mut self,
985 is_win: bool,
986 pvp_state: &PVPState,
987 prepare_fight_delay_ticks: u64,
988 current_fight: &FightTemplate,
989 mut state: OverlordState,
990 ) -> EventHandleResult<OverlordEvent, OverlordState> {
991 let mut events = vec![];
992 if let Some(vassal) = &pvp_state.vassal {
993 if is_win {
994 state.character_state.vassals.push(vassal.clone());
995 if let Some(bundle_id) = current_fight.bundle_reward_id {
996 events.push(EventPluginized::now(OverlordEvent::AddBundleGroup {
997 bundle_ids: vec![bundle_id],
998 source: essences::currency::CurrencySource::PvpVassalReward,
999 }));
1000 }
1001 } else {
1002 tracing::error!("Expected to add vassal, but PVP is lost");
1003 }
1004 }
1005
1006 if let Some(rating_change) = &pvp_state.rating_change {
1007 if is_win {
1008 state.character_state.character.arena_rating +=
1009 rating_change.winner_rating_increase;
1010 } else {
1011 state.character_state.character.arena_rating += rating_change.loser_rating_decrease;
1012 }
1013 }
1014
1015 state.pvp_state = None;
1016
1017 self.continue_or_stop_fight_loop(
1018 &mut state,
1019 &mut events,
1020 current_fight,
1021 is_win,
1022 prepare_fight_delay_ticks,
1023 );
1024
1025 EventHandleResult::ok_events(state, events)
1026 }
1027
1028 fn end_pve_fight(
1029 &mut self,
1030 is_win: bool,
1031 current_chapter: &Chapter,
1032 prepare_fight_delay_ticks: u64,
1033 current_fight: &FightTemplate,
1034 mut state: OverlordState,
1035 ) -> EventHandleResult<OverlordEvent, OverlordState> {
1036 let game_config = self.game_config.get();
1037
1038 let mut events = vec![];
1039
1040 let prev_chapter_level = state.character_state.character.current_chapter_level;
1041 let mut next_chapter_level = prev_chapter_level;
1042 let mut next_fight_number = state.character_state.character.current_fight_number + 1;
1043
1044 if is_win {
1045 if next_fight_number >= current_chapter.fight_ids.len() as i64 {
1046 next_fight_number = 0;
1047 if game_config
1048 .chapter_by_level(next_chapter_level + 1)
1049 .is_some()
1050 {
1051 state.character_state.character.last_boss_fight_won = true;
1052 next_chapter_level += 1;
1053 }
1054 } else {
1055 let next_fight_id = ¤t_chapter.fight_ids[next_fight_number as usize];
1056
1057 let Ok(next_fight) = game_config.require_fight_template(*next_fight_id) else {
1058 tracing::error!(
1059 "Failed to get next fight_template with id={}",
1060 next_fight_id
1061 );
1062 return EventHandleResult::fail(state);
1063 };
1064
1065 let _ = next_fight;
1073 }
1074
1075 state.character_state.character.current_chapter_level = next_chapter_level;
1076 state.character_state.character.current_fight_number = next_fight_number;
1077
1078 if self.should_reset_afk_timer_on_gating_unlock(prev_chapter_level, &state) {
1079 events.push(EventPluginized::now(
1080 OverlordEvent::AfkRewardsGatingUnlocked {},
1081 ));
1082 }
1083
1084 if !state.character_state.character.rate_us_shown
1085 && let Some(threshold) = game_config.game_settings.rate_us_chapter
1086 && prev_chapter_level < threshold
1087 && next_chapter_level >= threshold
1088 {
1089 events.push(EventPluginized::now(OverlordEvent::ShowRateUs {}));
1090 tracing::info!(
1091 target: METRICS_TARGET,
1092 event_type = "show_rate_us",
1093 character_id = %state.character_state.character.id,
1094 chapter_level = next_chapter_level,
1095 trigger = "chapter_advance",
1096 "Show rate us",
1097 );
1098 }
1099
1100 if let Some(bundle_id) = current_fight.bundle_reward_id {
1101 events.push(EventPluginized::now(OverlordEvent::AddBundleGroup {
1102 bundle_ids: vec![bundle_id],
1103 source: essences::currency::CurrencySource::ChapterReward,
1104 }));
1105 }
1106 } else {
1107 if current_fight.fight_type == FightType::CampaignBossFight {
1108 state.character_state.character.last_boss_fight_won = false;
1109 }
1110
1111 next_fight_number = 0;
1112 state.character_state.character.current_fight_number = next_fight_number;
1113 }
1114
1115 self.continue_or_stop_fight_loop(
1116 &mut state,
1117 &mut events,
1118 current_fight,
1119 is_win,
1120 prepare_fight_delay_ticks,
1121 );
1122
1123 EventHandleResult::ok_events(state, events)
1124 }
1125
1126 fn end_dungeon_fight(
1127 &mut self,
1128 is_win: bool,
1129 prepare_fight_delay_ticks: u64,
1130 active_dungeon: &ActiveDungeon,
1131 current_fight: &FightTemplate,
1132 mut state: OverlordState,
1133 ) -> EventHandleResult<OverlordEvent, OverlordState> {
1134 let game_config = self.game_config.get();
1135
1136 let Ok(dungeon) = game_config.require_dungeon_template(active_dungeon.id) else {
1137 tracing::error!(
1138 "Failed to get dungeon_template with id {} ",
1139 active_dungeon.id
1140 );
1141 return EventHandleResult::fail(state);
1142 };
1143
1144 let mut events = vec![];
1145
1146 if is_win {
1147 let keys_price = vec![CurrencyUnit {
1148 currency_id: dungeon.key_currency_id,
1149 amount: 1,
1150 }];
1151
1152 let Some(currency_event) =
1153 Self::currency_decrease(&state, &keys_price, CurrencyConsumer::DungeonFightEnd)
1154 else {
1155 return EventHandleResult::fail(state);
1156 };
1157 events.push(currency_event);
1158
1159 if let Some(bundle_id) = current_fight.bundle_reward_id {
1160 events.push(EventPluginized::now(OverlordEvent::AddBundleGroup {
1161 bundle_ids: vec![bundle_id],
1162 source: essences::currency::CurrencySource::DungeonReward,
1163 }));
1164 }
1165
1166 state
1167 .dungeons
1168 .completed_difficulties
1169 .entry(dungeon.id)
1170 .and_modify(|v| {
1171 if active_dungeon.difficulty > *v {
1172 *v = active_dungeon.difficulty;
1173 }
1174 })
1175 .or_insert(active_dungeon.difficulty);
1176 }
1177
1178 self.continue_or_stop_fight_loop(
1179 &mut state,
1180 &mut events,
1181 current_fight,
1182 is_win,
1183 prepare_fight_delay_ticks,
1184 );
1185
1186 EventHandleResult::ok_events(state, events)
1187 }
1188
1189 pub(crate) fn should_reset_afk_timer_on_gating_unlock(
1195 &self,
1196 prev_chapter_level: i64,
1197 state: &OverlordState,
1198 ) -> bool {
1199 let game_config = self.game_config.get();
1200 let unlock_chapter = game_config.gatings.afk_rewards_button_unlock_chapter;
1201 let new_chapter_level = state.character_state.character.current_chapter_level;
1202
1203 if prev_chapter_level >= unlock_chapter || new_chapter_level < unlock_chapter {
1204 return false;
1205 }
1206
1207 let min_required_time_sec = game_config.afk_rewards_settings.min_required_time_sec as i64;
1208 let elapsed = ::time::utc_now()
1209 .signed_duration_since(state.character_state.character.last_afk_reward_claimed_at)
1210 .num_seconds();
1211
1212 elapsed < min_required_time_sec
1213 }
1214
1215 pub fn handle_afk_rewards_gating_unlocked(
1216 &self,
1217 mut state: OverlordState,
1218 ) -> EventHandleResult<OverlordEvent, OverlordState> {
1219 let min_required_time_sec = self
1220 .game_config
1221 .get()
1222 .afk_rewards_settings
1223 .min_required_time_sec as i64;
1224 state.character_state.character.last_afk_reward_claimed_at =
1225 ::time::utc_now() - chrono::Duration::seconds(min_required_time_sec);
1226 EventHandleResult::ok(state)
1227 }
1228
1229 pub fn handle_end_fight(
1230 &mut self,
1231 fight_id: uuid::Uuid,
1232 is_win: bool,
1233 pvp_state: Option<&PVPState>,
1234 mut state: OverlordState,
1235 ) -> EventHandleResult<OverlordEvent, OverlordState> {
1236 let game_config = self.game_config.get();
1237
1238 let Some(active_fight) = &mut state.active_fight else {
1239 tracing::error!("No active fight for end_fight");
1240 return EventHandleResult::fail(state);
1241 };
1242
1243 if active_fight.id != fight_id {
1244 tracing::error!(
1245 "EndFight fight_id mismatch: expected {}, got {}",
1246 active_fight.id,
1247 fight_id
1248 );
1249 return EventHandleResult::fail(state);
1250 }
1251
1252 if self.ended_fight_id == Some(fight_id) {
1253 tracing::info!("Fight {fight_id} already ended, ignoring duplicate EndFight");
1261 return EventHandleResult::fail(state);
1262 }
1263
1264 let Ok(current_fight) = game_config.require_fight_template(active_fight.fight_id) else {
1265 tracing::error!(
1266 "Failed to get currrent fight_template with id={}",
1267 active_fight.fight_id
1268 );
1269 return EventHandleResult::fail(state);
1270 };
1271
1272 active_fight.fight_ended = true;
1273 self.ended_fight_id = Some(fight_id);
1274
1275 if current_fight.fight_type == FightType::SingleFight {
1276 self.fight_clock.schedule(
1277 OverlordEvent::PrepareFight {
1278 prepare_fight_type: PrepareFightType::PVEFight,
1279 },
1280 game_config
1281 .fight_settings
1282 .prepare_fight_win_delay_ticks_default,
1283 );
1284 return EventHandleResult::ok_events(
1285 state,
1286 vec![EventPluginized::now(
1287 OverlordEvent::RefreshPartyMemberState {},
1288 )],
1289 );
1290 }
1291
1292 let active_fight = active_fight.clone();
1293
1294 let Ok(current_chapter) = game_config
1295 .require_chapter_by_level(state.character_state.character.current_chapter_level)
1296 .cloned()
1297 else {
1298 tracing::error!(
1299 "Failed to get chapter with chapter_level={}",
1300 state.character_state.character.current_chapter_level
1301 );
1302 return EventHandleResult::fail(state);
1303 };
1304
1305 let prepare_fight_delay_ticks =
1306 self.get_prepare_fight_delay(is_win, ¤t_chapter, &state);
1307
1308 if let Some(pvp_state) = pvp_state {
1309 return self.end_pvp_fight(
1310 is_win,
1311 pvp_state,
1312 prepare_fight_delay_ticks,
1313 current_fight,
1314 state,
1315 );
1316 }
1317
1318 if let Some(active_dungeon) = &active_fight.dungeon {
1319 return self.end_dungeon_fight(
1320 is_win,
1321 prepare_fight_delay_ticks,
1322 active_dungeon,
1323 current_fight,
1324 state,
1325 );
1326 }
1327
1328 self.end_pve_fight(
1329 is_win,
1330 ¤t_chapter,
1331 prepare_fight_delay_ticks,
1332 current_fight,
1333 state,
1334 )
1335 }
1336}