azalea/pathfinder/moves/
basic.rs

1use std::f32::consts::SQRT_2;
2
3use azalea_client::{SprintDirection, WalkDirection};
4use azalea_core::{
5    direction::CardinalDirection,
6    position::{BlockPos, Vec3},
7};
8
9use super::{Edge, ExecuteCtx, IsReachedCtx, MoveData, PathfinderCtx, default_is_reached};
10use crate::pathfinder::{astar, costs::*, rel_block_pos::RelBlockPos};
11
12pub fn basic_move(ctx: &mut PathfinderCtx, node: RelBlockPos) {
13    forward_move(ctx, node);
14    ascend_move(ctx, node);
15    descend_move(ctx, node);
16    diagonal_move(ctx, node);
17    descend_forward_1_move(ctx, node);
18    downward_move(ctx, node);
19}
20
21fn forward_move(ctx: &mut PathfinderCtx, pos: RelBlockPos) {
22    for dir in CardinalDirection::iter() {
23        let offset = RelBlockPos::new(dir.x(), 0, dir.z());
24
25        let mut cost = SPRINT_ONE_BLOCK_COST;
26
27        let break_cost = ctx.world.cost_for_standing(pos + offset, ctx.mining_cache);
28        if break_cost == f32::INFINITY {
29            continue;
30        }
31        cost += break_cost;
32
33        ctx.edges.push(Edge {
34            movement: astar::Movement {
35                target: pos + offset,
36                data: MoveData {
37                    execute: &execute_forward_move,
38                    is_reached: &default_is_reached,
39                },
40            },
41            cost,
42        })
43    }
44}
45
46fn execute_forward_move(mut ctx: ExecuteCtx) {
47    let center = ctx.target.center();
48
49    if ctx.mine_while_at_start(ctx.target.up(1)) {
50        return;
51    }
52    if ctx.mine_while_at_start(ctx.target) {
53        return;
54    }
55
56    ctx.look_at(center);
57    ctx.sprint(SprintDirection::Forward);
58}
59
60fn ascend_move(ctx: &mut PathfinderCtx, pos: RelBlockPos) {
61    for dir in CardinalDirection::iter() {
62        let offset = RelBlockPos::new(dir.x(), 1, dir.z());
63
64        let break_cost_1 = ctx
65            .world
66            .cost_for_breaking_block(pos.up(2), ctx.mining_cache);
67        if break_cost_1 == f32::INFINITY {
68            continue;
69        }
70        let break_cost_2 = ctx.world.cost_for_standing(pos + offset, ctx.mining_cache);
71        if break_cost_2 == f32::INFINITY {
72            continue;
73        }
74
75        let cost = SPRINT_ONE_BLOCK_COST
76            + JUMP_PENALTY
77            + *JUMP_ONE_BLOCK_COST
78            + break_cost_1
79            + break_cost_2;
80
81        ctx.edges.push(Edge {
82            movement: astar::Movement {
83                target: pos + offset,
84                data: MoveData {
85                    execute: &execute_ascend_move,
86                    is_reached: &ascend_is_reached,
87                },
88            },
89            cost,
90        })
91    }
92}
93fn execute_ascend_move(mut ctx: ExecuteCtx) {
94    let ExecuteCtx {
95        target,
96        start,
97        position,
98        physics,
99        ..
100    } = ctx;
101
102    if ctx.mine_while_at_start(start.up(2)) {
103        return;
104    }
105    if ctx.mine_while_at_start(target) {
106        return;
107    }
108    if ctx.mine_while_at_start(target.up(1)) {
109        return;
110    }
111
112    let target_center = target.center();
113
114    ctx.look_at(target_center);
115    ctx.walk(WalkDirection::Forward);
116
117    // these checks are to make sure we don't fall if our velocity is too high in
118    // the wrong direction
119
120    let x_axis = (start.x - target.x).abs(); // either 0 or 1
121    let z_axis = (start.z - target.z).abs(); // either 0 or 1
122
123    let flat_distance_to_next = x_axis as f64 * (target_center.x - position.x)
124        + z_axis as f64 * (target_center.z - position.z);
125    let side_distance = z_axis as f64 * (target_center.x - position.x).abs()
126        + x_axis as f64 * (target_center.z - position.z).abs();
127
128    let lateral_motion = x_axis as f64 * physics.velocity.z + z_axis as f64 * physics.velocity.x;
129    if lateral_motion.abs() > 0.1 {
130        return;
131    }
132
133    if flat_distance_to_next > 1.2 || side_distance > 0.2 {
134        return;
135    }
136
137    if BlockPos::from(position) == start {
138        ctx.jump();
139    }
140}
141#[must_use]
142pub fn ascend_is_reached(
143    IsReachedCtx {
144        position, target, ..
145    }: IsReachedCtx,
146) -> bool {
147    BlockPos::from(position) == target || BlockPos::from(position) == target.down(1)
148}
149
150fn descend_move(ctx: &mut PathfinderCtx, pos: RelBlockPos) {
151    for dir in CardinalDirection::iter() {
152        let dir_delta = RelBlockPos::new(dir.x(), 0, dir.z());
153        let new_horizontal_position = pos + dir_delta;
154
155        let break_cost_1 = ctx
156            .world
157            .cost_for_passing(new_horizontal_position, ctx.mining_cache);
158        if break_cost_1 == f32::INFINITY {
159            continue;
160        }
161
162        let mut fall_distance = ctx.world.fall_distance(new_horizontal_position);
163        if fall_distance > 3 {
164            continue;
165        }
166
167        if fall_distance == 0 {
168            // if the fall distance is 0, set it to 1 so we try mining
169            fall_distance = 1
170        }
171
172        let new_position = new_horizontal_position.down(fall_distance as i32);
173
174        // only mine if we're descending 1 block
175        let break_cost_2;
176        if fall_distance == 1 {
177            break_cost_2 = ctx.world.cost_for_standing(new_position, ctx.mining_cache);
178            if break_cost_2 == f32::INFINITY {
179                continue;
180            }
181        } else {
182            // check whether we can stand on the target position
183            if !ctx.world.is_standable(new_position) {
184                continue;
185            }
186            break_cost_2 = 0.;
187        }
188
189        let cost = WALK_OFF_BLOCK_COST
190            + f32::max(
191                FALL_N_BLOCKS_COST
192                    .get(fall_distance as usize)
193                    .copied()
194                    // avoid panicking if we fall more than the size of FALL_N_BLOCKS_COST
195                    // probably not possible but just in case
196                    .unwrap_or(f32::INFINITY),
197                CENTER_AFTER_FALL_COST,
198            )
199            + break_cost_1
200            + break_cost_2;
201
202        ctx.edges.push(Edge {
203            movement: astar::Movement {
204                target: new_position,
205                data: MoveData {
206                    execute: &execute_descend_move,
207                    is_reached: &descend_is_reached,
208                },
209            },
210            cost,
211        })
212    }
213}
214fn execute_descend_move(mut ctx: ExecuteCtx) {
215    let ExecuteCtx {
216        target,
217        start,
218        position,
219        ..
220    } = ctx;
221
222    for i in (0..=(start.y - target.y + 1)).rev() {
223        if ctx.mine_while_at_start(target.up(i)) {
224            return;
225        }
226    }
227
228    let start_center = start.center();
229    let center = target.center();
230
231    let horizontal_distance_from_target = (center - position).horizontal_distance_squared().sqrt();
232    let horizontal_distance_from_start = (start.center() - position)
233        .horizontal_distance_squared()
234        .sqrt();
235
236    let dest_ahead = Vec3::new(
237        start_center.x + (center.x - start_center.x) * 1.5,
238        center.y,
239        start_center.z + (center.z - start_center.z) * 1.5,
240    );
241
242    if BlockPos::from(position) != target || horizontal_distance_from_target > 0.25 {
243        if horizontal_distance_from_start < 1.25 {
244            // this basically just exists to avoid doing spins while we're falling
245            ctx.look_at(dest_ahead);
246            ctx.walk(WalkDirection::Forward);
247        } else {
248            ctx.look_at(center);
249            ctx.walk(WalkDirection::Forward);
250        }
251    } else {
252        ctx.walk(WalkDirection::None);
253    }
254}
255#[must_use]
256pub fn descend_is_reached(
257    IsReachedCtx {
258        target,
259        start,
260        position,
261        ..
262    }: IsReachedCtx,
263) -> bool {
264    let dest_ahead = BlockPos::new(
265        start.x + (target.x - start.x) * 2,
266        target.y,
267        start.z + (target.z - start.z) * 2,
268    );
269
270    (BlockPos::from(position) == target || BlockPos::from(position) == dest_ahead)
271        && (position.y - target.y as f64) < 0.5
272}
273
274fn descend_forward_1_move(ctx: &mut PathfinderCtx, pos: RelBlockPos) {
275    for dir in CardinalDirection::iter() {
276        let dir_delta = RelBlockPos::new(dir.x(), 0, dir.z());
277        let gap_horizontal_position = pos + dir_delta;
278        let new_horizontal_position = pos + dir_delta * 2;
279
280        let gap_fall_distance = ctx.world.fall_distance(gap_horizontal_position);
281        let fall_distance = ctx.world.fall_distance(new_horizontal_position);
282
283        if fall_distance == 0 || fall_distance > 3 || gap_fall_distance < fall_distance {
284            continue;
285        }
286
287        let new_position = new_horizontal_position.down(fall_distance as i32);
288
289        // check whether 2 blocks vertically forward are passable
290        if !ctx.world.is_passable(new_horizontal_position) {
291            continue;
292        }
293        if !ctx.world.is_passable(gap_horizontal_position) {
294            continue;
295        }
296        // check whether we can stand on the target position
297        if !ctx.world.is_standable(new_position) {
298            continue;
299        }
300
301        let cost = WALK_OFF_BLOCK_COST
302            + WALK_ONE_BLOCK_COST
303            + f32::max(
304                FALL_N_BLOCKS_COST
305                    .get(fall_distance as usize)
306                    .copied()
307                    // avoid panicking if we fall more than the size of FALL_N_BLOCKS_COST
308                    // probably not possible but just in case
309                    .unwrap_or(f32::INFINITY),
310                CENTER_AFTER_FALL_COST,
311            );
312
313        ctx.edges.push(Edge {
314            movement: astar::Movement {
315                target: new_position,
316                data: MoveData {
317                    execute: &execute_descend_move,
318                    is_reached: &descend_is_reached,
319                },
320            },
321            cost,
322        })
323    }
324}
325
326fn diagonal_move(ctx: &mut PathfinderCtx, pos: RelBlockPos) {
327    for dir in CardinalDirection::iter() {
328        let right = dir.right();
329        let offset = RelBlockPos::new(dir.x() + right.x(), 0, dir.z() + right.z());
330        let left_pos = RelBlockPos::new(pos.x + dir.x(), pos.y, pos.z + dir.z());
331        let right_pos = RelBlockPos::new(pos.x + right.x(), pos.y, pos.z + right.z());
332
333        // +0.001 so it doesn't unnecessarily go diagonal sometimes
334        let mut cost = SPRINT_ONE_BLOCK_COST * SQRT_2 + 0.001;
335
336        let left_passable = ctx.world.is_passable(left_pos);
337        let right_passable = ctx.world.is_passable(right_pos);
338
339        if !left_passable && !right_passable {
340            continue;
341        }
342
343        if !left_passable || !right_passable {
344            // add a bit of cost because it'll probably be hugging a wall here
345            cost += WALK_ONE_BLOCK_COST / 2.;
346        }
347
348        if !ctx.world.is_standable(pos + offset) {
349            continue;
350        }
351
352        ctx.edges.push(Edge {
353            movement: astar::Movement {
354                target: pos + offset,
355                data: MoveData {
356                    execute: &execute_diagonal_move,
357                    is_reached: &default_is_reached,
358                },
359            },
360            cost,
361        })
362    }
363}
364fn execute_diagonal_move(mut ctx: ExecuteCtx) {
365    let target_center = ctx.target.center();
366
367    ctx.look_at(target_center);
368    ctx.sprint(SprintDirection::Forward);
369}
370
371/// Go directly down, usually by mining.
372fn downward_move(ctx: &mut PathfinderCtx, pos: RelBlockPos) {
373    // make sure we land on a solid block after breaking the one below us
374    if !ctx.world.is_block_solid(pos.down(2)) {
375        return;
376    }
377
378    let break_cost = ctx
379        .world
380        .cost_for_breaking_block(pos.down(1), ctx.mining_cache);
381    if break_cost == f32::INFINITY {
382        return;
383    }
384
385    let cost = FALL_N_BLOCKS_COST[1] + break_cost;
386
387    ctx.edges.push(Edge {
388        movement: astar::Movement {
389            target: pos.down(1),
390            data: MoveData {
391                execute: &execute_downward_move,
392                is_reached: &default_is_reached,
393            },
394        },
395        cost,
396    })
397}
398fn execute_downward_move(mut ctx: ExecuteCtx) {
399    let ExecuteCtx {
400        target, position, ..
401    } = ctx;
402
403    let target_center = target.center();
404
405    let horizontal_distance_from_target = (target_center - position)
406        .horizontal_distance_squared()
407        .sqrt();
408
409    if horizontal_distance_from_target > 0.25 {
410        ctx.look_at(target_center);
411        ctx.walk(WalkDirection::Forward);
412    } else if ctx.mine_while_at_start(target) {
413        ctx.walk(WalkDirection::None);
414    } else if BlockPos::from(position) != target {
415        ctx.look_at(target_center);
416        ctx.walk(WalkDirection::Forward);
417    } else {
418        ctx.walk(WalkDirection::None);
419    }
420}