|
1 | 1 | use azalea_client::{SprintDirection, WalkDirection}; |
2 | | -use azalea_core::{direction::CardinalDirection, position::BlockPos}; |
3 | | -use tracing::trace; |
4 | 2 |
|
5 | 3 | use super::{Edge, ExecuteCtx, IsReachedCtx, MoveData, PathfinderCtx}; |
6 | 4 | use crate::pathfinder::{astar, costs::*, player_pos_to_block_pos, rel_block_pos::RelBlockPos}; |
7 | 5 |
|
8 | 6 | pub fn parkour_move(ctx: &mut PathfinderCtx, node: RelBlockPos) { |
9 | 7 | if !ctx.world.is_block_solid(node.down(1)) { |
10 | | - // we can only parkour from solid blocks (not just standable blocks like slabs) |
11 | 8 | return; |
12 | 9 | } |
13 | 10 |
|
14 | | - parkour_forward_1_move(ctx, node); |
15 | | - parkour_forward_2_move(ctx, node); |
16 | | - parkour_forward_3_move(ctx, node); |
17 | | -} |
18 | | - |
19 | | -fn 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 | | - } |
| 11 | + let distance = 5; |
31 | 12 |
|
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 | | - } |
| 13 | + for dx in -distance..=distance { |
| 14 | + for dz in -distance..=distance { |
| 15 | + if ((-1..=1).contains(&dx) && (-1..=1).contains(&dz)) |
| 16 | + || dx * dx + dz * dz > distance * distance |
| 17 | + { |
| 18 | + continue; |
| 19 | + } |
46 | 20 |
|
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; |
| 21 | + parkour_direction_move(ctx, node, dx, dz, dx.abs().max(dz.abs())); |
54 | 22 | } |
| 23 | + } |
| 24 | +} |
55 | 25 |
|
56 | | - let cost = JUMP_PENALTY + WALK_ONE_BLOCK_COST * 2. + CENTER_AFTER_FALL_COST; |
| 26 | +fn parkour_direction_move( |
| 27 | + ctx: &mut PathfinderCtx, |
| 28 | + pos: RelBlockPos, |
| 29 | + dx: i16, |
| 30 | + dz: i16, |
| 31 | + distance: i16, |
| 32 | +) { |
| 33 | + let target_offset = RelBlockPos::new(dx, 0, dz); |
| 34 | + let target_pos = pos + target_offset; |
| 35 | + |
| 36 | + if !are_gaps_valid(ctx, pos, dx, dz) |
| 37 | + || (!ctx.world.is_block_passable(pos.up(2)) |
| 38 | + || !ctx.world.is_block_passable(target_pos.up(2))) |
| 39 | + { |
| 40 | + return; |
| 41 | + } |
57 | 42 |
|
| 43 | + if let Some((target, cost)) = find_landing_position(ctx, target_pos, distance - 1) { |
58 | 44 | ctx.edges.push(Edge { |
59 | 45 | movement: astar::Movement { |
60 | | - target: pos + offset.up(ascend), |
| 46 | + target, |
61 | 47 | data: MoveData { |
62 | 48 | execute: &execute_parkour_move, |
63 | 49 | is_reached: &parkour_is_reached, |
64 | 50 | }, |
65 | 51 | }, |
66 | 52 | cost, |
67 | | - }) |
| 53 | + }); |
68 | 54 | } |
69 | 55 | } |
70 | 56 |
|
71 | | -fn 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 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 | | - }; |
| 57 | +fn are_gaps_valid(ctx: &mut PathfinderCtx, pos: RelBlockPos, dx: i16, dz: i16) -> bool { |
| 58 | + let line = get_line_bresenham(0, 0, dx, dz); |
| 59 | + |
| 60 | + line.iter() |
| 61 | + .enumerate() |
| 62 | + .skip(1) |
| 63 | + .take(line.len().saturating_sub(2)) |
| 64 | + .all(|(_, &(x, z))| { |
| 65 | + let gap_pos = pos + RelBlockPos::new(x, 0, z); |
| 66 | + !ctx.world.is_block_solid(gap_pos.down(1)) |
| 67 | + && ctx.world.is_passable(gap_pos) |
| 68 | + && ctx.world.is_block_passable(gap_pos.up(2)) |
| 69 | + }) |
| 70 | +} |
97 | 71 |
|
98 | | - // make sure we have space to jump |
99 | | - 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 | | - } |
| 72 | +fn get_line_bresenham(x0: i16, y0: i16, x1: i16, y1: i16) -> Vec<(i16, i16)> { |
| 73 | + let mut points = Vec::new(); |
| 74 | + let (dx, dy) = ((x1 - x0).abs(), (y1 - y0).abs()); |
| 75 | + let (sx, sy) = (x0.cmp(&x1).reverse() as i16, y0.cmp(&y1).reverse() as i16); |
| 76 | + let mut err = dx - dy; |
| 77 | + let (mut x, mut y) = (x0, y0); |
| 78 | + |
| 79 | + loop { |
| 80 | + points.push((x, y)); |
| 81 | + if x == x1 && y == y1 { |
| 82 | + break; |
106 | 83 | } |
107 | | - // make sure there's not a block above us |
108 | | - if !ctx.world.is_block_passable(pos.up(2)) { |
109 | | - continue; |
| 84 | + let e2 = 2 * err; |
| 85 | + if e2 > -dy { |
| 86 | + err -= dy; |
| 87 | + x += sx; |
110 | 88 | } |
111 | | - // make sure there's not a block above the target |
112 | | - if !ctx.world.is_block_passable((pos + offset).up(2)) { |
113 | | - continue; |
| 89 | + if e2 < dx { |
| 90 | + err += dx; |
| 91 | + y += sy; |
114 | 92 | } |
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 | 93 | } |
| 94 | + points |
127 | 95 | } |
128 | 96 |
|
129 | | -fn 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 | | - // make sure we actually have to jump |
137 | | - 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 | | - // make sure we have space to jump |
149 | | - for offset in [gap_1_offset, gap_2_offset, gap_3_offset] { |
150 | | - if !ctx.world.is_passable(pos + offset) { |
151 | | - continue 'dir; |
| 97 | +fn find_landing_position( |
| 98 | + ctx: &mut PathfinderCtx, |
| 99 | + base_target: RelBlockPos, |
| 100 | + distance: i16, |
| 101 | +) -> Option<(RelBlockPos, f32)> { |
| 102 | + generate_height_checks(distance) |
| 103 | + .into_iter() |
| 104 | + .find_map(|(height_offset, fall_cost)| { |
| 105 | + let target = base_target.up(height_offset); |
| 106 | + if ctx.world.is_standable(target) { |
| 107 | + let movement_cost = if distance >= 4 { |
| 108 | + SPRINT_ONE_BLOCK_COST |
| 109 | + } else { |
| 110 | + WALK_ONE_BLOCK_COST |
| 111 | + }; |
| 112 | + let actual_distance = |
| 113 | + ((base_target.x as f32).powi(2) + (base_target.z as f32).powi(2)).sqrt(); |
| 114 | + let cost = JUMP_PENALTY |
| 115 | + + movement_cost * actual_distance |
| 116 | + + fall_cost |
| 117 | + + CENTER_AFTER_FALL_COST; |
| 118 | + Some((target, cost)) |
| 119 | + } else { |
| 120 | + None |
152 | 121 | } |
153 | | - if !ctx.world.is_block_passable((pos + offset).up(2)) { |
154 | | - continue 'dir; |
| 122 | + }) |
| 123 | +} |
| 124 | + |
| 125 | +fn generate_height_checks(distance: i16) -> Vec<(i32, f32)> { |
| 126 | + match distance { |
| 127 | + 2 => vec![(1, 0.0), (0, 0.0)], |
| 128 | + 3 => vec![ |
| 129 | + (1, 0.0), |
| 130 | + (0, FALL_N_BLOCKS_COST[1]), |
| 131 | + (-1, FALL_N_BLOCKS_COST[2]), |
| 132 | + ], |
| 133 | + _ => { |
| 134 | + let mut checks = Vec::with_capacity(5); |
| 135 | + if distance <= 3 { |
| 136 | + checks.push((1, 0.0)); |
155 | 137 | } |
| 138 | + checks.push((0, FALL_N_BLOCKS_COST[1])); |
| 139 | + checks.extend((1..=3).map(|h| (-(h as i32), FALL_N_BLOCKS_COST[h]))); |
| 140 | + checks |
156 | 141 | } |
157 | | - // make sure there's not a block above us |
158 | | - if !ctx.world.is_block_passable(pos.up(2)) { |
159 | | - continue; |
160 | | - } |
161 | | - // make sure there's not a block above the target |
162 | | - 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 | 142 | } |
179 | 143 | } |
180 | 144 |
|
181 | 145 | fn 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 | | - // 3 block gap OR 2 block gap with ascend |
| 146 | + let delta = ctx.target - ctx.start; |
| 147 | + let jump_distance = (delta.x as f64).hypot(delta.z as f64); |
| 148 | + |
| 149 | + if jump_distance >= 3.0 { |
198 | 150 | ctx.sprint(SprintDirection::Forward); |
199 | 151 | } else { |
200 | 152 | ctx.walk(WalkDirection::Forward); |
201 | 153 | } |
202 | 154 |
|
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 | | - // 1 block gap |
213 | | - 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 | | - // we have to be on the start block to jump |
228 | | - 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 | | - { |
| 155 | + let should_jump = [(delta.x, ctx.start.x, ctx.position.x), (delta.z, ctx.start.z, ctx.position.z)] |
| 156 | + .iter() |
| 157 | + .any(|&(d, start, pos)| { |
| 158 | + if d == 0 { return false } |
| 159 | + |
| 160 | + let edge = if d > 0 { start + 1 } else { start } as f64; |
| 161 | + (d > 0 && pos >= edge) || (d < 0 && pos <= edge) |
| 162 | + }); |
| 163 | + |
| 164 | + ctx.look_at(ctx.target.center()); |
| 165 | + |
| 166 | + if should_jump { |
237 | 167 | ctx.jump(); |
238 | 168 | } |
239 | 169 | } |
240 | 170 |
|
241 | 171 | #[must_use] |
242 | | -pub fn parkour_is_reached( |
243 | | - IsReachedCtx { |
244 | | - position, |
245 | | - target, |
246 | | - physics, |
247 | | - .. |
248 | | - }: IsReachedCtx, |
249 | | -) -> bool { |
250 | | - // 0.094 and not 0 for lilypads |
251 | | - if player_pos_to_block_pos(position) == target && (position.y - target.y as f64) < 0.094 { |
252 | | - return true; |
253 | | - } |
254 | | - |
255 | | - // this is to make it handle things like slabs correctly |
256 | | - player_pos_to_block_pos(position) == target && physics.on_ground() |
| 172 | +pub fn parkour_is_reached(ctx: IsReachedCtx) -> bool { |
| 173 | + player_pos_to_block_pos(ctx.position) == ctx.target |
| 174 | + && (ctx.position.y - (ctx.target.y as f64) < 0.094 || ctx.physics.on_ground()) |
257 | 175 | } |
0 commit comments