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