veloren_common/comp/
buff.rs

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