From 75df69a3a2bed12b427d0a43558c3e85b6478b20 Mon Sep 17 00:00:00 2001
From: Louis Capitanchik <contact@louiscap.co>
Date: Wed, 5 Oct 2022 12:09:14 +0100
Subject: [PATCH] Set up basic pathing algorithms for level maps

---
 Cargo.lock                                    |  40 ++++
 game_core/Cargo.toml                          |   1 +
 game_core/src/control/mod.rs                  |   1 +
 game_core/src/control/player.rs               |  33 ++-
 game_core/src/entities/spawner.rs             |  24 ++-
 game_core/src/lib.rs                          |   2 +
 game_core/src/world/generators/blobular.rs    |   5 +
 .../src/world/generators/drunkard_corridor.rs |   1 +
 game_core/src/world/level_map.rs              |  58 ++++-
 game_core/src/world/mod.rs                    |   1 +
 game_core/src/world/pathing.rs                | 198 ++++++++++++++++++
 11 files changed, 360 insertions(+), 4 deletions(-)
 create mode 100644 game_core/src/world/pathing.rs

diff --git a/Cargo.lock b/Cargo.lock
index 02d372e..4190e9c 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -1336,6 +1336,12 @@ version = "1.2.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "9ea835d29036a4087793836fa931b08837ad5e957da9e23886b29586fb9b6650"
 
+[[package]]
+name = "either"
+version = "1.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "90e5c1c8368803113bf0c9584fc495a58b86dc8a29edbf8fe877d21d9507e797"
+
 [[package]]
 name = "encase"
 version = "0.3.0"
