overlord_event_system/async_handler/
fighting.rs

1use crate::{
2    TICKER_UNIT_DURATION_MS,
3    async_handler::handler::OverlordAsyncEventHandler,
4    entities::{create_pve_entity, event_from_entity_action},
5    event::{
6        OverlordEvent, OverlordEventCastAbility, OverlordEventCastProjectile,
7        OverlordEventStartCastAbility, OverlordEventStartCastProjectile,
8    },
9    game_config_helpers::GameConfigLookup,
10    script::{CustomEventData, StartCastAbilityResult},
11    state::OverlordState,
12};
13
14use essences::{
15    abilities::{AbilityId, AbilitySlotId},
16    currency::{CurrencySource, CurrencyUnit},
17    entity::{ActionWithDeadline, Coordinates, Entity, EntityAction, EntityAttributes, EntityId},
18    fighting::{EntityTeam, EntityType, FightEntity, FightType},
19    game::EntityTemplateId,
20};
21use event_system::{
22    event::{EventPluginized, EventRhaiEnum, EventStruct},
23    script::runner::ScriptRandom,
24    system::EventHandleResult,
25};
26
27use rand::Rng;
28use uuid::Uuid;
29
30impl OverlordAsyncEventHandler {
31    fn compute_ability_slot_level(
32        &self,
33        slot_id: Option<AbilitySlotId>,
34        state: &OverlordState,
35    ) -> i64 {
36        let Some(slot_id) = slot_id else {
37            return 0;
38        };
39
40        let game_config = self.game_config.get();
41        let slot_level = state
42            .character_state
43            .character
44            .ability_slot_levels
45            .get(slot_id)
46            .copied()
47            .unwrap_or(0)
48            .max(0);
49        game_config
50            .game_settings
51            .ability_gacha
52            .slot_level_bonus_levels
53            .get(slot_level as usize)
54            .copied()
55            .or_else(|| {
56                game_config
57                    .game_settings
58                    .ability_gacha
59                    .slot_level_bonus_levels
60                    .last()
61                    .copied()
62            })
63            .unwrap_or(0)
64    }
65
66    /// Add charge to the pet combat state. If charge reaches max, queue a StartCastAbility
67    /// for the pet and reset charge to 0.
68    fn charge_pet_ability(&self, state: &mut OverlordState, charge_delta: i64, current_tick: u64) {
69        let Some(active_fight) = &mut state.active_fight else {
70            return;
71        };
72        let Some(pet_state) = &mut active_fight.pet_combat_state else {
73            return;
74        };
75
76        pet_state.charge = (pet_state.charge + charge_delta).min(pet_state.max_charge);
77
78        if pet_state.charge >= pet_state.max_charge {
79            let ability_id = pet_state.ability_id;
80            let pet_template_id = pet_state.pet_template_id;
81            let player_id = active_fight.player_id;
82
83            pet_state.charge = 0;
84
85            if let Some(player) = active_fight.entities.iter_mut().find(|e| e.id == player_id) {
86                player.actions_queue.push(&ActionWithDeadline {
87                    action: EntityAction::StartCastAbility {
88                        ability_id,
89                        by_entity_id: player_id,
90                        pet_id: Some(pet_template_id),
91                    },
92                    deadline_tick: current_tick,
93                });
94            }
95        }
96    }
97
98    #[allow(clippy::too_many_arguments)]
99    pub fn handle_spawn_entity(
100        &self,
101        id: EntityId,
102        entity_template_id: EntityTemplateId,
103        position: Coordinates,
104        team: EntityTeam,
105        has_big_hp_bar: bool,
106        entity_attributes: EntityAttributes,
107        current_tick: u64,
108        mut state: OverlordState,
109    ) -> EventHandleResult<OverlordEvent, OverlordState> {
110        let game_config = self.game_config.get();
111
112        let Some(active_fight) = &mut state.active_fight else {
113            return EventHandleResult::ok(state);
114        };
115
116        if active_fight.entities.iter().any(|entity| entity.id == id) {
117            tracing::error!("There is already an entity with id: {id}");
118            return EventHandleResult::fail(state);
119        }
120
121        let fight_entity = FightEntity {
122            entity_type: EntityType::PVEEntity { entity_template_id },
123            position,
124            has_big_hp_bar,
125            team,
126        };
127
128        let mut created_entity =
129            match create_pve_entity(id, &fight_entity, &game_config, Some(entity_attributes)) {
130                Ok(entity) => entity,
131                Err(err) => {
132                    tracing::error!("Couldn't create entity: {}", err.to_string());
133                    return EventHandleResult::fail(state);
134                }
135            };
136
137        if active_fight.current_wave > 1 {
138            created_entity.abilities.iter().for_each(|ability| {
139                let cooldown = game_config
140                    .ability_template(ability.ability.template_id)
141                    .map(|t| t.cooldown)
142                    .unwrap_or(0);
143                created_entity.actions_queue.push(&ActionWithDeadline {
144                    action: self.make_start_cast_ability_action(
145                        created_entity.id,
146                        ability.ability.template_id,
147                    ),
148                    deadline_tick: current_tick + cooldown,
149                })
150            });
151        }
152
153        active_fight.entities.push(created_entity);
154
155        EventHandleResult::ok(state)
156    }
157    pub fn handle_start_move(
158        &self,
159        entity_id: Uuid,
160        to: Coordinates,
161        duration_ticks: u64,
162        mut state: OverlordState,
163    ) -> EventHandleResult<OverlordEvent, OverlordState> {
164        let Some(active_fight) = &mut state.active_fight else {
165            return EventHandleResult::ok(state);
166        };
167
168        let mut events = Vec::new();
169
170        let Some(entity) = active_fight
171            .entities
172            .iter_mut()
173            .find(|entity| entity.id == entity_id)
174        else {
175            tracing::error!("Failed to find entity in state with id={}", entity_id);
176            return EventHandleResult::fail(state);
177        };
178
179        entity.being_moved = true;
180        entity.coordinates = to.clone();
181
182        events.push(EventPluginized::delayed(
183            OverlordEvent::EndMove { entity_id },
184            duration_ticks,
185        ));
186
187        EventHandleResult::ok_events(state, events)
188    }
189
190    pub fn handle_end_move(
191        &self,
192        entity_id: Uuid,
193        mut state: OverlordState,
194    ) -> EventHandleResult<OverlordEvent, OverlordState> {
195        let Some(active_fight) = &mut state.active_fight else {
196            return EventHandleResult::ok(state);
197        };
198
199        let Some(entity) = active_fight
200            .entities
201            .iter_mut()
202            .find(|entity| entity.id == entity_id)
203        else {
204            tracing::error!("Failed to find entity in state with id={}", entity_id);
205            return EventHandleResult::fail(state);
206        };
207
208        entity.being_moved = false;
209
210        EventHandleResult::ok_events(
211            state,
212            vec![EventPluginized::now(OverlordEvent::FightProgress {})],
213        )
214    }
215
216    pub fn handle_entity_incr_attribute(
217        &mut self,
218        entity_id: Uuid,
219        attribute: &str,
220        delta: i64,
221        mut state: OverlordState,
222    ) -> EventHandleResult<OverlordEvent, OverlordState> {
223        let game_config = self.game_config.get();
224
225        let Some(active_fight) = &mut state.active_fight else {
226            return EventHandleResult::ok(state);
227        };
228
229        let Some(entity) = active_fight.entities.iter_mut().find(|e| e.id == entity_id) else {
230            tracing::debug!("Couldn't find entity_id = {}", entity_id);
231            return EventHandleResult::fail(state);
232        };
233
234        entity.attributes.add(attribute, delta);
235
236        entity.effect_ids = entity
237            .effect_ids
238            .iter()
239            .filter(|effect_id| {
240                if let Some(effect) = game_config.effect(**effect_id) {
241                    // tracing::error!("Found this effect: {effect:?}");
242                    // tracing::error!("Got this attributes: {:?}", entity.attributes);
243                    if effect.has_at_least_one_required_attribute(&entity.attributes) {
244                        true
245                    } else {
246                        if effect.interval_ticks.is_some() {
247                            entity.actions_queue.remove_cast_effect_action(effect.id);
248                        }
249                        false
250                    }
251                } else {
252                    false
253                }
254            })
255            .cloned()
256            .collect();
257
258        EventHandleResult::ok(state)
259    }
260
261    pub fn handle_entity_apply_effect(
262        &mut self,
263        entity_id: Uuid,
264        effect_id: Uuid,
265        current_tick: u64,
266        mut state: OverlordState,
267    ) -> EventHandleResult<OverlordEvent, OverlordState> {
268        let Some(active_fight) = &mut state.active_fight else {
269            return EventHandleResult::ok(state);
270        };
271
272        let game_config = self.game_config.get();
273
274        let Some(entity) = active_fight.entities.iter_mut().find(|e| e.id == entity_id) else {
275            tracing::debug!("Couldn't find entity_id = {}", entity_id);
276            return EventHandleResult::fail(state);
277        };
278
279        let Ok(effect) = game_config.require_effect(effect_id) else {
280            tracing::debug!("Couldn't find effect_id = {}", effect_id);
281            return EventHandleResult::fail(state);
282        };
283
284        if let Some(required_attributes) = &effect.required_attributes
285            && !required_attributes
286                .iter()
287                .any(|attr| entity.attributes.0.contains_key(attr))
288        {
289            tracing::error!("Effect has required attributes, but they are not set");
290            return EventHandleResult::fail(state);
291        }
292
293        for existing_effect_id in &entity.effect_ids {
294            if *existing_effect_id == effect.id {
295                tracing::error!("Effect is already set on entity");
296                return EventHandleResult::fail(state);
297            }
298        }
299
300        entity.effect_ids.push(effect.id);
301
302        if let Some(interval_ticks) = &effect.interval_ticks {
303            entity.actions_queue.push(&ActionWithDeadline {
304                action: self.make_cast_effect_action(entity_id, effect.id),
305                deadline_tick: current_tick + interval_ticks,
306            });
307        }
308
309        EventHandleResult::ok(state)
310    }
311
312    #[allow(clippy::too_many_arguments)]
313    pub fn handle_cast_effect(
314        &mut self,
315        entity_id: Uuid,
316        effect_id: Uuid,
317        caller_event: Option<Box<OverlordEvent>>,
318        rand_gen: rand::rngs::StdRng,
319        current_tick: u64,
320        mut state: OverlordState,
321    ) -> EventHandleResult<OverlordEvent, OverlordState> {
322        let state_cloned = state.clone();
323
324        let Some(active_fight) = &mut state.active_fight else {
325            return EventHandleResult::ok(state);
326        };
327
328        let active_fight_cloned = active_fight.clone();
329
330        let game_config = self.game_config.get();
331
332        let Some(entity) = active_fight.entities.iter_mut().find(|e| e.id == entity_id) else {
333            tracing::debug!("Couldn't find entity_id = {}", entity_id);
334            return EventHandleResult::fail(state);
335        };
336
337        let Ok(effect) = game_config.require_effect(effect_id) else {
338            tracing::debug!("Couldn't find effect_id = {}", effect_id);
339            return EventHandleResult::fail(state);
340        };
341
342        if !entity.effect_ids.contains(&effect_id) {
343            tracing::error!("entity_id = {} has no effect_id = {}", entity_id, effect_id);
344            return EventHandleResult::fail(state);
345        }
346
347        let enttiy_cloned = entity.clone();
348
349        match self.script_runner.run_event(
350            |mut scope_setter| {
351                scope_setter.set_const("Entity", enttiy_cloned);
352                if let Some(caller_event) = caller_event {
353                    caller_event.add_event_to_scope(&mut scope_setter, "Event");
354                }
355                scope_setter.set_const("Random", ScriptRandom::new(rand_gen));
356                scope_setter.set_const("Fight", active_fight_cloned);
357                scope_setter.set_const("FightDurationTicks", current_tick - self.start_fight_tick);
358                scope_setter
359            },
360            &state_cloned,
361            &effect.script,
362        ) {
363            Ok(events) => {
364                if let Some(interval_ticks) = effect.interval_ticks {
365                    entity.actions_queue.push(&ActionWithDeadline {
366                        action: self.make_cast_effect_action(entity_id, effect.id),
367                        deadline_tick: current_tick + interval_ticks,
368                    });
369                }
370
371                EventHandleResult::ok_events(
372                    state,
373                    events.into_iter().map(EventPluginized::now).collect(),
374                )
375            }
376            Err(err) => {
377                tracing::error!("Effect script failed with error: {err:?}");
378                EventHandleResult::fail(state)
379            }
380        }
381    }
382
383    pub fn handle_start_cast_ability(
384        &mut self,
385        event: OverlordEvent,
386        by_entity_id: Uuid,
387        ability_id: AbilityId,
388        rand_gen: rand::rngs::StdRng,
389        current_tick: u64,
390        mut state: OverlordState,
391    ) -> EventHandleResult<OverlordEvent, OverlordState> {
392        let game_config = self.game_config.get();
393
394        let state_cloned = state.clone();
395
396        let Some(active_fight) = &mut state.active_fight else {
397            tracing::error!("No active fight for start_cast_ability");
398            return EventHandleResult::ok(state);
399        };
400        let active_fight_cloned = active_fight.clone();
401        let Some(casted_by_entity) = active_fight
402            .entities
403            .iter_mut()
404            .find(|e| e.id == by_entity_id)
405        else {
406            tracing::debug!("Couldn't find caster entity_id = {}", by_entity_id);
407            return EventHandleResult::fail(state);
408        };
409        let casted_by_entity_cloned = casted_by_entity.clone();
410        let Some(active_ability) = casted_by_entity
411            .abilities
412            .iter_mut()
413            .find(|equipped_ability| equipped_ability.ability.template_id == ability_id)
414        else {
415            tracing::error!(
416                "Couldn't find ability_id = {} in caster entity {:?}",
417                ability_id,
418                casted_by_entity
419            );
420            return EventHandleResult::fail(state);
421        };
422        let ability = &active_ability.ability;
423        let ability_template_id = ability.template_id;
424        let ability_level = ability.level;
425
426        let Some(ability_template) = game_config.ability_template(ability_template_id).cloned()
427        else {
428            tracing::error!(
429                "Couldn't find template for ability_id = {}",
430                ability_template_id
431            );
432            return EventHandleResult::fail(state);
433        };
434        let ability_cooldown = ability_template.cooldown;
435
436        let ability_slot_level =
437            self.compute_ability_slot_level(active_ability.slot_id, &state_cloned);
438
439        let results = match self.script_runner.run_start_cast_ability(
440            |mut scope_setter| {
441                scope_setter.set_event(OverlordEventStartCastAbility::from_enum(event));
442                scope_setter.set_const("CasterEntity", casted_by_entity_cloned);
443                scope_setter.set_const("Fight", active_fight_cloned);
444                scope_setter.set_const("Random", ScriptRandom::new(rand_gen));
445                scope_setter.set_const("AbilityLevel", ability_level);
446                scope_setter.set_const("AbilitySlotLevel", ability_slot_level);
447                scope_setter
448            },
449            state_cloned,
450            &ability_template.start_script,
451        ) {
452            Ok(v) => v,
453            Err(err) => {
454                if let Some(e) = state
455                    .active_fight
456                    .as_mut()
457                    .and_then(|af| af.entities.iter_mut().find(|e| e.id == by_entity_id))
458                {
459                    e.actions_queue.push(&ActionWithDeadline {
460                        action: self
461                            .make_start_cast_ability_action(by_entity_id, ability_template_id),
462                        deadline_tick: current_tick + ability_cooldown,
463                    });
464                }
465
466                tracing::error!("Ability start cast script failed with error: {err:?}");
467                return EventHandleResult::fail(state);
468            }
469        };
470
471        let (actions, events) =
472            match StartCastAbilityResult::vec_into_actions_with_deadlines_and_events(
473                &results,
474                state.character_state.character.class,
475                &game_config,
476                ability_template_id,
477                by_entity_id,
478                current_tick,
479            ) {
480                Ok((actions, events)) => (actions, events),
481                Err(e) => {
482                    tracing::error!(
483                        "Error converting StartCastAbilityResultVec = {:?} into EntityActionVec = {:?}",
484                        results,
485                        e
486                    );
487                    return EventHandleResult::fail(state);
488                }
489            };
490
491        casted_by_entity
492            .actions_queue
493            .append_start_cast_ability_result_actions(
494                &actions,
495                current_tick,
496                ability_template_id,
497                ability_cooldown,
498            );
499        if casted_by_entity.id == active_fight.player_id && !actions.is_empty() {
500            active_ability.deadline = Some(
501                ::time::utc_now()
502                    + chrono::TimeDelta::milliseconds(
503                        (ability_cooldown as u128 * TICKER_UNIT_DURATION_MS) as i64,
504                    ),
505            );
506        }
507
508        EventHandleResult::ok_events(state, events)
509    }
510
511    #[allow(clippy::too_many_arguments)]
512    pub fn handle_cast_ability(
513        &mut self,
514        event: OverlordEvent,
515        by_entity_id: Uuid,
516        to_entity_id: Uuid,
517        ability_id: AbilityId,
518        rand_gen: rand::rngs::StdRng,
519        current_tick: u64,
520        mut state: OverlordState,
521    ) -> EventHandleResult<OverlordEvent, OverlordState> {
522        let game_config = self.game_config.get();
523        let state_cloned = state.clone();
524
525        let Some(active_fight) = &mut state.active_fight else {
526            return EventHandleResult::ok(state);
527        };
528
529        let Some(target_entity) = active_fight
530            .entities
531            .iter()
532            .find(|e| e.id == to_entity_id)
533            .cloned()
534        else {
535            tracing::debug!("Couldn't find target entity_id = {to_entity_id}");
536            return EventHandleResult::fail(state);
537        };
538
539        let active_fight_clone = active_fight.clone();
540
541        let Some(casted_by_entity) = active_fight
542            .entities
543            .iter_mut()
544            .find(|e| e.id == by_entity_id)
545        else {
546            tracing::debug!("Couldn't find caster entity_id = {by_entity_id}");
547            return EventHandleResult::fail(state);
548        };
549
550        let Some(active_ability) = casted_by_entity
551            .abilities
552            .iter()
553            .find(|equipped_ability| equipped_ability.ability.template_id == ability_id)
554            .cloned()
555        else {
556            tracing::error!(
557                "Couldn't find ability_id = {} in caster entity {:?}",
558                ability_id,
559                casted_by_entity
560            );
561            return EventHandleResult::fail(state);
562        };
563        let ability = active_ability.ability;
564        let ability_slot_level =
565            self.compute_ability_slot_level(active_ability.slot_id, &state_cloned);
566
567        let player_id = active_fight.player_id;
568
569        let Some(ability_template) = game_config.ability_template(ability.template_id).cloned()
570        else {
571            tracing::error!(
572                "Couldn't find template for ability_id = {}",
573                ability.template_id
574            );
575            return EventHandleResult::fail(state);
576        };
577
578        match self.script_runner.run_event(
579            |mut scope_setter| {
580                scope_setter.set_event(OverlordEventCastAbility::from_enum(event));
581                scope_setter.set_const("CasterEntity", casted_by_entity.clone());
582                scope_setter.set_const("Fight", active_fight_clone);
583                scope_setter.set_const("TargetEntity", target_entity);
584                scope_setter.set_const("Random", ScriptRandom::new(rand_gen));
585                scope_setter.set_const("AbilityLevel", ability.level);
586                scope_setter.set_const("AbilitySlotLevel", ability_slot_level);
587                scope_setter.set_const("FightDurationTicks", current_tick - self.start_fight_tick);
588                scope_setter
589            },
590            &state_cloned,
591            &ability_template.script,
592        ) {
593            Ok(events) => {
594                // Charge pet ability on player skill use
595                if by_entity_id == player_id {
596                    let game_config = self.game_config.get();
597                    if let Some(charge_rate) = state
598                        .active_fight
599                        .as_ref()
600                        .and_then(|af| af.pet_combat_state.as_ref())
601                        .and_then(|ps| game_config.pet_template(ps.pet_template_id))
602                        .map(|t| t.charge_rate_on_skill_use)
603                    {
604                        self.charge_pet_ability(&mut state, charge_rate, current_tick);
605                    }
606                }
607
608                EventHandleResult::ok_events(
609                    state,
610                    events
611                        .into_iter()
612                        .map(|x| {
613                            if let OverlordEvent::StartCastProjectile { delay, .. } = x {
614                                EventPluginized::delayed(x, delay.max(1))
615                            } else {
616                                EventPluginized::now(x)
617                            }
618                        })
619                        .collect(),
620                )
621            }
622            Err(err) => {
623                tracing::error!("Ability cast script failed with error: {err:?}");
624                EventHandleResult::fail(state)
625            }
626        }
627    }
628
629    #[allow(clippy::too_many_arguments)]
630    pub fn handle_start_cast_projectile(
631        &mut self,
632        event: OverlordEvent,
633        by_entity_id: Uuid,
634        to_entity_id: Uuid,
635        projectile_id: Uuid,
636        level: i64,
637        current_tick: u64,
638        mut state: OverlordState,
639    ) -> EventHandleResult<OverlordEvent, OverlordState> {
640        let game_config = self.game_config.get();
641
642        let Some(active_fight) = &mut state.active_fight else {
643            return EventHandleResult::ok(state);
644        };
645
646        let Some(target_entity) = active_fight
647            .entities
648            .iter()
649            .find(|e| e.id == to_entity_id)
650            .cloned()
651        else {
652            tracing::debug!("Couldn't find entity_id = {}", to_entity_id);
653            return EventHandleResult::fail(state);
654        };
655
656        let Ok(projectile) = game_config.require_projectile(projectile_id) else {
657            tracing::error!("Couldn't find projectile_id = {} in config", projectile_id);
658            return EventHandleResult::fail(state);
659        };
660
661        let active_fight_clone = active_fight.clone();
662
663        let Some(casted_by_entity) = active_fight
664            .entities
665            .iter_mut()
666            .find(|e| e.id == by_entity_id)
667        else {
668            tracing::debug!("Couldn't find caster entity_id = {}", by_entity_id);
669            return EventHandleResult::fail(state);
670        };
671
672        let result = match self.script_runner.run_start_cast_projectile(
673            |mut scope_setter| {
674                scope_setter.set_event(OverlordEventStartCastProjectile::from_enum(event));
675                scope_setter.set_const("CasterEntity", casted_by_entity.clone());
676                scope_setter.set_const("Fight", active_fight_clone);
677                scope_setter.set_const("TargetEntity", target_entity.clone());
678                scope_setter.set_const("ProjectileLevel", level);
679                scope_setter.set_const("FightDurationTicks", current_tick - self.start_fight_tick);
680                scope_setter
681            },
682            &projectile.start_script,
683        ) {
684            Ok(v) => v,
685            Err(err) => {
686                tracing::error!("Projectile start cast script failed with error: {err:?}");
687                return EventHandleResult::fail(state);
688            }
689        };
690
691        EventHandleResult::ok_events(
692            state,
693            vec![
694                EventPluginized::now(OverlordEvent::StartedCastProjectile {
695                    by_entity_id,
696                    to_entity_id,
697                    projectile_id,
698                    duration_ticks: result.animation_duration_ticks as u64,
699                }),
700                EventPluginized::delayed(
701                    OverlordEvent::CastProjectile {
702                        by_entity_id,
703                        to_entity_id,
704                        projectile_id,
705                        level,
706                        projectile_data: result.projectile_data,
707                    },
708                    result.animation_duration_ticks as u64,
709                ),
710            ],
711        )
712    }
713
714    #[allow(clippy::too_many_arguments)]
715    pub fn handle_cast_projectile(
716        &mut self,
717        event: OverlordEvent,
718        by_entity_id: Uuid,
719        to_entity_id: Uuid,
720        projectile_id: Uuid,
721        level: i64,
722        projectile_data: &CustomEventData,
723        rand_gen: rand::rngs::StdRng,
724        current_tick: u64,
725        state: OverlordState,
726    ) -> EventHandleResult<OverlordEvent, OverlordState> {
727        let game_config = self.game_config.get();
728
729        let Some(active_fight) = &state.active_fight else {
730            return EventHandleResult::ok(state);
731        };
732
733        let Some(casted_by_entity) = active_fight.entities.iter().find(|e| e.id == by_entity_id)
734        else {
735            tracing::debug!("Couldn't find caster entity_id = {}", by_entity_id);
736            return EventHandleResult::fail(state);
737        };
738
739        let Some(target_entity) = active_fight
740            .entities
741            .iter()
742            .find(|e| e.id == to_entity_id)
743            .cloned()
744        else {
745            tracing::debug!("Couldn't find target entity_id = {}", to_entity_id);
746            return EventHandleResult::fail(state);
747        };
748
749        let Ok(projectile) = game_config.require_projectile(projectile_id) else {
750            tracing::error!("Couldn't find projectile_id = {} in config", projectile_id);
751            return EventHandleResult::fail(state);
752        };
753
754        match self.script_runner.run_event(
755            |mut scope_setter| {
756                scope_setter.set_event(OverlordEventCastProjectile::from_enum(event));
757                scope_setter.set_const("Random", ScriptRandom::new(rand_gen));
758                scope_setter.set_const("CasterEntity", casted_by_entity.clone());
759                scope_setter.set_const("Fight", active_fight.clone());
760                scope_setter.set_const("TargetEntity", target_entity);
761                scope_setter.set_const("ProjectileLevel", level);
762                scope_setter.set_const("CustomEventData", projectile_data.clone());
763                scope_setter.set_const("FightDurationTicks", current_tick - self.start_fight_tick);
764                scope_setter
765            },
766            &state,
767            &projectile.script,
768        ) {
769            Ok(events) => EventHandleResult::ok_events(
770                state,
771                events
772                    .into_iter()
773                    .map(|x| {
774                        if let OverlordEvent::StartCastProjectile { delay, .. } = x {
775                            EventPluginized::delayed(x, delay.max(1))
776                        } else {
777                            EventPluginized::now(x)
778                        }
779                    })
780                    .collect(),
781            ),
782            Err(err) => {
783                tracing::error!("Projectile cast script failed with error: {err:?}");
784                EventHandleResult::fail(state)
785            }
786        }
787    }
788
789    pub fn handle_player_death(
790        &self,
791        mut state: OverlordState,
792    ) -> EventHandleResult<OverlordEvent, OverlordState> {
793        let Some(active_fight) = &mut state.active_fight else {
794            return EventHandleResult::ok(state);
795        };
796
797        let mut events = Vec::new();
798
799        active_fight.entities = Vec::new();
800
801        let fight_uuid = active_fight.id;
802        active_fight.fight_ended = true;
803
804        events.push(EventPluginized::delayed(
805            OverlordEvent::EndFight {
806                fight_id: fight_uuid,
807                is_win: false,
808                pvp_state: state.pvp_state.clone(),
809            },
810            self.get_end_fight_delay(active_fight.fight_id),
811        ));
812
813        EventHandleResult::ok_events(state, events)
814    }
815
816    pub fn handle_entity_death(
817        &self,
818        entity_id: Uuid,
819        reward: Vec<CurrencyUnit>,
820        rand_gen: rand::rngs::StdRng,
821        mut state: OverlordState,
822    ) -> EventHandleResult<OverlordEvent, OverlordState> {
823        let game_config = self.game_config.get();
824
825        let Some(active_fight) = &mut state.active_fight else {
826            return EventHandleResult::ok(state);
827        };
828
829        let Some(entity_idx) = active_fight
830            .entities
831            .iter()
832            .position(|entity| entity.id == entity_id)
833        else {
834            tracing::error!("Failed to get entity with entity_id={}", entity_id);
835            return EventHandleResult::fail(state);
836        };
837
838        active_fight.entities.swap_remove(entity_idx);
839
840        let mut events = Vec::new();
841
842        events.push(Self::currency_increase(
843            &reward,
844            CurrencySource::EntityDeath,
845        ));
846
847        let Some(active_fight) = &mut state.active_fight else {
848            return EventHandleResult::ok(state);
849        };
850        let Ok(fight) = game_config.require_fight_template(active_fight.fight_id) else {
851            tracing::error!(
852                "Failed to get fight_template with id {} ",
853                active_fight.fight_id
854            );
855            return EventHandleResult::fail(state);
856        };
857
858        let has_any_ally = active_fight
859            .entities
860            .iter()
861            .any(|e| e.team == EntityTeam::Ally);
862
863        if active_fight.get_enemies_amount() == 0 && has_any_ally {
864            if active_fight.current_wave == fight.waves_amount {
865                let fight_uuid = active_fight.id;
866                active_fight.fight_ended = true;
867                events.reserve(2);
868                events.push(EventPluginized::delayed(
869                    OverlordEvent::EndFight {
870                        fight_id: fight_uuid,
871                        is_win: true,
872                        pvp_state: state.pvp_state.clone(),
873                    },
874                    self.get_end_fight_delay(active_fight.fight_id),
875                ));
876                if fight.fight_type == FightType::CampaignBossFight {
877                    events.push(EventPluginized::now(OverlordEvent::StageCleared {}));
878                }
879            } else {
880                let fight_uuid = active_fight.id;
881                active_fight.current_wave += 1;
882                let active_fight_cloned = active_fight.clone();
883                let current_chapter = state.character_state.character.current_chapter_level;
884                let prepare_fight_events = match self.script_runner.run_event(
885                    |mut scope_setter| {
886                        scope_setter.set_const("Random", ScriptRandom::new(rand_gen));
887                        scope_setter.set_const("Fight", active_fight_cloned);
888                        scope_setter.set_const("CurrentChapter", current_chapter);
889                        scope_setter
890                    },
891                    &state,
892                    &fight.prepare_fight_script,
893                ) {
894                    Ok(events) => events,
895                    Err(err) => {
896                        tracing::error!(
897                            "Prepare wave script for new wave failed with error: {err:?}"
898                        );
899                        events.push(EventPluginized::now(OverlordEvent::EndFight {
900                            fight_id: fight_uuid,
901                            is_win: false,
902                            pvp_state: state.pvp_state.clone(),
903                        }));
904                        return EventHandleResult::ok_events(state, events);
905                    }
906                };
907
908                if prepare_fight_events.is_empty() {
909                    tracing::error!("Prepare wave script returned no events");
910                    events.push(EventPluginized::now(OverlordEvent::EndFight {
911                        fight_id: fight_uuid,
912                        is_win: false,
913                        pvp_state: state.pvp_state.clone(),
914                    }));
915                    return EventHandleResult::ok_events(state, events);
916                }
917
918                if !prepare_fight_events
919                    .iter()
920                    .any(|ev| matches!(ev, OverlordEvent::SpawnEntity { .. }))
921                {
922                    tracing::error!("Prepare wave script returned no SpawnEntity events");
923                    events.push(EventPluginized::now(OverlordEvent::EndFight {
924                        fight_id: fight_uuid,
925                        is_win: false,
926                        pvp_state: state.pvp_state.clone(),
927                    }));
928                    return EventHandleResult::ok_events(state, events);
929                }
930
931                events.append(
932                    &mut prepare_fight_events
933                        .into_iter()
934                        .map(EventPluginized::now)
935                        .collect(),
936                );
937
938                events.push(EventPluginized::now(OverlordEvent::WaveCleared {}));
939            }
940        }
941
942        EventHandleResult::ok_events(state, events)
943    }
944
945    pub fn handle_heal(
946        &self,
947        entity_id: Uuid,
948        heal: u64,
949        mut state: OverlordState,
950    ) -> EventHandleResult<OverlordEvent, OverlordState> {
951        let Some(active_fight) = &mut state.active_fight else {
952            return EventHandleResult::ok(state);
953        };
954
955        let Some(healed_entity) = active_fight
956            .entities
957            .iter_mut()
958            .find(|entity| entity.id == entity_id)
959        else {
960            tracing::error!("Failed to get entity with entity_id={}", entity_id);
961            return EventHandleResult::fail(state);
962        };
963
964        healed_entity.hp = healed_entity
965            .hp
966            .saturating_add(heal)
967            .min(healed_entity.max_hp);
968
969        EventHandleResult::ok(state)
970    }
971
972    pub fn handle_damage(
973        &self,
974        entity_id: Uuid,
975        damage: u64,
976        current_tick: u64,
977        mut rand_gen: rand::rngs::StdRng,
978        mut state: OverlordState,
979    ) -> EventHandleResult<OverlordEvent, OverlordState> {
980        let Some(active_fight) = &mut state.active_fight else {
981            return EventHandleResult::ok(state);
982        };
983
984        let player_id = active_fight.player_id;
985
986        let Some(damaged_entity) = active_fight
987            .entities
988            .iter_mut()
989            .find(|entity| entity.id == entity_id)
990        else {
991            tracing::error!("Failed to get entity with entity_id={}", entity_id);
992            return EventHandleResult::fail(state);
993        };
994
995        let new_hp = damaged_entity.hp - damage.min(damaged_entity.hp);
996        damaged_entity.hp = new_hp;
997
998        // Charge pet only when the player is involved in the damage
999        let is_player_taking_damage = entity_id == player_id;
1000        let is_player_dealing_damage = !is_player_taking_damage
1001            && state
1002                .active_fight
1003                .as_ref()
1004                .and_then(|af| af.entities.iter().find(|e| e.id == entity_id))
1005                .is_some_and(|e| e.team == EntityTeam::Enemy);
1006        let game_config = self.game_config.get();
1007        let charge_rate = if is_player_taking_damage || is_player_dealing_damage {
1008            state
1009                .active_fight
1010                .as_ref()
1011                .and_then(|af| af.pet_combat_state.as_ref())
1012                .and_then(|ps| game_config.pet_template(ps.pet_template_id))
1013                .map(|t| {
1014                    if is_player_taking_damage {
1015                        t.charge_rate_on_damage_taken
1016                    } else {
1017                        t.charge_rate_on_damage_dealt
1018                    }
1019                })
1020        } else {
1021            None
1022        };
1023
1024        if let Some(rate) = charge_rate
1025            && rate > 0
1026        {
1027            self.charge_pet_ability(&mut state, rate, current_tick);
1028        }
1029
1030        if new_hp > 0 {
1031            return EventHandleResult::ok(state);
1032        }
1033
1034        let Some(active_fight) = &mut state.active_fight else {
1035            return EventHandleResult::ok(state);
1036        };
1037
1038        let Some(damaged_entity) = active_fight
1039            .entities
1040            .iter_mut()
1041            .find(|entity| entity.id == entity_id)
1042        else {
1043            return EventHandleResult::fail(state);
1044        };
1045
1046        // Capture values before dropping the mutable borrow
1047        let damaged_id = damaged_entity.id;
1048        let damaged_team = damaged_entity.team.clone();
1049        let damaged_rewards = damaged_entity.rewards.clone();
1050
1051        let has_remaining_allies = active_fight
1052            .entities
1053            .iter()
1054            .any(|e| e.team == EntityTeam::Ally && e.id != damaged_id);
1055
1056        let mut events = Vec::new();
1057
1058        if damaged_team == EntityTeam::Enemy {
1059            // Enemy death — compute and drop rewards
1060            let mut currencies = Vec::new();
1061
1062            if state.pvp_state.is_none() {
1063                if let Some(rewards) = damaged_rewards {
1064                    for reward in rewards {
1065                        if rand_gen.random_range(0.0..100.0) < reward.drop_chance.clamp(0.0, 100.0)
1066                        {
1067                            let amount = if reward.from <= reward.to {
1068                                rand_gen.random_range(reward.from..=reward.to)
1069                            } else {
1070                                tracing::error!(
1071                                    "Entity {} has a bad reward range: {:?}",
1072                                    damaged_id,
1073                                    reward
1074                                );
1075                                0
1076                            };
1077                            currencies.push(CurrencyUnit {
1078                                currency_id: reward.currency_id,
1079                                amount,
1080                            });
1081                        }
1082                    }
1083                } else {
1084                    tracing::error!(
1085                        "Failed to get reward from damaged_entity with entity_id={}",
1086                        entity_id
1087                    );
1088                };
1089            }
1090
1091            events.push(EventPluginized::now(OverlordEvent::EntityDeath {
1092                entity_id: damaged_id,
1093                reward: currencies,
1094            }));
1095        } else if has_remaining_allies {
1096            // An ally dies but other allies survive — remove from fight, continue
1097            events.push(EventPluginized::now(OverlordEvent::EntityDeath {
1098                entity_id: damaged_id,
1099                reward: Vec::new(),
1100            }));
1101        } else {
1102            // Last ally dies — fight lost
1103            events.push(EventPluginized::now(OverlordEvent::PlayerDeath {}));
1104        }
1105
1106        EventHandleResult::ok_events(state, events)
1107    }
1108
1109    #[allow(dead_code)]
1110    fn get_entity_index_with_closest_start_cast(&self, entities: &[Entity]) -> usize {
1111        entities
1112            .iter()
1113            .enumerate()
1114            .filter_map(|(index, entity)| {
1115                entity
1116                    .actions_queue
1117                    .get_closest_start_cast_action_deadline()
1118                    .map(|deadline| (index, deadline))
1119            })
1120            .min_by_key(|&(_, deadline)| deadline)
1121            .map(|(index, _)| index)
1122            .unwrap_or(0)
1123    }
1124
1125    pub fn handle_fight_progress(
1126        &self,
1127        current_tick: u64,
1128        mut state: OverlordState,
1129    ) -> EventHandleResult<OverlordEvent, OverlordState> {
1130        let Some(active_fight) = &mut state.active_fight else {
1131            tracing::error!("No active_fight for fight_progress");
1132            return EventHandleResult::ok(state);
1133        };
1134
1135        if active_fight.fight_ended {
1136            return EventHandleResult::ok(state);
1137        }
1138
1139        if active_fight.fight_stopped {
1140            return EventHandleResult::ok(state);
1141        }
1142
1143        if active_fight.entities.is_empty() {
1144            tracing::error!("No entities in fight");
1145            return EventHandleResult::ok(state);
1146        }
1147
1148        if current_tick - self.start_fight_tick >= active_fight.max_duration_ticks {
1149            active_fight.fight_ended = true;
1150            tracing::debug!("Fight lasted too long, ending it");
1151            let fight_uuid = active_fight.id;
1152            let fight_id = active_fight.fight_id;
1153            let end_fight_delay = self.get_end_fight_delay(fight_id);
1154            let pvp_state = state.pvp_state.clone();
1155            return EventHandleResult::ok_events(
1156                state,
1157                vec![EventPluginized::delayed(
1158                    OverlordEvent::EndFight {
1159                        fight_id: fight_uuid,
1160                        is_win: false,
1161                        pvp_state,
1162                    },
1163                    end_fight_delay,
1164                )],
1165            );
1166        }
1167
1168        let mut events = vec![];
1169
1170        for entity in &mut active_fight.entities {
1171            if !entity.being_moved
1172                && let Some(action) = entity.actions_queue.pop(current_tick)
1173            {
1174                events.push(event_from_entity_action(action, entity.id));
1175            }
1176        }
1177
1178        EventHandleResult::ok_events(state, events)
1179    }
1180
1181    pub fn handle_set_max_hp(
1182        &mut self,
1183        entity_id: EntityId,
1184        new_max_hp: u64,
1185        new_hp: u64,
1186        mut state: OverlordState,
1187    ) -> EventHandleResult<OverlordEvent, OverlordState> {
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        let Some(entity) = active_fight
1194            .entities
1195            .iter_mut()
1196            .find(|entity| entity.id == entity_id)
1197        else {
1198            tracing::error!("Failed to get entity with entity_id={}", entity_id);
1199            return EventHandleResult::fail(state);
1200        };
1201
1202        entity.max_hp = new_max_hp;
1203        entity.hp = new_hp.min(new_max_hp);
1204
1205        EventHandleResult::ok(state)
1206    }
1207}