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