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