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