From b6358463440abb101303712d8588a710294d3275 Mon Sep 17 00:00:00 2001 From: mat Date: Sat, 30 Sep 2023 18:17:30 -0500 Subject: [PATCH] start implementing parkour --- azalea/src/pathfinder/astar.rs | 15 +- azalea/src/pathfinder/mod.rs | 11 +- azalea/src/pathfinder/moves/basic.rs | 3 +- azalea/src/pathfinder/moves/mod.rs | 8 + azalea/src/pathfinder/moves/parkour.rs | 233 +++++++++++++++++++++++++ 5 files changed, 257 insertions(+), 13 deletions(-) create mode 100644 azalea/src/pathfinder/moves/parkour.rs diff --git a/azalea/src/pathfinder/astar.rs b/azalea/src/pathfinder/astar.rs index d4812e9b1..e55425314 100644 --- a/azalea/src/pathfinder/astar.rs +++ b/azalea/src/pathfinder/astar.rs @@ -106,7 +106,7 @@ where } } - let best_path = determine_best_path(&best_paths, &heuristic); + let best_path = determine_best_path(&best_paths, &start); Path { movements: reconstruct_path(nodes, best_path), @@ -114,23 +114,20 @@ where } } -const MIN_DISTANCE_PATH: f32 = 5.; - -fn determine_best_path(best_node: &[P; 7], heuristic: &HeuristicFn) -> P +fn determine_best_path

