1use crate::{
2 TICKER_UNIT_DURATION_MS,
3 behaviors::combat::start_cast::StartCastAbilityResult,
4 entities::{create_pve_entity, event_from_entity_action},
5 event::CustomEventData,
6 event::OverlordEvent,
7 game_config_helpers::GameConfigLookup,
8 logic::handler::OverlordLogic,
9 state::OverlordState,
10};
11
12use essences::{
13 abilities::{AbilityId, AbilitySlotId},
14 currency::{CurrencySource, CurrencyUnit},
15 entity::{ActionWithDeadline, Coordinates, Entity, EntityAction, EntityAttributes, EntityId},
16 fighting::{EntityTeam, EntityType, FightEntity, FightType},
17 game::EntityTemplateId,
18};
19use event_system::{event::EventPluginized, script::random::GameRng, system::EventHandleResult};
20
21use rand::Rng;
22use uuid::Uuid;
23
24fn boss_reward_chapter_multiplier(
30 growth: Option<f64>,
31 cap: Option<f64>,
32 chapter_level: i64,
33) -> f64 {
34 let Some(growth) = growth else { return 1.0 };
35 if growth <= 1.0 {
36 return 1.0;
37 }
38 let cap = cap.unwrap_or(f64::INFINITY).min(1.0e6);
41 let exp = chapter_level.clamp(0, 1000) as i32;
42 growth.powi(exp).clamp(1.0, cap)
43}
44
45impl OverlordLogic {
46 fn compute_ability_slot_level(
47 &self,
48 slot_id: Option<AbilitySlotId>,
49 state: &OverlordState,
50 ) -> i64 {
51 let Some(slot_id) = slot_id else {
52 return 0;
53 };
54
55 let game_config = self.game_config.get();
56 let slot_level = state
57 .character_state
58 .character
59 .ability_slot_levels
60 .get(slot_id)
61 .copied()
62 .unwrap_or(0)
63 .max(0);
64 game_config
65 .game_settings
66 .ability_gacha
67 .slot_level_bonus_levels
68 .get(slot_level as usize)
69 .copied()
70 .or_else(|| {
71 game_config
72 .game_settings
73 .ability_gacha
74 .slot_level_bonus_levels
75 .last()
76 .copied()
77 })
78 .unwrap_or(0)
79 }
80
81 fn charge_pet_ability(&self, state: &mut OverlordState, charge_delta: i64, current_tick: u64) {
84 let Some(active_fight) = &mut state.active_fight else {
85 return;
86 };
87 let Some(pet_state) = &mut active_fight.pet_combat_state else {
88 return;
89 };
90
91 pet_state.charge = (pet_state.charge + charge_delta).min(pet_state.max_charge);
92
93 if pet_state.charge >= pet_state.max_charge {
94 let ability_id = pet_state.ability_id;
95 let pet_template_id = pet_state.pet_template_id;
96 let player_id = active_fight.player_id;
97
98 pet_state.charge = 0;
99
100 if let Some(player) = active_fight.entities.iter_mut().find(|e| e.id == player_id) {
101 player.actions_queue.push(&ActionWithDeadline {
102 action: EntityAction::StartCastAbility {
103 ability_id,
104 by_entity_id: player_id,
105 pet_id: Some(pet_template_id),
106 },
107 deadline_tick: current_tick,
108 });
109 }
110 }
111 }
112
113 #[allow(clippy::too_many_arguments)]
114 pub fn handle_spawn_entity(
115 &self,
116 id: EntityId,
117 entity_template_id: EntityTemplateId,
118 position: Coordinates,
119 team: EntityTeam,
120 has_big_hp_bar: bool,
121 entity_attributes: EntityAttributes,
122 current_tick: u64,
123 mut state: OverlordState,
124 ) -> EventHandleResult<OverlordEvent, OverlordState> {
125 let game_config = self.game_config.get();
126
127 let Some(active_fight) = &mut state.active_fight else {
128 return EventHandleResult::ok(state);
129 };
130
131 if active_fight.entities.iter().any(|entity| entity.id == id) {
132 tracing::error!("There is already an entity with id: {id}");
133 return EventHandleResult::fail(state);
134 }
135
136 let fight_entity = FightEntity {
137 entity_type: EntityType::PVEEntity { entity_template_id },
138 position,
139 has_big_hp_bar,
140 team,
141 };
142
143 let mut created_entity =
144 match create_pve_entity(id, &fight_entity, &game_config, Some(entity_attributes)) {
145 Ok(entity) => entity,
146 Err(err) => {
147 tracing::error!("Couldn't create entity: {}", err.to_string());
148 return EventHandleResult::fail(state);
149 }
150 };
151
152 if active_fight.current_wave > 1 {
153 created_entity.abilities.iter().for_each(|ability| {
154 let cooldown = game_config
155 .ability_template(ability.ability.template_id)
156 .map(|t| t.cooldown)
157 .unwrap_or(0);
158 created_entity.actions_queue.push(&ActionWithDeadline {
159 action: self.make_start_cast_ability_action(
160 created_entity.id,
161 ability.ability.template_id,
162 ),
163 deadline_tick: current_tick + cooldown,
164 })
165 });
166 }
167
168 active_fight.entities.push(created_entity);
169
170 EventHandleResult::ok(state)
171 }
172 pub fn handle_start_move(
173 &mut self,
174 entity_id: Uuid,
175 to: Coordinates,
176 duration_ticks: u64,
177 mut state: OverlordState,
178 ) -> EventHandleResult<OverlordEvent, OverlordState> {
179 let Some(active_fight) = &mut state.active_fight else {
180 return EventHandleResult::ok(state);
181 };
182
183 let Some(entity) = active_fight
184 .entities
185 .iter_mut()
186 .find(|entity| entity.id == entity_id)
187 else {
188 tracing::error!("Failed to find entity in state with id={}", entity_id);
189 return EventHandleResult::fail(state);
190 };
191
192 entity.move_target = Some(to.clone());
196
197 let steps = move_progress_steps(&entity.coordinates, &to, duration_ticks);
202 if let Some((_, first)) = steps.first() {
203 entity.coordinates = first.clone();
204 }
205 for (delay_ticks, cell) in steps.into_iter().skip(1) {
206 self.fight_clock.schedule(
207 OverlordEvent::MoveProgress {
208 entity_id,
209 to: cell,
210 },
211 delay_ticks,
212 );
213 }
214
215 self.fight_clock
216 .schedule(OverlordEvent::EndMove { entity_id }, duration_ticks);
217
218 EventHandleResult::ok(state)
219 }
220
221 pub fn handle_move_progress(
222 &self,
223 entity_id: Uuid,
224 to: Coordinates,
225 mut state: OverlordState,
226 ) -> EventHandleResult<OverlordEvent, OverlordState> {
227 let Some(active_fight) = &mut state.active_fight else {
228 return EventHandleResult::ok(state);
229 };
230
231 if let Some(entity) = active_fight
234 .entities
235 .iter_mut()
236 .find(|entity| entity.id == entity_id)
237 && entity.move_target.is_some()
238 {
239 entity.coordinates = to;
240 }
241
242 EventHandleResult::ok(state)
243 }
244
245 pub fn handle_end_move(
246 &self,
247 entity_id: Uuid,
248 mut state: OverlordState,
249 ) -> EventHandleResult<OverlordEvent, OverlordState> {
250 let Some(active_fight) = &mut state.active_fight else {
251 return EventHandleResult::ok(state);
252 };
253
254 let Some(entity) = active_fight
255 .entities
256 .iter_mut()
257 .find(|entity| entity.id == entity_id)
258 else {
259 tracing::error!("Failed to find entity in state with id={}", entity_id);
260 return EventHandleResult::fail(state);
261 };
262
263 entity.move_target = None;
265
266 EventHandleResult::ok_events(
267 state,
268 vec![EventPluginized::now(OverlordEvent::FightProgress {})],
269 )
270 }
271
272 pub fn handle_entity_stun(
273 &mut self,
274 entity_id: Uuid,
275 duration_ticks: u64,
276 current_tick: u64,
277 mut state: OverlordState,
278 ) -> EventHandleResult<OverlordEvent, OverlordState> {
279 let game_config = self.game_config.get();
280 let baseline_speed = game_config.game_settings.baseline_speed;
281 let player_id = state.active_fight.as_ref().map(|f| f.player_id);
282
283 let Some(active_fight) = &mut state.active_fight else {
284 tracing::error!("EntityStun received with no active fight (entity_id = {entity_id})");
285 return EventHandleResult::fail(state);
286 };
287
288 let Some(entity) = active_fight.entities.iter_mut().find(|e| e.id == entity_id) else {
289 tracing::error!("EntityStun: entity_id = {entity_id} not found in active fight");
290 return EventHandleResult::fail(state);
291 };
292
293 let entity_speed = entity.attributes.speed_or_baseline(baseline_speed);
294 let ability_ids: Vec<AbilityId> = entity
295 .abilities
296 .iter()
297 .map(|aa| aa.ability.template_id)
298 .collect();
299
300 for ability_id in ability_ids {
301 let base_cooldown = game_config
302 .ability_template(ability_id)
303 .map(|t| t.cooldown)
304 .unwrap_or(0);
305 let scaled_cooldown = essences::entity::scale_cooldown_for_speed(
306 base_cooldown,
307 entity_speed,
308 baseline_speed,
309 );
310 entity.actions_queue.stun_ability(
311 ability_id,
312 duration_ticks,
313 scaled_cooldown,
314 current_tick,
315 );
316 }
317
318 if Some(entity.id) == player_id {
324 let now = ::time::utc_now();
325 let stun_ms = (duration_ticks as u128 * TICKER_UNIT_DURATION_MS) as i64;
326 for active_ability in entity.abilities.iter_mut() {
327 let base_cooldown = game_config
328 .ability_template(active_ability.ability.template_id)
329 .map(|t| t.cooldown)
330 .unwrap_or(0);
331 let scaled_cooldown = essences::entity::scale_cooldown_for_speed(
332 base_cooldown,
333 entity_speed,
334 baseline_speed,
335 );
336 let cooldown_ms = (scaled_cooldown as u128 * TICKER_UNIT_DURATION_MS) as i64;
337
338 let new_deadline = match active_ability.deadline {
339 Some(existing) if existing > now => {
340 existing + chrono::TimeDelta::milliseconds(stun_ms)
342 }
343 _ => {
344 let total_ms = if base_cooldown > 0 {
350 cooldown_ms + stun_ms
351 } else {
352 stun_ms
353 };
354 now + chrono::TimeDelta::milliseconds(total_ms)
355 }
356 };
357 active_ability.deadline = Some(new_deadline);
358 }
359 }
360
361 EventHandleResult::ok(state)
362 }
363
364 pub fn handle_entity_cancel_cast_with_cooldown(
365 &mut self,
366 entity_id: Uuid,
367 ability_id: Uuid,
368 current_tick: u64,
369 mut state: OverlordState,
370 ) -> EventHandleResult<OverlordEvent, OverlordState> {
371 let game_config = self.game_config.get();
372
373 let Some(active_fight) = &mut state.active_fight else {
374 tracing::error!(
375 "EntityCancelCastWithCooldown received with no active fight (entity_id = {entity_id})"
376 );
377 return EventHandleResult::fail(state);
378 };
379
380 let Some(entity) = active_fight.entities.iter_mut().find(|e| e.id == entity_id) else {
381 tracing::error!(
382 "EntityCancelCastWithCooldown: entity_id = {entity_id} not found in active fight"
383 );
384 return EventHandleResult::fail(state);
385 };
386
387 let Some(ability_template) = game_config.ability_template(ability_id) else {
388 tracing::error!(
389 "EntityCancelCastWithCooldown: ability_template not found for ability_id = {ability_id}"
390 );
391 return EventHandleResult::fail(state);
392 };
393 let cooldown = ability_template.cooldown;
394
395 let cancelled = entity
396 .actions_queue
397 .cancel_cast_and_set_cooldown(ability_id, current_tick + cooldown);
398
399 if !cancelled {
400 tracing::error!(
401 "EntityCancelCastWithCooldown: no in-flight cast for ability_id = {ability_id} on entity {entity_id}"
402 );
403 return EventHandleResult::fail(state);
404 }
405
406 EventHandleResult::ok(state)
407 }
408
409 pub fn handle_entity_add_ability_cooldown(
410 &mut self,
411 entity_id: Uuid,
412 ability_id: Uuid,
413 delta_ticks: i64,
414 current_tick: u64,
415 mut state: OverlordState,
416 ) -> EventHandleResult<OverlordEvent, OverlordState> {
417 let Some(active_fight) = &mut state.active_fight else {
418 tracing::error!(
419 "EntityAddAbilityCooldown received with no active fight (entity_id = {entity_id})"
420 );
421 return EventHandleResult::fail(state);
422 };
423
424 let Some(entity) = active_fight.entities.iter_mut().find(|e| e.id == entity_id) else {
425 tracing::error!(
426 "EntityAddAbilityCooldown: entity_id = {entity_id} not found in active fight"
427 );
428 return EventHandleResult::fail(state);
429 };
430
431 if !entity
432 .actions_queue
433 .adjust_ability_cooldown(ability_id, delta_ticks, current_tick)
434 {
435 tracing::error!(
436 "EntityAddAbilityCooldown: ability_id = {ability_id} not in cooldown queue for entity {entity_id}"
437 );
438 return EventHandleResult::fail(state);
439 }
440
441 EventHandleResult::ok(state)
442 }
443
444 pub fn handle_entity_incr_attribute(
445 &mut self,
446 entity_id: Uuid,
447 attribute: &str,
448 delta: i64,
449 current_tick: u64,
450 mut state: OverlordState,
451 ) -> EventHandleResult<OverlordEvent, OverlordState> {
452 let game_config = self.game_config.get();
453 let baseline_speed = game_config.game_settings.baseline_speed;
454 let player_id = state.active_fight.as_ref().map(|f| f.player_id);
455
456 let Some(active_fight) = &mut state.active_fight else {
457 return EventHandleResult::ok(state);
458 };
459
460 let Some(entity) = active_fight.entities.iter_mut().find(|e| e.id == entity_id) else {
461 tracing::debug!("Couldn't find entity_id = {}", entity_id);
462 return EventHandleResult::fail(state);
463 };
464
465 let old_speed = entity.attributes.speed_or_baseline(baseline_speed);
466 entity.attributes.add(attribute, delta);
467 let new_speed = entity.attributes.speed_or_baseline(baseline_speed);
468
469 if attribute == "speed" && old_speed != new_speed {
470 entity.actions_queue.rescale_cooldowns(
471 old_speed,
472 new_speed,
473 current_tick,
474 baseline_speed,
475 );
476
477 if Some(entity.id) == player_id {
478 let now = ::time::utc_now();
479 let baseline = baseline_speed.max(1) as i128;
480 let old_s = if old_speed <= 0 {
481 baseline
482 } else {
483 old_speed as i128
484 };
485 let new_s = if new_speed <= 0 {
486 baseline
487 } else {
488 new_speed as i128
489 };
490 for active_ability in entity.abilities.iter_mut() {
491 let Some(deadline) = active_ability.deadline else {
492 continue;
493 };
494 let remaining_ms = (deadline - now).num_milliseconds();
495 if remaining_ms <= 0 {
496 continue;
497 }
498 let scaled_ms = ((remaining_ms as i128) * old_s / new_s) as i64;
499 let scaled_ms = scaled_ms.max(1);
500 active_ability.deadline =
501 Some(now + chrono::TimeDelta::milliseconds(scaled_ms));
502 }
503 }
504 }
505
506 entity.effect_ids = entity
507 .effect_ids
508 .iter()
509 .filter(|effect_id| {
510 if let Some(effect) = game_config.effect(**effect_id) {
511 if effect.has_at_least_one_required_attribute(&entity.attributes) {
514 true
515 } else {
516 if effect.interval_ticks.is_some() {
517 entity.actions_queue.remove_cast_effect_action(effect.id);
518 }
519 false
520 }
521 } else {
522 false
523 }
524 })
525 .cloned()
526 .collect();
527
528 EventHandleResult::ok(state)
529 }
530
531 pub fn handle_entity_apply_effect(
532 &mut self,
533 entity_id: Uuid,
534 effect_id: Uuid,
535 current_tick: u64,
536 mut state: OverlordState,
537 ) -> EventHandleResult<OverlordEvent, OverlordState> {
538 let Some(active_fight) = &mut state.active_fight else {
539 return EventHandleResult::ok(state);
540 };
541
542 let game_config = self.game_config.get();
543
544 let Some(entity) = active_fight.entities.iter_mut().find(|e| e.id == entity_id) else {
545 tracing::debug!("Couldn't find entity_id = {}", entity_id);
546 return EventHandleResult::fail(state);
547 };
548
549 let Ok(effect) = game_config.require_effect(effect_id) else {
550 tracing::debug!("Couldn't find effect_id = {}", effect_id);
551 return EventHandleResult::fail(state);
552 };
553
554 if let Some(required_attributes) = &effect.required_attributes
555 && !required_attributes
556 .iter()
557 .any(|attr| entity.attributes.0.contains_key(attr))
558 {
559 tracing::error!("Effect has required attributes, but they are not set");
560 return EventHandleResult::fail(state);
561 }
562
563 for existing_effect_id in &entity.effect_ids {
564 if *existing_effect_id == effect.id {
565 tracing::error!("Effect is already set on entity");
566 return EventHandleResult::fail(state);
567 }
568 }
569
570 entity.effect_ids.push(effect.id);
571
572 if let Some(interval_ticks) = &effect.interval_ticks {
573 entity.actions_queue.push(&ActionWithDeadline {
574 action: self.make_cast_effect_action(entity_id, effect.id),
575 deadline_tick: current_tick + interval_ticks,
576 });
577 }
578
579 EventHandleResult::ok(state)
580 }
581
582 #[allow(clippy::too_many_arguments)]
583 pub fn handle_cast_effect(
584 &mut self,
585 entity_id: Uuid,
586 effect_id: Uuid,
587 caller_event: Option<Box<OverlordEvent>>,
588 rand_gen: rand::rngs::StdRng,
589 current_tick: u64,
590 mut state: OverlordState,
591 ) -> EventHandleResult<OverlordEvent, OverlordState> {
592 let Some(active_fight) = &mut state.active_fight else {
593 return EventHandleResult::ok(state);
594 };
595
596 let active_fight_cloned = active_fight.clone();
597
598 let game_config = self.game_config.get();
599
600 let Some(entity) = active_fight.entities.iter_mut().find(|e| e.id == entity_id) else {
601 tracing::debug!("Couldn't find entity_id = {}", entity_id);
602 return EventHandleResult::fail(state);
603 };
604
605 let Ok(effect) = game_config.require_effect(effect_id) else {
606 tracing::debug!("Couldn't find effect_id = {}", effect_id);
607 return EventHandleResult::fail(state);
608 };
609
610 if !entity.effect_ids.contains(&effect_id) {
611 tracing::error!("entity_id = {} has no effect_id = {}", entity_id, effect_id);
612 return EventHandleResult::fail(state);
613 }
614
615 let entity_cloned = entity.clone();
616
617 let Some(native_name) = effect.behavior.as_deref() else {
620 tracing::error!("Effect {} has no script registered", effect_id);
621 return EventHandleResult::fail(state);
622 };
623 let Some(native_fn) = self.behaviors.event_fn(native_name) else {
624 tracing::error!("No native event fn registered for {native_name}");
625 return EventHandleResult::fail(state);
626 };
627
628 let rng = GameRng::new(rand_gen);
629 let events = match native_fn(&crate::behaviors::combat::effects::EventCtx {
630 entity: &entity_cloned,
631 fight: &active_fight_cloned,
632 rng: &rng,
633 current_tick,
634 fight_duration_ticks: current_tick - self.start_fight_tick,
635 caller_event: caller_event.as_deref(),
636 config: &game_config,
637 lookups: self.behaviors.lookups(),
638 }) {
639 Ok(events) => events,
640 Err(err) => {
641 tracing::error!("Effect script failed with error: {err:?}");
642 return EventHandleResult::fail(state);
643 }
644 };
645
646 if let Some(interval_ticks) = effect.interval_ticks {
647 entity.actions_queue.push(&ActionWithDeadline {
648 action: self.make_cast_effect_action(entity_id, effect.id),
649 deadline_tick: current_tick + interval_ticks,
650 });
651 }
652
653 EventHandleResult::ok_events(
654 state,
655 events.into_iter().map(EventPluginized::now).collect(),
656 )
657 }
658
659 pub fn handle_start_cast_ability(
660 &mut self,
661 _event: OverlordEvent,
662 by_entity_id: Uuid,
663 ability_id: AbilityId,
664 rand_gen: rand::rngs::StdRng,
665 current_tick: u64,
666 mut state: OverlordState,
667 ) -> EventHandleResult<OverlordEvent, OverlordState> {
668 let game_config = self.game_config.get();
669
670 let state_cloned = state.clone();
671
672 let Some(active_fight) = &mut state.active_fight else {
673 tracing::error!("No active fight for start_cast_ability");
674 return EventHandleResult::ok(state);
675 };
676 let active_fight_cloned = active_fight.clone();
677 let Some(casted_by_entity) = active_fight
678 .entities
679 .iter_mut()
680 .find(|e| e.id == by_entity_id)
681 else {
682 tracing::debug!("Couldn't find caster entity_id = {}", by_entity_id);
683 return EventHandleResult::fail(state);
684 };
685 let casted_by_entity_cloned = casted_by_entity.clone();
686 let Some(active_ability) = casted_by_entity
687 .abilities
688 .iter_mut()
689 .find(|equipped_ability| equipped_ability.ability.template_id == ability_id)
690 else {
691 tracing::error!(
692 "Couldn't find ability_id = {} in caster entity {:?}",
693 ability_id,
694 casted_by_entity
695 );
696 return EventHandleResult::fail(state);
697 };
698 let ability = &active_ability.ability;
699 let ability_template_id = ability.template_id;
700 let ability_level = ability.level;
701
702 let Some(ability_template) = game_config.ability_template(ability_template_id).cloned()
703 else {
704 tracing::error!(
705 "Couldn't find template for ability_id = {}",
706 ability_template_id
707 );
708 return EventHandleResult::fail(state);
709 };
710 let ability_cooldown = ability_template.cooldown;
711
712 let _ability_slot_level =
713 self.compute_ability_slot_level(active_ability.slot_id, &state_cloned);
714 let _ = (ability_level, &state_cloned);
715
716 let native_result = (|| {
719 let name = ability_template.start_behavior.as_deref()?;
720 let f = self.behaviors.start_cast_ability_fn(name)?;
721 Some(f(
722 &crate::behaviors::combat::start_cast::StartCastAbilityCtx {
723 caster: &casted_by_entity_cloned,
724 fight: &active_fight_cloned,
725 rng: &GameRng::new(rand_gen),
726 ability_template_id,
727 config: &game_config,
728 lookups: self.behaviors.lookups(),
729 },
730 ))
731 })();
732
733 let results = match native_result {
734 Some(Ok(v)) => v,
735 other => {
736 if let Some(e) = state
737 .active_fight
738 .as_mut()
739 .and_then(|af| af.entities.iter_mut().find(|e| e.id == by_entity_id))
740 {
741 let baseline_speed = game_config.game_settings.baseline_speed;
742 let scaled_cooldown = essences::entity::scale_cooldown_for_speed(
743 ability_cooldown,
744 e.attributes.speed_or_baseline(baseline_speed),
745 baseline_speed,
746 );
747 e.actions_queue.push(&ActionWithDeadline {
748 action: self
749 .make_start_cast_ability_action(by_entity_id, ability_template_id),
750 deadline_tick: current_tick + scaled_cooldown,
751 });
752 }
753
754 match other {
755 Some(Err(err)) => {
756 tracing::error!("Ability start cast script failed with error: {err:?}")
757 }
758 _ => tracing::error!(
759 "Ability {ability_template_id} has no start_behavior registered"
760 ),
761 }
762 return EventHandleResult::fail(state);
763 }
764 };
765
766 let (actions, events) =
767 match StartCastAbilityResult::vec_into_actions_with_deadlines_and_events(
768 &results,
769 state.character_state.character.class,
770 &game_config,
771 ability_template_id,
772 by_entity_id,
773 current_tick,
774 ) {
775 Ok((actions, events)) => (actions, events),
776 Err(err) => {
777 tracing::error!(
778 "Error converting StartCastAbilityResultVec = {:?} into EntityActionVec = {:?}",
779 results,
780 err
781 );
782 if let Some(entity) = state
788 .active_fight
789 .as_mut()
790 .and_then(|af| af.entities.iter_mut().find(|e| e.id == by_entity_id))
791 {
792 let baseline_speed = game_config.game_settings.baseline_speed;
793 let scaled_cooldown = essences::entity::scale_cooldown_for_speed(
794 ability_cooldown,
795 entity.attributes.speed_or_baseline(baseline_speed),
796 baseline_speed,
797 );
798 entity.actions_queue.push(&ActionWithDeadline {
799 action: self
800 .make_start_cast_ability_action(by_entity_id, ability_template_id),
801 deadline_tick: current_tick + scaled_cooldown,
802 });
803 }
804 return EventHandleResult::fail(state);
805 }
806 };
807
808 let baseline_speed = game_config.game_settings.baseline_speed;
809 let scaled_cooldown = essences::entity::scale_cooldown_for_speed(
810 ability_cooldown,
811 casted_by_entity
812 .attributes
813 .speed_or_baseline(baseline_speed),
814 baseline_speed,
815 );
816 casted_by_entity
817 .actions_queue
818 .append_start_cast_ability_result_actions(
819 &actions,
820 current_tick,
821 ability_template_id,
822 scaled_cooldown,
823 );
824 if casted_by_entity.id == active_fight.player_id && !actions.is_empty() {
825 active_ability.deadline = Some(
826 ::time::utc_now()
827 + chrono::TimeDelta::milliseconds(
828 (scaled_cooldown as u128 * TICKER_UNIT_DURATION_MS) as i64,
829 ),
830 );
831 }
832
833 let now_events = self.route_delayed_to_clock(events);
834
835 EventHandleResult::ok_events(state, now_events)
836 }
837
838 fn route_delayed_to_clock(
841 &mut self,
842 events: Vec<EventPluginized<OverlordEvent, OverlordState>>,
843 ) -> Vec<EventPluginized<OverlordEvent, OverlordState>> {
844 events
845 .into_iter()
846 .filter_map(|pluginized| {
847 let (event, delayed, _cron) = pluginized.into_parts();
848 if let Some(delayed) = delayed {
849 self.fight_clock.schedule(event, delayed.ticks);
850 None
851 } else {
852 Some(EventPluginized::now(event))
853 }
854 })
855 .collect()
856 }
857
858 fn route_projectiles_to_clock(
862 &mut self,
863 events: Vec<OverlordEvent>,
864 ) -> Vec<EventPluginized<OverlordEvent, OverlordState>> {
865 events
866 .into_iter()
867 .filter_map(|x| {
868 if let OverlordEvent::StartCastProjectile { delay, .. } = x {
869 let delay = delay.max(1);
870 self.fight_clock.schedule(x, delay);
871 None
872 } else {
873 Some(EventPluginized::now(x))
874 }
875 })
876 .collect()
877 }
878
879 #[allow(clippy::too_many_arguments)]
880 pub fn handle_cast_ability(
881 &mut self,
882 _event: OverlordEvent,
883 by_entity_id: Uuid,
884 to_entity_id: Uuid,
885 ability_id: AbilityId,
886 rand_gen: rand::rngs::StdRng,
887 current_tick: u64,
888 mut state: OverlordState,
889 ) -> EventHandleResult<OverlordEvent, OverlordState> {
890 let game_config = self.game_config.get();
891 let state_cloned = state.clone();
892
893 let Some(active_fight) = &mut state.active_fight else {
894 return EventHandleResult::ok(state);
895 };
896
897 let Some(target_entity) = active_fight
898 .entities
899 .iter()
900 .find(|e| e.id == to_entity_id)
901 .cloned()
902 else {
903 tracing::debug!("Couldn't find target entity_id = {to_entity_id}");
904 return EventHandleResult::fail(state);
905 };
906
907 let active_fight_clone = active_fight.clone();
908
909 let Some(casted_by_entity) = active_fight
910 .entities
911 .iter_mut()
912 .find(|e| e.id == by_entity_id)
913 else {
914 tracing::debug!("Couldn't find caster entity_id = {by_entity_id}");
915 return EventHandleResult::fail(state);
916 };
917
918 let Some(active_ability) = casted_by_entity
919 .abilities
920 .iter()
921 .find(|equipped_ability| equipped_ability.ability.template_id == ability_id)
922 .cloned()
923 else {
924 tracing::error!(
925 "Couldn't find ability_id = {} in caster entity {:?}",
926 ability_id,
927 casted_by_entity
928 );
929 return EventHandleResult::fail(state);
930 };
931 let ability = active_ability.ability;
932 let ability_slot_level =
933 self.compute_ability_slot_level(active_ability.slot_id, &state_cloned);
934
935 let player_id = active_fight.player_id;
936
937 let Some(ability_template) = game_config.ability_template(ability.template_id).cloned()
938 else {
939 tracing::error!(
940 "Couldn't find template for ability_id = {}",
941 ability.template_id
942 );
943 return EventHandleResult::fail(state);
944 };
945
946 let _ = (&state_cloned, ability_slot_level);
947
948 let native_result = (|| {
951 let name = ability_template.behavior.as_deref()?;
952 let f = self.behaviors.cast_ability_fn(name)?;
953 Some(f(&crate::behaviors::combat::cast_ability::CastAbilityCtx {
954 caster_entity: casted_by_entity,
955 target_entity: &target_entity,
956 fight: &active_fight_clone,
957 rng: &GameRng::new(rand_gen),
958 ability_level: ability.level,
959 config: &game_config,
960 lookups: self.behaviors.lookups(),
961 }))
962 })();
963
964 match native_result {
965 Some(Ok(events)) => {
966 if by_entity_id == player_id {
968 let game_config = self.game_config.get();
969 if let Some(charge_rate) = state
970 .active_fight
971 .as_ref()
972 .and_then(|af| af.pet_combat_state.as_ref())
973 .and_then(|ps| game_config.pet_template(ps.pet_template_id))
974 .map(|t| t.charge_rate_on_skill_use)
975 {
976 self.charge_pet_ability(&mut state, charge_rate, current_tick);
977 }
978 }
979
980 let now_events = self.route_projectiles_to_clock(events);
981 EventHandleResult::ok_events(state, now_events)
982 }
983 Some(Err(err)) => {
984 tracing::error!("Ability cast script failed with error: {err:?}");
985 EventHandleResult::fail(state)
986 }
987 None => {
988 tracing::error!("Ability {} has no script registered", ability.template_id);
989 EventHandleResult::fail(state)
990 }
991 }
992 }
993
994 #[allow(clippy::too_many_arguments)]
995 pub fn handle_start_cast_projectile(
996 &mut self,
997 _event: OverlordEvent,
998 by_entity_id: Uuid,
999 to_entity_id: Uuid,
1000 projectile_id: Uuid,
1001 level: i64,
1002 current_tick: u64,
1003 mut state: OverlordState,
1004 ) -> EventHandleResult<OverlordEvent, OverlordState> {
1005 let game_config = self.game_config.get();
1006
1007 let Some(active_fight) = &mut state.active_fight else {
1008 return EventHandleResult::ok(state);
1009 };
1010
1011 let Some(target_entity) = active_fight
1012 .entities
1013 .iter()
1014 .find(|e| e.id == to_entity_id)
1015 .cloned()
1016 else {
1017 tracing::debug!("Couldn't find entity_id = {}", to_entity_id);
1018 return EventHandleResult::fail(state);
1019 };
1020
1021 let Ok(projectile) = game_config.require_projectile(projectile_id) else {
1022 tracing::error!("Couldn't find projectile_id = {} in config", projectile_id);
1023 return EventHandleResult::fail(state);
1024 };
1025
1026 let active_fight_clone = active_fight.clone();
1027
1028 let Some(casted_by_entity) = active_fight
1029 .entities
1030 .iter_mut()
1031 .find(|e| e.id == by_entity_id)
1032 else {
1033 tracing::debug!("Couldn't find caster entity_id = {}", by_entity_id);
1034 return EventHandleResult::fail(state);
1035 };
1036
1037 let _ = (&active_fight_clone, current_tick);
1038
1039 let native_result = (|| {
1042 let name = projectile.start_behavior.as_deref()?;
1043 let f = self.behaviors.start_cast_projectile_fn(name)?;
1044 Some(f(
1045 &crate::behaviors::combat::start_cast::StartCastProjectileCtx {
1046 caster_entity: casted_by_entity,
1047 target_entity: &target_entity,
1048 },
1049 ))
1050 })();
1051
1052 let result = match native_result {
1053 Some(Ok(v)) => v,
1054 Some(Err(err)) => {
1055 tracing::error!("Projectile start cast script failed with error: {err:?}");
1056 return EventHandleResult::fail(state);
1057 }
1058 None => {
1059 tracing::error!("Projectile {projectile_id} has no start_behavior registered");
1060 return EventHandleResult::fail(state);
1061 }
1062 };
1063
1064 self.fight_clock.schedule(
1065 OverlordEvent::CastProjectile {
1066 by_entity_id,
1067 to_entity_id,
1068 projectile_id,
1069 level,
1070 projectile_data: result.projectile_data,
1071 },
1072 result.animation_duration_ticks as u64,
1073 );
1074
1075 EventHandleResult::ok_events(
1076 state,
1077 vec![EventPluginized::now(OverlordEvent::StartedCastProjectile {
1078 by_entity_id,
1079 to_entity_id,
1080 projectile_id,
1081 duration_ticks: result.animation_duration_ticks as u64,
1082 })],
1083 )
1084 }
1085
1086 #[allow(clippy::too_many_arguments)]
1087 pub fn handle_cast_projectile(
1088 &mut self,
1089 _event: OverlordEvent,
1090 by_entity_id: Uuid,
1091 to_entity_id: Uuid,
1092 projectile_id: Uuid,
1093 level: i64,
1094 projectile_data: &CustomEventData,
1095 rand_gen: rand::rngs::StdRng,
1096 current_tick: u64,
1097 state: OverlordState,
1098 ) -> EventHandleResult<OverlordEvent, OverlordState> {
1099 let game_config = self.game_config.get();
1100
1101 let Some(active_fight) = &state.active_fight else {
1102 return EventHandleResult::ok(state);
1103 };
1104
1105 let Some(casted_by_entity) = active_fight.entities.iter().find(|e| e.id == by_entity_id)
1106 else {
1107 tracing::debug!("Couldn't find caster entity_id = {}", by_entity_id);
1108 return EventHandleResult::fail(state);
1109 };
1110
1111 let Some(target_entity) = active_fight
1112 .entities
1113 .iter()
1114 .find(|e| e.id == to_entity_id)
1115 .cloned()
1116 else {
1117 tracing::debug!("Couldn't find target entity_id = {}", to_entity_id);
1118 return EventHandleResult::fail(state);
1119 };
1120
1121 let Ok(projectile) = game_config.require_projectile(projectile_id) else {
1122 tracing::error!("Couldn't find projectile_id = {} in config", projectile_id);
1123 return EventHandleResult::fail(state);
1124 };
1125
1126 let _ = (projectile_data, current_tick);
1127
1128 let native_result = (|| {
1131 let name = projectile.behavior.as_deref()?;
1132 let f = self.behaviors.cast_projectile_fn(name)?;
1133 Some(f(
1134 &crate::behaviors::combat::cast_projectile::CastProjectileCtx {
1135 caster_entity: casted_by_entity,
1136 target_entity: &target_entity,
1137 fight: active_fight,
1138 rng: &GameRng::new(rand_gen),
1139 projectile_level: level,
1140 config: &game_config,
1141 lookups: self.behaviors.lookups(),
1142 },
1143 ))
1144 })();
1145
1146 match native_result {
1147 Some(Ok(events)) => {
1148 let now_events = self.route_projectiles_to_clock(events);
1149 EventHandleResult::ok_events(state, now_events)
1150 }
1151 Some(Err(err)) => {
1152 tracing::error!("Projectile cast script failed with error: {err:?}");
1153 EventHandleResult::fail(state)
1154 }
1155 None => {
1156 tracing::error!("Projectile {projectile_id} has no script registered");
1157 EventHandleResult::fail(state)
1158 }
1159 }
1160 }
1161
1162 pub fn handle_player_death(
1163 &mut self,
1164 mut state: OverlordState,
1165 ) -> EventHandleResult<OverlordEvent, OverlordState> {
1166 let Some(active_fight) = &mut state.active_fight else {
1167 return EventHandleResult::ok(state);
1168 };
1169
1170 if active_fight.fight_ended {
1174 return EventHandleResult::ok(state);
1175 }
1176
1177 active_fight.entities = Vec::new();
1178
1179 let fight_uuid = active_fight.id;
1180 active_fight.fight_ended = true;
1181 let end_fight_delay = self.get_end_fight_delay(active_fight.fight_id);
1182
1183 self.fight_clock.schedule(
1184 OverlordEvent::EndFight {
1185 fight_id: fight_uuid,
1186 is_win: false,
1187 pvp_state: state.pvp_state.clone().map(Box::new),
1188 },
1189 end_fight_delay,
1190 );
1191
1192 EventHandleResult::ok(state)
1193 }
1194
1195 pub fn handle_entity_death(
1196 &mut self,
1197 entity_id: Uuid,
1198 reward: Vec<CurrencyUnit>,
1199 rand_gen: rand::rngs::StdRng,
1200 mut state: OverlordState,
1201 ) -> EventHandleResult<OverlordEvent, OverlordState> {
1202 let game_config = self.game_config.get();
1203
1204 let Some(active_fight) = &mut state.active_fight else {
1205 return EventHandleResult::ok(state);
1206 };
1207
1208 if active_fight.fight_ended {
1213 return EventHandleResult::ok(state);
1214 }
1215
1216 let Some(entity_idx) = active_fight
1217 .entities
1218 .iter()
1219 .position(|entity| entity.id == entity_id)
1220 else {
1221 tracing::error!("Failed to get entity with entity_id={}", entity_id);
1222 return EventHandleResult::fail(state);
1223 };
1224
1225 active_fight.entities.swap_remove(entity_idx);
1226
1227 let mut events = Vec::new();
1228
1229 events.push(Self::currency_increase(
1230 &reward,
1231 CurrencySource::EntityDeath,
1232 ));
1233
1234 let Some(active_fight) = &mut state.active_fight else {
1235 return EventHandleResult::ok(state);
1236 };
1237 let Ok(fight) = game_config.require_fight_template(active_fight.fight_id) else {
1238 tracing::error!(
1239 "Failed to get fight_template with id {} ",
1240 active_fight.fight_id
1241 );
1242 return EventHandleResult::fail(state);
1243 };
1244
1245 let has_any_ally = active_fight
1246 .entities
1247 .iter()
1248 .any(|e| e.team == EntityTeam::Ally);
1249
1250 if active_fight.get_enemies_amount() == 0 && has_any_ally {
1251 if active_fight.current_wave == fight.waves_amount {
1252 let fight_uuid = active_fight.id;
1253 active_fight.fight_ended = true;
1254 let end_fight_delay = self.get_end_fight_delay(active_fight.fight_id);
1255 self.fight_clock.schedule(
1256 OverlordEvent::EndFight {
1257 fight_id: fight_uuid,
1258 is_win: true,
1259 pvp_state: state.pvp_state.clone().map(Box::new),
1260 },
1261 end_fight_delay,
1262 );
1263 if fight.fight_type == FightType::CampaignBossFight {
1264 events.push(EventPluginized::now(OverlordEvent::StageCleared {}));
1265 }
1266 } else {
1267 let fight_uuid = active_fight.id;
1268 active_fight.current_wave += 1;
1269 let active_fight_cloned = active_fight.clone();
1270 let current_chapter = state.character_state.character.current_chapter_level;
1271
1272 let prepare_fight_events = match fight.prepare_fight_waves.as_ref() {
1283 Some(waves_cfg) => {
1284 let wave_data = crate::mechanics::fight::wave_data_from_config(waves_cfg);
1285 let fight_type_str = format!("{:?}", fight.fight_type);
1286 let mut sink = crate::mechanics::fight::NativeSink::default();
1287 let rng = GameRng::new(rand_gen);
1288 match crate::mechanics::fight::spawn_wave(
1289 &mut sink,
1290 &rng,
1291 &game_config,
1292 self.behaviors.lookups(),
1293 &active_fight_cloned,
1294 &wave_data,
1295 fight.power.map(|p| p as f64).unwrap_or(0.0),
1296 current_chapter,
1297 &fight_type_str,
1298 ) {
1299 Ok(()) => sink.events,
1300 Err(err) => {
1301 tracing::error!(
1302 "Prepare wave for new wave failed with error: {err:?}"
1303 );
1304 events.push(EventPluginized::now(OverlordEvent::EndFight {
1305 fight_id: fight_uuid,
1306 is_win: false,
1307 pvp_state: state.pvp_state.clone().map(Box::new),
1308 }));
1309 return EventHandleResult::ok_events(state, events);
1310 }
1311 }
1312 }
1313 None => {
1314 tracing::error!(
1315 "Fight {} has no prepare_fight_waves for next wave",
1316 fight.id
1317 );
1318 events.push(EventPluginized::now(OverlordEvent::EndFight {
1319 fight_id: fight_uuid,
1320 is_win: false,
1321 pvp_state: state.pvp_state.clone().map(Box::new),
1322 }));
1323 return EventHandleResult::ok_events(state, events);
1324 }
1325 };
1326
1327 if prepare_fight_events.is_empty() {
1328 tracing::error!("Prepare wave script returned no events");
1329 events.push(EventPluginized::now(OverlordEvent::EndFight {
1330 fight_id: fight_uuid,
1331 is_win: false,
1332 pvp_state: state.pvp_state.clone().map(Box::new),
1333 }));
1334 return EventHandleResult::ok_events(state, events);
1335 }
1336
1337 if !prepare_fight_events
1338 .iter()
1339 .any(|ev| matches!(ev, OverlordEvent::SpawnEntity { .. }))
1340 {
1341 tracing::error!("Prepare wave script returned no SpawnEntity events");
1342 events.push(EventPluginized::now(OverlordEvent::EndFight {
1343 fight_id: fight_uuid,
1344 is_win: false,
1345 pvp_state: state.pvp_state.clone().map(Box::new),
1346 }));
1347 return EventHandleResult::ok_events(state, events);
1348 }
1349
1350 events.append(
1351 &mut prepare_fight_events
1352 .into_iter()
1353 .map(EventPluginized::now)
1354 .collect(),
1355 );
1356
1357 events.push(EventPluginized::now(OverlordEvent::WaveCleared {}));
1358 }
1359 }
1360
1361 EventHandleResult::ok_events(state, events)
1362 }
1363
1364 pub fn handle_heal(
1365 &self,
1366 entity_id: Uuid,
1367 heal: u64,
1368 mut state: OverlordState,
1369 ) -> EventHandleResult<OverlordEvent, OverlordState> {
1370 let Some(active_fight) = &mut state.active_fight else {
1371 return EventHandleResult::ok(state);
1372 };
1373
1374 let Some(healed_entity) = active_fight
1375 .entities
1376 .iter_mut()
1377 .find(|entity| entity.id == entity_id)
1378 else {
1379 tracing::error!("Failed to get entity with entity_id={}", entity_id);
1380 return EventHandleResult::fail(state);
1381 };
1382
1383 healed_entity.hp = healed_entity
1384 .hp
1385 .saturating_add(heal)
1386 .min(healed_entity.max_hp);
1387
1388 EventHandleResult::ok(state)
1389 }
1390
1391 pub fn handle_damage(
1392 &self,
1393 entity_id: Uuid,
1394 damage: u64,
1395 current_tick: u64,
1396 mut rand_gen: rand::rngs::StdRng,
1397 mut state: OverlordState,
1398 ) -> EventHandleResult<OverlordEvent, OverlordState> {
1399 let Some(active_fight) = &mut state.active_fight else {
1400 return EventHandleResult::ok(state);
1401 };
1402
1403 let player_id = active_fight.player_id;
1404
1405 let Some(damaged_entity) = active_fight
1406 .entities
1407 .iter_mut()
1408 .find(|entity| entity.id == entity_id)
1409 else {
1410 tracing::error!("Failed to get entity with entity_id={}", entity_id);
1411 return EventHandleResult::fail(state);
1412 };
1413
1414 let new_hp = damaged_entity.hp - damage.min(damaged_entity.hp);
1415 damaged_entity.hp = new_hp;
1416
1417 let is_player_taking_damage = entity_id == player_id;
1425 let is_player_dealing_damage = !is_player_taking_damage
1426 && state
1427 .active_fight
1428 .as_ref()
1429 .and_then(|af| af.entities.iter().find(|e| e.id == entity_id))
1430 .is_some_and(|e| e.team == EntityTeam::Enemy);
1431 let game_config = self.game_config.get();
1432 let charge_rate = if is_player_taking_damage || is_player_dealing_damage {
1433 state
1434 .active_fight
1435 .as_ref()
1436 .and_then(|af| af.pet_combat_state.as_ref())
1437 .and_then(|ps| game_config.pet_template(ps.pet_template_id))
1438 .map(|t| {
1439 if is_player_taking_damage {
1440 t.charge_rate_on_damage_taken
1441 } else {
1442 t.charge_rate_on_damage_dealt
1443 }
1444 })
1445 } else {
1446 None
1447 };
1448
1449 if let Some(rate) = charge_rate
1450 && rate > 0
1451 {
1452 self.charge_pet_ability(&mut state, rate, current_tick);
1453 }
1454
1455 if new_hp > 0 {
1456 return EventHandleResult::ok(state);
1457 }
1458
1459 let Some(active_fight) = &mut state.active_fight else {
1460 return EventHandleResult::ok(state);
1461 };
1462
1463 let Some(damaged_entity) = active_fight
1464 .entities
1465 .iter_mut()
1466 .find(|entity| entity.id == entity_id)
1467 else {
1468 return EventHandleResult::fail(state);
1469 };
1470
1471 let damaged_id = damaged_entity.id;
1473 let damaged_team = damaged_entity.team.clone();
1474 let damaged_rewards = damaged_entity.rewards.clone();
1475 let damaged_template_id = damaged_entity.entity_template_id;
1476
1477 let has_remaining_allies = active_fight
1478 .entities
1479 .iter()
1480 .any(|e| e.team == EntityTeam::Ally && e.id != damaged_id);
1481
1482 let mut events = Vec::new();
1483
1484 if damaged_team == EntityTeam::Enemy {
1485 let mut currencies = Vec::new();
1487
1488 if state.pvp_state.is_none() {
1489 let reward_multiplier = {
1496 let is_boss = damaged_template_id
1497 .and_then(|tid| game_config.entity_template(tid))
1498 .is_some_and(|t| t.is_boss);
1499 let is_campaign = game_config
1500 .require_fight_template(active_fight.fight_id)
1501 .map(|f| {
1502 matches!(
1503 f.fight_type,
1504 FightType::CampaignFight | FightType::CampaignBossFight
1505 )
1506 })
1507 .unwrap_or(false);
1508 if is_boss && is_campaign {
1509 boss_reward_chapter_multiplier(
1510 game_config.game_settings.boss_reward_chapter_growth,
1511 game_config.game_settings.boss_reward_max_multiplier,
1512 state.character_state.character.current_chapter_level,
1513 )
1514 } else {
1515 1.0
1516 }
1517 };
1518
1519 if let Some(rewards) = damaged_rewards {
1520 for reward in rewards {
1521 if rand_gen.random_range(0.0..100.0) < reward.drop_chance.clamp(0.0, 100.0)
1522 {
1523 let rolled = if reward.from <= reward.to {
1524 rand_gen.random_range(reward.from..=reward.to)
1525 } else {
1526 tracing::error!(
1527 "Entity {} has a bad reward range: {:?}",
1528 damaged_id,
1529 reward
1530 );
1531 0
1532 };
1533 let amount = if reward_multiplier > 1.0 {
1534 ((rolled as f64) * reward_multiplier).round() as i64
1535 } else {
1536 rolled
1537 };
1538 currencies.push(CurrencyUnit {
1539 currency_id: reward.currency_id,
1540 amount,
1541 });
1542 }
1543 }
1544 } else {
1545 tracing::error!(
1546 "Failed to get reward from damaged_entity with entity_id={}",
1547 entity_id
1548 );
1549 };
1550 }
1551
1552 events.push(EventPluginized::now(OverlordEvent::EntityDeath {
1553 entity_id: damaged_id,
1554 reward: currencies,
1555 }));
1556 } else if has_remaining_allies {
1557 events.push(EventPluginized::now(OverlordEvent::EntityDeath {
1559 entity_id: damaged_id,
1560 reward: Vec::new(),
1561 }));
1562 } else {
1563 events.push(EventPluginized::now(OverlordEvent::PlayerDeath {}));
1565 }
1566
1567 EventHandleResult::ok_events(state, events)
1568 }
1569
1570 #[allow(dead_code)]
1571 fn get_entity_index_with_closest_start_cast(&self, entities: &[Entity]) -> usize {
1572 entities
1573 .iter()
1574 .enumerate()
1575 .filter_map(|(index, entity)| {
1576 entity
1577 .actions_queue
1578 .get_closest_start_cast_action_deadline()
1579 .map(|deadline| (index, deadline))
1580 })
1581 .min_by_key(|&(_, deadline)| deadline)
1582 .map(|(index, _)| index)
1583 .unwrap_or(0)
1584 }
1585
1586 pub fn handle_fight_progress(
1587 &mut self,
1588 current_tick: u64,
1589 mut state: OverlordState,
1590 ) -> EventHandleResult<OverlordEvent, OverlordState> {
1591 let Some(active_fight) = &mut state.active_fight else {
1592 tracing::error!("No active_fight for fight_progress");
1593 return EventHandleResult::ok(state);
1594 };
1595
1596 if active_fight.fight_ended {
1597 return EventHandleResult::ok(state);
1598 }
1599
1600 if active_fight.fight_stopped {
1601 return EventHandleResult::ok(state);
1602 }
1603
1604 if active_fight.entities.is_empty() {
1605 tracing::error!("No entities in fight");
1606 return EventHandleResult::ok(state);
1607 }
1608
1609 if current_tick - self.start_fight_tick >= active_fight.max_duration_ticks {
1610 active_fight.fight_ended = true;
1611 tracing::debug!("Fight lasted too long, ending it");
1612 let fight_uuid = active_fight.id;
1613 let fight_id = active_fight.fight_id;
1614 let end_fight_delay = self.get_end_fight_delay(fight_id);
1615 let pvp_state = state.pvp_state.clone().map(Box::new);
1616 self.fight_clock.schedule(
1617 OverlordEvent::EndFight {
1618 fight_id: fight_uuid,
1619 is_win: false,
1620 pvp_state,
1621 },
1622 end_fight_delay,
1623 );
1624 return EventHandleResult::ok(state);
1625 }
1626
1627 let mut events = vec![];
1628
1629 for entity in &mut active_fight.entities {
1630 if entity.move_target.is_none()
1631 && let Some(action) = entity.actions_queue.pop(current_tick)
1632 {
1633 events.push(event_from_entity_action(action, entity.id));
1634 }
1635 }
1636
1637 EventHandleResult::ok_events(state, events)
1638 }
1639
1640 pub fn handle_set_max_hp(
1641 &mut self,
1642 entity_id: EntityId,
1643 new_max_hp: u64,
1644 new_hp: u64,
1645 mut state: OverlordState,
1646 ) -> EventHandleResult<OverlordEvent, OverlordState> {
1647 let Some(active_fight) = &mut state.active_fight else {
1648 tracing::error!("No active fight for end_fight");
1649 return EventHandleResult::fail(state);
1650 };
1651
1652 let Some(entity) = active_fight
1653 .entities
1654 .iter_mut()
1655 .find(|entity| entity.id == entity_id)
1656 else {
1657 tracing::error!("Failed to get entity with entity_id={}", entity_id);
1658 return EventHandleResult::fail(state);
1659 };
1660
1661 entity.max_hp = new_max_hp;
1662 entity.hp = new_hp.min(new_max_hp);
1663
1664 EventHandleResult::ok(state)
1665 }
1666}
1667
1668fn move_progress_steps(
1675 from: &Coordinates,
1676 to: &Coordinates,
1677 duration_ticks: u64,
1678) -> Vec<(u64, Coordinates)> {
1679 let dx = to.x - from.x;
1680 let dy = to.y - from.y;
1681 let steps = dx.abs().max(dy.abs()).max(1);
1682 (1..=steps)
1683 .map(|k| {
1684 let cell = Coordinates {
1685 x: from.x + dx * k / steps,
1686 y: from.y + dy * k / steps,
1687 };
1688 (duration_ticks * (k as u64 - 1) / steps as u64, cell)
1689 })
1690 .collect()
1691}
1692
1693#[cfg(test)]
1694mod tests {
1695 use super::*;
1696
1697 fn at(x: i64, y: i64) -> Coordinates {
1698 Coordinates { x, y }
1699 }
1700
1701 #[test]
1705 fn move_progress_steps_match_per_cell_timeline() {
1706 assert_eq!(
1708 move_progress_steps(&at(0, 1), &at(4, 1), 2000),
1709 vec![
1710 (0, at(1, 1)),
1711 (500, at(2, 1)),
1712 (1000, at(3, 1)),
1713 (1500, at(4, 1)),
1714 ]
1715 );
1716
1717 assert_eq!(
1719 move_progress_steps(&at(2, 1), &at(3, 2), 707),
1720 vec![(0, at(3, 2))]
1721 );
1722
1723 assert_eq!(
1725 move_progress_steps(&at(2, 1), &at(2, 1), 0),
1726 vec![(0, at(2, 1))]
1727 );
1728 }
1729}