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::RngExt;
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 let emit_terrain_buff = |emitters: &mut EventEmitters, kind, data| {
171 emitters.emit(BuffEvent {
172 entity,
173 buff_change: BuffChange::Add(Buff::new(
174 kind,
175 data,
176 vec![],
177 BuffSource::World,
178 *read_data.time,
179 dest_info,
180 None,
181 None,
183 )),
184 });
185 };
186 if let Some((_, burning)) = buff_comp.iter_kind(BuffKind::Burning).next() {
188 for t_entity in physics_state.touch_entities.keys().filter_map(|te_uid| {
189 read_data.id_maps.uid_entity(*te_uid).filter(|te| {
190 combat::permit_pvp(
191 &read_data.alignments,
192 &read_data.players,
193 &read_data.entered_auras,
194 &read_data.id_maps,
195 Some(entity),
196 *te,
197 )
198 })
199 }) {
200 let duration = burning.data.duration.map(|d| d * 0.9);
201 if duration.is_none_or(|d| d.0 >= 1.0)
202 && rng.random_bool(
203 (dt * burning.data.strength / 5.0).clamp(0.0, 1.0).into(),
204 )
205 {
206 emitters.emit(BuffEvent {
220 entity: t_entity,
221 buff_change: BuffChange::Add(Buff::new(
222 BuffKind::Burning,
223 BuffData::new(burning.data.strength, duration),
224 vec![],
225 BuffSource::World,
226 *read_data.time,
227 DestInfo {
228 stats: None,
231 mass: read_data.masses.get(t_entity),
232 },
233 mass,
234 None,
237 )),
238 });
239 }
240 }
241 }
242 if matches!(
243 physics_state.on_ground.and_then(|b| b.get_sprite()),
244 Some(SpriteKind::EnsnaringVines)
245 ) {
246 emit_terrain_buff(
248 &mut emitters,
249 BuffKind::Ensnared,
250 BuffData::new(0.5, Some(Secs(0.1))),
251 );
252 }
253 if matches!(
254 physics_state.on_ground.and_then(|b| b.get_sprite()),
255 Some(SpriteKind::EnsnaringWeb)
256 ) {
257 emit_terrain_buff(
259 &mut emitters,
260 BuffKind::Ensnared,
261 BuffData::new(1.0, Some(Secs(1.0))),
262 );
263 }
264 if matches!(
265 physics_state.on_ground.and_then(|b| b.get_sprite()),
266 Some(SpriteKind::SeaUrchin)
267 ) {
268 emit_terrain_buff(
270 &mut emitters,
271 BuffKind::Bleeding,
272 BuffData::new(1.0, Some(Secs(6.0))),
273 );
274 }
275 if matches!(
276 physics_state.on_ground.and_then(|b| b.get_sprite()),
277 Some(SpriteKind::HaniwaTrap)
278 ) && !body.immune_to(BuffKind::Bleeding)
279 {
280 if let Some(pos) = read_data.positions.get(entity) {
282 emitters.emit(CreateSpriteEvent {
284 pos: Vec3::new(pos.0.x as i32, pos.0.y as i32, pos.0.z as i32 - 1),
285 sprite: SpriteKind::HaniwaTrapTriggered,
286 del_timeout: Some((4.0, 1.0)),
287 });
288 emitters.emit(SoundEvent {
289 sound: Sound::new(SoundKind::Trap, pos.0, 12.0, read_data.time.0),
290 });
291 emitters.emit(Outcome::Slash { pos: pos.0 });
292
293 emit_terrain_buff(
294 &mut emitters,
295 BuffKind::Bleeding,
296 BuffData::new(5.0, Some(Secs(3.0))),
297 );
298 }
299 }
300 if matches!(
301 physics_state.on_ground.and_then(|b| b.get_sprite()),
302 Some(SpriteKind::IronSpike | SpriteKind::HaniwaTrapTriggered)
303 ) {
304 emit_terrain_buff(
306 &mut emitters,
307 BuffKind::Bleeding,
308 BuffData::new(1.0, Some(Secs(4.0))),
309 );
310 }
311 if matches!(
312 physics_state.on_ground.and_then(|b| b.get_sprite()),
313 Some(SpriteKind::HotSurface)
314 ) {
315 emit_terrain_buff(&mut emitters, BuffKind::Burning, BuffData::new(10.0, None));
317 }
318 if matches!(
319 physics_state.on_ground.and_then(|b| b.get_sprite()),
320 Some(SpriteKind::IceSpike)
321 ) {
322 emit_terrain_buff(
324 &mut emitters,
325 BuffKind::Bleeding,
326 BuffData::new(15.0, Some(Secs(0.1))),
327 );
328 emit_terrain_buff(
330 &mut emitters,
331 BuffKind::Frozen,
332 BuffData::new(0.2, Some(Secs(3.0))),
333 );
334 }
335 if matches!(
336 physics_state.on_ground.and_then(|b| b.get_sprite()),
337 Some(SpriteKind::FireBlock)
338 ) {
339 emit_terrain_buff(&mut emitters, BuffKind::Burning, BuffData::new(20.0, None));
341 }
342 if matches!(
343 physics_state.in_fluid,
344 Some(Fluid::Liquid {
345 kind: LiquidKind::Lava,
346 ..
347 })
348 ) && !body.negates_buff(BuffKind::Burning)
349 {
350 emit_terrain_buff(&mut emitters, BuffKind::Burning, BuffData::new(20.0, None));
352 } else if matches!(
353 physics_state.in_fluid,
354 Some(Fluid::Liquid {
355 kind: LiquidKind::Water,
356 ..
357 })
358 ) && buff_comp.kinds[BuffKind::Burning].is_some()
359 {
360 emitters.emit(BuffEvent {
362 entity,
363 buff_change: BuffChange::RemoveByKind(BuffKind::Burning),
364 });
365 }
366 }
367
368 let mut expired_buffs = Vec::<BuffKey>::new();
369
370 for (buff_key, buff) in &buff_comp.buffs {
373 let keep = buff.cat_ids.iter().all(|cat| match cat {
374 BuffCategory::FromActiveAura(source, key) => {
375 let Some(source_entity) = read_data.id_maps.uid_entity(*source) else {
376 return false;
377 };
378
379 let Some(aura) = read_data
380 .auras
381 .get(source_entity)
382 .and_then(|aura| aura.auras.get(*key))
383 else {
384 return false;
385 };
386
387 let (Some(pos), Some(aura_pos)) = (
388 read_data.positions.get(entity),
389 read_data.positions.get(source_entity),
390 ) else {
391 return false;
392 };
393
394 pos.0.distance_squared(aura_pos.0) <= aura.radius.powi(2)
395 },
396 BuffCategory::FromLink(l) => l.exists(),
397 _ => true,
398 });
399
400 if !keep {
401 expired_buffs.push(buff_key);
402 emitters.emit(BuffEvent {
403 entity,
404 buff_change: BuffChange::Add(Buff::new(
405 buff.kind,
406 buff.data,
407 buff.cat_ids
408 .iter()
409 .filter(|cat_id| {
410 !matches!(
411 cat_id,
412 BuffCategory::FromActiveAura(..)
413 | BuffCategory::FromLink(..)
414 )
415 })
416 .cloned()
417 .collect::<Vec<_>>(),
418 buff.source,
419 *read_data.time,
420 dest_info,
421 None,
422 None,
426 )),
427 });
428 }
429 }
430
431 buff_comp.buffs.iter().for_each(|(buff_key, buff)| {
432 if buff.end_time.is_some_and(|end| end.0 < read_data.time.0) {
433 expired_buffs.push(buff_key)
434 }
435 });
436
437 let infinite_damage_reduction = (Damage::compute_damage_reduction(
438 None,
439 read_data.inventories.get(entity),
440 Some(&stat),
441 &read_data.msm,
442 ) - 1.0)
443 .abs()
444 < f32::EPSILON;
445 if infinite_damage_reduction {
446 for (key, buff) in buff_comp.buffs.iter() {
447 if !buff.kind.is_buff() {
448 expired_buffs.push(key);
449 }
450 }
451 }
452
453 stat.reset_temp_modifiers();
455
456 let mut body_override = None;
457
458 let mut buff_kinds = buff_comp
460 .kinds
461 .iter()
462 .filter_map(|(kind, keys)| keys.as_ref().map(|keys| (kind, keys.clone())))
463 .collect::<Vec<(BuffKind, (Vec<BuffKey>, Time))>>();
464 buff_kinds.sort_by_key(|(kind, _)| !kind.affects_subsequent_buffs());
465 for (buff_kind, (buff_keys, kind_start_time)) in buff_kinds.into_iter() {
466 let mut active_buff_keys = Vec::new();
467 if infinite_damage_reduction && !buff_kind.is_buff() {
468 continue;
469 }
470
471 if buff_kind.stacks() {
472 active_buff_keys = buff_keys;
474 } else {
475 active_buff_keys.push(buff_keys[0]);
477 }
478 for buff_key in active_buff_keys.into_iter() {
479 if let Some(buff) = buff_comp.buffs.get(buff_key) {
480 if buff.start_time.0 > read_data.time.0 {
482 continue;
483 }
484 let buff_owner =
486 if let BuffSource::Character { by: owner, .. } = buff.source {
487 Some(owner)
488 } else {
489 None
490 };
491
492 for effect in &buff.effects {
494 execute_effect(
495 effect,
496 buff.kind,
497 buff.start_time,
498 kind_start_time,
499 &read_data,
500 &mut stat,
501 body,
502 &mut body_override,
503 health,
504 energy,
505 entity,
506 buff_owner,
507 &mut emitters,
508 dt,
509 *read_data.time,
510 expired_buffs.contains(&buff_key),
511 buff_comp,
512 );
513 }
514 }
515 }
516 }
517
518 let new_body = body_override.unwrap_or(stat.original_body);
520 if new_body != *body {
521 emitters.emit(ChangeBodyEvent {
522 entity,
523 new_body,
524 permanent_change: None,
525 });
526 }
527
528 if !expired_buffs.is_empty() {
530 emitters.emit(BuffEvent {
531 entity,
532 buff_change: BuffChange::RemoveByKey(expired_buffs),
533 });
534 }
535
536 if health.is_dead {
538 emitters.emit(BuffEvent {
539 entity,
540 buff_change: BuffChange::RemoveByCategory {
541 all_required: vec![],
542 any_required: vec![],
543 none_required: vec![BuffCategory::PersistOnDeath],
544 },
545 });
546 }
547 });
548 stats.set_event_emission(true);
550 }
551}
552
553#[expect(clippy::too_many_arguments)]
555fn execute_effect(
556 effect: &BuffEffect,
557 buff_kind: BuffKind,
558 buff_start_time: Time,
559 buff_kind_start_time: Time,
560 read_data: &ReadData,
561 stat: &mut Stats,
562 current_body: &Body,
563 body_override: &mut Option<Body>,
564 health: &Health,
565 energy: &Energy,
566 entity: Entity,
567 buff_owner: Option<Uid>,
568 server_emitter: &mut (
569 impl EmitExt<HealthChangeEvent>
570 + EmitExt<EnergyChangeEvent>
571 + EmitExt<ComboChangeEvent>
572 + EmitExt<BuffEvent>
573 ),
574 dt: f32,
575 time: Time,
576 buff_will_expire: bool,
577 buffs_comp: &Buffs,
578) {
579 let num_ticks = |tick_dur: Secs| {
580 let time_passed = time.0 - buff_start_time.0;
581 let dt = dt as f64;
582 let curr_tick = (time_passed / tick_dur.0).floor();
597 let prev_tick = ((time_passed - dt).max(0.0) / tick_dur.0).floor();
598 let whole_ticks = curr_tick - prev_tick;
599
600 if buff_will_expire {
601 let fractional_tick = (time_passed % tick_dur.0) / tick_dur.0;
604 Some((whole_ticks + fractional_tick) as f32)
605 } else if whole_ticks >= 1.0 {
606 Some(whole_ticks as f32)
607 } else {
608 None
609 }
610 };
611 match effect {
612 BuffEffect::HealthChangeOverTime {
613 rate,
614 kind,
615 instance,
616 tick_dur,
617 } => {
618 if let Some(num_ticks) = num_ticks(*tick_dur) {
619 let amount = *rate * num_ticks * tick_dur.0 as f32;
620
621 let (cause, by) = if amount != 0.0 {
622 (Some(DamageSource::Buff(buff_kind)), buff_owner)
623 } else {
624 (None, None)
625 };
626 let amount = match *kind {
627 ModifierKind::Additive => amount,
628 ModifierKind::Multiplicative => health.maximum() * amount,
629 };
630 let damage_contributor = by.and_then(|uid| {
631 read_data.id_maps.uid_entity(uid).map(|entity| {
632 DamageContributor::new(uid, read_data.groups.get(entity).cloned())
633 })
634 });
635 server_emitter.emit(HealthChangeEvent {
636 entity,
637 change: HealthChange {
638 amount,
639 by: damage_contributor,
640 cause,
641 time: *read_data.time,
642 precise: false,
643 instance: *instance,
644 },
645 });
646 };
647 },
648 BuffEffect::EnergyChangeOverTime {
649 rate,
650 kind,
651 tick_dur,
652 reset_rate_on_tick,
653 } => {
654 if let Some(num_ticks) = num_ticks(*tick_dur) {
655 let amount = *rate * num_ticks * tick_dur.0 as f32;
656
657 let amount = match *kind {
658 ModifierKind::Additive => amount,
659 ModifierKind::Multiplicative => energy.maximum() * amount,
660 };
661 server_emitter.emit(EnergyChangeEvent {
662 entity,
663 change: amount,
664 reset_rate: *reset_rate_on_tick,
665 });
666 };
667 },
668 BuffEffect::ComboChangeOverTime { rate, tick_dur } => {
669 if let Some(num_ticks) = num_ticks(*tick_dur) {
670 let amount = (*rate * num_ticks * tick_dur.0 as f32) as i32;
671
672 server_emitter.emit(ComboChangeEvent {
673 entity,
674 change: amount,
675 });
676 };
677 },
678 BuffEffect::MaxHealthModifier { value, kind } => match kind {
679 ModifierKind::Additive => {
680 stat.max_health_modifiers.add_mod += *value;
681 },
682 ModifierKind::Multiplicative => {
683 stat.max_health_modifiers.mult_mod *= *value;
684 },
685 },
686 BuffEffect::MaxEnergyModifier { value, kind } => match kind {
687 ModifierKind::Additive => {
688 stat.max_energy_modifiers.add_mod += *value;
689 },
690 ModifierKind::Multiplicative => {
691 stat.max_energy_modifiers.mult_mod *= *value;
692 },
693 },
694 BuffEffect::DamageReduction(dr) => {
695 if *dr > 0.0 {
696 stat.damage_reduction.pos_mod = stat.damage_reduction.pos_mod.max(*dr);
697 } else {
698 stat.damage_reduction.neg_mod += dr;
699 }
700 },
701 BuffEffect::MaxHealthChangeOverTime {
702 rate,
703 kind,
704 target_fraction,
705 } => {
706 let potential_amount = (time.0 - buff_kind_start_time.0) as f32 * rate;
707
708 let potential_fraction = 1.0
710 + match kind {
711 ModifierKind::Additive => {
712 potential_amount / health.base_max()
715 },
716 ModifierKind::Multiplicative => {
717 potential_amount
719 },
720 };
721
722 let progress = if (1.0 - *target_fraction).abs() > f32::EPSILON {
726 (1.0 - potential_fraction) / (1.0 - *target_fraction)
727 } else {
728 1.0
729 };
730
731 let achieved_fraction = if progress > 1.0 {
734 *target_fraction
738 } else {
739 potential_fraction
741 };
742
743 stat.max_health_modifiers.mult_mod *= achieved_fraction;
745 },
746 BuffEffect::MovementSpeed(speed) => {
747 stat.move_speed_modifier *= *speed;
748 },
749 BuffEffect::ChargeMoveSpeed(speed) => {
750 stat.charge_move_speed_modifier *= *speed;
751 },
752 BuffEffect::BuildupMoveSpeed(speed) => {
753 stat.buildup_move_speed_modifier *= *speed;
754 },
755 BuffEffect::AttackSpeed(speed) => {
756 stat.attack_speed_modifier *= *speed;
757 },
758 BuffEffect::RecoverySpeed(speed) => {
759 stat.recovery_speed_modifier *= *speed;
760 },
761 BuffEffect::ChargingSpeed(speed) => {
762 stat.charge_speed_modifier *= *speed;
763 },
764 BuffEffect::BuildupSpeed(speed) => {
765 stat.buildup_speed_modifier *= *speed;
766 },
767 BuffEffect::GroundFriction(gf) => {
768 stat.friction_modifier *= *gf;
769 },
770 BuffEffect::PoiseReduction(pr) => {
771 if *pr > 0.0 {
772 stat.poise_reduction.pos_mod = stat.poise_reduction.pos_mod.max(*pr);
773 } else {
774 stat.poise_reduction.neg_mod += pr;
775 }
776 },
777 BuffEffect::PoiseDamageFromLostHealth(strength) => {
778 stat.poise_damage_modifier *= 1.0 + (1.0 - health.fraction()) * *strength;
779 },
780 BuffEffect::AttackDamage(dam) => {
781 stat.attack_damage_modifier *= *dam;
782 },
783 BuffEffect::PrecisionModifier(req, val, ovrd) => {
784 stat.conditional_precision_modifiers
785 .push((*req, *val, *ovrd));
786 },
787 BuffEffect::PrecisionVulnerabilityOverride(val) => {
788 stat.precision_vulnerability_multiplier_override = stat
790 .precision_vulnerability_multiplier_override
791 .map(|mult| mult.max(*val))
792 .or(Some(*val));
793 },
794 BuffEffect::BodyChange(b) => {
795 if Some(current_body) != body_override.as_ref() {
802 *body_override = Some(*b)
803 }
804 },
805 BuffEffect::BuffImmunity(buff_kind) => {
806 if buffs_comp.contains(*buff_kind) {
807 server_emitter.emit(BuffEvent {
808 entity,
809 buff_change: BuffChange::RemoveByKind(*buff_kind),
810 });
811 }
812 },
813 BuffEffect::SwimSpeed(speed) => {
814 stat.swim_speed_modifier *= speed;
815 },
816 BuffEffect::AttackEffect(effect) => stat.effects_on_attack.push(effect.clone()),
817 BuffEffect::AttackPoise(p) => {
818 stat.poise_damage_modifier *= p;
819 },
820 BuffEffect::MitigationsPenetration(mp) => {
821 stat.mitigations_penetration =
822 1.0 - ((1.0 - stat.mitigations_penetration) * (1.0 - *mp));
823 },
824 BuffEffect::EnergyReward(er) => {
825 stat.energy_reward_modifier *= er;
826 },
827 BuffEffect::EnergyEfficiency(ef) => {
828 stat.energy_efficiency_modifier *= *ef;
829 },
830 BuffEffect::DamagedEffect(effect) => stat.effects_on_damaged.push(effect.clone()),
831 BuffEffect::DeathEffect(effect) => stat.effects_on_death.push(effect.clone()),
832 BuffEffect::DisableAuxiliaryAbilities => stat.disable_auxiliary_abilities = true,
833 BuffEffect::CrowdControlResistance(ccr) => {
834 stat.crowd_control_resistance += ccr;
835 },
836 BuffEffect::ItemEffectReduction(ier) => {
837 stat.item_effect_reduction *= 1.0 - ier;
838 },
839 BuffEffect::AttackedModification(am) => {
840 stat.attacked_modifications.push(am.clone());
841 },
842 BuffEffect::PrecisionPowerMult(ppm) => {
843 stat.precision_power_mult *= ppm;
844 },
845 BuffEffect::KnockbackMult(km) => {
846 stat.knockback_mult *= km;
847 },
848 BuffEffect::ProjectileSpeedMult(ps) => {
849 stat.projectile_speed_mult *= *ps;
850 },
851 BuffEffect::ProjectileConstructorEffect(pce) => {
852 stat.projectile_constructor_effects.push(pce.clone())
853 },
854 BuffEffect::MarkEntity(e) => {
855 stat.marked_entities.push(*e);
856 },
857 };
858}