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::{Coordinates, Player, PointLike}, exploration::{ExplorationType, Mappable}, log::Log, utils::target_and_other, visibility::BlocksVisibility, }; impl From for Coordinates { fn from(point: mapgen::geometry::Point) -> Self { Self((point.x as f32, point.y as f32)) } } #[derive(Clone, Debug, Default, Reflect)] #[reflect(Component)] pub struct AreaTag; #[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 { 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>) { for (map_entity, map) in maps.iter() { commands.entity(map_entity).insert_bundle(RigidBodyBundle { body_type: RigidBodyType::Static, ..Default::default() }); 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: map_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); } } } } for room in &map.rooms { let shape = ColliderShape::cuboid((room.width() / 2) as f32, (room.height() / 2) as f32); let position = Vec2::new(room.center().x(), room.center().y()).into(); commands .spawn_bundle(ColliderBundle { collider_type: ColliderType::Sensor, shape: shape.clone(), flags: ActiveEvents::INTERSECTION_EVENTS.into(), ..Default::default() }) .insert(ColliderParent { handle: map_entity.handle(), pos_wrt_parent: position, }) .insert(shape.compute_aabb(&position)) .insert(AreaTag); } } } fn portal_spawner( mut commands: Commands, map: Query<(Entity, &Map), Added>, config: Res, ) { 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( mut events: EventReader, areas: Query<&AreaTag>, players: Query<&Player>, names: Query<&Name>, config: Res, shapes: Query<&ColliderShape>, mut log: Query<&mut Log>, ) { for event in events.iter() { if event.intersecting { if let Some((area, other)) = target_and_other(event.collider1.entity(), event.collider2.entity(), &|v| { areas.get(v).is_ok() }) { if players.get(other).is_ok() { if let Ok(mut log) = log.single_mut() { if let Ok(name) = names.get(area) { log.push(name.to_string()); } else if config.describe_undescribed_areas { if let Ok(shape) = shapes.get(area) { let aabb = shape.compute_local_aabb(); log.push(format!( "{}-by-{} area", aabb.extents().x, aabb.extents().y )); } } } } } } } } pub struct MapPlugin; impl Plugin for MapPlugin { fn build(&self, app: &mut AppBuilder) { if !app.world().contains_resource::() { app.insert_resource(MapConfig::default()); } let config = app.world().get_resource::().unwrap().clone(); app.register_type::() .add_system(add_map_colliders.system()) .add_system(portal_spawner.system()); if config.speak_area_descriptions { app.add_system_to_stage(CoreStage::PostUpdate, area_description.system()); } } }