Merge branch 'sam/smol-fixes' into 'master'

Small Fixes 2

See merge request veloren/veloren!1471
This commit is contained in:
Samuel Keiffer 2020-11-05 05:19:12 +00:00
commit ee7d4d57c7
59 changed files with 1204 additions and 983 deletions

View File

@ -188,13 +188,14 @@ https://account.veloren.net.
"hud.chat.pvp_ranged_kill_msg": "[{attacker}] hat [{victim}] aus der Ferne getötet",
"hud.chat.pvp_explosion_kill_msg": "[{attacker}] hat [{victim}] hochgejagt",
"hud.chat.pvp_energy_kill_msg": "[{attacker}] hat [{victim}] mit Magie erledigt",
"hud.chat.pvp_buff_kill_msg" : "[{attacker}] hat [{victim}] mit Magie erledigt",
"hud.chat.pvp_other_kill_msg" : "[{attacker}] hat [{victim}] mit Magie erledigt",
"hud.chat.npc_melee_kill_msg": "{attacker} tötete [{victim}]",
"hud.chat.npc_ranged_kill_msg": "{attacker} tötete [{victim}]",
"hud.chat.npc_explosion_kill_msg": "{attacker} hat [{victim}] hochgejagt",
"hud.chat.npc_buff_kill_msg": "[{attacker}] tötete [{victim}]",
"hud.chat.npc_energy_kill_msg": "[{attacker}] hat [{victim}] mit Magie erledigt",
"hud.chat.npc_other_kill_msg": "[{attacker}] tötete [{victim}]",
// SCT outputs
"hud.sct.experience": "{amount} Erf",

View File

@ -184,13 +184,14 @@ https://account.veloren.net."#,
"hud.chat.pvp_ranged_kill_msg": "[{attacker}] shot [{victim}]",
"hud.chat.pvp_explosion_kill_msg": "[{attacker}] blew up [{victim}]",
"hud.chat.pvp_energy_kill_msg": "[{attacker}] killed [{victim}] with magic",
"hud.chat.pvp_buff_kill_msg": "[{attacker}] killed [{victim}]",
"hud.chat.pvp_other_kill_msg": "[{attacker}] killed [{victim}]",
"hud.chat.npc_melee_kill_msg": "{attacker} killed [{victim}]",
"hud.chat.npc_ranged_kill_msg": "{attacker} shot [{victim}]",
"hud.chat.npc_explosion_kill_msg": "{attacker} blew up [{victim}]",
"hud.chat.npc_buff_kill_msg": "[{attacker}] killed [{victim}]",
"hud.chat.npc_energy_kill_msg": "[{attacker}] killed [{victim}] with magic",
"hud.chat.npc_other_kill_msg": "[{attacker}] killed [{victim}]",
"hud.chat.loot_msg": "You picked up [{item}]",
"hud.chat.loot_fail": "Your Inventory is full!",

View File

