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