Skip to main content

veloren_common/
combat.rs

1use crate::{
2    assets::{AssetExt, Ron},
3    comp::{
4        Alignment, Body, Buffs, CharacterState, Combo, Energy, Group, Health, HealthChange,
5        InputKind, Inventory, Mass, Ori, Player, Poise, PoiseChange, SkillSet, Stats,
6        ability::Capability,
7        aura::{AuraKindVariant, EnteredAuras},
8        buff::{Buff, BuffChange, BuffData, BuffDescriptor, BuffKind, BuffSource, DestInfo},
9        inventory::{
10            item::{
11                ItemDesc, ItemKind, MaterialStatManifest,
12                armor::Protection,
13                tool::{self, ToolKind},
14            },
15            slot::EquipSlot,
16        },
17        skillset::SkillGroupKind,
18    },
19    effect::BuffEffect,
20    event::{
21        BuffEvent, ComboChangeEvent, EmitExt, EnergyChangeEvent, EntityAttackedHookEvent,
22        HealthChangeEvent, KnockbackEvent, ParryHookEvent, PoiseChangeEvent, TransformEvent,
23    },
24    generation::{EntityConfig, EntityInfo},
25    outcome::Outcome,
26    resources::{Secs, Time},
27    states::utils::{AbilityInfo, StageSection},
28    uid::{IdMaps, Uid},
29    util::Dir,
30};
31use rand::RngExt;
32use serde::{Deserialize, Serialize};
33use specs::{Entity as EcsEntity, ReadStorage};
34use std::ops::{Mul, MulAssign};
35use tracing::error;
36use vek::*;
37
38pub enum AttackTarget {
39    AllInRange(f32),
40    Pos(Vec3<f32>),
41    Entity(EcsEntity),
42}
43
44#[derive(Copy, Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
45pub enum GroupTarget {
46    InGroup,
47    OutOfGroup,
48    All,
49}
50
51#[derive(Copy, Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
52pub enum StatEffectTarget {
53    Attacker,
54    Target,
55}
56
57#[derive(Copy, Clone, PartialEq, Eq, Debug, Serialize, Deserialize, Hash)]
58pub enum AttackSource {
59    Melee,
60    Projectile,
61    Beam,
62    GroundShockwave,
63    AirShockwave,
64    UndodgeableShockwave,
65    Explosion,
66    Arc,
67    Pool,
68}
69
70pub const FULL_FLANK_ANGLE: f32 = std::f32::consts::PI / 4.0;
71pub const PARTIAL_FLANK_ANGLE: f32 = std::f32::consts::PI * 3.0 / 4.0;
72pub const BEAM_DURATION_PRECISION: f32 = 2.5;
73pub const MAX_BACK_FLANK_PRECISION: f32 = 0.75;
74pub const MAX_SIDE_FLANK_PRECISION: f32 = 0.25;
75pub const MAX_HEADSHOT_PRECISION: f32 = 1.0;
76pub const MAX_TOP_HEADSHOT_PRECISION: f32 = 0.5;
77pub const MAX_BEAM_DUR_PRECISION: f32 = 0.25;
78pub const MAX_MELEE_POISE_PRECISION: f32 = 0.5;
79pub const MAX_BLOCK_POISE_COST: f32 = 25.0;
80pub const FALLBACK_BLOCK_STRENGTH: f32 = 3.3;
81pub const BEHIND_TARGET_ANGLE: f32 = 45.0;
82pub const BASE_PARRIED_POISE_PUNISHMENT: f32 = 100.0 / 3.5;
83
84#[derive(Copy, Clone)]
85pub struct AttackerInfo<'a> {
86    pub entity: EcsEntity,
87    pub uid: Uid,
88    pub group: Option<&'a Group>,
89    pub energy: Option<&'a Energy>,
90    pub combo: Option<&'a Combo>,
91    pub inventory: Option<&'a Inventory>,
92    pub stats: Option<&'a Stats>,
93    pub mass: Option<&'a Mass>,
94    pub pos: Option<Vec3<f32>>,
95}
96
97#[derive(Copy, Clone)]
98pub struct TargetInfo<'a> {
99    pub entity: EcsEntity,
100    pub uid: Uid,
101    pub inventory: Option<&'a Inventory>,
102    pub stats: Option<&'a Stats>,
103    pub health: Option<&'a Health>,
104    pub pos: Vec3<f32>,
105    pub ori: Option<&'a Ori>,
106    pub char_state: Option<&'a CharacterState>,
107    pub energy: Option<&'a Energy>,
108    pub buffs: Option<&'a Buffs>,
109    pub mass: Option<&'a Mass>,
110    pub player: Option<&'a Player>,
111}
112
113#[derive(Clone, Copy)]
114pub struct AttackOptions {
115    pub target_dodging: bool,
116    /// Result of [`permit_pvp`]
117    pub permit_pvp: bool,
118    pub target_group: GroupTarget,
119    /// When set to `true`, entities in the same group or pets & pet owners may
120    /// hit eachother albeit the target_group being OutOfGroup
121    pub allow_friendly_fire: bool,
122    pub precision_mult: Option<f32>,
123}
124
125#[derive(Clone, Debug, Serialize, Deserialize)] // TODO: Yeet clone derive
126pub struct Attack {
127    damages: Vec<AttackDamage>,
128    effects: Vec<AttackEffect>,
129    precision_multiplier: f32,
130    pub(crate) blockable: bool,
131    ability_info: Option<AbilityInfo>,
132}
133
134impl Attack {
135    pub fn new(ability_info: Option<AbilityInfo>) -> Self {
136        Self {
137            damages: Vec::new(),
138            effects: Vec::new(),
139            precision_multiplier: 1.0,
140            blockable: true,
141            ability_info,
142        }
143    }
144
145    #[must_use]
146    pub fn with_damage(mut self, damage: AttackDamage) -> Self {
147        self.damages.push(damage);
148        self
149    }
150
151    #[must_use]
152    pub fn with_effect(mut self, effect: AttackEffect) -> Self {
153        self.effects.push(effect);
154        self
155    }
156
157    #[must_use]
158    pub fn with_precision(mut self, precision_multiplier: f32) -> Self {
159        self.precision_multiplier = precision_multiplier;
160        self
161    }
162
163    #[must_use]
164    pub fn with_blockable(mut self, blockable: bool) -> Self {
165        self.blockable = blockable;
166        self
167    }
168
169    #[must_use]
170    pub fn with_combo_requirement(self, combo: i32, requirement: CombatRequirement) -> Self {
171        self.with_effect(
172            AttackEffect::new(None, CombatEffect::Combo(combo)).with_requirement(requirement),
173        )
174    }
175
176    #[must_use]
177    pub fn with_combo(self, combo: i32) -> Self {
178        self.with_combo_requirement(combo, CombatRequirement::AnyDamage)
179    }
180
181    #[must_use]
182    pub fn with_combo_increment(self) -> Self { self.with_combo(1) }
183
184    pub fn effects(&self) -> impl Iterator<Item = &AttackEffect> { self.effects.iter() }
185
186    pub fn compute_block_damage_decrement(
187        blockable: bool,
188        attacker: Option<&AttackerInfo>,
189        target: &TargetInfo,
190        source: AttackSource,
191        dir: Dir,
192        damage: f32,
193        msm: &MaterialStatManifest,
194        time: Time,
195        emitters: &mut (impl EmitExt<ParryHookEvent> + EmitExt<PoiseChangeEvent>),
196        mut emit_outcome: impl FnMut(Outcome),
197    ) -> f32 {
198        if blockable && damage > 0.0 {
199            if let (Some(char_state), Some(ori), Some(inventory)) =
200                (target.char_state, target.ori, target.inventory)
201            {
202                let is_parry = char_state.is_parry(source);
203                let is_block = char_state.is_block(source);
204                let mut block_strength = block_strength(inventory, char_state);
205
206                if ori.look_vec().angle_between(-dir.with_z(0.0)) < char_state.block_angle()
207                    && (is_parry || is_block)
208                    && block_strength > 0.0
209                {
210                    if is_parry {
211                        block_strength = damage;
212
213                        emitters.emit(ParryHookEvent {
214                            defender: target.entity,
215                            attacker: attacker.map(|a| a.entity),
216                            source,
217                            poise_multiplier: 2.0 - (damage / block_strength).min(1.0),
218                        });
219                    }
220
221                    let poise_cost = (damage / block_strength).min(1.0) * MAX_BLOCK_POISE_COST;
222
223                    let poise_change = Poise::apply_poise_reduction(
224                        poise_cost,
225                        target.inventory,
226                        msm,
227                        target.char_state,
228                        target.stats,
229                    );
230
231                    emit_outcome(Outcome::Block {
232                        parry: is_parry,
233                        pos: target.pos,
234                        uid: target.uid,
235                    });
236                    emitters.emit(PoiseChangeEvent {
237                        entity: target.entity,
238                        change: PoiseChange {
239                            amount: -poise_change,
240                            impulse: *dir,
241                            by: attacker.map(|x| (*x).into()),
242                            cause: Some(DamageSource::from(source)),
243                            time,
244                        },
245                    });
246
247                    block_strength
248                } else {
249                    0.0
250                }
251            } else {
252                0.0
253            }
254        } else {
255            0.0
256        }
257    }
258
259    pub fn compute_damage_reduction(
260        attacker: Option<&AttackerInfo>,
261        target: &TargetInfo,
262        damage: Damage,
263        msm: &MaterialStatManifest,
264    ) -> f32 {
265        if damage.value > 0.0 {
266            let attacker_penetration = attacker
267                .and_then(|a| a.stats)
268                .map_or(0.0, |s| s.mitigations_penetration)
269                .clamp(0.0, 1.0);
270            let raw_damage_reduction =
271                Damage::compute_damage_reduction(Some(damage), target.inventory, target.stats, msm);
272
273            if raw_damage_reduction >= 1.0 {
274                raw_damage_reduction
275            } else {
276                (1.0 - attacker_penetration) * raw_damage_reduction
277            }
278        } else {
279            0.0
280        }
281    }
282
283    pub fn apply_attack(
284        &self,
285        attacker: Option<AttackerInfo>,
286        target: &TargetInfo,
287        dir: Dir,
288        options: AttackOptions,
289        // Currently strength_modifier just modifies damage,
290        // maybe look into modifying strength of other effects?
291        strength_modifier: f32,
292        attack_source: AttackSource,
293        time: Time,
294        emitters: &mut (
295                 impl EmitExt<HealthChangeEvent>
296                 + EmitExt<EnergyChangeEvent>
297                 + EmitExt<ParryHookEvent>
298                 + EmitExt<KnockbackEvent>
299                 + EmitExt<BuffEvent>
300                 + EmitExt<PoiseChangeEvent>
301                 + EmitExt<ComboChangeEvent>
302                 + EmitExt<EntityAttackedHookEvent>
303                 + EmitExt<TransformEvent>
304             ),
305        mut emit_outcome: impl FnMut(Outcome),
306        rng: &mut rand::rngs::ThreadRng,
307        damage_instance_offset: u64,
308    ) -> bool {
309        // TODO: Maybe move this higher and pass it as argument into this function?
310        let msm = &MaterialStatManifest::load().read();
311
312        let AttackOptions {
313            target_dodging,
314            permit_pvp,
315            allow_friendly_fire,
316            target_group,
317            precision_mult,
318        } = options;
319
320        // target == OutOfGroup is basic heuristic that this
321        // "attack" has negative effects.
322        //
323        // so if target dodges this "attack" or we don't want to harm target,
324        // it should avoid such "damage" or effect
325        let avoid_damage = |attack_damage: &AttackDamage| {
326            target_dodging
327                || (!permit_pvp && matches!(attack_damage.target, Some(GroupTarget::OutOfGroup)))
328        };
329        let avoid_effect = |attack_effect: &AttackEffect| {
330            target_dodging
331                || (!permit_pvp && matches!(attack_effect.target, Some(GroupTarget::OutOfGroup)))
332        };
333
334        let from_precision_mult = attacker
335            .and_then(|a| a.stats)
336            .and_then(|s| {
337                s.conditional_precision_modifiers
338                    .iter()
339                    .filter_map(|(req, mult, ovrd)| {
340                        req.is_none_or(|r| {
341                            r.requirement_met(
342                                (
343                                    target.health,
344                                    target.buffs,
345                                    target.char_state,
346                                    target.ori,
347                                    Some(target.uid),
348                                ),
349                                (
350                                    attacker.map(|a| a.entity),
351                                    attacker.and_then(|a| a.energy),
352                                    attacker.and_then(|a| a.combo),
353                                ),
354                                attacker.map(|a| a.uid),
355                                0.0,
356                                emitters,
357                                dir,
358                                Some(attack_source),
359                                self.ability_info,
360                            )
361                        })
362                        .then_some((*mult, *ovrd))
363                    })
364                    .chain(precision_mult.iter().map(|val| (*val, false)))
365                    .reduce(|(val_a, ovrd_a), (val_b, ovrd_b)| {
366                        if ovrd_a || ovrd_b {
367                            (val_a.min(val_b), true)
368                        } else {
369                            (val_a.max(val_b), false)
370                        }
371                    })
372            })
373            .map(|(val, _)| val);
374
375        let from_precision_vulnerability_mult = target
376            .stats
377            .and_then(|s| s.precision_vulnerability_multiplier_override);
378
379        let precision_mult = match (from_precision_mult, from_precision_vulnerability_mult) {
380            (Some(a), Some(b)) => Some(a.max(b)),
381            (Some(a), None) | (None, Some(a)) => Some(a),
382            (None, None) => None,
383        };
384
385        let precision_power = 1.0
386            + ((self.precision_multiplier - 1.0)
387                * attacker
388                    .and_then(|a| a.stats)
389                    .map_or(1.0, |s| s.precision_power_mult));
390
391        let attacked_modifiers = AttackedModification::attacked_modifiers(
392            target,
393            attacker,
394            emitters,
395            dir,
396            Some(attack_source),
397            self.ability_info,
398        );
399
400        let mut is_applied = false;
401        let mut accumulated_damage = 0.0;
402        let damage_modifier = attacker
403            .and_then(|a| a.stats)
404            .map_or(1.0, |s| s.attack_damage_modifier);
405        for damage in self
406            .damages
407            .iter()
408            .filter(|d| {
409                allow_friendly_fire
410                    || d.target
411                        .is_none_or(|t| t == GroupTarget::All || t == target_group)
412            })
413            .filter(|d| !avoid_damage(d))
414        {
415            let damage_instance = damage.instance + damage_instance_offset;
416            is_applied = true;
417
418            let damage_reduction =
419                Attack::compute_damage_reduction(attacker.as_ref(), target, damage.damage, msm);
420
421            let damage_with_modifier = damage.damage.value * strength_modifier * damage_modifier;
422            let damage_with_precision = damage_with_modifier
423                * (1.0 + precision_mult.unwrap_or(0.0) * (precision_power - 1.0));
424
425            let block_damage_decrement = Attack::compute_block_damage_decrement(
426                self.blockable,
427                attacker.as_ref(),
428                target,
429                attack_source,
430                dir,
431                damage_with_precision,
432                msm,
433                time,
434                emitters,
435                &mut emit_outcome,
436            );
437
438            let change = damage.damage.calculate_health_change(
439                damage_reduction,
440                block_damage_decrement,
441                attacker.map(|x| x.into()),
442                precision_mult,
443                precision_power,
444                strength_modifier * damage_modifier,
445                time,
446                damage_instance,
447                DamageSource::from(attack_source),
448            );
449            let applied_damage = -change.amount;
450            accumulated_damage += applied_damage;
451
452            if change.amount.abs() > Health::HEALTH_EPSILON {
453                emitters.emit(HealthChangeEvent {
454                    entity: target.entity,
455                    change,
456                });
457                match damage.damage.kind {
458                    DamageKind::Slashing => {
459                        // For slashing damage, reduce target energy by some fraction of applied
460                        // damage. When target would lose more energy than they have, deal an
461                        // equivalent amount of damage
462                        if let Some(target_energy) = target.energy {
463                            let energy_change = applied_damage * SLASHING_ENERGY_FRACTION;
464                            if energy_change > target_energy.current() {
465                                let health_damage = energy_change - target_energy.current();
466                                accumulated_damage += health_damage;
467                                let health_change = HealthChange {
468                                    amount: -health_damage,
469                                    by: attacker.map(|x| x.into()),
470                                    cause: Some(DamageSource::from(attack_source)),
471                                    time,
472                                    precise: precision_mult.is_some(),
473                                    instance: damage_instance,
474                                };
475                                emitters.emit(HealthChangeEvent {
476                                    entity: target.entity,
477                                    change: health_change,
478                                });
479                            }
480                            emitters.emit(EnergyChangeEvent {
481                                entity: target.entity,
482                                change: -energy_change,
483                                reset_rate: false,
484                            });
485                        }
486                    },
487                    DamageKind::Crushing => {
488                        // For crushing damage, reduce target poise by some fraction of the amount
489                        // of damage that was reduced by target's protection
490                        // Damage reduction should never equal 1 here as otherwise the check above
491                        // that health change amount is greater than 0 would fail.
492                        let reduced_damage =
493                            applied_damage * damage_reduction / (1.0 - damage_reduction);
494                        let poise = reduced_damage
495                            * CRUSHING_POISE_FRACTION
496                            * attacker
497                                .and_then(|a| a.stats)
498                                .map_or(1.0, |s| s.poise_damage_modifier);
499                        let change = -Poise::apply_poise_reduction(
500                            poise,
501                            target.inventory,
502                            msm,
503                            target.char_state,
504                            target.stats,
505                        );
506                        let poise_change = PoiseChange {
507                            amount: change,
508                            impulse: *dir,
509                            by: attacker.map(|x| x.into()),
510                            cause: Some(DamageSource::from(attack_source)),
511                            time,
512                        };
513                        if change.abs() > Poise::POISE_EPSILON {
514                            // If target is in a stunned state, apply extra poise damage as health
515                            // damage instead
516                            if let Some(CharacterState::Stunned(data)) = target.char_state {
517                                let health_change =
518                                    change * data.static_data.poise_state.damage_multiplier();
519                                let health_change = HealthChange {
520                                    amount: health_change,
521                                    by: attacker.map(|x| x.into()),
522                                    cause: Some(DamageSource::from(attack_source)),
523                                    instance: damage_instance,
524                                    precise: precision_mult.is_some(),
525                                    time,
526                                };
527                                accumulated_damage -= health_change.amount;
528                                emitters.emit(HealthChangeEvent {
529                                    entity: target.entity,
530                                    change: health_change,
531                                });
532                            } else {
533                                emitters.emit(PoiseChangeEvent {
534                                    entity: target.entity,
535                                    change: poise_change,
536                                });
537                            }
538                        }
539                    },
540                    // Piercing damage ignores some penetration, and is handled when damage
541                    // reduction is computed Energy is a placeholder damage type
542                    DamageKind::Piercing | DamageKind::Energy => {},
543                }
544                for effect in damage.effects.iter() {
545                    match effect {
546                        CombatEffect::Knockback(kb) => {
547                            let impulse = kb.calculate_impulse(
548                                dir,
549                                target.char_state,
550                                attacker.and_then(|a| a.stats),
551                            ) * strength_modifier;
552                            if !impulse.is_approx_zero() {
553                                emitters.emit(KnockbackEvent {
554                                    entity: target.entity,
555                                    impulse,
556                                });
557                            }
558                        },
559                        CombatEffect::EnergyReward(ec) => {
560                            if let Some(attacker) = attacker {
561                                emitters.emit(EnergyChangeEvent {
562                                    entity: attacker.entity,
563                                    change: *ec
564                                        * compute_energy_reward_mod(attacker.inventory, msm)
565                                        * strength_modifier
566                                        * attacker.stats.map_or(1.0, |s| s.energy_reward_modifier)
567                                        * attacked_modifiers.energy_reward,
568                                    reset_rate: false,
569                                });
570                            }
571                        },
572                        CombatEffect::Buff(b) => {
573                            if rng.random::<f32>() < b.chance {
574                                emitters.emit(BuffEvent {
575                                    entity: target.entity,
576                                    buff_change: BuffChange::Add(b.to_buff(
577                                        time,
578                                        (attacker.map(|a| a.uid), attacker.and_then(|a| a.mass)),
579                                        (target.stats, target.mass),
580                                        applied_damage,
581                                        strength_modifier,
582                                        self.ability_info,
583                                    )),
584                                });
585                            }
586                        },
587                        CombatEffect::Lifesteal(l) => {
588                            if let Some(attacker_entity) = attacker.map(|a| a.entity) {
589                                let change = HealthChange {
590                                    amount: applied_damage * l * strength_modifier,
591                                    by: attacker.map(|a| a.into()),
592                                    cause: None,
593                                    time,
594                                    precise: false,
595                                    instance: rand::random(),
596                                };
597                                if change.amount.abs() > Health::HEALTH_EPSILON {
598                                    emitters.emit(HealthChangeEvent {
599                                        entity: attacker_entity,
600                                        change,
601                                    });
602                                }
603                            }
604                        },
605                        CombatEffect::Poise(p) => {
606                            let change = -Poise::apply_poise_reduction(
607                                *p,
608                                target.inventory,
609                                msm,
610                                target.char_state,
611                                target.stats,
612                            ) * strength_modifier
613                                * attacker
614                                    .and_then(|a| a.stats)
615                                    .map_or(1.0, |s| s.poise_damage_modifier);
616                            if change.abs() > Poise::POISE_EPSILON {
617                                let poise_change = PoiseChange {
618                                    amount: change,
619                                    impulse: *dir,
620                                    by: attacker.map(|x| x.into()),
621                                    cause: Some(DamageSource::from(attack_source)),
622                                    time,
623                                };
624                                emitters.emit(PoiseChangeEvent {
625                                    entity: target.entity,
626                                    change: poise_change,
627                                });
628                            }
629                        },
630                        CombatEffect::Heal(h) => {
631                            let change = HealthChange {
632                                amount: *h * strength_modifier,
633                                by: attacker.map(|a| a.into()),
634                                cause: None,
635                                time,
636                                precise: false,
637                                instance: rand::random(),
638                            };
639                            if change.amount.abs() > Health::HEALTH_EPSILON {
640                                emitters.emit(HealthChangeEvent {
641                                    entity: target.entity,
642                                    change,
643                                });
644                            }
645                        },
646                        CombatEffect::Combo(c) => {
647                            if let Some(attacker_entity) = attacker.map(|a| a.entity) {
648                                emitters.emit(ComboChangeEvent {
649                                    entity: attacker_entity,
650                                    change: (*c as f32 * strength_modifier).ceil() as i32,
651                                });
652                            }
653                        },
654                        CombatEffect::AdditionalDamage(damage) => {
655                            let change = {
656                                let mut change = change;
657                                change.amount *= damage * strength_modifier;
658                                change.instance = rand::random();
659                                change
660                            };
661                            accumulated_damage -= change.amount;
662                            emitters.emit(HealthChangeEvent {
663                                entity: target.entity,
664                                change,
665                            });
666                        },
667                        CombatEffect::RefreshBuff(chance, b) => {
668                            if rng.random::<f32>() < *chance {
669                                emitters.emit(BuffEvent {
670                                    entity: target.entity,
671                                    buff_change: BuffChange::Refresh(*b),
672                                });
673                            }
674                        },
675                        CombatEffect::SelfBuff(b) => {
676                            if let Some(attacker) = attacker
677                                && rng.random::<f32>() < b.chance
678                            {
679                                emitters.emit(BuffEvent {
680                                    entity: attacker.entity,
681                                    buff_change: BuffChange::Add(b.to_self_buff(
682                                        time,
683                                        (Some(attacker.uid), attacker.stats, attacker.mass),
684                                        applied_damage,
685                                        strength_modifier,
686                                        self.ability_info,
687                                    )),
688                                });
689                            }
690                        },
691                        CombatEffect::Energy(e) => {
692                            emitters.emit(EnergyChangeEvent {
693                                entity: target.entity,
694                                change: e * strength_modifier,
695                                reset_rate: true,
696                            });
697                        },
698                        CombatEffect::Transform {
699                            entity_spec,
700                            allow_players,
701                        } => {
702                            if target.player.is_none() || *allow_players {
703                                emitters.emit(TransformEvent {
704                                    target_entity: target.uid,
705                                    entity_info: {
706                                        let Ok(entity_config) = Ron::<EntityConfig>::load(
707                                            entity_spec,
708                                        )
709                                        .inspect_err(|error| {
710                                            error!(
711                                                ?entity_spec,
712                                                ?error,
713                                                "Could not load entity configuration for death \
714                                                 effect"
715                                            )
716                                        }) else {
717                                            continue;
718                                        };
719
720                                        EntityInfo::at(target.pos).with_entity_config(
721                                            entity_config.read().clone().into_inner(),
722                                            Some(entity_spec),
723                                            rng,
724                                            None,
725                                        )
726                                    },
727                                    allow_players: *allow_players,
728                                    delete_on_failure: false,
729                                });
730                            }
731                        },
732                        CombatEffect::DebuffsVulnerable {
733                            mult,
734                            scaling,
735                            filter_attacker,
736                            filter_weapon,
737                        } => {
738                            if let Some(buffs) = target.buffs {
739                                let num_debuffs = buffs.iter_active().flatten().filter(|b| {
740                                    let debuff_filter = matches!(b.kind.differentiate(), BuffDescriptor::SimpleNegative);
741                                    let attacker_filter = !filter_attacker || matches!(b.source, BuffSource::Character { by, .. } if Some(by) == attacker.map(|a| a.uid));
742                                    let weapon_filter = filter_weapon.is_none_or(|w| matches!(b.source, BuffSource::Character { tool_kind, .. } if Some(w) == tool_kind));
743                                    debuff_filter && attacker_filter && weapon_filter
744                                }).count();
745                                if num_debuffs > 0 {
746                                    let change = {
747                                        let mut change = change;
748                                        change.amount *= scaling.factor(num_debuffs as f32, 1.0)
749                                            * mult
750                                            * strength_modifier;
751                                        change.instance = rand::random();
752                                        change
753                                    };
754                                    accumulated_damage -= change.amount;
755                                    emitters.emit(HealthChangeEvent {
756                                        entity: target.entity,
757                                        change,
758                                    });
759                                }
760                            }
761                        },
762                    }
763                }
764            }
765        }
766        for effect in self
767            .effects
768            .iter()
769            .chain(
770                attacker
771                    .and_then(|a| a.stats)
772                    .map(|s| s.effects_on_attack.iter())
773                    .into_iter()
774                    .flatten(),
775            )
776            .filter(|e| {
777                allow_friendly_fire
778                    || e.target
779                        .is_none_or(|t| t == GroupTarget::All || t == target_group)
780            })
781            .filter(|e| !avoid_effect(e))
782        {
783            let requirements_met = effect.requirements.iter().all(|req| {
784                req.requirement_met(
785                    (
786                        target.health,
787                        target.buffs,
788                        target.char_state,
789                        target.ori,
790                        Some(target.uid),
791                    ),
792                    (
793                        attacker.map(|a| a.entity),
794                        attacker.and_then(|a| a.energy),
795                        attacker.and_then(|a| a.combo),
796                    ),
797                    attacker.map(|a| a.uid),
798                    accumulated_damage,
799                    emitters,
800                    dir,
801                    Some(attack_source),
802                    self.ability_info,
803                )
804            });
805            if requirements_met {
806                let mut strength_modifier = strength_modifier;
807                for modification in effect.modifications.iter() {
808                    modification.apply_mod(
809                        attacker.and_then(|a| a.pos),
810                        Some(target.pos),
811                        &mut strength_modifier,
812                    );
813                }
814                let strength_modifier = strength_modifier;
815                is_applied = true;
816                match &effect.effect {
817                    CombatEffect::Knockback(kb) => {
818                        let impulse = kb.calculate_impulse(
819                            dir,
820                            target.char_state,
821                            attacker.and_then(|a| a.stats),
822                        ) * strength_modifier;
823                        if !impulse.is_approx_zero() {
824                            emitters.emit(KnockbackEvent {
825                                entity: target.entity,
826                                impulse,
827                            });
828                        }
829                    },
830                    CombatEffect::EnergyReward(ec) => {
831                        if let Some(attacker) = attacker {
832                            emitters.emit(EnergyChangeEvent {
833                                entity: attacker.entity,
834                                change: ec
835                                    * compute_energy_reward_mod(attacker.inventory, msm)
836                                    * strength_modifier
837                                    * attacker.stats.map_or(1.0, |s| s.energy_reward_modifier)
838                                    * attacked_modifiers.energy_reward,
839                                reset_rate: false,
840                            });
841                        }
842                    },
843                    CombatEffect::Buff(b) => {
844                        if rng.random::<f32>() < b.chance {
845                            emitters.emit(BuffEvent {
846                                entity: target.entity,
847                                buff_change: BuffChange::Add(b.to_buff(
848                                    time,
849                                    (attacker.map(|a| a.uid), attacker.and_then(|a| a.mass)),
850                                    (target.stats, target.mass),
851                                    accumulated_damage,
852                                    strength_modifier,
853                                    self.ability_info,
854                                )),
855                            });
856                        }
857                    },
858                    CombatEffect::Lifesteal(l) => {
859                        if let Some(attacker_entity) = attacker.map(|a| a.entity) {
860                            let change = HealthChange {
861                                amount: accumulated_damage * l * strength_modifier,
862                                by: attacker.map(|a| a.into()),
863                                cause: None,
864                                time,
865                                precise: false,
866                                instance: rand::random(),
867                            };
868                            if change.amount.abs() > Health::HEALTH_EPSILON {
869                                emitters.emit(HealthChangeEvent {
870                                    entity: attacker_entity,
871                                    change,
872                                });
873                            }
874                        }
875                    },
876                    CombatEffect::Poise(p) => {
877                        let change = -Poise::apply_poise_reduction(
878                            *p,
879                            target.inventory,
880                            msm,
881                            target.char_state,
882                            target.stats,
883                        ) * strength_modifier
884                            * attacker
885                                .and_then(|a| a.stats)
886                                .map_or(1.0, |s| s.poise_damage_modifier);
887                        if change.abs() > Poise::POISE_EPSILON {
888                            let poise_change = PoiseChange {
889                                amount: change,
890                                impulse: *dir,
891                                by: attacker.map(|x| x.into()),
892                                cause: Some(attack_source.into()),
893                                time,
894                            };
895                            emitters.emit(PoiseChangeEvent {
896                                entity: target.entity,
897                                change: poise_change,
898                            });
899                        }
900                    },
901                    CombatEffect::Heal(h) => {
902                        let change = HealthChange {
903                            amount: h * strength_modifier,
904                            by: attacker.map(|a| a.into()),
905                            cause: None,
906                            time,
907                            precise: false,
908                            instance: rand::random(),
909                        };
910                        if change.amount.abs() > Health::HEALTH_EPSILON {
911                            emitters.emit(HealthChangeEvent {
912                                entity: target.entity,
913                                change,
914                            });
915                        }
916                    },
917                    CombatEffect::Combo(c) => {
918                        if let Some(attacker_entity) = attacker.map(|a| a.entity) {
919                            emitters.emit(ComboChangeEvent {
920                                entity: attacker_entity,
921                                change: (*c as f32 * strength_modifier).ceil() as i32,
922                            });
923                        }
924                    },
925                    CombatEffect::AdditionalDamage(damage) => {
926                        let change = HealthChange {
927                            amount: -accumulated_damage * damage * strength_modifier,
928                            by: attacker.map(|a| a.into()),
929                            cause: Some(DamageSource::from(attack_source)),
930                            time,
931                            precise: precision_mult.is_some(),
932                            instance: rand::random(),
933                        };
934                        accumulated_damage -= change.amount;
935                        emitters.emit(HealthChangeEvent {
936                            entity: target.entity,
937                            change,
938                        });
939                    },
940                    CombatEffect::RefreshBuff(chance, b) => {
941                        if rng.random::<f32>() < *chance {
942                            emitters.emit(BuffEvent {
943                                entity: target.entity,
944                                buff_change: BuffChange::Refresh(*b),
945                            });
946                        }
947                    },
948                    CombatEffect::SelfBuff(b) => {
949                        if let Some(attacker) = attacker
950                            && rng.random::<f32>() < b.chance
951                        {
952                            emitters.emit(BuffEvent {
953                                entity: attacker.entity,
954                                buff_change: BuffChange::Add(b.to_self_buff(
955                                    time,
956                                    (Some(attacker.uid), attacker.stats, attacker.mass),
957                                    accumulated_damage,
958                                    strength_modifier,
959                                    self.ability_info,
960                                )),
961                            });
962                        }
963                    },
964                    CombatEffect::Energy(e) => {
965                        emitters.emit(EnergyChangeEvent {
966                            entity: target.entity,
967                            change: e * strength_modifier,
968                            reset_rate: true,
969                        });
970                    },
971                    CombatEffect::Transform {
972                        entity_spec,
973                        allow_players,
974                    } => {
975                        if target.player.is_none() || *allow_players {
976                            emitters.emit(TransformEvent {
977                                target_entity: target.uid,
978                                entity_info: {
979                                    let Ok(entity_config) = Ron::<EntityConfig>::load(entity_spec)
980                                        .inspect_err(|error| {
981                                            error!(
982                                                ?entity_spec,
983                                                ?error,
984                                                "Could not load entity configuration for death \
985                                                 effect"
986                                            )
987                                        })
988                                    else {
989                                        continue;
990                                    };
991
992                                    EntityInfo::at(target.pos).with_entity_config(
993                                        entity_config.read().clone().into_inner(),
994                                        Some(entity_spec),
995                                        rng,
996                                        None,
997                                    )
998                                },
999                                allow_players: *allow_players,
1000                                delete_on_failure: false,
1001                            });
1002                        }
1003                    },
1004                    CombatEffect::DebuffsVulnerable {
1005                        mult,
1006                        scaling,
1007                        filter_attacker,
1008                        filter_weapon,
1009                    } => {
1010                        if let Some(buffs) = target.buffs {
1011                            let num_debuffs = buffs.iter_active().flatten().filter(|b| {
1012                                let debuff_filter = matches!(b.kind.differentiate(), BuffDescriptor::SimpleNegative);
1013                                let attacker_filter = !filter_attacker || matches!(b.source, BuffSource::Character { by, .. } if Some(by) == attacker.map(|a| a.uid));
1014                                let weapon_filter = filter_weapon.is_none_or(|w| matches!(b.source, BuffSource::Character { tool_kind, .. } if Some(w) == tool_kind));
1015                                debuff_filter && attacker_filter && weapon_filter
1016                            }).count();
1017                            if num_debuffs > 0 {
1018                                let change = HealthChange {
1019                                    amount: -accumulated_damage
1020                                        * scaling.factor(num_debuffs as f32, 1.0)
1021                                        * mult
1022                                        * strength_modifier,
1023                                    by: attacker.map(|a| a.into()),
1024                                    cause: Some(DamageSource::from(attack_source)),
1025                                    time,
1026                                    precise: precision_mult.is_some(),
1027                                    instance: rand::random(),
1028                                };
1029                                accumulated_damage -= change.amount;
1030                                emitters.emit(HealthChangeEvent {
1031                                    entity: target.entity,
1032                                    change,
1033                                });
1034                            }
1035                        }
1036                    },
1037                }
1038            }
1039        }
1040        // Emits event to handle things that should happen for any successful attack,
1041        // regardless of if the attack had any damages or effects in it
1042        if is_applied {
1043            emitters.emit(EntityAttackedHookEvent {
1044                entity: target.entity,
1045                attacker: attacker.map(|a| a.entity),
1046                attack_dir: dir,
1047                damage_dealt: accumulated_damage,
1048                attack_source,
1049            });
1050        }
1051        is_applied
1052    }
1053}
1054
1055pub fn allow_friendly_fire(
1056    entered_auras: &ReadStorage<EnteredAuras>,
1057    attacker: EcsEntity,
1058    target: EcsEntity,
1059) -> bool {
1060    entered_auras
1061        .get(attacker)
1062        .zip(entered_auras.get(target))
1063        .and_then(|(attacker, target)| {
1064            Some((
1065                attacker.auras.get(&AuraKindVariant::FriendlyFire)?,
1066                target.auras.get(&AuraKindVariant::FriendlyFire)?,
1067            ))
1068        })
1069        // Only allow friendly fire if both entities are affectd by the same FriendlyFire aura
1070        .is_some_and(|(attacker, target)| attacker.intersection(target).next().is_some())
1071}
1072
1073/// Function that checks for unintentional PvP between players.
1074///
1075/// Returns `false` if attack will create unintentional conflict,
1076/// e.g. if player with PvE mode will harm pets of other players
1077/// or other players will do the same to such player.
1078///
1079/// If both players have PvP mode enabled, interact with NPC and
1080/// in any other case, this function will return `true`
1081// TODO: add parameter for doing self-harm?
1082pub fn permit_pvp(
1083    alignments: &ReadStorage<Alignment>,
1084    players: &ReadStorage<Player>,
1085    entered_auras: &ReadStorage<EnteredAuras>,
1086    id_maps: &IdMaps,
1087    attacker: Option<EcsEntity>,
1088    target: EcsEntity,
1089) -> bool {
1090    // Return owner entity if pet,
1091    // or just return entity back otherwise
1092    let owner_if_pet = |entity| {
1093        let alignment = alignments.get(entity).copied();
1094        if let Some(Alignment::Owned(uid)) = alignment {
1095            // return original entity
1096            // if can't get owner
1097            id_maps.uid_entity(uid).unwrap_or(entity)
1098        } else {
1099            entity
1100        }
1101    };
1102
1103    // Just return ok if attacker is unknown, it's probably
1104    // environment or command.
1105    let attacker = match attacker {
1106        Some(attacker) => attacker,
1107        None => return true,
1108    };
1109
1110    // "Dereference" to owner if this is a pet.
1111    let attacker_owner = owner_if_pet(attacker);
1112    let target_owner = owner_if_pet(target);
1113
1114    // If both players are in the same ForcePvP aura, allow them to harm eachother
1115    if let (Some(attacker_auras), Some(target_auras)) = (
1116        entered_auras.get(attacker_owner),
1117        entered_auras.get(target_owner),
1118    ) && attacker_auras
1119        .auras
1120        .get(&AuraKindVariant::ForcePvP)
1121        .zip(target_auras.auras.get(&AuraKindVariant::ForcePvP))
1122        // Only allow forced pvp if both entities are affectd by the same FriendlyFire aura
1123        .is_some_and(|(attacker, target)| attacker.intersection(target).next().is_some())
1124    {
1125        return true;
1126    }
1127
1128    // Prevent PvP between pets, unless friendly fire is enabled
1129    //
1130    // This code is NOT intended to prevent pet <-> owner combat,
1131    // pets and their owners being in the same group should take care of that
1132    if attacker_owner == target_owner {
1133        return allow_friendly_fire(entered_auras, attacker, target);
1134    }
1135
1136    // Get player components
1137    let attacker_info = players.get(attacker_owner);
1138    let target_info = players.get(target_owner);
1139
1140    // Return `true` if not players.
1141    attacker_info
1142        .zip(target_info)
1143        .is_none_or(|(a, t)| a.may_harm(t))
1144}
1145
1146#[derive(Clone, Debug, Serialize, Deserialize)]
1147pub struct AttackDamage {
1148    damage: Damage,
1149    target: Option<GroupTarget>,
1150    effects: Vec<CombatEffect>,
1151    /// A random ID, used to group up attacks
1152    instance: u64,
1153}
1154
1155impl AttackDamage {
1156    pub fn new(damage: Damage, target: Option<GroupTarget>, instance: u64) -> Self {
1157        Self {
1158            damage,
1159            target,
1160            effects: Vec::new(),
1161            instance,
1162        }
1163    }
1164
1165    #[must_use]
1166    pub fn with_effect(mut self, effect: CombatEffect) -> Self {
1167        self.effects.push(effect);
1168        self
1169    }
1170}
1171
1172#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
1173pub struct AttackEffect {
1174    target: Option<GroupTarget>,
1175    effect: CombatEffect,
1176    requirements: Vec<CombatRequirement>,
1177    modifications: Vec<CombatModification>,
1178}
1179
1180impl AttackEffect {
1181    pub fn new(target: Option<GroupTarget>, effect: CombatEffect) -> Self {
1182        Self {
1183            target,
1184            effect,
1185            requirements: Vec::new(),
1186            modifications: Vec::new(),
1187        }
1188    }
1189
1190    #[must_use]
1191    pub fn with_requirement(mut self, requirement: CombatRequirement) -> Self {
1192        self.requirements.push(requirement);
1193        self
1194    }
1195
1196    #[must_use]
1197    pub fn with_modification(mut self, modification: CombatModification) -> Self {
1198        self.modifications.push(modification);
1199        self
1200    }
1201
1202    pub fn effect(&self) -> &CombatEffect { &self.effect }
1203}
1204
1205#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
1206pub struct StatEffect {
1207    pub target: StatEffectTarget,
1208    pub effect: CombatEffect,
1209    requirements: Vec<CombatRequirement>,
1210    modifications: Vec<CombatModification>,
1211}
1212
1213impl StatEffect {
1214    pub fn new(target: StatEffectTarget, effect: CombatEffect) -> Self {
1215        Self {
1216            target,
1217            effect,
1218            requirements: Vec::new(),
1219            modifications: Vec::new(),
1220        }
1221    }
1222
1223    #[must_use]
1224    pub fn with_requirement(mut self, requirement: CombatRequirement) -> Self {
1225        self.requirements.push(requirement);
1226        self
1227    }
1228
1229    #[must_use]
1230    pub fn with_modification(mut self, modification: CombatModification) -> Self {
1231        self.modifications.push(modification);
1232        self
1233    }
1234
1235    pub fn requirements(&self) -> impl Iterator<Item = &CombatRequirement> {
1236        self.requirements.iter()
1237    }
1238
1239    pub fn modifications(&self) -> impl Iterator<Item = &CombatModification> {
1240        self.modifications.iter()
1241    }
1242}
1243
1244#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
1245pub enum CombatEffect {
1246    Heal(f32),
1247    Buff(CombatBuff),
1248    Knockback(Knockback),
1249    EnergyReward(f32),
1250    Lifesteal(f32),
1251    Poise(f32),
1252    Combo(i32),
1253    /// Intended to be used when gating additional damage behind some
1254    /// requirement
1255    AdditionalDamage(f32),
1256    /// Resets duration of all buffs of this buffkind, with some probability
1257    RefreshBuff(f32, BuffKind),
1258    /// Applies buff to yourself after attack is applied
1259    SelfBuff(CombatBuff),
1260    /// Changes energy of target
1261    Energy(f32),
1262    /// String is the entity_spec
1263    Transform {
1264        entity_spec: String,
1265        /// Whether this effect applies to players or not
1266        #[serde(default)]
1267        allow_players: bool,
1268    },
1269    /// If the target hit by an attack has debuffs, they will take increased
1270    /// damage scaling with the number of active debuffs they have
1271    DebuffsVulnerable {
1272        mult: f32,
1273        scaling: ScalingKind,
1274        /// Should debuffs only be counted if they were inflicted by the
1275        /// attacker
1276        filter_attacker: bool,
1277        /// Should debuffs only be counted if they were inflicted by a specific
1278        /// weapon
1279        filter_weapon: Option<ToolKind>,
1280    },
1281}
1282
1283impl CombatEffect {
1284    pub fn apply_multiplier(self, mult: f32) -> Self {
1285        match self {
1286            CombatEffect::Heal(h) => CombatEffect::Heal(h * mult),
1287            CombatEffect::Buff(CombatBuff {
1288                kind,
1289                dur_secs,
1290                strength,
1291                chance,
1292            }) => CombatEffect::Buff(CombatBuff {
1293                kind,
1294                dur_secs,
1295                strength: strength * mult,
1296                chance,
1297            }),
1298            CombatEffect::Knockback(Knockback {
1299                direction,
1300                strength,
1301            }) => CombatEffect::Knockback(Knockback {
1302                direction,
1303                strength: strength * mult,
1304            }),
1305            CombatEffect::EnergyReward(e) => CombatEffect::EnergyReward(e * mult),
1306            CombatEffect::Lifesteal(l) => CombatEffect::Lifesteal(l * mult),
1307            CombatEffect::Poise(p) => CombatEffect::Poise(p * mult),
1308            CombatEffect::Combo(c) => CombatEffect::Combo((c as f32 * mult).ceil() as i32),
1309            CombatEffect::AdditionalDamage(v) => CombatEffect::AdditionalDamage(v * mult),
1310            CombatEffect::RefreshBuff(c, b) => CombatEffect::RefreshBuff(c, b),
1311            CombatEffect::SelfBuff(CombatBuff {
1312                kind,
1313                dur_secs,
1314                strength,
1315                chance,
1316            }) => CombatEffect::SelfBuff(CombatBuff {
1317                kind,
1318                dur_secs,
1319                strength: strength * mult,
1320                chance,
1321            }),
1322            CombatEffect::Energy(e) => CombatEffect::Energy(e * mult),
1323            effect @ CombatEffect::Transform { .. } => effect,
1324            CombatEffect::DebuffsVulnerable {
1325                mult: a,
1326                scaling,
1327                filter_attacker,
1328                filter_weapon,
1329            } => CombatEffect::DebuffsVulnerable {
1330                mult: a * mult,
1331                scaling,
1332                filter_attacker,
1333                filter_weapon,
1334            },
1335        }
1336    }
1337
1338    pub fn adjusted_by_stats(self, stats: tool::Stats) -> Self {
1339        match self {
1340            CombatEffect::Heal(h) => CombatEffect::Heal(h * stats.effect_power),
1341            CombatEffect::Buff(CombatBuff {
1342                kind,
1343                dur_secs,
1344                strength,
1345                chance,
1346            }) => CombatEffect::Buff(CombatBuff {
1347                kind,
1348                dur_secs,
1349                strength: strength * stats.buff_strength,
1350                chance,
1351            }),
1352            CombatEffect::Knockback(Knockback {
1353                direction,
1354                strength,
1355            }) => CombatEffect::Knockback(Knockback {
1356                direction,
1357                strength: strength * stats.effect_power,
1358            }),
1359            CombatEffect::EnergyReward(e) => CombatEffect::EnergyReward(e),
1360            CombatEffect::Lifesteal(l) => CombatEffect::Lifesteal(l * stats.effect_power),
1361            CombatEffect::Poise(p) => CombatEffect::Poise(p * stats.effect_power),
1362            CombatEffect::Combo(c) => CombatEffect::Combo(c),
1363            CombatEffect::AdditionalDamage(v) => {
1364                CombatEffect::AdditionalDamage(v * stats.effect_power)
1365            },
1366            CombatEffect::RefreshBuff(c, b) => CombatEffect::RefreshBuff(c, b),
1367            CombatEffect::SelfBuff(CombatBuff {
1368                kind,
1369                dur_secs,
1370                strength,
1371                chance,
1372            }) => CombatEffect::SelfBuff(CombatBuff {
1373                kind,
1374                dur_secs,
1375                strength: strength * stats.buff_strength,
1376                chance,
1377            }),
1378            CombatEffect::Energy(e) => CombatEffect::Energy(e * stats.effect_power),
1379            effect @ CombatEffect::Transform { .. } => effect,
1380            CombatEffect::DebuffsVulnerable {
1381                mult,
1382                scaling,
1383                filter_attacker,
1384                filter_weapon,
1385            } => CombatEffect::DebuffsVulnerable {
1386                mult: mult * stats.effect_power,
1387                scaling,
1388                filter_attacker,
1389                filter_weapon,
1390            },
1391        }
1392    }
1393}
1394
1395#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq)]
1396struct AttackedModifiers {
1397    energy_reward: f32,
1398    damage_mult: f32,
1399}
1400
1401impl Default for AttackedModifiers {
1402    fn default() -> Self {
1403        Self {
1404            energy_reward: 1.0,
1405            damage_mult: 1.0,
1406        }
1407    }
1408}
1409
1410#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
1411pub struct AttackedModification {
1412    modifier: AttackedModifier,
1413    requirements: Vec<CombatRequirement>,
1414    modifications: Vec<CombatModification>,
1415}
1416
1417impl AttackedModification {
1418    pub fn new(modifier: AttackedModifier) -> Self {
1419        Self {
1420            modifier,
1421            requirements: Vec::new(),
1422            modifications: Vec::new(),
1423        }
1424    }
1425
1426    #[must_use]
1427    pub fn with_requirement(mut self, requirement: CombatRequirement) -> Self {
1428        self.requirements.push(requirement);
1429        self
1430    }
1431
1432    #[must_use]
1433    pub fn with_modification(mut self, modification: CombatModification) -> Self {
1434        self.modifications.push(modification);
1435        self
1436    }
1437
1438    fn attacked_modifiers(
1439        target: &TargetInfo,
1440        attacker: Option<AttackerInfo>,
1441        emitters: &mut (impl EmitExt<EnergyChangeEvent> + EmitExt<ComboChangeEvent>),
1442        dir: Dir,
1443        attack_source: Option<AttackSource>,
1444        ability_info: Option<AbilityInfo>,
1445    ) -> AttackedModifiers {
1446        if let Some(stats) = target.stats {
1447            stats.attacked_modifications.iter().fold(
1448                AttackedModifiers::default(),
1449                |mut a_mods, a_mod| {
1450                    let requirements_met = a_mod.requirements.iter().all(|req| {
1451                        req.requirement_met(
1452                            (
1453                                target.health,
1454                                target.buffs,
1455                                target.char_state,
1456                                target.ori,
1457                                Some(target.uid),
1458                            ),
1459                            (
1460                                attacker.map(|a| a.entity),
1461                                attacker.and_then(|a| a.energy),
1462                                attacker.and_then(|a| a.combo),
1463                            ),
1464                            attacker.map(|a| a.uid),
1465                            0.0, /* When we call this function, no damage has been
1466                                  * calculated yet, so the AnyDamage requirement is
1467                                  * effectively broken, not sure if this will be issue in
1468                                  * future? */
1469                            emitters,
1470                            dir,
1471                            attack_source,
1472                            ability_info,
1473                        )
1474                    });
1475
1476                    let mut strength_modifier = 1.0;
1477                    for modification in a_mod.modifications.iter() {
1478                        modification.apply_mod(
1479                            attacker.and_then(|a| a.pos),
1480                            Some(target.pos),
1481                            &mut strength_modifier,
1482                        );
1483                    }
1484                    let strength_modifier = strength_modifier;
1485
1486                    if requirements_met {
1487                        match a_mod.modifier {
1488                            AttackedModifier::EnergyReward(er) => {
1489                                a_mods.energy_reward *= 1.0 + (er * strength_modifier);
1490                            },
1491                            AttackedModifier::DamageMultiplier(dm) => {
1492                                a_mods.damage_mult *= 1.0 + (dm * strength_modifier);
1493                            },
1494                        }
1495                    }
1496
1497                    a_mods
1498                },
1499            )
1500        } else {
1501            AttackedModifiers::default()
1502        }
1503    }
1504}
1505
1506#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq)]
1507pub enum AttackedModifier {
1508    EnergyReward(f32),
1509    DamageMultiplier(f32),
1510}
1511
1512#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq)]
1513pub enum CombatRequirement {
1514    AnyDamage,
1515    Energy(f32),
1516    Combo(u32),
1517    TargetHasBuff(BuffKind),
1518    TargetPoised,
1519    BehindTarget,
1520    TargetBlocking,
1521    TargetUnwielded,
1522    AttackSource(AttackSource),
1523    AttackInput(InputKind),
1524    Attacker(Uid),
1525    Target(Uid),
1526    StageSection(StageSection),
1527}
1528
1529impl CombatRequirement {
1530    pub fn requirement_met(
1531        &self,
1532        target: (
1533            Option<&Health>,
1534            Option<&Buffs>,
1535            Option<&CharacterState>,
1536            Option<&Ori>,
1537            Option<Uid>,
1538        ),
1539        // originator refers to the cause of the effect that requirements are being checked for.
1540        // For combat effects on an attack this will be the attacker, for damaged and death effects
1541        // this will be the target.
1542        originator: (Option<EcsEntity>, Option<&Energy>, Option<&Combo>),
1543        attacker: Option<Uid>,
1544        damage: f32,
1545        emitters: &mut (impl EmitExt<EnergyChangeEvent> + EmitExt<ComboChangeEvent>),
1546        dir: Dir,
1547        attack_source: Option<AttackSource>,
1548        ability_info: Option<AbilityInfo>,
1549    ) -> bool {
1550        let (target_health, target_buffs, target_char_state, target_ori, target_uid) = target;
1551        let (originator_entity, originator_energy, originator_combo) = originator;
1552        match self {
1553            CombatRequirement::AnyDamage => damage > 0.0 && target_health.is_some(),
1554            CombatRequirement::Energy(r) => {
1555                if let (Some(entity), Some(energy)) = (originator_entity, originator_energy) {
1556                    let sufficient_energy = energy.current() >= *r;
1557                    if sufficient_energy {
1558                        emitters.emit(EnergyChangeEvent {
1559                            entity,
1560                            change: -*r,
1561                            reset_rate: false,
1562                        });
1563                    }
1564
1565                    sufficient_energy
1566                } else {
1567                    false
1568                }
1569            },
1570            CombatRequirement::Combo(r) => {
1571                if let (Some(entity), Some(combo)) = (originator_entity, originator_combo) {
1572                    let sufficient_combo = combo.counter() >= *r;
1573                    if sufficient_combo {
1574                        emitters.emit(ComboChangeEvent {
1575                            entity,
1576                            change: -(*r as i32),
1577                        });
1578                    }
1579
1580                    sufficient_combo
1581                } else {
1582                    false
1583                }
1584            },
1585            CombatRequirement::TargetHasBuff(buff) => {
1586                target_buffs.is_some_and(|buffs| buffs.contains(*buff))
1587            },
1588            CombatRequirement::TargetPoised => target_char_state.is_some_and(|cs| cs.is_stunned()),
1589            CombatRequirement::BehindTarget => {
1590                if let Some(ori) = target_ori {
1591                    ori.look_vec().angle_between(dir.with_z(0.0)) < BEHIND_TARGET_ANGLE
1592                } else {
1593                    false
1594                }
1595            },
1596            CombatRequirement::TargetBlocking => target_char_state
1597                .zip(attack_source)
1598                .is_some_and(|(cs, attack)| cs.is_block(attack) || cs.is_parry(attack)),
1599            CombatRequirement::TargetUnwielded => {
1600                target_char_state.is_some_and(|cs| !cs.is_wield())
1601            },
1602            CombatRequirement::AttackSource(source) => attack_source == Some(*source),
1603            CombatRequirement::AttackInput(input) => {
1604                ability_info.is_some_and(|ai| ai.input == *input)
1605            },
1606            CombatRequirement::Attacker(uid) => Some(*uid) == attacker,
1607            CombatRequirement::Target(uid) => Some(*uid) == target_uid,
1608            CombatRequirement::StageSection(s) => {
1609                Some(*s) == target_char_state.and_then(|cs| cs.stage_section())
1610            },
1611        }
1612    }
1613}
1614
1615#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq)]
1616pub enum CombatModification {
1617    /// Linearly decreases effect strength starting with 1 strength at some
1618    /// distance, ending at a minimum strength by some end distance
1619    RangeWeakening {
1620        start_dist: f32,
1621        end_dist: f32,
1622        min_str: f32,
1623    },
1624}
1625
1626impl CombatModification {
1627    pub fn apply_mod(
1628        &self,
1629        attacker_pos: Option<Vec3<f32>>,
1630        target_pos: Option<Vec3<f32>>,
1631        strength_mod: &mut f32,
1632    ) {
1633        match self {
1634            Self::RangeWeakening {
1635                start_dist,
1636                end_dist,
1637                min_str,
1638            } => {
1639                if let Some((attacker_pos, target_pos)) = attacker_pos.zip(target_pos) {
1640                    let dist = attacker_pos.distance(target_pos);
1641                    // a = (y2 - y1) / (x2 - x1)
1642                    let gradient = (*min_str - 1.0) / (end_dist - start_dist).max(0.1);
1643                    // c = y2 - a*x1
1644                    let intercept = 1.0 - gradient * start_dist;
1645                    // y = clamp(a*x + c)
1646                    let strength = (gradient * dist + intercept).clamp(*min_str, 1.0);
1647                    *strength_mod *= strength;
1648                }
1649            },
1650        }
1651    }
1652}
1653
1654/// Effects applied to the rider of this entity while riding.
1655#[derive(Clone, Debug, PartialEq)]
1656pub struct RiderEffects(pub Vec<BuffEffect>);
1657
1658impl specs::Component for RiderEffects {
1659    type Storage = specs::DenseVecStorage<RiderEffects>;
1660}
1661
1662#[derive(Clone, Debug, PartialEq)]
1663/// Permanent entity death effects (unlike `Stats::effects_on_death` which is
1664/// only active as long as ie. it has a certain buff)
1665pub struct DeathEffects(pub Vec<StatEffect>);
1666
1667impl specs::Component for DeathEffects {
1668    type Storage = specs::DenseVecStorage<DeathEffects>;
1669}
1670
1671#[derive(Clone, Copy, Debug, Hash, Eq, PartialEq, Serialize, Deserialize)]
1672pub enum DamageContributor {
1673    Solo(Uid),
1674    Group { entity_uid: Uid, group: Group },
1675}
1676
1677impl DamageContributor {
1678    pub fn new(uid: Uid, group: Option<Group>) -> Self {
1679        if let Some(group) = group {
1680            DamageContributor::Group {
1681                entity_uid: uid,
1682                group,
1683            }
1684        } else {
1685            DamageContributor::Solo(uid)
1686        }
1687    }
1688
1689    pub fn uid(&self) -> Uid {
1690        match self {
1691            DamageContributor::Solo(uid) => *uid,
1692            DamageContributor::Group {
1693                entity_uid,
1694                group: _,
1695            } => *entity_uid,
1696        }
1697    }
1698}
1699
1700impl From<AttackerInfo<'_>> for DamageContributor {
1701    fn from(attacker_info: AttackerInfo) -> Self {
1702        DamageContributor::new(attacker_info.uid, attacker_info.group.copied())
1703    }
1704}
1705
1706#[derive(Copy, Clone, Debug, Hash, PartialEq, Eq, Serialize, Deserialize)]
1707pub enum DamageSource {
1708    Buff(BuffKind),
1709    Attack(AttackSource),
1710    Falling,
1711    Other,
1712}
1713
1714impl From<AttackSource> for DamageSource {
1715    fn from(attack: AttackSource) -> Self { DamageSource::Attack(attack) }
1716}
1717
1718/// DamageKind for the purpose of differentiating damage reduction
1719#[derive(Copy, Clone, Debug, Hash, PartialEq, Eq, Serialize, Deserialize)]
1720pub enum DamageKind {
1721    /// Bypasses some protection from armor
1722    Piercing,
1723    /// Reduces energy of target, dealing additional damage when target energy
1724    /// is 0
1725    Slashing,
1726    /// Deals additional poise damage the more armored the target is
1727    Crushing,
1728    /// Catch all for remaining damage kinds (TODO: differentiate further with
1729    /// staff/sceptre reworks
1730    Energy,
1731}
1732
1733const PIERCING_PENETRATION_FRACTION: f32 = 0.75;
1734const SLASHING_ENERGY_FRACTION: f32 = 0.5;
1735const CRUSHING_POISE_FRACTION: f32 = 1.0;
1736
1737#[derive(Copy, Clone, Debug, PartialEq, Serialize, Deserialize)]
1738#[serde(deny_unknown_fields)]
1739pub struct Damage {
1740    pub kind: DamageKind,
1741    pub value: f32,
1742}
1743
1744impl Damage {
1745    /// Returns the total damage reduction provided by all equipped items
1746    pub fn compute_damage_reduction(
1747        damage: Option<Self>,
1748        inventory: Option<&Inventory>,
1749        stats: Option<&Stats>,
1750        msm: &MaterialStatManifest,
1751    ) -> f32 {
1752        let protection = compute_protection(inventory, msm);
1753
1754        let penetration = if let Some(damage) = damage {
1755            if let DamageKind::Piercing = damage.kind {
1756                (damage.value * PIERCING_PENETRATION_FRACTION)
1757                    .clamp(0.0, protection.unwrap_or(0.0).max(0.0))
1758            } else {
1759                0.0
1760            }
1761        } else {
1762            0.0
1763        };
1764
1765        let protection = protection.map(|p| p - penetration);
1766
1767        const FIFTY_PERCENT_DR_THRESHOLD: f32 = 60.0;
1768
1769        let inventory_dr = match protection {
1770            Some(dr) => dr / (FIFTY_PERCENT_DR_THRESHOLD + dr.abs()),
1771            None => 1.0,
1772        };
1773
1774        let stats_dr = if let Some(stats) = stats {
1775            stats.damage_reduction.modifier()
1776        } else {
1777            0.0
1778        };
1779        // Return 100% if either DR is at 100% (admin tabard or safezone buff)
1780        if protection.is_none() || stats_dr >= 1.0 {
1781            1.0
1782        } else {
1783            1.0 - (1.0 - inventory_dr) * (1.0 - stats_dr)
1784        }
1785    }
1786
1787    pub fn calculate_health_change(
1788        self,
1789        damage_reduction: f32,
1790        block_damage_decrement: f32,
1791        damage_contributor: Option<DamageContributor>,
1792        precision_mult: Option<f32>,
1793        precision_power: f32,
1794        damage_modifier: f32,
1795        time: Time,
1796        instance: u64,
1797        damage_source: DamageSource,
1798    ) -> HealthChange {
1799        let mut damage = self.value * damage_modifier;
1800        let precise_damage = damage * precision_mult.unwrap_or(0.0) * (precision_power - 1.0);
1801        match damage_source {
1802            DamageSource::Attack(_) => {
1803                // Precise hit
1804                damage += precise_damage;
1805                // Block
1806                damage = f32::max(damage - block_damage_decrement, 0.0);
1807                // Armor
1808                damage *= 1.0 - damage_reduction;
1809
1810                HealthChange {
1811                    amount: -damage,
1812                    by: damage_contributor,
1813                    cause: Some(damage_source),
1814                    time,
1815                    precise: precision_mult.is_some(),
1816                    instance,
1817                }
1818            },
1819            DamageSource::Falling => {
1820                // Armor
1821                if (damage_reduction - 1.0).abs() < f32::EPSILON {
1822                    damage = 0.0;
1823                }
1824                HealthChange {
1825                    amount: -damage,
1826                    by: None,
1827                    cause: Some(damage_source),
1828                    time,
1829                    precise: false,
1830                    instance,
1831                }
1832            },
1833            DamageSource::Buff(_) | DamageSource::Other => HealthChange {
1834                amount: -damage,
1835                by: None,
1836                cause: Some(damage_source),
1837                time,
1838                precise: false,
1839                instance,
1840            },
1841        }
1842    }
1843
1844    pub fn interpolate_damage(&mut self, frac: f32, min: f32) {
1845        let new_damage = min + frac * (self.value - min);
1846        self.value = new_damage;
1847    }
1848}
1849
1850#[derive(Copy, Clone, Debug, PartialEq, Serialize, Deserialize)]
1851pub struct Knockback {
1852    pub direction: KnockbackDir,
1853    pub strength: f32,
1854}
1855
1856#[derive(Copy, Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
1857pub enum KnockbackDir {
1858    Away,
1859    Towards,
1860    Up,
1861    TowardsUp,
1862}
1863
1864impl Knockback {
1865    pub fn calculate_impulse(
1866        self,
1867        dir: Dir,
1868        tgt_char_state: Option<&CharacterState>,
1869        attacker_stats: Option<&Stats>,
1870    ) -> Vec3<f32> {
1871        let from_char = {
1872            let resistant = tgt_char_state
1873                .and_then(|cs| cs.ability_info())
1874                .map(|a| a.ability_meta)
1875                .is_some_and(|a| a.capabilities.contains(Capability::KNOCKBACK_RESISTANT));
1876            if resistant { 0.5 } else { 1.0 }
1877        };
1878        // TEMP: 50.0 multiplication kept until source knockback values have been
1879        // updated
1880        50.0 * self.strength
1881            * from_char
1882            * attacker_stats.map_or(1.0, |s| s.knockback_mult)
1883            * match self.direction {
1884                KnockbackDir::Away => *Dir::slerp(dir, Dir::new(Vec3::unit_z()), 0.5),
1885                KnockbackDir::Towards => *Dir::slerp(-dir, Dir::new(Vec3::unit_z()), 0.5),
1886                KnockbackDir::Up => Vec3::unit_z(),
1887                KnockbackDir::TowardsUp => *Dir::slerp(-dir, Dir::new(Vec3::unit_z()), 0.85),
1888            }
1889    }
1890
1891    #[must_use]
1892    pub fn modify_strength(mut self, power: f32) -> Self {
1893        self.strength *= power;
1894        self
1895    }
1896}
1897
1898#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq)]
1899pub struct CombatBuff {
1900    pub kind: BuffKind,
1901    pub dur_secs: Secs,
1902    pub strength: CombatBuffStrength,
1903    pub chance: f32,
1904}
1905
1906#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq)]
1907pub enum CombatBuffStrength {
1908    DamageFraction(f32),
1909    Value(f32),
1910}
1911
1912impl CombatBuffStrength {
1913    fn to_strength(self, damage: f32, strength_modifier: f32) -> f32 {
1914        match self {
1915            // Not affected by strength modifier as damage already is
1916            CombatBuffStrength::DamageFraction(f) => damage * f,
1917            CombatBuffStrength::Value(v) => v * strength_modifier,
1918        }
1919    }
1920}
1921
1922impl MulAssign<f32> for CombatBuffStrength {
1923    fn mul_assign(&mut self, mul: f32) { *self = *self * mul; }
1924}
1925
1926impl Mul<f32> for CombatBuffStrength {
1927    type Output = Self;
1928
1929    fn mul(self, mult: f32) -> Self {
1930        match self {
1931            Self::DamageFraction(val) => Self::DamageFraction(val * mult),
1932            Self::Value(val) => Self::Value(val * mult),
1933        }
1934    }
1935}
1936
1937impl CombatBuff {
1938    pub fn to_buff(
1939        self,
1940        time: Time,
1941        attacker_info: (Option<Uid>, Option<&Mass>),
1942        target_info: (Option<&Stats>, Option<&Mass>),
1943        damage: f32,
1944        strength_modifier: f32,
1945        ability_info: Option<AbilityInfo>,
1946    ) -> Buff {
1947        let (attacker_uid, attacker_mass) = attacker_info;
1948        let (target_stats, target_mass) = target_info;
1949        // TODO: Generate BufCategoryId vec (probably requires damage overhaul?)
1950        let source = if let Some(uid) = attacker_uid {
1951            BuffSource::Character {
1952                by: uid,
1953                tool_kind: ability_info.and_then(|ai| ai.tool),
1954            }
1955        } else {
1956            BuffSource::Unknown
1957        };
1958        let dest_info = DestInfo {
1959            stats: target_stats,
1960            mass: target_mass,
1961        };
1962        let target_uid = ability_info
1963            .and_then(|ai| ai.input_attr)
1964            .and_then(|ia| ia.target_entity);
1965        Buff::new(
1966            self.kind,
1967            BuffData::new(
1968                self.strength.to_strength(damage, strength_modifier),
1969                Some(self.dur_secs),
1970            ),
1971            Vec::new(),
1972            source,
1973            time,
1974            dest_info,
1975            attacker_mass,
1976            target_uid,
1977        )
1978    }
1979
1980    pub fn to_self_buff(
1981        self,
1982        time: Time,
1983        entity_info: (Option<Uid>, Option<&Stats>, Option<&Mass>),
1984        damage: f32,
1985        strength_modifier: f32,
1986        ability_info: Option<AbilityInfo>,
1987    ) -> Buff {
1988        let (entity_uid, entity_stats, entity_mass) = entity_info;
1989        // TODO: Generate BufCategoryId vec (probably requires damage overhaul?)
1990        let source = if let Some(uid) = entity_uid {
1991            BuffSource::Character {
1992                by: uid,
1993                tool_kind: ability_info.and_then(|ai| ai.tool),
1994            }
1995        } else {
1996            BuffSource::Unknown
1997        };
1998        let dest_info = DestInfo {
1999            stats: entity_stats,
2000            mass: entity_mass,
2001        };
2002        let target_uid = ability_info
2003            .and_then(|ai| ai.input_attr)
2004            .and_then(|ia| ia.target_entity);
2005        Buff::new(
2006            self.kind,
2007            BuffData::new(
2008                self.strength.to_strength(damage, strength_modifier),
2009                Some(self.dur_secs),
2010            ),
2011            Vec::new(),
2012            source,
2013            time,
2014            dest_info,
2015            entity_mass,
2016            target_uid,
2017        )
2018    }
2019}
2020
2021#[derive(Copy, Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
2022pub enum ScalingKind {
2023    Linear,
2024    Sqrt,
2025}
2026
2027impl ScalingKind {
2028    pub fn factor(&self, val: f32, norm: f32) -> f32 {
2029        match self {
2030            Self::Linear => val / norm,
2031            Self::Sqrt => (val / norm).sqrt(),
2032        }
2033    }
2034}
2035
2036pub fn get_weapon_kinds(inv: &Inventory) -> (Option<ToolKind>, Option<ToolKind>) {
2037    (
2038        inv.equipped(EquipSlot::ActiveMainhand).and_then(|i| {
2039            if let ItemKind::Tool(tool) = &*i.kind() {
2040                Some(tool.kind)
2041            } else {
2042                None
2043            }
2044        }),
2045        inv.equipped(EquipSlot::ActiveOffhand).and_then(|i| {
2046            if let ItemKind::Tool(tool) = &*i.kind() {
2047                Some(tool.kind)
2048            } else {
2049                None
2050            }
2051        }),
2052    )
2053}
2054
2055// TODO: Either remove msm or use it as argument in fn kind
2056fn weapon_rating<T: ItemDesc>(item: &T, _msm: &MaterialStatManifest) -> f32 {
2057    const POWER_WEIGHT: f32 = 2.0;
2058    const SPEED_WEIGHT: f32 = 3.0;
2059    const RANGE_WEIGHT: f32 = 0.8;
2060    const EFFECT_WEIGHT: f32 = 1.5;
2061    const EQUIP_TIME_WEIGHT: f32 = 0.0;
2062    const ENERGY_EFFICIENCY_WEIGHT: f32 = 1.5;
2063    const BUFF_STRENGTH_WEIGHT: f32 = 1.5;
2064
2065    let rating = if let ItemKind::Tool(tool) = &*item.kind() {
2066        let stats = tool.stats(item.stats_durability_multiplier());
2067
2068        // TODO: Look into changing the 0.5 to reflect armor later maybe?
2069        // Since it is only for weapon though, it probably makes sense to leave
2070        // independent for now
2071
2072        let power_rating = stats.power;
2073        let speed_rating = stats.speed - 1.0;
2074        let range_rating = stats.range - 1.0;
2075        let effect_rating = stats.effect_power - 1.0;
2076        let equip_time_rating = 0.5 - stats.equip_time_secs;
2077        let energy_efficiency_rating = stats.energy_efficiency - 1.0;
2078        let buff_strength_rating = stats.buff_strength - 1.0;
2079
2080        power_rating * POWER_WEIGHT
2081            + speed_rating * SPEED_WEIGHT
2082            + range_rating * RANGE_WEIGHT
2083            + effect_rating * EFFECT_WEIGHT
2084            + equip_time_rating * EQUIP_TIME_WEIGHT
2085            + energy_efficiency_rating * ENERGY_EFFICIENCY_WEIGHT
2086            + buff_strength_rating * BUFF_STRENGTH_WEIGHT
2087    } else {
2088        0.0
2089    };
2090    rating.max(0.0)
2091}
2092
2093fn weapon_skills(inventory: &Inventory, skill_set: &SkillSet) -> f32 {
2094    let (mainhand, offhand) = get_weapon_kinds(inventory);
2095    let mainhand_skills = if let Some(tool) = mainhand {
2096        skill_set.earned_sp(SkillGroupKind::Weapon(tool)) as f32
2097    } else {
2098        0.0
2099    };
2100    let offhand_skills = if let Some(tool) = offhand {
2101        skill_set.earned_sp(SkillGroupKind::Weapon(tool)) as f32
2102    } else {
2103        0.0
2104    };
2105    mainhand_skills.max(offhand_skills)
2106}
2107
2108fn get_weapon_rating(inventory: &Inventory, msm: &MaterialStatManifest) -> f32 {
2109    let mainhand_rating = if let Some(item) = inventory.equipped(EquipSlot::ActiveMainhand) {
2110        weapon_rating(item, msm)
2111    } else {
2112        0.0
2113    };
2114
2115    let offhand_rating = if let Some(item) = inventory.equipped(EquipSlot::ActiveOffhand) {
2116        weapon_rating(item, msm)
2117    } else {
2118        0.0
2119    };
2120
2121    mainhand_rating.max(offhand_rating)
2122}
2123
2124pub fn combat_rating(
2125    inventory: &Inventory,
2126    health: &Health,
2127    energy: &Energy,
2128    poise: &Poise,
2129    skill_set: &SkillSet,
2130    body: Body,
2131    msm: &MaterialStatManifest,
2132) -> f32 {
2133    const WEAPON_WEIGHT: f32 = 1.0;
2134    const HEALTH_WEIGHT: f32 = 1.5;
2135    const ENERGY_WEIGHT: f32 = 0.5;
2136    const SKILLS_WEIGHT: f32 = 1.0;
2137    const POISE_WEIGHT: f32 = 0.5;
2138    const PRECISION_WEIGHT: f32 = 0.5;
2139    // Normalized with a standard max health of 100
2140    let health_rating = health.base_max()
2141        / 100.0
2142        / (1.0 - Damage::compute_damage_reduction(None, Some(inventory), None, msm)).max(0.00001);
2143
2144    // Normalized with a standard max energy of 100 and energy reward multiplier of
2145    // x1
2146    let energy_rating = (energy.base_max() + compute_max_energy_mod(Some(inventory), msm)) / 100.0
2147        * compute_energy_reward_mod(Some(inventory), msm);
2148
2149    // Normalized with a standard max poise of 100
2150    let poise_rating = poise.base_max()
2151        / 100.0
2152        / (1.0 - Poise::compute_poise_damage_reduction(Some(inventory), msm, None, None))
2153            .max(0.00001);
2154
2155    // Normalized with a standard precision multiplier of 1.2
2156    let precision_rating = compute_precision_mult(Some(inventory), msm) / 1.2;
2157
2158    // Assumes a standard person has earned 20 skill points in the general skill
2159    // tree and 10 skill points for the weapon skill tree
2160    let skills_rating = (skill_set.earned_sp(SkillGroupKind::General) as f32 / 20.0
2161        + weapon_skills(inventory, skill_set) / 10.0)
2162        / 2.0;
2163
2164    let weapon_rating = get_weapon_rating(inventory, msm);
2165
2166    let combined_rating = (health_rating * HEALTH_WEIGHT
2167        + energy_rating * ENERGY_WEIGHT
2168        + poise_rating * POISE_WEIGHT
2169        + precision_rating * PRECISION_WEIGHT
2170        + skills_rating * SKILLS_WEIGHT
2171        + weapon_rating * WEAPON_WEIGHT)
2172        / (HEALTH_WEIGHT
2173            + ENERGY_WEIGHT
2174            + POISE_WEIGHT
2175            + PRECISION_WEIGHT
2176            + SKILLS_WEIGHT
2177            + WEAPON_WEIGHT);
2178
2179    // Body multiplier meant to account for an enemy being harder than equipment and
2180    // skills would account for. It should only not be 1.0 for non-humanoids
2181    combined_rating * body.combat_multiplier()
2182}
2183
2184pub fn compute_precision_mult(inventory: Option<&Inventory>, msm: &MaterialStatManifest) -> f32 {
2185    // Starts with a value of 0.1 when summing the stats from each armor piece, and
2186    // defaults to a value of 0.1 if no inventory is equipped. Precision multiplier
2187    // cannot go below 1
2188    1.0 + inventory
2189        .map_or(0.1, |inv| {
2190            inv.equipped_items()
2191                .filter_map(|item| {
2192                    if let ItemKind::Armor(armor) = &*item.kind() {
2193                        armor
2194                            .stats(msm, item.stats_durability_multiplier())
2195                            .precision_power
2196                    } else {
2197                        None
2198                    }
2199                })
2200                .fold(0.1, |a, b| a + b)
2201        })
2202        .max(0.0)
2203}
2204
2205/// Computes the energy reward modifier from worn armor
2206pub fn compute_energy_reward_mod(inventory: Option<&Inventory>, msm: &MaterialStatManifest) -> f32 {
2207    // Starts with a value of 1.0 when summing the stats from each armor piece, and
2208    // defaults to a value of 1.0 if no inventory is present
2209    inventory.map_or(1.0, |inv| {
2210        inv.equipped_items()
2211            .filter_map(|item| {
2212                if let ItemKind::Armor(armor) = &*item.kind() {
2213                    armor
2214                        .stats(msm, item.stats_durability_multiplier())
2215                        .energy_reward
2216                } else {
2217                    None
2218                }
2219            })
2220            .fold(1.0, |a, b| a + b)
2221    })
2222}
2223
2224/// Computes the additive modifier that should be applied to max energy from the
2225/// currently equipped items
2226pub fn compute_max_energy_mod(inventory: Option<&Inventory>, msm: &MaterialStatManifest) -> f32 {
2227    // Defaults to a value of 0 if no inventory is present
2228    inventory.map_or(0.0, |inv| {
2229        inv.equipped_items()
2230            .filter_map(|item| {
2231                if let ItemKind::Armor(armor) = &*item.kind() {
2232                    armor
2233                        .stats(msm, item.stats_durability_multiplier())
2234                        .energy_max
2235                } else {
2236                    None
2237                }
2238            })
2239            .sum()
2240    })
2241}
2242
2243/// Returns a value to be included as a multiplicative factor in perception
2244/// distance checks.
2245pub fn perception_dist_multiplier_from_stealth(
2246    inventory: Option<&Inventory>,
2247    character_state: Option<&CharacterState>,
2248    msm: &MaterialStatManifest,
2249) -> f32 {
2250    const SNEAK_MULTIPLIER: f32 = 0.7;
2251
2252    let item_stealth_multiplier = stealth_multiplier_from_items(inventory, msm);
2253    let is_sneaking = character_state.is_some_and(|state| state.is_stealthy());
2254
2255    let multiplier = item_stealth_multiplier * if is_sneaking { SNEAK_MULTIPLIER } else { 1.0 };
2256
2257    multiplier.clamp(0.0, 1.0)
2258}
2259
2260pub fn compute_stealth(inventory: Option<&Inventory>, msm: &MaterialStatManifest) -> f32 {
2261    inventory.map_or(0.0, |inv| {
2262        inv.equipped_items()
2263            .filter_map(|item| {
2264                if let ItemKind::Armor(armor) = &*item.kind() {
2265                    armor.stats(msm, item.stats_durability_multiplier()).stealth
2266                } else {
2267                    None
2268                }
2269            })
2270            .sum()
2271    })
2272}
2273
2274pub fn stealth_multiplier_from_items(
2275    inventory: Option<&Inventory>,
2276    msm: &MaterialStatManifest,
2277) -> f32 {
2278    let stealth_sum = compute_stealth(inventory, msm);
2279
2280    (1.0 / (1.0 + stealth_sum)).clamp(0.0, 1.0)
2281}
2282
2283/// Computes the total protection provided from armor. Is used to determine the
2284/// damage reduction applied to damage received by an entity None indicates that
2285/// the armor equipped makes the entity invulnerable
2286pub fn compute_protection(
2287    inventory: Option<&Inventory>,
2288    msm: &MaterialStatManifest,
2289) -> Option<f32> {
2290    inventory.map_or(Some(0.0), |inv| {
2291        inv.equipped_items()
2292            .filter_map(|item| {
2293                if let ItemKind::Armor(armor) = &*item.kind() {
2294                    armor
2295                        .stats(msm, item.stats_durability_multiplier())
2296                        .protection
2297                } else {
2298                    None
2299                }
2300            })
2301            .map(|protection| match protection {
2302                Protection::Normal(protection) => Some(protection),
2303                Protection::Invincible => None,
2304            })
2305            .sum::<Option<f32>>()
2306    })
2307}
2308
2309/// Computes the total resilience provided from armor. Is used to determine the
2310/// reduction applied to poise damage received by an entity. None indicates that
2311/// the armor equipped makes the entity invulnerable to poise damage.
2312pub fn compute_poise_resilience(
2313    inventory: Option<&Inventory>,
2314    msm: &MaterialStatManifest,
2315) -> Option<f32> {
2316    inventory.map_or(Some(0.0), |inv| {
2317        inv.equipped_items()
2318            .filter_map(|item| {
2319                if let ItemKind::Armor(armor) = &*item.kind() {
2320                    armor
2321                        .stats(msm, item.stats_durability_multiplier())
2322                        .poise_resilience
2323                } else {
2324                    None
2325                }
2326            })
2327            .map(|protection| match protection {
2328                Protection::Normal(protection) => Some(protection),
2329                Protection::Invincible => None,
2330            })
2331            .sum::<Option<f32>>()
2332    })
2333}
2334
2335/// Used to compute the precision multiplier achieved by flanking a target
2336pub fn precision_mult_from_flank(
2337    attack_dir: Vec3<f32>,
2338    target_ori: Option<&Ori>,
2339    precision_flank_multipliers: FlankMults,
2340    precision_flank_invert: bool,
2341) -> Option<f32> {
2342    let angle = target_ori.map(|t_ori| {
2343        t_ori.look_dir().angle_between(if precision_flank_invert {
2344            -attack_dir
2345        } else {
2346            attack_dir
2347        })
2348    });
2349    match angle {
2350        Some(angle) if angle < FULL_FLANK_ANGLE => Some(
2351            MAX_BACK_FLANK_PRECISION
2352                * if precision_flank_invert {
2353                    precision_flank_multipliers.front
2354                } else {
2355                    precision_flank_multipliers.back
2356                },
2357        ),
2358        Some(angle) if angle < PARTIAL_FLANK_ANGLE => {
2359            Some(MAX_SIDE_FLANK_PRECISION * precision_flank_multipliers.side)
2360        },
2361        Some(_) | None => None,
2362    }
2363}
2364
2365#[derive(Copy, Clone, Debug, PartialEq, Serialize, Deserialize)]
2366pub struct FlankMults {
2367    pub back: f32,
2368    pub front: f32,
2369    pub side: f32,
2370}
2371
2372impl Default for FlankMults {
2373    fn default() -> Self {
2374        FlankMults {
2375            back: 1.0,
2376            front: 1.0,
2377            side: 1.0,
2378        }
2379    }
2380}
2381
2382pub fn block_strength(inventory: &Inventory, char_state: &CharacterState) -> f32 {
2383    let (ability_block_strength, hand) = match char_state {
2384        CharacterState::BasicBlock(data) => (
2385            data.static_data.block_strength,
2386            data.static_data.ability_info.hand,
2387        ),
2388        CharacterState::RiposteMelee(data) => (
2389            data.static_data.block_strength,
2390            data.static_data.ability_info.hand,
2391        ),
2392        _ => char_state
2393            .ability_info()
2394            .map(|ability| (ability.ability_meta.capabilities, ability.hand))
2395            .map_or((0.0, None), |(capabilities, hand)| {
2396                (
2397                    if capabilities.contains(Capability::PARRIES)
2398                        || capabilities.contains(Capability::PARRIES_MELEE)
2399                        || capabilities.contains(Capability::BLOCKS)
2400                    {
2401                        FALLBACK_BLOCK_STRENGTH
2402                    } else {
2403                        0.0
2404                    },
2405                    hand,
2406                )
2407            }),
2408    };
2409
2410    let tool_block_strength = hand
2411        .and_then(|hand| inventory.equipped(hand.to_equip_slot()))
2412        .map_or(1.0, |item| match &*item.kind() {
2413            ItemKind::Tool(tool) => tool.stats(item.stats_durability_multiplier()).power,
2414            _ => 1.0,
2415        });
2416
2417    ability_block_strength * tool_block_strength
2418}
2419
2420pub fn get_equip_slot_by_block_priority(inventory: Option<&Inventory>) -> EquipSlot {
2421    inventory
2422        .map(get_weapon_kinds)
2423        .map_or(
2424            EquipSlot::ActiveMainhand,
2425            |weapon_kinds| match weapon_kinds {
2426                (Some(mainhand), Some(offhand)) => {
2427                    if mainhand.block_priority() >= offhand.block_priority() {
2428                        EquipSlot::ActiveMainhand
2429                    } else {
2430                        EquipSlot::ActiveOffhand
2431                    }
2432                },
2433                (Some(_), None) => EquipSlot::ActiveMainhand,
2434                (None, Some(_)) => EquipSlot::ActiveOffhand,
2435                (None, None) => EquipSlot::ActiveMainhand,
2436            },
2437        )
2438}