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::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
31/// Thrown damage dealt to a blaze (vanilla `Snowball.onHitEntity`).
32const BLAZE_HIT_DAMAGE: f32 = 3.0;
33
34/// A thrown snowball.
35#[entity_behavior(class = "Snowball")]
36pub struct SnowballEntity {
37    /// Common entity fields (id, uuid, position, etc.).
38    base: EntityBase,
39    /// Vanilla entity type registered for this implementation.
40    entity_type: EntityTypeRef,
41    /// Synced data carrying the rendered item stack.
42    entity_data: SyncMutex<SnowballEntityData>,
43    /// Shared `Projectile` state (owner / left-owner / has-been-shot).
44    projectile_base: ProjectileBase,
45}
46
47// SAFETY: This key is owned by Steel and uniquely identifies `SnowballEntity`.
48unsafe impl DowncastType for SnowballEntity {
49    const TYPE_KEY: DowncastTypeKey = DowncastTypeKey::new("steel:entity/snowball");
50}
51
52impl SnowballEntity {
53    /// Creates a new thrown snowball with no owner and the default rendered item.
54    #[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    /// Creates a thrown snowball from saved base data.
65    #[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    /// Vanilla `Snowball.onHitEntity` damage: 3 against blazes, otherwise 0.
76    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        // Vanilla `Snowball.onHitEntity`: super.onHitEntity() (no-op), then
148        // `entity.hurt(thrown(this, owner), blaze ? 3 : 0)`.
149        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        // Vanilla `Snowball.onHit`: super.onHit() then the server-side break.
161        self.projectile_on_hit(hit);
162
163        // VANILLA CLIENT-LOCAL: entity event 3 renders the snowball break
164        // particles on clients via `Snowball.handleEntityEvent`; the server
165        // only relays the event. The shared `EntityStatus::Death` variant
166        // carries byte 3.
167        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}