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 => damage > 0.0 && target_health.is_some(),
1554 CombatRequirement::Energy(r) => {
1555 if let (Some(entity), Some(energy)) = (originator_entity, originator_energy) {
1556 let sufficient_energy = energy.current() >= *r;
1557 if sufficient_energy {
1558 emitters.emit(EnergyChangeEvent {
1559 entity,
1560 change: -*r,
1561 reset_rate: false,
1562 });
1563 }
1564
1565 sufficient_energy
1566 } else {
1567 false
1568 }
1569 },
1570 CombatRequirement::Combo(r) => {
1571 if let (Some(entity), Some(combo)) = (originator_entity, originator_combo) {
1572 let sufficient_combo = combo.counter() >= *r;
1573 if sufficient_combo {
1574 emitters.emit(ComboChangeEvent {
1575 entity,
1576 change: -(*r as i32),
1577 });
1578 }
1579
1580 sufficient_combo
1581 } else {
1582 false
1583 }
1584 },
1585 CombatRequirement::TargetHasBuff(buff) => {
1586 target_buffs.is_some_and(|buffs| buffs.contains(*buff))
1587 },
1588 CombatRequirement::TargetPoised => target_char_state.is_some_and(|cs| cs.is_stunned()),
1589 CombatRequirement::BehindTarget => {
1590 if let Some(ori) = target_ori {
1591 ori.look_vec().angle_between(dir.with_z(0.0)) < BEHIND_TARGET_ANGLE
1592 } else {
1593 false
1594 }
1595 },
1596 CombatRequirement::TargetBlocking => target_char_state
1597 .zip(attack_source)
1598 .is_some_and(|(cs, attack)| cs.is_block(attack) || cs.is_parry(attack)),
1599 CombatRequirement::TargetUnwielded => {
1600 target_char_state.is_some_and(|cs| !cs.is_wield())
1601 },
1602 CombatRequirement::AttackSource(source) => attack_source == Some(*source),
1603 CombatRequirement::AttackInput(input) => {
1604 ability_info.is_some_and(|ai| ai.input == *input)
1605 },
1606 CombatRequirement::Attacker(uid) => Some(*uid) == attacker,
1607 CombatRequirement::Target(uid) => Some(*uid) == target_uid,
1608 CombatRequirement::StageSection(s) => {
1609 Some(*s) == target_char_state.and_then(|cs| cs.stage_section())
1610 },
1611 }
1612 }
1613}
1614
1615#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq)]
1616pub enum CombatModification {
1617 RangeWeakening {
1620 start_dist: f32,
1621 end_dist: f32,
1622 min_str: f32,
1623 },
1624}
1625
1626impl CombatModification {
1627 pub fn apply_mod(
1628 &self,
1629 attacker_pos: Option<Vec3<f32>>,
1630 target_pos: Option<Vec3<f32>>,
1631 strength_mod: &mut f32,
1632 ) {
1633 match self {
1634 Self::RangeWeakening {
1635 start_dist,
1636 end_dist,
1637 min_str,
1638 } => {
1639 if let Some((attacker_pos, target_pos)) = attacker_pos.zip(target_pos) {
1640 let dist = attacker_pos.distance(target_pos);
1641 let gradient = (*min_str - 1.0) / (end_dist - start_dist).max(0.1);
1643 let intercept = 1.0 - gradient * start_dist;
1645 let strength = (gradient * dist + intercept).clamp(*min_str, 1.0);
1647 *strength_mod *= strength;
1648 }
1649 },
1650 }
1651 }
1652}
1653
1654#[derive(Clone, Debug, PartialEq)]
1656pub struct RiderEffects(pub Vec<BuffEffect>);
1657
1658impl specs::Component for RiderEffects {
1659 type Storage = specs::DenseVecStorage<RiderEffects>;
1660}
1661
1662#[derive(Clone, Debug, PartialEq)]
1663pub struct DeathEffects(pub Vec<StatEffect>);
1666
1667impl specs::Component for DeathEffects {
1668 type Storage = specs::DenseVecStorage<DeathEffects>;
1669}
1670
1671#[derive(Clone, Copy, Debug, Hash, Eq, PartialEq, Serialize, Deserialize)]
1672pub enum DamageContributor {
1673 Solo(Uid),
1674 Group { entity_uid: Uid, group: Group },
1675}
1676
1677impl DamageContributor {
1678 pub fn new(uid: Uid, group: Option<Group>) -> Self {
1679 if let Some(group) = group {
1680 DamageContributor::Group {
1681 entity_uid: uid,
1682 group,
1683 }
1684 } else {
1685 DamageContributor::Solo(uid)
1686 }
1687 }
1688
1689 pub fn uid(&self) -> Uid {
1690 match self {
1691 DamageContributor::Solo(uid) => *uid,
1692 DamageContributor::Group {
1693 entity_uid,
1694 group: _,
1695 } => *entity_uid,
1696 }
1697 }
1698}
1699
1700impl From<AttackerInfo<'_>> for DamageContributor {
1701 fn from(attacker_info: AttackerInfo) -> Self {
1702 DamageContributor::new(attacker_info.uid, attacker_info.group.copied())
1703 }
1704}
1705
1706#[derive(Copy, Clone, Debug, Hash, PartialEq, Eq, Serialize, Deserialize)]
1707pub enum DamageSource {
1708 Buff(BuffKind),
1709 Attack(AttackSource),
1710 Falling,
1711 Other,
1712}
1713
1714impl From<AttackSource> for DamageSource {
1715 fn from(attack: AttackSource) -> Self { DamageSource::Attack(attack) }
1716}
1717
1718#[derive(Copy, Clone, Debug, Hash, PartialEq, Eq, Serialize, Deserialize)]
1720pub enum DamageKind {
1721 Piercing,
1723 Slashing,
1726 Crushing,
1728 Energy,
1731}
1732
1733const PIERCING_PENETRATION_FRACTION: f32 = 0.75;
1734const SLASHING_ENERGY_FRACTION: f32 = 0.5;
1735const CRUSHING_POISE_FRACTION: f32 = 1.0;
1736
1737#[derive(Copy, Clone, Debug, PartialEq, Serialize, Deserialize)]
1738#[serde(deny_unknown_fields)]
1739pub struct Damage {
1740 pub kind: DamageKind,
1741 pub value: f32,
1742}
1743
1744impl Damage {
1745 pub fn compute_damage_reduction(
1747 damage: Option<Self>,
1748 inventory: Option<&Inventory>,
1749 stats: Option<&Stats>,
1750 msm: &MaterialStatManifest,
1751 ) -> f32 {
1752 let protection = compute_protection(inventory, msm);
1753
1754 let penetration = if let Some(damage) = damage {
1755 if let DamageKind::Piercing = damage.kind {
1756 (damage.value * PIERCING_PENETRATION_FRACTION)
1757 .clamp(0.0, protection.unwrap_or(0.0).max(0.0))
1758 } else {
1759 0.0
1760 }
1761 } else {
1762 0.0
1763 };
1764
1765 let protection = protection.map(|p| p - penetration);
1766
1767 const FIFTY_PERCENT_DR_THRESHOLD: f32 = 60.0;
1768
1769 let inventory_dr = match protection {
1770 Some(dr) => dr / (FIFTY_PERCENT_DR_THRESHOLD + dr.abs()),
1771 None => 1.0,
1772 };
1773
1774 let stats_dr = if let Some(stats) = stats {
1775 stats.damage_reduction.modifier()
1776 } else {
1777 0.0
1778 };
1779 if protection.is_none() || stats_dr >= 1.0 {
1781 1.0
1782 } else {
1783 1.0 - (1.0 - inventory_dr) * (1.0 - stats_dr)
1784 }
1785 }
1786
1787 pub fn calculate_health_change(
1788 self,
1789 damage_reduction: f32,
1790 block_damage_decrement: f32,
1791 damage_contributor: Option<DamageContributor>,
1792 precision_mult: Option<f32>,
1793 precision_power: f32,
1794 damage_modifier: f32,
1795 time: Time,
1796 instance: u64,
1797 damage_source: DamageSource,
1798 ) -> HealthChange {
1799 let mut damage = self.value * damage_modifier;
1800 let precise_damage = damage * precision_mult.unwrap_or(0.0) * (precision_power - 1.0);
1801 match damage_source {
1802 DamageSource::Attack(_) => {
1803 damage += precise_damage;
1805 damage = f32::max(damage - block_damage_decrement, 0.0);
1807 damage *= 1.0 - damage_reduction;
1809
1810 HealthChange {
1811 amount: -damage,
1812 by: damage_contributor,
1813 cause: Some(damage_source),
1814 time,
1815 precise: precision_mult.is_some(),
1816 instance,
1817 }
1818 },
1819 DamageSource::Falling => {
1820 if (damage_reduction - 1.0).abs() < f32::EPSILON {
1822 damage = 0.0;
1823 }
1824 HealthChange {
1825 amount: -damage,
1826 by: None,
1827 cause: Some(damage_source),
1828 time,
1829 precise: false,
1830 instance,
1831 }
1832 },
1833 DamageSource::Buff(_) | DamageSource::Other => HealthChange {
1834 amount: -damage,
1835 by: None,
1836 cause: Some(damage_source),
1837 time,
1838 precise: false,
1839 instance,
1840 },
1841 }
1842 }
1843
1844 pub fn interpolate_damage(&mut self, frac: f32, min: f32) {
1845 let new_damage = min + frac * (self.value - min);
1846 self.value = new_damage;
1847 }
1848}
1849
1850#[derive(Copy, Clone, Debug, PartialEq, Serialize, Deserialize)]
1851pub struct Knockback {
1852 pub direction: KnockbackDir,
1853 pub strength: f32,
1854}
1855
1856#[derive(Copy, Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
1857pub enum KnockbackDir {
1858 Away,
1859 Towards,
1860 Up,
1861 TowardsUp,
1862}
1863
1864impl Knockback {
1865 pub fn calculate_impulse(
1866 self,
1867 dir: Dir,
1868 tgt_char_state: Option<&CharacterState>,
1869 attacker_stats: Option<&Stats>,
1870 ) -> Vec3<f32> {
1871 let from_char = {
1872 let resistant = tgt_char_state
1873 .and_then(|cs| cs.ability_info())
1874 .map(|a| a.ability_meta)
1875 .is_some_and(|a| a.capabilities.contains(Capability::KNOCKBACK_RESISTANT));
1876 if resistant { 0.5 } else { 1.0 }
1877 };
1878 50.0 * self.strength
1881 * from_char
1882 * attacker_stats.map_or(1.0, |s| s.knockback_mult)
1883 * match self.direction {
1884 KnockbackDir::Away => *Dir::slerp(dir, Dir::new(Vec3::unit_z()), 0.5),
1885 KnockbackDir::Towards => *Dir::slerp(-dir, Dir::new(Vec3::unit_z()), 0.5),
1886 KnockbackDir::Up => Vec3::unit_z(),
1887 KnockbackDir::TowardsUp => *Dir::slerp(-dir, Dir::new(Vec3::unit_z()), 0.85),
1888 }
1889 }
1890
1891 #[must_use]
1892 pub fn modify_strength(mut self, power: f32) -> Self {
1893 self.strength *= power;
1894 self
1895 }
1896}
1897
1898#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq)]
1899pub struct CombatBuff {
1900 pub kind: BuffKind,
1901 pub dur_secs: Secs,
1902 pub strength: CombatBuffStrength,
1903 pub chance: f32,
1904}
1905
1906#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq)]
1907pub enum CombatBuffStrength {
1908 DamageFraction(f32),
1909 Value(f32),
1910}
1911
1912impl CombatBuffStrength {
1913 fn to_strength(self, damage: f32, strength_modifier: f32) -> f32 {
1914 match self {
1915 CombatBuffStrength::DamageFraction(f) => damage * f,
1917 CombatBuffStrength::Value(v) => v * strength_modifier,
1918 }
1919 }
1920}
1921
1922impl MulAssign<f32> for CombatBuffStrength {
1923 fn mul_assign(&mut self, mul: f32) { *self = *self * mul; }
1924}
1925
1926impl Mul<f32> for CombatBuffStrength {
1927 type Output = Self;
1928
1929 fn mul(self, mult: f32) -> Self {
1930 match self {
1931 Self::DamageFraction(val) => Self::DamageFraction(val * mult),
1932 Self::Value(val) => Self::Value(val * mult),
1933 }
1934 }
1935}
1936
1937impl CombatBuff {
1938 pub fn to_buff(
1939 self,
1940 time: Time,
1941 attacker_info: (Option<Uid>, Option<&Mass>),
1942 target_info: (Option<&Stats>, Option<&Mass>),
1943 damage: f32,
1944 strength_modifier: f32,
1945 ability_info: Option<AbilityInfo>,
1946 ) -> Buff {
1947 let (attacker_uid, attacker_mass) = attacker_info;
1948 let (target_stats, target_mass) = target_info;
1949 let source = if let Some(uid) = attacker_uid {
1951 BuffSource::Character {
1952 by: uid,
1953 tool_kind: ability_info.and_then(|ai| ai.tool),
1954 }
1955 } else {
1956 BuffSource::Unknown
1957 };
1958 let dest_info = DestInfo {
1959 stats: target_stats,
1960 mass: target_mass,
1961 };
1962 let target_uid = ability_info
1963 .and_then(|ai| ai.input_attr)
1964 .and_then(|ia| ia.target_entity);
1965 Buff::new(
1966 self.kind,
1967 BuffData::new(
1968 self.strength.to_strength(damage, strength_modifier),
1969 Some(self.dur_secs),
1970 ),
1971 Vec::new(),
1972 source,
1973 time,
1974 dest_info,
1975 attacker_mass,
1976 target_uid,
1977 )
1978 }
1979
1980 pub fn to_self_buff(
1981 self,
1982 time: Time,
1983 entity_info: (Option<Uid>, Option<&Stats>, Option<&Mass>),
1984 damage: f32,
1985 strength_modifier: f32,
1986 ability_info: Option<AbilityInfo>,
1987 ) -> Buff {
1988 let (entity_uid, entity_stats, entity_mass) = entity_info;
1989 let source = if let Some(uid) = entity_uid {
1991 BuffSource::Character {
1992 by: uid,
1993 tool_kind: ability_info.and_then(|ai| ai.tool),
1994 }
1995 } else {
1996 BuffSource::Unknown
1997 };
1998 let dest_info = DestInfo {
1999 stats: entity_stats,
2000 mass: entity_mass,
2001 };
2002 let target_uid = ability_info
2003 .and_then(|ai| ai.input_attr)
2004 .and_then(|ia| ia.target_entity);
2005 Buff::new(
2006 self.kind,
2007 BuffData::new(
2008 self.strength.to_strength(damage, strength_modifier),
2009 Some(self.dur_secs),
2010 ),
2011 Vec::new(),
2012 source,
2013 time,
2014 dest_info,
2015 entity_mass,
2016 target_uid,
2017 )
2018 }
2019}
2020
2021#[derive(Copy, Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
2022pub enum ScalingKind {
2023 Linear,
2024 Sqrt,
2025}
2026
2027impl ScalingKind {
2028 pub fn factor(&self, val: f32, norm: f32) -> f32 {
2029 match self {
2030 Self::Linear => val / norm,
2031 Self::Sqrt => (val / norm).sqrt(),
2032 }
2033 }
2034}
2035
2036pub fn get_weapon_kinds(inv: &Inventory) -> (Option<ToolKind>, Option<ToolKind>) {
2037 (
2038 inv.equipped(EquipSlot::ActiveMainhand).and_then(|i| {
2039 if let ItemKind::Tool(tool) = &*i.kind() {
2040 Some(tool.kind)
2041 } else {
2042 None
2043 }
2044 }),
2045 inv.equipped(EquipSlot::ActiveOffhand).and_then(|i| {
2046 if let ItemKind::Tool(tool) = &*i.kind() {
2047 Some(tool.kind)
2048 } else {
2049 None
2050 }
2051 }),
2052 )
2053}
2054
2055fn weapon_rating<T: ItemDesc>(item: &T, _msm: &MaterialStatManifest) -> f32 {
2057 const POWER_WEIGHT: f32 = 2.0;
2058 const SPEED_WEIGHT: f32 = 3.0;
2059 const RANGE_WEIGHT: f32 = 0.8;
2060 const EFFECT_WEIGHT: f32 = 1.5;
2061 const EQUIP_TIME_WEIGHT: f32 = 0.0;
2062 const ENERGY_EFFICIENCY_WEIGHT: f32 = 1.5;
2063 const BUFF_STRENGTH_WEIGHT: f32 = 1.5;
2064
2065 let rating = if let ItemKind::Tool(tool) = &*item.kind() {
2066 let stats = tool.stats(item.stats_durability_multiplier());
2067
2068 let power_rating = stats.power;
2073 let speed_rating = stats.speed - 1.0;
2074 let range_rating = stats.range - 1.0;
2075 let effect_rating = stats.effect_power - 1.0;
2076 let equip_time_rating = 0.5 - stats.equip_time_secs;
2077 let energy_efficiency_rating = stats.energy_efficiency - 1.0;
2078 let buff_strength_rating = stats.buff_strength - 1.0;
2079
2080 power_rating * POWER_WEIGHT
2081 + speed_rating * SPEED_WEIGHT
2082 + range_rating * RANGE_WEIGHT
2083 + effect_rating * EFFECT_WEIGHT
2084 + equip_time_rating * EQUIP_TIME_WEIGHT
2085 + energy_efficiency_rating * ENERGY_EFFICIENCY_WEIGHT
2086 + buff_strength_rating * BUFF_STRENGTH_WEIGHT
2087 } else {
2088 0.0
2089 };
2090 rating.max(0.0)
2091}
2092
2093fn weapon_skills(inventory: &Inventory, skill_set: &SkillSet) -> f32 {
2094 let (mainhand, offhand) = get_weapon_kinds(inventory);
2095 let mainhand_skills = if let Some(tool) = mainhand {
2096 skill_set.earned_sp(SkillGroupKind::Weapon(tool)) as f32
2097 } else {
2098 0.0
2099 };
2100 let offhand_skills = if let Some(tool) = offhand {
2101 skill_set.earned_sp(SkillGroupKind::Weapon(tool)) as f32
2102 } else {
2103 0.0
2104 };
2105 mainhand_skills.max(offhand_skills)
2106}
2107
2108fn get_weapon_rating(inventory: &Inventory, msm: &MaterialStatManifest) -> f32 {
2109 let mainhand_rating = if let Some(item) = inventory.equipped(EquipSlot::ActiveMainhand) {
2110 weapon_rating(item, msm)
2111 } else {
2112 0.0
2113 };
2114
2115 let offhand_rating = if let Some(item) = inventory.equipped(EquipSlot::ActiveOffhand) {
2116 weapon_rating(item, msm)
2117 } else {
2118 0.0
2119 };
2120
2121 mainhand_rating.max(offhand_rating)
2122}
2123
2124pub fn combat_rating(
2125 inventory: &Inventory,
2126 health: &Health,
2127 energy: &Energy,
2128 poise: &Poise,
2129 skill_set: &SkillSet,
2130 body: Body,
2131 msm: &MaterialStatManifest,
2132) -> f32 {
2133 const WEAPON_WEIGHT: f32 = 1.0;
2134 const HEALTH_WEIGHT: f32 = 1.5;
2135 const ENERGY_WEIGHT: f32 = 0.5;
2136 const SKILLS_WEIGHT: f32 = 1.0;
2137 const POISE_WEIGHT: f32 = 0.5;
2138 const PRECISION_WEIGHT: f32 = 0.5;
2139 let health_rating = health.base_max()
2141 / 100.0
2142 / (1.0 - Damage::compute_damage_reduction(None, Some(inventory), None, msm)).max(0.00001);
2143
2144 let energy_rating = (energy.base_max() + compute_max_energy_mod(Some(inventory), msm)) / 100.0
2147 * compute_energy_reward_mod(Some(inventory), msm);
2148
2149 let poise_rating = poise.base_max()
2151 / 100.0
2152 / (1.0 - Poise::compute_poise_damage_reduction(Some(inventory), msm, None, None))
2153 .max(0.00001);
2154
2155 let precision_rating = compute_precision_mult(Some(inventory), msm) / 1.2;
2157
2158 let skills_rating = (skill_set.earned_sp(SkillGroupKind::General) as f32 / 20.0
2161 + weapon_skills(inventory, skill_set) / 10.0)
2162 / 2.0;
2163
2164 let weapon_rating = get_weapon_rating(inventory, msm);
2165
2166 let combined_rating = (health_rating * HEALTH_WEIGHT
2167 + energy_rating * ENERGY_WEIGHT
2168 + poise_rating * POISE_WEIGHT
2169 + precision_rating * PRECISION_WEIGHT
2170 + skills_rating * SKILLS_WEIGHT
2171 + weapon_rating * WEAPON_WEIGHT)
2172 / (HEALTH_WEIGHT
2173 + ENERGY_WEIGHT
2174 + POISE_WEIGHT
2175 + PRECISION_WEIGHT
2176 + SKILLS_WEIGHT
2177 + WEAPON_WEIGHT);
2178
2179 combined_rating * body.combat_multiplier()
2182}
2183
2184pub fn compute_precision_mult(inventory: Option<&Inventory>, msm: &MaterialStatManifest) -> f32 {
2185 1.0 + inventory
2189 .map_or(0.1, |inv| {
2190 inv.equipped_items()
2191 .filter_map(|item| {
2192 if let ItemKind::Armor(armor) = &*item.kind() {
2193 armor
2194 .stats(msm, item.stats_durability_multiplier())
2195 .precision_power
2196 } else {
2197 None
2198 }
2199 })
2200 .fold(0.1, |a, b| a + b)
2201 })
2202 .max(0.0)
2203}
2204
2205pub fn compute_energy_reward_mod(inventory: Option<&Inventory>, msm: &MaterialStatManifest) -> f32 {
2207 inventory.map_or(1.0, |inv| {
2210 inv.equipped_items()
2211 .filter_map(|item| {
2212 if let ItemKind::Armor(armor) = &*item.kind() {
2213 armor
2214 .stats(msm, item.stats_durability_multiplier())
2215 .energy_reward
2216 } else {
2217 None
2218 }
2219 })
2220 .fold(1.0, |a, b| a + b)
2221 })
2222}
2223
2224pub fn compute_max_energy_mod(inventory: Option<&Inventory>, msm: &MaterialStatManifest) -> f32 {
2227 inventory.map_or(0.0, |inv| {
2229 inv.equipped_items()
2230 .filter_map(|item| {
2231 if let ItemKind::Armor(armor) = &*item.kind() {
2232 armor
2233 .stats(msm, item.stats_durability_multiplier())
2234 .energy_max
2235 } else {
2236 None
2237 }
2238 })
2239 .sum()
2240 })
2241}
2242
2243pub fn perception_dist_multiplier_from_stealth(
2246 inventory: Option<&Inventory>,
2247 character_state: Option<&CharacterState>,
2248 msm: &MaterialStatManifest,
2249) -> f32 {
2250 const SNEAK_MULTIPLIER: f32 = 0.7;
2251
2252 let item_stealth_multiplier = stealth_multiplier_from_items(inventory, msm);
2253 let is_sneaking = character_state.is_some_and(|state| state.is_stealthy());
2254
2255 let multiplier = item_stealth_multiplier * if is_sneaking { SNEAK_MULTIPLIER } else { 1.0 };
2256
2257 multiplier.clamp(0.0, 1.0)
2258}
2259
2260pub fn compute_stealth(inventory: Option<&Inventory>, msm: &MaterialStatManifest) -> f32 {
2261 inventory.map_or(0.0, |inv| {
2262 inv.equipped_items()
2263 .filter_map(|item| {
2264 if let ItemKind::Armor(armor) = &*item.kind() {
2265 armor.stats(msm, item.stats_durability_multiplier()).stealth
2266 } else {
2267 None
2268 }
2269 })
2270 .sum()
2271 })
2272}
2273
2274pub fn stealth_multiplier_from_items(
2275 inventory: Option<&Inventory>,
2276 msm: &MaterialStatManifest,
2277) -> f32 {
2278 let stealth_sum = compute_stealth(inventory, msm);
2279
2280 (1.0 / (1.0 + stealth_sum)).clamp(0.0, 1.0)
2281}
2282
2283pub fn compute_protection(
2287 inventory: Option<&Inventory>,
2288 msm: &MaterialStatManifest,
2289) -> Option<f32> {
2290 inventory.map_or(Some(0.0), |inv| {
2291 inv.equipped_items()
2292 .filter_map(|item| {
2293 if let ItemKind::Armor(armor) = &*item.kind() {
2294 armor
2295 .stats(msm, item.stats_durability_multiplier())
2296 .protection
2297 } else {
2298 None
2299 }
2300 })
2301 .map(|protection| match protection {
2302 Protection::Normal(protection) => Some(protection),
2303 Protection::Invincible => None,
2304 })
2305 .sum::<Option<f32>>()
2306 })
2307}
2308
2309pub fn compute_poise_resilience(
2313 inventory: Option<&Inventory>,
2314 msm: &MaterialStatManifest,
2315) -> Option<f32> {
2316 inventory.map_or(Some(0.0), |inv| {
2317 inv.equipped_items()
2318 .filter_map(|item| {
2319 if let ItemKind::Armor(armor) = &*item.kind() {
2320 armor
2321 .stats(msm, item.stats_durability_multiplier())
2322 .poise_resilience
2323 } else {
2324 None
2325 }
2326 })
2327 .map(|protection| match protection {
2328 Protection::Normal(protection) => Some(protection),
2329 Protection::Invincible => None,
2330 })
2331 .sum::<Option<f32>>()
2332 })
2333}
2334
2335pub fn precision_mult_from_flank(
2337 attack_dir: Vec3<f32>,
2338 target_ori: Option<&Ori>,
2339 precision_flank_multipliers: FlankMults,
2340 precision_flank_invert: bool,
2341) -> Option<f32> {
2342 let angle = target_ori.map(|t_ori| {
2343 t_ori.look_dir().angle_between(if precision_flank_invert {
2344 -attack_dir
2345 } else {
2346 attack_dir
2347 })
2348 });
2349 match angle {
2350 Some(angle) if angle < FULL_FLANK_ANGLE => Some(
2351 MAX_BACK_FLANK_PRECISION
2352 * if precision_flank_invert {
2353 precision_flank_multipliers.front
2354 } else {
2355 precision_flank_multipliers.back
2356 },
2357 ),
2358 Some(angle) if angle < PARTIAL_FLANK_ANGLE => {
2359 Some(MAX_SIDE_FLANK_PRECISION * precision_flank_multipliers.side)
2360 },
2361 Some(_) | None => None,
2362 }
2363}
2364
2365#[derive(Copy, Clone, Debug, PartialEq, Serialize, Deserialize)]
2366pub struct FlankMults {
2367 pub back: f32,
2368 pub front: f32,
2369 pub side: f32,
2370}
2371
2372impl Default for FlankMults {
2373 fn default() -> Self {
2374 FlankMults {
2375 back: 1.0,
2376 front: 1.0,
2377 side: 1.0,
2378 }
2379 }
2380}
2381
2382pub fn block_strength(inventory: &Inventory, char_state: &CharacterState) -> f32 {
2383 let (ability_block_strength, hand) = match char_state {
2384 CharacterState::BasicBlock(data) => (
2385 data.static_data.block_strength,
2386 data.static_data.ability_info.hand,
2387 ),
2388 CharacterState::RiposteMelee(data) => (
2389 data.static_data.block_strength,
2390 data.static_data.ability_info.hand,
2391 ),
2392 _ => char_state
2393 .ability_info()
2394 .map(|ability| (ability.ability_meta.capabilities, ability.hand))
2395 .map_or((0.0, None), |(capabilities, hand)| {
2396 (
2397 if capabilities.contains(Capability::PARRIES)
2398 || capabilities.contains(Capability::PARRIES_MELEE)
2399 || capabilities.contains(Capability::BLOCKS)
2400 {
2401 FALLBACK_BLOCK_STRENGTH
2402 } else {
2403 0.0
2404 },
2405 hand,
2406 )
2407 }),
2408 };
2409
2410 let tool_block_strength = hand
2411 .and_then(|hand| inventory.equipped(hand.to_equip_slot()))
2412 .map_or(1.0, |item| match &*item.kind() {
2413 ItemKind::Tool(tool) => tool.stats(item.stats_durability_multiplier()).power,
2414 _ => 1.0,
2415 });
2416
2417 ability_block_strength * tool_block_strength
2418}
2419
2420pub fn get_equip_slot_by_block_priority(inventory: Option<&Inventory>) -> EquipSlot {
2421 inventory
2422 .map(get_weapon_kinds)
2423 .map_or(
2424 EquipSlot::ActiveMainhand,
2425 |weapon_kinds| match weapon_kinds {
2426 (Some(mainhand), Some(offhand)) => {
2427 if mainhand.block_priority() >= offhand.block_priority() {
2428 EquipSlot::ActiveMainhand
2429 } else {
2430 EquipSlot::ActiveOffhand
2431 }
2432 },
2433 (Some(_), None) => EquipSlot::ActiveMainhand,
2434 (None, Some(_)) => EquipSlot::ActiveOffhand,
2435 (None, None) => EquipSlot::ActiveMainhand,
2436 },
2437 )
2438}