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