1use 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#[derive(Debug)]
47pub struct AgeableMobBase {
48 state: SyncMutex<AgeableMobState>,
49}
50
51impl AgeableMobBase {
52 #[must_use]
54 pub const fn new() -> Self {
55 Self {
56 state: SyncMutex::new(AgeableMobState::new()),
57 }
58 }
59
60 #[must_use]
62 pub fn age(&self) -> i32 {
63 self.state.lock().age
64 }
65
66 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 #[must_use]
76 pub fn forced_age(&self) -> i32 {
77 self.state.lock().forced_age
78 }
79
80 pub fn set_forced_age(&self, forced_age: i32) {
82 self.state.lock().forced_age = forced_age;
83 }
84
85 #[must_use]
87 pub fn forced_age_timer(&self) -> i32 {
88 self.state.lock().forced_age_timer
89 }
90
91 pub fn set_forced_age_timer(&self, forced_age_timer: i32) {
93 self.state.lock().forced_age_timer = forced_age_timer;
94 }
95
96 #[must_use]
98 pub fn age_lock_particle_timer(&self) -> i32 {
99 self.state.lock().age_lock_particle_timer
100 }
101
102 pub fn set_age_lock_particle_timer(&self, timer: i32) {
104 self.state.lock().age_lock_particle_timer = timer;
105 }
106
107 pub fn add_forced_age(&self, delta: i32) {
109 self.state.lock().forced_age += delta;
110 }
111
112 #[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
125pub trait AgeableMob: Mob {
127 fn ageable_base(&self) -> &AgeableMobBase;
129
130 fn is_age_locked(&self) -> bool;
132
133 fn set_age_locked(&self, age_locked: bool);
135
136 fn set_synced_baby(&self, baby: bool);
138
139 fn age_boundary_changed(&self, _baby: bool) {
141 self.refresh_dimensions();
142 }
143
144 fn get_baby_start_age(&self) -> i32 {
146 BABY_START_AGE
147 }
148
149 fn get_age(&self) -> i32 {
151 self.ageable_base().age()
152 }
153
154 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 fn is_baby(&self) -> bool {
165 self.get_age() < 0
166 }
167
168 fn set_baby(&self, baby: bool) {
170 self.set_age(if baby { self.get_baby_start_age() } else { 0 });
171 }
172
173 fn forced_age(&self) -> i32 {
175 self.ageable_base().forced_age()
176 }
177
178 fn set_forced_age(&self, forced_age: i32) {
180 self.ageable_base().set_forced_age(forced_age);
181 }
182
183 fn forced_age_timer(&self) -> i32 {
185 self.ageable_base().forced_age_timer()
186 }
187
188 fn set_forced_age_timer(&self, forced_age_timer: i32) {
190 self.ageable_base().set_forced_age_timer(forced_age_timer);
191 }
192
193 fn age_lock_particle_timer(&self) -> i32 {
195 self.ageable_base().age_lock_particle_timer()
196 }
197
198 fn set_age_lock_particle_timer(&self, timer: i32) {
200 self.ageable_base().set_age_lock_particle_timer(timer);
201 }
202
203 fn can_age_up(&self) -> bool {
205 AgeableMob::is_baby(self) && !self.is_age_locked()
206 }
207
208 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 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 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 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 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 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 fn breed_variant_key(&self) -> Option<&Identifier> {
337 None
338 }
339
340 fn set_breed_variant_key(&self, _key: &Identifier) -> bool {
342 false
343 }
344
345 fn initialize_breed_offspring(&self, _partner: &dyn AgeableMob, _offspring: &dyn AgeableMob) {}
347
348 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 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 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}