mirror of
https://gitlab.com/veloren/veloren.git
synced 2024-08-30 18:12:32 +00:00
323 lines
12 KiB
Rust
323 lines
12 KiB
Rust
use crate::{
|
|
combat::{DamageContributor, DamageSource},
|
|
comp::{
|
|
self,
|
|
ability::Capability,
|
|
inventory::item::{armor::Protection, ItemKind, MaterialStatManifest},
|
|
CharacterState, Inventory, Stats,
|
|
},
|
|
resources::Time,
|
|
states,
|
|
util::Dir,
|
|
};
|
|
use serde::{Deserialize, Serialize};
|
|
use specs::{Component, DerefFlaggedStorage, VecStorage};
|
|
use std::{ops::Mul, time::Duration};
|
|
use vek::*;
|
|
|
|
#[derive(Clone, Copy, Debug, Serialize, Deserialize)]
|
|
pub struct PoiseChange {
|
|
/// The amount of the poise change
|
|
pub amount: f32,
|
|
/// The direction that the poise change came from, used for when the target
|
|
/// is knocked down
|
|
pub impulse: Vec3<f32>,
|
|
/// The individual or group who caused the poise change (None if the
|
|
/// damage wasn't caused by an entity)
|
|
pub by: Option<DamageContributor>,
|
|
/// The category of action that resulted in the poise change
|
|
pub cause: Option<DamageSource>,
|
|
/// The time that the poise change occurred at
|
|
pub time: Time,
|
|
}
|
|
|
|
#[derive(Clone, Copy, Debug, Serialize, Deserialize)]
|
|
/// Poise is represented by u32s within the module, but treated as a float by
|
|
/// the rest of the game.
|
|
// As a general rule, all input and output values to public functions should be
|
|
// floats rather than integers.
|
|
pub struct Poise {
|
|
// Current and base_max are scaled by 256 within this module compared to what is visible to
|
|
// outside this module. The scaling is done to allow poise to function as a fixed point while
|
|
// still having the advantages of being an integer. The scaling of 256 was chosen so that max
|
|
// poise could be u16::MAX - 1, and then the scaled poise could fit inside an f32 with no
|
|
// precision loss
|
|
/// Current poise is how much poise the entity currently has
|
|
current: u32,
|
|
/// Base max is the amount of poise the entity has without considering
|
|
/// temporary modifiers such as buffs
|
|
base_max: u32,
|
|
/// Maximum is the amount of poise the entity has after temporary modifiers
|
|
/// are considered
|
|
maximum: u32,
|
|
/// Direction that the last poise change came from
|
|
pub last_change: Dir,
|
|
/// Rate of regeneration per tick. Starts at zero and accelerates.
|
|
regen_rate: f32,
|
|
/// Time that entity was last in a poise state
|
|
last_stun_time: Option<Time>,
|
|
/// The previous poise state
|
|
pub previous_state: PoiseState,
|
|
}
|
|
|
|
/// States to define effects of a poise change
|
|
#[derive(Clone, Copy, PartialEq, Debug, Serialize, Deserialize, Eq, Hash)]
|
|
pub enum PoiseState {
|
|
/// No effect applied
|
|
Normal,
|
|
/// Poise reset, and target briefly stunned
|
|
Interrupted,
|
|
/// Poise reset, target stunned and knocked back horizontally
|
|
Stunned,
|
|
/// Poise reset, target staggered
|
|
Dazed,
|
|
/// Poise reset, target staggered and knocked back further
|
|
KnockedDown,
|
|
}
|
|
|
|
impl PoiseState {
|
|
/// Returns the optional stunned character state and duration of stun, and
|
|
/// optional impulse strength corresponding to a particular poise state
|
|
pub fn poise_effect(&self, was_wielded: bool) -> (Option<(CharacterState, f64)>, Option<f32>) {
|
|
use states::{
|
|
stunned::{Data, StaticData},
|
|
utils::StageSection,
|
|
};
|
|
// charstate_parameters is Option<(buildup_duration, recover_duration,
|
|
// movement_speed)>
|
|
let (charstate_parameters, impulse) = match self {
|
|
PoiseState::Normal => (None, None),
|
|
PoiseState::Interrupted => (
|
|
Some((Duration::from_millis(250), Duration::from_millis(250), 0.8)),
|
|
None,
|
|
),
|
|
PoiseState::Stunned => (
|
|
Some((Duration::from_millis(750), Duration::from_millis(750), 0.5)),
|
|
None,
|
|
),
|
|
PoiseState::Dazed => (
|
|
Some((
|
|
Duration::from_millis(1000),
|
|
Duration::from_millis(1000),
|
|
0.2,
|
|
)),
|
|
None,
|
|
),
|
|
PoiseState::KnockedDown => (
|
|
Some((
|
|
Duration::from_millis(2000),
|
|
Duration::from_millis(2000),
|
|
0.0,
|
|
)),
|
|
Some(10.0),
|
|
),
|
|
};
|
|
(
|
|
charstate_parameters.map(|(buildup_duration, recover_duration, movement_speed)| {
|
|
(
|
|
CharacterState::Stunned(Data {
|
|
static_data: StaticData {
|
|
buildup_duration,
|
|
recover_duration,
|
|
movement_speed,
|
|
poise_state: *self,
|
|
},
|
|
timer: Duration::default(),
|
|
stage_section: StageSection::Buildup,
|
|
was_wielded,
|
|
}),
|
|
buildup_duration.as_secs_f64() + recover_duration.as_secs_f64(),
|
|
)
|
|
}),
|
|
impulse,
|
|
)
|
|
}
|
|
|
|
/// Returns the multiplier on poise damage to health damage for when the
|
|
/// target is in a poise state
|
|
pub fn damage_multiplier(&self) -> f32 {
|
|
match self {
|
|
Self::Interrupted => 0.1,
|
|
Self::Stunned => 0.25,
|
|
Self::Dazed => 0.5,
|
|
Self::KnockedDown => 1.0,
|
|
// Should never be reached
|
|
Self::Normal => 0.0,
|
|
}
|
|
}
|
|
}
|
|
|
|
impl Poise {
|
|
/// Maximum value allowed for poise before scaling
|
|
const MAX_POISE: u16 = u16::MAX - 1;
|
|
/// The maximum value allowed for current and maximum poise
|
|
/// Maximum value is (u16:MAX - 1) * 256, which only requires 24 bits. This
|
|
/// can fit into an f32 with no loss to precision
|
|
// Cast to u32 done as u32::from cannot be called inside constant
|
|
const MAX_SCALED_POISE: u32 = Self::MAX_POISE as u32 * Self::SCALING_FACTOR_INT;
|
|
/// The amount of time after being in a poise state before you can take
|
|
/// poise damage again
|
|
pub const POISE_BUFFER_TIME: f64 = 1.0;
|
|
/// Used when comparisons to poise are needed outside this module.
|
|
// This value is chosen as anything smaller than this is more precise than our
|
|
// units of poise.
|
|
pub const POISE_EPSILON: f32 = 0.5 / Self::MAX_SCALED_POISE as f32;
|
|
/// The thresholds where poise changes to a different state
|
|
pub const POISE_THRESHOLDS: [f32; 4] = [50.0, 30.0, 15.0, 5.0];
|
|
/// The amount poise is scaled by within this module
|
|
const SCALING_FACTOR_FLOAT: f32 = 256.;
|
|
const SCALING_FACTOR_INT: u32 = Self::SCALING_FACTOR_FLOAT as u32;
|
|
|
|
/// Returns the current value of poise casted to a float
|
|
pub fn current(&self) -> f32 { self.current as f32 / Self::SCALING_FACTOR_FLOAT }
|
|
|
|
/// Returns the base maximum value of poise casted to a float
|
|
pub fn base_max(&self) -> f32 { self.base_max as f32 / Self::SCALING_FACTOR_FLOAT }
|
|
|
|
/// Returns the maximum value of poise casted to a float
|
|
pub fn maximum(&self) -> f32 { self.maximum as f32 / Self::SCALING_FACTOR_FLOAT }
|
|
|
|
/// Returns the fraction of poise an entity has remaining
|
|
pub fn fraction(&self) -> f32 { self.current() / self.maximum().max(1.0) }
|
|
|
|
/// Updates the maximum value for poise
|
|
pub fn update_maximum(&mut self, modifiers: comp::stats::StatsModifier) {
|
|
let maximum = modifiers
|
|
.compute_maximum(self.base_max())
|
|
.mul(Self::SCALING_FACTOR_FLOAT)
|
|
// NaN does not need to be handled here as rust will automatically change to 0 when casting to u32
|
|
.clamp(0.0, Self::MAX_SCALED_POISE as f32) as u32;
|
|
self.maximum = maximum;
|
|
self.current = self.current.min(self.maximum);
|
|
}
|
|
|
|
pub fn new(body: comp::Body) -> Self {
|
|
let poise = u32::from(body.base_poise()) * Self::SCALING_FACTOR_INT;
|
|
Poise {
|
|
current: poise,
|
|
base_max: poise,
|
|
maximum: poise,
|
|
last_change: Dir::default(),
|
|
regen_rate: 0.0,
|
|
last_stun_time: None,
|
|
previous_state: PoiseState::Normal,
|
|
}
|
|
}
|
|
|
|
pub fn change(&mut self, change: PoiseChange) {
|
|
match self.last_stun_time {
|
|
Some(last_time) if last_time.0 + Poise::POISE_BUFFER_TIME > change.time.0 => {},
|
|
_ => {
|
|
// if self.previous_state != self.poise_state() {
|
|
self.previous_state = self.poise_state();
|
|
// };
|
|
self.current = (((self.current() + change.amount)
|
|
.clamp(0.0, f32::from(Self::MAX_POISE))
|
|
* Self::SCALING_FACTOR_FLOAT) as u32)
|
|
.min(self.maximum);
|
|
self.last_change = Dir::from_unnormalized(change.impulse).unwrap_or_default();
|
|
},
|
|
}
|
|
}
|
|
|
|
/// Returns `true` if the current value is less than the maximum
|
|
pub fn needs_regen(&self) -> bool { self.current < self.maximum }
|
|
|
|
/// Regenerates poise based on a provided acceleration
|
|
pub fn regen(&mut self, accel: f32, dt: f32, now: Time) {
|
|
if self.current < self.maximum {
|
|
let poise_change = PoiseChange {
|
|
amount: self.regen_rate * dt,
|
|
impulse: Vec3::zero(),
|
|
by: None,
|
|
cause: None,
|
|
time: now,
|
|
};
|
|
self.change(poise_change);
|
|
self.regen_rate = (self.regen_rate + accel * dt).min(10.0);
|
|
}
|
|
}
|
|
|
|
pub fn reset(&mut self, time: Time, poise_state_time: f64) {
|
|
self.current = self.maximum;
|
|
self.last_stun_time = Some(Time(time.0 + poise_state_time));
|
|
}
|
|
|
|
/// Returns knockback as a Dir
|
|
/// Kept as helper function should additional fields ever be added to last
|
|
/// change
|
|
pub fn knockback(&self) -> Dir { self.last_change }
|
|
|
|
/// Defines the poise states based on current poise value
|
|
pub fn poise_state(&self) -> PoiseState {
|
|
match self.current() {
|
|
x if x > Self::POISE_THRESHOLDS[0] => PoiseState::Normal,
|
|
x if x > Self::POISE_THRESHOLDS[1] => PoiseState::Interrupted,
|
|
x if x > Self::POISE_THRESHOLDS[2] => PoiseState::Stunned,
|
|
x if x > Self::POISE_THRESHOLDS[3] => PoiseState::Dazed,
|
|
_ => PoiseState::KnockedDown,
|
|
}
|
|
}
|
|
|
|
/// Returns the total poise damage reduction provided by all equipped items
|
|
pub fn compute_poise_damage_reduction(
|
|
inventory: Option<&Inventory>,
|
|
msm: &MaterialStatManifest,
|
|
char_state: Option<&CharacterState>,
|
|
stats: Option<&Stats>,
|
|
) -> f32 {
|
|
let protection = inventory.map_or(Some(0.0), |inv| {
|
|
inv.equipped_items()
|
|
.filter_map(|item| {
|
|
if let ItemKind::Armor(armor) = &*item.kind() {
|
|
armor
|
|
.stats(msm, item.stats_durability_multiplier())
|
|
.poise_resilience
|
|
} else {
|
|
None
|
|
}
|
|
})
|
|
.map(|protection| match protection {
|
|
Protection::Normal(protection) => Some(protection),
|
|
Protection::Invincible => None,
|
|
})
|
|
.sum::<Option<f32>>()
|
|
});
|
|
let from_inventory = match protection {
|
|
Some(dr) => dr / (60.0 + dr.abs()),
|
|
None => 1.0,
|
|
};
|
|
let from_char = {
|
|
let resistant = char_state
|
|
.and_then(|cs| cs.ability_info())
|
|
.map(|a| a.ability_meta)
|
|
.map_or(false, |a| {
|
|
a.capabilities.contains(Capability::POISE_RESISTANT)
|
|
});
|
|
if resistant { 0.5 } else { 0.0 }
|
|
};
|
|
let from_stats = if let Some(stats) = stats {
|
|
stats.poise_reduction
|
|
} else {
|
|
0.0
|
|
};
|
|
1.0 - (1.0 - from_inventory) * (1.0 - from_char) * (1.0 - from_stats)
|
|
}
|
|
|
|
/// Modifies a poise change when optionally given an inventory and character
|
|
/// state to aid in calculation of poise damage reduction
|
|
pub fn apply_poise_reduction(
|
|
value: f32,
|
|
inventory: Option<&Inventory>,
|
|
msm: &MaterialStatManifest,
|
|
char_state: Option<&CharacterState>,
|
|
stats: Option<&Stats>,
|
|
) -> f32 {
|
|
value * (1.0 - Poise::compute_poise_damage_reduction(inventory, msm, char_state, stats))
|
|
}
|
|
}
|
|
|
|
impl Component for Poise {
|
|
type Storage = DerefFlaggedStorage<Self, VecStorage<Self>>;
|
|
}
|