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: Arc<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 synced_data(&self) -> Option<&dyn EntitySyncedData> {
187        Some(&self.entity_data)
188    }
189
190    fn save_additional(&self, nbt: &mut NbtCompound) {
191        self.save_projectile(nbt);
192        self.save_throwable_item(nbt);
193    }
194
195    fn load_additional(&self, nbt: BorrowedNbtCompoundView<'_, '_>) {
196        self.load_projectile(nbt);
197        self.load_throwable_item(nbt);
198    }
199}
200
201impl Projectile for ThrownEggEntity {
202    fn projectile_base(&self) -> &ProjectileBase {
203        &self.projectile_base
204    }
205
206    fn on_hit_entity(self: Arc<Self>, entity: &SharedEntity, _location: DVec3) {
207        // Vanilla `ThrownEgg.onHitEntity`: super.onHitEntity() (no-op), then
208        // deal 0 damage with a `thrown` source so the hit registers the impact.
209        let mut damage = DamageSource::environment(&vanilla_damage_types::THROWN)
210            .with_direct_entity(Arc::<Self>::clone(&self));
211        if let Some(owner) = self.get_owner() {
212            damage = damage.with_causing_entity(owner);
213        }
214        if let Some(world) = entity.level() {
215            entity.hurt(&world, &damage, 0.0);
216        }
217    }
218
219    fn on_hit(self: Arc<Self>, hit: &ProjectileHit) {
220        // Vanilla `ThrownEgg.onHit`: super.onHit() then the server-side hatch.
221        Arc::clone(&self).projectile_on_hit(hit);
222
223        let Some(world) = self.level() else {
224            return;
225        };
226
227        let count = Self::hatch_count(
228            rand::random_range(0..HATCH_ROLL_DENOMINATOR),
229            rand::random_range(0..QUAD_HATCH_ROLL_DENOMINATOR),
230        );
231        for _ in 0..count {
232            if !self.spawn_hatchling(&world) {
233                break;
234            }
235        }
236
237        // VANILLA CLIENT-LOCAL: entity event 3 renders the egg break particles
238        // on clients via `ThrownEgg.handleEntityEvent`; the server only relays
239        // the event. The shared `EntityStatus::Death` variant carries byte 3.
240        self.broadcast_entity_event(EntityStatus::Death);
241        self.set_removed(RemovalReason::Discarded);
242    }
243}
244
245impl ThrowableProjectile for ThrownEggEntity {}
246
247impl ThrowableItemProjectile for ThrownEggEntity {
248    fn get_default_item(&self) -> ItemRef {
249        &vanilla_items::EGG
250    }
251
252    fn set_item(&self, item: ItemStack) {
253        self.entity_data
254            .lock()
255            .throwable_item_projectile
256            .item_stack
257            .set(item);
258    }
259
260    fn get_item(&self) -> ItemStack {
261        self.entity_data
262            .lock()
263            .throwable_item_projectile
264            .item_stack
265            .get()
266            .clone()
267    }
268}
269
270#[cfg(test)]
271mod tests {
272    use std::sync::{Arc, Weak};
273
274    use glam::DVec3;
275    use steel_registry::{
276        RegistryReference, init_vanilla_registry, vanilla_blocks, vanilla_chicken_variants,
277        vanilla_damage_types, vanilla_entities, vanilla_items,
278    };
279    use steel_utils::types::UpdateFlags;
280    use steel_utils::{BlockPos, ChunkPos, Downcast};
281
282    use crate::behavior::init_behaviors;
283    use crate::entity::damage::DamageSource;
284    use crate::entity::entities::ChickenEntity;
285    use crate::entity::{AgeableMob, Entity, ThrowableItemProjectile, WorldAabb};
286    use crate::test_support::{fresh_test_world, insert_ready_full_chunk, test_world};
287
288    use super::*;
289
290    #[test]
291    fn hurt_marks_egg_unless_base_invulnerable_and_always_returns_false() {
292        init_vanilla_registry();
293
294        let egg =
295            ThrownEggEntity::new(&vanilla_entities::EGG, 1, DVec3::ZERO, Weak::<World>::new());
296        let source = DamageSource::environment(&vanilla_damage_types::GENERIC);
297
298        assert!(!Entity::hurt(&egg, test_world(), &source, 1.0));
299        assert!(egg.hurt_marked());
300
301        egg.clear_hurt_mark();
302        egg.set_invulnerable(true);
303        assert!(!Entity::hurt(&egg, test_world(), &source, 1.0));
304        assert!(!egg.hurt_marked());
305    }
306
307    #[test]
308    fn hatch_count_matches_vanilla_odds() {
309        assert_eq!(ThrownEggEntity::hatch_count(1, 0), 0);
310        assert_eq!(ThrownEggEntity::hatch_count(7, 5), 0);
311        assert_eq!(ThrownEggEntity::hatch_count(0, 3), 1);
312        assert_eq!(ThrownEggEntity::hatch_count(0, 0), QUAD_HATCH_COUNT);
313    }
314
315    #[test]
316    fn hatchling_inherits_egg_variant_component() {
317        init_vanilla_registry();
318
319        let egg =
320            ThrownEggEntity::new(&vanilla_entities::EGG, 1, DVec3::ZERO, Weak::<World>::new());
321        let mut stack = ItemStack::new(&vanilla_items::EGG);
322        stack.set(
323            CHICKEN_VARIANT,
324            RegistryReference::new(&vanilla_chicken_variants::COLD),
325        );
326        egg.set_item(stack);
327
328        let chicken = ChickenEntity::new(
329            &vanilla_entities::CHICKEN,
330            2,
331            DVec3::ZERO,
332            Weak::<World>::new(),
333        );
334        ThrownEggEntity::apply_hatchling_variant(&egg.get_item(), &chicken);
335
336        assert_eq!(chicken.variant().key, vanilla_chicken_variants::COLD.key);
337    }
338
339    #[test]
340    fn hatchling_without_variant_keeps_default() {
341        init_vanilla_registry();
342
343        let egg =
344            ThrownEggEntity::new(&vanilla_entities::EGG, 1, DVec3::ZERO, Weak::<World>::new());
345        egg.set_item(ItemStack::new(&vanilla_items::EGG));
346
347        let chicken = ChickenEntity::new(
348            &vanilla_entities::CHICKEN,
349            2,
350            DVec3::ZERO,
351            Weak::<World>::new(),
352        );
353        ThrownEggEntity::apply_hatchling_variant(&egg.get_item(), &chicken);
354
355        assert_eq!(
356            chicken.variant().key,
357            vanilla_chicken_variants::TEMPERATE.key
358        );
359    }
360
361    #[test]
362    fn hatchling_spawns_baby_chicken_at_egg_position() {
363        init_vanilla_registry();
364
365        let world_fixture = fresh_test_world("thrown_egg_hatchling");
366        let world = &world_fixture.world;
367        let pos = DVec3::new(0.5, 80.0, 0.5);
368        insert_ready_full_chunk(world, ChunkPos::from_entity_pos(pos));
369        let egg = Arc::new(ThrownEggEntity::new(
370            &vanilla_entities::EGG,
371            1,
372            pos,
373            Arc::downgrade(world),
374        ));
375        egg.set_item(ItemStack::new(&vanilla_items::EGG));
376
377        assert!(egg.spawn_hatchling(world));
378
379        let chicks = world.get_entities_in_aabb(&WorldAabb::of_size(pos, 1.0, 2.0, 1.0));
380        let chick = chicks
381            .iter()
382            .find(|entity| entity.entity_type() == &vanilla_entities::CHICKEN)
383            .expect("hatched chick should be in the world");
384        let chicken = chick
385            .as_ref()
386            .downcast_ref::<ChickenEntity>()
387            .expect("chick should be a chicken");
388        assert!(AgeableMob::is_baby(chicken));
389        assert!((chicken.position() - pos).length() < 1.0);
390    }
391
392    #[test]
393    fn hatchling_does_not_spawn_fully_enclosed_in_solid_block() {
394        init_vanilla_registry();
395        init_behaviors();
396
397        let world_fixture = fresh_test_world("thrown_egg_enclosed");
398        let world = &world_fixture.world;
399        insert_ready_full_chunk(world, ChunkPos::new(0, 0));
400        assert!(world.set_block(
401            BlockPos::new(0, 80, 0),
402            vanilla_blocks::STONE.default_state(),
403            UpdateFlags::UPDATE_NONE,
404        ));
405
406        // The egg lands at the center of the stone block, where the chick box
407        // cannot fit; the fudge finds no free position and the hatch aborts.
408        let egg = Arc::new(ThrownEggEntity::new(
409            &vanilla_entities::EGG,
410            1,
411            DVec3::new(0.5, 80.5, 0.5),
412            Arc::downgrade(world),
413        ));
414        egg.set_item(ItemStack::new(&vanilla_items::EGG));
415
416        assert!(!egg.spawn_hatchling(world));
417        assert!(
418            world
419                .get_entities_in_aabb(&WorldAabb::of_size(
420                    DVec3::new(0.5, 80.5, 0.5),
421                    1.0,
422                    2.0,
423                    1.0,
424                ))
425                .iter()
426                .all(|entity| entity.entity_type() != &vanilla_entities::CHICKEN)
427        );
428    }
429
430    #[test]
431    fn hatchling_fits_when_egg_lands_on_top_of_solid_block() {
432        init_vanilla_registry();
433        init_behaviors();
434
435        let world_fixture = fresh_test_world("thrown_egg_on_block");
436        let world = &world_fixture.world;
437        insert_ready_full_chunk(world, ChunkPos::new(0, 0));
438        assert!(world.set_block(
439            BlockPos::new(0, 80, 0),
440            vanilla_blocks::STONE.default_state(),
441            UpdateFlags::UPDATE_NONE,
442        ));
443
444        // The egg rests on the block surface, so the chick fits right there.
445        let pos = DVec3::new(0.5, 81.0, 0.5);
446        let egg = Arc::new(ThrownEggEntity::new(
447            &vanilla_entities::EGG,
448            1,
449            pos,
450            Arc::downgrade(world),
451        ));
452        egg.set_item(ItemStack::new(&vanilla_items::EGG));
453
454        assert!(egg.spawn_hatchling(world));
455        assert!(
456            world
457                .get_entities_in_aabb(&WorldAabb::of_size(pos, 1.0, 2.0, 1.0))
458                .iter()
459                .any(|entity| entity.entity_type() == &vanilla_entities::CHICKEN)
460        );
461    }
462}