steel_core/entity/entities/objects/projectiles/
thrown_egg.rs1use 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
35const BABY_CHICK_AGE: i32 = -24000;
37
38const HATCH_ROLL_DENOMINATOR: u32 = 8;
40const QUAD_HATCH_ROLL_DENOMINATOR: u32 = 32;
42const QUAD_HATCH_COUNT: usize = 4;
44
45const ZERO_SIZED_DIMENSIONS: EntityDimensions = EntityDimensions::new(0.0, 0.0, 0.0);
48
49#[entity_behavior(class = "ThrownEgg")]
51pub struct ThrownEggEntity {
52 base: EntityBase,
54 entity_type: EntityTypeRef,
56 entity_data: SyncMutex<EggEntityData>,
58 projectile_base: ProjectileBase,
60}
61
62unsafe impl DowncastType for ThrownEggEntity {
64 const TYPE_KEY: DowncastTypeKey = DowncastTypeKey::new("steel:entity/thrown_egg");
65}
66
67impl ThrownEggEntity {
68 #[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 #[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 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 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 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 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 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 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 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 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 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}