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