diff --git a/raws/spawns.json b/raws/spawns.json index 036199d..c2a34ae 100644 --- a/raws/spawns.json +++ b/raws/spawns.json @@ -184,6 +184,49 @@ } } }, + { + "name": "Dried Sausage", + "renderable": { + "glyph": "%", + "fg": "#00FF00", + "bg": "#000000", + "order": 2 + }, + "consumable": { + "effects": { + "food": "" + } + } + }, + { + "name": "Beer", + "renderable": { + "glyph": "!", + "fg": "#FF00FF", + "bg": "#000000", + "order": 2 + }, + "consumable": { + "effects": { + "provides_healing": "4" + } + } + }, + { + "name": "Rusty Longsword", + "renderable": { + "glyph": "/", + "fg": "#BB77BB", + "bg": "#000000", + "order": 2 + }, + "weapon": { + "range": "melee", + "attribute": "Might", + "base_damage": "1d8-1", + "hit_bonus": -1 + } + }, { "name": "Dagger", "renderable": { @@ -194,7 +237,9 @@ }, "weapon": { "range": "melee", - "power_bonus": 2 + "attribute": "Quickness", + "base_damage": "1d4", + "hit_bonus": 0 } }, { @@ -207,7 +252,9 @@ }, "weapon": { "range": "melee", - "power_bonus": 4 + "attribute": "Might", + "base_damage": "1d8", + "hit_bonus": 0 } }, { @@ -220,7 +267,9 @@ }, "weapon": { "range": "melee", - "power_bonus": 5 + "attribute": "Might", + "base_damage": "1d8+1", + "hit_bonus": 0 } }, { @@ -231,8 +280,9 @@ "bg": "#000000", "order": 2 }, - "shield": { - "defense_bonus": 1 + "wearable": { + "slot": "Shield", + "armor_class": 1.0 } }, { @@ -243,8 +293,102 @@ "bg": "#000000", "order": 2 }, - "shield": { - "defense_bonus": 3 + "wearable": { + "slot": "Shield", + "armor_class": 2.0 + } + }, + { + "name": "Stained Tunic", + "renderable": { + "glyph": "[", + "fg": "#00FF00", + "bg": "#000000", + "order": 2 + }, + "wearable": { + "slot": "Torso", + "armor_class": 0.1 + } + }, + { + "name": "Torn Trousers", + "renderable": { + "glyph": "[", + "fg": "#00FFFF", + "bg": "#000000", + "order": 2 + }, + "wearable": { + "slot": "Legs", + "armor_class": 0.1 + } + }, + { + "name": "Old Boots", + "renderable": { + "glyph": "[", + "fg": "#FF9999", + "bg": "#000000", + "order": 2 + }, + "wearable": { + "slot": "Legs", + "armor_class": 0.1 + } + }, + { + "name": "Cudgel", + "renderable": { + "glyph": "/", + "fg": "#A52A2A", + "bg": "#000000", + "order": 2 + }, + "weapon": { + "range": "melee", + "attribute": "Quickness", + "base_damage": "1d4", + "hit_bonus": 0 + } + }, + { + "name": "Cloth Tunic", + "renderable": { + "glyph": "[", + "fg": "#00FF00", + "bg": "#000000", + "order": 2 + }, + "wearable": { + "slot": "Torso", + "armor_class": 0.1 + } + }, + { + "name": "Cloth Pants", + "renderable": { + "glyph": "[", + "fg": "#00FFFF", + "bg": "#000000", + "order": 2 + }, + "wearable": { + "slot": "Legs", + "armor_class": 0.1 + } + }, + { + "name": "Slippers", + "renderable": { + "glyph": "[", + "fg": "#FF9999", + "bg": "#000000", + "order": 2 + }, + "wearable": { + "slot": "Legs", + "armor_class": 0.1 } } ], @@ -265,7 +409,13 @@ }, "skills": { "Melee": 2 - } + }, + "equipped": [ + "Cudgel", + "Cloth Tunic", + "Cloth Pants", + "Slippers" + ] }, { "name": "Shady Salesman", @@ -278,7 +428,13 @@ "blocks_tile": true, "vision_range": 4, "ai": "vendor", - "attributes": {} + "attributes": {}, + "equipped": [ + "Cudgel", + "Cloth Tunic", + "Cloth Pants", + "Slippers" + ] }, { "name": "Patron", @@ -296,7 +452,13 @@ "Oh my, I drank too much.", "Still saving the world, eh?" ], - "attributes": {} + "attributes": {}, + "equipped": [ + "Cudgel", + "Cloth Tunic", + "Cloth Pants", + "Slippers" + ] }, { "name": "Priest", @@ -309,7 +471,13 @@ "blocks_tile": true, "vision_range": 4, "ai": "bystander", - "attributes": {} + "attributes": {}, + "equipped": [ + "Cudgel", + "Cloth Tunic", + "Cloth Pants", + "Slippers" + ] }, { "name": "Parishioner", @@ -327,7 +495,13 @@ "I hear there's going to be a good sermon on tea", "Want some cake?" ], - "attributes": {} + "attributes": {}, + "equipped": [ + "Cudgel", + "Cloth Tunic", + "Cloth Pants", + "Slippers" + ] }, { "name": "Blacksmith", @@ -340,7 +514,13 @@ "blocks_tile": true, "vision_range": 4, "ai": "vendor", - "attributes": {} + "attributes": {}, + "equipped": [ + "Cudgel", + "Cloth Tunic", + "Cloth Pants", + "Slippers" + ] }, { "name": "Clothier", @@ -353,7 +533,13 @@ "blocks_tile": true, "vision_range": 4, "ai": "vendor", - "attributes": {} + "attributes": {}, + "equipped": [ + "Cudgel", + "Cloth Tunic", + "Cloth Pants", + "Slippers" + ] }, { "name": "Alchemist", @@ -366,7 +552,13 @@ "blocks_tile": true, "vision_range": 4, "ai": "vendor", - "attributes": {} + "attributes": {}, + "equipped": [ + "Cudgel", + "Cloth Tunic", + "Cloth Pants", + "Slippers" + ] }, { "name": "Mom", @@ -385,7 +577,13 @@ "Be careful in the dungeon!", "Your father would be so proud, were he here." ], - "attributes": {} + "attributes": {}, + "equipped": [ + "Cudgel", + "Cloth Tunic", + "Cloth Pants", + "Slippers" + ] }, { "name": "Peasant", @@ -401,7 +599,13 @@ "quips": [ "Why are you in my house?" ], - "attributes": {} + "attributes": {}, + "equipped": [ + "Cudgel", + "Cloth Tunic", + "Cloth Pants", + "Slippers" + ] }, { "name": "Dock Worker", @@ -419,7 +623,13 @@ "Nice weather", "Hello" ], - "attributes": {} + "attributes": {}, + "equipped": [ + "Cudgel", + "Cloth Tunic", + "Cloth Pants", + "Slippers" + ] }, { "name": "Fisher", @@ -437,7 +647,13 @@ "I caught something, but it wasn't a fish!", "Looks like rain" ], - "attributes": {} + "attributes": {}, + "equipped": [ + "Cudgel", + "Cloth Tunic", + "Cloth Pants", + "Slippers" + ] }, { "name": "Wannabe Pirate", @@ -455,7 +671,13 @@ "Grog!", "Booze!" ], - "attributes": {} + "attributes": {}, + "equipped": [ + "Cudgel", + "Cloth Tunic", + "Cloth Pants", + "Slippers" + ] }, { "name": "Drunk", @@ -473,7 +695,13 @@ "Need... more... booze!", "Spare a copper?" ], - "attributes": {} + "attributes": {}, + "equipped": [ + "Cudgel", + "Cloth Tunic", + "Cloth Pants", + "Slippers" + ] }, { "name": "Rat", @@ -493,6 +721,16 @@ "skills": { "Melee": -1, "Defense": -1 + }, + "natural": { + "armor_class": 11, + "attacks": [ + { + "name": "bite", + "hit_bonus": 0, + "damage": "1d4" + } + ] } }, { diff --git a/src/components.rs b/src/components.rs index a841393..527cc3e 100644 --- a/src/components.rs +++ b/src/components.rs @@ -165,6 +165,11 @@ pub struct WantsToRemoveItem { pub enum EquipmentSlot { Melee, Shield, + Head, + Torso, + Legs, + Feet, + Hands, } #[derive(Component, Serialize, Deserialize, Clone)] @@ -194,8 +199,9 @@ pub struct MeleeWeapon { } #[derive(Component, ConvertSaveload, Clone)] -pub struct DefenseBonus { - pub defense: i32, +pub struct Wearable { + pub armor_class: f32, + pub slot: EquipmentSlot, } #[derive(Component, Serialize, Deserialize, Clone)] diff --git a/src/main.rs b/src/main.rs index ca8d5be..005b3f3 100644 --- a/src/main.rs +++ b/src/main.rs @@ -519,7 +519,7 @@ fn main() -> ::rltk::BError { Equippable, Equipped, MeleeWeapon, - DefenseBonus, + Wearable, WantsToRemoveItem, ParticleLifetime, HungerClock, diff --git a/src/melee_combat_system.rs b/src/melee_combat_system.rs index 61af954..d997c61 100644 --- a/src/melee_combat_system.rs +++ b/src/melee_combat_system.rs @@ -2,12 +2,13 @@ use ::rltk::{RandomNumberGenerator, RGB}; use ::specs::prelude::*; use crate::components::{ - Attributes, HungerClock, HungerState, Name, Pools, Skill, Skills, SufferDamage, WantsToMelee, + Attributes, Equipped, HungerClock, HungerState, MeleeWeapon, Name, Pools, Skill, Skills, + SufferDamage, WantsToMelee, Wearable, }; use crate::game_log::GameLog; use crate::gamesystem::skill_bonus; use crate::particle_system::ParticleBuilder; -use crate::Position; +use crate::{EquipmentSlot, Position, WeaponAttribute}; pub struct MeleeCombatSystem {} @@ -26,6 +27,9 @@ impl<'a> System<'a> for MeleeCombatSystem { ReadStorage<'a, HungerClock>, ReadStorage<'a, Pools>, WriteExpect<'a, RandomNumberGenerator>, + ReadStorage<'a, Equipped>, + ReadStorage<'a, MeleeWeapon>, + ReadStorage<'a, Wearable>, ); fn run(&mut self, data: Self::SystemData) { @@ -42,6 +46,9 @@ impl<'a> System<'a> for MeleeCombatSystem { hunger_clock, pools, mut rng, + equipped_items, + meleeweapons, + wearables, ) = data; for (entity, wants_melee, name, attacker_attributes, attacker_skills, attacker_pools) in ( @@ -61,10 +68,28 @@ impl<'a> System<'a> for MeleeCombatSystem { if attacker_pools.hit_points.current > 0 && target_pools.hit_points.current > 0 { let target_name = names.get(wants_melee.target).unwrap(); + let mut weapon_info = MeleeWeapon { + attribute: WeaponAttribute::Might, + hit_bonus: 0, + damage_n_dice: 1, + damage_die_type: 4, + damage_bonus: 0, + }; + + for (wielded, melee) in (&equipped_items, &meleeweapons).join() { + if wielded.owner == entity && wielded.slot == EquipmentSlot::Melee { + weapon_info = melee.clone(); + } + } + let natural_roll = rng.roll_dice(1, 20); - let attribute_hit_bonus = attacker_attributes.might.bonus; + let attribute_hit_bonus = if weapon_info.attribute == WeaponAttribute::Might { + attacker_attributes.might.bonus + } else { + attacker_attributes.quickness.bonus + }; let skill_hit_bonus = skill_bonus(Skill::Melee, &*attacker_skills); - let weapon_hit_bonus = 0; // TODO: Once weapons support this + let weapon_hit_bonus = weapon_info.hit_bonus; let mut status_hit_bonus = 0; if let Some(hc) = hunger_clock.get(entity) { // Well-Fed grants +1 @@ -78,10 +103,21 @@ impl<'a> System<'a> for MeleeCombatSystem { + weapon_hit_bonus + status_hit_bonus; + let mut armor_item_bonus_f = 0.0; + for (wielded, armor) in (&equipped_items, &wearables).join() { + if wielded.owner == wants_melee.target { + armor_item_bonus_f += armor.armor_class; + } + } + + // let base_armor_class = match natural.get(wants_melee.target) { + // None => 10, + // Some(nat) = nat.armor_class.unwrap_or(10); + // }; let base_armor_class = 10; let armor_quickness_bonus = target_attributes.quickness.bonus; let armor_skill_bonus = skill_bonus(Skill::Defense, &*target_skills); - let armor_item_bonus = 0; // TODO: Once armor supports this + let armor_item_bonus = armor_item_bonus_f as i32; let armor_class = base_armor_class + armor_quickness_bonus + armor_skill_bonus + armor_item_bonus; @@ -90,7 +126,7 @@ impl<'a> System<'a> for MeleeCombatSystem { let base_damage = rng.roll_dice(1, 4); let attr_damage_bonus = attacker_attributes.might.bonus; let skill_damage_bonus = skill_bonus(Skill::Melee, &*attacker_skills); - let weapon_damage_bonus = 0; + let weapon_damage_bonus = weapon_info.damage_bonus; let damage = i32::max( 0, diff --git a/src/raws/item_structs.rs b/src/raws/item_structs.rs index 36cb922..478887b 100644 --- a/src/raws/item_structs.rs +++ b/src/raws/item_structs.rs @@ -8,7 +8,7 @@ pub struct Item { pub renderable: Option, pub consumable: Option, pub weapon: Option, - pub shield: Option, + pub wearable: Option, } #[derive(Deserialize, Debug)] @@ -33,6 +33,7 @@ pub struct Weapon { } #[derive(Deserialize, Debug)] -pub struct Shield { - pub defense_bonus: i32, +pub struct Wearable { + pub armor_class: f32, + pub slot: String, } diff --git a/src/raws/mob_structs.rs b/src/raws/mob_structs.rs index 1708fc7..8e7895f 100644 --- a/src/raws/mob_structs.rs +++ b/src/raws/mob_structs.rs @@ -17,6 +17,7 @@ pub struct Mob { pub level: Option, pub hp: Option, pub mana: Option, + pub equipped: Option>, } #[derive(Deserialize, Debug)] diff --git a/src/raws/rawmaster.rs b/src/raws/rawmaster.rs index aadb19b..8505fa6 100644 --- a/src/raws/rawmaster.rs +++ b/src/raws/rawmaster.rs @@ -2,6 +2,7 @@ use std::collections::{HashMap, HashSet}; use ::regex::Regex; use ::specs::prelude::*; +use ::specs::saveload::{MarkedBuilder, SimpleMarker}; use crate::components::*; use crate::gamesystem::{mana_at_level, npc_hp}; @@ -16,7 +17,7 @@ pub fn parse_dice_string(dice: &str) -> (i32, i32, i32) { let mut die_type = 4; let mut die_bonus = 0; - for cap in DIC_RE.captures_iter(dice) { + 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"); } @@ -33,6 +34,8 @@ pub fn parse_dice_string(dice: &str) -> (i32, i32, i32) { pub enum SpawnType { AtPosition { x: i32, y: i32 }, + Equipped { by: Entity }, + Carried { by: Entity }, } pub struct RawMaster { @@ -103,17 +106,40 @@ impl RawMaster { } } -fn spawn_position(pos: SpawnType, new_entity: EntityBuilder) -> EntityBuilder { - let mut eb = new_entity; +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 item.weapon.is_some() { + return EquipmentSlot::Melee; + } else if let Some(wearable) = &item.wearable { + return string_to_slot(&wearable.slot); + } + + panic!("Trying to equip {}, but it has not slot tag", tag); +} + +fn spawn_position<'a>( + pos: SpawnType, + new_entity: EntityBuilder<'a>, + tag: &str, + raws: &RawMaster, +) -> EntityBuilder<'a> { + let eb = new_entity; // Spawn in the specified location match pos { - SpawnType::AtPosition { x, y } => { - eb = eb.with(Position { x, y }); + SpawnType::AtPosition { x, y } => eb.with(Position { x, y }), + SpawnType::Carried { by } => eb.with(InBackpack { owner: by }), + SpawnType::Equipped { by } => { + let slot = find_slot_for_equippable_item(tag, raws); + eb.with(Equipped { owner: by, slot }) } } - - eb } fn get_renderable_component( @@ -127,19 +153,36 @@ fn get_renderable_component( } } +pub fn string_to_slot(slot: &str) -> EquipmentSlot { + match slot { + "Shield" => EquipmentSlot::Shield, + "Head" => EquipmentSlot::Head, + "Torso" => EquipmentSlot::Torso, + "Legs" => EquipmentSlot::Legs, + "Feet" => EquipmentSlot::Feet, + "Hands" => EquipmentSlot::Hands, + "Melee" => EquipmentSlot::Melee, + _ => { + rltk::console::log(format!("Warning: unknown equipment slot type [{}]", slot)); + + EquipmentSlot::Melee + } + } +} + pub fn spawn_named_item( raws: &RawMaster, - new_entity: EntityBuilder, + 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::>(); // Spawn in the specified location - eb = spawn_position(pos, eb); + eb = spawn_position(pos, eb, key, raws); // Renderable if let Some(renderable) = &item_template.renderable { @@ -196,17 +239,29 @@ pub fn spawn_named_item( eb = eb.with(Equippable { slot: EquipmentSlot::Melee, }); - eb = eb.with(MeleePowerBonus { - power: weapon.power_bonus, - }); + let (n_dice, die_type, bonus) = parse_dice_string(&weapon.base_damage); + let mut wpn = MeleeWeapon { + attribute: WeaponAttribute::Might, + damage_n_dice: n_dice, + damage_die_type: die_type, + damage_bonus: bonus, + hit_bonus: weapon.hit_bonus, + }; + + wpn.attribute = match weapon.attribute.as_str() { + "Quickness" => WeaponAttribute::Quickness, + _ => WeaponAttribute::Might, + }; + + eb = eb.with(wpn); } - if let Some(shield) = &item_template.shield { - eb = eb.with(Equippable { - slot: EquipmentSlot::Shield, - }); - eb = eb.with(DefenseBonus { - defense: shield.defense_bonus, + if let Some(wearable) = &item_template.wearable { + let slot = string_to_slot(&wearable.slot); + eb = eb.with(Equippable { slot }); + eb = eb.with(Wearable { + slot, + armor_class: wearable.armor_class, }); } @@ -218,17 +273,17 @@ pub fn spawn_named_item( pub fn spawn_named_mob( raws: &RawMaster, - new_entity: EntityBuilder, + 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; + let mut eb = ecs.create_entity().marked::>(); // Spawn in the specified location - eb = spawn_position(pos, eb); + eb = spawn_position(pos, eb, key, raws); // Renderable if let Some(renderable) = &mob_template.renderable { @@ -251,6 +306,10 @@ pub fn spawn_named_mob( }); } + if mob_template.blocks_tile { + eb = eb.with(BlocksTile {}); + } + let mut mob_fitness = 11; let mut mob_int = 11; let mut attr = Attributes { @@ -318,17 +377,22 @@ pub fn spawn_named_mob( } eb = eb.with(skills); - if mob_template.blocks_tile { - eb = eb.with(BlocksTile {}); - } - eb = eb.with(Viewshed { visible_tiles: Vec::new(), range: mob_template.vision_range, dirty: true, }); - return Some(eb.build()); + let new_mob = eb.build(); + + // Are they weilding anything? + if let Some(wielding) = &mob_template.equipped { + for tag in wielding.iter() { + spawn_named_entity(raws, ecs, tag, SpawnType::Equipped { by: new_mob }); + } + } + + return Some(new_mob); } None @@ -336,17 +400,17 @@ pub fn spawn_named_mob( pub fn spawn_named_prop( raws: &RawMaster, - new_entity: EntityBuilder, + 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; + let mut eb = ecs.create_entity().marked::>(); // Spawn in the specified location - eb = spawn_position(pos, eb); + eb = spawn_position(pos, eb, key, raws); // Renderable if let Some(renderable) = &prop_template.renderable { @@ -396,16 +460,16 @@ pub fn spawn_named_prop( pub fn spawn_named_entity( raws: &RawMaster, - new_entity: EntityBuilder, + 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); + 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 diff --git a/src/saveload_system.rs b/src/saveload_system.rs index 483a2c2..c9bcb36 100644 --- a/src/saveload_system.rs +++ b/src/saveload_system.rs @@ -76,7 +76,7 @@ pub fn save_game(ecs: &mut World) { Equippable, Equipped, MeleeWeapon, - DefenseBonus, + Wearable, WantsToRemoveItem, ParticleLifetime, HungerClock, @@ -172,7 +172,7 @@ pub fn load_game(ecs: &mut World) { Equippable, Equipped, MeleeWeapon, - DefenseBonus, + Wearable, WantsToRemoveItem, ParticleLifetime, HungerClock, diff --git a/src/spawner.rs b/src/spawner.rs index 9f71ae8..c9c1ddc 100644 --- a/src/spawner.rs +++ b/src/spawner.rs @@ -12,7 +12,8 @@ use crate::{Map, Rect, TileType}; /// Spawns the player and returns their entity object pub fn player(ecs: &mut World, player_x: i32, player_y: i32) -> Entity { - ecs.create_entity() + let player = ecs + .create_entity() .with(Position { x: player_x, y: player_y, @@ -50,7 +51,47 @@ pub fn player(ecs: &mut World, player_x: i32, player_y: i32) -> Entity { level: 1, }) .marked::>() - .build() + .build(); + + // Starting equipment + spawn_named_entity( + &RAWS.lock().unwrap(), + ecs, + "Rusty Longsword", + SpawnType::Equipped { by: player }, + ); + spawn_named_entity( + &RAWS.lock().unwrap(), + ecs, + "Dried Sausage", + SpawnType::Carried { by: player }, + ); + spawn_named_entity( + &RAWS.lock().unwrap(), + ecs, + "Beer", + SpawnType::Carried { by: player }, + ); + spawn_named_entity( + &RAWS.lock().unwrap(), + ecs, + "Stained Tunic", + SpawnType::Equipped { by: player }, + ); + spawn_named_entity( + &RAWS.lock().unwrap(), + ecs, + "Torn Trousers", + SpawnType::Equipped { by: player }, + ); + spawn_named_entity( + &RAWS.lock().unwrap(), + ecs, + "Old Boots", + SpawnType::Equipped { by: player }, + ); + + player } const MAX_MONSTERS: i32 = 4; @@ -136,7 +177,7 @@ pub fn spawn_entity(ecs: &mut World, spawn: &(&usize, &String)) { let item_result = spawn_named_entity( &RAWS.lock().unwrap(), - ecs.create_entity(), + ecs, spawn.1, SpawnType::AtPosition { x, y }, );