diff --git a/src/camera.rs b/src/camera.rs new file mode 100644 index 0000000..ed9e3ea --- /dev/null +++ b/src/camera.rs @@ -0,0 +1,279 @@ +use super::{Door, Hidden, Map, Mind, Position, Renderable, TileType}; +use rltk::{Point, Rltk, RGB}; +use specs::prelude::*; +use std::ops::{Add, Mul}; + +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 centre_x = (x_chars / 2) as i32; + let centre_y = (y_chars / 2) as i32; + + let min_x = player_pos.x - centre_x; + let min_y = player_pos.y - centre_y; + let max_x = min_x + x_chars as i32; + 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); + + // Might need to -1 here? + let map_width = map.width; + let map_height = map.height; + + // Render map + let mut y = 0; + for t_y in min_y..max_y { + let mut x = 0; + for t_x in min_x..max_x { + if t_x >= 0 && t_x < map.width && t_y >= 0 && t_y < map_height { + let idx = map.xy_idx(t_x, t_y); + 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::DARKSLATEGRAY), RGB::named(rltk::BLACK), rltk::to_cp437('#')); + } + x += 1; + } + y += 1; + } + + // Render entities + { + let positions = ecs.read_storage::(); + let renderables = ecs.read_storage::(); + let minds = ecs.read_storage::(); + let hidden = ecs.read_storage::(); + let doors = ecs.write_storage::(); + let map = ecs.fetch::(); + let entities = ecs.entities(); + + let mut data = (&positions, &renderables, &entities, !&hidden).join().collect::>(); + data.sort_by(|&a, &b| b.1.render_order.cmp(&a.1.render_order)); + for (pos, render, ent, _hidden) in data.iter() { + let idx = map.xy_idx(pos.x, pos.y); + let entity_offset_x = pos.x - min_x; + let entity_offset_y = pos.y - min_y; + if entity_offset_x > 0 && entity_offset_x < map_width && entity_offset_y > 0 && entity_offset_y < map_height + { + let offsets = RGB::from_u8(map.red_offset[idx], map.green_offset[idx], map.blue_offset[idx]); + let mut draw = false; + let mut fg = render.fg; + let mut bg = render.bg.add(RGB::from_u8(26, 45, 45)).add(offsets); + // Get bloodstain colours + if map.bloodstains.contains(&idx) { + bg = bg.add(RGB::from_f32(0.6, 0., 0.)); + } + // Draw entities on visible tiles + if map.visible_tiles[idx] { + draw = true; + } + // Draw entities with minds within telepath range + if map.telepath_tiles[idx] { + let has_mind = minds.get(*ent); + if let Some(_) = has_mind { + draw = true; + } + } + // Draw all doors + let is_door = doors.get(*ent); + if let Some(_) = is_door { + if map.revealed_tiles[idx] { + if !map.visible_tiles[idx] { + fg = fg.mul(0.6); + bg = bg.mul(0.6); + } + draw = true; + } + } + if draw { + ctx.set(entity_offset_x, entity_offset_y, fg, bg, render.glyph); + } + } + } + } +} + +fn get_tile_glyph(idx: usize, map: &Map) -> (rltk::FontCharType, RGB, RGB) { + let offsets = RGB::from_u8(map.red_offset[idx], map.green_offset[idx], map.blue_offset[idx]); + let glyph; + let mut fg = offsets.mul(2.0); + let mut bg = offsets.add(RGB::from_u8(26, 45, 45)); + + match map.tiles[idx] { + TileType::Floor => { + glyph = rltk::to_cp437('.'); + fg = fg.add(RGB::from_f32(0.1, 0.8, 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 = fg.add(RGB::from_f32(0.6, 0.5, 0.25)); + } + TileType::DownStair => { + glyph = rltk::to_cp437('>'); + fg = RGB::from_f32(0., 1., 1.); + } + } + if map.bloodstains.contains(&idx) { + bg = bg.add(RGB::from_f32(0.6, 0., 0.)); + } + if !map.visible_tiles[idx] { + fg = fg.mul(0.6); + bg = bg.mul(0.6); + } + + return (glyph, fg, bg); +} + +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] +} + +fn wall_glyph(map: &Map, x: i32, y: i32) -> rltk::FontCharType { + if x < 1 || x > map.width - 2 || y < 1 || y > map.height - 2 as i32 { + return 35; + } + let mut mask: u8 = 0; + let diagonals_matter: Vec = vec![7, 11, 13, 14, 15]; + + if is_revealed_and_wall(map, x, y - 1) { + // N + mask += 1; + } + if is_revealed_and_wall(map, x, y + 1) { + // S + mask += 2; + } + if is_revealed_and_wall(map, x - 1, y) { + // W + mask += 4; + } + if is_revealed_and_wall(map, x + 1, y) { + // E + mask += 8; + } + + if diagonals_matter.contains(&mask) { + if is_revealed_and_wall(map, x + 1, y - 1) { + // Top right + mask += 16; + } + if is_revealed_and_wall(map, x - 1, y - 1) { + // Top left + mask += 32; + } + if is_revealed_and_wall(map, x + 1, y + 1) { + // Bottom right + mask += 64; + } + if is_revealed_and_wall(map, x - 1, y + 1) { + // Bottom left + mask += 128; + } + } + + match mask { + 0 => 254, // ■ (254) square pillar; but maybe ○ (9) looks better + 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 south 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 north + 14 => 203, // Wall to the east, west, and south + 15 => 206, // ╬ Wall on all sides + 29 => 202, + 31 => 206, + 45 => 202, + 46 => 203, + 47 => 206, + 55 => 185, + 59 => 204, + 63 => 203, + 87 => 185, + 126 => 203, + 143 => 206, + 77 => 202, + 171 => 204, + 187 => 204, + 215 => 185, + 190 => 203, + 237 => 202, + 30 => 203, + 110 => 203, + 111 => 206, + 119 => 185, + 142 => 203, + 158 => 203, + 235 => 204, + 93 => 202, + 109 => 202, + 94 => 203, + 174 => 203, + 159 => 206, + 221 => 202, + 157 => 202, + 79 => 206, + 95 => 185, + 23 => 185, // NSW and NSE + 1 diagonal + 39 => 185, + 71 => 185, + 103 => 185, + 135 => 185, + 151 => 185, + 199 => 185, + 78 => 203, + 27 => 204, + 43 => 204, + 75 => 204, + 107 => 204, + 139 => 204, + 155 => 204, + 173 => 202, + 141 => 202, + 205 => 202, + 175 => 204, + 203 => 204, + 61 => 205, // NEW cases + 125 => 205, // NEW cases + 189 => 205, // NEW cases + 206 => 205, + 207 => 202, + 222 => 205, + 238 => 205, + 253 => 205, + 254 => 205, + 167 => 186, // NSW, NW, SW + 91 => 186, // NSE, NE, SE + 183 => 186, // NSW, NW, SW, NE + 123 => 186, // NSE, NE, SE, NW + 231 => 186, // NSW, NW, SW, SE + 219 => 186, // NSE, NE, SE, SW + 247 => 186, + 251 => 186, + 127 => 187, // Everything except NE + 191 => 201, // Everything except NW + 223 => 188, // Everything except SE + 239 => 200, // Everything except SW + _ => 35, // We missed one? + } +} diff --git a/src/components.rs b/src/components.rs index eb195e1..e050e41 100644 --- a/src/components.rs +++ b/src/components.rs @@ -235,6 +235,9 @@ pub struct Wand { #[derive(Component, Debug, Serialize, Deserialize, Clone)] pub struct Destructible {} +#[derive(Component, Debug, Serialize, Deserialize, Clone)] +pub struct Digger {} + #[derive(Component, Debug, Serialize, Deserialize, Clone)] pub struct Hidden {} diff --git a/src/gui.rs b/src/gui.rs index 388ebe5..f454fa7 100644 --- a/src/gui.rs +++ b/src/gui.rs @@ -1,6 +1,6 @@ use super::{ - gamelog, rex_assets::RexAssets, CombatStats, Equipped, Hidden, HungerClock, HungerState, InBackpack, Map, Name, - Player, Point, Position, RunState, State, Viewshed, + camera, gamelog, rex_assets::RexAssets, CombatStats, Equipped, Hidden, HungerClock, HungerState, InBackpack, Map, + Name, Player, Point, Position, RunState, State, Viewshed, }; use rltk::{Rltk, VirtualKeyCode, RGB}; use specs::prelude::*; @@ -83,19 +83,31 @@ pub fn get_input_direction( } 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_pos_adjusted = mouse_pos; + mouse_pos_adjusted.0 += min_x; + mouse_pos_adjusted.1 += min_y; + if mouse_pos_adjusted.0 >= map.width + || mouse_pos_adjusted.1 >= map.height + || mouse_pos_adjusted.1 < 0 // Might need to be 1, and -1 from map height/width. + || mouse_pos_adjusted.0 < 0 + { + return; + } + if !(map.visible_tiles[map.xy_idx(mouse_pos_adjusted.0, mouse_pos_adjusted.1)] + || map.telepath_tiles[map.xy_idx(mouse_pos_adjusted.0, mouse_pos_adjusted.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_pos_adjusted.0 && position.y == mouse_pos_adjusted.1 { tooltip.push(name.name.to_string()); } } @@ -367,11 +379,12 @@ pub fn remove_item_menu(gs: &mut State, ctx: &mut Rltk) -> (ItemMenuResult, Opti } pub fn ranged_target(gs: &mut State, ctx: &mut Rltk, range: i32, aoe: 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::(); - ctx.print_color(5, 0, RGB::named(rltk::YELLOW), RGB::named(rltk::BLACK), "select target"); + ctx.print_color(1, 1, RGB::named(rltk::WHITE), RGB::named(rltk::BLACK), "Targeting which tile? [mouse input]"); // Highlight available cells let mut available_cells = Vec::new(); @@ -381,8 +394,12 @@ pub fn ranged_target(gs: &mut State, ctx: &mut Rltk, range: i32, aoe: i32) -> (I 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 > 0 && screen_x < (max_x - min_x) && screen_y > 0 && screen_y < (max_y - min_y) { + ctx.set_bg(screen_x, screen_y, RGB::named(rltk::BLUE)); + available_cells.push(idx); + } } } } else { @@ -391,24 +408,30 @@ pub fn ranged_target(gs: &mut State, ctx: &mut Rltk, range: i32, aoe: i32) -> (I // Draw mouse cursor let mouse_pos = ctx.mouse_pos(); + let mut mouse_pos_adjusted = mouse_pos; + mouse_pos_adjusted.0 += min_x; + mouse_pos_adjusted.1 += min_y; let map = gs.ecs.fetch::(); 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_pos_adjusted.0 && idx.y == mouse_pos_adjusted.1 { valid_target = true; } } if valid_target { if aoe > 0 { - let mut blast_tiles = rltk::field_of_view(Point::new(mouse_pos.0, mouse_pos.1), aoe, &*map); + // We adjust for camera position when getting FOV, but then we need to adjust back + // when iterating through the tiles themselves, by taking away min_x/min_y. + let mut blast_tiles = + rltk::field_of_view(Point::new(mouse_pos_adjusted.0, mouse_pos_adjusted.1), aoe, &*map); blast_tiles.retain(|p| p.x > 0 && p.x < map.width - 1 && p.y > 0 && p.y < map.height - 1); for tile in blast_tiles.iter() { - ctx.set_bg(tile.x, tile.y, RGB::named(rltk::DARKCYAN)); + ctx.set_bg(tile.x - min_x, tile.y - min_y, RGB::named(rltk::DARKCYAN)); } } ctx.set_bg(mouse_pos.0, mouse_pos.1, RGB::named(rltk::CYAN)); if ctx.left_click { - return (ItemMenuResult::Selected, Some(Point::new(mouse_pos.0, mouse_pos.1))); + return (ItemMenuResult::Selected, Some(Point::new(mouse_pos_adjusted.0, mouse_pos_adjusted.1))); } } else { ctx.set_bg(mouse_pos.0, mouse_pos.1, RGB::named(rltk::RED)); diff --git a/src/inventory_system.rs b/src/inventory_system.rs index b8c13e2..ce4ef82 100644 --- a/src/inventory_system.rs +++ b/src/inventory_system.rs @@ -1,8 +1,8 @@ use super::{ - gamelog, CombatStats, Confusion, Consumable, Cursed, Destructible, Equippable, Equipped, HungerClock, HungerState, - InBackpack, InflictsDamage, MagicMapper, Map, Name, ParticleBuilder, Point, Position, ProvidesHealing, - ProvidesNutrition, RandomNumberGenerator, RunState, SufferDamage, Wand, WantsToDropItem, WantsToPickupItem, - WantsToRemoveItem, WantsToUseItem, AOE, DEFAULT_PARTICLE_LIFETIME, LONG_PARTICLE_LIFETIME, + gamelog, CombatStats, Confusion, Consumable, Cursed, Destructible, Digger, Equippable, Equipped, HungerClock, + HungerState, InBackpack, InflictsDamage, MagicMapper, Map, Name, ParticleBuilder, Point, Position, ProvidesHealing, + ProvidesNutrition, RandomNumberGenerator, RunState, SufferDamage, TileType, Viewshed, Wand, WantsToDropItem, + WantsToPickupItem, WantsToRemoveItem, WantsToUseItem, AOE, DEFAULT_PARTICLE_LIFETIME, LONG_PARTICLE_LIFETIME, }; use specs::prelude::*; @@ -38,16 +38,21 @@ impl<'a> System<'a> for ItemCollectionSystem { } } +// Grouping together components because of type complexity issues - SystemData was too large. +// This is a temporary solution that'll be fixed once inventory use is refactored into separate +// systems. +type EquipComponents<'a> = (ReadStorage<'a, Equippable>, WriteStorage<'a, Equipped>); + pub struct ItemUseSystem {} impl<'a> System<'a> for ItemUseSystem { #[allow(clippy::type_complexity)] type SystemData = ( ReadExpect<'a, Entity>, - ReadExpect<'a, Map>, + WriteExpect<'a, Map>, WriteExpect<'a, RandomNumberGenerator>, Entities<'a>, WriteStorage<'a, WantsToUseItem>, - ReadStorage<'a, Name>, + WriteStorage<'a, Name>, WriteStorage<'a, Consumable>, WriteStorage<'a, Wand>, ReadStorage<'a, Destructible>, @@ -61,22 +66,23 @@ impl<'a> System<'a> for ItemUseSystem { ReadStorage<'a, Position>, ReadStorage<'a, InflictsDamage>, ReadStorage<'a, AOE>, + ReadStorage<'a, Digger>, WriteStorage<'a, Confusion>, ReadStorage<'a, MagicMapper>, WriteExpect<'a, RunState>, - ReadStorage<'a, Equippable>, - WriteStorage<'a, Equipped>, + EquipComponents<'a>, WriteStorage<'a, InBackpack>, + WriteStorage<'a, Viewshed>, ); fn run(&mut self, data: Self::SystemData) { let ( player_entity, - map, + mut map, mut rng, entities, mut wants_to_use, - names, + mut names, mut consumables, mut wands, destructibles, @@ -90,23 +96,24 @@ impl<'a> System<'a> for ItemUseSystem { positions, inflicts_damage, aoe, + digger, mut confused, magic_mapper, mut runstate, - equippable, - mut equipped, + (equippable, mut equipped), mut backpack, + mut viewsheds, ) = data; for (entity, wants_to_use) in (&entities, &wants_to_use).join() { let mut verb = "use"; let mut used_item = true; let mut aoe_item = false; - let item_being_used = names.get(wants_to_use.item).unwrap(); let is_cursed = cursed_items.get(wants_to_use.item); let wand = wands.get_mut(wants_to_use.item); if let Some(wand) = wand { + let name = names.get_mut(wants_to_use.item).unwrap(); // If want has no uses, roll 1d121. On a 121, wrest the wand, then delete it. if wand.uses == 0 { if rng.roll_dice(1, 121) != 121 { @@ -120,9 +127,14 @@ impl<'a> System<'a> for ItemUseSystem { consumables.insert(wants_to_use.item, Consumable {}).expect("Could not insert consumable"); } verb = "zap"; + // TODO: Change this to track uses better, after adding in identification. + name.name.push_str("*"); + name.plural.push_str("*"); wand.uses -= 1; } + let item_being_used = names.get(wants_to_use.item).unwrap(); + let is_edible = provides_nutrition.get(wants_to_use.item); if let Some(_) = is_edible { verb = "eat"; @@ -140,9 +152,14 @@ impl<'a> System<'a> for ItemUseSystem { // TARGETING let mut targets: Vec = Vec::new(); + let mut target_idxs: Vec = Vec::new(); match wants_to_use.target { None => { targets.push(*player_entity); + let pos = positions.get(*player_entity); + if let Some(pos) = pos { + target_idxs.push(map.xy_idx(pos.x, pos.y)); + } } Some(mut target) => { let area_effect = aoe.get(wants_to_use.item); @@ -150,6 +167,7 @@ impl<'a> System<'a> for ItemUseSystem { None => { // Single target in a tile let idx = map.xy_idx(target.x, target.y); + target_idxs.push(idx); for mob in map.tile_content[idx].iter() { targets.push(*mob); } @@ -177,6 +195,7 @@ impl<'a> System<'a> for ItemUseSystem { blast_tiles.retain(|p| p.x > 0 && p.x < map.width - 1 && p.y > 0 && p.y < map.height - 1); for tile_idx in blast_tiles.iter() { let idx = map.xy_idx(tile_idx.x, tile_idx.y); + target_idxs.push(idx); for mob in map.tile_content[idx].iter() { targets.push(*mob); } @@ -250,13 +269,7 @@ impl<'a> System<'a> for ItemUseSystem { if let Some(stats) = stats { stats.hp = i32::min(stats.max_hp, stats.hp + heal.amount); if entity == *player_entity { - gamelog::Logger::new() - .append("Quaffing, you heal") - .colour(rltk::GREEN) - .append(heal.amount) - .colour(rltk::WHITE) - .append("hit points.") - .log(); + gamelog::Logger::new().append("Quaffing, you recover some vigour.").log(); } let pos = positions.get(entity); if let Some(pos) = pos { @@ -371,6 +384,27 @@ impl<'a> System<'a> for ItemUseSystem { } } + let is_digger = digger.get(wants_to_use.item); + match is_digger { + None => {} + Some(_) => { + used_item = true; + for idx in target_idxs { + if map.tiles[idx] == TileType::Wall { + map.tiles[idx] = TileType::Floor; + } + for viewshed in (&mut viewsheds).join() { + if viewshed + .visible_tiles + .contains(&Point::new(idx % map.width as usize, idx / map.width as usize)) + { + viewshed.dirty = true; + } + } + } + } + } + // ITEM DELETION AFTER USE if used_item { let consumable = consumables.get(wants_to_use.item); diff --git a/src/main.rs b/src/main.rs index a856c5c..73f13a7 100644 --- a/src/main.rs +++ b/src/main.rs @@ -4,6 +4,7 @@ use specs::saveload::{SimpleMarker, SimpleMarkerAllocator}; use std::ops::{Add, Mul}; extern crate serde; +pub mod camera; mod components; pub use components::*; mod map; @@ -251,52 +252,8 @@ impl GameState for State { RunState::MainMenu { .. } => {} _ => { // Draw map and ui - draw_map(&self.ecs.fetch::(), ctx); - { - let positions = self.ecs.read_storage::(); - let renderables = self.ecs.read_storage::(); - let minds = self.ecs.read_storage::(); - let hidden = self.ecs.read_storage::(); - let doors = self.ecs.write_storage::(); - let map = self.ecs.fetch::(); - let entities = self.ecs.entities(); - - let mut data = (&positions, &renderables, &entities, !&hidden).join().collect::>(); - data.sort_by(|&a, &b| b.1.render_order.cmp(&a.1.render_order)); - for (pos, render, ent, _hidden) in data.iter() { - let idx = map.xy_idx(pos.x, pos.y); - let offsets = RGB::from_u8(map.red_offset[idx], map.green_offset[idx], map.blue_offset[idx]); - let mut fg = render.fg; - let mut bg = render.bg.add(RGB::from_u8(26, 45, 45)).add(offsets); - // Get bloodstain colours - if map.bloodstains.contains(&idx) { - bg = bg.add(RGB::from_f32(0.6, 0., 0.)); - } - // Draw entities on visible tiles - if map.visible_tiles[idx] { - ctx.set(pos.x, pos.y, fg, bg, render.glyph); - } - // Draw entities with minds within telepath range - if map.telepath_tiles[idx] { - let has_mind = minds.get(*ent); - if let Some(_) = has_mind { - ctx.set(pos.x, pos.y, render.fg, RGB::named(rltk::BLACK), render.glyph); - } - } - // Draw all doors - let is_door = doors.get(*ent); - if let Some(_) = is_door { - if map.revealed_tiles[idx] { - if !map.visible_tiles[idx] { - fg = fg.mul(0.6); - bg = bg.mul(0.6); - } - ctx.set(pos.x, pos.y, fg, bg, render.glyph); - } - } - } - gui::draw_ui(&self.ecs, ctx); - } + camera::render_camera(&self.ecs, ctx); + gui::draw_ui(&self.ecs, ctx); } } @@ -566,6 +523,7 @@ fn main() -> rltk::BError { gs.ecs.register::(); gs.ecs.register::(); gs.ecs.register::(); + gs.ecs.register::(); gs.ecs.register::(); gs.ecs.register::(); gs.ecs.register::(); diff --git a/src/map.rs b/src/map.rs index 213ac28..1b626fe 100644 --- a/src/map.rs +++ b/src/map.rs @@ -11,8 +11,11 @@ pub enum TileType { DownStair, } -pub const MAPWIDTH: usize = 80; -pub const MAPHEIGHT: usize = 43; +// FIXME: If the map size gets too small, entities stop being rendered starting from the right. +// i.e. on a map size of 40*40, only entities to the left of the player are rendered. +// on a map size of 42*42, the player can see entities up to 2 tiles to their right. +pub const MAPWIDTH: usize = 64; +pub const MAPHEIGHT: usize = 64; pub const MAPCOUNT: usize = MAPHEIGHT * MAPWIDTH; #[derive(Default, Serialize, Deserialize, Clone)] diff --git a/src/player.rs b/src/player.rs index 00fb02b..afcc7dc 100644 --- a/src/player.rs +++ b/src/player.rs @@ -19,6 +19,7 @@ pub fn try_door(i: i32, j: i32, ecs: &mut World) -> RunState { let mut blocks_movement = ecs.write_storage::(); let mut renderables = ecs.write_storage::(); let names = ecs.read_storage::(); + let mut rng = ecs.write_resource::(); let mut result = RunState::AwaitingInput; let mut door_pos: Option = None; @@ -45,6 +46,10 @@ pub fn try_door(i: i32, j: i32, ecs: &mut World) -> RunState { if let Some(name) = names.get(*potential_target) { gamelog::Logger::new().append("The").item_name(&name.name).append("is blocked.").log(); } + } else if rng.roll_dice(1, 6) == 1 { + if let Some(name) = names.get(*potential_target) { + gamelog::Logger::new().append("The").item_name(&name.name).append("resists!").log(); + } } else { door.open = false; blocks_visibility @@ -59,8 +64,8 @@ pub fn try_door(i: i32, j: i32, ecs: &mut World) -> RunState { } render_data.glyph = rltk::to_cp437('+'); // Nethack open door, maybe just use '/' instead. door_pos = Some(Point::new(pos.x + delta_x, pos.y + delta_y)); - result = RunState::PlayerTurn; } + result = RunState::PlayerTurn; } else { gamelog::Logger::new().append("It's already closed.").log(); } @@ -94,6 +99,7 @@ pub fn open(i: i32, j: i32, ecs: &mut World) -> RunState { let mut blocks_movement = ecs.write_storage::(); let mut renderables = ecs.write_storage::(); let names = ecs.read_storage::(); + let mut rng = ecs.write_resource::(); let mut result = RunState::AwaitingInput; let mut door_pos: Option = None; @@ -116,15 +122,21 @@ pub fn open(i: i32, j: i32, ecs: &mut World) -> RunState { let door = doors.get_mut(*potential_target); if let Some(door) = door { if door.open == false { - door.open = true; - blocks_visibility.remove(*potential_target); - blocks_movement.remove(*potential_target); - let render_data = renderables.get_mut(*potential_target).unwrap(); - if let Some(name) = names.get(*potential_target) { - gamelog::Logger::new().append("You open the").item_name_n(&name.name).period().log(); + if rng.roll_dice(1, 6) == 1 { + if let Some(name) = names.get(*potential_target) { + gamelog::Logger::new().append("The").item_name(&name.name).append("resists!").log(); + } + } else { + door.open = true; + blocks_visibility.remove(*potential_target); + blocks_movement.remove(*potential_target); + let render_data = renderables.get_mut(*potential_target).unwrap(); + if let Some(name) = names.get(*potential_target) { + gamelog::Logger::new().append("You open the").item_name_n(&name.name).period().log(); + } + render_data.glyph = rltk::to_cp437('▓'); // Nethack open door, maybe just use '/' instead. + door_pos = Some(Point::new(pos.x + delta_x, pos.y + delta_y)); } - render_data.glyph = rltk::to_cp437('▓'); // Nethack open door, maybe just use '/' instead. - door_pos = Some(Point::new(pos.x + delta_x, pos.y + delta_y)); result = RunState::PlayerTurn; } else { gamelog::Logger::new().append("It's already open.").log(); diff --git a/src/saveload_system.rs b/src/saveload_system.rs index 0bc56a9..2aa7047 100644 --- a/src/saveload_system.rs +++ b/src/saveload_system.rs @@ -57,6 +57,7 @@ pub fn save_game(ecs: &mut World) { Cursed, DefenceBonus, Destructible, + Digger, Door, EntityMoved, EntryTrigger, @@ -152,6 +153,7 @@ pub fn load_game(ecs: &mut World) { Cursed, DefenceBonus, Destructible, + Digger, Door, EntityMoved, EntryTrigger, diff --git a/src/spawner.rs b/src/spawner.rs index 3bf7891..44e5d4e 100644 --- a/src/spawner.rs +++ b/src/spawner.rs @@ -1,6 +1,6 @@ use super::{ random_table::RandomTable, Attribute, Attributes, BlocksTile, BlocksVisibility, CombatStats, Confusion, Consumable, - Cursed, DefenceBonus, Destructible, Door, EntryTrigger, EquipmentSlot, Equippable, Hidden, HungerClock, + Cursed, DefenceBonus, Destructible, Digger, Door, EntryTrigger, EquipmentSlot, Equippable, Hidden, HungerClock, HungerState, InflictsDamage, Item, MagicMapper, Map, MeleePowerBonus, Mind, Monster, Name, Player, Position, ProvidesHealing, ProvidesNutrition, Ranged, Rect, Renderable, SerializeMe, SingleActivation, TileType, Viewshed, Wand, AOE, MAPWIDTH, @@ -168,6 +168,7 @@ pub fn spawn_entity(ecs: &mut World, spawn: &(&usize, &String)) { "magic missile wand" => magic_missile_wand(ecs, x, y), "fireball wand" => fireball_wand(ecs, x, y), "confusion wand" => confusion_wand(ecs, x, y), + "digging wand" => digging_wand(ecs, x, y), // Food "rations" => rations(ecs, x, y), "apple" => apple(ecs, x, y), @@ -180,7 +181,7 @@ pub fn spawn_entity(ecs: &mut World, spawn: &(&usize, &String)) { } } -// 20 mobs : 6 items : 2 food : 1 trap +// 12 mobs : 6 items : 2 food : 1 trap fn category_table() -> RandomTable { return RandomTable::new().add("mob", 12).add("item", 6).add("food", 2).add("trap", 1); } @@ -230,7 +231,11 @@ pub fn scroll_table(_map_depth: i32) -> RandomTable { } pub fn wand_table(_map_depth: i32) -> RandomTable { - return RandomTable::new().add("magic missile wand", 1).add("fireball wand", 1).add("confusion wand", 1); + return RandomTable::new() + .add("magic missile wand", 1) + .add("fireball wand", 1) + .add("confusion wand", 1) + .add("digging wand", 1); } pub fn food_table(_map_depth: i32) -> RandomTable { @@ -600,6 +605,25 @@ fn confusion_wand(ecs: &mut World, x: i32, y: i32) { .build(); } +fn digging_wand(ecs: &mut World, x: i32, y: i32) { + ecs.create_entity() + .with(Position { x, y }) + .with(Renderable { + glyph: rltk::to_cp437('/'), + fg: RGB::named(rltk::PURPLE), + bg: RGB::named(rltk::BLACK), + render_order: 2, + }) + .with(Name { name: "wand of digging".to_string(), plural: "wands of digging".to_string() }) + .with(Item {}) + .with(Wand { uses: 3, max_uses: 3 }) + .with(Destructible {}) + .with(Ranged { range: 10 }) + .with(Digger {}) + .marked::>() + .build(); +} + // TRAPS fn bear_trap(ecs: &mut World, x: i32, y: i32) { ecs.create_entity()