From a17a2c8f11e79113908c4e52367e9974c2529c30 Mon Sep 17 00:00:00 2001 From: Llywelwyn Date: Sun, 30 Jul 2023 05:30:01 +0100 Subject: [PATCH] combat system 2nd pass - multiattacks for natural attackers --- raws/mobs.json | 6 +- raws/spawn_tables.json | 2 +- src/components.rs | 3 + src/main.rs | 3 +- src/melee_combat_system.rs | 404 +++++++++++++++++++++---------------- src/raws/rawmaster.rs | 1 + src/saveload_system.rs | 2 + 7 files changed, 247 insertions(+), 174 deletions(-) diff --git a/raws/mobs.json b/raws/mobs.json index 6442d0c..f9792e0 100644 --- a/raws/mobs.json +++ b/raws/mobs.json @@ -127,7 +127,7 @@ "id": "horse_little", "name": "pony", "renderable": { "glyph": "u", "fg": "#b36c29", "bg": "#000000", "order": 1 }, - "flags": ["BYSTANDER", "BLOCKS_TILE"], + "flags": ["BYSTANDER", "BLOCKS_TILE", "MULTIATTACK"], "level": 3, "bac": 6, "vision_range": 8, @@ -140,7 +140,7 @@ "id": "horse", "name": "horse", "renderable": { "glyph": "u", "fg": "#744d29", "bg": "#000000", "order": 1 }, - "flags": ["MONSTER", "BLOCKS_TILE"], + "flags": ["MONSTER", "BLOCKS_TILE", "MULTIATTACK"], "level": 5, "bac": 5, "vision_range": 8, @@ -153,7 +153,7 @@ "id": "horse_large", "name": "warhorse", "renderable": { "glyph": "u", "fg": "#8a3520", "bg": "#000000", "order": 1 }, - "flags": ["MONSTER", "BLOCKS_TILE"], + "flags": ["MONSTER", "BLOCKS_TILE", "MULTIATTACK"], "level": 7, "bac": 4, "vision_range": 8, diff --git a/raws/spawn_tables.json b/raws/spawn_tables.json index 15a5357..c35d381 100644 --- a/raws/spawn_tables.json +++ b/raws/spawn_tables.json @@ -77,7 +77,7 @@ { "id": "goblin_chieftain", "weight": 1, "difficulty": 3}, { "id": "ogre", "weight": 1, "difficulty": 4}, { "id": "horse_little", "weight": 2, "difficulty": 4}, - { "id": "dog", "weight": 1, "difficulty": 5}, + { "id": "dog", "weight": 1, "difficulty": 5}, { "id": "wolf", "weight": 2, "difficulty": 6}, { "id": "dog_large", "weight": 1, "difficulty": 7}, { "id": "horse", "weight": 2, "difficulty": 7}, diff --git a/src/components.rs b/src/components.rs index c17978f..2e6a503 100644 --- a/src/components.rs +++ b/src/components.rs @@ -320,3 +320,6 @@ pub struct EntryTrigger {} #[derive(Component, Debug, Serialize, Deserialize, Clone)] pub struct EntityMoved {} + +#[derive(Component, Debug, Serialize, Deserialize, Clone)] +pub struct MultiAttack {} diff --git a/src/main.rs b/src/main.rs index 8938009..88f9092 100644 --- a/src/main.rs +++ b/src/main.rs @@ -43,7 +43,7 @@ mod rex_assets; extern crate lazy_static; //Consts -pub const SHOW_MAPGEN: bool = true; +pub const SHOW_MAPGEN: bool = false; #[derive(PartialEq, Copy, Clone)] pub enum RunState { @@ -558,6 +558,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/melee_combat_system.rs b/src/melee_combat_system.rs index 3d03703..66a6bcb 100644 --- a/src/melee_combat_system.rs +++ b/src/melee_combat_system.rs @@ -1,6 +1,7 @@ use super::{ gamelog, gamesystem, ArmourClassBonus, Attributes, EquipmentSlot, Equipped, HungerClock, HungerState, MeleeWeapon, - Name, NaturalAttacks, ParticleBuilder, Pools, Position, Skill, Skills, SufferDamage, WantsToMelee, WeaponAttribute, + MultiAttack, Name, NaturalAttack, NaturalAttacks, ParticleBuilder, Pools, Position, Skill, Skills, SufferDamage, + WantsToMelee, WeaponAttribute, }; use specs::prelude::*; @@ -23,6 +24,7 @@ impl<'a> System<'a> for MeleeCombatSystem { ReadStorage<'a, NaturalAttacks>, ReadStorage<'a, ArmourClassBonus>, ReadStorage<'a, HungerClock>, + ReadStorage<'a, MultiAttack>, WriteExpect<'a, rltk::RandomNumberGenerator>, ); @@ -43,6 +45,7 @@ impl<'a> System<'a> for MeleeCombatSystem { natural_attacks, ac, hunger_clock, + multi_attackers, mut rng, ) = data; @@ -64,201 +67,214 @@ impl<'a> System<'a> for MeleeCombatSystem { 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; + // Create blank vector of attacks being attempted. + let mut attacks: Vec<(MeleeWeapon, String)> = Vec::new(); + let mut multi_attack = false; + // Check if attacker can multi-attack. + if let Some(_) = multi_attackers.get(entity) { + multi_attack = true; } - if target_pools.hit_points.current <= 0 { - break; - } - - let target_name = names.get(wants_melee.target).unwrap(); - - 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) { - 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; - } - } - + // Check if attacker is using a weapon. + let mut using_weapon = false; for (wielded, melee) in (&equipped, &melee_weapons).join() { if wielded.owner == entity && wielded.slot == EquipmentSlot::Melee { - weapon_info = melee.clone(); + using_weapon = get_weapon_attack(wielded, melee, entity, &mut attacks); } } - - // Get all offensive bonuses - let d20 = rng.roll_dice(1, 20); - let attribute_hit_bonus = attacker_attributes.dexterity.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 => { - status_hit_bonus += 1; - } - HungerState::Weak => { - status_hit_bonus -= 1; - } - HungerState::Fainting => { - status_hit_bonus -= 2; - } - _ => {} + // If not using a weapon, get natural attacks. If we + // are a multiattacker, get every natural attack. If + // not, just pick one at random. + if !using_weapon { + if let Some(nat) = natural_attacks.get(entity) { + get_natural_attacks(&mut rng, nat.clone(), multi_attack, &mut attacks); + } else { + attacks.push(( + MeleeWeapon { + attribute: WeaponAttribute::Strength, + hit_bonus: 0, + damage_n_dice: 1, + damage_die_type: 4, + damage_bonus: 0, + }, + "punches".to_string(), + )); } } - let attacker_bonuses = - attacker_pools.level + attribute_hit_bonus + skill_hit_bonus + weapon_hit_bonus + status_hit_bonus; - - // 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; + // For every attack, do combat calcs. Break if someone dies. + for attack in attacks { + 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; + } + if target_pools.hit_points.current <= 0 { + break; } - } - let actual_armour_class = bac - attribute_ac_bonus - skill_ac_bonus - armour_ac_bonus; - let mut armour_class_roll = actual_armour_class; - if actual_armour_class < 0 { - // Invert armour class so we can roll 1d(AC) - armour_class_roll = rng.roll_dice(1, -actual_armour_class); - // Invert result so it's a negative again - armour_class_roll = -armour_class_roll; - } + let weapon_info = attack.0; + let attack_verb = attack.1; - let target_number = 10 + armour_class_roll + attacker_bonuses; - - if COMBAT_LOGGING { - rltk::console::log(format!( - "ATTACKLOG: {} *{}* {}: rolled ({}) 1d20 vs. {} (10 + {}AC + {}to-hit)", - &name.name, attack_verb, &target_name.name, d20, target_number, armour_class_roll, attacker_bonuses - )); - } - - if d20 < target_number { - // Target hit! - let base_damage = rng.roll_dice(weapon_info.damage_n_dice, weapon_info.damage_die_type); - let skill_damage_bonus = gamesystem::skill_bonus(Skill::Melee, &*attacker_skills); - let mut attribute_damage_bonus = weapon_info.damage_bonus; - match weapon_info.attribute { - WeaponAttribute::Dexterity => attribute_damage_bonus += attacker_attributes.dexterity.bonus, - WeaponAttribute::Strength => attribute_damage_bonus += attacker_attributes.strength.bonus, - WeaponAttribute::Finesse => { - if attacker_attributes.dexterity.bonus > attacker_attributes.strength.bonus { - attribute_damage_bonus += attacker_attributes.dexterity.bonus; - } else { - attribute_damage_bonus += attacker_attributes.strength.bonus; + // Get all offensive bonuses + let d20 = rng.roll_dice(1, 20); + let attribute_hit_bonus = attacker_attributes.dexterity.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 => { + status_hit_bonus += 1; } + HungerState::Weak => { + status_hit_bonus -= 1; + } + HungerState::Fainting => { + status_hit_bonus -= 2; + } + _ => {} } } - let mut damage = i32::max(0, base_damage + skill_damage_bonus + attribute_damage_bonus); + let attacker_bonuses = + attacker_pools.level + attribute_hit_bonus + skill_hit_bonus + weapon_hit_bonus + status_hit_bonus; + // 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 actual_armour_class = bac - attribute_ac_bonus - skill_ac_bonus - armour_ac_bonus; + let mut armour_class_roll = actual_armour_class; + + if actual_armour_class < 0 { + // Invert armour class so we can roll 1d(AC) + armour_class_roll = rng.roll_dice(1, -actual_armour_class); + // Invert result so it's a negative again + armour_class_roll = -armour_class_roll; + } + + let target_number = 10 + armour_class_roll + attacker_bonuses; + + let target_name = names.get(wants_melee.target).unwrap(); if COMBAT_LOGGING { rltk::console::log(format!( - "ATTACKLOG: {} HIT for {} ({}[{}d{}]+{}[skill]+{}[attr])", + "ATTACKLOG: {} *{}* {}: rolled ({}) 1d20 vs. {} (10 + {}AC + {}to-hit)", &name.name, - damage, - base_damage, - weapon_info.damage_n_dice, - weapon_info.damage_die_type, - skill_damage_bonus, - attribute_damage_bonus + attack_verb, + &target_name.name, + d20, + target_number, + armour_class_roll, + attacker_bonuses )); } - if actual_armour_class < 0 { - let ac_damage_reduction = rng.roll_dice(1, -actual_armour_class); - damage = i32::min(1, damage - ac_damage_reduction); + if d20 < target_number { + // Target hit! + let base_damage = rng.roll_dice(weapon_info.damage_n_dice, weapon_info.damage_die_type); + let skill_damage_bonus = gamesystem::skill_bonus(Skill::Melee, &*attacker_skills); + let mut attribute_damage_bonus = weapon_info.damage_bonus; + match weapon_info.attribute { + WeaponAttribute::Dexterity => attribute_damage_bonus += attacker_attributes.dexterity.bonus, + WeaponAttribute::Strength => attribute_damage_bonus += attacker_attributes.strength.bonus, + WeaponAttribute::Finesse => { + if attacker_attributes.dexterity.bonus > attacker_attributes.strength.bonus { + attribute_damage_bonus += attacker_attributes.dexterity.bonus; + } else { + attribute_damage_bonus += attacker_attributes.strength.bonus; + } + } + } + let mut damage = i32::max(0, base_damage + skill_damage_bonus + attribute_damage_bonus); + if COMBAT_LOGGING { rltk::console::log(format!( - "ATTACKLOG: {} reduced their damage taken by {} (1dAC), and took {} hp damage.", - &target_name.name, ac_damage_reduction, damage + "ATTACKLOG: {} HIT for {} ({}[{}d{}]+{}[skill]+{}[attr])", + &name.name, + damage, + base_damage, + weapon_info.damage_n_dice, + weapon_info.damage_die_type, + skill_damage_bonus, + attribute_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, entity == *player_entity); - 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 { - if COMBAT_LOGGING { - rltk::console::log(format!("ATTACKLOG: {} *MISSED*", &name.name)); - } + if actual_armour_class < 0 { + let ac_damage_reduction = rng.roll_dice(1, -actual_armour_class); + damage = i32::min(1, damage - ac_damage_reduction); + if COMBAT_LOGGING { + rltk::console::log(format!( + "ATTACKLOG: {} reduced their damage taken by {} (1dAC), and took {} hp damage.", + &target_name.name, ac_damage_reduction, damage + )); + } + } - 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.") - .log(); - } else if wants_melee.target == *player_entity { - gamelog::Logger::new() // misses! - .append("The") - .npc_name(&name.name) - .colour(rltk::WHITE) - .append("misses!") - .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, entity == *player_entity); + 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 { - gamelog::Logger::new() // misses the . - .append("The") - .npc_name(&name.name) - .colour(rltk::WHITE) - .append("misses the") - .npc_name_n(&target_name.name) - .period() - .log(); + if COMBAT_LOGGING { + rltk::console::log(format!("ATTACKLOG: {} *MISSED*", &name.name)); + } + + 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.") + .log(); + } else if wants_melee.target == *player_entity { + gamelog::Logger::new() // misses! + .append("The") + .npc_name(&name.name) + .colour(rltk::WHITE) + .append("misses!") + .log(); + } else { + gamelog::Logger::new() // misses the . + .append("The") + .npc_name(&name.name) + .colour(rltk::WHITE) + .append("misses the") + .npc_name_n(&target_name.name) + .period() + .log(); + } } } } @@ -266,3 +282,53 @@ impl<'a> System<'a> for MeleeCombatSystem { wants_melee.clear(); } } + +fn get_natural_attacks( + rng: &mut rltk::RandomNumberGenerator, + nat: NaturalAttacks, + multi_attack: bool, + attacks: &mut Vec<(MeleeWeapon, String)>, +) { + if !nat.attacks.is_empty() { + if multi_attack { + for a in nat.attacks.iter() { + attacks.push(( + MeleeWeapon { + attribute: WeaponAttribute::Strength, + hit_bonus: a.hit_bonus, + damage_n_dice: a.damage_n_dice, + damage_die_type: a.damage_die_type, + damage_bonus: a.damage_bonus, + }, + a.name.to_string(), + )); + } + } else { + let attack_index = + if nat.attacks.len() == 1 { 0 } else { rng.roll_dice(1, nat.attacks.len() as i32) as usize - 1 }; + attacks.push(( + MeleeWeapon { + attribute: WeaponAttribute::Strength, + hit_bonus: nat.attacks[attack_index].hit_bonus, + damage_n_dice: nat.attacks[attack_index].damage_n_dice, + damage_die_type: nat.attacks[attack_index].damage_die_type, + damage_bonus: nat.attacks[attack_index].damage_bonus, + }, + nat.attacks[attack_index].name.to_string(), + )); + } + } +} + +fn get_weapon_attack( + wielded: &Equipped, + melee: &MeleeWeapon, + entity: Entity, + attacks: &mut Vec<(MeleeWeapon, String)>, +) -> bool { + if wielded.owner == entity && wielded.slot == EquipmentSlot::Melee { + attacks.push((melee.clone(), "hits".to_string())); + return true; + } + return false; +} diff --git a/src/raws/rawmaster.rs b/src/raws/rawmaster.rs index af4c0dd..50cce51 100644 --- a/src/raws/rawmaster.rs +++ b/src/raws/rawmaster.rs @@ -209,6 +209,7 @@ pub fn spawn_named_mob( "BLOCKS_TILE" => eb = eb.with(BlocksTile {}), "BYSTANDER" => eb = eb.with(Bystander {}), "MONSTER" => eb = eb.with(Monster {}), + "MULTIATTACK" => eb = eb.with(MultiAttack {}), _ => rltk::console::log(format!("Unrecognised flag: {}", flag.as_str())), } } diff --git a/src/saveload_system.rs b/src/saveload_system.rs index 6be50ae..1c693a6 100644 --- a/src/saveload_system.rs +++ b/src/saveload_system.rs @@ -73,6 +73,7 @@ pub fn save_game(ecs: &mut World) { MeleeWeapon, Mind, Monster, + MultiAttack, NaturalAttacks, Name, ParticleLifetime, @@ -175,6 +176,7 @@ pub fn load_game(ecs: &mut World) { MeleeWeapon, Mind, Monster, + MultiAttack, NaturalAttacks, Name, ParticleLifetime,