1use crate::{
2 assets::{AssetExt, Ron},
3 comp::{
4 Alignment, Body, Buffs, CharacterState, Combo, Energy, Group, Health, HealthChange,
5 InputKind, Inventory, Mass, Ori, Player, Poise, PoiseChange, SkillSet, Stats,
6 ability::Capability,
7 aura::{AuraKindVariant, EnteredAuras},
8 buff::{Buff, BuffChange, BuffData, BuffDescriptor, BuffKind, BuffSource, DestInfo},
9 inventory::{
10 item::{
11 ItemDesc, ItemKind, MaterialStatManifest,
12 armor::Protection,
13 tool::{self, ToolKind},
14 },
15 slot::EquipSlot,
16 },
17 skillset::SkillGroupKind,
18 },
19 effect::BuffEffect,
20 event::{
21 BuffEvent, ComboChangeEvent, EmitExt, EnergyChangeEvent, EntityAttackedHookEvent,
22 HealthChangeEvent, KnockbackEvent, ParryHookEvent, PoiseChangeEvent, TransformEvent,
23 },
24 generation::{EntityConfig, EntityInfo},
25 outcome::Outcome,
26 resources::{Secs, Time},
27 states::utils::{AbilityInfo, StageSection},
28 uid::{IdMaps, Uid},
29 util::Dir,
30};
31use rand::RngExt;
32use serde::{Deserialize, Serialize};
33use specs::{Entity as EcsEntity, ReadStorage};
34use std::ops::{Mul, MulAssign};
35use tracing::error;
36use vek::*;
37
38pub enum AttackTarget {
39 AllInRange(f32),
40 Pos(Vec3<f32>),
41 Entity(EcsEntity),
42}
43
44#[derive(Copy, Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
45pub enum GroupTarget {
46 InGroup,
47 OutOfGroup,
48 All,
49}
50
51#[derive(Copy, Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
52pub enum StatEffectTarget {
53 Attacker,
54 Target,
55}
56
57#[derive(Copy, Clone, PartialEq, Eq, Debug, Serialize, Deserialize, Hash)]
58pub enum AttackSource {
59 Melee,
60 Projectile,
61 Beam,
62 GroundShockwave,
63 AirShockwave,
64 UndodgeableShockwave,
65 Explosion,
66 Arc,
67 Pool,
68}
69
70pub const FULL_FLANK_ANGLE: f32 = std::f32::consts::PI / 4.0;
71pub const PARTIAL_FLANK_ANGLE: f32 = std::f32::consts::PI * 3.0 / 4.0;
72pub const BEAM_DURATION_PRECISION: f32 = 2.5;
73pub const MAX_BACK_FLANK_PRECISION: f32 = 0.75;
74pub const MAX_SIDE_FLANK_PRECISION: f32 = 0.25;
75pub const MAX_HEADSHOT_PRECISION: f32 = 1.0;
76pub const MAX_TOP_HEADSHOT_PRECISION: f32 = 0.5;
77pub const MAX_BEAM_DUR_PRECISION: f32 = 0.25;
78pub const MAX_MELEE_POISE_PRECISION: f32 = 0.5;
79pub const MAX_BLOCK_POISE_COST: f32 = 25.0;
80pub const FALLBACK_BLOCK_STRENGTH: f32 = 3.3;
81pub const BEHIND_TARGET_ANGLE: f32 = 45.0;
82pub const BASE_PARRIED_POISE_PUNISHMENT: f32 = 100.0 / 3.5;
83
84#[derive(Copy, Clone)]
85pub struct AttackerInfo<'a> {
86 pub entity: EcsEntity,
87 pub uid: Uid,
88 pub group: Option<&'a Group>,
89 pub energy: Option<&'a Energy>,
90 pub combo: Option<&'a Combo>,
91 pub inventory: Option<&'a Inventory>,
92 pub stats: Option<&'a Stats>,
93 pub mass: Option<&'a Mass>,
94 pub pos: Option<Vec3<f32>>,
95}
96
97#[derive(Copy, Clone)]
98pub struct TargetInfo<'a> {
99 pub entity: EcsEntity,
100 pub uid: Uid,
101 pub inventory: Option<&'a Inventory>,
102 pub stats: Option<&'a Stats>,
103 pub health: Option<&'a Health>,
104 pub pos: Vec3<f32>,
105 pub ori: Option<&'a Ori>,
106 pub char_state: Option<&'a CharacterState>,
107 pub energy: Option<&'a Energy>,
108 pub buffs: Option<&'a Buffs>,
109 pub mass: Option<&'a Mass>,
110 pub player: Option<&'a Player>,
111}
112
113#[derive(Clone, Copy)]
114pub struct AttackOptions {
115 pub target_dodging: bool,
116 pub permit_pvp: bool,
118 pub target_group: GroupTarget,
119 pub allow_friendly_fire: bool,
122 pub precision_mult: Option<f32>,
123}
124
125#[derive(Clone, Debug, Serialize, Deserialize)] pub struct Attack {
127 damages: Vec<AttackDamage>,
128 effects: Vec<AttackEffect>,
129 precision_multiplier: f32,
130 pub(crate) blockable: bool,
131 ability_info: Option<AbilityInfo>,
132}
133
134impl Attack {
135 pub fn new(ability_info: Option<AbilityInfo>) -> Self {
136 Self {
137 damages: Vec::new(),
138 effects: Vec::new(),
139 precision_multiplier: 1.0,
140 blockable: true,
141 ability_info,
142 }
143 }
144
145 #[must_use]
146 pub fn with_damage(mut self, damage: AttackDamage) -> Self {
147 self.damages.push(damage);
148 self
149 }
150
151 #[must_use]
152 pub fn with_effect(mut self, effect: AttackEffect) -> Self {
153 self.effects.push(effect);
154 self
155 }
156
157 #[must_use]
158 pub fn with_precision(mut self, precision_multiplier: f32) -> Self {
159 self.precision_multiplier = precision_multiplier;
160 self
161 }
162
163 #[must_use]
164 pub fn with_blockable(mut self, blockable: bool) -> Self {
165 self.blockable = blockable;
166 self
167 }
168
169 #[must_use]
170 pub fn with_combo_requirement(self, combo: i32, requirement: CombatRequirement) -> Self {
171 self.with_effect(
172 AttackEffect::new(None, CombatEffect::Combo(combo)).with_requirement(requirement),
173 )
174 }
175
176 #[must_use]
177 pub fn with_combo(self, combo: i32) -> Self {
178 self.with_combo_requirement(combo, CombatRequirement::AnyDamage)
179 }
180
181 #[must_use]
182 pub fn with_combo_increment(self) -> Self { self.with_combo(1) }
183
184 pub fn effects(&self) -> impl Iterator<Item = &AttackEffect> { self.effects.iter() }
185
186 pub fn compute_block_damage_decrement(
187 blockable: bool,
188 attacker: Option<&AttackerInfo>,
189 target: &TargetInfo,
190 source: AttackSource,
191 dir: Dir,
192 damage: f32,
193 msm: &MaterialStatManifest,
194 time: Time,
195 emitters: &mut (impl EmitExt<ParryHookEvent> + EmitExt<PoiseChangeEvent>),
196 mut emit_outcome: impl FnMut(Outcome),
197 ) -> f32 {
198 if blockable && damage > 0.0 {
199 if let (Some(char_state), Some(ori), Some(inventory)) =
200 (target.char_state, target.ori, target.inventory)
201 {
202 let is_parry = char_state.is_parry(source);
203 let is_block = char_state.is_block(source);
204 let mut block_strength = block_strength(inventory, char_state);
205
206 if ori.look_vec().angle_between(-dir.with_z(0.0)) < char_state.block_angle()
207 && (is_parry || is_block)
208 && block_strength > 0.0
209 {
210 if is_parry {
211 block_strength = damage;
212
213 emitters.emit(ParryHookEvent {
214 defender: target.entity,
215 attacker: attacker.map(|a| a.entity),
216 source,
217 poise_multiplier: 2.0 - (damage / block_strength).min(1.0),
218 });
219 }
220
221 let poise_cost = (damage / block_strength).min(1.0) * MAX_BLOCK_POISE_COST;
222
223 let poise_change = Poise::apply_poise_reduction(
224 poise_cost,
225 target.inventory,
226 msm,
227 target.char_state,
228 target.stats,
229 );
230
231 emit_outcome(Outcome::Block {
232 parry: is_parry,
233 pos: target.pos,
234 uid: target.uid,
235 });
236 emitters.emit(PoiseChangeEvent {
237 entity: target.entity,
238 change: PoiseChange {
239 amount: -poise_change,
240 impulse: *dir,
241 by: attacker.map(|x| (*x).into()),
242 cause: Some(DamageSource::from(source)),
243 time,
244 },
245 });
246
247 block_strength
248 } else {
249 0.0
250 }
251 } else {
252 0.0
253 }
254 } else {
255 0.0
256 }
257 }
258
259 pub fn compute_damage_reduction(
260 attacker: Option<&AttackerInfo>,
261 target: &TargetInfo,
262 damage: Damage,
263 msm: &MaterialStatManifest,
264 ) -> f32 {
265 if damage.value > 0.0 {
266 let attacker_penetration = attacker
267 .and_then(|a| a.stats)
268 .map_or(0.0, |s| s.mitigations_penetration)
269 .clamp(0.0, 1.0);
270 let raw_damage_reduction =
271 Damage::compute_damage_reduction(Some(damage), target.inventory, target.stats, msm);
272
273 if raw_damage_reduction >= 1.0 {
274 raw_damage_reduction
275 } else {
276 (1.0 - attacker_penetration) * raw_damage_reduction
277 }
278 } else {
279 0.0
280 }
281 }
282
283 pub fn apply_attack(
284 &self,
285 attacker: Option<AttackerInfo>,
286 target: &TargetInfo,
287 dir: Dir,
288 options: AttackOptions,
289 strength_modifier: f32,
292 attack_source: AttackSource,
293 time: Time,
294 emitters: &mut (
295 impl EmitExt<HealthChangeEvent>
296 + EmitExt<EnergyChangeEvent>
297 + EmitExt<ParryHookEvent>
298 + EmitExt<KnockbackEvent>
299 + EmitExt<BuffEvent>
300 + EmitExt<PoiseChangeEvent>
301 + EmitExt<ComboChangeEvent>
302 + EmitExt<EntityAttackedHookEvent>
303 + EmitExt<TransformEvent>
304 ),
305 mut emit_outcome: impl FnMut(Outcome),
306 rng: &mut rand::rngs::ThreadRng,
307 damage_instance_offset: u64,
308 ) -> bool {
309 let msm = &MaterialStatManifest::load().read();
311
312 let AttackOptions {
313 target_dodging,
314 permit_pvp,
315 allow_friendly_fire,
316 target_group,
317 precision_mult,
318 } = options;
319
320 let avoid_damage = |attack_damage: &AttackDamage| {
326 target_dodging
327 || (!permit_pvp && matches!(attack_damage.target, Some(GroupTarget::OutOfGroup)))
328 };
329 let avoid_effect = |attack_effect: &AttackEffect| {
330 target_dodging
331 || (!permit_pvp && matches!(attack_effect.target, Some(GroupTarget::OutOfGroup)))
332 };
333
334 let from_precision_mult = attacker
335 .and_then(|a| a.stats)
336 .and_then(|s| {
337 s.conditional_precision_modifiers
338 .iter()
339 .filter_map(|(req, mult, ovrd)| {
340 req.is_none_or(|r| {
341 r.requirement_met(
342 (
343 target.health,
344 target.buffs,
345 target.char_state,
346 target.ori,
347 Some(target.uid),
348 ),
349 (
350 attacker.map(|a| a.entity),
351 attacker.and_then(|a| a.energy),
352 attacker.and_then(|a| a.combo),
353 ),
354 attacker.map(|a| a.uid),
355 0.0,
356 emitters,
357 dir,
358 Some(attack_source),
359 self.ability_info,
360 )
361 })
362 .then_some((*mult, *ovrd))
363 })
364 .chain(precision_mult.iter().map(|val| (*val, false)))
365 .reduce(|(val_a, ovrd_a), (val_b, ovrd_b)| {
366 if ovrd_a || ovrd_b {
367 (val_a.min(val_b), true)
368 } else {
369 (val_a.max(val_b), false)
370 }
371 })
372 })
373 .map(|(val, _)| val);
374
375 let from_precision_vulnerability_mult = target
376 .stats
377 .and_then(|s| s.precision_vulnerability_multiplier_override);
378
379 let precision_mult = match (from_precision_mult, from_precision_vulnerability_mult) {
380 (Some(a), Some(b)) => Some(a.max(b)),
381 (Some(a), None) | (None, Some(a)) => Some(a),
382 (None, None) => None,
383 };
384
385 let precision_power = 1.0
386 + ((self.precision_multiplier - 1.0)
387 * attacker
388 .and_then(|a| a.stats)
389 .map_or(1.0, |s| s.precision_power_mult));
390
391 let attacked_modifiers = AttackedModification::attacked_modifiers(
392 target,
393 attacker,
394 emitters,
395 dir,
396 Some(attack_source),
397 self.ability_info,
398 );
399
400 let mut is_applied = false;
401 let mut accumulated_damage = 0.0;
402 let damage_modifier = attacker
403 .and_then(|a| a.stats)
404 .map_or(1.0, |s| s.attack_damage_modifier);
405 for damage in self
406 .damages
407 .iter()
408 .filter(|d| {
409 allow_friendly_fire
410 || d.target
411 .is_none_or(|t| t == GroupTarget::All || t == target_group)
412 })
413 .filter(|d| !avoid_damage(d))
414 {
415 let damage_instance = damage.instance + damage_instance_offset;
416 is_applied = true;
417
418 let damage_reduction =
419 Attack::compute_damage_reduction(attacker.as_ref(), target, damage.damage, msm);
420
421 let damage_with_modifier = damage.damage.value * strength_modifier * damage_modifier;
422 let damage_with_precision = damage_with_modifier
423 * (1.0 + precision_mult.unwrap_or(0.0) * (precision_power - 1.0));
424
425 let block_damage_decrement = Attack::compute_block_damage_decrement(
426 self.blockable,
427 attacker.as_ref(),
428 target,
429 attack_source,
430 dir,
431 damage_with_precision,
432 msm,
433 time,
434 emitters,
435 &mut emit_outcome,
436 );
437
438 let change = damage.damage.calculate_health_change(
439 damage_reduction,
440 block_damage_decrement,
441 attacker.map(|x| x.into()),
442 precision_mult,
443 precision_power,
444 strength_modifier * damage_modifier,
445 time,
446 damage_instance,
447 DamageSource::from(attack_source),
448 );
449 let applied_damage = -change.amount;
450 accumulated_damage += applied_damage;
451
452 if change.amount.abs() > Health::HEALTH_EPSILON {
453 emitters.emit(HealthChangeEvent {
454 entity: target.entity,
455 change,
456 });
457 match damage.damage.kind {
458 DamageKind::Slashing => {
459 if let Some(target_energy) = target.energy {
463 let energy_change = applied_damage * SLASHING_ENERGY_FRACTION;
464 if energy_change > target_energy.current() {
465 let health_damage = energy_change - target_energy.current();
466 accumulated_damage += health_damage;
467 let health_change = HealthChange {
468 amount: -health_damage,
469 by: attacker.map(|x| x.into()),
470 cause: Some(DamageSource::from(attack_source)),
471 time,
472 precise: precision_mult.is_some(),
473 instance: damage_instance,
474 };
475 emitters.emit(HealthChangeEvent {
476 entity: target.entity,
477 change: health_change,
478 });
479 }
480 emitters.emit(EnergyChangeEvent {
481 entity: target.entity,
482 change: -energy_change,
483 reset_rate: false,
484 });
485 }
486 },
487 DamageKind::Crushing => {
488 let reduced_damage =
493 applied_damage * damage_reduction / (1.0 - damage_reduction);
494 let poise = reduced_damage
495 * CRUSHING_POISE_FRACTION
496 * attacker
497 .and_then(|a| a.stats)
498 .map_or(1.0, |s| s.poise_damage_modifier);
499 let change = -Poise::apply_poise_reduction(
500 poise,
501 target.inventory,
502 msm,
503 target.char_state,
504 target.stats,
505 );
506 let poise_change = PoiseChange {
507 amount: change,
508 impulse: *dir,
509 by: attacker.map(|x| x.into()),
510 cause: Some(DamageSource::from(attack_source)),
511 time,
512 };
513 if change.abs() > Poise::POISE_EPSILON {
514 if let Some(CharacterState::Stunned(data)) = target.char_state {
517 let health_change =
518 change * data.static_data.poise_state.damage_multiplier();
519 let health_change = HealthChange {
520 amount: health_change,
521 by: attacker.map(|x| x.into()),
522 cause: Some(DamageSource::from(attack_source)),
523 instance: damage_instance,
524 precise: precision_mult.is_some(),
525 time,
526 };
527 accumulated_damage -= health_change.amount;
528 emitters.emit(HealthChangeEvent {
529 entity: target.entity,
530 change: health_change,
531 });
532 } else {
533 emitters.emit(PoiseChangeEvent {
534 entity: target.entity,
535 change: poise_change,
536 });
537 }
538 }
539 },
540 DamageKind::Piercing | DamageKind::Energy => {},
543 }
544 for effect in damage.effects.iter() {
545 match effect {
546 CombatEffect::Knockback(kb) => {
547 let impulse = kb.calculate_impulse(
548 dir,
549 target.char_state,
550 attacker.and_then(|a| a.stats),
551 ) * strength_modifier;
552 if !impulse.is_approx_zero() {
553 emitters.emit(KnockbackEvent {
554 entity: target.entity,
555 impulse,
556 });
557 }
558 },
559 CombatEffect::EnergyReward(ec) => {
560 if let Some(attacker) = attacker {
561 emitters.emit(EnergyChangeEvent {
562 entity: attacker.entity,
563 change: *ec
564 * compute_energy_reward_mod(attacker.inventory, msm)
565 * strength_modifier
566 * attacker.stats.map_or(1.0, |s| s.energy_reward_modifier)
567 * attacked_modifiers.energy_reward,
568 reset_rate: false,
569 });
570 }
571 },
572 CombatEffect::Buff(b) => {
573 if rng.random::<f32>() < b.chance {
574 emitters.emit(BuffEvent {
575 entity: target.entity,
576 buff_change: BuffChange::Add(b.to_buff(
577 time,
578 (attacker.map(|a| a.uid), attacker.and_then(|a| a.mass)),
579 (target.stats, target.mass),
580 applied_damage,
581 strength_modifier,
582 self.ability_info,
583 )),
584 });
585 }
586 },
587 CombatEffect::Lifesteal(l) => {
588 if let Some(attacker_entity) = attacker.map(|a| a.entity) {
589 let change = HealthChange {
590 amount: applied_damage * l * strength_modifier,
591 by: attacker.map(|a| a.into()),
592 cause: None,
593 time,
594 precise: false,
595 instance: rand::random(),
596 };
597 if change.amount.abs() > Health::HEALTH_EPSILON {
598 emitters.emit(HealthChangeEvent {
599 entity: attacker_entity,
600 change,
601 });
602 }
603 }
604 },
605 CombatEffect::Poise(p) => {
606 let change = -Poise::apply_poise_reduction(
607 *p,
608 target.inventory,
609 msm,
610 target.char_state,
611 target.stats,
612 ) * strength_modifier
613 * attacker
614 .and_then(|a| a.stats)
615 .map_or(1.0, |s| s.poise_damage_modifier);
616 if change.abs() > Poise::POISE_EPSILON {
617 let poise_change = PoiseChange {
618 amount: change,
619 impulse: *dir,
620 by: attacker.map(|x| x.into()),
621 cause: Some(DamageSource::from(attack_source)),
622 time,
623 };
624 emitters.emit(PoiseChangeEvent {
625 entity: target.entity,
626 change: poise_change,
627 });
628 }
629 },
630 CombatEffect::Heal(h) => {
631 let change = HealthChange {
632 amount: *h * strength_modifier,
633 by: attacker.map(|a| a.into()),
634 cause: None,
635 time,
636 precise: false,
637 instance: rand::random(),
638 };
639 if change.amount.abs() > Health::HEALTH_EPSILON {
640 emitters.emit(HealthChangeEvent {
641 entity: target.entity,
642 change,
643 });
644 }
645 },
646 CombatEffect::Combo(c) => {
647 if let Some(attacker_entity) = attacker.map(|a| a.entity) {
648 emitters.emit(ComboChangeEvent {
649 entity: attacker_entity,
650 change: (*c as f32 * strength_modifier).ceil() as i32,
651 });
652 }
653 },
654 CombatEffect::AdditionalDamage(damage) => {
655 let change = {
656 let mut change = change;
657 change.amount *= damage * strength_modifier;
658 change.instance = rand::random();
659 change
660 };
661 accumulated_damage -= change.amount;
662 emitters.emit(HealthChangeEvent {
663 entity: target.entity,
664 change,
665 });
666 },
667 CombatEffect::RefreshBuff(chance, b) => {
668 if rng.random::<f32>() < *chance {
669 emitters.emit(BuffEvent {
670 entity: target.entity,
671 buff_change: BuffChange::Refresh(*b),
672 });
673 }
674 },
675 CombatEffect::SelfBuff(b) => {
676 if let Some(attacker) = attacker
677 && rng.random::<f32>() < b.chance
678 {
679 emitters.emit(BuffEvent {
680 entity: attacker.entity,
681 buff_change: BuffChange::Add(b.to_self_buff(
682 time,
683 (Some(attacker.uid), attacker.stats, attacker.mass),
684 applied_damage,
685 strength_modifier,
686 self.ability_info,
687 )),
688 });
689 }
690 },
691 CombatEffect::Energy(e) => {
692 emitters.emit(EnergyChangeEvent {
693 entity: target.entity,
694 change: e * strength_modifier,
695 reset_rate: true,
696 });
697 },
698 CombatEffect::Transform {
699 entity_spec,
700 allow_players,
701 } => {
702 if target.player.is_none() || *allow_players {
703 emitters.emit(TransformEvent {
704 target_entity: target.uid,
705 entity_info: {
706 let Ok(entity_config) = Ron::<EntityConfig>::load(
707 entity_spec,
708 )
709 .inspect_err(|error| {
710 error!(
711 ?entity_spec,
712 ?error,
713 "Could not load entity configuration for death \
714 effect"
715 )
716 }) else {
717 continue;
718 };
719
720 EntityInfo::at(target.pos).with_entity_config(
721 entity_config.read().clone().into_inner(),
722 Some(entity_spec),
723 rng,
724 None,
725 )
726 },
727 allow_players: *allow_players,
728 delete_on_failure: false,
729 });
730 }
731 },
732 CombatEffect::DebuffsVulnerable {
733 mult,
734 scaling,
735 filter_attacker,
736 filter_weapon,
737 } => {
738 if let Some(buffs) = target.buffs {
739 let num_debuffs = buffs.iter_active().flatten().filter(|b| {
740 let debuff_filter = matches!(b.kind.differentiate(), BuffDescriptor::SimpleNegative);
741 let attacker_filter = !filter_attacker || matches!(b.source, BuffSource::Character { by, .. } if Some(by) == attacker.map(|a| a.uid));
742 let weapon_filter = filter_weapon.is_none_or(|w| matches!(b.source, BuffSource::Character { tool_kind, .. } if Some(w) == tool_kind));
743 debuff_filter && attacker_filter && weapon_filter
744 }).count();
745 if num_debuffs > 0 {
746 let change = {
747 let mut change = change;
748 change.amount *= scaling.factor(num_debuffs as f32, 1.0)
749 * mult
750 * strength_modifier;
751 change.instance = rand::random();
752 change
753 };
754 accumulated_damage -= change.amount;
755 emitters.emit(HealthChangeEvent {
756 entity: target.entity,
757 change,
758 });
759 }
760 }
761 },
762 }
763 }
764 }
765 }
766 for effect in self
767 .effects
768 .iter()
769 .chain(
770 attacker
771 .and_then(|a| a.stats)
772 .map(|s| s.effects_on_attack.iter())
773 .into_iter()
774 .flatten(),
775 )
776 .filter(|e| {
777 allow_friendly_fire
778 || e.target
779 .is_none_or(|t| t == GroupTarget::All || t == target_group)
780 })
781 .filter(|e| !avoid_effect(e))
782 {
783 let requirements_met = effect.requirements.iter().all(|req| {
784 req.requirement_met(
785 (
786 target.health,
787 target.buffs,
788 target.char_state,
789 target.ori,
790 Some(target.uid),
791 ),
792 (
793 attacker.map(|a| a.entity),
794 attacker.and_then(|a| a.energy),
795 attacker.and_then(|a| a.combo),
796 ),
797 attacker.map(|a| a.uid),
798 accumulated_damage,
799 emitters,
800 dir,
801 Some(attack_source),
802 self.ability_info,
803 )
804 });
805 if requirements_met {
806 let mut strength_modifier = strength_modifier;
807 for modification in effect.modifications.iter() {
808 modification.apply_mod(
809 attacker.and_then(|a| a.pos),
810 Some(target.pos),
811 &mut strength_modifier,
812 );
813 }
814 let strength_modifier = strength_modifier;
815 is_applied = true;
816 match &effect.effect {
817 CombatEffect::Knockback(kb) => {
818 let impulse = kb.calculate_impulse(
819 dir,
820 target.char_state,
821 attacker.and_then(|a| a.stats),
822 ) * strength_modifier;
823 if !impulse.is_approx_zero() {
824 emitters.emit(KnockbackEvent {
825 entity: target.entity,
826 impulse,
827 });
828 }
829 },
830 CombatEffect::EnergyReward(ec) => {
831 if let Some(attacker) = attacker {
832 emitters.emit(EnergyChangeEvent {
833 entity: attacker.entity,
834 change: ec
835 * compute_energy_reward_mod(attacker.inventory, msm)
836 * strength_modifier
837 * attacker.stats.map_or(1.0, |s| s.energy_reward_modifier)
838 * attacked_modifiers.energy_reward,
839 reset_rate: false,
840 });
841 }
842 },
843 CombatEffect::Buff(b) => {
844 if rng.random::<f32>() < b.chance {
845 emitters.emit(BuffEvent {
846 entity: target.entity,
847 buff_change: BuffChange::Add(b.to_buff(
848 time,
849 (attacker.map(|a| a.uid), attacker.and_then(|a| a.mass)),
850 (target.stats, target.mass),
851 accumulated_damage,
852 strength_modifier,
853 self.ability_info,
854 )),
855 });
856 }
857 },
858 CombatEffect::Lifesteal(l) => {
859 if let Some(attacker_entity) = attacker.map(|a| a.entity) {
860 let change = HealthChange {
861 amount: accumulated_damage * l * strength_modifier,
862 by: attacker.map(|a| a.into()),
863 cause: None,
864 time,
865 precise: false,
866 instance: rand::random(),
867 };
868 if change.amount.abs() > Health::HEALTH_EPSILON {
869 emitters.emit(HealthChangeEvent {
870 entity: attacker_entity,
871 change,
872 });
873 }
874 }
875 },
876 CombatEffect::Poise(p) => {
877 let change = -Poise::apply_poise_reduction(
878 *p,
879 target.inventory,
880 msm,
881 target.char_state,
882 target.stats,
883 ) * strength_modifier
884 * attacker
885 .and_then(|a| a.stats)
886 .map_or(1.0, |s| s.poise_damage_modifier);
887 if change.abs() > Poise::POISE_EPSILON {
888 let poise_change = PoiseChange {
889 amount: change,
890 impulse: *dir,
891 by: attacker.map(|x| x.into()),
892 cause: Some(attack_source.into()),
893 time,
894 };
895 emitters.emit(PoiseChangeEvent {
896 entity: target.entity,
897 change: poise_change,
898 });
899 }
900 },
901 CombatEffect::Heal(h) => {
902 let change = HealthChange {
903 amount: h * strength_modifier,
904 by: attacker.map(|a| a.into()),
905 cause: None,
906 time,
907 precise: false,
908 instance: rand::random(),
909 };
910 if change.amount.abs() > Health::HEALTH_EPSILON {
911 emitters.emit(HealthChangeEvent {
912 entity: target.entity,
913 change,
914 });
915 }
916 },
917 CombatEffect::Combo(c) => {
918 if let Some(attacker_entity) = attacker.map(|a| a.entity) {
919 emitters.emit(ComboChangeEvent {
920 entity: attacker_entity,
921 change: (*c as f32 * strength_modifier).ceil() as i32,
922 });
923 }
924 },
925 CombatEffect::AdditionalDamage(damage) => {
926 let change = HealthChange {
927 amount: -accumulated_damage * damage * strength_modifier,
928 by: attacker.map(|a| a.into()),
929 cause: Some(DamageSource::from(attack_source)),
930 time,
931 precise: precision_mult.is_some(),
932 instance: rand::random(),
933 };
934 accumulated_damage -= change.amount;
935 emitters.emit(HealthChangeEvent {
936 entity: target.entity,
937 change,
938 });
939 },
940 CombatEffect::RefreshBuff(chance, b) => {
941 if rng.random::<f32>() < *chance {
942 emitters.emit(BuffEvent {
943 entity: target.entity,
944 buff_change: BuffChange::Refresh(*b),
945 });
946 }
947 },
948 CombatEffect::SelfBuff(b) => {
949 if let Some(attacker) = attacker
950 && rng.random::<f32>() < b.chance
951 {
952 emitters.emit(BuffEvent {
953 entity: attacker.entity,
954 buff_change: BuffChange::Add(b.to_self_buff(
955 time,
956 (Some(attacker.uid), attacker.stats, attacker.mass),
957 accumulated_damage,
958 strength_modifier,
959 self.ability_info,
960 )),
961 });
962 }
963 },
964 CombatEffect::Energy(e) => {
965 emitters.emit(EnergyChangeEvent {
966 entity: target.entity,
967 change: e * strength_modifier,
968 reset_rate: true,
969 });
970 },
971 CombatEffect::Transform {
972 entity_spec,
973 allow_players,
974 } => {
975 if target.player.is_none() || *allow_players {
976 emitters.emit(TransformEvent {
977 target_entity: target.uid,
978 entity_info: {
979 let Ok(entity_config) = Ron::<EntityConfig>::load(entity_spec)
980 .inspect_err(|error| {
981 error!(
982 ?entity_spec,
983 ?error,
984 "Could not load entity configuration for death \
985 effect"
986 )
987 })
988 else {
989 continue;
990 };
991
992 EntityInfo::at(target.pos).with_entity_config(
993 entity_config.read().clone().into_inner(),
994 Some(entity_spec),
995 rng,
996 None,
997 )
998 },
999 allow_players: *allow_players,
1000 delete_on_failure: false,
1001 });
1002 }
1003 },
1004 CombatEffect::DebuffsVulnerable {
1005 mult,
1006 scaling,
1007 filter_attacker,
1008 filter_weapon,
1009 } => {
1010 if let Some(buffs) = target.buffs {
1011 let num_debuffs = buffs.iter_active().flatten().filter(|b| {
1012 let debuff_filter = matches!(b.kind.differentiate(), BuffDescriptor::SimpleNegative);
1013 let attacker_filter = !filter_attacker || matches!(b.source, BuffSource::Character { by, .. } if Some(by) == attacker.map(|a| a.uid));
1014 let weapon_filter = filter_weapon.is_none_or(|w| matches!(b.source, BuffSource::Character { tool_kind, .. } if Some(w) == tool_kind));
1015 debuff_filter && attacker_filter && weapon_filter
1016 }).count();
1017 if num_debuffs > 0 {
1018 let change = HealthChange {
1019 amount: -accumulated_damage
1020 * scaling.factor(num_debuffs as f32, 1.0)
1021 * mult
1022 * strength_modifier,
1023 by: attacker.map(|a| a.into()),
1024 cause: Some(DamageSource::from(attack_source)),
1025 time,
1026 precise: precision_mult.is_some(),
1027 instance: rand::random(),
1028 };
1029 accumulated_damage -= change.amount;
1030 emitters.emit(HealthChangeEvent {
1031 entity: target.entity,
1032 change,
1033 });
1034 }
1035 }
1036 },
1037 }
1038 }
1039 }
1040 if is_applied {
1043 emitters.emit(EntityAttackedHookEvent {
1044 entity: target.entity,
1045 attacker: attacker.map(|a| a.entity),
1046 attack_dir: dir,
1047 damage_dealt: accumulated_damage,
1048 attack_source,
1049 });
1050 }
1051 is_applied
1052 }
1053}
1054
1055pub fn allow_friendly_fire(
1056 entered_auras: &ReadStorage<EnteredAuras>,
1057 attacker: EcsEntity,
1058 target: EcsEntity,
1059) -> bool {
1060 entered_auras
1061 .get(attacker)
1062 .zip(entered_auras.get(target))
1063 .and_then(|(attacker, target)| {
1064 Some((
1065 attacker.auras.get(&AuraKindVariant::FriendlyFire)?,
1066 target.auras.get(&AuraKindVariant::FriendlyFire)?,
1067 ))
1068 })
1069 .is_some_and(|(attacker, target)| attacker.intersection(target).next().is_some())
1071}
1072
1073pub fn permit_pvp(
1083 alignments: &ReadStorage<Alignment>,
1084 players: &ReadStorage<Player>,
1085 entered_auras: &ReadStorage<EnteredAuras>,
1086 id_maps: &IdMaps,
1087 attacker: Option<EcsEntity>,
1088 target: EcsEntity,
1089) -> bool {
1090 let owner_if_pet = |entity| {
1093 let alignment = alignments.get(entity).copied();
1094 if let Some(Alignment::Owned(uid)) = alignment {
1095 id_maps.uid_entity(uid).unwrap_or(entity)
1098 } else {
1099 entity
1100 }
1101 };
1102
1103 let attacker = match attacker {
1106 Some(attacker) => attacker,
1107 None => return true,
1108 };
1109
1110 let attacker_owner = owner_if_pet(attacker);
1112 let target_owner = owner_if_pet(target);
1113
1114 if let (Some(attacker_auras), Some(target_auras)) = (
1116 entered_auras.get(attacker_owner),
1117 entered_auras.get(target_owner),
1118 ) && attacker_auras
1119 .auras
1120 .get(&AuraKindVariant::ForcePvP)
1121 .zip(target_auras.auras.get(&AuraKindVariant::ForcePvP))
1122 .is_some_and(|(attacker, target)| attacker.intersection(target).next().is_some())
1124 {
1125 return true;
1126 }
1127
1128 if attacker_owner == target_owner {
1133 return allow_friendly_fire(entered_auras, attacker, target);
1134 }
1135
1136 let attacker_info = players.get(attacker_owner);
1138 let target_info = players.get(target_owner);
1139
1140 attacker_info
1142 .zip(target_info)
1143 .is_none_or(|(a, t)| a.may_harm(t))
1144}
1145
1146#[derive(Clone, Debug, Serialize, Deserialize)]
1147pub struct AttackDamage {
1148 damage: Damage,
1149 target: Option<GroupTarget>,
1150 effects: Vec<CombatEffect>,
1151 instance: u64,
1153}
1154
1155impl AttackDamage {
1156 pub fn new(damage: Damage, target: Option<GroupTarget>, instance: u64) -> Self {
1157 Self {
1158 damage,
1159 target,
1160 effects: Vec::new(),
1161 instance,
1162 }
1163 }
1164
1165 #[must_use]
1166 pub fn with_effect(mut self, effect: CombatEffect) -> Self {
1167 self.effects.push(effect);
1168 self
1169 }
1170}
1171
1172#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
1173pub struct AttackEffect {
1174 target: Option<GroupTarget>,
1175 effect: CombatEffect,
1176 requirements: Vec<CombatRequirement>,
1177 modifications: Vec<CombatModification>,
1178}
1179
1180impl AttackEffect {
1181 pub fn new(target: Option<GroupTarget>, effect: CombatEffect) -> Self {
1182 Self {
1183 target,
1184 effect,
1185 requirements: Vec::new(),
1186 modifications: Vec::new(),
1187 }
1188 }
1189
1190 #[must_use]
1191 pub fn with_requirement(mut self, requirement: CombatRequirement) -> Self {
1192 self.requirements.push(requirement);
1193 self
1194 }
1195
1196 #[must_use]
1197 pub fn with_modification(mut self, modification: CombatModification) -> Self {
1198 self.modifications.push(modification);
1199 self
1200 }
1201
1202 pub fn effect(&self) -> &CombatEffect { &self.effect }
1203}
1204
1205#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
1206pub struct StatEffect {
1207 pub target: StatEffectTarget,
1208 pub effect: CombatEffect,
1209 requirements: Vec<CombatRequirement>,
1210 modifications: Vec<CombatModification>,
1211}
1212
1213impl StatEffect {
1214 pub fn new(target: StatEffectTarget, effect: CombatEffect) -> Self {
1215 Self {
1216 target,
1217 effect,
1218 requirements: Vec::new(),
1219 modifications: Vec::new(),
1220 }
1221 }
1222
1223 #[must_use]
1224 pub fn with_requirement(mut self, requirement: CombatRequirement) -> Self {
1225 self.requirements.push(requirement);
1226 self
1227 }
1228
1229 #[must_use]
1230 pub fn with_modification(mut self, modification: CombatModification) -> Self {
1231 self.modifications.push(modification);
1232 self
1233 }
1234
1235 pub fn requirements(&self) -> impl Iterator<Item = &CombatRequirement> {
1236 self.requirements.iter()
1237 }
1238
1239 pub fn modifications(&self) -> impl Iterator<Item = &CombatModification> {
1240 self.modifications.iter()
1241 }
1242}
1243
1244#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
1245pub enum CombatEffect {
1246 Heal(f32),
1247 Buff(CombatBuff),
1248 Knockback(Knockback),
1249 EnergyReward(f32),
1250 Lifesteal(f32),
1251 Poise(f32),
1252 Combo(i32),
1253 AdditionalDamage(f32),
1256 RefreshBuff(f32, BuffKind),
1258 SelfBuff(CombatBuff),
1260 Energy(f32),
1262 Transform {
1264 entity_spec: String,
1265 #[serde(default)]
1267 allow_players: bool,
1268 },
1269 DebuffsVulnerable {
1272 mult: f32,
1273 scaling: ScalingKind,
1274 filter_attacker: bool,
1277 filter_weapon: Option<ToolKind>,
1280 },
1281}
1282
1283impl CombatEffect {
1284 pub fn apply_multiplier(self, mult: f32) -> Self {
1285 match self {
1286 CombatEffect::Heal(h) => CombatEffect::Heal(h * mult),
1287 CombatEffect::Buff(CombatBuff {
1288 kind,
1289 dur_secs,
1290 strength,
1291 chance,
1292 }) => CombatEffect::Buff(CombatBuff {
1293 kind,
1294 dur_secs,
1295 strength: strength * mult,
1296 chance,
1297 }),
1298 CombatEffect::Knockback(Knockback {
1299 direction,
1300 strength,
1301 }) => CombatEffect::Knockback(Knockback {
1302 direction,
1303 strength: strength * mult,
1304 }),
1305 CombatEffect::EnergyReward(e) => CombatEffect::EnergyReward(e * mult),
1306 CombatEffect::Lifesteal(l) => CombatEffect::Lifesteal(l * mult),
1307 CombatEffect::Poise(p) => CombatEffect::Poise(p * mult),
1308 CombatEffect::Combo(c) => CombatEffect::Combo((c as f32 * mult).ceil() as i32),
1309 CombatEffect::AdditionalDamage(v) => CombatEffect::AdditionalDamage(v * mult),
1310 CombatEffect::RefreshBuff(c, b) => CombatEffect::RefreshBuff(c, b),
1311 CombatEffect::SelfBuff(CombatBuff {
1312 kind,
1313 dur_secs,
1314 strength,
1315 chance,
1316 }) => CombatEffect::SelfBuff(CombatBuff {
1317 kind,
1318 dur_secs,
1319 strength: strength * mult,
1320 chance,
1321 }),
1322 CombatEffect::Energy(e) => CombatEffect::Energy(e * mult),
1323 effect @ CombatEffect::Transform { .. } => effect,
1324 CombatEffect::DebuffsVulnerable {
1325 mult: a,
1326 scaling,
1327 filter_attacker,
1328 filter_weapon,
1329 } => CombatEffect::DebuffsVulnerable {
1330 mult: a * mult,
1331 scaling,
1332 filter_attacker,
1333 filter_weapon,
1334 },
1335 }
1336 }
1337
1338 pub fn adjusted_by_stats(self, stats: tool::Stats) -> Self {
1339 match self {
1340 CombatEffect::Heal(h) => CombatEffect::Heal(h * stats.effect_power),
1341 CombatEffect::Buff(CombatBuff {
1342 kind,
1343 dur_secs,
1344 strength,
1345 chance,
1346 }) => CombatEffect::Buff(CombatBuff {
1347 kind,
1348 dur_secs,
1349 strength: strength * stats.buff_strength,
1350 chance,
1351 }),
1352 CombatEffect::Knockback(Knockback {
1353 direction,
1354 strength,
1355 }) => CombatEffect::Knockback(Knockback {
1356 direction,
1357 strength: strength * stats.effect_power,
1358 }),
1359 CombatEffect::EnergyReward(e) => CombatEffect::EnergyReward(e),
1360 CombatEffect::Lifesteal(l) => CombatEffect::Lifesteal(l * stats.effect_power),
1361 CombatEffect::Poise(p) => CombatEffect::Poise(p * stats.effect_power),
1362 CombatEffect::Combo(c) => CombatEffect::Combo(c),
1363 CombatEffect::AdditionalDamage(v) => {
1364 CombatEffect::AdditionalDamage(v * stats.effect_power)
1365 },
1366 CombatEffect::RefreshBuff(c, b) => CombatEffect::RefreshBuff(c, b),
1367 CombatEffect::SelfBuff(CombatBuff {
1368 kind,
1369 dur_secs,
1370 strength,
1371 chance,
1372 }) => CombatEffect::SelfBuff(CombatBuff {
1373 kind,
1374 dur_secs,
1375 strength: strength * stats.buff_strength,
1376 chance,
1377 }),
1378 CombatEffect::Energy(e) => CombatEffect::Energy(e * stats.effect_power),
1379 effect @ CombatEffect::Transform { .. } => effect,
1380 CombatEffect::DebuffsVulnerable {
1381 mult,
1382 scaling,
1383 filter_attacker,
1384 filter_weapon,
1385 } => CombatEffect::DebuffsVulnerable {
1386 mult: mult * stats.effect_power,
1387 scaling,
1388 filter_attacker,
1389 filter_weapon,
1390 },
1391 }
1392 }
1393}
1394
1395#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq)]
1396struct AttackedModifiers {
1397 energy_reward: f32,
1398 damage_mult: f32,
1399}
1400
1401impl Default for AttackedModifiers {
1402 fn default() -> Self {
1403 Self {
1404 energy_reward: 1.0,
1405 damage_mult: 1.0,
1406 }
1407 }
1408}
1409
1410#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
1411pub struct AttackedModification {
1412 modifier: AttackedModifier,
1413 requirements: Vec<CombatRequirement>,
1414 modifications: Vec<CombatModification>,
1415}
1416
1417impl AttackedModification {
1418 pub fn new(modifier: AttackedModifier) -> Self {
1419 Self {
1420 modifier,
1421 requirements: Vec::new(),
1422 modifications: Vec::new(),
1423 }
1424 }
1425
1426 #[must_use]
1427 pub fn with_requirement(mut self, requirement: CombatRequirement) -> Self {
1428 self.requirements.push(requirement);
1429 self
1430 }
1431
1432 #[must_use]
1433 pub fn with_modification(mut self, modification: CombatModification) -> Self {
1434 self.modifications.push(modification);
1435 self
1436 }
1437
1438 fn attacked_modifiers(
1439 target: &TargetInfo,
1440 attacker: Option<AttackerInfo>,
1441 emitters: &mut (impl EmitExt<EnergyChangeEvent> + EmitExt<ComboChangeEvent>),
1442 dir: Dir,
1443 attack_source: Option<AttackSource>,
1444 ability_info: Option<AbilityInfo>,
1445 ) -> AttackedModifiers {
1446 if let Some(stats) = target.stats {
1447 stats.attacked_modifications.iter().fold(
1448 AttackedModifiers::default(),
1449 |mut a_mods, a_mod| {
1450 let requirements_met = a_mod.requirements.iter().all(|req| {
1451 req.requirement_met(
1452 (
1453 target.health,
1454 target.buffs,
1455 target.char_state,
1456 target.ori,
1457 Some(target.uid),
1458 ),
1459 (
1460 attacker.map(|a| a.entity),
1461 attacker.and_then(|a| a.energy),
1462 attacker.and_then(|a| a.combo),
1463 ),
1464 attacker.map(|a| a.uid),
1465 0.0, emitters,
1470 dir,
1471 attack_source,
1472 ability_info,
1473 )
1474 });
1475
1476 let mut strength_modifier = 1.0;
1477 for modification in a_mod.modifications.iter() {
1478 modification.apply_mod(
1479 attacker.and_then(|a| a.pos),
1480 Some(target.pos),
1481 &mut strength_modifier,
1482 );
1483 }
1484 let strength_modifier = strength_modifier;
1485
1486 if requirements_met {
1487 match a_mod.modifier {
1488 AttackedModifier::EnergyReward(er) => {
1489 a_mods.energy_reward *= 1.0 + (er * strength_modifier);
1490 },
1491 AttackedModifier::DamageMultiplier(dm) => {
1492 a_mods.damage_mult *= 1.0 + (dm * strength_modifier);
1493 },
1494 }
1495 }
1496
1497 a_mods
1498 },
1499 )
1500 } else {
1501 AttackedModifiers::default()
1502 }
1503 }
1504}
1505
1506#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq)]
1507pub enum AttackedModifier {
1508 EnergyReward(f32),
1509 DamageMultiplier(f32),
1510}
1511
1512#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq)]
1513pub enum CombatRequirement {
1514 AnyDamage,
1515 Energy(f32),
1516 Combo(u32),
1517 TargetHasBuff(BuffKind),
1518 TargetPoised,
1519 BehindTarget,
1520 TargetBlocking,
1521 TargetUnwielded,
1522 AttackSource(AttackSource),
1523 AttackInput(InputKind),
1524 Attacker(Uid),
1525 Target(Uid),
1526 StageSection(StageSection),
1527}
1528
1529impl CombatRequirement {
1530 pub fn requirement_met(
1531 &self,
1532 target: (
1533 Option<&Health>,
1534 Option<&Buffs>,
1535 Option<&CharacterState>,
1536 Option<&Ori>,
1537 Option<Uid>,
1538 ),
1539 originator: (Option<EcsEntity>, Option<&Energy>, Option<&Combo>),
1543 attacker: Option<Uid>,
1544 damage: f32,
1545 emitters: &mut (impl EmitExt<EnergyChangeEvent> + EmitExt<ComboChangeEvent>),
1546 dir: Dir,
1547 attack_source: Option<AttackSource>,
1548 ability_info: Option<AbilityInfo>,
1549 ) -> bool {
1550 let (target_health, target_buffs, target_char_state, target_ori, target_uid) = target;
1551 let (originator_entity, originator_energy, originator_combo) = originator;
1552 match self {
1553 CombatRequirement::AnyDamage => {
1554 damage > Health::HEALTH_EPSILON && target_health.is_some()
1555 },
1556 CombatRequirement::Energy(r) => {
1557 if let (Some(entity), Some(energy)) = (originator_entity, originator_energy) {
1558 let sufficient_energy = energy.current() >= *r;
1559 if sufficient_energy {
1560 emitters.emit(EnergyChangeEvent {
1561 entity,
1562 change: -*r,
1563 reset_rate: false,
1564 });
1565 }
1566
1567 sufficient_energy
1568 } else {
1569 false
1570 }
1571 },
1572 CombatRequirement::Combo(r) => {
1573 if let (Some(entity), Some(combo)) = (originator_entity, originator_combo) {
1574 let sufficient_combo = combo.counter() >= *r;
1575 if sufficient_combo {
1576 emitters.emit(ComboChangeEvent {
1577 entity,
1578 change: -(*r as i32),
1579 });
1580 }
1581
1582 sufficient_combo
1583 } else {
1584 false
1585 }
1586 },
1587 CombatRequirement::TargetHasBuff(buff) => {
1588 target_buffs.is_some_and(|buffs| buffs.contains(*buff))
1589 },
1590 CombatRequirement::TargetPoised => target_char_state.is_some_and(|cs| cs.is_stunned()),
1591 CombatRequirement::BehindTarget => {
1592 if let Some(ori) = target_ori {
1593 ori.look_vec().angle_between(dir.with_z(0.0)) < BEHIND_TARGET_ANGLE.to_radians()
1594 } else {
1595 false
1596 }
1597 },
1598 CombatRequirement::TargetBlocking => target_char_state
1599 .zip(attack_source)
1600 .is_some_and(|(cs, attack)| cs.is_block(attack) || cs.is_parry(attack)),
1601 CombatRequirement::TargetUnwielded => {
1602 target_char_state.is_some_and(|cs| !cs.is_wield())
1603 },
1604 CombatRequirement::AttackSource(source) => attack_source == Some(*source),
1605 CombatRequirement::AttackInput(input) => {
1606 ability_info.is_some_and(|ai| ai.input == *input)
1607 },
1608 CombatRequirement::Attacker(uid) => Some(*uid) == attacker,
1609 CombatRequirement::Target(uid) => Some(*uid) == target_uid,
1610 CombatRequirement::StageSection(s) => {
1611 Some(*s) == target_char_state.and_then(|cs| cs.stage_section())
1612 },
1613 }
1614 }
1615}
1616
1617#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq)]
1618pub enum CombatModification {
1619 RangeWeakening {
1622 start_dist: f32,
1623 end_dist: f32,
1624 min_str: f32,
1625 },
1626}
1627
1628impl CombatModification {
1629 pub fn apply_mod(
1630 &self,
1631 attacker_pos: Option<Vec3<f32>>,
1632 target_pos: Option<Vec3<f32>>,
1633 strength_mod: &mut f32,
1634 ) {
1635 match self {
1636 Self::RangeWeakening {
1637 start_dist,
1638 end_dist,
1639 min_str,
1640 } => {
1641 if let Some((attacker_pos, target_pos)) = attacker_pos.zip(target_pos) {
1642 let dist = attacker_pos.distance(target_pos);
1643 let gradient = (*min_str - 1.0) / (end_dist - start_dist).max(0.1);
1645 let intercept = 1.0 - gradient * start_dist;
1647 let strength = (gradient * dist + intercept).clamp(*min_str, 1.0);
1649 *strength_mod *= strength;
1650 }
1651 },
1652 }
1653 }
1654}
1655
1656#[derive(Clone, Debug, PartialEq)]
1658pub struct RiderEffects(pub Vec<BuffEffect>);
1659
1660impl specs::Component for RiderEffects {
1661 type Storage = specs::DenseVecStorage<RiderEffects>;
1662}
1663
1664#[derive(Clone, Debug, PartialEq)]
1665pub struct DeathEffects(pub Vec<StatEffect>);
1668
1669impl specs::Component for DeathEffects {
1670 type Storage = specs::DenseVecStorage<DeathEffects>;
1671}
1672
1673#[derive(Clone, Copy, Debug, Hash, Eq, PartialEq, Serialize, Deserialize)]
1674pub enum DamageContributor {
1675 Solo(Uid),
1676 Group { entity_uid: Uid, group: Group },
1677}
1678
1679impl DamageContributor {
1680 pub fn new(uid: Uid, group: Option<Group>) -> Self {
1681 if let Some(group) = group {
1682 DamageContributor::Group {
1683 entity_uid: uid,
1684 group,
1685 }
1686 } else {
1687 DamageContributor::Solo(uid)
1688 }
1689 }
1690
1691 pub fn uid(&self) -> Uid {
1692 match self {
1693 DamageContributor::Solo(uid) => *uid,
1694 DamageContributor::Group {
1695 entity_uid,
1696 group: _,
1697 } => *entity_uid,
1698 }
1699 }
1700}
1701
1702impl From<AttackerInfo<'_>> for DamageContributor {
1703 fn from(attacker_info: AttackerInfo) -> Self {
1704 DamageContributor::new(attacker_info.uid, attacker_info.group.copied())
1705 }
1706}
1707
1708#[derive(Copy, Clone, Debug, Hash, PartialEq, Eq, Serialize, Deserialize)]
1709pub enum DamageSource {
1710 Buff(BuffKind),
1711 Attack(AttackSource),
1712 Falling,
1713 Other,
1714}
1715
1716impl From<AttackSource> for DamageSource {
1717 fn from(attack: AttackSource) -> Self { DamageSource::Attack(attack) }
1718}
1719
1720#[derive(Copy, Clone, Debug, Hash, PartialEq, Eq, Serialize, Deserialize)]
1722pub enum DamageKind {
1723 Piercing,
1725 Slashing,
1728 Crushing,
1730 Energy,
1733}
1734
1735const PIERCING_PENETRATION_FRACTION: f32 = 0.75;
1736const SLASHING_ENERGY_FRACTION: f32 = 0.5;
1737const CRUSHING_POISE_FRACTION: f32 = 1.0;
1738
1739#[derive(Copy, Clone, Debug, PartialEq, Serialize, Deserialize)]
1740#[serde(deny_unknown_fields)]
1741pub struct Damage {
1742 pub kind: DamageKind,
1743 pub value: f32,
1744}
1745
1746impl Damage {
1747 pub fn compute_damage_reduction(
1749 damage: Option<Self>,
1750 inventory: Option<&Inventory>,
1751 stats: Option<&Stats>,
1752 msm: &MaterialStatManifest,
1753 ) -> f32 {
1754 let protection = compute_protection(inventory, msm);
1755
1756 let penetration = if let Some(damage) = damage {
1757 if let DamageKind::Piercing = damage.kind {
1758 (damage.value * PIERCING_PENETRATION_FRACTION)
1759 .clamp(0.0, protection.unwrap_or(0.0).max(0.0))
1760 } else {
1761 0.0
1762 }
1763 } else {
1764 0.0
1765 };
1766
1767 let protection = protection.map(|p| p - penetration);
1768
1769 const FIFTY_PERCENT_DR_THRESHOLD: f32 = 60.0;
1770
1771 let inventory_dr = match protection {
1772 Some(dr) => dr / (FIFTY_PERCENT_DR_THRESHOLD + dr.abs()),
1773 None => 1.0,
1774 };
1775
1776 let stats_dr = if let Some(stats) = stats {
1777 stats.damage_reduction.modifier()
1778 } else {
1779 0.0
1780 };
1781 if protection.is_none() || stats_dr >= 1.0 {
1783 1.0
1784 } else {
1785 1.0 - (1.0 - inventory_dr) * (1.0 - stats_dr)
1786 }
1787 }
1788
1789 pub fn calculate_health_change(
1790 self,
1791 damage_reduction: f32,
1792 block_damage_decrement: f32,
1793 damage_contributor: Option<DamageContributor>,
1794 precision_mult: Option<f32>,
1795 precision_power: f32,
1796 damage_modifier: f32,
1797 time: Time,
1798 instance: u64,
1799 damage_source: DamageSource,
1800 ) -> HealthChange {
1801 let mut damage = self.value * damage_modifier;
1802 let precise_damage = damage * precision_mult.unwrap_or(0.0) * (precision_power - 1.0);
1803 match damage_source {
1804 DamageSource::Attack(_) => {
1805 damage += precise_damage;
1807 damage = f32::max(damage - block_damage_decrement, 0.0);
1809 damage *= 1.0 - damage_reduction;
1811
1812 HealthChange {
1813 amount: -damage,
1814 by: damage_contributor,
1815 cause: Some(damage_source),
1816 time,
1817 precise: precision_mult.is_some(),
1818 instance,
1819 }
1820 },
1821 DamageSource::Falling => {
1822 if (damage_reduction - 1.0).abs() < f32::EPSILON {
1824 damage = 0.0;
1825 }
1826 HealthChange {
1827 amount: -damage,
1828 by: None,
1829 cause: Some(damage_source),
1830 time,
1831 precise: false,
1832 instance,
1833 }
1834 },
1835 DamageSource::Buff(_) | DamageSource::Other => HealthChange {
1836 amount: -damage,
1837 by: None,
1838 cause: Some(damage_source),
1839 time,
1840 precise: false,
1841 instance,
1842 },
1843 }
1844 }
1845
1846 pub fn interpolate_damage(&mut self, frac: f32, min: f32) {
1847 let new_damage = min + frac * (self.value - min);
1848 self.value = new_damage;
1849 }
1850}
1851
1852#[derive(Copy, Clone, Debug, PartialEq, Serialize, Deserialize)]
1853pub struct Knockback {
1854 pub direction: KnockbackDir,
1855 pub strength: f32,
1856}
1857
1858#[derive(Copy, Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
1859pub enum KnockbackDir {
1860 Away,
1861 Towards,
1862 Up,
1863 TowardsUp,
1864}
1865
1866impl Knockback {
1867 pub fn calculate_impulse(
1868 self,
1869 dir: Dir,
1870 tgt_char_state: Option<&CharacterState>,
1871 attacker_stats: Option<&Stats>,
1872 ) -> Vec3<f32> {
1873 let from_char = {
1874 let resistant = tgt_char_state
1875 .and_then(|cs| cs.ability_info())
1876 .map(|a| a.ability_meta)
1877 .is_some_and(|a| a.capabilities.contains(Capability::KNOCKBACK_RESISTANT));
1878 if resistant { 0.5 } else { 1.0 }
1879 };
1880 50.0 * self.strength
1883 * from_char
1884 * attacker_stats.map_or(1.0, |s| s.knockback_mult)
1885 * match self.direction {
1886 KnockbackDir::Away => *Dir::slerp(dir, Dir::new(Vec3::unit_z()), 0.5),
1887 KnockbackDir::Towards => *Dir::slerp(-dir, Dir::new(Vec3::unit_z()), 0.5),
1888 KnockbackDir::Up => Vec3::unit_z(),
1889 KnockbackDir::TowardsUp => *Dir::slerp(-dir, Dir::new(Vec3::unit_z()), 0.85),
1890 }
1891 }
1892
1893 #[must_use]
1894 pub fn modify_strength(mut self, power: f32) -> Self {
1895 self.strength *= power;
1896 self
1897 }
1898}
1899
1900#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq)]
1901pub struct CombatBuff {
1902 pub kind: BuffKind,
1903 pub dur_secs: Secs,
1904 pub strength: CombatBuffStrength,
1905 pub chance: f32,
1906}
1907
1908#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq)]
1909pub enum CombatBuffStrength {
1910 DamageFraction(f32),
1911 Value(f32),
1912}
1913
1914impl CombatBuffStrength {
1915 fn to_strength(self, damage: f32, strength_modifier: f32) -> f32 {
1916 match self {
1917 CombatBuffStrength::DamageFraction(f) => damage * f,
1919 CombatBuffStrength::Value(v) => v * strength_modifier,
1920 }
1921 }
1922}
1923
1924impl MulAssign<f32> for CombatBuffStrength {
1925 fn mul_assign(&mut self, mul: f32) { *self = *self * mul; }
1926}
1927
1928impl Mul<f32> for CombatBuffStrength {
1929 type Output = Self;
1930
1931 fn mul(self, mult: f32) -> Self {
1932 match self {
1933 Self::DamageFraction(val) => Self::DamageFraction(val * mult),
1934 Self::Value(val) => Self::Value(val * mult),
1935 }
1936 }
1937}
1938
1939impl CombatBuff {
1940 pub fn to_buff(
1941 self,
1942 time: Time,
1943 attacker_info: (Option<Uid>, Option<&Mass>),
1944 target_info: (Option<&Stats>, Option<&Mass>),
1945 damage: f32,
1946 strength_modifier: f32,
1947 ability_info: Option<AbilityInfo>,
1948 ) -> Buff {
1949 let (attacker_uid, attacker_mass) = attacker_info;
1950 let (target_stats, target_mass) = target_info;
1951 let source = if let Some(uid) = attacker_uid {
1953 BuffSource::Character {
1954 by: uid,
1955 tool_kind: ability_info.and_then(|ai| ai.tool),
1956 }
1957 } else {
1958 BuffSource::Unknown
1959 };
1960 let dest_info = DestInfo {
1961 stats: target_stats,
1962 mass: target_mass,
1963 };
1964 let target_uid = ability_info
1965 .and_then(|ai| ai.input_attr)
1966 .and_then(|ia| ia.target_entity);
1967 Buff::new(
1968 self.kind,
1969 BuffData::new(
1970 self.strength.to_strength(damage, strength_modifier),
1971 Some(self.dur_secs),
1972 ),
1973 Vec::new(),
1974 source,
1975 time,
1976 dest_info,
1977 attacker_mass,
1978 target_uid,
1979 )
1980 }
1981
1982 pub fn to_self_buff(
1983 self,
1984 time: Time,
1985 entity_info: (Option<Uid>, Option<&Stats>, Option<&Mass>),
1986 damage: f32,
1987 strength_modifier: f32,
1988 ability_info: Option<AbilityInfo>,
1989 ) -> Buff {
1990 let (entity_uid, entity_stats, entity_mass) = entity_info;
1991 let source = if let Some(uid) = entity_uid {
1993 BuffSource::Character {
1994 by: uid,
1995 tool_kind: ability_info.and_then(|ai| ai.tool),
1996 }
1997 } else {
1998 BuffSource::Unknown
1999 };
2000 let dest_info = DestInfo {
2001 stats: entity_stats,
2002 mass: entity_mass,
2003 };
2004 let target_uid = ability_info
2005 .and_then(|ai| ai.input_attr)
2006 .and_then(|ia| ia.target_entity);
2007 Buff::new(
2008 self.kind,
2009 BuffData::new(
2010 self.strength.to_strength(damage, strength_modifier),
2011 Some(self.dur_secs),
2012 ),
2013 Vec::new(),
2014 source,
2015 time,
2016 dest_info,
2017 entity_mass,
2018 target_uid,
2019 )
2020 }
2021}
2022
2023#[derive(Copy, Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
2024pub enum ScalingKind {
2025 Linear,
2026 Sqrt,
2027}
2028
2029impl ScalingKind {
2030 pub fn factor(&self, val: f32, norm: f32) -> f32 {
2031 match self {
2032 Self::Linear => val / norm,
2033 Self::Sqrt => (val / norm).sqrt(),
2034 }
2035 }
2036}
2037
2038pub fn get_weapon_kinds(inv: &Inventory) -> (Option<ToolKind>, Option<ToolKind>) {
2039 (
2040 inv.equipped(EquipSlot::ActiveMainhand).and_then(|i| {
2041 if let ItemKind::Tool(tool) = &*i.kind() {
2042 Some(tool.kind)
2043 } else {
2044 None
2045 }
2046 }),
2047 inv.equipped(EquipSlot::ActiveOffhand).and_then(|i| {
2048 if let ItemKind::Tool(tool) = &*i.kind() {
2049 Some(tool.kind)
2050 } else {
2051 None
2052 }
2053 }),
2054 )
2055}
2056
2057fn weapon_rating<T: ItemDesc>(item: &T, _msm: &MaterialStatManifest) -> f32 {
2059 const POWER_WEIGHT: f32 = 2.0;
2060 const SPEED_WEIGHT: f32 = 3.0;
2061 const RANGE_WEIGHT: f32 = 0.8;
2062 const EFFECT_WEIGHT: f32 = 1.5;
2063 const EQUIP_TIME_WEIGHT: f32 = 0.0;
2064 const ENERGY_EFFICIENCY_WEIGHT: f32 = 1.5;
2065 const BUFF_STRENGTH_WEIGHT: f32 = 1.5;
2066
2067 let rating = if let ItemKind::Tool(tool) = &*item.kind() {
2068 let stats = tool.stats(item.stats_durability_multiplier());
2069
2070 let power_rating = stats.power;
2075 let speed_rating = stats.speed - 1.0;
2076 let range_rating = stats.range - 1.0;
2077 let effect_rating = stats.effect_power - 1.0;
2078 let equip_time_rating = 0.5 - stats.equip_time_secs;
2079 let energy_efficiency_rating = stats.energy_efficiency - 1.0;
2080 let buff_strength_rating = stats.buff_strength - 1.0;
2081
2082 power_rating * POWER_WEIGHT
2083 + speed_rating * SPEED_WEIGHT
2084 + range_rating * RANGE_WEIGHT
2085 + effect_rating * EFFECT_WEIGHT
2086 + equip_time_rating * EQUIP_TIME_WEIGHT
2087 + energy_efficiency_rating * ENERGY_EFFICIENCY_WEIGHT
2088 + buff_strength_rating * BUFF_STRENGTH_WEIGHT
2089 } else {
2090 0.0
2091 };
2092 rating.max(0.0)
2093}
2094
2095fn weapon_skills(inventory: &Inventory, skill_set: &SkillSet) -> f32 {
2096 let (mainhand, offhand) = get_weapon_kinds(inventory);
2097 let mainhand_skills = if let Some(tool) = mainhand {
2098 skill_set.earned_sp(SkillGroupKind::Weapon(tool)) as f32
2099 } else {
2100 0.0
2101 };
2102 let offhand_skills = if let Some(tool) = offhand {
2103 skill_set.earned_sp(SkillGroupKind::Weapon(tool)) as f32
2104 } else {
2105 0.0
2106 };
2107 mainhand_skills.max(offhand_skills)
2108}
2109
2110fn get_weapon_rating(inventory: &Inventory, msm: &MaterialStatManifest) -> f32 {
2111 let mainhand_rating = if let Some(item) = inventory.equipped(EquipSlot::ActiveMainhand) {
2112 weapon_rating(item, msm)
2113 } else {
2114 0.0
2115 };
2116
2117 let offhand_rating = if let Some(item) = inventory.equipped(EquipSlot::ActiveOffhand) {
2118 weapon_rating(item, msm)
2119 } else {
2120 0.0
2121 };
2122
2123 mainhand_rating.max(offhand_rating)
2124}
2125
2126pub fn combat_rating(
2127 inventory: &Inventory,
2128 health: &Health,
2129 energy: &Energy,
2130 poise: &Poise,
2131 skill_set: &SkillSet,
2132 body: Body,
2133 msm: &MaterialStatManifest,
2134) -> f32 {
2135 const WEAPON_WEIGHT: f32 = 1.0;
2136 const HEALTH_WEIGHT: f32 = 1.5;
2137 const ENERGY_WEIGHT: f32 = 0.5;
2138 const SKILLS_WEIGHT: f32 = 1.0;
2139 const POISE_WEIGHT: f32 = 0.5;
2140 const PRECISION_WEIGHT: f32 = 0.5;
2141 let health_rating = health.base_max()
2143 / 100.0
2144 / (1.0 - Damage::compute_damage_reduction(None, Some(inventory), None, msm)).max(0.00001);
2145
2146 let energy_rating = (energy.base_max() + compute_max_energy_mod(Some(inventory), msm)) / 100.0
2149 * compute_energy_reward_mod(Some(inventory), msm);
2150
2151 let poise_rating = poise.base_max()
2153 / 100.0
2154 / (1.0 - Poise::compute_poise_damage_reduction(Some(inventory), msm, None, None))
2155 .max(0.00001);
2156
2157 let precision_rating = compute_precision_mult(Some(inventory), msm) / 1.2;
2159
2160 let skills_rating = (skill_set.earned_sp(SkillGroupKind::General) as f32 / 20.0
2163 + weapon_skills(inventory, skill_set) / 10.0)
2164 / 2.0;
2165
2166 let weapon_rating = get_weapon_rating(inventory, msm);
2167
2168 let combined_rating = (health_rating * HEALTH_WEIGHT
2169 + energy_rating * ENERGY_WEIGHT
2170 + poise_rating * POISE_WEIGHT
2171 + precision_rating * PRECISION_WEIGHT
2172 + skills_rating * SKILLS_WEIGHT
2173 + weapon_rating * WEAPON_WEIGHT)
2174 / (HEALTH_WEIGHT
2175 + ENERGY_WEIGHT
2176 + POISE_WEIGHT
2177 + PRECISION_WEIGHT
2178 + SKILLS_WEIGHT
2179 + WEAPON_WEIGHT);
2180
2181 combined_rating * body.combat_multiplier()
2184}
2185
2186pub fn compute_precision_mult(inventory: Option<&Inventory>, msm: &MaterialStatManifest) -> f32 {
2187 1.0 + inventory
2191 .map_or(0.1, |inv| {
2192 inv.equipped_items()
2193 .filter_map(|item| {
2194 if let ItemKind::Armor(armor) = &*item.kind() {
2195 armor
2196 .stats(msm, item.stats_durability_multiplier())
2197 .precision_power
2198 } else {
2199 None
2200 }
2201 })
2202 .fold(0.1, |a, b| a + b)
2203 })
2204 .max(0.0)
2205}
2206
2207pub fn compute_energy_reward_mod(inventory: Option<&Inventory>, msm: &MaterialStatManifest) -> f32 {
2209 inventory.map_or(1.0, |inv| {
2212 inv.equipped_items()
2213 .filter_map(|item| {
2214 if let ItemKind::Armor(armor) = &*item.kind() {
2215 armor
2216 .stats(msm, item.stats_durability_multiplier())
2217 .energy_reward
2218 } else {
2219 None
2220 }
2221 })
2222 .fold(1.0, |a, b| a + b)
2223 })
2224}
2225
2226pub fn compute_max_energy_mod(inventory: Option<&Inventory>, msm: &MaterialStatManifest) -> f32 {
2229 inventory.map_or(0.0, |inv| {
2231 inv.equipped_items()
2232 .filter_map(|item| {
2233 if let ItemKind::Armor(armor) = &*item.kind() {
2234 armor
2235 .stats(msm, item.stats_durability_multiplier())
2236 .energy_max
2237 } else {
2238 None
2239 }
2240 })
2241 .sum()
2242 })
2243}
2244
2245pub fn perception_dist_multiplier_from_stealth(
2248 inventory: Option<&Inventory>,
2249 character_state: Option<&CharacterState>,
2250 msm: &MaterialStatManifest,
2251) -> f32 {
2252 const SNEAK_MULTIPLIER: f32 = 0.7;
2253
2254 let item_stealth_multiplier = stealth_multiplier_from_items(inventory, msm);
2255 let is_sneaking = character_state.is_some_and(|state| state.is_stealthy());
2256
2257 let multiplier = item_stealth_multiplier * if is_sneaking { SNEAK_MULTIPLIER } else { 1.0 };
2258
2259 multiplier.clamp(0.0, 1.0)
2260}
2261
2262pub fn compute_stealth(inventory: Option<&Inventory>, msm: &MaterialStatManifest) -> f32 {
2263 inventory.map_or(0.0, |inv| {
2264 inv.equipped_items()
2265 .filter_map(|item| {
2266 if let ItemKind::Armor(armor) = &*item.kind() {
2267 armor.stats(msm, item.stats_durability_multiplier()).stealth
2268 } else {
2269 None
2270 }
2271 })
2272 .sum()
2273 })
2274}
2275
2276pub fn stealth_multiplier_from_items(
2277 inventory: Option<&Inventory>,
2278 msm: &MaterialStatManifest,
2279) -> f32 {
2280 let stealth_sum = compute_stealth(inventory, msm);
2281
2282 (1.0 / (1.0 + stealth_sum)).clamp(0.0, 1.0)
2283}
2284
2285pub fn compute_protection(
2289 inventory: Option<&Inventory>,
2290 msm: &MaterialStatManifest,
2291) -> Option<f32> {
2292 inventory.map_or(Some(0.0), |inv| {
2293 inv.equipped_items()
2294 .filter_map(|item| {
2295 if let ItemKind::Armor(armor) = &*item.kind() {
2296 armor
2297 .stats(msm, item.stats_durability_multiplier())
2298 .protection
2299 } else {
2300 None
2301 }
2302 })
2303 .map(|protection| match protection {
2304 Protection::Normal(protection) => Some(protection),
2305 Protection::Invincible => None,
2306 })
2307 .sum::<Option<f32>>()
2308 })
2309}
2310
2311pub fn compute_poise_resilience(
2315 inventory: Option<&Inventory>,
2316 msm: &MaterialStatManifest,
2317) -> Option<f32> {
2318 inventory.map_or(Some(0.0), |inv| {
2319 inv.equipped_items()
2320 .filter_map(|item| {
2321 if let ItemKind::Armor(armor) = &*item.kind() {
2322 armor
2323 .stats(msm, item.stats_durability_multiplier())
2324 .poise_resilience
2325 } else {
2326 None
2327 }
2328 })
2329 .map(|protection| match protection {
2330 Protection::Normal(protection) => Some(protection),
2331 Protection::Invincible => None,
2332 })
2333 .sum::<Option<f32>>()
2334 })
2335}
2336
2337pub fn precision_mult_from_flank(
2339 attack_dir: Vec3<f32>,
2340 target_ori: Option<&Ori>,
2341 precision_flank_multipliers: FlankMults,
2342 precision_flank_invert: bool,
2343) -> Option<f32> {
2344 let angle = target_ori.map(|t_ori| {
2345 t_ori.look_dir().angle_between(if precision_flank_invert {
2346 -attack_dir
2347 } else {
2348 attack_dir
2349 })
2350 });
2351 match angle {
2352 Some(angle) if angle < FULL_FLANK_ANGLE => Some(
2353 MAX_BACK_FLANK_PRECISION
2354 * if precision_flank_invert {
2355 precision_flank_multipliers.front
2356 } else {
2357 precision_flank_multipliers.back
2358 },
2359 ),
2360 Some(angle) if angle < PARTIAL_FLANK_ANGLE => {
2361 Some(MAX_SIDE_FLANK_PRECISION * precision_flank_multipliers.side)
2362 },
2363 Some(_) | None => None,
2364 }
2365}
2366
2367#[derive(Copy, Clone, Debug, PartialEq, Serialize, Deserialize)]
2368pub struct FlankMults {
2369 pub back: f32,
2370 pub front: f32,
2371 pub side: f32,
2372}
2373
2374impl Default for FlankMults {
2375 fn default() -> Self {
2376 FlankMults {
2377 back: 1.0,
2378 front: 1.0,
2379 side: 1.0,
2380 }
2381 }
2382}
2383
2384pub fn block_strength(inventory: &Inventory, char_state: &CharacterState) -> f32 {
2385 let (ability_block_strength, hand) = match char_state {
2386 CharacterState::BasicBlock(data) => (
2387 data.static_data.block_strength,
2388 data.static_data.ability_info.hand,
2389 ),
2390 CharacterState::RiposteMelee(data) => (
2391 data.static_data.block_strength,
2392 data.static_data.ability_info.hand,
2393 ),
2394 _ => char_state
2395 .ability_info()
2396 .map(|ability| (ability.ability_meta.capabilities, ability.hand))
2397 .map_or((0.0, None), |(capabilities, hand)| {
2398 (
2399 if capabilities.contains(Capability::PARRIES)
2400 || capabilities.contains(Capability::PARRIES_MELEE)
2401 || capabilities.contains(Capability::BLOCKS)
2402 {
2403 FALLBACK_BLOCK_STRENGTH
2404 } else {
2405 0.0
2406 },
2407 hand,
2408 )
2409 }),
2410 };
2411
2412 let tool_block_strength = hand
2413 .and_then(|hand| inventory.equipped(hand.to_equip_slot()))
2414 .map_or(1.0, |item| match &*item.kind() {
2415 ItemKind::Tool(tool) => tool.stats(item.stats_durability_multiplier()).power,
2416 _ => 1.0,
2417 });
2418
2419 ability_block_strength * tool_block_strength
2420}
2421
2422pub fn get_equip_slot_by_block_priority(inventory: Option<&Inventory>) -> EquipSlot {
2423 inventory
2424 .map(get_weapon_kinds)
2425 .map_or(
2426 EquipSlot::ActiveMainhand,
2427 |weapon_kinds| match weapon_kinds {
2428 (Some(mainhand), Some(offhand)) => {
2429 if mainhand.block_priority() >= offhand.block_priority() {
2430 EquipSlot::ActiveMainhand
2431 } else {
2432 EquipSlot::ActiveOffhand
2433 }
2434 },
2435 (Some(_), None) => EquipSlot::ActiveMainhand,
2436 (None, Some(_)) => EquipSlot::ActiveOffhand,
2437 (None, None) => EquipSlot::ActiveMainhand,
2438 },
2439 )
2440}