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