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 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 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 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 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 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 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 events.push(EventPluginized::now(OverlordEvent::EntityDeath {
1098 entity_id: damaged_id,
1099 reward: Vec::new(),
1100 }));
1101 } else {
1102 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}