1use 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
55const COLOR_ID_MASK: i8 = 0x0F;
57const SHEARED_BIT: i8 = 0x10;
59const ATE_AGE_UP_SECONDS: i32 = 60;
61
62const 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#[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
101unsafe impl DowncastType for SheepEntity {
103 const TYPE_KEY: DowncastTypeKey = DowncastTypeKey::new("steel:entity/sheep");
104}
105
106impl SheepEntity {
107 #[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 #[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 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 #[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 #[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 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 #[must_use]
216 pub fn is_sheared(&self) -> bool {
217 (*self.entity_data.lock().wool.get() & SHEARED_BIT) != 0
218 }
219
220 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 #[must_use]
234 pub fn ready_for_shearing(&self) -> bool {
235 !self.is_sheared() && !AgeableMob::is_baby(self)
236 }
237
238 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 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 #[must_use]
280 pub fn get_mixed_color(color1: DyeColor, color2: DyeColor) -> DyeColor {
281 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 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 #[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 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 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 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 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;