steel_core/entity/ai/goal/
breed_goal.rs1use glam::DVec3;
2
3use super::reduced_tick_delay;
4use super::selector::{Goal, GoalControls};
5use crate::entity::ai::targeting::TargetingConditions;
6use crate::entity::{Animal, PathfinderMob, SharedEntity};
7
8const PARTNER_SEARCH_RANGE: f64 = 8.0;
9const BREED_DISTANCE_SQR: f64 = 9.0;
10const BREED_TIME: i32 = 60;
11
12pub struct BreedGoal {
13 partner: Option<SharedEntity>,
14 love_time: i32,
15 speed_modifier: f64,
16}
17
18impl BreedGoal {
19 #[must_use]
20 pub(crate) const fn new(speed_modifier: f64) -> Self {
21 Self {
22 partner: None,
23 love_time: 0,
24 speed_modifier,
25 }
26 }
27
28 fn get_free_partner(mob: &dyn PathfinderMob, animal: &dyn Animal) -> Option<SharedEntity> {
29 let world = mob.level()?;
30 let search_box = mob.bounding_box().inflate(PARTNER_SEARCH_RANGE);
31 let partner_targeting = TargetingConditions::for_non_combat()
32 .range(PARTNER_SEARCH_RANGE)
33 .ignore_line_of_sight();
34
35 world.nearest_entity_in_aabb_matching(&search_box, mob.position(), |entity| {
36 let Some(candidate) = entity.as_animal() else {
37 return false;
38 };
39 if !partner_targeting.test(world.as_ref(), Some(mob), candidate) {
40 return false;
41 }
42 if !animal.can_mate(candidate) {
43 return false;
44 }
45
46 !entity
47 .as_pathfinder_mob()
48 .is_some_and(PathfinderMob::is_panicking)
49 })
50 }
51}
52
53impl Goal for BreedGoal {
54 fn controls(&self) -> GoalControls {
55 GoalControls::MOVE | GoalControls::LOOK
56 }
57
58 fn can_use(&mut self, mob: &dyn PathfinderMob) -> bool {
59 let Some(animal) = mob.as_animal() else {
60 return false;
61 };
62 if !animal.is_in_love() {
63 return false;
64 }
65
66 self.partner = Self::get_free_partner(mob, animal);
67 self.partner.is_some()
68 }
69
70 fn can_continue_to_use(&mut self, _mob: &dyn PathfinderMob) -> bool {
71 let Some(partner) = &self.partner else {
72 return false;
73 };
74 if !partner.is_alive() {
75 return false;
76 }
77 if self.love_time >= BREED_TIME {
78 return false;
79 }
80 if partner
81 .as_pathfinder_mob()
82 .is_some_and(PathfinderMob::is_panicking)
83 {
84 return false;
85 }
86
87 partner.as_animal().is_some_and(Animal::is_in_love)
88 }
89
90 fn stop(&mut self, _mob: &dyn PathfinderMob) {
91 self.partner = None;
92 self.love_time = 0;
93 }
94
95 fn tick(&mut self, mob: &dyn PathfinderMob) {
96 let Some(partner) = &self.partner else {
97 return;
98 };
99 let Some(animal) = mob.as_animal() else {
100 return;
101 };
102 let Some(partner_animal) = partner.as_animal() else {
103 return;
104 };
105
106 let partner_position = partner.position();
107 mob.mob_base().controls().lock().look_control.set_look_at(
108 DVec3::new(partner_position.x, partner.get_eye_y(), partner_position.z),
109 10.0,
110 mob.max_head_x_rot(),
111 );
112 mob.move_to_pos(partner_position, self.speed_modifier);
113
114 self.love_time += 1;
115 if self.love_time < reduced_tick_delay(BREED_TIME)
116 || mob.position().distance_squared(partner_position) >= BREED_DISTANCE_SQR
117 {
118 return;
119 }
120
121 let Some(world) = mob.level() else {
122 return;
123 };
124 animal.spawn_child_from_breeding(&world, partner_animal);
125 }
126}
127
128#[cfg(test)]
129mod tests {
130 use std::sync::Weak;
131
132 use glam::DVec3;
133 use steel_registry::{init_vanilla_registry, vanilla_entities};
134
135 use super::*;
136 use crate::entity::entities::PigEntity;
137
138 #[test]
139 fn breed_goal_uses_move_and_look_controls() {
140 let goal = BreedGoal::new(1.0);
141
142 assert_eq!(goal.controls(), GoalControls::MOVE | GoalControls::LOOK);
143 }
144
145 #[test]
146 fn breed_goal_requires_love_mode() {
147 init_vanilla_registry();
148 let pig = PigEntity::new(&vanilla_entities::PIG, 1, DVec3::ZERO, Weak::new());
149 let mut goal = BreedGoal::new(1.0);
150
151 assert!(!goal.can_use(&pig));
152 }
153}