1use std::f32::consts::SQRT_2;
2
3use azalea_block::{BlockState, properties};
4use azalea_client::{SprintDirection, WalkDirection};
5use azalea_core::{
6 direction::CardinalDirection,
7 position::{BlockPos, Vec3},
8};
9
10use super::{Edge, ExecuteCtx, IsReachedCtx, MoveData, PathfinderCtx, default_is_reached};
11use crate::pathfinder::{astar, costs::*, player_pos_to_block_pos, rel_block_pos::RelBlockPos};
12
13pub fn basic_move(ctx: &mut PathfinderCtx, node: RelBlockPos) {
14 forward_move(ctx, node);
15 ascend_move(ctx, node);
16 descend_move(ctx, node);
17 diagonal_move(ctx, node);
18 descend_forward_1_move(ctx, node);
19 downward_move(ctx, node);
20}
21
22fn forward_move(ctx: &mut PathfinderCtx, pos: RelBlockPos) {
23 for dir in CardinalDirection::iter() {
24 let offset = RelBlockPos::new(dir.x(), 0, dir.z());
25
26 let mut cost = SPRINT_ONE_BLOCK_COST;
27
28 let break_cost = ctx.world.cost_for_standing(pos + offset, ctx.mining_cache);
29 if break_cost == f32::INFINITY {
30 continue;
31 }
32 cost += break_cost;
33
34 ctx.edges.push(Edge {
35 movement: astar::Movement {
36 target: pos + offset,
37 data: MoveData {
38 execute: &execute_forward_move,
39 is_reached: &default_is_reached,
40 },
41 },
42 cost,
43 })
44 }
45}
46
47fn execute_forward_move(mut ctx: ExecuteCtx) {
48 let center = ctx.target.center();
49
50 if ctx.mine_while_at_start(ctx.target.up(1)) {
51 return;
52 }
53 if ctx.mine_while_at_start(ctx.target) {
54 return;
55 }
56
57 ctx.look_at(center);
58 ctx.jump_if_in_water();
59 ctx.sprint(SprintDirection::Forward);
60}
61
62fn ascend_move(ctx: &mut PathfinderCtx, pos: RelBlockPos) {
63 let is_unusual_shape = !ctx.world.is_block_solid(pos.down(1));
67 let mut stair_facing = None;
68
69 if is_unusual_shape {
70 let block_below = ctx.world.get_block_state(pos.down(1));
73
74 let Some(found_stair_facing) = validate_stair_and_get_facing(block_below) else {
75 return;
78 };
79
80 stair_facing = Some(found_stair_facing);
81 }
82
83 for dir in CardinalDirection::iter() {
84 if let Some(stair_facing) = stair_facing {
85 let expected_stair_facing = cardinal_direction_to_facing_property(dir);
86 if stair_facing != expected_stair_facing {
87 continue;
88 }
89 }
90
91 let offset = RelBlockPos::new(dir.x(), 1, dir.z());
92
93 let break_cost_1 = ctx
94 .world
95 .cost_for_breaking_block(pos.up(2), ctx.mining_cache);
96 if break_cost_1 == f32::INFINITY {
97 continue;
98 }
99 let break_cost_2 = ctx.world.cost_for_standing(pos + offset, ctx.mining_cache);
100 if break_cost_2 == f32::INFINITY {
101 continue;
102 }
103
104 let cost = SPRINT_ONE_BLOCK_COST
105 + JUMP_PENALTY
106 + *JUMP_ONE_BLOCK_COST
107 + break_cost_1
108 + break_cost_2;
109
110 ctx.edges.push(Edge {
111 movement: astar::Movement {
112 target: pos + offset,
113 data: MoveData {
114 execute: &execute_ascend_move,
115 is_reached: &ascend_is_reached,
116 },
117 },
118 cost,
119 })
120 }
121}
122fn execute_ascend_move(mut ctx: ExecuteCtx) {
123 let ExecuteCtx {
124 target,
125 start,
126 position,
127 physics,
128 ..
129 } = ctx;
130
131 if ctx.mine_while_at_start(start.up(2)) {
132 return;
133 }
134 if ctx.mine_while_at_start(target) {
135 return;
136 }
137 if ctx.mine_while_at_start(target.up(1)) {
138 return;
139 }
140
141 let target_center = target.center();
142
143 ctx.look_at(target_center);
144 ctx.walk(WalkDirection::Forward);
145 ctx.jump_if_in_water();
146
147 let x_axis = target.x - start.x; let z_axis = target.z - start.z; let x_axis_abs = x_axis.abs(); let z_axis_abs = z_axis.abs(); let flat_distance_to_next = x_axis_abs as f64 * (target_center.x - position.x)
157 + z_axis_abs as f64 * (target_center.z - position.z);
158 let side_distance = z_axis_abs as f64 * (target_center.x - position.x).abs()
159 + x_axis_abs as f64 * (target_center.z - position.z).abs();
160
161 let lateral_motion =
162 x_axis_abs as f64 * physics.velocity.z + z_axis_abs as f64 * physics.velocity.x;
163 if lateral_motion.abs() > 0.1 {
164 return;
165 }
166
167 if flat_distance_to_next > 1.2 || side_distance > 0.2 {
168 return;
169 }
170
171 let block_below_target = ctx.get_block_state(target.down(1));
174 if let Some(stair_facing) = validate_stair_and_get_facing(block_below_target) {
175 let expected_stair_facing = match (x_axis, z_axis) {
176 (0, 1) => Some(properties::FacingCardinal::North),
177 (1, 0) => Some(properties::FacingCardinal::East),
178 (0, -1) => Some(properties::FacingCardinal::South),
179 (-1, 0) => Some(properties::FacingCardinal::West),
180 _ => None,
181 };
182 if let Some(expected_stair_facing) = expected_stair_facing
183 && stair_facing == expected_stair_facing
184 {
185 return;
186 }
187 }
188
189 if player_pos_to_block_pos(position) == start {
190 if target.y as f64 - position.y > 0.5 {
192 ctx.jump();
193 }
194 }
195}
196#[must_use]
197pub fn ascend_is_reached(
198 IsReachedCtx {
199 position, target, ..
200 }: IsReachedCtx,
201) -> bool {
202 BlockPos::from(position) == target || BlockPos::from(position) == target.down(1)
203}
204
205fn validate_stair_and_get_facing(block_state: BlockState) -> Option<properties::FacingCardinal> {
206 let top_bottom = block_state.property::<properties::TopBottom>();
207 if top_bottom != Some(properties::TopBottom::Bottom) {
208 return None;
209 }
210
211 block_state.property::<properties::FacingCardinal>()
212}
213fn cardinal_direction_to_facing_property(dir: CardinalDirection) -> properties::FacingCardinal {
214 match dir {
215 CardinalDirection::North => properties::FacingCardinal::North,
216 CardinalDirection::East => properties::FacingCardinal::East,
217 CardinalDirection::South => properties::FacingCardinal::South,
218 CardinalDirection::West => properties::FacingCardinal::West,
219 }
220}
221
222fn descend_move(ctx: &mut PathfinderCtx, pos: RelBlockPos) {
223 for dir in CardinalDirection::iter() {
224 let dir_delta = RelBlockPos::new(dir.x(), 0, dir.z());
225 let new_horizontal_position = pos + dir_delta;
226
227 let break_cost_1 = ctx
228 .world
229 .cost_for_passing(new_horizontal_position, ctx.mining_cache);
230 if break_cost_1 == f32::INFINITY {
231 continue;
232 }
233
234 let mut fall_distance = ctx.world.fall_distance(new_horizontal_position);
235 if fall_distance > 3 {
236 continue;
237 }
238
239 if fall_distance == 0 {
240 fall_distance = 1
242 }
243
244 let new_position = new_horizontal_position.down(fall_distance as i32);
245
246 let break_cost_2;
248 if fall_distance == 1 {
249 break_cost_2 = ctx.world.cost_for_standing(new_position, ctx.mining_cache);
250 if break_cost_2 == f32::INFINITY {
251 continue;
252 }
253 } else {
254 if !ctx.world.is_standable(new_position) {
256 continue;
257 }
258 break_cost_2 = 0.;
259 }
260
261 let cost = WALK_OFF_BLOCK_COST
262 + f32::max(
263 FALL_N_BLOCKS_COST
264 .get(fall_distance as usize)
265 .copied()
266 .unwrap_or(f32::INFINITY),
269 CENTER_AFTER_FALL_COST,
270 )
271 + break_cost_1
272 + break_cost_2;
273
274 ctx.edges.push(Edge {
275 movement: astar::Movement {
276 target: new_position,
277 data: MoveData {
278 execute: &execute_descend_move,
279 is_reached: &descend_is_reached,
280 },
281 },
282 cost,
283 })
284 }
285}
286fn execute_descend_move(mut ctx: ExecuteCtx) {
287 let ExecuteCtx {
288 target,
289 start,
290 position,
291 ..
292 } = ctx;
293
294 for i in (0..=(start.y - target.y + 1)).rev() {
295 if ctx.mine_while_at_start(target.up(i)) {
296 return;
297 }
298 }
299
300 let start_center = start.center();
301 let center = target.center();
302
303 let horizontal_distance_from_target = (center - position).horizontal_distance_squared().sqrt();
304 let horizontal_distance_from_start = (start.center() - position)
305 .horizontal_distance_squared()
306 .sqrt();
307
308 let dest_ahead = Vec3::new(
309 start_center.x + (center.x - start_center.x) * 1.5,
310 center.y,
311 start_center.z + (center.z - start_center.z) * 1.5,
312 );
313
314 if player_pos_to_block_pos(position) != target || horizontal_distance_from_target > 0.25 {
315 if horizontal_distance_from_start < 1.25 {
316 ctx.look_at(dest_ahead);
318 ctx.walk(WalkDirection::Forward);
319 } else {
320 ctx.look_at(center);
321 ctx.walk(WalkDirection::Forward);
322 }
323 } else {
324 ctx.walk(WalkDirection::None);
325 }
326}
327#[must_use]
328pub fn descend_is_reached(
329 IsReachedCtx {
330 target,
331 start,
332 position,
333 physics,
334 ..
335 }: IsReachedCtx,
336) -> bool {
337 let dest_ahead = BlockPos::new(
338 start.x + (target.x - start.x) * 2,
339 target.y,
340 start.z + (target.z - start.z) * 2,
341 );
342
343 if player_pos_to_block_pos(position) == target
344 || player_pos_to_block_pos(position) == dest_ahead
345 {
346 if (position.y - target.y as f64) < 0.5 {
347 return true;
348 }
349 } else if player_pos_to_block_pos(position).up(1) == target && physics.on_ground() {
350 return true;
351 }
352 false
353}
354
355fn descend_forward_1_move(ctx: &mut PathfinderCtx, pos: RelBlockPos) {
356 for dir in CardinalDirection::iter() {
357 let dir_delta = RelBlockPos::new(dir.x(), 0, dir.z());
358 let gap_horizontal_position = pos + dir_delta;
359 let new_horizontal_position = pos + dir_delta * 2;
360
361 let gap_fall_distance = ctx.world.fall_distance(gap_horizontal_position);
362 let fall_distance = ctx.world.fall_distance(new_horizontal_position);
363
364 if fall_distance == 0 || fall_distance > 3 || gap_fall_distance < fall_distance {
365 continue;
366 }
367
368 let new_position = new_horizontal_position.down(fall_distance as i32);
369
370 if !ctx.world.is_passable(new_horizontal_position) {
372 continue;
373 }
374 if !ctx.world.is_passable(gap_horizontal_position) {
375 continue;
376 }
377 if !ctx.world.is_standable(new_position) {
379 continue;
380 }
381
382 let cost = WALK_OFF_BLOCK_COST
383 + WALK_ONE_BLOCK_COST
384 + f32::max(
385 FALL_N_BLOCKS_COST
386 .get(fall_distance as usize)
387 .copied()
388 .unwrap_or(f32::INFINITY),
391 CENTER_AFTER_FALL_COST,
392 );
393
394 ctx.edges.push(Edge {
395 movement: astar::Movement {
396 target: new_position,
397 data: MoveData {
398 execute: &execute_descend_move,
399 is_reached: &descend_is_reached,
400 },
401 },
402 cost,
403 })
404 }
405}
406
407fn diagonal_move(ctx: &mut PathfinderCtx, pos: RelBlockPos) {
408 for dir in CardinalDirection::iter() {
409 let right = dir.right();
410 let offset = RelBlockPos::new(dir.x() + right.x(), 0, dir.z() + right.z());
411 let left_pos = RelBlockPos::new(pos.x + dir.x(), pos.y, pos.z + dir.z());
412 let right_pos = RelBlockPos::new(pos.x + right.x(), pos.y, pos.z + right.z());
413
414 let mut cost = SPRINT_ONE_BLOCK_COST * SQRT_2 + 0.001;
416
417 let left_passable = ctx.world.is_passable(left_pos);
418 let right_passable = ctx.world.is_passable(right_pos);
419
420 if !left_passable && !right_passable {
421 continue;
422 }
423
424 if !left_passable || !right_passable {
425 cost += WALK_ONE_BLOCK_COST / 2.;
427 }
428
429 if !ctx.world.is_standable(pos + offset) {
430 continue;
431 }
432
433 ctx.edges.push(Edge {
434 movement: astar::Movement {
435 target: pos + offset,
436 data: MoveData {
437 execute: &execute_diagonal_move,
438 is_reached: &default_is_reached,
439 },
440 },
441 cost,
442 })
443 }
444}
445fn execute_diagonal_move(mut ctx: ExecuteCtx) {
446 let target_center = ctx.target.center();
447
448 ctx.look_at(target_center);
449 ctx.sprint(SprintDirection::Forward);
450 ctx.jump_if_in_water();
451}
452
453fn downward_move(ctx: &mut PathfinderCtx, pos: RelBlockPos) {
455 if !ctx.world.is_block_solid(pos.down(2)) {
457 return;
458 }
459
460 let break_cost = ctx
461 .world
462 .cost_for_breaking_block(pos.down(1), ctx.mining_cache);
463 if break_cost == f32::INFINITY {
464 return;
465 }
466
467 let cost = FALL_N_BLOCKS_COST[1] + break_cost;
468
469 ctx.edges.push(Edge {
470 movement: astar::Movement {
471 target: pos.down(1),
472 data: MoveData {
473 execute: &execute_downward_move,
474 is_reached: &default_is_reached,
475 },
476 },
477 cost,
478 })
479}
480fn execute_downward_move(mut ctx: ExecuteCtx) {
481 let ExecuteCtx {
482 target, position, ..
483 } = ctx;
484
485 let target_center = target.center();
486
487 let horizontal_distance_from_target = (target_center - position)
488 .horizontal_distance_squared()
489 .sqrt();
490
491 if horizontal_distance_from_target > 0.25 {
492 ctx.look_at(target_center);
493 ctx.walk(WalkDirection::Forward);
494 } else if ctx.mine_while_at_start(target) {
495 ctx.walk(WalkDirection::None);
496 } else if BlockPos::from(position) != target {
497 ctx.look_at(target_center);
498 ctx.walk(WalkDirection::Forward);
499 } else {
500 ctx.walk(WalkDirection::None);
501 }
502}