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 pub amount: f32,
20 pub impulse: Vec3<f32>,
23 pub by: Option<DamageContributor>,
26 pub cause: Option<DamageSource>,
28 pub time: Time,
30}
31
32#[derive(Clone, Copy, Debug, Serialize, Deserialize)]
33pub struct Poise {
38 current: u32,
45 base_max: u32,
48 maximum: u32,
51 pub last_change: Dir,
53 regen_rate: f32,
55 last_stun_time: Option<Time>,
57 pub previous_state: PoiseState,
59}
60
61#[derive(Clone, Copy, PartialEq, Debug, Serialize, Deserialize, Eq, Hash, strum::EnumString)]
63pub enum PoiseState {
64 Normal,
66 Interrupted,
68 Stunned,
70 Dazed,
72 KnockedDown,
74}
75
76impl PoiseState {
77 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 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 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 Self::Normal => 0.0,
159 }
160 }
161}
162
163impl Poise {
164 const MAX_POISE: u16 = u16::MAX - 1;
166 const MAX_SCALED_POISE: u32 = Self::MAX_POISE as u32 * Self::SCALING_FACTOR_INT;
171 pub const POISE_BUFFER_TIME: f64 = 1.0;
174 pub const POISE_EPSILON: f32 = 0.5 / Self::MAX_SCALED_POISE as f32;
178 pub const POISE_THRESHOLDS: [f32; 4] = [50.0, 30.0, 15.0, 5.0];
180 const SCALING_FACTOR_FLOAT: f32 = 256.;
182 const SCALING_FACTOR_INT: u32 = Self::SCALING_FACTOR_FLOAT as u32;
183
184 pub fn current(&self) -> f32 { self.current as f32 / Self::SCALING_FACTOR_FLOAT }
186
187 pub fn base_max(&self) -> f32 { self.base_max as f32 / Self::SCALING_FACTOR_FLOAT }
189
190 pub fn maximum(&self) -> f32 { self.maximum as f32 / Self::SCALING_FACTOR_FLOAT }
192
193 pub fn fraction(&self) -> f32 { self.current() / self.maximum().max(1.0) }
195
196 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 .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 self.previous_state = self.poise_state();
226 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 pub fn needs_regen(&self) -> bool { self.current < self.maximum }
238
239 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 pub fn knockback(&self) -> Dir { self.last_change }
263
264 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 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 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}