1use std::str::FromStr;
5use std::sync::{Arc, Weak};
6
7use glam::DVec3;
8use simdnbt::borrow::NbtCompound as BorrowedNbtCompoundView;
9use simdnbt::owned::NbtCompound;
10use steel_macros::entity_behavior;
11use steel_registry::chicken_sound_variant::{ChickenAge, ChickenSoundVariantRef};
12use steel_registry::chicken_variant::ChickenVariantRef;
13use steel_registry::data_components::vanilla_components::{CHICKEN_SOUND_VARIANT, CHICKEN_VARIANT};
14use steel_registry::entity_type::{
15 EntityAttachmentPoint, EntityAttachments, EntityDimensions, EntityTypeRef,
16};
17use steel_registry::item_stack::ItemStack;
18use steel_registry::loot_table::{LootContext, LootTableRef};
19use steel_registry::sound_event::SoundEventRef;
20use steel_registry::vanilla_entity_data::ChickenEntityData;
21use steel_registry::vanilla_item_tags::ItemTag;
22use steel_registry::vanilla_loot_tables;
23use steel_registry::{
24 REGISTRY, RegistryExt, RegistryReference, TaggedRegistryExt, sound_events, vanilla_game_events,
25};
26use steel_utils::locks::SyncMutex;
27use steel_utils::random::legacy_random::LegacyRandom;
28use steel_utils::types::InteractionHand;
29use steel_utils::{BlockPos, BlockStateId, DowncastType, DowncastTypeKey, Identifier};
30
31use crate::behavior::InteractionResult;
32use crate::entity::ai::goal::{
33 BreedGoal, FloatGoal, FollowParentGoal, LookAtPlayerGoal, PanicGoal, RandomLookAroundGoal,
34 TemptGoal, WaterAvoidingRandomStrollGoal,
35};
36use crate::entity::ai::path::PathType;
37use crate::entity::damage::DamageSource;
38use crate::entity::{
39 AgeableMob, AgeableMobBase, Animal, AnimalBase, Entity, EntityBase, EntityBaseLoad, EntityPose,
40 EntitySpawnReason, EntitySyncedData, LivingEntity, LivingEntityBase, Mob, MobBase,
41 PathfinderMob, SpawnGroupData, entity_loot_ref, position_rider_default,
42};
43use crate::physics::MoveResult;
44use crate::player::Player;
45use crate::world::World;
46
47const CHICKEN_BABY_PASSENGER_ATTACHMENTS: [EntityAttachmentPoint; 1] =
48 [EntityAttachmentPoint::new(0.0, 0.375, 0.0)];
49const CHICKEN_BABY_WIDTH: f32 = 0.3;
50const CHICKEN_BABY_HEIGHT: f32 = 0.4;
51const CHICKEN_BABY_EYE_HEIGHT: f32 = 0.28125;
52
53const CHICKEN_BABY_DIMENSIONS: EntityDimensions = EntityDimensions::new_with_attachments(
54 CHICKEN_BABY_WIDTH,
55 CHICKEN_BABY_HEIGHT,
56 CHICKEN_BABY_EYE_HEIGHT,
57 EntityAttachments::new(&CHICKEN_BABY_PASSENGER_ATTACHMENTS, &[], &[], &[]),
58);
59
60const FLAP_SPEED_AIR_GAIN: f32 = 4.0;
62const FLAP_SPEED_GROUND_LOSS: f32 = 1.0;
64const FLAP_SPEED_ADJUST_SCALE: f32 = 0.3;
66const FLAP_SPEED_MIN: f32 = 0.0;
67const FLAP_SPEED_MAX: f32 = 1.0;
68const MIN_FLAPPING_STRENGTH: f32 = 1.0;
70const FLAPPING_STRENGTH_DECAY: f32 = 0.9;
72const FLAP_ROTATION_SCALE: f32 = 2.0;
74const FALL_DRAG_Y: f32 = 0.6;
76const NEXT_FLAP_SPEED_DIVISOR: f32 = 2.0;
78
79const EGG_LAY_MIN_DELAY_TICKS: i32 = 6000;
81const EGG_LAY_RANDOM_RANGE_TICKS: i32 = 6000;
83const EGG_LAY_SOUND_VOLUME: f32 = 1.0;
84const EGG_LAY_SOUND_BASE_PITCH: f32 = 1.0;
85const EGG_LAY_SOUND_PITCH_JITTER: f32 = 0.2;
87const EGG_LAY_SPAWN_Y_OFFSET: f64 = 0.0;
89
90const CHICKEN_JOCKEY_EXPERIENCE_REWARD: i32 = 10;
92
93#[derive(Debug, Clone, Copy)]
95struct ChickenState {
96 flap: f32,
97 flap_speed: f32,
98 o_flap: f32,
99 o_flap_speed: f32,
100 flapping: f32,
101 next_flap: f32,
102 egg_time: i32,
103 is_chicken_jockey: bool,
104}
105
106impl ChickenState {
107 const fn new() -> Self {
108 Self {
109 flap: 0.0,
110 flap_speed: 0.0,
111 o_flap: 0.0,
112 o_flap_speed: 0.0,
113 flapping: 1.0,
114 next_flap: 1.0,
115 egg_time: 0,
116 is_chicken_jockey: false,
117 }
118 }
119}
120
121#[entity_behavior(class = "Chicken")]
123pub struct ChickenEntity {
124 base: EntityBase,
125 entity_type: EntityTypeRef,
126 living_base: LivingEntityBase,
127 mob_base: MobBase,
128 ageable_base: AgeableMobBase,
129 animal_base: AnimalBase,
130 chicken_state: SyncMutex<ChickenState>,
131 entity_data: SyncMutex<ChickenEntityData>,
132}
133
134unsafe impl DowncastType for ChickenEntity {
136 const TYPE_KEY: DowncastTypeKey = DowncastTypeKey::new("steel:entity/chicken");
137}
138
139impl ChickenEntity {
140 #[must_use]
142 pub fn new(entity_type: EntityTypeRef, id: i32, position: DVec3, world: Weak<World>) -> Self {
143 Self::new_with_base(
144 EntityBase::new(id, position, entity_type.dimensions, world),
145 entity_type,
146 )
147 }
148
149 #[must_use]
151 pub fn from_saved(entity_type: EntityTypeRef, load: EntityBaseLoad) -> Self {
152 Self::new_with_base(
153 EntityBase::from_load(load, entity_type.dimensions),
154 entity_type,
155 )
156 }
157
158 fn new_with_base(base: EntityBase, entity_type: EntityTypeRef) -> Self {
159 let living_base = LivingEntityBase::new(entity_type);
160 let mob_base = MobBase::new();
161 let ageable_base = AgeableMobBase::new();
162 let animal_base = AnimalBase::new();
163 AnimalBase::initialize_pathfinding_malus(&mob_base);
164 mob_base
166 .pathfinding_malus()
167 .lock()
168 .set(PathType::Water, 0.0);
169 let mut chicken_state = ChickenState::new();
170 chicken_state.egg_time = Self::roll_egg_lay_time();
171 let mut entity_data = ChickenEntityData::new();
172 living_base.initialize_synced_data(&mut entity_data);
173
174 {
175 let mut goal_selector = mob_base.goal_selector().lock();
177 goal_selector.add_goal(0, FloatGoal::new(&mob_base));
178 goal_selector.add_goal(1, PanicGoal::new(1.4));
179 goal_selector.add_goal(2, BreedGoal::new(1.0));
180 goal_selector.add_goal(
181 3,
182 TemptGoal::new(
183 1.0,
184 |item_stack| {
185 REGISTRY
186 .items
187 .is_in_tag(item_stack.item(), &ItemTag::CHICKEN_FOOD)
188 },
189 false,
190 ),
191 );
192 goal_selector.add_goal(4, FollowParentGoal::new(1.1));
193 goal_selector.add_goal(5, WaterAvoidingRandomStrollGoal::new(1.0));
194 goal_selector.add_goal(6, LookAtPlayerGoal::new(6.0));
195 goal_selector.add_goal(7, RandomLookAroundGoal::new());
196 }
197
198 Self {
199 base,
200 entity_type,
201 living_base,
202 mob_base,
203 ageable_base,
204 animal_base,
205 chicken_state: SyncMutex::new(chicken_state),
206 entity_data: SyncMutex::new(entity_data),
207 }
208 }
209
210 pub fn set_variant(&self, variant: ChickenVariantRef) {
212 self.entity_data
213 .lock()
214 .variant
215 .set(RegistryReference::new(variant));
216 }
217
218 #[must_use]
220 pub fn variant(&self) -> ChickenVariantRef {
221 self.entity_data.lock().variant.get().value()
222 }
223
224 pub fn set_sound_variant(&self, sound_variant: ChickenSoundVariantRef) {
226 self.entity_data
227 .lock()
228 .sound_variant
229 .set(RegistryReference::new(sound_variant));
230 }
231
232 #[must_use]
234 pub fn sound_variant(&self) -> ChickenSoundVariantRef {
235 self.entity_data.lock().sound_variant.get().value()
236 }
237
238 fn set_variant_by_key(&self, key: &Identifier) -> bool {
239 let Some(variant) = REGISTRY.chicken_variants.by_key(key) else {
240 return false;
241 };
242 self.set_variant(variant);
243 true
244 }
245
246 fn set_sound_variant_by_key(&self, key: &Identifier) {
247 if let Some(sound_variant) = REGISTRY.chicken_sound_variants.by_key(key) {
248 self.set_sound_variant(sound_variant);
249 }
250 }
251
252 fn current_sound_set(&self) -> &'static ChickenAge {
254 let sound_variant = self.sound_variant();
255 if AgeableMob::is_baby(self) {
256 &sound_variant.baby_sounds
257 } else {
258 &sound_variant.adult_sounds
259 }
260 }
261
262 fn update_dirty_mob_effect_entity_data(&self) {
263 if !self.living_base.take_effects_dirty() {
264 return;
265 }
266
267 let display = self.living_base.mob_effect_display_state();
268
269 {
270 let mut entity_data = self.entity_data.lock();
271 let living = entity_data.living_entity_mut();
272 living.effect_particles.set(display.particles);
273 living.effect_ambience.set(display.ambient);
274 }
275
276 self.entity_data.set_base_invisible_flag(display.invisible);
277 self.entity_data
278 .set_base_glowing_flag(self.has_glowing_tag() || display.glowing);
279 }
280
281 #[must_use]
283 pub fn is_food(item_stack: &ItemStack) -> bool {
284 REGISTRY
285 .items
286 .is_in_tag(item_stack.item(), &ItemTag::CHICKEN_FOOD)
287 }
288
289 #[must_use]
291 pub fn is_chicken_jockey(&self) -> bool {
292 self.chicken_state.lock().is_chicken_jockey
293 }
294
295 pub fn set_chicken_jockey(&self, is_chicken_jockey: bool) {
297 self.chicken_state.lock().is_chicken_jockey = is_chicken_jockey;
298 }
299
300 fn egg_time(&self) -> i32 {
302 self.chicken_state.lock().egg_time
303 }
304
305 fn set_egg_time(&self, egg_time: i32) {
307 self.chicken_state.lock().egg_time = egg_time;
308 }
309
310 fn roll_egg_lay_time() -> i32 {
312 EGG_LAY_MIN_DELAY_TICKS + rand::random_range(0..EGG_LAY_RANDOM_RANGE_TICKS)
313 }
314
315 fn tick_flapping(&self) {
317 let on_ground = self.on_ground();
318 let velocity = self.velocity();
319
320 {
321 let mut state = self.chicken_state.lock();
322 state.o_flap = state.flap;
323 state.o_flap_speed = state.flap_speed;
324 let adjustment = if on_ground {
325 -FLAP_SPEED_GROUND_LOSS
326 } else {
327 FLAP_SPEED_AIR_GAIN
328 };
329 state.flap_speed = (state.flap_speed + adjustment * FLAP_SPEED_ADJUST_SCALE)
330 .clamp(FLAP_SPEED_MIN, FLAP_SPEED_MAX);
331 if !on_ground && state.flapping < MIN_FLAPPING_STRENGTH {
332 state.flapping = MIN_FLAPPING_STRENGTH;
333 }
334 state.flapping *= FLAPPING_STRENGTH_DECAY;
335 state.flap += state.flapping * FLAP_ROTATION_SCALE;
336 }
337
338 if !on_ground && velocity.y < 0.0 {
339 self.set_velocity(DVec3::new(
340 velocity.x,
341 velocity.y * f64::from(FALL_DRAG_Y),
342 velocity.z,
343 ));
344 }
345 }
346
347 fn tick_egg_laying(&self) {
349 if self.level().is_none() || !Entity::is_alive(self) || AgeableMob::is_baby(self) {
350 return;
351 }
352
353 let should_lay_egg = {
354 let mut state = self.chicken_state.lock();
355 if state.is_chicken_jockey {
357 return;
358 }
359 state.egg_time -= 1;
360 state.egg_time <= 0
361 };
362
363 if should_lay_egg {
364 if self.drop_gift_loot_table(&vanilla_loot_tables::GAMEPLAY_CHICKEN_LAY) {
365 let pitch = EGG_LAY_SOUND_BASE_PITCH
366 + (rand::random::<f32>() - rand::random::<f32>()) * EGG_LAY_SOUND_PITCH_JITTER;
367 self.play_sound(
368 &sound_events::ENTITY_CHICKEN_EGG,
369 EGG_LAY_SOUND_VOLUME,
370 pitch,
371 );
372 self.game_event(&vanilla_game_events::ENTITY_PLACE);
373 }
374 self.set_egg_time(Self::roll_egg_lay_time());
375 }
376 }
377
378 fn drop_gift_loot_table(&self, loot_table: LootTableRef) -> bool {
383 let position = self.position();
384 let mut rng = rand::rng();
385 let mut context = LootContext::new(&mut rng)
387 .with_origin(position.x, position.y, position.z)
388 .with_this_entity(entity_loot_ref(self));
389
390 let items = loot_table.get_random_items(&mut context);
391 let dropped_any = !items.is_empty();
392 for item in items {
393 self.spawn_at_location(item, EGG_LAY_SPAWN_Y_OFFSET);
394 }
395 dropped_any
396 }
397}
398
399impl Entity for ChickenEntity {
400 fn base(&self) -> &EntityBase {
401 &self.base
402 }
403
404 fn entity_type(&self) -> EntityTypeRef {
405 self.entity_type
406 }
407
408 fn apply_implicit_item_components(&self, item_stack: &ItemStack) {
409 if let Some(variant) = item_stack.get(CHICKEN_VARIANT) {
410 self.set_variant(variant.value());
411 }
412 if let Some(sound_variant) = item_stack.get(CHICKEN_SOUND_VARIANT) {
413 self.set_sound_variant(sound_variant.value());
414 }
415 }
416
417 fn base_tick(&self) {
418 Mob::base_tick_mob(self);
419 }
420
421 fn dimensions_for_pose(&self, _pose: EntityPose) -> EntityDimensions {
422 let scale = LivingEntity::get_scale(self);
423 if AgeableMob::is_baby(self) {
424 CHICKEN_BABY_DIMENSIONS.scale(scale)
425 } else if self.entity_type.fixed {
426 self.entity_type.dimensions
427 } else {
428 self.entity_type.dimensions.scale(scale)
429 }
430 }
431
432 fn synced_data(&self) -> Option<&dyn EntitySyncedData> {
433 Some(&self.entity_data)
434 }
435
436 fn update_data_before_sync(&self) {
437 self.update_dirty_mob_effect_entity_data();
438 }
439
440 fn play_step_sound(&self, _pos: BlockPos, _block_state: BlockStateId) {
441 self.play_sound(self.current_sound_set().step_sound, 0.15, 1.0);
442 }
443
444 fn is_flapping(&self) -> bool {
445 let fly_dist = self.base().movement_progress().fly_dist();
446 fly_dist > self.chicken_state.lock().next_flap
447 }
448
449 fn on_flap(&self) {
450 let fly_dist = self.base().movement_progress().fly_dist();
451 let mut state = self.chicken_state.lock();
452 state.next_flap = fly_dist + state.flap_speed / NEXT_FLAP_SPEED_DIVISOR;
453 }
454
455 fn position_rider(&self, passenger: &dyn Entity) {
456 position_rider_default(self, passenger);
457 if let Some(living) = passenger.as_living_entity() {
458 living.set_y_body_rot(self.y_body_rot());
459 }
460 }
461
462 fn save_additional(&self, nbt: &mut NbtCompound) {
463 self.save_mob(nbt);
464 self.save_ageable_mob(nbt);
465 self.save_animal(nbt);
466 nbt.insert("IsChickenJockey", i8::from(self.is_chicken_jockey()));
467 nbt.insert("EggLayTime", self.egg_time());
468 nbt.insert("variant", self.variant().key.to_string());
469 nbt.insert("sound_variant", self.sound_variant().key.to_string());
470 }
471
472 fn load_additional(&self, nbt: BorrowedNbtCompoundView<'_, '_>) {
473 self.load_mob(nbt);
474 self.load_ageable_mob(nbt);
475 self.load_animal(nbt);
476
477 self.set_chicken_jockey(nbt.byte("IsChickenJockey").is_some_and(|value| value != 0));
478 if let Some(egg_time) = nbt.int("EggLayTime") {
479 self.set_egg_time(egg_time);
480 }
481 if let Some(variant) = nbt.string("variant")
482 && let Ok(key) = Identifier::from_str(variant.to_str().as_ref())
483 {
484 self.set_variant_by_key(&key);
485 }
486 if let Some(sound_variant) = nbt.string("sound_variant")
487 && let Ok(key) = Identifier::from_str(sound_variant.to_str().as_ref())
488 {
489 self.set_sound_variant_by_key(&key);
490 }
491 }
492}
493
494impl LivingEntity for ChickenEntity {
495 fn living_base(&self) -> &LivingEntityBase {
496 &self.living_base
497 }
498
499 fn get_health(&self) -> f32 {
500 *self.entity_data.lock().living_entity().health.get()
501 }
502
503 fn set_health(&self, health: f32) {
504 let max_health = self.get_max_health();
505 let clamped = health.clamp(0.0, max_health);
506 self.entity_data
507 .lock()
508 .living_entity_mut()
509 .health
510 .set(clamped);
511 }
512
513 fn hurt_sound(&self, _source: &DamageSource) -> Option<SoundEventRef> {
514 Some(self.current_sound_set().hurt_sound)
515 }
516
517 fn death_sound(&self) -> Option<SoundEventRef> {
518 Some(self.current_sound_set().death_sound)
519 }
520
521 fn chicken_loot_variant(&self) -> Option<&Identifier> {
522 Some(&self.variant().key)
523 }
524
525 fn base_experience_reward(&self) -> i32 {
526 if self.is_chicken_jockey() {
527 CHICKEN_JOCKEY_EXPERIENCE_REWARD
528 } else {
529 Animal::base_experience_reward_animal(self)
530 }
531 }
532
533 fn server_ai_step(&self) {
534 Mob::mob_server_ai_step(self);
535 }
536
537 fn ai_step(&self) -> Option<MoveResult> {
538 let result = Mob::mob_ai_step(self);
539
540 AgeableMob::tick_ageable_mob(self);
541 Animal::tick_animal_love(self);
542 self.tick_flapping();
543 self.tick_egg_laying();
544 result
545 }
546}
547
548impl AgeableMob for ChickenEntity {
549 fn ageable_base(&self) -> &AgeableMobBase {
550 &self.ageable_base
551 }
552
553 fn is_age_locked(&self) -> bool {
554 *self.entity_data.lock().ageable_mob().age_locked.get()
555 }
556
557 fn set_age_locked(&self, age_locked: bool) {
558 self.entity_data
559 .lock()
560 .ageable_mob_mut()
561 .age_locked
562 .set(age_locked);
563 }
564
565 fn set_synced_baby(&self, baby: bool) {
566 self.entity_data.lock().ageable_mob_mut().baby.set(baby);
567 }
568
569 fn age_boundary_changed(&self, _baby: bool) {
570 self.refresh_dimensions();
571 }
572
573 fn breed_variant_key(&self) -> Option<&Identifier> {
574 Some(&self.variant().key)
575 }
576
577 fn set_breed_variant_key(&self, key: &Identifier) -> bool {
578 self.set_variant_by_key(key)
579 }
580
581 fn initialize_breed_offspring(&self, partner: &dyn AgeableMob, offspring: &dyn AgeableMob) {
582 let use_self_variant = rand::random::<bool>();
583 let variant_key = if use_self_variant {
584 self.breed_variant_key()
585 } else {
586 partner.breed_variant_key()
587 };
588 let Some(variant_key) = variant_key else {
589 return;
590 };
591
592 if !offspring.set_breed_variant_key(variant_key) {
593 log::error!("chicken offspring could not inherit breeding variant {variant_key}");
594 }
595 }
596}
597
598impl Animal for ChickenEntity {
599 fn animal_base(&self) -> &AnimalBase {
600 &self.animal_base
601 }
602
603 fn is_food(&self, item_stack: &ItemStack) -> bool {
604 ChickenEntity::is_food(item_stack)
605 }
606}
607
608impl Mob for ChickenEntity {
609 fn mob_base(&self) -> &MobBase {
610 &self.mob_base
611 }
612
613 fn tick_goal_selectors(&self) {
614 PathfinderMob::tick_pathfinder_goal_selectors(self);
615 }
616
617 fn tick_path_navigation(&self) {
618 PathfinderMob::tick_pathfinder_path_navigation(self);
619 }
620
621 fn custom_server_ai_step(&self) {
622 Animal::custom_server_ai_step_animal(self);
623 }
624
625 fn ambient_sound(&self) -> Option<SoundEventRef> {
626 Some(self.current_sound_set().ambient_sound)
627 }
628
629 fn remove_when_far_away(&self, _dist_sqr: f64) -> bool {
630 self.is_chicken_jockey()
632 }
633
634 fn finalize_spawn(
635 &self,
636 world: &Arc<World>,
637 spawn_reason: EntitySpawnReason,
638 group_data: Option<SpawnGroupData>,
639 ) -> Option<SpawnGroupData> {
640 let biome = world.biome_at(self.block_position());
641 let (variant, sound_variant) = {
643 let mut random = LegacyRandom::from_seed(rand::random());
644 let variant = biome.and_then(|biome| {
645 REGISTRY
646 .chicken_variants
647 .select_spawn_variant(biome, &mut random)
648 });
649 let sound_variant = REGISTRY.chicken_sound_variants.pick_random(&mut random);
650 (variant, sound_variant)
651 };
652
653 if let Some(variant) = variant {
654 self.set_variant(variant);
655 }
656
657 if let Some(sound_variant) = sound_variant {
658 self.set_sound_variant(sound_variant);
659 }
660
661 self.finalize_spawn_ageable_mob(world, spawn_reason, group_data)
662 }
663
664 fn mob_interact(&self, player: &Player, hand: InteractionHand) -> InteractionResult {
665 Animal::mob_interact_animal(self, player, hand)
666 }
667
668 fn mob_flags(&self) -> i8 {
669 *self.entity_data.lock().mob().mob_flags.get()
670 }
671
672 fn set_mob_flags(&self, flags: i8) {
673 self.entity_data.lock().mob_mut().mob_flags.set(flags);
674 }
675}
676
677impl PathfinderMob for ChickenEntity {}
678
679#[cfg(test)]
680mod tests;