@@ -1506,6 +1512,7 @@ dependencies = [
  "iyes_loopless",
  "log 0.4.17",
  "micro_musicbox",
+ "pathfinding",
  "remote_events",
  "serde",
  "serde_json",
@@ -1810,6 +1817,15 @@ dependencies = [
  "web-sys",
 ]
 
+[[package]]
+name = "integer-sqrt"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "276ec31bcb4a9ee45f58bec6f9ec700ae4cf4f4f8f2fa7e06cb406bd5ffdd770"
+dependencies = [
+ "num-traits",
+]
+
 [[package]]
 name = "interpolation"
 version = "0.2.0"
@@ -1825,6 +1841,15 @@ dependencies = [
  "libc",
 ]
 
+[[package]]
+name = "itertools"
+version = "0.10.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473"
+dependencies = [
+ "either",
+]
+
 [[package]]
 name = "itoa"
 version = "1.0.3"
@@ -2463,6 +2488,21 @@ dependencies = [
  "windows-sys",
 ]
 
+[[package]]
+name = "pathfinding"
+version = "3.0.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cb45190a18e771c500291c549959777a3be38d30113a860930bc1f2119f0cc13"
+dependencies = [
+ "fixedbitset",
+ "indexmap",
+ "integer-sqrt",
+ "itertools",
+ "num-traits",
+ "rustc-hash",
+ "thiserror",
+]
+
 [[package]]
 name = "peeking_take_while"
 version = "0.1.2"
diff --git a/game_core/Cargo.toml b/game_core/Cargo.toml
index 56d0c1e..c9948bc 100644
--- a/game_core/Cargo.toml
+++ b/game_core/Cargo.toml
@@ -22,6 +22,7 @@ iyes_loopless = "0.7.1"
 micro_musicbox = { version = "0.4.0", features = ["serde", "mp3"] }
 remote_events = { git = "https://lab.lcr.gr/microhacks/micro-bevy-remote-events.git", rev = "be0c6b43a73e4c5e7ece20797e3d6f59340147b4"}
 bevy_tweening = "0.5.0"
+pathfinding = "3.0.14"
 
 [target.'cfg(target_arch = "wasm32")'.dependencies]
 web-sys = { version = "0.3.60", features = ["Window"] }
diff --git a/game_core/src/control/mod.rs b/game_core/src/control/mod.rs
index 1f1c2de..0c684ec 100644
--- a/game_core/src/control/mod.rs
+++ b/game_core/src/control/mod.rs
@@ -17,6 +17,7 @@ mod __plugin {
 				ConditionSet::new()
 					.run_in_state(AppState::InGame)
 					.with_system(super::player::handle_player_input)
+					.with_system(super::player::handle_wait)
 					.into(),
 			)
 			.add_system_set_to_stage(
diff --git a/game_core/src/control/player.rs b/game_core/src/control/player.rs
index aadd605..730d18f 100644
--- a/game_core/src/control/player.rs
+++ b/game_core/src/control/player.rs
@@ -7,7 +7,38 @@ use crate::control::ai::{Automated, ShouldAct};
 use crate::entities::animations::{PositionBundle, PositionTween};
 use crate::entities::lifecycle::Player;
 use crate::entities::timing::ActionCooldown;
-use crate::world::level_map::{GridPosition, WORLD_TILE_SIZE};
+use crate::world::level_map::{GridPosition, LevelMap, WORLD_TILE_SIZE};
+use crate::world::pathing::PathingReqs;
+
+pub fn handle_wait(
+	mut commands: Commands,
+	input: Res<Input<KeyCode>>,
+	player_query: Query<
+		(Entity, &GridPosition),
+		(With<Player>, With<ShouldAct>, Without<ActionCooldown>),
+	>,
+	level_query: Query<&LevelMap>,
+	npc_query: Query<Entity, (With<Automated>, Without<ShouldAct>)>,
+) {
+	if input.pressed(KeyCode::Space) && let Ok((entity, pos)) = player_query.get_single() {
+		if let Ok(level) = level_query.get_single() {
+			let found = level.flood_fill(pos.0, 5, PathingReqs {
+				can_walk: Some(true),
+				..Default::default()
+			});
+
+			log::info!("Found walkable tiles: {:?}", found);
+		}
+
+		commands
+			.entity(entity)
+			.insert(ActionCooldown::from(Duration::from_millis(250)))
+			.remove::<ShouldAct>();
+		for entity in &npc_query {
+			commands.entity(entity).insert(ShouldAct);
+		}
+	}
+}
 
 pub fn handle_player_input(
 	mut commands: Commands,
diff --git a/game_core/src/entities/spawner.rs b/game_core/src/entities/spawner.rs
index 545508f..f236162 100644
--- a/game_core/src/entities/spawner.rs
+++ b/game_core/src/entities/spawner.rs
@@ -6,7 +6,7 @@ use bevy_tweening::lens::{TextColorLens, UiPositionLens};
 use bevy_tweening::{Animator, EaseFunction, Tween, TweeningType};
 
 use crate::assets::AssetHandles;
-use crate::control::ai::ShouldAct;
+use crate::control::ai::{Automated, ShouldAct};
 use crate::entities::animations::FrameAnimation;
 use crate::entities::curses::{CurseEvent, CurseText};
 use crate::entities::lifecycle::{GameEntity, Player};
@@ -48,6 +48,28 @@ impl<'w, 's> EntitySpawner<'w, 's> {
 		});
 	}
 
+	pub fn spawn_creature(&mut self, sprite: usize, grid_position: UVec2) {
+		let mut entity = self.commands.spawn();
+		entity
+			.insert(GameEntity)
+			.insert(Automated)
+			.insert(FrameAnimation::new(
+				vec![sprite, sprite + 1],
+				Duration::from_millis(250),
+			))
+			.insert(GridPosition(grid_position));
+
+		entity.insert_bundle(SpriteSheetBundle {
+			texture_atlas: self.handles.atlas("creatures"),
+			transform: Transform::from_translation(grid_position.as_vec2().extend(LAYER_CREATURE)),
+			sprite: TextureAtlasSprite {
+				index: sprite,
+				..Default::default()
+			},
+			..Default::default()
+		});
+	}
+
 	pub fn spawn_curse_text(&mut self, curse: CurseEvent) {
 		let position_animation = Tween::new(
 			EaseFunction::ExponentialIn,
diff --git a/game_core/src/lib.rs b/game_core/src/lib.rs
index 96e9124..c1933bf 100644
--- a/game_core/src/lib.rs
+++ b/game_core/src/lib.rs
@@ -1,3 +1,5 @@
+#![feature(map_first_last)]
+#![feature(let_chains)]
 extern crate core;
 
 pub mod assets;
diff --git a/game_core/src/world/generators/blobular.rs b/game_core/src/world/generators/blobular.rs
index 5484713..a3873e3 100644
--- a/game_core/src/world/generators/blobular.rs
+++ b/game_core/src/world/generators/blobular.rs
@@ -15,6 +15,10 @@ impl MapGenerator for Blobular {
 		let mut wall_layer = vec![0; width * height];
 		let mut decoration_layer = vec![0; width * height];
 
+		let target_monsters = (indexer.width() * indexer.height()) / 25;
+		let monster_attempts = target_monsters * 10;
+		let mut entities = Vec::with_capacity(target_monsters);
+
 		let initial_x = width / 2;
 		let initial_y = width / 2;
 		let initial_width = rng.usize(3..=5);
@@ -155,6 +159,7 @@ impl MapGenerator for Blobular {
 					MapLayerType::None,
 				),
 			],
+			entities,
 		}
 	}
 }
