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