1use std::ops::RangeInclusive;
4use std::sync::Arc;
5
6use rand::Rng;
7use simdnbt::borrow::NbtCompound as BorrowedNbtCompoundView;
8use simdnbt::owned::{NbtCompound, NbtTag};
9use steel_registry::blocks::block_state_ext::BlockStateExt as _;
10use steel_registry::entity_type::EntityTypeRef;
11use steel_registry::item_stack::ItemStack;
12use steel_registry::vanilla_block_tags::BlockTag;
13use steel_registry::vanilla_game_rules::MOB_DROPS;
14use steel_registry::{vanilla_blocks, vanilla_entities};
15use steel_utils::entity_events::EntityStatus;
16use steel_utils::locks::SyncMutex;
17use steel_utils::nbt::NbtValueInput as _;
18use steel_utils::types::InteractionHand;
19use steel_utils::{BlockPos, UuidExt};
20use uuid::Uuid;
21
22use crate::behavior::InteractionResult;
23use crate::entity::ai::path::PathType;
24use crate::entity::entities::ExperienceOrbEntity;
25use crate::entity::{
26 AgeableMob, AgeableMobBase, EntitySpawnReason, Mob, MobBase, SharedEntity, next_entity_id,
27};
28use crate::player::Player;
29use crate::world::{LevelReader, World};
30
31const PARENT_AGE_AFTER_BREEDING: i32 = 6000;
32const IN_LOVE_TIME: i32 = 600;
33const BREEDING_XP: RangeInclusive<i32> = 1..=7;
34
35#[derive(Debug, Clone, Copy, PartialEq, Eq)]
36struct AnimalState {
37 in_love: i32,
38 love_cause: Option<Uuid>,
39}
40
41impl AnimalState {
42 const fn new() -> Self {
43 Self {
44 in_love: 0,
45 love_cause: None,
46 }
47 }
48}
49
50#[derive(Debug)]
52pub struct AnimalBase {
53 state: SyncMutex<AnimalState>,
54}
55
56impl AnimalBase {
57 #[must_use]
59 pub const fn new() -> Self {
60 Self {
61 state: SyncMutex::new(AnimalState::new()),
62 }
63 }
64
65 pub fn initialize_pathfinding_malus(mob_base: &MobBase) {
66 let mut malus = mob_base.pathfinding_malus().lock();
67 malus.set(PathType::FireInNeighbor, 16.0);
68 malus.set(PathType::Fire, -1.0);
69 }
70
71 #[must_use]
73 pub fn in_love_time(&self) -> i32 {
74 self.state.lock().in_love
75 }
76
77 pub fn set_in_love_time(&self, in_love: i32) {
79 self.state.lock().in_love = in_love;
80 }
81
82 pub fn tick_in_love_time(&self) {
84 let mut state = self.state.lock();
85 if state.in_love > 0 {
86 state.in_love -= 1;
87 }
88 }
89
90 #[must_use]
92 pub fn love_cause_uuid(&self) -> Option<Uuid> {
93 self.state.lock().love_cause
94 }
95
96 pub fn set_love_cause_uuid(&self, love_cause: Option<Uuid>) {
98 self.state.lock().love_cause = love_cause;
99 }
100}
101
102impl Default for AnimalBase {
103 fn default() -> Self {
104 Self::new()
105 }
106}
107
108pub trait Animal: AgeableMob {
110 fn animal_base(&self) -> &AnimalBase;
112
113 fn in_love_time(&self) -> i32 {
115 self.animal_base().in_love_time()
116 }
117
118 fn set_in_love_time(&self, in_love: i32) {
120 self.animal_base().set_in_love_time(in_love);
121 }
122
123 fn love_cause_uuid(&self) -> Option<Uuid> {
125 self.animal_base().love_cause_uuid()
126 }
127
128 fn set_love_cause_uuid(&self, love_cause: Option<Uuid>) {
130 self.animal_base().set_love_cause_uuid(love_cause);
131 }
132
133 fn is_in_love(&self) -> bool {
135 self.in_love_time() > 0
136 }
137
138 fn can_fall_in_love(&self) -> bool {
140 self.in_love_time() <= 0
141 }
142
143 fn set_in_love(&self, player: Option<&Player>) {
145 self.set_in_love_time(IN_LOVE_TIME);
146 if let Some(player) = player {
147 self.set_love_cause_uuid(Some(player.gameprofile.id()));
148 }
149
150 self.broadcast_entity_event(EntityStatus::InLoveHearts);
151 }
152
153 fn reset_love(&self) {
155 self.set_in_love_time(0);
156 }
157
158 fn can_mate(&self, partner: &dyn Animal) -> bool {
161 self.uuid() != partner.uuid()
162 && self.entity_type() == partner.entity_type()
163 && self.is_in_love()
164 && partner.is_in_love()
165 }
166
167 fn is_food(&self, _item_stack: &ItemStack) -> bool {
169 false
170 }
171
172 fn base_experience_reward_animal(&self) -> i32 {
174 1 + rand::random_range(0..3)
175 }
176
177 fn ambient_sound_interval_animal(&self) -> i32 {
179 120
180 }
181
182 fn animal_walk_target_value(&self, pos: BlockPos) -> f32 {
185 let Some(world) = self.level() else {
186 return 0.0;
187 };
188
189 if world.get_block_state(pos.below()).get_block() == &vanilla_blocks::GRASS_BLOCK {
190 10.0
191 } else {
192 world.pathfinding_cost_from_light_levels(pos)
193 }
194 }
195
196 fn is_bright_enough_to_spawn(level: &(impl LevelReader + ?Sized), pos: BlockPos) -> bool
198 where
199 Self: Sized,
200 {
201 level.raw_brightness(pos, 0) > 8
202 }
203
204 fn check_animal_spawn_rules(
208 _entity_type: EntityTypeRef,
209 level: &(impl LevelReader + ?Sized),
210 spawn_reason: EntitySpawnReason,
211 pos: BlockPos,
212 _random: &mut dyn Rng,
213 ) -> bool
214 where
215 Self: Sized,
216 {
217 let bright_enough = spawn_reason.ignores_light_requirements()
218 || Self::is_bright_enough_to_spawn(level, pos);
219 level
220 .get_block_state(pos.below())
221 .get_block()
222 .has_tag(&BlockTag::ANIMALS_SPAWNABLE_ON)
223 && bright_enough
224 }
225
226 fn play_eating_sound(&self) {}
228
229 fn mob_interact_animal(&self, player: &Player, hand: InteractionHand) -> InteractionResult {
233 let item_stack = {
234 let inventory = player.inventory.lock();
235 let item_stack = inventory.get_item_in_hand(hand);
236 item_stack.copy_with_count(item_stack.count())
237 };
238
239 if !self.is_food(&item_stack) {
240 return self.mob_interact_ageable(player, hand);
241 }
242
243 let age = self.get_age();
244 if age == 0 && self.can_fall_in_love() {
245 Mob::use_player_item(self, player, hand);
246 self.set_in_love(Some(player));
247 self.play_eating_sound();
248 return InteractionResult::SuccessServer;
249 }
250
251 if self.can_age_up() {
252 Mob::use_player_item(self, player, hand);
253 self.age_up(
254 AgeableMobBase::get_speed_up_seconds_when_feeding(-age),
255 true,
256 );
257 self.play_eating_sound();
258 return InteractionResult::Success;
259 }
260
261 self.mob_interact_ageable(player, hand)
262 }
263
264 fn spawn_child_from_breeding(&self, world: &Arc<World>, partner: &dyn Animal) {
266 let Some(offspring) = self.get_breed_offspring(world, partner) else {
267 return;
268 };
269
270 {
271 let Some(offspring_animal) = offspring.as_animal() else {
272 log::error!(
273 "breeding entity type {} created non-animal offspring",
274 self.entity_type().key
275 );
276 return;
277 };
278 offspring_animal.set_baby(true);
279 if let Err(error) = offspring_animal.try_set_position(self.position()) {
280 log::error!(
281 "failed to position breeding offspring {} at parent {}: {error}",
282 offspring.id(),
283 self.id()
284 );
285 return;
286 }
287 offspring_animal.set_rotation((0.0, 0.0));
288 offspring_animal.set_old_position_to_current();
289
290 self.finalize_spawn_child_from_breeding(world, partner, Some(offspring_animal));
291 }
292
293 if let Err(error) = world.try_add_entity(offspring) {
294 log::error!(
295 "failed to add breeding offspring for entity {} to world: {error}",
296 self.id()
297 );
298 }
299 }
300
301 fn finalize_spawn_child_from_breeding(
303 &self,
304 world: &Arc<World>,
305 partner: &dyn Animal,
306 _offspring: Option<&dyn Animal>,
307 ) {
308 if self
309 .love_cause_uuid()
310 .or_else(|| partner.love_cause_uuid())
311 .is_some()
312 {
313 }
315
316 self.set_age(PARENT_AGE_AFTER_BREEDING);
317 partner.set_age(PARENT_AGE_AFTER_BREEDING);
318 self.reset_love();
319 partner.reset_love();
320 self.broadcast_entity_event(EntityStatus::InLoveHearts);
321
322 if world.get_game_rule(&MOB_DROPS) {
323 let orb: SharedEntity = Arc::new(ExperienceOrbEntity::with_value(
324 &vanilla_entities::EXPERIENCE_ORB,
325 next_entity_id(),
326 self.position(),
327 rand::random_range(BREEDING_XP),
328 Arc::downgrade(world),
329 ));
330 if let Err(error) = world.try_add_entity(orb) {
331 log::debug!("failed to add breeding experience orb: {error}");
332 }
333 }
334 }
335
336 fn tick_animal_love(&self) {
338 if self.get_age() != 0 {
339 self.reset_love();
340 return;
341 }
342
343 self.animal_base().tick_in_love_time();
344 }
346
347 fn custom_server_ai_step_animal(&self) {
349 if self.get_age() != 0 {
350 self.reset_love();
351 }
352 }
353
354 fn remove_when_far_away_animal(&self, _dist_sqr: f64) -> bool {
356 false
357 }
358
359 fn save_animal(&self, nbt: &mut NbtCompound) {
361 nbt.insert("InLove", self.in_love_time());
362 if let Some(love_cause) = self.love_cause_uuid() {
363 nbt.insert(
364 "LoveCause",
365 NbtTag::IntArray(love_cause.to_int_array().to_vec()),
366 );
367 }
368 }
369
370 fn load_animal(&self, nbt: BorrowedNbtCompoundView<'_, '_>) {
372 self.set_in_love_time(nbt.int_value("InLove").unwrap_or(0));
373 if let Some(love_cause) = nbt.int_array("LoveCause")
374 && let Some(uuid) = Uuid::from_int_array(&love_cause)
375 {
376 self.set_love_cause_uuid(Some(uuid));
377 }
378 }
379}
380
381#[cfg(test)]
382mod tests {
383 use steel_registry::{REGISTRY, init_vanilla_registry, vanilla_blocks};
384 use steel_utils::BlockStateId;
385 use steel_worldgen::density_functions::overworld::OverworldNoiseSettings;
386
387 use super::*;
388 use crate::entity::entities::PigEntity;
389
390 struct SpawnRuleLevel {
391 below_pos: BlockPos,
392 below_state: BlockStateId,
393 raw_brightness: u8,
394 }
395
396 impl LevelReader for SpawnRuleLevel {
397 fn get_block_state(&self, pos: BlockPos) -> BlockStateId {
398 if pos == self.below_pos {
399 return self.below_state;
400 }
401
402 REGISTRY.blocks.get_default_state_id(&vanilla_blocks::AIR)
403 }
404
405 fn raw_brightness(&self, _pos: BlockPos, _sky_darkening: u8) -> u8 {
406 self.raw_brightness
407 }
408
409 fn min_y(&self) -> i32 {
410 OverworldNoiseSettings::MIN_Y
411 }
412
413 fn height(&self) -> i32 {
414 OverworldNoiseSettings::HEIGHT
415 }
416
417 fn sea_level(&self) -> i32 {
418 OverworldNoiseSettings::SEA_LEVEL
419 }
420 }
421
422 fn spawn_rule_level(block_below: BlockStateId, raw_brightness: u8) -> SpawnRuleLevel {
423 SpawnRuleLevel {
424 below_pos: BlockPos::new(0, 63, 0),
425 below_state: block_below,
426 raw_brightness,
427 }
428 }
429
430 #[test]
431 fn animal_spawn_rules_require_spawnable_block_tag() {
432 init_vanilla_registry();
433 let level = spawn_rule_level(vanilla_blocks::STONE.default_state(), 15);
434
435 assert!(!<PigEntity as Animal>::check_animal_spawn_rules(
436 &vanilla_entities::PIG,
437 &level,
438 EntitySpawnReason::Natural,
439 BlockPos::new(0, 64, 0),
440 &mut rand::rng(),
441 ));
442 }
443
444 #[test]
445 fn animal_spawn_rules_require_raw_brightness_above_eight() {
446 init_vanilla_registry();
447 let level = spawn_rule_level(vanilla_blocks::GRASS_BLOCK.default_state(), 8);
448
449 assert!(!<PigEntity as Animal>::check_animal_spawn_rules(
450 &vanilla_entities::PIG,
451 &level,
452 EntitySpawnReason::Natural,
453 BlockPos::new(0, 64, 0),
454 &mut rand::rng(),
455 ));
456
457 let level = spawn_rule_level(vanilla_blocks::GRASS_BLOCK.default_state(), 9);
458
459 assert!(<PigEntity as Animal>::check_animal_spawn_rules(
460 &vanilla_entities::PIG,
461 &level,
462 EntitySpawnReason::Natural,
463 BlockPos::new(0, 64, 0),
464 &mut rand::rng(),
465 ));
466 }
467
468 #[test]
469 fn animal_spawn_rules_trial_spawner_ignores_light() {
470 init_vanilla_registry();
471 let level = spawn_rule_level(vanilla_blocks::GRASS_BLOCK.default_state(), 0);
472
473 assert!(<PigEntity as Animal>::check_animal_spawn_rules(
474 &vanilla_entities::PIG,
475 &level,
476 EntitySpawnReason::TrialSpawner,
477 BlockPos::new(0, 64, 0),
478 &mut rand::rng(),
479 ));
480 }
481}