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::<comp::Stats>()
+                .read_storage::<comp::Health>()
                 .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::<comp::Stats>()
+            .read_storage::<comp::Health>()
             .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<Uid> },
+    Explosion { owner: Option<Uid> },
+    Energy { owner: Option<Uid> },
+    Buff { owner: Option<Uid> },
+    Suicide,
+    World,
+    Revive,
+    Command,
+    LevelUp,
+    Item,
+    Healing { by: Option<Uid> },
+    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<Body>, 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<Self, IdvStorage<Self>>;
+}
+
+#[derive(Copy, Clone, Debug, Serialize, Deserialize)]
+pub struct Dying {
+    pub cause: HealthSource,
+}
+
+impl Component for Dying {
+    type Storage = IdvStorage<Self>;
+}
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<Uid> },
-    Explosion { owner: Option<Uid> },
-    Energy { owner: Option<Uid> },
-    Buff { owner: Option<Uid> },
-    Suicide,
-    World,
-    Revive,
-    Command,
-    LevelUp,
-    Item,
-    Healing { by: Option<Uid> },
-    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<Self, IdvStorage<Self>>;
 }
-
-#[derive(Copy, Clone, Debug, Serialize, Deserialize)]
-pub struct Dying {
-    pub cause: HealthSource,
-}
-
-impl Component for Dying {
-    type Storage = IdvStorage<Self>;
-}
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<comp::Agent>,
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<comp::Stats>),
         Buffs(PhantomData<comp::Buffs>),
         Energy(PhantomData<comp::Energy>),
+        Health(PhantomData<comp::Health>),
         LightEmitter(PhantomData<comp::LightEmitter>),
         Item(PhantomData<comp::Item>),
         Scale(PhantomData<comp::Scale>),
@@ -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::<comp::Stats>(entity, world),
             EcsCompPhantom::Buffs(_) => sync::handle_remove::<comp::Buffs>(entity, world),
             EcsCompPhantom::Energy(_) => sync::handle_remove::<comp::Energy>(entity, world),
