Initial commit.
This commit is contained in:
commit
3332d0a88d
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
|
@ -0,0 +1,2 @@
|
|||
target
|
||||
Cargo.lock
|
35
Cargo.toml
Normal file
35
Cargo.toml
Normal file
|
@ -0,0 +1,35 @@
|
|||
[package]
|
||||
name = "blackout"
|
||||
version = "0.1.0"
|
||||
authors = ["Nolan Darilek <nolan@thewordnerd.info>"]
|
||||
edition = "2018"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies.bevy]
|
||||
version = "0.5"
|
||||
default-features = false
|
||||
features = [
|
||||
"bevy_gilrs",
|
||||
"bevy_wgpu",
|
||||
"bevy_winit",
|
||||
"render",
|
||||
"x11",
|
||||
"wayland",
|
||||
"serialize",
|
||||
]
|
||||
|
||||
[dependencies]
|
||||
backtrace = "0.3"
|
||||
bevy_input_actionmap = { path = "../bevy_input_actionmap" }
|
||||
bevy_openal = { path = "../bevy_openal" }
|
||||
bevy_tts = { path = "../bevy_tts" }
|
||||
coord_2d = "0.3"
|
||||
crossbeam-channel = "0.5"
|
||||
derive_more = "0.99"
|
||||
gilrs = "0.8"
|
||||
mapgen = "0.4"
|
||||
maze_generator = "1"
|
||||
pathfinding = "2"
|
||||
rand = "0.8"
|
||||
shadowcast = "0.8"
|
433
src/core.rs
Normal file
433
src/core.rs
Normal file
|
@ -0,0 +1,433 @@
|
|||
use std::{
|
||||
cmp::{max, min},
|
||||
fmt::Display,
|
||||
};
|
||||
|
||||
use bevy::{core::FloatOrd, prelude::*, transform::TransformSystem};
|
||||
use derive_more::{Deref, DerefMut};
|
||||
|
||||
#[derive(Clone, Copy, Debug, Default, Deref, DerefMut, PartialEq, PartialOrd, Reflect)]
|
||||
#[reflect(Component)]
|
||||
pub struct Coordinates(pub (f32, f32));
|
||||
|
||||
impl From<(f32, f32)> for Coordinates {
|
||||
fn from(v: (f32, f32)) -> Self {
|
||||
Coordinates((v.0, v.1))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<(i32, i32)> for Coordinates {
|
||||
fn from(v: (i32, i32)) -> Self {
|
||||
Coordinates((v.0 as f32, v.1 as f32))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<(u32, u32)> for Coordinates {
|
||||
fn from(v: (u32, u32)) -> Self {
|
||||
Coordinates((v.0 as f32, v.1 as f32))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<(usize, usize)> for Coordinates {
|
||||
fn from(v: (usize, usize)) -> Self {
|
||||
Coordinates((v.0 as f32, v.1 as f32))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct Area {
|
||||
pub rect: mapgen::geometry::Rect,
|
||||
pub description: Option<String>,
|
||||
}
|
||||
|
||||
impl Area {
|
||||
pub fn contains(&self, point: &dyn PointLike) -> bool {
|
||||
let x = point.x() as usize;
|
||||
let y = point.y() as usize;
|
||||
x >= self.rect.x1 && x <= self.rect.x2 && y >= self.rect.y1 && y <= self.rect.y2
|
||||
}
|
||||
|
||||
pub fn center(&self) -> (usize, usize) {
|
||||
let center = self.rect.center();
|
||||
(center.x, center.y)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Reflect)]
|
||||
pub enum Angle {
|
||||
Degrees(f32),
|
||||
Radians(f32),
|
||||
}
|
||||
|
||||
impl Default for Angle {
|
||||
fn default() -> Self {
|
||||
Self::Radians(0.)
|
||||
}
|
||||
}
|
||||
|
||||
impl Angle {
|
||||
pub fn degrees(&self) -> f32 {
|
||||
use Angle::*;
|
||||
let mut degrees: f32 = match self {
|
||||
Degrees(v) => *v,
|
||||
Radians(v) => v.to_degrees(),
|
||||
};
|
||||
while degrees < 0. {
|
||||
degrees += 360.;
|
||||
}
|
||||
while degrees >= 360. {
|
||||
degrees %= 360.;
|
||||
}
|
||||
degrees
|
||||
}
|
||||
|
||||
pub fn degrees_u32(&self) -> u32 {
|
||||
self.degrees() as u32
|
||||
}
|
||||
|
||||
pub fn radians(&self) -> f32 {
|
||||
use Angle::*;
|
||||
match self {
|
||||
Degrees(v) => v.to_radians(),
|
||||
Radians(v) => *v,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
pub enum MovementDirection {
|
||||
North,
|
||||
NorthNortheast,
|
||||
Northeast,
|
||||
EastNortheast,
|
||||
East,
|
||||
EastSoutheast,
|
||||
Southeast,
|
||||
SouthSoutheast,
|
||||
South,
|
||||
SouthSouthwest,
|
||||
Southwest,
|
||||
WestSouthwest,
|
||||
West,
|
||||
WestNorthwest,
|
||||
Northwest,
|
||||
NorthNorthwest,
|
||||
}
|
||||
|
||||
impl MovementDirection {
|
||||
pub fn new(heading: f32) -> Self {
|
||||
use MovementDirection::*;
|
||||
let mut heading = heading;
|
||||
while heading >= 360. {
|
||||
heading -= 360.;
|
||||
}
|
||||
while heading < 0. {
|
||||
heading += 360.;
|
||||
}
|
||||
match heading {
|
||||
h if h < 11.5 => East,
|
||||
h if h < 34.0 => EastNortheast,
|
||||
h if h < 56.5 => Northeast,
|
||||
h if h < 79.0 => NorthNortheast,
|
||||
h if h < 101.5 => North,
|
||||
h if h < 124.0 => NorthNorthwest,
|
||||
h if h < 146.5 => Northwest,
|
||||
h if h < 169.0 => WestNorthwest,
|
||||
h if h < 191.5 => West,
|
||||
h if h < 214.0 => WestSouthwest,
|
||||
h if h < 236.5 => Southwest,
|
||||
h if h < 259.0 => SouthSouthwest,
|
||||
h if h < 281.5 => South,
|
||||
h if h < 304.0 => SouthSoutheast,
|
||||
h if h < 326.5 => Southeast,
|
||||
h if h <= 349.0 => EastSoutheast,
|
||||
_ => East,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Angle> for MovementDirection {
|
||||
fn from(angle: Angle) -> Self {
|
||||
MovementDirection::new(angle.degrees())
|
||||
}
|
||||
}
|
||||
|
||||
// Converting from strings into directions doesn't make sense.
|
||||
#[allow(clippy::from_over_into)]
|
||||
impl Into<String> for MovementDirection {
|
||||
fn into(self) -> String {
|
||||
use MovementDirection::*;
|
||||
match self {
|
||||
North => "north".to_string(),
|
||||
NorthNortheast => "north northeast".to_string(),
|
||||
Northeast => "northeast".to_string(),
|
||||
EastNortheast => "east northeast".to_string(),
|
||||
East => "east".to_string(),
|
||||
EastSoutheast => "east southeast".to_string(),
|
||||
Southeast => "southeast".to_string(),
|
||||
SouthSoutheast => "south southeast".to_string(),
|
||||
South => "south".to_string(),
|
||||
SouthSouthwest => "south southwest".to_string(),
|
||||
Southwest => "southwest".to_string(),
|
||||
WestSouthwest => "west southwest".to_string(),
|
||||
West => "west".to_string(),
|
||||
WestNorthwest => "west northwest".to_string(),
|
||||
Northwest => "northwest".to_string(),
|
||||
NorthNorthwest => "north northwest".to_string(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for MovementDirection {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
let str: String = (*self).into();
|
||||
write!(f, "{}", str)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
pub enum CardinalDirection {
|
||||
North,
|
||||
East,
|
||||
South,
|
||||
West,
|
||||
}
|
||||
|
||||
impl CardinalDirection {
|
||||
pub fn new(heading: f32) -> Self {
|
||||
use CardinalDirection::*;
|
||||
let mut heading = heading;
|
||||
while heading >= 360. {
|
||||
heading -= 360.;
|
||||
}
|
||||
while heading < 0. {
|
||||
heading += 360.;
|
||||
}
|
||||
match heading {
|
||||
h if h <= 45. => East,
|
||||
h if h <= 135. => North,
|
||||
h if h <= 225. => West,
|
||||
h if h <= 315. => South,
|
||||
_ => East,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Angle> for CardinalDirection {
|
||||
fn from(angle: Angle) -> Self {
|
||||
CardinalDirection::new(angle.degrees())
|
||||
}
|
||||
}
|
||||
|
||||
// Converting from strings into directions doesn't make sense.
|
||||
#[allow(clippy::from_over_into)]
|
||||
impl Into<String> for CardinalDirection {
|
||||
fn into(self) -> String {
|
||||
use CardinalDirection::*;
|
||||
match self {
|
||||
North => "north".to_string(),
|
||||
East => "east".to_string(),
|
||||
South => "south".to_string(),
|
||||
West => "west".to_string(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for CardinalDirection {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
let str: String = (*self).into();
|
||||
write!(f, "{}", str)
|
||||
}
|
||||
}
|
||||
|
||||
pub trait PointLike {
|
||||
fn x(&self) -> f32;
|
||||
|
||||
fn y(&self) -> f32;
|
||||
|
||||
fn x_i32(&self) -> i32 {
|
||||
self.x() as i32
|
||||
}
|
||||
|
||||
fn y_i32(&self) -> i32 {
|
||||
self.y() as i32
|
||||
}
|
||||
|
||||
fn x_usize(&self) -> usize {
|
||||
self.x() as usize
|
||||
}
|
||||
|
||||
fn y_usize(&self) -> usize {
|
||||
self.y() as usize
|
||||
}
|
||||
fn i32(&self) -> (i32, i32) {
|
||||
(self.x_i32(), self.y_i32())
|
||||
}
|
||||
|
||||
fn to_index(&self, width: usize) -> usize {
|
||||
((self.y_i32() * width as i32) + self.x_i32()) as usize
|
||||
}
|
||||
|
||||
fn distance_squared(&self, other: &dyn PointLike) -> f32 {
|
||||
let x1 = FloatOrd(self.x());
|
||||
let y1 = FloatOrd(self.y());
|
||||
let x2 = FloatOrd(other.x());
|
||||
let y2 = FloatOrd(other.y());
|
||||
let dx = max(x1, x2).0 - min(x1, x2).0;
|
||||
let dy = max(y1, y2).0 - min(y1, y2).0;
|
||||
(dx * dx) + (dy * dy)
|
||||
}
|
||||
|
||||
fn distance(&self, other: &dyn PointLike) -> f32 {
|
||||
self.distance_squared(other).sqrt()
|
||||
}
|
||||
|
||||
fn bearing(&self, other: &dyn PointLike) -> f32 {
|
||||
let y = other.y() - self.y();
|
||||
let x = other.x() - self.x();
|
||||
y.atan2(x)
|
||||
}
|
||||
|
||||
fn direction(&self, other: &dyn PointLike) -> MovementDirection {
|
||||
let heading = self.bearing(other);
|
||||
MovementDirection::new(heading.to_degrees())
|
||||
}
|
||||
|
||||
fn distance_and_direction(&self, other: &dyn PointLike) -> String {
|
||||
let mut tokens: Vec<String> = vec![];
|
||||
let distance = self.distance(other).round() as i32;
|
||||
if distance > 0 {
|
||||
let tile_or_tiles = if distance == 1 { "tile" } else { "tiles" };
|
||||
let direction: String = self.direction(other).into();
|
||||
tokens.push(format!("{} {} {}", distance, tile_or_tiles, direction));
|
||||
}
|
||||
tokens.join(" ")
|
||||
}
|
||||
}
|
||||
|
||||
impl PointLike for (i32, i32) {
|
||||
fn x(&self) -> f32 {
|
||||
self.0 as f32
|
||||
}
|
||||
|
||||
fn y(&self) -> f32 {
|
||||
self.1 as f32
|
||||
}
|
||||
}
|
||||
|
||||
impl PointLike for (f32, f32) {
|
||||
fn x(&self) -> f32 {
|
||||
self.0
|
||||
}
|
||||
|
||||
fn y(&self) -> f32 {
|
||||
self.1
|
||||
}
|
||||
}
|
||||
|
||||
impl PointLike for (usize, usize) {
|
||||
fn x(&self) -> f32 {
|
||||
self.0 as f32
|
||||
}
|
||||
|
||||
fn y(&self) -> f32 {
|
||||
self.1 as f32
|
||||
}
|
||||
}
|
||||
|
||||
impl PointLike for &Coordinates {
|
||||
fn x(&self) -> f32 {
|
||||
self.0 .0
|
||||
}
|
||||
|
||||
fn y(&self) -> f32 {
|
||||
self.0 .1
|
||||
}
|
||||
}
|
||||
|
||||
impl PointLike for mapgen::geometry::Point {
|
||||
fn x(&self) -> f32 {
|
||||
self.x as f32
|
||||
}
|
||||
|
||||
fn y(&self) -> f32 {
|
||||
self.y as f32
|
||||
}
|
||||
}
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! impl_pointlike_for_tuple_component {
|
||||
($source:ty) => {
|
||||
impl PointLike for $source {
|
||||
fn x(&self) -> f32 {
|
||||
self.0 .0 as f32
|
||||
}
|
||||
|
||||
fn y(&self) -> f32 {
|
||||
self.0 .1 as f32
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
impl_pointlike_for_tuple_component!(Coordinates);
|
||||
|
||||
impl From<&dyn PointLike> for (i32, i32) {
|
||||
fn from(val: &dyn PointLike) -> Self {
|
||||
(val.x_i32(), val.y_i32())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Default, Reflect)]
|
||||
#[reflect(Component)]
|
||||
pub struct Player;
|
||||
|
||||
fn copy_coordinates_to_transform(
|
||||
config: Res<CoreConfig>,
|
||||
mut query: Query<(&Coordinates, &mut Transform), Changed<Coordinates>>,
|
||||
) {
|
||||
for (coordinates, mut transform) in query.iter_mut() {
|
||||
transform.translation.x = coordinates.0 .0 * config.pixels_per_unit as f32;
|
||||
transform.translation.y = coordinates.0 .1 * config.pixels_per_unit as f32;
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub struct CoreConfig {
|
||||
pub pixels_per_unit: u8,
|
||||
}
|
||||
|
||||
impl Default for CoreConfig {
|
||||
fn default() -> Self {
|
||||
Self { pixels_per_unit: 1 }
|
||||
}
|
||||
}
|
||||
|
||||
pub struct CorePlugin;
|
||||
|
||||
impl Plugin for CorePlugin {
|
||||
fn build(&self, app: &mut AppBuilder) {
|
||||
if !app.world().contains_resource::<CoreConfig>() {
|
||||
app.insert_resource(CoreConfig::default());
|
||||
}
|
||||
app.register_type::<Coordinates>()
|
||||
.add_system(copy_coordinates_to_transform.system())
|
||||
.add_system_to_stage(
|
||||
CoreStage::PostUpdate,
|
||||
copy_coordinates_to_transform
|
||||
.system()
|
||||
.before(TransformSystem::TransformPropagate),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
pub struct CorePlugins;
|
||||
|
||||
impl PluginGroup for CorePlugins {
|
||||
fn build(&mut self, group: &mut bevy::app::PluginGroupBuilder) {
|
||||
group
|
||||
.add(crate::bevy_tts::TtsPlugin)
|
||||
.add(crate::bevy_openal::OpenAlPlugin)
|
||||
.add(CorePlugin);
|
||||
}
|
||||
}
|
56
src/error.rs
Normal file
56
src/error.rs
Normal file
|
@ -0,0 +1,56 @@
|
|||
use std::error::Error;
|
||||
use std::{panic, thread};
|
||||
|
||||
use backtrace::Backtrace;
|
||||
use bevy::prelude::*;
|
||||
|
||||
pub fn error_handler(In(result): In<Result<(), Box<dyn Error>>>) {
|
||||
if let Err(e) = result {
|
||||
error!("{}", e);
|
||||
}
|
||||
}
|
||||
|
||||
fn init_panic_handler() {
|
||||
panic::set_hook(Box::new(|info| {
|
||||
let backtrace = Backtrace::default();
|
||||
|
||||
let thread = thread::current();
|
||||
let thread = thread.name().unwrap_or("<unnamed>");
|
||||
|
||||
let msg = match info.payload().downcast_ref::<&'static str>() {
|
||||
Some(s) => *s,
|
||||
None => match info.payload().downcast_ref::<String>() {
|
||||
Some(s) => &**s,
|
||||
None => "Box<Any>",
|
||||
},
|
||||
};
|
||||
|
||||
match info.location() {
|
||||
Some(location) => {
|
||||
error!(
|
||||
target: "panic", "thread '{}' panicked at '{}': {}:{}{:?}",
|
||||
thread,
|
||||
msg,
|
||||
location.file(),
|
||||
location.line(),
|
||||
backtrace
|
||||
);
|
||||
}
|
||||
None => error!(
|
||||
target: "panic",
|
||||
"thread '{}' panicked at '{}'{:?}",
|
||||
thread,
|
||||
msg,
|
||||
backtrace
|
||||
),
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
pub struct ErrorPlugin;
|
||||
|
||||
impl Plugin for ErrorPlugin {
|
||||
fn build(&self, _: &mut AppBuilder) {
|
||||
init_panic_handler();
|
||||
}
|
||||
}
|
386
src/exploration.rs
Normal file
386
src/exploration.rs
Normal file
|
@ -0,0 +1,386 @@
|
|||
use std::error::Error;
|
||||
|
||||
use bevy::prelude::*;
|
||||
use bevy_input_actionmap::InputMap;
|
||||
use bevy_tts::Tts;
|
||||
use derive_more::{Deref, DerefMut};
|
||||
use mapgen::TileType;
|
||||
|
||||
use crate::{
|
||||
core::{Coordinates, Player, PointLike},
|
||||
error::error_handler,
|
||||
map::Map,
|
||||
pathfinding::Destination,
|
||||
visibility::{RevealedTiles, Viewshed, VisibleTiles},
|
||||
};
|
||||
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq, Reflect)]
|
||||
#[reflect(Component)]
|
||||
pub struct ExplorationFocused;
|
||||
|
||||
#[allow(dead_code)]
|
||||
#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd, Reflect)]
|
||||
pub enum ExplorationType {
|
||||
Exit = 0,
|
||||
Item = 1,
|
||||
Character = 2,
|
||||
Ally = 3,
|
||||
Enemy = 4,
|
||||
}
|
||||
|
||||
// Doesn't make sense to create from a `String`.
|
||||
#[allow(clippy::from_over_into)]
|
||||
impl Into<String> for ExplorationType {
|
||||
fn into(self) -> String {
|
||||
match self {
|
||||
ExplorationType::Exit => "Exit".into(),
|
||||
ExplorationType::Item => "Item".into(),
|
||||
ExplorationType::Character => "Character".into(),
|
||||
ExplorationType::Ally => "Ally".into(),
|
||||
ExplorationType::Enemy => "Enemy".into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Likewise.
|
||||
#[allow(clippy::from_over_into)]
|
||||
impl Into<&str> for ExplorationType {
|
||||
fn into(self) -> &'static str {
|
||||
match self {
|
||||
ExplorationType::Exit => "exit",
|
||||
ExplorationType::Item => "item",
|
||||
ExplorationType::Character => "character",
|
||||
ExplorationType::Ally => "ally",
|
||||
ExplorationType::Enemy => "enemy",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Default, Deref, DerefMut, Reflect)]
|
||||
#[reflect(Component)]
|
||||
pub struct Exploring(pub (f32, f32));
|
||||
|
||||
impl_pointlike_for_tuple_component!(Exploring);
|
||||
|
||||
#[derive(Clone, Debug, Default, Deref, DerefMut)]
|
||||
pub struct FocusedExplorationType(pub Option<ExplorationType>);
|
||||
|
||||
#[derive(Clone, Copy, Debug, Default, Reflect)]
|
||||
#[reflect(Component)]
|
||||
pub struct Mappable;
|
||||
|
||||
pub const ACTION_EXPLORE_FORWARD: &str = "explore_forward";
|
||||
pub const ACTION_EXPLORE_BACKWARD: &str = "explore_backward";
|
||||
pub const ACTION_EXPLORE_LEFT: &str = "explore_left";
|
||||
pub const ACTION_EXPLORE_RIGHT: &str = "explore_right";
|
||||
pub const ACTION_EXPLORE_FOCUS_NEXT: &str = "explore_focus_next";
|
||||
pub const ACTION_EXPLORE_FOCUS_PREV: &str = "explore_focus_prev";
|
||||
pub const ACTION_EXPLORE_SELECT_NEXT_TYPE: &str = "explore_select_next_type";
|
||||
pub const ACTION_EXPLORE_SELECT_PREV_TYPE: &str = "explore_select_prev_type";
|
||||
pub const ACTION_NAVIGATE_TO_EXPLORED: &str = "navigate_to";
|
||||
|
||||
fn exploration_type_change(
|
||||
mut tts: ResMut<Tts>,
|
||||
input: Res<InputMap<String>>,
|
||||
mut explorers: Query<(&Player, &Viewshed, &mut FocusedExplorationType)>,
|
||||
features: Query<(&Coordinates, &ExplorationType)>,
|
||||
) -> Result<(), Box<dyn Error>> {
|
||||
let changed = input.just_active(ACTION_EXPLORE_SELECT_NEXT_TYPE)
|
||||
|| input.just_active(ACTION_EXPLORE_SELECT_PREV_TYPE);
|
||||
if !changed {
|
||||
return Ok(());
|
||||
}
|
||||
for (_, viewshed, mut focused) in explorers.iter_mut() {
|
||||
let mut types: Vec<ExplorationType> = vec![];
|
||||
for (coordinates, t) in features.iter() {
|
||||
let (x, y) = **coordinates;
|
||||
let x = x as i32;
|
||||
let y = y as i32;
|
||||
if viewshed.visible.contains(&(x, y)) {
|
||||
types.push(*t);
|
||||
}
|
||||
}
|
||||
types.sort();
|
||||
types.dedup();
|
||||
if types.is_empty() {
|
||||
tts.speak("Nothing visible.", true)?;
|
||||
} else if input.just_active(ACTION_EXPLORE_SELECT_PREV_TYPE) {
|
||||
if let Some(t) = &focused.0 {
|
||||
if let Some(i) = types.iter().position(|v| *v == *t) {
|
||||
if i == 0 {
|
||||
focused.0 = None;
|
||||
} else {
|
||||
let t = &types[i - 1];
|
||||
focused.0 = Some(*t);
|
||||
}
|
||||
} else {
|
||||
let t = types.last().unwrap();
|
||||
focused.0 = Some(*t);
|
||||
}
|
||||
} else {
|
||||
let t = types.last().unwrap();
|
||||
focused.0 = Some(*t);
|
||||
}
|
||||
} else if input.just_active(ACTION_EXPLORE_SELECT_NEXT_TYPE) {
|
||||
if let Some(t) = &focused.0 {
|
||||
if let Some(i) = types.iter().position(|v| *v == *t) {
|
||||
if i == types.len() - 1 {
|
||||
focused.0 = None;
|
||||
} else {
|
||||
let t = &types[i + 1];
|
||||
focused.0 = Some(*t);
|
||||
}
|
||||
} else {
|
||||
let t = types.first().unwrap();
|
||||
focused.0 = Some(*t);
|
||||
}
|
||||
} else {
|
||||
let t = types.first().unwrap();
|
||||
focused.0 = Some(*t)
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn exploration_type_focus(
|
||||
mut commands: Commands,
|
||||
input: Res<InputMap<String>>,
|
||||
mut tts: ResMut<Tts>,
|
||||
explorers: Query<(
|
||||
Entity,
|
||||
&Player,
|
||||
&Viewshed,
|
||||
&FocusedExplorationType,
|
||||
Option<&Exploring>,
|
||||
)>,
|
||||
features: Query<(&Coordinates, &ExplorationType)>,
|
||||
) -> Result<(), Box<dyn Error>> {
|
||||
let changed = input.just_active(ACTION_EXPLORE_FOCUS_NEXT)
|
||||
|| input.just_active(ACTION_EXPLORE_FOCUS_PREV);
|
||||
if !changed {
|
||||
return Ok(());
|
||||
}
|
||||
for (entity, _, viewshed, focused, exploring) in explorers.iter() {
|
||||
let mut features = features
|
||||
.iter()
|
||||
.filter(|(coordinates, _)| {
|
||||
let (x, y) = ***coordinates;
|
||||
let x = x as i32;
|
||||
let y = y as i32;
|
||||
viewshed.visible.contains(&(x, y))
|
||||
})
|
||||
.collect::<Vec<(&Coordinates, &ExplorationType)>>();
|
||||
features.sort_by(|(c1, _), (c2, _)| c1.partial_cmp(c2).unwrap());
|
||||
if let Some(focused) = &focused.0 {
|
||||
features.retain(|(_, t)| **t == *focused);
|
||||
}
|
||||
if features.is_empty() {
|
||||
tts.speak("Nothing visible.", true)?;
|
||||
} else {
|
||||
let mut target: Option<&(&Coordinates, &ExplorationType)> = None;
|
||||
if input.just_active(ACTION_EXPLORE_FOCUS_NEXT) {
|
||||
if let Some(exploring) = exploring {
|
||||
target = features.iter().find(|(c, _)| ***c > **exploring);
|
||||
if target.is_none() {
|
||||
target = features.first();
|
||||
}
|
||||
} else {
|
||||
target = features.first();
|
||||
}
|
||||
} else if input.just_active(ACTION_EXPLORE_FOCUS_PREV) {
|
||||
if let Some(exploring) = exploring {
|
||||
features.reverse();
|
||||
target = features.iter().find(|(c, _)| ***c < **exploring);
|
||||
if target.is_none() {
|
||||
target = features.first();
|
||||
}
|
||||
} else {
|
||||
target = features.last();
|
||||
}
|
||||
}
|
||||
if let Some((coordinates, _)) = target {
|
||||
commands.entity(entity).insert(Exploring(***coordinates));
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn exploration_type_changed_announcement(
|
||||
mut tts: ResMut<Tts>,
|
||||
focused: Query<
|
||||
(
|
||||
&FocusedExplorationType,
|
||||
ChangeTrackers<FocusedExplorationType>,
|
||||
),
|
||||
Changed<FocusedExplorationType>,
|
||||
>,
|
||||
) -> Result<(), Box<dyn Error>> {
|
||||
for (focused, changed) in focused.iter() {
|
||||
if changed.is_added() {
|
||||
return Ok(());
|
||||
}
|
||||
match &focused.0 {
|
||||
Some(v) => {
|
||||
let v: String = (*v).into();
|
||||
tts.speak(v, true)?;
|
||||
}
|
||||
None => {
|
||||
tts.speak("Everything", true)?;
|
||||
}
|
||||
};
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn exploration_focus(
|
||||
mut commands: Commands,
|
||||
input: Res<InputMap<String>>,
|
||||
map: Query<&Map>,
|
||||
explorers: Query<(Entity, &Player, &Coordinates, Option<&Exploring>)>,
|
||||
) {
|
||||
for map in map.iter() {
|
||||
for (entity, _, coordinates, exploring) in explorers.iter() {
|
||||
let coordinates = **coordinates;
|
||||
let coordinates = (coordinates.0.floor(), coordinates.1.floor());
|
||||
let mut exploring = if let Some(exploring) = exploring {
|
||||
**exploring
|
||||
} else {
|
||||
coordinates
|
||||
};
|
||||
let orig = exploring;
|
||||
if input.just_active(ACTION_EXPLORE_FORWARD) {
|
||||
exploring.1 += 1.;
|
||||
} else if input.just_active(ACTION_EXPLORE_BACKWARD) {
|
||||
exploring.1 -= 1.;
|
||||
} else if input.just_active(ACTION_EXPLORE_LEFT) {
|
||||
exploring.0 -= 1.;
|
||||
} else if input.just_active(ACTION_EXPLORE_RIGHT) {
|
||||
exploring.0 += 1.;
|
||||
}
|
||||
if orig != exploring
|
||||
&& exploring.0 >= 0.
|
||||
&& exploring.0 < map.width() as f32
|
||||
&& exploring.1 >= 0.
|
||||
&& exploring.1 < map.height() as f32
|
||||
{
|
||||
commands.entity(entity).insert(Exploring(exploring));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn navigate_to_explored(
|
||||
mut commands: Commands,
|
||||
input: Res<InputMap<String>>,
|
||||
map: Query<(&Map, &RevealedTiles)>,
|
||||
explorers: Query<(Entity, &Exploring)>,
|
||||
) {
|
||||
for (entity, exploring) in explorers.iter() {
|
||||
for (map, revealed_tiles) in map.iter() {
|
||||
let point = **exploring;
|
||||
let idx = point.to_index(map.width());
|
||||
let known = revealed_tiles[idx];
|
||||
if input.just_active(ACTION_NAVIGATE_TO_EXPLORED) && known {
|
||||
commands
|
||||
.entity(entity)
|
||||
.insert(Destination((point.x_i32(), point.y_i32())));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn exploration_changed_announcement(
|
||||
mut commands: Commands,
|
||||
mut tts: ResMut<Tts>,
|
||||
map: Query<(&Map, &RevealedTiles, &VisibleTiles)>,
|
||||
explorers: Query<(&Coordinates, &Exploring), Changed<Exploring>>,
|
||||
focused: Query<(Entity, &ExplorationFocused)>,
|
||||
names: Query<&Name>,
|
||||
types: Query<&ExplorationType>,
|
||||
mappables: Query<&Mappable>,
|
||||
) -> Result<(), Box<dyn Error>> {
|
||||
for (coordinates, exploring) in explorers.iter() {
|
||||
let coordinates = **coordinates;
|
||||
let coordinates = (coordinates.0.floor(), coordinates.1.floor());
|
||||
for (map, revealed_tiles, visible_tiles) in map.iter() {
|
||||
let point = **exploring;
|
||||
let idx = point.to_index(map.width());
|
||||
let known = revealed_tiles[idx];
|
||||
let visible = visible_tiles[idx];
|
||||
let fog_of_war = known && !visible;
|
||||
let description = if known {
|
||||
let mut tokens: Vec<&str> = vec![];
|
||||
for (entity, _) in focused.iter() {
|
||||
commands.entity(entity).remove::<ExplorationFocused>();
|
||||
}
|
||||
for entity in &map.entities[idx] {
|
||||
commands
|
||||
.entity(*entity)
|
||||
.insert(ExplorationFocused::default());
|
||||
if visible || mappables.get(*entity).is_ok() {
|
||||
if let Ok(name) = names.get(*entity) {
|
||||
tokens.push(name.as_str());
|
||||
}
|
||||
if tokens.is_empty() {
|
||||
if let Ok(t) = types.get(*entity) {
|
||||
tokens.push((*t).into());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if tokens.is_empty() {
|
||||
match map.base.tiles[idx] {
|
||||
TileType::Floor => "Floor".to_string(),
|
||||
TileType::Wall => "Wall".to_string(),
|
||||
}
|
||||
} else {
|
||||
tokens.join(": ")
|
||||
}
|
||||
} else {
|
||||
"Unknown".to_string()
|
||||
};
|
||||
let mut tokens: Vec<String> = vec![coordinates.distance_and_direction(exploring)];
|
||||
if fog_of_war {
|
||||
tokens.push("in the fog of war".into());
|
||||
}
|
||||
tts.speak(format!("{}: {}", description, tokens.join(", ")), true)?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub struct ExplorationPlugin;
|
||||
|
||||
impl Plugin for ExplorationPlugin {
|
||||
fn build(&self, app: &mut AppBuilder) {
|
||||
app.register_type::<ExplorationFocused>()
|
||||
.register_type::<ExplorationType>()
|
||||
.register_type::<Mappable>()
|
||||
.add_system(exploration_focus.system())
|
||||
.add_system(
|
||||
exploration_type_focus
|
||||
.system()
|
||||
.chain(error_handler.system()),
|
||||
)
|
||||
.add_system(
|
||||
exploration_type_change
|
||||
.system()
|
||||
.chain(error_handler.system()),
|
||||
)
|
||||
.add_system(navigate_to_explored.system())
|
||||
.add_system_to_stage(
|
||||
CoreStage::PostUpdate,
|
||||
exploration_type_changed_announcement
|
||||
.system()
|
||||
.chain(error_handler.system()),
|
||||
)
|
||||
.add_system_to_stage(
|
||||
CoreStage::PostUpdate,
|
||||
exploration_changed_announcement
|
||||
.system()
|
||||
.chain(error_handler.system()),
|
||||
);
|
||||
}
|
||||
}
|
21
src/lib.rs
Normal file
21
src/lib.rs
Normal file
|
@ -0,0 +1,21 @@
|
|||
#![allow(clippy::too_many_arguments)]
|
||||
#![allow(clippy::type_complexity)]
|
||||
|
||||
pub use bevy_input_actionmap;
|
||||
pub use bevy_openal;
|
||||
pub use bevy_tts;
|
||||
#[macro_use]
|
||||
pub mod core;
|
||||
pub use crossbeam_channel;
|
||||
pub use derive_more;
|
||||
pub mod error;
|
||||
pub mod exploration;
|
||||
pub use gilrs;
|
||||
pub mod log;
|
||||
pub mod map;
|
||||
pub use mapgen;
|
||||
pub mod navigation;
|
||||
pub mod pathfinding;
|
||||
pub use rand;
|
||||
pub mod sound;
|
||||
pub mod visibility;
|
59
src/log.rs
Normal file
59
src/log.rs
Normal file
|
@ -0,0 +1,59 @@
|
|||
use std::{error::Error, time::Instant};
|
||||
|
||||
use bevy::prelude::*;
|
||||
use bevy_tts::Tts;
|
||||
use derive_more::{Deref, DerefMut};
|
||||
|
||||
use crate::error::error_handler;
|
||||
|
||||
#[derive(Clone, Debug, Default, Deref, DerefMut)]
|
||||
pub struct Log(pub Vec<LogEntry>);
|
||||
|
||||
impl Log {
|
||||
pub fn push<S: Into<String>>(&mut self, message: S) {
|
||||
self.0.push(LogEntry {
|
||||
time: Instant::now(),
|
||||
message: message.into(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct LogEntry {
|
||||
pub time: Instant,
|
||||
pub message: String,
|
||||
}
|
||||
|
||||
fn setup(mut commands: Commands) {
|
||||
commands.spawn().insert(Log::default());
|
||||
}
|
||||
|
||||
fn read_log(
|
||||
mut tts: ResMut<Tts>,
|
||||
mut position: Local<usize>,
|
||||
log: Query<&Log, Changed<Log>>,
|
||||
) -> Result<(), Box<dyn Error>> {
|
||||
for log in log.iter() {
|
||||
for (index, entry) in log.iter().enumerate() {
|
||||
if index >= *position {
|
||||
tts.speak(entry.message.clone(), false)?;
|
||||
*position = index + 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub struct LogPlugin;
|
||||
|
||||
impl Plugin for LogPlugin {
|
||||
fn build(&self, app: &mut AppBuilder) {
|
||||
app.add_startup_system(setup.system()).add_system_to_stage(
|
||||
CoreStage::PostUpdate,
|
||||
read_log
|
||||
.system()
|
||||
.chain(error_handler.system())
|
||||
.after(crate::visibility::LOG_VISIBLE_LABEL),
|
||||
);
|
||||
}
|
||||
}
|
370
src/map.rs
Normal file
370
src/map.rs
Normal file
|
@ -0,0 +1,370 @@
|
|||
use std::collections::{HashMap, HashSet};
|
||||
|
||||
use bevy::prelude::*;
|
||||
use derive_more::{Deref, DerefMut};
|
||||
use mapgen::{geometry::Rect as MRect, Map as MapgenMap, MapFilter, TileType};
|
||||
use maze_generator::{prelude::*, recursive_backtracking::RbGenerator};
|
||||
use rand::prelude::StdRng;
|
||||
|
||||
use crate::{
|
||||
core::{Area, Coordinates, Player, PointLike},
|
||||
exploration::{ExplorationType, Mappable},
|
||||
log::Log,
|
||||
};
|
||||
|
||||
impl From<mapgen::geometry::Point> for Coordinates {
|
||||
fn from(point: mapgen::geometry::Point) -> Self {
|
||||
Self((point.x as f32, point.y as f32))
|
||||
}
|
||||
}
|
||||
#[derive(Clone, Debug, Default, Deref, DerefMut)]
|
||||
pub struct Areas(pub Vec<Area>);
|
||||
|
||||
#[derive(Clone, Copy, Debug, Default, Reflect)]
|
||||
#[reflect(Component)]
|
||||
pub struct Exit;
|
||||
|
||||
#[derive(Clone, Default)]
|
||||
pub struct Map {
|
||||
pub base: MapgenMap,
|
||||
pub entities: Vec<HashSet<Entity>>,
|
||||
}
|
||||
|
||||
impl Map {
|
||||
pub fn new(base: MapgenMap) -> Self {
|
||||
let count = (base.width * base.height) as usize;
|
||||
Self {
|
||||
base,
|
||||
entities: vec![HashSet::new(); count],
|
||||
}
|
||||
}
|
||||
|
||||
pub fn width(&self) -> usize {
|
||||
self.base.width
|
||||
}
|
||||
|
||||
pub fn height(&self) -> usize {
|
||||
self.base.height
|
||||
}
|
||||
|
||||
pub fn count(&self) -> usize {
|
||||
self.width() * self.height()
|
||||
}
|
||||
|
||||
pub fn start(&self) -> Option<mapgen::geometry::Point> {
|
||||
self.base.starting_point
|
||||
}
|
||||
|
||||
pub fn exit(&self) -> Option<mapgen::geometry::Point> {
|
||||
self.base.exit_point
|
||||
}
|
||||
}
|
||||
|
||||
pub trait ITileType {
|
||||
fn blocks_motion(&self) -> bool;
|
||||
fn blocks_visibility(&self) -> bool;
|
||||
}
|
||||
|
||||
impl ITileType for TileType {
|
||||
fn blocks_motion(&self) -> bool {
|
||||
match self {
|
||||
TileType::Wall => true,
|
||||
TileType::Floor => false,
|
||||
}
|
||||
}
|
||||
|
||||
fn blocks_visibility(&self) -> bool {
|
||||
match self {
|
||||
TileType::Wall => true,
|
||||
TileType::Floor => false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct MapConfig {
|
||||
pub autospawn_exits: bool,
|
||||
pub speak_area_descriptions: bool,
|
||||
pub start_revealed: bool,
|
||||
}
|
||||
|
||||
impl Default for MapConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
autospawn_exits: true,
|
||||
speak_area_descriptions: true,
|
||||
start_revealed: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Bundle)]
|
||||
pub struct ExitBundle {
|
||||
pub coordinates: Coordinates,
|
||||
pub exit: Exit,
|
||||
pub exploration_type: ExplorationType,
|
||||
pub mappable: Mappable,
|
||||
pub transform: Transform,
|
||||
pub global_transform: GlobalTransform,
|
||||
}
|
||||
|
||||
impl Default for ExitBundle {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
coordinates: Default::default(),
|
||||
exit: Default::default(),
|
||||
exploration_type: ExplorationType::Exit,
|
||||
mappable: Default::default(),
|
||||
transform: Default::default(),
|
||||
global_transform: Default::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Bundle, Clone, Default)]
|
||||
pub struct MapBundle {
|
||||
pub map: Map,
|
||||
pub children: Children,
|
||||
pub transform: Transform,
|
||||
pub global_transform: GlobalTransform,
|
||||
}
|
||||
|
||||
pub struct GridBuilder {
|
||||
width_in_rooms: u32,
|
||||
height_in_rooms: u32,
|
||||
room_width: u32,
|
||||
room_height: u32,
|
||||
}
|
||||
|
||||
impl GridBuilder {
|
||||
pub fn new(
|
||||
width_in_rooms: u32,
|
||||
height_in_rooms: u32,
|
||||
room_width: u32,
|
||||
room_height: u32,
|
||||
) -> Box<GridBuilder> {
|
||||
Box::new(GridBuilder {
|
||||
width_in_rooms,
|
||||
height_in_rooms,
|
||||
room_width,
|
||||
room_height,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl MapFilter for GridBuilder {
|
||||
fn modify_map(&self, _rng: &mut StdRng, map: &MapgenMap) -> MapgenMap {
|
||||
let mut map = map.clone();
|
||||
let mut generator = RbGenerator::new(None);
|
||||
let maze = generator.generate(self.width_in_rooms as i32, self.height_in_rooms as i32);
|
||||
let total_height = (self.room_height + 1) * self.height_in_rooms + 1;
|
||||
for y in 0..self.height_in_rooms {
|
||||
for x in 0..self.width_in_rooms {
|
||||
let x_offset = x * (self.room_width + 1);
|
||||
let y_offset = total_height - (y * (self.room_height + 1)) - self.room_height - 2;
|
||||
let room = MRect::new_i32(
|
||||
x_offset as i32 + 1,
|
||||
y_offset as i32 + 1,
|
||||
self.room_width as i32,
|
||||
self.room_height as i32,
|
||||
);
|
||||
map.add_room(room);
|
||||
let coords = maze_generator::prelude::Coordinates::new(x as i32, y as i32);
|
||||
if let Some(field) = maze.get_field(&coords) {
|
||||
let half_width = self.room_width / 2;
|
||||
let half_height = self.room_height / 2;
|
||||
use maze_generator::prelude::Direction::*;
|
||||
if field.has_passage(&North) {
|
||||
let x = x_offset + half_width;
|
||||
let y = y_offset + self.room_height;
|
||||
map.set_tile(x as usize, y as usize, TileType::Floor);
|
||||
}
|
||||
if field.has_passage(&South) {
|
||||
let x = x_offset + half_width;
|
||||
let y = y_offset;
|
||||
map.set_tile(x as usize, y as usize, TileType::Floor);
|
||||
}
|
||||
if field.has_passage(&East) {
|
||||
let x = x_offset + self.room_width;
|
||||
let y = y_offset + half_height;
|
||||
map.set_tile(x as usize, y as usize, TileType::Floor);
|
||||
}
|
||||
if field.has_passage(&West) {
|
||||
let x = x_offset;
|
||||
let y = y_offset + half_height;
|
||||
map.set_tile(x as usize, y as usize, TileType::Floor);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
map
|
||||
}
|
||||
}
|
||||
|
||||
fn exit_spawner(
|
||||
mut commands: Commands,
|
||||
map: Query<(Entity, &Map), Added<Map>>,
|
||||
config: Res<MapConfig>,
|
||||
) {
|
||||
for (entity, map) in map.iter() {
|
||||
if config.autospawn_exits {
|
||||
let mut exits: Vec<(f32, f32)> = vec![];
|
||||
for x in 1..map.width() {
|
||||
for y in 1..map.height() {
|
||||
let mut spawn_exit = false;
|
||||
if map.base.get_available_exits(x, y).len() > 2 {
|
||||
let idx = (x, y).to_index(map.width());
|
||||
if map.base.tiles[idx] == TileType::Floor
|
||||
&& (x > 1 && map.base.tiles[idx - 1] == TileType::Floor)
|
||||
&& (x < map.width() - 2 && map.base.tiles[idx + 1] == TileType::Floor)
|
||||
&& (y > 1
|
||||
&& map.base.tiles[idx - map.width() as usize] == TileType::Wall)
|
||||
&& (y < map.height() - 2
|
||||
&& map.base.tiles[idx + map.width() as usize] == TileType::Wall)
|
||||
{
|
||||
spawn_exit = true;
|
||||
}
|
||||
if map.base.tiles[idx] == TileType::Floor
|
||||
&& (x > 1 && map.base.tiles[idx - 1] == TileType::Wall)
|
||||
&& (x < map.width() - 2 && map.base.tiles[idx + 1] == TileType::Wall)
|
||||
&& (y > 1
|
||||
&& map.base.tiles[idx - map.width() as usize] == TileType::Floor)
|
||||
&& (y < map.height() - 2
|
||||
&& map.base.tiles[idx + map.width() as usize] == TileType::Floor)
|
||||
{
|
||||
spawn_exit = true;
|
||||
}
|
||||
}
|
||||
if spawn_exit {
|
||||
let x = x as f32;
|
||||
let y = y as f32;
|
||||
if !exits.contains(&(x, y)) {
|
||||
exits.push((x, y));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
for exit in exits {
|
||||
let x = exit.0 as f32;
|
||||
let y = exit.1 as f32;
|
||||
let exit = commands
|
||||
.spawn()
|
||||
.insert_bundle(ExitBundle {
|
||||
coordinates: Coordinates((x, y)),
|
||||
exit: Default::default(),
|
||||
transform: Transform::from_translation(Vec3::new(x, y, 0.)),
|
||||
..Default::default()
|
||||
})
|
||||
.id();
|
||||
commands.entity(entity).push_children(&[exit]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn area_description(
|
||||
mut prev_area: Local<Option<Area>>,
|
||||
query: Query<(&Player, &Coordinates), Changed<Coordinates>>,
|
||||
map: Query<(&Map, &Areas)>,
|
||||
mut log: Query<&mut Log>,
|
||||
) {
|
||||
for (_, coordinates) in query.iter() {
|
||||
for (_, areas) in map.iter() {
|
||||
let mut should_describe_area = false;
|
||||
let mut current_area: Option<Area> = None;
|
||||
for area in areas.iter() {
|
||||
if area.contains(&*coordinates) {
|
||||
current_area = Some(area.clone());
|
||||
if let Some(prev_area) = &*prev_area {
|
||||
if prev_area != area {
|
||||
should_describe_area = true;
|
||||
}
|
||||
} else {
|
||||
should_describe_area = true;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
if should_describe_area {
|
||||
if let Some(ref area) = current_area {
|
||||
let description = if area.description.is_some() {
|
||||
area.description.as_ref().unwrap().clone()
|
||||
} else {
|
||||
format!("{} by {} area.", area.rect.width(), area.rect.height())
|
||||
};
|
||||
for mut log in log.iter_mut() {
|
||||
log.push(description.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
*prev_area = current_area;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default, Deref, DerefMut)]
|
||||
struct PreviousIndex(HashMap<Entity, usize>);
|
||||
|
||||
fn entity_indexing(
|
||||
mut map: Query<&mut Map>,
|
||||
mut previous_index: ResMut<PreviousIndex>,
|
||||
query: Query<(Entity, &Coordinates), Changed<Coordinates>>,
|
||||
) {
|
||||
for (entity, coordinates) in query.iter() {
|
||||
for mut map in map.iter_mut() {
|
||||
let idx = coordinates.to_index(map.width());
|
||||
if let Some(prev_idx) = previous_index.get(&entity) {
|
||||
if idx != *prev_idx {
|
||||
map.entities[*prev_idx].retain(|&e| e != entity);
|
||||
}
|
||||
}
|
||||
map.entities[idx].insert(entity);
|
||||
previous_index.insert(entity, idx);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn add_areas(mut commands: Commands, query: Query<(Entity, &Map), (Added<Map>, Without<Areas>)>) {
|
||||
for (entity, map) in query.iter() {
|
||||
let mut v = vec![];
|
||||
for room in &map.base.rooms {
|
||||
v.push(Area {
|
||||
rect: *room,
|
||||
description: None,
|
||||
});
|
||||
}
|
||||
commands.entity(entity).insert(Areas(v));
|
||||
}
|
||||
}
|
||||
|
||||
pub const UPDATE_ENTITY_INDEX_LABEL: &str = "UPDATE_ENTITY_INDEX";
|
||||
|
||||
pub struct MapPlugin;
|
||||
|
||||
impl Plugin for MapPlugin {
|
||||
fn build(&self, app: &mut AppBuilder) {
|
||||
if !app.world().contains_resource::<MapConfig>() {
|
||||
app.insert_resource(MapConfig::default());
|
||||
}
|
||||
let config = app.world().get_resource::<MapConfig>().unwrap().clone();
|
||||
const SPAWN_EXITS: &str = "SPAWN_EXITS";
|
||||
app.register_type::<Exit>()
|
||||
.insert_resource(PreviousIndex::default())
|
||||
.add_system(entity_indexing.system().label(UPDATE_ENTITY_INDEX_LABEL))
|
||||
.add_system(
|
||||
exit_spawner
|
||||
.system()
|
||||
.label(SPAWN_EXITS)
|
||||
.before(UPDATE_ENTITY_INDEX_LABEL),
|
||||
)
|
||||
.add_system_to_stage(
|
||||
CoreStage::PostUpdate,
|
||||
entity_indexing.system().label(UPDATE_ENTITY_INDEX_LABEL),
|
||||
)
|
||||
.add_system_to_stage(CoreStage::Update, add_areas.system())
|
||||
.add_system_to_stage(CoreStage::PostUpdate, add_areas.system());
|
||||
if config.speak_area_descriptions {
|
||||
app.add_system_to_stage(CoreStage::PostUpdate, area_description.system());
|
||||
}
|
||||
}
|
||||
}
|
500
src/navigation.rs
Normal file
500
src/navigation.rs
Normal file
|
@ -0,0 +1,500 @@
|
|||
use std::{collections::HashMap, error::Error, fmt::Debug, hash::Hash};
|
||||
|
||||
use bevy::prelude::*;
|
||||
use bevy_input_actionmap::InputMap;
|
||||
use bevy_tts::Tts;
|
||||
use derive_more::{Deref, DerefMut};
|
||||
|
||||
use crate::{
|
||||
core::{Angle, CardinalDirection, Coordinates, Player, PointLike},
|
||||
error::error_handler,
|
||||
exploration::{ExplorationFocused, Exploring},
|
||||
map::{ITileType, Map},
|
||||
pathfinding::Destination,
|
||||
};
|
||||
|
||||
#[derive(Clone, Copy, Debug, Default, Reflect)]
|
||||
#[reflect(Component)]
|
||||
pub struct BlocksMotion;
|
||||
|
||||
#[derive(Clone, Debug, Default, Deref, DerefMut, Reflect)]
|
||||
#[reflect(Component)]
|
||||
pub struct CollisionsMonitored(pub Vec<bool>);
|
||||
|
||||
#[derive(Clone, Copy, Debug, Deref, DerefMut, Reflect)]
|
||||
#[reflect(Component)]
|
||||
pub struct MaxSpeed(pub f32);
|
||||
|
||||
impl Default for MaxSpeed {
|
||||
fn default() -> Self {
|
||||
MaxSpeed(2.)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Default, Reflect)]
|
||||
#[reflect(Component)]
|
||||
pub struct MonitorsCollisions;
|
||||
|
||||
#[derive(Clone, Debug, Default, Deref, DerefMut, Reflect)]
|
||||
#[reflect(Component)]
|
||||
pub struct MotionBlocked(pub Vec<bool>);
|
||||
|
||||
#[derive(Clone, Copy, Debug, Deref, DerefMut, Reflect)]
|
||||
#[reflect(Component)]
|
||||
pub struct RotationSpeed(pub Angle);
|
||||
|
||||
impl Default for RotationSpeed {
|
||||
fn default() -> Self {
|
||||
Self(Angle::Radians(0.))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Default, Deref, DerefMut, Reflect)]
|
||||
#[reflect(Component)]
|
||||
pub struct Speed(pub f32);
|
||||
|
||||
#[derive(Clone, Copy, Debug, Default, Reflect)]
|
||||
#[reflect(Component)]
|
||||
pub struct Sprinting;
|
||||
|
||||
#[derive(Clone, Copy, Debug, Default, Deref, DerefMut, Reflect)]
|
||||
#[reflect(Component)]
|
||||
pub struct Velocity(pub Vec2);
|
||||
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub struct Collision {
|
||||
pub entity: Entity,
|
||||
pub coordinates: (f32, f32),
|
||||
pub index: usize,
|
||||
}
|
||||
|
||||
pub const ACTION_FORWARD: &str = "forward";
|
||||
pub const ACTION_BACKWARD: &str = "backward";
|
||||
pub const ACTION_LEFT: &str = "left";
|
||||
pub const ACTION_RIGHT: &str = "right";
|
||||
pub const ACTION_ROTATE_LEFT: &str = "ROTATE_LEFT";
|
||||
pub const ACTION_ROTATE_RIGHT: &str = "ROTATE_RIGHT";
|
||||
pub const ACTION_SPRINT: &str = "SPRINT";
|
||||
|
||||
fn movement_controls(
|
||||
mut commands: Commands,
|
||||
input: Res<InputMap<String>>,
|
||||
time: Res<Time>,
|
||||
mut query: Query<(
|
||||
Entity,
|
||||
&Player,
|
||||
&mut Velocity,
|
||||
&mut Speed,
|
||||
&MaxSpeed,
|
||||
Option<&RotationSpeed>,
|
||||
&mut Transform,
|
||||
Option<&Destination>,
|
||||
)>,
|
||||
exploration_focused: Query<(Entity, &ExplorationFocused)>,
|
||||
) {
|
||||
for (
|
||||
entity,
|
||||
_,
|
||||
mut velocity,
|
||||
mut speed,
|
||||
max_speed,
|
||||
rotation_speed,
|
||||
mut transform,
|
||||
destination,
|
||||
) in query.iter_mut()
|
||||
{
|
||||
let sprinting = input.active(ACTION_SPRINT);
|
||||
if sprinting {
|
||||
commands.entity(entity).insert(Sprinting::default());
|
||||
} else {
|
||||
commands.entity(entity).remove::<Sprinting>();
|
||||
}
|
||||
let mut direction = Vec3::default();
|
||||
if input.active(ACTION_FORWARD) {
|
||||
direction.x += 1.;
|
||||
}
|
||||
if input.active(ACTION_BACKWARD) {
|
||||
direction.x -= 1.;
|
||||
}
|
||||
if input.active(ACTION_LEFT) {
|
||||
direction.y += 1.;
|
||||
}
|
||||
if input.active(ACTION_RIGHT) {
|
||||
direction.y -= 1.;
|
||||
}
|
||||
if let Some(rotation_speed) = rotation_speed {
|
||||
let delta = rotation_speed.radians() * time.delta_seconds();
|
||||
if input.active(ACTION_ROTATE_LEFT) {
|
||||
transform.rotate(Quat::from_rotation_z(delta));
|
||||
}
|
||||
if input.active(ACTION_ROTATE_RIGHT) {
|
||||
transform.rotate(Quat::from_rotation_z(-delta));
|
||||
}
|
||||
}
|
||||
if direction.length_squared() != 0. {
|
||||
direction = direction.normalize();
|
||||
let forward_x = input.strength(ACTION_FORWARD).abs();
|
||||
let backward_x = input.strength(ACTION_BACKWARD).abs();
|
||||
let x = if forward_x > backward_x {
|
||||
forward_x
|
||||
} else {
|
||||
backward_x
|
||||
};
|
||||
let right_y = input.strength(ACTION_RIGHT).abs();
|
||||
let left_y = input.strength(ACTION_LEFT).abs();
|
||||
let y = if right_y > left_y { right_y } else { left_y };
|
||||
let strength = Vec3::new(x, y, 0.);
|
||||
let s = if sprinting {
|
||||
**max_speed
|
||||
} else {
|
||||
**max_speed / 3.
|
||||
};
|
||||
speed.0 = s;
|
||||
direction *= s;
|
||||
direction *= strength;
|
||||
commands.entity(entity).remove::<Destination>();
|
||||
commands.entity(entity).remove::<Exploring>();
|
||||
for (entity, _) in exploration_focused.iter() {
|
||||
commands.entity(entity).remove::<ExplorationFocused>();
|
||||
}
|
||||
direction = transform.compute_matrix().transform_vector3(direction);
|
||||
let direction = Vec2::new(direction.x, direction.y);
|
||||
**velocity = direction;
|
||||
} else if destination.is_none() {
|
||||
**velocity = Vec2::ZERO;
|
||||
speed.0 = 0.;
|
||||
} else if sprinting {
|
||||
speed.0 = max_speed.0;
|
||||
} else {
|
||||
speed.0 = max_speed.0 / 3.;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn movement(
|
||||
time: Res<Time>,
|
||||
mut collision_events: EventWriter<Collision>,
|
||||
map: Query<(&Map, &MotionBlocked, &CollisionsMonitored)>,
|
||||
mut entities: Query<(Entity, &Velocity, &mut Coordinates, Option<&BlocksMotion>)>,
|
||||
) {
|
||||
for (entity, velocity, mut coordinates, blocks_motion) in entities.iter_mut() {
|
||||
if **velocity != Vec2::ZERO {
|
||||
let displacement = **velocity * time.delta_seconds();
|
||||
let mut point = **coordinates;
|
||||
point.0 += displacement.x;
|
||||
point.1 += displacement.y;
|
||||
if let Ok((map, motion_blocked, collisions_monitored)) = map.single() {
|
||||
let idx = point.to_index(map.width());
|
||||
if idx < map.base.tiles.len() {
|
||||
let current_entities = &map.entities[idx];
|
||||
if blocks_motion.is_some()
|
||||
&& motion_blocked[idx]
|
||||
&& !current_entities.contains(&entity)
|
||||
{
|
||||
collision_events.send(Collision {
|
||||
entity,
|
||||
coordinates: point,
|
||||
index: idx,
|
||||
});
|
||||
} else {
|
||||
**coordinates = point;
|
||||
let current_entities = &map.entities[idx];
|
||||
if collisions_monitored[idx] && !current_entities.contains(&entity) {
|
||||
collision_events.send(Collision {
|
||||
entity,
|
||||
coordinates: point,
|
||||
index: idx,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
**coordinates = point;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub const UPDATE_COLLISION_INDEX_LABEL: &str = "UPDATE_COLLISION_INDEX";
|
||||
|
||||
#[derive(Default, Deref, DerefMut)]
|
||||
struct PreviousBlocksMotionIndex(HashMap<Entity, usize>);
|
||||
|
||||
fn blocks_motion_indexing(
|
||||
mut map: Query<(&Map, &mut MotionBlocked)>,
|
||||
mut prev_index: ResMut<PreviousBlocksMotionIndex>,
|
||||
query: Query<
|
||||
(Entity, &Coordinates, &BlocksMotion),
|
||||
Or<(Changed<Coordinates>, Changed<BlocksMotion>)>,
|
||||
>,
|
||||
motion_blockers: Query<&BlocksMotion>,
|
||||
) {
|
||||
for (entity, coordinates, _) in query.iter() {
|
||||
for (map, mut motion_blocked) in map.iter_mut() {
|
||||
let idx = coordinates.to_index(map.width());
|
||||
if let Some(prev_idx) = prev_index.get(&entity) {
|
||||
if *prev_idx == idx {
|
||||
continue;
|
||||
}
|
||||
let tile = map.base.tiles[*prev_idx];
|
||||
let mut new_motion_blocked = tile.blocks_motion();
|
||||
if !new_motion_blocked {
|
||||
for e in &map.entities[*prev_idx] {
|
||||
if motion_blockers.get(*e).is_ok() {
|
||||
new_motion_blocked = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
motion_blocked[*prev_idx] = new_motion_blocked;
|
||||
}
|
||||
motion_blocked[idx] = true;
|
||||
prev_index.insert(entity, idx);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn remove_blocks_motion(
|
||||
mut prev_index: ResMut<PreviousBlocksMotionIndex>,
|
||||
mut map: Query<(&Map, &mut MotionBlocked)>,
|
||||
removed: RemovedComponents<BlocksMotion>,
|
||||
coordinates: Query<&Coordinates>,
|
||||
blocks_motion: Query<&BlocksMotion>,
|
||||
) {
|
||||
for entity in removed.iter() {
|
||||
if let Ok(coordinates) = coordinates.get_component::<Coordinates>(entity) {
|
||||
prev_index.remove(&entity);
|
||||
for (map, mut motion_blocked) in map.iter_mut() {
|
||||
let idx = (**coordinates).to_index(map.width());
|
||||
let tile = map.base.tiles[idx];
|
||||
let mut new_motion_blocked = tile.blocks_motion();
|
||||
for e in &map.entities[idx] {
|
||||
new_motion_blocked = new_motion_blocked
|
||||
|| blocks_motion.get_component::<BlocksMotion>(*e).is_ok();
|
||||
}
|
||||
motion_blocked[idx] = new_motion_blocked;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default, Deref, DerefMut)]
|
||||
struct PreviousMonitorsCollisionsIndex(HashMap<Entity, usize>);
|
||||
|
||||
fn monitors_collisions_indexing(
|
||||
mut map: Query<(&Map, &mut CollisionsMonitored)>,
|
||||
mut prev_index: ResMut<PreviousMonitorsCollisionsIndex>,
|
||||
query: Query<
|
||||
(Entity, &Coordinates, &MonitorsCollisions),
|
||||
Or<(Changed<Coordinates>, Changed<MonitorsCollisions>)>,
|
||||
>,
|
||||
collision_monitors: Query<&MonitorsCollisions>,
|
||||
) {
|
||||
for (entity, coordinates, _) in query.iter() {
|
||||
for (map, mut collisions_monitored) in map.iter_mut() {
|
||||
let idx = coordinates.to_index(map.width());
|
||||
if let Some(prev_idx) = prev_index.get(&entity) {
|
||||
if *prev_idx == idx {
|
||||
continue;
|
||||
}
|
||||
let mut new_collisions_monitored = false;
|
||||
for e in &map.entities[*prev_idx] {
|
||||
if collision_monitors.get(*e).is_ok() {
|
||||
new_collisions_monitored = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
collisions_monitored[*prev_idx] = new_collisions_monitored;
|
||||
}
|
||||
collisions_monitored[idx] = true;
|
||||
prev_index.insert(entity, idx);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn remove_monitors_collisions(
|
||||
mut prev_index: ResMut<PreviousMonitorsCollisionsIndex>,
|
||||
mut map: Query<(&Map, &mut CollisionsMonitored)>,
|
||||
removed: RemovedComponents<MonitorsCollisions>,
|
||||
coordinates: Query<&Coordinates>,
|
||||
monitors_collisions: Query<&MonitorsCollisions>,
|
||||
) {
|
||||
for entity in removed.iter() {
|
||||
if let Ok(coordinates) = coordinates.get_component::<Coordinates>(entity) {
|
||||
prev_index.remove(&entity);
|
||||
for (map, mut collisions_monitored) in map.iter_mut() {
|
||||
let idx = (**coordinates).to_index(map.width());
|
||||
let mut new_collisions_monitored = false;
|
||||
for e in &map.entities[idx] {
|
||||
new_collisions_monitored = new_collisions_monitored
|
||||
|| monitors_collisions
|
||||
.get_component::<MonitorsCollisions>(*e)
|
||||
.is_ok();
|
||||
}
|
||||
collisions_monitored[idx] = new_collisions_monitored;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn add_collision_indices(
|
||||
mut commands: Commands,
|
||||
query: Query<
|
||||
(Entity, &Map),
|
||||
(
|
||||
Added<Map>,
|
||||
Without<MotionBlocked>,
|
||||
Without<CollisionsMonitored>,
|
||||
),
|
||||
>,
|
||||
) {
|
||||
for (entity, map) in query.iter() {
|
||||
let mut v = vec![];
|
||||
for tile in &map.base.tiles {
|
||||
v.push(tile.blocks_motion());
|
||||
}
|
||||
commands.entity(entity).insert(MotionBlocked(v));
|
||||
let count = (map.width() * map.height()) as usize;
|
||||
commands
|
||||
.entity(entity)
|
||||
.insert(CollisionsMonitored(vec![false; count]));
|
||||
}
|
||||
}
|
||||
|
||||
fn speak_direction(
|
||||
mut tts: ResMut<Tts>,
|
||||
mut cache: Local<HashMap<Entity, CardinalDirection>>,
|
||||
player: Query<(Entity, &Player, &Transform), Changed<Transform>>,
|
||||
) -> Result<(), Box<dyn Error>> {
|
||||
if let Ok((entity, _, transform)) = player.single() {
|
||||
let forward = transform.local_x();
|
||||
let yaw = Angle::Radians(forward.y.atan2(forward.x));
|
||||
if let Some(old_direction) = cache.get(&entity) {
|
||||
let old_direction = *old_direction;
|
||||
let direction: CardinalDirection = yaw.into();
|
||||
if old_direction != direction {
|
||||
let direction: String = direction.into();
|
||||
tts.speak(direction, false)?;
|
||||
}
|
||||
cache.insert(entity, direction);
|
||||
} else {
|
||||
cache.insert(entity, yaw.into());
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub const MOVEMENT_LABEL: &str = "MOVEMENT";
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct NavigationConfig<S> {
|
||||
pub movement_states: Vec<S>,
|
||||
pub movement_control_states: Vec<S>,
|
||||
}
|
||||
|
||||
impl<S> Default for NavigationConfig<S> {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
movement_states: vec![],
|
||||
movement_control_states: vec![],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct NavigationPlugin<'a, S>(std::marker::PhantomData<&'a S>);
|
||||
|
||||
impl<'a, S> Default for NavigationPlugin<'a, S> {
|
||||
fn default() -> Self {
|
||||
Self(std::marker::PhantomData)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, S> Plugin for NavigationPlugin<'a, S>
|
||||
where
|
||||
S: bevy::ecs::component::Component + Clone + Debug + Eq + Hash,
|
||||
'a: 'static,
|
||||
{
|
||||
fn build(&self, app: &mut AppBuilder) {
|
||||
if !app.world().contains_resource::<NavigationConfig<S>>() {
|
||||
app.insert_resource(NavigationConfig::<S>::default());
|
||||
}
|
||||
let config = app
|
||||
.world()
|
||||
.get_resource::<NavigationConfig<S>>()
|
||||
.unwrap()
|
||||
.clone();
|
||||
app.register_type::<MaxSpeed>()
|
||||
.register_type::<RotationSpeed>()
|
||||
.register_type::<Sprinting>()
|
||||
.add_event::<Collision>()
|
||||
.insert_resource(PreviousBlocksMotionIndex::default())
|
||||
.add_system_to_stage(
|
||||
CoreStage::PostUpdate,
|
||||
blocks_motion_indexing
|
||||
.system()
|
||||
.after(crate::map::UPDATE_ENTITY_INDEX_LABEL)
|
||||
.label(UPDATE_COLLISION_INDEX_LABEL),
|
||||
)
|
||||
.add_system_to_stage(
|
||||
CoreStage::PostUpdate,
|
||||
remove_blocks_motion
|
||||
.system()
|
||||
.before(UPDATE_COLLISION_INDEX_LABEL),
|
||||
)
|
||||
.insert_resource(PreviousMonitorsCollisionsIndex::default())
|
||||
.add_system_to_stage(
|
||||
CoreStage::PostUpdate,
|
||||
monitors_collisions_indexing
|
||||
.system()
|
||||
.after(crate::map::UPDATE_ENTITY_INDEX_LABEL)
|
||||
.label(UPDATE_COLLISION_INDEX_LABEL),
|
||||
)
|
||||
.add_system_to_stage(
|
||||
CoreStage::PostUpdate,
|
||||
remove_monitors_collisions
|
||||
.system()
|
||||
.before(UPDATE_COLLISION_INDEX_LABEL),
|
||||
)
|
||||
.add_system_to_stage(
|
||||
CoreStage::PostUpdate,
|
||||
monitors_collisions_indexing
|
||||
.system()
|
||||
.after(crate::map::UPDATE_ENTITY_INDEX_LABEL)
|
||||
.label(UPDATE_COLLISION_INDEX_LABEL),
|
||||
)
|
||||
.add_system(add_collision_indices.system())
|
||||
.add_system(speak_direction.system().chain(error_handler.system()))
|
||||
.add_system_to_stage(CoreStage::PostUpdate, add_collision_indices.system());
|
||||
if config.movement_states.is_empty() {
|
||||
app.add_system(
|
||||
movement
|
||||
.system()
|
||||
.label(MOVEMENT_LABEL)
|
||||
.before(crate::map::UPDATE_ENTITY_INDEX_LABEL),
|
||||
);
|
||||
} else {
|
||||
let states = config.movement_states;
|
||||
for state in states {
|
||||
app.add_system_set(
|
||||
SystemSet::on_update(state).with_system(
|
||||
movement
|
||||
.system()
|
||||
.label(MOVEMENT_LABEL)
|
||||
.before(crate::map::UPDATE_ENTITY_INDEX_LABEL),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
if config.movement_control_states.is_empty() {
|
||||
app.add_system(movement_controls.system().before(MOVEMENT_LABEL));
|
||||
} else {
|
||||
let states = config.movement_control_states;
|
||||
for state in states {
|
||||
app.add_system_set(
|
||||
SystemSet::on_update(state)
|
||||
.with_system(movement_controls.system().before(MOVEMENT_LABEL)),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
219
src/pathfinding.rs
Normal file
219
src/pathfinding.rs
Normal file
|
@ -0,0 +1,219 @@
|
|||
use std::collections::HashMap;
|
||||
|
||||
use bevy::{prelude::*, tasks::prelude::*};
|
||||
use crossbeam_channel::{unbounded, Receiver};
|
||||
use derive_more::{Deref, DerefMut};
|
||||
use pathfinding::prelude::*;
|
||||
|
||||
use crate::{
|
||||
core::{Coordinates, PointLike},
|
||||
map::Map,
|
||||
navigation::{MotionBlocked, RotationSpeed, Speed, Velocity},
|
||||
};
|
||||
|
||||
#[derive(Clone, Copy, Debug, Default, Deref, DerefMut, Eq, Hash, PartialEq, Reflect)]
|
||||
#[reflect(Component)]
|
||||
pub struct Destination(pub (i32, i32));
|
||||
|
||||
impl_pointlike_for_tuple_component!(Destination);
|
||||
|
||||
#[derive(Clone, Debug, Default, Deref, DerefMut, Reflect)]
|
||||
#[reflect(Component)]
|
||||
pub struct Path(pub Vec<(i32, i32)>);
|
||||
|
||||
pub fn find_path(
|
||||
start: &dyn PointLike,
|
||||
destination: &dyn PointLike,
|
||||
map: &Map,
|
||||
) -> Option<(Vec<(i32, i32)>, u32)> {
|
||||
astar(
|
||||
&start.into(),
|
||||
|p| {
|
||||
let mut successors: Vec<((i32, i32), u32)> = vec![];
|
||||
for tile in map.base.get_available_exits(p.0 as usize, p.1 as usize) {
|
||||
successors.push(((tile.0 as i32, tile.1 as i32), (tile.2 * 100.) as u32));
|
||||
}
|
||||
successors
|
||||
},
|
||||
|p| (p.distance_squared(destination) * 100.) as u32,
|
||||
|p| *p == destination.into(),
|
||||
)
|
||||
}
|
||||
|
||||
fn nearest_extreme(from: f32, to: i32) -> f32 {
|
||||
let to = to as f32;
|
||||
let range = to..=(to + 0.999);
|
||||
if from <= *range.start() {
|
||||
*range.start()
|
||||
} else {
|
||||
*range.end()
|
||||
}
|
||||
}
|
||||
|
||||
fn cheat_assign(
|
||||
start: (f32, f32),
|
||||
end: (i32, i32),
|
||||
map_width: usize,
|
||||
motion_blocked: Vec<bool>,
|
||||
) -> Option<(f32, f32)> {
|
||||
let x;
|
||||
let y;
|
||||
if start.0 as i32 == end.0 {
|
||||
x = start.0;
|
||||
} else {
|
||||
x = nearest_extreme(start.0, end.0);
|
||||
}
|
||||
if start.1 as i32 == end.1 {
|
||||
y = start.1;
|
||||
} else {
|
||||
y = nearest_extreme(start.1, end.1);
|
||||
}
|
||||
let point = (x, y);
|
||||
let index = point.to_index(map_width);
|
||||
if motion_blocked[index] {
|
||||
None
|
||||
} else {
|
||||
Some(point)
|
||||
}
|
||||
}
|
||||
|
||||
fn calculate_path(
|
||||
mut commands: Commands,
|
||||
pool: Res<AsyncComputeTaskPool>,
|
||||
mut calculating: Local<HashMap<Entity, Receiver<Path>>>,
|
||||
query: Query<(Entity, &Destination, &Coordinates), Changed<Destination>>,
|
||||
destinations: Query<&Destination>,
|
||||
map: Query<&Map>,
|
||||
) {
|
||||
let calculating_clone = calculating.clone();
|
||||
for (entity, rx) in calculating_clone.iter() {
|
||||
if destinations.get(*entity).is_ok() {
|
||||
if let Ok(path) = rx.try_recv() {
|
||||
commands.entity(*entity).insert(path);
|
||||
calculating.remove(&entity);
|
||||
}
|
||||
} else {
|
||||
calculating.remove(&entity);
|
||||
}
|
||||
}
|
||||
for (entity, destination, coordinates) in query.iter() {
|
||||
if !calculating.contains_key(&entity) {
|
||||
let (tx, rx) = unbounded();
|
||||
calculating.insert(entity, rx);
|
||||
for map in map.iter() {
|
||||
let start_clone = *coordinates;
|
||||
let destination_clone = *destination;
|
||||
let map_clone = map.clone();
|
||||
let tx_clone = tx.clone();
|
||||
pool.spawn(async move {
|
||||
if let Some(result) = find_path(&start_clone, &destination_clone, &map_clone) {
|
||||
tx_clone.send(Path(result.0)).expect("Channel should exist");
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn negotiate_path(
|
||||
mut commands: Commands,
|
||||
time: Res<Time>,
|
||||
mut query: Query<(
|
||||
Entity,
|
||||
&mut Path,
|
||||
&mut Coordinates,
|
||||
&mut Velocity,
|
||||
&Speed,
|
||||
Option<&RotationSpeed>,
|
||||
&mut Transform,
|
||||
)>,
|
||||
map: Query<(&Map, &MotionBlocked)>,
|
||||
) {
|
||||
for (entity, mut path, mut coordinates, mut velocity, speed, rotation_speed, mut transform) in
|
||||
query.iter_mut()
|
||||
{
|
||||
for (map, motion_blocked) in map.iter() {
|
||||
let mut new_path = path.0.clone();
|
||||
let start_i32 = coordinates.i32();
|
||||
let new_path_clone = new_path.clone();
|
||||
let mut iter = new_path_clone.split(|p| *p == start_i32);
|
||||
if iter.next().is_some() {
|
||||
if let Some(upcoming) = iter.next() {
|
||||
new_path = vec![start_i32];
|
||||
new_path.append(&mut upcoming.to_vec());
|
||||
} else {
|
||||
let point = new_path[0];
|
||||
if let Some(new_coords) =
|
||||
cheat_assign(**coordinates, point, map.width(), motion_blocked.0.clone())
|
||||
{
|
||||
**coordinates = new_coords;
|
||||
}
|
||||
}
|
||||
}
|
||||
**path = new_path;
|
||||
if path.len() >= 2 {
|
||||
let start = **coordinates;
|
||||
let start_index = start.to_index(map.width());
|
||||
let start = Vec2::new(start.0, start.1);
|
||||
let next = path[1];
|
||||
if motion_blocked[next.to_index(map.width())] {
|
||||
// TODO: Should probably handle.
|
||||
}
|
||||
let next = Vec2::new(next.0 as f32, next.1 as f32);
|
||||
if rotation_speed.is_some() {
|
||||
let start = start.floor();
|
||||
let v = next - start;
|
||||
let angle = v.y.atan2(v.x);
|
||||
transform.rotation = Quat::from_rotation_z(angle);
|
||||
}
|
||||
let mut direction = next - start;
|
||||
direction = direction.normalize();
|
||||
direction *= speed.0;
|
||||
let displacement = direction * time.delta_seconds();
|
||||
let dest = start + displacement;
|
||||
let dest = (dest.x, dest.y);
|
||||
let index = dest.to_index(map.width());
|
||||
if start_index != index && motion_blocked[index] {
|
||||
let (normal_x, normal_y) = **coordinates;
|
||||
let next = path[1];
|
||||
if let Some((cheat_x, cheat_y)) =
|
||||
cheat_assign(**coordinates, next, map.width(), motion_blocked.0.clone())
|
||||
{
|
||||
let index = (normal_x, cheat_y).to_index(map.width());
|
||||
if !motion_blocked.0[index] {
|
||||
**coordinates = (normal_x, cheat_y);
|
||||
return;
|
||||
}
|
||||
let index = (cheat_x, normal_y).to_index(map.width());
|
||||
if !motion_blocked.0[index] {
|
||||
**coordinates = (cheat_x, normal_y);
|
||||
return;
|
||||
}
|
||||
**coordinates = (cheat_x, cheat_y);
|
||||
}
|
||||
**velocity = Vec2::ZERO;
|
||||
} else {
|
||||
**velocity = direction;
|
||||
}
|
||||
} else {
|
||||
commands.entity(entity).remove::<Path>();
|
||||
commands.entity(entity).remove::<Destination>();
|
||||
**velocity = Vec2::ZERO;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct PathfindingPlugin;
|
||||
|
||||
impl Plugin for PathfindingPlugin {
|
||||
fn build(&self, app: &mut AppBuilder) {
|
||||
app.add_system_to_stage(CoreStage::PostUpdate, calculate_path.system())
|
||||
.add_system(
|
||||
negotiate_path
|
||||
.system()
|
||||
.before(crate::navigation::MOVEMENT_LABEL),
|
||||
);
|
||||
}
|
||||
}
|
333
src/sound.rs
Normal file
333
src/sound.rs
Normal file
|
@ -0,0 +1,333 @@
|
|||
use std::{collections::HashMap, fmt::Debug, hash::Hash, time::Duration};
|
||||
|
||||
use bevy::{
|
||||
asset::{HandleId, LoadState},
|
||||
ecs::component::Component,
|
||||
prelude::*,
|
||||
transform::TransformSystem,
|
||||
};
|
||||
use bevy_openal::{Buffer, Context, Sound, SoundState};
|
||||
|
||||
use rand::random;
|
||||
|
||||
use crate::{
|
||||
core::{Coordinates, CoreConfig, Player, PointLike},
|
||||
exploration::ExplorationFocused,
|
||||
visibility::Viewshed,
|
||||
};
|
||||
|
||||
#[derive(Clone, Debug, Reflect)]
|
||||
#[reflect(Component)]
|
||||
pub struct Footstep {
|
||||
pub sound: HandleId,
|
||||
pub step_length: f32,
|
||||
pub gain: f32,
|
||||
pub reference_distance: f32,
|
||||
pub max_distance: f32,
|
||||
pub rolloff_factor: f32,
|
||||
pub pitch_variation: Option<f32>,
|
||||
}
|
||||
|
||||
impl Default for Footstep {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
sound: "".into(),
|
||||
step_length: 0.8,
|
||||
gain: 0.05,
|
||||
reference_distance: 1.,
|
||||
max_distance: f32::MAX,
|
||||
rolloff_factor: 1.,
|
||||
pitch_variation: Some(0.15),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct SoundIcon {
|
||||
pub sound: HandleId,
|
||||
pub gain: f32,
|
||||
pub pitch: f32,
|
||||
pub reference_distance: f32,
|
||||
pub max_distance: f32,
|
||||
pub rolloff_factor: f32,
|
||||
pub interval: Option<Timer>,
|
||||
}
|
||||
|
||||
impl Default for SoundIcon {
|
||||
fn default() -> Self {
|
||||
let seconds = random::<f32>() + 4.5;
|
||||
let mut icon = Self {
|
||||
sound: "".into(),
|
||||
gain: 0.3,
|
||||
pitch: 1.,
|
||||
reference_distance: 1.,
|
||||
max_distance: f32::MAX,
|
||||
rolloff_factor: 1.,
|
||||
interval: Some(Timer::from_seconds(seconds, true)),
|
||||
};
|
||||
if let Some(ref mut interval) = icon.interval {
|
||||
let seconds = Duration::from_secs_f32(seconds - 0.1);
|
||||
interval.set_elapsed(seconds);
|
||||
}
|
||||
icon
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Bundle, Default)]
|
||||
pub struct FootstepBundle {
|
||||
pub footstep: Footstep,
|
||||
pub transform: Transform,
|
||||
pub global_transform: GlobalTransform,
|
||||
}
|
||||
|
||||
#[derive(Bundle, Clone, Debug, Default)]
|
||||
pub struct SoundIconBundle {
|
||||
pub sound_icon: SoundIcon,
|
||||
pub transform: Transform,
|
||||
pub global_transform: GlobalTransform,
|
||||
}
|
||||
|
||||
fn footstep(
|
||||
mut commands: Commands,
|
||||
assets: Res<Assets<Buffer>>,
|
||||
mut last_step_distance: Local<HashMap<Entity, (f32, Coordinates)>>,
|
||||
footsteps: Query<(Entity, &Footstep, &Parent, Option<&Children>), Changed<GlobalTransform>>,
|
||||
coordinates_storage: Query<&Coordinates>,
|
||||
mut sounds: Query<&mut Sound>,
|
||||
) {
|
||||
for (entity, footstep, parent, children) in footsteps.iter() {
|
||||
let coordinates = coordinates_storage.get(**parent).unwrap();
|
||||
if let Some(children) = children {
|
||||
if let Some(last) = last_step_distance.get(&entity) {
|
||||
let distance = last.0 + (last.1.distance(coordinates));
|
||||
if distance >= footstep.step_length {
|
||||
last_step_distance.insert(entity, (0., *coordinates));
|
||||
let sound = children[0];
|
||||
if let Ok(mut sound) = sounds.get_mut(sound) {
|
||||
sound.gain = footstep.gain;
|
||||
sound.reference_distance = footstep.reference_distance;
|
||||
sound.max_distance = footstep.max_distance;
|
||||
sound.rolloff_factor = footstep.rolloff_factor;
|
||||
if let Some(pitch_variation) = footstep.pitch_variation {
|
||||
let mut pitch = 1. - pitch_variation / 2.;
|
||||
pitch += random::<f32>() * pitch_variation;
|
||||
sound.pitch = pitch;
|
||||
}
|
||||
sound.play();
|
||||
}
|
||||
} else if last.1 != *coordinates {
|
||||
last_step_distance.insert(entity, (distance, *coordinates));
|
||||
}
|
||||
} else {
|
||||
last_step_distance.insert(entity, (0., *coordinates));
|
||||
}
|
||||
} else {
|
||||
let buffer = assets.get_handle(footstep.sound);
|
||||
let sound = Sound {
|
||||
buffer,
|
||||
state: SoundState::Stopped,
|
||||
gain: footstep.gain,
|
||||
..Default::default()
|
||||
};
|
||||
let child = commands
|
||||
.spawn()
|
||||
.insert(sound)
|
||||
.insert(Transform::default())
|
||||
.insert(GlobalTransform::default())
|
||||
.id();
|
||||
commands.entity(entity).push_children(&[child]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn sound_icon<S>(
|
||||
mut commands: Commands,
|
||||
config: Res<SoundConfig<S>>,
|
||||
state: Res<State<S>>,
|
||||
time: Res<Time>,
|
||||
asset_server: Res<AssetServer>,
|
||||
viewers: Query<(&Player, &Viewshed)>,
|
||||
mut icons: Query<(
|
||||
Entity,
|
||||
&mut SoundIcon,
|
||||
Option<&Coordinates>,
|
||||
Option<&Parent>,
|
||||
Option<&Children>,
|
||||
)>,
|
||||
coordinates_storage: Query<&Coordinates>,
|
||||
mut sounds: Query<&mut Sound>,
|
||||
) where
|
||||
S: Component + Clone + Debug + Eq + Hash,
|
||||
{
|
||||
if !(*config).sound_icon_states.is_empty() {
|
||||
if !config.sound_icon_states.contains(state.current()) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
for (_, viewer) in viewers.iter() {
|
||||
for (entity, mut icon, coordinates, parent, children) in icons.iter_mut() {
|
||||
let coords = if let Some(coordinates) = coordinates {
|
||||
*coordinates
|
||||
} else if let Some(parent) = parent {
|
||||
*coordinates_storage
|
||||
.get(**parent)
|
||||
.expect("If `SoundIcon` is a child, its parent must have `Coordinates`")
|
||||
} else {
|
||||
panic!("No `Coordinates` on `SoundIcon` or parent");
|
||||
};
|
||||
if viewer.is_visible(&coords) {
|
||||
let buffer = asset_server.get_handle(icon.sound);
|
||||
if asset_server.get_load_state(&buffer) == LoadState::Loaded {
|
||||
let looping = icon.interval.is_none();
|
||||
let sound = Sound {
|
||||
buffer,
|
||||
gain: icon.gain,
|
||||
pitch: icon.pitch,
|
||||
looping,
|
||||
state: SoundState::Playing,
|
||||
..Default::default()
|
||||
};
|
||||
if looping && children.is_none() {
|
||||
let child = commands
|
||||
.spawn()
|
||||
.insert(sound)
|
||||
.insert(Transform::default())
|
||||
.insert(GlobalTransform::default())
|
||||
.id();
|
||||
commands.entity(entity).push_children(&[child]);
|
||||
} else if let Some(ref mut interval) = icon.interval {
|
||||
interval.tick(time.delta());
|
||||
if interval.finished() {
|
||||
if let Some(children) = children {
|
||||
for child in children.iter() {
|
||||
commands.entity(*child).despawn();
|
||||
}
|
||||
}
|
||||
let child = commands
|
||||
.spawn()
|
||||
.insert(sound)
|
||||
.insert(Transform::default())
|
||||
.insert(GlobalTransform::default())
|
||||
.id();
|
||||
commands.entity(entity).push_children(&[child]);
|
||||
interval.reset();
|
||||
}
|
||||
}
|
||||
if let Some(children) = children {
|
||||
if let Some(child) = children.get(0) {
|
||||
if let Ok(mut sound) = sounds.get_mut(*child) {
|
||||
let buffer = asset_server.get_handle(icon.sound);
|
||||
if sound.buffer != buffer {
|
||||
sound.stop();
|
||||
sound.buffer = buffer;
|
||||
sound.play();
|
||||
}
|
||||
sound.gain = icon.gain;
|
||||
sound.pitch = icon.pitch;
|
||||
sound.reference_distance = icon.reference_distance;
|
||||
sound.max_distance = icon.max_distance;
|
||||
sound.rolloff_factor = icon.rolloff_factor;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn sound_icon_exploration_focus_changed(
|
||||
mut focused: Query<(&ExplorationFocused, Option<&mut SoundIcon>), Changed<ExplorationFocused>>,
|
||||
) {
|
||||
for (_, icon) in focused.iter_mut() {
|
||||
if let Some(mut icon) = icon {
|
||||
icon.gain *= 3.;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn sound_icon_exploration_focus_removed(
|
||||
removed: RemovedComponents<ExplorationFocused>,
|
||||
mut icons: Query<&mut SoundIcon>,
|
||||
) {
|
||||
for entity in removed.iter() {
|
||||
if let Ok(mut icon) = icons.get_component_mut::<SoundIcon>(entity) {
|
||||
icon.gain /= 3.;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn scale_sounds(config: Res<CoreConfig>, mut sounds: Query<&mut Sound>) {
|
||||
let pixels_per_unit = config.pixels_per_unit as f32;
|
||||
for mut sound in sounds.iter_mut() {
|
||||
sound.reference_distance *= pixels_per_unit;
|
||||
if sound.max_distance != f32::MAX {
|
||||
sound.max_distance *= pixels_per_unit;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct SoundConfig<S> {
|
||||
pub sound_icon_states: Vec<S>,
|
||||
}
|
||||
|
||||
impl<S> Default for SoundConfig<S> {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
sound_icon_states: vec![],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct SoundPlugin<'a, S>(std::marker::PhantomData<&'a S>);
|
||||
|
||||
impl<'a, S> Default for SoundPlugin<'a, S> {
|
||||
fn default() -> Self {
|
||||
Self(std::marker::PhantomData)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, S> Plugin for SoundPlugin<'a, S>
|
||||
where
|
||||
S: bevy::ecs::component::Component + Clone + Debug + Eq + Hash,
|
||||
'a: 'static,
|
||||
{
|
||||
fn build(&self, app: &mut AppBuilder) {
|
||||
if !app.world().contains_resource::<SoundConfig<S>>() {
|
||||
app.insert_resource(SoundConfig::<S>::default());
|
||||
}
|
||||
const SOUND_ICON_AND_EXPLORATION_STAGE: &str = "sound_icon_and_exploration";
|
||||
let core_config = *app.world().get_resource::<CoreConfig>().unwrap();
|
||||
if let Some(context) = app.world().get_resource::<Context>() {
|
||||
context
|
||||
.set_meters_per_unit(1. / core_config.pixels_per_unit as f32)
|
||||
.unwrap();
|
||||
}
|
||||
app.register_type::<Footstep>()
|
||||
.add_system_to_stage(
|
||||
CoreStage::PostUpdate,
|
||||
footstep.system().after(TransformSystem::TransformPropagate),
|
||||
)
|
||||
.add_system_to_stage(
|
||||
CoreStage::PostUpdate,
|
||||
sound_icon::<S>
|
||||
.system()
|
||||
.after(TransformSystem::TransformPropagate),
|
||||
)
|
||||
.add_stage_after(
|
||||
CoreStage::PostUpdate,
|
||||
SOUND_ICON_AND_EXPLORATION_STAGE,
|
||||
SystemStage::parallel(),
|
||||
)
|
||||
.add_system_to_stage(
|
||||
SOUND_ICON_AND_EXPLORATION_STAGE,
|
||||
sound_icon_exploration_focus_changed.system(),
|
||||
)
|
||||
.add_system_to_stage(
|
||||
SOUND_ICON_AND_EXPLORATION_STAGE,
|
||||
sound_icon_exploration_focus_removed.system(),
|
||||
)
|
||||
.add_system(scale_sounds.system());
|
||||
}
|
||||
}
|
324
src/visibility.rs
Normal file
324
src/visibility.rs
Normal file
|
@ -0,0 +1,324 @@
|
|||
use std::collections::{HashMap, HashSet};
|
||||
|
||||
use bevy::prelude::*;
|
||||
use coord_2d::{Coord, Size};
|
||||
use derive_more::{Deref, DerefMut};
|
||||
use shadowcast::{vision_distance, Context, InputGrid};
|
||||
|
||||
use crate::{
|
||||
core::{Coordinates, Player, PointLike},
|
||||
log::Log,
|
||||
map::{ITileType, Map, MapConfig},
|
||||
};
|
||||
|
||||
#[derive(Clone, Copy, Debug, Default, Reflect)]
|
||||
#[reflect(Component)]
|
||||
pub struct BlocksVisibility;
|
||||
|
||||
#[derive(Clone, Debug, Default, Deref, DerefMut, Reflect)]
|
||||
#[reflect(Component)]
|
||||
pub struct RevealedTiles(pub Vec<bool>);
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct Viewshed {
|
||||
pub visible: HashSet<(i32, i32)>,
|
||||
pub range: u32,
|
||||
}
|
||||
|
||||
impl Default for Viewshed {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
range: 15,
|
||||
visible: HashSet::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
impl Viewshed {
|
||||
pub fn is_visible(&self, point: &dyn PointLike) -> bool {
|
||||
self.visible.contains(&point.into())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default, Deref, DerefMut, Reflect)]
|
||||
#[reflect(Component)]
|
||||
pub struct VisibilityBlocked(pub Vec<bool>);
|
||||
|
||||
#[derive(Clone, Debug, Default, Deref, DerefMut, Reflect)]
|
||||
#[reflect(Component)]
|
||||
pub struct VisibleTiles(pub Vec<bool>);
|
||||
|
||||
fn add_visibility_indices(
|
||||
mut commands: Commands,
|
||||
query: Query<
|
||||
(Entity, &Map),
|
||||
(
|
||||
Added<Map>,
|
||||
Without<VisibilityBlocked>,
|
||||
Without<VisibleTiles>,
|
||||
Without<RevealedTiles>,
|
||||
),
|
||||
>,
|
||||
map_config: Res<MapConfig>,
|
||||
) {
|
||||
for (entity, map) in query.iter() {
|
||||
let mut v = vec![];
|
||||
for tile in &map.base.tiles {
|
||||
v.push(tile.blocks_visibility());
|
||||
}
|
||||
commands.entity(entity).insert(VisibilityBlocked(v));
|
||||
let count = map.count();
|
||||
commands
|
||||
.entity(entity)
|
||||
.insert(VisibleTiles(vec![false; count]));
|
||||
commands
|
||||
.entity(entity)
|
||||
.insert(RevealedTiles(vec![map_config.start_revealed; count]));
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default, Deref, DerefMut)]
|
||||
struct PreviousIndex(HashMap<Entity, usize>);
|
||||
|
||||
fn map_visibility_indexing(
|
||||
mut map: Query<(&Map, &mut VisibilityBlocked)>,
|
||||
mut prev_index: ResMut<PreviousIndex>,
|
||||
query: Query<
|
||||
(Entity, &Coordinates, &BlocksVisibility),
|
||||
Or<(Changed<Coordinates>, Changed<BlocksVisibility>)>,
|
||||
>,
|
||||
visibility_blockers: Query<&BlocksVisibility>,
|
||||
) {
|
||||
for (entity, coordinates, _) in query.iter() {
|
||||
for (map, mut visibility_blocked) in map.iter_mut() {
|
||||
let idx = coordinates.to_index(map.width());
|
||||
if let Some(prev_idx) = prev_index.get(&entity) {
|
||||
if *prev_idx == idx {
|
||||
continue;
|
||||
}
|
||||
let tile = map.base.tiles[*prev_idx];
|
||||
let mut new_visibility_blocked = tile.blocks_visibility();
|
||||
if !new_visibility_blocked {
|
||||
for e in &map.entities[*prev_idx] {
|
||||
if visibility_blockers.get(*e).is_ok() {
|
||||
new_visibility_blocked = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
visibility_blocked[*prev_idx] = new_visibility_blocked;
|
||||
}
|
||||
visibility_blocked[idx] = true;
|
||||
prev_index.insert(entity, idx);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn remove_blocks_visibility(
|
||||
mut prev_index: ResMut<PreviousIndex>,
|
||||
mut map: Query<(&Map, &mut VisibilityBlocked)>,
|
||||
removed: RemovedComponents<BlocksVisibility>,
|
||||
coordinates: Query<&Coordinates>,
|
||||
blocks_visibility: Query<&BlocksVisibility>,
|
||||
) {
|
||||
for entity in removed.iter() {
|
||||
if let Ok(coordinates) = coordinates.get_component::<Coordinates>(entity) {
|
||||
prev_index.remove(&entity);
|
||||
for (map, mut visibility_blocked) in map.iter_mut() {
|
||||
let idx = (**coordinates).to_index(map.width());
|
||||
let tile = map.base.tiles[idx];
|
||||
let mut new_visibility_blocked = tile.blocks_visibility();
|
||||
for e in &map.entities[idx] {
|
||||
new_visibility_blocked = new_visibility_blocked
|
||||
|| blocks_visibility
|
||||
.get_component::<BlocksVisibility>(*e)
|
||||
.is_ok();
|
||||
}
|
||||
visibility_blocked[idx] = new_visibility_blocked;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct VisibilityGrid(Map, VisibilityBlocked);
|
||||
|
||||
impl InputGrid for VisibilityGrid {
|
||||
type Grid = VisibilityGrid;
|
||||
|
||||
type Opacity = u8;
|
||||
|
||||
fn size(&self, grid: &Self::Grid) -> Size {
|
||||
Size::new(grid.0.width() as u32, grid.0.height() as u32)
|
||||
}
|
||||
|
||||
fn get_opacity(&self, grid: &Self::Grid, coord: Coord) -> Self::Opacity {
|
||||
let point = (coord.x, coord.y);
|
||||
let index = point.to_index(grid.0.width());
|
||||
if grid.1 .0[index] {
|
||||
255
|
||||
} else {
|
||||
0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn update_viewshed(
|
||||
mut viewers: Query<
|
||||
(&mut Viewshed, &Coordinates),
|
||||
Or<(Changed<VisibilityBlocked>, Changed<Coordinates>)>,
|
||||
>,
|
||||
map: Query<(&Map, &VisibilityBlocked)>,
|
||||
) {
|
||||
for (mut viewshed, start) in viewers.iter_mut() {
|
||||
for (map, visibility_blocked) in map.iter() {
|
||||
let mut context: Context<u8> = Context::default();
|
||||
let vision_distance = vision_distance::Circle::new(viewshed.range);
|
||||
let coord = Coord::new(start.x_i32(), start.y_i32());
|
||||
viewshed.visible.clear();
|
||||
let visibility_grid = VisibilityGrid(map.clone(), visibility_blocked.clone());
|
||||
context.for_each_visible(
|
||||
coord,
|
||||
&visibility_grid,
|
||||
&visibility_grid,
|
||||
vision_distance,
|
||||
255,
|
||||
|coord, _directions, _visibility| {
|
||||
viewshed.visible.insert((coord.x, coord.y));
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn map_visibility(
|
||||
mut map: Query<
|
||||
(
|
||||
&Map,
|
||||
&VisibilityBlocked,
|
||||
&mut RevealedTiles,
|
||||
&mut VisibleTiles,
|
||||
),
|
||||
Or<(Changed<Map>, Changed<VisibilityBlocked>)>,
|
||||
>,
|
||||
viewers: Query<(&Player, &Viewshed)>,
|
||||
) {
|
||||
for (_, viewshed) in viewers.iter() {
|
||||
for (map, _, mut revealed_tiles, mut visible_tiles) in map.iter_mut() {
|
||||
for t in visible_tiles.iter_mut() {
|
||||
*t = false
|
||||
}
|
||||
for v in viewshed.visible.iter() {
|
||||
let idx = (*v).to_index(map.width());
|
||||
revealed_tiles[idx] = true;
|
||||
visible_tiles[idx] = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn log_visible(
|
||||
time: Res<Time>,
|
||||
mut seen: Local<HashSet<Entity>>,
|
||||
mut recently_lost: Local<HashMap<Entity, Timer>>,
|
||||
mut log: Query<&mut Log>,
|
||||
viewers: Query<(&Viewshed, &Coordinates, &Player)>,
|
||||
map: Query<&Map>,
|
||||
names: Query<&Name>,
|
||||
players: Query<&Player>,
|
||||
) {
|
||||
for timer in recently_lost.values_mut() {
|
||||
timer.tick(time.delta());
|
||||
}
|
||||
let recently_lost_clone = recently_lost.clone();
|
||||
for (entity, timer) in recently_lost_clone.iter() {
|
||||
if timer.finished() {
|
||||
recently_lost.remove(&entity);
|
||||
}
|
||||
}
|
||||
let mut new_seen = HashSet::new();
|
||||
if let Ok(mut log) = log.single_mut() {
|
||||
for (viewshed, coordinates, _) in viewers.iter() {
|
||||
for viewed_coordinates in &viewshed.visible {
|
||||
for map in map.iter() {
|
||||
let index = viewed_coordinates.to_index(map.width());
|
||||
for entity in &map.entities[index] {
|
||||
if recently_lost.contains_key(&entity) {
|
||||
continue;
|
||||
}
|
||||
if let Ok(name) = names.get(*entity) {
|
||||
if players.get(*entity).is_err() {
|
||||
if !seen.contains(&*entity) {
|
||||
let name = name.to_string();
|
||||
let location =
|
||||
coordinates.distance_and_direction(viewed_coordinates);
|
||||
log.push(format!("{}: {}", name, location));
|
||||
}
|
||||
new_seen.insert(*entity);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
let recently_lost_entities = seen.difference(&new_seen);
|
||||
for entity in recently_lost_entities {
|
||||
recently_lost.insert(*entity, Timer::from_seconds(2., false));
|
||||
}
|
||||
*seen = new_seen;
|
||||
}
|
||||
|
||||
pub const LOG_VISIBLE_LABEL: &str = "LOG_VISIBLE";
|
||||
|
||||
pub struct VisibilityPlugin;
|
||||
|
||||
impl Plugin for VisibilityPlugin {
|
||||
fn build(&self, app: &mut AppBuilder) {
|
||||
const UPDATE_VISIBILITY_INDEX: &str = "UPDATE_VISIBILITY_INDEX";
|
||||
const UPDATE_VIEWSHED: &str = "UPDATE_VIEWSHED";
|
||||
const MAP_VISIBILITY: &str = "MAP_VISIBILITY";
|
||||
app.insert_resource(PreviousIndex::default())
|
||||
.add_system(add_visibility_indices.system())
|
||||
.add_system_to_stage(
|
||||
CoreStage::PostUpdate,
|
||||
add_visibility_indices
|
||||
.system()
|
||||
.before(UPDATE_VISIBILITY_INDEX),
|
||||
)
|
||||
.add_system_to_stage(
|
||||
CoreStage::PostUpdate,
|
||||
remove_blocks_visibility
|
||||
.system()
|
||||
.before(UPDATE_VISIBILITY_INDEX),
|
||||
)
|
||||
.add_system_to_stage(
|
||||
CoreStage::PostUpdate,
|
||||
map_visibility_indexing
|
||||
.system()
|
||||
.label(UPDATE_VISIBILITY_INDEX)
|
||||
.after(crate::map::UPDATE_ENTITY_INDEX_LABEL),
|
||||
)
|
||||
.add_system_to_stage(
|
||||
CoreStage::PostUpdate,
|
||||
update_viewshed
|
||||
.system()
|
||||
.label(UPDATE_VIEWSHED)
|
||||
.after(UPDATE_VISIBILITY_INDEX),
|
||||
)
|
||||
.add_system_to_stage(
|
||||
CoreStage::PostUpdate,
|
||||
map_visibility
|
||||
.system()
|
||||
.label(MAP_VISIBILITY)
|
||||
.after(UPDATE_VIEWSHED),
|
||||
)
|
||||
.add_system_to_stage(
|
||||
CoreStage::PostUpdate,
|
||||
log_visible
|
||||
.system()
|
||||
.label(LOG_VISIBLE_LABEL)
|
||||
.after(MAP_VISIBILITY),
|
||||
);
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user