Skip to main content

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

1//! Vanilla Sheep entity with wool color, shearing, and breeding parity.
2use std::sync::{Arc, Weak};
3
4use glam::DVec3;
5use simdnbt::borrow::NbtCompound as BorrowedNbtCompoundView;
6use simdnbt::owned::NbtCompound;
7use steel_macros::entity_behavior;
8use steel_protocol::packets::game::SoundSource;
9use steel_registry::data_components::vanilla_components::{DYE, SHEEP_COLOR};
10use steel_registry::entity_type::{
11    EntityAttachmentPoint, EntityAttachments, EntityDimensions, EntityTypeRef,
12};
13use steel_registry::item_stack::ItemStack;
14use steel_registry::recipe::{CraftingInput, vanilla_recipe_types};
15use steel_registry::sound_event::SoundEventRef;
16use steel_registry::vanilla_entity_data::SheepEntityData;
17use steel_registry::vanilla_game_events;
18use steel_registry::vanilla_item_tags::ItemTag;
19use steel_registry::vanilla_loot_tables;
20use steel_registry::{DyeColor, REGISTRY, TaggedRegistryExt, sound_events, vanilla_items};
21use steel_utils::locks::SyncMutex;
22use steel_utils::nbt::NbtNumeric as _;
23use steel_utils::nbt::NbtValueInput as _;
24use steel_utils::random::legacy_random::LegacyRandom;
25use steel_utils::types::InteractionHand;
26use steel_utils::{BlockPos, BlockStateId, Downcast as _, DowncastType, DowncastTypeKey};
27
28use crate::behavior::InteractionResult;
29use crate::entity::ai::goal::{
30    BreedGoal, EatBlockGoal, FloatGoal, FollowParentGoal, LookAtPlayerGoal, PanicGoal,
31    RandomLookAroundGoal, TemptGoal, WaterAvoidingRandomStrollGoal,
32};
33use crate::entity::damage::DamageSource;
34use crate::entity::living_entity::shearing_loot_items_with_rng;
35use crate::entity::{
36    AgeableMob, AgeableMobBase, Animal, AnimalBase, DefaultEntityPush, Entity, EntityBase,
37    EntityBaseLoad, EntityPose, EntitySpawnReason, EntitySyncedData, LivingEntity,
38    LivingEntityBase, Mob, MobBase, PathfinderMob, SharedEntity, SpawnGroupData, SpawnRule,
39    sync_dirty_mob_effects,
40};
41use crate::inventory::recipe_manager;
42use crate::physics::MoveResult;
43use crate::player::Player;
44use crate::world::World;
45
46use color_spawn_rules::TEMPERATE_SPAWN_CONFIGURATION;
47mod color_spawn_rules;
48
49const SHEEP_BABY_PASSENGER_ATTACHMENTS: [EntityAttachmentPoint; 1] =
50    [EntityAttachmentPoint::new(0.0, 0.5625, 0.0)];
51const SHEEP_BABY_DIMENSIONS: EntityDimensions = EntityDimensions::new_with_attachments(
52    0.45,
53    0.65,
54    0.65625,
55    EntityAttachments::new(&SHEEP_BABY_PASSENGER_ATTACHMENTS, &[], &[], &[]),
56);
57
58/// Low 4 bits of the synced wool byte hold the wool color id.
59const COLOR_ID_MASK: i8 = 0x0F;
60/// Bit 5 of the synced wool byte marks the sheep as sheared.
61const SHEARED_BIT: i8 = 0x10;
62/// Vanilla `Sheep.ate` ages babies up by this many seconds (`ageUp(60)`).
63const ATE_AGE_UP_SECONDS: i32 = 60;
64
65/// Vanilla sheep entity.
66#[entity_behavior(class = "Sheep", spawn_rule)]
67pub struct SheepEntity {
68    base: EntityBase,
69    entity_type: EntityTypeRef,
70    living_base: LivingEntityBase,
71    mob_base: MobBase,
72    ageable_base: AgeableMobBase,
73    animal_base: AnimalBase,
74    entity_data: SyncMutex<SheepEntityData>,
75}
76
77// SAFETY: This key is owned by Steel and uniquely identifies `SheepEntity`.
78unsafe impl DowncastType for SheepEntity {
79    const TYPE_KEY: DowncastTypeKey = DowncastTypeKey::new("steel:entity/sheep");
80}
81
82impl SheepEntity {
83    /// Vanilla `SpawnPlacements` predicate: `Animal.checkAnimalSpawnRules`.
84    pub(crate) const SPAWN_RULE: SpawnRule = |entity_type, level, spawn_reason, pos, random| {
85        <Self as Animal>::check_animal_spawn_rules(entity_type, level, spawn_reason, pos, random)
86    };
87
88    /// Creates a new sheep entity.
89    #[must_use]
90    pub fn new(entity_type: EntityTypeRef, id: i32, position: DVec3, world: Weak<World>) -> Self {
91        Self::new_with_base(
92            EntityBase::new(id, position, entity_type.dimensions, world),
93            entity_type,
94        )
95    }
96
97    /// Creates a sheep entity from saved base data.
98    #[must_use]
99    pub fn from_saved(entity_type: EntityTypeRef, load: EntityBaseLoad) -> Self {
100        Self::new_with_base(
101            EntityBase::from_load(load, entity_type.dimensions),
102            entity_type,
103        )
104    }
105
106    fn new_with_base(base: EntityBase, entity_type: EntityTypeRef) -> Self {
107        let living_base = LivingEntityBase::new(entity_type);
108        let mob_base = MobBase::new();
109        let ageable_base = AgeableMobBase::new();
110        let animal_base = AnimalBase::new();
111        AnimalBase::initialize_pathfinding_malus(&mob_base);
112        let mut entity_data = SheepEntityData::new();
113        living_base.initialize_synced_data(&mut entity_data);
114
115        {
116            // Keep vanilla Sheep goal priorities and speeds in the same order.
117            let mut goal_selector = mob_base.goal_selector().lock();
118            goal_selector.add_goal(0, FloatGoal::new(&mob_base));
119            goal_selector.add_goal(1, PanicGoal::new(1.25));
120            goal_selector.add_goal(2, BreedGoal::new(1.0));
121            goal_selector.add_goal(
122                3,
123                TemptGoal::new(
124                    1.1,
125                    |item_stack| {
126                        REGISTRY
127                            .items
128                            .is_in_tag(item_stack.item(), &ItemTag::SHEEP_FOOD)
129                    },
130                    false,
131                ),
132            );
133            goal_selector.add_goal(4, FollowParentGoal::new(1.1));
134            goal_selector.add_goal(5, EatBlockGoal::new());
135            goal_selector.add_goal(6, WaterAvoidingRandomStrollGoal::new(1.0));
136            goal_selector.add_goal(7, LookAtPlayerGoal::new(6.0));
137            goal_selector.add_goal(8, RandomLookAroundGoal::new());
138        }
139
140        Self {
141            base,
142            entity_type,
143            living_base,
144            mob_base,
145            ageable_base,
146            animal_base,
147            entity_data: SyncMutex::new(entity_data),
148        }
149    }
150
151    fn update_dirty_mob_effect_entity_data(&self) {
152        if let Some(display) = sync_dirty_mob_effects(&self.living_base, &self.entity_data) {
153            self.entity_data
154                .set_base_glowing_flag(self.has_glowing_tag() || display.glowing);
155        }
156    }
157
158    /// Returns whether the stack is vanilla sheep food.
159    #[must_use]
160    pub fn is_food(item_stack: &ItemStack) -> bool {
161        REGISTRY
162            .items
163            .is_in_tag(item_stack.item(), &ItemTag::SHEEP_FOOD)
164    }
165
166    /// Returns the current wool color (vanilla `Sheep.getColor`).
167    #[must_use]
168    pub fn color(&self) -> DyeColor {
169        DyeColor::by_id(i32::from(
170            self.entity_data.lock().wool.get() & COLOR_ID_MASK,
171        ))
172    }
173
174    /// Sets the wool color, preserving the sheared bit (vanilla `Sheep.setColor`).
175    pub fn set_color(&self, color: DyeColor) {
176        let mut entity_data = self.entity_data.lock();
177        let current = *entity_data.wool.get();
178        entity_data
179            .wool
180            .set((current & !COLOR_ID_MASK) | ((color.id() & 15) as i8));
181    }
182
183    /// Returns whether this sheep has been sheared (vanilla `Sheep.isSheared`).
184    #[must_use]
185    pub fn is_sheared(&self) -> bool {
186        (*self.entity_data.lock().wool.get() & SHEARED_BIT) != 0
187    }
188
189    /// Sets the sheared state, preserving the color bits (vanilla `Sheep.setSheared`).
190    pub fn set_sheared(&self, sheared: bool) {
191        let mut entity_data = self.entity_data.lock();
192        let current = *entity_data.wool.get();
193        let next = if sheared {
194            current | SHEARED_BIT
195        } else {
196            current & !SHEARED_BIT
197        };
198        entity_data.wool.set(next);
199    }
200
201    /// Returns whether this sheep can currently be sheared: not already
202    /// sheared and not a baby.
203    #[must_use]
204    pub fn ready_for_shearing(&self) -> bool {
205        !self.is_sheared() && !AgeableMob::is_baby(self)
206    }
207
208    /// Runs vanilla `Sheep.shear`: plays the shear sound, drops the shearing loot
209    /// one item at a time with the vanilla jitter, and marks the sheep as sheared.
210    pub fn shear(&self, world: &World, tool: &ItemStack) {
211        world.play_sound_at(
212            &sound_events::ENTITY_SHEEP_SHEAR,
213            SoundSource::Players,
214            self.position(),
215            1.0,
216            1.0,
217            None,
218        );
219
220        let mut rng = rand::rng();
221        for drop in
222            shearing_loot_items_with_rng(self, &vanilla_loot_tables::SHEARING_SHEEP, tool, &mut rng)
223        {
224            self.spawn_shearing_drop(&drop);
225        }
226
227        self.set_sheared(true);
228    }
229
230    /// Drops one stack from shearing, spawning a count-1 item entity per count
231    /// unit with the vanilla jitter (vanilla `Sheep.shear`'s spawn lambda).
232    fn spawn_shearing_drop(&self, drop: &ItemStack) {
233        for _ in 0..drop.count() {
234            let Some(item_entity) = self.spawn_at_location(drop.copy_with_count(1), 1.0) else {
235                continue;
236            };
237            let jitter = DVec3::new(
238                (rand::random::<f64>() - rand::random::<f64>()) * 0.1,
239                rand::random_range(0.0..0.05),
240                (rand::random::<f64>() - rand::random::<f64>()) * 0.1,
241            );
242            item_entity.set_velocity(item_entity.velocity() + jitter);
243        }
244    }
245
246    /// Returns the mixed offspring color for two parents, mirroring
247    /// `DyeColor.getMixedColor`: a 2x1 crafting lookup over all dye items of both
248    /// colors, falling back to a random parent when no mix recipe exists.
249    #[must_use]
250    pub fn get_mixed_color(color1: DyeColor, color2: DyeColor) -> DyeColor {
251        // TODO: add this function to DyeColor
252        Self::find_color_mix_in_recipes(color1, color2).unwrap_or_else(|| {
253            if rand::random::<bool>() {
254                color1
255            } else {
256                color2
257            }
258        })
259    }
260
261    fn find_color_mix_in_recipes(color1: DyeColor, color2: DyeColor) -> Option<DyeColor> {
262        // TODO: add this function to DyeColor
263        let dye1: Vec<_> = REGISTRY
264            .items
265            .iter()
266            .filter_map(|(_, item)| (item.components.get(DYE) == Some(color1)).then_some(item))
267            .collect();
268        if dye1.is_empty() {
269            return None;
270        }
271        let dye2: Vec<_> = REGISTRY
272            .items
273            .iter()
274            .filter_map(|(_, item)| (item.components.get(DYE) == Some(color2)).then_some(item))
275            .collect();
276        if dye2.is_empty() {
277            return None;
278        }
279
280        for dye1_item in &dye1 {
281            for dye2_item in &dye2 {
282                let input = CraftingInput::new(
283                    2,
284                    1,
285                    vec![ItemStack::new(dye1_item), ItemStack::new(dye2_item)],
286                );
287                let Some(recipe) = REGISTRY
288                    .recipes
289                    .find_match(&vanilla_recipe_types::CRAFTING, &input)
290                else {
291                    continue;
292                };
293                if let Some(color) = recipe_manager::assemble_recipe(recipe, &input)
294                    .get(DYE)
295                    .copied()
296                {
297                    return Some(color);
298                }
299            }
300        }
301
302        None
303    }
304}
305
306impl DefaultEntityPush for SheepEntity {}
307
308impl Entity for SheepEntity {
309    fn is_hard_collision_relevant(&self) -> bool {
310        false
311    }
312
313    fn as_default_entity_push(&self) -> Option<&dyn DefaultEntityPush> {
314        Some(self)
315    }
316
317    fn base(&self) -> &EntityBase {
318        &self.base
319    }
320
321    fn entity_type(&self) -> EntityTypeRef {
322        self.entity_type
323    }
324
325    fn apply_implicit_item_components(&self, item_stack: &ItemStack) {
326        if let Some(color) = item_stack.get(SHEEP_COLOR) {
327            self.set_color(*color);
328        }
329    }
330
331    fn base_tick(&self) {
332        Mob::base_tick_mob(self);
333    }
334
335    fn dimensions_for_pose(&self, _pose: EntityPose) -> EntityDimensions {
336        let scale = LivingEntity::get_scale(self);
337        if AgeableMob::is_baby(self) {
338            SHEEP_BABY_DIMENSIONS.scale(scale)
339        } else if self.entity_type.fixed {
340            self.entity_type.dimensions
341        } else {
342            self.entity_type.dimensions.scale(scale)
343        }
344    }
345
346    fn synced_data(&self) -> Option<&dyn EntitySyncedData> {
347        Some(&self.entity_data)
348    }
349
350    fn update_data_before_sync(&self) {
351        self.update_dirty_mob_effect_entity_data();
352    }
353
354    fn play_step_sound(&self, _pos: BlockPos, _block_state: BlockStateId) {
355        self.play_sound(&sound_events::ENTITY_SHEEP_STEP, 0.15, 1.0);
356    }
357
358    fn save_additional(&self, nbt: &mut NbtCompound) {
359        self.save_mob(nbt);
360        self.save_ageable_mob(nbt);
361        self.save_animal(nbt);
362        nbt.insert("Sheared", i8::from(self.is_sheared()));
363        // Vanilla stores `Color` through `DyeColor.LEGACY_ID_CODEC` (a byte).
364        nbt.insert("Color", self.color().id() as i8);
365    }
366
367    fn load_additional(&self, nbt: BorrowedNbtCompoundView<'_, '_>) {
368        self.load_mob(nbt);
369        self.load_ageable_mob(nbt);
370        self.load_animal(nbt);
371        self.set_sheared(nbt.byte_value("Sheared").is_some_and(|value| value != 0));
372        self.set_color(DyeColor::by_id(i32::from(
373            nbt.get("Color").and_then(|tag| tag.codec_i8()).unwrap_or(0),
374        )));
375    }
376}
377
378impl LivingEntity for SheepEntity {
379    fn living_base(&self) -> &LivingEntityBase {
380        &self.living_base
381    }
382
383    fn get_health(&self) -> f32 {
384        *self.entity_data.lock().living_entity().health.get()
385    }
386
387    fn set_health(&self, health: f32) {
388        let max_health = self.get_max_health();
389        let clamped = health.clamp(0.0, max_health);
390        self.entity_data
391            .lock()
392            .living_entity_mut()
393            .health
394            .set(clamped);
395    }
396
397    fn sound_volume(&self) -> f32 {
398        0.4
399    }
400
401    fn hurt_sound(&self, _source: &DamageSource) -> Option<SoundEventRef> {
402        Some(&sound_events::ENTITY_SHEEP_HURT)
403    }
404
405    fn death_sound(&self) -> Option<SoundEventRef> {
406        Some(&sound_events::ENTITY_SHEEP_DEATH)
407    }
408
409    fn sheep_loot_state(&self) -> Option<(DyeColor, bool)> {
410        Some((self.color(), self.is_sheared()))
411    }
412
413    fn server_ai_step(&self, entity: &SharedEntity) {
414        Mob::mob_server_ai_step(self, entity);
415    }
416
417    fn ai_step(&self, entity: &SharedEntity) -> Option<MoveResult> {
418        let result = Mob::mob_ai_step(self, entity);
419
420        AgeableMob::tick_ageable_mob(self);
421        Animal::tick_animal_love(self);
422        result
423    }
424}
425
426impl AgeableMob for SheepEntity {
427    fn ageable_base(&self) -> &AgeableMobBase {
428        &self.ageable_base
429    }
430
431    fn is_age_locked(&self) -> bool {
432        *self.entity_data.lock().ageable_mob().age_locked.get()
433    }
434
435    fn set_age_locked(&self, age_locked: bool) {
436        self.entity_data
437            .lock()
438            .ageable_mob_mut()
439            .age_locked
440            .set(age_locked);
441    }
442
443    fn set_synced_baby(&self, baby: bool) {
444        self.entity_data.lock().ageable_mob_mut().baby.set(baby);
445    }
446
447    fn age_boundary_changed(&self, _baby: bool) {
448        self.refresh_dimensions();
449    }
450
451    fn initialize_breed_offspring(&self, partner: &dyn AgeableMob, offspring: &dyn AgeableMob) {
452        let parent1_color = self.color();
453        let parent2_color = partner
454            .downcast_ref::<SheepEntity>()
455            .map_or(parent1_color, SheepEntity::color);
456        let mixed_color = SheepEntity::get_mixed_color(parent1_color, parent2_color);
457        if let Some(offspring) = offspring.downcast_ref::<SheepEntity>() {
458            offspring.set_color(mixed_color);
459        }
460    }
461}
462
463impl Animal for SheepEntity {
464    fn animal_base(&self) -> &AnimalBase {
465        &self.animal_base
466    }
467
468    fn is_food(&self, item_stack: &ItemStack) -> bool {
469        SheepEntity::is_food(item_stack)
470    }
471}
472
473impl Mob for SheepEntity {
474    fn mob_base(&self) -> &MobBase {
475        &self.mob_base
476    }
477
478    fn tick_goal_selectors(&self, entity: &SharedEntity) {
479        PathfinderMob::tick_pathfinder_goal_selectors(self, entity);
480    }
481
482    fn tick_path_navigation(&self) {
483        PathfinderMob::tick_pathfinder_path_navigation(self);
484    }
485
486    fn custom_server_ai_step(&self) {
487        Animal::custom_server_ai_step_animal(self);
488    }
489
490    fn ambient_sound(&self) -> Option<SoundEventRef> {
491        Some(&sound_events::ENTITY_SHEEP_AMBIENT)
492    }
493
494    fn ate(&self) {
495        // VANILLA CLIENT-LOCAL: `LivingEntity.ate` only spawns the eating particles.
496        self.set_sheared(false);
497        if self.can_age_up() {
498            self.age_up(ATE_AGE_UP_SECONDS, false);
499        }
500    }
501
502    fn finalize_spawn(
503        &self,
504        world: &Arc<World>,
505        spawn_reason: EntitySpawnReason,
506        group_data: Option<SpawnGroupData>,
507    ) -> Option<SpawnGroupData> {
508        let mut random = LegacyRandom::from_seed(rand::random());
509        let color = match world.biome_at(self.block_position()) {
510            Some(biome) => SheepEntity::random_sheep_color(biome, &mut random),
511            None => TEMPERATE_SPAWN_CONFIGURATION.get(&mut random),
512        };
513
514        self.set_color(color);
515
516        self.finalize_spawn_ageable_mob(world, spawn_reason, group_data)
517    }
518
519    fn mob_interact(&self, player: &Player, hand: InteractionHand) -> InteractionResult {
520        let item_stack = {
521            let inventory = player.inventory.lock();
522            let item_stack = inventory.get_item_in_hand(hand);
523            item_stack.copy_with_count(item_stack.count())
524        };
525
526        if item_stack.is(&vanilla_items::SHEARS) {
527            if !self.ready_for_shearing() {
528                return InteractionResult::Consume;
529            }
530            let Some(world) = self.level() else {
531                return InteractionResult::Consume;
532            };
533            self.shear(world.as_ref(), &item_stack);
534            // Vanilla `Sheep.mobInteract` sources the shear game event to the player.
535            self.game_event_with_source_entity(&vanilla_game_events::SHEAR, Some(player));
536            player
537                .inventory
538                .lock()
539                .hurt_item_in_hand(hand, 1, player.has_infinite_materials());
540            return InteractionResult::SuccessServer;
541        }
542
543        Animal::mob_interact_animal(self, player, hand)
544    }
545
546    fn mob_flags(&self) -> i8 {
547        *self.entity_data.lock().mob().mob_flags.get()
548    }
549
550    fn set_mob_flags(&self, flags: i8) {
551        self.entity_data.lock().mob_mut().mob_flags.set(flags);
552    }
553}
554
555impl PathfinderMob for SheepEntity {}
556
557#[cfg(test)]
558mod tests;