veloren_common_systems/
buff.rs

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        // Set to false to avoid spamming server
87        stats.set_event_emission(false);
88
89        // Put out underwater campfires. Logically belongs here since this system also
90        // removes burning, but campfires don't have healths/stats/energies/buffs, so
91        // this needs a separate loop.
92        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, //to improve iteration speed
98        )
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            // Assume that to_put_out_campfires is near to zero always, so this access isn't
136            // slower than parallel checking above
137            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            // Apply buffs to entity based off of their current physics_state
167            if let Some(physics_state) = physics_state {
168                // Set nearby entities on fire if burning
169                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                            // NOTE: setting source as the burned character is
188                            // problematic for whole array of reasons.
189                            // 1) It would show non-sensical death message.
190                            // 2) It makes NPCs hate the victim of fire, which is rather annoying.
191                            // 3) It makes NPCs hate other NPCs and attempting to attack them, even
192                            //    when they are in the same group.
193                            //
194                            // We could reference original source, but it might
195                            // have some cheesing & griefing potential.
196                            //
197                            // Yes, with this implementation you could put
198                            // yourself on fire, and then harras NPCs but good
199                            // luck with that.
200                            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                                        // Can't mutably access stats, and for burning debuff stats
210                                        // has no effect (for now)
211                                        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                    // If on ensnaring vines, apply partial ensnared debuff
225                    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                    // If on ensnaring web, apply ensnared debuff
243                    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                    // If touching Sea Urchin apply Bleeding buff
261                    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                    // TODO: Determine a better place to emit sprite change events
280                    if let Some(pos) = read_data.positions.get(entity) {
281                        // If touching Trap - change sprite and apply Bleeding buff
282                        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                    // If touching Iron Spike apply Bleeding buff
311                    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                    // If touching a hot surface apply Burning buff
329                    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                    // When standing on IceSpike, apply bleeding
347                    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                    // When standing on IceSpike also apply Frozen
360                    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                    // If on FireBlock vines, apply burning buff
378                    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 on FireBlock vines, apply burning buff
392                if matches!(
393                    physics_state.in_fluid,
394                    Some(Fluid::Liquid {
395                        kind: LiquidKind::Lava,
396                        ..
397                    })
398                ) {
399                    // If in lava fluid, apply burning debuff
400                    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                    // If in water fluid and currently burning, remove burning debuffs
421                    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            // Replace buffs from an active aura with a normal buff when out of range of the
431            // aura or link no longer active
432            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            // Call to reset stats to base values
510            stat.reset_temp_modifiers();
511
512            let mut body_override = None;
513
514            // Iterator over the lists of buffs by kind
515            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                    // Process all the buffs of this kind
529                    active_buff_keys = buff_keys;
530                } else {
531                    // Only process the strongest of this buff kind
532                    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                        // Skip the effect of buffs whose start delay hasn't expired.
537                        if buff.start_time.0 > read_data.time.0 {
538                            continue;
539                        }
540                        // Get buff owner?
541                        let buff_owner = if let BuffSource::Character { by: owner } = buff.source {
542                            Some(owner)
543                        } else {
544                            None
545                        };
546
547                        // Now, execute the buff, based on it's delta
548                        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            // Update body if needed.
574            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            // Remove buffs that expire
580            if !expired_buffs.is_empty() {
581                emitters.emit(BuffEvent {
582                    entity,
583                    buff_change: BuffChange::RemoveByKey(expired_buffs),
584                });
585            }
586
587            // Remove buffs that don't persist on death
588            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        // Turned back to true
600        stats.set_event_emission(true);
601    }
602}
603
604// TODO: Globally disable this clippy lint
605#[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        // Number of ticks has 3 parts
631        //
632        // First part checks if delta time was larger than the tick duration, if it was
633        // determines number of ticks in that time
634        //
635        // Second part checks if delta time has just passed the threshold for a tick
636        // ending/starting (and accounts for if that delta time was longer than the tick
637        // duration)
638        // 0.000001 is to account for floating imprecision so this is not applied on the
639        // first tick
640        //
641        // Third part returns the fraction of the current time passed since the last
642        // time a tick duration would have happened, this is ignored (by flooring) when
643        // the buff is not ending, but is used if the buff is ending this tick
644        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            // If the buff is ending, include the fraction of progress towards the next
650            // tick.
651            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            // Percentage change that should be applied to max_health
747            let potential_fraction = 1.0
748                + match kind {
749                    ModifierKind::Additive => {
750                        // `rate * dt` is amount of health, dividing by base max
751                        // creates fraction
752                        potential_amount / health.base_max()
753                    },
754                    ModifierKind::Multiplicative => {
755                        // `rate * dt` is the fraction
756                        potential_amount
757                    },
758                };
759
760            // Potential progress towards target fraction, if
761            // target_fraction ~ 1.0 then set progress to 1.0 to avoid
762            // divide by zero
763            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            // Change achieved_fraction depending on what other buffs have
770            // occurred
771            let achieved_fraction = if progress > 1.0 {
772                // If potential fraction already beyond target fraction,
773                // simply multiply max_health_modifier by the target
774                // fraction, and set achieved fraction to target_fraction
775                *target_fraction
776            } else {
777                // Else have not achieved target yet, use potential_fraction
778                potential_fraction
779            };
780
781            // Apply achieved_fraction to max_health_modifier
782            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            // Use lower of precision multiplier overrides
811            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            // Use higher of precision multiplier overrides
818            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            // For when an entity is under the effects of multiple de/buffs that change the
825            // body, to avoid flickering between many bodies only change the body if the
826            // override body is not equal to the current body. (If the buff that caused the
827            // current body is still active, body override will eventually pick up on it,
828            // otherwise this will end up with a new body, though random depending on
829            // iteration order)
830            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}