Skip to main content

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

1//! Thrown egg projectile entity (`ThrownEgg`).
2//!
3//! Mirrors vanilla `ThrownEgg` (yarn `EggEntity`) on the Steel
4//! `Projectile → ThrowableProjectile → ThrowableItemProjectile` trait stack.
5//! On impact it may hatch one chick (or four with a rarer roll), each born as a
6//! baby that inherits the egg stack's `chicken/variant` component when present.
7//! The egg then broadcasts the item-break entity event and discards itself.
8
9use std::sync::{Arc, 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::data_components::vanilla_components::CHICKEN_VARIANT;
17use steel_registry::entity_type::{EntityDimensions, EntityTypeRef};
18use steel_registry::item_stack::ItemStack;
19use steel_registry::items::ItemRef;
20use steel_registry::vanilla_entity_data::EggEntityData;
21use steel_registry::{vanilla_damage_types, vanilla_entities, vanilla_items};
22use steel_utils::entity_events::EntityStatus;
23use steel_utils::locks::SyncMutex;
24use steel_utils::{DowncastType, DowncastTypeKey};
25
26use crate::entity::damage::DamageSource;
27use crate::entity::entities::ChickenEntity;
28use crate::entity::{
29    AgeableMob, Entity, EntityBase, EntityBaseLoad, EntitySyncedData, Projectile, ProjectileBase,
30    ProjectileHit, RemovalReason, SharedEntity, ThrowableItemProjectile, ThrowableProjectile,
31    next_entity_id,
32};
33use crate::world::World;
34
35/// Baby chicken start age applied to hatched chicks (vanilla `getBabyStartAge`).
36const BABY_CHICK_AGE: i32 = -24000;
37
38/// Denomator of the one-in-eight hatch chance (vanilla `ThrownEgg.onHit`).
39const HATCH_ROLL_DENOMINATOR: u32 = 8;
40/// Denominator of the one-in-thirty-two quadruple-hatch chance.
41const QUAD_HATCH_ROLL_DENOMINATOR: u32 = 32;
42/// Chicks born when the quadruple roll succeeds.
43const QUAD_HATCH_COUNT: usize = 4;
44
45/// Zero-sized dimensions used as the pre-hatch footprint (vanilla
46/// `ThrownEgg.ZERO_SIZED_DIMENSIONS`).
47const ZERO_SIZED_DIMENSIONS: EntityDimensions = EntityDimensions::new(0.0, 0.0, 0.0);
48
49/// A thrown egg.
50#[entity_behavior(class = "ThrownEgg")]
51pub struct ThrownEggEntity {
52    /// Common entity fields (id, uuid, position, etc.).
53    base: EntityBase,
54    /// Vanilla entity type registered for this implementation.
55    entity_type: EntityTypeRef,
56    /// Synced data carrying the rendered item stack.
57    entity_data: SyncMutex<EggEntityData>,
58    /// Shared `Projectile` state (owner / left-owner / has-been-shot).
59    projectile_base: ProjectileBase,
60}
61
62// SAFETY: This key is owned by Steel and uniquely identifies `ThrownEggEntity`.
63unsafe impl DowncastType for ThrownEggEntity {
64    const TYPE_KEY: DowncastTypeKey = DowncastTypeKey::new("steel:entity/thrown_egg");
65}
66
67impl ThrownEggEntity {
68    /// Creates a new thrown egg with no owner and the default rendered item.
69    #[must_use]
70    pub fn new(entity_type: EntityTypeRef, id: i32, position: DVec3, world: Weak<World>) -> Self {
71        Self {
72            base: EntityBase::new(id, position, entity_type.dimensions, world),
73            entity_type,
74            entity_data: SyncMutex::new(EggEntityData::new()),
75            projectile_base: ProjectileBase::new(),
76        }
77    }
78
79    /// Creates a thrown egg from saved base data.
80    #[must_use]
81    pub fn from_saved(entity_type: EntityTypeRef, load: EntityBaseLoad) -> Self {
82        Self {
83            base: EntityBase::from_load(load, entity_type.dimensions),
84            entity_type,
85            entity_data: SyncMutex::new(EggEntityData::new()),
86            projectile_base: ProjectileBase::new(),
87        }
88    }
89
90    /// Returns the number of chicks to hatch from the two vanilla rolls.
91    ///
92    /// Vanilla rolls lazily (the second roll only happens when the first
93    /// succeeds); rolling both up front preserves the exact distribution.
94    const fn hatch_count(eighth_roll: u32, thirty_second_roll: u32) -> usize {
95        if eighth_roll != 0 {
96            0
97        } else if thirty_second_roll == 0 {
98            QUAD_HATCH_COUNT
99        } else {
100            1
101        }
102    }
103
104    /// Applies the egg stack's `chicken/variant` component to a hatched chick
105    /// (vanilla `ThrownEgg.onHit` component inheritance).
106    fn apply_hatchling_variant(item_stack: &ItemStack, chicken: &ChickenEntity) {
107        if let Some(variant) = item_stack.get(CHICKEN_VARIANT) {
108            chicken.set_variant(variant.value());
109        }
110    }
111
112    /// Creates and adds one hatched chick, returning whether it was placed.
113    ///
114    /// Mirrors the per-chick body of vanilla `ThrownEgg.onHit`: the chick is
115    /// born at the egg's position with the egg's yaw, aged into a baby, then
116    /// fudged into the nearest free spot. Returns `false` when the chick does
117    /// not fit, aborting the remaining hatch loop like vanilla.
118    fn spawn_hatchling(&self, world: &Arc<World>) -> bool {
119        let position = self.position();
120        let chicken = Arc::new(ChickenEntity::new(
121            &vanilla_entities::CHICKEN,
122            next_entity_id(),
123            position,
124            Arc::downgrade(world),
125        ));
126
127        AgeableMob::set_age(chicken.as_ref(), BABY_CHICK_AGE);
128        let (yaw, _) = self.rotation();
129        chicken.set_rotation((yaw, 0.0));
130        Self::apply_hatchling_variant(&self.get_item(), &chicken);
131
132        if !chicken.fudge_position_after_size_change(ZERO_SIZED_DIMENSIONS) {
133            return false;
134        }
135
136        let entity: SharedEntity = chicken;
137        match world.try_add_entity(entity) {
138            Ok(()) => true,
139            Err(error) => {
140                // Vanilla `addFreshEntity` drops the chick silently if the
141                // destination chunk is not loaded; mirror that by continuing.
142                log::debug!("failed to add hatched chick: {error}");
143                true
144            }
145        }
146    }
147}
148
149impl Entity for ThrownEggEntity {
150    fn base(&self) -> &EntityBase {
151        &self.base
152    }
153
154    fn entity_type(&self) -> EntityTypeRef {
155        self.entity_type
156    }
157
158    fn tick(&self) {
159        self.throwable_projectile_tick();
160    }
161
162    fn get_default_gravity(&self) -> f64 {
163        self.throwable_default_gravity()
164    }
165
166    fn sound_source(&self) -> SoundSource {
167        SoundSource::Neutral
168    }
169
170    fn spawn_data(&self) -> i32 {
171        self.get_owner().map_or(0, |owner| owner.id())
172    }
173
174    fn restore_owner_reference(&self, owner: &SharedEntity) {
175        self.cache_owner_entity(owner);
176    }
177
178    fn projectile_owner_uuid(&self) -> Option<uuid::Uuid> {
179        self.owner_uuid()
180    }
181
182    fn projectile_owner(&self) -> Option<SharedEntity> {
183        self.get_owner()
184    }
185
186    fn attackable(&self) -> bool {
187        false
188    }
189
190    fn synced_data(&self) -> Option<&dyn EntitySyncedData> {
191        Some(&self.entity_data)
192    }
193
194    fn save_additional(&self, nbt: &mut NbtCompound) {
195        self.save_projectile(nbt);
196        self.save_throwable_item(nbt);
197    }
198
199    fn load_additional(&self, nbt: BorrowedNbtCompoundView<'_, '_>) {
200        self.load_projectile(nbt);
201        self.load_throwable_item(nbt);
202    }
203}
204
205impl Projectile for ThrownEggEntity {
206    fn projectile_base(&self) -> &ProjectileBase {
207        &self.projectile_base
208    }
209
210    fn on_hit_entity(&self, entity: &SharedEntity, _location: DVec3) {
211        // Vanilla `ThrownEgg.onHitEntity`: super.onHitEntity() (no-op), then
212        // deal 0 damage with a `thrown` source so the hit registers the impact.
213        let mut damage =
214            DamageSource::environment(&vanilla_damage_types::THROWN).with_direct_entity(self.id());
215        if let Some(owner) = self.get_owner() {
216            damage = damage.with_causing_entity(owner.id());
217        }
218        if let Some(world) = entity.level() {
219            entity.hurt(&world, &damage, 0.0);
220        }
221    }
222
223    fn on_hit(&self, hit: &ProjectileHit) {
224        // Vanilla `ThrownEgg.onHit`: super.onHit() then the server-side hatch.
225        self.projectile_on_hit(hit);
226
227        let Some(world) = self.level() else {
228            return;
229        };
230
231        let count = Self::hatch_count(
232            rand::random_range(0..HATCH_ROLL_DENOMINATOR),
233            rand::random_range(0..QUAD_HATCH_ROLL_DENOMINATOR),
234        );
235        for _ in 0..count {
236            if !self.spawn_hatchling(&world) {
237                break;
238            }
239        }
240
241        // VANILLA CLIENT-LOCAL: entity event 3 renders the egg break particles
242        // on clients via `ThrownEgg.handleEntityEvent`; the server only relays
243        // the event. The shared `EntityStatus::Death` variant carries byte 3.
244        self.broadcast_entity_event(EntityStatus::Death);
245        self.set_removed(RemovalReason::Discarded);
246    }
247}
248
249impl ThrowableProjectile for ThrownEggEntity {}
250
251impl ThrowableItemProjectile for ThrownEggEntity {
252    fn get_default_item(&self) -> ItemRef {
253        &vanilla_items::EGG
254    }
255
256    fn set_item(&self, item: ItemStack) {
257        self.entity_data
258            .lock()
259            .throwable_item_projectile
260            .item_stack
261            .set(item);
262    }
263
264    fn get_item(&self) -> ItemStack {
265        self.entity_data
266            .lock()
267            .throwable_item_projectile
268            .item_stack
269            .get()
270            .clone()
271    }
272}
273
274#[cfg(test)]
275mod tests {
276    use std::sync::{Arc, Weak};
277
278    use glam::DVec3;
279    use steel_registry::{
280        RegistryReference, init_vanilla_registry, vanilla_blocks, vanilla_chicken_variants,
281        vanilla_damage_types, vanilla_entities, vanilla_items,
282    };
283    use steel_utils::types::UpdateFlags;
284    use steel_utils::{BlockPos, ChunkPos, Downcast};
285
286    use crate::behavior::init_behaviors;
287    use crate::entity::damage::DamageSource;
288    use crate::entity::entities::ChickenEntity;
289    use crate::entity::{AgeableMob, Entity, ThrowableItemProjectile, WorldAabb};
290    use crate::test_support::{fresh_test_world, insert_ready_full_chunk, test_world};
291
292    use super::*;
293
294    #[test]
295    fn hurt_marks_egg_unless_base_invulnerable_and_always_returns_false() {
296        init_vanilla_registry();
297
298        let egg =
299            ThrownEggEntity::new(&vanilla_entities::EGG, 1, DVec3::ZERO, Weak::<World>::new());
300        let source = DamageSource::environment(&vanilla_damage_types::GENERIC);
301
302        assert!(!Entity::hurt(&egg, test_world(), &source, 1.0));
303        assert!(egg.hurt_marked());
304
305        egg.clear_hurt_mark();
306        egg.set_invulnerable(true);
307        assert!(!Entity::hurt(&egg, test_world(), &source, 1.0));
308        assert!(!egg.hurt_marked());
309    }
310
311    #[test]
312    fn hatch_count_matches_vanilla_odds() {
313        assert_eq!(ThrownEggEntity::hatch_count(1, 0), 0);
314        assert_eq!(ThrownEggEntity::hatch_count(7, 5), 0);
315        assert_eq!(ThrownEggEntity::hatch_count(0, 3), 1);
316        assert_eq!(ThrownEggEntity::hatch_count(0, 0), QUAD_HATCH_COUNT);
317    }
318
319    #[test]
320    fn hatchling_inherits_egg_variant_component() {
321        init_vanilla_registry();
322
323        let egg =
324            ThrownEggEntity::new(&vanilla_entities::EGG, 1, DVec3::ZERO, Weak::<World>::new());
325        let mut stack = ItemStack::new(&vanilla_items::EGG);
326        stack.set(
327            CHICKEN_VARIANT,
328            RegistryReference::new(&vanilla_chicken_variants::COLD),
329        );
330        egg.set_item(stack);
331
332        let chicken = ChickenEntity::new(
333            &vanilla_entities::CHICKEN,
334            2,
335            DVec3::ZERO,
336            Weak::<World>::new(),
337        );
338        ThrownEggEntity::apply_hatchling_variant(&egg.get_item(), &chicken);
339
340        assert_eq!(chicken.variant().key, vanilla_chicken_variants::COLD.key);
341    }
342
343    #[test]
344    fn hatchling_without_variant_keeps_default() {
345        init_vanilla_registry();
346
347        let egg =
348            ThrownEggEntity::new(&vanilla_entities::EGG, 1, DVec3::ZERO, Weak::<World>::new());
349        egg.set_item(ItemStack::new(&vanilla_items::EGG));
350
351        let chicken = ChickenEntity::new(
352            &vanilla_entities::CHICKEN,
353            2,
354            DVec3::ZERO,
355            Weak::<World>::new(),
356        );
357        ThrownEggEntity::apply_hatchling_variant(&egg.get_item(), &chicken);
358
359        assert_eq!(
360            chicken.variant().key,
361            vanilla_chicken_variants::TEMPERATE.key
362        );
363    }
364
365    #[test]
366    fn hatchling_spawns_baby_chicken_at_egg_position() {
367        init_vanilla_registry();
368
369        let world = fresh_test_world("thrown_egg_hatchling");
370        let pos = DVec3::new(0.5, 80.0, 0.5);
371        insert_ready_full_chunk(&world, ChunkPos::from_entity_pos(pos));
372        let egg = Arc::new(ThrownEggEntity::new(
373            &vanilla_entities::EGG,
374            1,
375            pos,
376            Arc::downgrade(&world),
377        ));
378        egg.set_item(ItemStack::new(&vanilla_items::EGG));
379
380        assert!(egg.spawn_hatchling(&world));
381
382        let chicks = world.get_entities_in_aabb(&WorldAabb::of_size(pos, 1.0, 2.0, 1.0));
383        let chick = chicks
384            .iter()
385            .find(|entity| entity.entity_type() == &vanilla_entities::CHICKEN)
386            .expect("hatched chick should be in the world");
387        let chicken = chick
388            .as_ref()
389            .downcast_ref::<ChickenEntity>()
390            .expect("chick should be a chicken");
391        assert!(AgeableMob::is_baby(chicken));
392        assert!((chicken.position() - pos).length() < 1.0);
393    }
394
395    #[test]
396    fn hatchling_does_not_spawn_fully_enclosed_in_solid_block() {
397        init_vanilla_registry();
398        init_behaviors();
399
400        let world = fresh_test_world("thrown_egg_enclosed");
401        insert_ready_full_chunk(&world, ChunkPos::new(0, 0));
402        assert!(world.set_block(
403            BlockPos::new(0, 80, 0),
404            vanilla_blocks::STONE.default_state(),
405            UpdateFlags::UPDATE_NONE,
406        ));
407
408        // The egg lands at the center of the stone block, where the chick box
409        // cannot fit; the fudge finds no free position and the hatch aborts.
410        let egg = Arc::new(ThrownEggEntity::new(
411            &vanilla_entities::EGG,
412            1,
413            DVec3::new(0.5, 80.5, 0.5),
414            Arc::downgrade(&world),
415        ));
416        egg.set_item(ItemStack::new(&vanilla_items::EGG));
417
418        assert!(!egg.spawn_hatchling(&world));
419        assert!(
420            world
421                .get_entities_in_aabb(&WorldAabb::of_size(
422                    DVec3::new(0.5, 80.5, 0.5),
423                    1.0,
424                    2.0,
425                    1.0,
426                ))
427                .iter()
428                .all(|entity| entity.entity_type() != &vanilla_entities::CHICKEN)
429        );
430    }
431
432    #[test]
433    fn hatchling_fits_when_egg_lands_on_top_of_solid_block() {
434        init_vanilla_registry();
435        init_behaviors();
436
437        let world = fresh_test_world("thrown_egg_on_block");
438        insert_ready_full_chunk(&world, ChunkPos::new(0, 0));
439        assert!(world.set_block(
440            BlockPos::new(0, 80, 0),
441            vanilla_blocks::STONE.default_state(),
442            UpdateFlags::UPDATE_NONE,
443        ));
444
445        // The egg rests on the block surface, so the chick fits right there.
446        let pos = DVec3::new(0.5, 81.0, 0.5);
447        let egg = Arc::new(ThrownEggEntity::new(
448            &vanilla_entities::EGG,
449            1,
450            pos,
451            Arc::downgrade(&world),
452        ));
453        egg.set_item(ItemStack::new(&vanilla_items::EGG));
454
455        assert!(egg.spawn_hatchling(&world));
456        assert!(
457            world
458                .get_entities_in_aabb(&WorldAabb::of_size(pos, 1.0, 2.0, 1.0))
459                .iter()
460                .any(|entity| entity.entity_type() == &vanilla_entities::CHICKEN)
461        );
462    }
463}