From 475f96d4e601139b504cb019d241cba5f9890a86 Mon Sep 17 00:00:00 2001 From: Llywelwyn Date: Sun, 30 Jul 2023 14:16:57 +0100 Subject: [PATCH] cleans up spawning, rolling for items/mobs/traps separately --- raws/mobs.json | 21 ++++++--- src/components.rs | 6 +++ src/damage_system.rs | 36 ++++++++++++--- src/main.rs | 1 + src/raws/mob_structs.rs | 7 +++ src/raws/rawmaster.rs | 70 +++++++++++++++++++++------- src/saveload_system.rs | 2 + src/spawner.rs | 100 ++++++++++++++++++++++------------------ 8 files changed, 169 insertions(+), 74 deletions(-) diff --git a/raws/mobs.json b/raws/mobs.json index 8f4e33d..9a23720 100644 --- a/raws/mobs.json +++ b/raws/mobs.json @@ -86,7 +86,8 @@ "bac": 6, "vision_range": 8, "attacks": [{ "name": "bites", "hit_bonus": 0, "damage": "1d2" }], - "equipped": ["equip_shortsword", "equip_body_leather", "equip_head_leather"] + "equipped": ["equip_shortsword", "equip_body_leather"], + "loot": { "table": "scrolls", "chance": 0.05 } }, { "id": "chicken", @@ -171,7 +172,8 @@ "level": 1, "bac": 7, "vision_range": 8, - "attacks": [{ "name": "bites", "hit_bonus": 0, "damage": "1d3" }] + "attacks": [{ "name": "bites", "hit_bonus": 0, "damage": "1d3" }], + "loot": { "table": "scrolls", "chance": 0.05 } }, { "id": "dog", @@ -209,7 +211,8 @@ "flags": ["MONSTER", "BLOCKS_TILE"], "level": 1, "vision_range": 7, - "attacks": [{ "name": "hits", "hit_bonus": 0, "damage": "1d4" }] + "attacks": [{ "name": "hits", "hit_bonus": 0, "damage": "1d4" }], + "loot": { "table": "food", "chance": 0.05 } }, { "id": "jackal", @@ -256,7 +259,8 @@ "flags": ["MONSTER", "BLOCKS_TILE"], "level": 2, "vision_range": 12, - "attacks": [{ "name": "hits", "hit_bonus": 0, "damage": "1d8" }] + "attacks": [{ "name": "hits", "hit_bonus": 0, "damage": "1d8" }], + "loot": { "table": "wands", "chance": 0.05 } }, { "id": "orc", @@ -264,7 +268,8 @@ "renderable": { "glyph": "o", "fg": "#00FF00", "bg": "#000000", "order": 1 }, "flags": ["MONSTER", "BLOCKS_TILE"], "vision_range": 12, - "attacks": [{ "name": "hits", "hit_bonus": 0, "damage": "1d6" }] + "attacks": [{ "name": "hits", "hit_bonus": 0, "damage": "1d6" }], + "loot": { "table": "equipment", "chance": 0.05 } }, { "id": "orc_large", @@ -273,7 +278,8 @@ "flags": ["MONSTER", "BLOCKS_TILE"], "level": 2, "vision_range": 12, - "attacks": [{ "name": "hits", "hit_bonus": 0, "damage": "1d6" }] + "attacks": [{ "name": "hits", "hit_bonus": 0, "damage": "1d6" }], + "loot": { "table": "equipment", "chance": 0.05 } }, { "id": "ogre", @@ -282,6 +288,7 @@ "flags": ["MONSTER", "BLOCKS_TILE"], "level": 5, "bac": 5, - "vision_range": 8 + "vision_range": 8, + "loot": { "table": "food", "chance": 0.05 } } ] diff --git a/src/components.rs b/src/components.rs index 2e6a503..00d17c0 100644 --- a/src/components.rs +++ b/src/components.rs @@ -40,6 +40,12 @@ pub struct Prop {} #[derive(Component, Debug, Serialize, Deserialize, Clone)] pub struct Monster {} +#[derive(Component, Debug, Serialize, Deserialize, Clone)] +pub struct LootTable { + pub table: String, + pub chance: f32, +} + #[derive(Component, Debug, Serialize, Deserialize, Clone)] pub struct Bystander {} diff --git a/src/damage_system.rs b/src/damage_system.rs index ac303ba..e81c556 100644 --- a/src/damage_system.rs +++ b/src/damage_system.rs @@ -1,6 +1,6 @@ use super::{ - gamelog, Attributes, Equipped, GrantsXP, InBackpack, Item, Map, Name, ParticleBuilder, Player, Pools, Position, - RunState, SufferDamage, + gamelog, Attributes, Equipped, GrantsXP, InBackpack, Item, LootTable, Map, Name, ParticleBuilder, Player, Pools, + Position, RunState, SufferDamage, }; use crate::gamesystem::{mana_per_level, player_hp_per_level}; use rltk::prelude::*; @@ -155,7 +155,16 @@ pub fn delete_the_dead(ecs: &mut World) { } } } - let items_to_delete = drop_some_held_items_and_return_the_rest(ecs, &dead); + let (items_to_delete, loot_to_spawn) = handle_dead_entity_items(ecs, &dead); + for loot in loot_to_spawn { + crate::raws::spawn_named_entity( + &crate::raws::RAWS.lock().unwrap(), + ecs, + &loot.0, + crate::raws::SpawnType::AtPosition { x: loot.1.x, y: loot.1.y }, + 0, + ); + } for item in items_to_delete { ecs.delete_entity(item).expect("Unable to delete item."); } @@ -166,19 +175,21 @@ pub fn delete_the_dead(ecs: &mut World) { } } -fn drop_some_held_items_and_return_the_rest(ecs: &mut World, dead: &Vec) -> Vec { +fn handle_dead_entity_items(ecs: &mut World, dead: &Vec) -> (Vec, Vec<(String, Position)>) { let mut to_drop: Vec<(Entity, Position)> = Vec::new(); + let mut to_spawn: Vec<(String, Position)> = Vec::new(); let entities = ecs.entities(); let mut equipped = ecs.write_storage::(); let mut carried = ecs.write_storage::(); let mut positions = ecs.write_storage::(); + let loot_tables = ecs.read_storage::(); let mut rng = ecs.write_resource::(); // Make list of every item in every dead thing's inv/equip for victim in dead.iter() { + let pos = positions.get(*victim); for (entity, equipped) in (&entities, &equipped).join() { if equipped.owner == *victim { // Push equipped item entities and positions - let pos = positions.get(*victim); if let Some(pos) = pos { to_drop.push((entity, pos.clone())); } @@ -187,12 +198,23 @@ fn drop_some_held_items_and_return_the_rest(ecs: &mut World, dead: &Vec) for (entity, backpack) in (&entities, &carried).join() { if backpack.owner == *victim { // Push backpack item entities and positions - let pos = positions.get(*victim); if let Some(pos) = pos { to_drop.push((entity, pos.clone())); } } } + if let Some(table) = loot_tables.get(*victim) { + let roll: f32 = rng.rand(); + if roll < table.chance { + let potential_drop = + crate::raws::roll_on_loot_table(&crate::raws::RAWS.lock().unwrap(), &mut rng, &table.table); + if let Some(id) = potential_drop { + if let Some(pos) = pos { + to_spawn.push((id, pos.clone())); + } + } + } + } } const DROP_ONE_IN_THIS_MANY_TIMES: i32 = 6; let mut to_return: Vec = Vec::new(); @@ -205,5 +227,5 @@ fn drop_some_held_items_and_return_the_rest(ecs: &mut World, dead: &Vec) to_return.push(drop.0); } } - return to_return; + return (to_return, to_spawn); } diff --git a/src/main.rs b/src/main.rs index 88f9092..a2e90d4 100644 --- a/src/main.rs +++ b/src/main.rs @@ -532,6 +532,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/raws/mob_structs.rs b/src/raws/mob_structs.rs index 93cad89..656e69c 100644 --- a/src/raws/mob_structs.rs +++ b/src/raws/mob_structs.rs @@ -16,6 +16,7 @@ pub struct Mob { pub vision_range: i32, pub quips: Option>, pub equipped: Option>, + pub loot: Option, } #[derive(Deserialize, Debug)] @@ -34,3 +35,9 @@ pub struct NaturalAttack { pub hit_bonus: i32, pub damage: String, } + +#[derive(Deserialize, Debug)] +pub struct LootTableInfo { + pub table: String, + pub chance: f32, +} diff --git a/src/raws/rawmaster.rs b/src/raws/rawmaster.rs index 2d8c393..6892012 100644 --- a/src/raws/rawmaster.rs +++ b/src/raws/rawmaster.rs @@ -3,6 +3,7 @@ use crate::components::*; use crate::gamesystem::*; use crate::random_table::RandomTable; use regex::Regex; +use rltk::prelude::*; use specs::prelude::*; use specs::saveload::{MarkedBuilder, SimpleMarker}; use std::collections::{HashMap, HashSet}; @@ -46,39 +47,48 @@ impl RawMaster { self.raws = raws; let mut used_names: HashSet = HashSet::new(); for (i, item) in self.raws.items.iter().enumerate() { - if used_names.contains(&item.id) { - rltk::console::log(format!("DEBUGINFO: Duplicate Item ID found in raws [{}]", item.id)); - } + check_for_duplicate_entries(&used_names, &item.id); self.item_index.insert(item.id.clone(), i); used_names.insert(item.id.clone()); } for (i, mob) in self.raws.mobs.iter().enumerate() { - if used_names.contains(&mob.id) { - rltk::console::log(format!("DEBUGINFO: Duplicate Mob ID found in raws [{}]", mob.id)); - } + check_for_duplicate_entries(&used_names, &mob.id); self.mob_index.insert(mob.id.clone(), i); used_names.insert(mob.id.clone()); } for (i, prop) in self.raws.props.iter().enumerate() { - if used_names.contains(&prop.id) { - rltk::console::log(format!("DEBUGINFO: Duplicate Prop ID found in raws [{}]", prop.id)); - } + check_for_duplicate_entries(&used_names, &prop.id); self.prop_index.insert(prop.id.clone(), i); used_names.insert(prop.id.clone()); } for (i, table) in self.raws.spawn_tables.iter().enumerate() { - if used_names.contains(&table.id) { - rltk::console::log(format!("DEBUGINFO: Duplicate SpawnTable ID found in raws [{}]", table.id)); - } + check_for_duplicate_entries(&used_names, &table.id); self.table_index.insert(table.id.clone(), i); used_names.insert(table.id.clone()); - for entry in table.table.iter() { - if !used_names.contains(&entry.id) { - rltk::console::log(format!("DEBUGINFO: SpawnTables references unspecified entity [{}]", entry.id)); - } + check_for_unspecified_entity(&used_names, &entry.id) } } + for (i, loot_table) in self.raws.loot_tables.iter().enumerate() { + check_for_duplicate_entries(&used_names, &loot_table.id); + self.loot_index.insert(loot_table.id.clone(), i); + for entry in loot_table.table.iter() { + check_for_unspecified_entity(&used_names, &entry.id) + } + } + } +} + +/// Checks a string against a HashSet, logging if a duplicate is found. +fn check_for_duplicate_entries(used_names: &HashSet, id: &String) { + if used_names.contains(id) { + rltk::console::log(format!("DEBUGINFO: Duplicate ID found in raws [{}]", id)); + } +} +/// Checks a string against a HashSet, logging if the string isn't found. +fn check_for_unspecified_entity(used_names: &HashSet, id: &String) { + if !used_names.contains(id) { + rltk::console::log(format!("DEBUGINFO: Table references unspecified entity [{}]", id)); } } @@ -179,6 +189,7 @@ pub fn spawn_named_item(raws: &RawMaster, ecs: &mut World, key: &str, pos: Spawn return Some(eb.build()); } + console::log(format!("DEBUGINFO: Tried to spawn named item [{}] but failed", key)); None } @@ -354,6 +365,11 @@ pub fn spawn_named_mob( eb = eb.with(GrantsXP { amount: xp_value }); + // Setup loot table + if let Some(loot) = &mob_template.loot { + eb = eb.with(LootTable { table: loot.table.clone(), chance: loot.chance }); + } + if SPAWN_LOGGING { rltk::console::log(format!( "SPAWNLOG: {} ({}HP, {}MANA, {}BAC) spawned at level {} ({}[base], {}[map difficulty], {}[player level]), worth {} XP", @@ -518,3 +534,25 @@ fn find_slot_for_equippable_item(tag: &str, raws: &RawMaster) -> EquipmentSlot { } panic!("Trying to equip {}, but it has no slot tag.", tag); } + +pub fn roll_on_loot_table(raws: &RawMaster, rng: &mut RandomNumberGenerator, key: &str) -> Option { + if raws.loot_index.contains_key(key) { + console::log(format!("DEBUGINFO: Rolling on loot table: {}", key)); + let mut rt = RandomTable::new(); + let available_options = &raws.raws.loot_tables[raws.loot_index[key]]; + for item in available_options.table.iter() { + rt = rt.add(item.id.clone(), item.weight); + } + return Some(rt.roll(rng)); + } else if raws.table_index.contains_key(key) { + console::log(format!("DEBUGINFO: No loot table found, so using spawn table: {}", key)); + let mut rt = RandomTable::new(); + let available_options = &raws.raws.spawn_tables[raws.table_index[key]]; + for item in available_options.table.iter() { + rt = rt.add(item.id.clone(), item.weight); + } + return Some(rt.roll(rng)); + } + console::log(format!("DEBUGINFO: Unknown loot table {}", key)); + return None; +} diff --git a/src/saveload_system.rs b/src/saveload_system.rs index 1c693a6..a53e3da 100644 --- a/src/saveload_system.rs +++ b/src/saveload_system.rs @@ -69,6 +69,7 @@ pub fn save_game(ecs: &mut World) { InBackpack, InflictsDamage, Item, + LootTable, MagicMapper, MeleeWeapon, Mind, @@ -172,6 +173,7 @@ pub fn load_game(ecs: &mut World) { InBackpack, InflictsDamage, Item, + LootTable, MagicMapper, MeleeWeapon, Mind, diff --git a/src/spawner.rs b/src/spawner.rs index e213390..67dd1c9 100644 --- a/src/spawner.rs +++ b/src/spawner.rs @@ -75,9 +75,6 @@ pub fn player(ecs: &mut World, player_x: i32, player_y: i32) -> Entity { return player; } -// Consts -const MAX_ENTITIES: i32 = 2; - /// Fills a room with stuff! pub fn spawn_room(map: &Map, rng: &mut RandomNumberGenerator, room: &Rect, spawn_list: &mut Vec<(usize, String)>) { let mut possible_targets: Vec = Vec::new(); @@ -100,53 +97,61 @@ pub fn spawn_region(map: &Map, rng: &mut RandomNumberGenerator, area: &[usize], let mut spawn_points: HashMap = HashMap::new(); let mut areas: Vec = Vec::from(area); let difficulty = map.difficulty; - + // If no area, log and return. if areas.len() == 0 { rltk::console::log("DEBUGINFO: No areas capable of spawning mobs!"); return; } - - if rng.roll_dice(1, 3) == 1 { - let array_idx = if areas.len() == 1 { 0usize } else { (rng.roll_dice(1, areas.len() as i32) - 1) as usize }; - let map_idx = areas[array_idx]; - spawn_points.insert(map_idx, mob_table(difficulty).roll(rng)); - areas.remove(array_idx); + // 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 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 + 4 => 3, // 5% chance of spawning 3 items + _ => 0, // 80% chance of spawning nothing + }; + let num_traps = match rng.roll_dice(1, 20) { + 1 => 1, // 5% chance of spawning 1 trap + 2 => 2, // 5% chance of spawning 2 traps + _ => 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); } - - let num_spawns = i32::min(areas.len() as i32, rng.roll_dice(1, MAX_ENTITIES + 2) - 2); - if num_spawns <= 0 { - return; + for _i in 0..num_traps { + entity_from_table_to_spawn_list(rng, &mut areas, trap_table(difficulty), &mut spawn_points); } - - for _i in 0..num_spawns { - let category = category_table().roll(rng); - let spawn_table; - match category.as_ref() { - "item" => { - let item_category = item_category_table().roll(rng); - match item_category.as_ref() { - "equipment" => spawn_table = equipment_table(difficulty), - "potion" => spawn_table = potion_table(difficulty), - "scroll" => spawn_table = scroll_table(difficulty), - "wand" => spawn_table = wand_table(difficulty), - _ => spawn_table = debug_table(), - } - } - "food" => spawn_table = food_table(difficulty), - "trap" => spawn_table = trap_table(difficulty), - _ => spawn_table = debug_table(), - } - let array_idx = if areas.len() == 1 { 0usize } else { (rng.roll_dice(1, areas.len() as i32) - 1) as usize }; - let map_idx = areas[array_idx]; - spawn_points.insert(map_idx, spawn_table.roll(rng)); - areas.remove(array_idx); + 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); } - + // Push entities and their spawn points to map's spawn list for spawn in spawn_points.iter() { spawn_list.push((*spawn.0, spawn.1.to_string())); } } +fn entity_from_table_to_spawn_list( + rng: &mut RandomNumberGenerator, + possible_areas: &mut Vec, + table: RandomTable, + spawn_points: &mut HashMap, +) { + if possible_areas.len() == 0 { + return; + } + 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)); + possible_areas.remove(array_idx); +} + /// 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 map = ecs.fetch::(); @@ -170,20 +175,27 @@ pub fn spawn_entity(ecs: &mut World, spawn: &(&usize, &String)) { rltk::console::log(format!("WARNING: We don't know how to spawn [{}]!", spawn.1)); } -// 3 items : 1 food : 1 trap -fn category_table() -> RandomTable { - return RandomTable::new().add("item", 3).add("food", 1).add("trap", 1); -} - // 3 scrolls : 3 potions : 1 equipment : 1 wand? fn item_category_table() -> RandomTable { - return RandomTable::new().add("equipment", 1).add("potion", 3).add("scroll", 3).add("wand", 1); + return RandomTable::new().add("equipment", 20).add("food", 20).add("potion", 16).add("scroll", 16).add("wand", 4); } fn debug_table() -> RandomTable { return RandomTable::new().add("debug", 1); } +fn get_random_item_category(rng: &mut RandomNumberGenerator, difficulty: i32) -> RandomTable { + let item_category = item_category_table().roll(rng); + match item_category.as_ref() { + "equipment" => return equipment_table(difficulty), + "food" => return food_table(difficulty), + "potion" => return potion_table(difficulty), + "scroll" => return scroll_table(difficulty), + "wand" => return wand_table(difficulty), + _ => return debug_table(), + }; +} + pub fn equipment_table(difficulty: i32) -> RandomTable { raws::table_by_name(&raws::RAWS.lock().unwrap(), "equipment", difficulty) }