1use std::collections::{BTreeMap, HashMap};
2
3use crate::abilities::{AbilityId, ActiveAbility, EquippedAbilities};
4use crate::character_state::CharacterState;
5use crate::class::ClassId;
6use crate::effect::EffectId;
7use crate::fighting::EntityTeam;
8use crate::game::{EnemyReward, EntityTemplateId};
9use crate::items::Item;
10use crate::opponents::OpponentState;
11use crate::pets::{EquippedPets, PetId};
12
13use crate::prelude::*;
14use strum::{EnumIter, IntoEnumIterator};
15
16#[declare]
17pub type EntityId = Uuid;
18
19#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq, Tsify, JsonSchema)]
20pub struct EntityAttributes(pub BTreeMap<String, i64>);
21
22impl EntityAttributes {
23 pub fn add(&mut self, key: &str, delta: i64) {
24 let value = self
25 .0
26 .entry(key.to_owned())
27 .and_modify(|x| *x += delta)
28 .or_insert(delta);
29 if *value == 0 {
30 self.0.remove(key);
31 }
32 }
33
34 pub fn set(&mut self, key: &str, value: i64) {
35 let value = self
36 .0
37 .entry(key.to_owned())
38 .and_modify(|x| *x = value)
39 .or_insert(value);
40 if *value == 0 {
41 self.0.remove(key);
42 }
43 }
44
45 pub fn remove_zeroes(&mut self) {
46 self.0.retain(|_, v| *v != 0);
47 }
48
49 pub fn speed_or_baseline(&self, baseline_speed: u64) -> i64 {
53 self.0
54 .get("speed")
55 .copied()
56 .unwrap_or(baseline_speed as i64)
57 }
58}
59
60#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq, Tsify, JsonSchema)]
61pub struct Coordinates {
62 #[schemars(title = "Координата по х")]
63 pub x: i64,
64 #[schemars(title = "Координата по у")]
65 pub y: i64,
66}
67
68#[derive(Clone, Default, Debug, Copy, Serialize, Hash, Deserialize, PartialEq, Eq, EnumIter)]
69pub enum ActionPriority {
70 #[default]
71 First,
72 Second,
73 Third,
74 Fourth,
75}
76
77#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq)]
79pub struct EssencesCustomEventData(pub BTreeMap<String, i64>);
80
81#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
82pub enum EntityAction {
83 CastEffect {
84 entity_id: Uuid,
85 effect_id: Uuid,
86 },
87 CastAbility {
88 ability_id: AbilityId,
89 target_entity_id: EntityId,
90 },
91 CastBasicAbility {
92 ability_id: AbilityId,
93 target_entity_id: EntityId,
94 },
95 StartCastAbility {
96 ability_id: AbilityId,
97 by_entity_id: EntityId,
98 pet_id: Option<PetId>,
99 },
100}
101
102impl EntityAction {
103 fn get_action_priority(action: &EntityAction) -> ActionPriority {
104 match action {
105 EntityAction::CastEffect { .. } => ActionPriority::First,
106 EntityAction::CastAbility { .. } => ActionPriority::Second,
107 EntityAction::CastBasicAbility { .. } => ActionPriority::Third,
108 EntityAction::StartCastAbility { .. } => ActionPriority::Fourth,
109 }
110 }
111
112 fn get_actions_priorities(actions: &[Self]) -> Vec<ActionPriority> {
113 actions.iter().map(Self::get_action_priority).collect()
114 }
115
116 fn get_casting_spell_priorities() -> Vec<ActionPriority> {
117 Self::get_actions_priorities(&[
118 Self::CastAbility {
119 ability_id: Uuid::nil(),
120 target_entity_id: Uuid::nil(),
121 },
122 Self::CastBasicAbility {
123 ability_id: Uuid::nil(),
124 target_entity_id: Uuid::nil(),
125 },
126 ])
127 }
128
129 pub fn get_cast_ability_priority() -> ActionPriority {
130 Self::get_action_priority(&Self::CastAbility {
131 ability_id: Uuid::nil(),
132 target_entity_id: Uuid::nil(),
133 })
134 }
135
136 pub fn get_cast_basic_ability_priority() -> ActionPriority {
137 Self::get_action_priority(&Self::CastBasicAbility {
138 ability_id: Uuid::nil(),
139 target_entity_id: Uuid::nil(),
140 })
141 }
142
143 pub fn get_starting_cast_priority() -> ActionPriority {
144 Self::get_action_priority(&Self::StartCastAbility {
145 ability_id: Uuid::nil(),
146 by_entity_id: Uuid::nil(),
147 pet_id: None,
148 })
149 }
150
151 pub fn get_cast_effect_priority() -> ActionPriority {
152 Self::get_action_priority(&Self::CastEffect {
153 entity_id: Uuid::nil(),
154 effect_id: Uuid::nil(),
155 })
156 }
157}
158
159pub fn scale_cooldown_for_speed(base_cooldown_ticks: u64, speed: i64, baseline_speed: u64) -> u64 {
170 if base_cooldown_ticks == 0 {
171 return 0;
172 }
173 let baseline = baseline_speed.max(1);
174 let effective_speed = if speed <= 0 { baseline } else { speed as u64 };
175 let scaled = (base_cooldown_ticks as u128 * baseline as u128) / effective_speed as u128;
176 (scaled as u64).max(1)
177}
178
179#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
180pub struct ActionWithDeadline {
181 pub action: EntityAction,
182 pub deadline_tick: u64,
183}
184
185#[derive(Clone, Default, Debug, Serialize, Deserialize, PartialEq, Eq)]
186pub struct EntityActionsQueue {
187 action_queues: HashMap<ActionPriority, Vec<ActionWithDeadline>>,
188 entity_id: EntityId,
189}
190
191impl EntityActionsQueue {
192 pub fn new(entity_id: EntityId) -> Self {
193 Self {
194 action_queues: HashMap::new(),
195 entity_id,
196 }
197 }
198
199 pub fn push(&mut self, action_with_deadline: &ActionWithDeadline) {
201 let priority = EntityAction::get_action_priority(&action_with_deadline.action);
202
203 self.action_queues
204 .entry(priority)
205 .or_default()
206 .push(ActionWithDeadline {
207 action: action_with_deadline.action.clone(),
208 deadline_tick: action_with_deadline.deadline_tick,
209 });
210 }
211
212 fn add_start_cast_ability(
214 &mut self,
215 action_with_deadline: Option<&ActionWithDeadline>,
216 current_tick: u64,
217 ability_id: AbilityId,
218 ability_cooldown: u64,
219 ) {
220 if let Some(action_with_deadline) = action_with_deadline {
221 match action_with_deadline.action {
222 EntityAction::CastAbility { .. } => {
223 if ability_cooldown != 0 {
224 self.push(&ActionWithDeadline {
225 action: EntityAction::StartCastAbility {
226 ability_id,
227 by_entity_id: self.entity_id,
228 pet_id: None,
229 },
230 deadline_tick: current_tick + ability_cooldown,
231 })
232 }
233 }
234 EntityAction::CastBasicAbility { .. } => {
235 if ability_cooldown != 0 {
236 self.push(&ActionWithDeadline {
237 action: EntityAction::StartCastAbility {
238 ability_id,
239 by_entity_id: self.entity_id,
240 pet_id: None,
241 },
242 deadline_tick: current_tick + ability_cooldown,
243 })
244 }
245 }
246 EntityAction::StartCastAbility { .. } => {}
247 EntityAction::CastEffect { .. } => {}
248 }
249 } else {
250 self.push(&ActionWithDeadline {
251 action: EntityAction::StartCastAbility {
252 ability_id,
253 by_entity_id: self.entity_id,
254 pet_id: None,
255 },
256 deadline_tick: current_tick,
257 });
258 }
259 }
260
261 pub fn append_start_cast_ability_result_actions(
263 &mut self,
264 actions_with_deadlines: &Vec<ActionWithDeadline>,
265 current_tick: u64,
266 ability_id: AbilityId,
267 ability_cooldown: u64,
268 ) {
269 for action_with_deadline in actions_with_deadlines {
270 self.push(action_with_deadline);
271 }
272
273 self.add_start_cast_ability(
276 actions_with_deadlines.first(),
277 current_tick,
278 ability_id,
279 ability_cooldown,
280 );
281 }
282
283 fn is_casting_spell(&self) -> bool {
284 for priority in EntityAction::get_casting_spell_priorities() {
285 if let Some(queue) = self.action_queues.get(&priority)
286 && !queue.is_empty()
287 {
288 return true;
289 }
290 }
291 false
292 }
293
294 fn check_action_is_available(&self, action_priority: &ActionPriority) -> bool {
295 match action_priority {
296 ActionPriority::First => true,
297 ActionPriority::Second => true,
298 ActionPriority::Third => true,
299 ActionPriority::Fourth => !self.is_casting_spell(),
300 }
301 }
302
303 pub fn pop(&mut self, current_tick: u64) -> Option<EntityAction> {
304 for priority in ActionPriority::iter() {
305 if !self.check_action_is_available(&priority) {
306 continue;
307 }
308
309 if let Some(queue) = self.action_queues.get_mut(&priority)
310 && let Some((idx, action_with_deadline)) = queue
311 .iter()
312 .enumerate()
313 .min_by_key(|(_, action_with_deadline)| action_with_deadline.deadline_tick)
314 && action_with_deadline.deadline_tick <= current_tick
315 {
316 let action = queue.remove(idx);
317 return Some(action.action);
318 }
319 }
320
321 None
322 }
323
324 pub fn remove_start_cast_ability_action(&mut self, ability_id_to_remove: AbilityId) {
325 if let Some(queue) = self.action_queues.get_mut(&EntityAction::get_starting_cast_priority()) && let Some(pos) = queue.iter().position(|action_with_deadline| {
326 matches!(
327 action_with_deadline.action,
328 EntityAction::StartCastAbility { ability_id, .. } if ability_id == ability_id_to_remove
329 )
330 }) {
331 queue.remove(pos);
332 }
333 }
334
335 pub fn remove_cast_effect_action(&mut self, effect_id_to_remove: EffectId) {
336 if let Some(queue) = self
337 .action_queues
338 .get_mut(&EntityAction::get_cast_effect_priority())
339 && let Some(pos) = queue.iter().position(|action_with_deadline| {
340 matches!(
341 action_with_deadline.action,
342 EntityAction::CastEffect { effect_id, .. } if effect_id == effect_id_to_remove
343 )
344 })
345 {
346 queue.remove(pos);
347 }
348 }
349
350 pub fn get_closest_start_cast_action_deadline(&self) -> Option<u64> {
351 if let Some(queue) = self
352 .action_queues
353 .get(&EntityAction::get_starting_cast_priority())
354 {
355 return queue.iter().map(|action| action.deadline_tick).min();
356 }
357
358 None
359 }
360
361 pub fn rescale_cooldowns(
372 &mut self,
373 old_speed: i64,
374 new_speed: i64,
375 current_tick: u64,
376 baseline_speed: u64,
377 ) {
378 if old_speed == new_speed {
379 return;
380 }
381 let baseline = baseline_speed.max(1);
382 let old_speed = if old_speed <= 0 {
383 baseline
384 } else {
385 old_speed as u64
386 };
387 let new_speed = if new_speed <= 0 {
388 baseline
389 } else {
390 new_speed as u64
391 };
392
393 let Some(queue) = self
394 .action_queues
395 .get_mut(&EntityAction::get_starting_cast_priority())
396 else {
397 return;
398 };
399
400 for action in queue.iter_mut() {
401 if !matches!(action.action, EntityAction::StartCastAbility { .. }) {
402 continue;
403 }
404 let remaining = action.deadline_tick.saturating_sub(current_tick);
405 if remaining == 0 {
406 continue;
407 }
408 let scaled = (remaining as u128 * old_speed as u128) / new_speed as u128;
409 let scaled = (scaled as u64).max(1);
410 action.deadline_tick = current_tick + scaled;
411 }
412 }
413
414 pub fn stun_ability(
426 &mut self,
427 ability_id: AbilityId,
428 duration_ticks: u64,
429 full_cooldown_ticks: u64,
430 current_tick: u64,
431 ) {
432 let had_in_flight_cast = [
433 EntityAction::get_cast_ability_priority(),
434 EntityAction::get_cast_basic_ability_priority(),
435 ]
436 .iter()
437 .any(|priority| {
438 self.action_queues
439 .get(priority)
440 .is_some_and(|queue| {
441 queue.iter().any(|action_with_deadline| {
442 matches!(
443 action_with_deadline.action,
444 EntityAction::CastAbility { ability_id: aid, .. } | EntityAction::CastBasicAbility { ability_id: aid, .. } if aid == ability_id
445 )
446 })
447 })
448 });
449
450 if had_in_flight_cast {
451 self.cancel_cast_and_set_cooldown(
452 ability_id,
453 current_tick
454 .saturating_add(full_cooldown_ticks)
455 .saturating_add(duration_ticks),
456 );
457 return;
458 }
459
460 if self.adjust_ability_cooldown(ability_id, duration_ticks as i64, current_tick) {
461 return;
462 }
463
464 let entity_id = self.entity_id;
466 self.push(&ActionWithDeadline {
467 action: EntityAction::StartCastAbility {
468 ability_id,
469 by_entity_id: entity_id,
470 pet_id: None,
471 },
472 deadline_tick: current_tick.saturating_add(duration_ticks),
473 });
474 }
475
476 pub fn cancel_cast_and_set_cooldown(
480 &mut self,
481 ability_id_to_cancel: AbilityId,
482 new_deadline_tick: u64,
483 ) -> bool {
484 let mut had_in_flight = false;
485 for priority in [
486 EntityAction::get_cast_ability_priority(),
487 EntityAction::get_cast_basic_ability_priority(),
488 ] {
489 if let Some(queue) = self.action_queues.get_mut(&priority) {
490 let original_len = queue.len();
491 queue.retain(|action_with_deadline| {
492 !matches!(
493 action_with_deadline.action,
494 EntityAction::CastAbility { ability_id, .. } if ability_id == ability_id_to_cancel
495 ) && !matches!(
496 action_with_deadline.action,
497 EntityAction::CastBasicAbility { ability_id, .. } if ability_id == ability_id_to_cancel
498 )
499 });
500 if queue.len() != original_len {
501 had_in_flight = true;
502 }
503 }
504 }
505
506 if let Some(queue) = self
507 .action_queues
508 .get_mut(&EntityAction::get_starting_cast_priority())
509 {
510 queue.retain(|action_with_deadline| {
511 !matches!(
512 action_with_deadline.action,
513 EntityAction::StartCastAbility { ability_id, .. } if ability_id == ability_id_to_cancel
514 )
515 });
516 }
517
518 let entity_id = self.entity_id;
519 self.push(&ActionWithDeadline {
520 action: EntityAction::StartCastAbility {
521 ability_id: ability_id_to_cancel,
522 by_entity_id: entity_id,
523 pet_id: None,
524 },
525 deadline_tick: new_deadline_tick,
526 });
527
528 had_in_flight
529 }
530
531 pub fn adjust_ability_cooldown(
535 &mut self,
536 ability_id_to_adjust: AbilityId,
537 delta_ticks: i64,
538 current_tick: u64,
539 ) -> bool {
540 let Some(queue) = self
541 .action_queues
542 .get_mut(&EntityAction::get_starting_cast_priority())
543 else {
544 return false;
545 };
546
547 let Some(action) = queue.iter_mut().find(|action_with_deadline| {
548 matches!(
549 action_with_deadline.action,
550 EntityAction::StartCastAbility { ability_id, .. } if ability_id == ability_id_to_adjust
551 )
552 }) else {
553 return false;
554 };
555
556 action.deadline_tick = if delta_ticks >= 0 {
557 action.deadline_tick.saturating_add(delta_ticks as u64)
558 } else {
559 let abs = (-delta_ticks) as u64;
560 action.deadline_tick.saturating_sub(abs).max(current_tick)
561 };
562
563 true
564 }
565
566 pub fn view(&self) -> HashMap<ActionPriority, Vec<ActionWithDeadline>> {
567 self.action_queues.clone()
568 }
569}
570
571#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq, Tsify, JsonSchema)]
572#[tsify(from_wasm_abi, into_wasm_abi)]
573pub struct Entity {
574 pub id: EntityId,
575 pub max_hp: u64,
576 pub hp: u64,
577 pub abilities: Vec<ActiveAbility>,
578 #[schemars(skip)]
579 pub actions_queue: EntityActionsQueue,
580 pub attributes: EntityAttributes,
581 pub effect_ids: Vec<EffectId>,
582 pub coordinates: Coordinates,
583 pub move_target: Option<Coordinates>,
586 pub width: i8, pub rewards: Option<Vec<EnemyReward>>,
588 pub class_id: Option<ClassId>,
589 pub team: EntityTeam,
590 pub has_big_hp_bar: bool,
591 pub entity_template_id: Option<EntityTemplateId>,
592}
593
594#[derive(Debug, Clone, Eq, PartialEq)]
595pub enum EntityState<'a> {
596 Character(&'a CharacterState),
597 Opponent(&'a OpponentState),
598}
599
600impl<'a> EntityState<'a> {
601 pub fn id(&self) -> uuid::Uuid {
602 match self {
603 EntityState::Character(character_state) => character_state.character.id,
604 EntityState::Opponent(opponent_state) => opponent_state.id(),
605 }
606 }
607
608 pub fn level(&self) -> i64 {
609 match self {
610 EntityState::Character(character_state) => character_state.character.character_level,
611 EntityState::Opponent(opponent_state) => opponent_state.level(),
612 }
613 }
614
615 pub fn inventory(&self) -> &Vec<Item> {
616 match self {
617 EntityState::Character(character_state) => &character_state.inventory,
618 EntityState::Opponent(opponent_state) => opponent_state.inventory(),
619 }
620 }
621
622 pub fn class(&self) -> ClassId {
623 match self {
624 EntityState::Character(character_state) => character_state.character.class,
625 EntityState::Opponent(opponent_state) => opponent_state.class(),
626 }
627 }
628
629 pub fn equipped_abilities(&self) -> &EquippedAbilities {
630 match self {
631 EntityState::Character(character_state) => &character_state.equipped_abilities,
632 EntityState::Opponent(opponent_state) => opponent_state.equipped_abilities(),
633 }
634 }
635
636 pub fn equipped_pets(&self) -> Option<&EquippedPets> {
637 match self {
638 EntityState::Character(character_state) => Some(&character_state.equipped_pets),
639 EntityState::Opponent(opponent_state) => opponent_state.equipped_pets(),
640 }
641 }
642}