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::thread_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
187 .gen_bool((dt * burning.data.strength / 5.0).clamp(0.0, 1.0).into())
188 {
189 emitters.emit(BuffEvent {
203 entity: t_entity,
204 buff_change: BuffChange::Add(Buff::new(
205 BuffKind::Burning,
206 BuffData::new(burning.data.strength, duration),
207 vec![BuffCategory::Natural],
208 BuffSource::World,
209 *read_data.time,
210 DestInfo {
211 stats: None,
214 mass: read_data.masses.get(t_entity),
215 },
216 mass,
217 )),
218 });
219 }
220 }
221 }
222 if matches!(
223 physics_state.on_ground.and_then(|b| b.get_sprite()),
224 Some(SpriteKind::EnsnaringVines)
225 ) {
226 emitters.emit(BuffEvent {
228 entity,
229 buff_change: BuffChange::Add(Buff::new(
230 BuffKind::Ensnared,
231 BuffData::new(0.5, Some(Secs(0.1))),
232 Vec::new(),
233 BuffSource::World,
234 *read_data.time,
235 dest_info,
236 None,
237 )),
238 });
239 }
240 if matches!(
241 physics_state.on_ground.and_then(|b| b.get_sprite()),
242 Some(SpriteKind::EnsnaringWeb)
243 ) {
244 emitters.emit(BuffEvent {
246 entity,
247 buff_change: BuffChange::Add(Buff::new(
248 BuffKind::Ensnared,
249 BuffData::new(1.0, Some(Secs(1.0))),
250 Vec::new(),
251 BuffSource::World,
252 *read_data.time,
253 dest_info,
254 None,
255 )),
256 });
257 }
258 if matches!(
259 physics_state.on_ground.and_then(|b| b.get_sprite()),
260 Some(SpriteKind::SeaUrchin)
261 ) {
262 emitters.emit(BuffEvent {
264 entity,
265 buff_change: BuffChange::Add(Buff::new(
266 BuffKind::Bleeding,
267 BuffData::new(1.0, Some(Secs(6.0))),
268 Vec::new(),
269 BuffSource::World,
270 *read_data.time,
271 dest_info,
272 None,
273 )),
274 });
275 }
276 if matches!(
277 physics_state.on_ground.and_then(|b| b.get_sprite()),
278 Some(SpriteKind::HaniwaTrap)
279 ) && !body.immune_to(BuffKind::Bleeding)
280 {
281 if let Some(pos) = read_data.positions.get(entity) {
283 emitters.emit(CreateSpriteEvent {
285 pos: Vec3::new(pos.0.x as i32, pos.0.y as i32, pos.0.z as i32 - 1),
286 sprite: SpriteKind::HaniwaTrapTriggered,
287 del_timeout: Some((4.0, 1.0)),
288 });
289 emitters.emit(SoundEvent {
290 sound: Sound::new(SoundKind::Trap, pos.0, 12.0, read_data.time.0),
291 });
292 emitters.emit(Outcome::Slash { pos: pos.0 });
293
294 emitters.emit(BuffEvent {
295 entity,
296 buff_change: BuffChange::Add(Buff::new(
297 BuffKind::Bleeding,
298 BuffData::new(5.0, Some(Secs(3.0))),
299 Vec::new(),
300 BuffSource::World,
301 *read_data.time,
302 dest_info,
303 None,
304 )),
305 });
306 }
307 }
308 if matches!(
309 physics_state.on_ground.and_then(|b| b.get_sprite()),
310 Some(SpriteKind::IronSpike | SpriteKind::HaniwaTrapTriggered)
311 ) {
312 emitters.emit(BuffEvent {
314 entity,
315 buff_change: BuffChange::Add(Buff::new(
316 BuffKind::Bleeding,
317 BuffData::new(1.0, Some(Secs(4.0))),
318 Vec::new(),
319 BuffSource::World,
320 *read_data.time,
321 dest_info,
322 None,
323 )),
324 });
325 }
326 if matches!(
327 physics_state.on_ground.and_then(|b| b.get_sprite()),
328 Some(SpriteKind::HotSurface)
329 ) {
330 emitters.emit(BuffEvent {
332 entity,
333 buff_change: BuffChange::Add(Buff::new(
334 BuffKind::Burning,
335 BuffData::new(10.0, None),
336 Vec::new(),
337 BuffSource::World,
338 *read_data.time,
339 dest_info,
340 None,
341 )),
342 });
343 }
344 if matches!(
345 physics_state.on_ground.and_then(|b| b.get_sprite()),
346 Some(SpriteKind::IceSpike)
347 ) {
348 emitters.emit(BuffEvent {
350 entity,
351 buff_change: BuffChange::Add(Buff::new(
352 BuffKind::Bleeding,
353 BuffData::new(15.0, Some(Secs(0.1))),
354 Vec::new(),
355 BuffSource::World,
356 *read_data.time,
357 dest_info,
358 None,
359 )),
360 });
361 emitters.emit(BuffEvent {
363 entity,
364 buff_change: BuffChange::Add(Buff::new(
365 BuffKind::Frozen,
366 BuffData::new(0.2, Some(Secs(3.0))),
367 Vec::new(),
368 BuffSource::World,
369 *read_data.time,
370 dest_info,
371 None,
372 )),
373 });
374 }
375 if matches!(
376 physics_state.on_ground.and_then(|b| b.get_sprite()),
377 Some(SpriteKind::FireBlock)
378 ) {
379 emitters.emit(BuffEvent {
381 entity,
382 buff_change: BuffChange::Add(Buff::new(
383 BuffKind::Burning,
384 BuffData::new(20.0, None),
385 Vec::new(),
386 BuffSource::World,
387 *read_data.time,
388 dest_info,
389 None,
390 )),
391 });
392 }
393 if matches!(
395 physics_state.in_fluid,
396 Some(Fluid::Liquid {
397 kind: LiquidKind::Lava,
398 ..
399 })
400 ) {
401 emitters.emit(BuffEvent {
403 entity,
404 buff_change: BuffChange::Add(Buff::new(
405 BuffKind::Burning,
406 BuffData::new(20.0, None),
407 vec![BuffCategory::Natural],
408 BuffSource::World,
409 *read_data.time,
410 dest_info,
411 None,
412 )),
413 });
414 } else if matches!(
415 physics_state.in_fluid,
416 Some(Fluid::Liquid {
417 kind: LiquidKind::Water,
418 ..
419 })
420 ) && buff_comp.kinds[BuffKind::Burning].is_some()
421 {
422 emitters.emit(BuffEvent {
424 entity,
425 buff_change: BuffChange::RemoveByKind(BuffKind::Burning),
426 });
427 }
428 }
429
430 let mut expired_buffs = Vec::<BuffKey>::new();
431
432 for (buff_key, buff) in &buff_comp.buffs {
435 let keep = buff.cat_ids.iter().all(|cat| match cat {
436 BuffCategory::FromActiveAura(source, key) => {
437 let Some(source_entity) = read_data.id_maps.uid_entity(*source) else {
438 return false;
439 };
440
441 let Some(aura) = read_data
442 .auras
443 .get(source_entity)
444 .and_then(|aura| aura.auras.get(*key))
445 else {
446 return false;
447 };
448
449 let (Some(pos), Some(aura_pos)) = (
450 read_data.positions.get(entity),
451 read_data.positions.get(source_entity),
452 ) else {
453 return false;
454 };
455
456 pos.0.distance_squared(aura_pos.0) <= aura.radius.powi(2)
457 },
458 BuffCategory::FromLink(l) => l.exists(),
459 _ => true,
460 });
461
462 if !keep {
463 expired_buffs.push(buff_key);
464 emitters.emit(BuffEvent {
465 entity,
466 buff_change: BuffChange::Add(Buff::new(
467 buff.kind,
468 buff.data,
469 buff.cat_ids
470 .iter()
471 .filter(|cat_id| {
472 !matches!(
473 cat_id,
474 BuffCategory::FromActiveAura(..)
475 | BuffCategory::FromLink(..)
476 )
477 })
478 .cloned()
479 .collect::<Vec<_>>(),
480 buff.source,
481 *read_data.time,
482 dest_info,
483 None,
484 )),
485 });
486 }
487 }
488
489 buff_comp.buffs.iter().for_each(|(buff_key, buff)| {
490 if buff.end_time.is_some_and(|end| end.0 < read_data.time.0) {
491 expired_buffs.push(buff_key)
492 }
493 });
494
495 let infinite_damage_reduction = (Damage::compute_damage_reduction(
496 None,
497 read_data.inventories.get(entity),
498 Some(&stat),
499 &read_data.msm,
500 ) - 1.0)
501 .abs()
502 < f32::EPSILON;
503 if infinite_damage_reduction {
504 for (key, buff) in buff_comp.buffs.iter() {
505 if !buff.kind.is_buff() {
506 expired_buffs.push(key);
507 }
508 }
509 }
510
511 stat.reset_temp_modifiers();
513
514 let mut body_override = None;
515
516 let mut buff_kinds = buff_comp
518 .kinds
519 .iter()
520 .filter_map(|(kind, keys)| keys.as_ref().map(|keys| (kind, keys.clone())))
521 .collect::<Vec<(BuffKind, (Vec<BuffKey>, Time))>>();
522 buff_kinds.sort_by_key(|(kind, _)| !kind.affects_subsequent_buffs());
523 for (buff_kind, (buff_keys, kind_start_time)) in buff_kinds.into_iter() {
524 let mut active_buff_keys = Vec::new();
525 if infinite_damage_reduction && !buff_kind.is_buff() {
526 continue;
527 }
528
529 if buff_kind.stacks() {
530 active_buff_keys = buff_keys;
532 } else {
533 active_buff_keys.push(buff_keys[0]);
535 }
536 for buff_key in active_buff_keys.into_iter() {
537 if let Some(buff) = buff_comp.buffs.get(buff_key) {
538 if buff.start_time.0 > read_data.time.0 {
540 continue;
541 }
542 let buff_owner = if let BuffSource::Character { by: owner } = buff.source {
544 Some(owner)
545 } else {
546 None
547 };
548
549 for effect in &buff.effects {
551 execute_effect(
552 effect,
553 buff.kind,
554 buff.start_time,
555 kind_start_time,
556 &read_data,
557 &mut stat,
558 body,
559 &mut body_override,
560 health,
561 energy,
562 entity,
563 buff_owner,
564 &mut emitters,
565 dt,
566 *read_data.time,
567 expired_buffs.contains(&buff_key),
568 buff_comp,
569 );
570 }
571 }
572 }
573 }
574
575 let new_body = body_override.unwrap_or(stat.original_body);
577 if new_body != *body {
578 emitters.emit(ChangeBodyEvent {
579 entity,
580 new_body,
581 permanent_change: None,
582 });
583 }
584
585 if !expired_buffs.is_empty() {
587 emitters.emit(BuffEvent {
588 entity,
589 buff_change: BuffChange::RemoveByKey(expired_buffs),
590 });
591 }
592
593 if health.is_dead {
595 emitters.emit(BuffEvent {
596 entity,
597 buff_change: BuffChange::RemoveByCategory {
598 all_required: vec![],
599 any_required: vec![],
600 none_required: vec![BuffCategory::PersistOnDeath],
601 },
602 });
603 }
604 });
605 stats.set_event_emission(true);
607 }
608}
609
610#[expect(clippy::too_many_arguments)]
612fn execute_effect(
613 effect: &BuffEffect,
614 buff_kind: BuffKind,
615 buff_start_time: Time,
616 buff_kind_start_time: Time,
617 read_data: &ReadData,
618 stat: &mut Stats,
619 current_body: &Body,
620 body_override: &mut Option<Body>,
621 health: &Health,
622 energy: &Energy,
623 entity: Entity,
624 buff_owner: Option<Uid>,
625 server_emitter: &mut (
626 impl EmitExt<HealthChangeEvent>
627 + EmitExt<EnergyChangeEvent>
628 + EmitExt<ComboChangeEvent>
629 + EmitExt<BuffEvent>
630 ),
631 dt: f32,
632 time: Time,
633 buff_will_expire: bool,
634 buffs_comp: &Buffs,
635) {
636 let num_ticks = |tick_dur: Secs| {
637 let time_passed = time.0 - buff_start_time.0;
638 let dt = dt as f64;
639 let curr_tick = (time_passed / tick_dur.0).floor();
654 let prev_tick = ((time_passed - dt).max(0.0) / tick_dur.0).floor();
655 let whole_ticks = curr_tick - prev_tick;
656
657 if buff_will_expire {
658 let fractional_tick = (time_passed % tick_dur.0) / tick_dur.0;
661 Some((whole_ticks + fractional_tick) as f32)
662 } else if whole_ticks >= 1.0 {
663 Some(whole_ticks as f32)
664 } else {
665 None
666 }
667 };
668 match effect {
669 BuffEffect::HealthChangeOverTime {
670 rate,
671 kind,
672 instance,
673 tick_dur,
674 } => {
675 if let Some(num_ticks) = num_ticks(*tick_dur) {
676 let amount = *rate * num_ticks * tick_dur.0 as f32;
677
678 let (cause, by) = if amount != 0.0 {
679 (Some(DamageSource::Buff(buff_kind)), buff_owner)
680 } else {
681 (None, None)
682 };
683 let amount = match *kind {
684 ModifierKind::Additive => amount,
685 ModifierKind::Multiplicative => health.maximum() * amount,
686 };
687 let damage_contributor = by.and_then(|uid| {
688 read_data.id_maps.uid_entity(uid).map(|entity| {
689 DamageContributor::new(uid, read_data.groups.get(entity).cloned())
690 })
691 });
692 server_emitter.emit(HealthChangeEvent {
693 entity,
694 change: HealthChange {
695 amount,
696 by: damage_contributor,
697 cause,
698 time: *read_data.time,
699 precise: false,
700 instance: *instance,
701 },
702 });
703 };
704 },
705 BuffEffect::EnergyChangeOverTime {
706 rate,
707 kind,
708 tick_dur,
709 reset_rate_on_tick,
710 } => {
711 if let Some(num_ticks) = num_ticks(*tick_dur) {
712 let amount = *rate * num_ticks * tick_dur.0 as f32;
713
714 let amount = match *kind {
715 ModifierKind::Additive => amount,
716 ModifierKind::Multiplicative => energy.maximum() * amount,
717 };
718 server_emitter.emit(EnergyChangeEvent {
719 entity,
720 change: amount,
721 reset_rate: *reset_rate_on_tick,
722 });
723 };
724 },
725 BuffEffect::ComboChangeOverTime { rate, tick_dur } => {
726 if let Some(num_ticks) = num_ticks(*tick_dur) {
727 let amount = (*rate * num_ticks * tick_dur.0 as f32) as i32;
728
729 server_emitter.emit(ComboChangeEvent {
730 entity,
731 change: amount,
732 });
733 };
734 },
735 BuffEffect::MaxHealthModifier { value, kind } => match kind {
736 ModifierKind::Additive => {
737 stat.max_health_modifiers.add_mod += *value;
738 },
739 ModifierKind::Multiplicative => {
740 stat.max_health_modifiers.mult_mod *= *value;
741 },
742 },
743 BuffEffect::MaxEnergyModifier { value, kind } => match kind {
744 ModifierKind::Additive => {
745 stat.max_energy_modifiers.add_mod += *value;
746 },
747 ModifierKind::Multiplicative => {
748 stat.max_energy_modifiers.mult_mod *= *value;
749 },
750 },
751 BuffEffect::DamageReduction(dr) => {
752 if *dr > 0.0 {
753 stat.damage_reduction.pos_mod = stat.damage_reduction.pos_mod.max(*dr);
754 } else {
755 stat.damage_reduction.neg_mod += dr;
756 }
757 },
758 BuffEffect::MaxHealthChangeOverTime {
759 rate,
760 kind,
761 target_fraction,
762 } => {
763 let potential_amount = (time.0 - buff_kind_start_time.0) as f32 * rate;
764
765 let potential_fraction = 1.0
767 + match kind {
768 ModifierKind::Additive => {
769 potential_amount / health.base_max()
772 },
773 ModifierKind::Multiplicative => {
774 potential_amount
776 },
777 };
778
779 let progress = if (1.0 - *target_fraction).abs() > f32::EPSILON {
783 (1.0 - potential_fraction) / (1.0 - *target_fraction)
784 } else {
785 1.0
786 };
787
788 let achieved_fraction = if progress > 1.0 {
791 *target_fraction
795 } else {
796 potential_fraction
798 };
799
800 stat.max_health_modifiers.mult_mod *= achieved_fraction;
802 },
803 BuffEffect::MovementSpeed(speed) => {
804 stat.move_speed_modifier *= *speed;
805 },
806 BuffEffect::AttackSpeed(speed) => {
807 stat.attack_speed_modifier *= *speed;
808 },
809 BuffEffect::RecoverySpeed(speed) => {
810 stat.recovery_speed_modifier *= *speed;
811 },
812 BuffEffect::GroundFriction(gf) => {
813 stat.friction_modifier *= *gf;
814 },
815 BuffEffect::PoiseReduction(pr) => {
816 if *pr > 0.0 {
817 stat.poise_reduction.pos_mod = stat.poise_reduction.pos_mod.max(*pr);
818 } else {
819 stat.poise_reduction.neg_mod += pr;
820 }
821 },
822 BuffEffect::PoiseDamageFromLostHealth(strength) => {
823 stat.poise_damage_modifier *= 1.0 + (1.0 - health.fraction()) * *strength;
824 },
825 BuffEffect::AttackDamage(dam) => {
826 stat.attack_damage_modifier *= *dam;
827 },
828 BuffEffect::PrecisionOverride(val) => {
829 stat.precision_multiplier_override = stat
831 .precision_multiplier_override
832 .map(|mult| mult.min(*val))
833 .or(Some(*val));
834 },
835 BuffEffect::PrecisionVulnerabilityOverride(val) => {
836 stat.precision_vulnerability_multiplier_override = stat
838 .precision_vulnerability_multiplier_override
839 .map(|mult| mult.max(*val))
840 .or(Some(*val));
841 },
842 BuffEffect::BodyChange(b) => {
843 if Some(current_body) != body_override.as_ref() {
850 *body_override = Some(*b)
851 }
852 },
853 BuffEffect::BuffImmunity(buff_kind) => {
854 if buffs_comp.contains(*buff_kind) {
855 server_emitter.emit(BuffEvent {
856 entity,
857 buff_change: BuffChange::RemoveByKind(*buff_kind),
858 });
859 }
860 },
861 BuffEffect::SwimSpeed(speed) => {
862 stat.swim_speed_modifier *= speed;
863 },
864 BuffEffect::AttackEffect(effect) => stat.effects_on_attack.push(effect.clone()),
865 BuffEffect::AttackPoise(p) => {
866 stat.poise_damage_modifier *= p;
867 },
868 BuffEffect::MitigationsPenetration(mp) => {
869 stat.mitigations_penetration =
870 1.0 - ((1.0 - stat.mitigations_penetration) * (1.0 - *mp));
871 },
872 BuffEffect::EnergyReward(er) => {
873 stat.energy_reward_modifier *= er;
874 },
875 BuffEffect::DamagedEffect(effect) => stat.effects_on_damaged.push(effect.clone()),
876 BuffEffect::DeathEffect(effect) => stat.effects_on_death.push(effect.clone()),
877 BuffEffect::DisableAuxiliaryAbilities => stat.disable_auxiliary_abilities = true,
878 BuffEffect::CrowdControlResistance(ccr) => {
879 stat.crowd_control_resistance += ccr;
880 },
881 BuffEffect::ItemEffectReduction(ier) => {
882 stat.item_effect_reduction *= 1.0 - ier;
883 },
884 };
885}