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