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