diff --git a/src/camera.rs b/src/camera.rs new file mode 100644 index 0000000..2f945c0 --- /dev/null +++ b/src/camera.rs @@ -0,0 +1,168 @@ +use rltk::{Point, Rltk, RGB}; +use specs::prelude::*; + +use crate::{Hidden, Map, Position, Renderable, TileType}; + +const SHOW_BOUNDARIES: bool = true; + +pub fn get_screen_bounds(ecs: &World, ctx: &mut Rltk) -> (i32, i32, i32, i32) { + let player_pos = ecs.fetch::(); + let (x_chars, y_chars) = ctx.get_char_size(); + + let center_x = (x_chars / 2) as i32; + let center_y = (y_chars / 2) as i32; + + let min_x = player_pos.x - center_x; + let max_x = min_x + x_chars as i32; + let min_y = player_pos.y - center_y; + let max_y = min_y + y_chars as i32; + + (min_x, max_x, min_y, max_y) +} + +pub fn render_camera(ecs: &World, ctx: &mut Rltk) { + let map = ecs.fetch::(); + let (min_x, max_x, min_y, max_y) = get_screen_bounds(ecs, ctx); + + // Render the Map + let map_width = map.width - 1; + let map_height = map.height - 1; + + let mut y = 0; + #[allow(clippy::explicit_counter_loop)] + for ty in min_y..max_y { + let mut x = 0; + for tx in min_x..max_x { + if tx > 0 && tx < map_width && ty > 0 && ty < map_height { + let idx = map.xy_idx(tx, ty); + if map.revealed_tiles[idx] { + let (glyph, fg, bg) = get_tile_glyph(idx, &*map); + ctx.set(x, y, fg, bg, glyph); + } + } else if SHOW_BOUNDARIES { + ctx.set( + x, + y, + RGB::named(rltk::GRAY), + RGB::named(rltk::BLACK), + rltk::to_cp437('·'), + ); + } + x += 1; + } + y += 1; + } + + // Render entities + let positions = ecs.read_storage::(); + let renderables = ecs.read_storage::(); + let hidden = ecs.read_storage::(); + let map = ecs.fetch::(); + + let mut data = (&positions, &renderables, !&hidden) + .join() + .collect::>(); + data.sort_by(|&a, &b| b.1.render_order.cmp(&a.1.render_order)); + for (pos, render, _hidden) in data.iter() { + let idx = map.xy_idx(pos.x, pos.y); + if map.visible_tiles[idx] { + let entity_screen_x = pos.x - min_x; + let entity_screen_y = pos.y - min_y; + if entity_screen_x > 0 + && entity_screen_x < map_width + && entity_screen_y > 0 + && entity_screen_y < map_height + { + ctx.set( + entity_screen_x, + entity_screen_y, + render.fg, + render.bg, + render.glyph, + ); + } + } + } +} + +fn get_tile_glyph(idx: usize, map: &Map) -> (rltk::FontCharType, RGB, RGB) { + let glyph; + let mut fg; + let mut bg = RGB::from_f32(0., 0., 0.); + + match map.tiles[idx] { + TileType::Floor => { + glyph = rltk::to_cp437('.'); + fg = RGB::from_f32(0., 0.5, 0.5); + } + TileType::Wall => { + let x = idx as i32 % map.width; + let y = idx as i32 / map.width; + glyph = wall_glyph(&*map, x, y); + fg = RGB::from_f32(0., 1.0, 0.); + } + TileType::DownStairs => { + glyph = rltk::to_cp437('>'); + fg = RGB::from_f32(0., 1.0, 1.0); + } + }; + + if map.bloodstains.contains(&idx) { + bg = RGB::from_f32(0.75, 0., 0.); + } + + if !map.visible_tiles[idx] { + fg = fg.to_greyscale(); + + // Don't show bloodstains out of visual range + bg = RGB::from_f32(0., 0., 0.); + } + + (glyph, fg, bg) +} + +fn wall_glyph(map: &Map, x: i32, y: i32) -> rltk::FontCharType { + if x < 1 || x > map.width - 2 || y < 1 || y > map.height - 2 { + return 35; + } + + let mut mask = 0u8; + + if is_revealed_and_wall(map, x, y - 1) { + mask += 1; + } + if is_revealed_and_wall(map, x, y + 1) { + mask += 2; + } + if is_revealed_and_wall(map, x - 1, y) { + mask += 4; + } + if is_revealed_and_wall(map, x + 1, y) { + mask += 8; + } + + match mask { + 0 => 9, // Pillar because we can't see neighbors + 1 => 186, // Wall only to the north + 2 => 186, // Wall only to the south + 3 => 186, // Wall to the north and south + 4 => 205, // Wall only to the west + 5 => 188, // Wall to the north and west + 6 => 187, // Wall to the south and west + 7 => 185, // Wall to the north, south, and west + 8 => 205, // Wall only to the east + 9 => 200, // Wall to the north and east + 10 => 201, // Wall to the sound and east + 11 => 204, // Wall to the north, south, and east + 12 => 205, // Wall to the east and west + 13 => 202, // Wall to the east, west, and south + 14 => 203, // Wall to the east, west, and north + 15 => 206, // ╬ Wall on all sides + _ => 35, // We missed one? + } +} + +fn is_revealed_and_wall(map: &Map, x: i32, y: i32) -> bool { + let idx = map.xy_idx(x, y); + map.tiles[idx] == TileType::Wall && map.revealed_tiles[idx] +} diff --git a/src/gui.rs b/src/gui.rs index 510f5b4..0372b8e 100644 --- a/src/gui.rs +++ b/src/gui.rs @@ -6,7 +6,7 @@ use crate::components::{ }; use crate::game_log::GameLog; use crate::rex_assets::RexAssets; -use crate::{Equipped, Hidden, Map, RunState, State}; +use crate::{camera, Equipped, Hidden, Map, RunState, State}; pub fn draw_ui(ecs: &World, ctx: &mut Rltk) { ctx.draw_box( @@ -98,20 +98,32 @@ pub fn draw_ui(ecs: &World, ctx: &mut Rltk) { } fn draw_tooltips(ecs: &World, ctx: &mut Rltk) { + let (min_x, _max_x, min_y, _max_y) = camera::get_screen_bounds(ecs, ctx); let map = ecs.fetch::(); let names = ecs.read_storage::(); let positions = ecs.read_storage::(); let hidden = ecs.read_storage::(); let mouse_pos = ctx.mouse_pos(); - if mouse_pos.0 >= map.width || mouse_pos.1 >= map.height { + let mut mouse_map_pos = mouse_pos; + mouse_map_pos.0 += min_x; + mouse_map_pos.1 += min_y; + if mouse_map_pos.0 >= map.width - 1 + || mouse_map_pos.1 >= map.height - 1 + || mouse_map_pos.0 < 1 + || mouse_map_pos.1 < 1 + { return; } + + if !map.visible_tiles[map.xy_idx(mouse_map_pos.0, mouse_map_pos.1)] { + return; + } + let mut tooltip: Vec = Vec::new(); for (name, position, _hidden) in (&names, &positions, !&hidden).join() { - let idx = map.xy_idx(position.x, position.y); - if position.x == mouse_pos.0 && position.y == mouse_pos.1 && map.visible_tiles[idx] { + if position.x == mouse_map_pos.0 && position.y == mouse_map_pos.1 { tooltip.push(name.name.to_string()); } } @@ -475,6 +487,7 @@ pub fn ranged_target( ctx: &mut Rltk, range: i32, ) -> (ItemMenuResult, Option) { + let (min_x, max_x, min_y, max_y) = camera::get_screen_bounds(&gs.ecs, ctx); let player_entity = gs.ecs.fetch::(); let player_pos = gs.ecs.fetch::(); let viewsheds = gs.ecs.read_storage::(); @@ -493,8 +506,17 @@ pub fn ranged_target( for idx in visible.visible_tiles.iter() { let distance = rltk::DistanceAlg::Pythagoras.distance2d(*player_pos, *idx); if distance <= range as f32 { - ctx.set_bg(idx.x, idx.y, RGB::named(rltk::BLUE)); - available_cells.push(idx); + let screen_x = idx.x - min_x; + let screen_y = idx.y - min_y; + + if screen_x > 1 + && screen_x < (max_x - min_x) - 1 + && screen_y > 1 + && screen_y < (max_y - min_y) - 1 + { + ctx.set_bg(idx.x, idx.y, RGB::named(rltk::BLUE)); + available_cells.push(idx); + } } } } else { @@ -503,9 +525,12 @@ pub fn ranged_target( // Draw mouse cursor let mouse_pos = ctx.mouse_pos(); + let mut mouse_map_pos = mouse_pos; + mouse_map_pos.0 += min_x; + mouse_map_pos.1 += min_y; let mut valid_target = false; for idx in available_cells.iter() { - if idx.x == mouse_pos.0 && idx.y == mouse_pos.1 { + if idx.x == mouse_map_pos.0 && idx.y == mouse_map_pos.1 { valid_target = true; } } @@ -515,7 +540,7 @@ pub fn ranged_target( if ctx.left_click { return ( ItemMenuResult::Selected, - Some(Point::new(mouse_pos.0, mouse_pos.1)), + Some(Point::new(mouse_map_pos.0, mouse_map_pos.1)), ); } } else { diff --git a/src/main.rs b/src/main.rs index da518c7..b1b8ed7 100644 --- a/src/main.rs +++ b/src/main.rs @@ -2,6 +2,7 @@ use rltk::{GameState, Point, RandomNumberGenerator, Rltk}; use specs::prelude::*; use specs::saveload::{SimpleMarker, SimpleMarkerAllocator}; +pub mod camera; mod components; mod damage_system; mod game_log; @@ -150,26 +151,8 @@ impl GameState for State { RunState::MainMenu { .. } => {} RunState::GameOver { .. } => {} _ => { - // Draw the UI - draw_map(&self.ecs.fetch::(), ctx); - { - let positions = self.ecs.read_storage::(); - let renderables = self.ecs.read_storage::(); - let hidden = self.ecs.read_storage::(); - let map = self.ecs.fetch::(); - - let mut data: Vec<_> = (&positions, &renderables, !&hidden).join().collect(); - data.sort_by(|&a, &b| b.1.render_order.cmp(&a.1.render_order)); - for (pos, render, _hidden) in data.iter() { - let idx = map.xy_idx(pos.x, pos.y); - - if map.visible_tiles[idx] { - ctx.set(pos.x, pos.y, render.fg, render.bg, render.glyph) - } - } - - gui::draw_ui(&self.ecs, ctx); - } + camera::render_camera(&self.ecs, ctx); + gui::draw_ui(&self.ecs, ctx); } } @@ -345,12 +328,12 @@ impl GameState for State { } RunState::MagicMapReveal { row } => { let mut map = self.ecs.fetch_mut::(); - for x in 0..MAP_WIDTH { + for x in 0..map.width { let idx = map.xy_idx(x as i32, row); map.revealed_tiles[idx] = true; } - if row as usize == MAP_HEIGHT - 1 { + if row == map.height - 1 { newrunstate = RunState::MonsterTurn; } else { newrunstate = RunState::MagicMapReveal { row: row + 1 }; @@ -462,7 +445,7 @@ impl State { self.mapgen_history.clear(); let mut rng = self.ecs.write_resource::(); - let mut builder = map_builders::random_builder(new_depth, &mut rng); + let mut builder = map_builders::random_builder(new_depth, &mut rng, 64, 64); builder.build_map(&mut rng); std::mem::drop(rng); @@ -550,7 +533,7 @@ fn main() -> rltk::BError { gs.ecs.insert(SimpleMarkerAllocator::::new()); - gs.ecs.insert(Map::new(1)); + gs.ecs.insert(Map::new(1, 64, 64)); gs.ecs.insert(Point::zero()); gs.ecs.insert(rltk::RandomNumberGenerator::new()); diff --git a/src/map.rs b/src/map.rs index 9408085..8d3758d 100644 --- a/src/map.rs +++ b/src/map.rs @@ -4,10 +4,6 @@ use rltk::{Algorithm2D, BaseMap, Point, Rltk, SmallVec, RGB}; use serde::{Deserialize, Serialize}; use specs::prelude::*; -pub const MAP_WIDTH: usize = 80; -pub const MAP_HEIGHT: usize = 43; -pub const MAP_COUNT: usize = MAP_HEIGHT * MAP_WIDTH; - #[derive(PartialEq, Eq, Hash, Copy, Clone, Serialize, Deserialize)] pub enum TileType { Wall, @@ -60,15 +56,17 @@ impl Map { } /// Generates an empty map, consisting entirely of solid walls - pub fn new(new_depth: i32) -> Map { + pub fn new(new_depth: i32, width: i32, height: i32) -> Map { + let map_tile_count = (width * height) as usize; + Map { - tiles: vec![TileType::Wall; MAP_COUNT], - width: MAP_WIDTH as i32, - height: MAP_HEIGHT as i32, - revealed_tiles: vec![false; MAP_COUNT], - visible_tiles: vec![false; MAP_COUNT], - blocked: vec![false; MAP_COUNT], - tile_content: vec![Vec::new(); MAP_COUNT], + tiles: vec![TileType::Wall; map_tile_count], + width, + height, + revealed_tiles: vec![false; map_tile_count], + visible_tiles: vec![false; map_tile_count], + blocked: vec![false; map_tile_count], + tile_content: vec![Vec::new(); map_tile_count], depth: new_depth, bloodstains: HashSet::new(), view_blocked: HashSet::new(), @@ -173,7 +171,7 @@ pub fn draw_map(map: &Map, ctx: &mut Rltk) { // Move to the next set of coordinates x += 1; - if x > MAP_WIDTH as i32 - 1 { + if x > (map.width * map.height) as i32 - 1 { x = 0; y += 1; } diff --git a/src/map_builders.rs b/src/map_builders.rs index 840b826..6f7b653 100644 --- a/src/map_builders.rs +++ b/src/map_builders.rs @@ -66,17 +66,21 @@ pub struct BuilderMap { pub rooms: Option>, pub corridors: Option>>, pub history: Vec, + pub width: i32, + pub height: i32, } impl BuilderMap { - fn new(new_depth: i32) -> BuilderMap { + fn new(new_depth: i32, width: i32, height: i32) -> BuilderMap { BuilderMap { spawn_list: Vec::new(), - map: Map::new(new_depth), + map: Map::new(new_depth, width, height), starting_position: None, rooms: None, corridors: None, history: Vec::new(), + width, + height, } } @@ -98,11 +102,11 @@ pub struct BuilderChain { } impl BuilderChain { - pub fn new(new_depth: i32) -> BuilderChain { + pub fn new(new_depth: i32, width: i32, height: i32) -> BuilderChain { BuilderChain { starter: None, builders: Vec::new(), - build_data: BuilderMap::new(new_depth), + build_data: BuilderMap::new(new_depth, width, height), } } @@ -289,8 +293,13 @@ fn random_shape_builder(rng: &mut RandomNumberGenerator, builder: &mut BuilderCh builder.with(DistantExit::new()); } -pub fn random_builder(new_depth: i32, rng: &mut RandomNumberGenerator) -> BuilderChain { - let mut builder = BuilderChain::new(new_depth); +pub fn random_builder( + new_depth: i32, + rng: &mut RandomNumberGenerator, + width: i32, + height: i32, +) -> BuilderChain { + let mut builder = BuilderChain::new(new_depth, width, height); match rng.roll_dice(1, 2) { 1 => random_room_builder(rng, &mut builder), diff --git a/src/map_builders/waveform_collapse.rs b/src/map_builders/waveform_collapse.rs index 01df566..65812b0 100644 --- a/src/map_builders/waveform_collapse.rs +++ b/src/map_builders/waveform_collapse.rs @@ -33,7 +33,7 @@ impl WaveformCollapseBuilder { let constraints = patterns_to_constraints(patterns, CHUNK_SIZE); self.render_tile_gallery(&constraints, CHUNK_SIZE, build_data); - build_data.map = Map::new(build_data.map.depth); + build_data.map = Map::new(build_data.map.depth, build_data.width, build_data.height); loop { let mut solver = Solver::new(constraints.clone(), CHUNK_SIZE, &build_data.map); while !solver.iteration(&mut build_data.map, rng) { @@ -54,7 +54,7 @@ impl WaveformCollapseBuilder { chunk_size: i32, build_data: &mut BuilderMap, ) { - build_data.map = Map::new(0); + build_data.map = Map::new(0, build_data.width, build_data.height); let mut counter = 0; let mut x = 1; let mut y = 1; @@ -71,7 +71,7 @@ impl WaveformCollapseBuilder { if y + chunk_size > build_data.map.height { // Move to the next page build_data.take_snapshot(); - build_data.map = Map::new(0); + build_data.map = Map::new(0, build_data.width, build_data.height); x = 1; y = 1; diff --git a/src/player.rs b/src/player.rs index b7d658d..11130c5 100644 --- a/src/player.rs +++ b/src/player.rs @@ -61,8 +61,8 @@ pub fn try_move_player(delta_x: i32, delta_y: i32, ecs: &mut World) { } if !map.blocked[destination_idx] { - pos.x = min(79, max(0, pos.x + delta_x)); - pos.y = min(49, max(0, pos.y + delta_y)); + pos.x = min(map.width - 1, max(0, pos.x + delta_x)); + pos.y = min(map.height - 1, max(0, pos.y + delta_y)); entity_moved .insert(entity, EntityMoved {}) .expect("Failed to add EntityMoved flag to player"); diff --git a/src/saveload_system.rs b/src/saveload_system.rs index dd73e33..946295a 100644 --- a/src/saveload_system.rs +++ b/src/saveload_system.rs @@ -191,7 +191,7 @@ pub fn load_game(ecs: &mut World) { for (e, h) in (&entities, &helper).join() { let mut worldmap = ecs.write_resource::(); *worldmap = h.map.clone(); - worldmap.tile_content = vec![Vec::new(); crate::map::MAP_COUNT]; + worldmap.tile_content = vec![Vec::new(); (worldmap.height * worldmap.width) as usize]; deleteme = Some(e); } diff --git a/src/spawner.rs b/src/spawner.rs index 4ad047c..86bb6f2 100644 --- a/src/spawner.rs +++ b/src/spawner.rs @@ -5,7 +5,6 @@ use specs::prelude::*; use specs::saveload::{MarkedBuilder, SimpleMarker}; use crate::components::*; -use crate::map::MAP_WIDTH; use crate::random_table::RandomTable; use crate::{Map, Rect, TileType}; @@ -125,8 +124,13 @@ pub fn spawn_region( /// Spawns a named entity (name in tuple.1) at the location in (tuple.0) pub fn spawn_entity(ecs: &mut World, spawn: &(&usize, &String)) { - let x = (*spawn.0 % MAP_WIDTH) as i32; - let y = (*spawn.0 / MAP_WIDTH) as i32; + let map = ecs.fetch::(); + let width = map.width as usize; + let x = (*spawn.0 % width) as i32; + let y = (*spawn.0 / width) as i32; + + // Drop this map reference to make the borrow checker happy + std::mem::drop(map); match spawn.1.as_ref() { "Goblin" => goblin(ecs, x, y),