Skip to main content

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