From c48c022f7e970a2b753ede1763223d47ab5d5acb Mon Sep 17 00:00:00 2001 From: Sam Date: Sat, 31 Oct 2020 17:34:08 -0500 Subject: [PATCH] Separated out health from stats component. --- client/src/lib.rs | 10 +- common/src/comp/health.rs | 129 +++ common/src/comp/mod.rs | 4 +- common/src/comp/stats.rs | 135 +--- common/src/event.rs | 1 + common/src/msg/ecs_packet.rs | 5 + common/src/state.rs | 1 + common/src/sys/agent.rs | 43 +- common/src/sys/beam.rs | 14 +- common/src/sys/buff.rs | 31 +- common/src/sys/character_behavior.rs | 14 +- common/src/sys/melee.rs | 12 +- common/src/sys/shockwave.rs | 14 +- common/src/sys/stats.rs | 45 +- server/src/cmd.rs | 40 +- server/src/events/entity_creation.rs | 7 +- server/src/events/entity_manipulation.rs | 27 +- server/src/events/inventory_manip.rs | 8 +- server/src/events/mod.rs | 3 +- .../src/persistence/character/conversions.rs | 4 +- server/src/state_ext.rs | 7 +- server/src/sys/message.rs | 743 ++++++++++++++++++ server/src/sys/msg/in_game.rs | 10 +- server/src/sys/sentinel.rs | 17 +- server/src/sys/terrain.rs | 7 +- .../audio/sfx/event_mapper/combat/tests.rs | 2 + voxygen/src/ecs/sys/floater.rs | 33 +- voxygen/src/hud/group.rs | 137 ++-- voxygen/src/hud/mod.rs | 54 +- voxygen/src/hud/overhead.rs | 19 +- voxygen/src/hud/skillbar.rs | 22 +- voxygen/src/scene/figure/mod.rs | 29 +- voxygen/src/scene/mod.rs | 4 +- 33 files changed, 1228 insertions(+), 403 deletions(-) create mode 100644 common/src/comp/health.rs create mode 100644 server/src/sys/message.rs diff --git a/client/src/lib.rs b/client/src/lib.rs index 46fefef2d2..8aa29e35f3 100644 --- a/client/src/lib.rs +++ b/client/src/lib.rs @@ -583,16 +583,16 @@ impl Client { } pub fn pick_up(&mut self, entity: EcsEntity) { - // Get the stats component from the entity + // Get the health component from the entity if let Some(uid) = self.state.read_component_copied(entity) { // If we're dead, exit before sending the message if self .state .ecs() - .read_storage::() + .read_storage::() .get(self.entity) - .map_or(false, |s| s.is_dead) + .map_or(false, |h| h.is_dead) { return; } @@ -731,9 +731,9 @@ impl Client { if self .state .ecs() - .read_storage::() + .read_storage::() .get(self.entity) - .map_or(false, |s| s.is_dead) + .map_or(false, |h| h.is_dead) { self.send_msg(ClientGeneral::ControlEvent(ControlEvent::Respawn)); } diff --git a/common/src/comp/health.rs b/common/src/comp/health.rs new file mode 100644 index 0000000000..7dd3ff3385 --- /dev/null +++ b/common/src/comp/health.rs @@ -0,0 +1,129 @@ +use crate::{comp::Body, sync::Uid}; +use serde::{Deserialize, Serialize}; +use specs::{Component, FlaggedStorage}; +use specs_idvs::IdvStorage; + +/// Specifies what and how much changed current health +#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq, Serialize, Deserialize)] +pub struct HealthChange { + pub amount: i32, + pub cause: HealthSource, +} + +#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq, Serialize, Deserialize)] +pub enum HealthSource { + Attack { by: Uid }, // TODO: Implement weapon + Projectile { owner: Option }, + Explosion { owner: Option }, + Energy { owner: Option }, + Buff { owner: Option }, + Suicide, + World, + Revive, + Command, + LevelUp, + Item, + Healing { by: Option }, + Unknown, +} + +#[derive(Clone, Copy, Debug, Serialize, Deserialize)] +pub struct Health { + base_max: u32, + current: u32, + maximum: u32, + pub last_change: (f64, HealthChange), + pub is_dead: bool, +} + +impl Health { + pub fn new(body: Body, level: u32) -> Self { + let mut health = Health::empty(); + + health.update_max_hp(Some(body), level); + health.set_to(health.maximum(), HealthSource::Revive); + + health + } + + pub fn empty() -> Self { + Health { + current: 0, + maximum: 0, + base_max: 0, + last_change: (0.0, HealthChange { + amount: 0, + cause: HealthSource::Revive, + }), + is_dead: false, + } + } + + pub fn current(&self) -> u32 { self.current } + + pub fn maximum(&self) -> u32 { self.maximum } + + pub fn set_to(&mut self, amount: u32, cause: HealthSource) { + let amount = amount.min(self.maximum); + self.last_change = (0.0, HealthChange { + amount: amount as i32 - self.current as i32, + cause, + }); + self.current = amount; + } + + pub fn change_by(&mut self, change: HealthChange) { + self.current = ((self.current as i32 + change.amount).max(0) as u32).min(self.maximum); + self.last_change = (0.0, change); + } + + // This function changes the modified max health value, not the base health + // value. The modified health value takes into account buffs and other temporary + // changes to max health. + pub fn set_maximum(&mut self, amount: u32) { + self.maximum = amount; + self.current = self.current.min(self.maximum); + } + + // This is private because max hp is based on the level + fn set_base_max(&mut self, amount: u32) { + self.base_max = amount; + self.current = self.current.min(self.maximum); + } + + pub fn reset_max(&mut self) { self.maximum = self.base_max; } + + pub fn should_die(&self) -> bool { self.current == 0 } + + pub fn revive(&mut self) { + self.set_to(self.maximum(), HealthSource::Revive); + self.is_dead = false; + } + + // TODO: Delete this once stat points will be a thing + pub fn update_max_hp(&mut self, body: Option, level: u32) { + if let Some(body) = body { + self.set_base_max(body.base_health() + body.base_health_increase() * level); + self.set_maximum(body.base_health() + body.base_health_increase() * level); + } + } + + pub fn with_max_health(mut self, amount: u32) -> Self { + self.maximum = amount; + self.current = amount; + self + } +} + +impl Component for Health { + type Storage = FlaggedStorage>; +} + +#[derive(Copy, Clone, Debug, Serialize, Deserialize)] +pub struct Dying { + pub cause: HealthSource, +} + +impl Component for Dying { + type Storage = IdvStorage; +} diff --git a/common/src/comp/mod.rs b/common/src/comp/mod.rs index 621ac92c23..d86401d67e 100644 --- a/common/src/comp/mod.rs +++ b/common/src/comp/mod.rs @@ -9,6 +9,7 @@ pub mod chat; mod controller; mod energy; pub mod group; +mod health; mod inputs; mod inventory; mod last; @@ -45,6 +46,7 @@ pub use controller::{ }; pub use energy::{Energy, EnergyChange, EnergySource}; pub use group::Group; +pub use health::{Health, HealthChange, HealthSource}; pub use inputs::CanBuild; pub use inventory::{ item, @@ -59,5 +61,5 @@ pub use player::Player; pub use projectile::Projectile; pub use shockwave::{Shockwave, ShockwaveHitEntities}; pub use skills::{Skill, SkillGroup, SkillGroupType, SkillSet}; -pub use stats::{Exp, HealthChange, HealthSource, Level, Stats}; +pub use stats::{Exp, Level, Stats}; pub use visual::{LightAnimation, LightEmitter}; diff --git a/common/src/comp/stats.rs b/common/src/comp/stats.rs index 7289bfbdf7..11b063689a 100644 --- a/common/src/comp/stats.rs +++ b/common/src/comp/stats.rs @@ -1,45 +1,12 @@ use crate::{ comp, comp::{body::humanoid::Species, skills::SkillSet, Body}, - sync::Uid, }; use serde::{Deserialize, Serialize}; use specs::{Component, FlaggedStorage}; use specs_idvs::IdvStorage; use std::{error::Error, fmt}; -/// Specifies what and how much changed current health -#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq, Serialize, Deserialize)] -pub struct HealthChange { - pub amount: i32, - pub cause: HealthSource, -} - -#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq, Serialize, Deserialize)] -pub enum HealthSource { - Attack { by: Uid }, // TODO: Implement weapon - Projectile { owner: Option }, - Explosion { owner: Option }, - Energy { owner: Option }, - Buff { owner: Option }, - Suicide, - World, - Revive, - Command, - LevelUp, - Item, - Healing { by: Option }, - Unknown, -} - -#[derive(Clone, Copy, Debug, Serialize, Deserialize)] -pub struct Health { - base_max: u32, - current: u32, - maximum: u32, - pub last_change: (f64, HealthChange), -} - #[derive(Clone, Copy, Debug, Serialize, Deserialize)] pub struct Exp { current: u32, @@ -51,41 +18,6 @@ pub struct Level { amount: u32, } -impl Health { - pub fn current(&self) -> u32 { self.current } - - pub fn maximum(&self) -> u32 { self.maximum } - - pub fn set_to(&mut self, amount: u32, cause: HealthSource) { - let amount = amount.min(self.maximum); - self.last_change = (0.0, HealthChange { - amount: amount as i32 - self.current as i32, - cause, - }); - self.current = amount; - } - - pub fn change_by(&mut self, change: HealthChange) { - self.current = ((self.current as i32 + change.amount).max(0) as u32).min(self.maximum); - self.last_change = (0.0, change); - } - - // This function changes the modified max health value, not the base health - // value. The modified health value takes into account buffs and other temporary - // changes to max health. - pub fn set_maximum(&mut self, amount: u32) { - self.maximum = amount; - self.current = self.current.min(self.maximum); - } - - // This is private because max hp is based on the level - fn set_base_max(&mut self, amount: u32) { - self.base_max = amount; - self.current = self.current.min(self.maximum); - } - - pub fn reset_max(&mut self) { self.maximum = self.base_max; } -} #[derive(Debug)] pub enum StatChangeError { Underflow, @@ -139,35 +71,15 @@ impl Level { #[derive(Clone, Debug, Serialize, Deserialize)] pub struct Stats { pub name: String, - pub health: Health, pub level: Level, pub exp: Exp, pub skill_set: SkillSet, pub endurance: u32, pub fitness: u32, pub willpower: u32, - pub is_dead: bool, pub body_type: Body, } -impl Stats { - pub fn should_die(&self) -> bool { self.health.current == 0 } - - pub fn revive(&mut self) { - self.health - .set_to(self.health.maximum(), HealthSource::Revive); - self.is_dead = false; - } - - // TODO: Delete this once stat points will be a thing - pub fn update_max_hp(&mut self, body: Body) { - self.health - .set_base_max(body.base_health() + body.base_health_increase() * self.level.amount); - self.health - .set_maximum(body.base_health() + body.base_health_increase() * self.level.amount); - } -} - impl Stats { pub fn new(name: String, body: Body) -> Self { let species = if let comp::Body::Humanoid(hbody) = body { @@ -189,17 +101,8 @@ impl Stats { None => (0, 0, 0), }; - let mut stats = Self { + Self { name, - health: Health { - current: 0, - maximum: 0, - base_max: 0, - last_change: (0.0, HealthChange { - amount: 0, - cause: HealthSource::Revive, - }), - }, level: Level { amount: 1 }, exp: Exp { current: 0, @@ -209,17 +112,8 @@ impl Stats { endurance, fitness, willpower, - is_dead: false, body_type: body, - }; - - stats.update_max_hp(body); - - stats - .health - .set_to(stats.health.maximum(), HealthSource::Revive); - - stats + } } /// Creates an empty `Stats` instance - used during character loading from @@ -227,15 +121,6 @@ impl Stats { pub fn empty() -> Self { Self { name: "".to_owned(), - health: Health { - current: 0, - maximum: 0, - base_max: 0, - last_change: (0.0, HealthChange { - amount: 0, - cause: HealthSource::Revive, - }), - }, level: Level { amount: 1 }, exp: Exp { current: 0, @@ -245,27 +130,11 @@ impl Stats { endurance: 0, fitness: 0, willpower: 0, - is_dead: false, body_type: comp::Body::Humanoid(comp::body::humanoid::Body::random()), } } - - pub fn with_max_health(mut self, amount: u32) -> Self { - self.health.maximum = amount; - self.health.current = amount; - self - } } impl Component for Stats { type Storage = FlaggedStorage>; } - -#[derive(Copy, Clone, Debug, Serialize, Deserialize)] -pub struct Dying { - pub cause: HealthSource, -} - -impl Component for Dying { - type Storage = IdvStorage; -} diff --git a/common/src/event.rs b/common/src/event.rs index cd1d8aac12..41965191da 100644 --- a/common/src/event.rs +++ b/common/src/event.rs @@ -92,6 +92,7 @@ pub enum ServerEvent { CreateNpc { pos: comp::Pos, stats: comp::Stats, + health: comp::Health, loadout: comp::Loadout, body: comp::Body, agent: Option, diff --git a/common/src/msg/ecs_packet.rs b/common/src/msg/ecs_packet.rs index 41ab1bda71..4aff0fbdd5 100644 --- a/common/src/msg/ecs_packet.rs +++ b/common/src/msg/ecs_packet.rs @@ -15,6 +15,7 @@ sum_type! { Stats(comp::Stats), Buffs(comp::Buffs), Energy(comp::Energy), + Health(comp::Health), LightEmitter(comp::LightEmitter), Item(comp::Item), Scale(comp::Scale), @@ -45,6 +46,7 @@ sum_type! { Stats(PhantomData), Buffs(PhantomData), Energy(PhantomData), + Health(PhantomData), LightEmitter(PhantomData), Item(PhantomData), Scale(PhantomData), @@ -75,6 +77,7 @@ impl sync::CompPacket for EcsCompPacket { EcsCompPacket::Stats(comp) => sync::handle_insert(comp, entity, world), EcsCompPacket::Buffs(comp) => sync::handle_insert(comp, entity, world), EcsCompPacket::Energy(comp) => sync::handle_insert(comp, entity, world), + EcsCompPacket::Health(comp) => sync::handle_insert(comp, entity, world), EcsCompPacket::LightEmitter(comp) => sync::handle_insert(comp, entity, world), EcsCompPacket::Item(comp) => sync::handle_insert(comp, entity, world), EcsCompPacket::Scale(comp) => sync::handle_insert(comp, entity, world), @@ -103,6 +106,7 @@ impl sync::CompPacket for EcsCompPacket { EcsCompPacket::Stats(comp) => sync::handle_modify(comp, entity, world), EcsCompPacket::Buffs(comp) => sync::handle_modify(comp, entity, world), EcsCompPacket::Energy(comp) => sync::handle_modify(comp, entity, world), + EcsCompPacket::Health(comp) => sync::handle_modify(comp, entity, world), EcsCompPacket::LightEmitter(comp) => sync::handle_modify(comp, entity, world), EcsCompPacket::Item(comp) => sync::handle_modify(comp, entity, world), EcsCompPacket::Scale(comp) => sync::handle_modify(comp, entity, world), @@ -131,6 +135,7 @@ impl sync::CompPacket for EcsCompPacket { EcsCompPhantom::Stats(_) => sync::handle_remove::(entity, world), EcsCompPhantom::Buffs(_) => sync::handle_remove::(entity, world), EcsCompPhantom::Energy(_) => sync::handle_remove::(entity, world), + EcsCompPhantom::Health(_) => sync::handle_remove::(entity, world), EcsCompPhantom::LightEmitter(_) => { sync::handle_remove::(entity, world) }, diff --git a/common/src/state.rs b/common/src/state.rs index cbf00daaf2..c3ec24b5eb 100644 --- a/common/src/state.rs +++ b/common/src/state.rs @@ -114,6 +114,7 @@ impl State { ecs.register::(); ecs.register::(); ecs.register::(); + ecs.register::(); ecs.register::(); ecs.register::(); ecs.register::(); diff --git a/common/src/sys/agent.rs b/common/src/sys/agent.rs index fa9af1771e..27e4a8c600 100644 --- a/common/src/sys/agent.rs +++ b/common/src/sys/agent.rs @@ -6,7 +6,7 @@ use crate::{ group::Invite, item::{tool::ToolKind, ItemKind}, Agent, Alignment, Body, CharacterState, ControlAction, ControlEvent, Controller, Energy, - GroupManip, LightEmitter, Loadout, MountState, Ori, PhysicsState, Pos, Scale, Stats, + GroupManip, Health, LightEmitter, Loadout, MountState, Ori, PhysicsState, Pos, Scale, UnresolvedChatMsg, Vel, }, event::{EventBus, ServerEvent}, @@ -46,7 +46,7 @@ impl<'a> System<'a> for Sys { ReadStorage<'a, Vel>, ReadStorage<'a, Ori>, ReadStorage<'a, Scale>, - ReadStorage<'a, Stats>, + ReadStorage<'a, Health>, ReadStorage<'a, Loadout>, ReadStorage<'a, PhysicsState>, ReadStorage<'a, Uid>, @@ -76,7 +76,7 @@ impl<'a> System<'a> for Sys { velocities, orientations, scales, - stats, + healths, loadouts, physics_states, uids, @@ -261,8 +261,8 @@ impl<'a> System<'a> for Sys { } }, Activity::Follow { target, chaser } => { - if let (Some(tgt_pos), _tgt_stats) = - (positions.get(*target), stats.get(*target)) + if let (Some(tgt_pos), _tgt_health) = + (positions.get(*target), healths.get(*target)) { let dist = pos.0.distance(tgt_pos.0); // Follow, or return to idle @@ -329,9 +329,9 @@ impl<'a> System<'a> for Sys { _ => Tactic::Melee, }; - if let (Some(tgt_pos), Some(tgt_stats), tgt_alignment) = ( + if let (Some(tgt_pos), Some(tgt_health), tgt_alignment) = ( positions.get(*target), - stats.get(*target), + healths.get(*target), alignments.get(*target).copied().unwrap_or( uids.get(*target) .copied() @@ -346,7 +346,7 @@ impl<'a> System<'a> for Sys { // Don't attack entities we are passive towards // TODO: This is here, it's a bit of a hack if let Some(alignment) = alignment { - if alignment.passive_towards(tgt_alignment) || tgt_stats.is_dead { + if alignment.passive_towards(tgt_alignment) || tgt_health.is_dead { do_idle = true; break 'activity; } @@ -354,9 +354,9 @@ impl<'a> System<'a> for Sys { let dist_sqrd = pos.0.distance_squared(tgt_pos.0); - let damage = stats + let damage = healths .get(entity) - .map(|s| s.health.current() as f32 / s.health.maximum() as f32) + .map(|h| h.current() as f32 / h.maximum() as f32) .unwrap_or(0.5); // Flee @@ -557,9 +557,9 @@ impl<'a> System<'a> for Sys { if choose_target { // Search for new targets (this looks expensive, but it's only run occasionally) // TODO: Replace this with a better system that doesn't consider *all* entities - let closest_entity = (&entities, &positions, &stats, alignments.maybe(), char_states.maybe()) + let closest_entity = (&entities, &positions, &healths, alignments.maybe(), char_states.maybe()) .join() - .filter(|(e, e_pos, e_stats, e_alignment, char_state)| { + .filter(|(e, e_pos, e_health, e_alignment, char_state)| { let mut search_dist = SEARCH_DIST; let mut listen_dist = LISTEN_DIST; if char_state.map_or(false, |c_s| c_s.is_stealthy()) { @@ -573,7 +573,7 @@ impl<'a> System<'a> for Sys { // Within listen distance || e_pos.0.distance_squared(pos.0) < listen_dist.powf(2.0)) && *e != entity - && !e_stats.is_dead + && !e_health.is_dead && alignment .and_then(|a| e_alignment.map(|b| a.hostile_towards(*b))) .unwrap_or(false) @@ -602,20 +602,20 @@ impl<'a> System<'a> for Sys { // last!) --- // Attack a target that's attacking us - if let Some(my_stats) = stats.get(entity) { + if let Some(my_health) = healths.get(entity) { // Only if the attack was recent - if my_stats.health.last_change.0 < 3.0 { + if my_health.last_change.0 < 3.0 { if let comp::HealthSource::Attack { by } | comp::HealthSource::Projectile { owner: Some(by) } | comp::HealthSource::Energy { owner: Some(by) } | comp::HealthSource::Buff { owner: Some(by) } | comp::HealthSource::Explosion { owner: Some(by) } = - my_stats.health.last_change.1.cause + my_health.last_change.1.cause { if !agent.activity.is_attack() { if let Some(attacker) = uid_allocator.retrieve_entity_internal(by.id()) { - if stats.get(attacker).map_or(false, |a| !a.is_dead) { + if healths.get(attacker).map_or(false, |a| !a.is_dead) { match agent.activity { Activity::Attack { target, .. } if target == attacker => {}, _ => { @@ -658,12 +658,9 @@ impl<'a> System<'a> for Sys { } // Attack owner's attacker - let owner_stats = stats.get(owner)?; - if owner_stats.health.last_change.0 < 5.0 - && owner_stats.health.last_change.1.amount < 0 - { - if let comp::HealthSource::Attack { by } = - owner_stats.health.last_change.1.cause + let owner_health = healths.get(owner)?; + if owner_health.last_change.0 < 5.0 && owner_health.last_change.1.amount < 0 { + if let comp::HealthSource::Attack { by } = owner_health.last_change.1.cause { if !agent.activity.is_attack() { let attacker = uid_allocator.retrieve_entity_internal(by.id())?; diff --git a/common/src/sys/beam.rs b/common/src/sys/beam.rs index 143179a131..2f81ce7bc3 100644 --- a/common/src/sys/beam.rs +++ b/common/src/sys/beam.rs @@ -1,7 +1,7 @@ use crate::{ comp::{ - group, Beam, BeamSegment, Body, CharacterState, Energy, EnergyChange, EnergySource, - HealthChange, HealthSource, Last, Loadout, Ori, Pos, Scale, Stats, + group, Beam, BeamSegment, Body, CharacterState, Energy, EnergyChange, EnergySource, Health, + HealthChange, HealthSource, Last, Loadout, Ori, Pos, Scale, }, event::{EventBus, ServerEvent}, state::{DeltaTime, Time}, @@ -30,7 +30,7 @@ impl<'a> System<'a> for Sys { ReadStorage<'a, Ori>, ReadStorage<'a, Scale>, ReadStorage<'a, Body>, - ReadStorage<'a, Stats>, + ReadStorage<'a, Health>, ReadStorage<'a, Loadout>, ReadStorage<'a, group::Group>, ReadStorage<'a, CharacterState>, @@ -53,7 +53,7 @@ impl<'a> System<'a> for Sys { orientations, scales, bodies, - stats, + healths, loadouts, groups, character_states, @@ -124,7 +124,7 @@ impl<'a> System<'a> for Sys { ori_b, scale_b_maybe, character_b, - stats_b, + health_b, body_b, ) in ( &entities, @@ -135,7 +135,7 @@ impl<'a> System<'a> for Sys { &orientations, scales.maybe(), character_states.maybe(), - &stats, + &healths, &bodies, ) .join() @@ -152,7 +152,7 @@ impl<'a> System<'a> for Sys { // Check if it is a hit let hit = entity != b - && !stats_b.is_dead + && !health_b.is_dead // Collision shapes && (sphere_wedge_cylinder_collision(pos.0, frame_start_dist, frame_end_dist, *ori.0, beam_segment.angle, pos_b.0, rad_b, height_b) || last_pos_b_maybe.map_or(false, |pos_maybe| {sphere_wedge_cylinder_collision(pos.0, frame_start_dist, frame_end_dist, *ori.0, beam_segment.angle, (pos_maybe.0).0, rad_b, height_b)})); diff --git a/common/src/sys/buff.rs b/common/src/sys/buff.rs index 3cff89f8fc..271f19d49a 100644 --- a/common/src/sys/buff.rs +++ b/common/src/sys/buff.rs @@ -1,7 +1,7 @@ use crate::{ comp::{ - BuffCategory, BuffChange, BuffEffect, BuffId, BuffSource, Buffs, HealthChange, - HealthSource, Loadout, ModifierKind, Stats, + BuffCategory, BuffChange, BuffEffect, BuffId, BuffSource, Buffs, Health, HealthChange, + HealthSource, Loadout, ModifierKind, }, event::{EventBus, ServerEvent}, state::DeltaTime, @@ -19,19 +19,20 @@ impl<'a> System<'a> for Sys { Read<'a, EventBus>, ReadStorage<'a, Uid>, ReadStorage<'a, Loadout>, - WriteStorage<'a, Stats>, + WriteStorage<'a, Health>, WriteStorage<'a, Buffs>, ); fn run( &mut self, - (entities, dt, server_bus, uids, loadouts, mut stats, mut buffs): Self::SystemData, + (entities, dt, server_bus, uids, loadouts, mut healths, mut buffs): Self::SystemData, ) { let mut server_emitter = server_bus.emitter(); // Set to false to avoid spamming server buffs.set_event_emission(false); - stats.set_event_emission(false); - for (entity, buff_comp, uid, stat) in (&entities, &mut buffs, &uids, &mut stats).join() { + healths.set_event_emission(false); + for (entity, buff_comp, uid, health) in (&entities, &mut buffs, &uids, &mut healths).join() + { let mut expired_buffs = Vec::::new(); for (id, buff) in buff_comp.buffs.iter_mut() { // Tick the buff and subtract delta from it @@ -63,8 +64,8 @@ impl<'a> System<'a> for Sys { } } - // Call to reset stats to base values - stat.health.reset_max(); + // Call to reset health to base values + health.reset_max(); // Iterator over the lists of buffs by kind for buff_ids in buff_comp.kinds.values() { @@ -104,14 +105,10 @@ impl<'a> System<'a> for Sys { }, BuffEffect::MaxHealthModifier { value, kind } => match kind { ModifierKind::Multiplicative => { - stat.health.set_maximum( - (stat.health.maximum() as f32 * *value) as u32, - ); + health.set_maximum((health.maximum() as f32 * *value) as u32); }, ModifierKind::Additive => { - stat.health.set_maximum( - (stat.health.maximum() as f32 + *value) as u32, - ); + health.set_maximum((health.maximum() as f32 + *value) as u32); }, }, }; @@ -127,8 +124,8 @@ impl<'a> System<'a> for Sys { }); } - // Remove stats that don't persist on death - if stat.is_dead { + // Remove buffs that don't persist on death + if health.is_dead { server_emitter.emit(ServerEvent::Buff { entity, buff_change: BuffChange::RemoveByCategory { @@ -141,6 +138,6 @@ impl<'a> System<'a> for Sys { } // Turned back to true buffs.set_event_emission(true); - stats.set_event_emission(true); + healths.set_event_emission(true); } } diff --git a/common/src/sys/character_behavior.rs b/common/src/sys/character_behavior.rs index 8d06b8a89a..c0bb15cd57 100644 --- a/common/src/sys/character_behavior.rs +++ b/common/src/sys/character_behavior.rs @@ -1,7 +1,7 @@ use crate::{ comp::{ Attacking, Beam, Body, CharacterState, ControlAction, Controller, ControllerInputs, Energy, - Loadout, Mounting, Ori, PhysicsState, Pos, StateUpdate, Stats, Vel, + Health, Loadout, Mounting, Ori, PhysicsState, Pos, StateUpdate, Vel, }, event::{EventBus, LocalEvent, ServerEvent}, metrics::SysMetrics, @@ -58,7 +58,7 @@ pub struct JoinData<'a> { pub dt: &'a DeltaTime, pub controller: &'a Controller, pub inputs: &'a ControllerInputs, - pub stats: &'a Stats, + pub health: &'a Health, pub energy: &'a Energy, pub loadout: &'a Loadout, pub body: &'a Body, @@ -85,7 +85,7 @@ pub type JoinTuple<'a> = ( RestrictedMut<'a, Energy>, RestrictedMut<'a, Loadout>, &'a mut Controller, - &'a Stats, + &'a Health, &'a Body, &'a PhysicsState, Option<&'a Attacking>, @@ -123,7 +123,7 @@ impl<'a> JoinData<'a> { loadout: j.7.get_unchecked(), controller: j.8, inputs: &j.8.inputs, - stats: j.9, + health: j.9, body: j.10, physics: j.11, attacking: j.12, @@ -155,7 +155,7 @@ impl<'a> System<'a> for Sys { WriteStorage<'a, Energy>, WriteStorage<'a, Loadout>, WriteStorage<'a, Controller>, - ReadStorage<'a, Stats>, + ReadStorage<'a, Health>, ReadStorage<'a, Body>, ReadStorage<'a, PhysicsState>, ReadStorage<'a, Attacking>, @@ -182,7 +182,7 @@ impl<'a> System<'a> for Sys { mut energies, mut loadouts, mut controllers, - stats, + healths, bodies, physics_states, attacking_storage, @@ -206,7 +206,7 @@ impl<'a> System<'a> for Sys { &mut energies.restrict_mut(), &mut loadouts.restrict_mut(), &mut controllers, - &stats, + &healths, &bodies, &physics_states, attacking_storage.maybe(), diff --git a/common/src/sys/melee.rs b/common/src/sys/melee.rs index b974ca6bd4..93be75b795 100644 --- a/common/src/sys/melee.rs +++ b/common/src/sys/melee.rs @@ -1,5 +1,5 @@ use crate::{ - comp::{buff, group, Attacking, Body, CharacterState, Loadout, Ori, Pos, Scale, Stats}, + comp::{buff, group, Attacking, Body, CharacterState, Health, Loadout, Ori, Pos, Scale}, event::{EventBus, LocalEvent, ServerEvent}, metrics::SysMetrics, span, @@ -28,7 +28,7 @@ impl<'a> System<'a> for Sys { ReadStorage<'a, Ori>, ReadStorage<'a, Scale>, ReadStorage<'a, Body>, - ReadStorage<'a, Stats>, + ReadStorage<'a, Health>, ReadStorage<'a, Loadout>, ReadStorage<'a, group::Group>, ReadStorage<'a, CharacterState>, @@ -47,7 +47,7 @@ impl<'a> System<'a> for Sys { orientations, scales, bodies, - stats, + healths, loadouts, groups, character_states, @@ -75,14 +75,14 @@ impl<'a> System<'a> for Sys { attack.applied = true; // Go through all other entities - for (b, uid_b, pos_b, ori_b, scale_b_maybe, character_b, stats_b, body_b) in ( + for (b, uid_b, pos_b, ori_b, scale_b_maybe, character_b, health_b, body_b) in ( &entities, &uids, &positions, &orientations, scales.maybe(), character_states.maybe(), - &stats, + &healths, &bodies, ) .join() @@ -99,7 +99,7 @@ impl<'a> System<'a> for Sys { // Check if it is a hit if entity != b - && !stats_b.is_dead + && !health_b.is_dead // Spherical wedge shaped attack field && pos.0.distance_squared(pos_b.0) < (rad_b + scale * attack.range).powi(2) && ori2.angle_between(pos_b2 - pos2) < attack.max_angle + (rad_b / pos2.distance(pos_b2)).atan() diff --git a/common/src/sys/shockwave.rs b/common/src/sys/shockwave.rs index 0da87146bf..f85fe7cce6 100644 --- a/common/src/sys/shockwave.rs +++ b/common/src/sys/shockwave.rs @@ -1,7 +1,7 @@ use crate::{ comp::{ - group, Body, CharacterState, HealthSource, Last, Loadout, Ori, PhysicsState, Pos, Scale, - Shockwave, ShockwaveHitEntities, Stats, + group, Body, CharacterState, Health, HealthSource, Last, Loadout, Ori, PhysicsState, Pos, + Scale, Shockwave, ShockwaveHitEntities, }, event::{EventBus, LocalEvent, ServerEvent}, state::{DeltaTime, Time}, @@ -31,7 +31,7 @@ impl<'a> System<'a> for Sys { ReadStorage<'a, Ori>, ReadStorage<'a, Scale>, ReadStorage<'a, Body>, - ReadStorage<'a, Stats>, + ReadStorage<'a, Health>, ReadStorage<'a, Loadout>, ReadStorage<'a, group::Group>, ReadStorage<'a, CharacterState>, @@ -55,7 +55,7 @@ impl<'a> System<'a> for Sys { orientations, scales, bodies, - stats, + healths, loadouts, groups, character_states, @@ -133,7 +133,7 @@ impl<'a> System<'a> for Sys { ori_b, scale_b_maybe, character_b, - stats_b, + health_b, body_b, physics_state_b, ) in ( @@ -145,7 +145,7 @@ impl<'a> System<'a> for Sys { &orientations, scales.maybe(), character_states.maybe(), - &stats, + &healths, &bodies, &physics_states, ) @@ -179,7 +179,7 @@ impl<'a> System<'a> for Sys { // Check if it is a hit let hit = entity != b - && !stats_b.is_dead + && !health_b.is_dead // Collision shapes && { // TODO: write code to collide rect with the arc strip so that we can do diff --git a/common/src/sys/stats.rs b/common/src/sys/stats.rs index bf05054cb6..e76a900e8c 100644 --- a/common/src/sys/stats.rs +++ b/common/src/sys/stats.rs @@ -1,5 +1,5 @@ use crate::{ - comp::{CharacterState, Energy, EnergyChange, EnergySource, HealthSource, Stats}, + comp::{CharacterState, Energy, EnergyChange, EnergySource, Health, HealthSource, Stats}, event::{EventBus, ServerEvent}, metrics::SysMetrics, span, @@ -20,42 +20,59 @@ impl<'a> System<'a> for Sys { ReadExpect<'a, SysMetrics>, ReadStorage<'a, CharacterState>, WriteStorage<'a, Stats>, + WriteStorage<'a, Health>, WriteStorage<'a, Energy>, ); fn run( &mut self, - (entities, dt, server_event_bus, sys_metrics, character_states, mut stats, mut energies): Self::SystemData, + ( + entities, + dt, + server_event_bus, + sys_metrics, + character_states, + mut stats, + mut healths, + mut energies, + ): Self::SystemData, ) { let start_time = std::time::Instant::now(); span!(_guard, "run", "stats::Sys::run"); let mut server_event_emitter = server_event_bus.emitter(); // Increment last change timer - stats.set_event_emission(false); // avoid unnecessary syncing - for stat in (&mut stats).join() { - stat.health.last_change.0 += f64::from(dt.0); + healths.set_event_emission(false); // avoid unnecessary syncing + for health in (&mut healths).join() { + health.last_change.0 += f64::from(dt.0); } - stats.set_event_emission(true); + healths.set_event_emission(true); // Update stats - for (entity, mut stats) in (&entities, &mut stats.restrict_mut()).join() { + for (entity, mut stats, mut health) in ( + &entities, + &mut stats.restrict_mut(), + &mut healths.restrict_mut(), + ) + .join() + { let (set_dead, level_up) = { let stat = stats.get_unchecked(); + let health = health.get_unchecked(); ( - stat.should_die() && !stat.is_dead, + health.should_die() && !health.is_dead, stat.exp.current() >= stat.exp.maximum(), ) }; if set_dead { - let stat = stats.get_mut_unchecked(); + let health = health.get_mut_unchecked(); server_event_emitter.emit(ServerEvent::Destroy { entity, - cause: stat.health.last_change.1.cause, + cause: health.last_change.1.cause, }); - stat.is_dead = true; + health.is_dead = true; } if level_up { @@ -67,9 +84,9 @@ impl<'a> System<'a> for Sys { server_event_emitter.emit(ServerEvent::LevelUp(entity, stat.level.level())); } - stat.update_max_hp(stat.body_type); - stat.health - .set_to(stat.health.maximum(), HealthSource::LevelUp); + let health = health.get_mut_unchecked(); + health.update_max_hp(Some(stat.body_type), stat.level.level()); + health.set_to(health.maximum(), HealthSource::LevelUp); } } diff --git a/server/src/cmd.rs b/server/src/cmd.rs index 2db160010e..683915b11c 100644 --- a/server/src/cmd.rs +++ b/server/src/cmd.rs @@ -400,9 +400,9 @@ fn handle_kill( server .state .ecs_mut() - .write_storage::() + .write_storage::() .get_mut(target) - .map(|s| s.health.set_to(0, reason)); + .map(|h| h.set_to(0, reason)); } fn handle_time( @@ -471,13 +471,13 @@ fn handle_health( action: &ChatCommand, ) { if let Ok(hp) = scan_fmt!(&args, &action.arg_fmt(), u32) { - if let Some(stats) = server + if let Some(health) = server .state .ecs() - .write_storage::() + .write_storage::() .get_mut(target) { - stats.health.set_to(hp * 10, comp::HealthSource::Command); + health.set_to(hp * 10, comp::HealthSource::Command); } else { server.notify_client( client, @@ -656,6 +656,7 @@ fn handle_spawn( .create_npc( pos, comp::Stats::new(get_npc_name(id).into(), body), + comp::Health::new(body, 1), LoadoutBuilder::build_loadout(body, alignment, None, false) .build(), body, @@ -762,9 +763,11 @@ fn handle_spawn_training_dummy( // Level 0 will prevent exp gain from kill stats.level.set_level(0); + let health = comp::Health::new(body, 0); + server .state - .create_npc(pos, stats, comp::Loadout::default(), body) + .create_npc(pos, stats, health, comp::Loadout::default(), body) .with(comp::Vel(vel)) .with(comp::MountState::Unmounted) .build(); @@ -924,12 +927,12 @@ fn handle_kill_npcs( _action: &ChatCommand, ) { let ecs = server.state.ecs(); - let mut stats = ecs.write_storage::(); + let mut healths = ecs.write_storage::(); let players = ecs.read_storage::(); let mut count = 0; - for (stats, ()) in (&mut stats, !&players).join() { + for (health, ()) in (&mut healths, !&players).join() { count += 1; - stats.health.set_to(0, comp::HealthSource::Command); + health.set_to(0, comp::HealthSource::Command); } let text = if count > 0 { format!("Destroyed {} NPCs.", count) @@ -1702,6 +1705,8 @@ fn handle_set_level( PlayerListUpdate::LevelChange(uid, lvl), )); + let body_type: Option; + if let Some(stats) = server .state .ecs_mut() @@ -1709,13 +1714,20 @@ fn handle_set_level( .get_mut(player) { stats.level.set_level(lvl); - - stats.update_max_hp(stats.body_type); - stats - .health - .set_to(stats.health.maximum(), comp::HealthSource::LevelUp); + body_type = Some(stats.body_type); } else { error_msg = Some(ChatType::CommandError.server_msg("Player has no stats!")); + body_type = None; + } + + if let Some(health) = server + .state + .ecs_mut() + .write_storage::() + .get_mut(player) + { + health.update_max_hp(body_type, lvl); + health.set_to(health.maximum(), comp::HealthSource::LevelUp); } }, Err(e) => { diff --git a/server/src/events/entity_creation.rs b/server/src/events/entity_creation.rs index 9f52028e5c..5126406c04 100644 --- a/server/src/events/entity_creation.rs +++ b/server/src/events/entity_creation.rs @@ -3,8 +3,8 @@ use common::{ character::CharacterId, comp::{ self, beam, humanoid::DEFAULT_HUMANOID_EYE_HEIGHT, shockwave, Agent, Alignment, Body, - Gravity, Item, ItemDrop, LightEmitter, Loadout, Ori, Pos, Projectile, Scale, Stats, Vel, - WaypointArea, + Gravity, Health, Item, ItemDrop, LightEmitter, Loadout, Ori, Pos, Projectile, Scale, Stats, + Vel, WaypointArea, }, outcome::Outcome, util::Dir, @@ -37,6 +37,7 @@ pub fn handle_create_npc( server: &mut Server, pos: Pos, stats: Stats, + health: Health, loadout: Loadout, body: Body, agent: impl Into>, @@ -55,7 +56,7 @@ pub fn handle_create_npc( let entity = server .state - .create_npc(pos, stats, loadout, body) + .create_npc(pos, stats, health, loadout, body) .with(scale) .with(alignment); diff --git a/server/src/events/entity_manipulation.rs b/server/src/events/entity_manipulation.rs index 2f0e36269e..e38107577f 100644 --- a/server/src/events/entity_manipulation.rs +++ b/server/src/events/entity_manipulation.rs @@ -8,8 +8,8 @@ use common::{ comp::{ self, buff, chat::{KillSource, KillType}, - object, Alignment, Body, Energy, EnergyChange, Group, HealthChange, HealthSource, Item, - Player, Pos, Stats, + object, Alignment, Body, Energy, EnergyChange, Group, Health, HealthChange, HealthSource, + Item, Player, Pos, Stats, }, lottery::Lottery, msg::{PlayerListUpdate, ServerGeneral}, @@ -28,11 +28,10 @@ use tracing::error; use vek::Vec3; pub fn handle_damage(server: &Server, uid: Uid, change: HealthChange) { - let state = &server.state; - let ecs = state.ecs(); + let ecs = &server.state.ecs(); if let Some(entity) = ecs.entity_from_uid(uid.into()) { - if let Some(stats) = ecs.write_storage::().get_mut(entity) { - stats.health.change_by(change); + if let Some(health) = ecs.write_storage::().get_mut(entity) { + health.change_by(change); } } } @@ -453,7 +452,7 @@ pub fn handle_destroy(server: &mut Server, entity: EcsEntity, cause: HealthSourc pub fn handle_land_on_ground(server: &Server, entity: EcsEntity, vel: Vec3) { let state = &server.state; if vel.z <= -30.0 { - if let Some(stats) = state.ecs().write_storage::().get_mut(entity) { + if let Some(health) = state.ecs().write_storage::().get_mut(entity) { let falldmg = (vel.z.powi(2) / 20.0 - 40.0) * 10.0; let damage = Damage { source: DamageSource::Falling, @@ -461,7 +460,7 @@ pub fn handle_land_on_ground(server: &Server, entity: EcsEntity, vel: Vec3) }; let loadouts = state.ecs().read_storage::(); let change = damage.modify_damage(false, loadouts.get(entity), None); - stats.health.change_by(change); + health.change_by(change); } } } @@ -483,9 +482,9 @@ pub fn handle_respawn(server: &Server, entity: EcsEntity) { state .ecs() - .write_storage::() + .write_storage::() .get_mut(entity) - .map(|stats| stats.revive()); + .map(|health| health.revive()); state .ecs() .write_storage::() @@ -550,19 +549,19 @@ pub fn handle_explosion( for effect in explosion.effects { match effect { RadiusEffect::Damages(damages) => { - for (entity_b, pos_b, ori_b, character_b, stats_b, loadout_b) in ( + for (entity_b, pos_b, ori_b, character_b, health_b, loadout_b) in ( &ecs.entities(), &ecs.read_storage::(), &ecs.read_storage::(), ecs.read_storage::().maybe(), - &mut ecs.write_storage::(), + &mut ecs.write_storage::(), ecs.read_storage::().maybe(), ) .join() { let distance_squared = pos.distance_squared(pos_b.0); // Check if it is a hit - if !stats_b.is_dead + if !health_b.is_dead // RADIUS && distance_squared < explosion.radius.powi(2) { @@ -592,7 +591,7 @@ pub fn handle_explosion( let change = damage.modify_damage(block, loadout_b, owner); if change.amount != 0 { - stats_b.health.change_by(change); + health_b.change_by(change); if let Some(owner) = owner_entity { if let Some(energy) = ecs.write_storage::().get_mut(owner) diff --git a/server/src/events/inventory_manip.rs b/server/src/events/inventory_manip.rs index a9b936da70..f6b517321a 100644 --- a/server/src/events/inventory_manip.rs +++ b/server/src/events/inventory_manip.rs @@ -67,10 +67,10 @@ pub fn handle_inventory(server: &mut Server, entity: EcsEntity, manip: comp::Inv return; }; - // Grab the stats from the player and check if the player is dead. - let stats = state.ecs().read_storage::(); - if let Some(entity_stats) = stats.get(entity) { - if entity_stats.is_dead { + // Grab the health from the player and check if the player is dead. + let healths = state.ecs().read_storage::(); + if let Some(entity_health) = healths.get(entity) { + if entity_health.is_dead { debug!("Failed to pick up item as the player is dead"); return; // If dead, don't continue } diff --git a/server/src/events/mod.rs b/server/src/events/mod.rs index 358363c4a5..7c083d8be0 100644 --- a/server/src/events/mod.rs +++ b/server/src/events/mod.rs @@ -109,6 +109,7 @@ impl Server { ServerEvent::CreateNpc { pos, stats, + health, loadout, body, agent, @@ -116,7 +117,7 @@ impl Server { scale, drop_item, } => handle_create_npc( - self, pos, stats, loadout, body, agent, alignment, scale, drop_item, + self, pos, stats, health, loadout, body, agent, alignment, scale, drop_item, ), ServerEvent::CreateWaypoint(pos) => handle_create_waypoint(self, pos), ServerEvent::ClientDisconnect(entity) => { diff --git a/server/src/persistence/character/conversions.rs b/server/src/persistence/character/conversions.rs index fea9f8af05..79f2d6093c 100644 --- a/server/src/persistence/character/conversions.rs +++ b/server/src/persistence/character/conversions.rs @@ -318,11 +318,11 @@ pub fn convert_stats_from_database(stats: &Stats, alias: String) -> common::comp new_stats.level.set_level(stats.level as u32); new_stats.exp.update_maximum(stats.level as u32); new_stats.exp.set_current(stats.exp as u32); - new_stats.update_max_hp(new_stats.body_type); + /*new_stats.update_max_hp(new_stats.body_type); new_stats.health.set_to( new_stats.health.maximum(), common::comp::HealthSource::Revive, - ); + );*/ new_stats.endurance = stats.endurance as u32; new_stats.fitness = stats.fitness as u32; new_stats.willpower = stats.willpower as u32; diff --git a/server/src/state_ext.rs b/server/src/state_ext.rs index 46fe38fb12..cd806f88f6 100644 --- a/server/src/state_ext.rs +++ b/server/src/state_ext.rs @@ -26,6 +26,7 @@ pub trait StateExt { &mut self, pos: comp::Pos, stats: comp::Stats, + health: comp::Health, loadout: comp::Loadout, body: comp::Body, ) -> EcsEntityBuilder; @@ -74,9 +75,9 @@ impl StateExt for State { match effect { Effect::Health(change) => { self.ecs() - .write_storage::() + .write_storage::() .get_mut(entity) - .map(|stats| stats.health.change_by(change)); + .map(|health| health.change_by(change)); }, Effect::Xp(xp) => { self.ecs() @@ -91,6 +92,7 @@ impl StateExt for State { &mut self, pos: comp::Pos, stats: comp::Stats, + health: comp::Health, loadout: comp::Loadout, body: comp::Body, ) -> EcsEntityBuilder { @@ -107,6 +109,7 @@ impl StateExt for State { .with(comp::Controller::default()) .with(body) .with(stats) + .with(health) .with(comp::Alignment::Npc) .with(comp::Energy::new(body.base_energy())) .with(comp::Gravity(1.0)) diff --git a/server/src/sys/message.rs b/server/src/sys/message.rs new file mode 100644 index 0000000000..5fdb00985d --- /dev/null +++ b/server/src/sys/message.rs @@ -0,0 +1,743 @@ +use super::SysTimer; +use crate::{ + alias_validator::AliasValidator, + character_creator, + client::Client, + login_provider::LoginProvider, + metrics::{NetworkRequestMetrics, PlayerMetrics}, + persistence::character_loader::CharacterLoader, + EditableSettings, Settings, +}; +use common::{ + comp::{ + Admin, CanBuild, ChatMode, ChatType, ControlEvent, Controller, ForceUpdate, Health, Ori, + Player, Pos, Stats, UnresolvedChatMsg, Vel, + }, + event::{EventBus, ServerEvent}, + msg::{ + validate_chat_msg, CharacterInfo, ChatMsgValidationError, ClientGeneral, ClientInGame, + ClientRegister, DisconnectReason, PingMsg, PlayerInfo, PlayerListUpdate, RegisterError, + ServerGeneral, ServerRegisterAnswer, MAX_BYTES_CHAT_MSG, + }, + span, + state::{BlockChange, Time}, + sync::Uid, + terrain::{TerrainChunkSize, TerrainGrid}, + vol::{ReadVol, RectVolSize}, +}; +use futures_executor::block_on; +use futures_timer::Delay; +use futures_util::{select, FutureExt}; +use hashbrown::HashMap; +use specs::{ + Entities, Join, Read, ReadExpect, ReadStorage, System, Write, WriteExpect, WriteStorage, +}; +use tracing::{debug, error, info, trace, warn}; + +impl Sys { + #[allow(clippy::too_many_arguments)] + fn handle_client_msg( + server_emitter: &mut common::event::Emitter<'_, ServerEvent>, + new_chat_msgs: &mut Vec<(Option, UnresolvedChatMsg)>, + entity: specs::Entity, + client: &mut Client, + player_metrics: &ReadExpect<'_, PlayerMetrics>, + uids: &ReadStorage<'_, Uid>, + chat_modes: &ReadStorage<'_, ChatMode>, + msg: ClientGeneral, + ) -> Result<(), crate::error::Error> { + match msg { + ClientGeneral::ChatMsg(message) => { + if client.registered { + match validate_chat_msg(&message) { + Ok(()) => { + if let Some(from) = uids.get(entity) { + let mode = chat_modes.get(entity).cloned().unwrap_or_default(); + let msg = mode.new_message(*from, message); + new_chat_msgs.push((Some(entity), msg)); + } else { + error!("Could not send message. Missing player uid"); + } + }, + Err(ChatMsgValidationError::TooLong) => { + let max = MAX_BYTES_CHAT_MSG; + let len = message.len(); + warn!(?len, ?max, "Received a chat message that's too long") + }, + } + } + }, + ClientGeneral::Disconnect => { + client.send_msg(ServerGeneral::Disconnect(DisconnectReason::Requested)); + }, + ClientGeneral::Terminate => { + debug!(?entity, "Client send message to termitate session"); + player_metrics + .clients_disconnected + .with_label_values(&["gracefully"]) + .inc(); + server_emitter.emit(ServerEvent::ClientDisconnect(entity)); + }, + _ => unreachable!("not a client_general msg"), + } + Ok(()) + } + + #[allow(clippy::too_many_arguments)] + fn handle_client_in_game_msg( + server_emitter: &mut common::event::Emitter<'_, ServerEvent>, + entity: specs::Entity, + client: &mut Client, + terrain: &ReadExpect<'_, TerrainGrid>, + network_metrics: &ReadExpect<'_, NetworkRequestMetrics>, + can_build: &ReadStorage<'_, CanBuild>, + force_updates: &ReadStorage<'_, ForceUpdate>, + stats: &mut WriteStorage<'_, Stats>, + healths: &mut WriteStorage<'_, Health>, + block_changes: &mut Write<'_, BlockChange>, + positions: &mut WriteStorage<'_, Pos>, + velocities: &mut WriteStorage<'_, Vel>, + orientations: &mut WriteStorage<'_, Ori>, + players: &mut WriteStorage<'_, Player>, + controllers: &mut WriteStorage<'_, Controller>, + settings: &Read<'_, Settings>, + msg: ClientGeneral, + ) -> Result<(), crate::error::Error> { + if client.in_game.is_none() { + debug!(?entity, "client is not in_game, ignoring msg"); + trace!(?msg, "ignored msg content"); + if matches!(msg, ClientGeneral::TerrainChunkRequest{ .. }) { + network_metrics.chunks_request_dropped.inc(); + } + return Ok(()); + } + match msg { + // Go back to registered state (char selection screen) + ClientGeneral::ExitInGame => { + client.in_game = None; + server_emitter.emit(ServerEvent::ExitIngame { entity }); + client.send_msg(ServerGeneral::ExitInGameSuccess); + }, + ClientGeneral::SetViewDistance(view_distance) => { + players.get_mut(entity).map(|player| { + player.view_distance = Some( + settings + .max_view_distance + .map(|max| view_distance.min(max)) + .unwrap_or(view_distance), + ) + }); + + //correct client if its VD is to high + if settings + .max_view_distance + .map(|max| view_distance > max) + .unwrap_or(false) + { + client.send_msg(ServerGeneral::SetViewDistance( + settings.max_view_distance.unwrap_or(0), + )); + } + }, + ClientGeneral::ControllerInputs(inputs) => { + if let Some(ClientInGame::Character) = client.in_game { + if let Some(controller) = controllers.get_mut(entity) { + controller.inputs.update_with_new(inputs); + } + } + }, + ClientGeneral::ControlEvent(event) => { + if let Some(ClientInGame::Character) = client.in_game { + // Skip respawn if client entity is alive + if let ControlEvent::Respawn = event { + if healths.get(entity).map_or(true, |h| !h.is_dead) { + //Todo: comment why return! + return Ok(()); + } + } + if let Some(controller) = controllers.get_mut(entity) { + controller.events.push(event); + } + } + }, + ClientGeneral::ControlAction(event) => { + if let Some(ClientInGame::Character) = client.in_game { + if let Some(controller) = controllers.get_mut(entity) { + controller.actions.push(event); + } + } + }, + ClientGeneral::PlayerPhysics { pos, vel, ori } => { + if let Some(ClientInGame::Character) = client.in_game { + if force_updates.get(entity).is_none() + && healths.get(entity).map_or(true, |h| !h.is_dead) + { + let _ = positions.insert(entity, pos); + let _ = velocities.insert(entity, vel); + let _ = orientations.insert(entity, ori); + } + } + }, + ClientGeneral::BreakBlock(pos) => { + if let Some(block) = can_build.get(entity).and_then(|_| terrain.get(pos).ok()) { + block_changes.set(pos, block.into_vacant()); + } + }, + ClientGeneral::PlaceBlock(pos, block) => { + if can_build.get(entity).is_some() { + block_changes.try_set(pos, block); + } + }, + ClientGeneral::TerrainChunkRequest { key } => { + let in_vd = if let (Some(view_distance), Some(pos)) = ( + players.get(entity).and_then(|p| p.view_distance), + positions.get(entity), + ) { + pos.0.xy().map(|e| e as f64).distance( + key.map(|e| e as f64 + 0.5) * TerrainChunkSize::RECT_SIZE.map(|e| e as f64), + ) < (view_distance as f64 - 1.0 + 2.5 * 2.0_f64.sqrt()) + * TerrainChunkSize::RECT_SIZE.x as f64 + } else { + true + }; + if in_vd { + match terrain.get_key(key) { + Some(chunk) => { + network_metrics.chunks_served_from_memory.inc(); + client.send_msg(ServerGeneral::TerrainChunkUpdate { + key, + chunk: Ok(Box::new(chunk.clone())), + }) + }, + None => { + network_metrics.chunks_generation_triggered.inc(); + server_emitter.emit(ServerEvent::ChunkRequest(entity, key)) + }, + } + } else { + network_metrics.chunks_request_dropped.inc(); + } + }, + ClientGeneral::UnlockSkill(skill) => { + stats + .get_mut(entity) + .map(|s| s.skill_set.unlock_skill(skill)); + }, + ClientGeneral::RefundSkill(skill) => { + stats + .get_mut(entity) + .map(|s| s.skill_set.refund_skill(skill)); + }, + ClientGeneral::UnlockSkillGroup(skill_group_type) => { + stats + .get_mut(entity) + .map(|s| s.skill_set.unlock_skill_group(skill_group_type)); + }, + _ => unreachable!("not a client_in_game msg"), + } + Ok(()) + } + + #[allow(clippy::too_many_arguments)] + fn handle_client_character_screen_msg( + server_emitter: &mut common::event::Emitter<'_, ServerEvent>, + new_chat_msgs: &mut Vec<(Option, UnresolvedChatMsg)>, + entity: specs::Entity, + client: &mut Client, + character_loader: &ReadExpect<'_, CharacterLoader>, + uids: &ReadStorage<'_, Uid>, + players: &mut WriteStorage<'_, Player>, + editable_settings: &ReadExpect<'_, EditableSettings>, + alias_validator: &ReadExpect<'_, AliasValidator>, + msg: ClientGeneral, + ) -> Result<(), crate::error::Error> { + match msg { + // Request spectator state + ClientGeneral::Spectate if client.registered => { + client.in_game = Some(ClientInGame::Spectator) + }, + ClientGeneral::Spectate => debug!("dropped Spectate msg from unregistered client"), + ClientGeneral::Character(character_id) + if client.registered && client.in_game.is_none() => + { + if let Some(player) = players.get(entity) { + // Send a request to load the character's component data from the + // DB. Once loaded, persisted components such as stats and inventory + // will be inserted for the entity + character_loader.load_character_data( + entity, + player.uuid().to_string(), + character_id, + ); + + // Start inserting non-persisted/default components for the entity + // while we load the DB data + server_emitter.emit(ServerEvent::InitCharacterData { + entity, + character_id, + }); + + // Give the player a welcome message + if !editable_settings.server_description.is_empty() { + client.send_msg( + ChatType::CommandInfo + .server_msg(String::from(&*editable_settings.server_description)), + ); + } + + if !client.login_msg_sent { + if let Some(player_uid) = uids.get(entity) { + new_chat_msgs.push((None, UnresolvedChatMsg { + chat_type: ChatType::Online(*player_uid), + message: "".to_string(), + })); + + client.login_msg_sent = true; + } + } + } else { + client.send_msg(ServerGeneral::CharacterDataLoadError(String::from( + "Failed to fetch player entity", + ))) + } + } + ClientGeneral::Character(_) => { + let registered = client.registered; + let in_game = client.in_game; + debug!(?registered, ?in_game, "dropped Character msg from client"); + }, + ClientGeneral::RequestCharacterList => { + if let Some(player) = players.get(entity) { + character_loader.load_character_list(entity, player.uuid().to_string()) + } + }, + ClientGeneral::CreateCharacter { alias, tool, body } => { + if let Err(error) = alias_validator.validate(&alias) { + debug!(?error, ?alias, "denied alias as it contained a banned word"); + client.send_msg(ServerGeneral::CharacterActionError(error.to_string())); + } else if let Some(player) = players.get(entity) { + character_creator::create_character( + entity, + player.uuid().to_string(), + alias, + tool, + body, + character_loader, + ); + } + }, + ClientGeneral::DeleteCharacter(character_id) => { + if let Some(player) = players.get(entity) { + character_loader.delete_character( + entity, + player.uuid().to_string(), + character_id, + ); + } + }, + _ => unreachable!("not a client_character_screen msg"), + } + Ok(()) + } + + #[allow(clippy::too_many_arguments)] + fn handle_ping_msg(client: &mut Client, msg: PingMsg) -> Result<(), crate::error::Error> { + match msg { + PingMsg::Ping => client.send_msg(PingMsg::Pong), + PingMsg::Pong => {}, + } + Ok(()) + } + + #[allow(clippy::too_many_arguments)] + fn handle_register_msg( + player_list: &HashMap, + new_players: &mut Vec, + entity: specs::Entity, + client: &mut Client, + player_metrics: &ReadExpect<'_, PlayerMetrics>, + login_provider: &mut WriteExpect<'_, LoginProvider>, + admins: &mut WriteStorage<'_, Admin>, + players: &mut WriteStorage<'_, Player>, + editable_settings: &ReadExpect<'_, EditableSettings>, + msg: ClientRegister, + ) -> Result<(), crate::error::Error> { + let (username, uuid) = match login_provider.try_login( + &msg.token_or_username, + &*editable_settings.admins, + &*editable_settings.whitelist, + &*editable_settings.banlist, + ) { + Err(err) => { + client + .register_stream + .send(ServerRegisterAnswer::Err(err))?; + return Ok(()); + }, + Ok((username, uuid)) => (username, uuid), + }; + + const INITIAL_VD: Option = Some(5); //will be changed after login + let player = Player::new(username, None, INITIAL_VD, uuid); + let is_admin = editable_settings.admins.contains(&uuid); + + if !player.is_valid() { + // Invalid player + client + .register_stream + .send(ServerRegisterAnswer::Err(RegisterError::InvalidCharacter))?; + return Ok(()); + } + + if !client.registered && client.in_game.is_none() { + // Add Player component to this client + let _ = players.insert(entity, player); + player_metrics.players_connected.inc(); + + // Give the Admin component to the player if their name exists in + // admin list + if is_admin { + let _ = admins.insert(entity, Admin); + } + + // Tell the client its request was successful. + client.registered = true; + client.register_stream.send(ServerRegisterAnswer::Ok(()))?; + + // Send initial player list + client.send_msg(ServerGeneral::PlayerListUpdate(PlayerListUpdate::Init( + player_list.clone(), + ))); + + // Add to list to notify all clients of the new player + new_players.push(entity); + } + Ok(()) + } + + ///We needed to move this to a async fn, if we would use a async closures + /// the compiler generates to much recursion and fails to compile this + #[allow(clippy::too_many_arguments)] + async fn handle_messages( + server_emitter: &mut common::event::Emitter<'_, ServerEvent>, + new_chat_msgs: &mut Vec<(Option, UnresolvedChatMsg)>, + player_list: &HashMap, + new_players: &mut Vec, + entity: specs::Entity, + client: &mut Client, + cnt: &mut u64, + character_loader: &ReadExpect<'_, CharacterLoader>, + terrain: &ReadExpect<'_, TerrainGrid>, + network_metrics: &ReadExpect<'_, NetworkRequestMetrics>, + player_metrics: &ReadExpect<'_, PlayerMetrics>, + uids: &ReadStorage<'_, Uid>, + can_build: &ReadStorage<'_, CanBuild>, + force_updates: &ReadStorage<'_, ForceUpdate>, + stats: &mut WriteStorage<'_, Stats>, + healths: &mut WriteStorage<'_, Health>, + chat_modes: &ReadStorage<'_, ChatMode>, + login_provider: &mut WriteExpect<'_, LoginProvider>, + block_changes: &mut Write<'_, BlockChange>, + admins: &mut WriteStorage<'_, Admin>, + positions: &mut WriteStorage<'_, Pos>, + velocities: &mut WriteStorage<'_, Vel>, + orientations: &mut WriteStorage<'_, Ori>, + players: &mut WriteStorage<'_, Player>, + controllers: &mut WriteStorage<'_, Controller>, + settings: &Read<'_, Settings>, + editable_settings: &ReadExpect<'_, EditableSettings>, + alias_validator: &ReadExpect<'_, AliasValidator>, + ) -> Result<(), crate::error::Error> { + let (mut b1, mut b2, mut b3, mut b4, mut b5) = ( + client.network_error, + client.network_error, + client.network_error, + client.network_error, + client.network_error, + ); + loop { + /* + waiting for 1 of the 5 streams to return a massage asynchronous. + If so, handle that msg type. This code will be refactored soon + */ + + let q1 = Client::internal_recv(&mut b1, &mut client.general_stream); + let q2 = Client::internal_recv(&mut b2, &mut client.in_game_stream); + let q3 = Client::internal_recv(&mut b3, &mut client.character_screen_stream); + let q4 = Client::internal_recv(&mut b4, &mut client.ping_stream); + let q5 = Client::internal_recv(&mut b5, &mut client.register_stream); + + let (m1, m2, m3, m4, m5) = select!( + msg = q1.fuse() => (Some(msg), None, None, None, None), + msg = q2.fuse() => (None, Some(msg), None, None, None), + msg = q3.fuse() => (None, None, Some(msg), None, None), + msg = q4.fuse() => (None, None, None, Some(msg), None), + msg = q5.fuse() => (None, None, None, None,Some(msg)), + ); + *cnt += 1; + if let Some(msg) = m1 { + client.network_error |= b1; + Self::handle_client_msg( + server_emitter, + new_chat_msgs, + entity, + client, + player_metrics, + uids, + chat_modes, + msg?, + )?; + } + if let Some(msg) = m2 { + client.network_error |= b2; + Self::handle_client_in_game_msg( + server_emitter, + entity, + client, + terrain, + network_metrics, + can_build, + force_updates, + stats, + healths, + block_changes, + positions, + velocities, + orientations, + players, + controllers, + settings, + msg?, + )?; + } + if let Some(msg) = m3 { + client.network_error |= b3; + Self::handle_client_character_screen_msg( + server_emitter, + new_chat_msgs, + entity, + client, + character_loader, + uids, + players, + editable_settings, + alias_validator, + msg?, + )?; + } + if let Some(msg) = m4 { + client.network_error |= b4; + Self::handle_ping_msg(client, msg?)?; + } + if let Some(msg) = m5 { + client.network_error |= b5; + Self::handle_register_msg( + player_list, + new_players, + entity, + client, + player_metrics, + login_provider, + admins, + players, + editable_settings, + msg?, + )?; + } + } + } +} + +/// This system will handle new messages from clients +pub struct Sys; +impl<'a> System<'a> for Sys { + #[allow(clippy::type_complexity)] // TODO: Pending review in #587 + type SystemData = ( + Entities<'a>, + Read<'a, EventBus>, + Read<'a, Time>, + ReadExpect<'a, CharacterLoader>, + ReadExpect<'a, TerrainGrid>, + ReadExpect<'a, NetworkRequestMetrics>, + ReadExpect<'a, PlayerMetrics>, + Write<'a, SysTimer>, + ReadStorage<'a, Uid>, + ReadStorage<'a, CanBuild>, + ReadStorage<'a, ForceUpdate>, + WriteStorage<'a, Stats>, + WriteStorage<'a, Health>, + ReadStorage<'a, ChatMode>, + WriteExpect<'a, LoginProvider>, + Write<'a, BlockChange>, + WriteStorage<'a, Admin>, + WriteStorage<'a, Pos>, + WriteStorage<'a, Vel>, + WriteStorage<'a, Ori>, + WriteStorage<'a, Player>, + WriteStorage<'a, Client>, + WriteStorage<'a, Controller>, + Read<'a, Settings>, + ReadExpect<'a, EditableSettings>, + ReadExpect<'a, AliasValidator>, + ); + + #[allow(clippy::match_ref_pats)] // TODO: Pending review in #587 + #[allow(clippy::single_char_pattern)] // TODO: Pending review in #587 + #[allow(clippy::single_match)] // TODO: Pending review in #587 + fn run( + &mut self, + ( + entities, + server_event_bus, + time, + character_loader, + terrain, + network_metrics, + player_metrics, + mut timer, + uids, + can_build, + force_updates, + mut stats, + mut healths, + chat_modes, + mut accounts, + mut block_changes, + mut admins, + mut positions, + mut velocities, + mut orientations, + mut players, + mut clients, + mut controllers, + settings, + editable_settings, + alias_validator, + ): Self::SystemData, + ) { + span!(_guard, "run", "message::Sys::run"); + timer.start(); + + let mut server_emitter = server_event_bus.emitter(); + + let mut new_chat_msgs = Vec::new(); + + // Player list to send new players. + let player_list = (&uids, &players, stats.maybe(), admins.maybe()) + .join() + .map(|(uid, player, stats, admin)| { + (*uid, PlayerInfo { + is_online: true, + is_admin: admin.is_some(), + player_alias: player.alias.clone(), + character: stats.map(|stats| CharacterInfo { + name: stats.name.clone(), + level: stats.level.level(), + }), + }) + }) + .collect::>(); + // List of new players to update player lists of all clients. + let mut new_players = Vec::new(); + + for (entity, client) in (&entities, &mut clients).join() { + let mut cnt = 0; + + let network_err: Result<(), crate::error::Error> = block_on(async { + //TIMEOUT 0.02 ms for msg handling + let work_future = Self::handle_messages( + &mut server_emitter, + &mut new_chat_msgs, + &player_list, + &mut new_players, + entity, + client, + &mut cnt, + &character_loader, + &terrain, + &network_metrics, + &player_metrics, + &uids, + &can_build, + &force_updates, + &mut stats, + &mut healths, + &chat_modes, + &mut accounts, + &mut block_changes, + &mut admins, + &mut positions, + &mut velocities, + &mut orientations, + &mut players, + &mut controllers, + &settings, + &editable_settings, + &alias_validator, + ); + select!( + _ = Delay::new(std::time::Duration::from_micros(20)).fuse() => Ok(()), + err = work_future.fuse() => err, + ) + }); + + // Network error + if network_err.is_err() { + debug!(?entity, "postbox error with client, disconnecting"); + player_metrics + .clients_disconnected + .with_label_values(&["network_error"]) + .inc(); + server_emitter.emit(ServerEvent::ClientDisconnect(entity)); + } else if cnt > 0 { + // Update client ping. + client.last_ping = time.0 + } else if time.0 - client.last_ping > settings.client_timeout.as_secs() as f64 + // Timeout + { + info!(?entity, "timeout error with client, disconnecting"); + player_metrics + .clients_disconnected + .with_label_values(&["timeout"]) + .inc(); + server_emitter.emit(ServerEvent::ClientDisconnect(entity)); + } else if time.0 - client.last_ping > settings.client_timeout.as_secs() as f64 * 0.5 { + // Try pinging the client if the timeout is nearing. + client.send_msg(PingMsg::Ping); + } + } + + // Handle new players. + // Tell all clients to add them to the player list. + for entity in new_players { + if let (Some(uid), Some(player)) = (uids.get(entity), players.get(entity)) { + let msg = + ServerGeneral::PlayerListUpdate(PlayerListUpdate::Add(*uid, PlayerInfo { + player_alias: player.alias.clone(), + is_online: true, + is_admin: admins.get(entity).is_some(), + character: None, // new players will be on character select. + })); + for client in (&mut clients).join().filter(|c| c.registered) { + client.send_msg(msg.clone()) + } + } + } + + // Handle new chat messages. + for (entity, msg) in new_chat_msgs { + // Handle chat commands. + if msg.message.starts_with("/") { + if let (Some(entity), true) = (entity, msg.message.len() > 1) { + let argv = String::from(&msg.message[1..]); + server_emitter.emit(ServerEvent::ChatCmd(entity, argv)); + } + } else { + // Send chat message + server_emitter.emit(ServerEvent::Chat(msg)); + } + } + + timer.end() + } +} diff --git a/server/src/sys/msg/in_game.rs b/server/src/sys/msg/in_game.rs index ae49d6d8e2..c7a3bcf644 100644 --- a/server/src/sys/msg/in_game.rs +++ b/server/src/sys/msg/in_game.rs @@ -1,7 +1,7 @@ use super::super::SysTimer; use crate::{client::Client, metrics::NetworkRequestMetrics, presence::Presence, Settings}; use common::{ - comp::{CanBuild, ControlEvent, Controller, ForceUpdate, Ori, Pos, Stats, Vel}, + comp::{CanBuild, ControlEvent, Controller, ForceUpdate, Health, Ori, Pos, Stats, Vel}, event::{EventBus, ServerEvent}, msg::{ClientGeneral, PresenceKind, ServerGeneral}, span, @@ -24,6 +24,7 @@ impl Sys { can_build: &ReadStorage<'_, CanBuild>, force_updates: &ReadStorage<'_, ForceUpdate>, stats: &mut WriteStorage<'_, Stats>, + healths: &ReadStorage<'_, Health>, block_changes: &mut Write<'_, BlockChange>, positions: &mut WriteStorage<'_, Pos>, velocities: &mut WriteStorage<'_, Vel>, @@ -78,7 +79,7 @@ impl Sys { if matches!(presence.kind, PresenceKind::Character(_)) { // Skip respawn if client entity is alive if let ControlEvent::Respawn = event { - if stats.get(entity).map_or(true, |s| !s.is_dead) { + if healths.get(entity).map_or(true, |h| !h.is_dead) { //Todo: comment why return! return Ok(()); } @@ -98,7 +99,7 @@ impl Sys { ClientGeneral::PlayerPhysics { pos, vel, ori } => { if matches!(presence.kind, PresenceKind::Character(_)) && force_updates.get(entity).is_none() - && stats.get(entity).map_or(true, |s| !s.is_dead) + && healths.get(entity).map_or(true, |h| !h.is_dead) { let _ = positions.insert(entity, pos); let _ = velocities.insert(entity, vel); @@ -176,6 +177,7 @@ impl<'a> System<'a> for Sys { ReadStorage<'a, CanBuild>, ReadStorage<'a, ForceUpdate>, WriteStorage<'a, Stats>, + ReadStorage<'a, Health>, Write<'a, BlockChange>, WriteStorage<'a, Pos>, WriteStorage<'a, Vel>, @@ -197,6 +199,7 @@ impl<'a> System<'a> for Sys { can_build, force_updates, mut stats, + healths, mut block_changes, mut positions, mut velocities, @@ -226,6 +229,7 @@ impl<'a> System<'a> for Sys { &can_build, &force_updates, &mut stats, + &healths, &mut block_changes, &mut positions, &mut velocities, diff --git a/server/src/sys/sentinel.rs b/server/src/sys/sentinel.rs index 68d568b66c..a61dc65b6f 100644 --- a/server/src/sys/sentinel.rs +++ b/server/src/sys/sentinel.rs @@ -1,9 +1,9 @@ use super::SysTimer; use common::{ comp::{ - BeamSegment, Body, Buffs, CanBuild, CharacterState, Collider, Energy, Gravity, Group, Item, - LightEmitter, Loadout, Mass, MountState, Mounting, Ori, Player, Pos, Scale, Shockwave, - Stats, Sticky, Vel, + BeamSegment, Body, Buffs, CanBuild, CharacterState, Collider, Energy, Gravity, Group, + Health, Item, LightEmitter, Loadout, Mass, MountState, Mounting, Ori, Player, Pos, Scale, + Shockwave, Stats, Sticky, Vel, }, msg::EcsCompPacket, span, @@ -46,6 +46,7 @@ pub struct TrackedComps<'a> { pub stats: ReadStorage<'a, Stats>, pub buffs: ReadStorage<'a, Buffs>, pub energy: ReadStorage<'a, Energy>, + pub health: ReadStorage<'a, Health>, pub can_build: ReadStorage<'a, CanBuild>, pub light_emitter: ReadStorage<'a, LightEmitter>, pub item: ReadStorage<'a, Item>, @@ -94,6 +95,10 @@ impl<'a> TrackedComps<'a> { .get(entity) .cloned() .map(|c| comps.push(c.into())); + self.health + .get(entity) + .cloned() + .map(|c| comps.push(c.into())); self.can_build .get(entity) .cloned() @@ -164,6 +169,7 @@ pub struct ReadTrackers<'a> { pub stats: ReadExpect<'a, UpdateTracker>, pub buffs: ReadExpect<'a, UpdateTracker>, pub energy: ReadExpect<'a, UpdateTracker>, + pub health: ReadExpect<'a, UpdateTracker>, pub can_build: ReadExpect<'a, UpdateTracker>, pub light_emitter: ReadExpect<'a, UpdateTracker>, pub item: ReadExpect<'a, UpdateTracker>, @@ -195,6 +201,7 @@ impl<'a> ReadTrackers<'a> { .with_component(&comps.uid, &*self.stats, &comps.stats, filter) .with_component(&comps.uid, &*self.buffs, &comps.buffs, filter) .with_component(&comps.uid, &*self.energy, &comps.energy, filter) + .with_component(&comps.uid, &*self.health, &comps.health, filter) .with_component(&comps.uid, &*self.can_build, &comps.can_build, filter) .with_component( &comps.uid, @@ -233,6 +240,7 @@ pub struct WriteTrackers<'a> { stats: WriteExpect<'a, UpdateTracker>, buffs: WriteExpect<'a, UpdateTracker>, energy: WriteExpect<'a, UpdateTracker>, + health: WriteExpect<'a, UpdateTracker>, can_build: WriteExpect<'a, UpdateTracker>, light_emitter: WriteExpect<'a, UpdateTracker>, item: WriteExpect<'a, UpdateTracker>, @@ -258,6 +266,7 @@ fn record_changes(comps: &TrackedComps, trackers: &mut WriteTrackers) { trackers.stats.record_changes(&comps.stats); trackers.buffs.record_changes(&comps.buffs); trackers.energy.record_changes(&comps.energy); + trackers.health.record_changes(&comps.health); trackers.can_build.record_changes(&comps.can_build); trackers.light_emitter.record_changes(&comps.light_emitter); trackers.item.record_changes(&comps.item); @@ -296,6 +305,7 @@ fn record_changes(comps: &TrackedComps, trackers: &mut WriteTrackers) { log_counts!(player, "Players"); log_counts!(stats, "Stats"); log_counts!(energy, "Energies"); + log_vounts!(health, "Healths"); log_counts!(light_emitter, "Light emitters"); log_counts!(item, "Items"); log_counts!(scale, "Scales"); @@ -319,6 +329,7 @@ pub fn register_trackers(world: &mut World) { world.register_tracker::(); world.register_tracker::(); world.register_tracker::(); + world.register_tracker::(); world.register_tracker::(); world.register_tracker::(); world.register_tracker::(); diff --git a/server/src/sys/terrain.rs b/server/src/sys/terrain.rs index b2da237a3c..ec12b0bb34 100644 --- a/server/src/sys/terrain.rs +++ b/server/src/sys/terrain.rs @@ -146,11 +146,7 @@ impl<'a> System<'a> for Sys { LoadoutBuilder::build_loadout(body, alignment, main_tool, entity.is_giant) .build(); - stats.update_max_hp(stats.body_type); - - stats - .health - .set_to(stats.health.maximum(), comp::HealthSource::Revive); + let health = comp::Health::new(stats.body_type, stats.level.level()); let can_speak = match body { comp::Body::Humanoid(_) => alignment == comp::Alignment::Npc, @@ -174,6 +170,7 @@ impl<'a> System<'a> for Sys { server_emitter.emit(ServerEvent::CreateNpc { pos: Pos(entity.pos), stats, + health, loadout, agent: if entity.has_agency { Some(comp::Agent::new(entity.pos, can_speak, &body)) diff --git a/voxygen/src/audio/sfx/event_mapper/combat/tests.rs b/voxygen/src/audio/sfx/event_mapper/combat/tests.rs index 448c976f6b..5a01451d15 100644 --- a/voxygen/src/audio/sfx/event_mapper/combat/tests.rs +++ b/voxygen/src/audio/sfx/event_mapper/combat/tests.rs @@ -143,6 +143,7 @@ fn matches_ability_stage() { speed_increase: 0.05, max_speed_increase: 1.8, is_interruptible: true, + ability_key: states::utils::AbilityKey::Mouse1, }, stage: 1, combo: 0, @@ -203,6 +204,7 @@ fn ignores_different_ability_stage() { speed_increase: 0.05, max_speed_increase: 1.8, is_interruptible: true, + ability_key: states::utils::AbilityKey::Mouse1, }, stage: 1, combo: 0, diff --git a/voxygen/src/ecs/sys/floater.rs b/voxygen/src/ecs/sys/floater.rs index ad9c8c1de7..4947ddc560 100644 --- a/voxygen/src/ecs/sys/floater.rs +++ b/voxygen/src/ecs/sys/floater.rs @@ -3,7 +3,7 @@ use crate::ecs::{ ExpFloater, MyEntity, MyExpFloaterList, }; use common::{ - comp::{HealthSource, Pos, Stats}, + comp::{Health, HealthSource, Pos, Stats}, state::DeltaTime, sync::Uid, }; @@ -25,19 +25,30 @@ impl<'a> System<'a> for Sys { ReadStorage<'a, Uid>, ReadStorage<'a, Pos>, ReadStorage<'a, Stats>, + ReadStorage<'a, Health>, WriteStorage<'a, HpFloaterList>, ); #[allow(clippy::blocks_in_if_conditions)] // TODO: Pending review in #587 fn run( &mut self, - (entities, my_entity, dt, mut my_exp_floater_list, uids, pos, stats, mut hp_floater_lists): Self::SystemData, + ( + entities, + my_entity, + dt, + mut my_exp_floater_list, + uids, + pos, + stats, + healths, + mut hp_floater_lists, + ): Self::SystemData, ) { - // Add hp floater lists to all entities with stats and a position + // Add hp floater lists to all entities with health and a position // Note: necessary in order to know last_hp - for (entity, last_hp) in (&entities, &stats, &pos, !&hp_floater_lists) + for (entity, last_hp) in (&entities, &healths, &pos, !&hp_floater_lists) .join() - .map(|(e, s, _, _)| (e, s.health.current())) + .map(|(e, h, _, _)| (e, h.current())) .collect::>() { let _ = hp_floater_lists.insert(entity, HpFloaterList { @@ -49,9 +60,7 @@ impl<'a> System<'a> for Sys { // Add hp floaters to all entities that have been damaged let my_uid = uids.get(my_entity.0); - for (entity, health, hp_floater_list) in (&entities, &stats, &mut hp_floater_lists) - .join() - .map(|(e, s, fl)| (e, s.health, fl)) + for (entity, health, hp_floater_list) in (&entities, &healths, &mut hp_floater_lists).join() { // Increment timer for time since last damaged by me hp_floater_list @@ -64,8 +73,8 @@ impl<'a> System<'a> for Sys { if hp_floater_list.last_hp != health.current() { hp_floater_list.last_hp = health.current(); // TODO: What if multiple health changes occurred since last check here - // Also, If we make stats store a vec of the last_changes (from say the last - // frame), what if the client receives the stats component from + // Also, If we make health store a vec of the last_changes (from say the last + // frame), what if the client receives the health component from // two different server ticks at once, then one will be lost // (tbf this is probably a rare occurance and the results // would just be a transient glitch in the display of these damage numbers) @@ -101,8 +110,8 @@ impl<'a> System<'a> for Sys { } } - // Remove floater lists on entities without stats or without position - for entity in (&entities, !&stats, &hp_floater_lists) + // Remove floater lists on entities without health or without position + for entity in (&entities, !&healths, &hp_floater_lists) .join() .map(|(e, _, _)| e) .collect::>() diff --git a/voxygen/src/hud/group.rs b/voxygen/src/hud/group.rs index 1bfeaa1cb2..1d4d35d2c0 100644 --- a/voxygen/src/hud/group.rs +++ b/voxygen/src/hud/group.rs @@ -322,6 +322,7 @@ impl<'a> Widget for Group<'a> { let client_state = self.client.state(); let stats = client_state.ecs().read_storage::(); + let healths = client_state.ecs().read_storage::(); let energy = client_state.ecs().read_storage::(); let buffs = client_state.ecs().read_storage::(); let uid_allocator = client_state @@ -338,80 +339,84 @@ impl<'a> Widget for Group<'a> { self.show.group = true; let entity = uid_allocator.retrieve_entity_internal(uid.into()); let stats = entity.and_then(|entity| stats.get(entity)); + let health = entity.and_then(|entity| healths.get(entity)); let energy = entity.and_then(|entity| energy.get(entity)); let buffs = entity.and_then(|entity| buffs.get(entity)); + let is_leader = uid == leader; + if let Some(stats) = stats { let char_name = stats.name.to_string(); - let health_perc = stats.health.current() as f64 / stats.health.maximum() as f64; - - // change panel positions when debug info is shown - let back = if i == 0 { - Image::new(self.imgs.member_bg) - .top_left_with_margins_on(ui.window, offset, 20.0) - } else { - Image::new(self.imgs.member_bg) - .down_from(state.ids.member_panels_bg[i - 1], 45.0) - }; - let hp_ani = (self.pulse * 4.0/* speed factor */).cos() * 0.5 + 0.8; //Animation timer - let crit_hp_color: Color = Color::Rgba(0.79, 0.19, 0.17, hp_ani); - let health_col = match (health_perc * 100.0) as u8 { - 0..=20 => crit_hp_color, - 21..=40 => LOW_HP_COLOR, - _ => HP_COLOR, - }; - let is_leader = uid == leader; - // Don't show panel for the player! - // Panel BG - back.w_h(152.0, 36.0) - .color(if is_leader { - Some(ERROR_COLOR) + if let Some(health) = health { + let health_perc = health.current() as f64 / health.maximum() as f64; + // change panel positions when debug info is shown + let back = if i == 0 { + Image::new(self.imgs.member_bg) + .top_left_with_margins_on(ui.window, offset, 20.0) } else { - Some(TEXT_COLOR) - }) - .set(state.ids.member_panels_bg[i], ui); - // Health - Image::new(self.imgs.bar_content) - .w_h(148.0 * health_perc, 22.0) - .color(Some(health_col)) - .top_left_with_margins_on(state.ids.member_panels_bg[i], 2.0, 2.0) - .set(state.ids.member_health[i], ui); - if stats.is_dead { - // Death Text - Text::new(&self.localized_strings.get("hud.group.dead")) - .mid_top_with_margin_on(state.ids.member_panels_bg[i], 1.0) - .font_size(20) - .font_id(self.fonts.cyri.conrod_id) - .color(KILL_COLOR) - .set(state.ids.dead_txt[i], ui); - } else { - // Health Text - let txt = format!( - "{}/{}", - stats.health.current() / 10 as u32, - stats.health.maximum() / 10 as u32, - ); - // Change font size depending on health amount - let font_size = match stats.health.maximum() { - 0..=999 => 14, - 1000..=9999 => 13, - 10000..=99999 => 12, - _ => 11, + Image::new(self.imgs.member_bg) + .down_from(state.ids.member_panels_bg[i - 1], 45.0) }; - // Change text offset depending on health amount - let txt_offset = match stats.health.maximum() { - 0..=999 => 4.0, - 1000..=9999 => 4.5, - 10000..=99999 => 5.0, - _ => 5.5, + let hp_ani = (self.pulse * 4.0/* speed factor */).cos() * 0.5 + 0.8; //Animation timer + let crit_hp_color: Color = Color::Rgba(0.79, 0.19, 0.17, hp_ani); + let health_col = match (health_perc * 100.0) as u8 { + 0..=20 => crit_hp_color, + 21..=40 => LOW_HP_COLOR, + _ => HP_COLOR, }; - Text::new(&txt) - .mid_top_with_margin_on(state.ids.member_panels_bg[i], txt_offset) - .font_size(font_size) - .font_id(self.fonts.cyri.conrod_id) - .color(Color::Rgba(1.0, 1.0, 1.0, 0.5)) - .set(state.ids.health_txt[i], ui); - }; + // Don't show panel for the player! + // Panel BG + back.w_h(152.0, 36.0) + .color(if is_leader { + Some(ERROR_COLOR) + } else { + Some(TEXT_COLOR) + }) + .set(state.ids.member_panels_bg[i], ui); + // Health + Image::new(self.imgs.bar_content) + .w_h(148.0 * health_perc, 22.0) + .color(Some(health_col)) + .top_left_with_margins_on(state.ids.member_panels_bg[i], 2.0, 2.0) + .set(state.ids.member_health[i], ui); + if health.is_dead { + // Death Text + Text::new(&self.localized_strings.get("hud.group.dead")) + .mid_top_with_margin_on(state.ids.member_panels_bg[i], 1.0) + .font_size(20) + .font_id(self.fonts.cyri.conrod_id) + .color(KILL_COLOR) + .set(state.ids.dead_txt[i], ui); + } else { + // Health Text + let txt = format!( + "{}/{}", + health.current() / 10 as u32, + health.maximum() / 10 as u32, + ); + // Change font size depending on health amount + let font_size = match health.maximum() { + 0..=999 => 14, + 1000..=9999 => 13, + 10000..=99999 => 12, + _ => 11, + }; + // Change text offset depending on health amount + let txt_offset = match health.maximum() { + 0..=999 => 4.0, + 1000..=9999 => 4.5, + 10000..=99999 => 5.0, + _ => 5.5, + }; + Text::new(&txt) + .mid_top_with_margin_on(state.ids.member_panels_bg[i], txt_offset) + .font_size(font_size) + .font_id(self.fonts.cyri.conrod_id) + .color(Color::Rgba(1.0, 1.0, 1.0, 0.5)) + .set(state.ids.health_txt[i], ui); + }; + } + // Panel Frame Image::new(self.imgs.member_frame) .w_h(152.0, 36.0) diff --git a/voxygen/src/hud/mod.rs b/voxygen/src/hud/mod.rs index 1022a935dd..66a67c4bf6 100644 --- a/voxygen/src/hud/mod.rs +++ b/voxygen/src/hud/mod.rs @@ -753,6 +753,7 @@ impl Hud { let ecs = client.state().ecs(); let pos = ecs.read_storage::(); let stats = ecs.read_storage::(); + let healths = ecs.read_storage::(); let buffs = ecs.read_storage::(); let energy = ecs.read_storage::(); let hp_floater_lists = ecs.read_storage::(); @@ -767,11 +768,10 @@ impl Hud { .get(client.entity()) .map_or(0, |stats| stats.level.level()); //self.input = client.read_storage::(); - if let Some(stats) = stats.get(me) { + if let Some(health) = healths.get(me) { // Hurt Frame - let hp_percentage = - stats.health.current() as f32 / stats.health.maximum() as f32 * 100.0; - if hp_percentage < 10.0 && !stats.is_dead { + let hp_percentage = health.current() as f32 / health.maximum() as f32 * 100.0; + if hp_percentage < 10.0 && !health.is_dead { let hurt_fade = (self.pulse * (10.0 - hp_percentage as f32) * 0.1/* speed factor */).sin() * 0.5 @@ -792,7 +792,7 @@ impl Hud { .set(self.ids.alpha_text, ui_widgets); // Death Frame - if stats.is_dead { + if health.is_dead { Image::new(self.imgs.death_bg) .wh_of(ui_widgets.window) .middle_of(ui_widgets.window) @@ -801,7 +801,7 @@ impl Hud { .set(self.ids.death_bg, ui_widgets); } // Crosshair - let show_crosshair = (info.is_aiming || info.is_first_person) && !stats.is_dead; + let show_crosshair = (info.is_aiming || info.is_first_person) && !health.is_dead; self.crosshair_opacity = Lerp::lerp( self.crosshair_opacity, if show_crosshair { 1.0 } else { 0.0 }, @@ -850,11 +850,11 @@ impl Hud { // Render Player SCT numbers let mut player_sct_bg_id_walker = self.ids.player_sct_bgs.walk(); let mut player_sct_id_walker = self.ids.player_scts.walk(); - if let (Some(HpFloaterList { floaters, .. }), Some(stats)) = ( + if let (Some(HpFloaterList { floaters, .. }), Some(health)) = ( hp_floater_lists .get(me) .filter(|fl| !fl.floaters.is_empty()), - stats.get(me), + healths.get(me), ) { if global_state.settings.gameplay.sct_player_batch { let number_speed = 100.0; // Player Batched Numbers Speed @@ -871,7 +871,7 @@ impl Hud { let hp_damage = floaters.iter().fold(0, |acc, f| f.hp_change.min(0) + acc); // Divide by 10 to stay in the same dimension as the HP display let hp_dmg_rounded_abs = ((hp_damage + 5) / 10).abs(); - let max_hp_frac = hp_damage.abs() as f32 / stats.health.maximum() as f32; + let max_hp_frac = hp_damage.abs() as f32 / health.maximum() as f32; let timer = floaters .last() .expect("There must be at least one floater") @@ -927,8 +927,7 @@ impl Hud { &mut self.ids.player_scts, &mut ui_widgets.widget_id_generator(), ); - let max_hp_frac = - floater.hp_change.abs() as f32 / stats.health.maximum() as f32; + let max_hp_frac = floater.hp_change.abs() as f32 / health.maximum() as f32; // Increase font size based on fraction of maximum health // "flashes" by having a larger size in the first 100ms let font_size = 30 @@ -1152,11 +1151,12 @@ impl Hud { let speech_bubbles = &self.speech_bubbles; // Render overhead name tags and health bars - for (pos, info, bubble, stats, _, height_offset, hpfl, in_group) in ( + for (pos, info, bubble, _, health, _, height_offset, hpfl, in_group) in ( &entities, &pos, interpolated.maybe(), &stats, + &healths, &buffs, energy.maybe(), scales.maybe(), @@ -1166,12 +1166,24 @@ impl Hud { ) .join() .filter(|t| { - let stats = t.3; + let health = t.4; let entity = t.0; - entity != me && !stats.is_dead + entity != me && !health.is_dead }) .filter_map( - |(entity, pos, interpolated, stats, buffs, energy, scale, body, hpfl, uid)| { + |( + entity, + pos, + interpolated, + stats, + health, + buffs, + energy, + scale, + body, + hpfl, + uid, + )| { // Use interpolated position if available let pos = interpolated.map_or(pos.0, |i| i.pos); let in_group = client.group_members().contains_key(uid); @@ -1183,7 +1195,7 @@ impl Hud { let display_overhead_info = (info.target_entity.map_or(false, |e| e == entity) || info.selected_entity.map_or(false, |s| s.0 == entity) - || overhead::show_healthbar(stats) + || overhead::show_healthbar(health) || in_group) && dist_sqr < (if in_group { @@ -1201,6 +1213,7 @@ impl Hud { let info = display_overhead_info.then(|| overhead::Info { name: &stats.name, stats, + health, buffs, energy, }); @@ -1216,6 +1229,7 @@ impl Hud { info, bubble, stats, + health, buffs, body.height() * scale.map_or(1.0, |s| s.0) + 0.5, hpfl, @@ -1292,7 +1306,7 @@ impl Hud { }); // Divide by 10 to stay in the same dimension as the HP display let hp_dmg_rounded_abs = ((hp_damage + 5) / 10).abs(); - let max_hp_frac = hp_damage.abs() as f32 / stats.health.maximum() as f32; + let max_hp_frac = hp_damage.abs() as f32 / health.maximum() as f32; let timer = floaters .last() .expect("There must be at least one floater") @@ -1364,7 +1378,7 @@ impl Hud { .next(&mut self.ids.sct_bgs, &mut ui_widgets.widget_id_generator()); // Calculate total change let max_hp_frac = - floater.hp_change.abs() as f32 / stats.health.maximum() as f32; + floater.hp_change.abs() as f32 / health.maximum() as f32; // Increase font size based on fraction of maximum health // "flashes" by having a larger size in the first 100ms let font_size = 30 @@ -1897,6 +1911,7 @@ impl Hud { let ecs = client.state().ecs(); let entity = client.entity(); let stats = ecs.read_storage::(); + let healths = ecs.read_storage::(); let loadouts = ecs.read_storage::(); let energies = ecs.read_storage::(); let character_states = ecs.read_storage::(); @@ -1904,6 +1919,7 @@ impl Hud { let inventories = ecs.read_storage::(); if let ( Some(stats), + Some(health), Some(loadout), Some(energy), Some(_character_state), @@ -1911,6 +1927,7 @@ impl Hud { Some(inventory), ) = ( stats.get(entity), + healths.get(entity), loadouts.get(entity), energies.get(entity), character_states.get(entity), @@ -1924,6 +1941,7 @@ impl Hud { &self.fonts, &self.rot_imgs, &stats, + &health, &loadout, &energy, //&character_state, diff --git a/voxygen/src/hud/overhead.rs b/voxygen/src/hud/overhead.rs index 7ebfb29cfc..04ff0e2ab8 100644 --- a/voxygen/src/hud/overhead.rs +++ b/voxygen/src/hud/overhead.rs @@ -8,7 +8,7 @@ use crate::{ settings::GameplaySettings, ui::{fonts::ConrodVoxygenFonts, Ingameable}, }; -use common::comp::{BuffKind, Buffs, Energy, SpeechBubble, SpeechBubbleType, Stats}; +use common::comp::{BuffKind, Buffs, Energy, Health, SpeechBubble, SpeechBubbleType, Stats}; use conrod_core::{ color, position::Align, @@ -58,12 +58,13 @@ widget_ids! { pub struct Info<'a> { pub name: &'a str, pub stats: &'a Stats, + pub health: &'a Health, pub buffs: &'a Buffs, pub energy: Option<&'a Energy>, } /// Determines whether to show the healthbar -pub fn show_healthbar(stats: &Stats) -> bool { stats.health.current() != stats.health.maximum() } +pub fn show_healthbar(health: &Health) -> bool { health.current() != health.maximum() } /// ui widget containing everything that goes over a character's head /// (Speech bubble, Name, Level, HP/energy bars, etc.) @@ -141,7 +142,7 @@ impl<'a> Ingameable for Overhead<'a> { } else { 0 } - + if show_healthbar(info.stats) { + + if show_healthbar(info.health) { 5 + if info.energy.is_some() { 1 } else { 0 } } else { 0 @@ -171,17 +172,17 @@ impl<'a> Widget for Overhead<'a> { if let Some(Info { name, stats, + health, buffs, energy, }) = self.info { // Used to set healthbar colours based on hp_percentage - let hp_percentage = - stats.health.current() as f64 / stats.health.maximum() as f64 * 100.0; + let hp_percentage = health.current() as f64 / health.maximum() as f64 * 100.0; // Compare levels to decide if a skull is shown let level_comp = stats.level.level() as i64 - self.own_level as i64; - let health_current = (stats.health.current() / 10) as f64; - let health_max = (stats.health.maximum() / 10) as f64; + let health_current = (health.current() / 10) as f64; + let health_max = (health.maximum() / 10) as f64; let name_y = if (health_current - health_max).abs() < 1e-6 { MANA_BAR_Y + 20.0 } else if level_comp > 9 && !self.in_group { @@ -302,7 +303,7 @@ impl<'a> Widget for Overhead<'a> { .parent(id) .set(state.ids.name, ui); - if show_healthbar(stats) { + if show_healthbar(health) { // Show HP Bar let hp_ani = (self.pulse * 4.0/* speed factor */).cos() * 0.5 + 1.0; //Animation timer let crit_hp_color: Color = Color::Rgba(0.79, 0.19, 0.17, hp_ani); @@ -332,7 +333,7 @@ impl<'a> Widget for Overhead<'a> { .parent(id) .set(state.ids.health_bar, ui); let mut txt = format!("{}/{}", health_cur_txt, health_max_txt); - if stats.is_dead { + if health.is_dead { txt = self.voxygen_i18n.get("hud.group.dead").to_string() }; Text::new(&txt) diff --git a/voxygen/src/hud/skillbar.rs b/voxygen/src/hud/skillbar.rs index ad8045517d..0cebc454d3 100644 --- a/voxygen/src/hud/skillbar.rs +++ b/voxygen/src/hud/skillbar.rs @@ -20,7 +20,7 @@ use common::comp::{ tool::{Tool, ToolKind}, Hands, ItemKind, }, - Energy, Inventory, Loadout, Stats, + Energy, Health, Inventory, Loadout, Stats, }; use conrod_core::{ color, @@ -124,6 +124,7 @@ pub struct Skillbar<'a> { fonts: &'a ConrodVoxygenFonts, rot_imgs: &'a ImgsRot, stats: &'a Stats, + health: &'a Health, loadout: &'a Loadout, energy: &'a Energy, // character_state: &'a CharacterState, @@ -148,6 +149,7 @@ impl<'a> Skillbar<'a> { fonts: &'a ConrodVoxygenFonts, rot_imgs: &'a ImgsRot, stats: &'a Stats, + health: &'a Health, loadout: &'a Loadout, energy: &'a Energy, // character_state: &'a CharacterState, @@ -167,6 +169,7 @@ impl<'a> Skillbar<'a> { fonts, rot_imgs, stats, + health, loadout, energy, common: widget::CommonBuilder::default(), @@ -216,11 +219,10 @@ impl<'a> Widget for Skillbar<'a> { let exp_percentage = (self.stats.exp.current() as f64) / (self.stats.exp.maximum() as f64); - let mut hp_percentage = - self.stats.health.current() as f64 / self.stats.health.maximum() as f64 * 100.0; + let mut hp_percentage = self.health.current() as f64 / self.health.maximum() as f64 * 100.0; let mut energy_percentage = self.energy.current() as f64 / self.energy.maximum() as f64 * 100.0; - if self.stats.is_dead { + if self.health.is_dead { hp_percentage = 0.0; energy_percentage = 0.0; }; @@ -293,7 +295,7 @@ impl<'a> Widget for Skillbar<'a> { .set(state.ids.level_down, ui); } // Death message - if self.stats.is_dead { + if self.health.is_dead { if let Some(key) = self .global_state .settings @@ -400,12 +402,12 @@ impl<'a> Widget for Skillbar<'a> { if let BarNumbers::Values = bar_values { let mut hp_txt = format!( "{}/{}", - (self.stats.health.current() / 10).max(1) as u32, /* Don't show 0 health for - * living players */ - (self.stats.health.maximum() / 10) as u32 + (self.health.current() / 10).max(1) as u32, /* Don't show 0 health for + * living players */ + (self.health.maximum() / 10) as u32 ); let mut energy_txt = format!("{}", energy_percentage as u32); - if self.stats.is_dead { + if self.health.is_dead { hp_txt = self.localized_strings.get("hud.group.dead").to_string(); energy_txt = self.localized_strings.get("hud.group.dead").to_string(); }; @@ -438,7 +440,7 @@ impl<'a> Widget for Skillbar<'a> { if let BarNumbers::Percent = bar_values { let mut hp_txt = format!("{}%", hp_percentage as u32); let mut energy_txt = format!("{}", energy_percentage as u32); - if self.stats.is_dead { + if self.health.is_dead { hp_txt = self.localized_strings.get("hud.group.dead").to_string(); energy_txt = self.localized_strings.get("hud.group.dead").to_string(); }; diff --git a/voxygen/src/scene/figure/mod.rs b/voxygen/src/scene/figure/mod.rs index 6d9ace7ea6..e265152427 100644 --- a/voxygen/src/scene/figure/mod.rs +++ b/voxygen/src/scene/figure/mod.rs @@ -26,8 +26,8 @@ use anim::{ use common::{ comp::{ item::{ItemKind, ToolKind}, - Body, CharacterState, Item, Last, LightAnimation, LightEmitter, Loadout, Ori, PhysicsState, - Pos, Scale, Stats, Vel, + Body, CharacterState, Health, Item, Last, LightAnimation, LightEmitter, Loadout, Ori, + PhysicsState, Pos, Scale, Vel, }, span, state::{DeltaTime, State}, @@ -545,7 +545,7 @@ impl FigureMgr { character, last_character, physics, - stats, + health, loadout, item, ), @@ -559,7 +559,7 @@ impl FigureMgr { ecs.read_storage::().maybe(), ecs.read_storage::>().maybe(), &ecs.read_storage::(), - ecs.read_storage::().maybe(), + ecs.read_storage::().maybe(), ecs.read_storage::().maybe(), ecs.read_storage::().maybe(), ) @@ -662,11 +662,11 @@ impl FigureMgr { }; // Change in health as color! - let col = stats - .map(|s| { + let col = health + .map(|h| { vek::Rgba::broadcast(1.0) + vek::Rgba::new(2.0, 2.0, 2., 0.00).map(|c| { - (c / (1.0 + DAMAGE_FADE_COEFFICIENT * s.health.last_change.0)) as f32 + (c / (1.0 + DAMAGE_FADE_COEFFICIENT * h.last_change.0)) as f32 }) }) .unwrap_or(vek::Rgba::broadcast(1.0)) @@ -2749,13 +2749,13 @@ impl FigureMgr { &ecs.read_storage::(), ecs.read_storage::().maybe(), &ecs.read_storage::(), - ecs.read_storage::().maybe(), + ecs.read_storage::().maybe(), ecs.read_storage::().maybe(), ecs.read_storage::().maybe(), ) .join() // Don't render dead entities - .filter(|(_, _, _, _, stats, _, _)| stats.map_or(true, |s| !s.is_dead)) + .filter(|(_, _, _, _, health, _, _)| health.map_or(true, |h| !h.is_dead)) .for_each(|(entity, pos, _, body, _, loadout, _)| { if let Some((locals, bone_consts, model, _)) = self.get_model_for_render( tick, @@ -2803,13 +2803,13 @@ impl FigureMgr { &ecs.read_storage::(), ecs.read_storage::().maybe(), &ecs.read_storage::(), - ecs.read_storage::().maybe(), + ecs.read_storage::().maybe(), ecs.read_storage::().maybe(), ecs.read_storage::().maybe(), ) .join() // Don't render dead entities - .filter(|(_, _, _, _, stats, _, _)| stats.map_or(true, |s| !s.is_dead)) + .filter(|(_, _, _, _, health, _, _)| health.map_or(true, |h| !h.is_dead)) { let is_player = entity == player_entity; @@ -2853,10 +2853,9 @@ impl FigureMgr { ecs.read_storage::().get(player_entity), ecs.read_storage::().get(player_entity), ) { - let stats_storage = state.read_storage::(); - let stats = stats_storage.get(player_entity); - - if stats.map_or(false, |s| s.is_dead) { + let healths = state.read_storage::(); + let health = healths.get(player_entity); + if health.map_or(false, |h| h.is_dead) { return; } diff --git a/voxygen/src/scene/mod.rs b/voxygen/src/scene/mod.rs index 5e61543cb7..4cd499c7f8 100644 --- a/voxygen/src/scene/mod.rs +++ b/voxygen/src/scene/mod.rs @@ -604,10 +604,10 @@ impl Scene { .maybe(), scene_data.state.ecs().read_storage::().maybe(), &scene_data.state.ecs().read_storage::(), - &scene_data.state.ecs().read_storage::(), + &scene_data.state.ecs().read_storage::(), ) .join() - .filter(|(_, _, _, _, stats)| !stats.is_dead) + .filter(|(_, _, _, _, health)| !health.is_dead) .filter(|(pos, _, _, _, _)| { (pos.0.distance_squared(player_pos) as f32) < (loaded_distance.min(SHADOW_MAX_DIST) + SHADOW_DIST_RADIUS).powf(2.0)