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