blackout/src/pathfinding.rs

339 lines
11 KiB
Rust

use avian2d::prelude::*;
use bevy::{prelude::*, utils::HashMap};
use leafwing_input_manager::{plugin::InputManagerSystem, prelude::*};
use pathfinding::prelude::*;
use crate::{
core::{PointLike, TransformExt},
map::Map,
navigation::{NavigationAction, RotationSpeed, Speed},
};
#[derive(PartialEq, Eq, Clone, Copy, Hash, Debug, Reflect)]
pub struct NegotiatePathAction;
impl Actionlike for NegotiatePathAction {
fn input_control_kind(&self) -> InputControlKind {
InputControlKind::DualAxis
}
}
#[derive(Component, Clone, Copy, Debug, Default, Deref, DerefMut, Eq, Hash, PartialEq, Reflect)]
#[reflect(Component)]
pub struct Destination(pub (i32, i32));
impl_pointlike_for_tuple_component!(Destination);
impl_pointlike_for_tuple_component!(&Destination);
#[derive(Component, Clone, Debug, Default, Reflect)]
#[reflect(Component)]
pub struct NoPath;
#[derive(Component, Clone, Debug, Default, Deref, DerefMut, Reflect)]
#[reflect(Component)]
pub struct Path(pub Vec<(i32, i32)>);
#[derive(Component, Clone, Debug, Default, Reflect, Deref, DerefMut)]
#[reflect(Component)]
pub struct CostMap(pub HashMap<(i32, i32), f32>);
#[derive(Bundle, Deref, DerefMut)]
pub struct PathfindingControlsBundle(pub ActionState<NegotiatePathAction>);
impl Default for PathfindingControlsBundle {
fn default() -> Self {
let mut input: ActionState<NegotiatePathAction> = Default::default();
input.disable();
Self(input)
}
}
pub fn find_path<D: 'static + Clone + Default + Send + Sync>(
start: &dyn PointLike,
destination: &dyn PointLike,
map: &Map<D>,
cost_map: &Option<CostMap>,
) -> Option<(Vec<(i32, i32)>, u32)> {
astar(
&start.into(),
|p| {
let mut successors: Vec<((i32, i32), u32)> = vec![];
if p.0 >= 0 && p.1 >= 0 {
if let Some(tile) = map.at(p.0 as usize, p.1 as usize) {
if tile.is_walkable() {
for tile in map.get_available_exits(p.0 as usize, p.1 as usize) {
let mut cost = tile.2 * 100.;
if let Some(cost_map) = cost_map {
if let Some(modifier) =
cost_map.get(&(tile.0 as i32, tile.1 as i32))
{
cost *= modifier;
}
}
successors.push(((tile.0 as i32, tile.1 as i32), cost as u32));
}
}
}
}
successors
},
|p| (p.distance_squared(destination) * 100.) as u32,
|p| *p == destination.into(),
)
}
fn calculate_path(
mut commands: Commands,
spatial_query: SpatialQuery,
mut query: Query<
(
Entity,
&Destination,
&Transform,
&Collider,
Option<&CostMap>,
Option<&mut ActionState<NegotiatePathAction>>,
),
Changed<Destination>,
>,
) {
for (entity, destination, start, collider, cost_map, actions) in &mut query {
trace!("{entity}: destination: {destination:?}");
if start.i32() == **destination {
trace!("{entity}: remove1");
commands
.entity(entity)
.remove::<Path>()
.remove::<NoPath>()
.remove::<Destination>();
continue;
}
trace!(
"{entity:?}: Calculating path from {:?} to {destination:?}",
start.translation.truncate().i32()
);
trace!(
"{entity:?}: path: calculating from {:?} to {destination:?}",
start.i32(),
);
commands.entity(entity).remove::<Path>().remove::<NoPath>();
let path = astar(
&start.i32(),
|p| {
let mut successors: Vec<((i32, i32), u32)> = vec![];
let x = p.0;
let y = p.1;
let exits = vec![
((x - 1, y), 1.),
((x + 1, y), 1.),
((x, y - 1), 1.),
((x, y + 1), 1.),
((x - 1, y - 1), 1.5),
((x + 1, y - 1), 1.5),
((x - 1, y + 1), 1.5),
((x + 1, y + 1), 1.5),
];
for exit in &exits {
let mut should_push = true;
if !spatial_query
.shape_intersections(
collider,
start.translation.truncate(),
start.yaw().radians(),
SpatialQueryFilter::default(),
)
.is_empty()
{
should_push = false;
}
if should_push {
let mut cost = exit.1 * 100.;
if let Some(cost_map) = cost_map {
if let Some(modifier) = cost_map.get(&(exit.0 .0, exit.0 .1)) {
cost *= modifier;
}
}
successors.push((exit.0, cost as u32));
}
}
successors
},
|p| (p.distance_squared(&destination) * 100.) as u32,
|p| *p == destination.i32(),
);
if let Some(path) = path {
commands.entity(entity).insert(Path(path.0));
} else {
commands.entity(entity).insert(NoPath);
if let Some(mut actions) = actions {
trace!("{entity:?}: Disabling and resetting because no path");
actions.disable();
actions.reset_all();
}
}
}
}
fn remove_destination(mut commands: Commands, mut removed: RemovedComponents<Destination>) {
for entity in removed.read() {
if let Some(mut commands) = commands.get_entity(entity) {
commands.remove::<Path>().remove::<NoPath>();
}
}
}
fn negotiate_path(
mut commands: Commands,
time: Res<Time>,
spatial_query: SpatialQuery,
mut query: Query<(
Entity,
Option<&mut ActionState<NegotiatePathAction>>,
&mut ActionState<NavigationAction>,
&mut Path,
&mut Transform,
&Collider,
&Speed,
Option<&RotationSpeed>,
)>,
) {
for (
entity,
actions,
mut navigation_actions,
mut path,
mut transform,
collider,
speed,
rotation_speed,
) in &mut query
{
trace!("{entity:?}: negotiating path");
let start_i32 = transform.translation.truncate().i32();
trace!(
"{entity:?}: start pathfinding from {start_i32:?} to {:?}: at {:?}",
path.last(),
transform.translation.truncate().i32()
);
if path.len() > 0 && path[0] == start_i32 {
trace!("At start, removing");
path.remove(0);
}
if let Some(next) = path.first() {
let start = transform.translation.truncate();
let mut next = Vec2::new(next.0 as f32, next.1 as f32);
if next.x >= 0. {
next.x += 0.5;
} else {
next.x -= 0.5;
}
if next.y >= 0. {
next.y += 0.5;
} else {
next.y -= 0.5;
}
let mut direction = next - start;
direction = direction.normalize();
direction *= **speed;
direction *= time.delta_seconds();
trace!(
"{entity:?}: Direction: {direction:?}, Distance: {}",
(next - start).length()
);
if spatial_query
.cast_shape(
collider,
start,
transform.yaw().radians(),
Dir2::new_unchecked(direction.normalize()),
direction.length(),
true,
SpatialQueryFilter::default(),
)
.is_some()
{
trace!("{entity:?} is stuck");
// TODO: Remove when we have an actual character controller.
next.x = next.x.trunc();
next.y = next.y.trunc();
transform.translation = next.extend(0.);
} else {
navigation_actions.set_axis_pair(&NavigationAction::Translate, direction);
}
if rotation_speed.is_some() {
let angle = direction.y.atan2(direction.x);
transform.rotation = Quat::from_rotation_z(angle);
}
} else {
trace!("{entity:?}: empty path, cleaning");
if let Some(mut actions) = actions {
trace!("{entity:?}: Disabling pathfind because at destination");
actions.reset_all();
actions.disable();
}
commands
.entity(entity)
.remove::<Path>()
.remove::<NoPath>()
.remove::<Destination>();
trace!("{entity:?}: pathfinding: cleaned up");
}
}
}
fn actions(
mut commands: Commands,
mut query: Query<(
Entity,
&ActionState<NegotiatePathAction>,
&mut ActionState<NavigationAction>,
Option<&mut Destination>,
)>,
) {
for (entity, actions, mut navigation_action, destination) in &mut query {
if actions.action_disabled(&NegotiatePathAction) {
if destination.is_some() {
commands.entity(entity).remove::<Destination>();
}
} else {
let pair = actions.axis_pair(&NegotiatePathAction);
trace!("{entity:?}: Negotiating path to {pair:?}");
let dest = Destination(pair.xy().i32());
if let Some(mut current_dest) = destination {
trace!("Got a destination");
if *current_dest != dest {
trace
!("{entity:?}: New destination {dest:?} differs from {current_dest:?}, zeroing velocity");
navigation_action
.set_axis_pair(&NavigationAction::SetLinearVelocity, Vec2::ZERO);
*current_dest = dest;
}
} else {
trace!("{entity:?}: Adding destination, zeroing velocity");
navigation_action.set_axis_pair(&NavigationAction::SetLinearVelocity, Vec2::ZERO);
commands.entity(entity).insert(dest);
}
}
}
}
pub struct PathfindingPlugin;
impl Plugin for PathfindingPlugin {
fn build(&self, app: &mut App) {
app.add_plugins(InputManagerPlugin::<NegotiatePathAction>::default())
.register_type::<Destination>()
.register_type::<NoPath>()
.register_type::<Path>()
.register_type::<CostMap>()
.add_systems(PreUpdate, negotiate_path)
.add_systems(
PreUpdate,
(actions, calculate_path)
.chain()
.after(InputManagerSystem::Tick),
)
.add_systems(PostUpdate, remove_destination);
}
}