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_ranged_kill_msg": "[{attacker}] hat [{victim}] aus der Ferne getötet",
"hud.chat.pvp_explosion_kill_msg": "[{attacker}] hat [{victim}] hochgejagt", "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_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_melee_kill_msg": "{attacker} tötete [{victim}]",
"hud.chat.npc_ranged_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_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 // SCT outputs
"hud.sct.experience": "{amount} Erf", "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_ranged_kill_msg": "[{attacker}] shot [{victim}]",
"hud.chat.pvp_explosion_kill_msg": "[{attacker}] blew up [{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_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_melee_kill_msg": "{attacker} killed [{victim}]",
"hud.chat.npc_ranged_kill_msg": "{attacker} shot [{victim}]", "hud.chat.npc_ranged_kill_msg": "{attacker} shot [{victim}]",
"hud.chat.npc_explosion_kill_msg": "{attacker} blew up [{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_msg": "You picked up [{item}]",
"hud.chat.loot_fail": "Your Inventory is full!", "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_ranged_kill_msg": "[{victim}], [{attacker}] tarafından vuruldu.",
"hud.chat.pvp_explosion_kill_msg": "[{victim}], [{attacker}] tarafından havaya uçuruldu.", "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_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_melee_kill_msg": "[{victim}], [{attacker}] tarafından mağlup edildi.",
"hud.chat.npc_ranged_kill_msg": "[{victim}], [{attacker}] tarafından vuruldu.", "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_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_msg": "[{item}] topladın.",
"hud.chat.loot_fail": "Envanterin dolu!", "hud.chat.loot_fail": "Envanterin dolu!",

View File

