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