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