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