1use core::iter;
4
5use simdnbt::owned::{NbtCompound, NbtList};
6use steel_protocol::packets::game::{AttributeModifierData, AttributeSnapshot};
7pub use steel_registry::attribute::AttributeModifierOperation;
8use steel_registry::attribute::AttributeRef;
9use steel_registry::entity_type::EntityTypeRef;
10use steel_registry::{REGISTRY, RegistryEntry, RegistryExt};
11use steel_utils::Identifier;
12
13pub struct DirtySet {
15 chunks: Vec<u64>,
16}
17
18impl Default for DirtySet {
19 fn default() -> Self {
20 Self::new()
21 }
22}
23
24impl DirtySet {
25 #[must_use]
27 pub const fn new() -> Self {
28 Self { chunks: Vec::new() }
29 }
30
31 pub fn mark(&mut self, id: u16) {
33 let chunk = id as usize / 64;
34 let bit = id as usize % 64;
35 if chunk >= self.chunks.len() {
36 self.chunks.resize(chunk + 1, 0);
37 }
38 self.chunks[chunk] |= 1 << bit;
39 }
40
41 #[must_use]
43 pub fn is_empty(&self) -> bool {
44 self.chunks.iter().all(|&c| c == 0)
45 }
46
47 pub fn drain(&mut self) -> impl Iterator<Item = u16> + '_ {
49 self.chunks.iter_mut().enumerate().flat_map(|(i, chunk)| {
50 let mut bits = *chunk;
51 *chunk = 0;
52 iter::from_fn(move || {
53 if bits == 0 {
54 return None;
55 }
56 let bit = bits.trailing_zeros() as u16;
57 bits &= bits - 1;
58 Some(i as u16 * 64 + bit)
59 })
60 })
61 }
62}
63
64#[derive(Clone, Debug)]
66pub struct AttributeModifier {
67 pub id: Identifier,
69 pub amount: f64,
71 pub operation: AttributeModifierOperation,
73}
74
75pub struct AttributeInstance {
77 attribute: AttributeRef,
78 base_value: f64,
79 modifiers: Vec<AttributeModifier>,
80 persistent: Vec<bool>,
82 cached_value: f64,
83}
84
85impl AttributeInstance {
86 const fn new(attribute: AttributeRef, base_value: f64) -> Self {
87 let cached = attribute.sanitize_value(base_value);
88 Self {
89 attribute,
90 base_value,
91 modifiers: Vec::new(),
92 persistent: Vec::new(),
93 cached_value: cached,
94 }
95 }
96
97 #[must_use]
99 pub const fn attribute(&self) -> AttributeRef {
100 self.attribute
101 }
102
103 #[must_use]
105 pub const fn base_value(&self) -> f64 {
106 self.base_value
107 }
108
109 #[expect(
111 clippy::float_cmp,
112 reason = "vanilla uses exact base value equality for dirty checks"
113 )]
114 pub fn set_base_value(&mut self, value: f64) -> bool {
115 if self.base_value == value {
116 return false;
117 }
118 self.base_value = value;
119 self.recalculate();
120 true
121 }
122
123 #[must_use]
125 pub const fn value(&self) -> f64 {
126 self.cached_value
127 }
128
129 pub fn add_modifier(&mut self, modifier: AttributeModifier, persistent: bool) -> bool {
131 if self.modifiers.iter().any(|m| m.id == modifier.id) {
132 return false;
133 }
134 self.modifiers.push(modifier);
135 self.persistent.push(persistent);
136 self.recalculate();
137 true
138 }
139
140 #[must_use]
142 pub fn has_modifier(&self, id: &Identifier) -> bool {
143 self.modifiers.iter().any(|modifier| modifier.id == *id)
144 }
145
146 #[expect(
148 clippy::float_cmp,
149 reason = "exact equality is intentional — we want to skip recalculation when the modifier is identical"
150 )]
151 pub fn set_modifier(&mut self, modifier: AttributeModifier, persistent: bool) -> bool {
152 if let Some(idx) = self.modifiers.iter().position(|m| m.id == modifier.id) {
153 let existing = &self.modifiers[idx];
154 if existing.amount == modifier.amount
155 && existing.operation == modifier.operation
156 && self.persistent[idx] == persistent
157 {
158 return false;
159 }
160 self.modifiers[idx] = modifier;
161 self.persistent[idx] = persistent;
162 } else {
163 self.modifiers.push(modifier);
164 self.persistent.push(persistent);
165 }
166 self.recalculate();
167 true
168 }
169
170 pub fn remove_modifier(&mut self, id: &Identifier) -> bool {
172 let Some(idx) = self.modifiers.iter().position(|m| m.id == *id) else {
173 return false;
174 };
175 self.modifiers.swap_remove(idx);
176 self.persistent.swap_remove(idx);
177 self.recalculate();
178 true
179 }
180
181 pub fn permanent_modifiers(&self) -> impl Iterator<Item = &AttributeModifier> {
183 self.modifiers
184 .iter()
185 .zip(self.persistent.iter())
186 .filter(|&(_, p)| *p)
187 .map(|(m, _)| m)
188 }
189
190 fn remove_transient_modifiers(&mut self) -> bool {
192 if !self.persistent.iter().any(|&p| !p) {
193 return false;
194 }
195 let mut i = 0;
196 while i < self.modifiers.len() {
197 if self.persistent[i] {
198 i += 1;
199 } else {
200 self.modifiers.swap_remove(i);
201 self.persistent.swap_remove(i);
202 }
203 }
204 self.recalculate();
205 true
206 }
207
208 fn remove_modifiers(&mut self) -> bool {
210 if self.modifiers.is_empty() {
211 return false;
212 }
213
214 self.modifiers.clear();
215 self.persistent.clear();
216 self.recalculate();
217 true
218 }
219
220 fn recalculate(&mut self) {
225 let mut base = self.base_value;
226 for m in &self.modifiers {
227 if m.operation == AttributeModifierOperation::AddValue {
228 base += m.amount;
229 }
230 }
231
232 let mut result = base;
233 for m in &self.modifiers {
234 if m.operation == AttributeModifierOperation::AddMultipliedBase {
235 result += base * m.amount;
236 }
237 }
238 for m in &self.modifiers {
239 if m.operation == AttributeModifierOperation::AddMultipliedTotal {
240 result *= 1.0 + m.amount;
241 }
242 }
243
244 self.cached_value = self.attribute.sanitize_value(result);
245 }
246
247 fn to_snapshot(&self, attribute_id: i32) -> AttributeSnapshot {
249 AttributeSnapshot {
250 attribute_id,
251 base_value: self.base_value,
252 modifiers: self
253 .modifiers
254 .iter()
255 .map(|m| AttributeModifierData {
256 id: m.id.clone(),
257 amount: m.amount,
258 operation: m.operation,
259 })
260 .collect(),
261 }
262 }
263}
264
265pub struct AttributeMap {
271 instances: Vec<Option<AttributeInstance>>,
272 to_update: DirtySet,
273 to_sync: DirtySet,
274}
275
276impl AttributeMap {
277 #[must_use]
284 pub fn new_for_entity(entity_type: EntityTypeRef) -> Self {
285 let attr_count = REGISTRY.attributes.len();
286 let mut instances = Vec::with_capacity(attr_count);
287 instances.resize_with(attr_count, || None);
288
289 for &(attr_name, base_value) in entity_type.default_attributes {
290 let key = Identifier::vanilla_static(attr_name);
291 let Some(id) = REGISTRY.attributes.id_from_key(&key) else {
292 panic!(
293 "default attributes for entity type {} reference unregistered attribute {key}",
294 entity_type.key
295 );
296 };
297 let Some(attr) = REGISTRY.attributes.by_id(id) else {
298 panic!("attribute registry id {id} for default attribute {key} is not registered");
299 };
300 instances[id] = Some(AttributeInstance::new(attr, base_value));
301 }
302
303 Self {
304 instances,
305 to_update: DirtySet::new(),
306 to_sync: DirtySet::new(),
307 }
308 }
309
310 #[must_use]
312 pub fn has_attribute(&self, attribute: AttributeRef) -> bool {
313 attribute
314 .try_id()
315 .and_then(|id| self.instances.get(id))
316 .is_some_and(Option::is_some)
317 }
318
319 #[must_use]
321 pub fn get_value(&self, attribute: AttributeRef) -> Option<f64> {
322 let id = attribute.try_id()?;
323 self.instances
324 .get(id)?
325 .as_ref()
326 .map(AttributeInstance::value)
327 }
328
329 #[must_use]
338 pub fn required_value(&self, attribute: AttributeRef) -> f64 {
339 let Some(value) = self.get_value(attribute) else {
340 panic!("required attribute {} is missing", attribute.key);
341 };
342 value
343 }
344
345 #[must_use]
347 pub fn get_base_value(&self, attribute: AttributeRef) -> Option<f64> {
348 let id = attribute.try_id()?;
349 self.instances
350 .get(id)?
351 .as_ref()
352 .map(AttributeInstance::base_value)
353 }
354
355 #[must_use]
357 pub fn get_instance(&self, attribute: AttributeRef) -> Option<&AttributeInstance> {
358 let id = attribute.try_id()?;
359 self.instances.get(id)?.as_ref()
360 }
361
362 #[must_use]
364 pub(crate) fn to_vanilla_nbt(&self) -> NbtList {
365 let attributes = self
366 .instances
367 .iter()
368 .flatten()
369 .map(|instance| {
370 let mut attribute = NbtCompound::new();
371 attribute.insert("id", instance.attribute().key.to_string());
372 attribute.insert("base", instance.base_value());
373
374 let modifiers = instance
375 .permanent_modifiers()
376 .map(|modifier| {
377 let mut packed = NbtCompound::new();
378 packed.insert("id", modifier.id.to_string());
379 packed.insert("amount", modifier.amount);
380 packed.insert("operation", modifier.operation.name());
381 packed
382 })
383 .collect::<Vec<_>>();
384 if !modifiers.is_empty() {
385 attribute.insert("modifiers", NbtList::Compound(modifiers));
386 }
387
388 attribute
389 })
390 .collect();
391 NbtList::Compound(attributes)
392 }
393
394 #[must_use]
396 pub fn has_modifier(&self, attribute: AttributeRef, modifier_id: &Identifier) -> bool {
397 self.get_instance(attribute)
398 .is_some_and(|instance| instance.has_modifier(modifier_id))
399 }
400
401 pub fn set_base_value(&mut self, attribute: AttributeRef, value: f64) {
403 let Some(id) = attribute.try_id() else { return };
404 let Some(Some(instance)) = self.instances.get_mut(id) else {
405 return;
406 };
407 if instance.set_base_value(value) {
408 self.mark_dirty(id, attribute);
409 }
410 }
411
412 pub fn assign_base_values(&mut self, other: &Self) {
414 let Self {
415 instances,
416 to_update,
417 to_sync,
418 } = self;
419
420 for (id, other_instance) in other.instances.iter().enumerate() {
421 let Some(other_instance) = other_instance else {
422 continue;
423 };
424 let Some(Some(instance)) = instances.get_mut(id) else {
425 continue;
426 };
427 if instance.set_base_value(other_instance.base_value()) {
428 to_update.mark(id as u16);
429 if instance.attribute.syncable {
430 to_sync.mark(id as u16);
431 }
432 }
433 }
434 }
435
436 pub fn assign_permanent_modifiers(&mut self, other: &Self) {
443 let Self {
444 instances,
445 to_update,
446 to_sync,
447 } = self;
448
449 for (id, other_instance) in other.instances.iter().enumerate() {
450 let Some(other_instance) = other_instance else {
451 continue;
452 };
453 let Some(Some(instance)) = instances.get_mut(id) else {
454 continue;
455 };
456
457 for modifier in other_instance.permanent_modifiers() {
458 assert!(
459 instance.add_modifier(modifier.clone(), true),
460 "modifier {} is already applied to attribute {}",
461 modifier.id,
462 instance.attribute.key
463 );
464 to_update.mark(id as u16);
465 if instance.attribute.syncable {
466 to_sync.mark(id as u16);
467 }
468 }
469 }
470 }
471
472 pub fn add_modifier(
474 &mut self,
475 attribute: AttributeRef,
476 modifier: AttributeModifier,
477 persistent: bool,
478 ) -> bool {
479 let Some(id) = attribute.try_id() else {
480 return false;
481 };
482 let Some(Some(instance)) = self.instances.get_mut(id) else {
483 return false;
484 };
485 if instance.add_modifier(modifier, persistent) {
486 self.mark_dirty(id, attribute);
487 true
488 } else {
489 false
490 }
491 }
492
493 pub fn set_modifier(
495 &mut self,
496 attribute: AttributeRef,
497 modifier: AttributeModifier,
498 persistent: bool,
499 ) {
500 let Some(id) = attribute.try_id() else { return };
501 let Some(Some(instance)) = self.instances.get_mut(id) else {
502 return;
503 };
504 if instance.set_modifier(modifier, persistent) {
505 self.mark_dirty(id, attribute);
506 }
507 }
508
509 pub fn remove_modifier(&mut self, attribute: AttributeRef, modifier_id: &Identifier) -> bool {
511 let Some(id) = attribute.try_id() else {
512 return false;
513 };
514 let Some(Some(instance)) = self.instances.get_mut(id) else {
515 return false;
516 };
517 if instance.remove_modifier(modifier_id) {
518 self.mark_dirty(id, attribute);
519 true
520 } else {
521 false
522 }
523 }
524
525 fn mark_dirty(&mut self, id: usize, attribute: AttributeRef) {
526 let id = id as u16;
527 self.to_update.mark(id);
528 if attribute.syncable {
529 self.to_sync.mark(id);
530 }
531 }
532
533 #[must_use]
535 pub fn has_dirty_updates(&self) -> bool {
536 !self.to_update.is_empty()
537 }
538
539 #[must_use]
541 pub fn has_dirty_sync(&self) -> bool {
542 !self.to_sync.is_empty()
543 }
544
545 pub fn drain_dirty_updates(&mut self) -> Vec<AttributeRef> {
547 self.to_update
548 .drain()
549 .filter_map(|id| {
550 self.instances
551 .get(id as usize)?
552 .as_ref()
553 .map(|inst| inst.attribute)
554 })
555 .collect()
556 }
557
558 pub fn drain_dirty_sync(&mut self) -> Vec<AttributeSnapshot> {
560 let ids: Vec<u16> = self.to_sync.drain().collect();
561 let mut snapshots = Vec::with_capacity(ids.len());
562 for id in ids {
563 if let Some(Some(instance)) = self.instances.get(id as usize) {
564 snapshots.push(instance.to_snapshot(i32::from(id)));
565 }
566 }
567 snapshots
568 }
569
570 #[must_use]
572 pub fn syncable_snapshots(&self) -> Vec<AttributeSnapshot> {
573 let mut snapshots = Vec::new();
574 for (id, slot) in self.instances.iter().enumerate() {
575 if let Some(inst) = slot
576 && inst.attribute.syncable
577 {
578 snapshots.push(inst.to_snapshot(id as i32));
579 }
580 }
581 snapshots
582 }
583
584 pub fn remove_all_transient(&mut self) {
586 let Self {
587 instances,
588 to_update,
589 to_sync,
590 } = self;
591 for (id, slot) in instances.iter_mut().enumerate() {
592 if let Some(inst) = slot
593 && inst.remove_transient_modifiers()
594 {
595 to_update.mark(id as u16);
596 if inst.attribute.syncable {
597 to_sync.mark(id as u16);
598 }
599 }
600 }
601 }
602
603 pub fn remove_all_modifiers(&mut self) {
605 let Self {
606 instances,
607 to_update,
608 to_sync,
609 } = self;
610 for (id, slot) in instances.iter_mut().enumerate() {
611 if let Some(inst) = slot
612 && inst.remove_modifiers()
613 {
614 to_update.mark(id as u16);
615 if inst.attribute.syncable {
616 to_sync.mark(id as u16);
617 }
618 }
619 }
620 }
621}
622
623#[cfg(test)]
624mod tests {
625 use simdnbt::owned::NbtTag;
626 use steel_registry::{REGISTRY, init_vanilla_registry, vanilla_attributes, vanilla_entities};
627
628 use super::*;
629
630 #[test]
631 fn all_generated_entity_default_attributes_resolve() {
632 init_vanilla_registry();
633
634 for (_, entity_type) in REGISTRY.entity_types.iter() {
635 let _ = AttributeMap::new_for_entity(entity_type);
636 }
637 }
638
639 #[test]
640 fn player_gravity_is_initialized_from_default_attributes() {
641 init_vanilla_registry();
642
643 let attributes = AttributeMap::new_for_entity(&vanilla_entities::PLAYER);
644
645 assert_eq!(
646 attributes
647 .required_value(vanilla_attributes::GRAVITY)
648 .to_bits(),
649 vanilla_attributes::GRAVITY.default_value.to_bits()
650 );
651 }
652
653 #[test]
654 fn vanilla_nbt_only_contains_permanent_modifiers() {
655 init_vanilla_registry();
656 let mut attributes = AttributeMap::new_for_entity(&vanilla_entities::PLAYER);
657 attributes.add_modifier(
658 vanilla_attributes::MAX_HEALTH,
659 AttributeModifier {
660 id: Identifier::vanilla_static("transient_test"),
661 amount: 1.0,
662 operation: AttributeModifierOperation::AddValue,
663 },
664 false,
665 );
666 attributes.add_modifier(
667 vanilla_attributes::MAX_HEALTH,
668 AttributeModifier {
669 id: Identifier::vanilla_static("permanent_test"),
670 amount: 2.0,
671 operation: AttributeModifierOperation::AddMultipliedBase,
672 },
673 true,
674 );
675
676 let NbtList::Compound(packed_attributes) = attributes.to_vanilla_nbt() else {
677 panic!("attributes should serialize as a compound list");
678 };
679 let max_health = packed_attributes
680 .iter()
681 .find(|attribute| {
682 attribute.string("id").is_some_and(|id| {
683 id.to_str().as_ref() == vanilla_attributes::MAX_HEALTH.key.to_string()
684 })
685 })
686 .unwrap_or_else(|| panic!("max health should be serialized"));
687 let Some(NbtTag::List(NbtList::Compound(modifiers))) = max_health.get("modifiers") else {
688 panic!("permanent modifier should be serialized");
689 };
690
691 assert_eq!(modifiers.len(), 1);
692 assert_eq!(
693 modifiers[0].string("id").map(ToString::to_string),
694 Some("minecraft:permanent_test".to_owned())
695 );
696 assert_eq!(modifiers[0].double("amount"), Some(2.0));
697 assert_eq!(
698 modifiers[0].string("operation").map(ToString::to_string),
699 Some("add_multiplied_base".to_owned())
700 );
701 }
702
703 #[test]
704 fn removing_all_modifiers_preserves_base_value() {
705 init_vanilla_registry();
706 let mut attributes = AttributeMap::new_for_entity(&vanilla_entities::PLAYER);
707 attributes.set_base_value(vanilla_attributes::MAX_HEALTH, 30.0);
708
709 let transient_id = Identifier::vanilla_static("transient_test");
710 let permanent_id = Identifier::vanilla_static("permanent_test");
711 assert!(attributes.add_modifier(
712 vanilla_attributes::MAX_HEALTH,
713 AttributeModifier {
714 id: Identifier::vanilla_static("transient_test"),
715 amount: 2.0,
716 operation: AttributeModifierOperation::AddValue,
717 },
718 false,
719 ));
720 assert!(attributes.add_modifier(
721 vanilla_attributes::MAX_HEALTH,
722 AttributeModifier {
723 id: Identifier::vanilla_static("permanent_test"),
724 amount: 3.0,
725 operation: AttributeModifierOperation::AddValue,
726 },
727 true,
728 ));
729
730 attributes.remove_all_modifiers();
731
732 assert_eq!(
733 attributes
734 .get_base_value(vanilla_attributes::MAX_HEALTH)
735 .map(f64::to_bits),
736 Some(30.0_f64.to_bits())
737 );
738 assert_eq!(
739 attributes
740 .get_value(vanilla_attributes::MAX_HEALTH)
741 .map(f64::to_bits),
742 Some(30.0_f64.to_bits())
743 );
744 assert!(!attributes.has_modifier(vanilla_attributes::MAX_HEALTH, &transient_id));
745 assert!(!attributes.has_modifier(vanilla_attributes::MAX_HEALTH, &permanent_id));
746 }
747
748 #[test]
749 fn assigning_base_values_only_changes_attributes_present_in_both_maps() {
750 init_vanilla_registry();
751 let mut source = AttributeMap::new_for_entity(&vanilla_entities::ZOMBIE);
752 let mut destination = AttributeMap::new_for_entity(&vanilla_entities::PLAYER);
753
754 source.set_base_value(vanilla_attributes::MAX_HEALTH, 37.0);
755 source.set_base_value(vanilla_attributes::ATTACK_DAMAGE, 8.0);
756 destination.set_base_value(vanilla_attributes::LUCK, 9.0);
757 destination.drain_dirty_updates();
758 destination.drain_dirty_sync();
759
760 destination.assign_base_values(&source);
761 let Some(max_health_id) = vanilla_attributes::MAX_HEALTH.try_id() else {
762 panic!("max health should be registered");
763 };
764 let Some(attack_damage_id) = vanilla_attributes::ATTACK_DAMAGE.try_id() else {
765 panic!("attack damage should be registered");
766 };
767
768 assert_eq!(
769 destination
770 .get_base_value(vanilla_attributes::MAX_HEALTH)
771 .map(f64::to_bits),
772 Some(37.0_f64.to_bits())
773 );
774 assert_eq!(
775 destination
776 .get_base_value(vanilla_attributes::LUCK)
777 .map(f64::to_bits),
778 Some(9.0_f64.to_bits())
779 );
780 assert_eq!(
781 destination
782 .get_base_value(vanilla_attributes::ATTACK_DAMAGE)
783 .map(f64::to_bits),
784 Some(8.0_f64.to_bits())
785 );
786 let dirty_updates = destination.drain_dirty_updates();
787 assert!(
788 dirty_updates
789 .iter()
790 .any(|attribute| attribute.key == vanilla_attributes::MAX_HEALTH.key)
791 );
792 assert!(
793 dirty_updates
794 .iter()
795 .any(|attribute| attribute.key == vanilla_attributes::ATTACK_DAMAGE.key)
796 );
797 let dirty_sync = destination.drain_dirty_sync();
798 assert!(
799 dirty_sync
800 .iter()
801 .any(|snapshot| snapshot.attribute_id == max_health_id as i32)
802 );
803 assert!(
804 dirty_sync
805 .iter()
806 .all(|snapshot| snapshot.attribute_id != attack_damage_id as i32)
807 );
808
809 destination.assign_base_values(&source);
810
811 assert!(!destination.has_dirty_updates());
812 assert!(!destination.has_dirty_sync());
813 }
814
815 #[test]
816 fn assigning_permanent_modifiers_adds_only_permanent_source_modifiers() {
817 init_vanilla_registry();
818 let mut source = AttributeMap::new_for_entity(&vanilla_entities::PLAYER);
819 let mut destination = AttributeMap::new_for_entity(&vanilla_entities::PLAYER);
820 let source_transient_id = Identifier::vanilla_static("source_transient_test");
821 let source_permanent_id = Identifier::vanilla_static("source_permanent_test");
822 let destination_transient_id = Identifier::vanilla_static("destination_transient_test");
823 let destination_permanent_id = Identifier::vanilla_static("destination_permanent_test");
824
825 source.set_base_value(vanilla_attributes::MAX_HEALTH, 40.0);
826 assert!(source.add_modifier(
827 vanilla_attributes::MAX_HEALTH,
828 AttributeModifier {
829 id: source_transient_id.clone(),
830 amount: 7.0,
831 operation: AttributeModifierOperation::AddValue,
832 },
833 false,
834 ));
835 assert!(source.add_modifier(
836 vanilla_attributes::MAX_HEALTH,
837 AttributeModifier {
838 id: source_permanent_id.clone(),
839 amount: 5.0,
840 operation: AttributeModifierOperation::AddValue,
841 },
842 true,
843 ));
844 destination.set_base_value(vanilla_attributes::MAX_HEALTH, 25.0);
845 assert!(destination.add_modifier(
846 vanilla_attributes::MAX_HEALTH,
847 AttributeModifier {
848 id: destination_transient_id.clone(),
849 amount: 2.0,
850 operation: AttributeModifierOperation::AddValue,
851 },
852 false,
853 ));
854 assert!(destination.add_modifier(
855 vanilla_attributes::MAX_HEALTH,
856 AttributeModifier {
857 id: destination_permanent_id.clone(),
858 amount: 3.0,
859 operation: AttributeModifierOperation::AddValue,
860 },
861 true,
862 ));
863 destination.drain_dirty_updates();
864 destination.drain_dirty_sync();
865
866 destination.assign_permanent_modifiers(&source);
867
868 assert_eq!(
869 destination
870 .get_base_value(vanilla_attributes::MAX_HEALTH)
871 .map(f64::to_bits),
872 Some(25.0_f64.to_bits())
873 );
874 assert_eq!(
875 destination
876 .get_value(vanilla_attributes::MAX_HEALTH)
877 .map(f64::to_bits),
878 Some(35.0_f64.to_bits())
879 );
880 assert!(!destination.has_modifier(vanilla_attributes::MAX_HEALTH, &source_transient_id));
881 assert!(destination.has_modifier(vanilla_attributes::MAX_HEALTH, &source_permanent_id));
882 assert!(
883 destination.has_modifier(vanilla_attributes::MAX_HEALTH, &destination_transient_id)
884 );
885 assert!(
886 destination.has_modifier(vanilla_attributes::MAX_HEALTH, &destination_permanent_id)
887 );
888 assert!(
889 destination
890 .get_instance(vanilla_attributes::MAX_HEALTH)
891 .is_some_and(|instance| {
892 instance
893 .permanent_modifiers()
894 .any(|modifier| modifier.id == source_permanent_id)
895 })
896 );
897 assert!(destination.has_dirty_updates());
898 assert!(destination.has_dirty_sync());
899 }
900
901 #[test]
902 #[should_panic(expected = "is already applied")]
903 fn assigning_permanent_modifiers_rejects_duplicate_ids() {
904 init_vanilla_registry();
905 let mut source = AttributeMap::new_for_entity(&vanilla_entities::PLAYER);
906 let mut destination = AttributeMap::new_for_entity(&vanilla_entities::PLAYER);
907 let modifier_id = Identifier::vanilla_static("duplicate_test");
908
909 for attributes in [&mut source, &mut destination] {
910 assert!(attributes.add_modifier(
911 vanilla_attributes::MAX_HEALTH,
912 AttributeModifier {
913 id: modifier_id.clone(),
914 amount: 1.0,
915 operation: AttributeModifierOperation::AddValue,
916 },
917 true,
918 ));
919 }
920
921 destination.assign_permanent_modifiers(&source);
922 }
923}