veloren_common/comp/
buff.rs

1use crate::{
2    combat::{
3        AttackEffect, CombatBuff, CombatBuffStrength, CombatEffect, CombatRequirement,
4        DamagedEffect, DeathEffect,
5    },
6    comp::{Mass, Stats, aura::AuraKey},
7    resources::{Secs, Time},
8    uid::Uid,
9};
10
11use core::cmp::Ordering;
12use enum_map::{Enum, EnumMap};
13use itertools::Either;
14use serde::{Deserialize, Serialize};
15use slotmap::{SlotMap, new_key_type};
16use specs::{Component, DerefFlaggedStorage, VecStorage};
17use strum::EnumIter;
18
19use super::Body;
20
21new_key_type! { pub struct BuffKey; }
22
23/// De/buff Kind.
24/// This is used to determine what effects a buff will have
25#[derive(
26    Clone, Copy, PartialEq, Eq, Hash, Debug, Serialize, Deserialize, PartialOrd, Ord, EnumIter, Enum,
27)]
28pub enum BuffKind {
29    // =================
30    //       BUFFS
31    // =================
32    /// Restores health/time for some period.
33    /// Strength should be the healing per second.
34    Regeneration,
35    /// Restores health/time for some period for consumables.
36    /// Strength should be the healing per second.
37    Saturation,
38    /// Applied when drinking a potion.
39    /// Strength should be the healing per second.
40    Potion,
41    /// Increases movement speed and vulnerability to damage as well as
42    /// decreases the amount of damage dealt.
43    /// Movement speed increases linearly with strength 1.0 is an 100% increase
44    /// Damage vulnerability and damage reduction are both hard set to 100%
45    Agility,
46    /// Applied when sitting at a campfire.
47    /// Strength is fraction of health restored per second.
48    CampfireHeal,
49    /// Restores energy/time for some period.
50    /// Strength should be the healing per second.
51    EnergyRegen,
52    /// Raises maximum energy.
53    /// Strength should be 10x the effect to max energy.
54    IncreaseMaxEnergy,
55    /// Raises maximum health.
56    /// Strength should be the effect to max health.
57    IncreaseMaxHealth,
58    /// Makes you immune to attacks.
59    /// Strength does not affect this buff.
60    Invulnerability,
61    /// Reduces incoming damage.
62    /// Strength scales the damage reduction non-linearly. 0.5 provides 50% DR,
63    /// 1.0 provides 67% DR.
64    ProtectingWard,
65    /// Increases movement speed and gives health regeneration.
66    /// Strength scales the movement speed linearly. 0.5 is 150% speed, 1.0 is
67    /// 200% speed. Provides regeneration at 10x the value of the strength.
68    Frenzied,
69    /// Increases movement and attack speed, but removes chance to get critical
70    /// hits. Strength scales strength of both effects linearly. 0.5 is a
71    /// 50% increase, 1.0 is a 100% increase.
72    Hastened,
73    /// Increases resistance to incoming poise, and poise damage dealt as health
74    /// is lost.
75    /// Strength scales the resistance non-linearly. 0.5 provides 50%, 1.0
76    /// provides 67%.
77    /// Strength scales the poise damage increase linearly, a strength of 1.0
78    /// and n health less from maximum health will cause poise damage to
79    /// increase by n%.
80    Fortitude,
81    /// Increases both attack damage and vulnerability to damage.
82    /// Damage increases linearly with strength, 1.0 is a 100% increase.
83    /// Damage reduction decreases linearly with strength, 1.0 is a 100%
84    /// decrease.
85    Reckless,
86    /// Provides immunity to burning and increases movement speed in lava.
87    /// Movement speed increases linearly with strength, 1.0 is a 100% increase.
88    // SalamanderAspect, TODO: Readd in second dwarven mine MR
89    /// Your attacks cause targets to receive the burning debuff
90    /// Strength of burning debuff is a fraction of the damage, fraction
91    /// increases linearly with strength
92    Flame,
93    /// Your attacks cause targets to receive the frozen debuff
94    /// Strength of frozen debuff is equal to the strength of this buff
95    Frigid,
96    /// Your attacks have lifesteal
97    /// Strength increases the fraction of damage restored as life
98    Lifesteal,
99    /// Your attacks against bleeding targets have lifesteal
100    /// Strength increases the fraction of damage restored as life
101    Bloodfeast,
102    /// Guarantees that the next attack is a precise hit. Does this kind of
103    /// hackily by adding 100% to the precision, will need to be adjusted if we
104    /// ever allow double precision hits instead of treating 100 as a
105    /// ceiling.
106    ImminentCritical,
107    /// Increases combo gain, every 1 strength increases combo per strike by 1,
108    /// rounds to nearest integer
109    Fury,
110    /// Allows attacks to ignore DR and increases energy reward
111    /// DR penetration is non-linear, 0.5 is 50% penetration and 1.0 is a 67%
112    /// penetration. Energy reward is increased linearly to strength, 1.0 is a
113    /// 150 % increase.
114    Sunderer,
115    /// Increases damage resistance and poise resistance, causes combo to be
116    /// generated when damaged, and decreases movement speed.
117    /// Damage resistance increases non-linearly with strength, 0.5 is 50% and
118    /// 1.0 is 67%. Poise resistance increases non-linearly with strength, 0.5
119    /// is 50% and 1.0 is 67%. Movement speed is decreased to 50%. Combo
120    /// generation is linear with strength, 1.0 is 5 combo generated on being
121    /// hit.
122    Defiance,
123    /// Increases both attack damage, vulnerability to damage, attack speed, and
124    /// movement speed Damage increases linearly with strength, 1.0 is a
125    /// 100% increase. Damage reduction decreases linearly with strength,
126    /// 1.0 is a 100% Attack speed increases non-linearly with strength, 0.5
127    /// is a 25% increase, 1.0 is a 33% increase Movement speed increases
128    /// non-linearly with strength, 0.5 is a 12.5% increase, 1.0 is a 16.7%
129    /// increase decrease.
130    Berserk,
131    /// Increases poise resistance and energy reward. However if killed, buffs
132    /// killer with Reckless buff. Poise resistance scales non-linearly with
133    /// strength, 0.5 is 50% and 1.0 is 67%. Energy reward scales linearly with
134    /// strength, 0.5 is +50% and 1.0 is +100% strength. Reckless buff reward
135    /// strength is equal to scornful taunt buff strength.
136    ScornfulTaunt,
137    /// Increases damage resistance, causes energy to be generated when damaged,
138    /// and decreases movement speed. Damage resistance increases non-linearly
139    /// with strength, 0.5 is 50% and 1.0 is 67%. Energy generation is linear
140    /// with strength, 1.0 is 10 energy per hit. Movement speed is decreased to
141    /// 70%.
142    Tenacity,
143    /// Applies to some debuffs that have strong CC effects. Automatically
144    /// gained upon receiving those debuffs, and causes future instances of
145    /// those debuffs to be applied with reduced duration.
146    /// Strength linearly decreases the duration of newly applied, affected
147    /// debuffs, 0.5 is a 50% reduction.
148    Resilience,
149    // =================
150    //      DEBUFFS
151    // =================
152    /// Does damage to a creature over time.
153    /// Strength should be the DPS of the debuff.
154    /// Provides immunity against Frozen.
155    Burning,
156    /// Lowers health over time for some duration.
157    /// Strength should be the DPS of the debuff.
158    Bleeding,
159    /// Lower a creature's max health over time.
160    /// Strength only affects the target max health, 0.5 targets 50% of base
161    /// max, 1.0 targets 100% of base max.
162    Cursed,
163    /// Reduces movement speed and causes bleeding damage.
164    /// Strength scales the movement speed debuff non-linearly. 0.5 is 50%
165    /// speed, 1.0 is 33% speed. Bleeding is at 4x the value of the strength.
166    Crippled,
167    /// Slows movement and attack speed.
168    /// Strength scales the attack speed debuff non-linearly. 0.5 is ~50%
169    /// speed, 1.0 is 33% speed. Movement speed debuff is scaled to be slightly
170    /// smaller than attack speed debuff.
171    /// Provides immunity against Heatstroke.
172    Frozen,
173    /// Makes you wet and causes you to have reduced friction on the ground.
174    /// Strength scales the friction you ignore non-linearly. 0.5 is 50% ground
175    /// friction, 1.0 is 33% ground friction.
176    /// Provides immunity against Burning.
177    Wet,
178    /// Makes you move slower.
179    /// Strength scales the movement speed debuff non-linearly. 0.5 is 50%
180    /// speed, 1.0 is 33% speed.
181    Ensnared,
182    /// Drain stamina to a creature over time.
183    /// Strength should be the energy per second of the debuff.
184    Poisoned,
185    /// Results from having an attack parried.
186    /// Causes your attack speed to be slower to emulate the recover duration of
187    /// an ability being lengthened.
188    Parried,
189    /// Results from drinking a potion.
190    /// Decreases the health gained from subsequent potions.
191    PotionSickness,
192    /// Slows movement speed and reduces energy reward.
193    /// Both scales non-linearly to strength, 0.5 lead to movespeed reduction
194    /// by 25% and energy reward reduced by 150%, 1.0 lead to MS reduction by
195    /// 33.3% and energy reward reduced by 200%. Energy reward can't be
196    /// reduced by more than 200%, to a minimum value of -100%.
197    Heatstroke,
198    /// Reduces movement speed to 0.
199    /// Strength increases the relative mass of the creature that can be
200    /// targeted. A strength of 1.0 means that a creature of the same mass gets
201    /// rooted for the full duration. A strength of 2.0 means a creature of
202    /// twice the mass gets rooted for the full duration. If the target's mass
203    /// is higher than the strength allows for, duration gets reduced using a
204    /// mutiplier from the ratio of masses.
205    Rooted,
206    /// Slows movement speed and reduces energy reward
207    /// Both scale non-linearly with strength, 0.5 leads to 50% reduction of
208    /// energy reward and 33% reduction of move speed. 1.0 leads to 67%
209    /// reduction of energy reward and 50% reduction of move speed.
210    Winded,
211    /// Prevents use of auxiliary abilities.
212    /// Does not scale with strength
213    Concussion,
214    /// Increases amount of poise damage received
215    /// Scales linearly with strength, 1.0 leads to 100% more poise damage
216    /// received
217    Staggered,
218    // =================
219    //      COMPLEX
220    // =================
221    /// Changed into another body.
222    Polymorphed,
223}
224
225/// Tells a little more about the buff kind than simple buff/debuff
226#[derive(Clone, Copy, Debug, PartialEq, Eq)]
227pub enum BuffDescriptor {
228    /// Simple positive buffs, like `BuffKind::Saturation`
229    SimplePositive,
230    /// Simple negative buffs, like `BuffKind::Bleeding`
231    SimpleNegative,
232    /// Buffs that require unusual data that can't be governed just by strength
233    /// and duration, like `BuffKind::Polymorhped`
234    Complex,
235    // For future additions, we may want to tell about non-obvious buffs,
236    // like Agility.
237    // Also maybe extend Complex to differentiate between Positive, Negative
238    // and Neutral buffs?
239    // For now, Complex is assumed to be neutral/non-obvious.
240}
241
242impl BuffKind {
243    /// Tells a little more about buff kind than simple buff/debuff
244    ///
245    /// Read more in [BuffDescriptor].
246    pub fn differentiate(self) -> BuffDescriptor {
247        match self {
248            BuffKind::Regeneration
249            | BuffKind::Saturation
250            | BuffKind::Potion
251            | BuffKind::Agility
252            | BuffKind::CampfireHeal
253            | BuffKind::Frenzied
254            | BuffKind::EnergyRegen
255            | BuffKind::IncreaseMaxEnergy
256            | BuffKind::IncreaseMaxHealth
257            | BuffKind::Invulnerability
258            | BuffKind::ProtectingWard
259            | BuffKind::Hastened
260            | BuffKind::Fortitude
261            | BuffKind::Reckless
262            | BuffKind::Flame
263            | BuffKind::Frigid
264            | BuffKind::Lifesteal
265            //| BuffKind::SalamanderAspect
266            | BuffKind::ImminentCritical
267            | BuffKind::Fury
268            | BuffKind::Sunderer
269            | BuffKind::Defiance
270            | BuffKind::Bloodfeast
271            | BuffKind::Berserk
272            | BuffKind::ScornfulTaunt
273            | BuffKind::Tenacity
274            | BuffKind::Resilience => BuffDescriptor::SimplePositive,
275            BuffKind::Bleeding
276            | BuffKind::Cursed
277            | BuffKind::Burning
278            | BuffKind::Crippled
279            | BuffKind::Frozen
280            | BuffKind::Wet
281            | BuffKind::Ensnared
282            | BuffKind::Poisoned
283            | BuffKind::Parried
284            | BuffKind::PotionSickness
285            | BuffKind::Heatstroke
286            | BuffKind::Rooted
287            | BuffKind::Winded
288            | BuffKind::Concussion
289            | BuffKind::Staggered => BuffDescriptor::SimpleNegative,
290            BuffKind::Polymorphed => BuffDescriptor::Complex,
291        }
292    }
293
294    /// Checks if buff is buff or debuff.
295    pub fn is_buff(self) -> bool {
296        match self.differentiate() {
297            BuffDescriptor::SimplePositive => true,
298            BuffDescriptor::SimpleNegative | BuffDescriptor::Complex => false,
299        }
300    }
301
302    pub fn is_simple(self) -> bool {
303        match self.differentiate() {
304            BuffDescriptor::SimplePositive | BuffDescriptor::SimpleNegative => true,
305            BuffDescriptor::Complex => false,
306        }
307    }
308
309    /// Checks if buff should queue.
310    pub fn queues(self) -> bool { matches!(self, BuffKind::Saturation) }
311
312    /// Checks if the buff can affect other buff effects applied in the same
313    /// tick.
314    pub fn affects_subsequent_buffs(self) -> bool {
315        matches!(
316            self,
317            BuffKind::PotionSickness /* | BuffKind::SalamanderAspect */
318        )
319    }
320
321    /// Checks if multiple instances of the buff should be processed, instead of
322    /// only the strongest.
323    pub fn stacks(self) -> bool { matches!(self, BuffKind::PotionSickness | BuffKind::Resilience) }
324
325    pub fn effects(&self, data: &BuffData) -> Vec<BuffEffect> {
326        // Normalized nonlinear scaling
327        // TODO: Do we want to make denominator term parameterized. Come back to if we
328        // add nn_scaling3.
329        let nn_scaling = |a: f32| a.abs() / (a.abs() + 0.5) * a.signum();
330        let nn_scaling2 = |a: f32| a.abs() / (a.abs() + 1.0) * a.signum();
331        let instance = rand::random();
332        match self {
333            BuffKind::Bleeding => vec![BuffEffect::HealthChangeOverTime {
334                rate: -data.strength,
335                kind: ModifierKind::Additive,
336                instance,
337                tick_dur: Secs(0.5),
338            }],
339            BuffKind::Regeneration => vec![BuffEffect::HealthChangeOverTime {
340                rate: data.strength,
341                kind: ModifierKind::Additive,
342                instance,
343                tick_dur: Secs(1.0),
344            }],
345            BuffKind::Saturation => vec![BuffEffect::HealthChangeOverTime {
346                rate: data.strength,
347                kind: ModifierKind::Additive,
348                instance,
349                tick_dur: Secs(3.0),
350            }],
351            BuffKind::Potion => {
352                vec![BuffEffect::HealthChangeOverTime {
353                    rate: data.strength,
354                    kind: ModifierKind::Additive,
355                    instance,
356                    tick_dur: Secs(0.1),
357                }]
358            },
359            BuffKind::Agility => vec![
360                BuffEffect::MovementSpeed(1.0 + data.strength),
361                BuffEffect::DamageReduction(-1.0),
362                BuffEffect::AttackDamage(0.0),
363            ],
364            BuffKind::CampfireHeal => vec![BuffEffect::HealthChangeOverTime {
365                rate: data.strength,
366                kind: ModifierKind::Multiplicative,
367                instance,
368                tick_dur: Secs(2.0),
369            }],
370            BuffKind::Cursed => vec![
371                BuffEffect::MaxHealthChangeOverTime {
372                    rate: -1.0,
373                    kind: ModifierKind::Additive,
374                    target_fraction: 1.0 - data.strength,
375                },
376                BuffEffect::HealthChangeOverTime {
377                    rate: -1.0,
378                    kind: ModifierKind::Additive,
379                    instance,
380                    tick_dur: Secs(0.5),
381                },
382            ],
383            BuffKind::EnergyRegen => vec![BuffEffect::EnergyChangeOverTime {
384                rate: data.strength,
385                kind: ModifierKind::Additive,
386                tick_dur: Secs(0.25),
387                reset_rate_on_tick: false,
388            }],
389            BuffKind::IncreaseMaxEnergy => vec![BuffEffect::MaxEnergyModifier {
390                value: data.strength,
391                kind: ModifierKind::Additive,
392            }],
393            BuffKind::IncreaseMaxHealth => vec![BuffEffect::MaxHealthModifier {
394                value: data.strength,
395                kind: ModifierKind::Additive,
396            }],
397            BuffKind::Invulnerability => vec![BuffEffect::DamageReduction(1.0)],
398            BuffKind::ProtectingWard => vec![BuffEffect::DamageReduction(
399                // Causes non-linearity in effect strength, but necessary
400                // to allow for tool power and other things to affect the
401                // strength. 0.5 also still provides 50% damage reduction.
402                nn_scaling(data.strength),
403            )],
404            BuffKind::Burning => vec![
405                BuffEffect::HealthChangeOverTime {
406                    rate: -data.strength,
407                    kind: ModifierKind::Additive,
408                    instance,
409                    tick_dur: Secs(0.25),
410                },
411                BuffEffect::BuffImmunity(BuffKind::Frozen),
412            ],
413            BuffKind::Poisoned => vec![BuffEffect::EnergyChangeOverTime {
414                rate: -data.strength,
415                kind: ModifierKind::Additive,
416                tick_dur: Secs(0.5),
417                reset_rate_on_tick: true,
418            }],
419            BuffKind::Crippled => vec![
420                BuffEffect::MovementSpeed(1.0 - nn_scaling(data.strength)),
421                BuffEffect::HealthChangeOverTime {
422                    rate: -data.strength * 4.0,
423                    kind: ModifierKind::Additive,
424                    instance,
425                    tick_dur: Secs(0.5),
426                },
427            ],
428            BuffKind::Frenzied => vec![
429                BuffEffect::MovementSpeed(1.0 + data.strength),
430                BuffEffect::HealthChangeOverTime {
431                    rate: data.strength * 10.0,
432                    kind: ModifierKind::Additive,
433                    instance,
434                    tick_dur: Secs(1.0),
435                },
436            ],
437            BuffKind::Frozen => vec![
438                BuffEffect::MovementSpeed(f32::powf(1.0 - nn_scaling(data.strength), 1.1)),
439                BuffEffect::AttackSpeed(1.0 - nn_scaling(data.strength)),
440                BuffEffect::BuffImmunity(BuffKind::Heatstroke),
441            ],
442            BuffKind::Wet => vec![
443                BuffEffect::GroundFriction(1.0 - nn_scaling(data.strength)),
444                BuffEffect::BuffImmunity(BuffKind::Burning),
445            ],
446            BuffKind::Ensnared => vec![BuffEffect::MovementSpeed(1.0 - nn_scaling(data.strength))],
447            BuffKind::Hastened => vec![
448                BuffEffect::MovementSpeed(1.0 + data.strength),
449                BuffEffect::AttackSpeed(1.0 + data.strength),
450                BuffEffect::PrecisionOverride(0.0),
451            ],
452            BuffKind::Fortitude => vec![
453                BuffEffect::PoiseReduction(nn_scaling(data.strength)),
454                BuffEffect::PoiseDamageFromLostHealth(data.strength),
455            ],
456            BuffKind::Parried => vec![BuffEffect::PrecisionVulnerabilityOverride(1.0)],
457            BuffKind::PotionSickness => vec![BuffEffect::ItemEffectReduction(data.strength)],
458            BuffKind::Reckless => vec![
459                BuffEffect::DamageReduction(-data.strength),
460                BuffEffect::AttackDamage(1.0 + data.strength),
461            ],
462            BuffKind::Polymorphed => {
463                let mut effects = Vec::new();
464                if let Some(MiscBuffData::Body(body)) = data.misc_data {
465                    effects.push(BuffEffect::BodyChange(body));
466                }
467                effects
468            },
469            BuffKind::Flame => vec![BuffEffect::AttackEffect(AttackEffect::new(
470                None,
471                CombatEffect::Buff(CombatBuff {
472                    kind: BuffKind::Burning,
473                    dur_secs: data.secondary_duration.map_or(5.0, |dur| dur.0 as f32),
474                    strength: CombatBuffStrength::DamageFraction(data.strength),
475                    chance: 1.0,
476                }),
477            ))],
478            BuffKind::Frigid => vec![BuffEffect::AttackEffect(AttackEffect::new(
479                None,
480                CombatEffect::Buff(CombatBuff {
481                    kind: BuffKind::Frozen,
482                    dur_secs: data.secondary_duration.map_or(5.0, |dur| dur.0 as f32),
483                    strength: CombatBuffStrength::Value(data.strength),
484                    chance: 1.0,
485                }),
486            ))],
487            BuffKind::Lifesteal => vec![BuffEffect::AttackEffect(AttackEffect::new(
488                None,
489                CombatEffect::Lifesteal(data.strength),
490            ))],
491            /*BuffKind::SalamanderAspect => vec![
492                BuffEffect::BuffImmunity(BuffKind::Burning),
493                BuffEffect::SwimSpeed(1.0 + data.strength),
494            ],*/
495            BuffKind::Bloodfeast => vec![BuffEffect::AttackEffect(
496                AttackEffect::new(None, CombatEffect::Lifesteal(data.strength))
497                    .with_requirement(CombatRequirement::TargetHasBuff(BuffKind::Bleeding)),
498            )],
499            BuffKind::ImminentCritical => vec![BuffEffect::PrecisionOverride(1.0)],
500            BuffKind::Fury => vec![BuffEffect::AttackEffect(
501                AttackEffect::new(None, CombatEffect::Combo(data.strength.round() as i32))
502                    .with_requirement(CombatRequirement::AnyDamage),
503            )],
504            BuffKind::Sunderer => vec![
505                BuffEffect::MitigationsPenetration(nn_scaling(data.strength)),
506                BuffEffect::EnergyReward(1.0 + 1.5 * data.strength),
507            ],
508            BuffKind::Defiance => vec![
509                BuffEffect::DamageReduction(nn_scaling(data.strength)),
510                BuffEffect::PoiseReduction(nn_scaling(data.strength)),
511                BuffEffect::MovementSpeed(0.5),
512                BuffEffect::DamagedEffect(DamagedEffect::Combo(
513                    (data.strength * 5.0).round() as i32
514                )),
515            ],
516            BuffKind::Berserk => vec![
517                BuffEffect::DamageReduction(-data.strength),
518                BuffEffect::AttackDamage(1.0 + data.strength),
519                BuffEffect::AttackSpeed(1.0 + nn_scaling(data.strength) / 2.0),
520                BuffEffect::MovementSpeed(1.0 + nn_scaling(data.strength) / 4.0),
521            ],
522            BuffKind::Heatstroke => vec![
523                BuffEffect::MovementSpeed(1.0 - nn_scaling(data.strength) * 0.5),
524                BuffEffect::EnergyReward((1.0 - nn_scaling(data.strength) * 3.0).max(-1.0)),
525            ],
526            BuffKind::ScornfulTaunt => vec![
527                BuffEffect::PoiseReduction(nn_scaling(data.strength)),
528                BuffEffect::EnergyReward(1.0 + data.strength),
529                BuffEffect::DeathEffect(DeathEffect::AttackerBuff {
530                    kind: BuffKind::Reckless,
531                    strength: data.strength,
532                    duration: data.duration,
533                }),
534            ],
535            BuffKind::Rooted => vec![BuffEffect::MovementSpeed(0.0)],
536            BuffKind::Winded => vec![
537                BuffEffect::MovementSpeed(1.0 - nn_scaling2(data.strength)),
538                BuffEffect::EnergyReward(1.0 - nn_scaling(data.strength)),
539            ],
540            BuffKind::Concussion => vec![BuffEffect::DisableAuxiliaryAbilities],
541            BuffKind::Staggered => vec![BuffEffect::PoiseReduction(-data.strength)],
542            BuffKind::Tenacity => vec![
543                BuffEffect::DamageReduction(nn_scaling(data.strength)),
544                BuffEffect::MovementSpeed(0.7),
545                BuffEffect::DamagedEffect(DamagedEffect::Energy(data.strength * 10.0)),
546            ],
547            BuffKind::Resilience => vec![BuffEffect::CrowdControlResistance(data.strength)],
548        }
549    }
550
551    fn extend_cat_ids(&self, mut cat_ids: Vec<BuffCategory>) -> Vec<BuffCategory> {
552        match self {
553            BuffKind::ImminentCritical => {
554                cat_ids.push(BuffCategory::RemoveOnAttack);
555            },
556            BuffKind::PotionSickness => {
557                cat_ids.push(BuffCategory::PersistOnDowned);
558            },
559            _ => {},
560        }
561        cat_ids
562    }
563
564    fn modify_data(
565        &self,
566        mut data: BuffData,
567        source_mass: Option<&Mass>,
568        dest_info: DestInfo,
569        source: BuffSource,
570    ) -> BuffData {
571        // TODO: Remove clippy allow after another buff needs this
572        #[expect(clippy::single_match)]
573        match self {
574            BuffKind::Rooted => {
575                let source_mass = source_mass.map_or(50.0, |m| m.0 as f64);
576                let dest_mass = dest_info.mass.map_or(50.0, |m| m.0 as f64);
577                let ratio = (source_mass / dest_mass).min(1.0);
578                data.duration = data.duration.map(|dur| Secs(dur.0 * ratio));
579            },
580            _ => {},
581        }
582        if self.resilience_ccr_strength(data).is_some() {
583            let dur_mult = dest_info
584                .stats
585                .map_or(1.0, |s| (1.0 - s.crowd_control_resistance).max(0.0));
586            data.duration = data.duration.map(|dur| dur * dur_mult as f64);
587        }
588        self.apply_item_effect_reduction(&mut data, source, dest_info);
589        data
590    }
591
592    /// If a buff kind should also give resilience when applied, return the
593    /// strength that resilience should have, otherwise return None
594    pub fn resilience_ccr_strength(&self, data: BuffData) -> Option<f32> {
595        match self {
596            BuffKind::Concussion => Some(0.3),
597            BuffKind::Frozen => Some(data.strength),
598            _ => None,
599        }
600    }
601
602    pub fn apply_item_effect_reduction(
603        &self,
604        data: &mut BuffData,
605        source: BuffSource,
606        dest_info: DestInfo,
607    ) {
608        if !matches!(source, BuffSource::Item) {
609            return;
610        }
611        let item_effect_reduction = dest_info.stats.map_or(1.0, |s| s.item_effect_reduction);
612        match self {
613            BuffKind::Potion | BuffKind::Agility => {
614                data.strength *= item_effect_reduction;
615            },
616            BuffKind::Burning | BuffKind::Frozen | BuffKind::Resilience => {
617                data.duration = data.duration.map(|dur| dur * item_effect_reduction as f64);
618            },
619            _ => {},
620        };
621    }
622}
623
624// Struct used to store data relevant to a buff
625#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)]
626#[serde(deny_unknown_fields)]
627pub struct BuffData {
628    pub strength: f32,
629    pub duration: Option<Secs>,
630    pub delay: Option<Secs>,
631    /// Used for buffs that have rider buffs (e.g. Flame, Frigid)
632    pub secondary_duration: Option<Secs>,
633    /// Used to add random data to buffs if needed (e.g. polymorphed)
634    pub misc_data: Option<MiscBuffData>,
635}
636
637#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)]
638pub enum MiscBuffData {
639    Body(Body),
640}
641
642impl BuffData {
643    pub fn new(strength: f32, duration: Option<Secs>) -> Self {
644        Self {
645            strength,
646            duration,
647            delay: None,
648            secondary_duration: None,
649            misc_data: None,
650        }
651    }
652
653    pub fn with_delay(mut self, delay: Secs) -> Self {
654        self.delay = Some(delay);
655        self
656    }
657
658    pub fn with_secondary_duration(mut self, sec_dur: Secs) -> Self {
659        self.secondary_duration = Some(sec_dur);
660        self
661    }
662
663    pub fn with_misc_data(mut self, misc_data: MiscBuffData) -> Self {
664        self.misc_data = Some(misc_data);
665        self
666    }
667}
668
669/// De/buff category ID.
670/// Similar to `BuffKind`, but to mark a category (for more generic usage, like
671/// positive/negative buffs).
672#[derive(Clone, Copy, Eq, PartialEq, Debug, Serialize, Deserialize)]
673pub enum BuffCategory {
674    Natural,
675    Physical,
676    Magical,
677    Divine,
678    PersistOnDowned,
679    PersistOnDeath,
680    FromActiveAura(Uid, AuraKey),
681    RemoveOnAttack,
682    RemoveOnLoadoutChange,
683    SelfBuff,
684}
685
686#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
687pub enum ModifierKind {
688    Additive,
689    Multiplicative,
690}
691
692/// Data indicating and configuring behaviour of a de/buff.
693#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
694pub enum BuffEffect {
695    /// Periodically damages or heals entity
696    HealthChangeOverTime {
697        rate: f32,
698        kind: ModifierKind,
699        instance: u64,
700        tick_dur: Secs,
701    },
702    /// Periodically consume entity energy
703    EnergyChangeOverTime {
704        rate: f32,
705        kind: ModifierKind,
706        tick_dur: Secs,
707        reset_rate_on_tick: bool,
708    },
709    /// Changes maximum health by a certain amount
710    MaxHealthModifier {
711        value: f32,
712        kind: ModifierKind,
713    },
714    /// Changes maximum energy by a certain amount
715    MaxEnergyModifier {
716        value: f32,
717        kind: ModifierKind,
718    },
719    /// Reduces damage after armor is accounted for by this fraction
720    DamageReduction(f32),
721    /// Gradually changes an entities max health over time
722    MaxHealthChangeOverTime {
723        rate: f32,
724        kind: ModifierKind,
725        target_fraction: f32,
726    },
727    /// Modifies move speed of target
728    MovementSpeed(f32),
729    /// Modifies attack speed of target
730    AttackSpeed(f32),
731    /// Modifies recovery speed of target
732    RecoverySpeed(f32),
733    /// Modifies ground friction of target
734    GroundFriction(f32),
735    /// Reduces poise damage taken after armor is accounted for by this fraction
736    PoiseReduction(f32),
737    /// Increases poise damage dealt when health is lost
738    PoiseDamageFromLostHealth(f32),
739    /// Modifier to the amount of damage dealt with attacks
740    AttackDamage(f32),
741    /// Overrides the precision multiplier applied to an attack
742    PrecisionOverride(f32),
743    /// Overrides the precision multiplier applied to an incoming attack
744    PrecisionVulnerabilityOverride(f32),
745    /// Changes body.
746    BodyChange(Body),
747    BuffImmunity(BuffKind),
748    SwimSpeed(f32),
749    /// Add an attack effect to attacks made while buff is active
750    AttackEffect(AttackEffect),
751    /// Increases poise damage dealt by attacks
752    AttackPoise(f32),
753    /// Ignores some damage reduction on target
754    MitigationsPenetration(f32),
755    /// Modifies energy rewarded on successful strikes
756    EnergyReward(f32),
757    /// Add an effect to the entity when damaged by an attack
758    DamagedEffect(DamagedEffect),
759    /// Add an effect to the entity when killed
760    DeathEffect(DeathEffect),
761    /// Prevents use of auxiliary abilities
762    DisableAuxiliaryAbilities,
763    /// Reduces duration of crowd control debuffs
764    CrowdControlResistance(f32),
765    /// Reduces the strength or duration of item buff
766    ItemEffectReduction(f32),
767}
768
769/// Actual de/buff.
770/// Buff can timeout after some time if `time` is Some. If `time` is None,
771/// Buff will last indefinitely, until removed manually (by some action, like
772/// uncursing).
773///
774/// Buff has a kind, which is used to determine the effects in a builder
775/// function.
776///
777/// To provide more classification info when needed,
778/// buff can be in one or more buff category.
779#[derive(Clone, Debug, Serialize, Deserialize)]
780pub struct Buff {
781    pub kind: BuffKind,
782    pub data: BuffData,
783    pub cat_ids: Vec<BuffCategory>,
784    pub end_time: Option<Time>,
785    pub start_time: Time,
786    pub effects: Vec<BuffEffect>,
787    pub source: BuffSource,
788}
789
790/// Information about whether buff addition or removal was requested.
791/// This to implement "on_add" and "on_remove" hooks for constant buffs.
792#[derive(Clone, Debug)]
793pub enum BuffChange {
794    /// Adds this buff.
795    Add(Buff),
796    /// Removes all buffs with this ID.
797    RemoveByKind(BuffKind),
798    /// Removes all buffs with this ID, but not debuffs.
799    RemoveFromController(BuffKind),
800    /// Removes buffs of these indices, should only be called when buffs expire
801    RemoveByKey(Vec<BuffKey>),
802    /// Removes buffs of these categories (first vec is of categories of which
803    /// all are required, second vec is of categories of which at least one is
804    /// required, third vec is of categories that will not be removed)
805    RemoveByCategory {
806        all_required: Vec<BuffCategory>,
807        any_required: Vec<BuffCategory>,
808        none_required: Vec<BuffCategory>,
809    },
810    /// Refreshes durations of all buffs with this kind.
811    Refresh(BuffKind),
812}
813
814impl Buff {
815    /// Builder function for buffs
816    pub fn new(
817        kind: BuffKind,
818        data: BuffData,
819        cat_ids: Vec<BuffCategory>,
820        source: BuffSource,
821        time: Time,
822        dest_info: DestInfo,
823        // Create source_info if we need more parameters from source
824        source_mass: Option<&Mass>,
825    ) -> Self {
826        let data = kind.modify_data(data, source_mass, dest_info, source);
827        let effects = kind.effects(&data);
828        let cat_ids = kind.extend_cat_ids(cat_ids);
829        let start_time = Time(time.0 + data.delay.map_or(0.0, |delay| delay.0));
830        let end_time = if cat_ids
831            .iter()
832            .any(|cat_id| matches!(cat_id, BuffCategory::FromActiveAura(..)))
833        {
834            None
835        } else {
836            data.duration.map(|dur| Time(start_time.0 + dur.0))
837        };
838        Buff {
839            kind,
840            data,
841            cat_ids,
842            start_time,
843            end_time,
844            effects,
845            source,
846        }
847    }
848
849    /// Calculate how much time has elapsed since the buff was applied
850    pub fn elapsed(&self, time: Time) -> Secs { Secs(time.0 - self.start_time.0) }
851}
852
853impl PartialOrd for Buff {
854    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
855        if self == other {
856            Some(Ordering::Equal)
857        } else if self.data.strength > other.data.strength {
858            Some(Ordering::Greater)
859        } else if self.data.strength < other.data.strength {
860            Some(Ordering::Less)
861        } else if self.data.delay.is_none() && other.data.delay.is_some() {
862            Some(Ordering::Greater)
863        } else if self.data.delay.is_some() && other.data.delay.is_none() {
864            Some(Ordering::Less)
865        } else if compare_end_time(self.end_time, other.end_time) {
866            Some(Ordering::Greater)
867        } else if compare_end_time(other.end_time, self.end_time) {
868            Some(Ordering::Less)
869        } else {
870            None
871        }
872    }
873}
874
875fn compare_end_time(a: Option<Time>, b: Option<Time>) -> bool {
876    a.is_none_or(|time_a| b.is_some_and(|time_b| time_a.0 > time_b.0))
877}
878
879impl PartialEq for Buff {
880    fn eq(&self, other: &Self) -> bool {
881        self.data.strength == other.data.strength
882            && self.end_time == other.end_time
883            && self.start_time == other.start_time
884    }
885}
886
887/// Source of the de/buff
888#[derive(Clone, Copy, PartialEq, Eq, Debug, Serialize, Deserialize)]
889pub enum BuffSource {
890    /// Applied by a character
891    Character { by: Uid },
892    /// Applied by world, like a poisonous fumes from a swamp
893    World,
894    /// Applied by command
895    Command,
896    /// Applied by an item
897    Item,
898    /// Applied by another buff (like an after-effect)
899    Buff,
900    /// Some other source
901    Unknown,
902}
903
904/// Component holding all de/buffs that gets resolved each tick.
905/// On each tick, remaining time of buffs get lowered and
906/// buff effect of each buff is applied or not, depending on the `BuffEffect`
907/// (specs system will decide based on `BuffEffect`, to simplify
908/// implementation). TODO: Something like `once` flag for `Buff` to remove the
909/// dependence on `BuffEffect` enum?
910///
911/// In case of one-time buffs, buff effects will be applied on addition
912/// and undone on removal of the buff (by the specs system).
913/// Example could be decreasing max health, which, if repeated each tick,
914/// would be probably an undesired effect).
915#[derive(Clone, Debug, Serialize, Deserialize, Default)]
916pub struct Buffs {
917    /// Maps kinds of buff to currently applied buffs of that kind and
918    /// the time that the first buff was added (time gets reset if entity no
919    /// longer has buffs of that kind)
920    pub kinds: EnumMap<BuffKind, Option<(Vec<BuffKey>, Time)>>,
921    // All buffs currently present on an entity
922    pub buffs: SlotMap<BuffKey, Buff>,
923}
924
925impl Buffs {
926    fn sort_kind(&mut self, kind: BuffKind) {
927        if let Some(buff_order) = self.kinds[kind].as_mut() {
928            if buff_order.0.is_empty() {
929                self.kinds[kind] = None;
930            } else {
931                let buffs = &self.buffs;
932                // Intentionally sorted in reverse so that the strongest buffs are earlier in
933                // the vector
934                buff_order
935                    .0
936                    .sort_by(|a, b| buffs[*b].partial_cmp(&buffs[*a]).unwrap_or(Ordering::Equal));
937            }
938        }
939    }
940
941    pub fn remove_kind(&mut self, kind: BuffKind) {
942        if let Some((buff_keys, _)) = self.kinds[kind].as_ref() {
943            for key in buff_keys {
944                self.buffs.remove(*key);
945            }
946            self.kinds[kind] = None;
947        }
948    }
949
950    pub fn insert(&mut self, buff: Buff, current_time: Time) -> BuffKey {
951        let kind = buff.kind;
952        // Try to find another overlaping non-queueable buff with same data, cat_ids and
953        // source.
954        let other_key = if kind.queues() {
955            None
956        } else {
957            self.kinds[kind].as_ref().and_then(|(keys, _)| {
958                keys.iter()
959                    .find(|key| {
960                        self.buffs.get(**key).is_some_and(|other_buff| {
961                            other_buff.data == buff.data
962                                && other_buff.cat_ids == buff.cat_ids
963                                && other_buff.source == buff.source
964                                && other_buff
965                                    .end_time
966                                    .is_none_or(|end_time| end_time.0 >= buff.start_time.0)
967                        })
968                    })
969                    .copied()
970            })
971        };
972
973        // If another buff with the same fields is found, update end_time and effects
974        let key = if !kind.stacks()
975            && let Some((other_buff, key)) =
976                other_key.and_then(|key| Some((self.buffs.get_mut(key)?, key)))
977        {
978            other_buff.end_time = buff.end_time;
979            other_buff.effects = buff.effects;
980            key
981        // Otherwise, insert a new buff
982        } else {
983            let key = self.buffs.insert(buff);
984            self.kinds[kind]
985                .get_or_insert_with(|| (Vec::new(), current_time))
986                .0
987                .push(key);
988            key
989        };
990
991        self.sort_kind(kind);
992        if kind.queues() {
993            self.delay_queueable_buffs(kind, current_time);
994        }
995        key
996    }
997
998    pub fn contains(&self, kind: BuffKind) -> bool { self.kinds[kind].is_some() }
999
1000    // Iterate through buffs of a given kind in effect order (most powerful first)
1001    pub fn iter_kind(&self, kind: BuffKind) -> impl Iterator<Item = (BuffKey, &Buff)> + '_ {
1002        self.kinds[kind]
1003            .as_ref()
1004            .map(|keys| keys.0.iter())
1005            .unwrap_or_else(|| [].iter())
1006            .map(move |&key| (key, &self.buffs[key]))
1007    }
1008
1009    // Iterates through all active buffs (the most powerful buff of each
1010    // non-stacking kind, and all of the stacking ones)
1011    pub fn iter_active(&self) -> impl Iterator<Item = impl Iterator<Item = &Buff>> + '_ {
1012        self.kinds
1013            .iter()
1014            .filter_map(|(kind, keys)| keys.as_ref().map(|keys| (kind, keys)))
1015            .map(move |(kind, keys)| {
1016                if kind.stacks() {
1017                    // Iterate stackable buffs in reverse order to show the timer of the soonest one
1018                    // to expire
1019                    Either::Left(keys.0.iter().filter_map(|key| self.buffs.get(*key)).rev())
1020                } else {
1021                    Either::Right(self.buffs.get(keys.0[0]).into_iter())
1022                }
1023            })
1024    }
1025
1026    // Gets most powerful buff of a given kind
1027    pub fn remove(&mut self, buff_key: BuffKey) {
1028        if let Some(buff) = self.buffs.remove(buff_key) {
1029            let kind = buff.kind;
1030            self.kinds[kind]
1031                .as_mut()
1032                .map(|keys| keys.0.retain(|key| *key != buff_key));
1033            self.sort_kind(kind);
1034        }
1035    }
1036
1037    fn delay_queueable_buffs(&mut self, kind: BuffKind, current_time: Time) {
1038        let mut next_start_time: Option<Time> = None;
1039        debug_assert!(kind.queues());
1040        if let Some(buffs) = self.kinds[kind].as_mut() {
1041            buffs.0.iter().for_each(|key| {
1042                if let Some(buff) = self.buffs.get_mut(*key) {
1043                    // End time only being updated when there is some next_start_time will
1044                    // technically cause buffs to "end early" if they have a weaker strength than a
1045                    // buff with an infinite duration, but this is fine since those buffs wouldn't
1046                    // matter anyways
1047                    if let Some(next_start_time) = next_start_time {
1048                        // Delays buff so that it has the same progress it has now at the time the
1049                        // previous buff would end.
1050                        //
1051                        // Shift should be relative to current time, unless the buff is delayed and
1052                        // hasn't started yet
1053                        let reference_time = current_time.0.max(buff.start_time.0);
1054                        // If buff has a delay, ensure that queueables shuffling queue does not
1055                        // potentially allow skipping delay
1056                        buff.start_time = Time(next_start_time.0.max(buff.start_time.0));
1057                        buff.end_time = buff.end_time.map(|end| {
1058                            Time(end.0 + next_start_time.0.max(reference_time) - reference_time)
1059                        });
1060                    }
1061                    next_start_time = buff.end_time;
1062                }
1063            })
1064        }
1065    }
1066}
1067
1068impl Component for Buffs {
1069    type Storage = DerefFlaggedStorage<Self, VecStorage<Self>>;
1070}
1071
1072#[derive(Default, Copy, Clone)]
1073pub struct DestInfo<'a> {
1074    pub stats: Option<&'a Stats>,
1075    pub mass: Option<&'a Mass>,
1076}
1077
1078#[cfg(test)]
1079pub mod tests {
1080    use crate::comp::buff::*;
1081
1082    #[cfg(test)]
1083    fn create_test_queueable_buff(buff_data: BuffData, time: Time) -> Buff {
1084        // Change to another buff that queues if we ever add one and remove saturation,
1085        // otherwise maybe add a test buff kind?
1086        debug_assert!(BuffKind::Saturation.queues());
1087        Buff::new(
1088            BuffKind::Saturation,
1089            buff_data,
1090            Vec::new(),
1091            BuffSource::Unknown,
1092            time,
1093            DestInfo::default(),
1094            None,
1095        )
1096    }
1097
1098    #[test]
1099    /// Tests a number of buffs with various progresses that queue to ensure
1100    /// queue has correct total duration
1101    fn test_queueable_buffs_three() {
1102        let mut buff_comp: Buffs = Default::default();
1103        let buff_data = BuffData::new(1.0, Some(Secs(10.0)));
1104        let time_a = Time(0.0);
1105        buff_comp.insert(create_test_queueable_buff(buff_data, time_a), time_a);
1106        let time_b = Time(6.0);
1107        buff_comp.insert(create_test_queueable_buff(buff_data, time_b), time_b);
1108        let time_c = Time(11.0);
1109        buff_comp.insert(create_test_queueable_buff(buff_data, time_c), time_c);
1110        // Check that all buffs have an end_time less than or equal to 30, and that at
1111        // least one has an end_time greater than or equal to 30.
1112        //
1113        // This should be true because 3 buffs that each lasted for 10 seconds were
1114        // inserted at various times, so the total duration should be 30 seconds.
1115        assert!(
1116            buff_comp
1117                .buffs
1118                .values()
1119                .all(|b| b.end_time.unwrap().0 < 30.01)
1120        );
1121        assert!(
1122            buff_comp
1123                .buffs
1124                .values()
1125                .any(|b| b.end_time.unwrap().0 > 29.99)
1126        );
1127    }
1128
1129    #[test]
1130    /// Tests that if a buff had a delay but will start soon, and an immediate
1131    /// queueable buff is added, delayed buff has correct start time
1132    fn test_queueable_buff_delay_start() {
1133        let mut buff_comp: Buffs = Default::default();
1134        let queued_buff_data = BuffData::new(1.0, Some(Secs(10.0))).with_delay(Secs(10.0));
1135        let buff_data = BuffData::new(1.0, Some(Secs(10.0)));
1136        let time_a = Time(0.0);
1137        buff_comp.insert(create_test_queueable_buff(queued_buff_data, time_a), time_a);
1138        let time_b = Time(6.0);
1139        buff_comp.insert(create_test_queueable_buff(buff_data, time_b), time_b);
1140        // Check that all buffs have an end_time less than or equal to 26, and that at
1141        // least one has an end_time greater than or equal to 26.
1142        //
1143        // This should be true because the first buff added had a delay of 10 seconds
1144        // and a duration of 10 seconds, the second buff added at 6 seconds had no
1145        // delay, and a duration of 10 seconds. When it finishes at 16 seconds the first
1146        // buff is past the delay time so should finish at 26 seconds.
1147        assert!(
1148            buff_comp
1149                .buffs
1150                .values()
1151                .all(|b| b.end_time.unwrap().0 < 26.01)
1152        );
1153        assert!(
1154            buff_comp
1155                .buffs
1156                .values()
1157                .any(|b| b.end_time.unwrap().0 > 25.99)
1158        );
1159    }
1160
1161    #[test]
1162    /// Tests that if a buff had a long delay, a short immediate queueable buff
1163    /// does not move delayed buff start or end times
1164    fn test_queueable_buff_long_delay() {
1165        let mut buff_comp: Buffs = Default::default();
1166        let queued_buff_data = BuffData::new(1.0, Some(Secs(10.0))).with_delay(Secs(50.0));
1167        let buff_data = BuffData::new(1.0, Some(Secs(10.0)));
1168        let time_a = Time(0.0);
1169        buff_comp.insert(create_test_queueable_buff(queued_buff_data, time_a), time_a);
1170        let time_b = Time(10.0);
1171        buff_comp.insert(create_test_queueable_buff(buff_data, time_b), time_b);
1172        // Check that all buffs have either an end time less than or equal to 20 seconds
1173        // XOR a start time greater than or equal to 50 seconds, that all buffs have a
1174        // start time less than or equal to 50 seconds, that all buffs have an end time
1175        // less than or equal to 60 seconds, and that at least one buff has an end time
1176        // greater than or equal to 60 seconds
1177        //
1178        // This should be true because the first buff has a delay of 50 seconds, the
1179        // second buff added has no delay at 10 seconds and lasts 10 seconds, so should
1180        // end at 20 seconds and not affect the start time of the delayed buff, and
1181        // since the delayed buff was not affected the end time should be 10 seconds
1182        // after the start time: 60 seconds != used here to emulate xor
1183        assert!(
1184            buff_comp
1185                .buffs
1186                .values()
1187                .all(|b| (b.end_time.unwrap().0 < 20.01) != (b.start_time.0 > 49.99))
1188        );
1189        assert!(buff_comp.buffs.values().all(|b| b.start_time.0 < 50.01));
1190        assert!(
1191            buff_comp
1192                .buffs
1193                .values()
1194                .all(|b| b.end_time.unwrap().0 < 60.01)
1195        );
1196        assert!(
1197            buff_comp
1198                .buffs
1199                .values()
1200                .any(|b| b.end_time.unwrap().0 > 59.99)
1201        );
1202    }
1203}