use azalea_client::{SprintDirection, WalkDirection}; use azalea_core::{direction::CardinalDirection, position::BlockPos}; use azalea_physics::collision::BlockWithShape; use tracing::trace; use super::{Edge, ExecuteCtx, IsReachedCtx, MoveData, MovesCtx}; use crate::pathfinder::{astar, costs::*, player_pos_to_block_pos, positions::RelBlockPos}; pub fn parkour_move(ctx: &mut MovesCtx, node: RelBlockPos) { if !ctx.world.is_block_solid(node.down(1)) { // we can only parkour from solid blocks (not just standable blocks like slabs) return; } parkour_forward_1_move(ctx, node); parkour_forward_2_move(ctx, node); parkour_forward_3_move(ctx, node); } fn parkour_forward_1_move(ctx: &mut MovesCtx, pos: RelBlockPos) { for dir in CardinalDirection::iter() { let gap_offset = RelBlockPos::new(dir.x(), 0, dir.z()); let offset = RelBlockPos::new(dir.x() * 2, 0, dir.z() * 2); // make sure we actually have to jump if ctx.world.is_block_standable((pos + gap_offset).down(1)) { continue; } if !ctx.world.is_passable(pos + gap_offset) { continue; } let ascend: i32 = if ctx.world.is_standable(pos + offset.up(1)) { // ascend 1 } else if ctx.world.is_standable(pos + offset) { // forward // no FALL_N_BLOCKS_COST[1] added here because we mostly don't have to wait for // falling 0 } else { continue; }; // make sure we have space to jump if !ctx.world.is_block_passable((pos + gap_offset).up(2)) { continue; } // make sure there's not a block above us if !ctx.world.is_block_passable(pos.up(2)) { continue; } // make sure there's not a block above the target if !ctx.world.is_block_passable((pos + offset).up(2)) { continue; } let cost = JUMP_PENALTY + WALK_ONE_BLOCK_COST * 2.; ctx.edges.push(Edge { movement: astar::Movement { target: pos + offset.up(ascend), data: MoveData { execute: &execute_parkour_move, is_reached: &parkour_is_reached, }, }, cost, }) } } fn parkour_forward_2_move(ctx: &mut MovesCtx, pos: RelBlockPos) { 'dir: for dir in CardinalDirection::iter() { let gap_1_offset = RelBlockPos::new(dir.x(), 0, dir.z()); let gap_2_offset = RelBlockPos::new(dir.x() * 2, 0, dir.z() * 2); let offset = RelBlockPos::new(dir.x() * 3, 0, dir.z() * 3); // make sure we actually have to jump if ctx.world.is_block_standable((pos + gap_1_offset).down(1)) || ctx.world.is_block_standable((pos + gap_2_offset).down(1)) { continue; } let mut cost = JUMP_PENALTY + WALK_ONE_BLOCK_COST * 3.; let ascend: i32 = if ctx.world.is_standable(pos + offset.up(1)) { 1 } else if ctx.world.is_standable(pos + offset) { 0 } else if ctx.world.is_standable(pos + offset.down(1)) { // we mostly don't really wait for falling during parkour except here cost += FALL_N_BLOCKS_COST[2] / 2.; -1 } else { continue; }; // make sure we have space to jump for offset in [gap_1_offset, gap_2_offset] { if !ctx.world.is_passable(pos + offset) { continue 'dir; } if !ctx.world.is_block_passable((pos + offset).up(2)) { continue 'dir; } } // make sure there's not a block above us if !ctx.world.is_block_passable(pos.up(2)) { continue; } // make sure there's not a block above the target if !ctx.world.is_block_passable((pos + offset).up(2)) { continue; } ctx.edges.push(Edge { movement: astar::Movement { target: pos + offset.up(ascend), data: MoveData { execute: &execute_parkour_move, is_reached: &parkour_is_reached, }, }, cost, }) } } fn parkour_forward_3_move(ctx: &mut MovesCtx, pos: RelBlockPos) { 'dir: for dir in CardinalDirection::iter() { let gap_1_offset = RelBlockPos::new(dir.x(), 0, dir.z()); let gap_2_offset = RelBlockPos::new(dir.x() * 2, 0, dir.z() * 2); let gap_3_offset = RelBlockPos::new(dir.x() * 3, 0, dir.z() * 3); let offset = RelBlockPos::new(dir.x() * 4, 0, dir.z() * 4); // make sure we actually have to jump if ctx.world.is_block_standable((pos + gap_1_offset).down(1)) || ctx.world.is_block_standable((pos + gap_2_offset).down(1)) || ctx.world.is_block_standable((pos + gap_3_offset).down(1)) { continue; } if !ctx.world.is_standable(pos + offset) { continue; }; // make sure we have space to jump for offset in [gap_1_offset, gap_2_offset, gap_3_offset] { if !ctx.world.is_passable(pos + offset) { continue 'dir; } if !ctx.world.is_block_passable((pos + offset).up(2)) { continue 'dir; } } // make sure there's not a block above us if !ctx.world.is_block_passable(pos.up(2)) { continue; } // make sure there's not a block above the target if !ctx.world.is_block_passable((pos + offset).up(2)) { continue; } let cost = JUMP_PENALTY + SPRINT_ONE_BLOCK_COST * 4.; ctx.edges.push(Edge { movement: astar::Movement { target: pos + offset, data: MoveData { execute: &execute_parkour_move, is_reached: &parkour_is_reached, }, }, cost, }) } } fn execute_parkour_move(mut ctx: ExecuteCtx) { let ExecuteCtx { position, target, start, .. } = ctx; let start_center = start.center(); let target_center = target.center(); let jump_distance = i32::max((target - start).x.abs(), (target - start).z.abs()); let ascend: i32 = target.y - start.y; if jump_distance >= 4 || (ascend > 0 && jump_distance >= 3) { // 3 block gap OR 2 block gap with ascend ctx.sprint(SprintDirection::Forward); } else { ctx.walk(WalkDirection::Forward); } let x_dir = (target.x - start.x).clamp(-1, 1); let z_dir = (target.z - start.z).clamp(-1, 1); let dir = BlockPos::new(x_dir, 0, z_dir); let jump_at_pos = start + dir; let is_at_start_block = player_pos_to_block_pos(position) == start; let is_at_jump_block = player_pos_to_block_pos(position) == jump_at_pos; let required_distance_from_center = if jump_distance <= 2 { // 1 block gap 0.0 } else { 0.6 }; let distance_from_start = f64::max( (position.x - start_center.x).abs(), (position.z - start_center.z).abs(), ); if !is_at_start_block && !is_at_jump_block && (position.y - start.y as f64) < 0.094 && distance_from_start < 0.85 { // we have to be on the start block to jump ctx.look_at(start_center); trace!("looking at start_center"); } else { ctx.look_at(target_center); trace!("looking at target_center"); // it's possible to hit our heads on a block when doing certain jumps (which // resets our horizontal velocity), but we can avoid that by sneaking if !ctx.physics.on_ground() && ctx.physics.velocity.y.abs() < 0.25 { let should_sneak = { let world = ctx.world.read(); let pos_above = ctx.position.up(1.8 + 0.25); let block_pos_above = BlockPos::from(pos_above); let block_pos_above_plus_velocity = BlockPos::from(pos_above + ctx.physics.velocity.with_y(0.) * 5.); let block_above = world.get_block_state(block_pos_above).unwrap_or_default(); let block_above_plus_velocity = world .get_block_state(block_pos_above_plus_velocity) .unwrap_or_default(); // these checks are overly lenient but it doesn't matter much !block_above.is_collision_shape_full() && !block_above_plus_velocity.is_collision_shape_empty() }; if should_sneak { ctx.sneak(); } } } if !is_at_start_block && is_at_jump_block && distance_from_start > required_distance_from_center { ctx.jump(); } } #[must_use] pub fn parkour_is_reached( IsReachedCtx { position, target, physics, .. }: IsReachedCtx, ) -> bool { // 0.094 and not 0 for lilypads if player_pos_to_block_pos(position) == target && (position.y - target.y as f64) < 0.094 { return true; } // this is to make it handle things like slabs correctly player_pos_to_block_pos(position) == target && physics.on_ground() }