veloren/common/src/combat.rs

1078 lines
36 KiB
Rust
Raw Normal View History

use crate::comp::buff::{Buff, BuffChange, BuffData, BuffKind, BuffSource};
2021-02-17 13:03:20 +00:00
#[cfg(not(target_arch = "wasm32"))]
use crate::{
comp::{
inventory::{
item::{
armor::Protection,
tool::{self, Tool, ToolKind},
2021-03-30 20:15:37 +00:00
Item, ItemDesc, ItemKind, MaterialStatManifest,
},
slot::EquipSlot,
},
skills::SkillGroupKind,
Alignment, Body, CharacterState, Combo, Energy, Health, HealthChange, Inventory, Ori,
Player, Poise, SkillSet, Stats,
},
2021-01-26 03:53:52 +00:00
event::ServerEvent,
2021-04-04 03:04:02 +00:00
outcome::Outcome,
states::utils::StageSection,
2021-08-27 18:49:58 +00:00
uid::{Uid, UidAllocator},
2020-10-18 18:21:58 +00:00
util::Dir,
};
2021-02-17 13:03:20 +00:00
#[cfg(not(target_arch = "wasm32"))]
2021-01-26 03:53:52 +00:00
use rand::{thread_rng, Rng};
2021-02-17 13:03:20 +00:00
use serde::{Deserialize, Serialize};
2021-02-17 13:03:20 +00:00
use crate::{comp::Group, resources::Time};
2021-02-17 13:03:20 +00:00
#[cfg(not(target_arch = "wasm32"))]
2021-08-27 18:49:58 +00:00
use specs::{saveload::MarkerAllocator, Entity as EcsEntity, ReadStorage};
2021-02-17 13:03:20 +00:00
#[cfg(not(target_arch = "wasm32"))]
2021-07-10 16:04:12 +00:00
use std::{ops::MulAssign, time::Duration};
2021-02-17 13:03:20 +00:00
#[cfg(not(target_arch = "wasm32"))] use vek::*;
2021-02-17 13:03:20 +00:00
#[cfg(not(target_arch = "wasm32"))]
#[derive(Copy, Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
pub enum GroupTarget {
InGroup,
OutOfGroup,
}
2021-04-10 03:40:20 +00:00
#[derive(Copy, Clone, Debug, Serialize, Deserialize)]
pub enum AttackSource {
Melee,
Projectile,
Beam,
Shockwave,
Explosion,
}
2021-02-17 13:03:20 +00:00
#[cfg(not(target_arch = "wasm32"))]
2021-02-02 18:02:40 +00:00
#[derive(Copy, Clone)]
pub struct AttackerInfo<'a> {
pub entity: EcsEntity,
pub uid: Uid,
pub group: Option<&'a Group>,
2021-02-02 18:02:40 +00:00
pub energy: Option<&'a Energy>,
pub combo: Option<&'a Combo>,
2021-05-19 01:42:14 +00:00
pub inventory: Option<&'a Inventory>,
2021-02-02 18:02:40 +00:00
}
#[cfg(not(target_arch = "wasm32"))]
pub struct TargetInfo<'a> {
pub entity: EcsEntity,
pub uid: Uid,
pub inventory: Option<&'a Inventory>,
pub stats: Option<&'a Stats>,
pub health: Option<&'a Health>,
2021-04-04 03:04:02 +00:00
pub pos: Vec3<f32>,
2021-04-10 03:40:20 +00:00
pub ori: Option<&'a Ori>,
pub char_state: Option<&'a CharacterState>,
}
#[derive(Clone, Copy)]
pub struct AttackOptions {
pub target_dodging: bool,
pub may_harm: bool,
pub target_group: GroupTarget,
}
2021-02-17 13:03:20 +00:00
#[cfg(not(target_arch = "wasm32"))]
#[derive(Clone, Debug, Serialize, Deserialize)] // TODO: Yeet clone derive
2021-01-26 00:01:07 +00:00
pub struct Attack {
2021-02-02 18:02:40 +00:00
damages: Vec<AttackDamage>,
effects: Vec<AttackEffect>,
2021-01-26 03:53:52 +00:00
crit_chance: f32,
crit_multiplier: f32,
2021-01-26 00:01:07 +00:00
}
2021-02-17 13:03:20 +00:00
#[cfg(not(target_arch = "wasm32"))]
2021-01-26 00:01:07 +00:00
impl Default for Attack {
fn default() -> Self {
Self {
damages: Vec::new(),
effects: Vec::new(),
crit_chance: 0.0,
crit_multiplier: 1.0,
}
}
}
2021-02-17 13:03:20 +00:00
#[cfg(not(target_arch = "wasm32"))]
2021-01-26 00:01:07 +00:00
impl Attack {
2021-02-02 18:02:40 +00:00
pub fn with_damage(mut self, damage: AttackDamage) -> Self {
2021-01-26 00:01:07 +00:00
self.damages.push(damage);
self
}
2021-02-02 18:02:40 +00:00
pub fn with_effect(mut self, effect: AttackEffect) -> Self {
2021-01-26 00:01:07 +00:00
self.effects.push(effect);
self
}
2021-02-02 18:02:40 +00:00
pub fn with_crit(mut self, crit_chance: f32, crit_multiplier: f32) -> Self {
self.crit_chance = crit_chance;
self.crit_multiplier = crit_multiplier;
2021-01-26 00:01:07 +00:00
self
}
2021-02-27 19:55:06 +00:00
pub fn with_combo_increment(self) -> Self {
self.with_effect(
AttackEffect::new(None, CombatEffect::Combo(1))
.with_requirement(CombatRequirement::AnyDamage),
)
}
2021-02-02 18:02:40 +00:00
pub fn effects(&self) -> impl Iterator<Item = &AttackEffect> { self.effects.iter() }
pub fn compute_damage_reduction(
target: &TargetInfo,
source: AttackSource,
dir: Dir,
kind: DamageKind,
mut emit: impl FnMut(ServerEvent),
mut emit_outcome: impl FnMut(Outcome),
) -> f32 {
let damage_reduction =
Damage::compute_damage_reduction(target.inventory, target.stats, Some(kind));
2021-04-10 03:40:20 +00:00
let block_reduction = match source {
AttackSource::Melee => {
if let (Some(CharacterState::BasicBlock(data)), Some(ori)) =
(target.char_state, target.ori)
{
if ori.look_vec().angle_between(-*dir) < data.static_data.max_angle.to_radians()
{
let parry = matches!(data.stage_section, StageSection::Buildup);
emit_outcome(Outcome::Block {
parry,
pos: target.pos,
uid: target.uid,
});
emit(ServerEvent::Parry {
entity: target.entity,
energy_cost: data.static_data.energy_cost,
});
if parry {
2021-04-13 05:46:42 +00:00
1.0
} else {
data.static_data.block_strength
}
2021-04-10 03:40:20 +00:00
} else {
0.0
}
} else {
0.0
}
},
_ => 0.0,
};
1.0 - (1.0 - damage_reduction) * (1.0 - block_reduction)
}
#[allow(clippy::too_many_arguments, clippy::redundant_closure)]
2021-01-26 03:53:52 +00:00
pub fn apply_attack(
&self,
attacker: Option<AttackerInfo>,
target: TargetInfo,
2021-01-26 03:53:52 +00:00
dir: Dir,
options: AttackOptions,
// Currently strength_modifier just modifies damage,
// maybe look into modifying strength of other effects?
strength_modifier: f32,
2021-04-10 03:40:20 +00:00
attack_source: AttackSource,
time: Time,
2021-02-02 18:02:40 +00:00
mut emit: impl FnMut(ServerEvent),
2021-04-04 03:04:02 +00:00
mut emit_outcome: impl FnMut(Outcome),
) -> bool {
let AttackOptions {
target_dodging,
may_harm,
target_group,
} = options;
2021-07-27 21:57:48 +00:00
// target == OutOfGroup is basic heuristic that this
// "attack" has negative effects.
//
// so if target dodges this "attack" or we don't want to harm target,
// it should avoid such "damage" or effect
let avoid_damage = |attack_damage: &AttackDamage| {
matches!(attack_damage.target, Some(GroupTarget::OutOfGroup))
&& (target_dodging || !may_harm)
};
let avoid_effect = |attack_effect: &AttackEffect| {
matches!(attack_effect.target, Some(GroupTarget::OutOfGroup))
&& (target_dodging || !may_harm)
};
2021-01-26 03:53:52 +00:00
let is_crit = thread_rng().gen::<f32>() < self.crit_chance;
let mut is_applied = false;
2021-01-26 03:53:52 +00:00
let mut accumulated_damage = 0.0;
for damage in self
.damages
.iter()
.filter(|d| d.target.map_or(true, |t| t == target_group))
.filter(|d| !avoid_damage(d))
2021-01-26 03:53:52 +00:00
{
is_applied = true;
let damage_reduction = Attack::compute_damage_reduction(
&target,
attack_source,
dir,
damage.damage.kind,
|e| emit(e),
|o| emit_outcome(o),
);
2021-02-02 18:02:40 +00:00
let change = damage.damage.calculate_health_change(
damage_reduction,
attacker.map(|x| x.into()),
2021-01-28 01:44:49 +00:00
is_crit,
self.crit_multiplier,
strength_modifier,
time,
2021-01-28 01:44:49 +00:00
);
let applied_damage = -change.amount;
accumulated_damage += applied_damage;
2021-04-04 03:04:02 +00:00
emit_outcome(Outcome::Damage { pos: target.pos });
2021-09-10 19:20:14 +00:00
if change.amount.abs() > Health::HEALTH_EPSILON {
emit(ServerEvent::HealthChange {
entity: target.entity,
2021-01-26 03:53:52 +00:00
change,
});
for effect in damage.effects.iter() {
match effect {
2021-02-02 18:02:40 +00:00
CombatEffect::Knockback(kb) => {
let impulse = kb.calculate_impulse(dir) * strength_modifier;
2021-01-26 03:53:52 +00:00
if !impulse.is_approx_zero() {
2021-02-02 18:02:40 +00:00
emit(ServerEvent::Knockback {
entity: target.entity,
2021-01-26 03:53:52 +00:00
impulse,
});
}
},
2021-02-02 18:02:40 +00:00
CombatEffect::EnergyReward(ec) => {
2021-05-19 01:42:14 +00:00
if let Some(attacker) = attacker {
2021-02-02 18:02:40 +00:00
emit(ServerEvent::EnergyChange {
2021-05-19 01:42:14 +00:00
entity: attacker.entity,
change: *ec
* compute_energy_reward_mod(attacker.inventory)
* strength_modifier,
});
}
2021-01-26 17:58:52 +00:00
},
2021-02-02 18:02:40 +00:00
CombatEffect::Buff(b) => {
2021-01-28 01:44:49 +00:00
if thread_rng().gen::<f32>() < b.chance {
2021-02-02 18:02:40 +00:00
emit(ServerEvent::Buff {
entity: target.entity,
buff_change: BuffChange::Add(b.to_buff(
attacker.map(|a| a.uid),
applied_damage,
strength_modifier,
)),
2021-01-28 01:44:49 +00:00
});
}
},
2021-02-02 18:02:40 +00:00
CombatEffect::Lifesteal(l) => {
// Not modified by strength_modifer as damage already is
if let Some(attacker_entity) = attacker.map(|a| a.entity) {
let change = HealthChange {
amount: applied_damage * l,
by: attacker.map(|a| a.into()),
cause: None,
time,
};
2021-09-10 19:20:14 +00:00
if change.amount.abs() > Health::HEALTH_EPSILON {
emit(ServerEvent::HealthChange {
2021-02-02 18:02:40 +00:00
entity: attacker_entity,
change,
});
}
}
},
CombatEffect::Poise(p) => {
let change = -Poise::apply_poise_reduction(*p, target.inventory)
* strength_modifier;
if change.abs() > Poise::POISE_EPSILON {
2021-02-02 18:02:40 +00:00
emit(ServerEvent::PoiseChange {
entity: target.entity,
change,
2021-02-02 18:02:40 +00:00
kb_dir: *dir,
});
}
2021-01-29 01:55:43 +00:00
},
2021-02-02 18:02:40 +00:00
CombatEffect::Heal(h) => {
2021-01-30 05:03:23 +00:00
let change = HealthChange {
amount: *h * strength_modifier,
by: attacker.map(|a| a.into()),
cause: None,
time,
2021-01-30 05:03:23 +00:00
};
2021-09-10 19:20:14 +00:00
if change.amount.abs() > Health::HEALTH_EPSILON {
emit(ServerEvent::HealthChange {
entity: target.entity,
2021-02-02 18:02:40 +00:00
change,
});
}
2021-01-30 05:03:23 +00:00
},
2021-02-27 19:55:06 +00:00
CombatEffect::Combo(c) => {
// Not affected by strength modifier as integer
if let Some(attacker_entity) = attacker.map(|a| a.entity) {
2021-02-27 19:55:06 +00:00
emit(ServerEvent::ComboChange {
entity: attacker_entity,
change: *c,
});
}
},
2021-01-26 03:53:52 +00:00
}
}
}
}
for effect in self
.effects
.iter()
.filter(|e| e.target.map_or(true, |t| t == target_group))
.filter(|e| !avoid_effect(e))
{
if effect.requirements.iter().all(|req| match req {
CombatRequirement::AnyDamage => accumulated_damage > 0.0 && target.health.is_some(),
CombatRequirement::Energy(r) => {
2021-02-02 18:02:40 +00:00
if let Some(AttackerInfo {
entity,
energy: Some(e),
..
}) = attacker
2021-02-02 18:02:40 +00:00
{
let sufficient_energy = e.current() >= *r;
2021-02-02 18:02:40 +00:00
if sufficient_energy {
emit(ServerEvent::EnergyChange {
entity,
change: -*r,
});
}
2021-02-02 18:02:40 +00:00
sufficient_energy
} else {
false
}
},
CombatRequirement::Combo(r) => {
if let Some(AttackerInfo {
entity,
combo: Some(c),
..
}) = attacker
{
let sufficient_combo = c.counter() >= *r;
if sufficient_combo {
emit(ServerEvent::ComboChange {
entity,
change: -(*r as i32),
});
}
sufficient_combo
} else {
false
}
},
}) {
is_applied = true;
match effect.effect {
2021-02-02 18:02:40 +00:00
CombatEffect::Knockback(kb) => {
let impulse = kb.calculate_impulse(dir) * strength_modifier;
if !impulse.is_approx_zero() {
2021-02-02 18:02:40 +00:00
emit(ServerEvent::Knockback {
entity: target.entity,
impulse,
});
}
},
2021-02-02 18:02:40 +00:00
CombatEffect::EnergyReward(ec) => {
2021-05-19 01:42:14 +00:00
if let Some(attacker) = attacker {
2021-02-02 18:02:40 +00:00
emit(ServerEvent::EnergyChange {
2021-05-19 01:42:14 +00:00
entity: attacker.entity,
change: ec
* compute_energy_reward_mod(attacker.inventory)
* strength_modifier,
});
}
},
2021-02-02 18:02:40 +00:00
CombatEffect::Buff(b) => {
if thread_rng().gen::<f32>() < b.chance {
2021-02-02 18:02:40 +00:00
emit(ServerEvent::Buff {
entity: target.entity,
buff_change: BuffChange::Add(b.to_buff(
attacker.map(|a| a.uid),
accumulated_damage,
strength_modifier,
)),
});
}
},
2021-02-02 18:02:40 +00:00
CombatEffect::Lifesteal(l) => {
// Not modified by strength_modifer as damage already is
if let Some(attacker_entity) = attacker.map(|a| a.entity) {
let change = HealthChange {
amount: accumulated_damage * l,
by: attacker.map(|a| a.into()),
cause: None,
time,
};
2021-09-10 19:20:14 +00:00
if change.amount.abs() > Health::HEALTH_EPSILON {
emit(ServerEvent::HealthChange {
2021-02-02 18:02:40 +00:00
entity: attacker_entity,
change,
});
}
}
},
CombatEffect::Poise(p) => {
let change =
-Poise::apply_poise_reduction(p, target.inventory) * strength_modifier;
if change.abs() > Poise::POISE_EPSILON {
2021-02-02 18:02:40 +00:00
emit(ServerEvent::PoiseChange {
entity: target.entity,
change,
2021-02-02 18:02:40 +00:00
kb_dir: *dir,
});
}
2021-01-29 01:55:43 +00:00
},
2021-02-02 18:02:40 +00:00
CombatEffect::Heal(h) => {
2021-01-30 05:03:23 +00:00
let change = HealthChange {
amount: h * strength_modifier,
by: attacker.map(|a| a.into()),
cause: None,
time,
2021-01-30 05:03:23 +00:00
};
2021-09-10 19:20:14 +00:00
if change.amount.abs() > Health::HEALTH_EPSILON {
emit(ServerEvent::HealthChange {
entity: target.entity,
2021-02-02 18:02:40 +00:00
change,
});
}
2021-01-30 05:03:23 +00:00
},
2021-02-27 19:55:06 +00:00
CombatEffect::Combo(c) => {
// Not affected by strength modifier as integer
if let Some(attacker_entity) = attacker.map(|a| a.entity) {
2021-02-27 19:55:06 +00:00
emit(ServerEvent::ComboChange {
entity: attacker_entity,
change: c,
});
}
},
}
}
}
// Emits event to handle things that should happen for any successful attack,
// regardless of if the attack had any damages or effects in it
if is_applied {
emit(ServerEvent::EntityAttackedHook {
entity: target.entity,
});
}
is_applied
}
2021-01-26 00:01:07 +00:00
}
2021-08-27 18:49:58 +00:00
/// Function that checks for unintentional PvP between players.
2021-08-27 12:52:52 +00:00
///
2021-08-27 18:49:58 +00:00
/// Returns `false` if attack will create unintentional conflict,
/// e.g. if player with PvE mode will harm pets of other players
/// or other players will do the same to such player.
///
/// If both players have PvP mode enabled, interact with NPC and
/// in any other case, this function will return `true`
// TODO: add parameter for doing self-harm?
pub fn may_harm(
alignments: &ReadStorage<Alignment>,
players: &ReadStorage<Player>,
uid_allocator: &UidAllocator,
attacker: Option<EcsEntity>,
target: EcsEntity,
) -> bool {
// Return owner entity if pet,
// or just return entity back otherwise
let owner_if_pet = |entity| {
let alignment = alignments.get(entity).copied();
if let Some(Alignment::Owned(uid)) = alignment {
// return original entity
// if can't get owner
uid_allocator
.retrieve_entity_internal(uid.into())
.unwrap_or(entity)
} else {
entity
}
};
// Just return ok if attacker is unknown, it's probably
// environment or command.
let attacker = match attacker {
Some(attacker) => attacker,
None => return true,
};
// "Dereference" to owner if this is a pet.
let attacker = owner_if_pet(attacker);
let target = owner_if_pet(target);
// Get player components
let attacker_info = players.get(attacker);
let target_info = players.get(target);
// Return `true` if not players.
attacker_info
.zip(target_info)
.map_or(true, |(a, t)| a.may_harm(t))
2021-07-27 21:57:48 +00:00
}
2021-02-17 13:03:20 +00:00
#[cfg(not(target_arch = "wasm32"))]
#[derive(Clone, Debug, Serialize, Deserialize)]
2021-02-02 18:02:40 +00:00
pub struct AttackDamage {
2021-01-26 00:01:07 +00:00
damage: Damage,
target: Option<GroupTarget>,
2021-02-02 18:02:40 +00:00
effects: Vec<CombatEffect>,
2021-01-26 00:01:07 +00:00
}
2021-02-17 13:03:20 +00:00
#[cfg(not(target_arch = "wasm32"))]
2021-02-02 18:02:40 +00:00
impl AttackDamage {
2021-01-26 00:01:07 +00:00
pub fn new(damage: Damage, target: Option<GroupTarget>) -> Self {
Self {
damage,
target,
effects: Vec::new(),
}
}
2021-02-02 18:02:40 +00:00
pub fn with_effect(mut self, effect: CombatEffect) -> Self {
2021-01-26 00:01:07 +00:00
self.effects.push(effect);
self
}
}
2021-02-17 13:03:20 +00:00
#[cfg(not(target_arch = "wasm32"))]
#[derive(Clone, Debug, Serialize, Deserialize)]
2021-02-02 18:02:40 +00:00
pub struct AttackEffect {
2021-01-26 00:01:07 +00:00
target: Option<GroupTarget>,
2021-02-02 18:02:40 +00:00
effect: CombatEffect,
requirements: Vec<CombatRequirement>,
2021-01-26 00:01:07 +00:00
}
2021-02-17 13:03:20 +00:00
#[cfg(not(target_arch = "wasm32"))]
2021-02-02 18:02:40 +00:00
impl AttackEffect {
pub fn new(target: Option<GroupTarget>, effect: CombatEffect) -> Self {
Self {
target,
effect,
requirements: Vec::new(),
}
}
pub fn with_requirement(mut self, requirement: CombatRequirement) -> Self {
self.requirements.push(requirement);
self
2021-01-26 00:01:07 +00:00
}
2021-02-02 18:02:40 +00:00
pub fn effect(&self) -> &CombatEffect { &self.effect }
2021-01-26 00:01:07 +00:00
}
2021-02-17 13:03:20 +00:00
#[cfg(not(target_arch = "wasm32"))]
#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq)]
2021-02-02 18:02:40 +00:00
pub enum CombatEffect {
2021-01-30 05:03:23 +00:00
Heal(f32),
2021-01-28 01:44:49 +00:00
Buff(CombatBuff),
Knockback(Knockback),
EnergyReward(f32),
2021-01-29 01:55:43 +00:00
Lifesteal(f32),
2021-01-30 04:40:03 +00:00
Poise(f32),
2021-02-27 19:55:06 +00:00
Combo(i32),
2021-01-26 00:01:07 +00:00
}
2021-02-17 13:03:20 +00:00
#[cfg(not(target_arch = "wasm32"))]
#[derive(Clone, Debug, Serialize, Deserialize)]
pub enum CombatRequirement {
AnyDamage,
Energy(f32),
Combo(u32),
}
#[derive(Clone, Copy, Debug, Hash, Eq, PartialEq, Serialize, Deserialize)]
pub enum DamageContributor {
Solo(Uid),
Group { entity_uid: Uid, group: Group },
}
impl DamageContributor {
pub fn new(uid: Uid, group: Option<Group>) -> Self {
if let Some(group) = group {
DamageContributor::Group {
entity_uid: uid,
group,
}
} else {
DamageContributor::Solo(uid)
}
}
pub fn uid(&self) -> Uid {
match self {
DamageContributor::Solo(uid) => *uid,
DamageContributor::Group {
entity_uid,
group: _,
} => *entity_uid,
}
}
}
impl From<AttackerInfo<'_>> for DamageContributor {
fn from(attacker_info: AttackerInfo) -> Self {
DamageContributor::new(attacker_info.uid, attacker_info.group.copied())
}
}
#[derive(Copy, Clone, Debug, Hash, PartialEq, Eq, Serialize, Deserialize)]
pub enum DamageSource {
Buff(BuffKind),
Melee,
Projectile,
Explosion,
Falling,
Shockwave,
Energy,
2020-11-05 01:21:42 +00:00
Other,
}
/// DamageKind for the purpose of differentiating damage reduction
#[derive(Copy, Clone, Debug, Hash, PartialEq, Eq, Serialize, Deserialize)]
pub enum DamageKind {
/// Arrows/Sword dash
Piercing,
/// Swords/axes
Slashing,
/// Hammers
Crushing,
/// Staves/sceptres (TODO: differentiate further once there are more magic
/// weapons)
Energy,
}
2021-02-17 13:03:20 +00:00
#[cfg(not(target_arch = "wasm32"))]
#[derive(Copy, Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct Damage {
pub source: DamageSource,
pub kind: DamageKind,
pub value: f32,
}
2021-02-17 13:03:20 +00:00
#[cfg(not(target_arch = "wasm32"))]
impl Damage {
/// Returns the total damage reduction provided by all equipped items
pub fn compute_damage_reduction(
inventory: Option<&Inventory>,
stats: Option<&Stats>,
kind: Option<DamageKind>,
) -> f32 {
2021-02-28 23:14:59 +00:00
let inventory_dr = if let Some(inventory) = inventory {
let protection = inventory
.equipped_items()
.filter_map(|item| {
if let ItemKind::Armor(armor) = &item.kind() {
Some(armor.protection())
} else {
None
}
})
.map(|protection| match protection {
Protection::Normal(protection) => Some(protection),
Protection::Invincible => None,
})
.sum::<Option<f32>>();
let kind_modifier = if matches!(kind, Some(DamageKind::Piercing)) {
0.75
} else {
1.0
};
let protection = protection.map(|dr| dr * kind_modifier);
const FIFTY_PERCENT_DR_THRESHOLD: f32 = 60.0;
match protection {
Some(dr) => dr / (FIFTY_PERCENT_DR_THRESHOLD + dr.abs()),
None => 1.0,
}
} else {
0.0
};
let stats_dr = if let Some(stats) = stats {
stats.damage_reduction
} else {
0.0
};
2021-02-28 23:14:59 +00:00
1.0 - (1.0 - inventory_dr) * (1.0 - stats_dr)
}
2021-02-02 18:02:40 +00:00
pub fn calculate_health_change(
2021-01-26 03:53:52 +00:00
self,
damage_reduction: f32,
damage_contributor: Option<DamageContributor>,
2021-01-26 03:53:52 +00:00
is_crit: bool,
crit_mult: f32,
damage_modifier: f32,
time: Time,
2021-01-26 03:53:52 +00:00
) -> HealthChange {
let mut damage = self.value * damage_modifier;
let critdamage = if is_crit {
damage * (crit_mult - 1.0)
} else {
0.0
};
match self.source {
2021-06-30 00:13:28 +00:00
DamageSource::Melee
| DamageSource::Projectile
| DamageSource::Explosion
| DamageSource::Shockwave
| DamageSource::Energy => {
// Critical hit
damage += critdamage;
// Armor
damage *= 1.0 - damage_reduction;
2020-12-06 02:29:46 +00:00
HealthChange {
amount: -damage,
by: damage_contributor,
cause: Some(self.source),
time,
2020-12-06 02:29:46 +00:00
}
},
DamageSource::Falling => {
// Armor
if (damage_reduction - 1.0).abs() < f32::EPSILON {
damage = 0.0;
}
2020-12-06 02:29:46 +00:00
HealthChange {
amount: -damage,
by: None,
cause: Some(self.source),
time,
2021-01-15 03:32:12 +00:00
}
},
2021-06-30 00:13:28 +00:00
DamageSource::Buff(_) | DamageSource::Other => HealthChange {
amount: -damage,
by: None,
cause: Some(self.source),
time,
2020-12-06 02:29:46 +00:00
},
}
}
pub fn interpolate_damage(&mut self, frac: f32, min: f32) {
let new_damage = min + frac * (self.value - min);
self.value = new_damage;
}
}
2020-10-18 18:21:58 +00:00
2021-02-17 13:03:20 +00:00
#[cfg(not(target_arch = "wasm32"))]
2020-10-18 18:21:58 +00:00
#[derive(Copy, Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct Knockback {
pub direction: KnockbackDir,
pub strength: f32,
}
2021-02-17 13:03:20 +00:00
#[cfg(not(target_arch = "wasm32"))]
#[derive(Copy, Clone, Debug, PartialEq, Serialize, Deserialize)]
pub enum KnockbackDir {
Away,
Towards,
Up,
TowardsUp,
2020-10-18 18:21:58 +00:00
}
2021-02-17 13:03:20 +00:00
#[cfg(not(target_arch = "wasm32"))]
2020-10-18 18:21:58 +00:00
impl Knockback {
2020-10-27 22:16:17 +00:00
pub fn calculate_impulse(self, dir: Dir) -> Vec3<f32> {
// TEMP until source knockback values have been updated
50.0 * match self.direction {
KnockbackDir::Away => self.strength * *Dir::slerp(dir, Dir::new(Vec3::unit_z()), 0.5),
KnockbackDir::Towards => {
self.strength * *Dir::slerp(-dir, Dir::new(Vec3::unit_z()), 0.5)
2020-10-18 18:21:58 +00:00
},
KnockbackDir::Up => self.strength * Vec3::unit_z(),
KnockbackDir::TowardsUp => {
self.strength * *Dir::slerp(-dir, Dir::new(Vec3::unit_z()), 0.85)
2020-10-18 18:21:58 +00:00
},
}
}
2020-12-24 17:54:00 +00:00
pub fn modify_strength(mut self, power: f32) -> Self {
self.strength *= power;
2020-12-24 17:54:00 +00:00
self
}
2020-10-18 18:21:58 +00:00
}
2021-02-17 13:03:20 +00:00
#[cfg(not(target_arch = "wasm32"))]
#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq)]
2021-01-28 01:44:49 +00:00
pub struct CombatBuff {
pub kind: BuffKind,
pub dur_secs: f32,
pub strength: CombatBuffStrength,
pub chance: f32,
}
2021-02-17 13:03:20 +00:00
#[cfg(not(target_arch = "wasm32"))]
#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq)]
2021-01-28 01:44:49 +00:00
pub enum CombatBuffStrength {
DamageFraction(f32),
Value(f32),
}
2021-02-17 13:03:20 +00:00
#[cfg(not(target_arch = "wasm32"))]
2021-01-28 01:44:49 +00:00
impl CombatBuffStrength {
fn to_strength(self, damage: f32, strength_modifier: f32) -> f32 {
2021-01-28 01:44:49 +00:00
match self {
// Not affected by strength modifier as damage already is
2021-01-28 01:44:49 +00:00
CombatBuffStrength::DamageFraction(f) => damage * f,
CombatBuffStrength::Value(v) => v * strength_modifier,
2021-01-28 01:44:49 +00:00
}
}
}
2021-07-10 16:04:12 +00:00
impl MulAssign<f32> for CombatBuffStrength {
fn mul_assign(&mut self, mul: f32) {
match self {
Self::DamageFraction(ref mut val) | Self::Value(ref mut val) => {
*val *= mul;
},
}
}
}
2021-02-17 13:03:20 +00:00
#[cfg(not(target_arch = "wasm32"))]
2021-01-28 01:44:49 +00:00
impl CombatBuff {
fn to_buff(self, uid: Option<Uid>, damage: f32, strength_modifier: f32) -> Buff {
2021-01-28 01:44:49 +00:00
// TODO: Generate BufCategoryId vec (probably requires damage overhaul?)
let source = if let Some(uid) = uid {
BuffSource::Character { by: uid }
} else {
BuffSource::Unknown
};
2021-01-28 01:44:49 +00:00
Buff::new(
self.kind,
BuffData::new(
self.strength.to_strength(damage, strength_modifier),
2021-01-28 01:44:49 +00:00
Some(Duration::from_secs_f32(self.dur_secs)),
),
Vec::new(),
source,
2021-01-28 01:44:49 +00:00
)
}
}
2021-02-17 13:03:20 +00:00
#[cfg(not(target_arch = "wasm32"))]
fn equipped_item_and_tool(inv: &Inventory, slot: EquipSlot) -> Option<(&Item, &Tool)> {
inv.equipped(slot).and_then(|i| {
if let ItemKind::Tool(tool) = &i.kind() {
Some((i, tool))
} else {
None
}
})
}
2021-02-17 13:03:20 +00:00
#[cfg(not(target_arch = "wasm32"))]
pub fn get_weapons(inv: &Inventory) -> (Option<ToolKind>, Option<ToolKind>) {
(
2021-05-09 21:18:36 +00:00
equipped_item_and_tool(inv, EquipSlot::ActiveMainhand).map(|(_, tool)| tool.kind),
equipped_item_and_tool(inv, EquipSlot::ActiveOffhand).map(|(_, tool)| tool.kind),
)
}
#[cfg(not(target_arch = "wasm32"))]
2021-03-30 20:15:37 +00:00
pub fn weapon_rating<T: ItemDesc>(item: &T, msm: &MaterialStatManifest) -> f32 {
2021-04-02 21:37:08 +00:00
const DAMAGE_WEIGHT: f32 = 2.0;
const EFFECT_WEIGHT: f32 = 1.0;
if let ItemKind::Tool(tool) = item.kind() {
let stats = tool::Stats::from((msm, item.components(), tool));
// TODO: Look into changing the 0.5 to reflect armor later maybe?
// Since it is only for weapon though, it probably makes sense to leave
// independent for now
let damage_rating = stats.power * stats.speed * (1.0 + stats.crit_chance * 0.5);
let effect_rating = stats.effect_power * stats.speed;
(damage_rating * DAMAGE_WEIGHT + effect_rating * EFFECT_WEIGHT)
/ (DAMAGE_WEIGHT + EFFECT_WEIGHT)
} else {
0.0
}
}
#[cfg(not(target_arch = "wasm32"))]
fn weapon_skills(inventory: &Inventory, skill_set: &SkillSet) -> f32 {
let (mainhand, offhand) = get_weapons(inventory);
let mainhand_skills = if let Some(tool) = mainhand {
skill_set.earned_sp(SkillGroupKind::Weapon(tool)) as f32
} else {
0.0
};
let offhand_skills = if let Some(tool) = offhand {
skill_set.earned_sp(SkillGroupKind::Weapon(tool)) as f32
} else {
0.0
};
mainhand_skills.max(offhand_skills)
}
#[cfg(not(target_arch = "wasm32"))]
fn get_weapon_rating(inventory: &Inventory, msm: &MaterialStatManifest) -> f32 {
let mainhand_rating =
2021-05-09 21:18:36 +00:00
if let Some((item, _)) = equipped_item_and_tool(inventory, EquipSlot::ActiveMainhand) {
weapon_rating(item, msm)
} else {
0.0
};
let offhand_rating =
2021-05-09 21:18:36 +00:00
if let Some((item, _)) = equipped_item_and_tool(inventory, EquipSlot::ActiveOffhand) {
weapon_rating(item, msm)
} else {
0.0
};
mainhand_rating.max(offhand_rating)
}
2021-02-17 13:03:20 +00:00
#[cfg(not(target_arch = "wasm32"))]
pub fn combat_rating(
inventory: &Inventory,
health: &Health,
energy: &Energy,
poise: &Poise,
skill_set: &SkillSet,
body: Body,
msm: &MaterialStatManifest,
) -> f32 {
const WEAPON_WEIGHT: f32 = 1.0;
const HEALTH_WEIGHT: f32 = 1.5;
const ENERGY_WEIGHT: f32 = 0.5;
const SKILLS_WEIGHT: f32 = 1.0;
const POISE_WEIGHT: f32 = 0.5;
const CRIT_WEIGHT: f32 = 0.5;
// Normalized with a standard max health of 100
let health_rating = health.base_max()
/ 100.0
/ (1.0 - Damage::compute_damage_reduction(Some(inventory), None, None)).max(0.00001);
// Normalized with a standard max energy of 100 and energy reward multiplier of
// x1
let energy_rating = (energy.base_max() + compute_max_energy_mod(Some(inventory))) / 100.0
* compute_energy_reward_mod(Some(inventory));
// Normalized with a standard max poise of 100
let poise_rating = poise.base_max() as f32
/ 100.0
/ (1.0 - Poise::compute_poise_damage_reduction(inventory)).max(0.00001);
// Normalized with a standard crit multiplier of 1.2
let crit_rating = compute_crit_mult(Some(inventory)) / 1.2;
// Assumes a standard person has earned 20 skill points in the general skill
2021-04-02 21:37:08 +00:00
// tree and 10 skill points for the weapon skill tree
let skills_rating = (skill_set.earned_sp(SkillGroupKind::General) as f32 / 20.0
+ weapon_skills(inventory, skill_set) / 10.0)
/ 2.0;
let weapon_rating = get_weapon_rating(inventory, msm);
let combined_rating = (health_rating * HEALTH_WEIGHT
+ energy_rating * ENERGY_WEIGHT
+ poise_rating * POISE_WEIGHT
+ crit_rating * CRIT_WEIGHT
+ skills_rating * SKILLS_WEIGHT
+ weapon_rating * WEAPON_WEIGHT)
/ (HEALTH_WEIGHT
+ ENERGY_WEIGHT
+ POISE_WEIGHT
+ CRIT_WEIGHT
+ SKILLS_WEIGHT
+ WEAPON_WEIGHT);
// Body multiplier meant to account for an enemy being harder than equipment and
// skills would account for. It should only not be 1.0 for non-humanoids
combined_rating * body.combat_multiplier()
}
#[cfg(not(target_arch = "wasm32"))]
pub fn compute_crit_mult(inventory: Option<&Inventory>) -> f32 {
// Starts with a value of 1.25 when summing the stats from each armor piece, and
// defaults to a value of 1.25 if no inventory is equipped
inventory.map_or(1.25, |inv| {
inv.equipped_items()
.filter_map(|item| {
if let ItemKind::Armor(armor) = &item.kind() {
Some(armor.crit_power())
} else {
None
}
})
.fold(1.25, |a, b| a + b)
})
}
/// Computes the energy reward modifer from worn armor
#[cfg(not(target_arch = "wasm32"))]
pub fn compute_energy_reward_mod(inventory: Option<&Inventory>) -> f32 {
// Starts with a value of 1.0 when summing the stats from each armor piece, and
// defaults to a value of 1.0 if no inventory is present
inventory.map_or(1.0, |inv| {
inv.equipped_items()
.filter_map(|item| {
if let ItemKind::Armor(armor) = &item.kind() {
Some(armor.energy_reward())
} else {
None
}
})
.fold(1.0, |a, b| a + b)
})
}
/// Computes the additive modifier that should be applied to max energy from the
/// currently equipped items
#[cfg(not(target_arch = "wasm32"))]
pub fn compute_max_energy_mod(inventory: Option<&Inventory>) -> f32 {
// Defaults to a value of 0 if no inventory is present
inventory.map_or(0.0, |inv| {
inv.equipped_items()
.filter_map(|item| {
if let ItemKind::Armor(armor) = &item.kind() {
Some(armor.energy_max())
} else {
None
}
})
.sum()
})
}
2021-10-24 05:31:49 +00:00
/// Computes the sneak coefficient from armor. Agent perception distances are
/// divided by the resulting f32.
#[cfg(not(target_arch = "wasm32"))]
pub fn compute_stealth_coefficient(inventory: Option<&Inventory>) -> f32 {
// Starts with a value of 2.0 when summing the stats from each armor piece, and
// defaults to a value of 2.0 if no inventory is equipped
inventory.map_or(2.0, |inv| {
inv.equipped_items()
.filter_map(|item| {
if let ItemKind::Armor(armor) = &item.kind() {
Some(armor.stealth())
} else {
None
}
})
.fold(2.0, |a, b| a + b.max(0.0))
})
}