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