+            EcsCompPhantom::Health(_) => sync::handle_remove::<comp::Health>(entity, world),
             EcsCompPhantom::LightEmitter(_) => {
                 sync::handle_remove::<comp::LightEmitter>(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::<comp::Stats>();
         ecs.register::<comp::Buffs>();
         ecs.register::<comp::Energy>();
+        ecs.register::<comp::Health>();
         ecs.register::<comp::CanBuild>();
         ecs.register::<comp::LightEmitter>();
         ecs.register::<comp::Item>();
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<ServerEvent>>,
         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::<BuffId>::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::<comp::Stats>()
+        .write_storage::<comp::Health>()
         .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::<comp::Stats>()
+            .write_storage::<comp::Health>()
             .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::<comp::Stats>();
+    let mut healths = ecs.write_storage::<comp::Health>();
     let players = ecs.read_storage::<comp::Player>();
     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<comp::Body>;
+
                 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::<comp::Health>()
+                    .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<Option<Agent>>,
@@ -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::<Stats>().get_mut(entity) {
-            stats.health.change_by(change);
+        if let Some(health) = ecs.write_storage::<Health>().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<f32>) {
     let state = &server.state;
     if vel.z <= -30.0 {
-        if let Some(stats) = state.ecs().write_storage::<comp::Stats>().get_mut(entity) {
+        if let Some(health) = state.ecs().write_storage::<comp::Health>().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<f32>)
             };
             let loadouts = state.ecs().read_storage::<comp::Loadout>();
             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::<comp::Stats>()
+            .write_storage::<comp::Health>()
             .get_mut(entity)
-            .map(|stats| stats.revive());
+            .map(|health| health.revive());
         state
             .ecs()
             .write_storage::<comp::Pos>()
@@ -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::<comp::Pos>(),
                     &ecs.read_storage::<comp::Ori>(),
                     ecs.read_storage::<comp::CharacterState>().maybe(),
-                    &mut ecs.write_storage::<comp::Stats>(),
+                    &mut ecs.write_storage::<comp::Health>(),
                     ecs.read_storage::<comp::Loadout>().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::<comp::Energy>().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::<comp::Stats>();
-                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::<comp::Health>();
+                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::<comp::Stats>()
+                    .write_storage::<comp::Health>()
                     .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<specs::Entity>, 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<specs::Entity>, 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<Uid, PlayerInfo>,
+        new_players: &mut Vec<specs::Entity>,
+        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<u32> = 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<specs::Entity>, UnresolvedChatMsg)>,
+        player_list: &HashMap<Uid, PlayerInfo>,
+        new_players: &mut Vec<specs::Entity>,
+        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<ServerEvent>>,
+        Read<'a, Time>,
+        ReadExpect<'a, CharacterLoader>,
+        ReadExpect<'a, TerrainGrid>,
+        ReadExpect<'a, NetworkRequestMetrics>,
+        ReadExpect<'a, PlayerMetrics>,
+        Write<'a, SysTimer<Self>>,
+        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::<HashMap<_, _>>();
+        // 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<Stats>>,
     pub buffs: ReadExpect<'a, UpdateTracker<Buffs>>,
     pub energy: ReadExpect<'a, UpdateTracker<Energy>>,
+    pub health: ReadExpect<'a, UpdateTracker<Health>>,
     pub can_build: ReadExpect<'a, UpdateTracker<CanBuild>>,
     pub light_emitter: ReadExpect<'a, UpdateTracker<LightEmitter>>,
     pub item: ReadExpect<'a, UpdateTracker<Item>>,
@@ -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<Stats>>,
     buffs: WriteExpect<'a, UpdateTracker<Buffs>>,
     energy: WriteExpect<'a, UpdateTracker<Energy>>,
+    health: WriteExpect<'a, UpdateTracker<Health>>,
     can_build: WriteExpect<'a, UpdateTracker<CanBuild>>,
     light_emitter: WriteExpect<'a, UpdateTracker<LightEmitter>>,
     item: WriteExpect<'a, UpdateTracker<Item>>,
@@ -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::<Stats>();
     world.register_tracker::<Buffs>();
     world.register_tracker::<Energy>();
+    world.register_tracker::<Health>();
     world.register_tracker::<CanBuild>();
     world.register_tracker::<LightEmitter>();
     world.register_tracker::<Item>();
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::<Vec<_>>()
         {
             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::<Vec<_>>()
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::<common::comp::Stats>();
+            let healths = client_state.ecs().read_storage::<common::comp::Health>();
             let energy = client_state.ecs().read_storage::<common::comp::Energy>();
             let buffs = client_state.ecs().read_storage::<common::comp::Buffs>();
             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::<comp::Pos>();
             let stats = ecs.read_storage::<comp::Stats>();
+            let healths = ecs.read_storage::<comp::Health>();
             let buffs = ecs.read_storage::<comp::Buffs>();
             let energy = ecs.read_storage::<comp::Energy>();
             let hp_floater_lists = ecs.read_storage::<vcomp::HpFloaterList>();
@@ -767,11 +768,10 @@ impl Hud {
                 .get(client.entity())
                 .map_or(0, |stats| stats.level.level());
             //self.input = client.read_storage::<comp::ControllerInputs>();
-            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::<comp::Stats>();
+        let healths = ecs.read_storage::<comp::Health>();
         let loadouts = ecs.read_storage::<comp::Loadout>();
         let energies = ecs.read_storage::<comp::Energy>();
         let character_states = ecs.read_storage::<comp::CharacterState>();
@@ -1904,6 +1919,7 @@ impl Hud {
         let inventories = ecs.read_storage::<comp::Inventory>();
         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::<CharacterState>().maybe(),
             ecs.read_storage::<Last<CharacterState>>().maybe(),
             &ecs.read_storage::<PhysicsState>(),
-            ecs.read_storage::<Stats>().maybe(),
+            ecs.read_storage::<Health>().maybe(),
             ecs.read_storage::<Loadout>().maybe(),
             ecs.read_storage::<Item>().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::<Pos>(),
                 ecs.read_storage::<Ori>().maybe(),
                 &ecs.read_storage::<Body>(),
-                ecs.read_storage::<Stats>().maybe(),
+                ecs.read_storage::<Health>().maybe(),
                 ecs.read_storage::<Loadout>().maybe(),
                 ecs.read_storage::<Scale>().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::<Pos>(),
             ecs.read_storage::<Ori>().maybe(),
             &ecs.read_storage::<Body>(),
-            ecs.read_storage::<Stats>().maybe(),
+            ecs.read_storage::<Health>().maybe(),
             ecs.read_storage::<Loadout>().maybe(),
             ecs.read_storage::<Scale>().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::<Pos>().get(player_entity),
             ecs.read_storage::<Body>().get(player_entity),
         ) {
-            let stats_storage = state.read_storage::<Stats>();
-            let stats = stats_storage.get(player_entity);
-
-            if stats.map_or(false, |s| s.is_dead) {
+            let healths = state.read_storage::<Health>();
+            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::<comp::Scale>().maybe(),
             &scene_data.state.ecs().read_storage::<comp::Body>(),
-            &scene_data.state.ecs().read_storage::<comp::Stats>(),
+            &scene_data.state.ecs().read_storage::<comp::Health>(),
         )
             .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)