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