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 rayon::iter::ParallelIterator;
30use specs::{
31    Entities, Entity, LendJoin, ParJoin, Read, ReadExpect, ReadStorage, SystemData, WriteStorage,
32    shred,
33};
34use vek::Vec3;
35
36event_emitters! {
37    struct Events[EventEmitters] {
38        buff: BuffEvent,
39        change_body: ChangeBodyEvent,
40        remove_light: RemoveLightEmitterEvent,
41        health_change: HealthChangeEvent,
42        energy_change: EnergyChangeEvent,
43        sound: SoundEvent,
44        create_sprite: CreateSpriteEvent,
45        outcome: Outcome,
46    }
47}
48
49#[derive(SystemData)]
50pub struct ReadData<'a> {
51    entities: Entities<'a>,
52    dt: Read<'a, DeltaTime>,
53    events: Events<'a>,
54    inventories: ReadStorage<'a, Inventory>,
55    healths: ReadStorage<'a, Health>,
56    energies: ReadStorage<'a, Energy>,
57    physics_states: ReadStorage<'a, PhysicsState>,
58    groups: ReadStorage<'a, Group>,
59    id_maps: Read<'a, IdMaps>,
60    time: Read<'a, Time>,
61    msm: ReadExpect<'a, MaterialStatManifest>,
62    buffs: ReadStorage<'a, Buffs>,
63    auras: ReadStorage<'a, Auras>,
64    entered_auras: ReadStorage<'a, EnteredAuras>,
65    positions: ReadStorage<'a, Pos>,
66    bodies: ReadStorage<'a, Body>,
67    light_emitters: ReadStorage<'a, LightEmitter>,
68    alignments: ReadStorage<'a, Alignment>,
69    players: ReadStorage<'a, Player>,
70    uids: ReadStorage<'a, Uid>,
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 buff_join = (
149            &read_data.entities,
150            &read_data.buffs,
151            &mut stats,
152            &read_data.bodies,
153            &read_data.healths,
154            &read_data.energies,
155            read_data.uids.maybe(),
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, uid, physics_state, mass) =
162                comps;
163            let dest_info = DestInfo {
164                stats: Some(&stat),
165                mass,
166            };
167            // Apply buffs to entity based off of their current physics_state
168            if let Some(physics_state) = physics_state {
169                // Set nearby entities on fire if burning
170                if let Some((_, burning)) = buff_comp.iter_kind(BuffKind::Burning).next() {
171                    for t_entity in physics_state.touch_entities.keys().filter_map(|te_uid| {
172                        read_data.id_maps.uid_entity(*te_uid).filter(|te| {
173                            combat::permit_pvp(
174                                &read_data.alignments,
175                                &read_data.players,
176                                &read_data.entered_auras,
177                                &read_data.id_maps,
178                                Some(entity),
179                                *te,
180                            )
181                        })
182                    }) {
183                        let duration = burning.data.duration.map(|d| d * 0.9);
184                        if duration.is_none_or(|d| d.0 >= 1.0) {
185                            let source =
186                                uid.map_or(BuffSource::World, |u| BuffSource::Character { by: *u });
187                            emitters.emit(BuffEvent {
188                                entity: t_entity,
189                                buff_change: BuffChange::Add(Buff::new(
190                                    BuffKind::Burning,
191                                    BuffData::new(burning.data.strength, duration),
192                                    vec![BuffCategory::Natural],
193                                    source,
194                                    *read_data.time,
195                                    DestInfo {
196                                        // Can't mutably access stats, and for burning debuff stats
197                                        // has no effect (for now)
198                                        stats: None,
199                                        mass: read_data.masses.get(t_entity),
200                                    },
201                                    mass,
202                                )),
203                            });
204                        }
205                    }
206                }
207                if matches!(
208                    physics_state.on_ground.and_then(|b| b.get_sprite()),
209                    Some(SpriteKind::EnsnaringVines)
210                ) {
211                    // If on ensnaring vines, apply partial ensnared debuff
212                    emitters.emit(BuffEvent {
213                        entity,
214                        buff_change: BuffChange::Add(Buff::new(
215                            BuffKind::Ensnared,
216                            BuffData::new(0.5, Some(Secs(0.1))),
217                            Vec::new(),
218                            BuffSource::World,
219                            *read_data.time,
220                            dest_info,
221                            None,
222                        )),
223                    });
224                }
225                if matches!(
226                    physics_state.on_ground.and_then(|b| b.get_sprite()),
227                    Some(SpriteKind::EnsnaringWeb)
228                ) {
229                    // If on ensnaring web, apply ensnared debuff
230                    emitters.emit(BuffEvent {
231                        entity,
232                        buff_change: BuffChange::Add(Buff::new(
233                            BuffKind::Ensnared,
234                            BuffData::new(1.0, Some(Secs(1.0))),
235                            Vec::new(),
236                            BuffSource::World,
237                            *read_data.time,
238                            dest_info,
239                            None,
240                        )),
241                    });
242                }
243                if matches!(
244                    physics_state.on_ground.and_then(|b| b.get_sprite()),
245                    Some(SpriteKind::SeaUrchin)
246                ) {
247                    // If touching Sea Urchin apply Bleeding buff
248                    emitters.emit(BuffEvent {
249                        entity,
250                        buff_change: BuffChange::Add(Buff::new(
251                            BuffKind::Bleeding,
252                            BuffData::new(1.0, Some(Secs(6.0))),
253                            Vec::new(),
254                            BuffSource::World,
255                            *read_data.time,
256                            dest_info,
257                            None,
258                        )),
259                    });
260                }
261                if matches!(
262                    physics_state.on_ground.and_then(|b| b.get_sprite()),
263                    Some(SpriteKind::HaniwaTrap)
264                ) && !body.immune_to(BuffKind::Bleeding)
265                {
266                    // TODO: Determine a better place to emit sprite change events
267                    if let Some(pos) = read_data.positions.get(entity) {
268                        // If touching Trap - change sprite and apply Bleeding buff
269                        emitters.emit(CreateSpriteEvent {
270                            pos: Vec3::new(pos.0.x as i32, pos.0.y as i32, pos.0.z as i32 - 1),
271                            sprite: SpriteKind::HaniwaTrapTriggered,
272                            del_timeout: Some((4.0, 1.0)),
273                        });
274                        emitters.emit(SoundEvent {
275                            sound: Sound::new(SoundKind::Trap, pos.0, 12.0, read_data.time.0),
276                        });
277                        emitters.emit(Outcome::Slash { pos: pos.0 });
278
279                        emitters.emit(BuffEvent {
280                            entity,
281                            buff_change: BuffChange::Add(Buff::new(
282                                BuffKind::Bleeding,
283                                BuffData::new(5.0, Some(Secs(3.0))),
284                                Vec::new(),
285                                BuffSource::World,
286                                *read_data.time,
287                                dest_info,
288                                None,
289                            )),
290                        });
291                    }
292                }
293                if matches!(
294                    physics_state.on_ground.and_then(|b| b.get_sprite()),
295                    Some(SpriteKind::IronSpike | SpriteKind::HaniwaTrapTriggered)
296                ) {
297                    // If touching Iron Spike apply Bleeding buff
298                    emitters.emit(BuffEvent {
299                        entity,
300                        buff_change: BuffChange::Add(Buff::new(
301                            BuffKind::Bleeding,
302                            BuffData::new(1.0, Some(Secs(4.0))),
303                            Vec::new(),
304                            BuffSource::World,
305                            *read_data.time,
306                            dest_info,
307                            None,
308                        )),
309                    });
310                }
311                if matches!(
312                    physics_state.on_ground.and_then(|b| b.get_sprite()),
313                    Some(SpriteKind::HotSurface)
314                ) {
315                    // If touching a hot surface apply Burning buff
316                    emitters.emit(BuffEvent {
317                        entity,
318                        buff_change: BuffChange::Add(Buff::new(
319                            BuffKind::Burning,
320                            BuffData::new(10.0, None),
321                            Vec::new(),
322                            BuffSource::World,
323                            *read_data.time,
324                            dest_info,
325                            None,
326                        )),
327                    });
328                }
329                if matches!(
330                    physics_state.on_ground.and_then(|b| b.get_sprite()),
331                    Some(SpriteKind::IceSpike)
332                ) {
333                    // When standing on IceSpike, apply bleeding
334                    emitters.emit(BuffEvent {
335                        entity,
336                        buff_change: BuffChange::Add(Buff::new(
337                            BuffKind::Bleeding,
338                            BuffData::new(15.0, Some(Secs(0.1))),
339                            Vec::new(),
340                            BuffSource::World,
341                            *read_data.time,
342                            dest_info,
343                            None,
344                        )),
345                    });
346                    // When standing on IceSpike also apply Frozen
347                    emitters.emit(BuffEvent {
348                        entity,
349                        buff_change: BuffChange::Add(Buff::new(
350                            BuffKind::Frozen,
351                            BuffData::new(0.2, Some(Secs(3.0))),
352                            Vec::new(),
353                            BuffSource::World,
354                            *read_data.time,
355                            dest_info,
356                            None,
357                        )),
358                    });
359                }
360                if matches!(
361                    physics_state.on_ground.and_then(|b| b.get_sprite()),
362                    Some(SpriteKind::FireBlock)
363                ) {
364                    // If on FireBlock vines, apply burning buff
365                    emitters.emit(BuffEvent {
366                        entity,
367                        buff_change: BuffChange::Add(Buff::new(
368                            BuffKind::Burning,
369                            BuffData::new(20.0, None),
370                            Vec::new(),
371                            BuffSource::World,
372                            *read_data.time,
373                            dest_info,
374                            None,
375                        )),
376                    });
377                }
378                // If on FireBlock vines, apply burning buff
379                if matches!(
380                    physics_state.in_fluid,
381                    Some(Fluid::Liquid {
382                        kind: LiquidKind::Lava,
383                        ..
384                    })
385                ) {
386                    // If in lava fluid, apply burning debuff
387                    emitters.emit(BuffEvent {
388                        entity,
389                        buff_change: BuffChange::Add(Buff::new(
390                            BuffKind::Burning,
391                            BuffData::new(20.0, None),
392                            vec![BuffCategory::Natural],
393                            BuffSource::World,
394                            *read_data.time,
395                            dest_info,
396                            None,
397                        )),
398                    });
399                } else if matches!(
400                    physics_state.in_fluid,
401                    Some(Fluid::Liquid {
402                        kind: LiquidKind::Water,
403                        ..
404                    })
405                ) && buff_comp.kinds[BuffKind::Burning].is_some()
406                {
407                    // If in water fluid and currently burning, remove burning debuffs
408                    emitters.emit(BuffEvent {
409                        entity,
410                        buff_change: BuffChange::RemoveByKind(BuffKind::Burning),
411                    });
412                }
413            }
414
415            let mut expired_buffs = Vec::<BuffKey>::new();
416
417            // Replace buffs from an active aura with a normal buff when out of range of the
418            // aura
419            buff_comp
420                .buffs
421                .iter()
422                .filter_map(|(buff_key, buff)| {
423                    if let Some((uid, aura_key)) = buff.cat_ids.iter().find_map(|cat_id| {
424                        if let BuffCategory::FromActiveAura(uid, aura_key) = cat_id {
425                            Some((uid, aura_key))
426                        } else {
427                            None
428                        }
429                    }) {
430                        Some((buff_key, buff, uid, aura_key))
431                    } else {
432                        None
433                    }
434                })
435                .for_each(|(buff_key, buff, uid, aura_key)| {
436                    let replace = if let Some(aura_entity) = read_data.id_maps.uid_entity(*uid) {
437                        if let Some(aura) = read_data
438                            .auras
439                            .get(aura_entity)
440                            .and_then(|auras| auras.auras.get(*aura_key))
441                        {
442                            if let (Some(pos), Some(aura_pos)) = (
443                                read_data.positions.get(entity),
444                                read_data.positions.get(aura_entity),
445                            ) {
446                                pos.0.distance_squared(aura_pos.0) > aura.radius.powi(2)
447                            } else {
448                                true
449                            }
450                        } else {
451                            true
452                        }
453                    } else {
454                        true
455                    };
456                    if replace {
457                        expired_buffs.push(buff_key);
458                        emitters.emit(BuffEvent {
459                            entity,
460                            buff_change: BuffChange::Add(Buff::new(
461                                buff.kind,
462                                buff.data,
463                                buff.cat_ids
464                                    .iter()
465                                    .copied()
466                                    .filter(|cat_id| {
467                                        !matches!(cat_id, BuffCategory::FromActiveAura(..))
468                                    })
469                                    .collect::<Vec<_>>(),
470                                buff.source,
471                                *read_data.time,
472                                dest_info,
473                                None,
474                            )),
475                        });
476                    }
477                });
478
479            buff_comp.buffs.iter().for_each(|(buff_key, buff)| {
480                if buff.end_time.is_some_and(|end| end.0 < read_data.time.0) {
481                    expired_buffs.push(buff_key)
482                }
483            });
484
485            let infinite_damage_reduction = (Damage::compute_damage_reduction(
486                None,
487                read_data.inventories.get(entity),
488                Some(&stat),
489                &read_data.msm,
490            ) - 1.0)
491                .abs()
492                < f32::EPSILON;
493            if infinite_damage_reduction {
494                for (key, buff) in buff_comp.buffs.iter() {
495                    if !buff.kind.is_buff() {
496                        expired_buffs.push(key);
497                    }
498                }
499            }
500
501            // Call to reset stats to base values
502            stat.reset_temp_modifiers();
503
504            let mut body_override = None;
505
506            // Iterator over the lists of buffs by kind
507            let mut buff_kinds = buff_comp
508                .kinds
509                .iter()
510                .filter_map(|(kind, keys)| keys.as_ref().map(|keys| (kind, keys.clone())))
511                .collect::<Vec<(BuffKind, (Vec<BuffKey>, Time))>>();
512            buff_kinds.sort_by_key(|(kind, _)| !kind.affects_subsequent_buffs());
513            for (buff_kind, (buff_keys, kind_start_time)) in buff_kinds.into_iter() {
514                let mut active_buff_keys = Vec::new();
515                if infinite_damage_reduction && !buff_kind.is_buff() {
516                    continue;
517                }
518
519                if buff_kind.stacks() {
520                    // Process all the buffs of this kind
521                    active_buff_keys = buff_keys;
522                } else {
523                    // Only process the strongest of this buff kind
524                    active_buff_keys.push(buff_keys[0]);
525                }
526                for buff_key in active_buff_keys.into_iter() {
527                    if let Some(buff) = buff_comp.buffs.get(buff_key) {
528                        // Skip the effect of buffs whose start delay hasn't expired.
529                        if buff.start_time.0 > read_data.time.0 {
530                            continue;
531                        }
532                        // Get buff owner?
533                        let buff_owner = if let BuffSource::Character { by: owner } = buff.source {
534                            Some(owner)
535                        } else {
536                            None
537                        };
538
539                        // Now, execute the buff, based on it's delta
540                        for effect in &buff.effects {
541                            execute_effect(
542                                effect,
543                                buff.kind,
544                                buff.start_time,
545                                kind_start_time,
546                                &read_data,
547                                &mut stat,
548                                body,
549                                &mut body_override,
550                                health,
551                                energy,
552                                entity,
553                                buff_owner,
554                                &mut emitters,
555                                dt,
556                                *read_data.time,
557                                expired_buffs.contains(&buff_key),
558                                buff_comp,
559                            );
560                        }
561                    }
562                }
563            }
564
565            // Update body if needed.
566            let new_body = body_override.unwrap_or(stat.original_body);
567            if new_body != *body {
568                emitters.emit(ChangeBodyEvent { entity, new_body });
569            }
570
571            // Remove buffs that expire
572            if !expired_buffs.is_empty() {
573                emitters.emit(BuffEvent {
574                    entity,
575                    buff_change: BuffChange::RemoveByKey(expired_buffs),
576                });
577            }
578
579            // Remove buffs that don't persist on death
580            if health.is_dead {
581                emitters.emit(BuffEvent {
582                    entity,
583                    buff_change: BuffChange::RemoveByCategory {
584                        all_required: vec![],
585                        any_required: vec![],
586                        none_required: vec![BuffCategory::PersistOnDeath],
587                    },
588                });
589            }
590        });
591        // Turned back to true
592        stats.set_event_emission(true);
593    }
594}
595
596// TODO: Globally disable this clippy lint
597#[expect(clippy::too_many_arguments)]
598fn execute_effect(
599    effect: &BuffEffect,
600    buff_kind: BuffKind,
601    buff_start_time: Time,
602    buff_kind_start_time: Time,
603    read_data: &ReadData,
604    stat: &mut Stats,
605    current_body: &Body,
606    body_override: &mut Option<Body>,
607    health: &Health,
608    energy: &Energy,
609    entity: Entity,
610    buff_owner: Option<Uid>,
611    server_emitter: &mut (
612             impl EmitExt<HealthChangeEvent> + EmitExt<EnergyChangeEvent> + EmitExt<BuffEvent>
613         ),
614    dt: f32,
615    time: Time,
616    buff_will_expire: bool,
617    buffs_comp: &Buffs,
618) {
619    let num_ticks = |tick_dur: Secs| {
620        let time_passed = time.0 - buff_start_time.0;
621        let dt = dt as f64;
622        // Number of ticks has 3 parts
623        //
624        // First part checks if delta time was larger than the tick duration, if it was
625        // determines number of ticks in that time
626        //
627        // Second part checks if delta time has just passed the threshold for a tick
628        // ending/starting (and accounts for if that delta time was longer than the tick
629        // duration)
630        // 0.000001 is to account for floating imprecision so this is not applied on the
631        // first tick
632        //
633        // Third part returns the fraction of the current time passed since the last
634        // time a tick duration would have happened, this is ignored (by flooring) when
635        // the buff is not ending, but is used if the buff is ending this tick
636        let curr_tick = (time_passed / tick_dur.0).floor();
637        let prev_tick = ((time_passed - dt).max(0.0) / tick_dur.0).floor();
638        let whole_ticks = curr_tick - prev_tick;
639
640        if buff_will_expire {
641            // If the buff is ending, include the fraction of progress towards the next
642            // tick.
643            let fractional_tick = (time_passed % tick_dur.0) / tick_dur.0;
644            Some((whole_ticks + fractional_tick) as f32)
645        } else if whole_ticks >= 1.0 {
646            Some(whole_ticks as f32)
647        } else {
648            None
649        }
650    };
651    match effect {
652        BuffEffect::HealthChangeOverTime {
653            rate,
654            kind,
655            instance,
656            tick_dur,
657        } => {
658            if let Some(num_ticks) = num_ticks(*tick_dur) {
659                let amount = *rate * num_ticks * tick_dur.0 as f32;
660
661                let (cause, by) = if amount != 0.0 {
662                    (Some(DamageSource::Buff(buff_kind)), buff_owner)
663                } else {
664                    (None, None)
665                };
666                let amount = match *kind {
667                    ModifierKind::Additive => amount,
668                    ModifierKind::Multiplicative => health.maximum() * amount,
669                };
670                let damage_contributor = by.and_then(|uid| {
671                    read_data.id_maps.uid_entity(uid).map(|entity| {
672                        DamageContributor::new(uid, read_data.groups.get(entity).cloned())
673                    })
674                });
675                server_emitter.emit(HealthChangeEvent {
676                    entity,
677                    change: HealthChange {
678                        amount,
679                        by: damage_contributor,
680                        cause,
681                        time: *read_data.time,
682                        precise: false,
683                        instance: *instance,
684                    },
685                });
686            };
687        },
688        BuffEffect::EnergyChangeOverTime {
689            rate,
690            kind,
691            tick_dur,
692            reset_rate_on_tick,
693        } => {
694            if let Some(num_ticks) = num_ticks(*tick_dur) {
695                let amount = *rate * num_ticks * tick_dur.0 as f32;
696
697                let amount = match *kind {
698                    ModifierKind::Additive => amount,
699                    ModifierKind::Multiplicative => energy.maximum() * amount,
700                };
701                server_emitter.emit(EnergyChangeEvent {
702                    entity,
703                    change: amount,
704                    reset_rate: *reset_rate_on_tick,
705                });
706            };
707        },
708        BuffEffect::MaxHealthModifier { value, kind } => match kind {
709            ModifierKind::Additive => {
710                stat.max_health_modifiers.add_mod += *value;
711            },
712            ModifierKind::Multiplicative => {
713                stat.max_health_modifiers.mult_mod *= *value;
714            },
715        },
716        BuffEffect::MaxEnergyModifier { value, kind } => match kind {
717            ModifierKind::Additive => {
718                stat.max_energy_modifiers.add_mod += *value;
719            },
720            ModifierKind::Multiplicative => {
721                stat.max_energy_modifiers.mult_mod *= *value;
722            },
723        },
724        BuffEffect::DamageReduction(dr) => {
725            if *dr > 0.0 {
726                stat.damage_reduction.pos_mod = stat.damage_reduction.pos_mod.max(*dr);
727            } else {
728                stat.damage_reduction.neg_mod += dr;
729            }
730        },
731        BuffEffect::MaxHealthChangeOverTime {
732            rate,
733            kind,
734            target_fraction,
735        } => {
736            let potential_amount = (time.0 - buff_kind_start_time.0) as f32 * rate;
737
738            // Percentage change that should be applied to max_health
739            let potential_fraction = 1.0
740                + match kind {
741                    ModifierKind::Additive => {
742                        // `rate * dt` is amount of health, dividing by base max
743                        // creates fraction
744                        potential_amount / health.base_max()
745                    },
746                    ModifierKind::Multiplicative => {
747                        // `rate * dt` is the fraction
748                        potential_amount
749                    },
750                };
751
752            // Potential progress towards target fraction, if
753            // target_fraction ~ 1.0 then set progress to 1.0 to avoid
754            // divide by zero
755            let progress = if (1.0 - *target_fraction).abs() > f32::EPSILON {
756                (1.0 - potential_fraction) / (1.0 - *target_fraction)
757            } else {
758                1.0
759            };
760
761            // Change achieved_fraction depending on what other buffs have
762            // occurred
763            let achieved_fraction = if progress > 1.0 {
764                // If potential fraction already beyond target fraction,
765                // simply multiply max_health_modifier by the target
766                // fraction, and set achieved fraction to target_fraction
767                *target_fraction
768            } else {
769                // Else have not achieved target yet, use potential_fraction
770                potential_fraction
771            };
772
773            // Apply achieved_fraction to max_health_modifier
774            stat.max_health_modifiers.mult_mod *= achieved_fraction;
775        },
776        BuffEffect::MovementSpeed(speed) => {
777            stat.move_speed_modifier *= *speed;
778        },
779        BuffEffect::AttackSpeed(speed) => {
780            stat.attack_speed_modifier *= *speed;
781        },
782        BuffEffect::RecoverySpeed(speed) => {
783            stat.recovery_speed_modifier *= *speed;
784        },
785        BuffEffect::GroundFriction(gf) => {
786            stat.friction_modifier *= *gf;
787        },
788        BuffEffect::PoiseReduction(pr) => {
789            if *pr > 0.0 {
790                stat.poise_reduction.pos_mod = stat.poise_reduction.pos_mod.max(*pr);
791            } else {
792                stat.poise_reduction.neg_mod += pr;
793            }
794        },
795        BuffEffect::PoiseDamageFromLostHealth(strength) => {
796            stat.poise_damage_modifier *= 1.0 + (1.0 - health.fraction()) * *strength;
797        },
798        BuffEffect::AttackDamage(dam) => {
799            stat.attack_damage_modifier *= *dam;
800        },
801        BuffEffect::PrecisionOverride(val) => {
802            // Use lower of precision multiplier overrides
803            stat.precision_multiplier_override = stat
804                .precision_multiplier_override
805                .map(|mult| mult.min(*val))
806                .or(Some(*val));
807        },
808        BuffEffect::PrecisionVulnerabilityOverride(val) => {
809            // Use higher of precision multiplier overrides
810            stat.precision_vulnerability_multiplier_override = stat
811                .precision_vulnerability_multiplier_override
812                .map(|mult| mult.max(*val))
813                .or(Some(*val));
814        },
815        BuffEffect::BodyChange(b) => {
816            // For when an entity is under the effects of multiple de/buffs that change the
817            // body, to avoid flickering between many bodies only change the body if the
818            // override body is not equal to the current body. (If the buff that caused the
819            // current body is still active, body override will eventually pick up on it,
820            // otherwise this will end up with a new body, though random depending on
821            // iteration order)
822            if Some(current_body) != body_override.as_ref() {
823                *body_override = Some(*b)
824            }
825        },
826        BuffEffect::BuffImmunity(buff_kind) => {
827            if buffs_comp.contains(*buff_kind) {
828                server_emitter.emit(BuffEvent {
829                    entity,
830                    buff_change: BuffChange::RemoveByKind(*buff_kind),
831                });
832            }
833        },
834        BuffEffect::SwimSpeed(speed) => {
835            stat.swim_speed_modifier *= speed;
836        },
837        BuffEffect::AttackEffect(effect) => stat.effects_on_attack.push(effect.clone()),
838        BuffEffect::AttackPoise(p) => {
839            stat.poise_damage_modifier *= p;
840        },
841        BuffEffect::MitigationsPenetration(mp) => {
842            stat.mitigations_penetration =
843                1.0 - ((1.0 - stat.mitigations_penetration) * (1.0 - *mp));
844        },
845        BuffEffect::EnergyReward(er) => {
846            stat.energy_reward_modifier *= er;
847        },
848        BuffEffect::DamagedEffect(effect) => stat.effects_on_damaged.push(effect.clone()),
849        BuffEffect::DeathEffect(effect) => stat.effects_on_death.push(effect.clone()),
850        BuffEffect::DisableAuxiliaryAbilities => stat.disable_auxiliary_abilities = true,
851        BuffEffect::CrowdControlResistance(ccr) => {
852            stat.crowd_control_resistance += ccr;
853        },
854        BuffEffect::ItemEffectReduction(ier) => {
855            stat.item_effect_reduction *= 1.0 - ier;
856        },
857    };
858}