diff --git a/changelog.md b/changelog.md index 82b9191..d4beb21 100644 --- a/changelog.md +++ b/changelog.md @@ -1,6 +1,7 @@ ## v0.1.4 ### 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. +- **damage types**: immunities, weaknesses, and resistances - **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 - **morgue files**: y/n prompt to write a morgue file on death to /morgue/foo.txt, or to the console for WASM builds diff --git a/src/components.rs b/src/components.rs index ee58646..bb76a93 100644 --- a/src/components.rs +++ b/src/components.rs @@ -316,6 +316,7 @@ pub enum WeaponAttribute { #[derive(Component, Serialize, Deserialize, Clone)] pub struct MeleeWeapon { + pub damage_type: DamageType, pub attribute: WeaponAttribute, pub damage_n_dice: i32, pub damage_die_type: i32, @@ -326,6 +327,7 @@ pub struct MeleeWeapon { #[derive(Serialize, Deserialize, Clone)] pub struct NaturalAttack { pub name: String, + pub damage_type: DamageType, pub damage_n_dice: i32, pub damage_die_type: i32, pub damage_bonus: i32, @@ -365,8 +367,55 @@ pub struct ProvidesHealing { 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, +} + +impl HasDamageModifiers { + pub fn modifier(&self, damage_type: &DamageType) -> &DamageModifier { + self.modifiers.get(damage_type).unwrap_or(&DamageModifier::None) + } +} + #[derive(Component, Debug, ConvertSaveload, Clone)] pub struct InflictsDamage { + pub damage_type: DamageType, pub n_dice: i32, pub sides: i32, pub modifier: i32, diff --git a/src/effects/damage.rs b/src/effects/damage.rs index 74ce8f5..80885e2 100644 --- a/src/effects/damage.rs +++ b/src/effects/damage.rs @@ -14,6 +14,7 @@ use crate::{ HungerClock, HungerState, Bleeds, + HasDamageModifiers, }; use crate::gui::with_article; 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::(); if let Some(target_pool) = pools.get_mut(target) { if !target_pool.god { - if let EffectType::Damage { amount } = damage.effect_type { - target_pool.hit_points.current -= amount; + if let EffectType::Damage { amount, damage_type } = damage.effect_type { + let mult = if + let Some(modifiers) = ecs.read_storage::().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::(); if let Some(bleeds) = bleeders.get(target) { add_effect( diff --git a/src/effects/mod.rs b/src/effects/mod.rs index 2527e28..c23b52b 100644 --- a/src/effects/mod.rs +++ b/src/effects/mod.rs @@ -4,6 +4,7 @@ use bracket_lib::prelude::*; use specs::prelude::*; use std::collections::VecDeque; use std::sync::Mutex; +use crate::components::DamageType; mod damage; mod hunger; @@ -24,6 +25,7 @@ lazy_static! { pub enum EffectType { Damage { amount: i32, + damage_type: DamageType, }, Healing { amount: i32, diff --git a/src/effects/triggers.rs b/src/effects/triggers.rs index 7b98241..ec64295 100644 --- a/src/effects/triggers.rs +++ b/src/effects/triggers.rs @@ -246,7 +246,11 @@ fn handle_damage( if let Some(damage_item) = ecs.read_storage::().get(event.entity) { let mut rng = ecs.write_resource::(); 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) { if ecs.read_storage::().get(target).is_some() { continue; diff --git a/src/hunger_system.rs b/src/hunger_system.rs index a0536a4..95d8dc3 100644 --- a/src/hunger_system.rs +++ b/src/hunger_system.rs @@ -5,6 +5,7 @@ use super::{ HungerClock, HungerState, TakingTurn, + DamageType, }; use bracket_lib::prelude::*; use specs::prelude::*; @@ -78,7 +79,7 @@ impl<'a> System<'a> for HungerSystem { if hunger_clock.state == HungerState::Starving { add_effect( None, - EffectType::Damage { amount: 1 }, + EffectType::Damage { amount: 1, damage_type: DamageType::Forced }, Targets::Entity { target: entity } ); } diff --git a/src/main.rs b/src/main.rs index 53f6943..453d1fa 100644 --- a/src/main.rs +++ b/src/main.rs @@ -109,6 +109,7 @@ fn main() -> 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 0acd974..cc63807 100644 --- a/src/melee_combat_system.rs +++ b/src/melee_combat_system.rs @@ -123,6 +123,7 @@ impl<'a> System<'a> for MeleeCombatSystem { } else { attacks.push(( MeleeWeapon { + damage_type: crate::DamageType::Physical, attribute: WeaponAttribute::Strength, damage_n_dice: 1, damage_die_type: 4, @@ -301,7 +302,7 @@ impl<'a> System<'a> for MeleeCombatSystem { } add_effect( Some(entity), - EffectType::Damage { amount: damage }, + EffectType::Damage { amount: damage, damage_type: weapon_info.damage_type }, Targets::Entity { target: wants_melee.target } ); if entity == *player_entity { @@ -392,6 +393,7 @@ fn get_natural_attacks( for a in nat.attacks.iter() { attacks.push(( MeleeWeapon { + damage_type: a.damage_type, attribute: WeaponAttribute::Strength, hit_bonus: a.hit_bonus, damage_n_dice: a.damage_n_dice, @@ -409,6 +411,7 @@ fn get_natural_attacks( }; attacks.push(( MeleeWeapon { + damage_type: nat.attacks[attack_index].damage_type, attribute: WeaponAttribute::Strength, hit_bonus: nat.attacks[attack_index].hit_bonus, damage_n_dice: nat.attacks[attack_index].damage_n_dice, diff --git a/src/player.rs b/src/player.rs index 1849b75..dd8ecf3 100644 --- a/src/player.rs +++ b/src/player.rs @@ -32,6 +32,7 @@ use super::{ WantsToPickupItem, get_dest, Destination, + DamageType, }; use bracket_lib::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 { add_effect( None, - EffectType::Damage { amount: 1 }, + EffectType::Damage { amount: 1, damage_type: DamageType::Physical }, Targets::Entity { target: entity } ); gamelog::Logger diff --git a/src/raws/rawmaster.rs b/src/raws/rawmaster.rs index aaabfd1..31a5a1f 100644 --- a/src/raws/rawmaster.rs +++ b/src/raws/rawmaster.rs @@ -25,8 +25,8 @@ macro_rules! apply_effects { } "ranged" => $eb = $eb.with(Ranged { range: effect.1.parse::().unwrap() }), "damage" => { - let (n_dice, sides, modifier) = parse_dice_string(effect.1.as_str()); - $eb = $eb.with(InflictsDamage { n_dice, sides, modifier }) + let (damage_type, dice) = parse_damage_string(effect.1.as_str()); + $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::().unwrap() }), "confusion" => $eb = $eb.with(Confusion { turns: effect.1.parse::().unwrap() }), @@ -45,6 +45,7 @@ macro_rules! apply_effects { /// flags are components that have no parameters to modify. macro_rules! apply_flags { ($flags:expr, $eb:expr) => { + let mut damage_modifiers: HashMap = HashMap::new(); for flag in $flags.iter() { match flag.as_str() { // --- PROP FLAGS BEGIN HERE --- @@ -89,6 +90,13 @@ macro_rules! apply_flags { "NEUTRAL" => $eb = $eb.with(Faction { name: "neutral".to_string() }), "HERBIVORE" => $eb = $eb.with(Faction { name: "herbivore".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 ) "STATIC" => $eb = $eb.with(MoveMode { mode: Movement::Static }), "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())), } } + 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 { - 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() { "DEXTERITY" => WeaponAttribute::Dexterity, "FINESSE" => WeaponAttribute::Finesse, _ => WeaponAttribute::Strength, }; let wpn = MeleeWeapon { + damage_type, attribute: weapon_attribute, damage_n_dice: n_dice, damage_die_type: die_type, @@ -526,9 +540,10 @@ pub fn spawn_named_mob( if let Some(natural_attacks) = &mob_template.attacks { let mut natural = NaturalAttacks { attacks: Vec::new() }; 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 { name: na.name.clone(), + damage_type, hit_bonus: na.hit_bonus, damage_n_dice: n, damage_die_type: d, @@ -1039,3 +1054,18 @@ fn parse_particle_burst(n: &str) -> SpawnParticleBurst { trail_lifetime_ms: tokens[7].parse::().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); +} diff --git a/src/saveload_system.rs b/src/saveload_system.rs index aa6576c..1b4ba57 100644 --- a/src/saveload_system.rs +++ b/src/saveload_system.rs @@ -88,6 +88,7 @@ pub fn save_game(ecs: &mut World) { GrantsXP, HasAncestry, HasClass, + HasDamageModifiers, Hidden, HungerClock, IdentifiedBeatitude, @@ -218,6 +219,7 @@ pub fn load_game(ecs: &mut World) { GrantsXP, HasAncestry, HasClass, + HasDamageModifiers, Hidden, HungerClock, IdentifiedBeatitude, diff --git a/tests/components_test.rs b/tests/components_test.rs new file mode 100644 index 0000000..6c7789e --- /dev/null +++ b/tests/components_test.rs @@ -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()); +}