1use crate::{
2 comp::{
3 Alignment, Body, Buffs, CharacterState, Combo, Energy, Group, Health, HealthChange,
4 Inventory, Mass, Ori, Player, Poise, PoiseChange, SkillSet, Stats,
5 ability::Capability,
6 aura::{AuraKindVariant, EnteredAuras},
7 buff::{Buff, BuffChange, BuffData, BuffKind, BuffSource, DestInfo},
8 inventory::{
9 item::{
10 ItemDesc, ItemKind, MaterialStatManifest,
11 armor::Protection,
12 tool::{self, ToolKind},
13 },
14 slot::EquipSlot,
15 },
16 skillset::SkillGroupKind,
17 },
18 event::{
19 BuffEvent, ComboChangeEvent, EmitExt, EnergyChangeEvent, EntityAttackedHookEvent,
20 HealthChangeEvent, KnockbackEvent, ParryHookEvent, PoiseChangeEvent,
21 },
22 outcome::Outcome,
23 resources::{Secs, Time},
24 states::utils::StageSection,
25 uid::{IdMaps, Uid},
26 util::Dir,
27};
28use rand::Rng;
29use serde::{Deserialize, Serialize};
30use specs::{Entity as EcsEntity, ReadStorage};
31use std::ops::{Mul, MulAssign};
32use vek::*;
33
34#[derive(Copy, Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
35pub enum GroupTarget {
36 InGroup,
37 OutOfGroup,
38}
39
40#[derive(Copy, Clone, Debug, Serialize, Deserialize)]
41pub enum AttackSource {
42 Melee,
43 Projectile,
44 Beam,
45 GroundShockwave,
46 AirShockwave,
47 UndodgeableShockwave,
48 Explosion,
49}
50
51pub const FULL_FLANK_ANGLE: f32 = std::f32::consts::PI / 4.0;
52pub const PARTIAL_FLANK_ANGLE: f32 = std::f32::consts::PI * 3.0 / 4.0;
53pub const PROJECTILE_HEADSHOT_PROPORTION: f32 = 0.1;
55pub const BEAM_DURATION_PRECISION: f32 = 2.5;
56pub const MAX_BACK_FLANK_PRECISION: f32 = 0.75;
57pub const MAX_SIDE_FLANK_PRECISION: f32 = 0.25;
58pub const MAX_HEADSHOT_PRECISION: f32 = 1.0;
59pub const MAX_TOP_HEADSHOT_PRECISION: f32 = 0.5;
60pub const MAX_BEAM_DUR_PRECISION: f32 = 0.25;
61pub const MAX_MELEE_POISE_PRECISION: f32 = 0.5;
62pub const MAX_BLOCK_POISE_COST: f32 = 25.0;
63pub const PARRY_BONUS_MULTIPLIER: f32 = 5.0;
64pub const FALLBACK_BLOCK_STRENGTH: f32 = 5.0;
65pub const BEHIND_TARGET_ANGLE: f32 = 45.0;
66pub const BASE_PARRIED_POISE_PUNISHMENT: f32 = 100.0 / 3.5;
67
68#[derive(Copy, Clone)]
69pub struct AttackerInfo<'a> {
70 pub entity: EcsEntity,
71 pub uid: Uid,
72 pub group: Option<&'a Group>,
73 pub energy: Option<&'a Energy>,
74 pub combo: Option<&'a Combo>,
75 pub inventory: Option<&'a Inventory>,
76 pub stats: Option<&'a Stats>,
77 pub mass: Option<&'a Mass>,
78}
79
80#[derive(Copy, Clone)]
81pub struct TargetInfo<'a> {
82 pub entity: EcsEntity,
83 pub uid: Uid,
84 pub inventory: Option<&'a Inventory>,
85 pub stats: Option<&'a Stats>,
86 pub health: Option<&'a Health>,
87 pub pos: Vec3<f32>,
88 pub ori: Option<&'a Ori>,
89 pub char_state: Option<&'a CharacterState>,
90 pub energy: Option<&'a Energy>,
91 pub buffs: Option<&'a Buffs>,
92 pub mass: Option<&'a Mass>,
93}
94
95#[derive(Clone, Copy)]
96pub struct AttackOptions {
97 pub target_dodging: bool,
98 pub permit_pvp: bool,
100 pub target_group: GroupTarget,
101 pub allow_friendly_fire: bool,
104 pub precision_mult: Option<f32>,
105}
106
107#[derive(Clone, Debug, Serialize, Deserialize)] pub struct Attack {
109 damages: Vec<AttackDamage>,
110 effects: Vec<AttackEffect>,
111 precision_multiplier: f32,
112}
113
114impl Default for Attack {
115 fn default() -> Self {
116 Self {
117 damages: Vec::new(),
118 effects: Vec::new(),
119 precision_multiplier: 1.0,
120 }
121 }
122}
123
124impl Attack {
125 #[must_use]
126 pub fn with_damage(mut self, damage: AttackDamage) -> Self {
127 self.damages.push(damage);
128 self
129 }
130
131 #[must_use]
132 pub fn with_effect(mut self, effect: AttackEffect) -> Self {
133 self.effects.push(effect);
134 self
135 }
136
137 #[must_use]
138 pub fn with_precision(mut self, precision_multiplier: f32) -> Self {
139 self.precision_multiplier = precision_multiplier;
140 self
141 }
142
143 #[must_use]
144 pub fn with_combo_requirement(self, combo: i32, requirement: CombatRequirement) -> Self {
145 self.with_effect(
146 AttackEffect::new(None, CombatEffect::Combo(combo)).with_requirement(requirement),
147 )
148 }
149
150 #[must_use]
151 pub fn with_combo(self, combo: i32) -> Self {
152 self.with_combo_requirement(combo, CombatRequirement::AnyDamage)
153 }
154
155 #[must_use]
156 pub fn with_combo_increment(self) -> Self { self.with_combo(1) }
157
158 pub fn effects(&self) -> impl Iterator<Item = &AttackEffect> { self.effects.iter() }
159
160 pub fn compute_block_damage_decrement(
161 attacker: Option<&AttackerInfo>,
162 damage_reduction: f32,
163 target: &TargetInfo,
164 source: AttackSource,
165 dir: Dir,
166 damage: Damage,
167 msm: &MaterialStatManifest,
168 time: Time,
169 emitters: &mut (impl EmitExt<ParryHookEvent> + EmitExt<PoiseChangeEvent>),
170 mut emit_outcome: impl FnMut(Outcome),
171 ) -> f32 {
172 if damage.value > 0.0 {
173 if let (Some(char_state), Some(ori), Some(inventory)) =
174 (target.char_state, target.ori, target.inventory)
175 {
176 let is_parry = char_state.is_parry(source);
177 let is_block = char_state.is_block(source);
178 let damage_value = damage.value * (1.0 - damage_reduction);
179 let mut block_strength = block_strength(inventory, char_state);
180
181 if ori.look_vec().angle_between(-dir.with_z(0.0)) < char_state.block_angle()
182 && (is_parry || is_block)
183 && block_strength > 0.0
184 {
185 if is_parry {
186 block_strength *= PARRY_BONUS_MULTIPLIER;
187
188 emitters.emit(ParryHookEvent {
189 defender: target.entity,
190 attacker: attacker.map(|a| a.entity),
191 source,
192 poise_multiplier: 2.0 - (damage_value / block_strength).min(1.0),
193 });
194 }
195
196 let poise_cost =
197 (damage_value / block_strength).min(1.0) * MAX_BLOCK_POISE_COST;
198
199 let poise_change = Poise::apply_poise_reduction(
200 poise_cost,
201 target.inventory,
202 msm,
203 target.char_state,
204 target.stats,
205 );
206
207 emit_outcome(Outcome::Block {
208 parry: is_parry,
209 pos: target.pos,
210 uid: target.uid,
211 });
212 emitters.emit(PoiseChangeEvent {
213 entity: target.entity,
214 change: PoiseChange {
215 amount: -poise_change,
216 impulse: *dir,
217 by: attacker.map(|x| (*x).into()),
218 cause: Some(damage.source),
219 time,
220 },
221 });
222 block_strength
223 } else {
224 0.0
225 }
226 } else {
227 0.0
228 }
229 } else {
230 0.0
231 }
232 }
233
234 pub fn compute_damage_reduction(
235 attacker: Option<&AttackerInfo>,
236 target: &TargetInfo,
237 damage: Damage,
238 msm: &MaterialStatManifest,
239 ) -> f32 {
240 if damage.value > 0.0 {
241 let attacker_penetration = attacker
242 .and_then(|a| a.stats)
243 .map_or(0.0, |s| s.mitigations_penetration)
244 .clamp(0.0, 1.0);
245 let raw_damage_reduction =
246 Damage::compute_damage_reduction(Some(damage), target.inventory, target.stats, msm);
247
248 if raw_damage_reduction >= 1.0 {
249 raw_damage_reduction
250 } else {
251 (1.0 - attacker_penetration) * raw_damage_reduction
252 }
253 } else {
254 0.0
255 }
256 }
257
258 pub fn apply_attack(
259 &self,
260 attacker: Option<AttackerInfo>,
261 target: &TargetInfo,
262 dir: Dir,
263 options: AttackOptions,
264 strength_modifier: f32,
267 attack_source: AttackSource,
268 time: Time,
269 emitters: &mut (
270 impl EmitExt<HealthChangeEvent>
271 + EmitExt<EnergyChangeEvent>
272 + EmitExt<ParryHookEvent>
273 + EmitExt<KnockbackEvent>
274 + EmitExt<BuffEvent>
275 + EmitExt<PoiseChangeEvent>
276 + EmitExt<ComboChangeEvent>
277 + EmitExt<EntityAttackedHookEvent>
278 ),
279 mut emit_outcome: impl FnMut(Outcome),
280 rng: &mut rand::rngs::ThreadRng,
281 damage_instance_offset: u64,
282 ) -> bool {
283 let msm = &MaterialStatManifest::load().read();
285
286 let AttackOptions {
287 target_dodging,
288 permit_pvp,
289 allow_friendly_fire,
290 target_group,
291 precision_mult,
292 } = options;
293
294 let avoid_damage = |attack_damage: &AttackDamage| {
300 target_dodging
301 || (!permit_pvp && matches!(attack_damage.target, Some(GroupTarget::OutOfGroup)))
302 };
303 let avoid_effect = |attack_effect: &AttackEffect| {
304 target_dodging
305 || (!permit_pvp && matches!(attack_effect.target, Some(GroupTarget::OutOfGroup)))
306 };
307
308 let from_precision_mult = attacker
309 .and_then(|a| a.stats)
310 .and_then(|s| s.precision_multiplier_override)
311 .or(precision_mult);
312
313 let from_precision_vulnerability_mult = target
314 .stats
315 .and_then(|s| s.precision_vulnerability_multiplier_override);
316
317 let precision_mult = match (from_precision_mult, from_precision_vulnerability_mult) {
318 (Some(a), Some(b)) => Some(a.max(b)),
319 (Some(a), None) | (None, Some(a)) => Some(a),
320 (None, None) => None,
321 };
322
323 let mut is_applied = false;
324 let mut accumulated_damage = 0.0;
325 let damage_modifier = attacker
326 .and_then(|a| a.stats)
327 .map_or(1.0, |s| s.attack_damage_modifier);
328 for damage in self
329 .damages
330 .iter()
331 .filter(|d| allow_friendly_fire || d.target.is_none_or(|t| t == target_group))
332 .filter(|d| !avoid_damage(d))
333 {
334 let damage_instance = damage.instance + damage_instance_offset;
335 is_applied = true;
336
337 let damage_reduction =
338 Attack::compute_damage_reduction(attacker.as_ref(), target, damage.damage, msm);
339
340 let block_damage_decrement = Attack::compute_block_damage_decrement(
341 attacker.as_ref(),
342 damage_reduction,
343 target,
344 attack_source,
345 dir,
346 damage.damage,
347 msm,
348 time,
349 emitters,
350 &mut emit_outcome,
351 );
352
353 let change = damage.damage.calculate_health_change(
354 damage_reduction,
355 block_damage_decrement,
356 attacker.map(|x| x.into()),
357 precision_mult,
358 self.precision_multiplier,
359 strength_modifier * damage_modifier,
360 time,
361 damage_instance,
362 );
363 let applied_damage = -change.amount;
364 accumulated_damage += applied_damage;
365
366 if change.amount.abs() > Health::HEALTH_EPSILON {
367 emitters.emit(HealthChangeEvent {
368 entity: target.entity,
369 change,
370 });
371 match damage.damage.kind {
372 DamageKind::Slashing => {
373 if let Some(target_energy) = target.energy {
377 let energy_change = applied_damage * SLASHING_ENERGY_FRACTION;
378 if energy_change > target_energy.current() {
379 let health_damage = energy_change - target_energy.current();
380 accumulated_damage += health_damage;
381 let health_change = HealthChange {
382 amount: -health_damage,
383 by: attacker.map(|x| x.into()),
384 cause: Some(damage.damage.source),
385 time,
386 precise: precision_mult.is_some(),
387 instance: damage_instance,
388 };
389 emitters.emit(HealthChangeEvent {
390 entity: target.entity,
391 change: health_change,
392 });
393 }
394 emitters.emit(EnergyChangeEvent {
395 entity: target.entity,
396 change: -energy_change,
397 reset_rate: false,
398 });
399 }
400 },
401 DamageKind::Crushing => {
402 let reduced_damage =
407 applied_damage * damage_reduction / (1.0 - damage_reduction);
408 let poise = reduced_damage
409 * CRUSHING_POISE_FRACTION
410 * attacker
411 .and_then(|a| a.stats)
412 .map_or(1.0, |s| s.poise_damage_modifier);
413 let change = -Poise::apply_poise_reduction(
414 poise,
415 target.inventory,
416 msm,
417 target.char_state,
418 target.stats,
419 );
420 let poise_change = PoiseChange {
421 amount: change,
422 impulse: *dir,
423 by: attacker.map(|x| x.into()),
424 cause: Some(damage.damage.source),
425 time,
426 };
427 if change.abs() > Poise::POISE_EPSILON {
428 if let Some(CharacterState::Stunned(data)) = target.char_state {
431 let health_change =
432 change * data.static_data.poise_state.damage_multiplier();
433 let health_change = HealthChange {
434 amount: health_change,
435 by: attacker.map(|x| x.into()),
436 cause: Some(damage.damage.source),
437 instance: damage_instance,
438 precise: precision_mult.is_some(),
439 time,
440 };
441 emitters.emit(HealthChangeEvent {
442 entity: target.entity,
443 change: health_change,
444 });
445 } else {
446 emitters.emit(PoiseChangeEvent {
447 entity: target.entity,
448 change: poise_change,
449 });
450 }
451 }
452 },
453 DamageKind::Piercing | DamageKind::Energy => {},
456 }
457 for effect in damage.effects.iter() {
458 match effect {
459 CombatEffect::Knockback(kb) => {
460 let impulse =
461 kb.calculate_impulse(dir, target.char_state) * strength_modifier;
462 if !impulse.is_approx_zero() {
463 emitters.emit(KnockbackEvent {
464 entity: target.entity,
465 impulse,
466 });
467 }
468 },
469 CombatEffect::EnergyReward(ec) => {
470 if let Some(attacker) = attacker {
471 emitters.emit(EnergyChangeEvent {
472 entity: attacker.entity,
473 change: *ec
474 * compute_energy_reward_mod(attacker.inventory, msm)
475 * strength_modifier
476 * attacker.stats.map_or(1.0, |s| s.energy_reward_modifier),
477 reset_rate: false,
478 });
479 }
480 },
481 CombatEffect::Buff(b) => {
482 if rng.gen::<f32>() < b.chance {
483 emitters.emit(BuffEvent {
484 entity: target.entity,
485 buff_change: BuffChange::Add(b.to_buff(
486 time,
487 attacker,
488 target,
489 applied_damage,
490 strength_modifier,
491 )),
492 });
493 }
494 },
495 CombatEffect::Lifesteal(l) => {
496 if let Some(attacker_entity) = attacker.map(|a| a.entity) {
498 let change = HealthChange {
499 amount: applied_damage * l,
500 by: attacker.map(|a| a.into()),
501 cause: None,
502 time,
503 precise: false,
504 instance: rand::random(),
505 };
506 if change.amount.abs() > Health::HEALTH_EPSILON {
507 emitters.emit(HealthChangeEvent {
508 entity: attacker_entity,
509 change,
510 });
511 }
512 }
513 },
514 CombatEffect::Poise(p) => {
515 let change = -Poise::apply_poise_reduction(
516 *p,
517 target.inventory,
518 msm,
519 target.char_state,
520 target.stats,
521 ) * strength_modifier
522 * attacker
523 .and_then(|a| a.stats)
524 .map_or(1.0, |s| s.poise_damage_modifier);
525 if change.abs() > Poise::POISE_EPSILON {
526 let poise_change = PoiseChange {
527 amount: change,
528 impulse: *dir,
529 by: attacker.map(|x| x.into()),
530 cause: Some(damage.damage.source),
531 time,
532 };
533 emitters.emit(PoiseChangeEvent {
534 entity: target.entity,
535 change: poise_change,
536 });
537 }
538 },
539 CombatEffect::Heal(h) => {
540 let change = HealthChange {
541 amount: *h * strength_modifier,
542 by: attacker.map(|a| a.into()),
543 cause: None,
544 time,
545 precise: false,
546 instance: rand::random(),
547 };
548 if change.amount.abs() > Health::HEALTH_EPSILON {
549 emitters.emit(HealthChangeEvent {
550 entity: target.entity,
551 change,
552 });
553 }
554 },
555 CombatEffect::Combo(c) => {
556 if let Some(attacker_entity) = attacker.map(|a| a.entity) {
558 emitters.emit(ComboChangeEvent {
559 entity: attacker_entity,
560 change: *c,
561 });
562 }
563 },
564 CombatEffect::StageVulnerable(damage, section) => {
565 if target
566 .char_state
567 .is_some_and(|cs| cs.stage_section() == Some(*section))
568 {
569 let change = {
570 let mut change = change;
571 change.amount *= damage;
572 change
573 };
574 emitters.emit(HealthChangeEvent {
575 entity: target.entity,
576 change,
577 });
578 }
579 },
580 CombatEffect::RefreshBuff(chance, b) => {
581 if rng.gen::<f32>() < *chance {
582 emitters.emit(BuffEvent {
583 entity: target.entity,
584 buff_change: BuffChange::Refresh(*b),
585 });
586 }
587 },
588 CombatEffect::BuffsVulnerable(damage, buff) => {
589 if target.buffs.is_some_and(|b| b.contains(*buff)) {
590 let change = {
591 let mut change = change;
592 change.amount *= damage;
593 change
594 };
595 emitters.emit(HealthChangeEvent {
596 entity: target.entity,
597 change,
598 });
599 }
600 },
601 CombatEffect::StunnedVulnerable(damage) => {
602 if target.char_state.is_some_and(|cs| cs.is_stunned()) {
603 let change = {
604 let mut change = change;
605 change.amount *= damage;
606 change
607 };
608 emitters.emit(HealthChangeEvent {
609 entity: target.entity,
610 change,
611 });
612 }
613 },
614 CombatEffect::SelfBuff(b) => {
615 if let Some(attacker) = attacker {
616 if rng.gen::<f32>() < b.chance {
617 emitters.emit(BuffEvent {
618 entity: attacker.entity,
619 buff_change: BuffChange::Add(b.to_self_buff(
620 time,
621 attacker,
622 applied_damage,
623 strength_modifier,
624 )),
625 });
626 }
627 }
628 },
629 }
630 }
631 }
632 }
633 for effect in self
634 .effects
635 .iter()
636 .chain(
637 attacker
638 .and_then(|attacker| attacker.stats)
639 .iter()
640 .flat_map(|stats| stats.effects_on_attack.iter()),
641 )
642 .filter(|e| allow_friendly_fire || e.target.is_none_or(|t| t == target_group))
643 .filter(|e| !avoid_effect(e))
644 {
645 let requirements_met = effect.requirements.iter().all(|req| match req {
646 CombatRequirement::AnyDamage => accumulated_damage > 0.0 && target.health.is_some(),
647 CombatRequirement::Energy(r) => {
648 if let Some(AttackerInfo {
649 entity,
650 energy: Some(e),
651 ..
652 }) = attacker
653 {
654 let sufficient_energy = e.current() >= *r;
655 if sufficient_energy {
656 emitters.emit(EnergyChangeEvent {
657 entity,
658 change: -*r,
659 reset_rate: false,
660 });
661 }
662
663 sufficient_energy
664 } else {
665 false
666 }
667 },
668 CombatRequirement::Combo(r) => {
669 if let Some(AttackerInfo {
670 entity,
671 combo: Some(c),
672 ..
673 }) = attacker
674 {
675 let sufficient_combo = c.counter() >= *r;
676 if sufficient_combo {
677 emitters.emit(ComboChangeEvent {
678 entity,
679 change: -(*r as i32),
680 });
681 }
682
683 sufficient_combo
684 } else {
685 false
686 }
687 },
688 CombatRequirement::TargetHasBuff(buff) => {
689 target.buffs.is_some_and(|buffs| buffs.contains(*buff))
690 },
691 CombatRequirement::TargetPoised => {
692 target.char_state.is_some_and(|cs| cs.is_stunned())
693 },
694 CombatRequirement::BehindTarget => {
695 if let Some(ori) = target.ori {
696 ori.look_vec().angle_between(dir.with_z(0.0)) < BEHIND_TARGET_ANGLE
697 } else {
698 false
699 }
700 },
701 CombatRequirement::TargetBlocking => target
702 .char_state
703 .is_some_and(|cs| cs.is_block(attack_source) || cs.is_parry(attack_source)),
704 });
705 if requirements_met {
706 is_applied = true;
707 match effect.effect {
708 CombatEffect::Knockback(kb) => {
709 let impulse =
710 kb.calculate_impulse(dir, target.char_state) * strength_modifier;
711 if !impulse.is_approx_zero() {
712 emitters.emit(KnockbackEvent {
713 entity: target.entity,
714 impulse,
715 });
716 }
717 },
718 CombatEffect::EnergyReward(ec) => {
719 if let Some(attacker) = attacker {
720 emitters.emit(EnergyChangeEvent {
721 entity: attacker.entity,
722 change: ec
723 * compute_energy_reward_mod(attacker.inventory, msm)
724 * strength_modifier
725 * attacker.stats.map_or(1.0, |s| s.energy_reward_modifier),
726 reset_rate: false,
727 });
728 }
729 },
730 CombatEffect::Buff(b) => {
731 if rng.gen::<f32>() < b.chance {
732 emitters.emit(BuffEvent {
733 entity: target.entity,
734 buff_change: BuffChange::Add(b.to_buff(
735 time,
736 attacker,
737 target,
738 accumulated_damage,
739 strength_modifier,
740 )),
741 });
742 }
743 },
744 CombatEffect::Lifesteal(l) => {
745 if let Some(attacker_entity) = attacker.map(|a| a.entity) {
747 let change = HealthChange {
748 amount: accumulated_damage * l,
749 by: attacker.map(|a| a.into()),
750 cause: None,
751 time,
752 precise: false,
753 instance: rand::random(),
754 };
755 if change.amount.abs() > Health::HEALTH_EPSILON {
756 emitters.emit(HealthChangeEvent {
757 entity: attacker_entity,
758 change,
759 });
760 }
761 }
762 },
763 CombatEffect::Poise(p) => {
764 let change = -Poise::apply_poise_reduction(
765 p,
766 target.inventory,
767 msm,
768 target.char_state,
769 target.stats,
770 ) * strength_modifier
771 * attacker
772 .and_then(|a| a.stats)
773 .map_or(1.0, |s| s.poise_damage_modifier);
774 if change.abs() > Poise::POISE_EPSILON {
775 let poise_change = PoiseChange {
776 amount: change,
777 impulse: *dir,
778 by: attacker.map(|x| x.into()),
779 cause: Some(attack_source.into()),
780 time,
781 };
782 emitters.emit(PoiseChangeEvent {
783 entity: target.entity,
784 change: poise_change,
785 });
786 }
787 },
788 CombatEffect::Heal(h) => {
789 let change = HealthChange {
790 amount: h * strength_modifier,
791 by: attacker.map(|a| a.into()),
792 cause: None,
793 time,
794 precise: false,
795 instance: rand::random(),
796 };
797 if change.amount.abs() > Health::HEALTH_EPSILON {
798 emitters.emit(HealthChangeEvent {
799 entity: target.entity,
800 change,
801 });
802 }
803 },
804 CombatEffect::Combo(c) => {
805 if let Some(attacker_entity) = attacker.map(|a| a.entity) {
807 emitters.emit(ComboChangeEvent {
808 entity: attacker_entity,
809 change: c,
810 });
811 }
812 },
813 CombatEffect::StageVulnerable(_, _) => {},
815 CombatEffect::RefreshBuff(chance, b) => {
816 if rng.gen::<f32>() < chance {
817 emitters.emit(BuffEvent {
818 entity: target.entity,
819 buff_change: BuffChange::Refresh(b),
820 });
821 }
822 },
823 CombatEffect::BuffsVulnerable(_, _) => {},
825 CombatEffect::StunnedVulnerable(_) => {},
827 CombatEffect::SelfBuff(b) => {
828 if let Some(attacker) = attacker {
829 if rng.gen::<f32>() < b.chance {
830 emitters.emit(BuffEvent {
831 entity: target.entity,
832 buff_change: BuffChange::Add(b.to_self_buff(
833 time,
834 attacker,
835 accumulated_damage,
836 strength_modifier,
837 )),
838 });
839 }
840 }
841 },
842 }
843 }
844 }
845 if is_applied {
848 emitters.emit(EntityAttackedHookEvent {
849 entity: target.entity,
850 attacker: attacker.map(|a| a.entity),
851 });
852 }
853 is_applied
854 }
855}
856
857pub fn allow_friendly_fire(
858 entered_auras: &ReadStorage<EnteredAuras>,
859 attacker: EcsEntity,
860 target: EcsEntity,
861) -> bool {
862 entered_auras
863 .get(attacker)
864 .zip(entered_auras.get(target))
865 .and_then(|(attacker, target)| {
866 Some((
867 attacker.auras.get(&AuraKindVariant::FriendlyFire)?,
868 target.auras.get(&AuraKindVariant::FriendlyFire)?,
869 ))
870 })
871 .is_some_and(|(attacker, target)| attacker.intersection(target).next().is_some())
873}
874
875pub fn permit_pvp(
885 alignments: &ReadStorage<Alignment>,
886 players: &ReadStorage<Player>,
887 entered_auras: &ReadStorage<EnteredAuras>,
888 id_maps: &IdMaps,
889 attacker: Option<EcsEntity>,
890 target: EcsEntity,
891) -> bool {
892 let owner_if_pet = |entity| {
895 let alignment = alignments.get(entity).copied();
896 if let Some(Alignment::Owned(uid)) = alignment {
897 id_maps.uid_entity(uid).unwrap_or(entity)
900 } else {
901 entity
902 }
903 };
904
905 let attacker = match attacker {
908 Some(attacker) => attacker,
909 None => return true,
910 };
911
912 let attacker_owner = owner_if_pet(attacker);
914 let target_owner = owner_if_pet(target);
915
916 if let (Some(attacker_auras), Some(target_auras)) = (
918 entered_auras.get(attacker_owner),
919 entered_auras.get(target_owner),
920 ) && attacker_auras
921 .auras
922 .get(&AuraKindVariant::ForcePvP)
923 .zip(target_auras.auras.get(&AuraKindVariant::ForcePvP))
924 .is_some_and(|(attacker, target)| attacker.intersection(target).next().is_some())
926 {
927 return true;
928 }
929
930 if attacker_owner == target_owner {
935 return allow_friendly_fire(entered_auras, attacker, target);
936 }
937
938 let attacker_info = players.get(attacker_owner);
940 let target_info = players.get(target_owner);
941
942 attacker_info
944 .zip(target_info)
945 .is_none_or(|(a, t)| a.may_harm(t))
946}
947
948#[derive(Clone, Debug, Serialize, Deserialize)]
949pub struct AttackDamage {
950 damage: Damage,
951 target: Option<GroupTarget>,
952 effects: Vec<CombatEffect>,
953 instance: u64,
955}
956
957impl AttackDamage {
958 pub fn new(damage: Damage, target: Option<GroupTarget>, instance: u64) -> Self {
959 Self {
960 damage,
961 target,
962 effects: Vec::new(),
963 instance,
964 }
965 }
966
967 #[must_use]
968 pub fn with_effect(mut self, effect: CombatEffect) -> Self {
969 self.effects.push(effect);
970 self
971 }
972}
973
974#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
975pub struct AttackEffect {
976 target: Option<GroupTarget>,
977 effect: CombatEffect,
978 requirements: Vec<CombatRequirement>,
979}
980
981impl AttackEffect {
982 pub fn new(target: Option<GroupTarget>, effect: CombatEffect) -> Self {
983 Self {
984 target,
985 effect,
986 requirements: Vec::new(),
987 }
988 }
989
990 #[must_use]
991 pub fn with_requirement(mut self, requirement: CombatRequirement) -> Self {
992 self.requirements.push(requirement);
993 self
994 }
995
996 pub fn effect(&self) -> &CombatEffect { &self.effect }
997}
998
999#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq)]
1000pub enum CombatEffect {
1001 Heal(f32),
1002 Buff(CombatBuff),
1003 Knockback(Knockback),
1004 EnergyReward(f32),
1005 Lifesteal(f32),
1006 Poise(f32),
1007 Combo(i32),
1008 StageVulnerable(f32, StageSection),
1015 RefreshBuff(f32, BuffKind),
1017 BuffsVulnerable(f32, BuffKind),
1024 StunnedVulnerable(f32),
1031 SelfBuff(CombatBuff),
1033}
1034
1035impl CombatEffect {
1036 pub fn adjusted_by_stats(self, stats: tool::Stats) -> Self {
1037 match self {
1038 CombatEffect::Heal(h) => CombatEffect::Heal(h * stats.effect_power),
1039 CombatEffect::Buff(CombatBuff {
1040 kind,
1041 dur_secs,
1042 strength,
1043 chance,
1044 }) => CombatEffect::Buff(CombatBuff {
1045 kind,
1046 dur_secs,
1047 strength: strength * stats.buff_strength,
1048 chance,
1049 }),
1050 CombatEffect::Knockback(Knockback {
1051 direction,
1052 strength,
1053 }) => CombatEffect::Knockback(Knockback {
1054 direction,
1055 strength: strength * stats.effect_power,
1056 }),
1057 CombatEffect::EnergyReward(e) => CombatEffect::EnergyReward(e),
1058 CombatEffect::Lifesteal(l) => CombatEffect::Lifesteal(l * stats.effect_power),
1059 CombatEffect::Poise(p) => CombatEffect::Poise(p * stats.effect_power),
1060 CombatEffect::Combo(c) => CombatEffect::Combo(c),
1061 CombatEffect::StageVulnerable(v, s) => {
1062 CombatEffect::StageVulnerable(v * stats.effect_power, s)
1063 },
1064 CombatEffect::RefreshBuff(c, b) => CombatEffect::RefreshBuff(c, b),
1065 CombatEffect::BuffsVulnerable(v, b) => {
1066 CombatEffect::BuffsVulnerable(v * stats.effect_power, b)
1067 },
1068 CombatEffect::StunnedVulnerable(v) => {
1069 CombatEffect::StunnedVulnerable(v * stats.effect_power)
1070 },
1071 CombatEffect::SelfBuff(CombatBuff {
1072 kind,
1073 dur_secs,
1074 strength,
1075 chance,
1076 }) => CombatEffect::SelfBuff(CombatBuff {
1077 kind,
1078 dur_secs,
1079 strength: strength * stats.buff_strength,
1080 chance,
1081 }),
1082 }
1083 }
1084}
1085
1086#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq)]
1087pub enum CombatRequirement {
1088 AnyDamage,
1089 Energy(f32),
1090 Combo(u32),
1091 TargetHasBuff(BuffKind),
1092 TargetPoised,
1093 BehindTarget,
1094 TargetBlocking,
1095}
1096
1097#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
1098pub enum DamagedEffect {
1099 Combo(i32),
1100 Energy(f32),
1101}
1102
1103#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
1104pub enum DeathEffect {
1105 AttackerBuff {
1107 kind: BuffKind,
1108 strength: f32,
1109 duration: Option<Secs>,
1110 },
1111 Transform {
1113 entity_spec: String,
1114 #[serde(default)]
1116 allow_players: bool,
1117 },
1118}
1119
1120#[derive(Clone, Debug, PartialEq)]
1121pub struct DeathEffects(pub Vec<DeathEffect>);
1124
1125impl specs::Component for DeathEffects {
1126 type Storage = specs::DenseVecStorage<DeathEffects>;
1127}
1128
1129#[derive(Clone, Copy, Debug, Hash, Eq, PartialEq, Serialize, Deserialize)]
1130pub enum DamageContributor {
1131 Solo(Uid),
1132 Group { entity_uid: Uid, group: Group },
1133}
1134
1135impl DamageContributor {
1136 pub fn new(uid: Uid, group: Option<Group>) -> Self {
1137 if let Some(group) = group {
1138 DamageContributor::Group {
1139 entity_uid: uid,
1140 group,
1141 }
1142 } else {
1143 DamageContributor::Solo(uid)
1144 }
1145 }
1146
1147 pub fn uid(&self) -> Uid {
1148 match self {
1149 DamageContributor::Solo(uid) => *uid,
1150 DamageContributor::Group {
1151 entity_uid,
1152 group: _,
1153 } => *entity_uid,
1154 }
1155 }
1156}
1157
1158impl From<AttackerInfo<'_>> for DamageContributor {
1159 fn from(attacker_info: AttackerInfo) -> Self {
1160 DamageContributor::new(attacker_info.uid, attacker_info.group.copied())
1161 }
1162}
1163
1164#[derive(Copy, Clone, Debug, Hash, PartialEq, Eq, Serialize, Deserialize)]
1165pub enum DamageSource {
1166 Buff(BuffKind),
1167 Melee,
1168 Projectile,
1169 Explosion,
1170 Falling,
1171 Shockwave,
1172 Energy,
1173 Other,
1174}
1175
1176impl From<AttackSource> for DamageSource {
1177 fn from(attack: AttackSource) -> Self {
1178 match attack {
1179 AttackSource::Melee => DamageSource::Melee,
1180 AttackSource::Projectile => DamageSource::Projectile,
1181 AttackSource::Explosion => DamageSource::Explosion,
1182 AttackSource::AirShockwave
1183 | AttackSource::GroundShockwave
1184 | AttackSource::UndodgeableShockwave => DamageSource::Shockwave,
1185 AttackSource::Beam => DamageSource::Energy,
1186 }
1187 }
1188}
1189
1190#[derive(Copy, Clone, Debug, Hash, PartialEq, Eq, Serialize, Deserialize)]
1192pub enum DamageKind {
1193 Piercing,
1195 Slashing,
1198 Crushing,
1200 Energy,
1203}
1204
1205const PIERCING_PENETRATION_FRACTION: f32 = 0.5;
1206const SLASHING_ENERGY_FRACTION: f32 = 0.5;
1207const CRUSHING_POISE_FRACTION: f32 = 1.0;
1208
1209#[derive(Copy, Clone, Debug, PartialEq, Serialize, Deserialize)]
1210#[serde(deny_unknown_fields)]
1211pub struct Damage {
1212 pub source: DamageSource,
1213 pub kind: DamageKind,
1214 pub value: f32,
1215}
1216
1217impl Damage {
1218 pub fn compute_damage_reduction(
1220 damage: Option<Self>,
1221 inventory: Option<&Inventory>,
1222 stats: Option<&Stats>,
1223 msm: &MaterialStatManifest,
1224 ) -> f32 {
1225 let protection = compute_protection(inventory, msm);
1226
1227 let penetration = if let Some(damage) = damage {
1228 if let DamageKind::Piercing = damage.kind {
1229 (damage.value * PIERCING_PENETRATION_FRACTION).clamp(0.0, protection.unwrap_or(0.0))
1230 } else {
1231 0.0
1232 }
1233 } else {
1234 0.0
1235 };
1236
1237 let protection = protection.map(|p| p - penetration);
1238
1239 const FIFTY_PERCENT_DR_THRESHOLD: f32 = 60.0;
1240
1241 let inventory_dr = match protection {
1242 Some(dr) => dr / (FIFTY_PERCENT_DR_THRESHOLD + dr.abs()),
1243 None => 1.0,
1244 };
1245
1246 let stats_dr = if let Some(stats) = stats {
1247 stats.damage_reduction.modifier()
1248 } else {
1249 0.0
1250 };
1251 if protection.is_none() || stats_dr >= 1.0 {
1253 1.0
1254 } else {
1255 1.0 - (1.0 - inventory_dr) * (1.0 - stats_dr)
1256 }
1257 }
1258
1259 pub fn calculate_health_change(
1260 self,
1261 damage_reduction: f32,
1262 block_damage_decrement: f32,
1263 damage_contributor: Option<DamageContributor>,
1264 precision_mult: Option<f32>,
1265 precision_power: f32,
1266 damage_modifier: f32,
1267 time: Time,
1268 instance: u64,
1269 ) -> HealthChange {
1270 let mut damage = self.value * damage_modifier;
1271 let precise_damage = damage * precision_mult.unwrap_or(0.0) * (precision_power - 1.0);
1272 match self.source {
1273 DamageSource::Melee
1274 | DamageSource::Projectile
1275 | DamageSource::Explosion
1276 | DamageSource::Shockwave
1277 | DamageSource::Energy => {
1278 damage += precise_damage;
1280 damage *= 1.0 - damage_reduction;
1282 damage = f32::max(damage - block_damage_decrement, 0.0);
1284
1285 HealthChange {
1286 amount: -damage,
1287 by: damage_contributor,
1288 cause: Some(self.source),
1289 time,
1290 precise: precision_mult.is_some(),
1291 instance,
1292 }
1293 },
1294 DamageSource::Falling => {
1295 if (damage_reduction - 1.0).abs() < f32::EPSILON {
1297 damage = 0.0;
1298 }
1299 HealthChange {
1300 amount: -damage,
1301 by: None,
1302 cause: Some(self.source),
1303 time,
1304 precise: false,
1305 instance,
1306 }
1307 },
1308 DamageSource::Buff(_) | DamageSource::Other => HealthChange {
1309 amount: -damage,
1310 by: None,
1311 cause: Some(self.source),
1312 time,
1313 precise: false,
1314 instance,
1315 },
1316 }
1317 }
1318
1319 pub fn interpolate_damage(&mut self, frac: f32, min: f32) {
1320 let new_damage = min + frac * (self.value - min);
1321 self.value = new_damage;
1322 }
1323}
1324
1325#[derive(Copy, Clone, Debug, PartialEq, Serialize, Deserialize)]
1326pub struct Knockback {
1327 pub direction: KnockbackDir,
1328 pub strength: f32,
1329}
1330
1331#[derive(Copy, Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
1332pub enum KnockbackDir {
1333 Away,
1334 Towards,
1335 Up,
1336 TowardsUp,
1337}
1338
1339impl Knockback {
1340 pub fn calculate_impulse(self, dir: Dir, char_state: Option<&CharacterState>) -> Vec3<f32> {
1341 let from_char = {
1342 let resistant = char_state
1343 .and_then(|cs| cs.ability_info())
1344 .map(|a| a.ability_meta)
1345 .is_some_and(|a| a.capabilities.contains(Capability::KNOCKBACK_RESISTANT));
1346 if resistant { 0.5 } else { 1.0 }
1347 };
1348 50.0 * self.strength
1351 * from_char
1352 * match self.direction {
1353 KnockbackDir::Away => *Dir::slerp(dir, Dir::new(Vec3::unit_z()), 0.5),
1354 KnockbackDir::Towards => *Dir::slerp(-dir, Dir::new(Vec3::unit_z()), 0.5),
1355 KnockbackDir::Up => Vec3::unit_z(),
1356 KnockbackDir::TowardsUp => *Dir::slerp(-dir, Dir::new(Vec3::unit_z()), 0.85),
1357 }
1358 }
1359
1360 #[must_use]
1361 pub fn modify_strength(mut self, power: f32) -> Self {
1362 self.strength *= power;
1363 self
1364 }
1365}
1366
1367#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq)]
1368pub struct CombatBuff {
1369 pub kind: BuffKind,
1370 pub dur_secs: f32,
1371 pub strength: CombatBuffStrength,
1372 pub chance: f32,
1373}
1374
1375#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq)]
1376pub enum CombatBuffStrength {
1377 DamageFraction(f32),
1378 Value(f32),
1379}
1380
1381impl CombatBuffStrength {
1382 fn to_strength(self, damage: f32, strength_modifier: f32) -> f32 {
1383 match self {
1384 CombatBuffStrength::DamageFraction(f) => damage * f,
1386 CombatBuffStrength::Value(v) => v * strength_modifier,
1387 }
1388 }
1389}
1390
1391impl MulAssign<f32> for CombatBuffStrength {
1392 fn mul_assign(&mut self, mul: f32) { *self = *self * mul; }
1393}
1394
1395impl Mul<f32> for CombatBuffStrength {
1396 type Output = Self;
1397
1398 fn mul(self, mult: f32) -> Self {
1399 match self {
1400 Self::DamageFraction(val) => Self::DamageFraction(val * mult),
1401 Self::Value(val) => Self::Value(val * mult),
1402 }
1403 }
1404}
1405
1406impl CombatBuff {
1407 fn to_buff(
1408 self,
1409 time: Time,
1410 attacker_info: Option<AttackerInfo>,
1411 target_info: &TargetInfo,
1412 damage: f32,
1413 strength_modifier: f32,
1414 ) -> Buff {
1415 let source = if let Some(uid) = attacker_info.map(|a| a.uid) {
1417 BuffSource::Character { by: uid }
1418 } else {
1419 BuffSource::Unknown
1420 };
1421 let dest_info = DestInfo {
1422 stats: target_info.stats,
1423 mass: target_info.mass,
1424 };
1425 Buff::new(
1426 self.kind,
1427 BuffData::new(
1428 self.strength.to_strength(damage, strength_modifier),
1429 Some(Secs(self.dur_secs as f64)),
1430 ),
1431 Vec::new(),
1432 source,
1433 time,
1434 dest_info,
1435 attacker_info.and_then(|a| a.mass),
1436 )
1437 }
1438
1439 fn to_self_buff(
1440 self,
1441 time: Time,
1442 attacker_info: AttackerInfo,
1443 damage: f32,
1444 strength_modifier: f32,
1445 ) -> Buff {
1446 let source = BuffSource::Character {
1448 by: attacker_info.uid,
1449 };
1450 let dest_info = DestInfo {
1451 stats: attacker_info.stats,
1452 mass: attacker_info.mass,
1453 };
1454 Buff::new(
1455 self.kind,
1456 BuffData::new(
1457 self.strength.to_strength(damage, strength_modifier),
1458 Some(Secs(self.dur_secs as f64)),
1459 ),
1460 Vec::new(),
1461 source,
1462 time,
1463 dest_info,
1464 attacker_info.mass,
1465 )
1466 }
1467}
1468
1469pub fn get_weapon_kinds(inv: &Inventory) -> (Option<ToolKind>, Option<ToolKind>) {
1470 (
1471 inv.equipped(EquipSlot::ActiveMainhand).and_then(|i| {
1472 if let ItemKind::Tool(tool) = &*i.kind() {
1473 Some(tool.kind)
1474 } else {
1475 None
1476 }
1477 }),
1478 inv.equipped(EquipSlot::ActiveOffhand).and_then(|i| {
1479 if let ItemKind::Tool(tool) = &*i.kind() {
1480 Some(tool.kind)
1481 } else {
1482 None
1483 }
1484 }),
1485 )
1486}
1487
1488fn weapon_rating<T: ItemDesc>(item: &T, _msm: &MaterialStatManifest) -> f32 {
1490 const POWER_WEIGHT: f32 = 2.0;
1491 const SPEED_WEIGHT: f32 = 3.0;
1492 const RANGE_WEIGHT: f32 = 0.8;
1493 const EFFECT_WEIGHT: f32 = 1.5;
1494 const EQUIP_TIME_WEIGHT: f32 = 0.0;
1495 const ENERGY_EFFICIENCY_WEIGHT: f32 = 1.5;
1496 const BUFF_STRENGTH_WEIGHT: f32 = 1.5;
1497
1498 let rating = if let ItemKind::Tool(tool) = &*item.kind() {
1499 let stats = tool.stats(item.stats_durability_multiplier());
1500
1501 let power_rating = stats.power;
1506 let speed_rating = stats.speed - 1.0;
1507 let range_rating = stats.range - 1.0;
1508 let effect_rating = stats.effect_power - 1.0;
1509 let equip_time_rating = 0.5 - stats.equip_time_secs;
1510 let energy_efficiency_rating = stats.energy_efficiency - 1.0;
1511 let buff_strength_rating = stats.buff_strength - 1.0;
1512
1513 power_rating * POWER_WEIGHT
1514 + speed_rating * SPEED_WEIGHT
1515 + range_rating * RANGE_WEIGHT
1516 + effect_rating * EFFECT_WEIGHT
1517 + equip_time_rating * EQUIP_TIME_WEIGHT
1518 + energy_efficiency_rating * ENERGY_EFFICIENCY_WEIGHT
1519 + buff_strength_rating * BUFF_STRENGTH_WEIGHT
1520 } else {
1521 0.0
1522 };
1523 rating.max(0.0)
1524}
1525
1526fn weapon_skills(inventory: &Inventory, skill_set: &SkillSet) -> f32 {
1527 let (mainhand, offhand) = get_weapon_kinds(inventory);
1528 let mainhand_skills = if let Some(tool) = mainhand {
1529 skill_set.earned_sp(SkillGroupKind::Weapon(tool)) as f32
1530 } else {
1531 0.0
1532 };
1533 let offhand_skills = if let Some(tool) = offhand {
1534 skill_set.earned_sp(SkillGroupKind::Weapon(tool)) as f32
1535 } else {
1536 0.0
1537 };
1538 mainhand_skills.max(offhand_skills)
1539}
1540
1541fn get_weapon_rating(inventory: &Inventory, msm: &MaterialStatManifest) -> f32 {
1542 let mainhand_rating = if let Some(item) = inventory.equipped(EquipSlot::ActiveMainhand) {
1543 weapon_rating(item, msm)
1544 } else {
1545 0.0
1546 };
1547
1548 let offhand_rating = if let Some(item) = inventory.equipped(EquipSlot::ActiveOffhand) {
1549 weapon_rating(item, msm)
1550 } else {
1551 0.0
1552 };
1553
1554 mainhand_rating.max(offhand_rating)
1555}
1556
1557pub fn combat_rating(
1558 inventory: &Inventory,
1559 health: &Health,
1560 energy: &Energy,
1561 poise: &Poise,
1562 skill_set: &SkillSet,
1563 body: Body,
1564 msm: &MaterialStatManifest,
1565) -> f32 {
1566 const WEAPON_WEIGHT: f32 = 1.0;
1567 const HEALTH_WEIGHT: f32 = 1.5;
1568 const ENERGY_WEIGHT: f32 = 0.5;
1569 const SKILLS_WEIGHT: f32 = 1.0;
1570 const POISE_WEIGHT: f32 = 0.5;
1571 const PRECISION_WEIGHT: f32 = 0.5;
1572 let health_rating = health.base_max()
1574 / 100.0
1575 / (1.0 - Damage::compute_damage_reduction(None, Some(inventory), None, msm)).max(0.00001);
1576
1577 let energy_rating = (energy.base_max() + compute_max_energy_mod(Some(inventory), msm)) / 100.0
1580 * compute_energy_reward_mod(Some(inventory), msm);
1581
1582 let poise_rating = poise.base_max()
1584 / 100.0
1585 / (1.0 - Poise::compute_poise_damage_reduction(Some(inventory), msm, None, None))
1586 .max(0.00001);
1587
1588 let precision_rating = compute_precision_mult(Some(inventory), msm) / 1.2;
1590
1591 let skills_rating = (skill_set.earned_sp(SkillGroupKind::General) as f32 / 20.0
1594 + weapon_skills(inventory, skill_set) / 10.0)
1595 / 2.0;
1596
1597 let weapon_rating = get_weapon_rating(inventory, msm);
1598
1599 let combined_rating = (health_rating * HEALTH_WEIGHT
1600 + energy_rating * ENERGY_WEIGHT
1601 + poise_rating * POISE_WEIGHT
1602 + precision_rating * PRECISION_WEIGHT
1603 + skills_rating * SKILLS_WEIGHT
1604 + weapon_rating * WEAPON_WEIGHT)
1605 / (HEALTH_WEIGHT
1606 + ENERGY_WEIGHT
1607 + POISE_WEIGHT
1608 + PRECISION_WEIGHT
1609 + SKILLS_WEIGHT
1610 + WEAPON_WEIGHT);
1611
1612 combined_rating * body.combat_multiplier()
1615}
1616
1617pub fn compute_precision_mult(inventory: Option<&Inventory>, msm: &MaterialStatManifest) -> f32 {
1618 1.0 + inventory
1622 .map_or(0.1, |inv| {
1623 inv.equipped_items()
1624 .filter_map(|item| {
1625 if let ItemKind::Armor(armor) = &*item.kind() {
1626 armor
1627 .stats(msm, item.stats_durability_multiplier())
1628 .precision_power
1629 } else {
1630 None
1631 }
1632 })
1633 .fold(0.1, |a, b| a + b)
1634 })
1635 .max(0.0)
1636}
1637
1638pub fn compute_energy_reward_mod(inventory: Option<&Inventory>, msm: &MaterialStatManifest) -> f32 {
1640 inventory.map_or(1.0, |inv| {
1643 inv.equipped_items()
1644 .filter_map(|item| {
1645 if let ItemKind::Armor(armor) = &*item.kind() {
1646 armor
1647 .stats(msm, item.stats_durability_multiplier())
1648 .energy_reward
1649 } else {
1650 None
1651 }
1652 })
1653 .fold(1.0, |a, b| a + b)
1654 })
1655}
1656
1657pub fn compute_max_energy_mod(inventory: Option<&Inventory>, msm: &MaterialStatManifest) -> f32 {
1660 inventory.map_or(0.0, |inv| {
1662 inv.equipped_items()
1663 .filter_map(|item| {
1664 if let ItemKind::Armor(armor) = &*item.kind() {
1665 armor
1666 .stats(msm, item.stats_durability_multiplier())
1667 .energy_max
1668 } else {
1669 None
1670 }
1671 })
1672 .sum()
1673 })
1674}
1675
1676pub fn perception_dist_multiplier_from_stealth(
1679 inventory: Option<&Inventory>,
1680 character_state: Option<&CharacterState>,
1681 msm: &MaterialStatManifest,
1682) -> f32 {
1683 const SNEAK_MULTIPLIER: f32 = 0.7;
1684
1685 let item_stealth_multiplier = stealth_multiplier_from_items(inventory, msm);
1686 let is_sneaking = character_state.is_some_and(|state| state.is_stealthy());
1687
1688 let multiplier = item_stealth_multiplier * if is_sneaking { SNEAK_MULTIPLIER } else { 1.0 };
1689
1690 multiplier.clamp(0.0, 1.0)
1691}
1692
1693pub fn stealth_multiplier_from_items(
1694 inventory: Option<&Inventory>,
1695 msm: &MaterialStatManifest,
1696) -> f32 {
1697 let stealth_sum = inventory.map_or(0.0, |inv| {
1698 inv.equipped_items()
1699 .filter_map(|item| {
1700 if let ItemKind::Armor(armor) = &*item.kind() {
1701 armor.stats(msm, item.stats_durability_multiplier()).stealth
1702 } else {
1703 None
1704 }
1705 })
1706 .sum()
1707 });
1708
1709 (1.0 / (1.0 + stealth_sum)).clamp(0.0, 1.0)
1710}
1711
1712pub fn compute_protection(
1716 inventory: Option<&Inventory>,
1717 msm: &MaterialStatManifest,
1718) -> Option<f32> {
1719 inventory.map_or(Some(0.0), |inv| {
1720 inv.equipped_items()
1721 .filter_map(|item| {
1722 if let ItemKind::Armor(armor) = &*item.kind() {
1723 armor
1724 .stats(msm, item.stats_durability_multiplier())
1725 .protection
1726 } else {
1727 None
1728 }
1729 })
1730 .map(|protection| match protection {
1731 Protection::Normal(protection) => Some(protection),
1732 Protection::Invincible => None,
1733 })
1734 .sum::<Option<f32>>()
1735 })
1736}
1737
1738pub fn compute_poise_resilience(
1742 inventory: Option<&Inventory>,
1743 msm: &MaterialStatManifest,
1744) -> Option<f32> {
1745 inventory.map_or(Some(0.0), |inv| {
1746 inv.equipped_items()
1747 .filter_map(|item| {
1748 if let ItemKind::Armor(armor) = &*item.kind() {
1749 armor
1750 .stats(msm, item.stats_durability_multiplier())
1751 .poise_resilience
1752 } else {
1753 None
1754 }
1755 })
1756 .map(|protection| match protection {
1757 Protection::Normal(protection) => Some(protection),
1758 Protection::Invincible => None,
1759 })
1760 .sum::<Option<f32>>()
1761 })
1762}
1763
1764pub fn precision_mult_from_flank(
1766 attack_dir: Vec3<f32>,
1767 target_ori: Option<&Ori>,
1768 precision_flank_multipliers: FlankMults,
1769 precision_flank_invert: bool,
1770) -> Option<f32> {
1771 let angle = target_ori.map(|t_ori| {
1772 t_ori.look_dir().angle_between(if precision_flank_invert {
1773 -attack_dir
1774 } else {
1775 attack_dir
1776 })
1777 });
1778 match angle {
1779 Some(angle) if angle < FULL_FLANK_ANGLE => Some(
1780 MAX_BACK_FLANK_PRECISION
1781 * if precision_flank_invert {
1782 precision_flank_multipliers.front
1783 } else {
1784 precision_flank_multipliers.back
1785 },
1786 ),
1787 Some(angle) if angle < PARTIAL_FLANK_ANGLE => {
1788 Some(MAX_SIDE_FLANK_PRECISION * precision_flank_multipliers.side)
1789 },
1790 Some(_) | None => None,
1791 }
1792}
1793
1794#[derive(Copy, Clone, Debug, PartialEq, Serialize, Deserialize)]
1795pub struct FlankMults {
1796 pub back: f32,
1797 pub front: f32,
1798 pub side: f32,
1799}
1800
1801impl Default for FlankMults {
1802 fn default() -> Self {
1803 FlankMults {
1804 back: 1.0,
1805 front: 1.0,
1806 side: 1.0,
1807 }
1808 }
1809}
1810
1811pub fn block_strength(inventory: &Inventory, char_state: &CharacterState) -> f32 {
1812 match char_state {
1813 CharacterState::BasicBlock(data) => data.static_data.block_strength,
1814 CharacterState::RiposteMelee(data) => data.static_data.block_strength,
1815 _ => char_state
1816 .ability_info()
1817 .map(|ability| (ability.ability_meta.capabilities, ability.hand))
1818 .map(|(capabilities, hand)| {
1819 (
1820 if capabilities.contains(Capability::PARRIES)
1821 || capabilities.contains(Capability::PARRIES_MELEE)
1822 || capabilities.contains(Capability::BLOCKS)
1823 {
1824 FALLBACK_BLOCK_STRENGTH
1825 } else {
1826 0.0
1827 },
1828 hand.and_then(|hand| inventory.equipped(hand.to_equip_slot()))
1829 .map_or(1.0, |item| match &*item.kind() {
1830 ItemKind::Tool(tool) => {
1831 tool.stats(item.stats_durability_multiplier()).power
1832 },
1833 _ => 1.0,
1834 }),
1835 )
1836 })
1837 .map_or(0.0, |(capability_strength, tool_block_strength)| {
1838 capability_strength * tool_block_strength
1839 }),
1840 }
1841}
1842
1843pub fn get_equip_slot_by_block_priority(inventory: Option<&Inventory>) -> EquipSlot {
1844 inventory
1845 .map(get_weapon_kinds)
1846 .map_or(
1847 EquipSlot::ActiveMainhand,
1848 |weapon_kinds| match weapon_kinds {
1849 (Some(mainhand), Some(offhand)) => {
1850 if mainhand.block_priority() >= offhand.block_priority() {
1851 EquipSlot::ActiveMainhand
1852 } else {
1853 EquipSlot::ActiveOffhand
1854 }
1855 },
1856 (Some(_), None) => EquipSlot::ActiveMainhand,
1857 (None, Some(_)) => EquipSlot::ActiveOffhand,
1858 (None, None) => EquipSlot::ActiveMainhand,
1859 },
1860 )
1861}