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::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#[derive(Debug)]
48pub struct AgeableMobBase {
49 state: SyncMutex<AgeableMobState>,
50}
51
52impl AgeableMobBase {
53 #[must_use]
55 pub const fn new() -> Self {
56 Self {
57 state: SyncMutex::new(AgeableMobState::new()),
58 }
59 }
60
61 #[must_use]
63 pub fn age(&self) -> i32 {
64 self.state.lock().age
65 }
66
67 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 #[must_use]
77 pub fn forced_age(&self) -> i32 {
78 self.state.lock().forced_age
79 }
80
81 pub fn set_forced_age(&self, forced_age: i32) {
83 self.state.lock().forced_age = forced_age;
84 }
85
86 #[must_use]
88 pub fn forced_age_timer(&self) -> i32 {
89 self.state.lock().forced_age_timer
90 }
91
92 pub fn set_forced_age_timer(&self, forced_age_timer: i32) {
94 self.state.lock().forced_age_timer = forced_age_timer;
95 }
96
97 #[must_use]
99 pub fn age_lock_particle_timer(&self) -> i32 {
100 self.state.lock().age_lock_particle_timer
101 }
102
103 pub fn set_age_lock_particle_timer(&self, timer: i32) {
105 self.state.lock().age_lock_particle_timer = timer;
106 }
107
108 pub fn add_forced_age(&self, delta: i32) {
110 self.state.lock().forced_age += delta;
111 }
112
113 #[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
126pub trait AgeableMob: Mob {
128 fn ageable_base(&self) -> &AgeableMobBase;
130
131 fn is_age_locked(&self) -> bool;
133
134 fn set_age_locked(&self, age_locked: bool);
136
137 fn set_synced_baby(&self, baby: bool);
139
140 fn age_boundary_changed(&self, _baby: bool) {
142 self.refresh_dimensions();
143 }
144
145 fn get_baby_start_age(&self) -> i32 {
147 BABY_START_AGE
148 }
149
150 fn get_age(&self) -> i32 {
152 self.ageable_base().age()
153 }
154
155 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 fn is_baby(&self) -> bool {
166 self.get_age() < 0
167 }
168
169 fn forced_age(&self) -> i32 {
171 self.ageable_base().forced_age()
172 }
173
174 fn set_forced_age(&self, forced_age: i32) {
176 self.ageable_base().set_forced_age(forced_age);
177 }
178
179 fn forced_age_timer(&self) -> i32 {
181 self.ageable_base().forced_age_timer()
182 }
183
184 fn set_forced_age_timer(&self, forced_age_timer: i32) {
186 self.ageable_base().set_forced_age_timer(forced_age_timer);
187 }
188
189 fn age_lock_particle_timer(&self) -> i32 {
191 self.ageable_base().age_lock_particle_timer()
192 }
193
194 fn set_age_lock_particle_timer(&self, timer: i32) {
196 self.ageable_base().set_age_lock_particle_timer(timer);
197 }
198
199 fn can_age_up(&self) -> bool {
201 AgeableMob::is_baby(self) && !self.is_age_locked()
202 }
203
204 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 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 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 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 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 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 fn breed_variant_key(&self) -> Option<&Identifier> {
334 None
335 }
336
337 fn set_breed_variant_key(&self, _key: &Identifier) -> bool {
339 false
340 }
341
342 fn initialize_breed_offspring(&self, _partner: &dyn AgeableMob, _offspring: &dyn AgeableMob) {}
344
345 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 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 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}