azalea/pathfinder/moves/
basic.rs

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