veloren_common/
combat.rs

1use crate::{
2    comp::{
3        Alignment, Body, Buffs, CharacterState, Combo, Energy, Group, Health, HealthChange,
4        Inventory, Mass, Ori, Player, Poise, PoiseChange, SkillSet, Stats,
5        ability::Capability,
6        aura::{AuraKindVariant, EnteredAuras},
7        buff::{Buff, BuffChange, BuffData, BuffKind, BuffSource, DestInfo},
8        inventory::{
9            item::{
10                ItemDesc, ItemKind, MaterialStatManifest,
11                armor::Protection,
12                tool::{self, ToolKind},
13            },
14            slot::EquipSlot,
15        },
16        skillset::SkillGroupKind,
17    },
18    effect::BuffEffect,
19    event::{
20        BuffEvent, ComboChangeEvent, EmitExt, EnergyChangeEvent, EntityAttackedHookEvent,
21        HealthChangeEvent, KnockbackEvent, ParryHookEvent, PoiseChangeEvent,
22    },
23    outcome::Outcome,
24    resources::{Secs, Time},
25    states::utils::StageSection,
26    uid::{IdMaps, Uid},
27    util::Dir,
28};
29use rand::Rng;
30use serde::{Deserialize, Serialize};
31use specs::{Entity as EcsEntity, ReadStorage};
32use std::ops::{Mul, MulAssign};
33use vek::*;
34
35#[derive(Copy, Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
36pub enum GroupTarget {
37    InGroup,
38    OutOfGroup,
39    All,
40}
41
42#[derive(Copy, Clone, Debug, Serialize, Deserialize)]
43pub enum AttackSource {
44    Melee,
45    Projectile,
46    Beam,
47    GroundShockwave,
48    AirShockwave,
49    UndodgeableShockwave,
50    Explosion,
51}
52
53pub const FULL_FLANK_ANGLE: f32 = std::f32::consts::PI / 4.0;
54pub const PARTIAL_FLANK_ANGLE: f32 = std::f32::consts::PI * 3.0 / 4.0;
55// NOTE: Do we want to change this to be a configurable parameter on body?
56pub const PROJECTILE_HEADSHOT_PROPORTION: f32 = 0.1;
57pub const BEAM_DURATION_PRECISION: f32 = 2.5;
58pub const MAX_BACK_FLANK_PRECISION: f32 = 0.75;
59pub const MAX_SIDE_FLANK_PRECISION: f32 = 0.25;
60pub const MAX_HEADSHOT_PRECISION: f32 = 1.0;
61pub const MAX_TOP_HEADSHOT_PRECISION: f32 = 0.5;
62pub const MAX_BEAM_DUR_PRECISION: f32 = 0.25;
63pub const MAX_MELEE_POISE_PRECISION: f32 = 0.5;
64pub const MAX_BLOCK_POISE_COST: f32 = 25.0;
65pub const PARRY_BONUS_MULTIPLIER: f32 = 5.0;
66pub const FALLBACK_BLOCK_STRENGTH: f32 = 5.0;
67pub const BEHIND_TARGET_ANGLE: f32 = 45.0;
68pub const BASE_PARRIED_POISE_PUNISHMENT: f32 = 100.0 / 3.5;
69
70#[derive(Copy, Clone)]
71pub struct AttackerInfo<'a> {
72    pub entity: EcsEntity,
73    pub uid: Uid,
74    pub group: Option<&'a Group>,
75    pub energy: Option<&'a Energy>,
76    pub combo: Option<&'a Combo>,
77    pub inventory: Option<&'a Inventory>,
78    pub stats: Option<&'a Stats>,
79    pub mass: Option<&'a Mass>,
80}
81
82#[derive(Copy, Clone)]
83pub struct TargetInfo<'a> {
84    pub entity: EcsEntity,
85    pub uid: Uid,
86    pub inventory: Option<&'a Inventory>,
87    pub stats: Option<&'a Stats>,
88    pub health: Option<&'a Health>,
89    pub pos: Vec3<f32>,
90    pub ori: Option<&'a Ori>,
91    pub char_state: Option<&'a CharacterState>,
92    pub energy: Option<&'a Energy>,
93    pub buffs: Option<&'a Buffs>,
94    pub mass: Option<&'a Mass>,
95}
96
97#[derive(Clone, Copy)]
98pub struct AttackOptions {
99    pub target_dodging: bool,
100    /// Result of [`permit_pvp`]
101    pub permit_pvp: bool,
102    pub target_group: GroupTarget,
103    /// When set to `true`, entities in the same group or pets & pet owners may
104    /// hit eachother albeit the target_group being OutOfGroup
105    pub allow_friendly_fire: bool,
106    pub precision_mult: Option<f32>,
107}
108
109#[derive(Clone, Debug, Serialize, Deserialize)] // TODO: Yeet clone derive
110pub struct Attack {
111    damages: Vec<AttackDamage>,
112    effects: Vec<AttackEffect>,
113    precision_multiplier: f32,
114}
115
116impl Default for Attack {
117    fn default() -> Self {
118        Self {
119            damages: Vec::new(),
120            effects: Vec::new(),
121            precision_multiplier: 1.0,
122        }
123    }
124}
125
126impl Attack {
127    #[must_use]
128    pub fn with_damage(mut self, damage: AttackDamage) -> Self {
129        self.damages.push(damage);
130        self
131    }
132
133    #[must_use]
134    pub fn with_effect(mut self, effect: AttackEffect) -> Self {
135        self.effects.push(effect);
136        self
137    }
138
139    #[must_use]
140    pub fn with_precision(mut self, precision_multiplier: f32) -> Self {
141        self.precision_multiplier = precision_multiplier;
142        self
143    }
144
145    #[must_use]
146    pub fn with_combo_requirement(self, combo: i32, requirement: CombatRequirement) -> Self {
147        self.with_effect(
148            AttackEffect::new(None, CombatEffect::Combo(combo)).with_requirement(requirement),
149        )
150    }
151
152    #[must_use]
153    pub fn with_combo(self, combo: i32) -> Self {
154        self.with_combo_requirement(combo, CombatRequirement::AnyDamage)
155    }
156
157    #[must_use]
158    pub fn with_combo_increment(self) -> Self { self.with_combo(1) }
159
160    pub fn effects(&self) -> impl Iterator<Item = &AttackEffect> { self.effects.iter() }
161
162    pub fn compute_block_damage_decrement(
163        attacker: Option<&AttackerInfo>,
164        damage_reduction: f32,
165        target: &TargetInfo,
166        source: AttackSource,
167        dir: Dir,
168        damage: Damage,
169        msm: &MaterialStatManifest,
170        time: Time,
171        emitters: &mut (impl EmitExt<ParryHookEvent> + EmitExt<PoiseChangeEvent>),
172        mut emit_outcome: impl FnMut(Outcome),
173    ) -> f32 {
174        if damage.value > 0.0 {
175            if let (Some(char_state), Some(ori), Some(inventory)) =
176                (target.char_state, target.ori, target.inventory)
177            {
178                let is_parry = char_state.is_parry(source);
179                let is_block = char_state.is_block(source);
180                let damage_value = damage.value * (1.0 - damage_reduction);
181                let mut block_strength = block_strength(inventory, char_state);
182
183                if ori.look_vec().angle_between(-dir.with_z(0.0)) < char_state.block_angle()
184                    && (is_parry || is_block)
185                    && block_strength > 0.0
186                {
187                    if is_parry {
188                        block_strength *= PARRY_BONUS_MULTIPLIER;
189
190                        emitters.emit(ParryHookEvent {
191                            defender: target.entity,
192                            attacker: attacker.map(|a| a.entity),
193                            source,
194                            poise_multiplier: 2.0 - (damage_value / block_strength).min(1.0),
195                        });
196                    }
197
198                    let poise_cost =
199                        (damage_value / block_strength).min(1.0) * MAX_BLOCK_POISE_COST;
200
201                    let poise_change = Poise::apply_poise_reduction(
202                        poise_cost,
203                        target.inventory,
204                        msm,
205                        target.char_state,
206                        target.stats,
207                    );
208
209                    emit_outcome(Outcome::Block {
210                        parry: is_parry,
211                        pos: target.pos,
212                        uid: target.uid,
213                    });
214                    emitters.emit(PoiseChangeEvent {
215                        entity: target.entity,
216                        change: PoiseChange {
217                            amount: -poise_change,
218                            impulse: *dir,
219                            by: attacker.map(|x| (*x).into()),
220                            cause: Some(damage.source),
221                            time,
222                        },
223                    });
224                    block_strength
225                } else {
226                    0.0
227                }
228            } else {
229                0.0
230            }
231        } else {
232            0.0
233        }
234    }
235
236    pub fn compute_damage_reduction(
237        attacker: Option<&AttackerInfo>,
238        target: &TargetInfo,
239        damage: Damage,
240        msm: &MaterialStatManifest,
241    ) -> f32 {
242        if damage.value > 0.0 {
243            let attacker_penetration = attacker
244                .and_then(|a| a.stats)
245                .map_or(0.0, |s| s.mitigations_penetration)
246                .clamp(0.0, 1.0);
247            let raw_damage_reduction =
248                Damage::compute_damage_reduction(Some(damage), target.inventory, target.stats, msm);
249
250            if raw_damage_reduction >= 1.0 {
251                raw_damage_reduction
252            } else {
253                (1.0 - attacker_penetration) * raw_damage_reduction
254            }
255        } else {
256            0.0
257        }
258    }
259
260    pub fn apply_attack(
261        &self,
262        attacker: Option<AttackerInfo>,
263        target: &TargetInfo,
264        dir: Dir,
265        options: AttackOptions,
266        // Currently strength_modifier just modifies damage,
267        // maybe look into modifying strength of other effects?
268        strength_modifier: f32,
269        attack_source: AttackSource,
270        time: Time,
271        emitters: &mut (
272                 impl EmitExt<HealthChangeEvent>
273                 + EmitExt<EnergyChangeEvent>
274                 + EmitExt<ParryHookEvent>
275                 + EmitExt<KnockbackEvent>
276                 + EmitExt<BuffEvent>
277                 + EmitExt<PoiseChangeEvent>
278                 + EmitExt<ComboChangeEvent>
279                 + EmitExt<EntityAttackedHookEvent>
280             ),
281        mut emit_outcome: impl FnMut(Outcome),
282        rng: &mut rand::rngs::ThreadRng,
283        damage_instance_offset: u64,
284    ) -> bool {
285        // TODO: Maybe move this higher and pass it as argument into this function?
286        let msm = &MaterialStatManifest::load().read();
287
288        let AttackOptions {
289            target_dodging,
290            permit_pvp,
291            allow_friendly_fire,
292            target_group,
293            precision_mult,
294        } = options;
295
296        // target == OutOfGroup is basic heuristic that this
297        // "attack" has negative effects.
298        //
299        // so if target dodges this "attack" or we don't want to harm target,
300        // it should avoid such "damage" or effect
301        let avoid_damage = |attack_damage: &AttackDamage| {
302            target_dodging
303                || (!permit_pvp && matches!(attack_damage.target, Some(GroupTarget::OutOfGroup)))
304        };
305        let avoid_effect = |attack_effect: &AttackEffect| {
306            target_dodging
307                || (!permit_pvp && matches!(attack_effect.target, Some(GroupTarget::OutOfGroup)))
308        };
309
310        let from_precision_mult = attacker
311            .and_then(|a| a.stats)
312            .and_then(|s| s.precision_multiplier_override)
313            .or(precision_mult);
314
315        let from_precision_vulnerability_mult = target
316            .stats
317            .and_then(|s| s.precision_vulnerability_multiplier_override);
318
319        let precision_mult = match (from_precision_mult, from_precision_vulnerability_mult) {
320            (Some(a), Some(b)) => Some(a.max(b)),
321            (Some(a), None) | (None, Some(a)) => Some(a),
322            (None, None) => None,
323        };
324
325        let mut is_applied = false;
326        let mut accumulated_damage = 0.0;
327        let damage_modifier = attacker
328            .and_then(|a| a.stats)
329            .map_or(1.0, |s| s.attack_damage_modifier);
330        for damage in self
331            .damages
332            .iter()
333            .filter(|d| {
334                allow_friendly_fire
335                    || d.target
336                        .is_none_or(|t| t == GroupTarget::All || t == target_group)
337            })
338            .filter(|d| !avoid_damage(d))
339        {
340            let damage_instance = damage.instance + damage_instance_offset;
341            is_applied = true;
342
343            let damage_reduction =
344                Attack::compute_damage_reduction(attacker.as_ref(), target, damage.damage, msm);
345
346            let block_damage_decrement = Attack::compute_block_damage_decrement(
347                attacker.as_ref(),
348                damage_reduction,
349                target,
350                attack_source,
351                dir,
352                damage.damage,
353                msm,
354                time,
355                emitters,
356                &mut emit_outcome,
357            );
358
359            let change = damage.damage.calculate_health_change(
360                damage_reduction,
361                block_damage_decrement,
362                attacker.map(|x| x.into()),
363                precision_mult,
364                self.precision_multiplier,
365                strength_modifier * damage_modifier,
366                time,
367                damage_instance,
368            );
369            let applied_damage = -change.amount;
370            accumulated_damage += applied_damage;
371
372            if change.amount.abs() > Health::HEALTH_EPSILON {
373                emitters.emit(HealthChangeEvent {
374                    entity: target.entity,
375                    change,
376                });
377                match damage.damage.kind {
378                    DamageKind::Slashing => {
379                        // For slashing damage, reduce target energy by some fraction of applied
380                        // damage. When target would lose more energy than they have, deal an
381                        // equivalent amount of damage
382                        if let Some(target_energy) = target.energy {
383                            let energy_change = applied_damage * SLASHING_ENERGY_FRACTION;
384                            if energy_change > target_energy.current() {
385                                let health_damage = energy_change - target_energy.current();
386                                accumulated_damage += health_damage;
387                                let health_change = HealthChange {
388                                    amount: -health_damage,
389                                    by: attacker.map(|x| x.into()),
390                                    cause: Some(damage.damage.source),
391                                    time,
392                                    precise: precision_mult.is_some(),
393                                    instance: damage_instance,
394                                };
395                                emitters.emit(HealthChangeEvent {
396                                    entity: target.entity,
397                                    change: health_change,
398                                });
399                            }
400                            emitters.emit(EnergyChangeEvent {
401                                entity: target.entity,
402                                change: -energy_change,
403                                reset_rate: false,
404                            });
405                        }
406                    },
407                    DamageKind::Crushing => {
408                        // For crushing damage, reduce target poise by some fraction of the amount
409                        // of damage that was reduced by target's protection
410                        // Damage reduction should never equal 1 here as otherwise the check above
411                        // that health change amount is greater than 0 would fail.
412                        let reduced_damage =
413                            applied_damage * damage_reduction / (1.0 - damage_reduction);
414                        let poise = reduced_damage
415                            * CRUSHING_POISE_FRACTION
416                            * attacker
417                                .and_then(|a| a.stats)
418                                .map_or(1.0, |s| s.poise_damage_modifier);
419                        let change = -Poise::apply_poise_reduction(
420                            poise,
421                            target.inventory,
422                            msm,
423                            target.char_state,
424                            target.stats,
425                        );
426                        let poise_change = PoiseChange {
427                            amount: change,
428                            impulse: *dir,
429                            by: attacker.map(|x| x.into()),
430                            cause: Some(damage.damage.source),
431                            time,
432                        };
433                        if change.abs() > Poise::POISE_EPSILON {
434                            // If target is in a stunned state, apply extra poise damage as health
435                            // damage instead
436                            if let Some(CharacterState::Stunned(data)) = target.char_state {
437                                let health_change =
438                                    change * data.static_data.poise_state.damage_multiplier();
439                                let health_change = HealthChange {
440                                    amount: health_change,
441                                    by: attacker.map(|x| x.into()),
442                                    cause: Some(damage.damage.source),
443                                    instance: damage_instance,
444                                    precise: precision_mult.is_some(),
445                                    time,
446                                };
447                                emitters.emit(HealthChangeEvent {
448                                    entity: target.entity,
449                                    change: health_change,
450                                });
451                            } else {
452                                emitters.emit(PoiseChangeEvent {
453                                    entity: target.entity,
454                                    change: poise_change,
455                                });
456                            }
457                        }
458                    },
459                    // Piercing damage ignores some penetration, and is handled when damage
460                    // reduction is computed Energy is a placeholder damage type
461                    DamageKind::Piercing | DamageKind::Energy => {},
462                }
463                for effect in damage.effects.iter() {
464                    match effect {
465                        CombatEffect::Knockback(kb) => {
466                            let impulse =
467                                kb.calculate_impulse(dir, target.char_state) * strength_modifier;
468                            if !impulse.is_approx_zero() {
469                                emitters.emit(KnockbackEvent {
470                                    entity: target.entity,
471                                    impulse,
472                                });
473                            }
474                        },
475                        CombatEffect::EnergyReward(ec) => {
476                            if let Some(attacker) = attacker {
477                                emitters.emit(EnergyChangeEvent {
478                                    entity: attacker.entity,
479                                    change: *ec
480                                        * compute_energy_reward_mod(attacker.inventory, msm)
481                                        * strength_modifier
482                                        * attacker.stats.map_or(1.0, |s| s.energy_reward_modifier),
483                                    reset_rate: false,
484                                });
485                            }
486                        },
487                        CombatEffect::Buff(b) => {
488                            if rng.gen::<f32>() < b.chance {
489                                emitters.emit(BuffEvent {
490                                    entity: target.entity,
491                                    buff_change: BuffChange::Add(b.to_buff(
492                                        time,
493                                        attacker,
494                                        target,
495                                        applied_damage,
496                                        strength_modifier,
497                                    )),
498                                });
499                            }
500                        },
501                        CombatEffect::Lifesteal(l) => {
502                            // Not modified by strength_modifier as damage already is
503                            if let Some(attacker_entity) = attacker.map(|a| a.entity) {
504                                let change = HealthChange {
505                                    amount: applied_damage * l,
506                                    by: attacker.map(|a| a.into()),
507                                    cause: None,
508                                    time,
509                                    precise: false,
510                                    instance: rand::random(),
511                                };
512                                if change.amount.abs() > Health::HEALTH_EPSILON {
513                                    emitters.emit(HealthChangeEvent {
514                                        entity: attacker_entity,
515                                        change,
516                                    });
517                                }
518                            }
519                        },
520                        CombatEffect::Poise(p) => {
521                            let change = -Poise::apply_poise_reduction(
522                                *p,
523                                target.inventory,
524                                msm,
525                                target.char_state,
526                                target.stats,
527                            ) * strength_modifier
528                                * attacker
529                                    .and_then(|a| a.stats)
530                                    .map_or(1.0, |s| s.poise_damage_modifier);
531                            if change.abs() > Poise::POISE_EPSILON {
532                                let poise_change = PoiseChange {
533                                    amount: change,
534                                    impulse: *dir,
535                                    by: attacker.map(|x| x.into()),
536                                    cause: Some(damage.damage.source),
537                                    time,
538                                };
539                                emitters.emit(PoiseChangeEvent {
540                                    entity: target.entity,
541                                    change: poise_change,
542                                });
543                            }
544                        },
545                        CombatEffect::Heal(h) => {
546                            let change = HealthChange {
547                                amount: *h * strength_modifier,
548                                by: attacker.map(|a| a.into()),
549                                cause: None,
550                                time,
551                                precise: false,
552                                instance: rand::random(),
553                            };
554                            if change.amount.abs() > Health::HEALTH_EPSILON {
555                                emitters.emit(HealthChangeEvent {
556                                    entity: target.entity,
557                                    change,
558                                });
559                            }
560                        },
561                        CombatEffect::Combo(c) => {
562                            // Not affected by strength modifier as integer
563                            if let Some(attacker_entity) = attacker.map(|a| a.entity) {
564                                emitters.emit(ComboChangeEvent {
565                                    entity: attacker_entity,
566                                    change: *c,
567                                });
568                            }
569                        },
570                        CombatEffect::StageVulnerable(damage, section) => {
571                            if target
572                                .char_state
573                                .is_some_and(|cs| cs.stage_section() == Some(*section))
574                            {
575                                let change = {
576                                    let mut change = change;
577                                    change.amount *= damage;
578                                    change
579                                };
580                                emitters.emit(HealthChangeEvent {
581                                    entity: target.entity,
582                                    change,
583                                });
584                            }
585                        },
586                        CombatEffect::RefreshBuff(chance, b) => {
587                            if rng.gen::<f32>() < *chance {
588                                emitters.emit(BuffEvent {
589                                    entity: target.entity,
590                                    buff_change: BuffChange::Refresh(*b),
591                                });
592                            }
593                        },
594                        CombatEffect::BuffsVulnerable(damage, buff) => {
595                            if target.buffs.is_some_and(|b| b.contains(*buff)) {
596                                let change = {
597                                    let mut change = change;
598                                    change.amount *= damage;
599                                    change
600                                };
601                                emitters.emit(HealthChangeEvent {
602                                    entity: target.entity,
603                                    change,
604                                });
605                            }
606                        },
607                        CombatEffect::StunnedVulnerable(damage) => {
608                            if target.char_state.is_some_and(|cs| cs.is_stunned()) {
609                                let change = {
610                                    let mut change = change;
611                                    change.amount *= damage;
612                                    change
613                                };
614                                emitters.emit(HealthChangeEvent {
615                                    entity: target.entity,
616                                    change,
617                                });
618                            }
619                        },
620                        CombatEffect::SelfBuff(b) => {
621                            if let Some(attacker) = attacker {
622                                if rng.gen::<f32>() < b.chance {
623                                    emitters.emit(BuffEvent {
624                                        entity: attacker.entity,
625                                        buff_change: BuffChange::Add(b.to_self_buff(
626                                            time,
627                                            attacker,
628                                            applied_damage,
629                                            strength_modifier,
630                                        )),
631                                    });
632                                }
633                            }
634                        },
635                    }
636                }
637            }
638        }
639        for effect in self
640            .effects
641            .iter()
642            .chain(
643                attacker
644                    .and_then(|attacker| attacker.stats)
645                    .iter()
646                    .flat_map(|stats| stats.effects_on_attack.iter()),
647            )
648            .filter(|e| {
649                allow_friendly_fire
650                    || e.target
651                        .is_none_or(|t| t == GroupTarget::All || t == target_group)
652            })
653            .filter(|e| !avoid_effect(e))
654        {
655            let requirements_met = effect.requirements.iter().all(|req| match req {
656                CombatRequirement::AnyDamage => accumulated_damage > 0.0 && target.health.is_some(),
657                CombatRequirement::Energy(r) => {
658                    if let Some(AttackerInfo {
659                        entity,
660                        energy: Some(e),
661                        ..
662                    }) = attacker
663                    {
664                        let sufficient_energy = e.current() >= *r;
665                        if sufficient_energy {
666                            emitters.emit(EnergyChangeEvent {
667                                entity,
668                                change: -*r,
669                                reset_rate: false,
670                            });
671                        }
672
673                        sufficient_energy
674                    } else {
675                        false
676                    }
677                },
678                CombatRequirement::Combo(r) => {
679                    if let Some(AttackerInfo {
680                        entity,
681                        combo: Some(c),
682                        ..
683                    }) = attacker
684                    {
685                        let sufficient_combo = c.counter() >= *r;
686                        if sufficient_combo {
687                            emitters.emit(ComboChangeEvent {
688                                entity,
689                                change: -(*r as i32),
690                            });
691                        }
692
693                        sufficient_combo
694                    } else {
695                        false
696                    }
697                },
698                CombatRequirement::TargetHasBuff(buff) => {
699                    target.buffs.is_some_and(|buffs| buffs.contains(*buff))
700                },
701                CombatRequirement::TargetPoised => {
702                    target.char_state.is_some_and(|cs| cs.is_stunned())
703                },
704                CombatRequirement::BehindTarget => {
705                    if let Some(ori) = target.ori {
706                        ori.look_vec().angle_between(dir.with_z(0.0)) < BEHIND_TARGET_ANGLE
707                    } else {
708                        false
709                    }
710                },
711                CombatRequirement::TargetBlocking => target
712                    .char_state
713                    .is_some_and(|cs| cs.is_block(attack_source) || cs.is_parry(attack_source)),
714            });
715            if requirements_met {
716                is_applied = true;
717                match effect.effect {
718                    CombatEffect::Knockback(kb) => {
719                        let impulse =
720                            kb.calculate_impulse(dir, target.char_state) * strength_modifier;
721                        if !impulse.is_approx_zero() {
722                            emitters.emit(KnockbackEvent {
723                                entity: target.entity,
724                                impulse,
725                            });
726                        }
727                    },
728                    CombatEffect::EnergyReward(ec) => {
729                        if let Some(attacker) = attacker {
730                            emitters.emit(EnergyChangeEvent {
731                                entity: attacker.entity,
732                                change: ec
733                                    * compute_energy_reward_mod(attacker.inventory, msm)
734                                    * strength_modifier
735                                    * attacker.stats.map_or(1.0, |s| s.energy_reward_modifier),
736                                reset_rate: false,
737                            });
738                        }
739                    },
740                    CombatEffect::Buff(b) => {
741                        if rng.gen::<f32>() < b.chance {
742                            emitters.emit(BuffEvent {
743                                entity: target.entity,
744                                buff_change: BuffChange::Add(b.to_buff(
745                                    time,
746                                    attacker,
747                                    target,
748                                    accumulated_damage,
749                                    strength_modifier,
750                                )),
751                            });
752                        }
753                    },
754                    CombatEffect::Lifesteal(l) => {
755                        // Not modified by strength_modifier as damage already is
756                        if let Some(attacker_entity) = attacker.map(|a| a.entity) {
757                            let change = HealthChange {
758                                amount: accumulated_damage * l,
759                                by: attacker.map(|a| a.into()),
760                                cause: None,
761                                time,
762                                precise: false,
763                                instance: rand::random(),
764                            };
765                            if change.amount.abs() > Health::HEALTH_EPSILON {
766                                emitters.emit(HealthChangeEvent {
767                                    entity: attacker_entity,
768                                    change,
769                                });
770                            }
771                        }
772                    },
773                    CombatEffect::Poise(p) => {
774                        let change = -Poise::apply_poise_reduction(
775                            p,
776                            target.inventory,
777                            msm,
778                            target.char_state,
779                            target.stats,
780                        ) * strength_modifier
781                            * attacker
782                                .and_then(|a| a.stats)
783                                .map_or(1.0, |s| s.poise_damage_modifier);
784                        if change.abs() > Poise::POISE_EPSILON {
785                            let poise_change = PoiseChange {
786                                amount: change,
787                                impulse: *dir,
788                                by: attacker.map(|x| x.into()),
789                                cause: Some(attack_source.into()),
790                                time,
791                            };
792                            emitters.emit(PoiseChangeEvent {
793                                entity: target.entity,
794                                change: poise_change,
795                            });
796                        }
797                    },
798                    CombatEffect::Heal(h) => {
799                        let change = HealthChange {
800                            amount: h * strength_modifier,
801                            by: attacker.map(|a| a.into()),
802                            cause: None,
803                            time,
804                            precise: false,
805                            instance: rand::random(),
806                        };
807                        if change.amount.abs() > Health::HEALTH_EPSILON {
808                            emitters.emit(HealthChangeEvent {
809                                entity: target.entity,
810                                change,
811                            });
812                        }
813                    },
814                    CombatEffect::Combo(c) => {
815                        // Not affected by strength modifier as integer
816                        if let Some(attacker_entity) = attacker.map(|a| a.entity) {
817                            emitters.emit(ComboChangeEvent {
818                                entity: attacker_entity,
819                                change: c,
820                            });
821                        }
822                    },
823                    // Only has an effect when attached to a damage
824                    CombatEffect::StageVulnerable(_, _) => {},
825                    CombatEffect::RefreshBuff(chance, b) => {
826                        if rng.gen::<f32>() < chance {
827                            emitters.emit(BuffEvent {
828                                entity: target.entity,
829                                buff_change: BuffChange::Refresh(b),
830                            });
831                        }
832                    },
833                    // Only has an effect when attached to a damage
834                    CombatEffect::BuffsVulnerable(_, _) => {},
835                    // Only has an effect when attached to a damage
836                    CombatEffect::StunnedVulnerable(_) => {},
837                    CombatEffect::SelfBuff(b) => {
838                        if let Some(attacker) = attacker {
839                            if rng.gen::<f32>() < b.chance {
840                                emitters.emit(BuffEvent {
841                                    entity: target.entity,
842                                    buff_change: BuffChange::Add(b.to_self_buff(
843                                        time,
844                                        attacker,
845                                        accumulated_damage,
846                                        strength_modifier,
847                                    )),
848                                });
849                            }
850                        }
851                    },
852                }
853            }
854        }
855        // Emits event to handle things that should happen for any successful attack,
856        // regardless of if the attack had any damages or effects in it
857        if is_applied {
858            emitters.emit(EntityAttackedHookEvent {
859                entity: target.entity,
860                attacker: attacker.map(|a| a.entity),
861            });
862        }
863        is_applied
864    }
865}
866
867pub fn allow_friendly_fire(
868    entered_auras: &ReadStorage<EnteredAuras>,
869    attacker: EcsEntity,
870    target: EcsEntity,
871) -> bool {
872    entered_auras
873        .get(attacker)
874        .zip(entered_auras.get(target))
875        .and_then(|(attacker, target)| {
876            Some((
877                attacker.auras.get(&AuraKindVariant::FriendlyFire)?,
878                target.auras.get(&AuraKindVariant::FriendlyFire)?,
879            ))
880        })
881        // Only allow friendly fire if both entities are affectd by the same FriendlyFire aura
882        .is_some_and(|(attacker, target)| attacker.intersection(target).next().is_some())
883}
884
885/// Function that checks for unintentional PvP between players.
886///
887/// Returns `false` if attack will create unintentional conflict,
888/// e.g. if player with PvE mode will harm pets of other players
889/// or other players will do the same to such player.
890///
891/// If both players have PvP mode enabled, interact with NPC and
892/// in any other case, this function will return `true`
893// TODO: add parameter for doing self-harm?
894pub fn permit_pvp(
895    alignments: &ReadStorage<Alignment>,
896    players: &ReadStorage<Player>,
897    entered_auras: &ReadStorage<EnteredAuras>,
898    id_maps: &IdMaps,
899    attacker: Option<EcsEntity>,
900    target: EcsEntity,
901) -> bool {
902    // Return owner entity if pet,
903    // or just return entity back otherwise
904    let owner_if_pet = |entity| {
905        let alignment = alignments.get(entity).copied();
906        if let Some(Alignment::Owned(uid)) = alignment {
907            // return original entity
908            // if can't get owner
909            id_maps.uid_entity(uid).unwrap_or(entity)
910        } else {
911            entity
912        }
913    };
914
915    // Just return ok if attacker is unknown, it's probably
916    // environment or command.
917    let attacker = match attacker {
918        Some(attacker) => attacker,
919        None => return true,
920    };
921
922    // "Dereference" to owner if this is a pet.
923    let attacker_owner = owner_if_pet(attacker);
924    let target_owner = owner_if_pet(target);
925
926    // If both players are in the same ForcePvP aura, allow them to harm eachother
927    if let (Some(attacker_auras), Some(target_auras)) = (
928        entered_auras.get(attacker_owner),
929        entered_auras.get(target_owner),
930    ) && attacker_auras
931        .auras
932        .get(&AuraKindVariant::ForcePvP)
933        .zip(target_auras.auras.get(&AuraKindVariant::ForcePvP))
934        // Only allow forced pvp if both entities are affectd by the same FriendlyFire aura
935        .is_some_and(|(attacker, target)| attacker.intersection(target).next().is_some())
936    {
937        return true;
938    }
939
940    // Prevent PvP between pets, unless friendly fire is enabled
941    //
942    // This code is NOT intended to prevent pet <-> owner combat,
943    // pets and their owners being in the same group should take care of that
944    if attacker_owner == target_owner {
945        return allow_friendly_fire(entered_auras, attacker, target);
946    }
947
948    // Get player components
949    let attacker_info = players.get(attacker_owner);
950    let target_info = players.get(target_owner);
951
952    // Return `true` if not players.
953    attacker_info
954        .zip(target_info)
955        .is_none_or(|(a, t)| a.may_harm(t))
956}
957
958#[derive(Clone, Debug, Serialize, Deserialize)]
959pub struct AttackDamage {
960    damage: Damage,
961    target: Option<GroupTarget>,
962    effects: Vec<CombatEffect>,
963    /// A random ID, used to group up attacks
964    instance: u64,
965}
966
967impl AttackDamage {
968    pub fn new(damage: Damage, target: Option<GroupTarget>, instance: u64) -> Self {
969        Self {
970            damage,
971            target,
972            effects: Vec::new(),
973            instance,
974        }
975    }
976
977    #[must_use]
978    pub fn with_effect(mut self, effect: CombatEffect) -> Self {
979        self.effects.push(effect);
980        self
981    }
982}
983
984#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
985pub struct AttackEffect {
986    target: Option<GroupTarget>,
987    effect: CombatEffect,
988    requirements: Vec<CombatRequirement>,
989}
990
991impl AttackEffect {
992    pub fn new(target: Option<GroupTarget>, effect: CombatEffect) -> Self {
993        Self {
994            target,
995            effect,
996            requirements: Vec::new(),
997        }
998    }
999
1000    #[must_use]
1001    pub fn with_requirement(mut self, requirement: CombatRequirement) -> Self {
1002        self.requirements.push(requirement);
1003        self
1004    }
1005
1006    pub fn effect(&self) -> &CombatEffect { &self.effect }
1007}
1008
1009#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq)]
1010pub enum CombatEffect {
1011    Heal(f32),
1012    Buff(CombatBuff),
1013    Knockback(Knockback),
1014    EnergyReward(f32),
1015    Lifesteal(f32),
1016    Poise(f32),
1017    Combo(i32),
1018    /// If the attack hits the target while they are in the buildup portion of a
1019    /// character state, deal increased damage
1020    /// Only has an effect when attached to a damage, otherwise does nothing if
1021    /// only attached to the attack
1022    // TODO: Maybe try to make it do something if tied to
1023    // attack, not sure if it should double count in that instance?
1024    StageVulnerable(f32, StageSection),
1025    /// Resets duration of all buffs of this buffkind, with some probability
1026    RefreshBuff(f32, BuffKind),
1027    /// If the target hit by an attack has this buff, they will take increased
1028    /// damage.
1029    /// Only has an effect when attached to a damage, otherwise does nothing if
1030    /// only attached to the attack
1031    // TODO: Maybe try to make it do something if tied to attack, not sure if it should double
1032    // count in that instance?
1033    BuffsVulnerable(f32, BuffKind),
1034    /// If the target hit by an attack is in a stunned state, they will take
1035    /// increased damage.
1036    /// Only has an effect when attached to a damage, otherwise does nothing if
1037    /// only attached to the attack
1038    // TODO: Maybe try to make it do something if tied to attack, not sure if it should double
1039    // count in that instance?
1040    StunnedVulnerable(f32),
1041    /// Applies buff to yourself after attack is applied
1042    SelfBuff(CombatBuff),
1043}
1044
1045impl CombatEffect {
1046    pub fn adjusted_by_stats(self, stats: tool::Stats) -> Self {
1047        match self {
1048            CombatEffect::Heal(h) => CombatEffect::Heal(h * stats.effect_power),
1049            CombatEffect::Buff(CombatBuff {
1050                kind,
1051                dur_secs,
1052                strength,
1053                chance,
1054            }) => CombatEffect::Buff(CombatBuff {
1055                kind,
1056                dur_secs,
1057                strength: strength * stats.buff_strength,
1058                chance,
1059            }),
1060            CombatEffect::Knockback(Knockback {
1061                direction,
1062                strength,
1063            }) => CombatEffect::Knockback(Knockback {
1064                direction,
1065                strength: strength * stats.effect_power,
1066            }),
1067            CombatEffect::EnergyReward(e) => CombatEffect::EnergyReward(e),
1068            CombatEffect::Lifesteal(l) => CombatEffect::Lifesteal(l * stats.effect_power),
1069            CombatEffect::Poise(p) => CombatEffect::Poise(p * stats.effect_power),
1070            CombatEffect::Combo(c) => CombatEffect::Combo(c),
1071            CombatEffect::StageVulnerable(v, s) => {
1072                CombatEffect::StageVulnerable(v * stats.effect_power, s)
1073            },
1074            CombatEffect::RefreshBuff(c, b) => CombatEffect::RefreshBuff(c, b),
1075            CombatEffect::BuffsVulnerable(v, b) => {
1076                CombatEffect::BuffsVulnerable(v * stats.effect_power, b)
1077            },
1078            CombatEffect::StunnedVulnerable(v) => {
1079                CombatEffect::StunnedVulnerable(v * stats.effect_power)
1080            },
1081            CombatEffect::SelfBuff(CombatBuff {
1082                kind,
1083                dur_secs,
1084                strength,
1085                chance,
1086            }) => CombatEffect::SelfBuff(CombatBuff {
1087                kind,
1088                dur_secs,
1089                strength: strength * stats.buff_strength,
1090                chance,
1091            }),
1092        }
1093    }
1094}
1095
1096#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq)]
1097pub enum CombatRequirement {
1098    AnyDamage,
1099    Energy(f32),
1100    Combo(u32),
1101    TargetHasBuff(BuffKind),
1102    TargetPoised,
1103    BehindTarget,
1104    TargetBlocking,
1105}
1106
1107#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
1108pub enum DamagedEffect {
1109    Combo(i32),
1110    Energy(f32),
1111}
1112
1113/// Effects applied to the rider of this entity while riding.
1114#[derive(Clone, Debug, PartialEq)]
1115pub struct RiderEffects(pub Vec<BuffEffect>);
1116
1117impl specs::Component for RiderEffects {
1118    type Storage = specs::DenseVecStorage<RiderEffects>;
1119}
1120
1121#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
1122pub enum DeathEffect {
1123    /// Adds buff to the attacker that killed this entity
1124    AttackerBuff {
1125        kind: BuffKind,
1126        strength: f32,
1127        duration: Option<Secs>,
1128    },
1129    /// Transform into another entity when killed, regaining full health
1130    Transform {
1131        entity_spec: String,
1132        /// Whether this death effect applies to players or not
1133        #[serde(default)]
1134        allow_players: bool,
1135    },
1136}
1137
1138#[derive(Clone, Debug, PartialEq)]
1139/// Permanent entity death effects (unlike `Stats::effects_on_death` which is
1140/// only active as long as ie. it has a certain buff)
1141pub struct DeathEffects(pub Vec<DeathEffect>);
1142
1143impl specs::Component for DeathEffects {
1144    type Storage = specs::DenseVecStorage<DeathEffects>;
1145}
1146
1147#[derive(Clone, Copy, Debug, Hash, Eq, PartialEq, Serialize, Deserialize)]
1148pub enum DamageContributor {
1149    Solo(Uid),
1150    Group { entity_uid: Uid, group: Group },
1151}
1152
1153impl DamageContributor {
1154    pub fn new(uid: Uid, group: Option<Group>) -> Self {
1155        if let Some(group) = group {
1156            DamageContributor::Group {
1157                entity_uid: uid,
1158                group,
1159            }
1160        } else {
1161            DamageContributor::Solo(uid)
1162        }
1163    }
1164
1165    pub fn uid(&self) -> Uid {
1166        match self {
1167            DamageContributor::Solo(uid) => *uid,
1168            DamageContributor::Group {
1169                entity_uid,
1170                group: _,
1171            } => *entity_uid,
1172        }
1173    }
1174}
1175
1176impl From<AttackerInfo<'_>> for DamageContributor {
1177    fn from(attacker_info: AttackerInfo) -> Self {
1178        DamageContributor::new(attacker_info.uid, attacker_info.group.copied())
1179    }
1180}
1181
1182#[derive(Copy, Clone, Debug, Hash, PartialEq, Eq, Serialize, Deserialize)]
1183pub enum DamageSource {
1184    Buff(BuffKind),
1185    Melee,
1186    Projectile,
1187    Explosion,
1188    Falling,
1189    Shockwave,
1190    Energy,
1191    Other,
1192}
1193
1194impl From<AttackSource> for DamageSource {
1195    fn from(attack: AttackSource) -> Self {
1196        match attack {
1197            AttackSource::Melee => DamageSource::Melee,
1198            AttackSource::Projectile => DamageSource::Projectile,
1199            AttackSource::Explosion => DamageSource::Explosion,
1200            AttackSource::AirShockwave
1201            | AttackSource::GroundShockwave
1202            | AttackSource::UndodgeableShockwave => DamageSource::Shockwave,
1203            AttackSource::Beam => DamageSource::Energy,
1204        }
1205    }
1206}
1207
1208/// DamageKind for the purpose of differentiating damage reduction
1209#[derive(Copy, Clone, Debug, Hash, PartialEq, Eq, Serialize, Deserialize)]
1210pub enum DamageKind {
1211    /// Bypasses some protection from armor
1212    Piercing,
1213    /// Reduces energy of target, dealing additional damage when target energy
1214    /// is 0
1215    Slashing,
1216    /// Deals additional poise damage the more armored the target is
1217    Crushing,
1218    /// Catch all for remaining damage kinds (TODO: differentiate further with
1219    /// staff/sceptre reworks
1220    Energy,
1221}
1222
1223const PIERCING_PENETRATION_FRACTION: f32 = 0.5;
1224const SLASHING_ENERGY_FRACTION: f32 = 0.5;
1225const CRUSHING_POISE_FRACTION: f32 = 1.0;
1226
1227#[derive(Copy, Clone, Debug, PartialEq, Serialize, Deserialize)]
1228#[serde(deny_unknown_fields)]
1229pub struct Damage {
1230    pub source: DamageSource,
1231    pub kind: DamageKind,
1232    pub value: f32,
1233}
1234
1235impl Damage {
1236    /// Returns the total damage reduction provided by all equipped items
1237    pub fn compute_damage_reduction(
1238        damage: Option<Self>,
1239        inventory: Option<&Inventory>,
1240        stats: Option<&Stats>,
1241        msm: &MaterialStatManifest,
1242    ) -> f32 {
1243        let protection = compute_protection(inventory, msm);
1244
1245        let penetration = if let Some(damage) = damage {
1246            if let DamageKind::Piercing = damage.kind {
1247                (damage.value * PIERCING_PENETRATION_FRACTION).clamp(0.0, protection.unwrap_or(0.0))
1248            } else {
1249                0.0
1250            }
1251        } else {
1252            0.0
1253        };
1254
1255        let protection = protection.map(|p| p - penetration);
1256
1257        const FIFTY_PERCENT_DR_THRESHOLD: f32 = 60.0;
1258
1259        let inventory_dr = match protection {
1260            Some(dr) => dr / (FIFTY_PERCENT_DR_THRESHOLD + dr.abs()),
1261            None => 1.0,
1262        };
1263
1264        let stats_dr = if let Some(stats) = stats {
1265            stats.damage_reduction.modifier()
1266        } else {
1267            0.0
1268        };
1269        // Return 100% if either DR is at 100% (admin tabard or safezone buff)
1270        if protection.is_none() || stats_dr >= 1.0 {
1271            1.0
1272        } else {
1273            1.0 - (1.0 - inventory_dr) * (1.0 - stats_dr)
1274        }
1275    }
1276
1277    pub fn calculate_health_change(
1278        self,
1279        damage_reduction: f32,
1280        block_damage_decrement: f32,
1281        damage_contributor: Option<DamageContributor>,
1282        precision_mult: Option<f32>,
1283        precision_power: f32,
1284        damage_modifier: f32,
1285        time: Time,
1286        instance: u64,
1287    ) -> HealthChange {
1288        let mut damage = self.value * damage_modifier;
1289        let precise_damage = damage * precision_mult.unwrap_or(0.0) * (precision_power - 1.0);
1290        match self.source {
1291            DamageSource::Melee
1292            | DamageSource::Projectile
1293            | DamageSource::Explosion
1294            | DamageSource::Shockwave
1295            | DamageSource::Energy => {
1296                // Precise hit
1297                damage += precise_damage;
1298                // Armor
1299                damage *= 1.0 - damage_reduction;
1300                // Block
1301                damage = f32::max(damage - block_damage_decrement, 0.0);
1302
1303                HealthChange {
1304                    amount: -damage,
1305                    by: damage_contributor,
1306                    cause: Some(self.source),
1307                    time,
1308                    precise: precision_mult.is_some(),
1309                    instance,
1310                }
1311            },
1312            DamageSource::Falling => {
1313                // Armor
1314                if (damage_reduction - 1.0).abs() < f32::EPSILON {
1315                    damage = 0.0;
1316                }
1317                HealthChange {
1318                    amount: -damage,
1319                    by: None,
1320                    cause: Some(self.source),
1321                    time,
1322                    precise: false,
1323                    instance,
1324                }
1325            },
1326            DamageSource::Buff(_) | DamageSource::Other => HealthChange {
1327                amount: -damage,
1328                by: None,
1329                cause: Some(self.source),
1330                time,
1331                precise: false,
1332                instance,
1333            },
1334        }
1335    }
1336
1337    pub fn interpolate_damage(&mut self, frac: f32, min: f32) {
1338        let new_damage = min + frac * (self.value - min);
1339        self.value = new_damage;
1340    }
1341}
1342
1343#[derive(Copy, Clone, Debug, PartialEq, Serialize, Deserialize)]
1344pub struct Knockback {
1345    pub direction: KnockbackDir,
1346    pub strength: f32,
1347}
1348
1349#[derive(Copy, Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
1350pub enum KnockbackDir {
1351    Away,
1352    Towards,
1353    Up,
1354    TowardsUp,
1355}
1356
1357impl Knockback {
1358    pub fn calculate_impulse(self, dir: Dir, char_state: Option<&CharacterState>) -> Vec3<f32> {
1359        let from_char = {
1360            let resistant = char_state
1361                .and_then(|cs| cs.ability_info())
1362                .map(|a| a.ability_meta)
1363                .is_some_and(|a| a.capabilities.contains(Capability::KNOCKBACK_RESISTANT));
1364            if resistant { 0.5 } else { 1.0 }
1365        };
1366        // TEMP: 50.0 multiplication kept until source knockback values have been
1367        // updated
1368        50.0 * self.strength
1369            * from_char
1370            * match self.direction {
1371                KnockbackDir::Away => *Dir::slerp(dir, Dir::new(Vec3::unit_z()), 0.5),
1372                KnockbackDir::Towards => *Dir::slerp(-dir, Dir::new(Vec3::unit_z()), 0.5),
1373                KnockbackDir::Up => Vec3::unit_z(),
1374                KnockbackDir::TowardsUp => *Dir::slerp(-dir, Dir::new(Vec3::unit_z()), 0.85),
1375            }
1376    }
1377
1378    #[must_use]
1379    pub fn modify_strength(mut self, power: f32) -> Self {
1380        self.strength *= power;
1381        self
1382    }
1383}
1384
1385#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq)]
1386pub struct CombatBuff {
1387    pub kind: BuffKind,
1388    pub dur_secs: f32,
1389    pub strength: CombatBuffStrength,
1390    pub chance: f32,
1391}
1392
1393#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq)]
1394pub enum CombatBuffStrength {
1395    DamageFraction(f32),
1396    Value(f32),
1397}
1398
1399impl CombatBuffStrength {
1400    fn to_strength(self, damage: f32, strength_modifier: f32) -> f32 {
1401        match self {
1402            // Not affected by strength modifier as damage already is
1403            CombatBuffStrength::DamageFraction(f) => damage * f,
1404            CombatBuffStrength::Value(v) => v * strength_modifier,
1405        }
1406    }
1407}
1408
1409impl MulAssign<f32> for CombatBuffStrength {
1410    fn mul_assign(&mut self, mul: f32) { *self = *self * mul; }
1411}
1412
1413impl Mul<f32> for CombatBuffStrength {
1414    type Output = Self;
1415
1416    fn mul(self, mult: f32) -> Self {
1417        match self {
1418            Self::DamageFraction(val) => Self::DamageFraction(val * mult),
1419            Self::Value(val) => Self::Value(val * mult),
1420        }
1421    }
1422}
1423
1424impl CombatBuff {
1425    fn to_buff(
1426        self,
1427        time: Time,
1428        attacker_info: Option<AttackerInfo>,
1429        target_info: &TargetInfo,
1430        damage: f32,
1431        strength_modifier: f32,
1432    ) -> Buff {
1433        // TODO: Generate BufCategoryId vec (probably requires damage overhaul?)
1434        let source = if let Some(uid) = attacker_info.map(|a| a.uid) {
1435            BuffSource::Character { by: uid }
1436        } else {
1437            BuffSource::Unknown
1438        };
1439        let dest_info = DestInfo {
1440            stats: target_info.stats,
1441            mass: target_info.mass,
1442        };
1443        Buff::new(
1444            self.kind,
1445            BuffData::new(
1446                self.strength.to_strength(damage, strength_modifier),
1447                Some(Secs(self.dur_secs as f64)),
1448            ),
1449            Vec::new(),
1450            source,
1451            time,
1452            dest_info,
1453            attacker_info.and_then(|a| a.mass),
1454        )
1455    }
1456
1457    fn to_self_buff(
1458        self,
1459        time: Time,
1460        attacker_info: AttackerInfo,
1461        damage: f32,
1462        strength_modifier: f32,
1463    ) -> Buff {
1464        // TODO: Generate BufCategoryId vec (probably requires damage overhaul?)
1465        let source = BuffSource::Character {
1466            by: attacker_info.uid,
1467        };
1468        let dest_info = DestInfo {
1469            stats: attacker_info.stats,
1470            mass: attacker_info.mass,
1471        };
1472        Buff::new(
1473            self.kind,
1474            BuffData::new(
1475                self.strength.to_strength(damage, strength_modifier),
1476                Some(Secs(self.dur_secs as f64)),
1477            ),
1478            Vec::new(),
1479            source,
1480            time,
1481            dest_info,
1482            attacker_info.mass,
1483        )
1484    }
1485}
1486
1487pub fn get_weapon_kinds(inv: &Inventory) -> (Option<ToolKind>, Option<ToolKind>) {
1488    (
1489        inv.equipped(EquipSlot::ActiveMainhand).and_then(|i| {
1490            if let ItemKind::Tool(tool) = &*i.kind() {
1491                Some(tool.kind)
1492            } else {
1493                None
1494            }
1495        }),
1496        inv.equipped(EquipSlot::ActiveOffhand).and_then(|i| {
1497            if let ItemKind::Tool(tool) = &*i.kind() {
1498                Some(tool.kind)
1499            } else {
1500                None
1501            }
1502        }),
1503    )
1504}
1505
1506// TODO: Either remove msm or use it as argument in fn kind
1507fn weapon_rating<T: ItemDesc>(item: &T, _msm: &MaterialStatManifest) -> f32 {
1508    const POWER_WEIGHT: f32 = 2.0;
1509    const SPEED_WEIGHT: f32 = 3.0;
1510    const RANGE_WEIGHT: f32 = 0.8;
1511    const EFFECT_WEIGHT: f32 = 1.5;
1512    const EQUIP_TIME_WEIGHT: f32 = 0.0;
1513    const ENERGY_EFFICIENCY_WEIGHT: f32 = 1.5;
1514    const BUFF_STRENGTH_WEIGHT: f32 = 1.5;
1515
1516    let rating = if let ItemKind::Tool(tool) = &*item.kind() {
1517        let stats = tool.stats(item.stats_durability_multiplier());
1518
1519        // TODO: Look into changing the 0.5 to reflect armor later maybe?
1520        // Since it is only for weapon though, it probably makes sense to leave
1521        // independent for now
1522
1523        let power_rating = stats.power;
1524        let speed_rating = stats.speed - 1.0;
1525        let range_rating = stats.range - 1.0;
1526        let effect_rating = stats.effect_power - 1.0;
1527        let equip_time_rating = 0.5 - stats.equip_time_secs;
1528        let energy_efficiency_rating = stats.energy_efficiency - 1.0;
1529        let buff_strength_rating = stats.buff_strength - 1.0;
1530
1531        power_rating * POWER_WEIGHT
1532            + speed_rating * SPEED_WEIGHT
1533            + range_rating * RANGE_WEIGHT
1534            + effect_rating * EFFECT_WEIGHT
1535            + equip_time_rating * EQUIP_TIME_WEIGHT
1536            + energy_efficiency_rating * ENERGY_EFFICIENCY_WEIGHT
1537            + buff_strength_rating * BUFF_STRENGTH_WEIGHT
1538    } else {
1539        0.0
1540    };
1541    rating.max(0.0)
1542}
1543
1544fn weapon_skills(inventory: &Inventory, skill_set: &SkillSet) -> f32 {
1545    let (mainhand, offhand) = get_weapon_kinds(inventory);
1546    let mainhand_skills = if let Some(tool) = mainhand {
1547        skill_set.earned_sp(SkillGroupKind::Weapon(tool)) as f32
1548    } else {
1549        0.0
1550    };
1551    let offhand_skills = if let Some(tool) = offhand {
1552        skill_set.earned_sp(SkillGroupKind::Weapon(tool)) as f32
1553    } else {
1554        0.0
1555    };
1556    mainhand_skills.max(offhand_skills)
1557}
1558
1559fn get_weapon_rating(inventory: &Inventory, msm: &MaterialStatManifest) -> f32 {
1560    let mainhand_rating = if let Some(item) = inventory.equipped(EquipSlot::ActiveMainhand) {
1561        weapon_rating(item, msm)
1562    } else {
1563        0.0
1564    };
1565
1566    let offhand_rating = if let Some(item) = inventory.equipped(EquipSlot::ActiveOffhand) {
1567        weapon_rating(item, msm)
1568    } else {
1569        0.0
1570    };
1571
1572    mainhand_rating.max(offhand_rating)
1573}
1574
1575pub fn combat_rating(
1576    inventory: &Inventory,
1577    health: &Health,
1578    energy: &Energy,
1579    poise: &Poise,
1580    skill_set: &SkillSet,
1581    body: Body,
1582    msm: &MaterialStatManifest,
1583) -> f32 {
1584    const WEAPON_WEIGHT: f32 = 1.0;
1585    const HEALTH_WEIGHT: f32 = 1.5;
1586    const ENERGY_WEIGHT: f32 = 0.5;
1587    const SKILLS_WEIGHT: f32 = 1.0;
1588    const POISE_WEIGHT: f32 = 0.5;
1589    const PRECISION_WEIGHT: f32 = 0.5;
1590    // Normalized with a standard max health of 100
1591    let health_rating = health.base_max()
1592        / 100.0
1593        / (1.0 - Damage::compute_damage_reduction(None, Some(inventory), None, msm)).max(0.00001);
1594
1595    // Normalized with a standard max energy of 100 and energy reward multiplier of
1596    // x1
1597    let energy_rating = (energy.base_max() + compute_max_energy_mod(Some(inventory), msm)) / 100.0
1598        * compute_energy_reward_mod(Some(inventory), msm);
1599
1600    // Normalized with a standard max poise of 100
1601    let poise_rating = poise.base_max()
1602        / 100.0
1603        / (1.0 - Poise::compute_poise_damage_reduction(Some(inventory), msm, None, None))
1604            .max(0.00001);
1605
1606    // Normalized with a standard precision multiplier of 1.2
1607    let precision_rating = compute_precision_mult(Some(inventory), msm) / 1.2;
1608
1609    // Assumes a standard person has earned 20 skill points in the general skill
1610    // tree and 10 skill points for the weapon skill tree
1611    let skills_rating = (skill_set.earned_sp(SkillGroupKind::General) as f32 / 20.0
1612        + weapon_skills(inventory, skill_set) / 10.0)
1613        / 2.0;
1614
1615    let weapon_rating = get_weapon_rating(inventory, msm);
1616
1617    let combined_rating = (health_rating * HEALTH_WEIGHT
1618        + energy_rating * ENERGY_WEIGHT
1619        + poise_rating * POISE_WEIGHT
1620        + precision_rating * PRECISION_WEIGHT
1621        + skills_rating * SKILLS_WEIGHT
1622        + weapon_rating * WEAPON_WEIGHT)
1623        / (HEALTH_WEIGHT
1624            + ENERGY_WEIGHT
1625            + POISE_WEIGHT
1626            + PRECISION_WEIGHT
1627            + SKILLS_WEIGHT
1628            + WEAPON_WEIGHT);
1629
1630    // Body multiplier meant to account for an enemy being harder than equipment and
1631    // skills would account for. It should only not be 1.0 for non-humanoids
1632    combined_rating * body.combat_multiplier()
1633}
1634
1635pub fn compute_precision_mult(inventory: Option<&Inventory>, msm: &MaterialStatManifest) -> f32 {
1636    // Starts with a value of 0.1 when summing the stats from each armor piece, and
1637    // defaults to a value of 0.1 if no inventory is equipped. Precision multiplier
1638    // cannot go below 1
1639    1.0 + inventory
1640        .map_or(0.1, |inv| {
1641            inv.equipped_items()
1642                .filter_map(|item| {
1643                    if let ItemKind::Armor(armor) = &*item.kind() {
1644                        armor
1645                            .stats(msm, item.stats_durability_multiplier())
1646                            .precision_power
1647                    } else {
1648                        None
1649                    }
1650                })
1651                .fold(0.1, |a, b| a + b)
1652        })
1653        .max(0.0)
1654}
1655
1656/// Computes the energy reward modifier from worn armor
1657pub fn compute_energy_reward_mod(inventory: Option<&Inventory>, msm: &MaterialStatManifest) -> f32 {
1658    // Starts with a value of 1.0 when summing the stats from each armor piece, and
1659    // defaults to a value of 1.0 if no inventory is present
1660    inventory.map_or(1.0, |inv| {
1661        inv.equipped_items()
1662            .filter_map(|item| {
1663                if let ItemKind::Armor(armor) = &*item.kind() {
1664                    armor
1665                        .stats(msm, item.stats_durability_multiplier())
1666                        .energy_reward
1667                } else {
1668                    None
1669                }
1670            })
1671            .fold(1.0, |a, b| a + b)
1672    })
1673}
1674
1675/// Computes the additive modifier that should be applied to max energy from the
1676/// currently equipped items
1677pub fn compute_max_energy_mod(inventory: Option<&Inventory>, msm: &MaterialStatManifest) -> f32 {
1678    // Defaults to a value of 0 if no inventory is present
1679    inventory.map_or(0.0, |inv| {
1680        inv.equipped_items()
1681            .filter_map(|item| {
1682                if let ItemKind::Armor(armor) = &*item.kind() {
1683                    armor
1684                        .stats(msm, item.stats_durability_multiplier())
1685                        .energy_max
1686                } else {
1687                    None
1688                }
1689            })
1690            .sum()
1691    })
1692}
1693
1694/// Returns a value to be included as a multiplicative factor in perception
1695/// distance checks.
1696pub fn perception_dist_multiplier_from_stealth(
1697    inventory: Option<&Inventory>,
1698    character_state: Option<&CharacterState>,
1699    msm: &MaterialStatManifest,
1700) -> f32 {
1701    const SNEAK_MULTIPLIER: f32 = 0.7;
1702
1703    let item_stealth_multiplier = stealth_multiplier_from_items(inventory, msm);
1704    let is_sneaking = character_state.is_some_and(|state| state.is_stealthy());
1705
1706    let multiplier = item_stealth_multiplier * if is_sneaking { SNEAK_MULTIPLIER } else { 1.0 };
1707
1708    multiplier.clamp(0.0, 1.0)
1709}
1710
1711pub fn stealth_multiplier_from_items(
1712    inventory: Option<&Inventory>,
1713    msm: &MaterialStatManifest,
1714) -> f32 {
1715    let stealth_sum = inventory.map_or(0.0, |inv| {
1716        inv.equipped_items()
1717            .filter_map(|item| {
1718                if let ItemKind::Armor(armor) = &*item.kind() {
1719                    armor.stats(msm, item.stats_durability_multiplier()).stealth
1720                } else {
1721                    None
1722                }
1723            })
1724            .sum()
1725    });
1726
1727    (1.0 / (1.0 + stealth_sum)).clamp(0.0, 1.0)
1728}
1729
1730/// Computes the total protection provided from armor. Is used to determine the
1731/// damage reduction applied to damage received by an entity None indicates that
1732/// the armor equipped makes the entity invulnerable
1733pub fn compute_protection(
1734    inventory: Option<&Inventory>,
1735    msm: &MaterialStatManifest,
1736) -> Option<f32> {
1737    inventory.map_or(Some(0.0), |inv| {
1738        inv.equipped_items()
1739            .filter_map(|item| {
1740                if let ItemKind::Armor(armor) = &*item.kind() {
1741                    armor
1742                        .stats(msm, item.stats_durability_multiplier())
1743                        .protection
1744                } else {
1745                    None
1746                }
1747            })
1748            .map(|protection| match protection {
1749                Protection::Normal(protection) => Some(protection),
1750                Protection::Invincible => None,
1751            })
1752            .sum::<Option<f32>>()
1753    })
1754}
1755
1756/// Computes the total resilience provided from armor. Is used to determine the
1757/// reduction applied to poise damage received by an entity. None indicates that
1758/// the armor equipped makes the entity invulnerable to poise damage.
1759pub fn compute_poise_resilience(
1760    inventory: Option<&Inventory>,
1761    msm: &MaterialStatManifest,
1762) -> Option<f32> {
1763    inventory.map_or(Some(0.0), |inv| {
1764        inv.equipped_items()
1765            .filter_map(|item| {
1766                if let ItemKind::Armor(armor) = &*item.kind() {
1767                    armor
1768                        .stats(msm, item.stats_durability_multiplier())
1769                        .poise_resilience
1770                } else {
1771                    None
1772                }
1773            })
1774            .map(|protection| match protection {
1775                Protection::Normal(protection) => Some(protection),
1776                Protection::Invincible => None,
1777            })
1778            .sum::<Option<f32>>()
1779    })
1780}
1781
1782/// Used to compute the precision multiplier achieved by flanking a target
1783pub fn precision_mult_from_flank(
1784    attack_dir: Vec3<f32>,
1785    target_ori: Option<&Ori>,
1786    precision_flank_multipliers: FlankMults,
1787    precision_flank_invert: bool,
1788) -> Option<f32> {
1789    let angle = target_ori.map(|t_ori| {
1790        t_ori.look_dir().angle_between(if precision_flank_invert {
1791            -attack_dir
1792        } else {
1793            attack_dir
1794        })
1795    });
1796    match angle {
1797        Some(angle) if angle < FULL_FLANK_ANGLE => Some(
1798            MAX_BACK_FLANK_PRECISION
1799                * if precision_flank_invert {
1800                    precision_flank_multipliers.front
1801                } else {
1802                    precision_flank_multipliers.back
1803                },
1804        ),
1805        Some(angle) if angle < PARTIAL_FLANK_ANGLE => {
1806            Some(MAX_SIDE_FLANK_PRECISION * precision_flank_multipliers.side)
1807        },
1808        Some(_) | None => None,
1809    }
1810}
1811
1812#[derive(Copy, Clone, Debug, PartialEq, Serialize, Deserialize)]
1813pub struct FlankMults {
1814    pub back: f32,
1815    pub front: f32,
1816    pub side: f32,
1817}
1818
1819impl Default for FlankMults {
1820    fn default() -> Self {
1821        FlankMults {
1822            back: 1.0,
1823            front: 1.0,
1824            side: 1.0,
1825        }
1826    }
1827}
1828
1829pub fn block_strength(inventory: &Inventory, char_state: &CharacterState) -> f32 {
1830    match char_state {
1831        CharacterState::BasicBlock(data) => data.static_data.block_strength,
1832        CharacterState::RiposteMelee(data) => data.static_data.block_strength,
1833        _ => char_state
1834            .ability_info()
1835            .map(|ability| (ability.ability_meta.capabilities, ability.hand))
1836            .map(|(capabilities, hand)| {
1837                (
1838                    if capabilities.contains(Capability::PARRIES)
1839                        || capabilities.contains(Capability::PARRIES_MELEE)
1840                        || capabilities.contains(Capability::BLOCKS)
1841                    {
1842                        FALLBACK_BLOCK_STRENGTH
1843                    } else {
1844                        0.0
1845                    },
1846                    hand.and_then(|hand| inventory.equipped(hand.to_equip_slot()))
1847                        .map_or(1.0, |item| match &*item.kind() {
1848                            ItemKind::Tool(tool) => {
1849                                tool.stats(item.stats_durability_multiplier()).power
1850                            },
1851                            _ => 1.0,
1852                        }),
1853                )
1854            })
1855            .map_or(0.0, |(capability_strength, tool_block_strength)| {
1856                capability_strength * tool_block_strength
1857            }),
1858    }
1859}
1860
1861pub fn get_equip_slot_by_block_priority(inventory: Option<&Inventory>) -> EquipSlot {
1862    inventory
1863        .map(get_weapon_kinds)
1864        .map_or(
1865            EquipSlot::ActiveMainhand,
1866            |weapon_kinds| match weapon_kinds {
1867                (Some(mainhand), Some(offhand)) => {
1868                    if mainhand.block_priority() >= offhand.block_priority() {
1869                        EquipSlot::ActiveMainhand
1870                    } else {
1871                        EquipSlot::ActiveOffhand
1872                    }
1873                },
1874                (Some(_), None) => EquipSlot::ActiveMainhand,
1875                (None, Some(_)) => EquipSlot::ActiveOffhand,
1876                (None, None) => EquipSlot::ActiveMainhand,
1877            },
1878        )
1879}