diff --git a/game_core/src/world/generators/drunkard_corridor.rs b/game_core/src/world/generators/drunkard_corridor.rs
index 6e815a8..00860ce 100644
--- a/game_core/src/world/generators/drunkard_corridor.rs
+++ b/game_core/src/world/generators/drunkard_corridor.rs
@@ -87,6 +87,7 @@ impl MapGenerator for DrunkardGenerator {
 					MapLayerType::Wall,
 				),
 			],
+			Vec::new(),
 		)
 	}
 }
diff --git a/game_core/src/world/level_map.rs b/game_core/src/world/level_map.rs
index e327719..cfc9c23 100644
--- a/game_core/src/world/level_map.rs
+++ b/game_core/src/world/level_map.rs
@@ -1,7 +1,7 @@
 use std::fmt::{Debug, Formatter};
 use std::ops::{Deref, DerefMut};
 
-use bevy::math::UVec2;
+use bevy::math::{uvec2, UVec2};
 use bevy::prelude::*;
 use fastrand::Rng;
 
@@ -131,6 +131,18 @@ impl MapLayer {
 		}
 	}
 
+	pub fn get_tile(&self, x: usize, y: usize) -> Option<MapTile> {
+		let indexer = Indexer::new(self.width, self.height);
+		indexer
+			.checked_index(x, y)
+			.and_then(|idx| self.tiles.get(idx))
+			.and_then(|found| *found)
+	}
+
+	pub fn get_tile_uvec2(&self, pos: UVec2) -> Option<MapTile> {
+		self.get_tile(pos.x as usize, pos.y as usize)
+	}
+
 	pub fn get_tile_adjacency(&self, x: usize, y: usize) -> u8 {
 		let indexer = Indexer::new(self.width, self.height);
 		let mut adjacency = NONE;
@@ -176,21 +188,40 @@ impl Debug for MapLayer {
 	}
 }
 
