azalea/pathfinder/moves/
parkour.rs

1use azalea_client::{SprintDirection, WalkDirection};
2use azalea_core::{direction::CardinalDirection, position::BlockPos};
3use tracing::trace;
4
5use super::{Edge, ExecuteCtx, IsReachedCtx, MoveData, PathfinderCtx};
6use crate::pathfinder::{astar, costs::*, player_pos_to_block_pos, rel_block_pos::RelBlockPos};
7
8pub fn parkour_move(ctx: &mut PathfinderCtx, node: RelBlockPos) {
9    if !ctx.world.is_block_solid(node.down(1)) {
10        // we can only parkour from solid blocks (not just standable blocks like slabs)
11        return;
12    }
13
14    parkour_forward_1_move(ctx, node);
15    parkour_forward_2_move(ctx, node);
16    parkour_forward_3_move(ctx, node);
17}
18
19fn parkour_forward_1_move(ctx: &mut PathfinderCtx, pos: RelBlockPos) {
20    for dir in CardinalDirection::iter() {
21        let gap_offset = RelBlockPos::new(dir.x(), 0, dir.z());
22        let offset = RelBlockPos::new(dir.x() * 2, 0, dir.z() * 2);
23
24        // make sure we actually have to jump
25        if ctx.world.is_block_solid((pos + gap_offset).down(1)) {
26            continue;
27        }
28        if !ctx.world.is_passable(pos + gap_offset) {
29            continue;
30        }
31
32        let ascend: i32 = if ctx.world.is_standable(pos + offset.up(1)) {
33            // ascend
34            1
35        } else if ctx.world.is_standable(pos + offset) {
36            // forward
37            0
38        } else {
39            continue;
40        };
41
42        // make sure we have space to jump
43        if !ctx.world.is_block_passable((pos + gap_offset).up(2)) {
44            continue;
45        }
46
47        // make sure there's not a block above us
48        if !ctx.world.is_block_passable(pos.up(2)) {
49            continue;
50        }
51        // make sure there's not a block above the target
52        if !ctx.world.is_block_passable((pos + offset).up(2)) {
53            continue;
54        }
55
56        let cost = JUMP_PENALTY + WALK_ONE_BLOCK_COST * 2. + CENTER_AFTER_FALL_COST;
57
58        ctx.edges.push(Edge {
59            movement: astar::Movement {
60                target: pos + offset.up(ascend),
61                data: MoveData {
62                    execute: &execute_parkour_move,
63                    is_reached: &parkour_is_reached,
64                },
65            },
66            cost,
67        })
68    }
69}
70
71fn parkour_forward_2_move(ctx: &mut PathfinderCtx, pos: RelBlockPos) {
72    'dir: for dir in CardinalDirection::iter() {
73        let gap_1_offset = RelBlockPos::new(dir.x(), 0, dir.z());
74        let gap_2_offset = RelBlockPos::new(dir.x() * 2, 0, dir.z() * 2);
75        let offset = RelBlockPos::new(dir.x() * 3, 0, dir.z() * 3);
76
77        // make sure we actually have to jump
78        if ctx.world.is_block_solid((pos + gap_1_offset).down(1))
79            || ctx.world.is_block_solid((pos + gap_2_offset).down(1))
80        {
81            continue;
82        }
83
84        let mut cost = JUMP_PENALTY + WALK_ONE_BLOCK_COST * 3. + CENTER_AFTER_FALL_COST;
85
86        let ascend: i32 = if ctx.world.is_standable(pos + offset.up(1)) {
87            1
88        } else if ctx.world.is_standable(pos + offset) {
89            cost += FALL_N_BLOCKS_COST[1];
90            0
91        } else if ctx.world.is_standable(pos + offset.down(1)) {
92            cost += FALL_N_BLOCKS_COST[2];
93            -1
94        } else {
95            continue;
96        };
97
98        // make sure we have space to jump
99        for offset in [gap_1_offset, gap_2_offset] {
100            if !ctx.world.is_passable(pos + offset) {
101                continue 'dir;
102            }
103            if !ctx.world.is_block_passable((pos + offset).up(2)) {
104                continue 'dir;
105            }
106        }
107        // make sure there's not a block above us
108        if !ctx.world.is_block_passable(pos.up(2)) {
109            continue;
110        }
111        // make sure there's not a block above the target
112        if !ctx.world.is_block_passable((pos + offset).up(2)) {
113            continue;
114        }
115
116        ctx.edges.push(Edge {
117            movement: astar::Movement {
118                target: pos + offset.up(ascend),
119                data: MoveData {
120                    execute: &execute_parkour_move,
121                    is_reached: &parkour_is_reached,
122                },
123            },
124            cost,
125        })
126    }
127}
128
129fn parkour_forward_3_move(ctx: &mut PathfinderCtx, pos: RelBlockPos) {
130    'dir: for dir in CardinalDirection::iter() {
131        let gap_1_offset = RelBlockPos::new(dir.x(), 0, dir.z());
132        let gap_2_offset = RelBlockPos::new(dir.x() * 2, 0, dir.z() * 2);
133        let gap_3_offset = RelBlockPos::new(dir.x() * 3, 0, dir.z() * 3);
134        let offset = RelBlockPos::new(dir.x() * 4, 0, dir.z() * 4);
135
136        // make sure we actually have to jump
137        if ctx.world.is_block_solid((pos + gap_1_offset).down(1))
138            || ctx.world.is_block_solid((pos + gap_2_offset).down(1))
139            || ctx.world.is_block_solid((pos + gap_3_offset).down(1))
140        {
141            continue;
142        }
143
144        if !ctx.world.is_standable(pos + offset) {
145            continue;
146        };
147
148        // make sure we have space to jump
149        for offset in [gap_1_offset, gap_2_offset, gap_3_offset] {
150            if !ctx.world.is_passable(pos + offset) {
151                continue 'dir;
152            }
153            if !ctx.world.is_block_passable((pos + offset).up(2)) {
154                continue 'dir;
155            }
156        }
157        // make sure there's not a block above us
158        if !ctx.world.is_block_passable(pos.up(2)) {
159            continue;
160        }
161        // make sure there's not a block above the target
162        if !ctx.world.is_block_passable((pos + offset).up(2)) {
163            continue;
164        }
165
166        let cost = JUMP_PENALTY + SPRINT_ONE_BLOCK_COST * 4. + CENTER_AFTER_FALL_COST;
167
168        ctx.edges.push(Edge {
169            movement: astar::Movement {
170                target: pos + offset,
171                data: MoveData {
172                    execute: &execute_parkour_move,
173                    is_reached: &parkour_is_reached,
174                },
175            },
176            cost,
177        })
178    }
179}
180
181fn execute_parkour_move(mut ctx: ExecuteCtx) {
182    let ExecuteCtx {
183        position,
184        target,
185        start,
186        ..
187    } = ctx;
188
189    let start_center = start.center();
190    let target_center = target.center();
191
192    let jump_distance = i32::max((target - start).x.abs(), (target - start).z.abs());
193
194    let ascend: i32 = target.y - start.y;
195
196    if jump_distance >= 4 || (ascend > 0 && jump_distance >= 3) {
197        // 3 block gap OR 2 block gap with ascend
198        ctx.sprint(SprintDirection::Forward);
199    } else {
200        ctx.walk(WalkDirection::Forward);
201    }
202
203    let x_dir = (target.x - start.x).clamp(-1, 1);
204    let z_dir = (target.z - start.z).clamp(-1, 1);
205    let dir = BlockPos::new(x_dir, 0, z_dir);
206    let jump_at_pos = start + dir;
207
208    let is_at_start_block = player_pos_to_block_pos(position) == start;
209    let is_at_jump_block = player_pos_to_block_pos(position) == jump_at_pos;
210
211    let required_distance_from_center = if jump_distance <= 2 {
212        // 1 block gap
213        0.0
214    } else {
215        0.6
216    };
217    let distance_from_start = f64::max(
218        (position.x - start_center.x).abs(),
219        (position.z - start_center.z).abs(),
220    );
221
222    if !is_at_start_block
223        && !is_at_jump_block
224        && (position.y - start.y as f64) < 0.094
225        && distance_from_start < 0.85
226    {
227        // we have to be on the start block to jump
228        ctx.look_at(start_center);
229        trace!("looking at start_center");
230    } else {
231        ctx.look_at(target_center);
232        trace!("looking at target_center");
233    }
234
235    if !is_at_start_block && is_at_jump_block && distance_from_start > required_distance_from_center
236    {
237        ctx.jump();
238    }
239}
240
241#[must_use]
242pub fn parkour_is_reached(
243    IsReachedCtx {
244        position,
245        target,
246        physics,
247        ..
248    }: IsReachedCtx,
249) -> bool {
250    // 0.094 and not 0 for lilypads
251    if player_pos_to_block_pos(position) == target && (position.y - target.y as f64) < 0.094 {
252        return true;
253    }
254
255    // this is to make it handle things like slabs correctly
256    player_pos_to_block_pos(position) == target && physics.on_ground()
257}