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