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) {
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 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 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 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 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 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}