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, SharedEntity};
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, _entity: &SharedEntity) {
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::{TestWorld, fresh_test_world, insert_ready_full_chunk};
143 use steel_utils::Downcast as _;
144
145 #[test]
146 fn eat_block_goal_mirrors_fixed_animation_duration() {
147 use steel_registry::vanilla_entities;
148
149 init_vanilla_registry();
150 let mut goal = EatBlockGoal::new();
151 let mob = PigEntity::new(&vanilla_entities::PIG, 1, DVec3::ZERO, Weak::new());
152 goal.start(&mob);
153
154 assert_eq!(goal.get_eat_animation_tick(), 20);
155 }
156
157 fn sheep_on_grass_world(name: &'static str) -> (TestWorld, SharedEntity) {
158 use steel_registry::vanilla_blocks;
159 use steel_registry::vanilla_entities;
160 use steel_utils::ChunkPos;
161
162 init_behaviors();
163 let world_fixture = fresh_test_world(name);
164 let world = &world_fixture.world;
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_fixture, 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_fixture, shared) = sheep_on_grass_world("eat_grass");
191 let world = &world_fixture.world;
192 let mob = shared
193 .as_pathfinder_mob()
194 .expect("sheep should be a pathfinder mob");
195
196 let mut goal = EatBlockGoal::new();
197 goal.start(mob);
198 for _ in 0..18 {
199 goal.tick(mob, &shared);
200 }
201
202 let sheep = shared
203 .downcast_ref::<SheepEntity>()
204 .expect("shared entity should be a sheep");
205 assert!(!sheep.is_sheared(), "eating grass should run Mob.ate");
206 assert_eq!(
207 world
208 .get_block_state(shared.block_position().below())
209 .get_block(),
210 &vanilla_blocks::DIRT,
211 "eating grass should turn it into dirt with mobGriefing enabled"
212 );
213 }
214
215 #[test]
216 fn eat_block_goal_does_not_grief_grass_with_mob_griefing_disabled() {
217 use steel_registry::vanilla_blocks;
218
219 init_vanilla_registry();
220 let (world_fixture, shared) = sheep_on_grass_world("eat_grass_no_grief");
221 let world = &world_fixture.world;
222 world.set_game_rule(&MOB_GRIEFING, false);
223 let mob = shared
224 .as_pathfinder_mob()
225 .expect("sheep should be a pathfinder mob");
226
227 let mut goal = EatBlockGoal::new();
228 goal.start(mob);
229 for _ in 0..18 {
230 goal.tick(mob, &shared);
231 }
232
233 let sheep = shared
234 .downcast_ref::<SheepEntity>()
235 .expect("shared entity should be a sheep");
236 assert!(
237 !sheep.is_sheared(),
238 "Mob.ate should still run without mobGriefing"
239 );
240 assert_eq!(
241 world
242 .get_block_state(shared.block_position().below())
243 .get_block(),
244 &vanilla_blocks::GRASS_BLOCK,
245 "grass should survive without mobGriefing"
246 );
247 }
248}