From 6cef899ef69ff214875ae07ab51ec95a65674c22 Mon Sep 17 00:00:00 2001 From: Llywelwyn Date: Mon, 31 Jul 2023 22:24:38 +0100 Subject: [PATCH] ai refactor, mob spawns now take into account player level, small fixes --- raws/mobs.json | 89 ++++++++++++------- raws/spawn_tables.json | 9 +- src/ai/energy_system.rs | 1 + src/ai/mod.rs | 6 +- src/ai/quip_system.rs | 38 ++++++++ .../{turn_status.rs => turn_status_system.rs} | 0 src/bystander_ai_system.rs | 43 +-------- src/gui/mod.rs | 2 +- src/gui/tooltip.rs | 55 ++++++------ src/main.rs | 20 +++-- src/map_builders/forest.rs | 3 +- src/map_builders/mod.rs | 28 ++++-- src/map_builders/prefab_builder/mod.rs | 11 +-- src/map_builders/room_based_spawner.rs | 8 +- src/map_builders/rooms_corridors_spawner.rs | 8 +- src/map_builders/town.rs | 12 ++- src/map_builders/voronoi_spawning.rs | 8 +- src/raws/mob_structs.rs | 2 +- src/raws/rawmaster.rs | 25 +++++- src/spawner.rs | 72 +++++++++++---- src/trigger_system.rs | 9 +- 21 files changed, 301 insertions(+), 148 deletions(-) create mode 100644 src/ai/quip_system.rs rename src/ai/{turn_status.rs => turn_status_system.rs} (100%) diff --git a/raws/mobs.json b/raws/mobs.json index 9a23720..c5423f1 100644 --- a/raws/mobs.json +++ b/raws/mobs.json @@ -44,8 +44,8 @@ "name": "priest", "renderable": { "glyph": "@", "fg": "#FFFFFF", "bg": "#000000", "order": 1 }, "flags": ["BYSTANDER", "BLOCKS_TILE"], - "quips": ["Light's givings.", "", "Bless you."], - "vision_range": 4 + "vision_range": 4, + "quips": ["Light's givings.", "", "Bless you."] }, { "id": "npc_miner", @@ -53,8 +53,8 @@ "renderable": { "glyph": "@", "fg": "#946123", "bg": "#000000", "order": 1 }, "flags": ["BYSTANDER", "BLOCKS_TILE"], "vision_range": 4, - "quips": ["You're not borrowing my pick."], - "attacks": [{ "name": "hits", "hit_bonus": 0, "damage": "1d8" }] + "attacks": [{ "name": "hits", "hit_bonus": 0, "damage": "1d8" }], + "quips": ["You're not borrowing my pick."] }, { "id": "npc_guard", @@ -63,20 +63,9 @@ "flags": ["BYSTANDER", "BLOCKS_TILE"], "level": 2, "vision_range": 4, - "quips": ["You wont catch me down the mine.", "Staying out of trouble?"], "attacks": [{ "name": "hits", "hit_bonus": 0, "damage": "1d8" }], - "equipped": ["equip_shortsword", "equip_body_leather"] - }, - { - "id": "dog_little", - "name": "little dog", - "renderable": { "glyph": "d", "fg": "#FFFFFF", "bg": "#000000", "order": 1 }, - "flags": ["BYSTANDER", "BLOCKS_TILE"], - "level": 2, - "bac": 6, - "vision_range": 12, - "quips": ["", "", ""], - "attacks": [{ "name": "bites", "hit_bonus": 0, "damage": "1d6" }] + "equipped": ["equip_shortsword", "equip_body_leather"], + "quips": ["You wont catch me down the mine.", "Staying out of trouble?"] }, { "id": "rat", @@ -86,8 +75,7 @@ "bac": 6, "vision_range": 8, "attacks": [{ "name": "bites", "hit_bonus": 0, "damage": "1d2" }], - "equipped": ["equip_shortsword", "equip_body_leather"], - "loot": { "table": "scrolls", "chance": 0.05 } + "loot": { "table": "food", "chance": 0.1 } }, { "id": "chicken", @@ -111,7 +99,7 @@ "id": "sheep_little", "name": "lamb", "renderable": { "glyph": "q", "fg": "#e7e7e7", "bg": "#000000", "order": 1 }, - "flags": ["BYSTANDER", "BLOCKS_TILE"], + "flags": ["BYSTANDER", "BLOCKS_TILE", "SMALL_GROUP"], "bac": 10, "vision_range": 4, "attacks": [{ "name": "kicks", "hit_bonus": 0, "damage": "1d2" }] @@ -132,11 +120,13 @@ "flags": ["BYSTANDER", "BLOCKS_TILE", "MULTIATTACK"], "level": 3, "bac": 6, + "speed": 16, "vision_range": 8, "attacks": [ { "name": "kicks", "hit_bonus": 0, "damage": "1d6" }, { "name": "bites", "hit_bonus": 0, "damage": "1d2" } - ] + ], + "quips": [""] }, { "id": "horse", @@ -145,6 +135,7 @@ "flags": ["MONSTER", "BLOCKS_TILE", "MULTIATTACK"], "level": 5, "bac": 5, + "speed": 20, "vision_range": 8, "attacks": [ { "name": "kicks", "hit_bonus": 0, "damage": "1d8" }, @@ -158,6 +149,7 @@ "flags": ["MONSTER", "BLOCKS_TILE", "MULTIATTACK"], "level": 7, "bac": 4, + "speed": 24, "vision_range": 8, "attacks": [ { "name": "kicks", "hit_bonus": 0, "damage": "1d10" }, @@ -168,13 +160,25 @@ "id": "rat_giant", "name": "giant rat", "renderable": { "glyph": "r", "fg": "#bb8000", "bg": "#000000", "order": 1 }, - "flags": ["MONSTER", "BLOCKS_TILE"], + "flags": ["MONSTER", "BLOCKS_TILE", "SMALL_GROUP"], "level": 1, "bac": 7, "vision_range": 8, "attacks": [{ "name": "bites", "hit_bonus": 0, "damage": "1d3" }], "loot": { "table": "scrolls", "chance": 0.05 } }, + { + "id": "dog_little", + "name": "little dog", + "renderable": { "glyph": "d", "fg": "#FFFFFF", "bg": "#000000", "order": 1 }, + "flags": ["BYSTANDER", "BLOCKS_TILE"], + "level": 2, + "bac": 6, + "speed": 18, + "vision_range": 12, + "quips": ["", "", ""], + "attacks": [{ "name": "bites", "hit_bonus": 0, "damage": "1d6" }] + }, { "id": "dog", "name": "dog", @@ -182,6 +186,7 @@ "flags": ["MONSTER", "BLOCKS_TILE"], "level": 4, "bac": 5, + "speed": 16, "vision_range": 12, "attacks": [{ "name": "bites", "hit_bonus": 0, "damage": "1d6" }] }, @@ -192,6 +197,7 @@ "flags": ["MONSTER", "BLOCKS_TILE"], "level": 6, "bac": 4, + "speed": 15, "vision_range": 12, "attacks": [{ "name": "bites", "hit_bonus": 0, "damage": "2d4" }] }, @@ -201,6 +207,7 @@ "renderable": { "glyph": "g", "fg": "#00FF00", "bg": "#000000", "order": 1 }, "flags": ["MONSTER", "BLOCKS_TILE"], "level": 1, + "speed": 9, "vision_range": 12, "attacks": [{ "name": "hits", "hit_bonus": 0, "damage": "1d4" }] }, @@ -218,7 +225,7 @@ "id": "jackal", "name": "jackal", "renderable": { "glyph": "d", "fg": "#AA5500", "bg": "#000000", "order": 1 }, - "flags": ["MONSTER", "BLOCKS_TILE"], + "flags": ["MONSTER", "BLOCKS_TILE", "SMALL_GROUP"], "bac": 7, "vision_range": 12, "attacks": [{ "name": "bites", "hit_bonus": 0, "damage": "1d2" }] @@ -236,7 +243,7 @@ "id": "coyote", "name": "coyote", "renderable": { "glyph": "d", "fg": "#6E3215", "bg": "#000000", "order": 1 }, - "flags": ["MONSTER", "BLOCKS_TILE"], + "flags": ["MONSTER", "BLOCKS_TILE", "SMALL_GROUP"], "level": 1, "bac": 7, "vision_range": 12, @@ -258,6 +265,7 @@ "renderable": { "glyph": "G", "fg": "#00FF00", "bg": "#000000", "order": 1 }, "flags": ["MONSTER", "BLOCKS_TILE"], "level": 2, + "speed": 9, "vision_range": 12, "attacks": [{ "name": "hits", "hit_bonus": 0, "damage": "1d8" }], "loot": { "table": "wands", "chance": 0.05 } @@ -266,29 +274,48 @@ "id": "orc", "name": "orc", "renderable": { "glyph": "o", "fg": "#00FF00", "bg": "#000000", "order": 1 }, - "flags": ["MONSTER", "BLOCKS_TILE"], + "flags": ["MONSTER", "BLOCKS_TILE", "SMALL_GROUP"], + "level": 1, + "speed": 9, "vision_range": 12, "attacks": [{ "name": "hits", "hit_bonus": 0, "damage": "1d6" }], "loot": { "table": "equipment", "chance": 0.05 } }, { - "id": "orc_large", - "name": "large orc", - "renderable": { "glyph": "o", "fg": "#008000", "bg": "#000000", "order": 1 }, - "flags": ["MONSTER", "BLOCKS_TILE"], + "id": "orc_hill", + "name": "hill orc", + "renderable": { "glyph": "o", "fg": "#dbd830", "bg": "#000000", "order": 1 }, + "flags": ["MONSTER", "BLOCKS_TILE", "LARGE_GROUP"], "level": 2, - "vision_range": 12, + "speed": 9, + "vision_range": 11, "attacks": [{ "name": "hits", "hit_bonus": 0, "damage": "1d6" }], "loot": { "table": "equipment", "chance": 0.05 } }, + { + "id": "orc_captain", + "name": "orc captain", + "renderable": { "glyph": "o", "fg": "#9331ac", "bg": "#000000", "order": 1 }, + "flags": ["MONSTER", "BLOCKS_TILE", "MULTIATTACK"], + "level": 5, + "speed": 5, + "vision_range": 12, + "attacks": [ + { "name": "hits", "hit_bonus": 0, "damage": "2d4" }, + { "name": "hits", "hit_bonus": 0, "damage": "2d4" } + ], + "loot": { "table": "equipment", "chance": 0.05 } + }, { "id": "ogre", "name": "ogre", "renderable": { "glyph": "O", "fg": "#00FF00", "bg": "#000000", "order": 1 }, - "flags": ["MONSTER", "BLOCKS_TILE"], + "flags": ["MONSTER", "BLOCKS_TILE", "SMALL_GROUP"], "level": 5, "bac": 5, + "speed": 10, "vision_range": 8, + "attacks": [{ "name": "hits", "hit_bonus": 0, "damage": "2d5" }], "loot": { "table": "food", "chance": 0.05 } } ] diff --git a/raws/spawn_tables.json b/raws/spawn_tables.json index c35d381..08f3c65 100644 --- a/raws/spawn_tables.json +++ b/raws/spawn_tables.json @@ -61,6 +61,7 @@ { "id": "mobs", "table": [ + { "id": "sheep_little", "weight": 1, "difficulty": 0}, { "id": "chicken", "weight": 1, "difficulty": 1}, { "id": "rat", "weight": 1, "difficulty": 1}, { "id": "goblin", "weight": 3, "difficulty": 1}, @@ -68,19 +69,19 @@ { "id": "fox", "weight": 1, "difficulty": 1}, { "id": "jackal", "weight": 4, "difficulty": 1}, { "id": "deer_little", "weight": 1, "difficulty": 1}, - { "id": "sheep_little", "weight": 1, "difficulty": 1}, { "id": "rat_giant", "weight": 2, "difficulty": 2}, { "id": "coyote", "weight": 4, "difficulty": 2}, { "id": "dog_little", "weight": 1, "difficulty": 3}, { "id": "orc", "weight": 2, "difficulty": 3}, - { "id": "orc_large", "weight": 1, "difficulty": 3}, + { "id": "orc_hill", "weight": 1, "difficulty": 4}, { "id": "goblin_chieftain", "weight": 1, "difficulty": 3}, - { "id": "ogre", "weight": 1, "difficulty": 4}, { "id": "horse_little", "weight": 2, "difficulty": 4}, { "id": "dog", "weight": 1, "difficulty": 5}, { "id": "wolf", "weight": 2, "difficulty": 6}, + { "id": "orc_captain", "weight": 1, "difficulty": 7}, { "id": "dog_large", "weight": 1, "difficulty": 7}, { "id": "horse", "weight": 2, "difficulty": 7}, + { "id": "ogre", "weight": 1, "difficulty": 7}, { "id": "horse_large", "weight": 2, "difficulty": 9} ] }, @@ -89,7 +90,7 @@ "table": [ { "id": "trap_bear", "weight": 2, "difficulty": 1}, { "id": "trap_confusion", "weight": 1, "difficulty": 1}, - { "id": "trap_stonefall", "weight": 1, "difficulty": 3} + { "id": "trap_stonefall", "weight": 1, "difficulty": 5} ] } ] diff --git a/src/ai/energy_system.rs b/src/ai/energy_system.rs index 21cf723..6b50bf9 100644 --- a/src/ai/energy_system.rs +++ b/src/ai/energy_system.rs @@ -32,6 +32,7 @@ impl<'a> System<'a> for EnergySystem { if energy.current >= TURN_COST { energy.current -= TURN_COST; crate::gamelog::record_event("turns", 1); + // Handle spawning mobs each turn if LOG_TICKS { console::log(format!("===== TURN {} =====", crate::gamelog::get_event_count("turns"))); } diff --git a/src/ai/mod.rs b/src/ai/mod.rs index b4d44db..6e480cf 100644 --- a/src/ai/mod.rs +++ b/src/ai/mod.rs @@ -1,4 +1,6 @@ mod energy_system; pub use energy_system::{EnergySystem, NORMAL_SPEED}; -mod turn_status; -pub use turn_status::TurnStatusSystem; +mod turn_status_system; +pub use turn_status_system::TurnStatusSystem; +mod quip_system; +pub use quip_system::QuipSystem; diff --git a/src/ai/quip_system.rs b/src/ai/quip_system.rs new file mode 100644 index 0000000..5f25cee --- /dev/null +++ b/src/ai/quip_system.rs @@ -0,0 +1,38 @@ +use crate::{gamelog, Name, Quips, TakingTurn, Viewshed}; +use rltk::prelude::*; +use specs::prelude::*; + +pub struct QuipSystem {} + +impl<'a> System<'a> for QuipSystem { + #[allow(clippy::type_complexity)] + type SystemData = ( + WriteStorage<'a, Quips>, + ReadStorage<'a, Name>, + ReadStorage<'a, TakingTurn>, + ReadExpect<'a, Point>, + ReadStorage<'a, Viewshed>, + WriteExpect<'a, RandomNumberGenerator>, + ); + + fn run(&mut self, data: Self::SystemData) { + let (mut quips, names, turns, player_pos, viewsheds, mut rng) = data; + for (quip, name, viewshed, _turn) in (&mut quips, &names, &viewsheds, &turns).join() { + if !quip.available.is_empty() && viewshed.visible_tiles.contains(&player_pos) && rng.roll_dice(1, 6) == 1 { + 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() + .append("The") + .npc_name(&name.name) + .append_n("says \"") + .append_n(&quip.available[quip_index]) + .append("\"") + .log(); + quip.available.remove(quip_index); + } + } + } +} diff --git a/src/ai/turn_status.rs b/src/ai/turn_status_system.rs similarity index 100% rename from src/ai/turn_status.rs rename to src/ai/turn_status_system.rs diff --git a/src/bystander_ai_system.rs b/src/bystander_ai_system.rs index 4856169..eaff950 100644 --- a/src/bystander_ai_system.rs +++ b/src/bystander_ai_system.rs @@ -1,4 +1,4 @@ -use super::{gamelog, Bystander, EntityMoved, Map, Name, Point, Position, Quips, TakingTurn, Viewshed}; +use super::{Bystander, EntityMoved, Map, Position, TakingTurn, Viewshed}; use specs::prelude::*; pub struct BystanderAI {} @@ -13,54 +13,15 @@ impl<'a> System<'a> for BystanderAI { WriteStorage<'a, Position>, WriteStorage<'a, EntityMoved>, WriteExpect<'a, rltk::RandomNumberGenerator>, - ReadExpect<'a, Point>, - WriteStorage<'a, Quips>, - ReadStorage<'a, Name>, ReadStorage<'a, TakingTurn>, ); fn run(&mut self, data: Self::SystemData) { - let ( - mut map, - entities, - mut viewshed, - bystander, - mut position, - mut entity_moved, - mut rng, - player_pos, - mut quips, - names, - turns, - ) = data; + let (mut map, entities, mut viewshed, bystander, mut position, mut entity_moved, mut rng, turns) = data; for (entity, mut viewshed, _bystander, mut pos, _turn) in (&entities, &mut viewshed, &bystander, &mut position, &turns).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() - .append("The") - .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; diff --git a/src/gui/mod.rs b/src/gui/mod.rs index f9e58f5..a14b5b0 100644 --- a/src/gui/mod.rs +++ b/src/gui/mod.rs @@ -468,7 +468,7 @@ pub fn remove_item_menu(gs: &mut State, ctx: &mut Rltk) -> (ItemMenuResult, Opti 1 + y_offset, RGB::named(rltk::WHITE), RGB::named(rltk::BLACK), - "Drop what? [aA-zZ][Esc.]", + "Unequip what? [aA-zZ][Esc.]", ); let mut equippable: Vec<(Entity, String)> = Vec::new(); diff --git a/src/gui/tooltip.rs b/src/gui/tooltip.rs index e0271c1..37cee9d 100644 --- a/src/gui/tooltip.rs +++ b/src/gui/tooltip.rs @@ -1,23 +1,29 @@ -use super::{camera::get_screen_bounds, Attributes, Hidden, Map, Name, Pools, Position, Rltk, World, RGB}; +use super::{camera::get_screen_bounds, Attributes, Hidden, Map, Name, Pools, Position, Renderable, Rltk, World, RGB}; use rltk::prelude::*; use specs::prelude::*; struct Tooltip { - lines: Vec, + lines: Vec<(String, RGB)>, } +const ATTRIBUTE_COLOUR: RGB = RGB { r: 1.0, g: 0.75, b: 0.8 }; +const RED_WARNING: RGB = RGB { r: 1.0, g: 0.0, b: 0.0 }; +const ORANGE_WARNING: RGB = RGB { r: 1.0, g: 0.65, b: 0.0 }; +const YELLOW_WARNING: RGB = RGB { r: 1.0, g: 1.0, b: 0.0 }; +const GREEN_WARNING: RGB = RGB { r: 0.0, g: 1.0, b: 0.0 }; + impl Tooltip { fn new() -> Tooltip { return Tooltip { lines: Vec::new() }; } - fn add(&mut self, line: S) { - self.lines.push(line.to_string()); + fn add(&mut self, line: S, fg: RGB) { + self.lines.push((line.to_string(), fg)); } fn width(&self) -> i32 { let mut max = 0; for s in self.lines.iter() { - if s.len() > max { - max = s.len(); + if s.0.len() > max { + max = s.0.len(); } } return max as i32 + 2i32; @@ -26,23 +32,9 @@ impl Tooltip { return self.lines.len() as i32 + 2i32; } fn render(&self, ctx: &mut Rltk, x: i32, y: i32) { - let white = RGB::named(rltk::WHITE); - let weak = RGB::named(rltk::CYAN); - let strong = RGB::named(rltk::ORANGE); - let attribute = RGB::named(rltk::PINK); - ctx.draw_box(x, y, self.width() - 1, self.height() - 1, RGB::named(WHITE), RGB::named(BLACK)); for (i, s) in self.lines.iter().enumerate() { - let col = if i == 0 { - white - } else if s.starts_with('-') { - weak - } else if s.starts_with('*') { - strong - } else { - attribute - }; - ctx.print_color(x + 1, y + i as i32 + 1, col, RGB::named(BLACK), &s); + ctx.print_color(x + 1, y + i as i32 + 1, s.1, RGB::named(BLACK), &s.0); } } } @@ -53,6 +45,7 @@ pub fn draw_tooltips(ecs: &World, ctx: &mut Rltk) { let map = ecs.fetch::(); let names = ecs.read_storage::(); let positions = ecs.read_storage::(); + let renderables = ecs.read_storage::(); let hidden = ecs.read_storage::(); let attributes = ecs.read_storage::(); let pools = ecs.read_storage::(); @@ -77,10 +70,10 @@ pub fn draw_tooltips(ecs: &World, ctx: &mut Rltk) { } let mut tooltips: Vec = Vec::new(); - for (entity, name, position, _hidden) in (&entities, &names, &positions, !&hidden).join() { + for (entity, name, position, renderable, _hidden) in (&entities, &names, &positions, &renderables, !&hidden).join() { if position.x == mouse_pos_adjusted.0 && position.y == mouse_pos_adjusted.1 { let mut tip = Tooltip::new(); - tip.add(name.name.to_string()); + tip.add(name.name.to_string(), renderable.fg); // Attributes let attr = attributes.get(entity); if let Some(a) = attr { @@ -101,7 +94,7 @@ pub fn draw_tooltips(ecs: &World, ctx: &mut Rltk) { if s.ends_with(" ") { s.pop(); } - tip.add(s); + tip.add(s, ATTRIBUTE_COLOUR); } } // Pools @@ -110,9 +103,19 @@ pub fn draw_tooltips(ecs: &World, ctx: &mut Rltk) { if let Some(p) = pool { let level_diff: i32 = p.level - player_pool.level; if level_diff <= -2 { - tip.add("-weak-"); + tip.add("-weak-", YELLOW_WARNING); } else if level_diff >= 2 { - tip.add("*threatening*"); + tip.add("*threatening*", ORANGE_WARNING); + } + let health_percent: f32 = p.hit_points.current as f32 / p.hit_points.max as f32; + if health_percent == 1.0 { + tip.add("healthy", GREEN_WARNING); + } else if health_percent <= 0.25 { + tip.add("*critical*", RED_WARNING); + } else if health_percent <= 0.5 { + tip.add("-bloodied-", ORANGE_WARNING); + } else if health_percent <= 0.75 { + tip.add("injured", YELLOW_WARNING); } } tooltips.push(tip); diff --git a/src/main.rs b/src/main.rs index 4ed2eac..264e95b 100644 --- a/src/main.rs +++ b/src/main.rs @@ -82,7 +82,15 @@ impl State { self.mapgen_timer = 0.0; self.mapgen_history.clear(); let mut rng = self.ecs.write_resource::(); - let mut builder = map_builders::level_builder(new_id, &mut rng, 100, 50); + let mut player_level = 1; + { + let player = self.ecs.read_storage::(); + let pools = self.ecs.read_storage::(); + for (_p, pool) in (&player, &pools).join() { + player_level = pool.level; + } + } + let mut builder = map_builders::level_builder(new_id, &mut rng, 100, 50, player_level); builder.build_map(&mut rng); std::mem::drop(rng); self.mapgen_history = builder.build_data.history.clone(); @@ -125,6 +133,7 @@ impl State { let mut vis = VisibilitySystem {}; let mut energy = ai::EnergySystem {}; let mut turn_status_system = ai::TurnStatusSystem {}; + let mut quip_system = ai::QuipSystem {}; let mut mob = MonsterAI {}; let mut bystanders = bystander_ai_system::BystanderAI {}; let mut trigger_system = trigger_system::TriggerSystem {}; @@ -141,6 +150,7 @@ impl State { vis.run_now(&self.ecs); energy.run_now(&self.ecs); turn_status_system.run_now(&self.ecs); + quip_system.run_now(&self.ecs); mob.run_now(&self.ecs); bystanders.run_now(&self.ecs); trigger_system.run_now(&self.ecs); @@ -595,11 +605,11 @@ fn main() -> rltk::BError { raws::load_raws(); gs.ecs.insert(rltk::RandomNumberGenerator::new()); - gs.ecs.insert(Map::new(1, 64, 64, 0, "New Map")); - gs.ecs.insert(Point::new(0, 0)); + gs.ecs.insert(Map::new(1, 64, 64, 0, "New Map")); // Map + gs.ecs.insert(Point::new(0, 0)); // Player pos let player_entity = spawner::player(&mut gs.ecs, 0, 0); - gs.ecs.insert(player_entity); - gs.ecs.insert(RunState::MapGeneration {}); + gs.ecs.insert(player_entity); // Player entity + gs.ecs.insert(RunState::MapGeneration {}); // RunState gs.ecs.insert(particle_system::ParticleBuilder::new()); gs.ecs.insert(rex_assets::RexAssets::new()); diff --git a/src/map_builders/forest.rs b/src/map_builders/forest.rs index 2d66a8d..caa80e4 100644 --- a/src/map_builders/forest.rs +++ b/src/map_builders/forest.rs @@ -10,8 +10,9 @@ pub fn forest_builder( width: i32, height: i32, difficulty: i32, + initial_player_level: i32, ) -> BuilderChain { - let mut chain = BuilderChain::new(new_id, width, height, difficulty, "Into the Woods"); + let mut chain = BuilderChain::new(new_id, width, height, difficulty, "Into the Woods", initial_player_level); chain.start_with(CellularAutomataBuilder::new()); chain.with(AreaStartingPosition::new(XStart::CENTRE, YStart::CENTRE)); chain.with(CullUnreachable::new()); diff --git a/src/map_builders/mod.rs b/src/map_builders/mod.rs index 69c1392..6a36568 100644 --- a/src/map_builders/mod.rs +++ b/src/map_builders/mod.rs @@ -73,6 +73,7 @@ pub struct BuilderMap { pub history: Vec, pub width: i32, pub height: i32, + pub initial_player_level: i32, } impl BuilderMap { @@ -94,7 +95,14 @@ pub struct BuilderChain { } impl BuilderChain { - pub fn new(new_id: i32, width: i32, height: i32, difficulty: i32, name: S) -> BuilderChain { + pub fn new( + new_id: i32, + width: i32, + height: i32, + difficulty: i32, + name: S, + initial_player_level: i32, + ) -> BuilderChain { BuilderChain { starter: None, builders: Vec::new(), @@ -107,6 +115,7 @@ impl BuilderChain { history: Vec::new(), width: width, height: height, + initial_player_level: initial_player_level, }, } } @@ -296,9 +305,10 @@ pub fn random_builder( width: i32, height: i32, difficulty: i32, + initial_player_level: i32, ) -> BuilderChain { rltk::console::log(format!("DEBUGINFO: Building random (ID:{}, DIFF:{})", new_id, difficulty)); - let mut builder = BuilderChain::new(new_id, width, height, difficulty, ""); + let mut builder = BuilderChain::new(new_id, width, height, difficulty, "", initial_player_level); let type_roll = rng.roll_dice(1, 2); let mut want_doors = true; match type_roll { @@ -339,12 +349,18 @@ pub fn random_builder( builder } -pub fn level_builder(new_id: i32, rng: &mut rltk::RandomNumberGenerator, width: i32, height: i32) -> BuilderChain { +pub fn level_builder( + new_id: i32, + rng: &mut rltk::RandomNumberGenerator, + width: i32, + height: i32, + initial_player_level: 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), - 2 => forest_builder(new_id, rng, width, height, difficulty), - _ => random_builder(new_id, rng, width, height, difficulty), + 1 => town_builder(new_id, rng, width, height, 0, initial_player_level), + 2 => forest_builder(new_id, rng, width, height, 1, initial_player_level), + _ => random_builder(new_id, rng, width, height, difficulty, initial_player_level), } } diff --git a/src/map_builders/prefab_builder/mod.rs b/src/map_builders/prefab_builder/mod.rs index 9cc63c6..c97b1f1 100644 --- a/src/map_builders/prefab_builder/mod.rs +++ b/src/map_builders/prefab_builder/mod.rs @@ -69,6 +69,7 @@ impl PrefabBuilder { } fn char_to_map(&mut self, ch: char, idx: usize, rng: &mut RandomNumberGenerator, build_data: &mut BuilderMap) { + let difficulty = (build_data.map.difficulty + build_data.initial_player_level) / 2; match ch { ' ' => build_data.map.tiles[idx] = TileType::Floor, '#' => build_data.map.tiles[idx] = TileType::Wall, @@ -102,23 +103,23 @@ impl PrefabBuilder { } '%' => { build_data.map.tiles[idx] = TileType::Floor; - build_data.spawn_list.push((idx, spawner::food_table(build_data.map.difficulty).roll(rng))); + build_data.spawn_list.push((idx, spawner::food_table(difficulty).roll(rng))); } '!' => { build_data.map.tiles[idx] = TileType::Floor; - build_data.spawn_list.push((idx, spawner::potion_table(build_data.map.difficulty).roll(rng))); + build_data.spawn_list.push((idx, spawner::potion_table(difficulty).roll(rng))); } '/' => { build_data.map.tiles[idx] = TileType::Floor; - build_data.spawn_list.push((idx, spawner::wand_table(build_data.map.difficulty).roll(rng))); + build_data.spawn_list.push((idx, spawner::wand_table(difficulty).roll(rng))); } '?' => { build_data.map.tiles[idx] = TileType::Floor; - build_data.spawn_list.push((idx, spawner::scroll_table(build_data.map.difficulty).roll(rng))); + build_data.spawn_list.push((idx, spawner::scroll_table(difficulty).roll(rng))); } ')' => { build_data.map.tiles[idx] = TileType::Floor; - build_data.spawn_list.push((idx, spawner::equipment_table(build_data.map.difficulty).roll(rng))); + build_data.spawn_list.push((idx, spawner::equipment_table(difficulty).roll(rng))); } _ => { rltk::console::log(format!("Unknown glyph '{}' when loading prefab", (ch as u8) as char)); diff --git a/src/map_builders/room_based_spawner.rs b/src/map_builders/room_based_spawner.rs index 8abe8ab..6d8f981 100644 --- a/src/map_builders/room_based_spawner.rs +++ b/src/map_builders/room_based_spawner.rs @@ -18,7 +18,13 @@ impl RoomBasedSpawner { fn build(&mut self, rng: &mut RandomNumberGenerator, build_data: &mut BuilderMap) { if let Some(rooms) = &build_data.rooms { for room in rooms.iter().skip(1) { - spawner::spawn_room(&build_data.map, rng, room, &mut build_data.spawn_list); + spawner::spawn_room( + &build_data.map, + rng, + room, + &mut build_data.spawn_list, + build_data.initial_player_level, + ); } } else { panic!("RoomBasedSpawner only works after rooms have been created"); diff --git a/src/map_builders/rooms_corridors_spawner.rs b/src/map_builders/rooms_corridors_spawner.rs index 61044b2..2a7d9b6 100644 --- a/src/map_builders/rooms_corridors_spawner.rs +++ b/src/map_builders/rooms_corridors_spawner.rs @@ -18,7 +18,13 @@ impl CorridorSpawner { fn build(&mut self, rng: &mut RandomNumberGenerator, build_data: &mut BuilderMap) { if let Some(corridors) = &build_data.corridors { for corridor in corridors.iter() { - spawner::spawn_region(&build_data.map, rng, &corridor, &mut build_data.spawn_list); + spawner::spawn_region( + &build_data.map, + rng, + &corridor, + &mut build_data.spawn_list, + build_data.initial_player_level, + ); } } else { panic!("CorridorSpawner only works after corridors have been created"); diff --git a/src/map_builders/town.rs b/src/map_builders/town.rs index f065fd0..d6af07c 100644 --- a/src/map_builders/town.rs +++ b/src/map_builders/town.rs @@ -1,10 +1,16 @@ use super::{BuilderChain, BuilderMap, InitialMapBuilder, Position, TileType}; use std::collections::HashSet; -pub fn town_builder(new_id: i32, _rng: &mut rltk::RandomNumberGenerator, width: i32, height: i32) -> BuilderChain { - let difficulty = 0; +pub fn town_builder( + new_id: i32, + _rng: &mut rltk::RandomNumberGenerator, + width: i32, + height: i32, + difficulty: i32, + initial_player_level: i32, +) -> BuilderChain { rltk::console::log(format!("DEBUGINFO: Building town (ID:{}, DIFF:{})", new_id, difficulty)); - let mut chain = BuilderChain::new(new_id, width, height, difficulty, ""); + let mut chain = BuilderChain::new(new_id, width, height, difficulty, "", initial_player_level); chain.start_with(TownBuilder::new()); return chain; diff --git a/src/map_builders/voronoi_spawning.rs b/src/map_builders/voronoi_spawning.rs index 4d80535..81e15b4 100644 --- a/src/map_builders/voronoi_spawning.rs +++ b/src/map_builders/voronoi_spawning.rs @@ -42,7 +42,13 @@ impl VoronoiSpawning { // Spawn the entities for area in noise_areas.iter() { - spawner::spawn_region(&build_data.map, rng, area.1, &mut build_data.spawn_list); + spawner::spawn_region( + &build_data.map, + rng, + area.1, + &mut build_data.spawn_list, + build_data.initial_player_level, + ); } } } diff --git a/src/raws/mob_structs.rs b/src/raws/mob_structs.rs index 68f0ab3..3f0f03e 100644 --- a/src/raws/mob_structs.rs +++ b/src/raws/mob_structs.rs @@ -15,9 +15,9 @@ pub struct Mob { pub attributes: Option, pub skills: Option>, pub vision_range: i32, - pub quips: Option>, pub equipped: Option>, pub loot: Option, + pub quips: Option>, } #[derive(Deserialize, Debug)] diff --git a/src/raws/rawmaster.rs b/src/raws/rawmaster.rs index 5ea6be9..2e69c91 100644 --- a/src/raws/rawmaster.rs +++ b/src/raws/rawmaster.rs @@ -469,7 +469,7 @@ pub fn table_by_name(raws: &RawMaster, key: &str, difficulty: i32) -> RandomTabl use super::SpawnTableEntry; let upper_bound = difficulty; - let lower_bound = difficulty / 6; + let lower_bound = if key != "mobs" { 0 } else { difficulty / 6 }; let available_options: Vec<&SpawnTableEntry> = spawn_table .table @@ -559,3 +559,26 @@ pub fn roll_on_loot_table(raws: &RawMaster, rng: &mut RandomNumberGenerator, key console::log(format!("DEBUGINFO: Unknown loot table {}", key)); return None; } + +#[derive(PartialEq, Copy, Clone)] +pub enum SpawnsAs { + Single, + SmallGroup, + LargeGroup, +} + +pub fn check_if_mob_spawns_in_group(raws: &RawMaster, key: &str) -> SpawnsAs { + if raws.mob_index.contains_key(key) { + let mob_template = &raws.raws.mobs[raws.mob_index[key]]; + if let Some(flags) = &mob_template.flags { + for flag in flags { + match flag.as_str() { + "SMALL_GROUP" => return SpawnsAs::SmallGroup, + "LARGE_GROUP" => return SpawnsAs::LargeGroup, + _ => {} + } + } + } + } + return SpawnsAs::Single; +} diff --git a/src/spawner.rs b/src/spawner.rs index 77efe3c..033bad8 100644 --- a/src/spawner.rs +++ b/src/spawner.rs @@ -78,7 +78,13 @@ pub fn player(ecs: &mut World, player_x: i32, player_y: i32) -> Entity { } /// Fills a room with stuff! -pub fn spawn_room(map: &Map, rng: &mut RandomNumberGenerator, room: &Rect, spawn_list: &mut Vec<(usize, String)>) { +pub fn spawn_room( + map: &Map, + rng: &mut RandomNumberGenerator, + room: &Rect, + spawn_list: &mut Vec<(usize, String)>, + player_level: i32, +) { let mut possible_targets: Vec = Vec::new(); { // Borrow scope - to keep access to the map separated @@ -92,24 +98,26 @@ pub fn spawn_room(map: &Map, rng: &mut RandomNumberGenerator, room: &Rect, spawn } } - spawn_region(map, rng, &possible_targets, spawn_list); + spawn_region(map, rng, &possible_targets, spawn_list, player_level); } -pub fn spawn_region(map: &Map, rng: &mut RandomNumberGenerator, area: &[usize], spawn_list: &mut Vec<(usize, String)>) { +pub fn spawn_region( + map: &Map, + rng: &mut RandomNumberGenerator, + area: &[usize], + spawn_list: &mut Vec<(usize, String)>, + player_level: i32, +) { let mut spawn_points: HashMap = HashMap::new(); let mut areas: Vec = Vec::from(area); - let difficulty = map.difficulty; + let difficulty = (map.difficulty + player_level) / 2; // If no area, log and return. if areas.len() == 0 { rltk::console::log("DEBUGINFO: No areas capable of spawning mobs!"); return; } // Get num of each entity type. - let num_mobs = match rng.roll_dice(1, 20) { - 1..=4 => 1, // 20% chance of spawning 1 mob. - 5 => 3, // 5% chance of spawning 3 mobs. - _ => 0, // 75% chance of spawning 0 - }; + let spawn_mob: bool = rng.roll_dice(1, 3) == 1; let num_items = match rng.roll_dice(1, 20) { 1..=2 => 1, // 10% chance of spawning 1 item 3 => 2, // 5% chance of spawning 2 items @@ -122,15 +130,45 @@ pub fn spawn_region(map: &Map, rng: &mut RandomNumberGenerator, area: &[usize], _ => 0, // 85% chance of spawning nothing }; // Roll on each table, getting an entity + spawn point - for _i in 0..num_mobs { - entity_from_table_to_spawn_list(rng, &mut areas, mob_table(difficulty), &mut spawn_points); + if spawn_mob { + let key = mob_table(difficulty).roll(rng); + let spawn_type = raws::check_if_mob_spawns_in_group(&raws::RAWS.lock().unwrap(), &key); + let n = match spawn_type { + raws::SpawnsAs::Single => 1, + raws::SpawnsAs::SmallGroup => { + if rng.roll_dice(1, 2) == 1 { + 1 + } else { + 4 + } + } + raws::SpawnsAs::LargeGroup => { + if rng.roll_dice(1, 2) == 1 { + 4 + } else { + 11 + } + } + }; + let mut roll = if n == 1 { 1 } else { rng.roll_dice(2, n) }; + roll = match player_level { + 0..=2 => i32::min(1, roll / 4), + 3..=4 => i32::min(1, roll / 2), + _ => roll, + }; + for _i in 0..roll { + entity_to_spawn_list(rng, &mut areas, key.clone(), &mut spawn_points); + } } for _i in 0..num_traps { - entity_from_table_to_spawn_list(rng, &mut areas, trap_table(difficulty), &mut spawn_points); + let key = trap_table(difficulty).roll(rng); + entity_to_spawn_list(rng, &mut areas, key, &mut spawn_points); } for _i in 0..num_items { - let spawn_table = get_random_item_category(rng, difficulty); - entity_from_table_to_spawn_list(rng, &mut areas, spawn_table, &mut spawn_points); + // Player level isn't taken into account for item spawning, to encourage + // delving deeper to gear up more quickly. + let key = get_random_item_category(rng, map.difficulty).roll(rng); + entity_to_spawn_list(rng, &mut areas, key, &mut spawn_points); } // Push entities and their spawn points to map's spawn list for spawn in spawn_points.iter() { @@ -138,10 +176,10 @@ pub fn spawn_region(map: &Map, rng: &mut RandomNumberGenerator, area: &[usize], } } -fn entity_from_table_to_spawn_list( +fn entity_to_spawn_list( rng: &mut RandomNumberGenerator, possible_areas: &mut Vec, - table: RandomTable, + key: String, spawn_points: &mut HashMap, ) { if possible_areas.len() == 0 { @@ -150,7 +188,7 @@ fn entity_from_table_to_spawn_list( let array_idx = if possible_areas.len() == 1 { 0usize } else { (rng.roll_dice(1, possible_areas.len() as i32) - 1) as usize }; let map_idx = possible_areas[array_idx]; - spawn_points.insert(map_idx, table.roll(rng)); + spawn_points.insert(map_idx, key); possible_areas.remove(array_idx); } diff --git a/src/trigger_system.rs b/src/trigger_system.rs index 231c097..160c4ee 100644 --- a/src/trigger_system.rs +++ b/src/trigger_system.rs @@ -21,6 +21,7 @@ impl<'a> System<'a> for TriggerSystem { ReadStorage<'a, Name>, WriteExpect<'a, ParticleBuilder>, Entities<'a>, + WriteExpect<'a, rltk::RandomNumberGenerator>, ); fn run(&mut self, data: Self::SystemData) { @@ -37,6 +38,7 @@ impl<'a> System<'a> for TriggerSystem { names, mut particle_builder, entities, + mut rng, ) = data; // Iterate entities that moved, and their final position @@ -60,7 +62,12 @@ impl<'a> System<'a> for TriggerSystem { let damage = inflicts_damage.get(*entity_id); if let Some(damage) = damage { particle_builder.damage_taken(pos.x, pos.y); - SufferDamage::new_damage(&mut inflict_damage, entity, damage.amount, false); + SufferDamage::new_damage( + &mut inflict_damage, + entity, + rng.roll_dice(1, damage.amount), + false, + ); } let confuses = confusion.get(*entity_id);