From c169a1eae65c715740817cb72148c1a394ab06e4 Mon Sep 17 00:00:00 2001 From: Llywelwyn Date: Fri, 28 Jul 2023 06:29:59 +0100 Subject: [PATCH] combat system overhaul - d20/hack-like --- Cargo.lock | 1 + Cargo.toml | 1 + raws/items.json | 18 +- raws/mobs.json | 167 +++++++----- raws/spawn_tables.json | 7 +- src/components.rs | 74 +++++- src/damage_system.rs | 10 +- src/gamesystem.rs | 62 +++++ src/gui.rs | 70 ++++- src/inventory_system.rs | 9 +- src/main.rs | 21 +- .../prefab_builder/prefab_vaults.rs | 10 +- src/map_builders/town.rs | 10 +- src/melee_combat_system.rs | 173 ++++++++---- src/particle_system.rs | 11 + src/player.rs | 35 +-- src/raws/mob_structs.rs | 27 +- src/raws/rawmaster.rs | 249 ++++++++++++++---- src/saveload_system.rs | 16 +- src/spawner.rs | 83 ++++-- 20 files changed, 762 insertions(+), 292 deletions(-) create mode 100644 src/gamesystem.rs diff --git a/Cargo.lock b/Cargo.lock index 856b572..35d24e7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2278,6 +2278,7 @@ dependencies = [ "bracket-lib 0.8.7 (git+https://github.com/amethyst/bracket-lib.git?rev=851f6f08675444fb6fa088b9e67bee9fd75554c6)", "criterion", "lazy_static", + "regex", "rltk", "serde", "serde_json", diff --git a/Cargo.toml b/Cargo.toml index 995b367..543c203 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,6 +8,7 @@ edition = "2021" [dependencies] rltk = { version = "^0.8.7", features = ["serde"] } bracket-lib = { git = "https://github.com/amethyst/bracket-lib.git", rev = "851f6f08675444fb6fa088b9e67bee9fd75554c6", features = ["serde"] } +regex = "1.3.6" specs = { version = "0.16.1", features = ["serde"] } specs-derive = "0.4.1" serde = { version = "1.0.93", features = ["derive"]} diff --git a/raws/items.json b/raws/items.json index 0b6b562..c1b59b1 100644 --- a/raws/items.json +++ b/raws/items.json @@ -59,43 +59,43 @@ "id": "equip_dagger", "name": { "name": "dagger", "plural": "daggers" }, "renderable": { "glyph": ")", "fg": "#808080", "bg": "#000000", "order": 2 }, - "flags": ["EQUIP_MELEE"], - "effects": { "melee_power_bonus": "1" } + "flags": ["EQUIP_MELEE", "FINESSE"], + "effects": { "base_damage": "1d4"} }, { "id": "equip_shortsword", "name": { "name": "shortsword", "plural": "shortswords" }, "renderable": { "glyph": ")", "fg": "#C0C0C0", "bg": "#000000", "order": 2 }, - "flags": ["EQUIP_MELEE"], - "effects": { "melee_power_bonus": "2" } + "flags": ["EQUIP_MELEE", "STRENGTH"], + "effects": { "base_damage": "1d6"} }, { "id": "equip_longsword", "name": { "name": "longsword", "plural": "longswords" }, "renderable": { "glyph": ")", "fg": "#FFF8DC", "bg": "#000000", "order": 2 }, - "flags": ["EQUIP_MELEE"], - "effects": { "melee_power_bonus": "3" } + "flags": ["EQUIP_MELEE", "STRENGTH"], + "effects": { "base_damage": "1d8" } }, { "id": "equip_smallshield", "name": { "name": "buckler", "plural": "bucklers" }, "renderable": { "glyph": "[", "fg": "#808080", "bg": "#000000", "order": 2 }, "flags": ["EQUIP_SHIELD"], - "effects": { "defence_bonus": "1" } + "effects": { "ac": "1" } }, { "id": "equip_mediumshield", "name": { "name": "medium shield", "plural": "medium shields" }, "renderable": { "glyph": "[", "fg": "#C0C0C0", "bg": "#000000", "order": 2 }, "flags": ["EQUIP_SHIELD"], - "effects": { "defence_bonus": "2", "melee_power_bonus": "-1" } + "effects": { "ac": "2", "melee_power_bonus": "-1" } }, { "id": "equip_largeshield", "name": { "name": "large shield", "plural": "large shields" }, "renderable": { "glyph": "[", "fg": "#FFF8DC", "bg": "#000000", "order": 2 }, "flags": ["EQUIP_SHIELD"], - "effects": { "defence_bonus": "4", "melee_power_bonus": "-2" } + "effects": { "ac": "4", "melee_power_bonus": "-2" } }, { "id": "wand_magicmissile", diff --git a/raws/mobs.json b/raws/mobs.json index 69dd27e..39c5d0a 100644 --- a/raws/mobs.json +++ b/raws/mobs.json @@ -3,204 +3,233 @@ "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 }, + "flags": ["BYSTANDER", "BLOCKS_TILE"], "vision_range": 4, - "ai": "bystander", "quips": ["Drink?"] }, { "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 }, + "flags": ["BYSTANDER", "BLOCKS_TILE"], "vision_range": 4, - "ai": "bystander", "quips": ["You won't catch me quipping."] }, { "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 }, + "flags": ["BYSTANDER", "BLOCKS_TILE"], "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 }, + "flags": ["BYSTANDER", "BLOCKS_TILE"], "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 }, + "flags": ["BYSTANDER", "BLOCKS_TILE"], "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" + "flags": ["BYSTANDER", "BLOCKS_TILE"], + "vision_range": 4 }, { "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 }, + "flags": ["BYSTANDER", "BLOCKS_TILE"], "vision_range": 4, - "ai": "bystander" + "attacks": [ + { "name": "hits", "hit_bonus": 0, "damage": "1d8"} + ] }, { "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 }, + "flags": ["BYSTANDER", "BLOCKS_TILE"], + "level": 2, "vision_range": 4, - "ai": "bystander", - "quips": ["You wont catch me down the mine.", "I'm not paid enough for that."] + "quips": ["You wont catch me down the mine.", "I'm not paid enough for that."], + "attacks": [ + { "name": "hits", "hit_bonus": 0, "damage": "1d8"} + ] }, { "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 }, + "flags": ["BYSTANDER", "BLOCKS_TILE"], + "level": 2, + "bac": 6, "vision_range": 12, - "ai": "melee" + "attacks": [ + { "name": "bites", "hit_bonus": 0, "damage": "1d6"} + ] }, { "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 }, + "flags": ["MONSTER", "BLOCKS_TILE"], + "bac": 6, "vision_range": 8, - "ai": "melee" + "attacks": [ + { "name": "bites", "hit_bonus": 0, "damage": "1d2"} + ] }, { "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 }, + "flags": ["MONSTER", "BLOCKS_TILE"], + "level": 2, + "bac": 7, "vision_range": 8, - "ai": "melee" + "attacks": [ + { "name": "bites", "hit_bonus": 0, "damage": "1d3"} + ] }, { "id": "dog", "name": "dog", "renderable": { "glyph": "d", "fg": "#EEEEEE", "bg": "#000000", "order": 1 }, - "flags": ["BLOCKS_TILE"], - "stats": { "max_hp": 8, "hp": 8, "defence": 0, "power": 2 }, + "flags": ["MONSTER", "BLOCKS_TILE"], + "level": 4, + "bac": 5, "vision_range": 12, - "ai": "melee" + "attacks": [ + { "name": "bites", "hit_bonus": 0, "damage": "1d6"} + ] }, { "id": "dog_large", "name": "large dog", "renderable": { "glyph": "d", "fg": "#DDDDDD", "bg": "#000000", "order": 1 }, - "flags": ["BLOCKS_TILE"], - "stats": { "max_hp": 12, "hp": 12, "defence": 0, "power": 3 }, + "flags": ["MONSTER", "BLOCKS_TILE"], + "level": 6, + "bac": 4, "vision_range": 12, - "ai": "melee" + "attacks": [ + { "name": "bites", "hit_bonus": 0, "damage": "2d4"} + ] }, { "id": "goblin", "name": "goblin", "renderable": { "glyph": "g", "fg": "#00FF00", "bg": "#000000", "order": 1 }, - "flags": ["BLOCKS_TILE"], - "stats": { "max_hp": 6, "hp": 6, "defence": 0, "power": 2 }, + "flags": ["MONSTER", "BLOCKS_TILE"], "vision_range": 12, - "ai": "melee" + "attacks": [ + { "name": "hits", "hit_bonus": 0, "damage": "1d4"} + ] }, { "id": "kobold", "name": "kobold", "renderable": { "glyph": "k", "fg": "#AA5500", "bg": "#000000", "order": 1 }, - "flags": ["BLOCKS_TILE"], - "stats": { "max_hp": 6, "hp": 6, "defence": 0, "power": 1 }, + "flags": ["MONSTER", "BLOCKS_TILE"], "vision_range": 7, - "ai": "melee" + "attacks": [ + { "name": "hits", "hit_bonus": 0, "damage": "1d4"} + ] }, { "id": "jackal", "name": "jackal", "renderable": { "glyph": "d", "fg": "#AA5500", "bg": "#000000", "order": 1 }, - "flags": ["BLOCKS_TILE"], - "stats": { "max_hp": 6, "hp": 6, "defence": 0, "power": 1 }, + "flags": ["MONSTER", "BLOCKS_TILE"], + "bac": 7, "vision_range": 12, - "ai": "melee" + "attacks": [ + { "name": "bites", "hit_bonus": 0, "damage": "1d2"} + ] }, { "id": "fox", "name": "fox", "renderable": { "glyph": "d", "fg": "#FF0000", "bg": "#000000", "order": 1 }, - "flags": ["BLOCKS_TILE"], - "stats": { "max_hp": 4, "hp": 4, "defence": 0, "power": 1 }, + "flags": ["MONSTER", "BLOCKS_TILE"], + "bac": 7, "vision_range": 12, - "ai": "melee" + "attacks": [ + { "name": "bites", "hit_bonus": 0, "damage": "1d3"} + ] }, { "id": "coyote", "name": "coyote", "renderable": { "glyph": "d", "fg": "#6E3215", "bg": "#000000", "order": 1 }, - "flags": ["BLOCKS_TILE"], - "stats": { "max_hp": 8, "hp": 8, "defence": 0, "power": 2 }, + "flags": ["MONSTER", "BLOCKS_TILE"], + "bac": 7, "vision_range": 12, - "ai": "melee" + "attacks": [ + { "name": "bites", "hit_bonus": 0, "damage": "1d4"} + ] + }, + { + "id": "wolf", + "name": "wolf", + "renderable": { "glyph": "d", "fg": "#5E4225", "bg": "#000000", "order": 1 }, + "flags": ["MONSTER", "BLOCKS_TILE"], + "bac": 4, + "vision_range": 12, + "attacks": [ + { "name": "bites", "hit_bonus": 0, "damage": "2d4"} + ] }, { "id": "goblin_chieftain", "name": "goblin chieftain", "renderable": { "glyph": "G", "fg": "#00FF00", "bg": "#000000", "order": 1 }, - "flags": ["BLOCKS_TILE"], - "stats": { "max_hp": 8, "hp": 8, "defence": 1, "power": 2 }, + "flags": ["MONSTER", "BLOCKS_TILE"], + "level": 2, "vision_range": 12, - "ai": "melee" + "attacks": [ + { "name": "hits", "hit_bonus": 0, "damage": "1d8"} + ] }, { "id": "orc", "name": "orc", "renderable": { "glyph": "o", "fg": "#00FF00", "bg": "#000000", "order": 1 }, - "flags": ["BLOCKS_TILE"], - "stats": { "max_hp": 8, "hp": 8, "defence": 0, "power": 3 }, + "flags": ["MONSTER", "BLOCKS_TILE"], "vision_range": 12, - "ai": "melee" + "attacks": [ + { "name": "hits", "hit_bonus": 0, "damage": "1d6"} + ] }, { "id": "orc_large", "name": "large orc", "renderable": { "glyph": "o", "fg": "#008000", "bg": "#000000", "order": 1 }, - "flags": ["BLOCKS_TILE"], - "stats": { "max_hp": 12, "hp": 12, "defence": 1, "power": 3 }, + "flags": ["MONSTER", "BLOCKS_TILE"], + "level": 2, "vision_range": 12, - "ai": "melee" + "attacks": [ + { "name": "hits", "hit_bonus": 0, "damage": "1d6"} + ] }, { "id": "ogre", "name": "ogre", "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, - "ai": "melee" + "flags": ["MONSTER", "BLOCKS_TILE"], + "level": 5, + "bac": 5, + "vision_range": 8 } ] diff --git a/raws/spawn_tables.json b/raws/spawn_tables.json index db3a83f..d28e501 100644 --- a/raws/spawn_tables.json +++ b/raws/spawn_tables.json @@ -60,10 +60,13 @@ { "id": "orc_large", "weight": 1, "difficulty": 3}, { "id": "goblin_chieftain", "weight": 1, "difficulty": 3}, - { "id": "dog", "weight": 1, "difficulty": 4}, { "id": "ogre", "weight": 1, "difficulty": 4}, - { "id": "dog_large", "weight": 1, "difficulty": 5} + { "id": "dog", "weight": 1, "difficulty": 5}, + + { "id": "wolf", "weight": 2, "difficulty": 6}, + + { "id": "dog_large", "weight": 1, "difficulty": 7} ] }, { diff --git a/src/components.rs b/src/components.rs index ca1c5ec..abcb6b9 100644 --- a/src/components.rs +++ b/src/components.rs @@ -100,12 +100,19 @@ pub struct HungerClock { #[derive(Component, Debug, Serialize, Deserialize, Clone)] pub struct ProvidesNutrition {} -#[derive(Component, Debug, ConvertSaveload, Clone)] -pub struct CombatStats { - pub max_hp: i32, - pub hp: i32, - pub defence: i32, - pub power: i32, +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct Pool { + pub max: i32, + pub current: i32, +} + +#[derive(Component, Debug, Serialize, Deserialize, Clone)] +pub struct Pools { + pub hit_points: Pool, + pub mana: Pool, + pub xp: i32, + pub bac: i32, + pub level: i32, } #[derive(Debug, Serialize, Deserialize, Clone)] @@ -115,6 +122,18 @@ pub struct Attribute { pub bonus: i32, } +#[derive(Debug, Serialize, Deserialize, Clone, Eq, PartialEq, Hash)] +pub enum Skill { + Melee, + Defence, + Magic, +} + +#[derive(Component, Debug, Serialize, Deserialize, Clone)] +pub struct Skills { + pub skills: HashMap, +} + #[derive(Component, Debug, Serialize, Deserialize, Clone)] pub struct Attributes { pub strength: Attribute, @@ -153,15 +172,46 @@ pub struct Item {} pub enum EquipmentSlot { Melee, Shield, + Head, + Neck, + Torso, + Hands, + Legs, + Feet, +} + +#[derive(PartialEq, Copy, Clone, Serialize, Deserialize)] +pub enum WeaponAttribute { + Strength, + Dexterity, + Finesse, +} + +#[derive(Component, Serialize, Deserialize, Clone)] +pub struct MeleeWeapon { + pub attribute: WeaponAttribute, + pub damage_n_dice: i32, + pub damage_die_type: i32, + pub damage_bonus: i32, + pub hit_bonus: i32, +} + +#[derive(Serialize, Deserialize, Clone)] +pub struct NaturalAttack { + pub name: String, + pub damage_n_dice: i32, + pub damage_die_type: i32, + pub damage_bonus: i32, + pub hit_bonus: i32, +} + +#[derive(Component, Serialize, Deserialize, Clone)] +pub struct NaturalAttacks { + pub attacks: Vec, } #[derive(Component, ConvertSaveload, Clone)] -pub struct MeleePowerBonus { - pub amount: i32, -} - -#[derive(Component, ConvertSaveload, Clone)] -pub struct DefenceBonus { +pub struct ArmourClassBonus { pub amount: i32, } diff --git a/src/damage_system.rs b/src/damage_system.rs index acb5313..b535a62 100644 --- a/src/damage_system.rs +++ b/src/damage_system.rs @@ -1,11 +1,11 @@ -use super::{gamelog, CombatStats, Entities, Item, Map, Name, Player, Position, RunState, SufferDamage}; +use super::{gamelog, Entities, Item, Map, Name, Player, Pools, Position, RunState, SufferDamage}; use specs::prelude::*; pub struct DamageSystem {} impl<'a> System<'a> for DamageSystem { type SystemData = ( - WriteStorage<'a, CombatStats>, + WriteStorage<'a, Pools>, WriteStorage<'a, SufferDamage>, WriteExpect<'a, Map>, ReadStorage<'a, Position>, @@ -16,7 +16,7 @@ impl<'a> System<'a> for DamageSystem { let (mut stats, mut damage, mut map, positions, entities) = data; for (entity, mut stats, damage) in (&entities, &mut stats, &damage).join() { - stats.hp -= damage.amount.iter().sum::(); + stats.hit_points.current -= damage.amount.iter().sum::(); let pos = positions.get(entity); if let Some(pos) = pos { let idx = map.xy_idx(pos.x, pos.y); @@ -32,13 +32,13 @@ pub fn delete_the_dead(ecs: &mut World) { let mut dead: Vec = Vec::new(); // Using scope to make borrow checker happy { - let combat_stats = ecs.read_storage::(); + let combat_stats = ecs.read_storage::(); let players = ecs.read_storage::(); let names = ecs.read_storage::(); let items = ecs.read_storage::(); let entities = ecs.entities(); for (entity, stats) in (&entities, &combat_stats).join() { - if stats.hp < 1 { + if stats.hit_points.current < 1 { let player = players.get(entity); match player { None => { diff --git a/src/gamesystem.rs b/src/gamesystem.rs new file mode 100644 index 0000000..81b676b --- /dev/null +++ b/src/gamesystem.rs @@ -0,0 +1,62 @@ +use super::{Skill, Skills}; + +pub fn attr_bonus(value: i32) -> i32 { + return (value - 10) / 2; +} + +pub fn player_hp_per_level(rng: &mut rltk::RandomNumberGenerator, constitution: i32) -> i32 { + return rng.roll_dice(1, 8) + attr_bonus(constitution); +} + +pub fn player_hp_at_level(rng: &mut rltk::RandomNumberGenerator, constitution: i32, level: i32) -> i32 { + let mut total = 10 + attr_bonus(constitution); + for _i in 0..level { + total += player_hp_per_level(rng, constitution); + } + return total; +} + +pub fn npc_hp(rng: &mut rltk::RandomNumberGenerator, constitution: i32, level: i32) -> i32 { + let mut total = 1; + for _i in 0..level { + total += rng.roll_dice(1, 8) + attr_bonus(constitution); + } + return total; +} + +pub fn mana_per_level(rng: &mut rltk::RandomNumberGenerator, intelligence: i32) -> i32 { + return rng.roll_dice(1, 4) + attr_bonus(intelligence); +} + +pub fn mana_at_level(rng: &mut rltk::RandomNumberGenerator, intelligence: i32, level: i32) -> i32 { + let mut total = 12; + for _i in 0..level { + total += mana_per_level(rng, intelligence); + } + return total; +} + +pub fn skill_bonus(skill: Skill, skills: &Skills) -> i32 { + if skills.skills.contains_key(&skill) { + return skills.skills[&skill]; + } else { + return -4; + } +} + +pub fn roll_4d6(rng: &mut rltk::RandomNumberGenerator) -> i32 { + let mut rolls: Vec = Vec::new(); + for _i in 0..4 { + rolls.push(rng.roll_dice(1, 6)); + } + rolls.sort_unstable(); + + let mut roll = 0; + rltk::console::log(format!("roll 0")); + for i in 1..rolls.len() { + roll += rolls[i]; + rltk::console::log(format!("+ {}", rolls[i])); + } + + return roll; +} diff --git a/src/gui.rs b/src/gui.rs index f87563c..38fe52e 100644 --- a/src/gui.rs +++ b/src/gui.rs @@ -1,6 +1,6 @@ use super::{ - camera, gamelog, rex_assets::RexAssets, CombatStats, Equipped, Hidden, HungerClock, HungerState, InBackpack, Map, - Name, Player, Point, Position, RunState, State, Viewshed, + camera, gamelog, gamesystem, rex_assets::RexAssets, ArmourClassBonus, Attributes, Equipped, Hidden, HungerClock, + HungerState, InBackpack, Map, Name, Player, Point, Pools, Position, RunState, Skill, Skills, State, Viewshed, }; use rltk::{Rltk, VirtualKeyCode, RGB}; use specs::prelude::*; @@ -40,13 +40,69 @@ pub fn draw_ui(ecs: &World, ctx: &mut Rltk) { ctx.draw_hollow_box(71, 0, 28, 55, RGB::named(rltk::WHITE), RGB::named(rltk::BLACK)); // Side box // Render stats - let combat_stats = ecs.read_storage::(); + let pools = ecs.read_storage::(); + let attributes = ecs.read_storage::(); let players = ecs.read_storage::(); let hunger = ecs.read_storage::(); - for (_player, stats, hunger) in (&players, &combat_stats, &hunger).join() { - draw_lerping_bar(ctx, 2, 53, 26, stats.hp, stats.max_hp, RGB::from_u8(0, 255, 0), RGB::from_u8(255, 0, 0)); - //ctx.draw_bar_horizontal(2, 53, 26, stats.hp, stats.max_hp, RGB::named(rltk::GREEN), RGB::named(rltk::BLACK)); - draw_lerping_bar(ctx, 2, 54, 26, stats.hp, stats.max_hp, RGB::named(rltk::BLUE), RGB::named(rltk::BLACK)); + let skills = ecs.read_storage::(); + for (_player, stats, attributes, hunger, skills) in (&players, &pools, &attributes, &hunger, &skills).join() { + // Draw hp/mana bars + draw_lerping_bar( + ctx, + 2, + 53, + 26, + stats.hit_points.current, + stats.hit_points.max, + RGB::from_u8(0, 255, 0), + RGB::from_u8(255, 0, 0), + ); + draw_lerping_bar( + ctx, + 2, + 54, + 26, + stats.mana.current, + stats.mana.max, + RGB::named(rltk::BLUE), + RGB::named(rltk::BLACK), + ); + // Draw AC + let skill_ac_bonus = gamesystem::skill_bonus(Skill::Defence, &*skills); + let mut armour_ac_bonus = 0; + let equipped = ecs.read_storage::(); + let ac = ecs.read_storage::(); + let player_entity = ecs.fetch::(); + for (wielded, ac) in (&equipped, &ac).join() { + if wielded.owner == *player_entity { + armour_ac_bonus += ac.amount; + } + } + let armour_class = stats.bac - attributes.dexterity.bonus - skill_ac_bonus - armour_ac_bonus; + ctx.print_color(30, 53, RGB::named(rltk::PINK), RGB::named(rltk::BLACK), "AC"); + ctx.print_color(32, 53, RGB::named(rltk::WHITE), RGB::named(rltk::BLACK), armour_class); + // Draw level + ctx.print_color( + 30, + 54, + RGB::named(rltk::WHITE), + RGB::named(rltk::BLACK), + format!("XP{}/{}", stats.level, stats.xp), + ); + // Draw attributes + ctx.print_color(36, 53, RGB::named(rltk::RED), RGB::named(rltk::BLACK), "STR"); + ctx.print_color(39, 53, RGB::named(rltk::WHITE), RGB::named(rltk::BLACK), attributes.strength.base); + ctx.print_color(43, 53, RGB::named(rltk::GREEN), RGB::named(rltk::BLACK), "DEX"); + ctx.print_color(46, 53, RGB::named(rltk::WHITE), RGB::named(rltk::BLACK), attributes.dexterity.base); + ctx.print_color(50, 53, RGB::named(rltk::ORANGE), RGB::named(rltk::BLACK), "CON"); + ctx.print_color(53, 53, RGB::named(rltk::WHITE), RGB::named(rltk::BLACK), attributes.constitution.base); + ctx.print_color(36, 54, RGB::named(rltk::CYAN), RGB::named(rltk::BLACK), "INT"); + ctx.print_color(39, 54, RGB::named(rltk::WHITE), RGB::named(rltk::BLACK), attributes.intelligence.base); + ctx.print_color(43, 54, RGB::named(rltk::YELLOW), RGB::named(rltk::BLACK), "WIS"); + ctx.print_color(46, 54, RGB::named(rltk::WHITE), RGB::named(rltk::BLACK), attributes.wisdom.base); + ctx.print_color(50, 54, RGB::named(rltk::PURPLE), RGB::named(rltk::BLACK), "CHA"); + ctx.print_color(53, 54, RGB::named(rltk::WHITE), RGB::named(rltk::BLACK), attributes.charisma.base); + // Draw hunger match hunger.state { HungerState::Satiated => { ctx.print_color_right(70, 53, RGB::named(rltk::GREEN), RGB::named(rltk::BLACK), "Satiated") diff --git a/src/inventory_system.rs b/src/inventory_system.rs index 5daf1df..3ab9f43 100644 --- a/src/inventory_system.rs +++ b/src/inventory_system.rs @@ -1,6 +1,6 @@ use super::{ - gamelog, CombatStats, Confusion, Consumable, Cursed, Destructible, Digger, Equippable, Equipped, HungerClock, - HungerState, InBackpack, InflictsDamage, MagicMapper, Map, Name, ParticleBuilder, Point, Position, ProvidesHealing, + gamelog, Confusion, Consumable, Cursed, Destructible, Digger, Equippable, Equipped, HungerClock, HungerState, + InBackpack, InflictsDamage, MagicMapper, Map, Name, ParticleBuilder, Point, Pools, Position, ProvidesHealing, ProvidesNutrition, RandomNumberGenerator, RunState, SufferDamage, TileType, Viewshed, Wand, WantsToDropItem, WantsToPickupItem, WantsToRemoveItem, WantsToUseItem, AOE, DEFAULT_PARTICLE_LIFETIME, LONG_PARTICLE_LIFETIME, }; @@ -60,7 +60,7 @@ impl<'a> System<'a> for ItemUseSystem { ReadStorage<'a, ProvidesHealing>, ReadStorage<'a, ProvidesNutrition>, WriteStorage<'a, HungerClock>, - WriteStorage<'a, CombatStats>, + WriteStorage<'a, Pools>, WriteStorage<'a, SufferDamage>, WriteExpect<'a, ParticleBuilder>, ReadStorage<'a, Position>, @@ -268,7 +268,8 @@ impl<'a> System<'a> for ItemUseSystem { for target in targets.iter() { let stats = combat_stats.get_mut(*target); if let Some(stats) = stats { - stats.hp = i32::min(stats.max_hp, stats.hp + heal.amount); + stats.hit_points.current = + i32::min(stats.hit_points.max, stats.hit_points.current + heal.amount); if entity == *player_entity { gamelog::Logger::new().append("Quaffing, you recover some vigour.").log(); } diff --git a/src/main.rs b/src/main.rs index fe2b458..d647978 100644 --- a/src/main.rs +++ b/src/main.rs @@ -35,6 +35,7 @@ mod inventory_system; use inventory_system::*; mod particle_system; use particle_system::{ParticleBuilder, DEFAULT_PARTICLE_LIFETIME, LONG_PARTICLE_LIFETIME}; +mod gamesystem; mod random_table; mod rex_assets; @@ -212,10 +213,10 @@ impl State { .append("recover some of your strength") .period() .log(); - let mut player_health_store = self.ecs.write_storage::(); - let player_health = player_health_store.get_mut(*player_entity); - if let Some(player_health) = player_health { - player_health.hp = i32::max(player_health.hp, player_health.max_hp / 2); + let mut pools = self.ecs.write_storage::(); + let stats = pools.get_mut(*player_entity); + if let Some(stats) = stats { + stats.hit_points.current = i32::max(stats.hit_points.current, stats.hit_points.max / 2); } } @@ -524,16 +525,18 @@ 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::(); 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::(); gs.ecs.register::(); @@ -562,11 +565,11 @@ fn main() -> rltk::BError { raws::load_raws(); - let player_entity = spawner::player(&mut gs.ecs, 0, 0); + gs.ecs.insert(rltk::RandomNumberGenerator::new()); gs.ecs.insert(Map::new(1, 64, 64, 0)); gs.ecs.insert(Point::new(0, 0)); + let player_entity = spawner::player(&mut gs.ecs, 0, 0); gs.ecs.insert(player_entity); - gs.ecs.insert(rltk::RandomNumberGenerator::new()); gs.ecs.insert(RunState::MapGeneration {}); gs.ecs.insert(particle_system::ParticleBuilder::new()); gs.ecs.insert(rex_assets::RexAssets::new()); diff --git a/src/map_builders/prefab_builder/prefab_vaults.rs b/src/map_builders/prefab_builder/prefab_vaults.rs index 22741de..a687d35 100644 --- a/src/map_builders/prefab_builder/prefab_vaults.rs +++ b/src/map_builders/prefab_builder/prefab_vaults.rs @@ -69,7 +69,7 @@ pub const GOBLINS_4X4: PrefabVault = PrefabVault { template: GOBLINS_4X4_V, width: 4, height: 4, first_id: 0, last_id: 100, can_flip: Flipping::Both }; const GOBLINS_4X4_V: &str = " #^   - #G# + #$# #g#  ^g^ "; @@ -78,13 +78,13 @@ pub const GOBLINS2_4X4: PrefabVault = PrefabVault { template: GOBLINS2_4X4_V, width: 4, height: 4, first_id: 0, last_id: 100, can_flip: Flipping::Both }; const GOBLINS2_4X4_V: &str = " #^#g -G# # +$# # g#  # g^ "; pub const GOBLINS_5X5: PrefabVault = - PrefabVault { template: GOBLINS_5X5_V, width: 5, height: 5, first_id: 0, last_id: 100, can_flip: Flipping::Both }; + PrefabVault { template: GOBLINS_5X5_V, width: 5, height: 5, first_id: 3, last_id: 100, can_flip: Flipping::Both }; const GOBLINS_5X5_V: &str = "  ^#g  G#?#^ @@ -94,7 +94,7 @@ G#?#^ "; pub const GOBLINS_6X6: PrefabVault = - PrefabVault { template: GOBLINS_6X6_V, width: 6, height: 6, first_id: 0, last_id: 100, can_flip: Flipping::Both }; + PrefabVault { template: GOBLINS_6X6_V, width: 6, height: 6, first_id: 5, last_id: 100, can_flip: Flipping::Both }; const GOBLINS_6X6_V: &str = "    #    #^#g  @@ -157,7 +157,7 @@ const HOUSE_TRAP_7X7_V: &str = " "; pub const ORC_HOUSE_8X8: PrefabVault = - PrefabVault { template: ORC_HOUSE_8X8_V, width: 8, height: 8, first_id: 0, last_id: 100, can_flip: Flipping::Both }; + PrefabVault { template: ORC_HOUSE_8X8_V, width: 8, height: 8, first_id: 2, last_id: 100, can_flip: Flipping::Both }; const ORC_HOUSE_8X8_V: &str = " ###### diff --git a/src/map_builders/town.rs b/src/map_builders/town.rs index ecf2325..eafcc59 100644 --- a/src/map_builders/town.rs +++ b/src/map_builders/town.rs @@ -233,14 +233,8 @@ impl TownBuilder { 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())); - } - } - } + let mut to_place: Vec<&str> = vec!["rat", "rat", "rat"]; + self.random_building_spawn(building, build_data, rng, &mut to_place, 0); } fn grass_layer(&mut self, build_data: &mut BuilderMap) { diff --git a/src/melee_combat_system.rs b/src/melee_combat_system.rs index a9c2e3b..51e9452 100644 --- a/src/melee_combat_system.rs +++ b/src/melee_combat_system.rs @@ -1,6 +1,6 @@ use super::{ - gamelog, CombatStats, DefenceBonus, Equipped, HungerClock, HungerState, MeleePowerBonus, Name, ParticleBuilder, - Position, SufferDamage, WantsToMelee, + gamelog, gamesystem, ArmourClassBonus, Attributes, EquipmentSlot, Equipped, HungerClock, HungerState, MeleeWeapon, + Name, NaturalAttacks, ParticleBuilder, Pools, Position, Skill, Skills, SufferDamage, WantsToMelee, WeaponAttribute, }; use specs::prelude::*; @@ -12,14 +12,18 @@ impl<'a> System<'a> for MeleeCombatSystem { ReadExpect<'a, Entity>, WriteStorage<'a, WantsToMelee>, ReadStorage<'a, Name>, - ReadStorage<'a, CombatStats>, + ReadStorage<'a, Attributes>, + ReadStorage<'a, Skills>, + ReadStorage<'a, Pools>, WriteStorage<'a, SufferDamage>, WriteExpect<'a, ParticleBuilder>, ReadStorage<'a, Position>, ReadStorage<'a, Equipped>, - ReadStorage<'a, DefenceBonus>, - ReadStorage<'a, MeleePowerBonus>, + ReadStorage<'a, MeleeWeapon>, + ReadStorage<'a, NaturalAttacks>, + ReadStorage<'a, ArmourClassBonus>, ReadStorage<'a, HungerClock>, + WriteExpect<'a, rltk::RandomNumberGenerator>, ); fn run(&mut self, data: Self::SystemData) { @@ -28,58 +32,146 @@ impl<'a> System<'a> for MeleeCombatSystem { player_entity, mut wants_melee, names, - combat_stats, + attributes, + skills, + pools, mut inflict_damage, mut particle_builder, positions, equipped, - defence_bonuses, - melee_power_bonuses, + melee_weapons, + natural_attacks, + ac, hunger_clock, + mut rng, ) = data; - for (entity, wants_melee, name, stats) in (&entities, &wants_melee, &names, &combat_stats).join() { - if stats.hp <= 0 { + for (entity, wants_melee, name, attacker_attributes, attacker_skills, attacker_pools) in + (&entities, &wants_melee, &names, &attributes, &skills, &pools).join() + { + let target_pools = pools.get(wants_melee.target).unwrap(); + let target_attributes = attributes.get(wants_melee.target).unwrap(); + let target_skills = skills.get(wants_melee.target).unwrap(); + + if attacker_pools.hit_points.current <= 0 { break; } - let target_stats = combat_stats.get(wants_melee.target).unwrap(); - if target_stats.hp <= 0 { + if target_pools.hit_points.current <= 0 { break; } let target_name = names.get(wants_melee.target).unwrap(); - let mut offensive_bonus = 0; - for (_item_entity, power_bonus, equipped_by) in (&entities, &melee_power_bonuses, &equipped).join() { - if equipped_by.owner == entity { - offensive_bonus += power_bonus.amount; + let mut weapon_info = MeleeWeapon { + attribute: WeaponAttribute::Strength, + hit_bonus: 0, + damage_n_dice: 1, + damage_die_type: 4, + damage_bonus: 0, + }; + let mut attack_verb = "hits"; + + if let Some(nat) = natural_attacks.get(entity) { + rltk::console::log("Natural attack found"); + if !nat.attacks.is_empty() { + let attack_index = if nat.attacks.len() == 1 { + 0 + } else { + rng.roll_dice(1, nat.attacks.len() as i32) as usize - 1 + }; + weapon_info.hit_bonus = nat.attacks[attack_index].hit_bonus; + weapon_info.damage_n_dice = nat.attacks[attack_index].damage_n_dice; + weapon_info.damage_die_type = nat.attacks[attack_index].damage_die_type; + weapon_info.damage_bonus = nat.attacks[attack_index].damage_bonus; + attack_verb = &nat.attacks[attack_index].name; } } - let mut defensive_bonus = 0; - for (_item_entity, defence_bonus, equipped_by) in (&entities, &defence_bonuses, &equipped).join() { - if equipped_by.owner == wants_melee.target { - defensive_bonus += defence_bonus.amount; + + for (wielded, melee) in (&equipped, &melee_weapons).join() { + if wielded.owner == entity && wielded.slot == EquipmentSlot::Melee { + weapon_info = melee.clone(); } } + + // Get all offensive bonuses + let d20 = rng.roll_dice(1, 20); + let attribute_hit_bonus = attacker_attributes.strength.bonus; + let skill_hit_bonus = gamesystem::skill_bonus(Skill::Melee, &*attacker_skills); + let weapon_hit_bonus = weapon_info.hit_bonus; + let mut status_hit_bonus = 0; let hc = hunger_clock.get(entity); if let Some(hc) = hc { match hc.state { HungerState::Satiated => { - offensive_bonus += 1; + status_hit_bonus += 1; } HungerState::Weak => { - offensive_bonus -= 1; + status_hit_bonus -= 1; } HungerState::Fainting => { - offensive_bonus -= 1; - defensive_bonus -= 1; + status_hit_bonus -= 2; } _ => {} } } - let damage = i32::max(0, (stats.power + offensive_bonus) - (target_stats.defence + defensive_bonus)); + let modified_hit_roll = d20 - attribute_hit_bonus - skill_hit_bonus - weapon_hit_bonus - status_hit_bonus; - if damage == 0 { + // Get armour class + let bac = target_pools.bac; + let attribute_ac_bonus = target_attributes.dexterity.bonus; + let skill_ac_bonus = gamesystem::skill_bonus(Skill::Defence, &*target_skills); + let mut armour_ac_bonus = 0; + for (wielded, ac) in (&equipped, &ac).join() { + if wielded.owner == wants_melee.target { + armour_ac_bonus += ac.amount; + } + } + let armour_class = bac - attribute_ac_bonus - skill_ac_bonus - armour_ac_bonus; + + let target_number = 10 + armour_class + attacker_pools.level; + + if d20 != 1 && (d20 == 20 || modified_hit_roll < target_number) { + // Target hit! + let base_damage = rng.roll_dice(weapon_info.damage_n_dice, weapon_info.damage_die_type); + let attribute_damage_bonus = attacker_attributes.strength.bonus; + let skill_damage_bonus = gamesystem::skill_bonus(Skill::Melee, &*attacker_skills); + let weapon_damage_bonus = weapon_info.damage_bonus; + let damage = + i32::max(0, base_damage + attribute_damage_bonus + skill_damage_bonus + weapon_damage_bonus); + + let pos = positions.get(wants_melee.target); + if let Some(pos) = pos { + particle_builder.damage_taken(pos.x, pos.y) + } + SufferDamage::new_damage(&mut inflict_damage, wants_melee.target, damage); + if entity == *player_entity { + gamelog::Logger::new() // You hit the . + .append("You hit the") + .npc_name_n(&target_name.name) + .period() + .log(); + } else if wants_melee.target == *player_entity { + gamelog::Logger::new() // hits you! + .append("The") + .npc_name(&name.name) + .append(attack_verb) + .append("you!") + .log(); + } else { + gamelog::Logger::new() // misses the . + .append("The") + .npc_name(&name.name) + .append(attack_verb) + .append("the") + .npc_name_n(&target_name.name) + .period() + .log(); + } + } else { + let pos = positions.get(wants_melee.target); + if let Some(pos) = pos { + particle_builder.attack_miss(pos.x, pos.y) + } if entity == *player_entity { gamelog::Logger::new() // You miss. .append("You miss.") @@ -101,35 +193,6 @@ impl<'a> System<'a> for MeleeCombatSystem { .period() .log(); } - } else { - if entity == *player_entity { - gamelog::Logger::new() // You hit the . - .append("You hit the") - .npc_name_n(&target_name.name) - .period() - .log(); - } else if wants_melee.target == *player_entity { - gamelog::Logger::new() // hits you! - .append("The") - .npc_name(&name.name) - .colour(rltk::WHITE) - .append("hits you!") - .log(); - } else { - gamelog::Logger::new() // misses the . - .append("The") - .npc_name(&name.name) - .colour(rltk::WHITE) - .append("hits the") - .npc_name_n(&target_name.name) - .period() - .log(); - } - let pos = positions.get(wants_melee.target); - if let Some(pos) = pos { - particle_builder.damage_taken(pos.x, pos.y) - } - SufferDamage::new_damage(&mut inflict_damage, wants_melee.target, damage); } } diff --git a/src/particle_system.rs b/src/particle_system.rs index 649a1a1..0afcbab 100644 --- a/src/particle_system.rs +++ b/src/particle_system.rs @@ -67,6 +67,17 @@ impl ParticleBuilder { ); } + pub fn attack_miss(&mut self, x: i32, y: i32) { + self.request( + x, + y, + rltk::RGB::named(rltk::CYAN), + rltk::RGB::named(rltk::BLACK), + rltk::to_cp437('‼'), + DEFAULT_PARTICLE_LIFETIME, + ); + } + pub fn trap_triggered(&mut self, x: i32, y: i32) { self.request( x, diff --git a/src/player.rs b/src/player.rs index b09a171..6762d1d 100644 --- a/src/player.rs +++ b/src/player.rs @@ -1,7 +1,7 @@ use super::{ - 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, + gamelog, Attributes, BlocksTile, BlocksVisibility, Bystander, Door, EntityMoved, Hidden, HungerClock, HungerState, + Item, Map, Monster, Name, ParticleBuilder, Player, Pools, Position, Renderable, RunState, State, SufferDamage, + Telepath, TileType, Viewshed, WantsToMelee, WantsToPickupItem, }; use rltk::{Point, RandomNumberGenerator, Rltk, VirtualKeyCode}; use specs::prelude::*; @@ -11,6 +11,7 @@ pub fn try_door(i: i32, j: i32, ecs: &mut World) -> RunState { let mut positions = ecs.write_storage::(); let mut players = ecs.write_storage::(); let mut viewsheds = ecs.write_storage::(); + let attributes = ecs.read_storage::(); let map = ecs.fetch::(); let entities = ecs.entities(); @@ -24,7 +25,7 @@ pub fn try_door(i: i32, j: i32, ecs: &mut World) -> RunState { let mut result = RunState::AwaitingInput; let mut door_pos: Option = None; - for (_entity, _player, pos) in (&entities, &mut players, &mut positions).join() { + for (_entity, _player, pos, attributes) in (&entities, &mut players, &mut positions, &attributes).join() { let delta_x = i; let delta_y = j; @@ -46,7 +47,7 @@ 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 { + } else if rng.roll_dice(1, 6) + attributes.strength.bonus < 5 { if let Some(name) = names.get(*potential_target) { gamelog::Logger::new().append("The").item_name(&name.name).append("resists!").log(); } @@ -91,6 +92,7 @@ pub fn open(i: i32, j: i32, ecs: &mut World) -> RunState { let mut positions = ecs.write_storage::(); let mut players = ecs.write_storage::(); let mut viewsheds = ecs.write_storage::(); + let attributes = ecs.read_storage::(); let map = ecs.fetch::(); let entities = ecs.entities(); @@ -104,7 +106,7 @@ pub fn open(i: i32, j: i32, ecs: &mut World) -> RunState { let mut result = RunState::AwaitingInput; let mut door_pos: Option = None; - for (_entity, _player, pos) in (&entities, &mut players, &mut positions).join() { + for (_entity, _player, pos, attributes) in (&entities, &mut players, &mut positions, &attributes).join() { let delta_x = i; let delta_y = j; @@ -122,7 +124,7 @@ 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 { - if rng.roll_dice(1, 6) == 1 { + if rng.roll_dice(1, 6) + attributes.strength.bonus < 5 { if let Some(name) = names.get(*potential_target) { gamelog::Logger::new().append("The").item_name(&name.name).append("resists!").log(); } @@ -166,6 +168,7 @@ pub fn kick(i: i32, j: i32, ecs: &mut World) -> RunState { let mut positions = ecs.write_storage::(); let mut players = ecs.write_storage::(); let mut viewsheds = ecs.write_storage::(); + let attributes = ecs.read_storage::(); let map = ecs.fetch::(); let entities = ecs.entities(); @@ -173,7 +176,7 @@ pub fn kick(i: i32, j: i32, ecs: &mut World) -> RunState { let names = ecs.read_storage::(); let mut rng = ecs.write_resource::(); - for (entity, _player, pos) in (&entities, &mut players, &mut positions).join() { + for (entity, _player, pos, attributes) in (&entities, &mut players, &mut positions, &attributes).join() { let delta_x = i; let delta_y = j; @@ -210,8 +213,8 @@ pub fn kick(i: i32, j: i32, ecs: &mut World) -> RunState { if door.open == false { let mut particle_builder = ecs.write_resource::(); particle_builder.kick(pos.x + delta_x, pos.y + delta_y); - // 33% chance of breaking it down. - if rng.roll_dice(1, 3) == 1 { + // 33% chance of breaking it down + str + if rng.roll_dice(1, 6) + attributes.strength.bonus > 4 { gamelog::Logger::new() .append("As you kick the") .item_name_n(target_name) @@ -273,7 +276,7 @@ pub fn try_move_player(delta_x: i32, delta_y: i32, ecs: &mut World) -> bool { 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 pools = ecs.read_storage::(); let map = ecs.fetch::(); let entities = ecs.entities(); @@ -304,7 +307,7 @@ pub fn try_move_player(delta_x: i32, delta_y: i32, ecs: &mut World) -> bool { ppos.x = pos.x; ppos.y = pos.y; } else { - let target = combat_stats.get(*potential_target); + let target = pools.get(*potential_target); if let Some(_target) = target { wants_to_melee .insert(entity, WantsToMelee { target: *potential_target }) @@ -538,12 +541,12 @@ fn skip_turn(ecs: &mut World) -> bool { let mut did_heal = false; if can_heal { - let mut health_components = ecs.write_storage::(); - let player_hp = health_components.get_mut(*player_entity).unwrap(); + let mut health_components = ecs.write_storage::(); + let pools = health_components.get_mut(*player_entity).unwrap(); let mut rng = ecs.write_resource::(); let roll = rng.roll_dice(1, 6); - if (roll == 6) && player_hp.hp < player_hp.max_hp { - player_hp.hp += 1; + if (roll == 6) && pools.hit_points.current < pools.hit_points.max { + pools.hit_points.current += 1; did_heal = true; } } diff --git a/src/raws/mob_structs.rs b/src/raws/mob_structs.rs index b12392b..bf0d480 100644 --- a/src/raws/mob_structs.rs +++ b/src/raws/mob_structs.rs @@ -1,5 +1,6 @@ use super::Renderable; use serde::Deserialize; +use std::collections::HashMap; #[derive(Deserialize, Debug)] pub struct Mob { @@ -7,16 +8,28 @@ pub struct Mob { pub name: String, pub renderable: Option, pub flags: Option>, - pub stats: MobStats, + pub level: Option, + pub bac: Option, + pub attacks: Option>, + pub attributes: Option, + pub skills: Option>, pub vision_range: i32, - pub ai: String, pub quips: Option>, } #[derive(Deserialize, Debug)] -pub struct MobStats { - pub max_hp: i32, - pub hp: i32, - pub power: i32, - pub defence: i32, +pub struct MobAttributes { + pub str: Option, + pub dex: Option, + pub con: Option, + pub int: Option, + pub wis: Option, + pub cha: Option, +} + +#[derive(Deserialize, Debug)] +pub struct NaturalAttack { + pub name: String, + pub hit_bonus: i32, + pub damage: String, } diff --git a/src/raws/rawmaster.rs b/src/raws/rawmaster.rs index 4ac7503..128f482 100644 --- a/src/raws/rawmaster.rs +++ b/src/raws/rawmaster.rs @@ -1,11 +1,16 @@ use super::Raws; use crate::components::*; +use crate::gamesystem::*; use crate::random_table::RandomTable; +use regex::Regex; use specs::prelude::*; +use specs::saveload::{MarkedBuilder, SimpleMarker}; use std::collections::{HashMap, HashSet}; pub enum SpawnType { AtPosition { x: i32, y: i32 }, + Equipped { by: Entity }, + Carried { by: Entity }, } pub struct RawMaster { @@ -67,36 +72,32 @@ impl RawMaster { } } -pub fn spawn_named_entity( - raws: &RawMaster, - new_entity: EntityBuilder, - key: &str, - pos: SpawnType, - rng: &mut rltk::RandomNumberGenerator, -) -> Option { +pub fn spawn_named_entity(raws: &RawMaster, ecs: &mut World, key: &str, pos: SpawnType) -> Option { if raws.item_index.contains_key(key) { - return spawn_named_item(raws, new_entity, key, pos); + return spawn_named_item(raws, ecs, key, pos); } else if raws.mob_index.contains_key(key) { - return spawn_named_mob(raws, new_entity, key, pos, rng); + return spawn_named_mob(raws, ecs, key, pos); } else if raws.prop_index.contains_key(key) { - return spawn_named_prop(raws, new_entity, key, pos); + return spawn_named_prop(raws, ecs, key, pos); } None } -pub fn spawn_named_item(raws: &RawMaster, new_entity: EntityBuilder, key: &str, pos: SpawnType) -> Option { +pub fn spawn_named_item(raws: &RawMaster, ecs: &mut World, key: &str, pos: SpawnType) -> Option { if raws.item_index.contains_key(key) { let item_template = &raws.raws.items[raws.item_index[key]]; - let mut eb = new_entity; + let mut eb = ecs.create_entity().marked::>(); eb = eb.with(Name { name: item_template.name.name.clone(), plural: item_template.name.plural.clone() }); eb = eb.with(Item {}); - eb = spawn_position(pos, eb); + eb = spawn_position(pos, eb, key, raws); if let Some(renderable) = &item_template.renderable { eb = eb.with(get_renderable_component(renderable)); } + let mut weapon_type = -1; + if let Some(flags) = &item_template.flags { for flag in flags.iter() { match flag.as_str() { @@ -105,13 +106,25 @@ pub fn spawn_named_item(raws: &RawMaster, new_entity: EntityBuilder, key: &str, "CURSED" => eb = eb.with(Cursed {}), "EQUIP_MELEE" => eb = eb.with(Equippable { slot: EquipmentSlot::Melee }), "EQUIP_SHIELD" => eb = eb.with(Equippable { slot: EquipmentSlot::Shield }), + "EQUIP_HEAD" => eb = eb.with(Equippable { slot: EquipmentSlot::Head }), + "EQUIP_TORSO" => eb = eb.with(Equippable { slot: EquipmentSlot::Torso }), + "EQUIP_LEGS" => eb = eb.with(Equippable { slot: EquipmentSlot::Legs }), + "EQUIP_FEET" => eb = eb.with(Equippable { slot: EquipmentSlot::Feet }), + "EQUIP_HANDS" => eb = eb.with(Equippable { slot: EquipmentSlot::Hands }), + "EQUIP_NECK" => eb = eb.with(Equippable { slot: EquipmentSlot::Neck }), "WAND" => eb = eb.with(Wand { uses: 3, max_uses: 3 }), "FOOD" => eb = eb.with(ProvidesNutrition {}), + "STRENGTH" => weapon_type = 0, + "DEXTERITY" => weapon_type = 2, + "FINESSE" => weapon_type = 3, _ => rltk::console::log(format!("Unrecognised flag: {}", flag.as_str())), } } } + let mut base_damage = "1d4"; + let mut hit_bonus = 0; + if let Some(effects_list) = &item_template.effects { for effect in effects_list.iter() { let effect_name = effect.0.as_str(); @@ -121,8 +134,9 @@ pub fn spawn_named_item(raws: &RawMaster, new_entity: EntityBuilder, key: &str, "damage" => eb = eb.with(InflictsDamage { amount: effect.1.parse::().unwrap() }), "aoe" => eb = eb.with(AOE { radius: effect.1.parse::().unwrap() }), "confusion" => eb = eb.with(Confusion { turns: effect.1.parse::().unwrap() }), - "melee_power_bonus" => eb = eb.with(MeleePowerBonus { amount: effect.1.parse::().unwrap() }), - "defence_bonus" => eb = eb.with(DefenceBonus { amount: effect.1.parse::().unwrap() }), + "base_damage" => base_damage = effect.1, + "hit_bonus" => hit_bonus = effect.1.parse::().unwrap(), + "ac" => eb = eb.with(ArmourClassBonus { amount: effect.1.parse::().unwrap() }), "magicmapper" => eb = eb.with(MagicMapper {}), "digger" => eb = eb.with(Digger {}), _ => rltk::console::log(format!("Warning: effect {} not implemented.", effect_name)), @@ -130,77 +144,158 @@ pub fn spawn_named_item(raws: &RawMaster, new_entity: EntityBuilder, key: &str, } } + if weapon_type != -1 { + let (n_dice, die_type, bonus) = parse_dice_string(base_damage); + let mut wpn = MeleeWeapon { + attribute: WeaponAttribute::Strength, + damage_n_dice: n_dice, + damage_die_type: die_type, + damage_bonus: bonus, + hit_bonus: hit_bonus, + }; + match weapon_type { + 0 => wpn.attribute = WeaponAttribute::Strength, + 1 => wpn.attribute = WeaponAttribute::Dexterity, + _ => wpn.attribute = WeaponAttribute::Finesse, + } + eb = eb.with(wpn); + } + return Some(eb.build()); } None } -pub fn spawn_named_mob( - raws: &RawMaster, - new_entity: EntityBuilder, - key: &str, - pos: SpawnType, - rng: &mut rltk::RandomNumberGenerator, -) -> Option { +pub fn spawn_named_mob(raws: &RawMaster, ecs: &mut World, key: &str, pos: SpawnType) -> Option { if raws.mob_index.contains_key(key) { let mob_template = &raws.raws.mobs[raws.mob_index[key]]; + let mut eb; // New entity with a position, name, combatstats, and viewshed - let mut eb = new_entity; - eb = spawn_position(pos, eb); - eb = eb.with(Name { name: mob_template.name.clone(), plural: mob_template.name.clone() }); - 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, - hp: rolled_hp, - power: mob_template.stats.power, - defence: mob_template.stats.defence, - }); - eb = eb.with(Viewshed { visible_tiles: Vec::new(), range: mob_template.vision_range, dirty: true }); + eb = ecs.create_entity().marked::>(); + eb = spawn_position(pos, eb, key, raws); + eb = eb.with(Name { name: mob_template.name.clone(), plural: mob_template.name.clone() }); + eb = eb.with(Viewshed { visible_tiles: Vec::new(), range: mob_template.vision_range, dirty: true }); if let Some(renderable) = &mob_template.renderable { eb = eb.with(get_renderable_component(renderable)); } - if let Some(flags) = &mob_template.flags { for flag in flags.iter() { match flag.as_str() { "BLOCKS_TILE" => eb = eb.with(BlocksTile {}), + "BYSTANDER" => eb = eb.with(Bystander {}), + "MONSTER" => eb = eb.with(Monster {}), _ => rltk::console::log(format!("Unrecognised flag: {}", flag.as_str())), } } } - if let Some(quips) = &mob_template.quips { eb = eb.with(Quips { available: quips.clone() }); } + // Setup combat stats + let mut attr = Attributes { + strength: Attribute { base: 10, modifiers: 0, bonus: 0 }, + dexterity: Attribute { base: 10, modifiers: 0, bonus: 0 }, + constitution: Attribute { base: 10, modifiers: 0, bonus: 0 }, + intelligence: Attribute { base: 10, modifiers: 0, bonus: 0 }, + wisdom: Attribute { base: 10, modifiers: 0, bonus: 0 }, + charisma: Attribute { base: 10, modifiers: 0, bonus: 0 }, + }; + let mut mob_con = 10; + let mut mob_int = 10; + if let Some(attributes) = &mob_template.attributes { + if let Some(str) = attributes.str { + attr.strength = Attribute { base: str, modifiers: 0, bonus: attr_bonus(str) }; + } + if let Some(dex) = attributes.dex { + attr.strength = Attribute { base: dex, modifiers: 0, bonus: attr_bonus(dex) }; + } + if let Some(con) = attributes.con { + attr.constitution = Attribute { base: con, modifiers: 0, bonus: attr_bonus(con) }; + mob_con = con; + } + if let Some(int) = attributes.int { + attr.intelligence = Attribute { base: int, modifiers: 0, bonus: attr_bonus(int) }; + mob_int = int; + } + if let Some(wis) = attributes.wis { + attr.wisdom = Attribute { base: wis, modifiers: 0, bonus: attr_bonus(wis) }; + } + if let Some(cha) = attributes.cha { + attr.charisma = Attribute { base: cha, modifiers: 0, bonus: attr_bonus(cha) }; + } + } + eb = eb.with(attr); + let mob_level = if mob_template.level.is_some() { mob_template.level.unwrap() } else { 1 }; + + // Should really use existing RNG here + let mut rng = rltk::RandomNumberGenerator::new(); + + let mob_hp = npc_hp(&mut rng, mob_con, mob_level); + let mob_mana = mana_at_level(&mut rng, mob_int, mob_level); + let mob_bac = if mob_template.bac.is_some() { mob_template.bac.unwrap() } else { 10 }; + + let pools = Pools { + level: mob_level, + xp: 0, + bac: mob_bac, + hit_points: Pool { current: mob_hp, max: mob_hp }, + mana: Pool { current: mob_mana, max: mob_mana }, + }; + eb = eb.with(pools); + + let mut skills = Skills { skills: HashMap::new() }; + skills.skills.insert(Skill::Melee, 0); + skills.skills.insert(Skill::Defence, 0); + if let Some(mobskills) = &mob_template.skills { + for sk in mobskills.iter() { + match sk.0.as_str() { + "melee" => { + skills.skills.insert(Skill::Melee, *sk.1); + } + "defence" => { + skills.skills.insert(Skill::Defence, *sk.1); + } + "magic" => { + skills.skills.insert(Skill::Magic, *sk.1); + } + _ => { + rltk::console::log(format!("Unknown skill referenced: [{}]", sk.0)); + } + } + } + } + eb = eb.with(skills); + + if let Some(natural_attacks) = &mob_template.attacks { + let mut natural = NaturalAttacks { attacks: Vec::new() }; + for na in natural_attacks.iter() { + let (n, d, b) = parse_dice_string(&na.damage); + let attack = NaturalAttack { + name: na.name.clone(), + hit_bonus: na.hit_bonus, + damage_n_dice: n, + damage_die_type: d, + damage_bonus: b, + }; + natural.attacks.push(attack); + } + eb = eb.with(natural); + } + return Some(eb.build()); } None } -pub fn roll_hit_dice(rng: &mut rltk::RandomNumberGenerator, n: i32, d: i32) -> i32 { - let mut rolled_hp: i32 = 0; - - for _i in 0..n { - rolled_hp += rng.roll_dice(1, d); - } - - return rolled_hp; -} - -pub fn spawn_named_prop(raws: &RawMaster, new_entity: EntityBuilder, key: &str, pos: SpawnType) -> Option { +pub fn spawn_named_prop(raws: &RawMaster, ecs: &mut World, key: &str, pos: SpawnType) -> Option { if raws.prop_index.contains_key(key) { let prop_template = &raws.raws.props[raws.prop_index[key]]; - let mut eb = new_entity; - eb = spawn_position(pos, eb); + let mut eb = ecs.create_entity().marked::>(); + eb = spawn_position(pos, eb, key, raws); if let Some(renderable) = &prop_template.renderable { eb = eb.with(get_renderable_component(renderable)); } @@ -237,12 +332,15 @@ pub fn spawn_named_prop(raws: &RawMaster, new_entity: EntityBuilder, key: &str, None } -fn spawn_position(pos: SpawnType, new_entity: EntityBuilder) -> EntityBuilder { +fn spawn_position<'a>(pos: SpawnType, new_entity: EntityBuilder<'a>, tag: &str, raws: &RawMaster) -> EntityBuilder<'a> { let mut eb = new_entity; match pos { - SpawnType::AtPosition { x, y } => { - eb = eb.with(Position { x, y }); + SpawnType::AtPosition { x, y } => eb = eb.with(Position { x, y }), + SpawnType::Carried { by } => eb = eb.with(InBackpack { owner: by }), + SpawnType::Equipped { by } => { + let slot = find_slot_for_equippable_item(tag, raws); + eb = eb.with(Equipped { owner: by, slot }) } } @@ -283,3 +381,42 @@ pub fn table_by_name(raws: &RawMaster, key: &str, difficulty: i32) -> RandomTabl return RandomTable::new().add("debug", 1); } } + +pub fn parse_dice_string(dice: &str) -> (i32, i32, i32) { + lazy_static! { + static ref DICE_RE: Regex = Regex::new(r"(\d+)d(\d+)([\+\-]\d+)?").unwrap(); + } + let mut n_dice = 1; + let mut die_type = 4; + let mut die_bonus = 0; + for cap in DICE_RE.captures_iter(dice) { + if let Some(group) = cap.get(1) { + n_dice = group.as_str().parse::().expect("Not a digit"); + } + if let Some(group) = cap.get(2) { + die_type = group.as_str().parse::().expect("Not a digit"); + } + if let Some(group) = cap.get(3) { + die_bonus = group.as_str().parse::().expect("Not a digit"); + } + } + (n_dice, die_type, die_bonus) +} + +fn find_slot_for_equippable_item(tag: &str, raws: &RawMaster) -> EquipmentSlot { + if !raws.item_index.contains_key(tag) { + panic!("Trying to equip an unknown item: {}", tag); + } + let item_index = raws.item_index[tag]; + let item = &raws.raws.items[item_index]; + if let Some(flags) = &item.flags { + for flag in flags { + match flag.as_str() { + "EQUIP_MELEE" => return EquipmentSlot::Melee, + "EQUIP_SHIELD" => return EquipmentSlot::Shield, + _ => {} + } + } + } + panic!("Trying to equip {}, but it has no slot tag.", tag); +} diff --git a/src/saveload_system.rs b/src/saveload_system.rs index 3fc18eb..d72c441 100644 --- a/src/saveload_system.rs +++ b/src/saveload_system.rs @@ -48,15 +48,14 @@ pub fn save_game(ecs: &mut World) { serializer, data, AOE, + ArmourClassBonus, Attributes, BlocksTile, BlocksVisibility, Bystander, - CombatStats, Confusion, Consumable, Cursed, - DefenceBonus, Destructible, Digger, Door, @@ -70,13 +69,15 @@ pub fn save_game(ecs: &mut World) { InflictsDamage, Item, MagicMapper, - MeleePowerBonus, + MeleeWeapon, Mind, Monster, + NaturalAttacks, Name, ParticleLifetime, Player, Position, + Pools, Prop, ProvidesHealing, ProvidesNutrition, @@ -84,6 +85,7 @@ pub fn save_game(ecs: &mut World) { Ranged, Renderable, SingleActivation, + Skills, SufferDamage, Telepath, Viewshed, @@ -147,15 +149,14 @@ pub fn load_game(ecs: &mut World) { de, d, AOE, + ArmourClassBonus, Attributes, BlocksTile, BlocksVisibility, Bystander, - CombatStats, Confusion, Consumable, Cursed, - DefenceBonus, Destructible, Digger, Door, @@ -169,12 +170,14 @@ pub fn load_game(ecs: &mut World) { InflictsDamage, Item, MagicMapper, - MeleePowerBonus, + MeleeWeapon, Mind, Monster, + NaturalAttacks, Name, ParticleLifetime, Player, + Pools, Position, Prop, ProvidesHealing, @@ -183,6 +186,7 @@ pub fn load_game(ecs: &mut World) { Ranged, Renderable, SingleActivation, + Skills, SufferDamage, Telepath, Viewshed, diff --git a/src/spawner.rs b/src/spawner.rs index 5d67076..00b71e2 100644 --- a/src/spawner.rs +++ b/src/spawner.rs @@ -1,6 +1,7 @@ use super::{ - random_table::RandomTable, raws, Attribute, Attributes, CombatStats, HungerClock, HungerState, Map, Name, Player, - Position, Rect, Renderable, SerializeMe, TileType, Viewshed, + gamesystem, gamesystem::attr_bonus, random_table::RandomTable, raws, Attribute, Attributes, HungerClock, + HungerState, Map, Name, Player, Pool, Pools, Position, Rect, Renderable, SerializeMe, Skill, Skills, TileType, + Viewshed, }; use rltk::{RandomNumberGenerator, RGB}; use specs::prelude::*; @@ -9,8 +10,23 @@ use std::collections::HashMap; /// Spawns the player and returns his/her entity object. pub fn player(ecs: &mut World, player_x: i32, player_y: i32) -> Entity { + let mut skills = Skills { skills: HashMap::new() }; + skills.skills.insert(Skill::Melee, 0); + skills.skills.insert(Skill::Defence, 0); + skills.skills.insert(Skill::Magic, 0); + + let mut rng = ecs.write_resource::(); + let str = gamesystem::roll_4d6(&mut rng); + let dex = gamesystem::roll_4d6(&mut rng); + let con = gamesystem::roll_4d6(&mut rng); + let int = gamesystem::roll_4d6(&mut rng); + let wis = gamesystem::roll_4d6(&mut rng); + let cha = gamesystem::roll_4d6(&mut rng); + std::mem::drop(rng); + // d8 hit die - but always maxxed at level 1, so player doesn't have to roll. - ecs.create_entity() + let player = ecs + .create_entity() .with(Position { x: player_x, y: player_y }) .with(Renderable { glyph: rltk::to_cp437('@'), @@ -21,22 +37,39 @@ pub fn player(ecs: &mut World, player_x: i32, player_y: i32) -> Entity { .with(Player {}) .with(Viewshed { visible_tiles: Vec::new(), range: 12, dirty: true }) .with(Name { name: "you".to_string(), plural: "you".to_string() }) - .with(CombatStats { max_hp: 12, hp: 12, defence: 0, power: 4 }) .with(HungerClock { state: HungerState::Satiated, duration: 50 }) .with(Attributes { - strength: Attribute { base: 10, modifiers: 0, bonus: 0 }, - dexterity: Attribute { base: 10, modifiers: 0, bonus: 0 }, - constitution: Attribute { base: 10, modifiers: 0, bonus: 0 }, - intelligence: Attribute { base: 10, modifiers: 0, bonus: 0 }, - wisdom: Attribute { base: 10, modifiers: 0, bonus: 0 }, - charisma: Attribute { base: 10, modifiers: 0, bonus: 0 }, + strength: Attribute { base: str, modifiers: 0, bonus: attr_bonus(str) }, + dexterity: Attribute { base: dex, modifiers: 0, bonus: attr_bonus(dex) }, + constitution: Attribute { base: con, modifiers: 0, bonus: attr_bonus(con) }, + intelligence: Attribute { base: int, modifiers: 0, bonus: attr_bonus(int) }, + wisdom: Attribute { base: wis, modifiers: 0, bonus: attr_bonus(wis) }, + charisma: Attribute { base: cha, modifiers: 0, bonus: attr_bonus(cha) }, }) + .with(Pools { + hit_points: Pool { current: 10 + attr_bonus(con), max: 10 + attr_bonus(con) }, + mana: Pool { current: 2 + attr_bonus(int), max: 2 + attr_bonus(int) }, + xp: 0, + level: 1, + bac: 10, + }) + .with(skills) .marked::>() - .build() + .build(); + + raws::spawn_named_entity( + &raws::RAWS.lock().unwrap(), + ecs, + "equip_dagger", + raws::SpawnType::Equipped { by: player }, + ); + raws::spawn_named_entity(&raws::RAWS.lock().unwrap(), ecs, "food_apple", raws::SpawnType::Carried { by: player }); + + return player; } // Consts -const MAX_ENTITIES: i32 = 3; +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)>) { @@ -61,6 +94,18 @@ pub fn spawn_region(map: &Map, rng: &mut RandomNumberGenerator, area: &[usize], let mut areas: Vec = Vec::from(area); let difficulty = map.difficulty; + 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); + } + let num_spawns = i32::min(areas.len() as i32, rng.roll_dice(1, MAX_ENTITIES + 2) - 2); if num_spawns <= 0 { return; @@ -70,7 +115,6 @@ pub fn spawn_region(map: &Map, rng: &mut RandomNumberGenerator, area: &[usize], let category = category_table().roll(rng); let spawn_table; match category.as_ref() { - "mob" => spawn_table = mob_table(difficulty), "item" => { let item_category = item_category_table().roll(rng); match item_category.as_ref() { @@ -104,13 +148,8 @@ pub fn spawn_entity(ecs: &mut World, spawn: &(&usize, &String)) { let y = (*spawn.0 / width) as i32; std::mem::drop(map); - let spawn_result = raws::spawn_named_entity( - &raws::RAWS.lock().unwrap(), - ecs.create_entity(), - &spawn.1, - raws::SpawnType::AtPosition { x, y }, - &mut rltk::RandomNumberGenerator::new(), - ); + let spawn_result = + raws::spawn_named_entity(&raws::RAWS.lock().unwrap(), ecs, &spawn.1, raws::SpawnType::AtPosition { x, y }); if spawn_result.is_some() { return; } @@ -118,9 +157,9 @@ pub fn spawn_entity(ecs: &mut World, spawn: &(&usize, &String)) { rltk::console::log(format!("WARNING: We don't know how to spawn [{}]!", spawn.1)); } -// 12 mobs : 6 items : 2 food : 1 trap +// 3 items : 1 food : 1 trap fn category_table() -> RandomTable { - return RandomTable::new().add("mob", 12).add("item", 6).add("food", 2).add("trap", 1); + return RandomTable::new().add("item", 3).add("food", 1).add("trap", 1); } // 3 scrolls : 3 potions : 1 equipment : 1 wand?