overlord_event_system/async_handler/
chapters_management.rs

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        // It triggers only on game start
88        // Should be moved to StartGame event later, when crons can be used in SyncHandler
89        // Moving this will break calculate_pvp_result function, should start calling fight_progress in it
90        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        // No party ally in PvP — arena is strictly 1v1
383        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        // Emit StartCastAbility for support pets' passive abilities at fight start
794        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                // Add passive as a one-shot ability on the player entity
805                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 && stop_on_win) or (!is_win && stop_on_lose) - we stop fighting
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            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 = &current_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 && stop_on_win) or (!is_win && stop_on_lose) - we stop fighting
1053        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 && stop_on_win) or (!is_win && stop_on_lose) - we stop fighting
1122        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    /// Pure predicate: is the chapter transition crossing the AFK rewards unlock
1140    /// threshold *and* is the elapsed time since the last claim shorter than
1141    /// `min_required_time_sec`? Callers use this to decide whether to emit
1142    /// `AfkRewardsGatingUnlocked`; the async handler for that event owns the
1143    /// actual state mutation.
1144    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, &current_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            &current_chapter,
1268            prepare_fight_delay_ticks,
1269            current_fight,
1270            state,
1271        )
1272    }
1273}