(best_paths: &[P; 7], start: &P) -> P where - HeuristicFn: Fn(P) -> f32, P: Eq + Hash + Copy + Debug, { // this basically makes sure we don't create a path that's really short - for node in best_node.iter() { - // square MIN_DISTANCE_PATH because we're comparing squared distances - if heuristic(*node) > MIN_DISTANCE_PATH * MIN_DISTANCE_PATH { + for node in best_paths.iter() { + if node != start { + println!("chose best node {:?}", node); return *node; } } warn!("No best node found, returning first node"); - best_node[0] + best_paths[0] } fn reconstruct_path(mut nodes: HashMap>, current: P) -> Vec> diff --git a/azalea/src/pathfinder/mod.rs b/azalea/src/pathfinder/mod.rs index 9ecc60931..b8a027987 100644 --- a/azalea/src/pathfinder/mod.rs +++ b/azalea/src/pathfinder/mod.rs @@ -86,7 +86,7 @@ pub struct GotoEvent { pub entity: Entity, pub goal: Arc, /// The function that's used for checking what moves are possible. Usually - /// `pathfinder::moves::basic::basic_move` + /// `pathfinder::moves::default_move` pub successors_fn: SuccessorsFn, } #[derive(Event)] @@ -124,7 +124,7 @@ impl PathfinderClientExt for azalea_client::Client { self.ecs.lock().send_event(GotoEvent { entity: self.entity, goal: Arc::new(goal), - successors_fn: moves::basic::basic_move, + successors_fn: moves::default_move, }); } } @@ -370,7 +370,12 @@ fn tick_execute_path( position: **position, physics, }; - if (movement.data.is_reached)(is_reached_ctx) { + let on_ground_if_last = if i == pathfinder.path.len() - 1 { + physics.on_ground + } else { + true + }; + if (movement.data.is_reached)(is_reached_ctx) && on_ground_if_last { pathfinder.path = pathfinder.path.split_off(i + 1); pathfinder.last_reached_node = Some(movement.target); pathfinder.last_node_reached_at = Some(Instant::now()); diff --git a/azalea/src/pathfinder/moves/basic.rs b/azalea/src/pathfinder/moves/basic.rs index af940dc29..b8cbbbb76 100644 --- a/azalea/src/pathfinder/moves/basic.rs +++ b/azalea/src/pathfinder/moves/basic.rs @@ -268,7 +268,8 @@ fn diagonal_move(world: &Instance, pos: BlockPos) -> Vec { if !is_standable(&(pos + offset), world) { continue; } - let cost = SPRINT_ONE_BLOCK_COST * SQRT_2; + // +0.001 so it doesn't unnecessarily go diagonal sometimes + let cost = SPRINT_ONE_BLOCK_COST * SQRT_2 + 0.001; edges.push(Edge { movement: astar::Movement { diff --git a/azalea/src/pathfinder/moves/mod.rs b/azalea/src/pathfinder/moves/mod.rs index 01df09297..61dc8b685 100644 --- a/azalea/src/pathfinder/moves/mod.rs +++ b/azalea/src/pathfinder/moves/mod.rs @@ -1,4 +1,5 @@ pub mod basic; +pub mod parkour; use std::fmt::Debug; @@ -114,6 +115,13 @@ pub struct IsReachedCtx<'a> { pub physics: &'a azalea_entity::Physics, } +pub fn default_move(world: &Instance, node: BlockPos) -> Vec { + let mut edges = Vec::new(); + edges.extend(basic::basic_move(world, node)); + edges.extend(parkour::parkour_move(world, node)); + edges +} + /// Returns whether the entity is at the node and should start going to the /// next node. #[must_use] diff --git a/azalea/src/pathfinder/moves/parkour.rs b/azalea/src/pathfinder/moves/parkour.rs new file mode 100644 index 000000000..2a2f132f5 --- /dev/null +++ b/azalea/src/pathfinder/moves/parkour.rs @@ -0,0 +1,233 @@ +use azalea_client::{SprintDirection, StartSprintEvent, StartWalkEvent, WalkDirection}; +use azalea_core::{BlockPos, CardinalDirection}; +use azalea_world::Instance; + +use crate::{ + pathfinder::{astar, costs::*}, + JumpEvent, LookAtEvent, +}; + +use super::{ + default_is_reached, is_block_passable, is_block_solid, is_passable, is_standable, Edge, + ExecuteCtx, MoveData, +}; + +pub fn parkour_move(world: &Instance, node: BlockPos) -> Vec { + let mut edges = Vec::new(); + edges.extend(parkour_forward_1_move(world, node)); + edges.extend(parkour_headhitter_forward_1_move(world, node)); + edges.extend(parkour_forward_2_move(world, node)); + edges +} + +fn parkour_forward_1_move(world: &Instance, pos: BlockPos) -> Vec { + let mut edges = Vec::new(); + for dir in CardinalDirection::iter() { + let gap_offset = BlockPos::new(dir.x() * 1, 0, dir.z() * 1); + let offset = BlockPos::new(dir.x() * 2, 0, dir.z() * 2); + + if !is_standable(&(pos + offset), world) { + continue; + } + if !is_passable(&(pos + gap_offset), world) { + continue; + } + if !is_block_passable(&(pos + gap_offset).up(2), world) { + continue; + } + // make sure we actually have to jump + if is_block_solid(&(pos + gap_offset).down(1), world) { + continue; + } + // make sure it's not a headhitter + if !is_block_passable(&pos.up(2), world) { + continue; + } + + let cost = *JUMP_ONE_BLOCK_COST + SPRINT_ONE_BLOCK_COST + SPRINT_ONE_BLOCK_COST; + + edges.push(Edge { + movement: astar::Movement { + target: pos + offset, + data: MoveData { + execute: &execute_parkour_move, + is_reached: &default_is_reached, + }, + }, + cost, + }) + } + + edges +} + +fn parkour_forward_2_move(world: &Instance, pos: BlockPos) -> Vec { + let mut edges = Vec::new(); + for dir in CardinalDirection::iter() { + let gap_1_offset = BlockPos::new(dir.x() * 1, 0, dir.z() * 1); + let gap_2_offset = BlockPos::new(dir.x() * 2, 0, dir.z() * 2); + let offset = BlockPos::new(dir.x() * 3, 0, dir.z() * 3); + + if !is_standable(&(pos + offset), world) { + continue; + } + if !is_passable(&(pos + gap_1_offset), world) { + continue; + } + if !is_block_passable(&(pos + gap_1_offset).up(2), world) { + continue; + } + if !is_passable(&(pos + gap_2_offset), world) { + continue; + } + if !is_block_passable(&(pos + gap_2_offset).up(2), world) { + continue; + } + // make sure we actually have to jump + if is_block_solid(&(pos + gap_1_offset).down(1), world) { + continue; + } + // make sure it's not a headhitter + if !is_block_passable(&pos.up(2), world) { + continue; + } + + let cost = *JUMP_ONE_BLOCK_COST + + SPRINT_ONE_BLOCK_COST + + SPRINT_ONE_BLOCK_COST + + SPRINT_ONE_BLOCK_COST; + + edges.push(Edge { + movement: astar::Movement { + target: pos + offset, + data: MoveData { + execute: &execute_parkour_move, + is_reached: &default_is_reached, + }, + }, + cost, + }) + } + + edges +} + +fn parkour_headhitter_forward_1_move(world: &Instance, pos: BlockPos) -> Vec { + let mut edges = Vec::new(); + for dir in CardinalDirection::iter() { + let gap_offset = BlockPos::new(dir.x() * 1, 0, dir.z() * 1); + let offset = BlockPos::new(dir.x() * 2, 0, dir.z() * 2); + + if !is_standable(&(pos + offset), world) { + continue; + } + if !is_passable(&(pos + gap_offset), world) { + continue; + } + if !is_block_passable(&(pos + gap_offset).up(2), world) { + continue; + } + // make sure we actually have to jump + if is_block_solid(&(pos + gap_offset).down(1), world) { + continue; + } + // make sure it is a headhitter + if !is_block_solid(&pos.up(2), world) { + continue; + } + + let cost = *JUMP_ONE_BLOCK_COST + WALK_ONE_BLOCK_COST + WALK_ONE_BLOCK_COST; + + edges.push(Edge { + movement: astar::Movement { + target: pos + offset, + data: MoveData { + execute: &execute_headhitter_parkour_move, + is_reached: &default_is_reached, + }, + }, + cost, + }) + } + + edges +} + +fn execute_parkour_move( + ExecuteCtx { + entity, + target, + start, + look_at_events, + sprint_events, + walk_events, + jump_events, + .. + }: ExecuteCtx, +) { + let center = target.center(); + look_at_events.send(LookAtEvent { + entity, + position: center, + }); + + let jump_distance = i32::max((target - start).x.abs(), (target - start).z.abs()); + + if jump_distance > 2 { + sprint_events.send(StartSprintEvent { + entity, + direction: SprintDirection::Forward, + }); + } else { + walk_events.send(StartWalkEvent { + entity, + direction: WalkDirection::Forward, + }); + } + + jump_events.send(JumpEvent { entity }); +} + +fn execute_headhitter_parkour_move( + ExecuteCtx { + entity, + target, + start, + position, + look_at_events, + sprint_events, + walk_events, + jump_events, + .. + }: ExecuteCtx, +) { + let center = target.center(); + look_at_events.send(LookAtEvent { + entity, + position: center, + }); + + let jump_distance = i32::max((target - start).x.abs(), (target - start).z.abs()); + + if jump_distance > 2 { + sprint_events.send(StartSprintEvent { + entity, + direction: SprintDirection::Forward, + }); + } else { + walk_events.send(StartWalkEvent { + entity, + direction: WalkDirection::Forward, + }); + } + + let start_center = start.center(); + let distance_from_start = f64::max( + (start_center.x as f64 - position.x).abs(), + (start_center.z as f64 - position.z).abs(), + ); + + if distance_from_start > 0.75 { + jump_events.send(JumpEvent { entity }); + } +}