Skip to main content

veloren_common/comp/
buff.rs

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