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