Skip to main content

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, 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        // Set to false to avoid spamming server
88        stats.set_event_emission(false);
89
90        // Put out underwater campfires. Logically belongs here since this system also
91        // removes burning, but campfires don't have healths/stats/energies/buffs, so
92        // this needs a separate loop.
93        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, //to improve iteration speed
99        )
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            // Assume that to_put_out_campfires is near to zero always, so this access isn't
137            // slower than parallel checking above
138            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            // Apply buffs to entity based off of their current physics_state
169            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                            // Terrain effects have no associated ability for there to be a target
182                            None,
183                        )),
184                    });
185                };
186                // Set nearby entities on fire if burning
187                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                            // NOTE: setting source as the burned character is
207                            // problematic for whole array of reasons.
208                            // 1) It would show non-sensical death message.
209                            // 2) It makes NPCs hate the victim of fire, which is rather annoying.
210                            // 3) It makes NPCs hate other NPCs and attempting to attack them, even
211                            //    when they are in the same group.
212                            //
213                            // We could reference original source, but it might
214                            // have some cheesing & griefing potential.
215                            //
216                            // Yes, with this implementation you could put
217                            // yourself on fire, and then harras NPCs but good
218                            // luck with that.
219                            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                                        // Can't mutably access stats, and for burning debuff stats
229                                        // has no effect (for now)
230                                        stats: None,
231                                        mass: read_data.masses.get(t_entity),
232                                    },
233                                    mass,
234                                    // There is no ability being cast that would cause there to be
235                                    // a target
236                                    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                    // If on ensnaring vines, apply partial ensnared debuff
247                    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                    // If on ensnaring web, apply ensnared debuff
258                    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                    // If touching Sea Urchin apply Bleeding buff
269                    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                    // TODO: Determine a better place to emit sprite change events
281                    if let Some(pos) = read_data.positions.get(entity) {
282                        // If touching Trap - change sprite and apply Bleeding buff
283                        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                    // If touching Iron Spike apply Bleeding buff
305                    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                    // If touching a hot surface apply Burning buff
316                    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                    // When standing on IceSpike, apply bleeding
323                    emit_terrain_buff(
324                        &mut emitters,
325                        BuffKind::Bleeding,
326                        BuffData::new(15.0, Some(Secs(0.1))),
327                    );
328                    // When standing on IceSpike also apply Frozen
329                    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                    // If on FireBlock vines, apply burning buff
340                    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                    // If in lava fluid, apply burning debuff
351                    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                    // If in water fluid and currently burning, remove burning debuffs
361                    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            // Replace buffs from an active aura with a normal buff when out of range of the
371            // aura or link no longer active
372            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                            // If we ever need to transfer the "target" entity from an expired
423                            // aura, we'll have to store the target entity somewhere (maybe buff,
424                            // maybe aura)? Revisit if this causes issues.
425                            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            // Call to reset stats to base values
454            stat.reset_temp_modifiers();
455
456            let mut body_override = None;
457
458            // Iterator over the lists of buffs by kind
459            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                    // Process all the buffs of this kind
473                    active_buff_keys = buff_keys;
474                } else {
475                    // Only process the strongest of this buff kind
476                    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                        // Skip the effect of buffs whose start delay hasn't expired.
481                        if buff.start_time.0 > read_data.time.0 {
482                            continue;
483                        }
484                        // Get buff owner?
485                        let buff_owner =
486                            if let BuffSource::Character { by: owner, .. } = buff.source {
487                                Some(owner)
488                            } else {
489                                None
490                            };
491
492                        // Now, execute the buff, based on it's delta
493                        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            // Update body if needed.
519            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            // Remove buffs that expire
529            if !expired_buffs.is_empty() {
530                emitters.emit(BuffEvent {
531                    entity,
532                    buff_change: BuffChange::RemoveByKey(expired_buffs),
533                });
534            }
535
536            // Remove buffs that don't persist on death
537            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        // Turned back to true
549        stats.set_event_emission(true);
550    }
551}
552
553// TODO: Globally disable this clippy lint
554#[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        // Number of ticks has 3 parts
583        //
584        // First part checks if delta time was larger than the tick duration, if it was
585        // determines number of ticks in that time
586        //
587        // Second part checks if delta time has just passed the threshold for a tick
588        // ending/starting (and accounts for if that delta time was longer than the tick
589        // duration)
590        // 0.000001 is to account for floating imprecision so this is not applied on the
591        // first tick
592        //
593        // Third part returns the fraction of the current time passed since the last
594        // time a tick duration would have happened, this is ignored (by flooring) when
595        // the buff is not ending, but is used if the buff is ending this tick
596        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            // If the buff is ending, include the fraction of progress towards the next
602            // tick.
603            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            // Percentage change that should be applied to max_health
709            let potential_fraction = 1.0
710                + match kind {
711                    ModifierKind::Additive => {
712                        // `rate * dt` is amount of health, dividing by base max
713                        // creates fraction
714                        potential_amount / health.base_max()
715                    },
716                    ModifierKind::Multiplicative => {
717                        // `rate * dt` is the fraction
718                        potential_amount
719                    },
720                };
721
722            // Potential progress towards target fraction, if
723            // target_fraction ~ 1.0 then set progress to 1.0 to avoid
724            // divide by zero
725            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            // Change achieved_fraction depending on what other buffs have
732            // occurred
733            let achieved_fraction = if progress > 1.0 {
734                // If potential fraction already beyond target fraction,
735                // simply multiply max_health_modifier by the target
736                // fraction, and set achieved fraction to target_fraction
737                *target_fraction
738            } else {
739                // Else have not achieved target yet, use potential_fraction
740                potential_fraction
741            };
742
743            // Apply achieved_fraction to max_health_modifier
744            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            // Use higher of precision multiplier overrides
789            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            // For when an entity is under the effects of multiple de/buffs that change the
796            // body, to avoid flickering between many bodies only change the body if the
797            // override body is not equal to the current body. (If the buff that caused the
798            // current body is still active, body override will eventually pick up on it,
799            // otherwise this will end up with a new body, though random depending on
800            // iteration order)
801            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}