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