Initial commit.

This commit is contained in:
Nolan Darilek 2021-05-13 12:25:45 -05:00
commit 3332d0a88d
12 changed files with 2738 additions and 0 deletions

2
.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
target
Cargo.lock

35
Cargo.toml Normal file
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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;