1use std::collections::{BTreeMap, HashMap};
2
3use crate::abilities::{AbilityId, ActiveAbility, EquippedAbilities};
4use crate::character_state::CharacterState;
5use crate::class::ClassId;
6use crate::combat_origin::CombatEventOrigin;
7use crate::effect::EffectId;
8use crate::fighting::EntityTeam;
9use crate::game::{EnemyReward, EntityTemplateId};
10use crate::items::Item;
11use crate::opponents::OpponentState;
12use crate::pets::{EquippedPets, PetId};
13
14use crate::prelude::*;
15use strum::{EnumIter, IntoEnumIterator};
16
17#[declare]
18pub type EntityId = Uuid;
19
20#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq, Tsify, JsonSchema)]
21pub struct EntityAttributes(pub BTreeMap<String, i64>);
22
23impl EntityAttributes {
24 pub fn add(&mut self, key: &str, delta: i64) {
25 let value = self
26 .0
27 .entry(key.to_owned())
28 .and_modify(|x| *x += delta)
29 .or_insert(delta);
30 if *value == 0 {
31 self.0.remove(key);
32 }
33 }
34
35 pub fn set(&mut self, key: &str, value: i64) {
36 let value = self
37 .0
38 .entry(key.to_owned())
39 .and_modify(|x| *x = value)
40 .or_insert(value);
41 if *value == 0 {
42 self.0.remove(key);
43 }
44 }
45
46 pub fn remove_zeroes(&mut self) {
47 self.0.retain(|_, v| *v != 0);
48 }
49
50 pub fn is_summoned(&self) -> bool {
57 self.0.contains_key("summoned")
58 }
59
60 pub fn wave_share(&self) -> f64 {
67 if self.is_summoned() {
68 return 0.0;
69 }
70 self.0.get("wave_share").copied().unwrap_or(10000) as f64 / 10000.0
71 }
72
73 pub fn gauge_hp_share(&self) -> f64 {
82 if self.is_summoned() {
83 return 0.0;
84 }
85 self.0
86 .get("gauge_hp_share")
87 .copied()
88 .unwrap_or(10000)
89 .max(0) as f64
90 / 10000.0
91 }
92
93 pub fn speed_or_baseline(&self, baseline_speed: u64) -> i64 {
97 self.0
98 .get("speed")
99 .copied()
100 .unwrap_or(baseline_speed as i64)
101 }
102}
103
104#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq, Tsify, JsonSchema)]
105pub struct Coordinates {
106 #[schemars(title = "Координата по х")]
107 pub x: i64,
108 #[schemars(title = "Координата по у")]
109 pub y: i64,
110}
111
112#[derive(Clone, Default, Debug, Copy, Serialize, Hash, Deserialize, PartialEq, Eq, EnumIter)]
113pub enum ActionPriority {
114 #[default]
115 First,
116 Second,
117 Third,
118 Fourth,
119}
120
121#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq)]
123pub struct EssencesCustomEventData(pub BTreeMap<String, i64>);
124
125#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
126pub enum EntityAction {
127 CastEffect {
128 entity_id: Uuid,
129 effect_id: Uuid,
130 },
131 CastAbility {
132 ability_id: AbilityId,
133 target_entity_id: EntityId,
134 },
135 CastBasicAbility {
136 ability_id: AbilityId,
137 target_entity_id: EntityId,
138 },
139 StartCastAbility {
140 ability_id: AbilityId,
141 by_entity_id: EntityId,
142 pet_id: Option<PetId>,
143 },
144}
145
146impl EntityAction {
147 fn get_action_priority(action: &EntityAction) -> ActionPriority {
148 match action {
149 EntityAction::CastEffect { .. } => ActionPriority::First,
150 EntityAction::CastAbility { .. } => ActionPriority::Second,
151 EntityAction::CastBasicAbility { .. } => ActionPriority::Third,
152 EntityAction::StartCastAbility { .. } => ActionPriority::Fourth,
153 }
154 }
155
156 fn get_actions_priorities(actions: &[Self]) -> Vec<ActionPriority> {
157 actions.iter().map(Self::get_action_priority).collect()
158 }
159
160 fn get_casting_spell_priorities() -> Vec<ActionPriority> {
161 Self::get_actions_priorities(&[
162 Self::CastAbility {
163 ability_id: Uuid::nil(),
164 target_entity_id: Uuid::nil(),
165 },
166 Self::CastBasicAbility {
167 ability_id: Uuid::nil(),
168 target_entity_id: Uuid::nil(),
169 },
170 ])
171 }
172
173 pub fn get_cast_ability_priority() -> ActionPriority {
174 Self::get_action_priority(&Self::CastAbility {
175 ability_id: Uuid::nil(),
176 target_entity_id: Uuid::nil(),
177 })
178 }
179
180 pub fn get_cast_basic_ability_priority() -> ActionPriority {
181 Self::get_action_priority(&Self::CastBasicAbility {
182 ability_id: Uuid::nil(),
183 target_entity_id: Uuid::nil(),
184 })
185 }
186
187 pub fn get_starting_cast_priority() -> ActionPriority {
188 Self::get_action_priority(&Self::StartCastAbility {
189 ability_id: Uuid::nil(),
190 by_entity_id: Uuid::nil(),
191 pet_id: None,
192 })
193 }
194
195 pub fn get_cast_effect_priority() -> ActionPriority {
196 Self::get_action_priority(&Self::CastEffect {
197 entity_id: Uuid::nil(),
198 effect_id: Uuid::nil(),
199 })
200 }
201}
202
203pub fn scale_cooldown_for_speed(base_cooldown_ticks: u64, speed: i64, baseline_speed: u64) -> u64 {
214 if base_cooldown_ticks == 0 {
215 return 0;
216 }
217 let baseline = baseline_speed.max(1);
218 let effective_speed = if speed <= 0 { baseline } else { speed as u64 };
219 let scaled = (base_cooldown_ticks as u128 * baseline as u128) / effective_speed as u128;
220 (scaled as u64).max(1)
221}
222
223#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
224pub struct ActionWithDeadline {
225 pub action: EntityAction,
226 pub deadline_tick: u64,
227 pub origin: CombatEventOrigin,
238}
239
240impl ActionWithDeadline {
241 pub fn core(action: EntityAction, deadline_tick: u64) -> Self {
243 Self {
244 action,
245 deadline_tick,
246 origin: CombatEventOrigin::Core,
247 }
248 }
249}
250
251#[derive(Clone, Default, Debug, Serialize, Deserialize, PartialEq, Eq)]
252pub struct EntityActionsQueue {
253 action_queues: HashMap<ActionPriority, Vec<ActionWithDeadline>>,
254 entity_id: EntityId,
255}
256
257impl EntityActionsQueue {
258 pub fn new(entity_id: EntityId) -> Self {
259 Self {
260 action_queues: HashMap::new(),
261 entity_id,
262 }
263 }
264
265 pub fn push(&mut self, action_with_deadline: &ActionWithDeadline) {
267 let priority = EntityAction::get_action_priority(&action_with_deadline.action);
268
269 self.action_queues
270 .entry(priority)
271 .or_default()
272 .push(action_with_deadline.clone());
273 }
274
275 fn add_start_cast_ability(
277 &mut self,
278 action_with_deadline: Option<&ActionWithDeadline>,
279 current_tick: u64,
280 ability_id: AbilityId,
281 ability_cooldown: u64,
282 ) {
283 if let Some(action_with_deadline) = action_with_deadline {
284 match action_with_deadline.action {
285 EntityAction::CastAbility { .. } | EntityAction::CastBasicAbility { .. } => {
286 if ability_cooldown != 0 {
287 self.push_start_cast_replacing(ability_id, current_tick + ability_cooldown)
288 }
289 }
290 EntityAction::StartCastAbility { .. } => {}
291 EntityAction::CastEffect { .. } => {}
292 }
293 } else {
294 self.push_start_cast_replacing(ability_id, current_tick);
295 }
296 }
297
298 pub fn append_start_cast_ability_result_actions(
300 &mut self,
301 actions_with_deadlines: &Vec<ActionWithDeadline>,
302 current_tick: u64,
303 ability_id: AbilityId,
304 ability_cooldown: u64,
305 ) {
306 for action_with_deadline in actions_with_deadlines {
307 self.push(action_with_deadline);
308 }
309
310 self.add_start_cast_ability(
313 actions_with_deadlines.first(),
314 current_tick,
315 ability_id,
316 ability_cooldown,
317 );
318 }
319
320 fn is_casting_spell(&self) -> bool {
321 for priority in EntityAction::get_casting_spell_priorities() {
322 if let Some(queue) = self.action_queues.get(&priority)
323 && !queue.is_empty()
324 {
325 return true;
326 }
327 }
328 false
329 }
330
331 fn check_action_is_available(&self, action_priority: &ActionPriority) -> bool {
332 match action_priority {
333 ActionPriority::First => true,
334 ActionPriority::Second => true,
335 ActionPriority::Third => true,
336 ActionPriority::Fourth => !self.is_casting_spell(),
337 }
338 }
339
340 pub fn pop(&mut self, current_tick: u64) -> Option<ActionWithDeadline> {
343 for priority in ActionPriority::iter() {
344 if !self.check_action_is_available(&priority) {
345 continue;
346 }
347
348 if let Some(queue) = self.action_queues.get_mut(&priority)
349 && let Some((idx, action_with_deadline)) = queue
350 .iter()
351 .enumerate()
352 .min_by_key(|(_, action_with_deadline)| action_with_deadline.deadline_tick)
353 && action_with_deadline.deadline_tick <= current_tick
354 {
355 return Some(queue.remove(idx));
356 }
357 }
358
359 None
360 }
361
362 pub fn remove_start_cast_ability_action(&mut self, ability_id_to_remove: AbilityId) {
363 if let Some(queue) = self.action_queues.get_mut(&EntityAction::get_starting_cast_priority()) && let Some(pos) = queue.iter().position(|action_with_deadline| {
364 matches!(
365 action_with_deadline.action,
366 EntityAction::StartCastAbility { ability_id, .. } if ability_id == ability_id_to_remove
367 )
368 }) {
369 queue.remove(pos);
370 }
371 }
372
373 pub fn remove_cast_effect_action(&mut self, effect_id_to_remove: EffectId) {
374 if let Some(queue) = self
375 .action_queues
376 .get_mut(&EntityAction::get_cast_effect_priority())
377 && let Some(pos) = queue.iter().position(|action_with_deadline| {
378 matches!(
379 action_with_deadline.action,
380 EntityAction::CastEffect { effect_id, .. } if effect_id == effect_id_to_remove
381 )
382 })
383 {
384 queue.remove(pos);
385 }
386 }
387
388 pub fn get_closest_start_cast_action_deadline(&self) -> Option<u64> {
389 if let Some(queue) = self
390 .action_queues
391 .get(&EntityAction::get_starting_cast_priority())
392 {
393 return queue.iter().map(|action| action.deadline_tick).min();
394 }
395
396 None
397 }
398
399 pub fn rescale_cooldowns(
410 &mut self,
411 old_speed: i64,
412 new_speed: i64,
413 current_tick: u64,
414 baseline_speed: u64,
415 ) {
416 if old_speed == new_speed {
417 return;
418 }
419 let baseline = baseline_speed.max(1);
420 let old_speed = if old_speed <= 0 {
421 baseline
422 } else {
423 old_speed as u64
424 };
425 let new_speed = if new_speed <= 0 {
426 baseline
427 } else {
428 new_speed as u64
429 };
430
431 let Some(queue) = self
432 .action_queues
433 .get_mut(&EntityAction::get_starting_cast_priority())
434 else {
435 return;
436 };
437
438 for action in queue.iter_mut() {
439 if !matches!(action.action, EntityAction::StartCastAbility { .. }) {
440 continue;
441 }
442 let remaining = action.deadline_tick.saturating_sub(current_tick);
443 if remaining == 0 {
444 continue;
445 }
446 let scaled = (remaining as u128 * old_speed as u128) / new_speed as u128;
447 let scaled = (scaled as u64).max(1);
448 action.deadline_tick = current_tick + scaled;
449 }
450 }
451
452 pub fn push_start_cast_replacing(&mut self, ability_id: AbilityId, deadline_tick: u64) {
469 let existing_max = self.start_cast_deadlines(ability_id).into_iter().max();
470 if let Some(queue) = self
471 .action_queues
472 .get_mut(&EntityAction::get_starting_cast_priority())
473 {
474 queue.retain(|a| {
475 !matches!(
476 a.action,
477 EntityAction::StartCastAbility { ability_id: aid, .. } if aid == ability_id
478 )
479 });
480 }
481 let entity_id = self.entity_id;
482 self.push(&ActionWithDeadline::core(
488 EntityAction::StartCastAbility {
489 ability_id,
490 by_entity_id: entity_id,
491 pet_id: None,
492 },
493 existing_max.map_or(deadline_tick, |d| d.max(deadline_tick)),
494 ));
495 }
496
497 pub fn start_cast_entries(&self) -> Vec<(AbilityId, u64)> {
503 self.action_queues
504 .get(&EntityAction::get_starting_cast_priority())
505 .map(|queue| {
506 queue
507 .iter()
508 .filter_map(|a| match a.action {
509 EntityAction::StartCastAbility { ability_id, .. } => {
510 Some((ability_id, a.deadline_tick))
511 }
512 _ => None,
513 })
514 .collect()
515 })
516 .unwrap_or_default()
517 }
518
519 pub fn ability_cooldowns(&self, current_tick: u64) -> Vec<(AbilityId, u64)> {
528 let Some(queue) = self
529 .action_queues
530 .get(&EntityAction::get_starting_cast_priority())
531 else {
532 return Vec::new();
533 };
534 let mut remaining: Vec<(AbilityId, u64)> = queue
535 .iter()
536 .filter_map(|action| match action.action {
537 EntityAction::StartCastAbility { ability_id, .. } => {
538 let left = action.deadline_tick.saturating_sub(current_tick);
539 (left > 0).then_some((ability_id, left))
540 }
541 _ => None,
542 })
543 .collect();
544 remaining.sort_by(|a, b| b.1.cmp(&a.1).then(a.0.cmp(&b.0)));
545 remaining
546 }
547
548 pub fn shorten_cooldowns_of(
557 &mut self,
558 current_tick: u64,
559 ability_ids: &[AbilityId],
560 by_ticks: u64,
561 ) -> usize {
562 if ability_ids.is_empty() || by_ticks == 0 {
563 return 0;
564 }
565 let Some(queue) = self
566 .action_queues
567 .get_mut(&EntityAction::get_starting_cast_priority())
568 else {
569 return 0;
570 };
571 let mut moved = 0;
572 for action in queue.iter_mut() {
573 let EntityAction::StartCastAbility { ability_id, .. } = action.action else {
574 continue;
575 };
576 if !ability_ids.contains(&ability_id) || action.deadline_tick <= current_tick {
577 continue;
578 }
579 action.deadline_tick = action
580 .deadline_tick
581 .saturating_sub(by_ticks)
582 .max(current_tick);
583 moved += 1;
584 }
585 moved
586 }
587
588 pub fn shorten_longest_cooldowns(
594 &mut self,
595 current_tick: u64,
596 count: usize,
597 by_ticks: u64,
598 ) -> usize {
599 if count == 0 || by_ticks == 0 {
600 return 0;
601 }
602 let chosen: Vec<AbilityId> = self
603 .ability_cooldowns(current_tick)
604 .into_iter()
605 .take(count)
606 .map(|(ability_id, _)| ability_id)
607 .collect();
608 if chosen.is_empty() {
609 return 0;
610 }
611 let Some(queue) = self
612 .action_queues
613 .get_mut(&EntityAction::get_starting_cast_priority())
614 else {
615 return 0;
616 };
617 let mut moved = 0;
618 for action in queue.iter_mut() {
619 let EntityAction::StartCastAbility { ability_id, .. } = action.action else {
620 continue;
621 };
622 if !chosen.contains(&ability_id) || action.deadline_tick <= current_tick {
623 continue;
624 }
625 action.deadline_tick = action
626 .deadline_tick
627 .saturating_sub(by_ticks)
628 .max(current_tick);
629 moved += 1;
630 }
631 moved
632 }
633
634 pub fn clear_ability_cooldowns(&mut self, current_tick: u64) -> usize {
637 let Some(queue) = self
638 .action_queues
639 .get_mut(&EntityAction::get_starting_cast_priority())
640 else {
641 return 0;
642 };
643 let mut cleared = 0;
644 for action in queue.iter_mut() {
645 if !matches!(action.action, EntityAction::StartCastAbility { .. })
646 || action.deadline_tick <= current_tick
647 {
648 continue;
649 }
650 action.deadline_tick = current_tick;
651 cleared += 1;
652 }
653 cleared
654 }
655
656 pub fn start_cast_deadlines(&self, ability_id: AbilityId) -> Vec<u64> {
660 self.action_queues
661 .get(&EntityAction::get_starting_cast_priority())
662 .map(|queue| {
663 queue
664 .iter()
665 .filter(|a| {
666 matches!(
667 a.action,
668 EntityAction::StartCastAbility { ability_id: aid, .. } if aid == ability_id
669 )
670 })
671 .map(|a| a.deadline_tick)
672 .collect()
673 })
674 .unwrap_or_default()
675 }
676
677 pub fn stun_ability(
678 &mut self,
679 ability_id: AbilityId,
680 duration_ticks: u64,
681 full_cooldown_ticks: u64,
682 current_tick: u64,
683 ) {
684 let had_in_flight_cast = [
685 EntityAction::get_cast_ability_priority(),
686 EntityAction::get_cast_basic_ability_priority(),
687 ]
688 .iter()
689 .any(|priority| {
690 self.action_queues
691 .get(priority)
692 .is_some_and(|queue| {
693 queue.iter().any(|action_with_deadline| {
694 matches!(
695 action_with_deadline.action,
696 EntityAction::CastAbility { ability_id: aid, .. } | EntityAction::CastBasicAbility { ability_id: aid, .. } if aid == ability_id
697 )
698 })
699 })
700 });
701
702 if had_in_flight_cast {
703 self.cancel_cast_and_set_cooldown(
704 ability_id,
705 current_tick
706 .saturating_add(full_cooldown_ticks)
707 .saturating_add(duration_ticks),
708 );
709 return;
710 }
711
712 if self.adjust_ability_cooldown(ability_id, duration_ticks as i64, current_tick) {
713 return;
714 }
715
716 let entity_id = self.entity_id;
720 self.push(&ActionWithDeadline::core(
722 EntityAction::StartCastAbility {
723 ability_id,
724 by_entity_id: entity_id,
725 pet_id: None,
726 },
727 current_tick.saturating_add(duration_ticks),
728 ));
729 }
730
731 pub fn cancel_cast_and_set_cooldown(
735 &mut self,
736 ability_id_to_cancel: AbilityId,
737 new_deadline_tick: u64,
738 ) -> bool {
739 let mut had_in_flight = false;
740 for priority in [
741 EntityAction::get_cast_ability_priority(),
742 EntityAction::get_cast_basic_ability_priority(),
743 ] {
744 if let Some(queue) = self.action_queues.get_mut(&priority) {
745 let original_len = queue.len();
746 queue.retain(|action_with_deadline| {
747 !matches!(
748 action_with_deadline.action,
749 EntityAction::CastAbility { ability_id, .. } if ability_id == ability_id_to_cancel
750 ) && !matches!(
751 action_with_deadline.action,
752 EntityAction::CastBasicAbility { ability_id, .. } if ability_id == ability_id_to_cancel
753 )
754 });
755 if queue.len() != original_len {
756 had_in_flight = true;
757 }
758 }
759 }
760
761 if let Some(queue) = self
762 .action_queues
763 .get_mut(&EntityAction::get_starting_cast_priority())
764 {
765 queue.retain(|action_with_deadline| {
766 !matches!(
767 action_with_deadline.action,
768 EntityAction::StartCastAbility { ability_id, .. } if ability_id == ability_id_to_cancel
769 )
770 });
771 }
772
773 let entity_id = self.entity_id;
774 self.push(&ActionWithDeadline::core(
776 EntityAction::StartCastAbility {
777 ability_id: ability_id_to_cancel,
778 by_entity_id: entity_id,
779 pet_id: None,
780 },
781 new_deadline_tick,
782 ));
783
784 had_in_flight
785 }
786
787 pub fn adjust_ability_cooldown(
791 &mut self,
792 ability_id_to_adjust: AbilityId,
793 delta_ticks: i64,
794 current_tick: u64,
795 ) -> bool {
796 let Some(queue) = self
797 .action_queues
798 .get_mut(&EntityAction::get_starting_cast_priority())
799 else {
800 return false;
801 };
802
803 let Some(action) = queue.iter_mut().find(|action_with_deadline| {
804 matches!(
805 action_with_deadline.action,
806 EntityAction::StartCastAbility { ability_id, .. } if ability_id == ability_id_to_adjust
807 )
808 }) else {
809 return false;
810 };
811
812 action.deadline_tick = if delta_ticks >= 0 {
813 action.deadline_tick.saturating_add(delta_ticks as u64)
814 } else {
815 let abs = (-delta_ticks) as u64;
816 action.deadline_tick.saturating_sub(abs).max(current_tick)
817 };
818
819 true
820 }
821
822 pub fn view(&self) -> HashMap<ActionPriority, Vec<ActionWithDeadline>> {
823 self.action_queues.clone()
824 }
825}
826
827#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq, Tsify, JsonSchema)]
828#[tsify(from_wasm_abi, into_wasm_abi)]
829pub struct Entity {
830 pub id: EntityId,
831 pub max_hp: u64,
832 pub hp: u64,
833 pub abilities: Vec<ActiveAbility>,
834 #[schemars(skip)]
835 pub actions_queue: EntityActionsQueue,
836 pub attributes: EntityAttributes,
837 pub effect_ids: Vec<EffectId>,
838 pub coordinates: Coordinates,
839 pub move_target: Option<Coordinates>,
842 pub width: i8, pub rewards: Option<Vec<EnemyReward>>,
844 pub class_id: Option<ClassId>,
845 pub team: EntityTeam,
846 pub has_big_hp_bar: bool,
847 pub entity_template_id: Option<EntityTemplateId>,
848 #[serde(default)]
851 pub flip_state: Option<crate::flip::FlipState>,
852 pub mana: Option<crate::mana::ManaPool>,
856 #[serde(skip)]
863 #[schemars(skip)]
864 pub proc_entropy: EntropyCell,
865 #[serde(skip)]
873 #[schemars(skip)]
874 pub law_bridges: crate::cores::LawBridgeCharges,
875 #[serde(skip)]
894 #[schemars(skip)]
895 pub law_cores: crate::cores::CoresState,
896}
897
898#[derive(Debug, Default)]
904pub struct EntropyCell(std::sync::Mutex<std::collections::HashMap<String, i64>>);
905
906impl EntropyCell {
907 pub fn bump(&self, key: &str, add: i64, init: impl FnOnce() -> i64) -> bool {
912 let mut m = self.0.lock().unwrap();
913 let acc = m.entry(key.to_string()).or_insert_with(init);
914 *acc += add;
915 if *acc >= 10000 {
916 *acc -= 10000;
917 true
918 } else {
919 false
920 }
921 }
922}
923
924impl Clone for EntropyCell {
925 fn clone(&self) -> Self {
926 Self(std::sync::Mutex::new(self.0.lock().unwrap().clone()))
927 }
928}
929
930impl PartialEq for EntropyCell {
931 fn eq(&self, other: &Self) -> bool {
932 *self.0.lock().unwrap() == *other.0.lock().unwrap()
933 }
934}
935
936impl Eq for EntropyCell {}
937
938#[derive(Debug, Clone, Eq, PartialEq)]
939pub enum EntityState<'a> {
940 Character(&'a CharacterState),
941 Opponent(&'a OpponentState),
942}
943
944impl<'a> EntityState<'a> {
945 pub fn id(&self) -> uuid::Uuid {
946 match self {
947 EntityState::Character(character_state) => character_state.character.id,
948 EntityState::Opponent(opponent_state) => opponent_state.id(),
949 }
950 }
951
952 pub fn level(&self) -> i64 {
953 match self {
954 EntityState::Character(character_state) => character_state.character.character_level,
955 EntityState::Opponent(opponent_state) => opponent_state.level(),
956 }
957 }
958
959 pub fn current_chapter_level(&self) -> i64 {
960 match self {
961 EntityState::Character(character_state) => {
962 character_state.character.current_chapter_level
963 }
964 EntityState::Opponent(OpponentState::Human(human)) => {
965 human.character_state.character.current_chapter_level
966 }
967 EntityState::Opponent(OpponentState::Bot(bot)) => bot.bot.level,
968 }
969 }
970
971 pub fn inventory(&self) -> &Vec<Item> {
972 match self {
973 EntityState::Character(character_state) => &character_state.inventory,
974 EntityState::Opponent(opponent_state) => opponent_state.inventory(),
975 }
976 }
977
978 pub fn class(&self) -> ClassId {
979 match self {
980 EntityState::Character(character_state) => character_state.character.class,
981 EntityState::Opponent(opponent_state) => opponent_state.class(),
982 }
983 }
984
985 pub fn equipped_abilities(&self) -> &EquippedAbilities {
986 match self {
987 EntityState::Character(character_state) => &character_state.equipped_abilities,
988 EntityState::Opponent(opponent_state) => opponent_state.equipped_abilities(),
989 }
990 }
991
992 pub fn equipped_pets(&self) -> Option<&EquippedPets> {
993 match self {
994 EntityState::Character(character_state) => Some(&character_state.equipped_pets),
995 EntityState::Opponent(opponent_state) => opponent_state.equipped_pets(),
996 }
997 }
998
999 pub fn cores(&self) -> Option<&crate::cores::CoresState> {
1003 match self {
1004 EntityState::Character(character_state) => Some(&character_state.cores),
1005 EntityState::Opponent(opponent_state) => {
1006 opponent_state.character_state().map(|state| &state.cores)
1007 }
1008 }
1009 }
1010
1011 pub fn talent_levels(&self) -> Option<&crate::talent_tree::TalentLevelsMap> {
1016 match self {
1017 EntityState::Character(character_state) => Some(&character_state.talent_levels),
1018 EntityState::Opponent(opponent_state) => opponent_state
1019 .character_state()
1020 .map(|state| &state.talent_levels),
1021 }
1022 }
1023
1024 pub fn statue_state(&self) -> Option<&crate::statue::StatueState> {
1027 match self {
1028 EntityState::Character(character_state) => Some(&character_state.statue_state),
1029 EntityState::Opponent(opponent_state) => opponent_state
1030 .character_state()
1031 .map(|state| &state.statue_state),
1032 }
1033 }
1034
1035 pub fn character_classes(&self) -> Option<&[crate::class::CharacterClass]> {
1039 match self {
1040 EntityState::Character(character_state) => Some(&character_state.character_classes),
1041 EntityState::Opponent(opponent_state) => opponent_state
1042 .character_state()
1043 .map(|state| state.character_classes.as_slice()),
1044 }
1045 }
1046
1047 pub fn stones(&self) -> Option<&crate::stones::StoneInventory> {
1052 match self {
1053 EntityState::Character(character_state) => Some(&character_state.stones),
1054 EntityState::Opponent(opponent_state) => {
1055 opponent_state.character_state().map(|state| &state.stones)
1056 }
1057 }
1058 }
1059
1060 pub fn ability_stones(&self) -> Option<&[crate::ability_stones::OwnedAbilityStone]> {
1064 match self {
1065 EntityState::Character(character_state) => {
1066 Some(character_state.ability_stones.as_slice())
1067 }
1068 EntityState::Opponent(opponent_state) => opponent_state
1069 .character_state()
1070 .map(|state| state.ability_stones.as_slice()),
1071 }
1072 }
1073
1074 pub fn all_abilities(&self) -> Option<&[crate::abilities::Ability]> {
1079 match self {
1080 EntityState::Character(character_state) => {
1081 Some(character_state.all_abilities.as_slice())
1082 }
1083 EntityState::Opponent(opponent_state) => opponent_state
1084 .character_state()
1085 .map(|state| state.all_abilities.as_slice()),
1086 }
1087 }
1088
1089 pub fn all_pets(&self) -> Option<&[crate::pets::Pet]> {
1093 match self {
1094 EntityState::Character(character_state) => Some(character_state.all_pets.as_slice()),
1095 EntityState::Opponent(opponent_state) => opponent_state
1096 .character_state()
1097 .map(|state| state.all_pets.as_slice()),
1098 }
1099 }
1100
1101 pub fn artifacts(&self) -> Option<&crate::artifacts::ArtifactCollection> {
1104 match self {
1105 EntityState::Character(character_state) => Some(&character_state.artifacts),
1106 EntityState::Opponent(opponent_state) => opponent_state
1107 .character_state()
1108 .map(|state| &state.artifacts),
1109 }
1110 }
1111
1112 pub fn plinko_pin_bonuses(&self) -> Option<&crate::plinko::PlinkoPinBonusesMap> {
1115 match self {
1116 EntityState::Character(character_state) => Some(&character_state.plinko_pin_bonuses),
1117 EntityState::Opponent(opponent_state) => opponent_state
1118 .character_state()
1119 .map(|state| &state.plinko_pin_bonuses),
1120 }
1121 }
1122}
1123
1124#[cfg(test)]
1125mod wave_share_tests {
1126 use super::*;
1127
1128 #[test]
1131 fn wave_share_reads_permyriad_or_defaults_to_one() {
1132 let mut attrs = EntityAttributes::default();
1133 assert_eq!(
1134 attrs.wave_share(),
1135 1.0,
1136 "absent wave_share must default to 1.0"
1137 );
1138
1139 attrs.add("wave_share", 5000);
1140 assert_eq!(attrs.wave_share(), 0.5);
1141
1142 attrs.set("wave_share", 10000);
1143 assert_eq!(attrs.wave_share(), 1.0);
1144
1145 attrs.set("wave_share", 3333);
1146 assert!((attrs.wave_share() - 0.3333).abs() < 1e-9);
1147 }
1148
1149 #[test]
1152 fn summoned_mob_has_zero_wave_share() {
1153 let mut attrs = EntityAttributes::default();
1154 assert!(!attrs.is_summoned());
1155
1156 attrs.add("summoned", 1);
1157 assert!(attrs.is_summoned());
1158 assert_eq!(
1159 attrs.wave_share(),
1160 0.0,
1161 "a summoned reinforcement contributes no drop/pet/counter faucet"
1162 );
1163
1164 attrs.add("wave_share", 5000);
1166 assert_eq!(attrs.wave_share(), 0.0);
1167 }
1168}
1169
1170#[cfg(test)]
1171mod entropy_tests {
1172 use super::*;
1173
1174 #[test]
1180 fn entropy_rate_is_exact_for_any_seed() {
1181 for seed in [0i64, 1, 2499, 2500, 5000, 9999] {
1182 let cell = EntropyCell::default();
1183 let procs: Vec<bool> = (0..40).map(|_| cell.bump("crit", 2500, || seed)).collect();
1184 let total: usize = procs.iter().filter(|p| **p).count();
1185 assert_eq!(
1186 total, 10,
1187 "25% over 40 checks must proc exactly 10 (seed {seed})"
1188 );
1189 for w in procs.windows(4) {
1191 let c = w.iter().filter(|p| **p).count();
1192 assert!(c <= 1, "double proc within a 4-window (seed {seed})");
1193 }
1194 }
1195 }
1196
1197 #[test]
1199 fn entropy_keys_are_independent() {
1200 let cell = EntropyCell::default();
1201 assert!(!cell.bump("a", 6000, || 0));
1202 assert!(cell.bump("a", 6000, || 0)); assert!(!cell.bump("b", 6000, || 0)); }
1205}
1206
1207#[cfg(test)]
1208mod ability_cooldown_tests {
1209 use super::*;
1210
1211 fn queue_with(deadlines: &[(u128, u64)]) -> EntityActionsQueue {
1212 let mut queue = EntityActionsQueue::new(Uuid::from_u128(1));
1213 for (ability, deadline) in deadlines {
1214 queue.push_start_cast_replacing(Uuid::from_u128(*ability), *deadline);
1215 }
1216 queue
1217 }
1218
1219 #[test]
1222 fn cooldowns_are_reported_longest_first_and_ready_abilities_are_omitted() {
1223 let queue = queue_with(&[(1, 1_500), (2, 3_000), (3, 1_000)]);
1224
1225 let remaining = queue.ability_cooldowns(1_000);
1226
1227 assert_eq!(
1228 remaining,
1229 vec![(Uuid::from_u128(2), 2_000), (Uuid::from_u128(1), 500),],
1230 "ability 3 is already ready at tick 1000 and is not a cooldown"
1231 );
1232 }
1233
1234 #[test]
1236 fn shortening_touches_only_the_longest_cooldowns() {
1237 let mut queue = queue_with(&[(1, 2_000), (2, 5_000)]);
1238
1239 assert_eq!(queue.shorten_longest_cooldowns(0, 1, 800), 1);
1240
1241 assert_eq!(
1242 queue.ability_cooldowns(0),
1243 vec![(Uuid::from_u128(2), 4_200), (Uuid::from_u128(1), 2_000),],
1244 "only the longest one moved"
1245 );
1246 }
1247
1248 #[test]
1251 fn shortening_past_now_just_makes_the_ability_ready() {
1252 let mut queue = queue_with(&[(1, 1_200)]);
1253
1254 queue.shorten_longest_cooldowns(1_000, 1, 10_000);
1255
1256 assert!(
1257 queue.ability_cooldowns(1_000).is_empty(),
1258 "the ability is ready, and no deadline went behind the clock"
1259 );
1260 assert_eq!(queue.start_cast_deadlines(Uuid::from_u128(1)), vec![1_000]);
1261 }
1262
1263 #[test]
1265 fn clearing_makes_every_cooldown_ready() {
1266 let mut queue = queue_with(&[(1, 2_000), (2, 9_000), (3, 500)]);
1267
1268 assert_eq!(
1269 queue.clear_ability_cooldowns(1_000),
1270 2,
1271 "the two still on cooldown at tick 1000"
1272 );
1273 assert!(queue.ability_cooldowns(1_000).is_empty());
1274 }
1275
1276 #[test]
1278 fn an_empty_queue_is_a_no_op() {
1279 let mut queue = EntityActionsQueue::new(Uuid::from_u128(1));
1280 assert!(queue.ability_cooldowns(0).is_empty());
1281 assert_eq!(queue.shorten_longest_cooldowns(0, 3, 500), 0);
1282 assert_eq!(queue.clear_ability_cooldowns(0), 0);
1283 }
1284}