1use std::sync::{Arc, Weak};
8
9use glam::DVec3;
10use steel_macros::entity_behavior;
11use steel_math::DEGREE_360;
12use steel_registry::entity_type::EntityTypeRef;
13use steel_registry::item_stack::ItemStack;
14use steel_registry::vanilla_entity_data::ItemEntityData;
15use steel_registry::vanilla_game_rules::MOB_GRIEFING;
16use steel_registry::{vanilla_custom_stats, vanilla_damage_types};
17use steel_utils::UuidExt;
18use steel_utils::locks::SyncMutex;
19use steel_utils::nbt::NbtValueInput as _;
20use steel_utils::{Downcast as _, DowncastType, DowncastTypeKey};
21use uuid::Uuid;
22
23use crate::entity::damage::DamageSource;
24use crate::entity::{
25 DEFAULT_AIR_DRAG, DefaultEntityPush, Entity, EntityBase, EntityBaseLoad, EntityBaseState,
26 EntityMovementEmission, EntitySyncedData, RemovalReason,
27};
28use crate::inventory::container::Container;
29use crate::physics::MoverType;
30use crate::player::Player;
31use crate::world::{Explosion, World};
32
33use simdnbt::ToNbtTag;
34use simdnbt::borrow::NbtCompound as BorrowedNbtCompoundView;
35use simdnbt::owned::{NbtCompound, NbtTag};
36use steel_protocol::packets::game::CTakeItemEntity;
37use steel_registry::blocks::block_state_ext::BlockStateExt;
38use steel_registry::stat::vanilla_stat_types;
39use steel_registry::vanilla_item_tags::ItemTag;
40use steel_utils::BlockPos;
41
42const LIFETIME: i32 = 6000;
44
45const DEFAULT_PICKUP_DELAY: i32 = 10;
48
49const INFINITE_PICKUP_DELAY: i32 = 32767;
51
52const INFINITE_LIFETIME: i32 = -32768;
54
55const DEFAULT_HEALTH: i32 = 5;
57
58const DEFAULT_GRAVITY: f64 = 0.04;
60
61const FLUID_VERTICAL_NUDGE: f64 = 5.0e-4;
63const ITEM_FLUID_HEIGHT_THRESHOLD: f64 = 0.1;
64const ITEM_WATER_DRAG: f64 = 0.99;
65const ITEM_LAVA_DRAG: f64 = 0.95;
66const MERGE_MAX_STACK_SIZE: i32 = 64;
67
68struct ItemEntityState {
71 age: i32,
73 pickup_delay: i32,
75 health: i32,
77 thrower: Option<Uuid>,
79 owner: Option<Uuid>,
82}
83
84impl ItemEntityState {
85 const fn new() -> Self {
86 Self {
87 age: 0,
88 pickup_delay: 0,
89 health: DEFAULT_HEALTH,
90 thrower: None,
91 owner: None,
92 }
93 }
94}
95
96#[entity_behavior]
104pub struct ItemEntity {
105 base: EntityBase,
107
108 entity_type: EntityTypeRef,
110
111 entity_data: SyncMutex<ItemEntityData>,
113
114 item_state: SyncMutex<ItemEntityState>,
116}
117
118unsafe impl DowncastType for ItemEntity {
120 const TYPE_KEY: DowncastTypeKey = DowncastTypeKey::new("steel:entity/item");
121}
122
123impl ItemEntity {
124 #[must_use]
128 pub fn new(entity_type: EntityTypeRef, id: i32, position: DVec3, world: Weak<World>) -> Self {
129 Self::with_item_and_velocity(
130 entity_type,
131 id,
132 position,
133 ItemStack::empty(),
134 DVec3::ZERO,
135 world,
136 )
137 }
138
139 #[must_use]
141 pub fn with_item(
142 entity_type: EntityTypeRef,
143 id: i32,
144 position: DVec3,
145 item: ItemStack,
146 world: Weak<World>,
147 ) -> Self {
148 Self::with_item_and_velocity(
149 entity_type,
150 id,
151 position,
152 item,
153 Self::default_spawn_velocity(),
154 world,
155 )
156 }
157
158 #[must_use]
160 pub fn with_item_and_velocity(
161 entity_type: EntityTypeRef,
162 id: i32,
163 position: DVec3,
164 item: ItemStack,
165 velocity: DVec3,
166 world: Weak<World>,
167 ) -> Self {
168 let yaw = rand::random_range(0.0..DEGREE_360);
170
171 let mut entity_data = ItemEntityData::new();
172 entity_data.item.set(item);
173
174 Self {
175 base: EntityBase::new_with_state(
176 id,
177 EntityBaseState::new(position, entity_type.dimensions)
178 .with_velocity(velocity)
179 .with_rotation((yaw, 0.0)),
180 world,
181 ),
182 entity_type,
183 entity_data: SyncMutex::new(entity_data),
184 item_state: SyncMutex::new(ItemEntityState::new()),
185 }
186 }
187
188 pub(crate) fn default_spawn_velocity() -> DVec3 {
189 DVec3::new(
190 rand::random_range(-0.1..0.1),
191 0.2,
192 rand::random_range(-0.1..0.1),
193 )
194 }
195
196 #[must_use]
201 pub fn from_saved(entity_type: EntityTypeRef, load: EntityBaseLoad) -> Self {
202 Self {
203 base: EntityBase::from_load(load, entity_type.dimensions),
204 entity_type,
205 entity_data: SyncMutex::new(ItemEntityData::new()),
206 item_state: SyncMutex::new(ItemEntityState::new()),
207 }
208 }
209
210 #[must_use]
212 pub fn get_item(&self) -> ItemStack {
213 self.entity_data.lock().item.get().clone()
214 }
215
216 pub fn set_item(&self, item: ItemStack) {
218 self.entity_data.lock().item.set(item);
219 }
220
221 #[must_use]
223 pub fn get_age(&self) -> i32 {
224 self.item_state.lock().age
225 }
226
227 pub fn set_age(&self, age: i32) {
229 self.item_state.lock().age = age;
230 }
231
232 pub fn make_fake_item(&self) {
234 let mut state = self.item_state.lock();
235 state.pickup_delay = INFINITE_PICKUP_DELAY;
236 state.age = LIFETIME - 1;
237 }
238
239 #[must_use]
241 pub fn get_pickup_delay(&self) -> i32 {
242 self.item_state.lock().pickup_delay
243 }
244
245 pub fn set_default_pickup_delay(&self) {
247 self.item_state.lock().pickup_delay = DEFAULT_PICKUP_DELAY;
248 }
249
250 pub fn set_no_pickup_delay(&self) {
252 self.item_state.lock().pickup_delay = 0;
253 }
254
255 pub fn set_pickup_delay(&self, delay: i32) {
257 self.item_state.lock().pickup_delay = delay;
258 }
259
260 #[must_use]
262 pub fn has_pickup_delay(&self) -> bool {
263 self.item_state.lock().pickup_delay > 0
264 }
265
266 #[must_use]
268 pub fn get_health(&self) -> i32 {
269 self.item_state.lock().health
270 }
271
272 pub fn set_health(&self, health: i32) {
274 self.item_state.lock().health = health;
275 }
276
277 pub fn set_thrower(&self, uuid: Uuid) {
279 self.item_state.lock().thrower = Some(uuid);
280 }
281
282 #[must_use]
284 pub fn get_thrower(&self) -> Option<Uuid> {
285 self.item_state.lock().thrower
286 }
287
288 pub fn set_owner(&self, uuid: Option<Uuid>) {
293 self.item_state.lock().owner = uuid;
294 }
295
296 #[must_use]
300 pub fn get_owner(&self) -> Option<Uuid> {
301 self.item_state.lock().owner
302 }
303
304 pub fn try_pickup(&self, player: &Arc<Player>) -> bool {
309 if self.has_pickup_delay() {
311 return false;
312 }
313
314 if let Some(owner_uuid) = self.get_owner()
316 && owner_uuid != player.gameprofile.id()
317 {
318 return false;
319 }
320
321 let mut item = self.get_item();
323 let original_count = item.count();
324
325 let added = player.inventory.lock().add(&mut item);
327
328 if item.count() == original_count {
330 return false;
331 }
332
333 let picked_up_count = original_count - item.count();
335
336 player.award_stat_with_count(
340 &vanilla_stat_types::ITEM_PICKED_UP,
341 item.item,
342 original_count,
343 );
344
345 if item.item.has_tag(&ItemTag::FISHES) {
346 player.award_custom_stat(&vanilla_custom_stats::FISH_CAUGHT);
347 }
348
349 if let Some(world) = self.level() {
351 let pos = self.position();
352 let chunk_pos = steel_utils::ChunkPos::from_entity_pos(pos);
353
354 let take_packet = CTakeItemEntity::new(self.id(), player.id(), picked_up_count);
355 world.broadcast_to_nearby(chunk_pos, take_packet, None);
356 }
357
358 if added {
360 self.set_removed(RemovalReason::Discarded);
362 true
363 } else {
364 self.set_item(item);
366 false
367 }
368 }
369
370 #[must_use]
379 pub fn is_mergeable(&self) -> bool {
380 let item = self.get_item();
381 let state = self.item_state.lock();
382 !self.is_removed()
383 && state.pickup_delay != INFINITE_PICKUP_DELAY
384 && state.age != INFINITE_LIFETIME
385 && state.age < LIFETIME
386 && item.count() < item.max_stack_size()
387 }
388
389 #[must_use]
394 pub fn are_mergeable(this_stack: &ItemStack, other_stack: &ItemStack) -> bool {
395 if other_stack.count() + this_stack.count() > other_stack.max_stack_size() {
397 return false;
398 }
399 ItemStack::is_same_item_same_components(this_stack, other_stack)
401 }
402
403 fn try_to_merge(&self, other: &Self) {
407 let this_stack = self.get_item();
408 let other_stack = other.get_item();
409
410 if self.get_owner() != other.get_owner() {
412 return;
413 }
414
415 if !Self::are_mergeable(&this_stack, &other_stack) {
416 return;
417 }
418
419 if other_stack.count() < this_stack.count() {
421 Self::merge_stacks(self, &this_stack, other, &other_stack);
422 } else {
423 Self::merge_stacks(other, &other_stack, self, &this_stack);
424 }
425 }
426
427 fn merge_stacks(
429 to_item: &Self,
430 to_stack: &ItemStack,
431 from_item: &Self,
432 from_stack: &ItemStack,
433 ) {
434 let max_count = to_stack.max_stack_size().min(MERGE_MAX_STACK_SIZE);
436 if to_stack.count() >= max_count {
437 return;
438 }
439 let space_available = max_count - to_stack.count();
440 let transfer_count = space_available.min(from_stack.count());
441
442 let new_to_stack = to_stack.copy_with_count(to_stack.count() + transfer_count);
444 let mut new_from_stack = from_stack.clone();
445 new_from_stack.shrink(transfer_count);
446
447 let (from_pickup_delay, from_age) = {
449 let state = from_item.item_state.lock();
450 (state.pickup_delay, state.age)
451 };
452 to_item.set_item(new_to_stack);
453
454 {
457 let mut state = to_item.item_state.lock();
458 state.pickup_delay = state.pickup_delay.max(from_pickup_delay);
459 state.age = state.age.min(from_age);
460 }
461
462 if new_from_stack.is_empty() {
463 from_item.set_removed(RemovalReason::Discarded);
464 } else {
465 from_item.set_item(new_from_stack);
466 }
467 }
468
469 pub fn merge_with_neighbors(&self, world: &Arc<World>) {
474 if !self.is_mergeable() {
475 return;
476 }
477
478 let search_box = self.bounding_box().inflate_xyz(0.5, 0.0, 0.5);
480
481 for entity in world.get_entities_in_aabb(&search_box) {
483 if entity.id() == self.id() {
485 continue;
486 }
487
488 if let Some(other_item) = entity.downcast_ref::<Self>() {
489 if other_item.is_mergeable() {
491 self.try_to_merge(other_item);
492
493 if self.is_removed() {
495 break;
496 }
497 }
498 }
499 }
500 }
501
502 fn apply_fluid_movement_or_gravity(&self) {
503 let contact = self.fluid_contact();
504 if contact.water_height() > ITEM_FLUID_HEIGHT_THRESHOLD {
505 self.apply_fluid_movement(ITEM_WATER_DRAG);
506 } else if contact.lava_height() > ITEM_FLUID_HEIGHT_THRESHOLD {
507 self.apply_fluid_movement(ITEM_LAVA_DRAG);
508 } else {
509 self.apply_gravity();
510 }
511 }
512
513 fn apply_fluid_movement(&self, horizontal_drag: f64) {
514 let movement = self.velocity();
515 self.set_velocity(DVec3::new(
516 movement.x * horizontal_drag,
517 movement.y
518 + if movement.y < 0.06 {
519 FLUID_VERTICAL_NUDGE
520 } else {
521 0.0
522 },
523 movement.z * horizontal_drag,
524 ));
525 }
526}
527
528impl DefaultEntityPush for ItemEntity {}
529
530impl Entity for ItemEntity {
531 fn is_hard_collision_relevant(&self) -> bool {
532 false
533 }
534
535 fn as_default_entity_push(&self) -> Option<&dyn DefaultEntityPush> {
536 Some(self)
537 }
538
539 fn base(&self) -> &EntityBase {
540 &self.base
541 }
542
543 fn entity_type(&self) -> EntityTypeRef {
544 self.entity_type
545 }
546
547 fn ignore_explosion(&self, explosion: &dyn Explosion) -> bool {
548 !explosion.should_affect_blocklike_entities()
549 }
550
551 fn movement_emission(&self) -> EntityMovementEmission {
552 EntityMovementEmission::None
553 }
554
555 fn tick(self: Arc<Self>) {
556 if self.get_item().is_empty() {
558 self.set_removed(RemovalReason::Discarded);
559 return;
560 }
561
562 self.default_tick();
563
564 {
565 let mut state = self.item_state.lock();
566 if state.pickup_delay > 0 && state.pickup_delay != INFINITE_PICKUP_DELAY {
567 state.pickup_delay -= 1;
568 }
569 }
570
571 self.set_old_position_to_current();
573 let old_pos = self.old_position();
574 let old_movement = self.velocity();
576 let old_on_ground = self.on_ground();
578
579 self.apply_fluid_movement_or_gravity();
580 self.update_no_physics_from_current_collision();
581
582 let vel = self.velocity();
589 let horizontal_movement_sq = vel.x * vel.x + vel.z * vel.z;
590 let should_move = !self.on_ground()
591 || horizontal_movement_sq > 1.0e-5
592 || (self.tick_count() + self.id()) % 4 == 0;
593
594 if should_move {
595 if let Some(result) =
597 Arc::clone(&self).move_entity(MoverType::SelfMovement, self.velocity())
598 {
599 self.apply_effects_from_blocks();
600 if self.is_removed() {
601 return;
602 }
603
604 if let Some(world) = self.level() {
606 let friction = if result.on_ground
608 && let Some(block_pos) = self.block_pos_below_that_affects_movement()
609 {
610 let block_state = world.get_block_state(block_pos);
611 f64::from(block_state.get_block().config.friction) * DEFAULT_AIR_DRAG
612 } else {
613 DEFAULT_AIR_DRAG
614 };
615
616 let mut velocity = self.velocity();
617 velocity.x *= friction;
618 velocity.z *= friction;
619 velocity.y *= DEFAULT_AIR_DRAG;
620
621 if result.on_ground && velocity.y < 0.0 {
623 velocity.y *= -0.5;
624 }
625
626 self.set_velocity(velocity);
627 }
628 }
629 } else {
630 self.apply_effects_from_blocks_for_last_movements();
631 if self.is_removed() {
632 return;
633 }
634 }
635
636 let current_pos = self.position();
639 let moved_block = old_pos.x.floor() as i32 != current_pos.x.floor() as i32
640 || old_pos.y.floor() as i32 != current_pos.y.floor() as i32
641 || old_pos.z.floor() as i32 != current_pos.z.floor() as i32;
642 let merge_rate = if moved_block { 2 } else { 40 };
643
644 if self.tick_count() % merge_rate == 0
645 && self.is_mergeable()
646 && let Some(world) = self.level()
647 {
648 self.merge_with_neighbors(&world);
649 }
650
651 if self.update_fluid_interaction() {
654 self.mark_velocity_sync();
655 }
656
657 let new_movement = self.velocity();
659 let diff = DVec3::new(
660 new_movement.x - old_movement.x,
661 new_movement.y - old_movement.y,
662 new_movement.z - old_movement.z,
663 );
664 let diff_sq = diff.x * diff.x + diff.y * diff.y + diff.z * diff.z;
665 if diff_sq > 0.01 {
666 self.mark_velocity_sync();
667 }
668
669 if self.on_ground() != old_on_ground {
672 self.mark_velocity_sync();
673 }
674
675 let should_despawn = {
676 let mut state = self.item_state.lock();
677 if state.age == INFINITE_LIFETIME {
678 false
679 } else {
680 state.age += 1;
681 state.age >= LIFETIME
682 }
683 };
684
685 if should_despawn {
686 self.set_removed(RemovalReason::Discarded);
687 }
688 }
689
690 fn get_default_gravity(&self) -> f64 {
691 DEFAULT_GRAVITY
692 }
693
694 fn synced_data(&self) -> Option<&dyn EntitySyncedData> {
695 Some(&self.entity_data)
696 }
697
698 fn block_pos_below_that_affects_movement(&self) -> Option<BlockPos> {
699 self.on_pos(0.999_999)
700 }
701
702 fn attackable(&self) -> bool {
703 false
704 }
705
706 fn should_play_lava_hurt_sound(&self) -> bool {
707 self.get_health() <= 0 || self.tick_count() % 10 == 0
708 }
709
710 fn fire_immune(&self) -> bool {
711 !self
712 .get_item()
713 .can_be_hurt_by(&vanilla_damage_types::IN_FIRE)
714 || self.entity_type().fire_immune
715 }
716
717 fn player_touch(self: Arc<Self>, player: &Arc<Player>) {
718 self.try_pickup(player);
719 }
720
721 fn hurt(&self, world: &World, source: &DamageSource, amount: f32) -> bool {
722 if !world.get_game_rule(&MOB_GRIEFING)
724 && source
725 .causing_entity()
726 .is_some_and(|entity| entity.as_mob().is_some())
727 {
728 return false;
729 }
730 if !self.get_item().can_be_hurt_by(source.damage_type) {
731 return false;
732 }
733 let new_health = {
734 let mut state = self.item_state.lock();
735 state.health = (state.health as f32 - amount) as i32;
736 state.health
737 };
738 if new_health <= 0 {
739 self.set_removed(RemovalReason::Killed);
741 }
742 true
743 }
744
745 fn save_additional(&self, nbt: &mut NbtCompound) {
746 let state = self.item_state.lock();
748 nbt.insert("Health", state.health as i16);
749 nbt.insert("Age", state.age as i16);
750 nbt.insert("PickupDelay", state.pickup_delay as i16);
751
752 if let Some(thrower) = state.thrower {
753 nbt.insert("Thrower", NbtTag::IntArray(thrower.to_int_array().to_vec()));
754 }
755 if let Some(owner) = state.owner {
756 nbt.insert("Owner", NbtTag::IntArray(owner.to_int_array().to_vec()));
757 }
758 drop(state);
759
760 let item = self.get_item();
761 if !item.is_empty() {
762 nbt.insert("Item", item.to_nbt_tag());
763 }
764 }
765
766 fn load_additional(&self, nbt: BorrowedNbtCompoundView<'_, '_>) {
767 let mut state = self.item_state.lock();
769 if let Some(health) = nbt.short_value("Health") {
770 state.health = i32::from(health);
771 }
772 if let Some(age) = nbt.short_value("Age") {
773 state.age = i32::from(age);
774 }
775 if let Some(pickup_delay) = nbt.short_value("PickupDelay") {
776 state.pickup_delay = i32::from(pickup_delay);
777 }
778
779 if let Some(thrower_arr) = nbt.int_array("Thrower")
780 && let Some(uuid) = Uuid::from_int_array(&thrower_arr)
781 {
782 state.thrower = Some(uuid);
783 }
784 if let Some(owner_arr) = nbt.int_array("Owner")
785 && let Some(uuid) = Uuid::from_int_array(&owner_arr)
786 {
787 state.owner = Some(uuid);
788 }
789 drop(state);
790
791 if let Some(item_tag) = nbt.compound("Item")
792 && let Some(item) = ItemStack::from_borrowed_compound(&item_tag)
793 {
794 self.entity_data.lock().item.set(item);
795 }
796
797 if self.get_item().is_empty() {
799 self.set_removed(RemovalReason::Discarded);
800 }
801 }
802}
803
804#[cfg(test)]
805mod tests {
806 use std::sync::{Arc, Weak};
807
808 use glam::DVec3;
809
810 use steel_registry::{
811 init_vanilla_registry, item_stack::ItemStack, vanilla_blocks, vanilla_damage_types,
812 vanilla_entities, vanilla_items,
813 };
814 use steel_utils::{BlockPos, ChunkPos, types::UpdateFlags};
815
816 use crate::behavior::init_behaviors;
817 use crate::entity::{Entity, damage::DamageSource};
818 use crate::test_support::{fresh_test_world, insert_ready_full_chunk, test_world};
819 use crate::world::World;
820
821 use super::ItemEntity;
822
823 #[test]
824 fn item_entities_do_not_obstruct_block_placement() {
825 let item = ItemEntity::new(
826 &vanilla_entities::ITEM,
827 1,
828 DVec3::ZERO,
829 Weak::<World>::new(),
830 );
831
832 assert!(!item.blocks_building());
833 }
834
835 #[test]
836 fn item_lava_hurt_sound_uses_vanilla_interval() {
837 let item = ItemEntity::new(
838 &vanilla_entities::ITEM,
839 1,
840 DVec3::ZERO,
841 Weak::<World>::new(),
842 );
843
844 assert!(item.should_play_lava_hurt_sound());
845 item.advance_tick_count();
846 assert!(!item.should_play_lava_hurt_sound());
847
848 for _ in 1..10 {
849 item.advance_tick_count();
850 }
851 assert!(item.should_play_lava_hurt_sound());
852
853 item.set_health(0);
854 item.advance_tick_count();
855 assert!(item.should_play_lava_hurt_sound());
856 }
857
858 #[test]
859 fn item_with_stack_uses_vanilla_default_velocity() {
860 let item = ItemEntity::with_item(
861 &vanilla_entities::ITEM,
862 1,
863 DVec3::ZERO,
864 ItemStack::new(&vanilla_items::STONE),
865 Weak::<World>::new(),
866 );
867 let velocity = item.velocity();
868
869 assert!(velocity.x >= -0.1);
870 assert!(velocity.x < 0.1);
871 assert_eq!(velocity.y.to_bits(), 0.2_f64.to_bits());
872 assert!(velocity.z >= -0.1);
873 assert!(velocity.z < 0.1);
874 }
875
876 #[test]
877 fn item_tick_refreshes_fluid_after_movement() {
878 init_vanilla_registry();
879 init_behaviors();
880 let fixture = fresh_test_world("item_post_move_fluid_update");
881 let world = &fixture.world;
882 insert_ready_full_chunk(world, ChunkPos::new(0, 0));
883 assert!(world.set_block(
884 BlockPos::new(8, 64, 8),
885 vanilla_blocks::WATER.default_state(),
886 UpdateFlags::UPDATE_NONE,
887 ));
888 let item = Arc::new(ItemEntity::with_item(
889 &vanilla_entities::ITEM,
890 1,
891 DVec3::new(8.5, 65.1, 8.5),
892 ItemStack::new(&vanilla_items::STONE),
893 Arc::downgrade(world),
894 ));
895 item.set_velocity(DVec3::new(0.0, -0.2, 0.0));
896
897 Arc::clone(&item).tick();
898
899 assert!(item.fluid_contact().water_height() > 0.0);
900 assert!(item.needs_velocity_sync());
901 }
902
903 #[test]
904 fn fake_item_is_never_pickable_and_expires_on_its_next_tick() {
905 let item = ItemEntity::with_item(
906 &vanilla_entities::ITEM,
907 1,
908 DVec3::ZERO,
909 ItemStack::new(&vanilla_items::STONE),
910 Weak::<World>::new(),
911 );
912
913 item.make_fake_item();
914
915 assert_eq!(item.get_pickup_delay(), 32_767);
916 assert_eq!(item.get_age(), 5_999);
917 }
918
919 #[test]
920 fn item_merge_preserves_vanilla_stack_and_timing() {
921 init_vanilla_registry();
922
923 let source = ItemEntity::with_item(
924 &vanilla_entities::ITEM,
925 1,
926 DVec3::ZERO,
927 ItemStack::with_count(&vanilla_items::STONE, 10),
928 Weak::<World>::new(),
929 );
930 source.set_pickup_delay(5);
931 source.set_age(20);
932
933 let target = ItemEntity::with_item(
934 &vanilla_entities::ITEM,
935 2,
936 DVec3::ZERO,
937 ItemStack::with_count(&vanilla_items::STONE, 20),
938 Weak::<World>::new(),
939 );
940 target.set_pickup_delay(1);
941 target.set_age(50);
942
943 source.try_to_merge(&target);
944
945 assert!(source.is_removed());
946 assert_eq!(target.get_item().count(), 30);
947 assert_eq!(target.get_pickup_delay(), 5);
948 assert_eq!(target.get_age(), 20);
949 }
950
951 #[test]
952 fn item_damage_truncates_after_fractional_subtraction() {
953 let item = ItemEntity::new(
954 &vanilla_entities::ITEM,
955 1,
956 DVec3::ZERO,
957 Weak::<World>::new(),
958 );
959
960 assert!(item.hurt(
961 test_world(),
962 &DamageSource::environment(&vanilla_damage_types::GENERIC),
963 0.75
964 ));
965
966 assert_eq!(item.get_health(), 4);
967 }
968
969 #[test]
970 fn damage_resistant_item_ignores_matching_damage() {
971 init_vanilla_registry();
972
973 let item = ItemEntity::with_item(
974 &vanilla_entities::ITEM,
975 1,
976 DVec3::ZERO,
977 ItemStack::new(&vanilla_items::NETHERITE_INGOT),
978 Weak::<World>::new(),
979 );
980
981 assert!(item.fire_immune());
982 assert!(!item.hurt(
983 test_world(),
984 &DamageSource::environment(&vanilla_damage_types::IN_FIRE),
985 1.0
986 ));
987 assert_eq!(item.get_health(), 5);
988
989 assert!(item.hurt(
990 test_world(),
991 &DamageSource::environment(&vanilla_damage_types::GENERIC),
992 1.0
993 ));
994 assert_eq!(item.get_health(), 4);
995 }
996}