Skip to main content

steel_core/entity/
animal.rs

1//! Shared vanilla `Animal` state and hooks.
2
3use std::sync::Arc;
4
5use simdnbt::borrow::NbtCompound as BorrowedNbtCompoundView;
6use simdnbt::owned::{NbtCompound, NbtTag};
7use steel_registry::blocks::block_state_ext::BlockStateExt as _;
8use steel_registry::item_stack::ItemStack;
9use steel_registry::vanilla_block_tags::BlockTag;
10use steel_registry::vanilla_blocks;
11use steel_registry::vanilla_game_rules::MOB_DROPS;
12use steel_utils::entity_events::EntityStatus;
13use steel_utils::locks::SyncMutex;
14use steel_utils::types::InteractionHand;
15use steel_utils::{BlockPos, Identifier, UuidExt};
16use uuid::Uuid;
17
18use crate::behavior::InteractionResult;
19use crate::entity::ai::path::PathType;
20use crate::entity::entities::ExperienceOrbEntity;
21use crate::entity::{
22    AgeableMob, AgeableMobBase, ENTITIES, EntitySpawnReason, Mob, MobBase, SharedEntity,
23    next_entity_id,
24};
25use crate::player::Player;
26use crate::world::{LevelReader, World};
27
28const PARENT_AGE_AFTER_BREEDING: i32 = 6000;
29const IN_LOVE_TIME: i32 = 600;
30
31#[derive(Debug, Clone, Copy, PartialEq, Eq)]
32struct AnimalState {
33    in_love: i32,
34    love_cause: Option<Uuid>,
35}
36
37impl AnimalState {
38    const fn new() -> Self {
39        Self {
40            in_love: 0,
41            love_cause: None,
42        }
43    }
44}
45
46/// Runtime fields shared by vanilla animals.
47#[derive(Debug)]
48pub struct AnimalBase {
49    state: SyncMutex<AnimalState>,
50}
51
52impl AnimalBase {
53    /// Creates default animal runtime state.
54    #[must_use]
55    pub const fn new() -> Self {
56        Self {
57            state: SyncMutex::new(AnimalState::new()),
58        }
59    }
60
61    pub fn initialize_pathfinding_malus(mob_base: &MobBase) {
62        let mut malus = mob_base.pathfinding_malus().lock();
63        malus.set(PathType::FireInNeighbor, 16.0);
64        malus.set(PathType::Fire, -1.0);
65    }
66
67    /// Returns vanilla `Animal.inLove`.
68    #[must_use]
69    pub fn in_love_time(&self) -> i32 {
70        self.state.lock().in_love
71    }
72
73    /// Sets vanilla `Animal.inLove`.
74    pub fn set_in_love_time(&self, in_love: i32) {
75        self.state.lock().in_love = in_love;
76    }
77
78    /// Decrements vanilla `Animal.inLove` when it is active.
79    pub fn tick_in_love_time(&self) {
80        let mut state = self.state.lock();
81        if state.in_love > 0 {
82            state.in_love -= 1;
83        }
84    }
85
86    /// Returns vanilla `Animal.loveCause` as a persisted UUID.
87    #[must_use]
88    pub fn love_cause_uuid(&self) -> Option<Uuid> {
89        self.state.lock().love_cause
90    }
91
92    /// Sets vanilla `Animal.loveCause` as a persisted UUID.
93    pub fn set_love_cause_uuid(&self, love_cause: Option<Uuid>) {
94        self.state.lock().love_cause = love_cause;
95    }
96}
97
98impl Default for AnimalBase {
99    fn default() -> Self {
100        Self::new()
101    }
102}
103
104/// Vanilla-shaped behavior shared by entities that extend `Animal`.
105pub trait Animal: AgeableMob {
106    /// Returns shared animal runtime state.
107    fn animal_base(&self) -> &AnimalBase;
108
109    /// Returns vanilla `Animal.inLove`.
110    fn in_love_time(&self) -> i32 {
111        self.animal_base().in_love_time()
112    }
113
114    /// Sets vanilla `Animal.inLove`.
115    fn set_in_love_time(&self, in_love: i32) {
116        self.animal_base().set_in_love_time(in_love);
117    }
118
119    /// Returns vanilla `Animal.loveCause` as a persisted UUID.
120    fn love_cause_uuid(&self) -> Option<Uuid> {
121        self.animal_base().love_cause_uuid()
122    }
123
124    /// Sets vanilla `Animal.loveCause` as a persisted UUID.
125    fn set_love_cause_uuid(&self, love_cause: Option<Uuid>) {
126        self.animal_base().set_love_cause_uuid(love_cause);
127    }
128
129    /// Returns vanilla `Animal.isInLove`.
130    fn is_in_love(&self) -> bool {
131        self.in_love_time() > 0
132    }
133
134    /// Returns vanilla `Animal.canFallInLove`.
135    fn can_fall_in_love(&self) -> bool {
136        self.in_love_time() <= 0
137    }
138
139    /// Sets vanilla love mode and records the player that caused it.
140    fn set_in_love(&self, player: Option<&Player>) {
141        self.set_in_love_time(IN_LOVE_TIME);
142        if let Some(player) = player {
143            self.set_love_cause_uuid(Some(player.gameprofile.id));
144        }
145
146        self.broadcast_entity_event(EntityStatus::InLoveHearts);
147    }
148
149    /// Resets vanilla love mode without clearing the stored love cause.
150    fn reset_love(&self) {
151        self.set_in_love_time(0);
152    }
153
154    /// Returns vanilla `Animal.canMate`.
155    fn can_mate(&self, partner: &dyn Animal) -> bool {
156        self.uuid() != partner.uuid()
157            && self.entity_type() == partner.entity_type()
158            && self.is_in_love()
159            && partner.is_in_love()
160    }
161
162    /// Returns whether the stack is valid food for this animal.
163    fn is_food(&self, _item_stack: &ItemStack) -> bool {
164        false
165    }
166
167    /// Returns vanilla `Animal.getBaseExperienceReward`.
168    fn base_experience_reward_animal(&self) -> i32 {
169        1 + rand::random_range(0..3)
170    }
171
172    /// Returns vanilla `Animal.getAmbientSoundInterval`.
173    fn ambient_sound_interval_animal(&self) -> i32 {
174        120
175    }
176
177    /// Returns vanilla `Animal.getWalkTargetValue`.
178    fn animal_walk_target_value(&self, pos: BlockPos) -> f32 {
179        let Some(world) = self.level() else {
180            return 0.0;
181        };
182
183        if world.get_block_state(pos.below()).get_block() == &vanilla_blocks::GRASS_BLOCK {
184            10.0
185        } else {
186            world.pathfinding_cost_from_light_levels(pos)
187        }
188    }
189
190    /// Returns vanilla `Animal.isBrightEnoughToSpawn`.
191    fn is_bright_enough_to_spawn(level: &dyn LevelReader, pos: BlockPos) -> bool
192    where
193        Self: Sized,
194    {
195        level.raw_brightness(pos, 0) > 8
196    }
197
198    /// Returns vanilla `Animal.checkAnimalSpawnRules`.
199    fn check_animal_spawn_rules(
200        level: &dyn LevelReader,
201        spawn_reason: EntitySpawnReason,
202        pos: BlockPos,
203    ) -> bool
204    where
205        Self: Sized,
206    {
207        let bright_enough = spawn_reason.ignores_light_requirements()
208            || Self::is_bright_enough_to_spawn(level, pos);
209        level
210            .get_block_state(pos.below())
211            .get_block()
212            .has_tag(&BlockTag::ANIMALS_SPAWNABLE_ON)
213            && bright_enough
214    }
215
216    /// Plays this animal's vanilla eating sound.
217    fn play_eating_sound(&self) {}
218
219    /// Handles vanilla `Animal.mobInteract`.
220    fn mob_interact_animal(&self, player: &Player, hand: InteractionHand) -> InteractionResult {
221        let item_stack = {
222            let inventory = player.inventory.lock();
223            let item_stack = inventory.get_item_in_hand(hand);
224            item_stack.copy_with_count(item_stack.count())
225        };
226
227        if !self.is_food(&item_stack) {
228            return self.mob_interact_ageable(player, hand);
229        }
230
231        let age = self.get_age();
232        if age == 0 && self.can_fall_in_love() {
233            Mob::use_player_item(self, player, hand);
234            self.set_in_love(Some(player));
235            self.play_eating_sound();
236            return InteractionResult::SuccessServer;
237        }
238
239        if self.can_age_up() {
240            Mob::use_player_item(self, player, hand);
241            self.age_up(
242                AgeableMobBase::get_speed_up_seconds_when_feeding(-age),
243                true,
244            );
245            self.play_eating_sound();
246            return InteractionResult::Success;
247        }
248
249        self.mob_interact_ageable(player, hand)
250    }
251
252    /// Creates a same-type offspring using the registered entity factory.
253    fn create_breed_offspring(&self, world: &Arc<World>) -> Option<SharedEntity> {
254        ENTITIES.create(
255            self.entity_type(),
256            next_entity_id(),
257            self.position(),
258            Arc::downgrade(world),
259        )
260    }
261
262    /// Returns this animal's breedable variant key when offspring inherit it.
263    fn breed_variant_key(&self) -> Option<&Identifier> {
264        None
265    }
266
267    /// Applies a breedable variant key to offspring that inherit one.
268    fn set_breed_variant_key(&self, _key: &Identifier) -> bool {
269        false
270    }
271
272    /// Applies entity-specific state to freshly created breeding offspring.
273    fn initialize_breed_offspring(&self, _partner: &dyn Animal, _offspring: &dyn Animal) {}
274
275    /// Creates this animal's vanilla breeding offspring.
276    fn get_breed_offspring(
277        &self,
278        world: &Arc<World>,
279        partner: &dyn Animal,
280    ) -> Option<SharedEntity> {
281        let offspring = self.create_breed_offspring(world)?;
282        let Some(offspring_animal) = offspring.as_animal() else {
283            log::error!(
284                "breeding entity type {} created non-animal offspring",
285                self.entity_type().key
286            );
287            return None;
288        };
289
290        self.initialize_breed_offspring(partner, offspring_animal);
291        Some(offspring)
292    }
293
294    /// Creates, initializes, and inserts vanilla breeding offspring.
295    fn spawn_child_from_breeding(&self, world: &Arc<World>, partner: &dyn Animal) {
296        let Some(offspring) = self.get_breed_offspring(world, partner) else {
297            return;
298        };
299
300        {
301            let Some(offspring_animal) = offspring.as_animal() else {
302                log::error!(
303                    "breeding entity type {} created non-animal offspring",
304                    self.entity_type().key
305                );
306                return;
307            };
308            offspring_animal.set_baby(true);
309            if let Err(error) = offspring_animal.try_set_position(self.position()) {
310                log::error!(
311                    "failed to position breeding offspring {} at parent {}: {error}",
312                    offspring.id(),
313                    self.id()
314                );
315                return;
316            }
317            offspring_animal.set_rotation((0.0, 0.0));
318            offspring_animal.set_old_position_to_current();
319
320            self.finalize_spawn_child_from_breeding(world, partner, Some(offspring_animal));
321        }
322
323        if let Err(error) = world.try_add_entity(offspring) {
324            log::error!(
325                "failed to add breeding offspring for entity {} to world: {error}",
326                self.id()
327            );
328        }
329    }
330
331    /// Applies vanilla breeding side effects after offspring creation.
332    fn finalize_spawn_child_from_breeding(
333        &self,
334        world: &Arc<World>,
335        partner: &dyn Animal,
336        _offspring: Option<&dyn Animal>,
337    ) {
338        if self
339            .love_cause_uuid()
340            .or_else(|| partner.love_cause_uuid())
341            .is_some()
342        {
343            // TODO: Award the animals-bred stat and advancement once those foundations exist.
344        }
345
346        self.set_age(PARENT_AGE_AFTER_BREEDING);
347        partner.set_age(PARENT_AGE_AFTER_BREEDING);
348        self.reset_love();
349        partner.reset_love();
350        self.broadcast_entity_event(EntityStatus::InLoveHearts);
351
352        if world.get_game_rule(&MOB_DROPS) {
353            let xp = rand::random_range(0..7) + 1;
354            ExperienceOrbEntity::award(world, self.position(), xp);
355        }
356    }
357
358    /// Ticks vanilla animal love state.
359    fn tick_animal_love(&self) {
360        if self.get_age() != 0 {
361            self.reset_love();
362            return;
363        }
364
365        self.animal_base().tick_in_love_time();
366        // VANILLA CLIENT-LOCAL: `Animal.aiStep` creates the periodic heart particles.
367    }
368
369    /// Runs vanilla `Animal.customServerAiStep`.
370    fn custom_server_ai_step_animal(&self) {
371        if self.get_age() != 0 {
372            self.reset_love();
373        }
374    }
375
376    /// Returns vanilla animal far-away despawn behavior.
377    fn remove_when_far_away_animal(&self, _dist_sqr: f64) -> bool {
378        false
379    }
380
381    /// Saves vanilla animal fields.
382    fn save_animal(&self, nbt: &mut NbtCompound) {
383        nbt.insert("InLove", self.in_love_time());
384        if let Some(love_cause) = self.love_cause_uuid() {
385            nbt.insert(
386                "LoveCause",
387                NbtTag::IntArray(love_cause.to_int_array().to_vec()),
388            );
389        }
390    }
391
392    /// Loads vanilla animal fields.
393    fn load_animal(&self, nbt: BorrowedNbtCompoundView<'_, '_>) {
394        self.set_in_love_time(nbt.int("InLove").unwrap_or(0));
395        if let Some(love_cause) = nbt.int_array("LoveCause")
396            && let Some(uuid) = Uuid::from_int_array(&love_cause)
397        {
398            self.set_love_cause_uuid(Some(uuid));
399        }
400    }
401}
402
403#[cfg(test)]
404mod tests {
405    use steel_registry::{REGISTRY, init_vanilla_registry, vanilla_blocks};
406    use steel_utils::BlockStateId;
407
408    use super::*;
409    use crate::entity::entities::PigEntity;
410
411    struct SpawnRuleLevel {
412        below_pos: BlockPos,
413        below_state: BlockStateId,
414        raw_brightness: u8,
415    }
416
417    impl LevelReader for SpawnRuleLevel {
418        fn get_block_state(&self, pos: BlockPos) -> BlockStateId {
419            if pos == self.below_pos {
420                return self.below_state;
421            }
422
423            REGISTRY.blocks.get_default_state_id(&vanilla_blocks::AIR)
424        }
425
426        fn raw_brightness(&self, _pos: BlockPos, _sky_darkening: u8) -> u8 {
427            self.raw_brightness
428        }
429
430        fn min_y(&self) -> i32 {
431            -64
432        }
433
434        fn height(&self) -> i32 {
435            384
436        }
437    }
438
439    fn spawn_rule_level(block_below: BlockStateId, raw_brightness: u8) -> SpawnRuleLevel {
440        SpawnRuleLevel {
441            below_pos: BlockPos::new(0, 63, 0),
442            below_state: block_below,
443            raw_brightness,
444        }
445    }
446
447    #[test]
448    fn animal_spawn_rules_require_spawnable_block_tag() {
449        init_vanilla_registry();
450        let level = spawn_rule_level(vanilla_blocks::STONE.default_state(), 15);
451
452        assert!(!<PigEntity as Animal>::check_animal_spawn_rules(
453            &level,
454            EntitySpawnReason::Natural,
455            BlockPos::new(0, 64, 0)
456        ));
457    }
458
459    #[test]
460    fn animal_spawn_rules_require_raw_brightness_above_eight() {
461        init_vanilla_registry();
462        let level = spawn_rule_level(vanilla_blocks::GRASS_BLOCK.default_state(), 8);
463
464        assert!(!<PigEntity as Animal>::check_animal_spawn_rules(
465            &level,
466            EntitySpawnReason::Natural,
467            BlockPos::new(0, 64, 0)
468        ));
469
470        let level = spawn_rule_level(vanilla_blocks::GRASS_BLOCK.default_state(), 9);
471
472        assert!(<PigEntity as Animal>::check_animal_spawn_rules(
473            &level,
474            EntitySpawnReason::Natural,
475            BlockPos::new(0, 64, 0)
476        ));
477    }
478
479    #[test]
480    fn animal_spawn_rules_trial_spawner_ignores_light() {
481        init_vanilla_registry();
482        let level = spawn_rule_level(vanilla_blocks::GRASS_BLOCK.default_state(), 0);
483
484        assert!(<PigEntity as Animal>::check_animal_spawn_rules(
485            &level,
486            EntitySpawnReason::TrialSpawner,
487            BlockPos::new(0, 64, 0)
488        ));
489    }
490}