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