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