Skip to main content

steel_core/entity/entities/objects/projectiles/
snowball.rs

1//! Thrown snowball projectile entity (`Snowball`).
2//!
3//! Mirrors vanilla `Snowball` on the Steel
4//! `Projectile → ThrowableProjectile → ThrowableItemProjectile` trait stack.
5//! On entity impact it deals 0 thrown damage (3 to blazes) so the hit
6//! registers, then broadcasts the item-break entity event and discards itself.
7
8use 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
32/// Thrown damage dealt to a blaze (vanilla `Snowball.onHitEntity`).
33const BLAZE_HIT_DAMAGE: f32 = 3.0;
34
35/// A thrown snowball.
36#[entity_behavior(class = "Snowball")]
37pub struct SnowballEntity {
38    /// Common entity fields (id, uuid, position, etc.).
39    base: EntityBase,
40    /// Vanilla entity type registered for this implementation.
41    entity_type: EntityTypeRef,
42    /// Synced data carrying the rendered item stack.
43    entity_data: SyncMutex<SnowballEntityData>,
44    /// Shared `Projectile` state (owner / left-owner / has-been-shot).
45    projectile_base: ProjectileBase,
46}
47
48// SAFETY: This key is owned by Steel and uniquely identifies `SnowballEntity`.
49unsafe impl DowncastType for SnowballEntity {
50    const TYPE_KEY: DowncastTypeKey = DowncastTypeKey::new("steel:entity/snowball");
51}
52
53impl SnowballEntity {
54    /// Creates a new thrown snowball with no owner and the default rendered item.
55    #[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    /// Creates a thrown snowball from saved base data.
66    #[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    /// Vanilla `Snowball.onHitEntity` damage: 3 against blazes, otherwise 0.
77    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        // Vanilla `Snowball.onHitEntity`: super.onHitEntity() (no-op), then
145        // `entity.hurt(thrown(this, owner), blaze ? 3 : 0)`.
146        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        // Vanilla `Snowball.onHit`: super.onHit() then the server-side break.
158        Arc::clone(&self).projectile_on_hit(hit);
159
160        // VANILLA CLIENT-LOCAL: entity event 3 renders the snowball break
161        // particles on clients via `Snowball.handleEntityEvent`; the server
162        // only relays the event. The shared `EntityStatus::Death` variant
163        // carries byte 3.
164        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}