combat system 2nd pass - multiattacks for natural attackers

This commit is contained in:
Llywelwyn 2023-07-30 05:30:01 +01:00
parent 42113ad6a4
commit a17a2c8f11
7 changed files with 247 additions and 174 deletions

View file

@ -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,

View file

@ -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 {}

View file

@ -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::<Hidden>();
gs.ecs.register::<EntryTrigger>();
gs.ecs.register::<EntityMoved>();
gs.ecs.register::<MultiAttack>();
gs.ecs.register::<ParticleLifetime>();
gs.ecs.register::<SimpleMarker<SerializeMe>>();
gs.ecs.register::<SerializationHelper>();

View file

@ -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 <name>.
.append("You hit the")
.npc_name_n(&target_name.name)
.period()
.log();
} else if wants_melee.target == *player_entity {
gamelog::Logger::new() // <name> hits you!
.append("The")
.npc_name(&name.name)
.append(attack_verb)
.append("you!")
.log();
} else {
gamelog::Logger::new() // <name> misses the <target>.
.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() // <name> 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 <name>.
.append("You hit the")
.npc_name_n(&target_name.name)
.period()
.log();
} else if wants_melee.target == *player_entity {
gamelog::Logger::new() // <name> hits you!
.append("The")
.npc_name(&name.name)
.append(attack_verb)
.append("you!")
.log();
} else {
gamelog::Logger::new() // <name> misses the <target>.
.append("The")
.npc_name(&name.name)
.append(attack_verb)
.append("the")
.npc_name_n(&target_name.name)
.period()
.log();
}
} else {
gamelog::Logger::new() // <name> misses the <target>.
.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() // <name> misses!
.append("The")
.npc_name(&name.name)
.colour(rltk::WHITE)
.append("misses!")
.log();
} else {
gamelog::Logger::new() // <name> misses the <target>.
.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;
}

View file

@ -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())),
}
}

View file

@ -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,