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