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::*, 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 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 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 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 if !ctx.world.is_block_passable(pos.up(2)) {
109 continue;
110 }
111 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 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 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 if !ctx.world.is_block_passable(pos.up(2)) {
159 continue;
160 }
161 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 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 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 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 if player_pos_to_block_pos(position) == target && (position.y - target.y as f64) < 0.094 {
252 return true;
253 }
254
255 player_pos_to_block_pos(position) == target && physics.on_ground()
257}