diff --git a/Cargo.lock b/Cargo.lock index 02d372ed069c5b2183897bd8a88602ed8f7d72e2..4190e9cd4b445fd7ee6d11562fd962ec2818c0f4 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 56d0c1efdeda1551239740b0c191bc7ba377c7b1..c9948bc119027f6759bfbd194e86a9d2b5697620 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 1f1c2de93e06ffb74e937b718c9b40234433fab8..0c684ec92ae6ec47cdc2fc13d5943f43d1cee52e 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 aadd6053e53a01d66423ccb39df381c4ddd17eaf..730d18f2d1aa2bc536a6ce5634e49971ad35bb01 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 545508fa0bb8fe53280bdd84b5b65ebc78c9e7e7..f236162095230b1c96a1f17c168278cc30e5a63c 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 96e91248bc7a603e2c359f06aef45aa6ebb9e66b..c1933bf53ee0978846add709478ea1bb9bc5e1f3 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 548471358c4e94fba521d71a669065df0fd42628..a3873e3978f21e2173edf8d76ecb230b862b53ff 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 6e815a8bb7b0438b5a7bb104f8bfbc63af483f44..00860cea7b65d9f565f2f0bb9a5b16bf71771c03 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 e327719c909e923362baf31e6eab01af2fab5656..cfc9c232556859bca891f5b99f51ae81917dd52b 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 0e3838efdb8c565847bac2a8e99342f67657c605..2a85eb4b9eb56748857cdda8d21118b821ae4154 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 0000000000000000000000000000000000000000..2e2b2727b5208a7a0855d29ac296bd8277a48bfc --- /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()) + } +}