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