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::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#[derive(Debug)]
42pub struct AgeableMobBase {
43 state: SyncMutex<AgeableMobState>,
44}
45
46impl AgeableMobBase {
47 #[must_use]
49 pub const fn new() -> Self {
50 Self {
51 state: SyncMutex::new(AgeableMobState::new()),
52 }
53 }
54
55 #[must_use]
57 pub fn age(&self) -> i32 {
58 self.state.lock().age
59 }
60
61 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 #[must_use]
71 pub fn forced_age(&self) -> i32 {
72 self.state.lock().forced_age
73 }
74
75 pub fn set_forced_age(&self, forced_age: i32) {
77 self.state.lock().forced_age = forced_age;
78 }
79
80 #[must_use]
82 pub fn forced_age_timer(&self) -> i32 {
83 self.state.lock().forced_age_timer
84 }
85
86 pub fn set_forced_age_timer(&self, forced_age_timer: i32) {
88 self.state.lock().forced_age_timer = forced_age_timer;
89 }
90
91 #[must_use]
93 pub fn age_lock_particle_timer(&self) -> i32 {
94 self.state.lock().age_lock_particle_timer
95 }
96
97 pub fn set_age_lock_particle_timer(&self, timer: i32) {
99 self.state.lock().age_lock_particle_timer = timer;
100 }
101
102 pub fn add_forced_age(&self, delta: i32) {
104 self.state.lock().forced_age += delta;
105 }
106
107 #[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
120pub trait AgeableMob: Mob {
122 fn ageable_base(&self) -> &AgeableMobBase;
124
125 fn is_age_locked(&self) -> bool;
127
128 fn set_age_locked(&self, age_locked: bool);
130
131 fn set_synced_baby(&self, baby: bool);
133
134 fn age_boundary_changed(&self, _baby: bool) {
136 self.refresh_dimensions();
137 }
138
139 fn get_baby_start_age(&self) -> i32 {
141 BABY_START_AGE
142 }
143
144 fn get_age(&self) -> i32 {
146 self.ageable_base().age()
147 }
148
149 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 fn is_baby(&self) -> bool {
160 self.get_age() < 0
161 }
162
163 fn set_baby(&self, baby: bool) {
165 self.set_age(if baby { self.get_baby_start_age() } else { 0 });
166 }
167
168 fn forced_age(&self) -> i32 {
170 self.ageable_base().forced_age()
171 }
172
173 fn set_forced_age(&self, forced_age: i32) {
175 self.ageable_base().set_forced_age(forced_age);
176 }
177
178 fn forced_age_timer(&self) -> i32 {
180 self.ageable_base().forced_age_timer()
181 }
182
183 fn set_forced_age_timer(&self, forced_age_timer: i32) {
185 self.ageable_base().set_forced_age_timer(forced_age_timer);
186 }
187
188 fn age_lock_particle_timer(&self) -> i32 {
190 self.ageable_base().age_lock_particle_timer()
191 }
192
193 fn set_age_lock_particle_timer(&self, timer: i32) {
195 self.ageable_base().set_age_lock_particle_timer(timer);
196 }
197
198 fn can_age_up(&self) -> bool {
200 AgeableMob::is_baby(self) && !self.is_age_locked()
201 }
202
203 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 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 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 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 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 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 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}