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