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