use std::marker::PhantomData; use bevy::prelude::*; use bevy_rapier2d::{ na::{Isometry2, Vector2}, prelude::*, rapier::prelude::AABB, }; pub use here_be_dragons::Map as MapgenMap; use here_be_dragons::{geometry::Rect as MRect, MapFilter, Tile}; use maze_generator::{prelude::*, recursive_backtracking::RbGenerator}; use rand::prelude::StdRng; use crate::{ core::{Player, PointLike}, exploration::{ExplorationType, Mappable}, log::Log, utils::target_and_other, visibility::Visible, }; #[derive(Component, Copy, Clone, Debug, Deref, DerefMut, PartialEq)] pub struct Area(pub AABB); #[derive(Component, Clone, Default, Deref, DerefMut)] pub struct Map(pub MapgenMap); impl From> for Map { fn from(v: MapgenMap) -> Self { Self(v) } } #[derive(Component, Clone, Debug, Default, Reflect)] #[reflect(Component)] pub struct MapObstruction; #[derive(Component, Clone, Debug, Default, Reflect)] #[reflect(Component)] pub struct Portal; #[derive(Component, Clone, Debug, Default, Deref, DerefMut, Reflect)] #[reflect(Component)] pub struct SpawnColliderPerTile(pub bool); impl From for SpawnColliderPerTile { fn from(v: bool) -> Self { Self(v) } } #[derive(Component, Clone, Debug, Deref, DerefMut, Reflect)] #[reflect(Component)] pub struct SpawnColliders(pub bool); impl Default for SpawnColliders { fn default() -> Self { Self(true) } } impl From for SpawnColliders { fn from(v: bool) -> Self { Self(v) } } #[derive(Component, Clone, Debug, Deref, DerefMut, Reflect)] #[reflect(Component)] pub struct SpawnPortals(pub bool); impl Default for SpawnPortals { fn default() -> Self { Self(true) } } pub trait ITileType { fn blocks_motion(&self) -> bool; fn blocks_visibility(&self) -> bool; } impl ITileType for Tile { fn blocks_motion(&self) -> bool { self.is_blocked() } fn blocks_visibility(&self) -> bool { self.is_blocked() } } #[derive(Clone, Debug)] pub struct MapConfig { pub describe_undescribed_areas: bool, pub speak_area_descriptions: bool, pub start_revealed: bool, } impl Default for MapConfig { fn default() -> Self { Self { describe_undescribed_areas: false, speak_area_descriptions: true, start_revealed: false, } } } #[derive(Bundle)] pub struct PortalBundle { 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 { 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 spawn_colliders: SpawnColliders, pub spawn_collider_per_tile: SpawnColliderPerTile, pub spawn_portals: SpawnPortals, pub transform: Transform, pub global_transform: GlobalTransform, } pub struct GridBuilder { width_in_rooms: usize, height_in_rooms: usize, room_width: usize, room_height: usize, } impl GridBuilder { pub fn new( width_in_rooms: usize, height_in_rooms: usize, room_width: usize, room_height: usize, ) -> 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: &MapgenMap) -> MapgenMap { let mut map = map.clone(); let mut generator = RbGenerator::new(None); if let Ok(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; let half_width = self.room_width / 2; let half_height = self.room_height / 2; 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) { 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, Tile::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, Tile::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, Tile::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, Tile::floor()); } } } } map.starting_point = Some(here_be_dragons::geometry::Point::new( maze.start.x as usize + 1, maze.start.y as usize + 1, )); map.exit_point = Some(here_be_dragons::geometry::Point::new( (maze.goal.x as usize) * self.room_width + half_width, (maze.goal.y as usize) * self.room_height + half_height, )); } map } } fn spawn_colliders( mut commands: Commands, maps: Query<(Entity, &Map, &SpawnColliders, &SpawnColliderPerTile), Changed>, ) { for (map_entity, map, spawn_colliders, spawn_collider_per_tile) in maps.iter() { if **spawn_colliders { commands .entity(map_entity) .remove::() .remove::() .insert(RigidBody::Fixed); if **spawn_collider_per_tile { for y in 0..map.height { for x in 0..map.width { let tile = map.at(x, y); if tile.blocks_motion() { let id = commands .spawn() .insert(Collider::cuboid(0.5, 0.5)) .insert(Transform::from_xyz(x as f32 + 0.5, y as f32 + 0.5, 0.)) .insert(MapObstruction) .id(); if tile.blocks_visibility() { commands.entity(id).insert(Visible::opaque()); } commands.entity(map_entity).push_children(&[id]); } } } } else { let mut has_body = vec![vec![false; map.height as usize + 1]; map.width as usize + 1]; let mut bottom_left = None; let mut bottom_right = None; for y in 0..map.height { for x in 0..map.width { trace!("Checking ({x}, {y})"); if bottom_left.is_some() { if has_body[x][y] { trace!("Hit another body, setting bottom right"); bottom_right = Some((x, y)); } else if map.at(x, y).is_walkable() { trace!("Hit an empty tile, setting bottom right to ({x}, {y})",); bottom_right = Some((x, y)); } } if map.at(x, y).is_blocked() && !has_body[x][y] { trace!("Blocked, setting has_body"); has_body[x][y] = true; if bottom_left.is_none() { trace!("Setting bottom left"); bottom_left = Some((x, y)); } if bottom_left.is_some() && x == map.width - 1 { trace!("Hit right edge, setting bottom right"); bottom_right = Some((x, y)); } } if let (Some(bl), Some(br)) = (bottom_left, bottom_right) { trace!("Got bottom, checking if can extend up"); let mut top_left = (bl.0, bl.1 + 1); let mut top_right = (br.0, br.1 + 1); if y != map.height - 1 { let mut can_extend_up = true; for y in bl.1 + 1..map.height { for x in bl.0..br.0 { trace!("Extension check: ({x}, {y})"); if map.at(x, y).is_walkable() { trace!("Can't, empty tile"); can_extend_up = false; break; } } if can_extend_up { trace!("Can extend up, setting has_body"); for x in top_left.0..=top_right.0 { has_body[x][top_left.1] = true; } top_left.1 += 1; top_right.1 += 1; } else { break; } } } let width = br.0 as f32 - bl.0 as f32; let half_width = width / 2.; let height = top_left.1 as f32 - bl.1 as f32; let half_height = height / 2.; trace!( "Top left: {:?}\ntop right: {:?}\nbottom left: {:?}\nbottom right: {:?}", top_left, top_right, bl, br ); let center = (bl.0 as f32 + half_width, br.1 as f32 + half_height); trace!( "Create shape at {:?} of width {:?} and height {:?}", center, width, height ); let id = commands .spawn() .insert(Collider::cuboid(half_width, half_height)) .insert(Transform::from_xyz(center.x(), center.y(), 0.)) .insert(MapObstruction) .id(); if map.at(x as usize, y as usize).blocks_visibility() { commands.entity(id).insert(Visible::opaque()); } commands.entity(map_entity).push_children(&[id]); bottom_left = None; bottom_right = None; } } } } for room in &map.rooms { let shape = Collider::cuboid((room.width() / 2) as f32, (room.height() / 2) as f32); let position = Isometry2::new(Vector2::new(room.center().x(), room.center().y()), 0.); let aabb = shape.raw.compute_aabb(&position); let id = commands .spawn() .insert(shape) .insert(Sensor(true)) .insert(ActiveEvents::COLLISION_EVENTS) .insert(Transform::from_xyz( position.translation.x, position.translation.y, 0., )) .insert(Area(aabb)) .id(); commands.entity(map_entity).push_children(&[id]); } } } } fn spawn_portals( mut commands: Commands, map: Query<(Entity, &Map, &SpawnPortals), Changed>, ) { for (map_entity, map, spawn_portals) in map.iter() { if **spawn_portals { commands.entity(map_entity).remove::(); 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].is_walkable() && (x > 1 && map.tiles[idx - 1].is_walkable()) && (x < map.width - 2 && map.tiles[idx + 1].is_walkable()) && (y > 1 && map.tiles[idx - map.width].is_blocked()) && (y < map.height - 2 && map.tiles[idx + map.width].is_blocked()) { spawn_portal = true; } if map.tiles[idx].is_walkable() && (x > 1 && map.tiles[idx - 1].is_blocked()) && (x < map.width - 2 && map.tiles[idx + 1].is_blocked()) && (y > 1 && map.tiles[idx - map.width].is_walkable()) && (y < map.height - 2 && map.tiles[idx + map.width].is_walkable()) { 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 portal = commands .spawn_bundle(PortalBundle { transform: Transform::from_translation(Vec3::new(x, y, 0.)), ..default() }) .id(); commands.entity(map_entity).push_children(&[portal]); } } } } fn spawn_portal_colliders( mut commands: Commands, map: Query<&SpawnColliders, With>>, portals: Query<(Entity, &Transform), (With, Without)>, ) { for spawn_colliders in map.iter() { if **spawn_colliders { for (portal_entity, coordinates) in portals.iter() { let position = Vec2::new(coordinates.x() + 0.5, coordinates.y() + 0.5); commands .entity(portal_entity) .insert(Transform::from_xyz(position.x(), position.y(), 0.)) .insert(Collider::cuboid(0.5, 0.5)) .insert(Sensor(true)) .insert(ActiveEvents::COLLISION_EVENTS); } } } } fn area_description( mut events: EventReader, areas: Query<(&Area, Option<&Name>)>, players: Query<&Player>, config: Res, mut log: Query<&mut Log>, ) { for event in events.iter() { let (entity1, entity2, started) = match event { CollisionEvent::Started(collider1, collider2, _) => (collider1, collider2, true), CollisionEvent::Stopped(collider1, collider2, _) => (collider1, collider2, false), }; if let Some((area, other)) = target_and_other(*entity1, *entity2, &|v| areas.get(v).is_ok()) { if players.get(other).is_ok() { if let Ok((aabb, area_name)) = areas.get(area) { let name = if let Some(name) = area_name { Some(name.to_string()) } else if config.describe_undescribed_areas { Some(format!("{}-by-{} area", aabb.extents().x, aabb.extents().y)) } else { None }; if let Some(name) = name { if let Ok(mut log) = log.get_single_mut() { if started { log.push(format!("Entering {name}.")); } else { log.push(format!("Leaving {name}.")); } } } } } } } } pub struct MapPlugin(PhantomData); impl Default for MapPlugin { fn default() -> Self { Self(Default::default()) } } impl Plugin for MapPlugin { fn build(&self, app: &mut App) { if !app.world.contains_resource::() { app.insert_resource(MapConfig::default()); } let config = app.world.get_resource::().unwrap().clone(); app.register_type::() .add_system(spawn_colliders::) .add_system(spawn_portals::) .add_system(spawn_portal_colliders::); if config.speak_area_descriptions { app.add_system_to_stage(CoreStage::PostUpdate, area_description); } } }