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", "id": "horse_little",
"name": "pony", "name": "pony",
"renderable": { "glyph": "u", "fg": "#b36c29", "bg": "#000000", "order": 1 }, "renderable": { "glyph": "u", "fg": "#b36c29", "bg": "#000000", "order": 1 },
"flags": ["BYSTANDER", "BLOCKS_TILE"], "flags": ["BYSTANDER", "BLOCKS_TILE", "MULTIATTACK"],
"level": 3, "level": 3,
"bac": 6, "bac": 6,
"vision_range": 8, "vision_range": 8,
@ -140,7 +140,7 @@
"id": "horse", "id": "horse",
"name": "horse", "name": "horse",
"renderable": { "glyph": "u", "fg": "#744d29", "bg": "#000000", "order": 1 }, "renderable": { "glyph": "u", "fg": "#744d29", "bg": "#000000", "order": 1 },
"flags": ["MONSTER", "BLOCKS_TILE"], "flags": ["MONSTER", "BLOCKS_TILE", "MULTIATTACK"],
"level": 5, "level": 5,
"bac": 5, "bac": 5,
"vision_range": 8, "vision_range": 8,
@ -153,7 +153,7 @@
"id": "horse_large", "id": "horse_large",
"name": "warhorse", "name": "warhorse",
"renderable": { "glyph": "u", "fg": "#8a3520", "bg": "#000000", "order": 1 }, "renderable": { "glyph": "u", "fg": "#8a3520", "bg": "#000000", "order": 1 },
"flags": ["MONSTER", "BLOCKS_TILE"], "flags": ["MONSTER", "BLOCKS_TILE", "MULTIATTACK"],
"level": 7, "level": 7,
"bac": 4, "bac": 4,
"vision_range": 8, "vision_range": 8,

View file

