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