Skip to main content

steel_core/entity/entities/mobs/passive/chicken/
mod.rs

1//! Vanilla Chicken entity with variant + sound-variant parity, wing-flap
2//! slow-fall, and periodic egg-laying behavior.
3
4use 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
62/// Flap speed gained per tick while airborne (vanilla `Chicken.aiStep`).
63const FLAP_SPEED_AIR_GAIN: f32 = 4.0;
64/// Flap speed lost per tick while grounded (vanilla `Chicken.aiStep`).
65const FLAP_SPEED_GROUND_LOSS: f32 = 1.0;
66/// Scales the per-tick flap-speed adjustment (vanilla `Chicken.aiStep`).
67const FLAP_SPEED_ADJUST_SCALE: f32 = 0.3;
68const FLAP_SPEED_MIN: f32 = 0.0;
69const FLAP_SPEED_MAX: f32 = 1.0;
70/// Flapping strength restored whenever the chicken is airborne.
71const MIN_FLAPPING_STRENGTH: f32 = 1.0;
72/// Multiplicative flapping-strength decay per tick.
73const FLAPPING_STRENGTH_DECAY: f32 = 0.9;
74/// Scales accumulated flap rotation (vanilla `Chicken.aiStep`).
75const FLAP_ROTATION_SCALE: f32 = 2.0;
76/// Downward velocity multiplier while airborne, producing the chicken slow-fall.
77const FALL_DRAG_Y: f32 = 0.6;
78/// Divisor converting flap speed into the next flap distance threshold.
79const NEXT_FLAP_SPEED_DIVISOR: f32 = 2.0;
80
81/// Minimum ticks between egg lays (vanilla `nextInt(6000) + 6000`).
82const EGG_LAY_MIN_DELAY_TICKS: i32 = 6000;
83/// Range of the randomized egg-lay delay in ticks.
84const EGG_LAY_RANDOM_RANGE_TICKS: i32 = 6000;
85const EGG_LAY_SOUND_VOLUME: f32 = 1.0;
86const EGG_LAY_SOUND_BASE_PITCH: f32 = 1.0;
87/// Pitch jitter around the base egg-lay sound pitch.
88const EGG_LAY_SOUND_PITCH_JITTER: f32 = 0.2;
89/// Offset at which laid eggs spawn above the chicken's feet.
90const EGG_LAY_SPAWN_Y_OFFSET: f64 = 0.0;
91
92/// Experience rewarded for a chicken jockey (vanilla `Chicken.getBaseExperienceReward`).
93const CHICKEN_JOCKEY_EXPERIENCE_REWARD: i32 = 10;
94
95/// Runtime state unique to chickens: wing-flap animation, egg timer, and jockey flag.
96#[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/// Vanilla chicken entity.
124#[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
136// SAFETY: This key is owned by Steel and uniquely identifies `ChickenEntity`.
137unsafe impl DowncastType for ChickenEntity {
138    const TYPE_KEY: DowncastTypeKey = DowncastTypeKey::new("steel:entity/chicken");
139}
140
141impl ChickenEntity {
142    /// Vanilla `SpawnPlacements` predicate: `Animal.checkAnimalSpawnRules`.
143    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    /// Creates a new chicken at runtime.
148    #[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    /// Reconstructs a chicken from persisted base entity state.
157    #[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        // Chickens do not avoid water (`setPathfindingMalus(PathType.WATER, 0.0F)`).
172        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            // Keep vanilla Chicken goal priorities and speeds in the same order.
183            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    /// Sets the active chicken variant by registry entry.
218    pub fn set_variant(&self, variant: ChickenVariantRef) {
219        self.entity_data
220            .lock()
221            .variant
222            .set(RegistryReference::new(variant));
223    }
224
225    /// Returns the active chicken variant, falling back to temperate when invalid.
226    #[must_use]
227    pub fn variant(&self) -> ChickenVariantRef {
228        self.entity_data.lock().variant.get().value()
229    }
230
231    /// Sets the active chicken sound variant by registry entry.
232    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    /// Returns the active chicken sound variant, falling back to classic when invalid.
240    #[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    /// Returns the sound set for this chicken's current age (vanilla `Chicken.getSoundSet`).
260    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    /// Returns whether an item stack matches the vanilla chicken food tag.
277    #[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    /// Returns whether this chicken is currently ridden by a chicken jockey mob.
285    #[must_use]
286    pub fn is_chicken_jockey(&self) -> bool {
287        self.chicken_state.lock().is_chicken_jockey
288    }
289
290    /// Sets whether this chicken is a chicken jockey mount.
291    pub fn set_chicken_jockey(&self, is_chicken_jockey: bool) {
292        self.chicken_state.lock().is_chicken_jockey = is_chicken_jockey;
293    }
294
295    /// Returns the remaining ticks before the next egg is laid.
296    fn egg_time(&self) -> i32 {
297        self.chicken_state.lock().egg_time
298    }
299
300    /// Sets the remaining ticks before the next egg is laid.
301    fn set_egg_time(&self, egg_time: i32) {
302        self.chicken_state.lock().egg_time = egg_time;
303    }
304
305    /// Rolls the vanilla randomized delay before the next egg lay.
306    fn roll_egg_lay_time() -> i32 {
307        EGG_LAY_MIN_DELAY_TICKS + rand::random_range(0..EGG_LAY_RANDOM_RANGE_TICKS)
308    }
309
310    /// Runs vanilla `Chicken.aiStep` wing-flap and slow-fall side effects.
311    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    /// Runs vanilla `Chicken.aiStep` egg-laying side effects.
343    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            // Vanilla gates egg laying on `!isChickenJockey()`.
351            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    /// Rolls a gift loot table at this entity's position and drops each result.
374    ///
375    /// Mirrors vanilla `LivingEntity.dropFromGiftLootTable` for the
376    /// `gameplay/chicken_lay` table, returning whether any item was dropped.
377    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        // Vanilla `LootContextParamSets.GIFT` carries only ORIGIN and THIS_ENTITY.
382        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        // Chicken jockeys persist like their rider (vanilla `Chicken.removeWhenFarAway`).
640        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        // Mirrors the cow/pig convention.
651        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;