1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
use crate::{
    combat::{compute_poise_resilience, DamageContributor, DamageSource},
    comp::{
        self, ability::Capability, inventory::item::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, strum::EnumString)]
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(200), Duration::from_millis(200), 0.8)),
                None,
            ),
            PoiseState::Stunned => (
                Some((Duration::from_millis(350), Duration::from_millis(350), 0.5)),
                None,
            ),
            PoiseState::Dazed => (
                Some((Duration::from_millis(750), Duration::from_millis(750), 0.2)),
                None,
            ),
            PoiseState::KnockedDown => (
                Some((
                    Duration::from_millis(1500),
                    Duration::from_millis(1500),
                    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, also is used for precision
    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 = compute_poise_resilience(inventory, msm);
        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.modifier()
        } 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>>;
}