1use common::{
2 Damage, DamageSource,
3 combat::{self, DamageContributor},
4 comp::{
5 Alignment, Energy, Group, Health, HealthChange, Inventory, LightEmitter, Mass,
6 ModifierKind, PhysicsState, Player, Pos, Stats,
7 agent::{Sound, SoundKind},
8 aura::{Auras, EnteredAuras},
9 body::{Body, object},
10 buff::{
11 Buff, BuffCategory, BuffChange, BuffData, BuffEffect, BuffKey, BuffKind, BuffSource,
12 Buffs, DestInfo,
13 },
14 fluid_dynamics::{Fluid, LiquidKind},
15 item::MaterialStatManifest,
16 },
17 event::{
18 BuffEvent, ChangeBodyEvent, CreateSpriteEvent, EmitExt, EnergyChangeEvent,
19 HealthChangeEvent, RemoveLightEmitterEvent, SoundEvent,
20 },
21 event_emitters,
22 outcome::Outcome,
23 resources::{DeltaTime, Secs, Time},
24 terrain::SpriteKind,
25 uid::{IdMaps, Uid},
26};
27use common_base::prof_span;
28use common_ecs::{Job, Origin, ParMode, Phase, System};
29use rand::Rng;
30use rayon::iter::ParallelIterator;
31use specs::{
32 Entities, Entity, LendJoin, ParJoin, Read, ReadExpect, ReadStorage, SystemData, WriteStorage,
33 shred,
34};
35use vek::Vec3;
36
37event_emitters! {
38 struct Events[EventEmitters] {
39 buff: BuffEvent,
40 change_body: ChangeBodyEvent,
41 remove_light: RemoveLightEmitterEvent,
42 health_change: HealthChangeEvent,
43 energy_change: EnergyChangeEvent,
44 sound: SoundEvent,
45 create_sprite: CreateSpriteEvent,
46 outcome: Outcome,
47 }
48}
49
50#[derive(SystemData)]
51pub struct ReadData<'a> {
52 entities: Entities<'a>,
53 dt: Read<'a, DeltaTime>,
54 events: Events<'a>,
55 inventories: ReadStorage<'a, Inventory>,
56 healths: ReadStorage<'a, Health>,
57 energies: ReadStorage<'a, Energy>,
58 physics_states: ReadStorage<'a, PhysicsState>,
59 groups: ReadStorage<'a, Group>,
60 id_maps: Read<'a, IdMaps>,
61 time: Read<'a, Time>,
62 msm: ReadExpect<'a, MaterialStatManifest>,
63 buffs: ReadStorage<'a, Buffs>,
64 auras: ReadStorage<'a, Auras>,
65 entered_auras: ReadStorage<'a, EnteredAuras>,
66 positions: ReadStorage<'a, Pos>,
67 bodies: ReadStorage<'a, Body>,
68 light_emitters: ReadStorage<'a, LightEmitter>,
69 alignments: ReadStorage<'a, Alignment>,
70 players: ReadStorage<'a, Player>,
71 masses: ReadStorage<'a, Mass>,
72}
73
74#[derive(Default)]
75pub struct Sys;
76impl<'a> System<'a> for Sys {
77 type SystemData = (ReadData<'a>, WriteStorage<'a, Stats>);
78
79 const NAME: &'static str = "buff";
80 const ORIGIN: Origin = Origin::Common;
81 const PHASE: Phase = Phase::Create;
82
83 fn run(job: &mut Job<Self>, (read_data, mut stats): Self::SystemData) {
84 let mut emitters = read_data.events.get_emitters();
85 let dt = read_data.dt.0;
86 stats.set_event_emission(false);
88
89 job.cpu_stats.measure(ParMode::Rayon);
93 let to_put_out_campfires = (
94 &read_data.entities,
95 &read_data.bodies,
96 &read_data.physics_states,
97 &read_data.light_emitters, )
99 .par_join()
100 .map_init(
101 || {
102 prof_span!(guard, "buff campfire deactivate");
103 guard
104 },
105 |_guard, (entity, body, physics_state, _)| {
106 if matches!(*body, Body::Object(object::Body::CampfireLit))
107 && matches!(
108 physics_state.in_fluid,
109 Some(Fluid::Liquid {
110 kind: LiquidKind::Water,
111 ..
112 })
113 )
114 {
115 Some(entity)
116 } else {
117 None
118 }
119 },
120 )
121 .fold(Vec::new, |mut to_put_out_campfires, put_out_campfire| {
122 put_out_campfire.map(|put| to_put_out_campfires.push(put));
123 to_put_out_campfires
124 })
125 .reduce(
126 Vec::new,
127 |mut to_put_out_campfires_a, mut to_put_out_campfires_b| {
128 to_put_out_campfires_a.append(&mut to_put_out_campfires_b);
129 to_put_out_campfires_a
130 },
131 );
132 job.cpu_stats.measure(ParMode::Single);
133 {
134 prof_span!(_guard, "write deferred campfire deletion");
135 for e in to_put_out_campfires {
138 {
139 emitters.emit(ChangeBodyEvent {
140 entity: e,
141 new_body: Body::Object(object::Body::Campfire),
142 });
143 emitters.emit(RemoveLightEmitterEvent { entity: e });
144 }
145 }
146 }
147
148 let mut rng = rand::thread_rng();
149 let buff_join = (
150 &read_data.entities,
151 &read_data.buffs,
152 &mut stats,
153 &read_data.bodies,
154 &read_data.healths,
155 &read_data.energies,
156 read_data.physics_states.maybe(),
157 read_data.masses.maybe(),
158 )
159 .lend_join();
160 buff_join.for_each(|comps| {
161 let (entity, buff_comp, mut stat, body, health, energy, physics_state, mass) = comps;
162 let dest_info = DestInfo {
163 stats: Some(&stat),
164 mass,
165 };
166 if let Some(physics_state) = physics_state {
168 if let Some((_, burning)) = buff_comp.iter_kind(BuffKind::Burning).next() {
170 for t_entity in physics_state.touch_entities.keys().filter_map(|te_uid| {
171 read_data.id_maps.uid_entity(*te_uid).filter(|te| {
172 combat::permit_pvp(
173 &read_data.alignments,
174 &read_data.players,
175 &read_data.entered_auras,
176 &read_data.id_maps,
177 Some(entity),
178 *te,
179 )
180 })
181 }) {
182 let duration = burning.data.duration.map(|d| d * 0.9);
183 if duration.is_none_or(|d| d.0 >= 1.0)
184 && rng
185 .gen_bool((dt * burning.data.strength / 5.0).clamp(0.0, 1.0).into())
186 {
187 emitters.emit(BuffEvent {
201 entity: t_entity,
202 buff_change: BuffChange::Add(Buff::new(
203 BuffKind::Burning,
204 BuffData::new(burning.data.strength, duration),
205 vec![BuffCategory::Natural],
206 BuffSource::World,
207 *read_data.time,
208 DestInfo {
209 stats: None,
212 mass: read_data.masses.get(t_entity),
213 },
214 mass,
215 )),
216 });
217 }
218 }
219 }
220 if matches!(
221 physics_state.on_ground.and_then(|b| b.get_sprite()),
222 Some(SpriteKind::EnsnaringVines)
223 ) {
224 emitters.emit(BuffEvent {
226 entity,
227 buff_change: BuffChange::Add(Buff::new(
228 BuffKind::Ensnared,
229 BuffData::new(0.5, Some(Secs(0.1))),
230 Vec::new(),
231 BuffSource::World,
232 *read_data.time,
233 dest_info,
234 None,
235 )),
236 });
237 }
238 if matches!(
239 physics_state.on_ground.and_then(|b| b.get_sprite()),
240 Some(SpriteKind::EnsnaringWeb)
241 ) {
242 emitters.emit(BuffEvent {
244 entity,
245 buff_change: BuffChange::Add(Buff::new(
246 BuffKind::Ensnared,
247 BuffData::new(1.0, Some(Secs(1.0))),
248 Vec::new(),
249 BuffSource::World,
250 *read_data.time,
251 dest_info,
252 None,
253 )),
254 });
255 }
256 if matches!(
257 physics_state.on_ground.and_then(|b| b.get_sprite()),
258 Some(SpriteKind::SeaUrchin)
259 ) {
260 emitters.emit(BuffEvent {
262 entity,
263 buff_change: BuffChange::Add(Buff::new(
264 BuffKind::Bleeding,
265 BuffData::new(1.0, Some(Secs(6.0))),
266 Vec::new(),
267 BuffSource::World,
268 *read_data.time,
269 dest_info,
270 None,
271 )),
272 });
273 }
274 if matches!(
275 physics_state.on_ground.and_then(|b| b.get_sprite()),
276 Some(SpriteKind::HaniwaTrap)
277 ) && !body.immune_to(BuffKind::Bleeding)
278 {
279 if let Some(pos) = read_data.positions.get(entity) {
281 emitters.emit(CreateSpriteEvent {
283 pos: Vec3::new(pos.0.x as i32, pos.0.y as i32, pos.0.z as i32 - 1),
284 sprite: SpriteKind::HaniwaTrapTriggered,
285 del_timeout: Some((4.0, 1.0)),
286 });
287 emitters.emit(SoundEvent {
288 sound: Sound::new(SoundKind::Trap, pos.0, 12.0, read_data.time.0),
289 });
290 emitters.emit(Outcome::Slash { pos: pos.0 });
291
292 emitters.emit(BuffEvent {
293 entity,
294 buff_change: BuffChange::Add(Buff::new(
295 BuffKind::Bleeding,
296 BuffData::new(5.0, Some(Secs(3.0))),
297 Vec::new(),
298 BuffSource::World,
299 *read_data.time,
300 dest_info,
301 None,
302 )),
303 });
304 }
305 }
306 if matches!(
307 physics_state.on_ground.and_then(|b| b.get_sprite()),
308 Some(SpriteKind::IronSpike | SpriteKind::HaniwaTrapTriggered)
309 ) {
310 emitters.emit(BuffEvent {
312 entity,
313 buff_change: BuffChange::Add(Buff::new(
314 BuffKind::Bleeding,
315 BuffData::new(1.0, Some(Secs(4.0))),
316 Vec::new(),
317 BuffSource::World,
318 *read_data.time,
319 dest_info,
320 None,
321 )),
322 });
323 }
324 if matches!(
325 physics_state.on_ground.and_then(|b| b.get_sprite()),
326 Some(SpriteKind::HotSurface)
327 ) {
328 emitters.emit(BuffEvent {
330 entity,
331 buff_change: BuffChange::Add(Buff::new(
332 BuffKind::Burning,
333 BuffData::new(10.0, None),
334 Vec::new(),
335 BuffSource::World,
336 *read_data.time,
337 dest_info,
338 None,
339 )),
340 });
341 }
342 if matches!(
343 physics_state.on_ground.and_then(|b| b.get_sprite()),
344 Some(SpriteKind::IceSpike)
345 ) {
346 emitters.emit(BuffEvent {
348 entity,
349 buff_change: BuffChange::Add(Buff::new(
350 BuffKind::Bleeding,
351 BuffData::new(15.0, Some(Secs(0.1))),
352 Vec::new(),
353 BuffSource::World,
354 *read_data.time,
355 dest_info,
356 None,
357 )),
358 });
359 emitters.emit(BuffEvent {
361 entity,
362 buff_change: BuffChange::Add(Buff::new(
363 BuffKind::Frozen,
364 BuffData::new(0.2, Some(Secs(3.0))),
365 Vec::new(),
366 BuffSource::World,
367 *read_data.time,
368 dest_info,
369 None,
370 )),
371 });
372 }
373 if matches!(
374 physics_state.on_ground.and_then(|b| b.get_sprite()),
375 Some(SpriteKind::FireBlock)
376 ) {
377 emitters.emit(BuffEvent {
379 entity,
380 buff_change: BuffChange::Add(Buff::new(
381 BuffKind::Burning,
382 BuffData::new(20.0, None),
383 Vec::new(),
384 BuffSource::World,
385 *read_data.time,
386 dest_info,
387 None,
388 )),
389 });
390 }
391 if matches!(
393 physics_state.in_fluid,
394 Some(Fluid::Liquid {
395 kind: LiquidKind::Lava,
396 ..
397 })
398 ) {
399 emitters.emit(BuffEvent {
401 entity,
402 buff_change: BuffChange::Add(Buff::new(
403 BuffKind::Burning,
404 BuffData::new(20.0, None),
405 vec![BuffCategory::Natural],
406 BuffSource::World,
407 *read_data.time,
408 dest_info,
409 None,
410 )),
411 });
412 } else if matches!(
413 physics_state.in_fluid,
414 Some(Fluid::Liquid {
415 kind: LiquidKind::Water,
416 ..
417 })
418 ) && buff_comp.kinds[BuffKind::Burning].is_some()
419 {
420 emitters.emit(BuffEvent {
422 entity,
423 buff_change: BuffChange::RemoveByKind(BuffKind::Burning),
424 });
425 }
426 }
427
428 let mut expired_buffs = Vec::<BuffKey>::new();
429
430 for (buff_key, buff) in &buff_comp.buffs {
433 let keep = buff.cat_ids.iter().all(|cat| match cat {
434 BuffCategory::FromActiveAura(source, key) => {
435 let Some(source_entity) = read_data.id_maps.uid_entity(*source) else {
436 return false;
437 };
438
439 let Some(aura) = read_data
440 .auras
441 .get(source_entity)
442 .and_then(|aura| aura.auras.get(*key))
443 else {
444 return false;
445 };
446
447 let (Some(pos), Some(aura_pos)) = (
448 read_data.positions.get(entity),
449 read_data.positions.get(source_entity),
450 ) else {
451 return false;
452 };
453
454 pos.0.distance_squared(aura_pos.0) <= aura.radius.powi(2)
455 },
456 BuffCategory::FromLink(l) => l.exists(),
457 _ => true,
458 });
459
460 if !keep {
461 expired_buffs.push(buff_key);
462 emitters.emit(BuffEvent {
463 entity,
464 buff_change: BuffChange::Add(Buff::new(
465 buff.kind,
466 buff.data,
467 buff.cat_ids
468 .iter()
469 .filter(|cat_id| {
470 !matches!(
471 cat_id,
472 BuffCategory::FromActiveAura(..)
473 | BuffCategory::FromLink(..)
474 )
475 })
476 .cloned()
477 .collect::<Vec<_>>(),
478 buff.source,
479 *read_data.time,
480 dest_info,
481 None,
482 )),
483 });
484 }
485 }
486
487 buff_comp.buffs.iter().for_each(|(buff_key, buff)| {
488 if buff.end_time.is_some_and(|end| end.0 < read_data.time.0) {
489 expired_buffs.push(buff_key)
490 }
491 });
492
493 let infinite_damage_reduction = (Damage::compute_damage_reduction(
494 None,
495 read_data.inventories.get(entity),
496 Some(&stat),
497 &read_data.msm,
498 ) - 1.0)
499 .abs()
500 < f32::EPSILON;
501 if infinite_damage_reduction {
502 for (key, buff) in buff_comp.buffs.iter() {
503 if !buff.kind.is_buff() {
504 expired_buffs.push(key);
505 }
506 }
507 }
508
509 stat.reset_temp_modifiers();
511
512 let mut body_override = None;
513
514 let mut buff_kinds = buff_comp
516 .kinds
517 .iter()
518 .filter_map(|(kind, keys)| keys.as_ref().map(|keys| (kind, keys.clone())))
519 .collect::<Vec<(BuffKind, (Vec<BuffKey>, Time))>>();
520 buff_kinds.sort_by_key(|(kind, _)| !kind.affects_subsequent_buffs());
521 for (buff_kind, (buff_keys, kind_start_time)) in buff_kinds.into_iter() {
522 let mut active_buff_keys = Vec::new();
523 if infinite_damage_reduction && !buff_kind.is_buff() {
524 continue;
525 }
526
527 if buff_kind.stacks() {
528 active_buff_keys = buff_keys;
530 } else {
531 active_buff_keys.push(buff_keys[0]);
533 }
534 for buff_key in active_buff_keys.into_iter() {
535 if let Some(buff) = buff_comp.buffs.get(buff_key) {
536 if buff.start_time.0 > read_data.time.0 {
538 continue;
539 }
540 let buff_owner = if let BuffSource::Character { by: owner } = buff.source {
542 Some(owner)
543 } else {
544 None
545 };
546
547 for effect in &buff.effects {
549 execute_effect(
550 effect,
551 buff.kind,
552 buff.start_time,
553 kind_start_time,
554 &read_data,
555 &mut stat,
556 body,
557 &mut body_override,
558 health,
559 energy,
560 entity,
561 buff_owner,
562 &mut emitters,
563 dt,
564 *read_data.time,
565 expired_buffs.contains(&buff_key),
566 buff_comp,
567 );
568 }
569 }
570 }
571 }
572
573 let new_body = body_override.unwrap_or(stat.original_body);
575 if new_body != *body {
576 emitters.emit(ChangeBodyEvent { entity, new_body });
577 }
578
579 if !expired_buffs.is_empty() {
581 emitters.emit(BuffEvent {
582 entity,
583 buff_change: BuffChange::RemoveByKey(expired_buffs),
584 });
585 }
586
587 if health.is_dead {
589 emitters.emit(BuffEvent {
590 entity,
591 buff_change: BuffChange::RemoveByCategory {
592 all_required: vec![],
593 any_required: vec![],
594 none_required: vec![BuffCategory::PersistOnDeath],
595 },
596 });
597 }
598 });
599 stats.set_event_emission(true);
601 }
602}
603
604#[expect(clippy::too_many_arguments)]
606fn execute_effect(
607 effect: &BuffEffect,
608 buff_kind: BuffKind,
609 buff_start_time: Time,
610 buff_kind_start_time: Time,
611 read_data: &ReadData,
612 stat: &mut Stats,
613 current_body: &Body,
614 body_override: &mut Option<Body>,
615 health: &Health,
616 energy: &Energy,
617 entity: Entity,
618 buff_owner: Option<Uid>,
619 server_emitter: &mut (
620 impl EmitExt<HealthChangeEvent> + EmitExt<EnergyChangeEvent> + EmitExt<BuffEvent>
621 ),
622 dt: f32,
623 time: Time,
624 buff_will_expire: bool,
625 buffs_comp: &Buffs,
626) {
627 let num_ticks = |tick_dur: Secs| {
628 let time_passed = time.0 - buff_start_time.0;
629 let dt = dt as f64;
630 let curr_tick = (time_passed / tick_dur.0).floor();
645 let prev_tick = ((time_passed - dt).max(0.0) / tick_dur.0).floor();
646 let whole_ticks = curr_tick - prev_tick;
647
648 if buff_will_expire {
649 let fractional_tick = (time_passed % tick_dur.0) / tick_dur.0;
652 Some((whole_ticks + fractional_tick) as f32)
653 } else if whole_ticks >= 1.0 {
654 Some(whole_ticks as f32)
655 } else {
656 None
657 }
658 };
659 match effect {
660 BuffEffect::HealthChangeOverTime {
661 rate,
662 kind,
663 instance,
664 tick_dur,
665 } => {
666 if let Some(num_ticks) = num_ticks(*tick_dur) {
667 let amount = *rate * num_ticks * tick_dur.0 as f32;
668
669 let (cause, by) = if amount != 0.0 {
670 (Some(DamageSource::Buff(buff_kind)), buff_owner)
671 } else {
672 (None, None)
673 };
674 let amount = match *kind {
675 ModifierKind::Additive => amount,
676 ModifierKind::Multiplicative => health.maximum() * amount,
677 };
678 let damage_contributor = by.and_then(|uid| {
679 read_data.id_maps.uid_entity(uid).map(|entity| {
680 DamageContributor::new(uid, read_data.groups.get(entity).cloned())
681 })
682 });
683 server_emitter.emit(HealthChangeEvent {
684 entity,
685 change: HealthChange {
686 amount,
687 by: damage_contributor,
688 cause,
689 time: *read_data.time,
690 precise: false,
691 instance: *instance,
692 },
693 });
694 };
695 },
696 BuffEffect::EnergyChangeOverTime {
697 rate,
698 kind,
699 tick_dur,
700 reset_rate_on_tick,
701 } => {
702 if let Some(num_ticks) = num_ticks(*tick_dur) {
703 let amount = *rate * num_ticks * tick_dur.0 as f32;
704
705 let amount = match *kind {
706 ModifierKind::Additive => amount,
707 ModifierKind::Multiplicative => energy.maximum() * amount,
708 };
709 server_emitter.emit(EnergyChangeEvent {
710 entity,
711 change: amount,
712 reset_rate: *reset_rate_on_tick,
713 });
714 };
715 },
716 BuffEffect::MaxHealthModifier { value, kind } => match kind {
717 ModifierKind::Additive => {
718 stat.max_health_modifiers.add_mod += *value;
719 },
720 ModifierKind::Multiplicative => {
721 stat.max_health_modifiers.mult_mod *= *value;
722 },
723 },
724 BuffEffect::MaxEnergyModifier { value, kind } => match kind {
725 ModifierKind::Additive => {
726 stat.max_energy_modifiers.add_mod += *value;
727 },
728 ModifierKind::Multiplicative => {
729 stat.max_energy_modifiers.mult_mod *= *value;
730 },
731 },
732 BuffEffect::DamageReduction(dr) => {
733 if *dr > 0.0 {
734 stat.damage_reduction.pos_mod = stat.damage_reduction.pos_mod.max(*dr);
735 } else {
736 stat.damage_reduction.neg_mod += dr;
737 }
738 },
739 BuffEffect::MaxHealthChangeOverTime {
740 rate,
741 kind,
742 target_fraction,
743 } => {
744 let potential_amount = (time.0 - buff_kind_start_time.0) as f32 * rate;
745
746 let potential_fraction = 1.0
748 + match kind {
749 ModifierKind::Additive => {
750 potential_amount / health.base_max()
753 },
754 ModifierKind::Multiplicative => {
755 potential_amount
757 },
758 };
759
760 let progress = if (1.0 - *target_fraction).abs() > f32::EPSILON {
764 (1.0 - potential_fraction) / (1.0 - *target_fraction)
765 } else {
766 1.0
767 };
768
769 let achieved_fraction = if progress > 1.0 {
772 *target_fraction
776 } else {
777 potential_fraction
779 };
780
781 stat.max_health_modifiers.mult_mod *= achieved_fraction;
783 },
784 BuffEffect::MovementSpeed(speed) => {
785 stat.move_speed_modifier *= *speed;
786 },
787 BuffEffect::AttackSpeed(speed) => {
788 stat.attack_speed_modifier *= *speed;
789 },
790 BuffEffect::RecoverySpeed(speed) => {
791 stat.recovery_speed_modifier *= *speed;
792 },
793 BuffEffect::GroundFriction(gf) => {
794 stat.friction_modifier *= *gf;
795 },
796 BuffEffect::PoiseReduction(pr) => {
797 if *pr > 0.0 {
798 stat.poise_reduction.pos_mod = stat.poise_reduction.pos_mod.max(*pr);
799 } else {
800 stat.poise_reduction.neg_mod += pr;
801 }
802 },
803 BuffEffect::PoiseDamageFromLostHealth(strength) => {
804 stat.poise_damage_modifier *= 1.0 + (1.0 - health.fraction()) * *strength;
805 },
806 BuffEffect::AttackDamage(dam) => {
807 stat.attack_damage_modifier *= *dam;
808 },
809 BuffEffect::PrecisionOverride(val) => {
810 stat.precision_multiplier_override = stat
812 .precision_multiplier_override
813 .map(|mult| mult.min(*val))
814 .or(Some(*val));
815 },
816 BuffEffect::PrecisionVulnerabilityOverride(val) => {
817 stat.precision_vulnerability_multiplier_override = stat
819 .precision_vulnerability_multiplier_override
820 .map(|mult| mult.max(*val))
821 .or(Some(*val));
822 },
823 BuffEffect::BodyChange(b) => {
824 if Some(current_body) != body_override.as_ref() {
831 *body_override = Some(*b)
832 }
833 },
834 BuffEffect::BuffImmunity(buff_kind) => {
835 if buffs_comp.contains(*buff_kind) {
836 server_emitter.emit(BuffEvent {
837 entity,
838 buff_change: BuffChange::RemoveByKind(*buff_kind),
839 });
840 }
841 },
842 BuffEffect::SwimSpeed(speed) => {
843 stat.swim_speed_modifier *= speed;
844 },
845 BuffEffect::AttackEffect(effect) => stat.effects_on_attack.push(effect.clone()),
846 BuffEffect::AttackPoise(p) => {
847 stat.poise_damage_modifier *= p;
848 },
849 BuffEffect::MitigationsPenetration(mp) => {
850 stat.mitigations_penetration =
851 1.0 - ((1.0 - stat.mitigations_penetration) * (1.0 - *mp));
852 },
853 BuffEffect::EnergyReward(er) => {
854 stat.energy_reward_modifier *= er;
855 },
856 BuffEffect::DamagedEffect(effect) => stat.effects_on_damaged.push(effect.clone()),
857 BuffEffect::DeathEffect(effect) => stat.effects_on_death.push(effect.clone()),
858 BuffEffect::DisableAuxiliaryAbilities => stat.disable_auxiliary_abilities = true,
859 BuffEffect::CrowdControlResistance(ccr) => {
860 stat.crowd_control_resistance += ccr;
861 },
862 BuffEffect::ItemEffectReduction(ier) => {
863 stat.item_effect_reduction *= 1.0 - ier;
864 },
865 };
866}