From b940142f16d4fc7aa832cd1e0605c38b26733f2a Mon Sep 17 00:00:00 2001 From: Llywelwyn Date: Thu, 27 Jul 2023 21:15:20 +0100 Subject: [PATCH] the town --- raws/mobs.json | 131 ++++++++++++++++++++-- raws/props.json | 32 +++++- raws/spawn_tables.json | 2 + src/bystander_ai_system.rs | 92 ++++++++++++++++ src/camera.rs | 15 ++- src/components.rs | 11 ++ src/main.rs | 6 + src/map/themes.rs | 3 +- src/map_builders/mod.rs | 1 + src/map_builders/town.rs | 218 ++++++++++++++++++++++++++++++++++--- src/player.rs | 44 ++++++-- src/raws/mob_structs.rs | 2 + src/raws/rawmaster.rs | 11 +- src/saveload_system.rs | 6 + 14 files changed, 529 insertions(+), 45 deletions(-) create mode 100644 src/bystander_ai_system.rs diff --git a/raws/mobs.json b/raws/mobs.json index b546e1d..4151602 100644 --- a/raws/mobs.json +++ b/raws/mobs.json @@ -1,11 +1,107 @@ [ + { + "id": "npc_barkeep", + "name": "barkeep", + "renderable": { "glyph": "@", "fg": "#EE82EE", "bg": "#000000", "order": 1 }, + "flags": ["BLOCKS_TILE"], + "stats": { "max_hp": 8, "hp": 8, "defence": 1, "power": 1 }, + "vision_range": 4, + "ai": "bystander" + }, + { + "id": "npc_townsperson", + "name": "townsperson", + "renderable": { "glyph": "@", "fg": "#9fa86c", "bg": "#000000", "order": 1 }, + "flags": ["BLOCKS_TILE"], + "stats": { "max_hp": 8, "hp": 8, "defence": 0, "power": 1 }, + "vision_range": 4, + "ai": "bystander", + "quips": [ "I don't get paid nearly enough to"] + }, + { + "id": "npc_drunk", + "name": "drunk", + "renderable": { "glyph": "@", "fg": "#a0a83c", "bg": "#000000", "order": 1 }, + "flags": ["BLOCKS_TILE"], + "stats": { "max_hp": 8, "hp": 8, "defence": 0, "power": 1 }, + "vision_range": 4, + "ai": "bystander", + "quips": [ "Hic!", "H-Hic'."] + }, + { + "id": "npc_fisher", + "name": "fisher", + "renderable": { "glyph": "@", "fg": "#3ca3a8", "bg": "#000000", "order": 1 }, + "flags": ["BLOCKS_TILE"], + "stats": { "max_hp": 8, "hp": 8, "defence": 0, "power": 1 }, + "vision_range": 4, + "ai": "bystander", + "quips": [ "Placeholder."] + }, + { + "id": "npc_dockworker", + "name": "dock worker", + "renderable": { "glyph": "@", "fg": "#68d8de", "bg": "#000000", "order": 1 }, + "flags": ["BLOCKS_TILE"], + "stats": { "max_hp": 8, "hp": 8, "defence": 1, "power": 1 }, + "vision_range": 4, + "ai": "bystander", + "quips": [ "Placeholder."] + }, + { + "id": "npc_priest", + "name": "priest", + "renderable": { "glyph": "@", "fg": "#FFFFFF", "bg": "#000000", "order": 1 }, + "flags": ["BLOCKS_TILE"], + "stats": { "max_hp": 8, "hp": 8, "defence": 0, "power": 1 }, + "vision_range": 4, + "ai": "bystander" + }, + { + "id": "npc_miner", + "name": "miner", + "renderable": { "glyph": "@", "fg": "#946123", "bg": "#000000", "order": 1 }, + "flags": ["BLOCKS_TILE"], + "stats": { "max_hp": 8, "hp": 8, "defence": 0, "power": 1 }, + "vision_range": 4, + "ai": "bystander" + }, + { + "id": "npc_guard", + "name": "smalltown guard", + "renderable": { "glyph": "@", "fg": "#034efc", "bg": "#000000", "order": 1 }, + "flags": ["BLOCKS_TILE"], + "stats": { "max_hp": 12, "hp": 12, "defence": 3, "power": 3 }, + "vision_range": 4, + "ai": "bystander", + "quips": ["I don't get paid nearly enough to go down the mine."] + }, { "id": "dog_little", "name": "little dog", "renderable": { "glyph": "d", "fg": "#FFFFFF", "bg": "#000000", "order": 1 }, "flags": ["BLOCKS_TILE"], "stats": { "max_hp": 6, "hp": 6, "defence": 0, "power": 1 }, - "vision_range": 12 + "vision_range": 12, + "ai": "melee" + }, + { + "id": "rat", + "name": "rat", + "renderable": { "glyph": "r", "fg": "#aa6000", "bg": "#000000", "order": 1 }, + "flags": ["BLOCKS_TILE"], + "stats": { "max_hp": 4, "hp": 4, "defence": 0, "power": 1 }, + "vision_range": 8, + "ai": "melee" + }, + { + "id": "rat_giant", + "name": "giant rat", + "renderable": { "glyph": "r", "fg": "#bb8000", "bg": "#000000", "order": 1 }, + "flags": ["BLOCKS_TILE"], + "stats": { "max_hp": 4, "hp": 4, "defence": 0, "power": 1 }, + "vision_range": 8, + "ai": "melee" }, { "id": "dog", @@ -13,7 +109,8 @@ "renderable": { "glyph": "d", "fg": "#EEEEEE", "bg": "#000000", "order": 1 }, "flags": ["BLOCKS_TILE"], "stats": { "max_hp": 8, "hp": 8, "defence": 0, "power": 2 }, - "vision_range": 12 + "vision_range": 12, + "ai": "melee" }, { "id": "dog_large", @@ -21,7 +118,8 @@ "renderable": { "glyph": "d", "fg": "#DDDDDD", "bg": "#000000", "order": 1 }, "flags": ["BLOCKS_TILE"], "stats": { "max_hp": 12, "hp": 12, "defence": 0, "power": 3 }, - "vision_range": 12 + "vision_range": 12, + "ai": "melee" }, { "id": "goblin", @@ -29,7 +127,8 @@ "renderable": { "glyph": "g", "fg": "#00FF00", "bg": "#000000", "order": 1 }, "flags": ["BLOCKS_TILE"], "stats": { "max_hp": 6, "hp": 6, "defence": 0, "power": 2 }, - "vision_range": 12 + "vision_range": 12, + "ai": "melee" }, { "id": "kobold", @@ -37,7 +136,8 @@ "renderable": { "glyph": "k", "fg": "#AA5500", "bg": "#000000", "order": 1 }, "flags": ["BLOCKS_TILE"], "stats": { "max_hp": 6, "hp": 6, "defence": 0, "power": 1 }, - "vision_range": 7 + "vision_range": 7, + "ai": "melee" }, { "id": "jackal", @@ -45,7 +145,8 @@ "renderable": { "glyph": "d", "fg": "#AA5500", "bg": "#000000", "order": 1 }, "flags": ["BLOCKS_TILE"], "stats": { "max_hp": 6, "hp": 6, "defence": 0, "power": 1 }, - "vision_range": 12 + "vision_range": 12, + "ai": "melee" }, { "id": "fox", @@ -53,7 +154,8 @@ "renderable": { "glyph": "d", "fg": "#FF0000", "bg": "#000000", "order": 1 }, "flags": ["BLOCKS_TILE"], "stats": { "max_hp": 4, "hp": 4, "defence": 0, "power": 1 }, - "vision_range": 12 + "vision_range": 12, + "ai": "melee" }, { "id": "coyote", @@ -61,7 +163,8 @@ "renderable": { "glyph": "d", "fg": "#6E3215", "bg": "#000000", "order": 1 }, "flags": ["BLOCKS_TILE"], "stats": { "max_hp": 8, "hp": 8, "defence": 0, "power": 2 }, - "vision_range": 12 + "vision_range": 12, + "ai": "melee" }, { "id": "goblin_chieftain", @@ -69,7 +172,8 @@ "renderable": { "glyph": "G", "fg": "#00FF00", "bg": "#000000", "order": 1 }, "flags": ["BLOCKS_TILE"], "stats": { "max_hp": 8, "hp": 8, "defence": 1, "power": 2 }, - "vision_range": 12 + "vision_range": 12, + "ai": "melee" }, { "id": "orc", @@ -77,7 +181,8 @@ "renderable": { "glyph": "o", "fg": "#00FF00", "bg": "#000000", "order": 1 }, "flags": ["BLOCKS_TILE"], "stats": { "max_hp": 8, "hp": 8, "defence": 0, "power": 3 }, - "vision_range": 12 + "vision_range": 12, + "ai": "melee" }, { "id": "orc_large", @@ -85,7 +190,8 @@ "renderable": { "glyph": "o", "fg": "#008000", "bg": "#000000", "order": 1 }, "flags": ["BLOCKS_TILE"], "stats": { "max_hp": 12, "hp": 12, "defence": 1, "power": 3 }, - "vision_range": 12 + "vision_range": 12, + "ai": "melee" }, { "id": "ogre", @@ -93,6 +199,7 @@ "renderable": { "glyph": "O", "fg": "#00FF00", "bg": "#000000", "order": 1 }, "flags": ["BLOCKS_TILE"], "stats": { "max_hp": 24, "hp": 24, "defence": 3, "power": 3 }, - "vision_range": 8 + "vision_range": 8, + "ai": "melee" } ] diff --git a/raws/props.json b/raws/props.json index d9a5408..1b8d75e 100644 --- a/raws/props.json +++ b/raws/props.json @@ -3,7 +3,37 @@ "id": "door", "name": "door", "renderable": { "glyph": "+", "fg": "#00FFFF", "bg": "#000000", "order": 2 }, - "flags": ["BLOCKS_TILE", "BLOCKS_VISIBILITY", "DOOR"] + "flags": ["BLOCKS_TILE", "BLOCKS_VISIBILITY", "DOOR", "PROP"] + }, + { + "id": "prop_keg", + "name": "keg", + "renderable": { "glyph": "φ", "fg": "#AAAAAA", "bg": "#000000", "order": 2 }, + "flags": ["PROP"] + }, + { + "id": "prop_table", + "name": "table", + "renderable": { "glyph": "-", "fg": "#AAAAAA", "bg": "#000000", "order": 2 }, + "flags": ["PROP"] + }, + { + "id": "prop_bed", + "name": "bed", + "renderable": { "glyph": "=", "fg": "#AAAAAA", "bg": "#000000", "order": 2 }, + "flags": ["PROP"] + }, + { + "id": "prop_chair", + "name": "chair", + "renderable": { "glyph": "└", "fg": "#AAAAAA", "bg": "#000000", "order": 2 }, + "flags": ["PROP"] + }, + { + "id": "prop_candle", + "name": "candle", + "renderable": { "glyph": "Ä", "fg": "#FFA500", "bg": "#000000", "order": 2 }, + "flags": ["PROP"] }, { "id": "trap_bear", diff --git a/raws/spawn_tables.json b/raws/spawn_tables.json index 5884877..db3a83f 100644 --- a/raws/spawn_tables.json +++ b/raws/spawn_tables.json @@ -46,11 +46,13 @@ { "id": "mobs", "table": [ + { "id": "rat", "weight": 1, "difficulty": 1}, { "id": "goblin", "weight": 3, "difficulty": 1}, { "id": "kobold", "weight": 1, "difficulty": 1}, { "id": "fox", "weight": 1, "difficulty": 1}, { "id": "jackal", "weight": 4, "difficulty": 1}, + { "id": "rat_giant", "weight": 2, "difficulty": 2}, { "id": "coyote", "weight": 4, "difficulty": 2}, { "id": "dog_little", "weight": 1, "difficulty": 3}, diff --git a/src/bystander_ai_system.rs b/src/bystander_ai_system.rs new file mode 100644 index 0000000..1542f5a --- /dev/null +++ b/src/bystander_ai_system.rs @@ -0,0 +1,92 @@ +use super::{gamelog, Bystander, EntityMoved, Map, Name, Point, Position, Quips, RunState, Viewshed}; +use specs::prelude::*; + +pub struct BystanderAI {} + +impl<'a> System<'a> for BystanderAI { + #[allow(clippy::type_complexity)] + type SystemData = ( + WriteExpect<'a, Map>, + ReadExpect<'a, RunState>, + Entities<'a>, + WriteStorage<'a, Viewshed>, + ReadStorage<'a, Bystander>, + WriteStorage<'a, Position>, + WriteStorage<'a, EntityMoved>, + WriteExpect<'a, rltk::RandomNumberGenerator>, + ReadExpect<'a, Point>, + WriteStorage<'a, Quips>, + ReadStorage<'a, Name>, + ); + + fn run(&mut self, data: Self::SystemData) { + let ( + mut map, + runstate, + entities, + mut viewshed, + bystander, + mut position, + mut entity_moved, + mut rng, + player_pos, + mut quips, + names, + ) = data; + + if *runstate != RunState::MonsterTurn { + return; + } + + for (entity, mut viewshed, _bystander, mut pos) in (&entities, &mut viewshed, &bystander, &mut position).join() + { + // Possibly quip + let quip = quips.get_mut(entity); + if let Some(quip) = quip { + if !quip.available.is_empty() + && viewshed.visible_tiles.contains(&player_pos) + && rng.roll_dice(1, 20) == 1 + { + let name = names.get(entity); + let quip_index = if quip.available.len() == 1 { + 0 + } else { + (rng.roll_dice(1, quip.available.len() as i32) - 1) as usize + }; + gamelog::Logger::new() + .npc_name(&name.unwrap().name) + .append_n("says \"") + .append_n(&quip.available[quip_index]) + .append("\"") + .log(); + quip.available.remove(quip_index); + } + } + + // Try to move randomly + let mut x = pos.x; + let mut y = pos.y; + let move_roll = rng.roll_dice(1, 8); + match move_roll { + 1 => x -= 1, + 2 => x += 1, + 3 => y -= 1, + 4 => y += 1, + _ => {} + } + + if x > 0 && x < map.width - 1 && y > 0 && y < map.height - 1 { + let dest_idx = map.xy_idx(x, y); + if !map.blocked[dest_idx] { + let idx = map.xy_idx(pos.x, pos.y); + map.blocked[idx] = false; + pos.x = x; + pos.y = y; + entity_moved.insert(entity, EntityMoved {}).expect("Unable to insert marker"); + map.blocked[dest_idx] = true; + viewshed.dirty = true; + } + } + } + } +} diff --git a/src/camera.rs b/src/camera.rs index 3325965..345b905 100644 --- a/src/camera.rs +++ b/src/camera.rs @@ -1,6 +1,7 @@ -use super::{Door, Hidden, Map, Mind, Position, Renderable}; +use super::{Hidden, Map, Mind, Position, Prop, Renderable}; use rltk::{Point, Rltk, RGB}; use specs::prelude::*; +use std::ops::Mul; const SHOW_BOUNDARIES: bool = false; @@ -53,7 +54,7 @@ pub fn render_camera(ecs: &World, ctx: &mut Rltk) { let renderables = ecs.read_storage::(); let minds = ecs.read_storage::(); let hidden = ecs.read_storage::(); - let doors = ecs.write_storage::(); + let props = ecs.write_storage::(); let map = ecs.fetch::(); let entities = ecs.entities(); @@ -66,11 +67,13 @@ pub fn render_camera(ecs: &World, ctx: &mut Rltk) { if entity_offset_x > 0 && entity_offset_x < map_width && entity_offset_y > 0 && entity_offset_y < map_height { let mut draw = false; - let fg = render.fg; + let mut fg = render.fg; let (_glyph, _fg, bg) = crate::map::themes::get_tile_glyph(idx, &*map); // Draw entities on visible tiles if map.visible_tiles[idx] { draw = true; + } else { + fg = fg.mul(0.75); } // Draw entities with minds within telepath range if map.telepath_tiles[idx] { @@ -79,9 +82,9 @@ pub fn render_camera(ecs: &World, ctx: &mut Rltk) { draw = true; } } - // Draw all doors - let is_door = doors.get(*ent); - if let Some(_) = is_door { + // Draw all props + let is_prop = props.get(*ent); + if let Some(_) = is_prop { if map.revealed_tiles[idx] { draw = true; } diff --git a/src/components.rs b/src/components.rs index e050e41..ca1c5ec 100644 --- a/src/components.rs +++ b/src/components.rs @@ -34,9 +34,20 @@ pub struct Renderable { #[derive(Component, Debug, Serialize, Deserialize, Clone)] pub struct Player {} +#[derive(Component, Debug, Serialize, Deserialize, Clone)] +pub struct Prop {} + #[derive(Component, Debug, Serialize, Deserialize, Clone)] pub struct Monster {} +#[derive(Component, Debug, Serialize, Deserialize, Clone)] +pub struct Bystander {} + +#[derive(Component, Debug, Serialize, Deserialize, Clone)] +pub struct Quips { + pub available: Vec, +} + #[derive(Component, Debug, Serialize, Deserialize, Clone)] pub struct Mind {} diff --git a/src/main.rs b/src/main.rs index b4fbb7d..dfdf398 100644 --- a/src/main.rs +++ b/src/main.rs @@ -22,6 +22,7 @@ mod visibility_system; use visibility_system::VisibilitySystem; mod monster_ai_system; use monster_ai_system::MonsterAI; +pub mod bystander_ai_system; mod map_indexing_system; use map_indexing_system::MapIndexingSystem; mod damage_system; @@ -123,6 +124,7 @@ impl State { fn run_systems(&mut self) { let mut vis = VisibilitySystem {}; let mut mob = MonsterAI {}; + let mut bystanders = bystander_ai_system::BystanderAI {}; let mut mapindex = MapIndexingSystem {}; let mut trigger_system = trigger_system::TriggerSystem {}; let mut melee_system = MeleeCombatSystem {}; @@ -136,6 +138,7 @@ impl State { vis.run_now(&self.ecs); mob.run_now(&self.ecs); + bystanders.run_now(&self.ecs); mapindex.run_now(&self.ecs); trigger_system.run_now(&self.ecs); inventory_system.run_now(&self.ecs); @@ -499,8 +502,11 @@ 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::(); gs.ecs.register::(); gs.ecs.register::(); gs.ecs.register::(); diff --git a/src/map/themes.rs b/src/map/themes.rs index 8804bed..32b5a0b 100644 --- a/src/map/themes.rs +++ b/src/map/themes.rs @@ -47,8 +47,7 @@ pub fn get_tile_glyph(idx: usize, map: &Map) -> (rltk::FontCharType, RGB, RGB) { } TileType::Bridge => { glyph = rltk::to_cp437('.'); - fg = default_bg; - bg = default_bg; + bg = RGB::from_u8(59, 49, 43); } TileType::Gravel => { glyph = rltk::to_cp437(';'); diff --git a/src/map_builders/mod.rs b/src/map_builders/mod.rs index f4420f4..6293f75 100644 --- a/src/map_builders/mod.rs +++ b/src/map_builders/mod.rs @@ -328,6 +328,7 @@ pub fn random_builder( } pub fn level_builder(new_id: i32, rng: &mut rltk::RandomNumberGenerator, width: i32, height: i32) -> BuilderChain { + // TODO: With difficulty and ID/depth decoupled, this can be used for branches later. let difficulty = new_id; match new_id { 1 => town_builder(new_id, rng, width, height), diff --git a/src/map_builders/town.rs b/src/map_builders/town.rs index e948977..ecf2325 100644 --- a/src/map_builders/town.rs +++ b/src/map_builders/town.rs @@ -19,6 +19,16 @@ impl InitialMapBuilder for TownBuilder { } } +enum BuildingTag { + Tavern, + Temple, + PlayerHouse, + NPCHouse, + Mine, + Abandoned, + Unassigned, +} + impl TownBuilder { pub fn new() -> Box { return Box::new(TownBuilder {}); @@ -30,6 +40,7 @@ impl TownBuilder { *t = true; } + // Build map self.grass_layer(build_data); let piers = self.water_and_piers(rng, build_data); let (mut available_building_tiles, wall_gap_y) = self.town_walls(rng, build_data); @@ -38,23 +49,198 @@ impl TownBuilder { self.path_from_tiles_to_nearest_tiletype(build_data, &doors, TileType::Road, TileType::Road, true); self.path_from_tiles_to_nearest_tiletype(build_data, &piers, TileType::Road, TileType::Road, false); - build_data.take_snapshot(); + // Spawn entities + let building_size = self.sort_buildings(&buildings); + self.building_factory(rng, build_data, &buildings, &building_size); + self.spawn_dockers(build_data, rng); + self.spawn_townsfolk(build_data, rng, &mut available_building_tiles); - // Sort buildings by size - let mut building_size: Vec<(usize, i32)> = Vec::new(); + build_data.take_snapshot(); + } + + fn sort_buildings(&mut self, buildings: &[(i32, i32, i32, i32)]) -> Vec<(usize, i32, BuildingTag)> { + // Sort buildings by size, defaulting them to Unassigned buildings + let mut building_size: Vec<(usize, i32, BuildingTag)> = Vec::new(); for (i, building) in buildings.iter().enumerate() { - building_size.push((i, building.2 * building.3)); + building_size.push((i, building.2 * building.3, BuildingTag::Unassigned)); } building_size.sort_by(|a, b| b.1.cmp(&a.1)); - // Largest building as start position - let tavern = &buildings[building_size[0].0]; - build_data.starting_position = Some(Position { x: tavern.0 + (tavern.2 / 2), y: tavern.1 + (tavern.3 / 2) }); + // Set individual buildings to their correct tags + building_size[0].2 = BuildingTag::Tavern; + building_size[1].2 = BuildingTag::Temple; + building_size[2].2 = BuildingTag::Mine; + building_size[3].2 = BuildingTag::PlayerHouse; + for b in building_size.iter_mut().skip(3) { + b.2 = BuildingTag::NPCHouse + } + let last_idx = building_size.len() - 1; + building_size[last_idx].2 = BuildingTag::Abandoned; - // Smallest building as mine entrance - let mine = &buildings[building_size[building_size.len() - 1].0]; - let exit_idx = build_data.map.xy_idx(mine.0 + (mine.2 / 2), mine.1 + (mine.3 / 2)); + return building_size; + } + + fn building_factory( + &mut self, + rng: &mut rltk::RandomNumberGenerator, + build_data: &mut BuilderMap, + buildings: &[(i32, i32, i32, i32)], + building_index: &[(usize, i32, BuildingTag)], + ) { + for (i, building) in buildings.iter().enumerate() { + let build_tag = &building_index[i].2; + match build_tag { + BuildingTag::Tavern => self.build_tavern(&building, build_data, rng), + BuildingTag::Temple => self.build_temple(&building, build_data, rng), + BuildingTag::Mine => self.build_mine(&building, build_data, rng), + BuildingTag::PlayerHouse => self.build_playerhouse(&building, build_data, rng), + BuildingTag::NPCHouse => self.build_npchouse(&building, build_data, rng), + BuildingTag::Abandoned => self.build_abandoned(&building, build_data, rng), + _ => {} + } + } + } + + fn spawn_dockers(&mut self, build_data: &mut BuilderMap, rng: &mut rltk::RandomNumberGenerator) { + for (idx, tt) in build_data.map.tiles.iter().enumerate() { + if *tt == TileType::Bridge && rng.roll_dice(1, 20) == 1 { + let roll = rng.roll_dice(1, 2); + match roll { + 1 => build_data.spawn_list.push((idx, "npc_fisher".to_string())), + _ => build_data.spawn_list.push((idx, "npc_dockworker".to_string())), + } + } + } + } + + fn spawn_townsfolk( + &mut self, + build_data: &mut BuilderMap, + rng: &mut rltk::RandomNumberGenerator, + available_building_tiles: &mut HashSet, + ) { + for idx in available_building_tiles.iter() { + if rng.roll_dice(1, 40) == 1 { + let roll = rng.roll_dice(1, 3); + match roll { + 1 => build_data.spawn_list.push((*idx, "npc_fisher".to_string())), + 2 => build_data.spawn_list.push((*idx, "npc_dockworker".to_string())), + 3 => build_data.spawn_list.push((*idx, "npc_townsperson".to_string())), + _ => build_data.spawn_list.push((*idx, "npc_drunk".to_string())), + } + } + } + } + + fn random_building_spawn( + &mut self, + building: &(i32, i32, i32, i32), + build_data: &mut BuilderMap, + rng: &mut rltk::RandomNumberGenerator, + to_place: &mut Vec<&str>, + avoid_tile: usize, + ) { + for y in building.1..building.1 + building.3 { + for x in building.0..building.0 + building.2 { + let idx = build_data.map.xy_idx(x, y); + if build_data.map.tiles[idx] == TileType::WoodFloor + && idx != avoid_tile + && rng.roll_dice(1, 3) == 1 + && !to_place.is_empty() + { + let entity_tag = to_place[0]; + to_place.remove(0); + build_data.spawn_list.push((idx, entity_tag.to_string())); + } + } + } + } + + fn build_tavern( + &mut self, + building: &(i32, i32, i32, i32), + build_data: &mut BuilderMap, + rng: &mut rltk::RandomNumberGenerator, + ) { + // Place player + build_data.starting_position = + Some(Position { x: building.0 + (building.2 / 2), y: building.1 + (building.3 / 2) }); + let player_idx = build_data.map.xy_idx(building.0 + (building.2 / 2), building.1 + (building.3 / 2)); + + // Place other items + let mut to_place: Vec<&str> = vec![ + "npc_barkeep", + "npc_townsperson", + "npc_townsperson", + "npc_drunk", + "npc_drunk", + "npc_guard", + "prop_keg", + "prop_table", + "prop_chair", + "prop_chair", + ]; + self.random_building_spawn(building, build_data, rng, &mut to_place, player_idx); + } + + fn build_temple( + &mut self, + building: &(i32, i32, i32, i32), + build_data: &mut BuilderMap, + rng: &mut rltk::RandomNumberGenerator, + ) { + let mut to_place: Vec<&str> = + vec!["npc_priest", "prop_chair", "prop_chair", "prop_table", "prop_candle", "prop_candle"]; + self.random_building_spawn(building, build_data, rng, &mut to_place, 0) + } + + fn build_mine( + &mut self, + building: &(i32, i32, i32, i32), + build_data: &mut BuilderMap, + rng: &mut rltk::RandomNumberGenerator, + ) { + // Place exit + let exit_idx = build_data.map.xy_idx(building.0 + (building.2 / 2), building.1 + (building.3 / 2)); build_data.map.tiles[exit_idx] = TileType::DownStair; + let mut to_place: Vec<&str> = vec!["npc_miner", "npc_miner", "npc_guard"]; + self.random_building_spawn(building, build_data, rng, &mut to_place, exit_idx) + } + + fn build_playerhouse( + &mut self, + building: &(i32, i32, i32, i32), + build_data: &mut BuilderMap, + rng: &mut rltk::RandomNumberGenerator, + ) { + let mut to_place: Vec<&str> = vec!["prop_bed", "prop_table"]; + self.random_building_spawn(building, build_data, rng, &mut to_place, 0); + } + + fn build_npchouse( + &mut self, + building: &(i32, i32, i32, i32), + build_data: &mut BuilderMap, + rng: &mut rltk::RandomNumberGenerator, + ) { + let mut to_place: Vec<&str> = vec!["prop_bed", "prop_table"]; + self.random_building_spawn(building, build_data, rng, &mut to_place, 0); + } + + fn build_abandoned( + &mut self, + building: &(i32, i32, i32, i32), + build_data: &mut BuilderMap, + rng: &mut rltk::RandomNumberGenerator, + ) { + for y in building.1..building.1 + building.3 { + for x in building.0..building.0 + building.2 { + let idx = build_data.map.xy_idx(x, y); + if build_data.map.tiles[idx] == TileType::WoodFloor && idx != 0 && rng.roll_dice(1, 3) == 1 { + build_data.spawn_list.push((idx, "rat".to_string())); + } + } + } } fn grass_layer(&mut self, build_data: &mut BuilderMap) { @@ -69,7 +255,7 @@ impl TownBuilder { let mut n = (rng.roll_dice(1, 65535) as f32) / 65535f32; let mut water_width: Vec = Vec::new(); let variance = 5; - let minimum_width = variance + 10; + let minimum_width = variance + 5; let shallow_width = 6; let sand_width = shallow_width + 4; @@ -123,9 +309,9 @@ impl TownBuilder { let idx = build_data.map.xy_idx(x, y); build_data.map.tiles[idx] = TileType::Fence; let idx = build_data.map.xy_idx(x, y + 1); - build_data.map.tiles[idx] = TileType::WoodFloor; + build_data.map.tiles[idx] = TileType::Bridge; let idx = build_data.map.xy_idx(x, y + 2); - build_data.map.tiles[idx] = TileType::WoodFloor; + build_data.map.tiles[idx] = TileType::Bridge; let idx = build_data.map.xy_idx(x, y + 3); build_data.map.tiles[idx] = TileType::Fence; } @@ -154,7 +340,7 @@ impl TownBuilder { let mut available_building_tiles: HashSet = HashSet::new(); const BORDER: i32 = 4; - const OFFSET_FROM_LEFT: i32 = 30 + BORDER; + const OFFSET_FROM_LEFT: i32 = 25 + BORDER; const PATH_OFFSET_FROM_CENTRE: i32 = 4; const HALF_PATH_THICKNESS: i32 = 3; @@ -216,8 +402,8 @@ impl TownBuilder { const BORDER: i32 = 2; const REQUIRED_BUILDINGS: i32 = 8; - const OFFSET_FROM_LEFT: i32 = 30; - const MIN_BUILDING_SIZE: i32 = 4; + const OFFSET_FROM_LEFT: i32 = 25; + const MIN_BUILDING_SIZE: i32 = 6; const MAX_BUILDING_SIZE: i32 = 10; while n_buildings < REQUIRED_BUILDINGS { diff --git a/src/player.rs b/src/player.rs index 90fdd52..b09a171 100644 --- a/src/player.rs +++ b/src/player.rs @@ -1,7 +1,7 @@ use super::{ - gamelog, BlocksTile, BlocksVisibility, CombatStats, Door, EntityMoved, Hidden, HungerClock, HungerState, Item, Map, - Monster, Name, ParticleBuilder, Player, Position, Renderable, RunState, State, SufferDamage, Telepath, TileType, - Viewshed, WantsToMelee, WantsToPickupItem, + gamelog, BlocksTile, BlocksVisibility, Bystander, CombatStats, Door, EntityMoved, Hidden, HungerClock, HungerState, + Item, Map, Monster, Name, ParticleBuilder, Player, Position, Renderable, RunState, State, SufferDamage, Telepath, + TileType, Viewshed, WantsToMelee, WantsToPickupItem, }; use rltk::{Point, RandomNumberGenerator, Rltk, VirtualKeyCode}; use specs::prelude::*; @@ -272,6 +272,7 @@ pub fn try_move_player(delta_x: i32, delta_y: i32, ecs: &mut World) -> bool { let mut viewsheds = ecs.write_storage::(); let mut telepaths = ecs.write_storage::(); let mut entity_moved = ecs.write_storage::(); + let friendlies = ecs.read_storage::(); let combat_stats = ecs.read_storage::(); let map = ecs.fetch::(); @@ -279,6 +280,7 @@ pub fn try_move_player(delta_x: i32, delta_y: i32, ecs: &mut World) -> bool { let mut wants_to_melee = ecs.write_storage::(); let mut doors = ecs.write_storage::(); let names = ecs.read_storage::(); + let mut swap_entities: Vec<(Entity, i32, i32)> = Vec::new(); for (entity, _player, pos, viewshed) in (&entities, &mut players, &mut positions, &mut viewsheds).join() { if pos.x + delta_x < 0 @@ -291,10 +293,24 @@ pub fn try_move_player(delta_x: i32, delta_y: i32, ecs: &mut World) -> bool { let destination_idx = map.xy_idx(pos.x + delta_x, pos.y + delta_y); for potential_target in map.tile_content[destination_idx].iter() { - let target = combat_stats.get(*potential_target); - if let Some(_target) = target { - wants_to_melee.insert(entity, WantsToMelee { target: *potential_target }).expect("Add target failed."); - return true; + let friendly = friendlies.get(*potential_target); + if friendly.is_some() { + swap_entities.push((*potential_target, pos.x, pos.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("Unable to insert marker"); + viewshed.dirty = true; + let mut ppos = ecs.write_resource::(); + ppos.x = pos.x; + ppos.y = pos.y; + } else { + let target = combat_stats.get(*potential_target); + if let Some(_target) = target { + wants_to_melee + .insert(entity, WantsToMelee { target: *potential_target }) + .expect("Add target failed."); + return true; + } } let door = doors.get_mut(*potential_target); if let Some(door) = door { @@ -307,6 +323,20 @@ pub fn try_move_player(delta_x: i32, delta_y: i32, ecs: &mut World) -> bool { } } + if swap_entities.len() > 0 { + for m in swap_entities.iter() { + let their_pos = positions.get_mut(m.0); + if let Some(name) = names.get(m.0) { + gamelog::Logger::new().append("You swap places with the").npc_name_n(&name.name).period().log(); + } + if let Some(their_pos) = their_pos { + their_pos.x = m.1; + their_pos.y = m.2; + } + } + + return true; + } if map.blocked[destination_idx] { gamelog::Logger::new().append("You can't move there.").log(); return false; diff --git a/src/raws/mob_structs.rs b/src/raws/mob_structs.rs index afdf46f..b12392b 100644 --- a/src/raws/mob_structs.rs +++ b/src/raws/mob_structs.rs @@ -9,6 +9,8 @@ pub struct Mob { pub flags: Option>, pub stats: MobStats, pub vision_range: i32, + pub ai: String, + pub quips: Option>, } #[derive(Deserialize, Debug)] diff --git a/src/raws/rawmaster.rs b/src/raws/rawmaster.rs index ccb7d0d..4ac7503 100644 --- a/src/raws/rawmaster.rs +++ b/src/raws/rawmaster.rs @@ -149,7 +149,11 @@ pub fn spawn_named_mob( let mut eb = new_entity; eb = spawn_position(pos, eb); eb = eb.with(Name { name: mob_template.name.clone(), plural: mob_template.name.clone() }); - eb = eb.with(Monster {}); + match mob_template.ai.as_ref() { + "bystander" => eb = eb.with(Bystander {}), + "melee" => eb = eb.with(Monster {}), + _ => {} + } let rolled_hp = roll_hit_dice(rng, 1, mob_template.stats.max_hp); eb = eb.with(CombatStats { max_hp: rolled_hp, @@ -172,6 +176,10 @@ pub fn spawn_named_mob( } } + if let Some(quips) = &mob_template.quips { + eb = eb.with(Quips { available: quips.clone() }); + } + return Some(eb.build()); } None @@ -207,6 +215,7 @@ pub fn spawn_named_prop(raws: &RawMaster, new_entity: EntityBuilder, key: &str, "ENTRY_TRIGGER" => eb = eb.with(EntryTrigger {}), "SINGLE_ACTIVATION" => eb = eb.with(SingleActivation {}), "DOOR" => eb = eb.with(Door { open: false }), + "PROP" => eb = eb.with(Prop {}), _ => rltk::console::log(format!("Unrecognised flag: {}", flag.as_str())), } } diff --git a/src/saveload_system.rs b/src/saveload_system.rs index 293ab0d..3fc18eb 100644 --- a/src/saveload_system.rs +++ b/src/saveload_system.rs @@ -51,6 +51,7 @@ pub fn save_game(ecs: &mut World) { Attributes, BlocksTile, BlocksVisibility, + Bystander, CombatStats, Confusion, Consumable, @@ -76,8 +77,10 @@ pub fn save_game(ecs: &mut World) { ParticleLifetime, Player, Position, + Prop, ProvidesHealing, ProvidesNutrition, + Quips, Ranged, Renderable, SingleActivation, @@ -147,6 +150,7 @@ pub fn load_game(ecs: &mut World) { Attributes, BlocksTile, BlocksVisibility, + Bystander, CombatStats, Confusion, Consumable, @@ -172,8 +176,10 @@ pub fn load_game(ecs: &mut World) { ParticleLifetime, Player, Position, + Prop, ProvidesHealing, ProvidesNutrition, + Quips, Ranged, Renderable, SingleActivation,