adds damage types and mods (weak/resist/immune), for all damage events

This commit is contained in:
Llywelwyn 2023-09-21 00:52:54 +01:00
parent 66013667d8
commit 8a44c94272
12 changed files with 131 additions and 10 deletions

View file

@ -1,6 +1,7 @@
## v0.1.4 ## v0.1.4
### added ### added
- **overmap**: bare, but exists. player now starts on the overworld, and can move to local maps (like the old starting town) via >. can leave local maps back to the overmap by walking out of the map boundaries. - **overmap**: bare, but exists. player now starts on the overworld, and can move to local maps (like the old starting town) via >. can leave local maps back to the overmap by walking out of the map boundaries.
- **damage types**: immunities, weaknesses, and resistances
- **full keyboard support**: examining and targeting can now be done via keyboard only - **full keyboard support**: examining and targeting can now be done via keyboard only
- **a config file** read at runtime, unfortunately not compatible with WASM builds yet - **a config file** read at runtime, unfortunately not compatible with WASM builds yet
- **morgue files**: y/n prompt to write a morgue file on death to /morgue/foo.txt, or to the console for WASM builds - **morgue files**: y/n prompt to write a morgue file on death to /morgue/foo.txt, or to the console for WASM builds

View file

@ -316,6 +316,7 @@ pub enum WeaponAttribute {
#[derive(Component, Serialize, Deserialize, Clone)] #[derive(Component, Serialize, Deserialize, Clone)]
pub struct MeleeWeapon { pub struct MeleeWeapon {
pub damage_type: DamageType,
pub attribute: WeaponAttribute, pub attribute: WeaponAttribute,
pub damage_n_dice: i32, pub damage_n_dice: i32,
pub damage_die_type: i32, pub damage_die_type: i32,
@ -326,6 +327,7 @@ pub struct MeleeWeapon {
#[derive(Serialize, Deserialize, Clone)] #[derive(Serialize, Deserialize, Clone)]
pub struct NaturalAttack { pub struct NaturalAttack {
pub name: String, pub name: String,
pub damage_type: DamageType,
pub damage_n_dice: i32, pub damage_n_dice: i32,
pub damage_die_type: i32, pub damage_die_type: i32,
pub damage_bonus: i32, pub damage_bonus: i32,
@ -365,8 +367,55 @@ pub struct ProvidesHealing {
pub modifier: i32, pub modifier: i32,
} }
#[derive(Debug, PartialEq, Eq, Hash, Copy, Clone, Serialize, Deserialize)]
pub enum DamageType {
Physical,
Magic,
Forced, // Bypasses any immunities. e.g. Hunger ticks.
}
impl DamageType {
pub fn is_magic(&self) -> bool {
match self {
DamageType::Magic => true,
_ => false,
}
}
}
#[derive(Debug, PartialEq, Copy, Clone, Serialize, Deserialize)]
pub enum DamageModifier {
None,
Weakness,
Resistance,
Immune,
}
impl DamageModifier {
pub fn multiplier(&self) -> f32 {
match self {
DamageModifier::None => 1.0,
DamageModifier::Weakness => 10.0,
DamageModifier::Resistance => 0.5,
DamageModifier::Immune => 0.0,
}
}
}
#[derive(Component, Serialize, Deserialize, Debug, Clone)]
pub struct HasDamageModifiers {
pub modifiers: HashMap<DamageType, DamageModifier>,
}
impl HasDamageModifiers {
pub fn modifier(&self, damage_type: &DamageType) -> &DamageModifier {
self.modifiers.get(damage_type).unwrap_or(&DamageModifier::None)
}
}
#[derive(Component, Debug, ConvertSaveload, Clone)] #[derive(Component, Debug, ConvertSaveload, Clone)]
pub struct InflictsDamage { pub struct InflictsDamage {
pub damage_type: DamageType,
pub n_dice: i32, pub n_dice: i32,
pub sides: i32, pub sides: i32,
pub modifier: i32, pub modifier: i32,

View file

@ -14,6 +14,7 @@ use crate::{
HungerClock, HungerClock,
HungerState, HungerState,
Bleeds, Bleeds,
HasDamageModifiers,
}; };
use crate::gui::with_article; use crate::gui::with_article;
use crate::data::visuals::{ DEFAULT_PARTICLE_LIFETIME, LONG_PARTICLE_LIFETIME }; use crate::data::visuals::{ DEFAULT_PARTICLE_LIFETIME, LONG_PARTICLE_LIFETIME };
@ -27,8 +28,15 @@ pub fn inflict_damage(ecs: &mut World, damage: &EffectSpawner, target: Entity) {
let mut pools = ecs.write_storage::<Pools>(); let mut pools = ecs.write_storage::<Pools>();
if let Some(target_pool) = pools.get_mut(target) { if let Some(target_pool) = pools.get_mut(target) {
if !target_pool.god { if !target_pool.god {
if let EffectType::Damage { amount } = damage.effect_type { if let EffectType::Damage { amount, damage_type } = damage.effect_type {
target_pool.hit_points.current -= amount; let mult = if
let Some(modifiers) = ecs.read_storage::<HasDamageModifiers>().get(target)
{
modifiers.modifier(&damage_type).multiplier()
} else {
1.0
};
target_pool.hit_points.current -= ((amount as f32) * mult) as i32;
let bleeders = ecs.read_storage::<Bleeds>(); let bleeders = ecs.read_storage::<Bleeds>();
if let Some(bleeds) = bleeders.get(target) { if let Some(bleeds) = bleeders.get(target) {
add_effect( add_effect(

View file

@ -4,6 +4,7 @@ use bracket_lib::prelude::*;
use specs::prelude::*; use specs::prelude::*;
use std::collections::VecDeque; use std::collections::VecDeque;
use std::sync::Mutex; use std::sync::Mutex;
use crate::components::DamageType;
mod damage; mod damage;
mod hunger; mod hunger;
@ -24,6 +25,7 @@ lazy_static! {
pub enum EffectType { pub enum EffectType {
Damage { Damage {
amount: i32, amount: i32,
damage_type: DamageType,
}, },
Healing { Healing {
amount: i32, amount: i32,

View file

@ -246,7 +246,11 @@ fn handle_damage(
if let Some(damage_item) = ecs.read_storage::<InflictsDamage>().get(event.entity) { if let Some(damage_item) = ecs.read_storage::<InflictsDamage>().get(event.entity) {
let mut rng = ecs.write_resource::<RandomNumberGenerator>(); let mut rng = ecs.write_resource::<RandomNumberGenerator>();
let roll = rng.roll_dice(damage_item.n_dice, damage_item.sides) + damage_item.modifier; let roll = rng.roll_dice(damage_item.n_dice, damage_item.sides) + damage_item.modifier;
add_effect(event.source, EffectType::Damage { amount: roll }, event.target.clone()); add_effect(
event.source,
EffectType::Damage { amount: roll, damage_type: damage_item.damage_type },
event.target.clone()
);
for target in get_entity_targets(&event.target) { for target in get_entity_targets(&event.target) {
if ecs.read_storage::<Prop>().get(target).is_some() { if ecs.read_storage::<Prop>().get(target).is_some() {
continue; continue;

View file

@ -5,6 +5,7 @@ use super::{
HungerClock, HungerClock,
HungerState, HungerState,
TakingTurn, TakingTurn,
DamageType,
}; };
use bracket_lib::prelude::*; use bracket_lib::prelude::*;
use specs::prelude::*; use specs::prelude::*;
@ -78,7 +79,7 @@ impl<'a> System<'a> for HungerSystem {
if hunger_clock.state == HungerState::Starving { if hunger_clock.state == HungerState::Starving {
add_effect( add_effect(
None, None,
EffectType::Damage { amount: 1 }, EffectType::Damage { amount: 1, damage_type: DamageType::Forced },
Targets::Entity { target: entity } Targets::Entity { target: entity }
); );
} }

View file

@ -109,6 +109,7 @@ fn main() -> BError {
gs.ecs.register::<SpawnParticleSimple>(); gs.ecs.register::<SpawnParticleSimple>();
gs.ecs.register::<SpawnParticleBurst>(); gs.ecs.register::<SpawnParticleBurst>();
gs.ecs.register::<SpawnParticleLine>(); gs.ecs.register::<SpawnParticleLine>();
gs.ecs.register::<HasDamageModifiers>();
gs.ecs.register::<SimpleMarker<SerializeMe>>(); gs.ecs.register::<SimpleMarker<SerializeMe>>();
gs.ecs.register::<SerializationHelper>(); gs.ecs.register::<SerializationHelper>();
gs.ecs.register::<DMSerializationHelper>(); gs.ecs.register::<DMSerializationHelper>();

View file

@ -123,6 +123,7 @@ impl<'a> System<'a> for MeleeCombatSystem {
} else { } else {
attacks.push(( attacks.push((
MeleeWeapon { MeleeWeapon {
damage_type: crate::DamageType::Physical,
attribute: WeaponAttribute::Strength, attribute: WeaponAttribute::Strength,
damage_n_dice: 1, damage_n_dice: 1,
damage_die_type: 4, damage_die_type: 4,
@ -301,7 +302,7 @@ impl<'a> System<'a> for MeleeCombatSystem {
} }
add_effect( add_effect(
Some(entity), Some(entity),
EffectType::Damage { amount: damage }, EffectType::Damage { amount: damage, damage_type: weapon_info.damage_type },
Targets::Entity { target: wants_melee.target } Targets::Entity { target: wants_melee.target }
); );
if entity == *player_entity { if entity == *player_entity {
@ -392,6 +393,7 @@ fn get_natural_attacks(
for a in nat.attacks.iter() { for a in nat.attacks.iter() {
attacks.push(( attacks.push((
MeleeWeapon { MeleeWeapon {
damage_type: a.damage_type,
attribute: WeaponAttribute::Strength, attribute: WeaponAttribute::Strength,
hit_bonus: a.hit_bonus, hit_bonus: a.hit_bonus,
damage_n_dice: a.damage_n_dice, damage_n_dice: a.damage_n_dice,
@ -409,6 +411,7 @@ fn get_natural_attacks(
}; };
attacks.push(( attacks.push((
MeleeWeapon { MeleeWeapon {
damage_type: nat.attacks[attack_index].damage_type,
attribute: WeaponAttribute::Strength, attribute: WeaponAttribute::Strength,
hit_bonus: nat.attacks[attack_index].hit_bonus, hit_bonus: nat.attacks[attack_index].hit_bonus,
damage_n_dice: nat.attacks[attack_index].damage_n_dice, damage_n_dice: nat.attacks[attack_index].damage_n_dice,

View file

@ -32,6 +32,7 @@ use super::{
WantsToPickupItem, WantsToPickupItem,
get_dest, get_dest,
Destination, Destination,
DamageType,
}; };
use bracket_lib::prelude::*; use bracket_lib::prelude::*;
use specs::prelude::*; use specs::prelude::*;
@ -290,7 +291,7 @@ pub fn kick(i: i32, j: i32, ecs: &mut World) -> RunState {
if rng.roll_dice(1, 20) == 20 { if rng.roll_dice(1, 20) == 20 {
add_effect( add_effect(
None, None,
EffectType::Damage { amount: 1 }, EffectType::Damage { amount: 1, damage_type: DamageType::Physical },
Targets::Entity { target: entity } Targets::Entity { target: entity }
); );
gamelog::Logger gamelog::Logger

View file

@ -25,8 +25,8 @@ macro_rules! apply_effects {
} }
"ranged" => $eb = $eb.with(Ranged { range: effect.1.parse::<i32>().unwrap() }), "ranged" => $eb = $eb.with(Ranged { range: effect.1.parse::<i32>().unwrap() }),
"damage" => { "damage" => {
let (n_dice, sides, modifier) = parse_dice_string(effect.1.as_str()); let (damage_type, dice) = parse_damage_string(effect.1.as_str());
$eb = $eb.with(InflictsDamage { n_dice, sides, modifier }) $eb = $eb.with(InflictsDamage { damage_type, n_dice: dice.0, sides: dice.1, modifier: dice.2 })
} }
"aoe" => $eb = $eb.with(AOE { radius: effect.1.parse::<i32>().unwrap() }), "aoe" => $eb = $eb.with(AOE { radius: effect.1.parse::<i32>().unwrap() }),
"confusion" => $eb = $eb.with(Confusion { turns: effect.1.parse::<i32>().unwrap() }), "confusion" => $eb = $eb.with(Confusion { turns: effect.1.parse::<i32>().unwrap() }),
@ -45,6 +45,7 @@ macro_rules! apply_effects {
/// flags are components that have no parameters to modify. /// flags are components that have no parameters to modify.
macro_rules! apply_flags { macro_rules! apply_flags {
($flags:expr, $eb:expr) => { ($flags:expr, $eb:expr) => {
let mut damage_modifiers: HashMap<DamageType, DamageModifier> = HashMap::new();
for flag in $flags.iter() { for flag in $flags.iter() {
match flag.as_str() { match flag.as_str() {
// --- PROP FLAGS BEGIN HERE --- // --- PROP FLAGS BEGIN HERE ---
@ -89,6 +90,13 @@ macro_rules! apply_flags {
"NEUTRAL" => $eb = $eb.with(Faction { name: "neutral".to_string() }), "NEUTRAL" => $eb = $eb.with(Faction { name: "neutral".to_string() }),
"HERBIVORE" => $eb = $eb.with(Faction { name: "herbivore".to_string() }), "HERBIVORE" => $eb = $eb.with(Faction { name: "herbivore".to_string() }),
"CARNIVORE" => $eb = $eb.with(Faction { name: "carnivore".to_string() }), "CARNIVORE" => $eb = $eb.with(Faction { name: "carnivore".to_string() }),
// --- DAMAGE MODIFIERS ---
"MAGIC_IMMUNITY" => { damage_modifiers.insert(DamageType::Magic, DamageModifier::Immune); }
"MAGIC_WEAKNESS" => { damage_modifiers.insert(DamageType::Magic, DamageModifier::Weakness); }
"MAGIC_RESISTANCE" => { damage_modifiers.insert(DamageType::Magic, DamageModifier::Resistance); }
"PHYSICAL_IMMUNITY" => { damage_modifiers.insert(DamageType::Physical, DamageModifier::Immune); }
"PHYSICAL_WEAKNESS" => { damage_modifiers.insert(DamageType::Physical, DamageModifier::Weakness); }
"PHYSICAL_RESISTANCE" => { damage_modifiers.insert(DamageType::Physical, DamageModifier::Resistance); }
// --- MOVEMENT MODES --- ( defaults to WANDER ) // --- MOVEMENT MODES --- ( defaults to WANDER )
"STATIC" => $eb = $eb.with(MoveMode { mode: Movement::Static }), "STATIC" => $eb = $eb.with(MoveMode { mode: Movement::Static }),
"RANDOM_PATH" => $eb = $eb.with(MoveMode { mode: Movement::RandomWaypoint { path: None } }), "RANDOM_PATH" => $eb = $eb.with(MoveMode { mode: Movement::RandomWaypoint { path: None } }),
@ -102,6 +110,9 @@ macro_rules! apply_flags {
_ => console::log(format!("Unrecognised flag: {}", flag.as_str())), _ => console::log(format!("Unrecognised flag: {}", flag.as_str())),
} }
} }
if damage_modifiers.len() > 0 {
$eb = $eb.with(HasDamageModifiers { modifiers: damage_modifiers });
}
}; };
} }
@ -340,13 +351,16 @@ pub fn spawn_named_item(
} }
if let Some(weapon) = &item_template.equip { if let Some(weapon) = &item_template.equip {
let (n_dice, die_type, bonus) = parse_dice_string(weapon.damage.as_str()); let (damage_type, (n_dice, die_type, bonus)) = parse_damage_string(
weapon.damage.as_str()
);
let weapon_attribute = match weapon.flag.as_str() { let weapon_attribute = match weapon.flag.as_str() {
"DEXTERITY" => WeaponAttribute::Dexterity, "DEXTERITY" => WeaponAttribute::Dexterity,
"FINESSE" => WeaponAttribute::Finesse, "FINESSE" => WeaponAttribute::Finesse,
_ => WeaponAttribute::Strength, _ => WeaponAttribute::Strength,
}; };
let wpn = MeleeWeapon { let wpn = MeleeWeapon {
damage_type,
attribute: weapon_attribute, attribute: weapon_attribute,
damage_n_dice: n_dice, damage_n_dice: n_dice,
damage_die_type: die_type, damage_die_type: die_type,
@ -526,9 +540,10 @@ pub fn spawn_named_mob(
if let Some(natural_attacks) = &mob_template.attacks { if let Some(natural_attacks) = &mob_template.attacks {
let mut natural = NaturalAttacks { attacks: Vec::new() }; let mut natural = NaturalAttacks { attacks: Vec::new() };
for na in natural_attacks.iter() { for na in natural_attacks.iter() {
let (n, d, b) = parse_dice_string(&na.damage); let (damage_type, (n, d, b)) = parse_damage_string(&na.damage);
let attack = NaturalAttack { let attack = NaturalAttack {
name: na.name.clone(), name: na.name.clone(),
damage_type,
hit_bonus: na.hit_bonus, hit_bonus: na.hit_bonus,
damage_n_dice: n, damage_n_dice: n,
damage_die_type: d, damage_die_type: d,
@ -1039,3 +1054,18 @@ fn parse_particle_burst(n: &str) -> SpawnParticleBurst {
trail_lifetime_ms: tokens[7].parse::<f32>().unwrap(), trail_lifetime_ms: tokens[7].parse::<f32>().unwrap(),
} }
} }
fn parse_damage_string(n: &str) -> (DamageType, (i32, i32, i32)) {
let tokens: Vec<_> = n.split(';').collect();
let damage_type = if tokens.len() > 1 {
match tokens[1] {
"physical" => DamageType::Physical,
"magic" => DamageType::Magic,
_ => panic!("Unrecognised damage type in raws: {}", tokens[1]),
}
} else {
DamageType::Physical
};
let dice = parse_dice_string(tokens[0]);
return (damage_type, dice);
}

View file

@ -88,6 +88,7 @@ pub fn save_game(ecs: &mut World) {
GrantsXP, GrantsXP,
HasAncestry, HasAncestry,
HasClass, HasClass,
HasDamageModifiers,
Hidden, Hidden,
HungerClock, HungerClock,
IdentifiedBeatitude, IdentifiedBeatitude,
@ -218,6 +219,7 @@ pub fn load_game(ecs: &mut World) {
GrantsXP, GrantsXP,
HasAncestry, HasAncestry,
HasClass, HasClass,
HasDamageModifiers,
Hidden, Hidden,
HungerClock, HungerClock,
IdentifiedBeatitude, IdentifiedBeatitude,

19
tests/components_test.rs Normal file
View file

@ -0,0 +1,19 @@
// tests/components_test.rs
use rust_rl::components::*;
#[test]
fn damagetype_equality() {
let dt1 = DamageType::Physical;
let dt2 = DamageType::Physical;
assert_eq!(dt1, dt2);
let dt3 = DamageType::Magic;
assert_ne!(dt1, dt3);
}
#[test]
fn damagetype_ismagic() {
let dt1 = DamageType::Physical;
let dt2 = DamageType::Magic;
assert!(!dt1.is_magic());
assert!(dt2.is_magic());
}