Skip to main content

steel_core/entity/
animal.rs

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