1use std::sync::{Arc, Weak};
4
5use glam::DVec3;
6use rand::Rng;
7use simdnbt::borrow::NbtCompound as BorrowedNbtCompoundView;
8use simdnbt::owned::NbtCompound;
9use simdnbt::{FromNbtTag as _, ToNbtTag as _};
10use steel_macros::entity_behavior;
11use steel_protocol::packets::game::SoundSource;
12use steel_registry::blocks::block_state_ext::BlockStateExt as _;
13use steel_registry::data_components::components::SuspiciousStewEffects;
14use steel_registry::data_components::vanilla_components::{
15 MOOSHROOM_VARIANT, SUSPICIOUS_STEW_EFFECTS,
16};
17use steel_registry::entity_type::{
18 EntityAttachmentPoint, EntityAttachments, EntityDimensions, EntityTypeRef,
19};
20use steel_registry::entity_variant::MooshroomVariant;
21use steel_registry::item_stack::ItemStack;
22use steel_registry::particle_type::ParticleData;
23use steel_registry::sound_event::SoundEventRef;
24use steel_registry::vanilla_block_tags::BlockTag;
25use steel_registry::vanilla_entity_data::MooshroomEntityData;
26use steel_registry::vanilla_game_events;
27use steel_registry::vanilla_item_tags::ItemTag;
28use steel_registry::{
29 REGISTRY, TaggedRegistryExt, sound_events, vanilla_attributes, vanilla_blocks,
30 vanilla_entities, vanilla_items, vanilla_loot_tables, vanilla_particle_types,
31 vanilla_suspicious_stew_effects,
32};
33use steel_utils::locks::SyncMutex;
34use steel_utils::types::InteractionHand;
35use steel_utils::{BlockPos, BlockStateId, Downcast as _, DowncastType, DowncastTypeKey};
36use uuid::Uuid;
37
38use crate::behavior::InteractionResult;
39use crate::entity::ai::goal::{
40 BreedGoal, FloatGoal, FollowParentGoal, LookAtPlayerGoal, PanicGoal, RandomLookAroundGoal,
41 TemptGoal, WaterAvoidingRandomStrollGoal,
42};
43use crate::entity::damage::DamageSource;
44use crate::entity::entities::CowEntity;
45use crate::entity::living_entity::shearing_loot_items_with_rng;
46use crate::entity::{
47 AgeableMob, AgeableMobBase, Animal, AnimalBase, DefaultEntityPush, Entity, EntityBase,
48 EntityBaseLoad, EntityPose, EntitySpawnReason, EntitySyncedData, LivingEntity,
49 LivingEntityBase, Mob, MobBase, PathfinderMob, RemovalReason, SharedEntity, SpawnGroupData,
50 SpawnRule, next_entity_id,
51};
52use crate::physics::MoveResult;
53use crate::player::Player;
54use crate::player::player_inventory::Prediction;
55use crate::world::game_event::GameEventContext;
56use crate::world::{LevelReader, World};
57
58const MOOSHROOM_BABY_PASSENGER_Y_OFFSET: f64 = 0.75;
59const MOOSHROOM_BABY_PASSENGER_ATTACHMENTS: [EntityAttachmentPoint; 1] =
60 [EntityAttachmentPoint::new(
61 0.0,
62 MOOSHROOM_BABY_PASSENGER_Y_OFFSET,
63 0.0,
64 )];
65const MOOSHROOM_BABY_WIDTH: f32 = 0.45;
66const MOOSHROOM_BABY_HEIGHT: f32 = 0.7;
67const MOOSHROOM_BABY_EYE_HEIGHT: f32 = 0.69;
68
69const MOOSHROOM_BABY_DIMENSIONS: EntityDimensions = EntityDimensions::new_with_attachments(
70 MOOSHROOM_BABY_WIDTH,
71 MOOSHROOM_BABY_HEIGHT,
72 MOOSHROOM_BABY_EYE_HEIGHT,
73 EntityAttachments::new(&MOOSHROOM_BABY_PASSENGER_ATTACHMENTS, &[], &[], &[]),
74);
75const DEFAULT_STEP_HEIGHT: f32 = 0.6;
76const MUTATE_CHANCE: u32 = 1024;
77const MYCELIUM_WALK_TARGET_VALUE: f32 = 10.0;
78
79#[entity_behavior(class = "MushroomCow", spawn_rule)]
81pub struct MushroomCowEntity {
82 base: EntityBase,
83 entity_type: EntityTypeRef,
84 living_base: LivingEntityBase,
85 mob_base: MobBase,
86 ageable_base: AgeableMobBase,
87 animal_base: AnimalBase,
88 entity_data: SyncMutex<MooshroomEntityData>,
89 stew_effects: SyncMutex<Option<SuspiciousStewEffects>>,
90 last_lightning_bolt_uuid: SyncMutex<Option<Uuid>>,
91}
92
93unsafe impl DowncastType for MushroomCowEntity {
95 const TYPE_KEY: DowncastTypeKey = DowncastTypeKey::new("steel:entity/mooshroom");
96}
97
98impl MushroomCowEntity {
99 pub(crate) const SPAWN_RULE: SpawnRule = Self::check_mushroom_spawn_rules;
100
101 pub(crate) fn check_mushroom_spawn_rules(
103 _entity_type: EntityTypeRef,
104 level: &dyn LevelReader,
105 _spawn_reason: EntitySpawnReason,
106 pos: BlockPos,
107 _random: &mut dyn Rng,
108 ) -> bool {
109 level
110 .get_block_state(pos.below())
111 .get_block()
112 .has_tag(&BlockTag::MOOSHROOMS_SPAWNABLE_ON)
113 && <Self as Animal>::is_bright_enough_to_spawn(level, pos)
114 }
115
116 #[must_use]
118 pub fn new(entity_type: EntityTypeRef, id: i32, position: DVec3, world: Weak<World>) -> Self {
119 Self::new_with_base(
120 EntityBase::new(id, position, entity_type.dimensions, world),
121 entity_type,
122 )
123 }
124
125 #[must_use]
127 pub fn from_saved(entity_type: EntityTypeRef, load: EntityBaseLoad) -> Self {
128 Self::new_with_base(
129 EntityBase::from_load(load, entity_type.dimensions),
130 entity_type,
131 )
132 }
133
134 fn new_with_base(base: EntityBase, entity_type: EntityTypeRef) -> Self {
135 let living_base = LivingEntityBase::new(entity_type);
136 let mob_base = MobBase::new();
137 let ageable_base = AgeableMobBase::new();
138 let animal_base = AnimalBase::new();
139 AnimalBase::initialize_pathfinding_malus(&mob_base);
140 let mut entity_data = MooshroomEntityData::new();
141 living_base.initialize_synced_data(&mut entity_data);
142
143 {
144 let mut goal_selector = mob_base.goal_selector().lock();
145 goal_selector.add_goal(0, FloatGoal::new(&mob_base));
146 goal_selector.add_goal(1, PanicGoal::new(2.0));
147 goal_selector.add_goal(2, BreedGoal::new(1.0));
148 goal_selector.add_goal(
149 3,
150 TemptGoal::new(
151 1.25,
152 |item_stack| {
153 REGISTRY
154 .items
155 .is_in_tag(item_stack.item(), &ItemTag::COW_FOOD)
156 },
157 false,
158 ),
159 );
160 goal_selector.add_goal(4, FollowParentGoal::new(1.25));
161 goal_selector.add_goal(5, WaterAvoidingRandomStrollGoal::new(1.0));
162 goal_selector.add_goal(6, LookAtPlayerGoal::new(6.0));
163 goal_selector.add_goal(7, RandomLookAroundGoal::new());
164 }
165
166 Self {
167 base,
168 entity_type,
169 living_base,
170 mob_base,
171 ageable_base,
172 animal_base,
173 entity_data: SyncMutex::new(entity_data),
174 stew_effects: SyncMutex::new(None),
175 last_lightning_bolt_uuid: SyncMutex::new(None),
176 }
177 }
178
179 pub fn set_variant(&self, variant: MooshroomVariant) {
181 self.entity_data
182 .lock()
183 .mushroom_cow_mut()
184 .variant_type
185 .set(variant.id());
186 }
187
188 #[must_use]
190 pub fn variant(&self) -> MooshroomVariant {
191 let binding = self.entity_data.lock();
192 let id = *binding.mushroom_cow().variant_type.get();
193 MooshroomVariant::by_id(id)
194 }
195
196 pub fn set_stew_effects(&self, effects: Option<SuspiciousStewEffects>) {
198 *self.stew_effects.lock() = effects;
199 }
200
201 #[must_use]
203 pub fn stew_effects(&self) -> Option<SuspiciousStewEffects> {
204 self.stew_effects.lock().clone()
205 }
206
207 pub fn thunder_hit(&self, lightning_uuid: Uuid) {
209 let mut last_uuid = self.last_lightning_bolt_uuid.lock();
211 if last_uuid.as_ref() == Some(&lightning_uuid) {
212 return;
213 }
214 *last_uuid = Some(lightning_uuid);
215 let next_variant = match self.variant() {
216 MooshroomVariant::Red => MooshroomVariant::Brown,
217 MooshroomVariant::Brown => MooshroomVariant::Red,
218 };
219 self.set_variant(next_variant);
220 self.play_sound(&sound_events::ENTITY_MOOSHROOM_CONVERT, 2.0, 1.0);
221 }
222
223 pub fn shear(&self, world: &Arc<World>, tool: &ItemStack) {
225 world.play_sound_at(
226 &sound_events::ENTITY_MOOSHROOM_SHEAR,
227 SoundSource::Players,
228 self.position(),
229 1.0,
230 1.0,
231 None,
232 );
233
234 let mut rng = rand::rng();
235 for drop in shearing_loot_items_with_rng(
236 self,
237 &vanilla_loot_tables::SHEARING_MOOSHROOM,
238 tool,
239 &mut rng,
240 ) {
241 self.spawn_shearing_drop(&drop);
242 }
243
244 let cow = Arc::new(CowEntity::new(
246 &vanilla_entities::COW,
247 next_entity_id(),
248 self.position(),
249 Arc::downgrade(world),
250 ));
251 cow.set_rotation(self.rotation());
252 if let Some(custom_name) = self.custom_name() {
253 cow.set_custom_name(Some(custom_name));
254 }
255 cow.set_custom_name_visible(self.is_custom_name_visible());
256 cow.set_invulnerable(self.is_invulnerable());
257 if self.is_no_ai() {
258 cow.set_no_ai(true);
259 }
260 cow.set_age(self.get_age());
261 cow.set_age_locked(self.is_age_locked());
262 for effect in self.active_mob_effects() {
263 cow.add_mob_effect(effect);
264 }
265
266 let _ = world.try_add_entity(cow);
267 self.set_removed(RemovalReason::Discarded);
268 world.send_particles(
269 ParticleData::simple(&vanilla_particle_types::EXPLOSION),
270 self.position() + DVec3::Y * (self.bounding_box().height() * 0.5),
271 1,
272 DVec3::ZERO,
273 0.0,
274 );
275 }
276
277 fn spawn_shearing_drop(&self, drop: &ItemStack) {
278 for _ in 0..drop.count() {
279 let _ = self.spawn_at_location(drop.copy_with_count(1), self.bounding_box().height());
280 }
281 }
282
283 fn try_interact_bowl(&self, player: &Player, hand: InteractionHand) -> bool {
284 if AgeableMob::is_baby(self) {
285 return false;
286 }
287
288 if !player
289 .inventory
290 .lock()
291 .get_item_in_hand(hand)
292 .is(&vanilla_items::BOWL)
293 {
294 return false;
295 }
296
297 let stew_effects = self.stew_effects.lock().take();
298 let (stew_stack, sound) = if let Some(effects) = stew_effects {
299 let mut stack = ItemStack::new(&vanilla_items::SUSPICIOUS_STEW);
300 stack.set(SUSPICIOUS_STEW_EFFECTS, effects);
301 (stack, &sound_events::ENTITY_MOOSHROOM_SUSPICIOUS_MILK)
302 } else {
303 (
304 ItemStack::new(&vanilla_items::MUSHROOM_STEW),
305 &sound_events::ENTITY_MOOSHROOM_MILK,
306 )
307 };
308
309 let overflow = {
310 let mut inventory = player.inventory.lock();
311
312 inventory.apply_filled_result(hand, stew_stack, player.has_infinite_materials(), false)
313 };
314
315 self.play_sound(sound, 1.0, 1.0);
316
317 if !overflow.is_empty() {
318 let _ = player.drop_item(overflow, false, Prediction::Predicted);
319 }
320
321 true
322 }
323
324 fn try_interact_flower(
325 &self,
326 player: &Player,
327 hand: InteractionHand,
328 item_stack: &ItemStack,
329 ) -> InteractionResult {
330 if self.variant() != MooshroomVariant::Brown || AgeableMob::is_baby(self) {
331 return InteractionResult::Pass;
332 }
333
334 let flower_effects = vanilla_suspicious_stew_effects::from_item(item_stack.item());
335
336 let Some(effects) = flower_effects else {
337 return InteractionResult::Pass;
338 };
339
340 if self.stew_effects.lock().is_some() {
341 InteractionResult::Success
342 } else {
343 *self.stew_effects.lock() = Some(effects);
344 self.play_sound(&sound_events::ENTITY_MOOSHROOM_EAT, 2.0, 1.0);
345 player.inventory.lock().mutate_item_in_hand(hand, |item| {
346 item.consume_one(player.has_infinite_materials());
347 });
348 InteractionResult::Success
349 }
350 }
351
352 fn try_interact_shears(&self, player: &Player, hand: InteractionHand) -> InteractionResult {
353 if AgeableMob::is_baby(self) {
354 return InteractionResult::Pass;
355 }
356
357 let Some(world) = self.level() else {
358 return InteractionResult::Consume;
359 };
360
361 let tool = {
362 let inventory = player.inventory.lock();
363 inventory.get_item_in_hand(hand).copy_with_count(1)
364 };
365 self.shear(&world, &tool);
366 world.game_event_at(
367 &vanilla_game_events::SHEAR,
368 self.position(),
369 &GameEventContext::new(Some(player as &dyn Entity), None),
370 );
371 player
372 .inventory
373 .lock()
374 .hurt_item_in_hand(hand, 1, player.has_infinite_materials());
375
376 InteractionResult::Success
377 }
378
379 fn try_milk(&self, player: &Player, hand: InteractionHand) -> bool {
380 if AgeableMob::is_baby(self) {
381 return false;
382 }
383
384 if !player
385 .inventory
386 .lock()
387 .get_item_in_hand(hand)
388 .is(&vanilla_items::BUCKET)
389 {
390 return false;
391 }
392
393 player.play_sound(&sound_events::ENTITY_COW_MILK, 1.0, 1.0);
394
395 let overflow = {
396 let mut inventory = player.inventory.lock();
397 inventory.apply_filled_result(
398 hand,
399 ItemStack::new(&vanilla_items::MILK_BUCKET),
400 player.has_infinite_materials(),
401 true,
402 )
403 };
404
405 if !overflow.is_empty() {
406 let _ = player.drop_item(overflow, false, Prediction::Predicted);
407 }
408
409 true
410 }
411
412 fn update_dirty_mob_effect_entity_data(&self) {
413 if !self.living_base.take_effects_dirty() {
414 return;
415 }
416
417 let display = self.living_base.mob_effect_display_state();
418
419 {
420 let mut entity_data = self.entity_data.lock();
421 let living = entity_data.living_entity_mut();
422 living.effect_particles.set(display.particles);
423 living.effect_ambience.set(display.ambient);
424 }
425
426 self.entity_data.set_base_invisible_flag(display.invisible);
427 self.entity_data
428 .set_base_glowing_flag(self.has_glowing_tag() || display.glowing);
429 }
430}
431
432impl DefaultEntityPush for MushroomCowEntity {}
433
434impl Entity for MushroomCowEntity {
435 fn is_hard_collision_relevant(&self) -> bool {
436 false
437 }
438
439 fn as_default_entity_push(&self) -> Option<&dyn DefaultEntityPush> {
440 Some(self)
441 }
442
443 fn base(&self) -> &EntityBase {
444 &self.base
445 }
446
447 fn entity_type(&self) -> EntityTypeRef {
448 self.entity_type
449 }
450
451 fn apply_implicit_item_components(&self, item_stack: &ItemStack) {
452 if let Some(variant) = item_stack.get(MOOSHROOM_VARIANT) {
453 self.set_variant(*variant);
454 }
455 }
456
457 fn base_tick(&self) {
458 Mob::base_tick_mob(self);
459 }
460
461 fn dimensions_for_pose(&self, _pose: EntityPose) -> EntityDimensions {
462 let scale = LivingEntity::get_scale(self);
463 if AgeableMob::is_baby(self) {
464 MOOSHROOM_BABY_DIMENSIONS.scale(scale)
465 } else if self.entity_type.fixed {
466 self.entity_type.dimensions
467 } else {
468 self.entity_type.dimensions.scale(scale)
469 }
470 }
471
472 fn synced_data(&self) -> Option<&dyn EntitySyncedData> {
473 Some(&self.entity_data)
474 }
475
476 fn update_data_before_sync(&self) {
477 self.update_dirty_mob_effect_entity_data();
478 }
479
480 fn max_up_step(&self) -> f32 {
481 self.attributes()
482 .lock()
483 .get_value(vanilla_attributes::STEP_HEIGHT)
484 .unwrap_or(f64::from(DEFAULT_STEP_HEIGHT)) as f32
485 }
486
487 fn sound_source(&self) -> SoundSource {
488 SoundSource::Neutral
489 }
490
491 fn play_step_sound(&self, _pos: BlockPos, _block_state: BlockStateId) {
492 self.play_sound(&sound_events::ENTITY_COW_STEP, 0.15, 1.0);
493 }
494
495 fn save_additional(&self, nbt: &mut NbtCompound) {
496 self.save_mob(nbt);
497 self.save_ageable_mob(nbt);
498 self.save_animal(nbt);
499 nbt.insert("Type", self.variant().serialized_name());
500 if let Some(stew_effects) = self.stew_effects.lock().as_ref() {
501 nbt.insert("stew_effects", stew_effects.to_nbt_tag());
502 }
503 }
504
505 fn load_additional(&self, nbt: BorrowedNbtCompoundView<'_, '_>) {
506 self.load_mob(nbt);
507 self.load_ageable_mob(nbt);
508 self.load_animal(nbt);
509
510 let variant = nbt
511 .string("Type")
512 .and_then(|name| MooshroomVariant::from_serialized_name(name.to_str().as_ref()))
513 .unwrap_or_default();
514 self.set_variant(variant);
515
516 *self.stew_effects.lock() = nbt
517 .get("stew_effects")
518 .and_then(SuspiciousStewEffects::from_nbt_tag);
519 }
520}
521
522impl LivingEntity for MushroomCowEntity {
523 fn living_base(&self) -> &LivingEntityBase {
524 &self.living_base
525 }
526
527 fn get_health(&self) -> f32 {
528 *self.entity_data.lock().living_entity().health.get()
529 }
530
531 fn set_health(&self, health: f32) {
532 let max_health = self.get_max_health();
533 let clamped = health.clamp(0.0, max_health);
534 self.entity_data
535 .lock()
536 .living_entity_mut()
537 .health
538 .set(clamped);
539 }
540
541 fn sound_volume(&self) -> f32 {
542 0.4
543 }
544
545 fn hurt_sound(&self, _source: &DamageSource) -> Option<SoundEventRef> {
546 Some(&sound_events::ENTITY_COW_HURT)
547 }
548
549 fn death_sound(&self) -> Option<SoundEventRef> {
550 Some(&sound_events::ENTITY_COW_DEATH)
551 }
552
553 fn server_ai_step(&self, entity: &SharedEntity) {
554 Mob::mob_server_ai_step(self, entity);
555 }
556
557 fn ai_step(&self, entity: &SharedEntity) -> Option<MoveResult> {
558 let result = Mob::mob_ai_step(self, entity);
559
560 AgeableMob::tick_ageable_mob(self);
561 Animal::tick_animal_love(self);
562 result
563 }
564}
565
566impl AgeableMob for MushroomCowEntity {
567 fn ageable_base(&self) -> &AgeableMobBase {
568 &self.ageable_base
569 }
570
571 fn is_age_locked(&self) -> bool {
572 *self.entity_data.lock().ageable_mob().age_locked.get()
573 }
574
575 fn set_age_locked(&self, age_locked: bool) {
576 self.entity_data
577 .lock()
578 .ageable_mob_mut()
579 .age_locked
580 .set(age_locked);
581 }
582
583 fn set_synced_baby(&self, baby: bool) {
584 self.entity_data.lock().ageable_mob_mut().baby.set(baby);
585 }
586
587 fn age_boundary_changed(&self, _baby: bool) {
588 self.refresh_dimensions();
589 }
590
591 fn initialize_breed_offspring(&self, partner: &dyn AgeableMob, offspring: &dyn AgeableMob) {
592 let mate_variant = partner
593 .downcast_ref::<MushroomCowEntity>()
594 .map_or(self.variant(), MushroomCowEntity::variant);
595
596 let self_variant = self.variant();
597 let baby_variant = if self_variant == mate_variant {
598 if rand::random::<u32>().is_multiple_of(MUTATE_CHANCE) {
599 match self_variant {
600 MooshroomVariant::Red => MooshroomVariant::Brown,
601 MooshroomVariant::Brown => MooshroomVariant::Red,
602 }
603 } else {
604 self_variant
605 }
606 } else if rand::random::<bool>() {
607 self_variant
608 } else {
609 mate_variant
610 };
611
612 if let Some(baby) = offspring.downcast_ref::<MushroomCowEntity>() {
613 baby.set_variant(baby_variant);
614 }
615 }
616}
617
618impl Animal for MushroomCowEntity {
619 fn animal_base(&self) -> &AnimalBase {
620 &self.animal_base
621 }
622
623 fn is_food(&self, item_stack: &ItemStack) -> bool {
624 CowEntity::is_food(item_stack)
625 }
626
627 fn animal_walk_target_value(&self, pos: BlockPos) -> f32 {
628 let Some(world) = self.level() else {
629 return 0.0;
630 };
631
632 if world.get_block_state(pos.below()).get_block() == &vanilla_blocks::MYCELIUM {
633 MYCELIUM_WALK_TARGET_VALUE
634 } else {
635 world.pathfinding_cost_from_light_levels(pos)
636 }
637 }
638}
639
640impl Mob for MushroomCowEntity {
641 fn mob_base(&self) -> &MobBase {
642 &self.mob_base
643 }
644
645 fn tick_goal_selectors(&self, entity: &SharedEntity) {
646 PathfinderMob::tick_pathfinder_goal_selectors(self, entity);
647 }
648
649 fn tick_path_navigation(&self) {
650 PathfinderMob::tick_pathfinder_path_navigation(self);
651 }
652
653 fn custom_server_ai_step(&self) {
654 Animal::custom_server_ai_step_animal(self);
655 }
656
657 fn ambient_sound(&self) -> Option<SoundEventRef> {
658 Some(&sound_events::ENTITY_COW_AMBIENT)
659 }
660
661 fn finalize_spawn(
662 &self,
663 world: &Arc<World>,
664 spawn_reason: EntitySpawnReason,
665 group_data: Option<SpawnGroupData>,
666 ) -> Option<SpawnGroupData> {
667 self.set_variant(MooshroomVariant::Red);
668 self.finalize_spawn_ageable_mob(world, spawn_reason, group_data)
669 }
670
671 fn mob_interact(&self, player: &Player, hand: InteractionHand) -> InteractionResult {
672 if self.try_interact_bowl(player, hand) {
673 return InteractionResult::Success;
674 }
675
676 let item_stack = {
677 let inventory = player.inventory.lock();
678 let stack = inventory.get_item_in_hand(hand);
679 stack.copy_with_count(stack.count())
680 };
681
682 if item_stack.is(&vanilla_items::SHEARS) {
683 let result = self.try_interact_shears(player, hand);
684 if result != InteractionResult::Pass {
685 return result;
686 }
687 }
688
689 let flower_result = self.try_interact_flower(player, hand, &item_stack);
690 if flower_result != InteractionResult::Pass {
691 return flower_result;
692 }
693
694 if self.try_milk(player, hand) {
695 return InteractionResult::Success;
696 }
697
698 Animal::mob_interact_animal(self, player, hand)
699 }
700
701 fn mob_flags(&self) -> i8 {
702 *self.entity_data.lock().mob().mob_flags.get()
703 }
704
705 fn set_mob_flags(&self, flags: i8) {
706 self.entity_data.lock().mob_mut().mob_flags.set(flags);
707 }
708}
709
710impl PathfinderMob for MushroomCowEntity {}
711
712#[cfg(test)]
713mod tests;