Skip to content
Snippets Groups Projects
Verified Commit 75df69a3 authored by Louis's avatar Louis :fire:
Browse files

Set up basic pathing algorithms for level maps

parent 1c9ba4b5
No related branches found
No related tags found
No related merge requests found
Pipeline #184 passed with stages
in 2 minutes and 27 seconds
...@@ -1336,6 +1336,12 @@ version = "1.2.0" ...@@ -1336,6 +1336,12 @@ version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9ea835d29036a4087793836fa931b08837ad5e957da9e23886b29586fb9b6650" checksum = "9ea835d29036a4087793836fa931b08837ad5e957da9e23886b29586fb9b6650"
[[package]]
name = "either"
version = "1.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "90e5c1c8368803113bf0c9584fc495a58b86dc8a29edbf8fe877d21d9507e797"
[[package]] [[package]]
name = "encase" name = "encase"
version = "0.3.0" version = "0.3.0"
...@@ -1506,6 +1512,7 @@ dependencies = [ ...@@ -1506,6 +1512,7 @@ dependencies = [
"iyes_loopless", "iyes_loopless",
"log 0.4.17", "log 0.4.17",
"micro_musicbox", "micro_musicbox",
"pathfinding",
"remote_events", "remote_events",
"serde", "serde",
"serde_json", "serde_json",
...@@ -1810,6 +1817,15 @@ dependencies = [ ...@@ -1810,6 +1817,15 @@ dependencies = [
"web-sys", "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]] [[package]]
name = "interpolation" name = "interpolation"
version = "0.2.0" version = "0.2.0"
...@@ -1825,6 +1841,15 @@ dependencies = [ ...@@ -1825,6 +1841,15 @@ dependencies = [
"libc", "libc",
] ]
[[package]]
name = "itertools"
version = "0.10.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473"
dependencies = [
"either",
]
[[package]] [[package]]
name = "itoa" name = "itoa"
version = "1.0.3" version = "1.0.3"
...@@ -2463,6 +2488,21 @@ dependencies = [ ...@@ -2463,6 +2488,21 @@ dependencies = [
"windows-sys", "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]] [[package]]
name = "peeking_take_while" name = "peeking_take_while"
version = "0.1.2" version = "0.1.2"
......
...@@ -22,6 +22,7 @@ iyes_loopless = "0.7.1" ...@@ -22,6 +22,7 @@ iyes_loopless = "0.7.1"
micro_musicbox = { version = "0.4.0", features = ["serde", "mp3"] } micro_musicbox = { version = "0.4.0", features = ["serde", "mp3"] }
remote_events = { git = "https://lab.lcr.gr/microhacks/micro-bevy-remote-events.git", rev = "be0c6b43a73e4c5e7ece20797e3d6f59340147b4"} remote_events = { git = "https://lab.lcr.gr/microhacks/micro-bevy-remote-events.git", rev = "be0c6b43a73e4c5e7ece20797e3d6f59340147b4"}
bevy_tweening = "0.5.0" bevy_tweening = "0.5.0"
pathfinding = "3.0.14"
[target.'cfg(target_arch = "wasm32")'.dependencies] [target.'cfg(target_arch = "wasm32")'.dependencies]
web-sys = { version = "0.3.60", features = ["Window"] } web-sys = { version = "0.3.60", features = ["Window"] }
......
...@@ -17,6 +17,7 @@ mod __plugin { ...@@ -17,6 +17,7 @@ mod __plugin {
ConditionSet::new() ConditionSet::new()
.run_in_state(AppState::InGame) .run_in_state(AppState::InGame)
.with_system(super::player::handle_player_input) .with_system(super::player::handle_player_input)
.with_system(super::player::handle_wait)
.into(), .into(),
) )
.add_system_set_to_stage( .add_system_set_to_stage(
......
...@@ -7,7 +7,38 @@ use crate::control::ai::{Automated, ShouldAct}; ...@@ -7,7 +7,38 @@ use crate::control::ai::{Automated, ShouldAct};
use crate::entities::animations::{PositionBundle, PositionTween}; use crate::entities::animations::{PositionBundle, PositionTween};
use crate::entities::lifecycle::Player; use crate::entities::lifecycle::Player;
use crate::entities::timing::ActionCooldown; 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( pub fn handle_player_input(
mut commands: Commands, mut commands: Commands,
......
...@@ -6,7 +6,7 @@ use bevy_tweening::lens::{TextColorLens, UiPositionLens}; ...@@ -6,7 +6,7 @@ use bevy_tweening::lens::{TextColorLens, UiPositionLens};
use bevy_tweening::{Animator, EaseFunction, Tween, TweeningType}; use bevy_tweening::{Animator, EaseFunction, Tween, TweeningType};
use crate::assets::AssetHandles; use crate::assets::AssetHandles;
use crate::control::ai::ShouldAct; use crate::control::ai::{Automated, ShouldAct};
use crate::entities::animations::FrameAnimation; use crate::entities::animations::FrameAnimation;
use crate::entities::curses::{CurseEvent, CurseText}; use crate::entities::curses::{CurseEvent, CurseText};
use crate::entities::lifecycle::{GameEntity, Player}; use crate::entities::lifecycle::{GameEntity, Player};
...@@ -48,6 +48,28 @@ impl<'w, 's> EntitySpawner<'w, 's> { ...@@ -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) { pub fn spawn_curse_text(&mut self, curse: CurseEvent) {
let position_animation = Tween::new( let position_animation = Tween::new(
EaseFunction::ExponentialIn, EaseFunction::ExponentialIn,
......
#![feature(map_first_last)]
#![feature(let_chains)]
extern crate core; extern crate core;
pub mod assets; pub mod assets;
......
...@@ -15,6 +15,10 @@ impl MapGenerator for Blobular { ...@@ -15,6 +15,10 @@ impl MapGenerator for Blobular {
let mut wall_layer = vec![0; width * height]; let mut wall_layer = vec![0; width * height];
let mut decoration_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_x = width / 2;
let initial_y = width / 2; let initial_y = width / 2;
let initial_width = rng.usize(3..=5); let initial_width = rng.usize(3..=5);
...@@ -155,6 +159,7 @@ impl MapGenerator for Blobular { ...@@ -155,6 +159,7 @@ impl MapGenerator for Blobular {
MapLayerType::None, MapLayerType::None,
), ),
], ],
entities,
} }
} }
} }
......
...@@ -87,6 +87,7 @@ impl MapGenerator for DrunkardGenerator { ...@@ -87,6 +87,7 @@ impl MapGenerator for DrunkardGenerator {
MapLayerType::Wall, MapLayerType::Wall,
), ),
], ],
Vec::new(),
) )
} }
} }
use std::fmt::{Debug, Formatter}; use std::fmt::{Debug, Formatter};
use std::ops::{Deref, DerefMut}; use std::ops::{Deref, DerefMut};
use bevy::math::UVec2; use bevy::math::{uvec2, UVec2};
use bevy::prelude::*; use bevy::prelude::*;
use fastrand::Rng; use fastrand::Rng;
...@@ -131,6 +131,18 @@ impl MapLayer { ...@@ -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 { pub fn get_tile_adjacency(&self, x: usize, y: usize) -> u8 {
let indexer = Indexer::new(self.width, self.height); let indexer = Indexer::new(self.width, self.height);
let mut adjacency = NONE; let mut adjacency = NONE;
...@@ -176,21 +188,40 @@ impl Debug for MapLayer { ...@@ -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)] #[derive(Debug, Clone, Eq, PartialEq, Component)]
pub struct LevelMap { pub struct LevelMap {
pub width: usize, pub width: usize,
pub height: usize, pub height: usize,
pub layers: Vec<MapLayer>, pub layers: Vec<MapLayer>,
pub spawn: UVec2, pub spawn: UVec2,
pub entities: Vec<EntityTemplate>,
} }
impl LevelMap { 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 { LevelMap {
width, width,
height, height,
layers, layers,
spawn, spawn,
entities,
} }
} }
} }
...@@ -238,13 +269,36 @@ impl Indexer { ...@@ -238,13 +269,36 @@ impl Indexer {
pub fn index(&self, x: usize, y: usize) -> usize { pub fn index(&self, x: usize, y: usize) -> usize {
(y * self.width) + x (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) { pub fn reverse(&self, index: usize) -> (usize, usize) {
(index % self.width, index / self.width) (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 { pub fn index_within(&self, idx: usize) -> bool {
let (x, y) = self.reverse(idx); let (x, y) = self.reverse(idx);
x >= 0 && x < self.width && y >= 0 && y < self.height 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 { pub fn width(&self) -> usize {
self.width self.width
} }
......
...@@ -2,6 +2,7 @@ pub mod adjacency; ...@@ -2,6 +2,7 @@ pub mod adjacency;
pub mod generators; pub mod generators;
pub mod handlers; pub mod handlers;
pub mod level_map; pub mod level_map;
pub mod pathing;
mod __plugin { mod __plugin {
use bevy::app::{App, CoreStage, Plugin}; use bevy::app::{App, CoreStage, Plugin};
......
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())
}
}
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment