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