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::*, 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 ascend: i32 = if ctx.world.is_standable(pos + offset.up(1)) {
85            1
86        } else if ctx.world.is_standable(pos + offset) {
87            0
88        } else {
89            continue;
90        };
91
92        // make sure we have space to jump
93        for offset in [gap_1_offset, gap_2_offset] {
94            if !ctx.world.is_passable(pos + offset) {
95                continue 'dir;
96            }
97            if !ctx.world.is_block_passable((pos + offset).up(2)) {
98                continue 'dir;
99            }
100        }
101        // make sure there's not a block above us
102        if !ctx.world.is_block_passable(pos.up(2)) {
103            continue;
104        }
105        // make sure there's not a block above the target
106        if !ctx.world.is_block_passable((pos + offset).up(2)) {
107            continue;
108        }
109
110        let cost = JUMP_PENALTY + WALK_ONE_BLOCK_COST * 3. + CENTER_AFTER_FALL_COST;
111
112        ctx.edges.push(Edge {
113            movement: astar::Movement {
114                target: pos + offset.up(ascend),
115                data: MoveData {
116                    execute: &execute_parkour_move,
117                    is_reached: &parkour_is_reached,
118                },
119            },
120            cost,
121        })
122    }
123}
124
125fn parkour_forward_3_move(ctx: &mut PathfinderCtx, pos: RelBlockPos) {
126    'dir: for dir in CardinalDirection::iter() {
127        let gap_1_offset = RelBlockPos::new(dir.x(), 0, dir.z());
128        let gap_2_offset = RelBlockPos::new(dir.x() * 2, 0, dir.z() * 2);
129        let gap_3_offset = RelBlockPos::new(dir.x() * 3, 0, dir.z() * 3);
130        let offset = RelBlockPos::new(dir.x() * 4, 0, dir.z() * 4);
131
132        // make sure we actually have to jump
133        if ctx.world.is_block_solid((pos + gap_1_offset).down(1))
134            || ctx.world.is_block_solid((pos + gap_2_offset).down(1))
135            || ctx.world.is_block_solid((pos + gap_3_offset).down(1))
136        {
137            continue;
138        }
139
140        if !ctx.world.is_standable(pos + offset) {
141            continue;
142        };
143
144        // make sure we have space to jump
145        for offset in [gap_1_offset, gap_2_offset, gap_3_offset] {
146            if !ctx.world.is_passable(pos + offset) {
147                continue 'dir;
148            }
149            if !ctx.world.is_block_passable((pos + offset).up(2)) {
150                continue 'dir;
151            }
152        }
153        // make sure there's not a block above us
154        if !ctx.world.is_block_passable(pos.up(2)) {
155            continue;
156        }
157        // make sure there's not a block above the target
158        if !ctx.world.is_block_passable((pos + offset).up(2)) {
159            continue;
160        }
161
162        let cost = JUMP_PENALTY + SPRINT_ONE_BLOCK_COST * 4. + CENTER_AFTER_FALL_COST;
163
164        ctx.edges.push(Edge {
165            movement: astar::Movement {
166                target: pos + offset,
167                data: MoveData {
168                    execute: &execute_parkour_move,
169                    is_reached: &parkour_is_reached,
170                },
171            },
172            cost,
173        })
174    }
175}
176
177fn execute_parkour_move(mut ctx: ExecuteCtx) {
178    let ExecuteCtx {
179        position,
180        target,
181        start,
182        ..
183    } = ctx;
184
185    let start_center = start.center();
186    let target_center = target.center();
187
188    let jump_distance = i32::max((target - start).x.abs(), (target - start).z.abs());
189
190    let ascend: i32 = target.y - start.y;
191
192    if jump_distance >= 4 || (ascend > 0 && jump_distance >= 3) {
193        // 3 block gap OR 2 block gap with ascend
194        ctx.sprint(SprintDirection::Forward);
195    } else {
196        ctx.walk(WalkDirection::Forward);
197    }
198
199    let x_dir = (target.x - start.x).clamp(-1, 1);
200    let z_dir = (target.z - start.z).clamp(-1, 1);
201    let dir = BlockPos::new(x_dir, 0, z_dir);
202    let jump_at_pos = start + dir;
203
204    let is_at_start_block = BlockPos::from(position) == start;
205    let is_at_jump_block = BlockPos::from(position) == jump_at_pos;
206
207    let required_distance_from_center = if jump_distance <= 2 {
208        // 1 block gap
209        0.0
210    } else {
211        0.6
212    };
213    let distance_from_start = f64::max(
214        (position.x - start_center.x).abs(),
215        (position.z - start_center.z).abs(),
216    );
217
218    if !is_at_start_block
219        && !is_at_jump_block
220        && (position.y - start.y as f64) < 0.094
221        && distance_from_start < 0.85
222    {
223        // we have to be on the start block to jump
224        ctx.look_at(start_center);
225        trace!("looking at start_center");
226    } else {
227        ctx.look_at(target_center);
228        trace!("looking at target_center");
229    }
230
231    if !is_at_start_block && is_at_jump_block && distance_from_start > required_distance_from_center
232    {
233        ctx.jump();
234    }
235}
236
237#[must_use]
238pub fn parkour_is_reached(
239    IsReachedCtx {
240        position,
241        target,
242        physics,
243        ..
244    }: IsReachedCtx,
245) -> bool {
246    // 0.094 and not 0 for lilypads
247    if BlockPos::from(position) == target && (position.y - target.y as f64) < 0.094 {
248        return true;
249    }
250
251    // this is to make it handle things like slabs correctly, if we're on the block
252    // below the target but on_ground
253    BlockPos::from(position).up(1) == target && physics.on_ground()
254}