1use crate::{
2 assets::{AssetExt, Ron},
3 comp::{
4 Alignment, Body, Buffs, CharacterState, Combo, Energy, Group, Health, HealthChange,
5 InputKind, Inventory, Mass, Ori, Player, Poise, PoiseChange, SkillSet, Stats,
6 ability::Capability,
7 aura::{AuraKindVariant, EnteredAuras},
8 buff::{Buff, BuffChange, BuffData, BuffDescriptor, BuffKind, BuffSource, DestInfo},
9 inventory::{
10 item::{
11 ItemDesc, ItemKind, MaterialStatManifest,
12 armor::Protection,
13 tool::{self, ToolKind},
14 },
15 slot::EquipSlot,
16 },
17 skillset::SkillGroupKind,
18 },
19 effect::BuffEffect,
20 event::{
21 BuffEvent, ComboChangeEvent, EmitExt, EnergyChangeEvent, EntityAttackedHookEvent,
22 HealthChangeEvent, KnockbackEvent, ParryHookEvent, PoiseChangeEvent, TransformEvent,
23 },
24 generation::{EntityConfig, EntityInfo},
25 outcome::Outcome,
26 resources::{Secs, Time},
27 states::utils::{AbilityInfo, StageSection},
28 uid::{IdMaps, Uid},
29 util::Dir,
30};
31use rand::Rng;
32use serde::{Deserialize, Serialize};
33use specs::{Entity as EcsEntity, ReadStorage};
34use std::ops::{Mul, MulAssign};
35use tracing::error;
36use vek::*;
37
38pub enum AttackTarget {
39 AllInRange(f32),
40 Pos(Vec3<f32>),
41 Entity(EcsEntity),
42}
43
44#[derive(Copy, Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
45pub enum GroupTarget {
46 InGroup,
47 OutOfGroup,
48 All,
49}
50
51#[derive(Copy, Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
52pub enum StatEffectTarget {
53 Attacker,
54 Target,
55}
56
57#[derive(Copy, Clone, PartialEq, Eq, Debug, Serialize, Deserialize, Hash)]
58pub enum AttackSource {
59 Melee,
60 Projectile,
61 Beam,
62 GroundShockwave,
63 AirShockwave,
64 UndodgeableShockwave,
65 Explosion,
66 Arc,
67}
68
69pub const FULL_FLANK_ANGLE: f32 = std::f32::consts::PI / 4.0;
70pub const PARTIAL_FLANK_ANGLE: f32 = std::f32::consts::PI * 3.0 / 4.0;
71pub const 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 pub permit_pvp: bool,
120 pub target_group: GroupTarget,
121 pub allow_friendly_fire: bool,
124 pub precision_mult: Option<f32>,
125}
126
127#[derive(Clone, Debug, Serialize, Deserialize)] pub 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 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 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 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 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 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 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 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 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 .is_some_and(|(attacker, target)| attacker.intersection(target).next().is_some())
1135}
1136
1137pub 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 let owner_if_pet = |entity| {
1157 let alignment = alignments.get(entity).copied();
1158 if let Some(Alignment::Owned(uid)) = alignment {
1159 id_maps.uid_entity(uid).unwrap_or(entity)
1162 } else {
1163 entity
1164 }
1165 };
1166
1167 let attacker = match attacker {
1170 Some(attacker) => attacker,
1171 None => return true,
1172 };
1173
1174 let attacker_owner = owner_if_pet(attacker);
1176 let target_owner = owner_if_pet(target);
1177
1178 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 .is_some_and(|(attacker, target)| attacker.intersection(target).next().is_some())
1188 {
1189 return true;
1190 }
1191
1192 if attacker_owner == target_owner {
1197 return allow_friendly_fire(entered_auras, attacker, target);
1198 }
1199
1200 let attacker_info = players.get(attacker_owner);
1202 let target_info = players.get(target_owner);
1203
1204 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 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 StageVulnerable(f32, StageSection),
1324 RefreshBuff(f32, BuffKind),
1326 BuffsVulnerable(f32, BuffKind),
1333 StunnedVulnerable(f32),
1340 SelfBuff(CombatBuff),
1342 Energy(f32),
1344 Transform {
1346 entity_spec: String,
1347 #[serde(default)]
1349 allow_players: bool,
1350 },
1351 DebuffsVulnerable {
1354 mult: f32,
1355 scaling: ScalingKind,
1356 filter_attacker: bool,
1359 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, 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: (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 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 let gradient = (*min_str - 1.0) / (end_dist - start_dist).max(0.1);
1717 let intercept = 1.0 - gradient * start_dist;
1719 let strength = (gradient * dist + intercept).clamp(*min_str, 1.0);
1721 *strength_mod *= strength;
1722 }
1723 },
1724 }
1725 }
1726}
1727
1728#[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)]
1737pub 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#[derive(Copy, Clone, Debug, Hash, PartialEq, Eq, Serialize, Deserialize)]
1794pub enum DamageKind {
1795 Piercing,
1797 Slashing,
1800 Crushing,
1802 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 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 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 damage += precise_damage;
1879 damage *= 1.0 - damage_reduction;
1881 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 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 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 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 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 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
2116fn 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 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 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 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 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 let precision_rating = compute_precision_mult(Some(inventory), msm) / 1.2;
2218
2219 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 combined_rating * body.combat_multiplier()
2243}
2244
2245pub fn compute_precision_mult(inventory: Option<&Inventory>, msm: &MaterialStatManifest) -> f32 {
2246 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
2266pub fn compute_energy_reward_mod(inventory: Option<&Inventory>, msm: &MaterialStatManifest) -> f32 {
2268 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
2285pub fn compute_max_energy_mod(inventory: Option<&Inventory>, msm: &MaterialStatManifest) -> f32 {
2288 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
2304pub 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
2344pub 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
2370pub 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
2396pub 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}