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