Skip to main content

azalea/pathfinder/
goals.rs

1//! The goals that a pathfinder can try to reach.
2
3use std::{
4    f32::consts::SQRT_2,
5    fmt::{self, Debug},
6};
7
8use azalea_core::position::{BlockPos, Vec3};
9use azalea_world::ChunkStorage;
10#[cfg(feature = "serde")]
11use serde::{Deserialize, Serialize};
12
13use super::costs::{COST_HEURISTIC, FALL_N_BLOCKS_COST, JUMP_ONE_BLOCK_COST};
14use crate::pathfinder::{
15    costs::{JUMP_PENALTY, SPRINT_ONE_BLOCK_COST, WALK_ONE_BLOCK_COST},
16    moves::BARITONE_COMPAT,
17};
18
19pub trait Goal: Debug + Send + Sync {
20    #[must_use]
21    fn heuristic(&self, n: BlockPos) -> f32;
22    #[must_use]
23    fn success(&self, n: BlockPos) -> bool;
24}
25
26/// Move to the given block position.
27///
28/// If you're converting to a `BlockPosGoal` from a [`Vec3`], you should move
29/// the Y up by 0.5 to avoid the pathfinding destroying the final block if it's
30/// non-full like farmland.
31///
32/// ```
33/// # use azalea::{prelude::*, pathfinder::goals::BlockPosGoal};
34/// # fn example(bot: &Client, entity_position: azalea::Vec3) {
35/// bot.start_goto(BlockPosGoal(entity_position.up(0.5).into()));
36/// # }
37/// ```
38#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
39#[cfg_attr(feature = "serde", derive(Deserialize, Serialize))]
40pub struct BlockPosGoal(pub BlockPos);
41impl Goal for BlockPosGoal {
42    fn heuristic(&self, n: BlockPos) -> f32 {
43        let dx = (self.0.x - n.x) as f32;
44        let dy = (self.0.y - n.y) as f32;
45        let dz = (self.0.z - n.z) as f32;
46
47        xz_heuristic(dx, dz) + y_heuristic(dy)
48    }
49    fn success(&self, n: BlockPos) -> bool {
50        n == self.0
51    }
52}
53
54fn xz_heuristic(dx: f32, dz: f32) -> f32 {
55    let x = dx.abs();
56    let z = dz.abs();
57
58    // Swap `x` and `z` depending on the size.
59    let (diagonal, straight) = if x < z { (x, z - x) } else { (z, x - z) };
60
61    (diagonal * SQRT_2 + straight) * COST_HEURISTIC
62}
63
64/// Move to the given block position, ignoring the y-axis.
65#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
66#[cfg_attr(feature = "serde", derive(Deserialize, Serialize))]
67pub struct XZGoal {
68    pub x: i32,
69    pub z: i32,
70}
71impl Goal for XZGoal {
72    fn heuristic(&self, n: BlockPos) -> f32 {
73        let dx = (self.x - n.x) as f32;
74        let dz = (self.z - n.z) as f32;
75        xz_heuristic(dx, dz)
76    }
77    fn success(&self, n: BlockPos) -> bool {
78        n.x == self.x && n.z == self.z
79    }
80}
81impl From<BlockPos> for XZGoal {
82    fn from(block: BlockPos) -> Self {
83        Self {
84            x: block.x,
85            z: block.z,
86        }
87    }
88}
89
90fn y_heuristic(dy: f32) -> f32 {
91    if dy > 0. {
92        if BARITONE_COMPAT {
93            // this is wrong because it can be an overestimate
94            return *JUMP_ONE_BLOCK_COST * dy;
95        }
96
97        // forward+up is the main way to ascend, so make sure to match that. we subtract
98        // SPRINT_ONE_BLOCK_COST to cancel out the value that should've been
99        // added by the xz heuristic.
100        (f32::max(*JUMP_ONE_BLOCK_COST, WALK_ONE_BLOCK_COST) + JUMP_PENALTY - SPRINT_ONE_BLOCK_COST)
101            * dy
102    } else if dy < 0. {
103        // this is also an overestimate (copied from baritone), but fixing it makes perf
104        // too much worse
105        (FALL_N_BLOCKS_COST[2] / 2.) * -dy
106    } else {
107        0.
108    }
109}
110
111/// Move to the given y coordinate.
112#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
113#[cfg_attr(feature = "serde", derive(Deserialize, Serialize))]
114pub struct YGoal {
115    pub y: i32,
116}
117impl Goal for YGoal {
118    fn heuristic(&self, n: BlockPos) -> f32 {
119        let dy = (self.y - n.y) as f32;
120        y_heuristic(dy)
121    }
122    fn success(&self, n: BlockPos) -> bool {
123        n.y == self.y
124    }
125}
126impl From<BlockPos> for YGoal {
127    fn from(block: BlockPos) -> Self {
128        Self { y: block.y }
129    }
130}
131
132/// Get within the given radius of the given position.
133#[derive(Clone, Copy, Debug, Default, PartialEq)]
134#[cfg_attr(feature = "serde", derive(Deserialize, Serialize))]
135pub struct RadiusGoal {
136    pub pos: Vec3,
137    pub radius: f32,
138}
139impl RadiusGoal {
140    pub fn new(pos: Vec3, radius: f32) -> Self {
141        Self { pos, radius }
142    }
143}
144impl Goal for RadiusGoal {
145    fn heuristic(&self, n: BlockPos) -> f32 {
146        let n = n.center();
147        let dx = (self.pos.x - n.x) as f32;
148        let dy = (self.pos.y - n.y) as f32;
149        let dz = (self.pos.z - n.z) as f32;
150
151        xz_heuristic(dx, dz) + y_heuristic(dy)
152    }
153    fn success(&self, n: BlockPos) -> bool {
154        let n = n.center();
155        let dx = (self.pos.x - n.x) as f32;
156        let dy = (self.pos.y - n.y) as f32;
157        let dz = (self.pos.z - n.z) as f32;
158        dx.powi(2) + dy.powi(2) + dz.powi(2) <= self.radius.powi(2)
159    }
160}
161
162/// Do the opposite of the given goal.
163#[derive(Debug)]
164#[deprecated = "`InverseGoal` has poor performance and often doesn't work as expected, consider using different goals."]
165pub struct InverseGoal<T: Goal>(pub T);
166#[allow(deprecated)]
167impl<T: Goal> Goal for InverseGoal<T> {
168    fn heuristic(&self, n: BlockPos) -> f32 {
169        -self.0.heuristic(n)
170    }
171    fn success(&self, n: BlockPos) -> bool {
172        !self.0.success(n)
173    }
174}
175
176/// Do either of the given goals, whichever is closer.
177#[derive(Debug)]
178pub struct OrGoal<T: Goal, U: Goal>(pub T, pub U);
179impl<T: Goal, U: Goal> Goal for OrGoal<T, U> {
180    fn heuristic(&self, n: BlockPos) -> f32 {
181        self.0.heuristic(n).min(self.1.heuristic(n))
182    }
183    fn success(&self, n: BlockPos) -> bool {
184        self.0.success(n) || self.1.success(n)
185    }
186}
187
188/// Do any of the given goals, whichever is closest.
189#[derive(Debug)]
190pub struct OrGoals<T: Goal>(pub Vec<T>);
191impl<T: Goal> Goal for OrGoals<T> {
192    fn heuristic(&self, n: BlockPos) -> f32 {
193        self.0
194            .iter()
195            .map(|goal| goal.heuristic(n))
196            .min_by(|a, b| a.partial_cmp(b).unwrap())
197            .unwrap_or(f32::INFINITY)
198    }
199    fn success(&self, n: BlockPos) -> bool {
200        self.0.iter().any(|goal| goal.success(n))
201    }
202}
203
204/// Try to reach both of the given goals.
205#[derive(Debug)]
206pub struct AndGoal<T: Goal, U: Goal>(pub T, pub U);
207impl<T: Goal, U: Goal> Goal for AndGoal<T, U> {
208    fn heuristic(&self, n: BlockPos) -> f32 {
209        self.0.heuristic(n).max(self.1.heuristic(n))
210    }
211    fn success(&self, n: BlockPos) -> bool {
212        self.0.success(n) && self.1.success(n)
213    }
214}
215
216/// Try to reach all the given goals.
217#[derive(Debug)]
218pub struct AndGoals<T: Goal>(pub Vec<T>);
219impl<T: Goal> Goal for AndGoals<T> {
220    fn heuristic(&self, n: BlockPos) -> f32 {
221        self.0
222            .iter()
223            .map(|goal| goal.heuristic(n))
224            .max_by(|a, b| a.partial_cmp(b).unwrap())
225            .unwrap_or(f32::INFINITY)
226    }
227    fn success(&self, n: BlockPos) -> bool {
228        self.0.iter().all(|goal| goal.success(n))
229    }
230}
231
232/// Move to a position where we can reach the given block.
233#[derive(Clone)]
234pub struct ReachBlockPosGoal {
235    pub pos: BlockPos,
236    pub distance: f64,
237    pub chunk_storage: ChunkStorage,
238
239    max_check_distance: i32,
240}
241impl ReachBlockPosGoal {
242    pub fn new(pos: BlockPos, chunk_storage: ChunkStorage) -> Self {
243        Self::new_with_distance(pos, 4.5, chunk_storage)
244    }
245
246    pub fn new_with_distance(pos: BlockPos, distance: f64, chunk_storage: ChunkStorage) -> Self {
247        Self {
248            pos,
249            distance,
250            chunk_storage,
251            max_check_distance: (distance + 2.).ceil() as i32,
252        }
253    }
254}
255impl Goal for ReachBlockPosGoal {
256    fn heuristic(&self, n: BlockPos) -> f32 {
257        BlockPosGoal(self.pos).heuristic(n)
258    }
259    fn success(&self, n: BlockPos) -> bool {
260        let head = n.up(1);
261        // the player's head is in the block or adjacent to it, so assume that it's
262        // always reachable (we do this to account for mining)
263        if head == self.pos
264            || head.north(1) == self.pos
265            || head.south(1) == self.pos
266            || head.east(1) == self.pos
267            || head.west(1) == self.pos
268            || head.up(1) == self.pos
269            || head.down(1) == self.pos
270        {
271            return true;
272        }
273
274        // only do the expensive check if we're close enough
275        let distance_squared = self.pos.distance_squared_to(n);
276        if distance_squared > self.max_check_distance.pow(2) {
277            return false;
278        }
279
280        let eye_position = n.center_bottom().up(1.62);
281        let look_direction = crate::bot::direction_looking_at(eye_position, self.pos.center());
282        let block_hit_result = azalea_client::interact::pick::pick_block(
283            look_direction,
284            eye_position,
285            &self.chunk_storage,
286            self.distance,
287        );
288
289        block_hit_result.block_pos == self.pos
290    }
291}
292impl Debug for ReachBlockPosGoal {
293    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
294        f.debug_struct("ReachBlockPosGoal")
295            .field("pos", &self.pos)
296            .field("distance", &self.distance)
297            .field("max_check_distance", &self.max_check_distance)
298            .finish()
299    }
300}