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