#![allow(clippy::nonstandard_macro_braces)] //tmp as of false positive !? use crate::{ combat::{ AttackEffect, CombatBuff, CombatBuffStrength, CombatEffect, CombatRequirement, DamagedEffect, }, comp::{aura::AuraKey, Health, Stats}, resources::{Secs, Time}, uid::Uid, }; use core::cmp::Ordering; use hashbrown::HashMap; use itertools::Either; use serde::{Deserialize, Serialize}; use specs::{Component, DerefFlaggedStorage, VecStorage}; use strum::EnumIter; use super::Body; /// De/buff Kind. /// This is used to determine what effects a buff will have #[derive( Clone, Copy, PartialEq, Eq, Hash, Debug, Serialize, Deserialize, PartialOrd, Ord, EnumIter, )] pub enum BuffKind { // Buffs /// Restores health/time for some period. /// Strength should be the healing per second. Regeneration, /// Restores health/time for some period for consumables. /// Strength should be the healing per second. Saturation, /// Applied when drinking a potion. /// Strength should be the healing per second. Potion, /// Applied when sitting at a campfire. /// Strength is fraction of health restored per second. CampfireHeal, /// Restores energy/time for some period. /// Strength should be the healing per second. EnergyRegen, /// Raises maximum energy. /// Strength should be 10x the effect to max energy. IncreaseMaxEnergy, /// Raises maximum health. /// Strength should be the effect to max health. IncreaseMaxHealth, /// Makes you immune to attacks. /// Strength does not affect this buff. Invulnerability, /// Reduces incoming damage. /// Strength scales the damage reduction non-linearly. 0.5 provides 50% DR, /// 1.0 provides 67% DR. ProtectingWard, /// Increases movement speed and gives health regeneration. /// Strength scales the movement speed linearly. 0.5 is 150% speed, 1.0 is /// 200% speed. Provides regeneration at 10x the value of the strength. Frenzied, /// Increases movement and attack speed, but removes chance to get critical /// hits. Strength scales strength of both effects linearly. 0.5 is a /// 50% increase, 1.0 is a 100% increase. Hastened, /// Increases resistance to incoming poise, and poise damage dealt as health /// is lost from the time the buff activated. /// Strength scales the resistance non-linearly. 0.5 provides 50%, 1.0 /// provides 67%. /// Strength scales the poise damage increase linearly, a strength of 1.0 /// and n health less from activation will cause poise damage to increase by /// n%. Fortitude, /// Increases both attack damage and vulnerability to damage. /// Damage increases linearly with strength, 1.0 is a 100% increase. /// Damage reduction decreases linearly with strength, 1.0 is a 100% /// decrease. Reckless, /// Provides immunity to burning and increases movement speed in lava. /// Movement speed increases linearly with strength, 1.0 is a 100% increase. // SalamanderAspect, TODO: Readd in second dwarven mine MR /// Your attacks cause targets to receive the burning debuff /// Strength of burning debuff is a fraction of the damage, fraction /// increases linearly with strength Flame, /// Your attacks cause targets to receive the frozen debuff /// Strength of frozen debuff is equal to the strength of this buff Frigid, /// Your attacks have lifesteal /// Strength increases the fraction of damage restored as life Lifesteal, /// Your attacks against bleeding targets have lifesteal /// Strength increases the fraction of damage restored as life Bloodfeast, /// Guarantees that the next attack is a critical hit. Does this kind of /// hackily by adding 100% to the crit, will need to be adjusted if we ever /// allow double crits instead of treating 100 as a ceiling. ImminentCritical, /// Increases combo gain, every 1 strength increases combo per strike by 1, /// rounds to nearest integer Fury, /// Allows attacks to ignore DR and increases energy reward /// DR penetration is non-linear, 0.5 is 50% penetration and 1.0 is a 67% /// penetration. Energy reward is increased linearly to strength, 1.0 is a /// 200 % increase. Sunderer, /// Increases damage resistance and poise resistance, causes combo to be /// generated when damaged, and decreases movement speed. /// Damage resistance increases non-linearly with strength, 0.5 is 50% and /// 1.0 is 67%. Poise resistance increases non-linearly with strength, 0.5 /// is 50% and 1.0 is 67%. Movement speed decreases linearly with strength, /// 0.5 is 50% and 1.0 is 33%. Combo generation is linear with strength, 1.0 /// is 5 combo generated on being hit. Defiance, /// Increases both attack damage, vulnerability to damage, attack speed, and /// movement speed Damage increases linearly with strength, 1.0 is a /// 100% increase. Damage reduction decreases linearly with strength, /// 1.0 is a 100% Attack speed increases non-linearly with strength, 0.5 /// is a 25% increase, 1.0 is a 33% increase Movement speed increases /// non-linearly with strength, 0.5 is a 12.5% increase, 1.0 is a 16.7% /// increase decrease. Berserk, // Debuffs /// Does damage to a creature over time. /// Strength should be the DPS of the debuff. Burning, /// Lowers health over time for some duration. /// Strength should be the DPS of the debuff. Bleeding, /// Lower a creature's max health over time. /// Strength only affects the target max health, 0.5 targets 50% of base /// max, 1.0 targets 100% of base max. Cursed, /// Reduces movement speed and causes bleeding damage. /// Strength scales the movement speed debuff non-linearly. 0.5 is 50% /// speed, 1.0 is 33% speed. Bleeding is at 4x the value of the strength. Crippled, /// Slows movement and attack speed. /// Strength scales the attack speed debuff non-linearly. 0.5 is ~50% /// speed, 1.0 is 33% speed. Movement speed debuff is scaled to be slightly /// smaller than attack speed debuff. Frozen, /// Makes you wet and causes you to have reduced friction on the ground. /// Strength scales the friction you ignore non-linearly. 0.5 is 50% ground /// friction, 1.0 is 33% ground friction. Wet, /// Makes you move slower. /// Strength scales the movement speed debuff non-linearly. 0.5 is 50% /// speed, 1.0 is 33% speed. Ensnared, /// Drain stamina to a creature over time. /// Strength should be the energy per second of the debuff. Poisoned, /// Results from having an attack parried. /// Causes your attack speed to be slower to emulate the recover duration of /// an ability being lengthened. Parried, /// Results from drinking a potion. /// Decreases the health gained from subsequent potions. PotionSickness, /// Changed into another body. Polymorphed(Body), } impl BuffKind { /// Checks if buff is buff or debuff. pub fn is_buff(self) -> bool { match self { BuffKind::Regeneration | BuffKind::Saturation | BuffKind::Potion | BuffKind::CampfireHeal | BuffKind::Frenzied | BuffKind::EnergyRegen | BuffKind::IncreaseMaxEnergy | BuffKind::IncreaseMaxHealth | BuffKind::Invulnerability | BuffKind::ProtectingWard | BuffKind::Hastened | BuffKind::Fortitude | BuffKind::Reckless | BuffKind::Flame | BuffKind::Frigid | BuffKind::Lifesteal //| BuffKind::SalamanderAspect | BuffKind::ImminentCritical | BuffKind::Fury | BuffKind::Sunderer | BuffKind::Defiance | BuffKind::Bloodfeast | BuffKind::Berserk => true, BuffKind::Bleeding | BuffKind::Cursed | BuffKind::Burning | BuffKind::Crippled | BuffKind::Frozen | BuffKind::Wet | BuffKind::Ensnared | BuffKind::Poisoned | BuffKind::Parried | BuffKind::PotionSickness | BuffKind::Polymorphed(_) => false, } } /// Checks if buff should queue. pub fn queues(self) -> bool { matches!(self, BuffKind::Saturation) } /// Checks if the buff can affect other buff effects applied in the same /// tick. pub fn affects_subsequent_buffs(self) -> bool { matches!( self, BuffKind::PotionSickness /* | BuffKind::SalamanderAspect */ ) } /// Checks if multiple instances of the buff should be processed, instead of /// only the strongest. pub fn stacks(self) -> bool { matches!(self, BuffKind::PotionSickness) } pub fn effects( &self, data: &BuffData, stats: Option<&Stats>, health: Option<&Health>, ) -> Vec { // Normalized nonlinear scaling let nn_scaling = |a| a / (a + 0.5); let instance = rand::random(); match self { BuffKind::Bleeding => vec![BuffEffect::HealthChangeOverTime { rate: -data.strength, kind: ModifierKind::Additive, instance, tick_dur: Secs(0.5), }], BuffKind::Regeneration => vec![BuffEffect::HealthChangeOverTime { rate: data.strength, kind: ModifierKind::Additive, instance, tick_dur: Secs(1.0), }], BuffKind::Saturation => vec![BuffEffect::HealthChangeOverTime { rate: data.strength, kind: ModifierKind::Additive, instance, tick_dur: Secs(3.0), }], BuffKind::Potion => { vec![BuffEffect::HealthChangeOverTime { rate: data.strength * stats.map_or(1.0, |s| s.heal_multiplier), kind: ModifierKind::Additive, instance, tick_dur: Secs(0.1), }] }, BuffKind::CampfireHeal => vec![BuffEffect::HealthChangeOverTime { rate: data.strength, kind: ModifierKind::Multiplicative, instance, tick_dur: Secs(2.0), }], BuffKind::Cursed => vec![ BuffEffect::MaxHealthChangeOverTime { rate: -1.0, kind: ModifierKind::Additive, target_fraction: 1.0 - data.strength, }, BuffEffect::HealthChangeOverTime { rate: -1.0, kind: ModifierKind::Additive, instance, tick_dur: Secs(0.5), }, ], BuffKind::EnergyRegen => vec![BuffEffect::EnergyChangeOverTime { rate: data.strength, kind: ModifierKind::Additive, tick_dur: Secs(0.25), }], BuffKind::IncreaseMaxEnergy => vec![BuffEffect::MaxEnergyModifier { value: data.strength, kind: ModifierKind::Additive, }], BuffKind::IncreaseMaxHealth => vec![BuffEffect::MaxHealthModifier { value: data.strength, kind: ModifierKind::Additive, }], BuffKind::Invulnerability => vec![BuffEffect::DamageReduction(1.0)], BuffKind::ProtectingWard => vec![BuffEffect::DamageReduction( // Causes non-linearity in effect strength, but necessary // to allow for tool power and other things to affect the // strength. 0.5 also still provides 50% damage reduction. nn_scaling(data.strength), )], BuffKind::Burning => vec![BuffEffect::HealthChangeOverTime { rate: -data.strength, kind: ModifierKind::Additive, instance, tick_dur: Secs(0.25), }], BuffKind::Poisoned => vec![BuffEffect::EnergyChangeOverTime { rate: -data.strength, kind: ModifierKind::Additive, tick_dur: Secs(0.5), }], BuffKind::Crippled => vec![ BuffEffect::MovementSpeed(1.0 - nn_scaling(data.strength)), BuffEffect::HealthChangeOverTime { rate: -data.strength * 4.0, kind: ModifierKind::Additive, instance, tick_dur: Secs(0.5), }, ], BuffKind::Frenzied => vec![ BuffEffect::MovementSpeed(1.0 + data.strength), BuffEffect::HealthChangeOverTime { rate: data.strength * 10.0, kind: ModifierKind::Additive, instance, tick_dur: Secs(1.0), }, ], BuffKind::Frozen => vec![ BuffEffect::MovementSpeed(f32::powf(1.0 - nn_scaling(data.strength), 1.1)), BuffEffect::AttackSpeed(1.0 - nn_scaling(data.strength)), ], BuffKind::Wet => vec![BuffEffect::GroundFriction(1.0 - nn_scaling(data.strength))], BuffKind::Ensnared => vec![BuffEffect::MovementSpeed(1.0 - nn_scaling(data.strength))], BuffKind::Hastened => vec![ BuffEffect::MovementSpeed(1.0 + data.strength), BuffEffect::AttackSpeed(1.0 + data.strength), BuffEffect::CriticalChance { kind: ModifierKind::Multiplicative, val: 0.0, }, ], BuffKind::Fortitude => vec![ BuffEffect::PoiseReduction(nn_scaling(data.strength)), BuffEffect::PoiseDamageFromLostHealth { initial_health: health.map_or(0.0, |h| h.current()), strength: data.strength, }, ], BuffKind::Parried => vec![BuffEffect::AttackSpeed(0.5)], BuffKind::PotionSickness => vec![BuffEffect::HealReduction(data.strength)], BuffKind::Reckless => vec![ BuffEffect::DamageReduction(-data.strength), BuffEffect::AttackDamage(1.0 + data.strength), ], BuffKind::Polymorphed(body) => vec![BuffEffect::BodyChange(*body)], BuffKind::Flame => vec![BuffEffect::AttackEffect(AttackEffect::new( None, CombatEffect::Buff(CombatBuff { kind: BuffKind::Burning, dur_secs: data.secondary_duration.map_or(5.0, |dur| dur.0 as f32), strength: CombatBuffStrength::DamageFraction(data.strength), chance: 1.0, }), ))], BuffKind::Frigid => vec![BuffEffect::AttackEffect(AttackEffect::new( None, CombatEffect::Buff(CombatBuff { kind: BuffKind::Frozen, dur_secs: data.secondary_duration.map_or(5.0, |dur| dur.0 as f32), strength: CombatBuffStrength::Value(data.strength), chance: 1.0, }), ))], BuffKind::Lifesteal => vec![BuffEffect::AttackEffect(AttackEffect::new( None, CombatEffect::Lifesteal(data.strength), ))], /*BuffKind::SalamanderAspect => vec![ BuffEffect::BuffImmunity(BuffKind::Burning), BuffEffect::SwimSpeed(1.0 + data.strength), ],*/ BuffKind::Bloodfeast => vec![BuffEffect::AttackEffect( AttackEffect::new(None, CombatEffect::Lifesteal(data.strength)) .with_requirement(CombatRequirement::TargetHasBuff(BuffKind::Bleeding)), )], BuffKind::ImminentCritical => vec![BuffEffect::CriticalChance { kind: ModifierKind::Additive, val: 1.0, }], BuffKind::Fury => vec![BuffEffect::AttackEffect( AttackEffect::new(None, CombatEffect::Combo(data.strength.round() as i32)) .with_requirement(CombatRequirement::AnyDamage), )], BuffKind::Sunderer => vec![ BuffEffect::MitigationsPenetration(nn_scaling(data.strength)), BuffEffect::EnergyReward(1.0 + 2.0 * data.strength), ], BuffKind::Defiance => vec![ BuffEffect::DamageReduction(nn_scaling(data.strength)), BuffEffect::PoiseReduction(nn_scaling(data.strength)), BuffEffect::MovementSpeed(1.0 - nn_scaling(data.strength)), BuffEffect::DamagedEffect(DamagedEffect::Combo( (data.strength * 5.0).round() as i32 )), ], BuffKind::Berserk => vec![ BuffEffect::DamageReduction(-data.strength), BuffEffect::AttackDamage(1.0 + data.strength), BuffEffect::AttackSpeed(1.0 + nn_scaling(data.strength) / 2.0), BuffEffect::MovementSpeed(1.0 + nn_scaling(data.strength) / 4.0), ], } } fn extend_cat_ids(&self, mut cat_ids: Vec) -> Vec { // TODO: Remove clippy allow after another buff needs this #[allow(clippy::single_match)] match self { BuffKind::ImminentCritical => { cat_ids.push(BuffCategory::RemoveOnAttack); }, _ => {}, } cat_ids } } // Struct used to store data relevant to a buff #[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)] #[serde(deny_unknown_fields)] pub struct BuffData { pub strength: f32, pub duration: Option, pub delay: Option, /// Force the buff effects to be applied each tick, ignoring num_ticks #[serde(default)] pub force_immediate: bool, /// Used for buffs that have rider buffs (e.g. Flame, Frigid) pub secondary_duration: Option, } impl BuffData { pub fn new(strength: f32, duration: Option) -> Self { Self { strength, duration, force_immediate: false, delay: None, secondary_duration: None, } } pub fn with_delay(mut self, delay: Secs) -> Self { self.delay = Some(delay); self } pub fn with_secondary_duration(mut self, sec_dur: Secs) -> Self { self.secondary_duration = Some(sec_dur); self } /// Force the buff effects to be applied each tick, ignoring num_ticks pub fn with_force_immediate(mut self, force_immediate: bool) -> Self { self.force_immediate = force_immediate; self } } /// De/buff category ID. /// Similar to `BuffKind`, but to mark a category (for more generic usage, like /// positive/negative buffs). #[derive(Clone, Copy, Eq, PartialEq, Debug, Serialize, Deserialize)] pub enum BuffCategory { Natural, Physical, Magical, Divine, PersistOnDeath, FromActiveAura(Uid, AuraKey), RemoveOnAttack, RemoveOnLoadoutChange, } #[derive(Clone, Debug, Serialize, Deserialize)] pub enum ModifierKind { Additive, Multiplicative, } /// Data indicating and configuring behaviour of a de/buff. #[derive(Clone, Debug, Serialize, Deserialize)] pub enum BuffEffect { /// Periodically damages or heals entity HealthChangeOverTime { rate: f32, kind: ModifierKind, instance: u64, tick_dur: Secs, }, /// Periodically consume entity energy EnergyChangeOverTime { rate: f32, kind: ModifierKind, tick_dur: Secs, }, /// Changes maximum health by a certain amount MaxHealthModifier { value: f32, kind: ModifierKind, }, /// Changes maximum energy by a certain amount MaxEnergyModifier { value: f32, kind: ModifierKind, }, /// Reduces damage after armor is accounted for by this fraction DamageReduction(f32), /// Gradually changes an entities max health over time MaxHealthChangeOverTime { rate: f32, kind: ModifierKind, target_fraction: f32, }, /// Modifies move speed of target MovementSpeed(f32), /// Modifies attack speed of target AttackSpeed(f32), /// Modifies ground friction of target GroundFriction(f32), /// Reduces poise damage taken after armor is accounted for by this fraction PoiseReduction(f32), /// Reduces amount healed by consumables HealReduction(f32), /// Increases poise damage dealt when health is lost PoiseDamageFromLostHealth { initial_health: f32, strength: f32, }, /// Modifier to the amount of damage dealt with attacks AttackDamage(f32), /// Multiplies crit chance of attacks CriticalChance { kind: ModifierKind, val: f32, }, /// Changes body. BodyChange(Body), BuffImmunity(BuffKind), SwimSpeed(f32), /// Add an attack effect to attacks made while buff is active AttackEffect(AttackEffect), /// Increases poise damage dealt by attacks AttackPoise(f32), /// Ignores some damage reduction on target MitigationsPenetration(f32), /// Modifies energy rewarded on successful strikes EnergyReward(f32), /// Add an effect to the entity when damaged by an attack DamagedEffect(DamagedEffect), } /// Actual de/buff. /// Buff can timeout after some time if `time` is Some. If `time` is None, /// Buff will last indefinitely, until removed manually (by some action, like /// uncursing). /// /// Buff has a kind, which is used to determine the effects in a builder /// function. /// /// To provide more classification info when needed, /// buff can be in one or more buff category. #[derive(Clone, Debug, Serialize, Deserialize)] pub struct Buff { pub kind: BuffKind, pub data: BuffData, pub cat_ids: Vec, pub end_time: Option