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