Skip to main content

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

1//! Vanilla Mooshroom (`MushroomCow`) entity with variant, shearing, and stew-feeding parity.
2
3use 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/// Vanilla Mooshroom entity.
80#[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
93// SAFETY: This key is owned by Steel and uniquely identifies `MushroomCowEntity`.
94unsafe 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    /// Unlike `check_animal_spawn_rules`, light is required for every spawn reason.
102    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    /// Creates a new mooshroom at runtime.
117    #[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    /// Reconstructs a mooshroom from persisted base entity state.
126    #[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    /// Sets the active mooshroom variant.
180    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    /// Returns the active mooshroom variant.
189    #[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    /// Sets the pending suspicious stew effects from feeding a flower.
197    pub fn set_stew_effects(&self, effects: Option<SuspiciousStewEffects>) {
198        *self.stew_effects.lock() = effects;
199    }
200
201    /// Returns the pending suspicious stew effects, if any.
202    #[must_use]
203    pub fn stew_effects(&self) -> Option<SuspiciousStewEffects> {
204        self.stew_effects.lock().clone()
205    }
206
207    /// Handles a lightning strike hitting the mooshroom.
208    pub fn thunder_hit(&self, lightning_uuid: Uuid) {
209        // TODO: Route server-side LightningBolt hits here once that shared entity and dispatch exist.
210        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    /// Shears the mooshroom, converting it into a normal cow and dropping vanilla loot.
224    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        // TODO: Replace these local transfers with the shared conversion state transfer API.
245        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;