Skip to content

Commit aab380a

Browse files
committed
Add advanced parkouring
1 parent 3f7b56f commit aab380a

File tree

1 file changed

+124
-206
lines changed

1 file changed

+124
-206
lines changed
Lines changed: 124 additions & 206 deletions
Original file line numberDiff line numberDiff line change
@@ -1,257 +1,175 @@
11
use azalea_client::{SprintDirection, WalkDirection};
2-
use azalea_core::{direction::CardinalDirection, position::BlockPos};
3-
use tracing::trace;
42

53
use super::{Edge, ExecuteCtx, IsReachedCtx, MoveData, PathfinderCtx};
64
use crate::pathfinder::{astar, costs::*, player_pos_to_block_pos, rel_block_pos::RelBlockPos};
75

86
pub fn parkour_move(ctx: &mut PathfinderCtx, node: RelBlockPos) {
97
if !ctx.world.is_block_solid(node.down(1)) {
10-
// we can only parkour from solid blocks (not just standable blocks like slabs)
118
return;
129
}
1310

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;
3112

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+
}
4620

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()));
5422
}
23+
}
24+
}
5525

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+
}
5742

43+
if let Some((target, cost)) = find_landing_position(ctx, target_pos, distance - 1) {
5844
ctx.edges.push(Edge {
5945
movement: astar::Movement {
60-
target: pos + offset.up(ascend),
46+
target,
6147
data: MoveData {
6248
execute: &execute_parkour_move,
6349
is_reached: &parkour_is_reached,
6450
},
6551
},
6652
cost,
67-
})
53+
});
6854
}
6955
}
7056

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+
}
9771

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;
10683
}
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;
11088
}
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;
11492
}
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-
})
12693
}
94+
points
12795
}
12896

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
152121
}
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));
155137
}
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
156141
}
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-
})
178142
}
179143
}
180144

181145
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 {
198150
ctx.sprint(SprintDirection::Forward);
199151
} else {
200152
ctx.walk(WalkDirection::Forward);
201153
}
202154

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 {
237167
ctx.jump();
238168
}
239169
}
240170

241171
#[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())
257175
}

0 commit comments

Comments
 (0)