Skip to main content

steel_core/entity/
ageable.rs

1//! Shared vanilla `AgeableMob` state and hooks.
2
3use std::sync::Arc;
4
5use simdnbt::borrow::NbtCompound as BorrowedNbtCompoundView;
6use simdnbt::owned::NbtCompound;
7use steel_protocol::packets::game::SoundSource;
8use steel_registry::vanilla_entity_type_tags::EntityTypeTag;
9use steel_registry::{REGISTRY, TaggedRegistryExt, sound_events, vanilla_items};
10use steel_utils::Identifier;
11use steel_utils::locks::SyncMutex;
12use steel_utils::nbt::NbtValueInput as _;
13use steel_utils::types::InteractionHand;
14
15use crate::behavior::InteractionResult;
16use crate::entity::{
17    AgeableMobGroupData, ENTITIES, Entity, EntitySpawnReason, Mob, SharedEntity, SpawnGroupData,
18    next_entity_id,
19};
20use crate::player::Player;
21use crate::world::World;
22
23const BABY_START_AGE: i32 = -24_000;
24const AGE_LOCK_COOLDOWN_TICKS: i32 = 40;
25const FORCED_AGE_PARTICLE_TICKS: i32 = 40;
26
27#[derive(Debug, Clone, Copy, PartialEq, Eq)]
28struct AgeableMobState {
29    age: i32,
30    forced_age: i32,
31    forced_age_timer: i32,
32    age_lock_particle_timer: i32,
33}
34
35impl AgeableMobState {
36    const fn new() -> Self {
37        Self {
38            age: 0,
39            forced_age: 0,
40            forced_age_timer: 0,
41            age_lock_particle_timer: 0,
42        }
43    }
44}
45
46/// Runtime fields shared by vanilla ageable mobs.
47#[derive(Debug)]
48pub struct AgeableMobBase {
49    state: SyncMutex<AgeableMobState>,
50}
51
52impl AgeableMobBase {
53    /// Creates default ageable runtime state.
54    #[must_use]
55    pub const fn new() -> Self {
56        Self {
57            state: SyncMutex::new(AgeableMobState::new()),
58        }
59    }
60
61    /// Age in ticks: negative while a baby, reaching zero at adulthood.
62    #[must_use]
63    pub fn age(&self) -> i32 {
64        self.state.lock().age
65    }
66
67    /// Sets vanilla `AgeableMob.age`, returning whether the baby/adult boundary changed.
68    pub fn set_age(&self, age: i32) -> bool {
69        let mut state = self.state.lock();
70        let old_age = state.age;
71        state.age = age;
72        old_age < 0 && age >= 0 || old_age >= 0 && age < 0
73    }
74
75    /// Extra age credited by forced growth, reapplied once natural age reaches zero.
76    #[must_use]
77    pub fn forced_age(&self) -> i32 {
78        self.state.lock().forced_age
79    }
80
81    /// Sets the extra age credited by forced growth.
82    pub fn set_forced_age(&self, forced_age: i32) {
83        self.state.lock().forced_age = forced_age;
84    }
85
86    /// Ticks remaining in the forced-growth particle effect.
87    #[must_use]
88    pub fn forced_age_timer(&self) -> i32 {
89        self.state.lock().forced_age_timer
90    }
91
92    /// Sets the ticks remaining in the forced-growth particle effect.
93    pub fn set_forced_age_timer(&self, forced_age_timer: i32) {
94        self.state.lock().forced_age_timer = forced_age_timer;
95    }
96
97    /// Ticks remaining before this mob's age-lock state can be toggled again.
98    #[must_use]
99    pub fn age_lock_particle_timer(&self) -> i32 {
100        self.state.lock().age_lock_particle_timer
101    }
102
103    /// Sets the cooldown before this mob's age-lock state can be toggled again.
104    pub fn set_age_lock_particle_timer(&self, timer: i32) {
105        self.state.lock().age_lock_particle_timer = timer;
106    }
107
108    /// Adds delta to the extra age credited by forced growth.
109    pub fn add_forced_age(&self, delta: i32) {
110        self.state.lock().forced_age += delta;
111    }
112
113    /// Seconds to reduce the remaining growth time by when fed, roughly a tenth of the time left in seconds.
114    #[must_use]
115    pub fn get_speed_up_seconds_when_feeding(ticks_until_adult: i32) -> i32 {
116        ((ticks_until_adult / 20) as f32 * 0.1) as i32
117    }
118}
119
120impl Default for AgeableMobBase {
121    fn default() -> Self {
122        Self::new()
123    }
124}
125
126/// Vanilla-shaped behavior shared by entities that extend `AgeableMob`.
127pub trait AgeableMob: Mob {
128    /// Returns shared ageable runtime state.
129    fn ageable_base(&self) -> &AgeableMobBase;
130
131    /// Returns the synchronized vanilla age-lock flag.
132    fn is_age_locked(&self) -> bool;
133
134    /// Sets the synchronized vanilla age-lock flag.
135    fn set_age_locked(&self, age_locked: bool);
136
137    /// Sets the synchronized baby flag.
138    fn set_synced_baby(&self, baby: bool);
139
140    /// Hook called after the baby/adult boundary changes.
141    fn age_boundary_changed(&self, _baby: bool) {
142        self.refresh_dimensions();
143    }
144
145    /// Starting age assigned to a newly spawned baby.
146    fn get_baby_start_age(&self) -> i32 {
147        BABY_START_AGE
148    }
149
150    /// Age in ticks: negative while a baby, reaching zero at adulthood.
151    fn get_age(&self) -> i32 {
152        self.ageable_base().age()
153    }
154
155    /// Sets vanilla `AgeableMob.age` and updates synchronized baby state.
156    fn set_age(&self, age: i32) {
157        if self.ageable_base().set_age(age) {
158            let baby = age < 0;
159            self.set_synced_baby(baby);
160            self.age_boundary_changed(baby);
161        }
162    }
163
164    /// Returns whether this ageable mob is a baby.
165    fn is_baby(&self) -> bool {
166        self.get_age() < 0
167    }
168
169    /// Extra age credited by forced growth, reapplied once natural age reaches zero.
170    fn forced_age(&self) -> i32 {
171        self.ageable_base().forced_age()
172    }
173
174    /// Sets the extra age credited by forced growth.
175    fn set_forced_age(&self, forced_age: i32) {
176        self.ageable_base().set_forced_age(forced_age);
177    }
178
179    /// Ticks remaining in the forced-growth particle effect.
180    fn forced_age_timer(&self) -> i32 {
181        self.ageable_base().forced_age_timer()
182    }
183
184    /// Sets the ticks remaining in the forced-growth particle effect.
185    fn set_forced_age_timer(&self, forced_age_timer: i32) {
186        self.ageable_base().set_forced_age_timer(forced_age_timer);
187    }
188
189    /// Ticks remaining before this mob's age-lock state can be toggled again.
190    fn age_lock_particle_timer(&self) -> i32 {
191        self.ageable_base().age_lock_particle_timer()
192    }
193
194    /// Sets the cooldown before this mob's age-lock state can be toggled again.
195    fn set_age_lock_particle_timer(&self, timer: i32) {
196        self.ageable_base().set_age_lock_particle_timer(timer);
197    }
198
199    /// Returns whether this mob can naturally age toward adulthood this tick.
200    fn can_age_up(&self) -> bool {
201        AgeableMob::is_baby(self) && !self.is_age_locked()
202    }
203
204    /// Seconds to reduce the remaining growth time by when fed, roughly a tenth of the time left in seconds.
205    fn get_speed_up_seconds_when_feeding(ticks_until_adult: i32) -> i32
206    where
207        Self: Sized,
208    {
209        AgeableMobBase::get_speed_up_seconds_when_feeding(ticks_until_adult)
210    }
211
212    /// Advances age toward adulthood by the given seconds; when forced, also credits
213    /// forced-growth bookkeeping and restarts its particle timer.
214    fn age_up(&self, seconds: i32, forced: bool) {
215        let old_age = self.get_age();
216        let mut age = old_age + seconds * 20;
217        if age > 0 {
218            age = 0;
219        }
220
221        let delta = age - old_age;
222        self.set_age(age);
223        if forced {
224            self.ageable_base().add_forced_age(delta);
225            if self.forced_age_timer() == 0 {
226                self.set_forced_age_timer(FORCED_AGE_PARTICLE_TICKS);
227            }
228        }
229
230        if self.get_age() == 0 {
231            self.set_age(self.forced_age());
232        }
233    }
234
235    /// Runs vanilla `AgeableMob.finalizeSpawn`, then delegates to `Mob.finalizeSpawn`.
236    fn finalize_spawn_ageable_mob(
237        &self,
238        world: &Arc<World>,
239        spawn_reason: EntitySpawnReason,
240        group_data: Option<SpawnGroupData>,
241    ) -> Option<SpawnGroupData> {
242        let mut group_data = match group_data {
243            Some(SpawnGroupData::AgeableMob(group_data)) => group_data,
244            None => AgeableMobGroupData::with_should_spawn_baby(true),
245        };
246        if group_data.finalize_ageable_spawn(rand::random::<f32>) {
247            self.set_age(self.get_baby_start_age());
248        }
249
250        self.finalize_spawn_mob_base(
251            world,
252            spawn_reason,
253            Some(SpawnGroupData::AgeableMob(group_data)),
254        )
255    }
256
257    /// Toggles this baby mob's age lock when fed a golden dandelion, preventing further growth while locked.
258    fn mob_interact_ageable(&self, player: &Player, hand: InteractionHand) -> InteractionResult {
259        let item_stack = {
260            let inventory = player.inventory.lock();
261            let item_stack = inventory.get_item_in_hand(hand);
262            item_stack.copy_with_count(item_stack.count())
263        };
264
265        if !item_stack.is(&vanilla_items::GOLDEN_DANDELION)
266            || !AgeableMob::is_baby(self)
267            || self.age_lock_particle_timer() != 0
268            || REGISTRY
269                .entity_types
270                .is_in_tag(self.entity_type(), &EntityTypeTag::CANNOT_BE_AGE_LOCKED)
271        {
272            return InteractionResult::Pass;
273        }
274
275        self.set_age_locked(!self.is_age_locked());
276        self.set_age(self.get_baby_start_age());
277        self.set_age_lock_particle_timer(AGE_LOCK_COOLDOWN_TICKS);
278        Mob::use_player_item(self, player, hand);
279
280        let is_age_locked = self.is_age_locked();
281        if is_age_locked {
282            self.set_persistence_required();
283        }
284        if let Some(world) = self.level() {
285            let sound = if is_age_locked {
286                &sound_events::ITEM_GOLDEN_DANDELION_USE
287            } else {
288                &sound_events::ITEM_GOLDEN_DANDELION_UNUSE
289            };
290            world.play_sound(
291                sound,
292                SoundSource::Players,
293                self.block_position(),
294                1.0,
295                1.0,
296                None,
297            );
298        }
299
300        InteractionResult::Success
301    }
302
303    /// Creates a same-type offspring using the registered entity factory.
304    fn create_breed_offspring(&self, world: &Arc<World>) -> Option<SharedEntity> {
305        ENTITIES.create(
306            self.entity_type(),
307            next_entity_id(),
308            self.position(),
309            Arc::downgrade(world),
310        )
311    }
312
313    /// Creates this animal's vanilla breeding offspring.
314    fn get_breed_offspring(
315        &self,
316        world: &Arc<World>,
317        partner: &dyn AgeableMob,
318    ) -> Option<SharedEntity> {
319        let offspring = self.create_breed_offspring(world)?;
320        let Some(offspring_animal) = offspring.as_ageable_mob() else {
321            log::error!(
322                "breeding entity type {} created non-ageable offspring",
323                self.entity_type().key
324            );
325            return None;
326        };
327
328        self.initialize_breed_offspring(partner, offspring_animal);
329        Some(offspring)
330    }
331
332    /// Returns this animal's breedable variant key when offspring inherit it.
333    fn breed_variant_key(&self) -> Option<&Identifier> {
334        None
335    }
336
337    /// Applies a breedable variant key to offspring that inherit one.
338    fn set_breed_variant_key(&self, _key: &Identifier) -> bool {
339        false
340    }
341
342    /// Applies entity-specific state to freshly created breeding offspring.
343    fn initialize_breed_offspring(&self, _partner: &dyn AgeableMob, _offspring: &dyn AgeableMob) {}
344
345    /// Ticks vanilla age progression.
346    fn tick_ageable_mob(&self) {
347        if !Entity::is_alive(self) {
348            return;
349        }
350
351        let age = self.get_age();
352        if self.can_age_up() {
353            self.set_age(age + 1);
354        } else if age > 0 {
355            self.set_age(age - 1);
356        }
357
358        let age_lock_particle_timer = self.age_lock_particle_timer();
359        if age_lock_particle_timer > 0 {
360            self.set_age_lock_particle_timer(age_lock_particle_timer - 1);
361        }
362    }
363
364    /// Saves vanilla ageable mob fields.
365    fn save_ageable_mob(&self, nbt: &mut NbtCompound) {
366        nbt.insert("Age", self.get_age());
367        nbt.insert("ForcedAge", self.forced_age());
368        nbt.insert("AgeLocked", i8::from(self.is_age_locked()));
369    }
370
371    /// Loads vanilla ageable mob fields.
372    fn load_ageable_mob(&self, nbt: BorrowedNbtCompoundView<'_, '_>) {
373        self.set_age(nbt.int_value("Age").unwrap_or(0));
374        self.set_forced_age(nbt.int_value("ForcedAge").unwrap_or(0));
375        self.set_age_locked(nbt.byte_value("AgeLocked").is_some_and(|value| value != 0));
376    }
377}
378
379#[cfg(test)]
380mod tests {
381    use std::sync::Weak;
382
383    use glam::DVec3;
384    use steel_registry::entity_type::EntityTypeRef;
385    use steel_registry::{init_vanilla_registry, vanilla_entities};
386
387    use super::*;
388    use crate::entity::{EntityBase, LivingEntity, LivingEntityBase, MobBase};
389
390    struct TestAgeableMob {
391        base: EntityBase,
392        living_base: LivingEntityBase,
393        mob_base: MobBase,
394        ageable_base: AgeableMobBase,
395        mob_flags: SyncMutex<i8>,
396        health: SyncMutex<f32>,
397        baby: SyncMutex<bool>,
398        age_locked: SyncMutex<bool>,
399    }
400
401    impl TestAgeableMob {
402        fn new() -> Self {
403            init_vanilla_registry();
404            Self {
405                base: EntityBase::new(
406                    1,
407                    DVec3::ZERO,
408                    vanilla_entities::PIG.dimensions,
409                    Weak::new(),
410                ),
411                living_base: LivingEntityBase::new(&vanilla_entities::PIG),
412                mob_base: MobBase::new(),
413                ageable_base: AgeableMobBase::new(),
414                mob_flags: SyncMutex::new(0),
415                health: SyncMutex::new(10.0),
416                baby: SyncMutex::new(false),
417                age_locked: SyncMutex::new(false),
418            }
419        }
420    }
421
422    crate::entity::impl_test_downcast_type!(TestAgeableMob);
423
424    impl Entity for TestAgeableMob {
425        fn base(&self) -> &EntityBase {
426            &self.base
427        }
428
429        fn entity_type(&self) -> EntityTypeRef {
430            &vanilla_entities::PIG
431        }
432    }
433
434    impl LivingEntity for TestAgeableMob {
435        fn living_base(&self) -> &LivingEntityBase {
436            &self.living_base
437        }
438
439        fn get_health(&self) -> f32 {
440            *self.health.lock()
441        }
442
443        fn set_health(&self, health: f32) {
444            *self.health.lock() = health;
445        }
446    }
447
448    impl Mob for TestAgeableMob {
449        fn mob_base(&self) -> &MobBase {
450            &self.mob_base
451        }
452
453        fn mob_flags(&self) -> i8 {
454            *self.mob_flags.lock()
455        }
456
457        fn set_mob_flags(&self, flags: i8) {
458            *self.mob_flags.lock() = flags;
459        }
460    }
461
462    impl AgeableMob for TestAgeableMob {
463        fn ageable_base(&self) -> &AgeableMobBase {
464            &self.ageable_base
465        }
466
467        fn is_age_locked(&self) -> bool {
468            *self.age_locked.lock()
469        }
470
471        fn set_age_locked(&self, age_locked: bool) {
472            *self.age_locked.lock() = age_locked;
473        }
474
475        fn set_synced_baby(&self, baby: bool) {
476            *self.baby.lock() = baby;
477        }
478    }
479
480    #[test]
481    fn age_boundary_updates_synced_baby_flag() {
482        let mob = TestAgeableMob::new();
483
484        mob.set_age(-1);
485        assert!(AgeableMob::is_baby(&mob));
486        assert!(*mob.baby.lock());
487
488        mob.set_age(0);
489        assert!(!AgeableMob::is_baby(&mob));
490        assert!(!*mob.baby.lock());
491    }
492
493    #[test]
494    fn age_tick_grows_babies_and_reduces_positive_forced_age() {
495        let mob = TestAgeableMob::new();
496
497        mob.set_age(-2);
498        mob.tick_ageable_mob();
499        assert_eq!(mob.get_age(), -1);
500
501        mob.set_age(2);
502        mob.tick_ageable_mob();
503        assert_eq!(mob.get_age(), 1);
504    }
505
506    #[test]
507    fn age_lock_blocks_baby_growth() {
508        let mob = TestAgeableMob::new();
509
510        mob.set_age(-2);
511        mob.set_age_locked(true);
512        mob.tick_ageable_mob();
513
514        assert_eq!(mob.get_age(), -2);
515    }
516
517    #[test]
518    fn age_up_adds_forced_age_and_timer() {
519        let mob = TestAgeableMob::new();
520
521        mob.set_age(-100);
522        mob.age_up(1, true);
523
524        assert_eq!(mob.get_age(), -80);
525        assert_eq!(mob.forced_age(), 20);
526        assert_eq!(mob.forced_age_timer(), 40);
527    }
528
529    #[test]
530    fn age_up_applies_forced_age_when_reaching_adulthood() {
531        let mob = TestAgeableMob::new();
532
533        mob.set_age(-10);
534        mob.age_up(1, true);
535
536        assert_eq!(mob.get_age(), 10);
537        assert_eq!(mob.forced_age(), 10);
538    }
539
540    #[test]
541    fn feeding_speed_up_seconds_matches_vanilla_integer_order() {
542        assert_eq!(
543            TestAgeableMob::get_speed_up_seconds_when_feeding(24_000),
544            120
545        );
546        assert_eq!(TestAgeableMob::get_speed_up_seconds_when_feeding(199), 0);
547    }
548}