steel_core/entity/entities/objects/projectiles/
snowball.rs1use std::sync::Arc;
9use std::sync::Weak;
10
11use glam::DVec3;
12use simdnbt::borrow::NbtCompound as BorrowedNbtCompoundView;
13use simdnbt::owned::NbtCompound;
14use steel_macros::entity_behavior;
15use steel_protocol::packets::game::SoundSource;
16use steel_registry::entity_type::EntityTypeRef;
17use steel_registry::item_stack::ItemStack;
18use steel_registry::items::ItemRef;
19use steel_registry::vanilla_entity_data::SnowballEntityData;
20use steel_registry::{vanilla_damage_types, vanilla_entities, vanilla_items};
21use steel_utils::entity_events::EntityStatus;
22use steel_utils::locks::SyncMutex;
23use steel_utils::{DowncastType, DowncastTypeKey};
24
25use crate::entity::damage::DamageSource;
26use crate::entity::{
27 Entity, EntityBase, EntityBaseLoad, EntitySyncedData, Projectile, ProjectileBase,
28 ProjectileHit, RemovalReason, SharedEntity, ThrowableItemProjectile, ThrowableProjectile,
29};
30use crate::world::World;
31
32const BLAZE_HIT_DAMAGE: f32 = 3.0;
34
35#[entity_behavior(class = "Snowball")]
37pub struct SnowballEntity {
38 base: EntityBase,
40 entity_type: EntityTypeRef,
42 entity_data: SyncMutex<SnowballEntityData>,
44 projectile_base: ProjectileBase,
46}
47
48unsafe impl DowncastType for SnowballEntity {
50 const TYPE_KEY: DowncastTypeKey = DowncastTypeKey::new("steel:entity/snowball");
51}
52
53impl SnowballEntity {
54 #[must_use]
56 pub fn new(entity_type: EntityTypeRef, id: i32, position: DVec3, world: Weak<World>) -> Self {
57 Self {
58 base: EntityBase::new(id, position, entity_type.dimensions, world),
59 entity_type,
60 entity_data: SyncMutex::new(SnowballEntityData::new()),
61 projectile_base: ProjectileBase::new(),
62 }
63 }
64
65 #[must_use]
67 pub fn from_saved(entity_type: EntityTypeRef, load: EntityBaseLoad) -> Self {
68 Self {
69 base: EntityBase::from_load(load, entity_type.dimensions),
70 entity_type,
71 entity_data: SyncMutex::new(SnowballEntityData::new()),
72 projectile_base: ProjectileBase::new(),
73 }
74 }
75
76 fn impact_damage(entity_type: EntityTypeRef) -> f32 {
78 if entity_type == &vanilla_entities::BLAZE {
79 BLAZE_HIT_DAMAGE
80 } else {
81 0.0
82 }
83 }
84}
85
86impl Entity for SnowballEntity {
87 fn base(&self) -> &EntityBase {
88 &self.base
89 }
90
91 fn entity_type(&self) -> EntityTypeRef {
92 self.entity_type
93 }
94
95 fn tick(self: Arc<Self>) {
96 self.throwable_projectile_tick();
97 }
98
99 fn get_default_gravity(&self) -> f64 {
100 self.throwable_default_gravity()
101 }
102
103 fn sound_source(&self) -> SoundSource {
104 SoundSource::Neutral
105 }
106
107 fn spawn_data(&self) -> i32 {
108 self.get_owner().map_or(0, |owner| owner.id())
109 }
110
111 fn restore_owner_reference(&self, owner: &SharedEntity) {
112 self.cache_owner_entity(owner);
113 }
114
115 fn projectile_owner_uuid(&self) -> Option<uuid::Uuid> {
116 self.owner_uuid()
117 }
118
119 fn projectile_owner(&self) -> Option<SharedEntity> {
120 self.get_owner()
121 }
122
123 fn synced_data(&self) -> Option<&dyn EntitySyncedData> {
124 Some(&self.entity_data)
125 }
126
127 fn save_additional(&self, nbt: &mut NbtCompound) {
128 self.save_projectile(nbt);
129 self.save_throwable_item(nbt);
130 }
131
132 fn load_additional(&self, nbt: BorrowedNbtCompoundView<'_, '_>) {
133 self.load_projectile(nbt);
134 self.load_throwable_item(nbt);
135 }
136}
137
138impl Projectile for SnowballEntity {
139 fn projectile_base(&self) -> &ProjectileBase {
140 &self.projectile_base
141 }
142
143 fn on_hit_entity(self: Arc<Self>, entity: &SharedEntity, _location: DVec3) {
144 let mut damage = DamageSource::environment(&vanilla_damage_types::THROWN)
147 .with_direct_entity(Arc::<Self>::clone(&self));
148 if let Some(owner) = self.get_owner() {
149 damage = damage.with_causing_entity(owner);
150 }
151 if let Some(world) = entity.level() {
152 entity.hurt(&world, &damage, Self::impact_damage(entity.entity_type()));
153 }
154 }
155
156 fn on_hit(self: Arc<Self>, hit: &ProjectileHit) {
157 Arc::clone(&self).projectile_on_hit(hit);
159
160 self.broadcast_entity_event(EntityStatus::Death);
165 self.set_removed(RemovalReason::Discarded);
166 }
167}
168
169impl ThrowableProjectile for SnowballEntity {}
170
171impl ThrowableItemProjectile for SnowballEntity {
172 fn get_default_item(&self) -> ItemRef {
173 &vanilla_items::SNOWBALL
174 }
175
176 fn set_item(&self, item: ItemStack) {
177 self.entity_data
178 .lock()
179 .throwable_item_projectile
180 .item_stack
181 .set(item);
182 }
183
184 fn get_item(&self) -> ItemStack {
185 self.entity_data
186 .lock()
187 .throwable_item_projectile
188 .item_stack
189 .get()
190 .clone()
191 }
192}
193
194#[cfg(test)]
195mod tests {
196 use std::sync::Weak;
197
198 use glam::DVec3;
199 use steel_registry::{init_vanilla_registry, vanilla_damage_types, vanilla_entities};
200 use steel_utils::{BlockPos, Direction};
201
202 use crate::entity::damage::DamageSource;
203 use crate::entity::{Entity, Projectile, ProjectileHit};
204 use crate::test_support::test_world;
205 use crate::world::{ClipHitResult, World};
206
207 use super::*;
208
209 #[test]
210 fn hurt_marks_snowball_unless_base_invulnerable_and_always_returns_false() {
211 init_vanilla_registry();
212
213 let snowball = SnowballEntity::new(
214 &vanilla_entities::SNOWBALL,
215 1,
216 DVec3::ZERO,
217 Weak::<World>::new(),
218 );
219 let source = DamageSource::environment(&vanilla_damage_types::GENERIC);
220
221 assert!(!Entity::hurt(&snowball, test_world(), &source, 1.0));
222 assert!(snowball.hurt_marked());
223
224 snowball.clear_hurt_mark();
225 snowball.set_invulnerable(true);
226 assert!(!Entity::hurt(&snowball, test_world(), &source, 1.0));
227 assert!(!snowball.hurt_marked());
228 }
229
230 #[test]
231 fn impact_damage_is_three_for_blazes_and_zero_otherwise() {
232 init_vanilla_registry();
233
234 assert_eq!(
235 SnowballEntity::impact_damage(&vanilla_entities::BLAZE),
236 BLAZE_HIT_DAMAGE
237 );
238 assert_eq!(SnowballEntity::impact_damage(&vanilla_entities::PIG), 0.0);
239 assert_eq!(
240 SnowballEntity::impact_damage(&vanilla_entities::SNOWBALL),
241 0.0
242 );
243 }
244
245 #[test]
246 fn on_hit_discards_the_snowball() {
247 init_vanilla_registry();
248
249 let snowball = Arc::new(SnowballEntity::new(
250 &vanilla_entities::SNOWBALL,
251 1,
252 DVec3::ZERO,
253 Weak::<World>::new(),
254 ));
255 let hit = ProjectileHit::Block {
256 location: DVec3::ZERO,
257 hit: ClipHitResult {
258 location: DVec3::ZERO,
259 direction: Direction::Up,
260 block_pos: BlockPos::new(0, 0, 0),
261 miss: false,
262 inside: false,
263 world_border_hit: false,
264 },
265 };
266
267 Arc::clone(&snowball).on_hit(&hit);
268 assert!(snowball.is_removed());
269 }
270}