diff --git a/common/src/combat.rs b/common/src/combat.rs index cdd5be06c6..0b4f503e61 100644 --- a/common/src/combat.rs +++ b/common/src/combat.rs @@ -10,7 +10,6 @@ use crate::{ }, slot::EquipSlot, }, - poise::PoiseChange, skills::SkillGroupKind, Alignment, Body, CharacterState, Combo, Energy, Health, HealthChange, Inventory, Ori, Player, Poise, SkillSet, Stats, @@ -281,8 +280,8 @@ impl Attack { } }, CombatEffect::Poise(p) => { - let change = PoiseChange::from_value(*p, target.inventory); - if change.amount != 0 { + let change = -Poise::apply_poise_reduction(*p, target.inventory); + if change.abs() > Poise::POISE_EPSILON { emit(ServerEvent::PoiseChange { entity: target.entity, change, @@ -409,8 +408,8 @@ impl Attack { } }, CombatEffect::Poise(p) => { - let change = PoiseChange::from_value(p, target.inventory); - if change.amount != 0 { + let change = -Poise::apply_poise_reduction(p, target.inventory); + if change.abs() > Poise::POISE_EPSILON { emit(ServerEvent::PoiseChange { entity: target.entity, change, diff --git a/common/src/comp/body.rs b/common/src/comp/body.rs index e4cfc07e56..82316716dc 100644 --- a/common/src/comp/body.rs +++ b/common/src/comp/body.rs @@ -774,7 +774,7 @@ impl Body { } } - pub fn base_poise(&self) -> u32 { + pub fn base_poise(&self) -> u16 { match self { Body::Humanoid(_) => 100, Body::BipedLarge(biped_large) => match biped_large.species { diff --git a/common/src/comp/mod.rs b/common/src/comp/mod.rs index c41978ddcd..2a602efe17 100644 --- a/common/src/comp/mod.rs +++ b/common/src/comp/mod.rs @@ -91,7 +91,7 @@ pub use self::{ }, player::DisconnectReason, player::Player, - poise::{Poise, PoiseChange, PoiseSource, PoiseState}, + poise::{Poise, PoiseState}, projectile::{Projectile, ProjectileConstructor}, shockwave::{Shockwave, ShockwaveHitEntities}, skills::{Skill, SkillGroup, SkillGroupKind, SkillSet}, diff --git a/common/src/comp/poise.rs b/common/src/comp/poise.rs index 8cbbdf1067..b7dfe5e81d 100644 --- a/common/src/comp/poise.rs +++ b/common/src/comp/poise.rs @@ -1,94 +1,42 @@ -use crate::comp::{ - inventory::item::{armor::Protection, ItemKind}, - Body, Inventory, +use crate::{ + comp::{ + self, + inventory::item::{armor::Protection, ItemKind}, + Inventory, + }, + util::Dir, }; use serde::{Deserialize, Serialize}; use specs::{Component, DerefFlaggedStorage}; use specs_idvs::IdvStorage; +use std::ops::Mul; use vek::*; -/// A change in the poise component. Stores the amount as a signed -/// integer to allow for added or removed poise. Also has a field to -/// label where the poise change came from. -#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)] -pub struct PoiseChange { - /// Value of the change in poise - pub amount: i32, - /// Source of change in poise - pub source: PoiseSource, -} - -impl PoiseChange { - /// Alters poise damage as a result of armor poise damage reduction - pub fn modify_poise_damage(self, inventory: Option<&Inventory>) -> PoiseChange { - let poise_damage_reduction = inventory.map_or(0.0, Poise::compute_poise_damage_reduction); - let poise_damage = self.amount as f32 * (1.0 - poise_damage_reduction); - // Add match on poise source when different calculations per source - // are needed/wanted - PoiseChange { - amount: poise_damage as i32, - source: self.source, - } - } - - /// Creates a poise change from a float - pub fn from_value(poise_damage: f32, inventory: Option<&Inventory>) -> Self { - let poise_damage_reduction = inventory.map_or(0.0, Poise::compute_poise_damage_reduction); - let poise_change = -poise_damage * (1.0 - poise_damage_reduction); - Self { - amount: poise_change as i32, - source: PoiseSource::Attack, - } - } -} - -/// Sources of poise change -#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize)] -pub enum PoiseSource { - LevelUp, - Attack, - Explosion, - Falling, - Revive, - Regen, - Other, -} - -/// Poise component #[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 { - /// Base poise amount for this entity - base_max: u32, - /// Poise of entity at any given moment + // 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, - /// Maximum poise of entity at a given time + /// 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, - /// Last poise change, storing time since last change, the change itself, - /// and the knockback direction vector - pub last_change: (f64, PoiseChange, Vec3), + /// Direction that the last poise change came from + pub last_change: Dir, /// Rate of poise regeneration per tick. Starts at zero and accelerates. pub regen_rate: f32, } -impl Default for Poise { - fn default() -> Self { - Self { - current: 0, - maximum: 0, - base_max: 0, - last_change: ( - 0.0, - PoiseChange { - amount: 0, - source: PoiseSource::Revive, - }, - Vec3::zero(), - ), - regen_rate: 0.0, - } - } -} - /// States to define effects of a poise change #[derive(Clone, Copy, PartialEq, Debug, Serialize, Deserialize, Eq, Hash)] pub enum PoiseState { @@ -105,95 +53,77 @@ pub enum PoiseState { } impl Poise { - /// Creates a new poise struct based on the body it is being assigned to - pub fn new(body: Body) -> Self { - let mut poise = Poise::default(); - poise.update_base_max(Some(body)); - poise.set_maximum(poise.base_max); - poise.set_to(poise.maximum, PoiseSource::Revive); + /// 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; + /// 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 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; - poise + /// 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); } - /// Returns knockback as a Vec3 - pub fn knockback(&self) -> Vec3 { self.last_change.2 } - - /// Defines the poise states based on fraction of maximum poise - pub fn poise_state(&self) -> PoiseState { - if self.current >= 7 * self.maximum / 10 { - PoiseState::Normal - } else if self.current >= 5 * self.maximum / 10 { - PoiseState::Interrupted - } else if self.current >= 4 * self.maximum / 10 { - PoiseState::Stunned - } else if self.current >= 2 * self.maximum / 10 { - PoiseState::Dazed - } else { - PoiseState::KnockedDown + 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, } } - /// Gets the current poise value - pub fn current(&self) -> u32 { self.current } - - /// Gets the maximum poise value - pub fn maximum(&self) -> u32 { self.maximum } - - /// Gets the base_max value - pub fn base_max(&self) -> u32 { self.base_max } - - /// Sets the poise value to a provided value. First cuts off the value - /// at the maximum. In most cases change_by() should be used. - pub fn set_to(&mut self, amount: u32, cause: PoiseSource) { - let amount = amount.min(self.maximum); - self.last_change = ( - 0.0, - PoiseChange { - amount: amount as i32 - self.current as i32, - source: cause, - }, - Vec3::zero(), - ); - self.current = amount; + pub fn change_by(&mut self, change: f32, impulse: Vec3) { + self.current = (((self.current() + change).clamp(0.0, f32::from(Self::MAX_POISE)) + * Self::SCALING_FACTOR_FLOAT) as u32) + .min(self.maximum); + self.last_change = Dir::from_unnormalized(impulse).unwrap_or_default(); } - /// Changes the current poise due to an in-game effect. - pub fn change_by(&mut self, change: PoiseChange, impulse: Vec3) { - self.current = ((self.current as i32 + change.amount).max(0) as u32).min(self.maximum); - self.last_change = ( - 0.0, - PoiseChange { - amount: change.amount, - source: change.source, - }, - impulse, - ); - } - - /// Resets current value to maximum pub fn reset(&mut self) { self.current = self.maximum; } - /// Sets the maximum and updates the current value to max out at the new - /// maximum - pub fn set_maximum(&mut self, amount: u32) { - self.maximum = amount; - self.current = self.current.min(self.maximum); - } + /// 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 } - /// Sets the `Poise` base_max - fn set_base_max(&mut self, amount: u32) { - self.base_max = amount; - self.current = self.current.min(self.maximum); - } - - /// Resets the maximum to the base_max. Example use would be a potion - /// wearing off - pub fn reset_max(&mut self) { self.maximum = self.base_max; } - - /// Sets the base_max based on the entity `Body` - pub fn update_base_max(&mut self, body: Option) { - if let Some(body) = body { - self.set_base_max(body.base_poise()); + /// Defines the poise states based on current poise value + pub fn poise_state(&self) -> PoiseState { + match self.current() { + x if x > 70.0 => PoiseState::Normal, + x if x > 50.0 => PoiseState::Interrupted, + x if x > 40.0 => PoiseState::Stunned, + x if x > 20.0 => PoiseState::Dazed, + _ => PoiseState::KnockedDown, } } @@ -218,6 +148,14 @@ impl Poise { None => 1.0, } } + + /// Modifies a poise change when optionally given an inventory to aid in + /// calculation of poise damage reduction + pub fn apply_poise_reduction(value: f32, inventory: Option<&Inventory>) -> f32 { + inventory.map_or(value, |inv| { + value * (1.0 - Poise::compute_poise_damage_reduction(inv)) + }) + } } impl Component for Poise { diff --git a/common/src/effect.rs b/common/src/effect.rs index 13a49e1a20..c55b338b71 100644 --- a/common/src/effect.rs +++ b/common/src/effect.rs @@ -5,7 +5,7 @@ use serde::{Deserialize, Serialize}; #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] pub enum Effect { Health(comp::HealthChange), - PoiseChange(comp::PoiseChange), + Poise(f32), Damage(combat::Damage), Buff(BuffEffect), } @@ -22,7 +22,7 @@ impl Effect { pub fn info(&self) -> String { match self { Effect::Health(c) => format!("{:+} health", c.amount), - Effect::PoiseChange(c) => format!("{:+} poise", c.amount), + Effect::Poise(p) => format!("{:+} poise", p), Effect::Damage(d) => format!("{:+}", d.value), Effect::Buff(e) => format!("{:?} buff", e), } @@ -31,7 +31,7 @@ impl Effect { pub fn is_harm(&self) -> bool { match self { Effect::Health(c) => c.amount < 0.0, - Effect::PoiseChange(c) => c.amount < 0, + Effect::Poise(p) => *p < 0.0, Effect::Damage(_) => true, Effect::Buff(e) => !e.kind.is_buff(), } @@ -42,8 +42,8 @@ impl Effect { Effect::Health(change) => { change.amount *= modifier; }, - Effect::PoiseChange(change) => { - change.amount = (change.amount as f32 * modifier) as i32; + Effect::Poise(poise) => { + *poise *= modifier; }, Effect::Damage(damage) => { damage.interpolate_damage(modifier, 0.0); diff --git a/common/src/event.rs b/common/src/event.rs index 6dfe89f24b..d3a22423d8 100644 --- a/common/src/event.rs +++ b/common/src/event.rs @@ -53,7 +53,7 @@ pub enum ServerEvent { }, PoiseChange { entity: EcsEntity, - change: comp::PoiseChange, + change: f32, kb_dir: Vec3, }, Delete(EcsEntity), diff --git a/common/systems/src/character_behavior.rs b/common/systems/src/character_behavior.rs index 1ff200a097..f43fc53a05 100644 --- a/common/systems/src/character_behavior.rs +++ b/common/systems/src/character_behavior.rs @@ -173,7 +173,7 @@ impl<'a> System<'a> for Sys { }); server_emitter.emit(ServerEvent::Knockback { entity, - impulse: 5.0 * poise.knockback(), + impulse: 5.0 * *poise.knockback(), }); }, PoiseState::Dazed => { @@ -195,7 +195,7 @@ impl<'a> System<'a> for Sys { }); server_emitter.emit(ServerEvent::Knockback { entity, - impulse: 10.0 * poise.knockback(), + impulse: 10.0 * *poise.knockback(), }); }, PoiseState::KnockedDown => { @@ -217,7 +217,7 @@ impl<'a> System<'a> for Sys { }); server_emitter.emit(ServerEvent::Knockback { entity, - impulse: 10.0 * poise.knockback(), + impulse: 10.0 * *poise.knockback(), }); }, } diff --git a/common/systems/src/stats.rs b/common/systems/src/stats.rs index 2101f37f33..19841b5faf 100644 --- a/common/systems/src/stats.rs +++ b/common/systems/src/stats.rs @@ -3,8 +3,8 @@ use common::{ comp::{ self, skills::{GeneralSkill, Skill}, - Body, CharacterState, Combo, Energy, Health, Inventory, Poise, PoiseChange, PoiseSource, - Pos, SkillSet, Stats, StatsModifier, + Body, CharacterState, Combo, Energy, Health, Inventory, Poise, Pos, SkillSet, Stats, + StatsModifier, }, event::{EventBus, ServerEvent}, outcome::Outcome, @@ -75,15 +75,10 @@ impl<'a> System<'a> for Sys { // Increment last change timer healths.set_event_emission(false); // avoid unnecessary syncing - poises.set_event_emission(false); // avoid unnecessary syncing for mut health in (&mut healths).join() { health.last_change.0 += f64::from(dt); } - for mut poise in (&mut poises).join() { - poise.last_change.0 += f64::from(dt); - } healths.set_event_emission(true); - poises.set_event_emission(true); // Update stats for (entity, uid, stats, mut skill_set, mut health, pos, mut energy, inventory) in ( @@ -217,15 +212,7 @@ impl<'a> System<'a> for Sys { if res_poise { let poise = &mut *poise; - poise.change_by( - PoiseChange { - amount: (poise.regen_rate * dt - + POISE_REGEN_ACCEL * dt.powi(2) / 2.0) - as i32, - source: PoiseSource::Regen, - }, - Vec3::zero(), - ); + poise.change_by(poise.regen_rate * dt, Vec3::zero()); poise.regen_rate = (poise.regen_rate + POISE_REGEN_ACCEL * dt).min(10.0); } }, diff --git a/server/src/events/entity_manipulation.rs b/server/src/events/entity_manipulation.rs index 66333056ee..5b647d9dfc 100644 --- a/server/src/events/entity_manipulation.rs +++ b/server/src/events/entity_manipulation.rs @@ -18,7 +18,7 @@ use common::{ chat::{KillSource, KillType}, inventory::item::MaterialStatManifest, object, Alignment, Auras, Body, CharacterState, Energy, Group, Health, HealthChange, - Inventory, Player, Poise, PoiseChange, PoiseSource, Pos, SkillSet, Stats, + Inventory, Player, Poise, Pos, SkillSet, Stats, }, event::{EventBus, ServerEvent}, lottery::{LootSpec, Lottery}, @@ -40,12 +40,7 @@ use specs::{join::Join, saveload::MarkerAllocator, Builder, Entity as EcsEntity, use tracing::error; use vek::{Vec2, Vec3}; -pub fn handle_poise( - server: &Server, - entity: EcsEntity, - change: PoiseChange, - knockback_dir: Vec3, -) { +pub fn handle_poise(server: &Server, entity: EcsEntity, change: f32, knockback_dir: Vec3) { let ecs = &server.state.ecs(); if let Some(character_state) = ecs.read_storage::().get(entity) { // Entity is invincible to poise change during stunned/staggered character state @@ -620,11 +615,8 @@ pub fn handle_land_on_ground(server: &Server, entity: EcsEntity, vel: Vec3) } // Handle poise change if let Some(mut poise) = ecs.write_storage::().get_mut(entity) { - let poise_damage = PoiseChange { - amount: -(mass.0 * vel.magnitude_squared() / 1500.0) as i32, - source: PoiseSource::Falling, - }; - let poise_change = poise_damage.modify_poise_damage(inventories.get(entity)); + let poise_damage = -(mass.0 * vel.magnitude_squared() / 1500.0); + let poise_change = Poise::apply_poise_reduction(poise_damage, inventories.get(entity)); poise.change_by(poise_change, Vec3::unit_z()); } } diff --git a/server/src/state_ext.rs b/server/src/state_ext.rs index ea791d163a..78f0bc6e49 100644 --- a/server/src/state_ext.rs +++ b/server/src/state_ext.rs @@ -142,9 +142,9 @@ impl StateExt for State { .get_mut(entity) .map(|mut health| health.change_by(change)); }, - Effect::PoiseChange(poise_damage) => { + Effect::Poise(poise) => { let inventories = self.ecs().read_storage::(); - let change = poise_damage.modify_poise_damage(inventories.get(entity)); + let change = Poise::apply_poise_reduction(poise, inventories.get(entity)); // Check to make sure the entity is not already stunned if let Some(character_state) = self .ecs() diff --git a/server/src/sys/object.rs b/server/src/sys/object.rs index 0daf0a081f..9736d157cc 100644 --- a/server/src/sys/object.rs +++ b/server/src/sys/object.rs @@ -1,5 +1,5 @@ use common::{ - comp::{Object, PhysicsState, PoiseChange, PoiseSource, Pos, Vel}, + comp::{Object, PhysicsState, Pos, Vel}, effect::Effect, event::{EventBus, ServerEvent}, resources::DeltaTime, @@ -56,10 +56,7 @@ impl<'a> System<'a> for Sys { kind: DamageKind::Energy, value: 40.0, })), - RadiusEffect::Entity(Effect::PoiseChange(PoiseChange { - source: PoiseSource::Explosion, - amount: -100, - })), + RadiusEffect::Entity(Effect::Poise(-100.0)), RadiusEffect::TerrainDestruction(4.0), ], radius: 12.0, @@ -151,10 +148,7 @@ impl<'a> System<'a> for Sys { kind: DamageKind::Energy, value: 5.0, })), - RadiusEffect::Entity(Effect::PoiseChange(PoiseChange { - source: PoiseSource::Explosion, - amount: -40, - })), + RadiusEffect::Entity(Effect::Poise(-40.0)), RadiusEffect::TerrainDestruction(4.0), ], radius: 12.0, diff --git a/voxygen/egui/src/lib.rs b/voxygen/egui/src/lib.rs index 74636be28e..e0a74c225f 100644 --- a/voxygen/egui/src/lib.rs +++ b/voxygen/egui/src/lib.rs @@ -603,8 +603,8 @@ fn selected_entity_window( ui.label("State"); poise_state_label(ui, poise); ui.end_row(); - two_col_row(ui, "Current", format!("{}/{}", poise.current(), poise.maximum())); - two_col_row(ui, "Base Max", poise.base_max().to_string()); + two_col_row(ui, "Current", format!("{:.1}/{:.1}", poise.current(), poise.maximum())); + two_col_row(ui, "Base Max", format!("{:.1}", poise.base_max())); }); }); }