+#[derive(Debug, Clone, Copy, Eq, PartialEq)]
+pub enum EntityType {
+	Monster { sprite: usize },
+}
+
+#[derive(Debug, Clone, Copy, Eq, PartialEq)]
+pub struct EntityTemplate {
+	spawn: UVec2,
+	entity_type: EntityType,
+}
+
 #[derive(Debug, Clone, Eq, PartialEq, Component)]
 pub struct LevelMap {
 	pub width: usize,
 	pub height: usize,
 	pub layers: Vec<MapLayer>,
 	pub spawn: UVec2,
+	pub entities: Vec<EntityTemplate>,
 }
 
 impl LevelMap {
-	pub fn new(width: usize, height: usize, spawn: UVec2, layers: Vec<MapLayer>) -> Self {
+	pub fn new(
+		width: usize,
+		height: usize,
+		spawn: UVec2,
+		layers: Vec<MapLayer>,
+		entities: Vec<EntityTemplate>,
+	) -> Self {
 		LevelMap {
 			width,
 			height,
 			layers,
 			spawn,
+			entities,
 		}
 	}
 }
@@ -238,13 +269,36 @@ impl Indexer {
 	pub fn index(&self, x: usize, y: usize) -> usize {
 		(y * self.width) + x
 	}
+	pub fn checked_index(&self, x: usize, y: usize) -> Option<usize> {
+		if self.is_coordinate_valid(x, y) {
+			Some(self.index(x, y))
+		} else {
+			None
+		}
+	}
 	pub fn reverse(&self, index: usize) -> (usize, usize) {
 		(index % self.width, index / self.width)
 	}
+	pub fn checked_reverse(&self, idx: usize) -> Option<(usize, usize)> {
+		if self.is_index_valid(idx) {
+			Some(self.reverse(idx))
+		} else {
+			None
+		}
+	}
 	pub fn index_within(&self, idx: usize) -> bool {
 		let (x, y) = self.reverse(idx);
 		x >= 0 && x < self.width && y >= 0 && y < self.height
 	}
+	pub fn is_uvec2_valid(&self, point: UVec2) -> bool {
+		self.is_coordinate_valid(point.x as usize, point.y as usize)
+	}
+	pub fn is_coordinate_valid(&self, x: usize, y: usize) -> bool {
+		x < self.width && y < self.height
+	}
+	pub fn is_index_valid(&self, idx: usize) -> bool {
+		idx < self.width * self.height
+	}
 	pub fn width(&self) -> usize {
 		self.width
 	}
diff --git a/game_core/src/world/mod.rs b/game_core/src/world/mod.rs
index 0e3838e..2a85eb4 100644
--- a/game_core/src/world/mod.rs
+++ b/game_core/src/world/mod.rs
@@ -2,6 +2,7 @@ pub mod adjacency;
 pub mod generators;
 pub mod handlers;
 pub mod level_map;
+pub mod pathing;
 
 mod __plugin {
 	use bevy::app::{App, CoreStage, Plugin};
diff --git a/game_core/src/world/pathing.rs b/game_core/src/world/pathing.rs
new file mode 100644
index 0000000..2e2b272
--- /dev/null
+++ b/game_core/src/world/pathing.rs
@@ -0,0 +1,198 @@
+use std::cmp::Ordering;
+use std::collections::{BTreeSet, HashSet};
+use std::hash::{Hash, Hasher};
+
+use bevy::math::{uvec2, UVec2};
+use pathfinding::prelude::{astar, astar_bag};
+
+use crate::world::level_map::{LevelMap, MapLayer, MapTile};
+
+#[derive(Copy, Clone, Debug, Default, Eq, PartialEq)]
+pub struct PathingReqs {
+	pub can_see: Option<bool>,
+	pub can_walk: Option<bool>,
+	pub can_fly: Option<bool>,
+}
+
+#[derive(Copy, Clone, Debug, Default)]
+pub struct DistancePoint {
+	distance: usize,
+	point: UVec2,
+}
+
+impl DistancePoint {
+	pub fn new(distance: usize, point: UVec2) -> Self {
+		DistancePoint { distance, point }
+	}
+}
+
+impl PartialEq for DistancePoint {
+	fn eq(&self, other: &Self) -> bool {
+		self.point.eq(&other.point)
+	}
+}
+
+impl Eq for DistancePoint {}
+
+impl PartialOrd for DistancePoint {
+	fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
+		self.distance.partial_cmp(&other.distance)
+	}
+}
+
+impl Ord for DistancePoint {
+	fn cmp(&self, other: &Self) -> Ordering {
+		self.distance.cmp(&other.distance)
+	}
+}
+
+impl Hash for DistancePoint {
+	fn hash<H: Hasher>(&self, state: &mut H) {
+		self.point.hash(state);
+	}
+}
+
+#[inline(always)]
+fn tile_matches(tile: &MapTile, req: &PathingReqs) -> bool {
+	req.can_walk
+		.map(|should_walk| tile.can_walk == should_walk)
+		.unwrap_or(true)
+		&& req
+			.can_see
+			.map(|should_see| tile.can_see == should_see)
+			.unwrap_or(true)
+		&& req
+			.can_fly
+			.map(|should_fly| tile.can_fly == should_fly)
+			.unwrap_or(true)
+}
+
+fn reduce_tiles(previous: Option<MapTile>, current: Option<MapTile>) -> Option<MapTile> {
+	match previous {
+		Some(tile) => match current {
+			Some(current) => Some(MapTile {
+				can_fly: tile.can_fly && current.can_fly,
+				can_walk: tile.can_fly && current.can_walk,
+				can_see: tile.can_fly && current.can_see,
+				tile_group: 0,
+			}),
+			None => previous,
+		},
+		None => current,
+	}
+}
+
+fn distance_between(a: &UVec2, b: &UVec2) -> usize {
+	(a.x.abs_diff(b.x) + a.y.abs_diff(b.y)) as usize
+}
+
+impl LevelMap {
+	pub fn get_adjacent(&self, initial: UVec2, reqs: PathingReqs) -> Vec<(UVec2, usize)> {
+		let mut tiles = Vec::with_capacity(4);
+
+		let right = self
+			.layers
+			.iter()
+			.map(|layer| layer.get_tile_uvec2(uvec2(initial.x.saturating_add(1), initial.y)))
+			.collect::<Vec<Option<MapTile>>>();
+		let left = self
+			.layers
+			.iter()
+			.map(|layer| layer.get_tile_uvec2(uvec2(initial.x.saturating_sub(1), initial.y)))
+			.collect::<Vec<Option<MapTile>>>();
+		let top = self
+			.layers
+			.iter()
+			.map(|layer| layer.get_tile_uvec2(uvec2(initial.x, initial.y.saturating_add(1))))
+			.collect::<Vec<Option<MapTile>>>();
+		let bottom = self
+			.layers
+			.iter()
+			.map(|layer| layer.get_tile_uvec2(uvec2(initial.x, initial.y.saturating_sub(1))))
+			.collect::<Vec<Option<MapTile>>>();
+
+		if let Some(tile) = left.into_iter().reduce(reduce_tiles).and_then(|f| f) {
+			if tile_matches(&tile, &reqs) {
+				tiles.push((uvec2(initial.x.saturating_sub(1), initial.y), 1));
+			}
+		}
+		if let Some(tile) = right.into_iter().reduce(reduce_tiles).and_then(|f| f) {
+			if tile_matches(&tile, &reqs) {
+				tiles.push((uvec2(initial.x.saturating_add(1), initial.y), 1));
+			}
+		}
+		if let Some(tile) = top.into_iter().reduce(reduce_tiles).and_then(|f| f) {
+			if tile_matches(&tile, &reqs) {
+				tiles.push((uvec2(initial.x, initial.y.saturating_add(1)), 1));
+			}
+		}
+		if let Some(tile) = bottom.into_iter().reduce(reduce_tiles).and_then(|f| f) {
+			if tile_matches(&tile, &reqs) {
+				tiles.push((uvec2(initial.x, initial.y.saturating_sub(1)), 1));
+			}
+		}
+
+		tiles
+	}
+
+	/// Find all tiles within a certain range that can be accessed based on
+	/// the given requirements. Will not include the starting point in the
+	/// returned list
+	pub fn flood_fill(&self, start: UVec2, distance: usize, reqs: PathingReqs) -> Vec<UVec2> {
+		let mut pending: BTreeSet<DistancePoint> =
+			BTreeSet::from_iter(vec![DistancePoint::new(0, start)]);
+		let mut found: HashSet<DistancePoint> = HashSet::new();
+
+		while let Some(next) = pending.pop_first() {
+			let adjacent = self.get_adjacent(next.point, reqs);
+			for (point, adjacent_distance) in adjacent {
+				let dp = DistancePoint::new(next.distance + adjacent_distance, point);
+				if let Some(existing) = found.get(&dp) {
+					if dp.distance < existing.distance {
+						found.remove(&dp);
+						found.insert(dp);
+						pending.insert(dp);
+					}
+				} else if dp.distance < distance {
+					found.insert(dp);
+					pending.insert(dp);
+				}
+			}
+		}
+
+		found.into_iter().map(|dp| dp.point).collect()
+	}
+
+	/// Find the shortest path between two points that meets the requirements.
+	/// Does not account for entity bottlenecks; to do so, use `custom_path_between`
+	/// instead to provide costings that account for entities
+	pub fn path_between(&self, start: UVec2, end: UVec2, reqs: PathingReqs) -> Vec<UVec2> {
+		astar(
+			&start,
+			|&point| self.get_adjacent(point, reqs),
+			|point| distance_between(point, &end),
+			|&point| point == end,
+		)
+		.map(|(list, _)| list)
+		.unwrap_or(Vec::new())
+	}
+
+	/// Find all paths that have the same shortest length between two points. Will take longer
+	/// to find a result than simply fetching the first shortest path, but allows for more control
+	/// over selection of the final path
+	pub fn all_paths_between(
+		&self,
+		start: UVec2,
+		end: UVec2,
+		reqs: PathingReqs,
+	) -> Vec<Vec<UVec2>> {
+		astar_bag(
+			&start,
+			|&point| self.get_adjacent(point, reqs),
+			|point| distance_between(point, &end),
+			|&point| point == end,
+		)
+		.map(|(list, _)| list.collect())
+		.unwrap_or(Vec::new())
+	}
+}
-- 
GitLab