@ -184,13 +184,14 @@ bir hesap oluşturabilirsin."#,
"hud.chat.pvp_ranged_kill_msg": "[{victim}], [{attacker}] tarafından vuruldu.",
"hud.chat.pvp_explosion_kill_msg": "[{victim}], [{attacker}] tarafından havaya uçuruldu.",
"hud.chat.pvp_energy_kill_msg": "[{victim}], [{attacker}] tarafından büyü ile mağlup edildi.",
"hud.chat.pvp_buff_kill_msg": "[{victim}], [{attacker}] tarafından öldürüldü.",
"hud.chat.pvp_other_kill_msg": "[{victim}], [{attacker}] tarafından öldürüldü.",
"hud.chat.npc_melee_kill_msg": "[{victim}], [{attacker}] tarafından mağlup edildi.",
"hud.chat.npc_ranged_kill_msg": "[{victim}], [{attacker}] tarafından vuruldu.",
"hud.chat.npc_explosion_kill_msg": "[{victim}], [{attacker}] tarafından havaya uçuruldu.",
"hud.chat.npc_buff_kill_msg": "[{victim}], [{attacker}] tarafından öldürüldü.",
"hud.chat.npc_energy_kill_msg": "[{victim}], [{attacker}] tarafından büyü ile mağlup edildi.",
"hud.chat.npc_other_kill_msg": "[{victim}], [{attacker}] tarafından öldürüldü.",
"hud.chat.loot_msg": "[{item}] topladın.",
"hud.chat.loot_fail": "Envanterin dolu!",

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));
}
@ -1733,7 +1733,7 @@ impl Client {
alias_of_uid(attacker_uid),
alias_of_uid(victim)
),
KillSource::Player(attacker_uid, KillType::Buff) => format!(
KillSource::Player(attacker_uid, KillType::Other) => format!(
"[{}] killed [{}]",
alias_of_uid(attacker_uid),
alias_of_uid(victim)
@ -1752,7 +1752,7 @@ impl Client {
attacker_name,
alias_of_uid(victim)
),
KillSource::NonPlayer(attacker_name, KillType::Buff) => {
KillSource::NonPlayer(attacker_name, KillType::Other) => {
format!("{} killed [{}]", attacker_name, alias_of_uid(victim))
},
KillSource::Environment(environment) => {
@ -1780,7 +1780,7 @@ impl Client {
KillSource::Player(attacker_uid, KillType::Energy) => message
.replace("{attacker}", &alias_of_uid(attacker_uid))
.replace("{victim}", &alias_of_uid(victim)),
KillSource::Player(attacker_uid, KillType::Buff) => message
KillSource::Player(attacker_uid, KillType::Other) => message
.replace("{attacker}", &alias_of_uid(attacker_uid))
.replace("{victim}", &alias_of_uid(victim)),
KillSource::NonPlayer(attacker_name, KillType::Melee) => message
@ -1795,7 +1795,7 @@ impl Client {
KillSource::NonPlayer(attacker_name, KillType::Energy) => message
.replace("{attacker}", attacker_name)
.replace("{victim}", &alias_of_uid(victim)),
KillSource::NonPlayer(attacker_name, KillType::Buff) => message
KillSource::NonPlayer(attacker_name, KillType::Other) => message
.replace("{attacker}", attacker_name)
.replace("{victim}", &alias_of_uid(victim)),
KillSource::Environment(environment) => message

View File

@ -6,56 +6,40 @@ use crate::{
use serde::{Deserialize, Serialize};
use vek::*;
pub const BLOCK_EFFICIENCY: f32 = 0.9;
/// Each section of this struct determines what damage is applied to a
/// particular target, using some identifier
#[derive(Copy, Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct Damages {
/// Targets enemies, and all other creatures not in your group
pub enemy: Option<Damage>,
/// Targets people in the same group as you, and any pets you have
pub group: Option<Damage>,
#[derive(Copy, Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
pub enum GroupTarget {
InGroup,
OutOfGroup,
}
impl Damages {
pub fn new(enemy: Option<Damage>, group: Option<Damage>) -> Self { Damages { enemy, group } }
pub fn get_damage(self, same_group: bool) -> Option<Damage> {
if same_group { self.group } else { self.enemy }
}
#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash, Serialize, Deserialize)]
pub enum DamageSource {
Melee,
Healing,
Projectile,
Explosion,
Falling,
Shockwave,
Energy,
Other,
}
#[derive(Copy, Clone, Debug, PartialEq, Serialize, Deserialize)]
pub enum Damage {
Melee(f32),
Healing(f32),
Projectile(f32),
Explosion(f32),
Falling(f32),
Shockwave(f32),
Energy(f32),
pub struct Damage {
pub source: DamageSource,
pub value: f32,
}
impl Damage {
pub fn modify_damage(
self,
block: bool,
loadout: Option<&Loadout>,
uid: Option<Uid>,
) -> HealthChange {
match self {
Damage::Melee(damage) => {
let mut damage = damage;
pub fn modify_damage(self, loadout: Option<&Loadout>, uid: Option<Uid>) -> HealthChange {
let mut damage = self.value;
match self.source {
DamageSource::Melee => {
// Critical hit
let mut critdamage = 0.0;
if rand::random() {
critdamage = damage * 0.3;
}
// Block
if block {
damage *= 1.0 - BLOCK_EFFICIENCY
}
// Armor
let damage_reduction = loadout.map_or(0.0, |l| l.get_damage_reduction());
damage *= 1.0 - damage_reduction;
@ -67,71 +51,73 @@ impl Damage {
HealthChange {
amount: -damage as i32,
cause: HealthSource::Attack { by: uid.unwrap() },
cause: HealthSource::Damage {
kind: self.source,
by: uid,
},
}
},
Damage::Projectile(damage) => {
let mut damage = damage;
DamageSource::Projectile => {
// Critical hit
if rand::random() {
damage *= 1.2;
}
// Block
if block {
damage *= 1.0 - BLOCK_EFFICIENCY
}
// Armor
let damage_reduction = loadout.map_or(0.0, |l| l.get_damage_reduction());
damage *= 1.0 - damage_reduction;
HealthChange {
amount: -damage as i32,
cause: HealthSource::Projectile { owner: uid },
cause: HealthSource::Damage {
kind: self.source,
by: uid,
},
}
},
Damage::Explosion(damage) => {
let mut damage = damage;
// Block
if block {
damage *= 1.0 - BLOCK_EFFICIENCY
}
DamageSource::Explosion => {
// Armor
let damage_reduction = loadout.map_or(0.0, |l| l.get_damage_reduction());
damage *= 1.0 - damage_reduction;
HealthChange {
amount: -damage as i32,
cause: HealthSource::Explosion { owner: uid },
cause: HealthSource::Damage {
kind: self.source,
by: uid,
},
}
},
Damage::Shockwave(damage) => {
let mut damage = damage;
DamageSource::Shockwave => {
// Armor
let damage_reduction = loadout.map_or(0.0, |l| l.get_damage_reduction());
damage *= 1.0 - damage_reduction;
HealthChange {
amount: -damage as i32,
cause: HealthSource::Attack { by: uid.unwrap() },
cause: HealthSource::Damage {
kind: self.source,
by: uid,
},
}
},
Damage::Energy(damage) => {
let mut damage = damage;
DamageSource::Energy => {
// Armor
let damage_reduction = loadout.map_or(0.0, |l| l.get_damage_reduction());
damage *= 1.0 - damage_reduction;
HealthChange {
amount: -damage as i32,
cause: HealthSource::Energy { owner: uid },
cause: HealthSource::Damage {
kind: self.source,
by: uid,
},
}
},
Damage::Healing(heal) => HealthChange {
amount: heal as i32,
cause: HealthSource::Healing { by: uid },
DamageSource::Healing => HealthChange {
amount: damage as i32,
cause: HealthSource::Heal { by: uid },
},
Damage::Falling(damage) => {
let mut damage = damage;
DamageSource::Falling => {
// Armor
let damage_reduction = loadout.map_or(0.0, |l| l.get_damage_reduction());
if (damage_reduction - 1.0).abs() < f32::EPSILON {
@ -142,8 +128,20 @@ impl Damage {
cause: HealthSource::World,
}
},
DamageSource::Other => HealthChange {
amount: -damage as i32,
cause: HealthSource::Damage {
kind: self.source,
by: uid,
},
},
}
}
pub fn interpolate_damage(&mut self, frac: f32, min: f32) {
let new_damage = min + frac * (self.value - min);
self.value = new_damage;
}
}
#[derive(Copy, Clone, Debug, PartialEq, Serialize, Deserialize)]

View File

@ -452,6 +452,7 @@ impl From<(&CharacterAbility, AbilityKey)> for CharacterState {
swing_duration: *swing_duration,
recover_duration: *recover_duration,
is_interruptible: *is_interruptible,
ability_key: key,
},
auto_charge: false,
timer: Duration::default(),
@ -489,6 +490,7 @@ impl From<(&CharacterAbility, AbilityKey)> for CharacterState {
speed_increase: 1.0 - *speed_increase,
max_speed_increase: *max_speed_increase - 1.0,
is_interruptible: *is_interruptible,
ability_key: key,
},
stage: 1,
combo: 0,
@ -552,6 +554,7 @@ impl From<(&CharacterAbility, AbilityKey)> for CharacterState {
is_interruptible: *is_interruptible,
forward_speed: *forward_speed,
num_spins: *num_spins,
ability_key: key,
},
timer: Duration::default(),
spins_remaining: *num_spins - 1,
@ -583,6 +586,7 @@ impl From<(&CharacterAbility, AbilityKey)> for CharacterState {
charge_duration: *charge_duration,
swing_duration: *swing_duration,
recover_duration: *recover_duration,
ability_key: key,
},
stage_section: StageSection::Charge,
timer: Duration::default(),
@ -619,6 +623,7 @@ impl From<(&CharacterAbility, AbilityKey)> for CharacterState {
projectile_gravity: *projectile_gravity,
initial_projectile_speed: *initial_projectile_speed,
max_projectile_speed: *max_projectile_speed,
ability_key: key,
},
timer: Duration::default(),
stage_section: StageSection::Buildup,

View File

@ -1,4 +1,4 @@
use crate::{sync::Uid, Damages};
use crate::{sync::Uid, Damage, GroupTarget};
use serde::{Deserialize, Serialize};
use specs::{Component, FlaggedStorage};
use specs_idvs::IdvStorage;
@ -8,7 +8,7 @@ use std::time::Duration;
pub struct Properties {
pub angle: f32,
pub speed: f32,
pub damages: Damages,
pub damages: Vec<(Option<GroupTarget>, Damage)>,
pub lifesteal_eff: f32,
pub energy_regen: u32,
pub energy_cost: u32,

View File

@ -3,7 +3,7 @@ use crate::{
event::{LocalEvent, ServerEvent},
states::*,
sys::character_behavior::JoinData,
Damages, Knockback,
Damage, GroupTarget, Knockback,
};
use serde::{Deserialize, Serialize};
use specs::{Component, FlaggedStorage, VecStorage};
@ -155,9 +155,9 @@ impl Component for CharacterState {
type Storage = FlaggedStorage<Self, IdvStorage<Self>>;
}
#[derive(Copy, Clone, Debug, PartialEq, Serialize, Deserialize)]
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct Attacking {
pub damages: Damages,
pub damages: Vec<(Option<GroupTarget>, Damage)>,
pub range: f32,
pub max_angle: f32,
pub applied: bool,

View File

@ -51,7 +51,7 @@ pub enum KillType {
Projectile,
Explosion,
Energy,
Buff,
Other,
// Projectile(String), TODO: add projectile name when available
}

View File

@ -47,9 +47,9 @@ impl Energy {
self.current = amount;
}
pub fn change_by(&mut self, amount: i32, cause: EnergySource) {
self.current = ((self.current as i32 + amount).max(0) as u32).min(self.maximum);
self.last_change = Some((amount, 0.0, cause));
pub fn change_by(&mut self, change: EnergyChange) {
self.current = ((self.current as i32 + change.amount).max(0) as u32).min(self.maximum);
self.last_change = Some((change.amount, 0.0, change.source));
}
pub fn try_change_by(
@ -62,7 +62,10 @@ impl Energy {
} else if self.current as i32 + amount > self.maximum as i32 {
Err(StatChangeError::Overflow)
} else {
self.change_by(amount, cause);
self.change_by(EnergyChange {
amount,
source: cause,
});
Ok(())
}
}
@ -73,6 +76,11 @@ impl Energy {
}
}
pub struct EnergyChange {
pub amount: i32,
pub source: EnergySource,
}
impl Component for Energy {
type Storage = FlaggedStorage<Self, IdvStorage<Self>>;
}

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

@ -0,0 +1,131 @@
use crate::{comp::Body, sync::Uid, DamageSource};
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 {
Damage { kind: DamageSource, by: Option<Uid> },
Heal { by: Option<Uid> },
//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 Dead {
pub cause: HealthSource,
}
impl Component for Dead {
type Storage = IdvStorage<Self>;
}

View File

@ -7,8 +7,9 @@ use crate::{
buff::{Buff, BuffCategory, BuffData, BuffKind, BuffSource},
projectile, Body, CharacterAbility, Gravity, LightEmitter, Projectile,
},
effect::Effect,
states::combo_melee,
Damage, Damages, Explosion, Knockback,
Damage, DamageSource, Explosion, GroupTarget, Knockback, RadiusEffect,
};
use serde::{Deserialize, Serialize};
use std::time::Duration;
@ -302,10 +303,10 @@ impl Tool {
projectile: Projectile {
hit_solid: vec![projectile::Effect::Stick],
hit_entity: vec![
projectile::Effect::Damages(Damages::new(
Some(Damage::Projectile(40.0 * self.base_power())),
None,
)),
projectile::Effect::Damage(Some(GroupTarget::OutOfGroup), Damage {
source: DamageSource::Projectile,
value: 40.0 * self.base_power(),
}),
projectile::Effect::Knockback(Knockback::Away(10.0)),
projectile::Effect::RewardEnergy(50),
projectile::Effect::Vanish,
@ -357,10 +358,10 @@ impl Tool {
projectile: Projectile {
hit_solid: vec![projectile::Effect::Stick],
hit_entity: vec![
projectile::Effect::Damages(Damages::new(
Some(Damage::Projectile(40.0 * self.base_power())),
None,
)),
projectile::Effect::Damage(Some(GroupTarget::OutOfGroup), Damage {
source: DamageSource::Projectile,
value: 40.0 * self.base_power(),
}),
projectile::Effect::Knockback(Knockback::Away(10.0)),
projectile::Effect::Vanish,
projectile::Effect::Buff {
@ -419,24 +420,46 @@ impl Tool {
projectile: Projectile {
hit_solid: vec![
projectile::Effect::Explode(Explosion {
effects: vec![
RadiusEffect::Entity(
Some(GroupTarget::OutOfGroup),
Effect::Damage(Damage {
source: DamageSource::Explosion,
value: 50.0 * self.base_power(),
}),
),
RadiusEffect::Entity(
Some(GroupTarget::InGroup),
Effect::Damage(Damage {
source: DamageSource::Healing,
value: 140.0 * self.base_power(),
}),
),
],
radius: 3.0 + 2.5 * self.base_power(),
max_damage: (50.0 * self.base_power()) as u32,
min_damage: (20.0 * self.base_power()) as u32,
max_heal: (140.0 * self.base_power()) as u32,
min_heal: (50.0 * self.base_power()) as u32,
terrain_destruction_power: 0.0,
energy_regen: 0,
}),
projectile::Effect::Vanish,
],
hit_entity: vec![
projectile::Effect::Explode(Explosion {
effects: vec![
RadiusEffect::Entity(
Some(GroupTarget::OutOfGroup),
Effect::Damage(Damage {
source: DamageSource::Explosion,
value: 50.0 * self.base_power(),
}),
),
RadiusEffect::Entity(
Some(GroupTarget::InGroup),
Effect::Damage(Damage {
source: DamageSource::Healing,
value: 140.0 * self.base_power(),
}),
),
],
radius: 3.0 + 2.5 * self.base_power(),
max_damage: (50.0 * self.base_power()) as u32,
min_damage: (20.0 * self.base_power()) as u32,
max_heal: (140.0 * self.base_power()) as u32,
min_heal: (50.0 * self.base_power()) as u32,
terrain_destruction_power: 0.0,
energy_regen: 0,
}),
projectile::Effect::Vanish,
@ -462,24 +485,28 @@ impl Tool {
projectile: Projectile {
hit_solid: vec![
projectile::Effect::Explode(Explosion {
effects: vec![RadiusEffect::Entity(
Some(GroupTarget::OutOfGroup),
Effect::Damage(Damage {
source: DamageSource::Explosion,
value: 100.0 * self.base_power(),
}),
)],
radius: 5.0,
max_damage: (100.0 * self.base_power()) as u32,
min_damage: 0,
max_heal: 0,
min_heal: 0,
terrain_destruction_power: 0.0,
energy_regen: 50,
}),
projectile::Effect::Vanish,
],
hit_entity: vec![
projectile::Effect::Explode(Explosion {
effects: vec![RadiusEffect::Entity(
Some(GroupTarget::OutOfGroup),
Effect::Damage(Damage {
source: DamageSource::Explosion,
value: 100.0 * self.base_power(),
}),
)],
radius: 5.0,
max_damage: (100.0 * self.base_power()) as u32,
min_damage: 0,
max_heal: 0,
min_heal: 0,
terrain_destruction_power: 0.0,
energy_regen: 50,
}),
projectile::Effect::Vanish,

View File

@ -9,6 +9,7 @@ pub mod chat;
mod controller;
mod energy;
pub mod group;
mod health;
mod inputs;
mod inventory;
mod last;
@ -43,8 +44,9 @@ pub use controller::{
Climb, ControlAction, ControlEvent, Controller, ControllerInputs, GroupManip, Input,
InventoryManip, MountState, Mounting,
};
pub use energy::{Energy, EnergySource};
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,4 +1,4 @@
use crate::{comp::Buff, sync::Uid, Damages, Explosion, Knockback};
use crate::{comp::Buff, sync::Uid, Damage, Explosion, GroupTarget, Knockback};
use serde::{Deserialize, Serialize};
use specs::{Component, FlaggedStorage};
use specs_idvs::IdvStorage;
@ -6,7 +6,7 @@ use std::time::Duration;
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub enum Effect {
Damages(Damages),
Damage(Option<GroupTarget>, Damage),
Knockback(Knockback),
RewardEnergy(u32),
Explode(Explosion),

View File

@ -1,4 +1,4 @@
use crate::{sync::Uid, Damages, Knockback};
use crate::{sync::Uid, Damage, GroupTarget, Knockback};
use serde::{Deserialize, Serialize};
use specs::{Component, FlaggedStorage};
use specs_idvs::IdvStorage;
@ -9,7 +9,7 @@ pub struct Properties {
pub angle: f32,
pub vertical_angle: f32,
pub speed: f32,
pub damages: Damages,
pub damages: Vec<(Option<GroupTarget>, Damage)>,
pub knockback: Knockback,
pub requires_ground: bool,
pub duration: Duration,

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

@ -1,11 +1,12 @@
use crate::comp;
use crate::{combat, comp};
use serde::{Deserialize, Serialize};
/// An effect that may be applied to an entity
#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[derive(Copy, Clone, Debug, PartialEq, Serialize, Deserialize)]
pub enum Effect {
Health(comp::HealthChange),
Xp(i64),
Damage(combat::Damage),
}
impl Effect {
@ -13,6 +14,21 @@ impl Effect {
match self {
Effect::Health(c) => format!("{:+} health", c.amount),
Effect::Xp(n) => format!("{:+} exp", n),
Effect::Damage(d) => format!("{:+}", d.value),
}
}
pub fn modify_strength(&mut self, modifier: f32) {
match self {
Effect::Health(change) => {
change.amount = (change.amount as f32 * modifier) as i32;
},
Effect::Xp(amount) => {
*amount = (*amount as f32 * modifier) as i64;
},
Effect::Damage(damage) => {
damage.interpolate_damage(modifier, 0.0);
},
}
}
}

View File

@ -31,11 +31,10 @@ pub enum ServerEvent {
pos: Vec3<f32>,
explosion: Explosion,
owner: Option<Uid>,
friendly_damage: bool,
reagent: Option<Reagent>,
},
Damage {
uid: Uid,
entity: EcsEntity,
change: comp::HealthChange,
},
Destroy {
@ -93,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>,
@ -110,6 +110,10 @@ pub enum ServerEvent {
entity: EcsEntity,
buff_change: comp::BuffChange,
},
EnergyChange {
entity: EcsEntity,
change: comp::EnergyChange,
},
}
pub struct EventBus<E> {

View File

@ -1,12 +1,15 @@
use crate::{combat::GroupTarget, effect::Effect};
use serde::{Deserialize, Serialize};
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct Explosion {
pub effects: Vec<RadiusEffect>,
pub radius: f32,
pub max_damage: u32,
pub min_damage: u32,
pub max_heal: u32,
pub min_heal: u32,
pub terrain_destruction_power: f32,
pub energy_regen: u32,
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub enum RadiusEffect {
TerrainDestruction(f32),
Entity(Option<GroupTarget>, Effect),
}

View File

@ -53,6 +53,6 @@ pub mod util;
pub mod vol;
pub mod volumes;
pub use combat::{Damage, Damages, Knockback};
pub use explosion::Explosion;
pub use combat::{Damage, DamageSource, GroupTarget, Knockback};
pub use explosion::{Explosion, RadiusEffect};
pub use loadout_builder::LoadoutBuilder;

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

@ -1,10 +1,12 @@
use crate::{
comp::{beam, humanoid, Body, CharacterState, EnergySource, Ori, Pos, StateUpdate},
comp::{
beam, humanoid, Body, CharacterState, EnergyChange, EnergySource, Ori, Pos, StateUpdate,
},
event::ServerEvent,
states::utils::*,
sync::Uid,
sys::character_behavior::{CharacterBehavior, JoinData},
Damage, Damages,
Damage, DamageSource, GroupTarget,
};
use serde::{Deserialize, Serialize};
use std::time::Duration;
@ -107,12 +109,14 @@ impl CharacterBehavior for Data {
if ability_key_is_pressed(data, self.static_data.ability_key)
&& (self.static_data.energy_drain == 0 || update.energy.current() > 0)
{
let damage = Damage::Energy(
self.static_data.base_dps as f32 / self.static_data.tick_rate,
);
let heal = Damage::Healing(
self.static_data.base_hps as f32 / self.static_data.tick_rate,
);
let damage = Damage {
source: DamageSource::Energy,
value: self.static_data.base_dps as f32 / self.static_data.tick_rate,
};
let heal = Damage {
source: DamageSource::Healing,
value: self.static_data.base_hps as f32 / self.static_data.tick_rate,
};
let energy_regen =
(self.static_data.energy_regen as f32 / self.static_data.tick_rate) as u32;
let energy_cost =
@ -122,7 +126,10 @@ impl CharacterBehavior for Data {
let properties = beam::Properties {
angle: self.static_data.max_angle.to_radians(),
speed,
damages: Damages::new(Some(damage), Some(heal)),
damages: vec![
(Some(GroupTarget::OutOfGroup), damage),
(Some(GroupTarget::InGroup), heal),
],
lifesteal_eff: self.static_data.lifesteal_eff,
energy_regen,
energy_cost,
@ -146,10 +153,10 @@ impl CharacterBehavior for Data {
});
// Consumes energy if there's enough left and ability key is held down
update.energy.change_by(
-(self.static_data.energy_drain as f32 * data.dt.0) as i32,
EnergySource::Ability,
);
update.energy.change_by(EnergyChange {
amount: -(self.static_data.energy_drain as f32 * data.dt.0) as i32,
source: EnergySource::Ability,
});
} else {
update.character = CharacterState::BasicBeam(Data {
timer: Duration::default(),

View File

@ -1,8 +1,8 @@
use crate::{
comp::{Attacking, CharacterState, EnergySource, StateUpdate},
comp::{Attacking, CharacterState, EnergyChange, EnergySource, StateUpdate},
states::utils::*,
sys::character_behavior::{CharacterBehavior, JoinData},
Damage, Damages, Knockback,
Damage, DamageSource, GroupTarget, Knockback,
};
use serde::{Deserialize, Serialize};
use std::time::Duration;
@ -76,10 +76,10 @@ impl CharacterBehavior for Data {
// Hit attempt
data.updater.insert(data.entity, Attacking {
damages: Damages::new(
Some(Damage::Melee(self.static_data.base_damage as f32)),
None,
),
damages: vec![(Some(GroupTarget::OutOfGroup), Damage {
source: DamageSource::Melee,
value: self.static_data.base_damage as f32,
})],
range: self.static_data.range,
max_angle: 180_f32.to_radians(),
applied: false,
@ -133,7 +133,10 @@ impl CharacterBehavior for Data {
if let Some(attack) = data.attacking {
if attack.applied && attack.hit_count > 0 {
data.updater.remove::<Attacking>(data.entity);
update.energy.change_by(50, EnergySource::HitEnemy);
update.energy.change_by(EnergyChange {
amount: 50,
source: EnergySource::HitEnemy,
});
}
}

View File

@ -1,8 +1,8 @@
use crate::{
comp::{Attacking, CharacterState, EnergySource, StateUpdate},
comp::{Attacking, CharacterState, EnergyChange, EnergySource, StateUpdate},
states::utils::{StageSection, *},
sys::character_behavior::*,
Damage, Damages, Knockback,
Damage, DamageSource, GroupTarget, Knockback,
};
use serde::{Deserialize, Serialize};
use std::time::Duration;
@ -32,6 +32,8 @@ pub struct StaticData {
pub swing_duration: Duration,
/// How long the state has until exiting
pub recover_duration: Duration,
/// What key is used to press ability
pub ability_key: AbilityKey,
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
@ -58,7 +60,7 @@ impl CharacterBehavior for Data {
match self.stage_section {
StageSection::Charge => {
if data.inputs.secondary.is_pressed()
if ability_key_is_pressed(data, self.static_data.ability_key)
&& update.energy.current() >= self.static_data.energy_cost
&& self.timer < self.static_data.charge_duration
{
@ -77,11 +79,11 @@ impl CharacterBehavior for Data {
});
// Consumes energy if there's enough left and RMB is held down
update.energy.change_by(
-(self.static_data.energy_drain as f32 * data.dt.0) as i32,
EnergySource::Ability,
);
} else if data.inputs.secondary.is_pressed()
update.energy.change_by(EnergyChange {
amount: -(self.static_data.energy_drain as f32 * data.dt.0) as i32,
source: EnergySource::Ability,
});
} else if ability_key_is_pressed(data, self.static_data.ability_key)
&& update.energy.current() >= self.static_data.energy_cost
{
// Maintains charge
@ -94,10 +96,10 @@ impl CharacterBehavior for Data {
});
// Consumes energy if there's enough left and RMB is held down
update.energy.change_by(
-(self.static_data.energy_drain as f32 * data.dt.0 / 5.0) as i32,
EnergySource::Ability,
);
update.energy.change_by(EnergyChange {
amount: -(self.static_data.energy_drain as f32 * data.dt.0 / 5.0) as i32,
source: EnergySource::Ability,
});
} else {
// Transitions to swing
update.character = CharacterState::ChargedMelee(Data {
@ -109,16 +111,21 @@ impl CharacterBehavior for Data {
},
StageSection::Swing => {
if !self.exhausted {
let damage = self.static_data.initial_damage as f32
+ (self.static_data.max_damage - self.static_data.initial_damage) as f32
* self.charge_amount;
let mut damage = Damage {
source: DamageSource::Melee,
value: self.static_data.max_damage as f32,
};
damage.interpolate_damage(
self.charge_amount,
self.static_data.initial_damage as f32,
);
let knockback = self.static_data.initial_knockback
+ (self.static_data.max_knockback - self.static_data.initial_knockback)
* self.charge_amount;
// Hit attempt
data.updater.insert(data.entity, Attacking {
damages: Damages::new(Some(Damage::Melee(damage)), None),
damages: vec![(Some(GroupTarget::OutOfGroup), damage)],
range: self.static_data.range,
max_angle: self.static_data.max_angle.to_radians(),
applied: false,

View File

@ -1,13 +1,13 @@
use crate::{
comp::{
buff::{Buff, BuffCategory, BuffData, BuffKind, BuffSource},
projectile, Body, CharacterState, EnergySource, Gravity, LightEmitter, Projectile,
StateUpdate,
projectile, Body, CharacterState, EnergyChange, EnergySource, Gravity, LightEmitter,
Projectile, StateUpdate,
},
event::ServerEvent,
states::utils::*,
sys::character_behavior::{CharacterBehavior, JoinData},
Damage, Damages, Knockback,
Damage, DamageSource, GroupTarget, Knockback,
};
use serde::{Deserialize, Serialize};
use std::time::Duration;
@ -37,6 +37,8 @@ pub struct StaticData {
pub projectile_gravity: Option<Gravity>,
pub initial_projectile_speed: f32,
pub max_projectile_speed: f32,
/// What key is used to press ability
pub ability_key: AbilityKey,
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
@ -80,14 +82,15 @@ impl CharacterBehavior for Data {
}
},
StageSection::Charge => {
if !data.inputs.secondary.is_pressed() && !self.exhausted {
if !ability_key_is_pressed(data, self.static_data.ability_key) && !self.exhausted {
let charge_frac = (self.timer.as_secs_f32()
/ self.static_data.charge_duration.as_secs_f32())
.min(1.0);
let damage = self.static_data.initial_damage as f32
+ (charge_frac
* (self.static_data.max_damage - self.static_data.initial_damage)
as f32);
let mut damage = Damage {
source: DamageSource::Projectile,
value: self.static_data.max_damage as f32,
};
damage.interpolate_damage(charge_frac, self.static_data.initial_damage as f32);
let knockback = self.static_data.initial_knockback as f32
+ (charge_frac
* (self.static_data.max_knockback - self.static_data.initial_knockback)
@ -96,17 +99,14 @@ impl CharacterBehavior for Data {
let mut projectile = Projectile {
hit_solid: vec![projectile::Effect::Stick],
hit_entity: vec![
projectile::Effect::Damages(Damages::new(
Some(Damage::Projectile(damage)),
None,
)),
projectile::Effect::Damage(Some(GroupTarget::OutOfGroup), damage),
projectile::Effect::Knockback(Knockback::Away(knockback)),
projectile::Effect::Vanish,
projectile::Effect::Buff {
buff: Buff::new(
BuffKind::Bleeding,
BuffData {
strength: damage / 5.0,
strength: damage.value / 5.0,
duration: Some(Duration::from_secs(5)),
},
vec![BuffCategory::Physical],
@ -140,7 +140,7 @@ impl CharacterBehavior for Data {
..*self
});
} else if self.timer < self.static_data.charge_duration
&& data.inputs.secondary.is_pressed()
&& ability_key_is_pressed(data, self.static_data.ability_key)
{
// Charges
update.character = CharacterState::ChargedRanged(Data {
@ -152,11 +152,11 @@ impl CharacterBehavior for Data {
});
// Consumes energy if there's enough left and RMB is held down
update.energy.change_by(
-(self.static_data.energy_drain as f32 * data.dt.0) as i32,
EnergySource::Ability,
);
} else if data.inputs.secondary.is_pressed() {
update.energy.change_by(EnergyChange {
amount: -(self.static_data.energy_drain as f32 * data.dt.0) as i32,
source: EnergySource::Ability,
});
} else if ability_key_is_pressed(data, self.static_data.ability_key) {
// Holds charge
update.character = CharacterState::ChargedRanged(Data {
timer: self
@ -167,10 +167,10 @@ impl CharacterBehavior for Data {
});
// Consumes energy if there's enough left and RMB is held down
update.energy.change_by(
-(self.static_data.energy_drain as f32 * data.dt.0 / 5.0) as i32,
EnergySource::Ability,
);
update.energy.change_by(EnergyChange {
amount: -(self.static_data.energy_drain as f32 * data.dt.0 / 5.0) as i32,
source: EnergySource::Ability,
});
}
},
StageSection::Recover => {

View File

@ -1,8 +1,8 @@
use crate::{
comp::{Attacking, CharacterState, EnergySource, StateUpdate},
comp::{Attacking, CharacterState, EnergyChange, EnergySource, StateUpdate},
states::utils::*,
sys::character_behavior::{CharacterBehavior, JoinData},
Damage, Damages, Knockback,
Damage, DamageSource, GroupTarget, Knockback,
};
use serde::{Deserialize, Serialize};
use std::time::Duration;
@ -54,6 +54,8 @@ pub struct StaticData {
pub max_speed_increase: f32,
/// Whether the state can be interrupted by other abilities
pub is_interruptible: bool,
/// What key is used to press ability
pub ability_key: AbilityKey,
}
/// A sequence of attacks that can incrementally become faster and more
/// damaging.
@ -84,7 +86,9 @@ impl CharacterBehavior for Data {
let stage_index = (self.stage - 1) as usize;
// Allows for other states to interrupt this state
if self.static_data.is_interruptible && !data.inputs.primary.is_pressed() {
if self.static_data.is_interruptible
&& !ability_key_is_pressed(data, self.static_data.ability_key)
{
handle_interrupt(data, &mut update);
match update.character {
CharacterState::ComboMelee(_) => {},
@ -127,7 +131,10 @@ impl CharacterBehavior for Data {
* self.static_data.stage_data[stage_index].damage_increase,
);
data.updater.insert(data.entity, Attacking {
damages: Damages::new(Some(Damage::Melee(damage as f32)), None),
damages: vec![(Some(GroupTarget::OutOfGroup), Damage {
source: DamageSource::Melee,
value: damage as f32,
})],
range: self.static_data.stage_data[stage_index].range,
max_angle: self.static_data.stage_data[stage_index].angle.to_radians(),
applied: false,
@ -177,7 +184,7 @@ impl CharacterBehavior for Data {
StageSection::Recover => {
if self.timer < self.static_data.stage_data[stage_index].base_recover_duration {
// Recovers
if data.inputs.primary.is_pressed() {
if ability_key_is_pressed(data, self.static_data.ability_key) {
// Checks if state will transition to next stage after recover
update.character = CharacterState::ComboMelee(Data {
static_data: self.static_data.clone(),
@ -255,7 +262,10 @@ impl CharacterBehavior for Data {
next_stage: self.next_stage,
});
data.updater.remove::<Attacking>(data.entity);
update.energy.change_by(energy, EnergySource::HitEnemy);
update.energy.change_by(EnergyChange {
amount: energy,
source: EnergySource::HitEnemy,
});
}
}

View File

@ -1,8 +1,8 @@
use crate::{
comp::{Attacking, CharacterState, EnergySource, StateUpdate},
comp::{Attacking, CharacterState, EnergyChange, EnergySource, StateUpdate},
states::utils::*,
sys::character_behavior::{CharacterBehavior, JoinData},
Damage, Damages, Knockback,
Damage, DamageSource, GroupTarget, Knockback,
};
use serde::{Deserialize, Serialize};
use std::time::Duration;
@ -39,6 +39,8 @@ pub struct StaticData {
pub recover_duration: Duration,
/// Whether the state can be interrupted by other abilities
pub is_interruptible: bool,
/// What key is used to press ability
pub ability_key: AbilityKey,
}
#[derive(Copy, Clone, Debug, PartialEq, Serialize, Deserialize)]
@ -66,7 +68,9 @@ impl CharacterBehavior for Data {
handle_move(data, &mut update, 0.1);
// Allows for other states to interrupt this state
if self.static_data.is_interruptible && !data.inputs.secondary.is_pressed() {
if self.static_data.is_interruptible
&& !ability_key_is_pressed(data, self.static_data.ability_key)
{
handle_interrupt(data, &mut update);
match update.character {
CharacterState::DashMelee(_) => {},
@ -90,7 +94,7 @@ impl CharacterBehavior for Data {
} else {
// Transitions to charge section of stage
update.character = CharacterState::DashMelee(Data {
auto_charge: !data.inputs.secondary.is_pressed(),
auto_charge: !ability_key_is_pressed(data, self.static_data.ability_key),
timer: Duration::default(),
stage_section: StageSection::Charge,
..*self
@ -100,7 +104,7 @@ impl CharacterBehavior for Data {
StageSection::Charge => {
if (self.static_data.infinite_charge
|| self.timer < self.static_data.charge_duration)
&& (data.inputs.secondary.is_pressed()
&& (ability_key_is_pressed(data, self.static_data.ability_key)
|| (self.auto_charge && self.timer < self.static_data.charge_duration))
&& update.energy.current() > 0
{
@ -122,16 +126,20 @@ impl CharacterBehavior for Data {
let charge_frac = (self.timer.as_secs_f32()
/ self.static_data.charge_duration.as_secs_f32())
.min(1.0);
let damage = (self.static_data.max_damage as f32
- self.static_data.base_damage as f32)
* charge_frac
+ self.static_data.base_damage as f32;
let mut damage = Damage {
source: DamageSource::Melee,
value: self.static_data.max_damage as f32,
};
damage.interpolate_damage(
charge_frac,
self.static_data.base_damage as f32,
);
let knockback = (self.static_data.max_knockback
- self.static_data.base_knockback)
* charge_frac
+ self.static_data.base_knockback;
data.updater.insert(data.entity, Attacking {
damages: Damages::new(Some(Damage::Melee(damage)), None),
damages: vec![(Some(GroupTarget::OutOfGroup), damage)],
range: self.static_data.range,
max_angle: self.static_data.angle.to_radians(),
applied: false,
@ -172,10 +180,10 @@ impl CharacterBehavior for Data {
}
// Consumes energy if there's enough left and charge has not stopped
update.energy.change_by(
-(self.static_data.energy_drain as f32 * data.dt.0) as i32,
EnergySource::Ability,
);
update.energy.change_by(EnergyChange {
amount: -(self.static_data.energy_drain as f32 * data.dt.0) as i32,
source: EnergySource::Ability,
});
} else {
// Transitions to swing section of stage
update.character = CharacterState::DashMelee(Data {

View File

@ -2,7 +2,7 @@ use crate::{
comp::{Attacking, CharacterState, StateUpdate},
states::utils::{StageSection, *},
sys::character_behavior::{CharacterBehavior, JoinData},
Damage, Damages, Knockback,
Damage, DamageSource, GroupTarget, Knockback,
};
use serde::{Deserialize, Serialize};
use std::time::Duration;
@ -133,10 +133,10 @@ impl CharacterBehavior for Data {
if !self.exhausted {
// Hit attempt, when animation plays
data.updater.insert(data.entity, Attacking {
damages: Damages::new(
Some(Damage::Melee(self.static_data.base_damage as f32)),
None,
),
damages: vec![(Some(GroupTarget::OutOfGroup), Damage {
source: DamageSource::Melee,
value: self.static_data.base_damage as f32,
})],
range: self.static_data.range,
max_angle: self.static_data.max_angle.to_radians(),
applied: false,

View File

@ -3,7 +3,7 @@ use crate::{
event::ServerEvent,
states::utils::*,
sys::character_behavior::{CharacterBehavior, JoinData},
Damage, Damages, Knockback,
Damage, DamageSource, GroupTarget, Knockback,
};
use serde::{Deserialize, Serialize};
use std::time::Duration;
@ -70,10 +70,10 @@ impl CharacterBehavior for Data {
vertical_angle: self.static_data.shockwave_vertical_angle,
speed: self.static_data.shockwave_speed,
duration: self.static_data.shockwave_duration,
damages: Damages::new(
Some(Damage::Shockwave(self.static_data.damage as f32)),
None,
),
damages: vec![(Some(GroupTarget::OutOfGroup), Damage {
source: DamageSource::Shockwave,
value: self.static_data.damage as f32,
})],
knockback: self.static_data.knockback,
requires_ground: self.static_data.requires_ground,
owner: Some(*data.uid),

View File

@ -1,11 +1,11 @@
use crate::{
comp::{Attacking, CharacterState, EnergySource, StateUpdate},
comp::{Attacking, CharacterState, EnergyChange, EnergySource, StateUpdate},
states::utils::*,
sys::{
character_behavior::{CharacterBehavior, JoinData},
phys::GRAVITY,
},
Damage, Damages, Knockback,
Damage, DamageSource, GroupTarget, Knockback,
};
use serde::{Deserialize, Serialize};
use std::time::Duration;
@ -38,6 +38,8 @@ pub struct StaticData {
pub forward_speed: f32,
/// Number of spins
pub num_spins: u32,
/// What key is used to press ability
pub ability_key: AbilityKey,
}
#[derive(Copy, Clone, Debug, PartialEq, Serialize, Deserialize)]
@ -65,7 +67,9 @@ impl CharacterBehavior for Data {
}
// Allows for other states to interrupt this state
if self.static_data.is_interruptible && !data.inputs.ability3.is_pressed() {
if self.static_data.is_interruptible
&& !ability_key_is_pressed(data, self.static_data.ability_key)
{
handle_interrupt(data, &mut update);
match update.character {
CharacterState::SpinMelee(_) => {},
@ -104,10 +108,10 @@ impl CharacterBehavior for Data {
});
// Hit attempt
data.updater.insert(data.entity, Attacking {
damages: Damages::new(
Some(Damage::Melee(self.static_data.base_damage as f32)),
None,
),
damages: vec![(Some(GroupTarget::OutOfGroup), Damage {
source: DamageSource::Melee,
value: self.static_data.base_damage as f32,
})],
range: self.static_data.range,
max_angle: 180_f32.to_radians(),
applied: false,
@ -137,7 +141,8 @@ impl CharacterBehavior for Data {
});
} else if update.energy.current() >= self.static_data.energy_cost
&& (self.spins_remaining != 0
|| (self.static_data.is_infinite && data.inputs.secondary.is_pressed()))
|| (self.static_data.is_infinite
&& ability_key_is_pressed(data, self.static_data.ability_key)))
{
let new_spins_remaining = if self.static_data.is_infinite {
self.spins_remaining
@ -151,10 +156,10 @@ impl CharacterBehavior for Data {
..*self
});
// Consumes energy if there's enough left and RMB is held down
update.energy.change_by(
-(self.static_data.energy_cost as i32),
EnergySource::Ability,
);
update.energy.change_by(EnergyChange {
amount: -(self.static_data.energy_cost as i32),
source: EnergySource::Ability,
});
} else {
// Transitions to recover section of stage
update.character = CharacterState::SpinMelee(Data {

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,16 @@ 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 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
if my_health.last_change.0 < 3.0 {
if let comp::HealthSource::Damage { by: Some(by), .. } =
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 +654,10 @@ 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::Damage { by: Some(by), .. } =
owner_health.last_change.1.cause
{
if !agent.activity.is_attack() {
let attacker = uid_allocator.retrieve_entity_internal(by.id())?;

View File

@ -1,19 +1,17 @@
use crate::{
comp::{
group, Beam, BeamSegment, Body, CharacterState, Energy, EnergySource, HealthChange,
HealthSource, Last, Loadout, Ori, Pos, Scale, Stats,
group, Beam, BeamSegment, Body, Energy, EnergyChange, EnergySource, Health, HealthChange,
HealthSource, Last, Loadout, Ori, Pos, Scale,
},
event::{EventBus, ServerEvent},
state::{DeltaTime, Time},
sync::{Uid, UidAllocator},
Damage,
GroupTarget,
};
use specs::{saveload::MarkerAllocator, Entities, Join, Read, ReadStorage, System, WriteStorage};
use std::time::Duration;
use vek::*;
pub const BLOCK_ANGLE: f32 = 180.0;
/// This system is responsible for handling beams that heal or do damage
pub struct Sys;
impl<'a> System<'a> for Sys {
@ -30,11 +28,10 @@ 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>,
WriteStorage<'a, Energy>,
ReadStorage<'a, Energy>,
WriteStorage<'a, BeamSegment>,
WriteStorage<'a, Beam>,
);
@ -53,11 +50,10 @@ impl<'a> System<'a> for Sys {
orientations,
scales,
bodies,
stats,
healths,
loadouts,
groups,
character_states,
mut energies,
energies,
mut beam_segments,
mut beams,
): Self::SystemData,
@ -68,8 +64,8 @@ impl<'a> System<'a> for Sys {
let dt = dt.0;
// Beams
for (entity, uid, pos, ori, beam_segment) in
(&entities, &uids, &positions, &orientations, &beam_segments).join()
for (entity, pos, ori, beam_segment) in
(&entities, &positions, &orientations, &beam_segments).join()
{
let creation_time = match beam_segment.creation {
Some(time) => time,
@ -116,26 +112,14 @@ impl<'a> System<'a> for Sys {
};
// Go through all other effectable entities
for (
b,
uid_b,
pos_b,
last_pos_b_maybe,
ori_b,
scale_b_maybe,
character_b,
stats_b,
body_b,
) in (
for (b, uid_b, pos_b, last_pos_b_maybe, scale_b_maybe, health_b, body_b) in (
&entities,
&uids,
&positions,
// TODO: make sure that these are maintained on the client and remove `.maybe()`
last_positions.maybe(),
&orientations,
scales.maybe(),
character_states.maybe(),
&stats,
&healths,
&bodies,
)
.join()
@ -152,7 +136,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)}));
@ -163,63 +147,74 @@ impl<'a> System<'a> for Sys {
.map(|group_a| Some(group_a) == groups.get(b))
.unwrap_or(Some(*uid_b) == beam_segment.owner);
let target_group = if same_group {
GroupTarget::InGroup
} else {
GroupTarget::OutOfGroup
};
// If owner, shouldn't heal or damage
if Some(*uid_b) == beam_segment.owner {
continue;
}
let damage = if let Some(damage) = beam_segment.damages.get_damage(same_group) {
damage
} else {
continue;
};
let block = character_b.map(|c_b| c_b.is_block()).unwrap_or(false)
// TODO: investigate whether this calculation is proper for beams
&& ori_b.0.angle_between(pos.0 - pos_b.0) < BLOCK_ANGLE.to_radians() / 2.0;
let change = damage.modify_damage(block, loadouts.get(b), beam_segment.owner);
if !matches!(damage, Damage::Healing(_)) {
server_emitter.emit(ServerEvent::Damage {
uid: *uid_b,
change,
});
if beam_segment.lifesteal_eff > 0.0 {
server_emitter.emit(ServerEvent::Damage {
uid: beam_segment.owner.unwrap_or(*uid),
change: HealthChange {
amount: (-change.amount as f32 * beam_segment.lifesteal_eff)
as i32,
cause: HealthSource::Healing {
by: beam_segment.owner,
},
},
});
for (target, damage) in beam_segment.damages.iter() {
if let Some(target) = target {
if *target != target_group {
continue;
}
}
if let Some(energy_mut) = beam_owner.and_then(|o| energies.get_mut(o)) {
energy_mut.change_by(
beam_segment.energy_regen as i32,
EnergySource::HitEnemy,
);
}
} else if let Some(energy_mut) = beam_owner.and_then(|o| energies.get_mut(o)) {
if energy_mut
.try_change_by(
-(beam_segment.energy_cost as i32), // Stamina use
EnergySource::Ability,
)
.is_ok()
{
server_emitter.emit(ServerEvent::Damage {
uid: *uid_b,
change,
});
// Modify damage
let change = damage.modify_damage(loadouts.get(b), beam_segment.owner);
match target {
Some(GroupTarget::OutOfGroup) => {
server_emitter.emit(ServerEvent::Damage { entity: b, change });
if let Some(entity) = beam_owner {
server_emitter.emit(ServerEvent::Damage {
entity,
change: HealthChange {
amount: (-change.amount as f32
* beam_segment.lifesteal_eff)
as i32,
cause: HealthSource::Heal {
by: beam_segment.owner,
},
},
});
server_emitter.emit(ServerEvent::EnergyChange {
entity,
change: EnergyChange {
amount: beam_segment.energy_regen as i32,
source: EnergySource::HitEnemy,
},
});
}
},
Some(GroupTarget::InGroup) => {
if let Some(energy) = beam_owner.and_then(|o| energies.get(o)) {
if energy.current() > beam_segment.energy_cost {
server_emitter.emit(ServerEvent::EnergyChange {
entity: beam_owner.unwrap(), /* If it's able to get an energy
* component, the entity exists */
change: EnergyChange {
amount: -(beam_segment.energy_cost as i32), // Stamina use
source: EnergySource::Ability,
},
});
server_emitter
.emit(ServerEvent::Damage { entity: b, change });
}
}
},
None => {},
}
// Adds entities that were hit to the hit_entities list on the beam, sees if
// it needs to purge the hit_entities list
hit_entities.push(*uid_b);
}
// Adds entities that were hit to the hit_entities list on the beam, sees if it
// needs to purge the hit_entities list
hit_entities.push(*uid_b);
}
}
}

View File

@ -1,11 +1,11 @@
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,
sync::Uid,
DamageSource,
};
use specs::{Entities, Join, Read, ReadStorage, System, WriteStorage};
use std::time::Duration;
@ -17,21 +17,20 @@ impl<'a> System<'a> for Sys {
Entities<'a>,
Read<'a, DeltaTime>,
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, 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, health) in (&entities, &mut buffs, &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 +62,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() {
@ -88,12 +87,15 @@ impl<'a> System<'a> for Sys {
|| buff.time.map_or(false, |dur| dur == Duration::default())
{
let cause = if *accumulated > 0.0 {
HealthSource::Healing { by: buff_owner }
HealthSource::Heal { by: buff_owner }
} else {
HealthSource::Buff { owner: buff_owner }
HealthSource::Damage {
kind: DamageSource::Other,
by: buff_owner,
}
};
server_emitter.emit(ServerEvent::Damage {
uid: *uid,
entity,
change: HealthChange {
amount: *accumulated as i32,
cause,
@ -104,14 +106,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 +125,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 +139,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,18 +1,17 @@
use crate::{
comp::{buff, group, Attacking, Body, CharacterState, Loadout, Ori, Pos, Scale, Stats},
comp::{buff, group, Attacking, Body, Health, Loadout, Ori, Pos, Scale},
event::{EventBus, LocalEvent, ServerEvent},
metrics::SysMetrics,
span,
sync::Uid,
util::Dir,
GroupTarget,
};
use rand::{thread_rng, Rng};
use specs::{Entities, Join, Read, ReadExpect, ReadStorage, System, WriteStorage};
use std::time::Duration;
use vek::*;
pub const BLOCK_ANGLE: f32 = 180.0;
/// This system is responsible for handling accepted inputs like moving or
/// attacking
pub struct Sys;
@ -28,10 +27,9 @@ 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>,
WriteStorage<'a, Attacking>,
);
@ -47,10 +45,9 @@ impl<'a> System<'a> for Sys {
orientations,
scales,
bodies,
stats,
healths,
loadouts,
groups,
character_states,
mut attacking_storage,
): Self::SystemData,
) {
@ -75,17 +72,8 @@ 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 (
&entities,
&uids,
&positions,
&orientations,
scales.maybe(),
character_states.maybe(),
&stats,
&bodies,
)
.join()
for (b, pos_b, scale_b_maybe, health_b, body_b) in
(&entities, &positions, scales.maybe(), &healths, &bodies).join()
{
// 2D versions
let pos2 = Vec2::from(pos.0);
@ -99,7 +87,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()
@ -110,23 +98,22 @@ impl<'a> System<'a> for Sys {
.map(|group_a| Some(group_a) == groups.get(b))
.unwrap_or(false);
let damage = if let Some(damage) = attack.damages.get_damage(same_group) {
damage
let target_group = if same_group {
GroupTarget::InGroup
} else {
continue;
GroupTarget::OutOfGroup
};
let block = character_b.map(|c_b| c_b.is_block()).unwrap_or(false)
&& ori_b.0.angle_between(pos.0 - pos_b.0) < BLOCK_ANGLE.to_radians() / 2.0;
for (target, damage) in attack.damages.iter() {
if let Some(target) = target {
if *target != target_group {
continue;
}
}
let change = damage.modify_damage(block, loadouts.get(b), Some(*uid));
if change.amount != 0 {
server_emitter.emit(ServerEvent::Damage {
uid: *uid_b,
change,
});
let change = damage.modify_damage(loadouts.get(b), Some(*uid));
server_emitter.emit(ServerEvent::Damage { entity: b, change });
// Apply bleeding buff on melee hits with 10% chance
// TODO: Don't have buff uniformly applied on all melee attacks
if change.amount < 0 && thread_rng().gen::<f32>() < 0.1 {
@ -144,15 +131,13 @@ impl<'a> System<'a> for Sys {
)),
});
}
attack.hit_count += 1;
}
if change.amount != 0 {
let kb_dir = Dir::new((pos_b.0 - pos.0).try_normalized().unwrap_or(*ori.0));
let impulse = attack.knockback.calculate_impulse(kb_dir);
if !impulse.is_approx_zero() {
server_emitter.emit(ServerEvent::Knockback { entity: b, impulse });
}
attack.hit_count += 1;
}
}
}

View File

@ -1,14 +1,15 @@
use crate::{
comp::{
buff::{BuffChange, BuffSource},
projectile, Energy, EnergySource, Group, HealthSource, Loadout, Ori, PhysicsState, Pos,
Projectile, Vel,
projectile, EnergyChange, EnergySource, Group, HealthSource, Loadout, Ori, PhysicsState,
Pos, Projectile, Vel,
},
event::{EventBus, LocalEvent, ServerEvent},
metrics::SysMetrics,
span,
state::DeltaTime,
sync::UidAllocator,
GroupTarget,
};
use rand::{thread_rng, Rng};
use specs::{
@ -32,7 +33,6 @@ impl<'a> System<'a> for Sys {
ReadStorage<'a, Vel>,
WriteStorage<'a, Ori>,
WriteStorage<'a, Projectile>,
WriteStorage<'a, Energy>,
ReadStorage<'a, Loadout>,
ReadStorage<'a, Group>,
);
@ -51,7 +51,6 @@ impl<'a> System<'a> for Sys {
velocities,
mut orientations,
mut projectiles,
mut energies,
loadouts,
groups,
): Self::SystemData,
@ -85,6 +84,13 @@ impl<'a> System<'a> for Sys {
.retrieve_entity_internal(other.into())
.and_then(|e| groups.get(e))
);
let target_group = if same_group {
GroupTarget::InGroup
} else {
GroupTarget::OutOfGroup
};
if projectile.ignore_group
// Skip if in the same group
&& same_group
@ -98,43 +104,54 @@ impl<'a> System<'a> for Sys {
for effect in projectile.hit_entity.drain(..) {
match effect {
projectile::Effect::Damages(damages) => {
projectile::Effect::Damage(target, damage) => {
if Some(other) == projectile.owner {
continue;
}
let damage = if let Some(damage) = damages.get_damage(same_group) {
damage
} else {
continue;
};
let other_entity_loadout = uid_allocator
.retrieve_entity_internal(other.into())
.and_then(|e| loadouts.get(e));
let change =
damage.modify_damage(false, other_entity_loadout, projectile.owner);
if change.amount != 0 {
server_emitter.emit(ServerEvent::Damage { uid: other, change });
if let Some(target) = target {
if target != target_group {
continue;
}
}
if let Some(other_entity) =
uid_allocator.retrieve_entity_internal(other.into())
{
let other_entity_loadout = loadouts.get(other_entity);
let change =
damage.modify_damage(other_entity_loadout, projectile.owner);
server_emitter.emit(ServerEvent::Damage {
entity: other_entity,
change,
});
}
},
projectile::Effect::Knockback(knockback) => {
if let Some(entity) =
if let Some(other_entity) =
uid_allocator.retrieve_entity_internal(other.into())
{
let impulse = knockback.calculate_impulse(ori.0);
if !impulse.is_approx_zero() {
local_emitter
.emit(LocalEvent::ApplyImpulse { entity, impulse });
local_emitter.emit(LocalEvent::ApplyImpulse {
entity: other_entity,
impulse,
});
}
}
},
projectile::Effect::RewardEnergy(energy) => {
if let Some(energy_mut) = projectile
if let Some(entity_owner) = projectile
.owner
.and_then(|o| uid_allocator.retrieve_entity_internal(o.into()))
.and_then(|o| energies.get_mut(o))
.and_then(|u| uid_allocator.retrieve_entity_internal(u.into()))
{
energy_mut.change_by(energy as i32, EnergySource::HitEnemy);
server_emitter.emit(ServerEvent::EnergyChange {
entity: entity_owner,
change: EnergyChange {
amount: energy as i32,
source: EnergySource::HitEnemy,
},
});
}
},
projectile::Effect::Explode(e) => {
@ -142,7 +159,6 @@ impl<'a> System<'a> for Sys {
pos: pos.0,
explosion: e,
owner: projectile.owner,
friendly_damage: false,
reagent: None,
})
},
@ -157,6 +173,7 @@ impl<'a> System<'a> for Sys {
}
}
},
// TODO: Change to effect after !1472 merges
projectile::Effect::Buff { buff, chance } => {
if let Some(entity) =
uid_allocator.retrieve_entity_internal(other.into())
@ -187,7 +204,6 @@ impl<'a> System<'a> for Sys {
pos: pos.0,
explosion: e,
owner: projectile.owner,
friendly_damage: false,
reagent: None,
})
},

View File

@ -1,18 +1,17 @@
use crate::{
comp::{
group, Body, CharacterState, HealthSource, Last, Loadout, Ori, PhysicsState, Pos, Scale,
Shockwave, ShockwaveHitEntities, Stats,
group, Body, Health, HealthSource, Last, Loadout, Ori, PhysicsState, Pos, Scale, Shockwave,
ShockwaveHitEntities,
},
event::{EventBus, LocalEvent, ServerEvent},
state::{DeltaTime, Time},
sync::{Uid, UidAllocator},
util::Dir,
GroupTarget,
};
use specs::{saveload::MarkerAllocator, Entities, Join, Read, ReadStorage, System, WriteStorage};
use vek::*;
pub const BLOCK_ANGLE: f32 = 180.0;
/// This system is responsible for handling accepted inputs like moving or
/// attacking
pub struct Sys;
@ -31,10 +30,9 @@ 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>,
ReadStorage<'a, PhysicsState>,
WriteStorage<'a, Shockwave>,
WriteStorage<'a, ShockwaveHitEntities>,
@ -55,10 +53,9 @@ impl<'a> System<'a> for Sys {
orientations,
scales,
bodies,
stats,
healths,
loadouts,
groups,
character_states,
physics_states,
mut shockwaves,
mut shockwave_hit_lists,
@ -130,10 +127,8 @@ impl<'a> System<'a> for Sys {
uid_b,
pos_b,
last_pos_b_maybe,
ori_b,
scale_b_maybe,
character_b,
stats_b,
health_b,
body_b,
physics_state_b,
) in (
@ -142,10 +137,8 @@ impl<'a> System<'a> for Sys {
&positions,
// TODO: make sure that these are maintained on the client and remove `.maybe()`
last_positions.maybe(),
&orientations,
scales.maybe(),
character_states.maybe(),
&stats,
&healths,
&bodies,
&physics_states,
)
@ -177,9 +170,15 @@ impl<'a> System<'a> for Sys {
.map(|group_a| Some(group_a) == groups.get(b))
.unwrap_or(Some(*uid_b) == shockwave.owner);
let target_group = if same_group {
GroupTarget::InGroup
} else {
GroupTarget::OutOfGroup
};
// 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
@ -192,24 +191,19 @@ impl<'a> System<'a> for Sys {
&& (!shockwave.requires_ground || physics_state_b.on_ground);
if hit {
let damage = if let Some(damage) = shockwave.damages.get_damage(same_group) {
damage
} else {
continue;
};
for (target, damage) in shockwave.damages.iter() {
if let Some(target) = target {
if *target != target_group {
continue;
}
}
let block = character_b.map(|c_b| c_b.is_block()).unwrap_or(false)
&& ori_b.0.angle_between(pos.0 - pos_b.0) < BLOCK_ANGLE.to_radians() / 2.0;
let owner_uid = shockwave.owner.unwrap_or(*uid);
let change = damage.modify_damage(loadouts.get(b), Some(owner_uid));
let owner_uid = shockwave.owner.unwrap_or(*uid);
let change = damage.modify_damage(block, loadouts.get(b), Some(owner_uid));
if change.amount != 0 {
server_emitter.emit(ServerEvent::Damage {
uid: *uid_b,
change,
});
server_emitter.emit(ServerEvent::Damage { entity: b, change });
shockwave_hit_list.hit_entities.push(*uid_b);
let kb_dir = Dir::new((pos_b.0 - pos.0).try_normalized().unwrap_or(*ori.0));
let impulse = shockwave.knockback.calculate_impulse(kb_dir);
if !impulse.is_approx_zero() {

View File

@ -1,5 +1,5 @@
use crate::{
comp::{CharacterState, Energy, 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);
}
}
@ -96,11 +113,12 @@ impl<'a> System<'a> for Sys {
if res {
let mut energy = energy.get_mut_unchecked();
// Have to account for Calc I differential equations due to acceleration
energy.change_by(
(energy.regen_rate * dt.0 + ENERGY_REGEN_ACCEL * dt.0.powf(2.0) / 2.0)
energy.change_by(EnergyChange {
amount: (energy.regen_rate * dt.0
+ ENERGY_REGEN_ACCEL * dt.0.powf(2.0) / 2.0)
as i32,
EnergySource::Regen,
);
source: EnergySource::Regen,
});
energy.regen_rate =
(energy.regen_rate + ENERGY_REGEN_ACCEL * dt.0).min(100.0);
}
@ -130,9 +148,10 @@ impl<'a> System<'a> for Sys {
};
if res {
energy
.get_mut_unchecked()
.change_by(-3, EnergySource::Regen);
energy.get_mut_unchecked().change_by(EnergyChange {
amount: -3,
source: EnergySource::Regen,
});
}
},
// Non-combat abilities that consume energy;

View File

@ -10,6 +10,7 @@ use chrono::{NaiveTime, Timelike};
use common::{
cmd::{ChatCommand, CHAT_COMMANDS, CHAT_SHORTCUTS},
comp::{self, ChatType, Item, LightEmitter, WaypointArea},
effect::Effect,
event::{EventBus, ServerEvent},
msg::{DisconnectReason, Notification, PlayerListUpdate, ServerGeneral},
npc::{self, get_npc_name},
@ -18,7 +19,7 @@ use common::{
terrain::{Block, BlockKind, SpriteKind, TerrainChunkSize},
util::Dir,
vol::RectVolSize,
Explosion, LoadoutBuilder,
Damage, DamageSource, Explosion, LoadoutBuilder, RadiusEffect,
};
use rand::Rng;
use specs::{Builder, Entity as EcsEntity, Join, WorldExt};
@ -393,16 +394,19 @@ fn handle_kill(
let reason = if client == target {
comp::HealthSource::Suicide
} else if let Some(uid) = server.state.read_storage::<Uid>().get(client) {
comp::HealthSource::Attack { by: *uid }
comp::HealthSource::Damage {
kind: DamageSource::Other,
by: Some(*uid),
}
} else {
comp::HealthSource::Command
};
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 +475,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 +660,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 +767,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 +931,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)
@ -1153,16 +1160,20 @@ fn handle_explosion(
.emit_now(ServerEvent::Explosion {
pos: pos.0,
explosion: Explosion {
effects: vec![
RadiusEffect::Entity(
None,
Effect::Damage(Damage {
source: DamageSource::Explosion,
value: 100.0 * power,
}),
),
RadiusEffect::TerrainDestruction(power),
],
radius: 3.0 * power,
max_damage: (100.0 * power) as u32,
min_damage: 0,
max_heal: 0,
min_heal: 0,
terrain_destruction_power: power,
energy_regen: 0,
},
owner: ecs.read_storage::<Uid>().get(target).copied(),
friendly_damage: true,
reagent: None,
})
},
@ -1695,6 +1706,8 @@ fn handle_set_level(
PlayerListUpdate::LevelChange(uid, lvl),
));
let body_type: Option<comp::Body>;
if let Some(stats) = server
.state
.ecs_mut()
@ -1702,13 +1715,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,17 +8,18 @@ use common::{
comp::{
self, buff,
chat::{KillSource, KillType},
object, Alignment, Body, Group, HealthChange, HealthSource, Item, Player, Pos, Stats,
object, Alignment, Body, Energy, EnergyChange, Group, Health, HealthChange, HealthSource,
Item, Player, Pos, Stats,
},
effect::Effect,
lottery::Lottery,
msg::{PlayerListUpdate, ServerGeneral},
outcome::Outcome,
state::BlockChange,
sync::{Uid, UidAllocator, WorldSyncExt},
sys::melee::BLOCK_ANGLE,
terrain::{Block, TerrainGrid},
vol::ReadVol,
Damage, Explosion,
Damage, DamageSource, Explosion, GroupTarget, RadiusEffect,
};
use comp::item::Reagent;
use rand::prelude::*;
@ -26,13 +27,10 @@ use specs::{join::Join, saveload::MarkerAllocator, Entity as EcsEntity, WorldExt
use tracing::error;
use vek::Vec3;
pub fn handle_damage(server: &Server, uid: Uid, change: HealthChange) {
let state = &server.state;
let ecs = 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);
}
pub fn handle_damage(server: &Server, entity: EcsEntity, change: HealthChange) {
let ecs = &server.state.ecs();
if let Some(health) = ecs.write_storage::<Health>().get_mut(entity) {
health.change_by(change);
}
}
@ -75,7 +73,10 @@ pub fn handle_destroy(server: &mut Server, entity: EcsEntity, cause: HealthSourc
if let Some(_player) = state.ecs().read_storage::<Player>().get(entity) {
if let Some(uid) = state.ecs().read_storage::<Uid>().get(entity) {
let kill_source = match cause {
HealthSource::Attack { by } => {
HealthSource::Damage {
kind: DamageSource::Melee,
by: Some(by),
} => {
// Get attacker entity
if let Some(char_entity) = state.ecs().entity_from_uid(by.into()) {
// Check if attacker is another player or entity with stats (npc)
@ -97,7 +98,10 @@ pub fn handle_destroy(server: &mut Server, entity: EcsEntity, cause: HealthSourc
KillSource::NonPlayer("<?>".to_string(), KillType::Melee)
}
},
HealthSource::Projectile { owner: Some(by) } => {
HealthSource::Damage {
kind: DamageSource::Projectile,
by: Some(by),
} => {
// Get projectile owner entity TODO: add names to projectiles and send in
// message
if let Some(char_entity) = state.ecs().entity_from_uid(by.into()) {
@ -120,7 +124,10 @@ pub fn handle_destroy(server: &mut Server, entity: EcsEntity, cause: HealthSourc
KillSource::NonPlayer("<?>".to_string(), KillType::Projectile)
}
},
HealthSource::Explosion { owner: Some(by) } => {
HealthSource::Damage {
kind: DamageSource::Explosion,
by: Some(by),
} => {
// Get explosion owner entity
if let Some(char_entity) = state.ecs().entity_from_uid(by.into()) {
// Check if attacker is another player or entity with stats (npc)
@ -142,7 +149,10 @@ pub fn handle_destroy(server: &mut Server, entity: EcsEntity, cause: HealthSourc
KillSource::NonPlayer("<?>".to_string(), KillType::Explosion)
}
},
HealthSource::Energy { owner: Some(by) } => {
HealthSource::Damage {
kind: DamageSource::Energy,
by: Some(by),
} => {
// Get energy owner entity
if let Some(char_entity) = state.ecs().entity_from_uid(by.into()) {
// Check if attacker is another player or entity with stats (npc)
@ -164,7 +174,10 @@ pub fn handle_destroy(server: &mut Server, entity: EcsEntity, cause: HealthSourc
KillSource::NonPlayer("<?>".to_string(), KillType::Energy)
}
},
HealthSource::Buff { owner: Some(by) } => {
HealthSource::Damage {
kind: DamageSource::Other,
by: Some(by),
} => {
// Get energy owner entity
if let Some(char_entity) = state.ecs().entity_from_uid(by.into()) {
// Check if attacker is another player or entity with stats (npc)
@ -174,29 +187,26 @@ pub fn handle_destroy(server: &mut Server, entity: EcsEntity, cause: HealthSourc
.get(char_entity)
.is_some()
{
KillSource::Player(by, KillType::Buff)
KillSource::Player(by, KillType::Other)
} else if let Some(stats) =
state.ecs().read_storage::<Stats>().get(char_entity)
{
KillSource::NonPlayer(stats.name.clone(), KillType::Buff)
KillSource::NonPlayer(stats.name.clone(), KillType::Other)
} else {
KillSource::NonPlayer("<?>".to_string(), KillType::Buff)
KillSource::NonPlayer("<?>".to_string(), KillType::Other)
}
} else {
KillSource::NonPlayer("<?>".to_string(), KillType::Buff)
KillSource::NonPlayer("<?>".to_string(), KillType::Other)
}
},
HealthSource::World => KillSource::FallDamage,
HealthSource::Suicide => KillSource::Suicide,
HealthSource::Projectile { owner: None }
| HealthSource::Explosion { owner: None }
| HealthSource::Energy { owner: None }
| HealthSource::Buff { owner: None }
HealthSource::Damage { .. }
| HealthSource::Revive
| HealthSource::Command
| HealthSource::LevelUp
| HealthSource::Item
| HealthSource::Healing { by: _ }
| HealthSource::Heal { by: _ }
| HealthSource::Unknown => KillSource::Other,
};
state
@ -207,12 +217,7 @@ pub fn handle_destroy(server: &mut Server, entity: EcsEntity, cause: HealthSourc
// Give EXP to the killer if entity had stats
(|| {
let mut stats = state.ecs().write_storage::<Stats>();
let by = if let HealthSource::Attack { by }
| HealthSource::Projectile { owner: Some(by) }
| HealthSource::Energy { owner: Some(by) }
| HealthSource::Buff { owner: Some(by) }
| HealthSource::Explosion { owner: Some(by) } = cause
{
let by = if let HealthSource::Damage { by: Some(by), .. } = cause {
by
} else {
return;
@ -452,12 +457,15 @@ 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::Falling(falldmg);
let damage = Damage {
source: DamageSource::Falling,
value: falldmg,
};
let loadouts = state.ecs().read_storage::<comp::Loadout>();
let change = damage.modify_damage(false, loadouts.get(entity), None);
stats.health.change_by(change);
let change = damage.modify_damage(loadouts.get(entity), None);
health.change_by(change);
}
}
}
@ -479,9 +487,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>()
@ -506,26 +514,30 @@ pub fn handle_explosion(
pos: Vec3<f32>,
explosion: Explosion,
owner: Option<Uid>,
friendly_damage: bool,
reagent: Option<Reagent>,
) {
// Go through all other entities
let ecs = &server.state.ecs();
let outcome_power = if explosion.max_heal > explosion.max_damage {
(-explosion.terrain_destruction_power).min(explosion.max_heal as f32 / -100.0)
} else {
explosion
.terrain_destruction_power
.max(explosion.max_damage as f32 / 100.0)
};
// Add an outcome
// Uses radius as outcome power, makes negative if explosion has healing effect
let outcome_power = explosion.radius
* if explosion.effects.iter().any(
|e| matches!(e, RadiusEffect::Entity(_, Effect::Damage(Damage { source: DamageSource::Healing, .. })))
) {
-1.0
} else {
1.0
};
ecs.write_resource::<Vec<Outcome>>()
.push(Outcome::Explosion {
pos,
power: outcome_power,
radius: explosion.radius,
is_attack: explosion.max_heal > 0 || explosion.max_damage > 0,
is_attack: explosion
.effects
.iter()
.any(|e| matches!(e, RadiusEffect::Entity(_, Effect::Damage(_)))),
reagent,
});
let owner_entity = owner.and_then(|uid| {
@ -534,129 +546,126 @@ pub fn handle_explosion(
});
let groups = ecs.read_storage::<comp::Group>();
for (entity_b, pos_b, ori_b, character_b, stats_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>(),
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
// RADIUS
&& distance_squared < explosion.radius.powi(2)
{
// See if entities are in the same group
let mut same_group = owner_entity
.and_then(|e| groups.get(e))
.map_or(false, |group_a| Some(group_a) == groups.get(entity_b));
if let Some(entity) = owner_entity {
if entity == entity_b {
same_group = true;
for effect in explosion.effects {
match effect {
RadiusEffect::TerrainDestruction(power) => {
const RAYS: usize = 500;
// Color terrain
let mut touched_blocks = Vec::new();
let color_range = power * 2.7;
for _ in 0..RAYS {
let dir = Vec3::new(
rand::random::<f32>() - 0.5,
rand::random::<f32>() - 0.5,
rand::random::<f32>() - 0.5,
)
.normalized();
let _ = ecs
.read_resource::<TerrainGrid>()
.ray(pos, pos + dir * color_range)
// TODO: Faster RNG
.until(|_| rand::random::<f32>() < 0.05)
.for_each(|_: &Block, pos| touched_blocks.push(pos))
.cast();
}
}
// Don't heal if outside group
// Don't damage in the same group
let is_damage = (friendly_damage || !same_group) && explosion.max_damage > 0;
let is_heal = same_group && explosion.max_heal > 0 && !friendly_damage;
if !is_heal && !is_damage {
continue;
}
let strength = 1.0 - distance_squared / explosion.radius.powi(2);
let damage = if is_heal {
Damage::Healing(
explosion.min_heal as f32
+ (explosion.max_heal - explosion.min_heal) as f32 * strength,
)
} else {
Damage::Explosion(
explosion.min_damage as f32
+ (explosion.max_damage - explosion.min_damage) as f32 * strength,
)
};
let block = character_b.map(|c_b| c_b.is_block()).unwrap_or(false)
&& ori_b.0.angle_between(pos - pos_b.0) < BLOCK_ANGLE.to_radians() / 2.0;
let change = damage.modify_damage(block, loadout_b, owner);
if change.amount != 0 {
stats_b.health.change_by(change);
if let Some(owner) = owner_entity {
if let Some(energy) = ecs.write_storage::<comp::Energy>().get_mut(owner) {
energy
.change_by(explosion.energy_regen as i32, comp::EnergySource::HitEnemy);
let terrain = ecs.read_resource::<TerrainGrid>();
let mut block_change = ecs.write_resource::<BlockChange>();
for block_pos in touched_blocks {
if let Ok(block) = terrain.get(block_pos) {
let diff2 = block_pos.map(|b| b as f32).distance_squared(pos);
let fade = (1.0 - diff2 / color_range.powi(2)).max(0.0);
if let Some(mut color) = block.get_color() {
let r = color[0] as f32
+ (fade * (color[0] as f32 * 0.5 - color[0] as f32));
let g = color[1] as f32
+ (fade * (color[1] as f32 * 0.3 - color[1] as f32));
let b = color[2] as f32
+ (fade * (color[2] as f32 * 0.3 - color[2] as f32));
color[0] = r as u8;
color[1] = g as u8;
color[2] = b as u8;
block_change.set(block_pos, Block::new(block.kind(), color));
}
}
}
}
}
}
const RAYS: usize = 500;
// Destroy terrain
for _ in 0..RAYS {
let dir = Vec3::new(
rand::random::<f32>() - 0.5,
rand::random::<f32>() - 0.5,
rand::random::<f32>() - 0.15,
)
.normalized();
// Color terrain
let mut touched_blocks = Vec::new();
let color_range = explosion.terrain_destruction_power * 2.7;
for _ in 0..RAYS {
let dir = Vec3::new(
rand::random::<f32>() - 0.5,
rand::random::<f32>() - 0.5,
rand::random::<f32>() - 0.5,
)
.normalized();
let _ = ecs
.read_resource::<TerrainGrid>()
.ray(pos, pos + dir * color_range)
// TODO: Faster RNG
.until(|_| rand::random::<f32>() < 0.05)
.for_each(|_: &Block, pos| touched_blocks.push(pos))
.cast();
}
let terrain = ecs.read_resource::<TerrainGrid>();
let mut block_change = ecs.write_resource::<BlockChange>();
for block_pos in touched_blocks {
if let Ok(block) = terrain.get(block_pos) {
let diff2 = block_pos.map(|b| b as f32).distance_squared(pos);
let fade = (1.0 - diff2 / color_range.powi(2)).max(0.0);
if let Some(mut color) = block.get_color() {
let r = color[0] as f32 + (fade * (color[0] as f32 * 0.5 - color[0] as f32));
let g = color[1] as f32 + (fade * (color[1] as f32 * 0.3 - color[1] as f32));
let b = color[2] as f32 + (fade * (color[2] as f32 * 0.3 - color[2] as f32));
color[0] = r as u8;
color[1] = g as u8;
color[2] = b as u8;
block_change.set(block_pos, Block::new(block.kind(), color));
}
}
}
// Destroy terrain
for _ in 0..RAYS {
let dir = Vec3::new(
rand::random::<f32>() - 0.5,
rand::random::<f32>() - 0.5,
rand::random::<f32>() - 0.15,
)
.normalized();
let terrain = ecs.read_resource::<TerrainGrid>();
let _ = terrain
.ray(pos, pos + dir * explosion.terrain_destruction_power)
// TODO: Faster RNG
.until(|block| block.is_liquid() || rand::random::<f32>() < 0.05)
.for_each(|block: &Block, pos| {
if block.is_explodable() {
block_change.set(pos, block.into_vacant());
let terrain = ecs.read_resource::<TerrainGrid>();
let _ = terrain
.ray(pos, pos + dir * power)
// TODO: Faster RNG
.until(|block| block.is_liquid() || rand::random::<f32>() < 0.05)
.for_each(|block: &Block, pos| {
if block.is_explodable() {
block_change.set(pos, block.into_vacant());
}
})
.cast();
}
})
.cast();
},
RadiusEffect::Entity(target, mut effect) => {
for (entity_b, pos_b) in (&ecs.entities(), &ecs.read_storage::<comp::Pos>()).join()
{
// See if entities are in the same group
let mut same_group = owner_entity
.and_then(|e| groups.get(e))
.map_or(false, |group_a| Some(group_a) == groups.get(entity_b));
if let Some(entity) = owner_entity {
if entity == entity_b {
same_group = true;
}
}
let target_group = if same_group {
GroupTarget::InGroup
} else {
GroupTarget::OutOfGroup
};
if let Some(target) = target {
if target != target_group {
continue;
}
}
let distance_squared = pos.distance_squared(pos_b.0);
let strength = 1.0 - distance_squared / explosion.radius.powi(2);
if strength > 0.0 {
let is_alive = ecs
.read_storage::<comp::Health>()
.get(entity_b)
.map_or(false, |h| !h.is_dead);
if is_alive {
effect.modify_strength(strength);
server.state().apply_effect(entity_b, effect, owner);
// Apply energy change
if let Some(owner) = owner_entity {
if let Some(energy) =
ecs.write_storage::<comp::Energy>().get_mut(owner)
{
energy.change_by(EnergyChange {
amount: explosion.energy_regen as i32,
source: comp::EnergySource::HitEnemy,
});
}
}
}
}
}
},
}
}
}
@ -732,3 +741,10 @@ pub fn handle_buff(server: &mut Server, entity: EcsEntity, buff_change: buff::Bu
}
}
}
pub fn handle_energy_change(server: &Server, entity: EcsEntity, change: EnergyChange) {
let ecs = &server.state.ecs();
if let Some(energy) = ecs.write_storage::<Energy>().get_mut(entity) {
energy.change_by(change);
}
}

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
}
@ -349,7 +349,7 @@ pub fn handle_inventory(server: &mut Server, entity: EcsEntity, manip: comp::Inv
drop(inventories);
if let Some(effect) = maybe_effect {
state.apply_effect(entity, effect);
state.apply_effect(entity, effect, None);
}
if let Some(event) = event {
state.write_component(entity, comp::InventoryUpdate::new(event));

View File

@ -8,8 +8,8 @@ use entity_creation::{
handle_loaded_character_data, handle_shockwave, handle_shoot,
};
use entity_manipulation::{
handle_buff, handle_damage, handle_destroy, handle_explosion, handle_knockback,
handle_land_on_ground, handle_level_up, handle_respawn,
handle_buff, handle_damage, handle_destroy, handle_energy_change, handle_explosion,
handle_knockback, handle_land_on_ground, handle_level_up, handle_respawn,
};
use group_manip::handle_group;
use interaction::{handle_lantern, handle_mount, handle_possess, handle_unmount};
@ -58,9 +58,8 @@ impl Server {
pos,
explosion,
owner,
friendly_damage,
reagent,
} => handle_explosion(&self, pos, explosion, owner, friendly_damage, reagent),
} => handle_explosion(&self, pos, explosion, owner, reagent),
ServerEvent::Shoot {
entity,
dir,
@ -83,7 +82,7 @@ impl Server {
ServerEvent::Knockback { entity, impulse } => {
handle_knockback(&self, entity, impulse)
},
ServerEvent::Damage { uid, change } => handle_damage(&self, uid, change),
ServerEvent::Damage { entity, change } => handle_damage(&self, entity, change),
ServerEvent::Destroy { entity, cause } => handle_destroy(self, entity, cause),
ServerEvent::InventoryManip(entity, manip) => handle_inventory(self, entity, manip),
ServerEvent::GroupManip(entity, manip) => handle_group(self, entity, manip),
@ -110,6 +109,7 @@ impl Server {
ServerEvent::CreateNpc {
pos,
stats,
health,
loadout,
body,
agent,
@ -117,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) => {
@ -137,6 +137,9 @@ impl Server {
entity,
buff_change,
} => handle_buff(self, entity, buff_change),
ServerEvent::EnergyChange { entity, change } => {
handle_energy_change(&self, entity, change)
},
}
}

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

@ -20,12 +20,13 @@ use vek::*;
pub trait StateExt {
/// Updates a component associated with the entity based on the `Effect`
fn apply_effect(&mut self, entity: EcsEntity, effect: Effect);
fn apply_effect(&self, entity: EcsEntity, effect: Effect, source: Option<Uid>);
/// Build a non-player character
fn create_npc(
&mut self,
pos: comp::Pos,
stats: comp::Stats,
health: comp::Health,
loadout: comp::Loadout,
body: comp::Body,
) -> EcsEntityBuilder;
@ -70,13 +71,13 @@ pub trait StateExt {
}
impl StateExt for State {
fn apply_effect(&mut self, entity: EcsEntity, effect: Effect) {
fn apply_effect(&self, entity: EcsEntity, effect: Effect, source: Option<Uid>) {
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()
@ -84,6 +85,14 @@ impl StateExt for State {
.get_mut(entity)
.map(|stats| stats.exp.change_by(xp));
},
Effect::Damage(damage) => {
let loadouts = self.ecs().read_storage::<comp::Loadout>();
let change = damage.modify_damage(loadouts.get(entity), source);
self.ecs()
.write_storage::<comp::Health>()
.get_mut(entity)
.map(|health| health.change_by(change));
},
}
}
@ -91,6 +100,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 +117,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))
@ -239,6 +250,10 @@ impl StateExt for State {
z_max: body.height(),
});
self.write_component(entity, body);
self.write_component(
entity,
comp::Health::new(stats.body_type, stats.level.level()),
);
self.write_component(entity, stats);
self.write_component(entity, inventory);
self.write_component(entity, loadout);

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,10 @@
use common::{
comp::{HealthSource, Object, PhysicsState, Pos, Vel},
effect::Effect,
event::{EventBus, ServerEvent},
span,
state::DeltaTime,
Explosion,
Damage, DamageSource, Explosion, RadiusEffect,
};
use specs::{Entities, Join, Read, ReadStorage, System, WriteStorage};
@ -48,16 +49,20 @@ impl<'a> System<'a> for Sys {
server_emitter.emit(ServerEvent::Explosion {
pos: pos.0,
explosion: Explosion {
effects: vec![
RadiusEffect::Entity(
None,
Effect::Damage(Damage {
source: DamageSource::Explosion,
value: 500.0,
}),
),
RadiusEffect::TerrainDestruction(4.0),
],
radius: 12.0,
max_damage: 500,
min_damage: 100,
max_heal: 0,
min_heal: 0,
terrain_destruction_power: 4.0,
energy_regen: 0,
},
owner: *owner,
friendly_damage: true,
reagent: None,
});
}
@ -71,16 +76,20 @@ impl<'a> System<'a> for Sys {
server_emitter.emit(ServerEvent::Explosion {
pos: pos.0,
explosion: Explosion {
effects: vec![
RadiusEffect::Entity(
None,
Effect::Damage(Damage {
source: DamageSource::Explosion,
value: 50.0,
}),
),
RadiusEffect::TerrainDestruction(4.0),
],
radius: 12.0,
max_damage: 50,
min_damage: 10,
max_heal: 0,
min_heal: 0,
terrain_destruction_power: 4.0,
energy_regen: 0,
},
owner: *owner,
friendly_damage: true,
reagent: Some(*reagent),
});
}

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,20 +73,16 @@ 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)
// (maybe health changes could be sent to the client as a list
// of events)
if match health.last_change.1.cause {
HealthSource::Attack { by }
| HealthSource::Projectile { owner: Some(by) }
| HealthSource::Energy { owner: Some(by) }
| HealthSource::Explosion { owner: Some(by) }
| HealthSource::Buff { owner: Some(by) }
| HealthSource::Healing { by: Some(by) } => {
HealthSource::Damage { by: Some(by), .. }
| HealthSource::Heal { by: Some(by) } => {
let by_me = my_uid.map_or(false, |&uid| by == uid);
// If the attack was by me also reset this timer
if by_me {
@ -101,8 +106,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

@ -373,9 +373,9 @@ impl<'a> Widget for Chat<'a> {
.localized_strings
.get("hud.chat.pvp_energy_kill_msg")
.to_string(),
KillSource::Player(_, KillType::Buff) => self
KillSource::Player(_, KillType::Other) => self
.localized_strings
.get("hud.chat.pvp_buff_kill_msg")
.get("hud.chat.pvp_other_kill_msg")
.to_string(),
KillSource::NonPlayer(_, KillType::Melee) => self
.localized_strings
@ -393,9 +393,9 @@ impl<'a> Widget for Chat<'a> {
.localized_strings
.get("hud.chat.npc_energy_kill_msg")
.to_string(),
KillSource::NonPlayer(_, KillType::Buff) => self
KillSource::NonPlayer(_, KillType::Other) => self
.localized_strings
.get("hud.chat.npc_buff_kill_msg")
.get("hud.chat.npc_other_kill_msg")
.to_string(),
KillSource::Environment(_) => self
.localized_strings

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::should_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 should_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 should_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 should_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)