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