@ -320,3 +320,6 @@ pub struct EntryTrigger {}
#[derive(Component, Debug, Serialize, Deserialize, Clone)] #[derive(Component, Debug, Serialize, Deserialize, Clone)]
pub struct EntityMoved {} 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; extern crate lazy_static;
//Consts //Consts
pub const SHOW_MAPGEN: bool = true; pub const SHOW_MAPGEN: bool = false;
#[derive(PartialEq, Copy, Clone)] #[derive(PartialEq, Copy, Clone)]
pub enum RunState { pub enum RunState {
@ -558,6 +558,7 @@ fn main() -> rltk::BError {
gs.ecs.register::<Hidden>(); gs.ecs.register::<Hidden>();
gs.ecs.register::<EntryTrigger>(); gs.ecs.register::<EntryTrigger>();
gs.ecs.register::<EntityMoved>(); gs.ecs.register::<EntityMoved>();
gs.ecs.register::<MultiAttack>();
gs.ecs.register::<ParticleLifetime>(); gs.ecs.register::<ParticleLifetime>();
gs.ecs.register::<SimpleMarker<SerializeMe>>(); gs.ecs.register::<SimpleMarker<SerializeMe>>();
gs.ecs.register::<SerializationHelper>(); gs.ecs.register::<SerializationHelper>();

View file

@ -1,6 +1,7 @@
use super::{ use super::{
gamelog, gamesystem, ArmourClassBonus, Attributes, EquipmentSlot, Equipped, HungerClock, HungerState, MeleeWeapon, 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::*; use specs::prelude::*;
@ -23,6 +24,7 @@ impl<'a> System<'a> for MeleeCombatSystem {
ReadStorage<'a, NaturalAttacks>, ReadStorage<'a, NaturalAttacks>,
ReadStorage<'a, ArmourClassBonus>, ReadStorage<'a, ArmourClassBonus>,
ReadStorage<'a, HungerClock>, ReadStorage<'a, HungerClock>,
ReadStorage<'a, MultiAttack>,
WriteExpect<'a, rltk::RandomNumberGenerator>, WriteExpect<'a, rltk::RandomNumberGenerator>,
); );
@ -43,6 +45,7 @@ impl<'a> System<'a> for MeleeCombatSystem {
natural_attacks, natural_attacks,
ac, ac,
hunger_clock, hunger_clock,
multi_attackers,
mut rng, mut rng,
) = data; ) = data;
@ -64,201 +67,214 @@ impl<'a> System<'a> for MeleeCombatSystem {
for (entity, wants_melee, name, attacker_attributes, attacker_skills, attacker_pools) in for (entity, wants_melee, name, attacker_attributes, attacker_skills, attacker_pools) in
(&entities, &wants_melee, &names, &attributes, &skills, &pools).join() (&entities, &wants_melee, &names, &attributes, &skills, &pools).join()
{ {
let target_pools = pools.get(wants_melee.target).unwrap(); // Create blank vector of attacks being attempted.
let target_attributes = attributes.get(wants_melee.target).unwrap(); let mut attacks: Vec<(MeleeWeapon, String)> = Vec::new();
let target_skills = skills.get(wants_melee.target).unwrap(); let mut multi_attack = false;
// Check if attacker can multi-attack.
if attacker_pools.hit_points.current <= 0 { if let Some(_) = multi_attackers.get(entity) {
break; multi_attack = true;
} }
if target_pools.hit_points.current <= 0 { // Check if attacker is using a weapon.
break; let mut using_weapon = false;
}
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;
}
}
for (wielded, melee) in (&equipped, &melee_weapons).join() { for (wielded, melee) in (&equipped, &melee_weapons).join() {
if wielded.owner == entity && wielded.slot == EquipmentSlot::Melee { if wielded.owner == entity && wielded.slot == EquipmentSlot::Melee {
weapon_info = melee.clone(); using_weapon = get_weapon_attack(wielded, melee, entity, &mut attacks);
} }
} }
// If not using a weapon, get natural attacks. If we
// Get all offensive bonuses // are a multiattacker, get every natural attack. If
let d20 = rng.roll_dice(1, 20); // not, just pick one at random.
let attribute_hit_bonus = attacker_attributes.dexterity.bonus; if !using_weapon {
let skill_hit_bonus = gamesystem::skill_bonus(Skill::Melee, &*attacker_skills); if let Some(nat) = natural_attacks.get(entity) {
let weapon_hit_bonus = weapon_info.hit_bonus; get_natural_attacks(&mut rng, nat.clone(), multi_attack, &mut attacks);
let mut status_hit_bonus = 0; } else {
let hc = hunger_clock.get(entity); attacks.push((
if let Some(hc) = hc { MeleeWeapon {
match hc.state { attribute: WeaponAttribute::Strength,
HungerState::Satiated => { hit_bonus: 0,
status_hit_bonus += 1; damage_n_dice: 1,
} damage_die_type: 4,
HungerState::Weak => { damage_bonus: 0,
status_hit_bonus -= 1; },
} "punches".to_string(),
HungerState::Fainting => { ));
status_hit_bonus -= 2;
}
_ => {}
} }
} }
let attacker_bonuses = // For every attack, do combat calcs. Break if someone dies.
attacker_pools.level + attribute_hit_bonus + skill_hit_bonus + weapon_hit_bonus + status_hit_bonus; for attack in attacks {
let target_pools = pools.get(wants_melee.target).unwrap();
// Get armour class let target_attributes = attributes.get(wants_melee.target).unwrap();
let bac = target_pools.bac; let target_skills = skills.get(wants_melee.target).unwrap();
let attribute_ac_bonus = target_attributes.dexterity.bonus; if attacker_pools.hit_points.current <= 0 {
let skill_ac_bonus = gamesystem::skill_bonus(Skill::Defence, &*target_skills); break;
let mut armour_ac_bonus = 0; }
for (wielded, ac) in (&equipped, &ac).join() { if target_pools.hit_points.current <= 0 {
if wielded.owner == wants_melee.target { break;
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 { let weapon_info = attack.0;
// Invert armour class so we can roll 1d(AC) let attack_verb = attack.1;
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; // Get all offensive bonuses
let d20 = rng.roll_dice(1, 20);
if COMBAT_LOGGING { let attribute_hit_bonus = attacker_attributes.dexterity.bonus;
rltk::console::log(format!( let skill_hit_bonus = gamesystem::skill_bonus(Skill::Melee, &*attacker_skills);
"ATTACKLOG: {} *{}* {}: rolled ({}) 1d20 vs. {} (10 + {}AC + {}to-hit)", let weapon_hit_bonus = weapon_info.hit_bonus;
&name.name, attack_verb, &target_name.name, d20, target_number, armour_class_roll, attacker_bonuses let mut status_hit_bonus = 0;
)); let hc = hunger_clock.get(entity);
} if let Some(hc) = hc {
match hc.state {
if d20 < target_number { HungerState::Satiated => {
// Target hit! status_hit_bonus += 1;
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;
} }
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 { if COMBAT_LOGGING {
rltk::console::log(format!( rltk::console::log(format!(
"ATTACKLOG: {} HIT for {} ({}[{}d{}]+{}[skill]+{}[attr])", "ATTACKLOG: {} *{}* {}: rolled ({}) 1d20 vs. {} (10 + {}AC + {}to-hit)",
&name.name, &name.name,
damage, attack_verb,
base_damage, &target_name.name,
weapon_info.damage_n_dice, d20,
weapon_info.damage_die_type, target_number,
skill_damage_bonus, armour_class_roll,
attribute_damage_bonus attacker_bonuses
)); ));
} }
if actual_armour_class < 0 { if d20 < target_number {
let ac_damage_reduction = rng.roll_dice(1, -actual_armour_class); // Target hit!
damage = i32::min(1, damage - ac_damage_reduction); 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 { if COMBAT_LOGGING {
rltk::console::log(format!( rltk::console::log(format!(
"ATTACKLOG: {} reduced their damage taken by {} (1dAC), and took {} hp damage.", "ATTACKLOG: {} HIT for {} ({}[{}d{}]+{}[skill]+{}[attr])",
&target_name.name, ac_damage_reduction, damage &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 actual_armour_class < 0 {
if let Some(pos) = pos { let ac_damage_reduction = rng.roll_dice(1, -actual_armour_class);
particle_builder.damage_taken(pos.x, pos.y) damage = i32::min(1, damage - ac_damage_reduction);
} if COMBAT_LOGGING {
SufferDamage::new_damage(&mut inflict_damage, wants_melee.target, damage, entity == *player_entity); rltk::console::log(format!(
if entity == *player_entity { "ATTACKLOG: {} reduced their damage taken by {} (1dAC), and took {} hp damage.",
gamelog::Logger::new() // You hit the <name>. &target_name.name, ac_damage_reduction, damage
.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));
}
let pos = positions.get(wants_melee.target); let pos = positions.get(wants_melee.target);
if let Some(pos) = pos { if let Some(pos) = pos {
particle_builder.attack_miss(pos.x, pos.y) particle_builder.damage_taken(pos.x, pos.y)
} }
if entity == *player_entity { SufferDamage::new_damage(&mut inflict_damage, wants_melee.target, damage, entity == *player_entity);
gamelog::Logger::new() // You miss. if entity == *player_entity {
.append("You miss.") gamelog::Logger::new() // You hit the <name>.
.log(); .append("You hit the")
} else if wants_melee.target == *player_entity { .npc_name_n(&target_name.name)
gamelog::Logger::new() // <name> misses! .period()
.append("The") .log();
.npc_name(&name.name) } else if wants_melee.target == *player_entity {
.colour(rltk::WHITE) gamelog::Logger::new() // <name> hits you!
.append("misses!") .append("The")
.log(); .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 { } else {
gamelog::Logger::new() // <name> misses the <target>. if COMBAT_LOGGING {
.append("The") rltk::console::log(format!("ATTACKLOG: {} *MISSED*", &name.name));
.npc_name(&name.name) }
.colour(rltk::WHITE)
.append("misses the") let pos = positions.get(wants_melee.target);
.npc_name_n(&target_name.name) if let Some(pos) = pos {
.period() particle_builder.attack_miss(pos.x, pos.y)
.log(); }
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(); 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 {}), "BLOCKS_TILE" => eb = eb.with(BlocksTile {}),
"BYSTANDER" => eb = eb.with(Bystander {}), "BYSTANDER" => eb = eb.with(Bystander {}),
"MONSTER" => eb = eb.with(Monster {}), "MONSTER" => eb = eb.with(Monster {}),
"MULTIATTACK" => eb = eb.with(MultiAttack {}),
_ => rltk::console::log(format!("Unrecognised flag: {}", flag.as_str())), _ => rltk::console::log(format!("Unrecognised flag: {}", flag.as_str())),
} }
} }

View file

@ -73,6 +73,7 @@ pub fn save_game(ecs: &mut World) {
MeleeWeapon, MeleeWeapon,
Mind, Mind,
Monster, Monster,
MultiAttack,
NaturalAttacks, NaturalAttacks,
Name, Name,
ParticleLifetime, ParticleLifetime,
@ -175,6 +176,7 @@ pub fn load_game(ecs: &mut World) {
MeleeWeapon, MeleeWeapon,
Mind, Mind,
Monster, Monster,
MultiAttack,
NaturalAttacks, NaturalAttacks,
Name, Name,
ParticleLifetime, ParticleLifetime,