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 parkour_forward_1_move(ctx, node);
10 parkour_forward_2_move(ctx, node);
11 parkour_forward_3_move(ctx, node);
12}
13
14fn parkour_forward_1_move(ctx: &mut PathfinderCtx, pos: RelBlockPos) {
15 for dir in CardinalDirection::iter() {
16 let gap_offset = RelBlockPos::new(dir.x(), 0, dir.z());
17 let offset = RelBlockPos::new(dir.x() * 2, 0, dir.z() * 2);
18
19 if ctx.world.is_block_solid((pos + gap_offset).down(1)) {
21 continue;
22 }
23 if !ctx.world.is_passable(pos + gap_offset) {
24 continue;
25 }
26
27 let ascend: i32 = if ctx.world.is_standable(pos + offset.up(1)) {
28 1
30 } else if ctx.world.is_standable(pos + offset) {
31 0
33 } else {
34 continue;
35 };
36
37 if !ctx.world.is_block_passable((pos + gap_offset).up(2)) {
39 continue;
40 }
41
42 if !ctx.world.is_block_passable(pos.up(2)) {
44 continue;
45 }
46 if !ctx.world.is_block_passable((pos + offset).up(2)) {
48 continue;
49 }
50
51 let cost = JUMP_PENALTY + WALK_ONE_BLOCK_COST * 2. + CENTER_AFTER_FALL_COST;
52
53 ctx.edges.push(Edge {
54 movement: astar::Movement {
55 target: pos + offset.up(ascend),
56 data: MoveData {
57 execute: &execute_parkour_move,
58 is_reached: &parkour_is_reached,
59 },
60 },
61 cost,
62 })
63 }
64}
65
66fn parkour_forward_2_move(ctx: &mut PathfinderCtx, pos: RelBlockPos) {
67 'dir: for dir in CardinalDirection::iter() {
68 let gap_1_offset = RelBlockPos::new(dir.x(), 0, dir.z());
69 let gap_2_offset = RelBlockPos::new(dir.x() * 2, 0, dir.z() * 2);
70 let offset = RelBlockPos::new(dir.x() * 3, 0, dir.z() * 3);
71
72 if ctx.world.is_block_solid((pos + gap_1_offset).down(1))
74 || ctx.world.is_block_solid((pos + gap_2_offset).down(1))
75 {
76 continue;
77 }
78
79 let ascend: i32 = if ctx.world.is_standable(pos + offset.up(1)) {
80 1
81 } else if ctx.world.is_standable(pos + offset) {
82 0
83 } else {
84 continue;
85 };
86
87 for offset in [gap_1_offset, gap_2_offset] {
89 if !ctx.world.is_passable(pos + offset) {
90 continue 'dir;
91 }
92 if !ctx.world.is_block_passable((pos + offset).up(2)) {
93 continue 'dir;
94 }
95 }
96 if !ctx.world.is_block_passable(pos.up(2)) {
98 continue;
99 }
100 if !ctx.world.is_block_passable((pos + offset).up(2)) {
102 continue;
103 }
104
105 let cost = JUMP_PENALTY + WALK_ONE_BLOCK_COST * 3. + CENTER_AFTER_FALL_COST;
106
107 ctx.edges.push(Edge {
108 movement: astar::Movement {
109 target: pos + offset.up(ascend),
110 data: MoveData {
111 execute: &execute_parkour_move,
112 is_reached: &parkour_is_reached,
113 },
114 },
115 cost,
116 })
117 }
118}
119
120fn parkour_forward_3_move(ctx: &mut PathfinderCtx, pos: RelBlockPos) {
121 'dir: for dir in CardinalDirection::iter() {
122 let gap_1_offset = RelBlockPos::new(dir.x(), 0, dir.z());
123 let gap_2_offset = RelBlockPos::new(dir.x() * 2, 0, dir.z() * 2);
124 let gap_3_offset = RelBlockPos::new(dir.x() * 3, 0, dir.z() * 3);
125 let offset = RelBlockPos::new(dir.x() * 4, 0, dir.z() * 4);
126
127 if ctx.world.is_block_solid((pos + gap_1_offset).down(1))
129 || ctx.world.is_block_solid((pos + gap_2_offset).down(1))
130 || ctx.world.is_block_solid((pos + gap_3_offset).down(1))
131 {
132 continue;
133 }
134
135 if !ctx.world.is_standable(pos + offset) {
136 continue;
137 };
138
139 for offset in [gap_1_offset, gap_2_offset, gap_3_offset] {
141 if !ctx.world.is_passable(pos + offset) {
142 continue 'dir;
143 }
144 if !ctx.world.is_block_passable((pos + offset).up(2)) {
145 continue 'dir;
146 }
147 }
148 if !ctx.world.is_block_passable(pos.up(2)) {
150 continue;
151 }
152 if !ctx.world.is_block_passable((pos + offset).up(2)) {
154 continue;
155 }
156
157 let cost = JUMP_PENALTY + SPRINT_ONE_BLOCK_COST * 4. + CENTER_AFTER_FALL_COST;
158
159 ctx.edges.push(Edge {
160 movement: astar::Movement {
161 target: pos + offset,
162 data: MoveData {
163 execute: &execute_parkour_move,
164 is_reached: &parkour_is_reached,
165 },
166 },
167 cost,
168 })
169 }
170}
171
172fn execute_parkour_move(mut ctx: ExecuteCtx) {
173 let ExecuteCtx {
174 position,
175 target,
176 start,
177 ..
178 } = ctx;
179
180 let start_center = start.center();
181 let target_center = target.center();
182
183 let jump_distance = i32::max((target - start).x.abs(), (target - start).z.abs());
184
185 let ascend: i32 = target.y - start.y;
186
187 if jump_distance >= 4 || (ascend > 0 && jump_distance >= 3) {
188 ctx.sprint(SprintDirection::Forward);
190 } else {
191 ctx.walk(WalkDirection::Forward);
192 }
193
194 let x_dir = (target.x - start.x).clamp(-1, 1);
195 let z_dir = (target.z - start.z).clamp(-1, 1);
196 let dir = BlockPos::new(x_dir, 0, z_dir);
197 let jump_at_pos = start + dir;
198
199 let is_at_start_block = BlockPos::from(position) == start;
200 let is_at_jump_block = BlockPos::from(position) == jump_at_pos;
201
202 let required_distance_from_center = if jump_distance <= 2 {
203 0.0
205 } else {
206 0.6
207 };
208 let distance_from_start = f64::max(
209 (position.x - start_center.x).abs(),
210 (position.z - start_center.z).abs(),
211 );
212
213 if !is_at_start_block
214 && !is_at_jump_block
215 && (position.y - start.y as f64) < 0.094
216 && distance_from_start < 0.85
217 {
218 ctx.look_at(start_center);
220 trace!("looking at start_center");
221 } else {
222 ctx.look_at(target_center);
223 trace!("looking at target_center");
224 }
225
226 if !is_at_start_block && is_at_jump_block && distance_from_start > required_distance_from_center
227 {
228 ctx.jump();
229 }
230}
231
232#[must_use]
233pub fn parkour_is_reached(
234 IsReachedCtx {
235 position, target, ..
236 }: IsReachedCtx,
237) -> bool {
238 BlockPos::from(position) == target && (position.y - target.y as f64) < 0.094
240}