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