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