Separated out health from stats component.

This commit is contained in:
Sam 2020-10-31 17:34:08 -05:00
parent b8f722af8d
commit c48c022f7e
33 changed files with 1228 additions and 403 deletions

View File

@ -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));
}

129
common/src/comp/health.rs Normal file
View File

@ -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>;
}

View File

@ -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};

View File

@ -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>;
}

View File

@ -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>,

View File

@ -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)
},

View File

@ -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>();

View File

@ -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())?;

View File

@ -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)}));

View File

@ -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);
}
}

View File

@ -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(),

View File

@ -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()

View File

@ -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

View File

@ -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);
}
}

View File

@ -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) => {

View File

@ -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);

View File

@ -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)

View File

@ -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
}

View File

@ -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) => {

View File

@ -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;

View File

@ -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))

743
server/src/sys/message.rs Normal file
View File

@ -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()
}
}

View File

@ -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,

View File

@ -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>();

View File

@ -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))

View File

@ -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,

View File

@ -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<_>>()

View File

@ -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)

View File

@ -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,

View File

@ -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)

View File

@ -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();
};

View File

@ -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;
}

View File

@ -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)