steel_core/entity/entities/mobs/passive/cow/
mod.rs1use crate::player::player_inventory::Prediction;
4use std::str::FromStr;
5use std::sync::{Arc, Weak};
6
7use glam::DVec3;
8use simdnbt::borrow::NbtCompound as BorrowedNbtCompoundView;
9use simdnbt::owned::NbtCompound;
10use steel_macros::entity_behavior;
11use steel_protocol::packets::game::SoundSource;
12use steel_registry::cow_sound_variant::CowSoundVariantRef;
13use steel_registry::cow_variant::CowVariantRef;
14use steel_registry::data_components::vanilla_components::{COW_SOUND_VARIANT, COW_VARIANT};
15use steel_registry::entity_type::{
16 EntityAttachmentPoint, EntityAttachments, EntityDimensions, EntityTypeRef,
17};
18use steel_registry::item_stack::ItemStack;
19use steel_registry::sound_event::SoundEventRef;
20use steel_registry::vanilla_entity_data::CowEntityData;
21use steel_registry::vanilla_item_tags::ItemTag;
22use steel_registry::{
23 REGISTRY, RegistryExt, RegistryReference, TaggedRegistryExt, sound_events, vanilla_attributes,
24 vanilla_items,
25};
26use steel_utils::locks::SyncMutex;
27use steel_utils::random::legacy_random::LegacyRandom;
28use steel_utils::types::InteractionHand;
29use steel_utils::{BlockPos, BlockStateId, DowncastType, DowncastTypeKey, Identifier};
30
31use crate::behavior::{ITEM_BEHAVIORS, InteractionResult};
32use crate::entity::ai::goal::{
33 BreedGoal, FloatGoal, FollowParentGoal, LookAtPlayerGoal, PanicGoal, RandomLookAroundGoal,
34 TemptGoal, WaterAvoidingRandomStrollGoal,
35};
36use crate::entity::damage::DamageSource;
37use crate::entity::{
38 AgeableMob, AgeableMobBase, Animal, AnimalBase, DefaultEntityPush, Entity, EntityBase,
39 EntityBaseLoad, EntityPose, EntitySpawnReason, EntitySyncedData, LivingEntity,
40 LivingEntityBase, Mob, MobBase, PathfinderMob, SharedEntity, SpawnGroupData, SpawnRule,
41 sync_dirty_mob_effects,
42};
43use crate::physics::MoveResult;
44use crate::player::Player;
45use crate::world::World;
46
47const COW_BABY_PASSENGER_ATTACHMENTS: [EntityAttachmentPoint; 1] =
48 [EntityAttachmentPoint::new(0.0, 0.75, 0.0)];
49const COW_BABY_WIDTH: f32 = 0.45;
50const COW_BABY_HEIGHT: f32 = 0.7;
51const COW_BABY_EYE_HEIGHT: f32 = 0.69;
52
53const COW_BABY_DIMENSIONS: EntityDimensions = EntityDimensions::new_with_attachments(
54 COW_BABY_WIDTH,
55 COW_BABY_HEIGHT,
56 COW_BABY_EYE_HEIGHT,
57 EntityAttachments::new(&COW_BABY_PASSENGER_ATTACHMENTS, &[], &[], &[]),
58);
59const DEFAULT_STEP_HEIGHT: f32 = 0.6;
60
61#[entity_behavior(class = "Cow", spawn_rule)]
62pub struct CowEntity {
64 base: EntityBase,
65 entity_type: EntityTypeRef,
66 living_base: LivingEntityBase,
67 mob_base: MobBase,
68 ageable_base: AgeableMobBase,
69 animal_base: AnimalBase,
70 entity_data: SyncMutex<CowEntityData>,
71}
72
73unsafe impl DowncastType for CowEntity {
75 const TYPE_KEY: DowncastTypeKey = DowncastTypeKey::new("steel:entity/cow");
76}
77
78impl CowEntity {
79 pub(crate) const SPAWN_RULE: SpawnRule = |entity_type, level, spawn_reason, pos, random| {
81 <Self as Animal>::check_animal_spawn_rules(entity_type, level, spawn_reason, pos, random)
82 };
83
84 #[must_use]
86 pub fn new(entity_type: EntityTypeRef, id: i32, position: DVec3, world: Weak<World>) -> Self {
87 Self::new_with_base(
88 EntityBase::new(id, position, entity_type.dimensions, world),
89 entity_type,
90 )
91 }
92
93 #[must_use]
95 pub fn from_saved(entity_type: EntityTypeRef, load: EntityBaseLoad) -> Self {
96 Self::new_with_base(
97 EntityBase::from_load(load, entity_type.dimensions),
98 entity_type,
99 )
100 }
101
102 fn new_with_base(base: EntityBase, entity_type: EntityTypeRef) -> Self {
103 let living_base = LivingEntityBase::new(entity_type);
104 let mob_base = MobBase::new();
105 let ageable_base = AgeableMobBase::new();
106 let animal_base = AnimalBase::new();
107 AnimalBase::initialize_pathfinding_malus(&mob_base);
108 let mut entity_data = CowEntityData::new();
109 living_base.initialize_synced_data(&mut entity_data);
110
111 {
112 let mut goal_selector = mob_base.goal_selector().lock();
114 goal_selector.add_goal(0, FloatGoal::new(&mob_base));
115 goal_selector.add_goal(1, PanicGoal::new(2.0));
116 goal_selector.add_goal(2, BreedGoal::new(1.0));
117 goal_selector.add_goal(
118 3,
119 TemptGoal::new(
120 1.25,
121 |item_stack| {
122 REGISTRY
123 .items
124 .is_in_tag(item_stack.item(), &ItemTag::COW_FOOD)
125 },
126 false,
127 ),
128 );
129 goal_selector.add_goal(4, FollowParentGoal::new(1.25));
130 goal_selector.add_goal(5, WaterAvoidingRandomStrollGoal::new(1.0));
131 goal_selector.add_goal(6, LookAtPlayerGoal::new(6.0));
132 goal_selector.add_goal(7, RandomLookAroundGoal::new());
133 }
134
135 Self {
136 base,
137 entity_type,
138 living_base,
139 mob_base,
140 ageable_base,
141 animal_base,
142 entity_data: SyncMutex::new(entity_data),
143 }
144 }
145
146 pub fn set_variant(&self, variant: CowVariantRef) {
148 self.entity_data
149 .lock()
150 .variant
151 .set(RegistryReference::new(variant));
152 }
153
154 #[must_use]
156 pub fn variant(&self) -> CowVariantRef {
157 self.entity_data.lock().variant.get().value()
158 }
159
160 pub fn set_sound_variant(&self, sound_variant: CowSoundVariantRef) {
162 self.entity_data
163 .lock()
164 .sound_variant
165 .set(RegistryReference::new(sound_variant));
166 }
167
168 #[must_use]
170 pub fn sound_variant(&self) -> CowSoundVariantRef {
171 self.entity_data.lock().sound_variant.get().value()
172 }
173
174 fn set_variant_by_key(&self, key: &Identifier) -> bool {
175 let Some(variant) = REGISTRY.cow_variants.by_key(key) else {
176 return false;
177 };
178 self.set_variant(variant);
179 true
180 }
181
182 fn set_sound_variant_by_key(&self, key: &Identifier) {
183 if let Some(sound_variant) = REGISTRY.cow_sound_variants.by_key(key) {
184 self.set_sound_variant(sound_variant);
185 }
186 }
187
188 fn update_dirty_mob_effect_entity_data(&self) {
189 if let Some(display) = sync_dirty_mob_effects(&self.living_base, &self.entity_data) {
190 self.entity_data
191 .set_base_glowing_flag(self.has_glowing_tag() || display.glowing);
192 }
193 }
194
195 #[must_use]
197 pub fn is_food(item_stack: &ItemStack) -> bool {
198 REGISTRY
199 .items
200 .is_in_tag(item_stack.item(), &ItemTag::COW_FOOD)
201 }
202
203 fn try_milk(&self, player: &Player, hand: InteractionHand) -> bool {
204 if AgeableMob::is_baby(self) {
205 return false;
206 }
207
208 let is_bucket = {
209 let inventory = player.inventory.lock();
210 inventory.get_item_in_hand(hand).is(&vanilla_items::BUCKET)
211 };
212 if !is_bucket {
213 return false;
214 }
215
216 player.play_sound(&sound_events::ENTITY_COW_MILK, 1.0, 1.0);
217
218 let overflow = {
219 let mut inventory = player.inventory.lock();
220 inventory.apply_filled_result(
221 hand,
222 ITEM_BEHAVIORS.default_instance(&vanilla_items::MILK_BUCKET),
223 player.has_infinite_materials(),
224 true,
225 )
226 };
227
228 if !overflow.is_empty() {
229 let _ = player.drop_item(overflow, false, Prediction::Predicted);
230 }
231
232 true
233 }
234}
235
236impl DefaultEntityPush for CowEntity {}
237
238impl Entity for CowEntity {
239 fn as_default_entity_push(&self) -> Option<&dyn DefaultEntityPush> {
240 Some(self)
241 }
242
243 fn is_hard_collision_relevant(&self) -> bool {
244 false
245 }
246
247 fn base(&self) -> &EntityBase {
248 &self.base
249 }
250
251 fn entity_type(&self) -> EntityTypeRef {
252 self.entity_type
253 }
254
255 fn apply_implicit_item_components(&self, item_stack: &ItemStack) {
256 if let Some(variant) = item_stack.get(COW_VARIANT) {
257 self.set_variant(variant.value());
258 }
259 if let Some(sound_variant) = item_stack.get(COW_SOUND_VARIANT) {
260 self.set_sound_variant(sound_variant.value());
261 }
262 }
263
264 fn base_tick(&self) {
265 Mob::base_tick_mob(self);
266 }
267
268 fn dimensions_for_pose(&self, _pose: EntityPose) -> EntityDimensions {
269 let scale = LivingEntity::get_scale(self);
270 if AgeableMob::is_baby(self) {
271 COW_BABY_DIMENSIONS.scale(scale)
272 } else if self.entity_type.fixed {
273 self.entity_type.dimensions
274 } else {
275 self.entity_type.dimensions.scale(scale)
276 }
277 }
278
279 fn synced_data(&self) -> Option<&dyn EntitySyncedData> {
280 Some(&self.entity_data)
281 }
282
283 fn update_data_before_sync(&self) {
284 self.update_dirty_mob_effect_entity_data();
285 }
286
287 fn max_up_step(&self) -> f32 {
288 self.attributes()
289 .lock()
290 .get_value(vanilla_attributes::STEP_HEIGHT)
291 .unwrap_or(f64::from(DEFAULT_STEP_HEIGHT)) as f32
292 }
293
294 fn sound_source(&self) -> SoundSource {
295 SoundSource::Neutral
296 }
297
298 fn play_step_sound(&self, _pos: BlockPos, _block_state: BlockStateId) {
299 self.play_sound(self.sound_variant().step_sound, 0.15, 1.0);
300 }
301
302 fn save_additional(&self, nbt: &mut NbtCompound) {
303 self.save_mob(nbt);
304 self.save_ageable_mob(nbt);
305 self.save_animal(nbt);
306 nbt.insert("variant", self.variant().key.to_string());
307 nbt.insert("sound_variant", self.sound_variant().key.to_string());
308 }
309
310 fn load_additional(&self, nbt: BorrowedNbtCompoundView<'_, '_>) {
311 self.load_mob(nbt);
312 self.load_ageable_mob(nbt);
313 self.load_animal(nbt);
314
315 if let Some(variant) = nbt.string("variant")
316 && let Ok(key) = Identifier::from_str(variant.to_str().as_ref())
317 {
318 self.set_variant_by_key(&key);
319 }
320 if let Some(sound_variant) = nbt.string("sound_variant")
321 && let Ok(key) = Identifier::from_str(sound_variant.to_str().as_ref())
322 {
323 self.set_sound_variant_by_key(&key);
324 }
325 }
326}
327
328impl LivingEntity for CowEntity {
329 fn living_base(&self) -> &LivingEntityBase {
330 &self.living_base
331 }
332
333 fn get_health(&self) -> f32 {
334 *self.entity_data.lock().living_entity().health.get()
335 }
336
337 fn set_health(&self, health: f32) {
338 let max_health = self.get_max_health();
339 let clamped = health.clamp(0.0, max_health);
340 self.entity_data
341 .lock()
342 .living_entity_mut()
343 .health
344 .set(clamped);
345 }
346
347 fn sound_volume(&self) -> f32 {
348 0.4
349 }
350
351 fn hurt_sound(&self, _source: &DamageSource) -> Option<SoundEventRef> {
352 Some(self.sound_variant().hurt_sound)
353 }
354
355 fn death_sound(&self) -> Option<SoundEventRef> {
356 Some(self.sound_variant().death_sound)
357 }
358
359 fn server_ai_step(&self, entity: &SharedEntity) {
360 Mob::mob_server_ai_step(self, entity);
361 }
362
363 fn ai_step(&self, entity: &SharedEntity) -> Option<MoveResult> {
364 let result = Mob::mob_ai_step(self, entity);
365
366 AgeableMob::tick_ageable_mob(self);
367 Animal::tick_animal_love(self);
368 result
369 }
370}
371
372impl AgeableMob for CowEntity {
373 fn ageable_base(&self) -> &AgeableMobBase {
374 &self.ageable_base
375 }
376
377 fn is_age_locked(&self) -> bool {
378 *self.entity_data.lock().ageable_mob().age_locked.get()
379 }
380
381 fn set_age_locked(&self, age_locked: bool) {
382 self.entity_data
383 .lock()
384 .ageable_mob_mut()
385 .age_locked
386 .set(age_locked);
387 }
388
389 fn set_synced_baby(&self, baby: bool) {
390 self.entity_data.lock().ageable_mob_mut().baby.set(baby);
391 }
392
393 fn age_boundary_changed(&self, _baby: bool) {
394 self.refresh_dimensions();
395 }
396
397 fn breed_variant_key(&self) -> Option<&Identifier> {
398 Some(&self.variant().key)
399 }
400
401 fn set_breed_variant_key(&self, key: &Identifier) -> bool {
402 self.set_variant_by_key(key)
403 }
404
405 fn initialize_breed_offspring(&self, partner: &dyn AgeableMob, offspring: &dyn AgeableMob) {
406 let use_self_variant = rand::random::<bool>();
407 let variant_key = if use_self_variant {
408 self.breed_variant_key()
409 } else {
410 partner.breed_variant_key()
411 };
412 let Some(variant_key) = variant_key else {
413 return;
414 };
415
416 if !offspring.set_breed_variant_key(variant_key) {
417 log::error!("cow offspring could not inherit breeding variant {variant_key}");
418 }
419 }
420}
421
422impl Animal for CowEntity {
423 fn animal_base(&self) -> &AnimalBase {
424 &self.animal_base
425 }
426
427 fn is_food(&self, item_stack: &ItemStack) -> bool {
428 CowEntity::is_food(item_stack)
429 }
430}
431
432impl Mob for CowEntity {
433 fn mob_base(&self) -> &MobBase {
434 &self.mob_base
435 }
436
437 fn tick_goal_selectors(&self, entity: &SharedEntity) {
438 PathfinderMob::tick_pathfinder_goal_selectors(self, entity);
439 }
440
441 fn tick_path_navigation(&self) {
442 PathfinderMob::tick_pathfinder_path_navigation(self);
443 }
444
445 fn custom_server_ai_step(&self) {
446 Animal::custom_server_ai_step_animal(self);
447 }
448
449 fn ambient_sound(&self) -> Option<SoundEventRef> {
450 Some(self.sound_variant().ambient_sound)
451 }
452
453 fn finalize_spawn(
454 &self,
455 world: &Arc<World>,
456 spawn_reason: EntitySpawnReason,
457 group_data: Option<SpawnGroupData>,
458 ) -> Option<SpawnGroupData> {
459 let biome = world.biome_at(self.block_position());
460 let (variant, sound_variant) = {
461 let mut random = LegacyRandom::from_seed(rand::random());
462 let variant = biome.and_then(|biome| {
463 REGISTRY
464 .cow_variants
465 .select_spawn_variant(biome, &mut random)
466 });
467 let sound_variant = REGISTRY.cow_sound_variants.pick_random(&mut random);
468 (variant, sound_variant)
469 };
470
471 if let Some(variant) = variant {
472 self.set_variant(variant);
473 }
474
475 if let Some(sound_variant) = sound_variant {
476 self.set_sound_variant(sound_variant);
477 }
478
479 self.finalize_spawn_ageable_mob(world, spawn_reason, group_data)
480 }
481
482 fn mob_interact(&self, player: &Player, hand: InteractionHand) -> InteractionResult {
483 if self.try_milk(player, hand) {
484 return InteractionResult::Success;
485 }
486
487 Animal::mob_interact_animal(self, player, hand)
488 }
489
490 fn mob_flags(&self) -> i8 {
491 *self.entity_data.lock().mob().mob_flags.get()
492 }
493
494 fn set_mob_flags(&self, flags: i8) {
495 self.entity_data.lock().mob_mut().mob_flags.set(flags);
496 }
497}
498
499impl PathfinderMob for CowEntity {}
500
501#[cfg(test)]
502mod tests;