azalea/pathfinder/moves/
parkour.rs1use 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 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 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 1
36 } else if ctx.world.is_standable(pos + offset) {
37 0
42 } else {
43 continue;
44 };
45
46 if !ctx.world.is_block_passable((pos + gap_offset).up(2)) {
48 continue;
49 }
50
51 if !ctx.world.is_block_passable(pos.up(2)) {
53 continue;
54 }
55 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 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 cost += FALL_N_BLOCKS_COST[2] / 2.;
96 -1
97 } else {
98 continue;
99 };
100
101 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 if !ctx.world.is_block_passable(pos.up(2)) {
112 continue;
113 }
114 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 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 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 if !ctx.world.is_block_passable(pos.up(2)) {
162 continue;
163 }
164 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 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 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 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 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 !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 if player_pos_to_block_pos(position) == target && (position.y - target.y as f64) < 0.094 {
279 return true;
280 }
281
282 player_pos_to_block_pos(position) == target && physics.on_ground()
284}