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