Skip to main content

steel_core/entity/ai/goal/
eat_block_goal.rs

1//! Vanilla `EatBlockGoal`: a mob stops moving and chews for a fixed duration before
2//! eating an edible block and running `Mob.ate`.
3
4use 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
17/// Constant mirroring vanilla `EatBlockGoal.EAT_ANIMATION_TICKS`, the full animation
18/// length before the goal's tick reaches the block-eating step.
19const EAT_ANIMATION_TICKS: i32 = 40;
20/// Constant mirroring vanilla `EatBlockGoal`'s check point (`adjustedTickDelay(4)`).
21const EAT_BLOCK_TICK: i32 = 4;
22/// Vanilla `EatBlockGoal.canUse` rolls `nextInt(adjustedTickDelay(baby ? 50 : 1000))`;
23/// babies check far more often so they regrow wool quickly.
24const BABY_EAT_CHECK_TICKS: i32 = 50;
25const ADULT_EAT_CHECK_TICKS: i32 = 1000;
26
27/// A goal where a mob stands still and chews before eating an edible block.
28///
29/// Ports vanilla 26.2 `EatBlockGoal`. The eat animation only turns grass into dirt
30/// when the `mobGriefing` game rule is enabled, matching vanilla.
31pub struct EatBlockGoal {
32    eat_animation_tick: i32,
33}
34
35impl EatBlockGoal {
36    /// Creates the goal with no running animation.
37    #[must_use]
38    pub const fn new() -> Self {
39        Self {
40            eat_animation_tick: 0,
41        }
42    }
43
44    /// Returns the current eat-animation tick (vanilla `EatBlockGoal.getEatAnimationTick`).
45    #[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}