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