steel_core/entity/ai/goal/
eat_block_goal.rs1use steel_registry::blocks::block_state_ext::BlockStateExt as _;
5use steel_registry::level_events;
6use steel_registry::vanilla_block_tags::BlockTag;
7use steel_registry::vanilla_blocks;
8use steel_registry::vanilla_game_rules::MOB_GRIEFING;
9use steel_utils::entity_events::EntityStatus;
10use steel_utils::types::UpdateFlags;
11
12use super::reduced_tick_delay;
13use super::selector::{Goal, GoalControls};
14use crate::entity::{AgeableMob, Mob, PathfinderMob};
15use crate::world::LevelAccessor;
16
17const EAT_ANIMATION_TICKS: i32 = 40;
20const EAT_BLOCK_TICK: i32 = 4;
22const BABY_EAT_CHECK_TICKS: i32 = 50;
25const ADULT_EAT_CHECK_TICKS: i32 = 1000;
26
27pub struct EatBlockGoal {
32 eat_animation_tick: i32,
33}
34
35impl EatBlockGoal {
36 #[must_use]
38 pub const fn new() -> Self {
39 Self {
40 eat_animation_tick: 0,
41 }
42 }
43
44 #[must_use]
46 pub const fn get_eat_animation_tick(&self) -> i32 {
47 self.eat_animation_tick
48 }
49}
50
51impl Goal for EatBlockGoal {
52 fn controls(&self) -> GoalControls {
53 GoalControls::MOVE | GoalControls::LOOK | GoalControls::JUMP
54 }
55
56 fn can_use(&mut self, mob: &dyn PathfinderMob) -> bool {
57 let interval = if mob.as_animal().is_some_and(AgeableMob::is_baby) {
58 BABY_EAT_CHECK_TICKS
59 } else {
60 ADULT_EAT_CHECK_TICKS
61 };
62 let adjusted = reduced_tick_delay(interval);
63 if rand::random_range(0..adjusted) != 0 {
64 return false;
65 }
66
67 let Some(world) = mob.level() else {
68 return false;
69 };
70 let pos = mob.block_position();
71 let state_at_feet = world.get_block_state(pos);
72 state_at_feet
73 .get_block()
74 .has_tag(&BlockTag::EDIBLE_FOR_SHEEP)
75 || world.get_block_state(pos.below()).get_block() == &vanilla_blocks::GRASS_BLOCK
76 }
77
78 fn can_continue_to_use(&mut self, _mob: &dyn PathfinderMob) -> bool {
79 self.eat_animation_tick > 0
80 }
81
82 fn start(&mut self, mob: &dyn PathfinderMob) {
83 self.eat_animation_tick = reduced_tick_delay(EAT_ANIMATION_TICKS);
84 mob.broadcast_entity_event(EntityStatus::EatGrass);
85 mob.mob_base().navigation().lock().stop();
86 }
87
88 fn stop(&mut self, _mob: &dyn PathfinderMob) {
89 self.eat_animation_tick = 0;
90 }
91
92 fn tick(&mut self, mob: &dyn PathfinderMob) {
93 self.eat_animation_tick = (self.eat_animation_tick - 1).max(0);
94 if self.eat_animation_tick != reduced_tick_delay(EAT_BLOCK_TICK) {
95 return;
96 }
97
98 let Some(world) = mob.level() else {
99 return;
100 };
101 let pos = mob.block_position();
102 let below = pos.below();
103 let at_feet = world.get_block_state(pos);
104 if at_feet.get_block().has_tag(&BlockTag::EDIBLE_FOR_SHEEP) {
105 if world.get_game_rule(&MOB_GRIEFING) {
106 world.destroy_block(pos, false);
107 }
108 Mob::ate(mob);
109 } else if world.get_block_state(below).get_block() == &vanilla_blocks::GRASS_BLOCK {
110 if world.get_game_rule(&MOB_GRIEFING) {
111 world.level_event(
112 level_events::PARTICLES_DESTROY_BLOCK,
113 below,
114 level_events::encode_block_state_data(u32::from(
115 vanilla_blocks::GRASS_BLOCK.default_state().0,
116 )),
117 None,
118 );
119 world.set_block_state(
120 below,
121 vanilla_blocks::DIRT.default_state(),
122 UpdateFlags::UPDATE_CLIENTS,
123 );
124 }
125 Mob::ate(mob);
126 }
127 }
128}
129
130#[cfg(test)]
131mod tests {
132 use std::sync::{Arc, Weak};
133
134 use glam::DVec3;
135 use steel_registry::init_vanilla_registry;
136 use steel_utils::types::UpdateFlags;
137
138 use super::*;
139 use crate::behavior::init_behaviors;
140 use crate::entity::SharedEntity;
141 use crate::entity::entities::{PigEntity, SheepEntity};
142 use crate::test_support::{fresh_test_world, insert_ready_full_chunk};
143 use crate::world::World;
144 use steel_utils::Downcast as _;
145
146 #[test]
147 fn eat_block_goal_mirrors_fixed_animation_duration() {
148 use steel_registry::vanilla_entities;
149
150 init_vanilla_registry();
151 let mut goal = EatBlockGoal::new();
152 let mob = PigEntity::new(&vanilla_entities::PIG, 1, DVec3::ZERO, Weak::new());
153 goal.start(&mob);
154
155 assert_eq!(goal.get_eat_animation_tick(), 20);
156 }
157
158 fn sheep_on_grass_world(name: &'static str) -> (Arc<World>, SharedEntity) {
159 use steel_registry::vanilla_blocks;
160 use steel_registry::vanilla_entities;
161 use steel_utils::ChunkPos;
162
163 init_behaviors();
164 let world = fresh_test_world(name);
165 insert_ready_full_chunk(&world, ChunkPos::new(0, 0));
166 let sheep = SheepEntity::new(
167 &vanilla_entities::SHEEP,
168 1,
169 DVec3::new(8.0, 65.0, 8.0),
170 Arc::downgrade(&world),
171 );
172 sheep.set_sheared(true);
173 let shared: SharedEntity = Arc::new(sheep);
174 world
175 .try_add_entity(Arc::clone(&shared))
176 .expect("sheep should attach to the loaded test chunk");
177 world.set_block_state(
178 shared.block_position().below(),
179 vanilla_blocks::GRASS_BLOCK.default_state(),
180 UpdateFlags::UPDATE_CLIENTS,
181 );
182 (world, shared)
183 }
184
185 #[test]
186 fn eat_block_goal_turns_grass_into_dirt_and_runs_mob_ate() {
187 use steel_registry::vanilla_blocks;
188
189 init_vanilla_registry();
190 let (world, shared) = sheep_on_grass_world("eat_grass");
191 let mob = shared
192 .as_pathfinder_mob()
193 .expect("sheep should be a pathfinder mob");
194
195 let mut goal = EatBlockGoal::new();
196 goal.start(mob);
197 for _ in 0..18 {
198 goal.tick(mob);
199 }
200
201 let sheep = shared
202 .downcast_ref::<SheepEntity>()
203 .expect("shared entity should be a sheep");
204 assert!(!sheep.is_sheared(), "eating grass should run Mob.ate");
205 assert_eq!(
206 world
207 .get_block_state(shared.block_position().below())
208 .get_block(),
209 &vanilla_blocks::DIRT,
210 "eating grass should turn it into dirt with mobGriefing enabled"
211 );
212 }
213
214 #[test]
215 fn eat_block_goal_does_not_grief_grass_with_mob_griefing_disabled() {
216 use steel_registry::vanilla_blocks;
217
218 init_vanilla_registry();
219 let (world, shared) = sheep_on_grass_world("eat_grass_no_grief");
220 world.set_game_rule(&MOB_GRIEFING, false);
221 let mob = shared
222 .as_pathfinder_mob()
223 .expect("sheep should be a pathfinder mob");
224
225 let mut goal = EatBlockGoal::new();
226 goal.start(mob);
227 for _ in 0..18 {
228 goal.tick(mob);
229 }
230
231 let sheep = shared
232 .downcast_ref::<SheepEntity>()
233 .expect("shared entity should be a sheep");
234 assert!(
235 !sheep.is_sheared(),
236 "Mob.ate should still run without mobGriefing"
237 );
238 assert_eq!(
239 world
240 .get_block_state(shared.block_position().below())
241 .get_block(),
242 &vanilla_blocks::GRASS_BLOCK,
243 "grass should survive without mobGriefing"
244 );
245 }
246}