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