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, SharedEntity};
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/// The eat animation only turns grass into dirt when the `mobGriefing` game
30/// rule is enabled.
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, _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}