357 lines
12 KiB
Rust
357 lines
12 KiB
Rust
use std::collections::{HashMap, HashSet};
|
|
|
|
use bevy::prelude::*;
|
|
use bevy_rapier2d::prelude::*;
|
|
use derive_more::{Deref, DerefMut};
|
|
pub use mapgen::Map;
|
|
use mapgen::{geometry::Rect as MRect, 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,
|
|
visibility::BlocksVisibility,
|
|
};
|
|
|
|
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, Debug, Default, Reflect)]
|
|
#[reflect(Component)]
|
|
pub struct MapObstruction;
|
|
|
|
#[derive(Clone, Debug, Default, Reflect)]
|
|
#[reflect(Component)]
|
|
pub struct Portal;
|
|
|
|
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_portals: bool,
|
|
pub describe_undescribed_areas: bool,
|
|
pub speak_area_descriptions: bool,
|
|
pub start_revealed: bool,
|
|
}
|
|
|
|
impl Default for MapConfig {
|
|
fn default() -> Self {
|
|
Self {
|
|
autospawn_portals: true,
|
|
describe_undescribed_areas: false,
|
|
speak_area_descriptions: true,
|
|
start_revealed: false,
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Bundle)]
|
|
pub struct PortalBundle {
|
|
pub coordinates: Coordinates,
|
|
pub portal: Portal,
|
|
pub exploration_type: ExplorationType,
|
|
pub mappable: Mappable,
|
|
pub transform: Transform,
|
|
pub global_transform: GlobalTransform,
|
|
}
|
|
|
|
impl Default for PortalBundle {
|
|
fn default() -> Self {
|
|
Self {
|
|
coordinates: Default::default(),
|
|
portal: Default::default(),
|
|
exploration_type: ExplorationType::Portal,
|
|
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: &Map) -> Map {
|
|
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 add_map_colliders(mut commands: Commands, maps: Query<(Entity, &Map), Added<Map>>) {
|
|
for (map_entity, map) in maps.iter() {
|
|
let rigid_body_entity = commands
|
|
.entity(map_entity)
|
|
.insert_bundle(RigidBodyBundle {
|
|
body_type: RigidBodyType::Static,
|
|
..Default::default()
|
|
})
|
|
.id();
|
|
for x in 0..map.width {
|
|
for y in 0..map.height {
|
|
let tile = map.at(x, y);
|
|
if tile.blocks_motion() {
|
|
let id = commands
|
|
.spawn_bundle(ColliderBundle {
|
|
shape: ColliderShape::cuboid(0.5, 0.5),
|
|
..Default::default()
|
|
})
|
|
.insert(ColliderParent {
|
|
handle: rigid_body_entity.handle(),
|
|
pos_wrt_parent: Vec2::new(x as f32 + 0.5, y as f32 + 0.5).into(),
|
|
})
|
|
.insert(MapObstruction)
|
|
.id();
|
|
if tile.blocks_visibility() {
|
|
commands.entity(id).insert(BlocksVisibility);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
fn portal_spawner(
|
|
mut commands: Commands,
|
|
map: Query<(Entity, &Map), Added<Map>>,
|
|
config: Res<MapConfig>,
|
|
) {
|
|
for (entity, map) in map.iter() {
|
|
if config.autospawn_portals {
|
|
let mut portals: Vec<(f32, f32)> = vec![];
|
|
for x in 1..map.width {
|
|
for y in 1..map.height {
|
|
let mut spawn_portal = false;
|
|
if map.get_available_exits(x, y).len() > 2 {
|
|
let idx = (x, y).to_index(map.width);
|
|
if map.tiles[idx] == TileType::Floor
|
|
&& (x > 1 && map.tiles[idx - 1] == TileType::Floor)
|
|
&& (x < map.width - 2 && map.tiles[idx + 1] == TileType::Floor)
|
|
&& (y > 1 && map.tiles[idx - map.width] == TileType::Wall)
|
|
&& (y < map.height - 2 && map.tiles[idx + map.width] == TileType::Wall)
|
|
{
|
|
spawn_portal = true;
|
|
}
|
|
if map.tiles[idx] == TileType::Floor
|
|
&& (x > 1 && map.tiles[idx - 1] == TileType::Wall)
|
|
&& (x < map.width - 2 && map.tiles[idx + 1] == TileType::Wall)
|
|
&& (y > 1 && map.tiles[idx - map.width] == TileType::Floor)
|
|
&& (y < map.height - 2 && map.tiles[idx + map.width] == TileType::Floor)
|
|
{
|
|
spawn_portal = true;
|
|
}
|
|
}
|
|
if spawn_portal {
|
|
let x = x as f32;
|
|
let y = y as f32;
|
|
if !portals.contains(&(x, y)) {
|
|
portals.push((x, y));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
for portal in portals {
|
|
let x = portal.0 as f32;
|
|
let y = portal.1 as f32;
|
|
let coordinates = Coordinates((x, y));
|
|
let portal = commands
|
|
.spawn_bundle(PortalBundle {
|
|
coordinates,
|
|
..Default::default()
|
|
})
|
|
.insert_bundle(ColliderBundle {
|
|
collider_type: ColliderType::Sensor,
|
|
shape: ColliderShape::cuboid(0.5, 0.5),
|
|
flags: ActiveEvents::INTERSECTION_EVENTS.into(),
|
|
..Default::default()
|
|
})
|
|
.insert(ColliderPositionSync::Discrete)
|
|
.insert(ColliderParent {
|
|
handle: entity.handle(),
|
|
pos_wrt_parent: Vec2::new(x + 0.5, y + 0.5).into(),
|
|
})
|
|
.id();
|
|
commands.entity(entity).push_children(&[portal]);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
fn area_description(
|
|
config: Res<MapConfig>,
|
|
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.clone()
|
|
} else if config.describe_undescribed_areas {
|
|
Some(format!(
|
|
"{} by {} area.",
|
|
area.rect.width(),
|
|
area.rect.height()
|
|
))
|
|
} else {
|
|
None
|
|
};
|
|
if let Some(description) = description {
|
|
for mut log in log.iter_mut() {
|
|
log.push(description.clone());
|
|
}
|
|
}
|
|
}
|
|
}
|
|
*prev_area = current_area;
|
|
}
|
|
}
|
|
}
|
|
|
|
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.rooms {
|
|
v.push(Area {
|
|
rect: *room,
|
|
description: None,
|
|
});
|
|
}
|
|
commands.entity(entity).insert(Areas(v));
|
|
}
|
|
}
|
|
|
|
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();
|
|
app.register_type::<Portal>()
|
|
.add_system(add_map_colliders.system())
|
|
.add_system(portal_spawner.system())
|
|
.add_system(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());
|
|
}
|
|
}
|
|
}
|