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::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
58const COLOR_ID_MASK: i8 = 0x0F;
60const SHEARED_BIT: i8 = 0x10;
62const ATE_AGE_UP_SECONDS: i32 = 60;
64
65#[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
77unsafe impl DowncastType for SheepEntity {
79 const TYPE_KEY: DowncastTypeKey = DowncastTypeKey::new("steel:entity/sheep");
80}
81
82impl SheepEntity {
83 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 #[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 #[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 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 #[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 #[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 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 #[must_use]
185 pub fn is_sheared(&self) -> bool {
186 (*self.entity_data.lock().wool.get() & SHEARED_BIT) != 0
187 }
188
189 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 #[must_use]
204 pub fn ready_for_shearing(&self) -> bool {
205 !self.is_sheared() && !AgeableMob::is_baby(self)
206 }
207
208 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 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 #[must_use]
250 pub fn get_mixed_color(color1: DyeColor, color2: DyeColor) -> DyeColor {
251 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 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 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 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 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;