veloren_common/comp/
poise.rs

1use crate::{
2    combat::{DamageContributor, DamageSource, compute_poise_resilience},
3    comp::{
4        self, CharacterState, Inventory, Stats, ability::Capability,
5        inventory::item::MaterialStatManifest,
6    },
7    resources::Time,
8    states,
9    util::Dir,
10};
11use serde::{Deserialize, Serialize};
12use specs::{Component, DerefFlaggedStorage, VecStorage};
13use std::{ops::Mul, time::Duration};
14use vek::*;
15
16#[derive(Clone, Copy, Debug, Serialize, Deserialize)]
17pub struct PoiseChange {
18    /// The amount of the poise change
19    pub amount: f32,
20    /// The direction that the poise change came from, used for when the target
21    /// is knocked down
22    pub impulse: Vec3<f32>,
23    /// The individual or group who caused the poise change (None if the
24    /// damage wasn't caused by an entity)
25    pub by: Option<DamageContributor>,
26    /// The category of action that resulted in the poise change
27    pub cause: Option<DamageSource>,
28    /// The time that the poise change occurred at
29    pub time: Time,
30}
31
32#[derive(Clone, Copy, Debug, Serialize, Deserialize)]
33/// Poise is represented by u32s within the module, but treated as a float by
34/// the rest of the game.
35// As a general rule, all input and output values to public functions should be
36// floats rather than integers.
37pub struct Poise {
38    // Current and base_max are scaled by 256 within this module compared to what is visible to
39    // outside this module. The scaling is done to allow poise to function as a fixed point while
40    // still having the advantages of being an integer. The scaling of 256 was chosen so that max
41    // poise could be u16::MAX - 1, and then the scaled poise could fit inside an f32 with no
42    // precision loss
43    /// Current poise is how much poise the entity currently has
44    current: u32,
45    /// Base max is the amount of poise the entity has without considering
46    /// temporary modifiers such as buffs
47    base_max: u32,
48    /// Maximum is the amount of poise the entity has after temporary modifiers
49    /// are considered
50    maximum: u32,
51    /// Direction that the last poise change came from
52    pub last_change: Dir,
53    /// Rate of regeneration per tick. Starts at zero and accelerates.
54    regen_rate: f32,
55    /// Time that entity was last in a poise state
56    last_stun_time: Option<Time>,
57    /// The previous poise state
58    pub previous_state: PoiseState,
59}
60
61/// States to define effects of a poise change
62#[derive(Clone, Copy, PartialEq, Debug, Serialize, Deserialize, Eq, Hash, strum::EnumString)]
63pub enum PoiseState {
64    /// No effect applied
65    Normal,
66    /// Poise reset, and target briefly stunned
67    Interrupted,
68    /// Poise reset, target stunned and knocked back horizontally
69    Stunned,
70    /// Poise reset, target staggered
71    Dazed,
72    /// Poise reset, target staggered and knocked back further
73    KnockedDown,
74}
75
76impl PoiseState {
77    /// Returns the optional stunned character state and duration of stun, and
78    /// optional impulse strength corresponding to a particular poise state
79    pub fn poise_effect(&self, was_wielded: bool) -> (Option<(CharacterState, f64)>, Option<f32>) {
80        use states::{
81            stunned::{Data, StaticData},
82            utils::StageSection,
83        };
84        // charstate_parameters is Option<(buildup_duration, recover_duration,
85        // movement_speed, ori_rate)>
86        let (charstate_parameters, impulse) = match self {
87            PoiseState::Normal => (None, None),
88            PoiseState::Interrupted => (
89                Some((
90                    Duration::from_millis(200),
91                    Duration::from_millis(200),
92                    0.8,
93                    0.8,
94                )),
95                None,
96            ),
97            PoiseState::Stunned => (
98                Some((
99                    Duration::from_millis(350),
100                    Duration::from_millis(350),
101                    0.5,
102                    0.5,
103                )),
104                None,
105            ),
106            PoiseState::Dazed => (
107                Some((
108                    Duration::from_millis(750),
109                    Duration::from_millis(750),
110                    0.2,
111                    0.0,
112                )),
113                None,
114            ),
115            PoiseState::KnockedDown => (
116                Some((
117                    Duration::from_millis(1500),
118                    Duration::from_millis(1500),
119                    0.0,
120                    0.0,
121                )),
122                Some(10.0),
123            ),
124        };
125        (
126            charstate_parameters.map(
127                |(buildup_duration, recover_duration, movement_speed, ori_rate)| {
128                    (
129                        CharacterState::Stunned(Data {
130                            static_data: StaticData {
131                                buildup_duration,
132                                recover_duration,
133                                movement_speed,
134                                ori_rate,
135                                poise_state: *self,
136                            },
137                            timer: Duration::default(),
138                            stage_section: StageSection::Buildup,
139                            was_wielded,
140                        }),
141                        buildup_duration.as_secs_f64() + recover_duration.as_secs_f64(),
142                    )
143                },
144            ),
145            impulse,
146        )
147    }
148
149    /// Returns the multiplier on poise damage to health damage for when the
150    /// target is in a poise state, also is used for precision
151    pub fn damage_multiplier(&self) -> f32 {
152        match self {
153            Self::Interrupted => 0.1,
154            Self::Stunned => 0.25,
155            Self::Dazed => 0.5,
156            Self::KnockedDown => 1.0,
157            // Should never be reached
158            Self::Normal => 0.0,
159        }
160    }
161}
162
163impl Poise {
164    /// Maximum value allowed for poise before scaling
165    const MAX_POISE: u16 = u16::MAX - 1;
166    /// The maximum value allowed for current and maximum poise
167    /// Maximum value is (u16:MAX - 1) * 256, which only requires 24 bits. This
168    /// can fit into an f32 with no loss to precision
169    // Cast to u32 done as u32::from cannot be called inside constant
170    const MAX_SCALED_POISE: u32 = Self::MAX_POISE as u32 * Self::SCALING_FACTOR_INT;
171    /// The amount of time after being in a poise state before you can take
172    /// poise damage again
173    pub const POISE_BUFFER_TIME: f64 = 1.0;
174    /// Used when comparisons to poise are needed outside this module.
175    // This value is chosen as anything smaller than this is more precise than our
176    // units of poise.
177    pub const POISE_EPSILON: f32 = 0.5 / Self::MAX_SCALED_POISE as f32;
178    /// The thresholds where poise changes to a different state
179    pub const POISE_THRESHOLDS: [f32; 4] = [50.0, 30.0, 15.0, 5.0];
180    /// The amount poise is scaled by within this module
181    const SCALING_FACTOR_FLOAT: f32 = 256.;
182    const SCALING_FACTOR_INT: u32 = Self::SCALING_FACTOR_FLOAT as u32;
183
184    /// Returns the current value of poise casted to a float
185    pub fn current(&self) -> f32 { self.current as f32 / Self::SCALING_FACTOR_FLOAT }
186
187    /// Returns the base maximum value of poise casted to a float
188    pub fn base_max(&self) -> f32 { self.base_max as f32 / Self::SCALING_FACTOR_FLOAT }
189
190    /// Returns the maximum value of poise casted to a float
191    pub fn maximum(&self) -> f32 { self.maximum as f32 / Self::SCALING_FACTOR_FLOAT }
192
193    /// Returns the fraction of poise an entity has remaining
194    pub fn fraction(&self) -> f32 { self.current() / self.maximum().max(1.0) }
195
196    /// Updates the maximum value for poise
197    pub fn update_maximum(&mut self, modifiers: comp::stats::StatsModifier) {
198        let maximum = modifiers
199            .compute_maximum(self.base_max())
200            .mul(Self::SCALING_FACTOR_FLOAT)
201            // NaN does not need to be handled here as rust will automatically change to 0 when casting to u32
202            .clamp(0.0, Self::MAX_SCALED_POISE as f32) as u32;
203        self.maximum = maximum;
204        self.current = self.current.min(self.maximum);
205    }
206
207    pub fn new(body: comp::Body) -> Self {
208        let poise = u32::from(body.base_poise()) * Self::SCALING_FACTOR_INT;
209        Poise {
210            current: poise,
211            base_max: poise,
212            maximum: poise,
213            last_change: Dir::default(),
214            regen_rate: 0.0,
215            last_stun_time: None,
216            previous_state: PoiseState::Normal,
217        }
218    }
219
220    pub fn change(&mut self, change: PoiseChange) {
221        match self.last_stun_time {
222            Some(last_time) if last_time.0 + Poise::POISE_BUFFER_TIME > change.time.0 => {},
223            _ => {
224                // if self.previous_state != self.poise_state() {
225                self.previous_state = self.poise_state();
226                // };
227                self.current = (((self.current() + change.amount)
228                    .clamp(0.0, f32::from(Self::MAX_POISE))
229                    * Self::SCALING_FACTOR_FLOAT) as u32)
230                    .min(self.maximum);
231                self.last_change = Dir::from_unnormalized(change.impulse).unwrap_or_default();
232            },
233        }
234    }
235
236    /// Returns `true` if the current value is less than the maximum
237    pub fn needs_regen(&self) -> bool { self.current < self.maximum }
238
239    /// Regenerates poise based on a provided acceleration
240    pub fn regen(&mut self, accel: f32, dt: f32, now: Time) {
241        if self.current < self.maximum {
242            let poise_change = PoiseChange {
243                amount: self.regen_rate * dt,
244                impulse: Vec3::zero(),
245                by: None,
246                cause: None,
247                time: now,
248            };
249            self.change(poise_change);
250            self.regen_rate = (self.regen_rate + accel * dt).min(10.0);
251        }
252    }
253
254    pub fn reset(&mut self, time: Time, poise_state_time: f64) {
255        self.current = self.maximum;
256        self.last_stun_time = Some(Time(time.0 + poise_state_time));
257    }
258
259    /// Returns knockback as a Dir
260    /// Kept as helper function should additional fields ever be added to last
261    /// change
262    pub fn knockback(&self) -> Dir { self.last_change }
263
264    /// Defines the poise states based on current poise value
265    pub fn poise_state(&self) -> PoiseState {
266        match self.current() {
267            x if x > Self::POISE_THRESHOLDS[0] => PoiseState::Normal,
268            x if x > Self::POISE_THRESHOLDS[1] => PoiseState::Interrupted,
269            x if x > Self::POISE_THRESHOLDS[2] => PoiseState::Stunned,
270            x if x > Self::POISE_THRESHOLDS[3] => PoiseState::Dazed,
271            _ => PoiseState::KnockedDown,
272        }
273    }
274
275    /// Returns the total poise damage reduction provided by all equipped items
276    pub fn compute_poise_damage_reduction(
277        inventory: Option<&Inventory>,
278        msm: &MaterialStatManifest,
279        char_state: Option<&CharacterState>,
280        stats: Option<&Stats>,
281    ) -> f32 {
282        let protection = compute_poise_resilience(inventory, msm);
283        let from_inventory = match protection {
284            Some(dr) => dr / (60.0 + dr.abs()),
285            None => 1.0,
286        };
287        let from_char = {
288            let resistant = char_state
289                .and_then(|cs| cs.ability_info())
290                .map(|a| a.ability_meta)
291                .is_some_and(|a| a.capabilities.contains(Capability::POISE_RESISTANT));
292            if resistant { 0.5 } else { 0.0 }
293        };
294        let from_stats = if let Some(stats) = stats {
295            stats.poise_reduction.modifier()
296        } else {
297            0.0
298        };
299        1.0 - (1.0 - from_inventory) * (1.0 - from_char) * (1.0 - from_stats)
300    }
301
302    /// Modifies a poise change when optionally given an inventory and character
303    /// state to aid in calculation of poise damage reduction
304    pub fn apply_poise_reduction(
305        value: f32,
306        inventory: Option<&Inventory>,
307        msm: &MaterialStatManifest,
308        char_state: Option<&CharacterState>,
309        stats: Option<&Stats>,
310    ) -> f32 {
311        value * (1.0 - Poise::compute_poise_damage_reduction(inventory, msm, char_state, stats))
312    }
313}
314
315impl Component for Poise {
316    type Storage = DerefFlaggedStorage<Self, VecStorage<Self>>;
317}