azalea/pathfinder/moves/
parkour.rs1use 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 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 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 1
35 } else if ctx.world.is_standable(pos + offset) {
36 0
38 } else {
39 continue;
40 };
41
42 if !ctx.world.is_block_passable((pos + gap_offset).up(2)) {
44 continue;
45 }
46
47 if !ctx.world.is_block_passable(pos.up(2)) {
49 continue;
50 }
51 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 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 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 if !ctx.world.is_block_passable(pos.up(2)) {
103 continue;
104 }
105 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 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 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 if !ctx.world.is_block_passable(pos.up(2)) {
155 continue;
156 }
157 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 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 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 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 if BlockPos::from(position) == target && (position.y - target.y as f64) < 0.094 {
248 return true;
249 }
250
251 BlockPos::from(position).up(1) == target && physics.on_ground()
254}