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