Skip to main content

azalea/pathfinder/moves/
parkour.rs

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