@ -583,16 +583,16 @@ impl Client {
} }
pub fn pick_up(&mut self, entity: EcsEntity) { 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 let Some(uid) = self.state.read_component_copied(entity) {
// If we're dead, exit before sending the message // If we're dead, exit before sending the message
if self if self
.state .state
.ecs() .ecs()
.read_storage::<comp::Stats>() .read_storage::<comp::Health>()
.get(self.entity) .get(self.entity)
.map_or(false, |s| s.is_dead) .map_or(false, |h| h.is_dead)
{ {
return; return;
} }
@ -731,9 +731,9 @@ impl Client {
if self if self
.state .state
.ecs() .ecs()
.read_storage::<comp::Stats>() .read_storage::<comp::Health>()
.get(self.entity) .get(self.entity)
.map_or(false, |s| s.is_dead) .map_or(false, |h| h.is_dead)
{ {
self.send_msg(ClientGeneral::ControlEvent(ControlEvent::Respawn)); self.send_msg(ClientGeneral::ControlEvent(ControlEvent::Respawn));
} }
@ -1733,7 +1733,7 @@ impl Client {
alias_of_uid(attacker_uid), alias_of_uid(attacker_uid),
alias_of_uid(victim) alias_of_uid(victim)
), ),
KillSource::Player(attacker_uid, KillType::Buff) => format!( KillSource::Player(attacker_uid, KillType::Other) => format!(
"[{}] killed [{}]", "[{}] killed [{}]",
alias_of_uid(attacker_uid), alias_of_uid(attacker_uid),
alias_of_uid(victim) alias_of_uid(victim)
@ -1752,7 +1752,7 @@ impl Client {
attacker_name, attacker_name,
alias_of_uid(victim) alias_of_uid(victim)
), ),
KillSource::NonPlayer(attacker_name, KillType::Buff) => { KillSource::NonPlayer(attacker_name, KillType::Other) => {
format!("{} killed [{}]", attacker_name, alias_of_uid(victim)) format!("{} killed [{}]", attacker_name, alias_of_uid(victim))
}, },
KillSource::Environment(environment) => { KillSource::Environment(environment) => {
@ -1780,7 +1780,7 @@ impl Client {
KillSource::Player(attacker_uid, KillType::Energy) => message KillSource::Player(attacker_uid, KillType::Energy) => message
.replace("{attacker}", &alias_of_uid(attacker_uid)) .replace("{attacker}", &alias_of_uid(attacker_uid))
.replace("{victim}", &alias_of_uid(victim)), .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("{attacker}", &alias_of_uid(attacker_uid))
.replace("{victim}", &alias_of_uid(victim)), .replace("{victim}", &alias_of_uid(victim)),
KillSource::NonPlayer(attacker_name, KillType::Melee) => message KillSource::NonPlayer(attacker_name, KillType::Melee) => message
@ -1795,7 +1795,7 @@ impl Client {
KillSource::NonPlayer(attacker_name, KillType::Energy) => message KillSource::NonPlayer(attacker_name, KillType::Energy) => message
.replace("{attacker}", attacker_name) .replace("{attacker}", attacker_name)
.replace("{victim}", &alias_of_uid(victim)), .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("{attacker}", attacker_name)
.replace("{victim}", &alias_of_uid(victim)), .replace("{victim}", &alias_of_uid(victim)),
KillSource::Environment(environment) => message KillSource::Environment(environment) => message

View File

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

View File

@ -452,6 +452,7 @@ impl From<(&CharacterAbility, AbilityKey)> for CharacterState {
swing_duration: *swing_duration, swing_duration: *swing_duration,
recover_duration: *recover_duration, recover_duration: *recover_duration,
is_interruptible: *is_interruptible, is_interruptible: *is_interruptible,
ability_key: key,
}, },
auto_charge: false, auto_charge: false,
timer: Duration::default(), timer: Duration::default(),
@ -489,6 +490,7 @@ impl From<(&CharacterAbility, AbilityKey)> for CharacterState {
speed_increase: 1.0 - *speed_increase, speed_increase: 1.0 - *speed_increase,
max_speed_increase: *max_speed_increase - 1.0, max_speed_increase: *max_speed_increase - 1.0,
is_interruptible: *is_interruptible, is_interruptible: *is_interruptible,
ability_key: key,
}, },
stage: 1, stage: 1,
combo: 0, combo: 0,
@ -552,6 +554,7 @@ impl From<(&CharacterAbility, AbilityKey)> for CharacterState {
is_interruptible: *is_interruptible, is_interruptible: *is_interruptible,
forward_speed: *forward_speed, forward_speed: *forward_speed,
num_spins: *num_spins, num_spins: *num_spins,
ability_key: key,
}, },
timer: Duration::default(), timer: Duration::default(),
spins_remaining: *num_spins - 1, spins_remaining: *num_spins - 1,
@ -583,6 +586,7 @@ impl From<(&CharacterAbility, AbilityKey)> for CharacterState {
charge_duration: *charge_duration, charge_duration: *charge_duration,
swing_duration: *swing_duration, swing_duration: *swing_duration,
recover_duration: *recover_duration, recover_duration: *recover_duration,
ability_key: key,
}, },
stage_section: StageSection::Charge, stage_section: StageSection::Charge,
timer: Duration::default(), timer: Duration::default(),
@ -619,6 +623,7 @@ impl From<(&CharacterAbility, AbilityKey)> for CharacterState {
projectile_gravity: *projectile_gravity, projectile_gravity: *projectile_gravity,
initial_projectile_speed: *initial_projectile_speed, initial_projectile_speed: *initial_projectile_speed,
max_projectile_speed: *max_projectile_speed, max_projectile_speed: *max_projectile_speed,
ability_key: key,
}, },
timer: Duration::default(), timer: Duration::default(),
stage_section: StageSection::Buildup, 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 serde::{Deserialize, Serialize};
use specs::{Component, FlaggedStorage}; use specs::{Component, FlaggedStorage};
use specs_idvs::IdvStorage; use specs_idvs::IdvStorage;
@ -8,7 +8,7 @@ use std::time::Duration;
pub struct Properties { pub struct Properties {
pub angle: f32, pub angle: f32,
pub speed: f32, pub speed: f32,
pub damages: Damages, pub damages: Vec<(Option<GroupTarget>, Damage)>,
pub lifesteal_eff: f32, pub lifesteal_eff: f32,
pub energy_regen: u32, pub energy_regen: u32,
pub energy_cost: u32, pub energy_cost: u32,

View File

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

View File

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

View File

@ -47,9 +47,9 @@ impl Energy {
self.current = amount; self.current = amount;
} }
pub fn change_by(&mut self, amount: i32, cause: EnergySource) { pub fn change_by(&mut self, change: EnergyChange) {
self.current = ((self.current as i32 + amount).max(0) as u32).min(self.maximum); self.current = ((self.current as i32 + change.amount).max(0) as u32).min(self.maximum);
self.last_change = Some((amount, 0.0, cause)); self.last_change = Some((change.amount, 0.0, change.source));
} }
pub fn try_change_by( pub fn try_change_by(
@ -62,7 +62,10 @@ impl Energy {
} else if self.current as i32 + amount > self.maximum as i32 { } else if self.current as i32 + amount > self.maximum as i32 {
Err(StatChangeError::Overflow) Err(StatChangeError::Overflow)
} else { } else {
self.change_by(amount, cause); self.change_by(EnergyChange {
amount,
source: cause,
});
Ok(()) Ok(())
} }
} }
@ -73,6 +76,11 @@ impl Energy {
} }
} }
pub struct EnergyChange {
pub amount: i32,
pub source: EnergySource,
}
impl Component for Energy { impl Component for Energy {
type Storage = FlaggedStorage<Self, IdvStorage<Self>>; 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}, buff::{Buff, BuffCategory, BuffData, BuffKind, BuffSource},
projectile, Body, CharacterAbility, Gravity, LightEmitter, Projectile, projectile, Body, CharacterAbility, Gravity, LightEmitter, Projectile,
}, },
effect::Effect,
states::combo_melee, states::combo_melee,
Damage, Damages, Explosion, Knockback, Damage, DamageSource, Explosion, GroupTarget, Knockback, RadiusEffect,
}; };
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::time::Duration; use std::time::Duration;
@ -302,10 +303,10 @@ impl Tool {
projectile: Projectile { projectile: Projectile {
hit_solid: vec![projectile::Effect::Stick], hit_solid: vec![projectile::Effect::Stick],
hit_entity: vec![ hit_entity: vec![
projectile::Effect::Damages(Damages::new( projectile::Effect::Damage(Some(GroupTarget::OutOfGroup), Damage {
Some(Damage::Projectile(40.0 * self.base_power())), source: DamageSource::Projectile,
None, value: 40.0 * self.base_power(),
)), }),
projectile::Effect::Knockback(Knockback::Away(10.0)), projectile::Effect::Knockback(Knockback::Away(10.0)),
projectile::Effect::RewardEnergy(50), projectile::Effect::RewardEnergy(50),
projectile::Effect::Vanish, projectile::Effect::Vanish,
@ -357,10 +358,10 @@ impl Tool {
projectile: Projectile { projectile: Projectile {
hit_solid: vec![projectile::Effect::Stick], hit_solid: vec![projectile::Effect::Stick],
hit_entity: vec![ hit_entity: vec![
projectile::Effect::Damages(Damages::new( projectile::Effect::Damage(Some(GroupTarget::OutOfGroup), Damage {
Some(Damage::Projectile(40.0 * self.base_power())), source: DamageSource::Projectile,
None, value: 40.0 * self.base_power(),
)), }),
projectile::Effect::Knockback(Knockback::Away(10.0)), projectile::Effect::Knockback(Knockback::Away(10.0)),
projectile::Effect::Vanish, projectile::Effect::Vanish,
projectile::Effect::Buff { projectile::Effect::Buff {
@ -419,24 +420,46 @@ impl Tool {
projectile: Projectile { projectile: Projectile {
hit_solid: vec![ hit_solid: vec![
projectile::Effect::Explode(Explosion { 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(), 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, energy_regen: 0,
}), }),
projectile::Effect::Vanish, projectile::Effect::Vanish,
], ],
hit_entity: vec![ hit_entity: vec![
projectile::Effect::Explode(Explosion { 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(), 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, energy_regen: 0,
}), }),
projectile::Effect::Vanish, projectile::Effect::Vanish,
@ -462,24 +485,28 @@ impl Tool {
projectile: Projectile { projectile: Projectile {
hit_solid: vec![ hit_solid: vec![
projectile::Effect::Explode(Explosion { 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, 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, energy_regen: 50,
}), }),
projectile::Effect::Vanish, projectile::Effect::Vanish,
], ],
hit_entity: vec![ hit_entity: vec![
projectile::Effect::Explode(Explosion { 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, 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, energy_regen: 50,
}), }),
projectile::Effect::Vanish, projectile::Effect::Vanish,

View File

@ -9,6 +9,7 @@ pub mod chat;
mod controller; mod controller;
mod energy; mod energy;
pub mod group; pub mod group;
mod health;
mod inputs; mod inputs;
mod inventory; mod inventory;
mod last; mod last;
@ -43,8 +44,9 @@ pub use controller::{
Climb, ControlAction, ControlEvent, Controller, ControllerInputs, GroupManip, Input, Climb, ControlAction, ControlEvent, Controller, ControllerInputs, GroupManip, Input,
InventoryManip, MountState, Mounting, InventoryManip, MountState, Mounting,
}; };
pub use energy::{Energy, EnergySource}; pub use energy::{Energy, EnergyChange, EnergySource};
pub use group::Group; pub use group::Group;
pub use health::{Health, HealthChange, HealthSource};
pub use inputs::CanBuild; pub use inputs::CanBuild;
pub use inventory::{ pub use inventory::{
item, item,
@ -59,5 +61,5 @@ pub use player::Player;
pub use projectile::Projectile; pub use projectile::Projectile;
pub use shockwave::{Shockwave, ShockwaveHitEntities}; pub use shockwave::{Shockwave, ShockwaveHitEntities};
pub use skills::{Skill, SkillGroup, SkillGroupType, SkillSet}; 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}; 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 serde::{Deserialize, Serialize};
use specs::{Component, FlaggedStorage}; use specs::{Component, FlaggedStorage};
use specs_idvs::IdvStorage; use specs_idvs::IdvStorage;
@ -6,7 +6,7 @@ use std::time::Duration;
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub enum Effect { pub enum Effect {
Damages(Damages), Damage(Option<GroupTarget>, Damage),
Knockback(Knockback), Knockback(Knockback),
RewardEnergy(u32), RewardEnergy(u32),
Explode(Explosion), 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 serde::{Deserialize, Serialize};
use specs::{Component, FlaggedStorage}; use specs::{Component, FlaggedStorage};
use specs_idvs::IdvStorage; use specs_idvs::IdvStorage;
@ -9,7 +9,7 @@ pub struct Properties {
pub angle: f32, pub angle: f32,
pub vertical_angle: f32, pub vertical_angle: f32,
pub speed: f32, pub speed: f32,
pub damages: Damages, pub damages: Vec<(Option<GroupTarget>, Damage)>,
pub knockback: Knockback, pub knockback: Knockback,
pub requires_ground: bool, pub requires_ground: bool,
pub duration: Duration, pub duration: Duration,

View File

@ -1,45 +1,12 @@
use crate::{ use crate::{
comp, comp,
comp::{body::humanoid::Species, skills::SkillSet, Body}, comp::{body::humanoid::Species, skills::SkillSet, Body},
sync::Uid,
}; };
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use specs::{Component, FlaggedStorage}; use specs::{Component, FlaggedStorage};
use specs_idvs::IdvStorage; use specs_idvs::IdvStorage;
use std::{error::Error, fmt}; 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)] #[derive(Clone, Copy, Debug, Serialize, Deserialize)]
pub struct Exp { pub struct Exp {
current: u32, current: u32,
@ -51,41 +18,6 @@ pub struct Level {
amount: u32, 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)] #[derive(Debug)]
pub enum StatChangeError { pub enum StatChangeError {
Underflow, Underflow,
@ -139,35 +71,15 @@ impl Level {
#[derive(Clone, Debug, Serialize, Deserialize)] #[derive(Clone, Debug, Serialize, Deserialize)]
pub struct Stats { pub struct Stats {
pub name: String, pub name: String,
pub health: Health,
pub level: Level, pub level: Level,
pub exp: Exp, pub exp: Exp,
pub skill_set: SkillSet, pub skill_set: SkillSet,
pub endurance: u32, pub endurance: u32,
pub fitness: u32, pub fitness: u32,
pub willpower: u32, pub willpower: u32,
pub is_dead: bool,
pub body_type: Body, 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 { impl Stats {
pub fn new(name: String, body: Body) -> Self { pub fn new(name: String, body: Body) -> Self {
let species = if let comp::Body::Humanoid(hbody) = body { let species = if let comp::Body::Humanoid(hbody) = body {
@ -189,17 +101,8 @@ impl Stats {
None => (0, 0, 0), None => (0, 0, 0),
}; };
let mut stats = Self { Self {
name, name,
health: Health {
current: 0,
maximum: 0,
base_max: 0,
last_change: (0.0, HealthChange {
amount: 0,
cause: HealthSource::Revive,
}),
},
level: Level { amount: 1 }, level: Level { amount: 1 },
exp: Exp { exp: Exp {
current: 0, current: 0,
@ -209,17 +112,8 @@ impl Stats {
endurance, endurance,
fitness, fitness,
willpower, willpower,
is_dead: false,
body_type: body, 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 /// Creates an empty `Stats` instance - used during character loading from
@ -227,15 +121,6 @@ impl Stats {
pub fn empty() -> Self { pub fn empty() -> Self {
Self { Self {
name: "".to_owned(), 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 }, level: Level { amount: 1 },
exp: Exp { exp: Exp {
current: 0, current: 0,
@ -245,27 +130,11 @@ impl Stats {
endurance: 0, endurance: 0,
fitness: 0, fitness: 0,
willpower: 0, willpower: 0,
is_dead: false,
body_type: comp::Body::Humanoid(comp::body::humanoid::Body::random()), 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 { impl Component for Stats {
type Storage = FlaggedStorage<Self, IdvStorage<Self>>; 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}; use serde::{Deserialize, Serialize};
/// An effect that may be applied to an entity /// 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 { pub enum Effect {
Health(comp::HealthChange), Health(comp::HealthChange),
Xp(i64), Xp(i64),
Damage(combat::Damage),
} }
impl Effect { impl Effect {
@ -13,6 +14,21 @@ impl Effect {
match self { match self {
Effect::Health(c) => format!("{:+} health", c.amount), Effect::Health(c) => format!("{:+} health", c.amount),
Effect::Xp(n) => format!("{:+} exp", n), 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>, pos: Vec3<f32>,
explosion: Explosion, explosion: Explosion,
owner: Option<Uid>, owner: Option<Uid>,
friendly_damage: bool,
reagent: Option<Reagent>, reagent: Option<Reagent>,
}, },
Damage { Damage {
uid: Uid, entity: EcsEntity,
change: comp::HealthChange, change: comp::HealthChange,
}, },
Destroy { Destroy {
@ -93,6 +92,7 @@ pub enum ServerEvent {
CreateNpc { CreateNpc {
pos: comp::Pos, pos: comp::Pos,
stats: comp::Stats, stats: comp::Stats,
health: comp::Health,
loadout: comp::Loadout, loadout: comp::Loadout,
body: comp::Body, body: comp::Body,
agent: Option<comp::Agent>, agent: Option<comp::Agent>,
@ -110,6 +110,10 @@ pub enum ServerEvent {
entity: EcsEntity, entity: EcsEntity,
buff_change: comp::BuffChange, buff_change: comp::BuffChange,
}, },
EnergyChange {
entity: EcsEntity,
change: comp::EnergyChange,
},
} }
pub struct EventBus<E> { pub struct EventBus<E> {

View File

@ -1,12 +1,15 @@
use crate::{combat::GroupTarget, effect::Effect};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct Explosion { pub struct Explosion {
pub effects: Vec<RadiusEffect>,
pub radius: f32, 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, 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 vol;
pub mod volumes; pub mod volumes;
pub use combat::{Damage, Damages, Knockback}; pub use combat::{Damage, DamageSource, GroupTarget, Knockback};
pub use explosion::Explosion; pub use explosion::{Explosion, RadiusEffect};
pub use loadout_builder::LoadoutBuilder; pub use loadout_builder::LoadoutBuilder;

View File

@ -15,6 +15,7 @@ sum_type! {
Stats(comp::Stats), Stats(comp::Stats),
Buffs(comp::Buffs), Buffs(comp::Buffs),
Energy(comp::Energy), Energy(comp::Energy),
Health(comp::Health),
LightEmitter(comp::LightEmitter), LightEmitter(comp::LightEmitter),
Item(comp::Item), Item(comp::Item),
Scale(comp::Scale), Scale(comp::Scale),
@ -45,6 +46,7 @@ sum_type! {
Stats(PhantomData<comp::Stats>), Stats(PhantomData<comp::Stats>),
Buffs(PhantomData<comp::Buffs>), Buffs(PhantomData<comp::Buffs>),
Energy(PhantomData<comp::Energy>), Energy(PhantomData<comp::Energy>),
Health(PhantomData<comp::Health>),
LightEmitter(PhantomData<comp::LightEmitter>), LightEmitter(PhantomData<comp::LightEmitter>),
Item(PhantomData<comp::Item>), Item(PhantomData<comp::Item>),
Scale(PhantomData<comp::Scale>), Scale(PhantomData<comp::Scale>),
@ -75,6 +77,7 @@ impl sync::CompPacket for EcsCompPacket {
EcsCompPacket::Stats(comp) => sync::handle_insert(comp, entity, world), EcsCompPacket::Stats(comp) => sync::handle_insert(comp, entity, world),
EcsCompPacket::Buffs(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::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::LightEmitter(comp) => sync::handle_insert(comp, entity, world),
EcsCompPacket::Item(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), 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::Stats(comp) => sync::handle_modify(comp, entity, world),
EcsCompPacket::Buffs(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::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::LightEmitter(comp) => sync::handle_modify(comp, entity, world),
EcsCompPacket::Item(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), 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::Stats(_) => sync::handle_remove::<comp::Stats>(entity, world),
EcsCompPhantom::Buffs(_) => sync::handle_remove::<comp::Buffs>(entity, world), EcsCompPhantom::Buffs(_) => sync::handle_remove::<comp::Buffs>(entity, world),
EcsCompPhantom::Energy(_) => sync::handle_remove::<comp::Energy>(entity, world), EcsCompPhantom::Energy(_) => sync::handle_remove::<comp::Energy>(entity, world),
EcsCompPhantom::Health(_) => sync::handle_remove::<comp::Health>(entity, world),
EcsCompPhantom::LightEmitter(_) => { EcsCompPhantom::LightEmitter(_) => {
sync::handle_remove::<comp::LightEmitter>(entity, world) sync::handle_remove::<comp::LightEmitter>(entity, world)
}, },

View File

@ -114,6 +114,7 @@ impl State {
ecs.register::<comp::Stats>(); ecs.register::<comp::Stats>();
ecs.register::<comp::Buffs>(); ecs.register::<comp::Buffs>();
ecs.register::<comp::Energy>(); ecs.register::<comp::Energy>();
ecs.register::<comp::Health>();
ecs.register::<comp::CanBuild>(); ecs.register::<comp::CanBuild>();
ecs.register::<comp::LightEmitter>(); ecs.register::<comp::LightEmitter>();
ecs.register::<comp::Item>(); ecs.register::<comp::Item>();

View File

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

View File

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

View File

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

View File

@ -1,8 +1,8 @@
use crate::{ use crate::{
comp::{Attacking, CharacterState, EnergySource, StateUpdate}, comp::{Attacking, CharacterState, EnergyChange, EnergySource, StateUpdate},
states::utils::*, states::utils::*,
sys::character_behavior::{CharacterBehavior, JoinData}, sys::character_behavior::{CharacterBehavior, JoinData},
Damage, Damages, Knockback, Damage, DamageSource, GroupTarget, Knockback,
}; };
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::time::Duration; use std::time::Duration;
@ -54,6 +54,8 @@ pub struct StaticData {
pub max_speed_increase: f32, pub max_speed_increase: f32,
/// Whether the state can be interrupted by other abilities /// Whether the state can be interrupted by other abilities
pub is_interruptible: bool, 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 /// A sequence of attacks that can incrementally become faster and more
/// damaging. /// damaging.
@ -84,7 +86,9 @@ impl CharacterBehavior for Data {
let stage_index = (self.stage - 1) as usize; let stage_index = (self.stage - 1) as usize;
// Allows for other states to interrupt this state // 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); handle_interrupt(data, &mut update);
match update.character { match update.character {
CharacterState::ComboMelee(_) => {}, CharacterState::ComboMelee(_) => {},
@ -127,7 +131,10 @@ impl CharacterBehavior for Data {
* self.static_data.stage_data[stage_index].damage_increase, * self.static_data.stage_data[stage_index].damage_increase,
); );
data.updater.insert(data.entity, Attacking { 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, range: self.static_data.stage_data[stage_index].range,
max_angle: self.static_data.stage_data[stage_index].angle.to_radians(), max_angle: self.static_data.stage_data[stage_index].angle.to_radians(),
applied: false, applied: false,
@ -177,7 +184,7 @@ impl CharacterBehavior for Data {
StageSection::Recover => { StageSection::Recover => {
if self.timer < self.static_data.stage_data[stage_index].base_recover_duration { if self.timer < self.static_data.stage_data[stage_index].base_recover_duration {
// Recovers // 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 // Checks if state will transition to next stage after recover
update.character = CharacterState::ComboMelee(Data { update.character = CharacterState::ComboMelee(Data {
static_data: self.static_data.clone(), static_data: self.static_data.clone(),
@ -255,7 +262,10 @@ impl CharacterBehavior for Data {
next_stage: self.next_stage, next_stage: self.next_stage,
}); });
data.updater.remove::<Attacking>(data.entity); 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::{ use crate::{
comp::{Attacking, CharacterState, EnergySource, StateUpdate}, comp::{Attacking, CharacterState, EnergyChange, EnergySource, StateUpdate},
states::utils::*, states::utils::*,
sys::character_behavior::{CharacterBehavior, JoinData}, sys::character_behavior::{CharacterBehavior, JoinData},
Damage, Damages, Knockback, Damage, DamageSource, GroupTarget, Knockback,
}; };
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::time::Duration; use std::time::Duration;
@ -39,6 +39,8 @@ pub struct StaticData {
pub recover_duration: Duration, pub recover_duration: Duration,
/// Whether the state can be interrupted by other abilities /// Whether the state can be interrupted by other abilities
pub is_interruptible: bool, pub is_interruptible: bool,
/// What key is used to press ability
pub ability_key: AbilityKey,
} }
#[derive(Copy, Clone, Debug, PartialEq, Serialize, Deserialize)] #[derive(Copy, Clone, Debug, PartialEq, Serialize, Deserialize)]
@ -66,7 +68,9 @@ impl CharacterBehavior for Data {
handle_move(data, &mut update, 0.1); handle_move(data, &mut update, 0.1);
// Allows for other states to interrupt this state // 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); handle_interrupt(data, &mut update);
match update.character { match update.character {
CharacterState::DashMelee(_) => {}, CharacterState::DashMelee(_) => {},
@ -90,7 +94,7 @@ impl CharacterBehavior for Data {
} else { } else {
// Transitions to charge section of stage // Transitions to charge section of stage
update.character = CharacterState::DashMelee(Data { 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(), timer: Duration::default(),
stage_section: StageSection::Charge, stage_section: StageSection::Charge,
..*self ..*self
@ -100,7 +104,7 @@ impl CharacterBehavior for Data {
StageSection::Charge => { StageSection::Charge => {
if (self.static_data.infinite_charge if (self.static_data.infinite_charge
|| self.timer < self.static_data.charge_duration) || 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)) || (self.auto_charge && self.timer < self.static_data.charge_duration))
&& update.energy.current() > 0 && update.energy.current() > 0
{ {
@ -122,16 +126,20 @@ impl CharacterBehavior for Data {
let charge_frac = (self.timer.as_secs_f32() let charge_frac = (self.timer.as_secs_f32()
/ self.static_data.charge_duration.as_secs_f32()) / self.static_data.charge_duration.as_secs_f32())
.min(1.0); .min(1.0);
let damage = (self.static_data.max_damage as f32 let mut damage = Damage {
- self.static_data.base_damage as f32) source: DamageSource::Melee,
* charge_frac value: self.static_data.max_damage as f32,
+ self.static_data.base_damage as f32; };
damage.interpolate_damage(
charge_frac,
self.static_data.base_damage as f32,
);
let knockback = (self.static_data.max_knockback let knockback = (self.static_data.max_knockback
- self.static_data.base_knockback) - self.static_data.base_knockback)
* charge_frac * charge_frac
+ self.static_data.base_knockback; + self.static_data.base_knockback;
data.updater.insert(data.entity, Attacking { 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, range: self.static_data.range,
max_angle: self.static_data.angle.to_radians(), max_angle: self.static_data.angle.to_radians(),
applied: false, applied: false,
@ -172,10 +180,10 @@ impl CharacterBehavior for Data {
} }
// Consumes energy if there's enough left and charge has not stopped // Consumes energy if there's enough left and charge has not stopped
update.energy.change_by( update.energy.change_by(EnergyChange {
-(self.static_data.energy_drain as f32 * data.dt.0) as i32, amount: -(self.static_data.energy_drain as f32 * data.dt.0) as i32,
EnergySource::Ability, source: EnergySource::Ability,
); });
} else { } else {
// Transitions to swing section of stage // Transitions to swing section of stage
update.character = CharacterState::DashMelee(Data { update.character = CharacterState::DashMelee(Data {

View File

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

View File

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

View File

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

View File

@ -6,7 +6,7 @@ use crate::{
group::Invite, group::Invite,
item::{tool::ToolKind, ItemKind}, item::{tool::ToolKind, ItemKind},
Agent, Alignment, Body, CharacterState, ControlAction, ControlEvent, Controller, Energy, 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, UnresolvedChatMsg, Vel,
}, },
event::{EventBus, ServerEvent}, event::{EventBus, ServerEvent},
@ -46,7 +46,7 @@ impl<'a> System<'a> for Sys {
ReadStorage<'a, Vel>, ReadStorage<'a, Vel>,
ReadStorage<'a, Ori>, ReadStorage<'a, Ori>,
ReadStorage<'a, Scale>, ReadStorage<'a, Scale>,
ReadStorage<'a, Stats>, ReadStorage<'a, Health>,
ReadStorage<'a, Loadout>, ReadStorage<'a, Loadout>,
ReadStorage<'a, PhysicsState>, ReadStorage<'a, PhysicsState>,
ReadStorage<'a, Uid>, ReadStorage<'a, Uid>,
@ -76,7 +76,7 @@ impl<'a> System<'a> for Sys {
velocities, velocities,
orientations, orientations,
scales, scales,
stats, healths,
loadouts, loadouts,
physics_states, physics_states,
uids, uids,
@ -261,8 +261,8 @@ impl<'a> System<'a> for Sys {
} }
}, },
Activity::Follow { target, chaser } => { Activity::Follow { target, chaser } => {
if let (Some(tgt_pos), _tgt_stats) = if let (Some(tgt_pos), _tgt_health) =
(positions.get(*target), stats.get(*target)) (positions.get(*target), healths.get(*target))
{ {
let dist = pos.0.distance(tgt_pos.0); let dist = pos.0.distance(tgt_pos.0);
// Follow, or return to idle // Follow, or return to idle
@ -329,9 +329,9 @@ impl<'a> System<'a> for Sys {
_ => Tactic::Melee, _ => 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), positions.get(*target),
stats.get(*target), healths.get(*target),
alignments.get(*target).copied().unwrap_or( alignments.get(*target).copied().unwrap_or(
uids.get(*target) uids.get(*target)
.copied() .copied()
@ -346,7 +346,7 @@ impl<'a> System<'a> for Sys {
// Don't attack entities we are passive towards // Don't attack entities we are passive towards
// TODO: This is here, it's a bit of a hack // TODO: This is here, it's a bit of a hack
if let Some(alignment) = alignment { 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; do_idle = true;
break 'activity; break 'activity;
} }
@ -354,9 +354,9 @@ impl<'a> System<'a> for Sys {
let dist_sqrd = pos.0.distance_squared(tgt_pos.0); let dist_sqrd = pos.0.distance_squared(tgt_pos.0);
let damage = stats let damage = healths
.get(entity) .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); .unwrap_or(0.5);
// Flee // Flee
@ -557,9 +557,9 @@ impl<'a> System<'a> for Sys {
if choose_target { if choose_target {
// Search for new targets (this looks expensive, but it's only run occasionally) // 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 // 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() .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 search_dist = SEARCH_DIST;
let mut listen_dist = LISTEN_DIST; let mut listen_dist = LISTEN_DIST;
if char_state.map_or(false, |c_s| c_s.is_stealthy()) { 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 // Within listen distance
|| e_pos.0.distance_squared(pos.0) < listen_dist.powf(2.0)) || e_pos.0.distance_squared(pos.0) < listen_dist.powf(2.0))
&& *e != entity && *e != entity
&& !e_stats.is_dead && !e_health.is_dead
&& alignment && alignment
.and_then(|a| e_alignment.map(|b| a.hostile_towards(*b))) .and_then(|a| e_alignment.map(|b| a.hostile_towards(*b)))
.unwrap_or(false) .unwrap_or(false)
@ -602,20 +602,16 @@ impl<'a> System<'a> for Sys {
// last!) --- // last!) ---
// Attack a target that's attacking us // 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 // Only if the attack was recent
if my_stats.health.last_change.0 < 3.0 { if my_health.last_change.0 < 3.0 {
if let comp::HealthSource::Attack { by } if let comp::HealthSource::Damage { by: Some(by), .. } =
| comp::HealthSource::Projectile { owner: Some(by) } my_health.last_change.1.cause
| 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 !agent.activity.is_attack() { if !agent.activity.is_attack() {
if let Some(attacker) = uid_allocator.retrieve_entity_internal(by.id()) 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 { match agent.activity {
Activity::Attack { target, .. } if target == attacker => {}, Activity::Attack { target, .. } if target == attacker => {},
_ => { _ => {
@ -658,12 +654,10 @@ impl<'a> System<'a> for Sys {
} }
// Attack owner's attacker // Attack owner's attacker
let owner_stats = stats.get(owner)?; let owner_health = healths.get(owner)?;
if owner_stats.health.last_change.0 < 5.0 if owner_health.last_change.0 < 5.0 && owner_health.last_change.1.amount < 0 {
&& owner_stats.health.last_change.1.amount < 0 if let comp::HealthSource::Damage { by: Some(by), .. } =
{ owner_health.last_change.1.cause
if let comp::HealthSource::Attack { by } =
owner_stats.health.last_change.1.cause
{ {
if !agent.activity.is_attack() { if !agent.activity.is_attack() {
let attacker = uid_allocator.retrieve_entity_internal(by.id())?; let attacker = uid_allocator.retrieve_entity_internal(by.id())?;

View File

@ -1,19 +1,17 @@
use crate::{ use crate::{
comp::{ comp::{
group, Beam, BeamSegment, Body, CharacterState, Energy, EnergySource, HealthChange, group, Beam, BeamSegment, Body, Energy, EnergyChange, EnergySource, Health, HealthChange,
HealthSource, Last, Loadout, Ori, Pos, Scale, Stats, HealthSource, Last, Loadout, Ori, Pos, Scale,
}, },
event::{EventBus, ServerEvent}, event::{EventBus, ServerEvent},
state::{DeltaTime, Time}, state::{DeltaTime, Time},
sync::{Uid, UidAllocator}, sync::{Uid, UidAllocator},
Damage, GroupTarget,
}; };
use specs::{saveload::MarkerAllocator, Entities, Join, Read, ReadStorage, System, WriteStorage}; use specs::{saveload::MarkerAllocator, Entities, Join, Read, ReadStorage, System, WriteStorage};
use std::time::Duration; use std::time::Duration;
use vek::*; use vek::*;
pub const BLOCK_ANGLE: f32 = 180.0;
/// This system is responsible for handling beams that heal or do damage /// This system is responsible for handling beams that heal or do damage
pub struct Sys; pub struct Sys;
impl<'a> System<'a> for Sys { impl<'a> System<'a> for Sys {
@ -30,11 +28,10 @@ impl<'a> System<'a> for Sys {
ReadStorage<'a, Ori>, ReadStorage<'a, Ori>,
ReadStorage<'a, Scale>, ReadStorage<'a, Scale>,
ReadStorage<'a, Body>, ReadStorage<'a, Body>,
ReadStorage<'a, Stats>, ReadStorage<'a, Health>,
ReadStorage<'a, Loadout>, ReadStorage<'a, Loadout>,
ReadStorage<'a, group::Group>, ReadStorage<'a, group::Group>,
ReadStorage<'a, CharacterState>, ReadStorage<'a, Energy>,
WriteStorage<'a, Energy>,
WriteStorage<'a, BeamSegment>, WriteStorage<'a, BeamSegment>,
WriteStorage<'a, Beam>, WriteStorage<'a, Beam>,
); );
@ -53,11 +50,10 @@ impl<'a> System<'a> for Sys {
orientations, orientations,
scales, scales,
bodies, bodies,
stats, healths,
loadouts, loadouts,
groups, groups,
character_states, energies,
mut energies,
mut beam_segments, mut beam_segments,
mut beams, mut beams,
): Self::SystemData, ): Self::SystemData,
@ -68,8 +64,8 @@ impl<'a> System<'a> for Sys {
let dt = dt.0; let dt = dt.0;
// Beams // Beams
for (entity, uid, pos, ori, beam_segment) in for (entity, pos, ori, beam_segment) in
(&entities, &uids, &positions, &orientations, &beam_segments).join() (&entities, &positions, &orientations, &beam_segments).join()
{ {
let creation_time = match beam_segment.creation { let creation_time = match beam_segment.creation {
Some(time) => time, Some(time) => time,
@ -116,26 +112,14 @@ impl<'a> System<'a> for Sys {
}; };
// Go through all other effectable entities // Go through all other effectable entities
for ( for (b, uid_b, pos_b, last_pos_b_maybe, scale_b_maybe, health_b, body_b) in (
b,
uid_b,
pos_b,
last_pos_b_maybe,
ori_b,
scale_b_maybe,
character_b,
stats_b,
body_b,
) in (
&entities, &entities,
&uids, &uids,
&positions, &positions,
// TODO: make sure that these are maintained on the client and remove `.maybe()` // TODO: make sure that these are maintained on the client and remove `.maybe()`
last_positions.maybe(), last_positions.maybe(),
&orientations,
scales.maybe(), scales.maybe(),
character_states.maybe(), &healths,
&stats,
&bodies, &bodies,
) )
.join() .join()
@ -152,7 +136,7 @@ impl<'a> System<'a> for Sys {
// Check if it is a hit // Check if it is a hit
let hit = entity != b let hit = entity != b
&& !stats_b.is_dead && !health_b.is_dead
// Collision shapes // 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) && (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)})); || 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)) .map(|group_a| Some(group_a) == groups.get(b))
.unwrap_or(Some(*uid_b) == beam_segment.owner); .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 owner, shouldn't heal or damage
if Some(*uid_b) == beam_segment.owner { if Some(*uid_b) == beam_segment.owner {
continue; continue;
} }
let damage = if let Some(damage) = beam_segment.damages.get_damage(same_group) { for (target, damage) in beam_segment.damages.iter() {
damage if let Some(target) = target {
} else { if *target != target_group {
continue; 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,
},
},
});
} }
if let Some(energy_mut) = beam_owner.and_then(|o| energies.get_mut(o)) {
energy_mut.change_by( // Modify damage
beam_segment.energy_regen as i32, let change = damage.modify_damage(loadouts.get(b), beam_segment.owner);
EnergySource::HitEnemy,
); match target {
} Some(GroupTarget::OutOfGroup) => {
} else if let Some(energy_mut) = beam_owner.and_then(|o| energies.get_mut(o)) { server_emitter.emit(ServerEvent::Damage { entity: b, change });
if energy_mut if let Some(entity) = beam_owner {
.try_change_by( server_emitter.emit(ServerEvent::Damage {
-(beam_segment.energy_cost as i32), // Stamina use entity,
EnergySource::Ability, change: HealthChange {
) amount: (-change.amount as f32
.is_ok() * beam_segment.lifesteal_eff)
{ as i32,
server_emitter.emit(ServerEvent::Damage { cause: HealthSource::Heal {
uid: *uid_b, by: beam_segment.owner,
change, },
}); },
});
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::{ use crate::{
comp::{ comp::{
BuffCategory, BuffChange, BuffEffect, BuffId, BuffSource, Buffs, HealthChange, BuffCategory, BuffChange, BuffEffect, BuffId, BuffSource, Buffs, Health, HealthChange,
HealthSource, Loadout, ModifierKind, Stats, HealthSource, Loadout, ModifierKind,
}, },
event::{EventBus, ServerEvent}, event::{EventBus, ServerEvent},
state::DeltaTime, state::DeltaTime,
sync::Uid, DamageSource,
}; };
use specs::{Entities, Join, Read, ReadStorage, System, WriteStorage}; use specs::{Entities, Join, Read, ReadStorage, System, WriteStorage};
use std::time::Duration; use std::time::Duration;
@ -17,21 +17,20 @@ impl<'a> System<'a> for Sys {
Entities<'a>, Entities<'a>,
Read<'a, DeltaTime>, Read<'a, DeltaTime>,
Read<'a, EventBus<ServerEvent>>, Read<'a, EventBus<ServerEvent>>,
ReadStorage<'a, Uid>,
ReadStorage<'a, Loadout>, ReadStorage<'a, Loadout>,
WriteStorage<'a, Stats>, WriteStorage<'a, Health>,
WriteStorage<'a, Buffs>, WriteStorage<'a, Buffs>,
); );
fn run( fn run(
&mut self, &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(); let mut server_emitter = server_bus.emitter();
// Set to false to avoid spamming server // Set to false to avoid spamming server
buffs.set_event_emission(false); buffs.set_event_emission(false);
stats.set_event_emission(false); healths.set_event_emission(false);
for (entity, buff_comp, uid, stat) in (&entities, &mut buffs, &uids, &mut stats).join() { for (entity, buff_comp, health) in (&entities, &mut buffs, &mut healths).join() {
let mut expired_buffs = Vec::<BuffId>::new(); let mut expired_buffs = Vec::<BuffId>::new();
for (id, buff) in buff_comp.buffs.iter_mut() { for (id, buff) in buff_comp.buffs.iter_mut() {
// Tick the buff and subtract delta from it // 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 // Call to reset health to base values
stat.health.reset_max(); health.reset_max();
// Iterator over the lists of buffs by kind // Iterator over the lists of buffs by kind
for buff_ids in buff_comp.kinds.values() { 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()) || buff.time.map_or(false, |dur| dur == Duration::default())
{ {
let cause = if *accumulated > 0.0 { let cause = if *accumulated > 0.0 {
HealthSource::Healing { by: buff_owner } HealthSource::Heal { by: buff_owner }
} else { } else {
HealthSource::Buff { owner: buff_owner } HealthSource::Damage {
kind: DamageSource::Other,
by: buff_owner,
}
}; };
server_emitter.emit(ServerEvent::Damage { server_emitter.emit(ServerEvent::Damage {
uid: *uid, entity,
change: HealthChange { change: HealthChange {
amount: *accumulated as i32, amount: *accumulated as i32,
cause, cause,
@ -104,14 +106,10 @@ impl<'a> System<'a> for Sys {
}, },
BuffEffect::MaxHealthModifier { value, kind } => match kind { BuffEffect::MaxHealthModifier { value, kind } => match kind {
ModifierKind::Multiplicative => { ModifierKind::Multiplicative => {
stat.health.set_maximum( health.set_maximum((health.maximum() as f32 * *value) as u32);
(stat.health.maximum() as f32 * *value) as u32,
);
}, },
ModifierKind::Additive => { ModifierKind::Additive => {
stat.health.set_maximum( health.set_maximum((health.maximum() as f32 + *value) as u32);
(stat.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 // Remove buffs that don't persist on death
if stat.is_dead { if health.is_dead {
server_emitter.emit(ServerEvent::Buff { server_emitter.emit(ServerEvent::Buff {
entity, entity,
buff_change: BuffChange::RemoveByCategory { buff_change: BuffChange::RemoveByCategory {
@ -141,6 +139,6 @@ impl<'a> System<'a> for Sys {
} }
// Turned back to true // Turned back to true
buffs.set_event_emission(true); buffs.set_event_emission(true);
stats.set_event_emission(true); healths.set_event_emission(true);
} }
} }

View File

@ -1,7 +1,7 @@
use crate::{ use crate::{
comp::{ comp::{
Attacking, Beam, Body, CharacterState, ControlAction, Controller, ControllerInputs, Energy, 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}, event::{EventBus, LocalEvent, ServerEvent},
metrics::SysMetrics, metrics::SysMetrics,
@ -58,7 +58,7 @@ pub struct JoinData<'a> {
pub dt: &'a DeltaTime, pub dt: &'a DeltaTime,
pub controller: &'a Controller, pub controller: &'a Controller,
pub inputs: &'a ControllerInputs, pub inputs: &'a ControllerInputs,
pub stats: &'a Stats, pub health: &'a Health,
pub energy: &'a Energy, pub energy: &'a Energy,
pub loadout: &'a Loadout, pub loadout: &'a Loadout,
pub body: &'a Body, pub body: &'a Body,
@ -85,7 +85,7 @@ pub type JoinTuple<'a> = (
RestrictedMut<'a, Energy>, RestrictedMut<'a, Energy>,
RestrictedMut<'a, Loadout>, RestrictedMut<'a, Loadout>,
&'a mut Controller, &'a mut Controller,
&'a Stats, &'a Health,
&'a Body, &'a Body,
&'a PhysicsState, &'a PhysicsState,
Option<&'a Attacking>, Option<&'a Attacking>,
@ -123,7 +123,7 @@ impl<'a> JoinData<'a> {
loadout: j.7.get_unchecked(), loadout: j.7.get_unchecked(),
controller: j.8, controller: j.8,
inputs: &j.8.inputs, inputs: &j.8.inputs,
stats: j.9, health: j.9,
body: j.10, body: j.10,
physics: j.11, physics: j.11,
attacking: j.12, attacking: j.12,
@ -155,7 +155,7 @@ impl<'a> System<'a> for Sys {
WriteStorage<'a, Energy>, WriteStorage<'a, Energy>,
WriteStorage<'a, Loadout>, WriteStorage<'a, Loadout>,
WriteStorage<'a, Controller>, WriteStorage<'a, Controller>,
ReadStorage<'a, Stats>, ReadStorage<'a, Health>,
ReadStorage<'a, Body>, ReadStorage<'a, Body>,
ReadStorage<'a, PhysicsState>, ReadStorage<'a, PhysicsState>,
ReadStorage<'a, Attacking>, ReadStorage<'a, Attacking>,
@ -182,7 +182,7 @@ impl<'a> System<'a> for Sys {
mut energies, mut energies,
mut loadouts, mut loadouts,
mut controllers, mut controllers,
stats, healths,
bodies, bodies,
physics_states, physics_states,
attacking_storage, attacking_storage,
@ -206,7 +206,7 @@ impl<'a> System<'a> for Sys {
&mut energies.restrict_mut(), &mut energies.restrict_mut(),
&mut loadouts.restrict_mut(), &mut loadouts.restrict_mut(),
&mut controllers, &mut controllers,
&stats, &healths,
&bodies, &bodies,
&physics_states, &physics_states,
attacking_storage.maybe(), attacking_storage.maybe(),

View File

@ -1,18 +1,17 @@
use crate::{ 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}, event::{EventBus, LocalEvent, ServerEvent},
metrics::SysMetrics, metrics::SysMetrics,
span, span,
sync::Uid, sync::Uid,
util::Dir, util::Dir,
GroupTarget,
}; };
use rand::{thread_rng, Rng}; use rand::{thread_rng, Rng};
use specs::{Entities, Join, Read, ReadExpect, ReadStorage, System, WriteStorage}; use specs::{Entities, Join, Read, ReadExpect, ReadStorage, System, WriteStorage};
use std::time::Duration; use std::time::Duration;
use vek::*; use vek::*;
pub const BLOCK_ANGLE: f32 = 180.0;
/// This system is responsible for handling accepted inputs like moving or /// This system is responsible for handling accepted inputs like moving or
/// attacking /// attacking
pub struct Sys; pub struct Sys;
@ -28,10 +27,9 @@ impl<'a> System<'a> for Sys {
ReadStorage<'a, Ori>, ReadStorage<'a, Ori>,
ReadStorage<'a, Scale>, ReadStorage<'a, Scale>,
ReadStorage<'a, Body>, ReadStorage<'a, Body>,
ReadStorage<'a, Stats>, ReadStorage<'a, Health>,
ReadStorage<'a, Loadout>, ReadStorage<'a, Loadout>,
ReadStorage<'a, group::Group>, ReadStorage<'a, group::Group>,
ReadStorage<'a, CharacterState>,
WriteStorage<'a, Attacking>, WriteStorage<'a, Attacking>,
); );
@ -47,10 +45,9 @@ impl<'a> System<'a> for Sys {
orientations, orientations,
scales, scales,
bodies, bodies,
stats, healths,
loadouts, loadouts,
groups, groups,
character_states,
mut attacking_storage, mut attacking_storage,
): Self::SystemData, ): Self::SystemData,
) { ) {
@ -75,17 +72,8 @@ impl<'a> System<'a> for Sys {
attack.applied = true; attack.applied = true;
// Go through all other entities // Go through all other entities
for (b, uid_b, pos_b, ori_b, scale_b_maybe, character_b, stats_b, body_b) in ( for (b, pos_b, scale_b_maybe, health_b, body_b) in
&entities, (&entities, &positions, scales.maybe(), &healths, &bodies).join()
&uids,
&positions,
&orientations,
scales.maybe(),
character_states.maybe(),
&stats,
&bodies,
)
.join()
{ {
// 2D versions // 2D versions
let pos2 = Vec2::from(pos.0); let pos2 = Vec2::from(pos.0);
@ -99,7 +87,7 @@ impl<'a> System<'a> for Sys {
// Check if it is a hit // Check if it is a hit
if entity != b if entity != b
&& !stats_b.is_dead && !health_b.is_dead
// Spherical wedge shaped attack field // Spherical wedge shaped attack field
&& pos.0.distance_squared(pos_b.0) < (rad_b + scale * attack.range).powi(2) && 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() && 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)) .map(|group_a| Some(group_a) == groups.get(b))
.unwrap_or(false); .unwrap_or(false);
let damage = if let Some(damage) = attack.damages.get_damage(same_group) { let target_group = if same_group {
damage GroupTarget::InGroup
} else { } else {
continue; GroupTarget::OutOfGroup
}; };
let block = character_b.map(|c_b| c_b.is_block()).unwrap_or(false) for (target, damage) in attack.damages.iter() {
&& ori_b.0.angle_between(pos.0 - pos_b.0) < BLOCK_ANGLE.to_radians() / 2.0; if let Some(target) = target {
if *target != target_group {
continue;
}
}
let change = damage.modify_damage(block, loadouts.get(b), Some(*uid)); let change = damage.modify_damage(loadouts.get(b), Some(*uid));
if change.amount != 0 {
server_emitter.emit(ServerEvent::Damage {
uid: *uid_b,
change,
});
server_emitter.emit(ServerEvent::Damage { entity: b, change });
// Apply bleeding buff on melee hits with 10% chance // Apply bleeding buff on melee hits with 10% chance
// TODO: Don't have buff uniformly applied on all melee attacks // TODO: Don't have buff uniformly applied on all melee attacks
if change.amount < 0 && thread_rng().gen::<f32>() < 0.1 { 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 kb_dir = Dir::new((pos_b.0 - pos.0).try_normalized().unwrap_or(*ori.0));
let impulse = attack.knockback.calculate_impulse(kb_dir); let impulse = attack.knockback.calculate_impulse(kb_dir);
if !impulse.is_approx_zero() { if !impulse.is_approx_zero() {
server_emitter.emit(ServerEvent::Knockback { entity: b, impulse }); server_emitter.emit(ServerEvent::Knockback { entity: b, impulse });
} }
attack.hit_count += 1;
} }
} }
} }

View File

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

View File

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

View File

@ -1,5 +1,5 @@
use crate::{ use crate::{
comp::{CharacterState, Energy, EnergySource, HealthSource, Stats}, comp::{CharacterState, Energy, EnergyChange, EnergySource, Health, HealthSource, Stats},
event::{EventBus, ServerEvent}, event::{EventBus, ServerEvent},
metrics::SysMetrics, metrics::SysMetrics,
span, span,
@ -20,42 +20,59 @@ impl<'a> System<'a> for Sys {
ReadExpect<'a, SysMetrics>, ReadExpect<'a, SysMetrics>,
ReadStorage<'a, CharacterState>, ReadStorage<'a, CharacterState>,
WriteStorage<'a, Stats>, WriteStorage<'a, Stats>,
WriteStorage<'a, Health>,
WriteStorage<'a, Energy>, WriteStorage<'a, Energy>,
); );
fn run( fn run(
&mut self, &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(); let start_time = std::time::Instant::now();
span!(_guard, "run", "stats::Sys::run"); span!(_guard, "run", "stats::Sys::run");
let mut server_event_emitter = server_event_bus.emitter(); let mut server_event_emitter = server_event_bus.emitter();
// Increment last change timer // Increment last change timer
stats.set_event_emission(false); // avoid unnecessary syncing healths.set_event_emission(false); // avoid unnecessary syncing
for stat in (&mut stats).join() { for health in (&mut healths).join() {
stat.health.last_change.0 += f64::from(dt.0); health.last_change.0 += f64::from(dt.0);
} }
stats.set_event_emission(true); healths.set_event_emission(true);
// Update stats // 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 (set_dead, level_up) = {
let stat = stats.get_unchecked(); 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(), stat.exp.current() >= stat.exp.maximum(),
) )
}; };
if set_dead { if set_dead {
let stat = stats.get_mut_unchecked(); let health = health.get_mut_unchecked();
server_event_emitter.emit(ServerEvent::Destroy { server_event_emitter.emit(ServerEvent::Destroy {
entity, 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 { if level_up {
@ -67,9 +84,9 @@ impl<'a> System<'a> for Sys {
server_event_emitter.emit(ServerEvent::LevelUp(entity, stat.level.level())); server_event_emitter.emit(ServerEvent::LevelUp(entity, stat.level.level()));
} }
stat.update_max_hp(stat.body_type); let health = health.get_mut_unchecked();
stat.health health.update_max_hp(Some(stat.body_type), stat.level.level());
.set_to(stat.health.maximum(), HealthSource::LevelUp); health.set_to(health.maximum(), HealthSource::LevelUp);
} }
} }
@ -96,11 +113,12 @@ impl<'a> System<'a> for Sys {
if res { if res {
let mut energy = energy.get_mut_unchecked(); let mut energy = energy.get_mut_unchecked();
// Have to account for Calc I differential equations due to acceleration // Have to account for Calc I differential equations due to acceleration
energy.change_by( energy.change_by(EnergyChange {
(energy.regen_rate * dt.0 + ENERGY_REGEN_ACCEL * dt.0.powf(2.0) / 2.0) amount: (energy.regen_rate * dt.0
+ ENERGY_REGEN_ACCEL * dt.0.powf(2.0) / 2.0)
as i32, as i32,
EnergySource::Regen, source: EnergySource::Regen,
); });
energy.regen_rate = energy.regen_rate =
(energy.regen_rate + ENERGY_REGEN_ACCEL * dt.0).min(100.0); (energy.regen_rate + ENERGY_REGEN_ACCEL * dt.0).min(100.0);
} }
@ -130,9 +148,10 @@ impl<'a> System<'a> for Sys {
}; };
if res { if res {
energy energy.get_mut_unchecked().change_by(EnergyChange {
.get_mut_unchecked() amount: -3,
.change_by(-3, EnergySource::Regen); source: EnergySource::Regen,
});
} }
}, },
// Non-combat abilities that consume energy; // Non-combat abilities that consume energy;

View File

@ -10,6 +10,7 @@ use chrono::{NaiveTime, Timelike};
use common::{ use common::{
cmd::{ChatCommand, CHAT_COMMANDS, CHAT_SHORTCUTS}, cmd::{ChatCommand, CHAT_COMMANDS, CHAT_SHORTCUTS},
comp::{self, ChatType, Item, LightEmitter, WaypointArea}, comp::{self, ChatType, Item, LightEmitter, WaypointArea},
effect::Effect,
event::{EventBus, ServerEvent}, event::{EventBus, ServerEvent},
msg::{DisconnectReason, Notification, PlayerListUpdate, ServerGeneral}, msg::{DisconnectReason, Notification, PlayerListUpdate, ServerGeneral},
npc::{self, get_npc_name}, npc::{self, get_npc_name},
@ -18,7 +19,7 @@ use common::{
terrain::{Block, BlockKind, SpriteKind, TerrainChunkSize}, terrain::{Block, BlockKind, SpriteKind, TerrainChunkSize},
util::Dir, util::Dir,
vol::RectVolSize, vol::RectVolSize,
Explosion, LoadoutBuilder, Damage, DamageSource, Explosion, LoadoutBuilder, RadiusEffect,
}; };
use rand::Rng; use rand::Rng;
use specs::{Builder, Entity as EcsEntity, Join, WorldExt}; use specs::{Builder, Entity as EcsEntity, Join, WorldExt};
@ -393,16 +394,19 @@ fn handle_kill(
let reason = if client == target { let reason = if client == target {
comp::HealthSource::Suicide comp::HealthSource::Suicide
} else if let Some(uid) = server.state.read_storage::<Uid>().get(client) { } 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 { } else {
comp::HealthSource::Command comp::HealthSource::Command
}; };
server server
.state .state
.ecs_mut() .ecs_mut()
.write_storage::<comp::Stats>() .write_storage::<comp::Health>()
.get_mut(target) .get_mut(target)
.map(|s| s.health.set_to(0, reason)); .map(|h| h.set_to(0, reason));
} }
fn handle_time( fn handle_time(
@ -471,13 +475,13 @@ fn handle_health(
action: &ChatCommand, action: &ChatCommand,
) { ) {
if let Ok(hp) = scan_fmt!(&args, &action.arg_fmt(), u32) { if let Ok(hp) = scan_fmt!(&args, &action.arg_fmt(), u32) {
if let Some(stats) = server if let Some(health) = server
.state .state
.ecs() .ecs()
.write_storage::<comp::Stats>() .write_storage::<comp::Health>()
.get_mut(target) .get_mut(target)
{ {
stats.health.set_to(hp * 10, comp::HealthSource::Command); health.set_to(hp * 10, comp::HealthSource::Command);
} else { } else {
server.notify_client( server.notify_client(
client, client,
@ -656,6 +660,7 @@ fn handle_spawn(
.create_npc( .create_npc(
pos, pos,
comp::Stats::new(get_npc_name(id).into(), body), comp::Stats::new(get_npc_name(id).into(), body),
comp::Health::new(body, 1),
LoadoutBuilder::build_loadout(body, alignment, None, false) LoadoutBuilder::build_loadout(body, alignment, None, false)
.build(), .build(),
body, body,
@ -762,9 +767,11 @@ fn handle_spawn_training_dummy(
// Level 0 will prevent exp gain from kill // Level 0 will prevent exp gain from kill
stats.level.set_level(0); stats.level.set_level(0);
let health = comp::Health::new(body, 0);
server server
.state .state
.create_npc(pos, stats, comp::Loadout::default(), body) .create_npc(pos, stats, health, comp::Loadout::default(), body)
.with(comp::Vel(vel)) .with(comp::Vel(vel))
.with(comp::MountState::Unmounted) .with(comp::MountState::Unmounted)
.build(); .build();
@ -924,12 +931,12 @@ fn handle_kill_npcs(
_action: &ChatCommand, _action: &ChatCommand,
) { ) {
let ecs = server.state.ecs(); 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 players = ecs.read_storage::<comp::Player>();
let mut count = 0; let mut count = 0;
for (stats, ()) in (&mut stats, !&players).join() { for (health, ()) in (&mut healths, !&players).join() {
count += 1; count += 1;
stats.health.set_to(0, comp::HealthSource::Command); health.set_to(0, comp::HealthSource::Command);
} }
let text = if count > 0 { let text = if count > 0 {
format!("Destroyed {} NPCs.", count) format!("Destroyed {} NPCs.", count)
@ -1153,16 +1160,20 @@ fn handle_explosion(
.emit_now(ServerEvent::Explosion { .emit_now(ServerEvent::Explosion {
pos: pos.0, pos: pos.0,
explosion: Explosion { explosion: Explosion {
effects: vec![
RadiusEffect::Entity(
None,
Effect::Damage(Damage {
source: DamageSource::Explosion,
value: 100.0 * power,
}),
),
RadiusEffect::TerrainDestruction(power),
],
radius: 3.0 * 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, energy_regen: 0,
}, },
owner: ecs.read_storage::<Uid>().get(target).copied(), owner: ecs.read_storage::<Uid>().get(target).copied(),
friendly_damage: true,
reagent: None, reagent: None,
}) })
}, },
@ -1695,6 +1706,8 @@ fn handle_set_level(
PlayerListUpdate::LevelChange(uid, lvl), PlayerListUpdate::LevelChange(uid, lvl),
)); ));
let body_type: Option<comp::Body>;
if let Some(stats) = server if let Some(stats) = server
.state .state
.ecs_mut() .ecs_mut()
@ -1702,13 +1715,20 @@ fn handle_set_level(
.get_mut(player) .get_mut(player)
{ {
stats.level.set_level(lvl); stats.level.set_level(lvl);
body_type = Some(stats.body_type);
stats.update_max_hp(stats.body_type);
stats
.health
.set_to(stats.health.maximum(), comp::HealthSource::LevelUp);
} else { } else {
error_msg = Some(ChatType::CommandError.server_msg("Player has no stats!")); 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) => { Err(e) => {

View File

@ -3,8 +3,8 @@ use common::{
character::CharacterId, character::CharacterId,
comp::{ comp::{
self, beam, humanoid::DEFAULT_HUMANOID_EYE_HEIGHT, shockwave, Agent, Alignment, Body, self, beam, humanoid::DEFAULT_HUMANOID_EYE_HEIGHT, shockwave, Agent, Alignment, Body,
Gravity, Item, ItemDrop, LightEmitter, Loadout, Ori, Pos, Projectile, Scale, Stats, Vel, Gravity, Health, Item, ItemDrop, LightEmitter, Loadout, Ori, Pos, Projectile, Scale, Stats,
WaypointArea, Vel, WaypointArea,
}, },
outcome::Outcome, outcome::Outcome,
util::Dir, util::Dir,
@ -37,6 +37,7 @@ pub fn handle_create_npc(
server: &mut Server, server: &mut Server,
pos: Pos, pos: Pos,
stats: Stats, stats: Stats,
health: Health,
loadout: Loadout, loadout: Loadout,
body: Body, body: Body,
agent: impl Into<Option<Agent>>, agent: impl Into<Option<Agent>>,
@ -55,7 +56,7 @@ pub fn handle_create_npc(
let entity = server let entity = server
.state .state
.create_npc(pos, stats, loadout, body) .create_npc(pos, stats, health, loadout, body)
.with(scale) .with(scale)
.with(alignment); .with(alignment);

View File

@ -8,17 +8,18 @@ use common::{
comp::{ comp::{
self, buff, self, buff,
chat::{KillSource, KillType}, 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, lottery::Lottery,
msg::{PlayerListUpdate, ServerGeneral}, msg::{PlayerListUpdate, ServerGeneral},
outcome::Outcome, outcome::Outcome,
state::BlockChange, state::BlockChange,
sync::{Uid, UidAllocator, WorldSyncExt}, sync::{Uid, UidAllocator, WorldSyncExt},
sys::melee::BLOCK_ANGLE,
terrain::{Block, TerrainGrid}, terrain::{Block, TerrainGrid},
vol::ReadVol, vol::ReadVol,
Damage, Explosion, Damage, DamageSource, Explosion, GroupTarget, RadiusEffect,
}; };
use comp::item::Reagent; use comp::item::Reagent;
use rand::prelude::*; use rand::prelude::*;
@ -26,13 +27,10 @@ use specs::{join::Join, saveload::MarkerAllocator, Entity as EcsEntity, WorldExt
use tracing::error; use tracing::error;
use vek::Vec3; use vek::Vec3;
pub fn handle_damage(server: &Server, uid: Uid, change: HealthChange) { pub fn handle_damage(server: &Server, entity: EcsEntity, change: HealthChange) {
let state = &server.state; let ecs = &server.state.ecs();
let ecs = state.ecs(); if let Some(health) = ecs.write_storage::<Health>().get_mut(entity) {
if let Some(entity) = ecs.entity_from_uid(uid.into()) { health.change_by(change);
if let Some(stats) = ecs.write_storage::<Stats>().get_mut(entity) {
stats.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(_player) = state.ecs().read_storage::<Player>().get(entity) {
if let Some(uid) = state.ecs().read_storage::<Uid>().get(entity) { if let Some(uid) = state.ecs().read_storage::<Uid>().get(entity) {
let kill_source = match cause { let kill_source = match cause {
HealthSource::Attack { by } => { HealthSource::Damage {
kind: DamageSource::Melee,
by: Some(by),
} => {
// Get attacker entity // Get attacker entity
if let Some(char_entity) = state.ecs().entity_from_uid(by.into()) { if let Some(char_entity) = state.ecs().entity_from_uid(by.into()) {
// Check if attacker is another player or entity with stats (npc) // 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) 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 // Get projectile owner entity TODO: add names to projectiles and send in
// message // message
if let Some(char_entity) = state.ecs().entity_from_uid(by.into()) { 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) KillSource::NonPlayer("<?>".to_string(), KillType::Projectile)
} }
}, },
HealthSource::Explosion { owner: Some(by) } => { HealthSource::Damage {
kind: DamageSource::Explosion,
by: Some(by),
} => {
// Get explosion owner entity // Get explosion owner entity
if let Some(char_entity) = state.ecs().entity_from_uid(by.into()) { if let Some(char_entity) = state.ecs().entity_from_uid(by.into()) {
// Check if attacker is another player or entity with stats (npc) // 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) KillSource::NonPlayer("<?>".to_string(), KillType::Explosion)
} }
}, },
HealthSource::Energy { owner: Some(by) } => { HealthSource::Damage {
kind: DamageSource::Energy,
by: Some(by),
} => {
// Get energy owner entity // Get energy owner entity
if let Some(char_entity) = state.ecs().entity_from_uid(by.into()) { if let Some(char_entity) = state.ecs().entity_from_uid(by.into()) {
// Check if attacker is another player or entity with stats (npc) // 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) KillSource::NonPlayer("<?>".to_string(), KillType::Energy)
} }
}, },
HealthSource::Buff { owner: Some(by) } => { HealthSource::Damage {
kind: DamageSource::Other,
by: Some(by),
} => {
// Get energy owner entity // Get energy owner entity
if let Some(char_entity) = state.ecs().entity_from_uid(by.into()) { if let Some(char_entity) = state.ecs().entity_from_uid(by.into()) {
// Check if attacker is another player or entity with stats (npc) // 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) .get(char_entity)
.is_some() .is_some()
{ {
KillSource::Player(by, KillType::Buff) KillSource::Player(by, KillType::Other)
} else if let Some(stats) = } else if let Some(stats) =
state.ecs().read_storage::<Stats>().get(char_entity) state.ecs().read_storage::<Stats>().get(char_entity)
{ {
KillSource::NonPlayer(stats.name.clone(), KillType::Buff) KillSource::NonPlayer(stats.name.clone(), KillType::Other)
} else { } else {
KillSource::NonPlayer("<?>".to_string(), KillType::Buff) KillSource::NonPlayer("<?>".to_string(), KillType::Other)
} }
} else { } else {
KillSource::NonPlayer("<?>".to_string(), KillType::Buff) KillSource::NonPlayer("<?>".to_string(), KillType::Other)
} }
}, },
HealthSource::World => KillSource::FallDamage, HealthSource::World => KillSource::FallDamage,
HealthSource::Suicide => KillSource::Suicide, HealthSource::Suicide => KillSource::Suicide,
HealthSource::Projectile { owner: None } HealthSource::Damage { .. }
| HealthSource::Explosion { owner: None }
| HealthSource::Energy { owner: None }
| HealthSource::Buff { owner: None }
| HealthSource::Revive | HealthSource::Revive
| HealthSource::Command | HealthSource::Command
| HealthSource::LevelUp | HealthSource::LevelUp
| HealthSource::Item | HealthSource::Item
| HealthSource::Healing { by: _ } | HealthSource::Heal { by: _ }
| HealthSource::Unknown => KillSource::Other, | HealthSource::Unknown => KillSource::Other,
}; };
state 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 // Give EXP to the killer if entity had stats
(|| { (|| {
let mut stats = state.ecs().write_storage::<Stats>(); let mut stats = state.ecs().write_storage::<Stats>();
let by = if let HealthSource::Attack { by } let by = if let HealthSource::Damage { by: Some(by), .. } = cause {
| HealthSource::Projectile { owner: Some(by) }
| HealthSource::Energy { owner: Some(by) }
| HealthSource::Buff { owner: Some(by) }
| HealthSource::Explosion { owner: Some(by) } = cause
{
by by
} else { } else {
return; 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>) { pub fn handle_land_on_ground(server: &Server, entity: EcsEntity, vel: Vec3<f32>) {
let state = &server.state; let state = &server.state;
if vel.z <= -30.0 { 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 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 loadouts = state.ecs().read_storage::<comp::Loadout>();
let change = damage.modify_damage(false, loadouts.get(entity), None); let change = damage.modify_damage(loadouts.get(entity), None);
stats.health.change_by(change); health.change_by(change);
} }
} }
} }
@ -479,9 +487,9 @@ pub fn handle_respawn(server: &Server, entity: EcsEntity) {
state state
.ecs() .ecs()
.write_storage::<comp::Stats>() .write_storage::<comp::Health>()
.get_mut(entity) .get_mut(entity)
.map(|stats| stats.revive()); .map(|health| health.revive());
state state
.ecs() .ecs()
.write_storage::<comp::Pos>() .write_storage::<comp::Pos>()
@ -506,26 +514,30 @@ pub fn handle_explosion(
pos: Vec3<f32>, pos: Vec3<f32>,
explosion: Explosion, explosion: Explosion,
owner: Option<Uid>, owner: Option<Uid>,
friendly_damage: bool,
reagent: Option<Reagent>, reagent: Option<Reagent>,
) { ) {
// Go through all other entities // Go through all other entities
let ecs = &server.state.ecs(); 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 // 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>>() ecs.write_resource::<Vec<Outcome>>()
.push(Outcome::Explosion { .push(Outcome::Explosion {
pos, pos,
power: outcome_power, power: outcome_power,
radius: explosion.radius, 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, reagent,
}); });
let owner_entity = owner.and_then(|uid| { let owner_entity = owner.and_then(|uid| {
@ -534,129 +546,126 @@ pub fn handle_explosion(
}); });
let groups = ecs.read_storage::<comp::Group>(); let groups = ecs.read_storage::<comp::Group>();
for (entity_b, pos_b, ori_b, character_b, stats_b, loadout_b) in ( for effect in explosion.effects {
&ecs.entities(), match effect {
&ecs.read_storage::<comp::Pos>(), RadiusEffect::TerrainDestruction(power) => {
&ecs.read_storage::<comp::Ori>(), const RAYS: usize = 500;
ecs.read_storage::<comp::CharacterState>().maybe(),
&mut ecs.write_storage::<comp::Stats>(), // Color terrain
ecs.read_storage::<comp::Loadout>().maybe(), let mut touched_blocks = Vec::new();
) let color_range = power * 2.7;
.join() for _ in 0..RAYS {
{ let dir = Vec3::new(
let distance_squared = pos.distance_squared(pos_b.0); rand::random::<f32>() - 0.5,
// Check if it is a hit rand::random::<f32>() - 0.5,
if !stats_b.is_dead rand::random::<f32>() - 0.5,
// RADIUS )
&& distance_squared < explosion.radius.powi(2) .normalized();
{
// See if entities are in the same group let _ = ecs
let mut same_group = owner_entity .read_resource::<TerrainGrid>()
.and_then(|e| groups.get(e)) .ray(pos, pos + dir * color_range)
.map_or(false, |group_a| Some(group_a) == groups.get(entity_b)); // TODO: Faster RNG
if let Some(entity) = owner_entity { .until(|_| rand::random::<f32>() < 0.05)
if entity == entity_b { .for_each(|_: &Block, pos| touched_blocks.push(pos))
same_group = true; .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 terrain = ecs.read_resource::<TerrainGrid>();
let damage = if is_heal { let mut block_change = ecs.write_resource::<BlockChange>();
Damage::Healing( for block_pos in touched_blocks {
explosion.min_heal as f32 if let Ok(block) = terrain.get(block_pos) {
+ (explosion.max_heal - explosion.min_heal) as f32 * strength, let diff2 = block_pos.map(|b| b as f32).distance_squared(pos);
) let fade = (1.0 - diff2 / color_range.powi(2)).max(0.0);
} else { if let Some(mut color) = block.get_color() {
Damage::Explosion( let r = color[0] as f32
explosion.min_damage as f32 + (fade * (color[0] as f32 * 0.5 - color[0] as f32));
+ (explosion.max_damage - explosion.min_damage) as f32 * strength, 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));
let block = character_b.map(|c_b| c_b.is_block()).unwrap_or(false) color[0] = r as u8;
&& ori_b.0.angle_between(pos - pos_b.0) < BLOCK_ANGLE.to_radians() / 2.0; color[1] = g as u8;
color[2] = b as u8;
let change = damage.modify_damage(block, loadout_b, owner); block_change.set(block_pos, Block::new(block.kind(), color));
}
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);
} }
} }
}
}
}
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 terrain = ecs.read_resource::<TerrainGrid>();
let mut touched_blocks = Vec::new(); let _ = terrain
let color_range = explosion.terrain_destruction_power * 2.7; .ray(pos, pos + dir * power)
for _ in 0..RAYS { // TODO: Faster RNG
let dir = Vec3::new( .until(|block| block.is_liquid() || rand::random::<f32>() < 0.05)
rand::random::<f32>() - 0.5, .for_each(|block: &Block, pos| {
rand::random::<f32>() - 0.5, if block.is_explodable() {
rand::random::<f32>() - 0.5, block_change.set(pos, block.into_vacant());
) }
.normalized(); })
.cast();
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());
} }
}) },
.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; return;
}; };
// Grab the stats from the player and check if the player is dead. // Grab the health from the player and check if the player is dead.
let stats = state.ecs().read_storage::<comp::Stats>(); let healths = state.ecs().read_storage::<comp::Health>();
if let Some(entity_stats) = stats.get(entity) { if let Some(entity_health) = healths.get(entity) {
if entity_stats.is_dead { if entity_health.is_dead {
debug!("Failed to pick up item as the player is dead"); debug!("Failed to pick up item as the player is dead");
return; // If dead, don't continue return; // If dead, don't continue
} }
@ -349,7 +349,7 @@ pub fn handle_inventory(server: &mut Server, entity: EcsEntity, manip: comp::Inv
drop(inventories); drop(inventories);
if let Some(effect) = maybe_effect { if let Some(effect) = maybe_effect {
state.apply_effect(entity, effect); state.apply_effect(entity, effect, None);
} }
if let Some(event) = event { if let Some(event) = event {
state.write_component(entity, comp::InventoryUpdate::new(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, handle_loaded_character_data, handle_shockwave, handle_shoot,
}; };
use entity_manipulation::{ use entity_manipulation::{
handle_buff, handle_damage, handle_destroy, handle_explosion, handle_knockback, handle_buff, handle_damage, handle_destroy, handle_energy_change, handle_explosion,
handle_land_on_ground, handle_level_up, handle_respawn, handle_knockback, handle_land_on_ground, handle_level_up, handle_respawn,
}; };
use group_manip::handle_group; use group_manip::handle_group;
use interaction::{handle_lantern, handle_mount, handle_possess, handle_unmount}; use interaction::{handle_lantern, handle_mount, handle_possess, handle_unmount};
@ -58,9 +58,8 @@ impl Server {
pos, pos,
explosion, explosion,
owner, owner,
friendly_damage,
reagent, reagent,
} => handle_explosion(&self, pos, explosion, owner, friendly_damage, reagent), } => handle_explosion(&self, pos, explosion, owner, reagent),
ServerEvent::Shoot { ServerEvent::Shoot {
entity, entity,
dir, dir,
@ -83,7 +82,7 @@ impl Server {
ServerEvent::Knockback { entity, impulse } => { ServerEvent::Knockback { entity, impulse } => {
handle_knockback(&self, 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::Destroy { entity, cause } => handle_destroy(self, entity, cause),
ServerEvent::InventoryManip(entity, manip) => handle_inventory(self, entity, manip), ServerEvent::InventoryManip(entity, manip) => handle_inventory(self, entity, manip),
ServerEvent::GroupManip(entity, manip) => handle_group(self, entity, manip), ServerEvent::GroupManip(entity, manip) => handle_group(self, entity, manip),
@ -110,6 +109,7 @@ impl Server {
ServerEvent::CreateNpc { ServerEvent::CreateNpc {
pos, pos,
stats, stats,
health,
loadout, loadout,
body, body,
agent, agent,
@ -117,7 +117,7 @@ impl Server {
scale, scale,
drop_item, drop_item,
} => handle_create_npc( } => 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::CreateWaypoint(pos) => handle_create_waypoint(self, pos),
ServerEvent::ClientDisconnect(entity) => { ServerEvent::ClientDisconnect(entity) => {
@ -137,6 +137,9 @@ impl Server {
entity, entity,
buff_change, buff_change,
} => handle_buff(self, 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.level.set_level(stats.level as u32);
new_stats.exp.update_maximum(stats.level as u32); new_stats.exp.update_maximum(stats.level as u32);
new_stats.exp.set_current(stats.exp 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.set_to(
new_stats.health.maximum(), new_stats.health.maximum(),
common::comp::HealthSource::Revive, common::comp::HealthSource::Revive,
); );*/
new_stats.endurance = stats.endurance as u32; new_stats.endurance = stats.endurance as u32;
new_stats.fitness = stats.fitness as u32; new_stats.fitness = stats.fitness as u32;
new_stats.willpower = stats.willpower as u32; new_stats.willpower = stats.willpower as u32;

View File

@ -20,12 +20,13 @@ use vek::*;
pub trait StateExt { pub trait StateExt {
/// Updates a component associated with the entity based on the `Effect` /// 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 /// Build a non-player character
fn create_npc( fn create_npc(
&mut self, &mut self,
pos: comp::Pos, pos: comp::Pos,
stats: comp::Stats, stats: comp::Stats,
health: comp::Health,
loadout: comp::Loadout, loadout: comp::Loadout,
body: comp::Body, body: comp::Body,
) -> EcsEntityBuilder; ) -> EcsEntityBuilder;
@ -70,13 +71,13 @@ pub trait StateExt {
} }
impl StateExt for State { 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 { match effect {
Effect::Health(change) => { Effect::Health(change) => {
self.ecs() self.ecs()
.write_storage::<comp::Stats>() .write_storage::<comp::Health>()
.get_mut(entity) .get_mut(entity)
.map(|stats| stats.health.change_by(change)); .map(|health| health.change_by(change));
}, },
Effect::Xp(xp) => { Effect::Xp(xp) => {
self.ecs() self.ecs()
@ -84,6 +85,14 @@ impl StateExt for State {
.get_mut(entity) .get_mut(entity)
.map(|stats| stats.exp.change_by(xp)); .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, &mut self,
pos: comp::Pos, pos: comp::Pos,
stats: comp::Stats, stats: comp::Stats,
health: comp::Health,
loadout: comp::Loadout, loadout: comp::Loadout,
body: comp::Body, body: comp::Body,
) -> EcsEntityBuilder { ) -> EcsEntityBuilder {
@ -107,6 +117,7 @@ impl StateExt for State {
.with(comp::Controller::default()) .with(comp::Controller::default())
.with(body) .with(body)
.with(stats) .with(stats)
.with(health)
.with(comp::Alignment::Npc) .with(comp::Alignment::Npc)
.with(comp::Energy::new(body.base_energy())) .with(comp::Energy::new(body.base_energy()))
.with(comp::Gravity(1.0)) .with(comp::Gravity(1.0))
@ -239,6 +250,10 @@ impl StateExt for State {
z_max: body.height(), z_max: body.height(),
}); });
self.write_component(entity, body); 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, stats);
self.write_component(entity, inventory); self.write_component(entity, inventory);
self.write_component(entity, loadout); self.write_component(entity, loadout);

View File

@ -1,7 +1,7 @@
use super::super::SysTimer; use super::super::SysTimer;
use crate::{client::Client, metrics::NetworkRequestMetrics, presence::Presence, Settings}; use crate::{client::Client, metrics::NetworkRequestMetrics, presence::Presence, Settings};
use common::{ use common::{
comp::{CanBuild, ControlEvent, Controller, ForceUpdate, Ori, Pos, Stats, Vel}, comp::{CanBuild, ControlEvent, Controller, ForceUpdate, Health, Ori, Pos, Stats, Vel},
event::{EventBus, ServerEvent}, event::{EventBus, ServerEvent},
msg::{ClientGeneral, PresenceKind, ServerGeneral}, msg::{ClientGeneral, PresenceKind, ServerGeneral},
span, span,
@ -24,6 +24,7 @@ impl Sys {
can_build: &ReadStorage<'_, CanBuild>, can_build: &ReadStorage<'_, CanBuild>,
force_updates: &ReadStorage<'_, ForceUpdate>, force_updates: &ReadStorage<'_, ForceUpdate>,
stats: &mut WriteStorage<'_, Stats>, stats: &mut WriteStorage<'_, Stats>,
healths: &ReadStorage<'_, Health>,
block_changes: &mut Write<'_, BlockChange>, block_changes: &mut Write<'_, BlockChange>,
positions: &mut WriteStorage<'_, Pos>, positions: &mut WriteStorage<'_, Pos>,
velocities: &mut WriteStorage<'_, Vel>, velocities: &mut WriteStorage<'_, Vel>,
@ -78,7 +79,7 @@ impl Sys {
if matches!(presence.kind, PresenceKind::Character(_)) { if matches!(presence.kind, PresenceKind::Character(_)) {
// Skip respawn if client entity is alive // Skip respawn if client entity is alive
if let ControlEvent::Respawn = event { 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! //Todo: comment why return!
return Ok(()); return Ok(());
} }
@ -98,7 +99,7 @@ impl Sys {
ClientGeneral::PlayerPhysics { pos, vel, ori } => { ClientGeneral::PlayerPhysics { pos, vel, ori } => {
if matches!(presence.kind, PresenceKind::Character(_)) if matches!(presence.kind, PresenceKind::Character(_))
&& force_updates.get(entity).is_none() && 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 _ = positions.insert(entity, pos);
let _ = velocities.insert(entity, vel); let _ = velocities.insert(entity, vel);
@ -176,6 +177,7 @@ impl<'a> System<'a> for Sys {
ReadStorage<'a, CanBuild>, ReadStorage<'a, CanBuild>,
ReadStorage<'a, ForceUpdate>, ReadStorage<'a, ForceUpdate>,
WriteStorage<'a, Stats>, WriteStorage<'a, Stats>,
ReadStorage<'a, Health>,
Write<'a, BlockChange>, Write<'a, BlockChange>,
WriteStorage<'a, Pos>, WriteStorage<'a, Pos>,
WriteStorage<'a, Vel>, WriteStorage<'a, Vel>,
@ -197,6 +199,7 @@ impl<'a> System<'a> for Sys {
can_build, can_build,
force_updates, force_updates,
mut stats, mut stats,
healths,
mut block_changes, mut block_changes,
mut positions, mut positions,
mut velocities, mut velocities,
@ -226,6 +229,7 @@ impl<'a> System<'a> for Sys {
&can_build, &can_build,
&force_updates, &force_updates,
&mut stats, &mut stats,
&healths,
&mut block_changes, &mut block_changes,
&mut positions, &mut positions,
&mut velocities, &mut velocities,

View File

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

View File

@ -1,9 +1,9 @@
use super::SysTimer; use super::SysTimer;
use common::{ use common::{
comp::{ comp::{
BeamSegment, Body, Buffs, CanBuild, CharacterState, Collider, Energy, Gravity, Group, Item, BeamSegment, Body, Buffs, CanBuild, CharacterState, Collider, Energy, Gravity, Group,
LightEmitter, Loadout, Mass, MountState, Mounting, Ori, Player, Pos, Scale, Shockwave, Health, Item, LightEmitter, Loadout, Mass, MountState, Mounting, Ori, Player, Pos, Scale,
Stats, Sticky, Vel, Shockwave, Stats, Sticky, Vel,
}, },
msg::EcsCompPacket, msg::EcsCompPacket,
span, span,
@ -46,6 +46,7 @@ pub struct TrackedComps<'a> {
pub stats: ReadStorage<'a, Stats>, pub stats: ReadStorage<'a, Stats>,
pub buffs: ReadStorage<'a, Buffs>, pub buffs: ReadStorage<'a, Buffs>,
pub energy: ReadStorage<'a, Energy>, pub energy: ReadStorage<'a, Energy>,
pub health: ReadStorage<'a, Health>,
pub can_build: ReadStorage<'a, CanBuild>, pub can_build: ReadStorage<'a, CanBuild>,
pub light_emitter: ReadStorage<'a, LightEmitter>, pub light_emitter: ReadStorage<'a, LightEmitter>,
pub item: ReadStorage<'a, Item>, pub item: ReadStorage<'a, Item>,
@ -94,6 +95,10 @@ impl<'a> TrackedComps<'a> {
.get(entity) .get(entity)
.cloned() .cloned()
.map(|c| comps.push(c.into())); .map(|c| comps.push(c.into()));
self.health
.get(entity)
.cloned()
.map(|c| comps.push(c.into()));
self.can_build self.can_build
.get(entity) .get(entity)
.cloned() .cloned()
@ -164,6 +169,7 @@ pub struct ReadTrackers<'a> {
pub stats: ReadExpect<'a, UpdateTracker<Stats>>, pub stats: ReadExpect<'a, UpdateTracker<Stats>>,
pub buffs: ReadExpect<'a, UpdateTracker<Buffs>>, pub buffs: ReadExpect<'a, UpdateTracker<Buffs>>,
pub energy: ReadExpect<'a, UpdateTracker<Energy>>, pub energy: ReadExpect<'a, UpdateTracker<Energy>>,
pub health: ReadExpect<'a, UpdateTracker<Health>>,
pub can_build: ReadExpect<'a, UpdateTracker<CanBuild>>, pub can_build: ReadExpect<'a, UpdateTracker<CanBuild>>,
pub light_emitter: ReadExpect<'a, UpdateTracker<LightEmitter>>, pub light_emitter: ReadExpect<'a, UpdateTracker<LightEmitter>>,
pub item: ReadExpect<'a, UpdateTracker<Item>>, 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.stats, &comps.stats, filter)
.with_component(&comps.uid, &*self.buffs, &comps.buffs, filter) .with_component(&comps.uid, &*self.buffs, &comps.buffs, filter)
.with_component(&comps.uid, &*self.energy, &comps.energy, 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, &*self.can_build, &comps.can_build, filter)
.with_component( .with_component(
&comps.uid, &comps.uid,
@ -233,6 +240,7 @@ pub struct WriteTrackers<'a> {
stats: WriteExpect<'a, UpdateTracker<Stats>>, stats: WriteExpect<'a, UpdateTracker<Stats>>,
buffs: WriteExpect<'a, UpdateTracker<Buffs>>, buffs: WriteExpect<'a, UpdateTracker<Buffs>>,
energy: WriteExpect<'a, UpdateTracker<Energy>>, energy: WriteExpect<'a, UpdateTracker<Energy>>,
health: WriteExpect<'a, UpdateTracker<Health>>,
can_build: WriteExpect<'a, UpdateTracker<CanBuild>>, can_build: WriteExpect<'a, UpdateTracker<CanBuild>>,
light_emitter: WriteExpect<'a, UpdateTracker<LightEmitter>>, light_emitter: WriteExpect<'a, UpdateTracker<LightEmitter>>,
item: WriteExpect<'a, UpdateTracker<Item>>, item: WriteExpect<'a, UpdateTracker<Item>>,
@ -258,6 +266,7 @@ fn record_changes(comps: &TrackedComps, trackers: &mut WriteTrackers) {
trackers.stats.record_changes(&comps.stats); trackers.stats.record_changes(&comps.stats);
trackers.buffs.record_changes(&comps.buffs); trackers.buffs.record_changes(&comps.buffs);
trackers.energy.record_changes(&comps.energy); trackers.energy.record_changes(&comps.energy);
trackers.health.record_changes(&comps.health);
trackers.can_build.record_changes(&comps.can_build); trackers.can_build.record_changes(&comps.can_build);
trackers.light_emitter.record_changes(&comps.light_emitter); trackers.light_emitter.record_changes(&comps.light_emitter);
trackers.item.record_changes(&comps.item); trackers.item.record_changes(&comps.item);
@ -296,6 +305,7 @@ fn record_changes(comps: &TrackedComps, trackers: &mut WriteTrackers) {
log_counts!(player, "Players"); log_counts!(player, "Players");
log_counts!(stats, "Stats"); log_counts!(stats, "Stats");
log_counts!(energy, "Energies"); log_counts!(energy, "Energies");
log_vounts!(health, "Healths");
log_counts!(light_emitter, "Light emitters"); log_counts!(light_emitter, "Light emitters");
log_counts!(item, "Items"); log_counts!(item, "Items");
log_counts!(scale, "Scales"); log_counts!(scale, "Scales");
@ -319,6 +329,7 @@ pub fn register_trackers(world: &mut World) {
world.register_tracker::<Stats>(); world.register_tracker::<Stats>();
world.register_tracker::<Buffs>(); world.register_tracker::<Buffs>();
world.register_tracker::<Energy>(); world.register_tracker::<Energy>();
world.register_tracker::<Health>();
world.register_tracker::<CanBuild>(); world.register_tracker::<CanBuild>();
world.register_tracker::<LightEmitter>(); world.register_tracker::<LightEmitter>();
world.register_tracker::<Item>(); 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) LoadoutBuilder::build_loadout(body, alignment, main_tool, entity.is_giant)
.build(); .build();
stats.update_max_hp(stats.body_type); let health = comp::Health::new(stats.body_type, stats.level.level());
stats
.health
.set_to(stats.health.maximum(), comp::HealthSource::Revive);
let can_speak = match body { let can_speak = match body {
comp::Body::Humanoid(_) => alignment == comp::Alignment::Npc, comp::Body::Humanoid(_) => alignment == comp::Alignment::Npc,
@ -174,6 +170,7 @@ impl<'a> System<'a> for Sys {
server_emitter.emit(ServerEvent::CreateNpc { server_emitter.emit(ServerEvent::CreateNpc {
pos: Pos(entity.pos), pos: Pos(entity.pos),
stats, stats,
health,
loadout, loadout,
agent: if entity.has_agency { agent: if entity.has_agency {
Some(comp::Agent::new(entity.pos, can_speak, &body)) Some(comp::Agent::new(entity.pos, can_speak, &body))

View File

@ -143,6 +143,7 @@ fn matches_ability_stage() {
speed_increase: 0.05, speed_increase: 0.05,
max_speed_increase: 1.8, max_speed_increase: 1.8,
is_interruptible: true, is_interruptible: true,
ability_key: states::utils::AbilityKey::Mouse1,
}, },
stage: 1, stage: 1,
combo: 0, combo: 0,
@ -203,6 +204,7 @@ fn ignores_different_ability_stage() {
speed_increase: 0.05, speed_increase: 0.05,
max_speed_increase: 1.8, max_speed_increase: 1.8,
is_interruptible: true, is_interruptible: true,
ability_key: states::utils::AbilityKey::Mouse1,
}, },
stage: 1, stage: 1,
combo: 0, combo: 0,

View File

@ -3,7 +3,7 @@ use crate::ecs::{
ExpFloater, MyEntity, MyExpFloaterList, ExpFloater, MyEntity, MyExpFloaterList,
}; };
use common::{ use common::{
comp::{HealthSource, Pos, Stats}, comp::{Health, HealthSource, Pos, Stats},
state::DeltaTime, state::DeltaTime,
sync::Uid, sync::Uid,
}; };
@ -25,19 +25,30 @@ impl<'a> System<'a> for Sys {
ReadStorage<'a, Uid>, ReadStorage<'a, Uid>,
ReadStorage<'a, Pos>, ReadStorage<'a, Pos>,
ReadStorage<'a, Stats>, ReadStorage<'a, Stats>,
ReadStorage<'a, Health>,
WriteStorage<'a, HpFloaterList>, WriteStorage<'a, HpFloaterList>,
); );
#[allow(clippy::blocks_in_if_conditions)] // TODO: Pending review in #587 #[allow(clippy::blocks_in_if_conditions)] // TODO: Pending review in #587
fn run( fn run(
&mut self, &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 // 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() .join()
.map(|(e, s, _, _)| (e, s.health.current())) .map(|(e, h, _, _)| (e, h.current()))
.collect::<Vec<_>>() .collect::<Vec<_>>()
{ {
let _ = hp_floater_lists.insert(entity, HpFloaterList { 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 // Add hp floaters to all entities that have been damaged
let my_uid = uids.get(my_entity.0); let my_uid = uids.get(my_entity.0);
for (entity, health, hp_floater_list) in (&entities, &stats, &mut hp_floater_lists) for (entity, health, hp_floater_list) in (&entities, &healths, &mut hp_floater_lists).join()
.join()
.map(|(e, s, fl)| (e, s.health, fl))
{ {
// Increment timer for time since last damaged by me // Increment timer for time since last damaged by me
hp_floater_list hp_floater_list
@ -64,20 +73,16 @@ impl<'a> System<'a> for Sys {
if hp_floater_list.last_hp != health.current() { if hp_floater_list.last_hp != health.current() {
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 // 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 // Also, If we make health store a vec of the last_changes (from say the last
// frame), what if the client receives the stats component from // frame), what if the client receives the health component from
// two different server ticks at once, then one will be lost // two different server ticks at once, then one will be lost
// (tbf this is probably a rare occurance and the results // (tbf this is probably a rare occurance and the results
// would just be a transient glitch in the display of these damage numbers) // 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 // (maybe health changes could be sent to the client as a list
// of events) // of events)
if match health.last_change.1.cause { if match health.last_change.1.cause {
HealthSource::Attack { by } HealthSource::Damage { by: Some(by), .. }
| HealthSource::Projectile { owner: Some(by) } | HealthSource::Heal { by: Some(by) } => {
| HealthSource::Energy { owner: Some(by) }
| HealthSource::Explosion { owner: Some(by) }
| HealthSource::Buff { owner: Some(by) }
| HealthSource::Healing { by: Some(by) } => {
let by_me = my_uid.map_or(false, |&uid| by == uid); let by_me = my_uid.map_or(false, |&uid| by == uid);
// If the attack was by me also reset this timer // If the attack was by me also reset this timer
if by_me { if by_me {
@ -101,8 +106,8 @@ impl<'a> System<'a> for Sys {
} }
} }
// Remove floater lists on entities without stats or without position // Remove floater lists on entities without health or without position
for entity in (&entities, !&stats, &hp_floater_lists) for entity in (&entities, !&healths, &hp_floater_lists)
.join() .join()
.map(|(e, _, _)| e) .map(|(e, _, _)| e)
.collect::<Vec<_>>() .collect::<Vec<_>>()

View File

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

View File

@ -322,6 +322,7 @@ impl<'a> Widget for Group<'a> {
let client_state = self.client.state(); let client_state = self.client.state();
let stats = client_state.ecs().read_storage::<common::comp::Stats>(); 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 energy = client_state.ecs().read_storage::<common::comp::Energy>();
let buffs = client_state.ecs().read_storage::<common::comp::Buffs>(); let buffs = client_state.ecs().read_storage::<common::comp::Buffs>();
let uid_allocator = client_state let uid_allocator = client_state
@ -338,80 +339,84 @@ impl<'a> Widget for Group<'a> {
self.show.group = true; self.show.group = true;
let entity = uid_allocator.retrieve_entity_internal(uid.into()); let entity = uid_allocator.retrieve_entity_internal(uid.into());
let stats = entity.and_then(|entity| stats.get(entity)); 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 energy = entity.and_then(|entity| energy.get(entity));
let buffs = entity.and_then(|entity| buffs.get(entity)); let buffs = entity.and_then(|entity| buffs.get(entity));
let is_leader = uid == leader;
if let Some(stats) = stats { if let Some(stats) = stats {
let char_name = stats.name.to_string(); let char_name = stats.name.to_string();
let health_perc = stats.health.current() as f64 / stats.health.maximum() as f64; if let Some(health) = health {
let health_perc = health.current() as f64 / health.maximum() as f64;
// change panel positions when debug info is shown // change panel positions when debug info is shown
let back = if i == 0 { let back = if i == 0 {
Image::new(self.imgs.member_bg) Image::new(self.imgs.member_bg)
.top_left_with_margins_on(ui.window, offset, 20.0) .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)
} else { } else {
Some(TEXT_COLOR) Image::new(self.imgs.member_bg)
}) .down_from(state.ids.member_panels_bg[i - 1], 45.0)
.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,
}; };
// Change text offset depending on health amount let hp_ani = (self.pulse * 4.0/* speed factor */).cos() * 0.5 + 0.8; //Animation timer
let txt_offset = match stats.health.maximum() { let crit_hp_color: Color = Color::Rgba(0.79, 0.19, 0.17, hp_ani);
0..=999 => 4.0, let health_col = match (health_perc * 100.0) as u8 {
1000..=9999 => 4.5, 0..=20 => crit_hp_color,
10000..=99999 => 5.0, 21..=40 => LOW_HP_COLOR,
_ => 5.5, _ => HP_COLOR,
}; };
Text::new(&txt) // Don't show panel for the player!
.mid_top_with_margin_on(state.ids.member_panels_bg[i], txt_offset) // Panel BG
.font_size(font_size) back.w_h(152.0, 36.0)
.font_id(self.fonts.cyri.conrod_id) .color(if is_leader {
.color(Color::Rgba(1.0, 1.0, 1.0, 0.5)) Some(ERROR_COLOR)
.set(state.ids.health_txt[i], ui); } 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 // Panel Frame
Image::new(self.imgs.member_frame) Image::new(self.imgs.member_frame)
.w_h(152.0, 36.0) .w_h(152.0, 36.0)

View File

@ -753,6 +753,7 @@ impl Hud {
let ecs = client.state().ecs(); let ecs = client.state().ecs();
let pos = ecs.read_storage::<comp::Pos>(); let pos = ecs.read_storage::<comp::Pos>();
let stats = ecs.read_storage::<comp::Stats>(); let stats = ecs.read_storage::<comp::Stats>();
let healths = ecs.read_storage::<comp::Health>();
let buffs = ecs.read_storage::<comp::Buffs>(); let buffs = ecs.read_storage::<comp::Buffs>();
let energy = ecs.read_storage::<comp::Energy>(); let energy = ecs.read_storage::<comp::Energy>();
let hp_floater_lists = ecs.read_storage::<vcomp::HpFloaterList>(); let hp_floater_lists = ecs.read_storage::<vcomp::HpFloaterList>();
@ -767,11 +768,10 @@ impl Hud {
.get(client.entity()) .get(client.entity())
.map_or(0, |stats| stats.level.level()); .map_or(0, |stats| stats.level.level());
//self.input = client.read_storage::<comp::ControllerInputs>(); //self.input = client.read_storage::<comp::ControllerInputs>();
if let Some(stats) = stats.get(me) { if let Some(health) = healths.get(me) {
// Hurt Frame // Hurt Frame
let hp_percentage = let hp_percentage = health.current() as f32 / health.maximum() as f32 * 100.0;
stats.health.current() as f32 / stats.health.maximum() as f32 * 100.0; if hp_percentage < 10.0 && !health.is_dead {
if hp_percentage < 10.0 && !stats.is_dead {
let hurt_fade = let hurt_fade =
(self.pulse * (10.0 - hp_percentage as f32) * 0.1/* speed factor */).sin() (self.pulse * (10.0 - hp_percentage as f32) * 0.1/* speed factor */).sin()
* 0.5 * 0.5
@ -792,7 +792,7 @@ impl Hud {
.set(self.ids.alpha_text, ui_widgets); .set(self.ids.alpha_text, ui_widgets);
// Death Frame // Death Frame
if stats.is_dead { if health.is_dead {
Image::new(self.imgs.death_bg) Image::new(self.imgs.death_bg)
.wh_of(ui_widgets.window) .wh_of(ui_widgets.window)
.middle_of(ui_widgets.window) .middle_of(ui_widgets.window)
@ -801,7 +801,7 @@ impl Hud {
.set(self.ids.death_bg, ui_widgets); .set(self.ids.death_bg, ui_widgets);
} }
// Crosshair // 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 = Lerp::lerp(
self.crosshair_opacity, self.crosshair_opacity,
if show_crosshair { 1.0 } else { 0.0 }, if show_crosshair { 1.0 } else { 0.0 },
@ -850,11 +850,11 @@ impl Hud {
// Render Player SCT numbers // Render Player SCT numbers
let mut player_sct_bg_id_walker = self.ids.player_sct_bgs.walk(); let mut player_sct_bg_id_walker = self.ids.player_sct_bgs.walk();
let mut player_sct_id_walker = self.ids.player_scts.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 hp_floater_lists
.get(me) .get(me)
.filter(|fl| !fl.floaters.is_empty()), .filter(|fl| !fl.floaters.is_empty()),
stats.get(me), healths.get(me),
) { ) {
if global_state.settings.gameplay.sct_player_batch { if global_state.settings.gameplay.sct_player_batch {
let number_speed = 100.0; // Player Batched Numbers Speed 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); 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 // Divide by 10 to stay in the same dimension as the HP display
let hp_dmg_rounded_abs = ((hp_damage + 5) / 10).abs(); 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 let timer = floaters
.last() .last()
.expect("There must be at least one floater") .expect("There must be at least one floater")
@ -927,8 +927,7 @@ impl Hud {
&mut self.ids.player_scts, &mut self.ids.player_scts,
&mut ui_widgets.widget_id_generator(), &mut ui_widgets.widget_id_generator(),
); );
let max_hp_frac = let max_hp_frac = floater.hp_change.abs() as f32 / health.maximum() as f32;
floater.hp_change.abs() as f32 / stats.health.maximum() as f32;
// Increase font size based on fraction of maximum health // Increase font size based on fraction of maximum health
// "flashes" by having a larger size in the first 100ms // "flashes" by having a larger size in the first 100ms
let font_size = 30 let font_size = 30
@ -1152,11 +1151,12 @@ impl Hud {
let speech_bubbles = &self.speech_bubbles; let speech_bubbles = &self.speech_bubbles;
// Render overhead name tags and health bars // 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, &entities,
&pos, &pos,
interpolated.maybe(), interpolated.maybe(),
&stats, &stats,
&healths,
&buffs, &buffs,
energy.maybe(), energy.maybe(),
scales.maybe(), scales.maybe(),
@ -1166,12 +1166,24 @@ impl Hud {
) )
.join() .join()
.filter(|t| { .filter(|t| {
let stats = t.3; let health = t.4;
let entity = t.0; let entity = t.0;
entity != me && !stats.is_dead entity != me && !health.is_dead
}) })
.filter_map( .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 // Use interpolated position if available
let pos = interpolated.map_or(pos.0, |i| i.pos); let pos = interpolated.map_or(pos.0, |i| i.pos);
let in_group = client.group_members().contains_key(uid); let in_group = client.group_members().contains_key(uid);
@ -1183,7 +1195,7 @@ impl Hud {
let display_overhead_info = let display_overhead_info =
(info.target_entity.map_or(false, |e| e == entity) (info.target_entity.map_or(false, |e| e == entity)
|| info.selected_entity.map_or(false, |s| s.0 == entity) || info.selected_entity.map_or(false, |s| s.0 == entity)
|| overhead::show_healthbar(stats) || overhead::should_show_healthbar(health)
|| in_group) || in_group)
&& dist_sqr && dist_sqr
< (if in_group { < (if in_group {
@ -1201,6 +1213,7 @@ impl Hud {
let info = display_overhead_info.then(|| overhead::Info { let info = display_overhead_info.then(|| overhead::Info {
name: &stats.name, name: &stats.name,
stats, stats,
health,
buffs, buffs,
energy, energy,
}); });
@ -1216,6 +1229,7 @@ impl Hud {
info, info,
bubble, bubble,
stats, stats,
health,
buffs, buffs,
body.height() * scale.map_or(1.0, |s| s.0) + 0.5, body.height() * scale.map_or(1.0, |s| s.0) + 0.5,
hpfl, hpfl,
@ -1292,7 +1306,7 @@ impl Hud {
}); });
// Divide by 10 to stay in the same dimension as the HP display // Divide by 10 to stay in the same dimension as the HP display
let hp_dmg_rounded_abs = ((hp_damage + 5) / 10).abs(); 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 let timer = floaters
.last() .last()
.expect("There must be at least one floater") .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()); .next(&mut self.ids.sct_bgs, &mut ui_widgets.widget_id_generator());
// Calculate total change // Calculate total change
let max_hp_frac = 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 // Increase font size based on fraction of maximum health
// "flashes" by having a larger size in the first 100ms // "flashes" by having a larger size in the first 100ms
let font_size = 30 let font_size = 30
@ -1897,6 +1911,7 @@ impl Hud {
let ecs = client.state().ecs(); let ecs = client.state().ecs();
let entity = client.entity(); let entity = client.entity();
let stats = ecs.read_storage::<comp::Stats>(); let stats = ecs.read_storage::<comp::Stats>();
let healths = ecs.read_storage::<comp::Health>();
let loadouts = ecs.read_storage::<comp::Loadout>(); let loadouts = ecs.read_storage::<comp::Loadout>();
let energies = ecs.read_storage::<comp::Energy>(); let energies = ecs.read_storage::<comp::Energy>();
let character_states = ecs.read_storage::<comp::CharacterState>(); let character_states = ecs.read_storage::<comp::CharacterState>();
@ -1904,6 +1919,7 @@ impl Hud {
let inventories = ecs.read_storage::<comp::Inventory>(); let inventories = ecs.read_storage::<comp::Inventory>();
if let ( if let (
Some(stats), Some(stats),
Some(health),
Some(loadout), Some(loadout),
Some(energy), Some(energy),
Some(_character_state), Some(_character_state),
@ -1911,6 +1927,7 @@ impl Hud {
Some(inventory), Some(inventory),
) = ( ) = (
stats.get(entity), stats.get(entity),
healths.get(entity),
loadouts.get(entity), loadouts.get(entity),
energies.get(entity), energies.get(entity),
character_states.get(entity), character_states.get(entity),
@ -1924,6 +1941,7 @@ impl Hud {
&self.fonts, &self.fonts,
&self.rot_imgs, &self.rot_imgs,
&stats, &stats,
&health,
&loadout, &loadout,
&energy, &energy,
//&character_state, //&character_state,

View File

@ -8,7 +8,7 @@ use crate::{
settings::GameplaySettings, settings::GameplaySettings,
ui::{fonts::ConrodVoxygenFonts, Ingameable}, 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::{ use conrod_core::{
color, color,
position::Align, position::Align,
@ -58,12 +58,13 @@ widget_ids! {
pub struct Info<'a> { pub struct Info<'a> {
pub name: &'a str, pub name: &'a str,
pub stats: &'a Stats, pub stats: &'a Stats,
pub health: &'a Health,
pub buffs: &'a Buffs, pub buffs: &'a Buffs,
pub energy: Option<&'a Energy>, pub energy: Option<&'a Energy>,
} }
/// Determines whether to show the healthbar /// 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 /// ui widget containing everything that goes over a character's head
/// (Speech bubble, Name, Level, HP/energy bars, etc.) /// (Speech bubble, Name, Level, HP/energy bars, etc.)
@ -141,7 +142,7 @@ impl<'a> Ingameable for Overhead<'a> {
} else { } else {
0 0
} }
+ if show_healthbar(info.stats) { + if should_show_healthbar(info.health) {
5 + if info.energy.is_some() { 1 } else { 0 } 5 + if info.energy.is_some() { 1 } else { 0 }
} else { } else {
0 0
@ -171,17 +172,17 @@ impl<'a> Widget for Overhead<'a> {
if let Some(Info { if let Some(Info {
name, name,
stats, stats,
health,
buffs, buffs,
energy, energy,
}) = self.info }) = self.info
{ {
// Used to set healthbar colours based on hp_percentage // Used to set healthbar colours based on hp_percentage
let hp_percentage = let hp_percentage = health.current() as f64 / health.maximum() as f64 * 100.0;
stats.health.current() as f64 / stats.health.maximum() as f64 * 100.0;
// Compare levels to decide if a skull is shown // Compare levels to decide if a skull is shown
let level_comp = stats.level.level() as i64 - self.own_level as i64; let level_comp = stats.level.level() as i64 - self.own_level as i64;
let health_current = (stats.health.current() / 10) as f64; let health_current = (health.current() / 10) as f64;
let health_max = (stats.health.maximum() / 10) as f64; let health_max = (health.maximum() / 10) as f64;
let name_y = if (health_current - health_max).abs() < 1e-6 { let name_y = if (health_current - health_max).abs() < 1e-6 {
MANA_BAR_Y + 20.0 MANA_BAR_Y + 20.0
} else if level_comp > 9 && !self.in_group { } else if level_comp > 9 && !self.in_group {
@ -302,7 +303,7 @@ impl<'a> Widget for Overhead<'a> {
.parent(id) .parent(id)
.set(state.ids.name, ui); .set(state.ids.name, ui);
if show_healthbar(stats) { if should_show_healthbar(health) {
// Show HP Bar // Show HP Bar
let hp_ani = (self.pulse * 4.0/* speed factor */).cos() * 0.5 + 1.0; //Animation timer 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); 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) .parent(id)
.set(state.ids.health_bar, ui); .set(state.ids.health_bar, ui);
let mut txt = format!("{}/{}", health_cur_txt, health_max_txt); 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() txt = self.voxygen_i18n.get("hud.group.dead").to_string()
}; };
Text::new(&txt) Text::new(&txt)

View File

@ -20,7 +20,7 @@ use common::comp::{
tool::{Tool, ToolKind}, tool::{Tool, ToolKind},
Hands, ItemKind, Hands, ItemKind,
}, },
Energy, Inventory, Loadout, Stats, Energy, Health, Inventory, Loadout, Stats,
}; };
use conrod_core::{ use conrod_core::{
color, color,
@ -124,6 +124,7 @@ pub struct Skillbar<'a> {
fonts: &'a ConrodVoxygenFonts, fonts: &'a ConrodVoxygenFonts,
rot_imgs: &'a ImgsRot, rot_imgs: &'a ImgsRot,
stats: &'a Stats, stats: &'a Stats,
health: &'a Health,
loadout: &'a Loadout, loadout: &'a Loadout,
energy: &'a Energy, energy: &'a Energy,
// character_state: &'a CharacterState, // character_state: &'a CharacterState,
@ -148,6 +149,7 @@ impl<'a> Skillbar<'a> {
fonts: &'a ConrodVoxygenFonts, fonts: &'a ConrodVoxygenFonts,
rot_imgs: &'a ImgsRot, rot_imgs: &'a ImgsRot,
stats: &'a Stats, stats: &'a Stats,
health: &'a Health,
loadout: &'a Loadout, loadout: &'a Loadout,
energy: &'a Energy, energy: &'a Energy,
// character_state: &'a CharacterState, // character_state: &'a CharacterState,
@ -167,6 +169,7 @@ impl<'a> Skillbar<'a> {
fonts, fonts,
rot_imgs, rot_imgs,
stats, stats,
health,
loadout, loadout,
energy, energy,
common: widget::CommonBuilder::default(), 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 exp_percentage = (self.stats.exp.current() as f64) / (self.stats.exp.maximum() as f64);
let mut hp_percentage = let mut hp_percentage = self.health.current() as f64 / self.health.maximum() as f64 * 100.0;
self.stats.health.current() as f64 / self.stats.health.maximum() as f64 * 100.0;
let mut energy_percentage = let mut energy_percentage =
self.energy.current() as f64 / self.energy.maximum() as f64 * 100.0; 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; hp_percentage = 0.0;
energy_percentage = 0.0; energy_percentage = 0.0;
}; };
@ -293,7 +295,7 @@ impl<'a> Widget for Skillbar<'a> {
.set(state.ids.level_down, ui); .set(state.ids.level_down, ui);
} }
// Death message // Death message
if self.stats.is_dead { if self.health.is_dead {
if let Some(key) = self if let Some(key) = self
.global_state .global_state
.settings .settings
@ -400,12 +402,12 @@ impl<'a> Widget for Skillbar<'a> {
if let BarNumbers::Values = bar_values { if let BarNumbers::Values = bar_values {
let mut hp_txt = format!( let mut hp_txt = format!(
"{}/{}", "{}/{}",
(self.stats.health.current() / 10).max(1) as u32, /* Don't show 0 health for (self.health.current() / 10).max(1) as u32, /* Don't show 0 health for
* living players */ * living players */
(self.stats.health.maximum() / 10) as u32 (self.health.maximum() / 10) as u32
); );
let mut energy_txt = format!("{}", energy_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(); hp_txt = self.localized_strings.get("hud.group.dead").to_string();
energy_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 { if let BarNumbers::Percent = bar_values {
let mut hp_txt = format!("{}%", hp_percentage as u32); let mut hp_txt = format!("{}%", hp_percentage as u32);
let mut energy_txt = format!("{}", energy_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(); hp_txt = self.localized_strings.get("hud.group.dead").to_string();
energy_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::{ use common::{
comp::{ comp::{
item::{ItemKind, ToolKind}, item::{ItemKind, ToolKind},
Body, CharacterState, Item, Last, LightAnimation, LightEmitter, Loadout, Ori, PhysicsState, Body, CharacterState, Health, Item, Last, LightAnimation, LightEmitter, Loadout, Ori,
Pos, Scale, Stats, Vel, PhysicsState, Pos, Scale, Vel,
}, },
span, span,
state::{DeltaTime, State}, state::{DeltaTime, State},
@ -545,7 +545,7 @@ impl FigureMgr {
character, character,
last_character, last_character,
physics, physics,
stats, health,
loadout, loadout,
item, item,
), ),
@ -559,7 +559,7 @@ impl FigureMgr {
ecs.read_storage::<CharacterState>().maybe(), ecs.read_storage::<CharacterState>().maybe(),
ecs.read_storage::<Last<CharacterState>>().maybe(), ecs.read_storage::<Last<CharacterState>>().maybe(),
&ecs.read_storage::<PhysicsState>(), &ecs.read_storage::<PhysicsState>(),
ecs.read_storage::<Stats>().maybe(), ecs.read_storage::<Health>().maybe(),
ecs.read_storage::<Loadout>().maybe(), ecs.read_storage::<Loadout>().maybe(),
ecs.read_storage::<Item>().maybe(), ecs.read_storage::<Item>().maybe(),
) )
@ -662,11 +662,11 @@ impl FigureMgr {
}; };
// Change in health as color! // Change in health as color!
let col = stats let col = health
.map(|s| { .map(|h| {
vek::Rgba::broadcast(1.0) vek::Rgba::broadcast(1.0)
+ vek::Rgba::new(2.0, 2.0, 2., 0.00).map(|c| { + 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)) .unwrap_or(vek::Rgba::broadcast(1.0))
@ -2749,13 +2749,13 @@ impl FigureMgr {
&ecs.read_storage::<Pos>(), &ecs.read_storage::<Pos>(),
ecs.read_storage::<Ori>().maybe(), ecs.read_storage::<Ori>().maybe(),
&ecs.read_storage::<Body>(), &ecs.read_storage::<Body>(),
ecs.read_storage::<Stats>().maybe(), ecs.read_storage::<Health>().maybe(),
ecs.read_storage::<Loadout>().maybe(), ecs.read_storage::<Loadout>().maybe(),
ecs.read_storage::<Scale>().maybe(), ecs.read_storage::<Scale>().maybe(),
) )
.join() .join()
// Don't render dead entities // 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, _)| { .for_each(|(entity, pos, _, body, _, loadout, _)| {
if let Some((locals, bone_consts, model, _)) = self.get_model_for_render( if let Some((locals, bone_consts, model, _)) = self.get_model_for_render(
tick, tick,
@ -2803,13 +2803,13 @@ impl FigureMgr {
&ecs.read_storage::<Pos>(), &ecs.read_storage::<Pos>(),
ecs.read_storage::<Ori>().maybe(), ecs.read_storage::<Ori>().maybe(),
&ecs.read_storage::<Body>(), &ecs.read_storage::<Body>(),
ecs.read_storage::<Stats>().maybe(), ecs.read_storage::<Health>().maybe(),
ecs.read_storage::<Loadout>().maybe(), ecs.read_storage::<Loadout>().maybe(),
ecs.read_storage::<Scale>().maybe(), ecs.read_storage::<Scale>().maybe(),
) )
.join() .join()
// Don't render dead entities // 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; let is_player = entity == player_entity;
@ -2853,10 +2853,9 @@ impl FigureMgr {
ecs.read_storage::<Pos>().get(player_entity), ecs.read_storage::<Pos>().get(player_entity),
ecs.read_storage::<Body>().get(player_entity), ecs.read_storage::<Body>().get(player_entity),
) { ) {
let stats_storage = state.read_storage::<Stats>(); let healths = state.read_storage::<Health>();
let stats = stats_storage.get(player_entity); let health = healths.get(player_entity);
if health.map_or(false, |h| h.is_dead) {
if stats.map_or(false, |s| s.is_dead) {
return; return;
} }

View File

@ -604,10 +604,10 @@ impl Scene {
.maybe(), .maybe(),
scene_data.state.ecs().read_storage::<comp::Scale>().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::Body>(),
&scene_data.state.ecs().read_storage::<comp::Stats>(), &scene_data.state.ecs().read_storage::<comp::Health>(),
) )
.join() .join()
.filter(|(_, _, _, _, stats)| !stats.is_dead) .filter(|(_, _, _, _, health)| !health.is_dead)
.filter(|(pos, _, _, _, _)| { .filter(|(pos, _, _, _, _)| {
(pos.0.distance_squared(player_pos) as f32) (pos.0.distance_squared(player_pos) as f32)
< (loaded_distance.min(SHADOW_MAX_DIST) + SHADOW_DIST_RADIUS).powf(2.0) < (loaded_distance.min(SHADOW_MAX_DIST) + SHADOW_DIST_RADIUS).powf(2.0)