Merge branch 'xMAC94x/FeuerzeugBierflasche' into 'master'

xMAC94x/feuerzeugBierflasche

See merge request veloren/veloren!1478
This commit is contained in:
Marcel 2020-11-05 09:22:50 +00:00
commit 121364821a
61 changed files with 1285 additions and 985 deletions

View File

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

View File

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

View File

@ -184,13 +184,14 @@ bir hesap oluşturabilirsin."#,
"hud.chat.pvp_ranged_kill_msg": "[{victim}], [{attacker}] tarafından vuruldu.",
"hud.chat.pvp_explosion_kill_msg": "[{victim}], [{attacker}] tarafından havaya uçuruldu.",
"hud.chat.pvp_energy_kill_msg": "[{victim}], [{attacker}] tarafından büyü ile mağlup edildi.",
"hud.chat.pvp_buff_kill_msg": "[{victim}], [{attacker}] tarafından öldürüldü.",
"hud.chat.pvp_other_kill_msg": "[{victim}], [{attacker}] tarafından öldürüldü.",
"hud.chat.npc_melee_kill_msg": "[{victim}], [{attacker}] tarafından mağlup edildi.",
"hud.chat.npc_ranged_kill_msg": "[{victim}], [{attacker}] tarafından vuruldu.",
"hud.chat.npc_explosion_kill_msg": "[{victim}], [{attacker}] tarafından havaya uçuruldu.",
"hud.chat.npc_buff_kill_msg": "[{victim}], [{attacker}] tarafından öldürüldü.",
"hud.chat.npc_energy_kill_msg": "[{victim}], [{attacker}] tarafından büyü ile mağlup edildi.",
"hud.chat.npc_other_kill_msg": "[{victim}], [{attacker}] tarafından öldürüldü.",
"hud.chat.loot_msg": "[{item}] topladın.",
"hud.chat.loot_fail": "Envanterin dolu!",
@ -584,6 +585,49 @@ Koruma
"Yardım edin! Yardım edin! Baskı altındayım!",
"Ah, artık sistemin doğasında var olan şiddeti görüyoruz.",
"Bu bana göre bir çizik bile değil!",
"Yapma şunu!",
"Ben sana ne yaptım ki?!",
"Lütfen bana saldırmayı kes!",
"Hey! Onu nereye yönelttiğine dikkat et!",
"Aşağılık herif, gözüm bile görmesin!",
"Durdur şunu! Git buradan!",
"Şimdi beni kızdırmaya başladın!",
"Hey! Sen kim olduğunu zannediyorsun ki?!",
"Bunun için kelleni alacağım!",
"Yapma, lütfen! Değerli hiçbir şeyim yok bile!",
"Kardeşimi üzerine salacağım, o benden bile büyük!",
"Olamaaaz, Seni anneme söyleyeceğim!",
"Lanet olsun sana!",
"Lütfen yapma şunu.",
"Bunu yapman pek kibarca değildi!",
"Evet silahın çalışıyor, şimdi kaldırabilir misin?",
"Bağışlayın beni!",
"Lütfen, benim bir ailem var!",
"Ölmek için çok gencim!",
"Bunu konuşarak çözebilir miyiz?",
"Şiddet hiçbir zaman çare değildir!",
"Günüm gittikçe kötüleşiyor...",
"Hey, bu acıttı!",
"Eek!",
"Ne kadar da kaba!",
"Dur, sana yalvarırım!",
"Lanet olsun sana!",
"Bu eğlenceli bile değil.",
"Ne cürret?!",
"Bunu sana ödeteceğim!",
"Yapmaya devam edersen bunun için pişman olacaksın!",
"Sana zarar vermek zorunda bırakma beni!",
"Bir yanlış anlaşılma olmalı!",
"Bunu yapmak zorunda değilsin!",
"Defol!",
"Bu gerçekten acıttı!",
"Bunu neden yaptın ki?",
"Ruhlar tarafından, dur!",
"Beni başkasıyla karıştırmış olmalısın!",
"Bunu haketmiyorum!",
"Lütfen bunu bir daha yapma.",
"Muhafızlar, şu canavarı göle atın!",
"Tarrasque'ımı üzerine salarım!",
],
}
)

View File

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

View File

@ -51,6 +51,7 @@ pub enum ChatCommand {
Group,
Health,
Help,
Home,
JoinFaction,
Jump,
Kick,
@ -98,6 +99,7 @@ pub static CHAT_COMMANDS: &[ChatCommand] = &[
ChatCommand::Group,
ChatCommand::Health,
ChatCommand::Help,
ChatCommand::Home,
ChatCommand::JoinFaction,
ChatCommand::Jump,
ChatCommand::Kick,
@ -267,6 +269,7 @@ impl ChatCommand {
"Display information about commands",
NoAdmin,
),
ChatCommand::Home => cmd(vec![], "Return to the home town", NoAdmin),
ChatCommand::JoinFaction => ChatCommandData::new(
vec![Any("faction", Optional)],
"Join/leave the specified faction",
@ -427,6 +430,7 @@ impl ChatCommand {
ChatCommand::Health => "health",
ChatCommand::JoinFaction => "join_faction",
ChatCommand::Help => "help",
ChatCommand::Home => "home",
ChatCommand::Jump => "jump",
ChatCommand::Kick => "kick",
ChatCommand::Kill => "kill",

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

@ -0,0 +1,131 @@
use crate::{comp::Body, sync::Uid, DamageSource};
use serde::{Deserialize, Serialize};
use specs::{Component, FlaggedStorage};
use specs_idvs::IdvStorage;
/// Specifies what and how much changed current health
#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq, Serialize, Deserialize)]
pub struct HealthChange {
pub amount: i32,
pub cause: HealthSource,
}
#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq, Serialize, Deserialize)]
pub enum HealthSource {
Damage { kind: DamageSource, by: Option<Uid> },
Heal { by: Option<Uid> },
//Attack { by: Uid }, // TODO: Implement weapon
//Projectile { owner: Option<Uid> },
//Explosion { owner: Option<Uid> },
//Energy { owner: Option<Uid> },
//Buff { owner: Option<Uid> },
Suicide,
World,
Revive,
Command,
LevelUp,
Item,
//Healing { by: Option<Uid> },
Unknown,
}
#[derive(Clone, Copy, Debug, Serialize, Deserialize)]
pub struct Health {
base_max: u32,
current: u32,
maximum: u32,
pub last_change: (f64, HealthChange),
pub is_dead: bool,
}
impl Health {
pub fn new(body: Body, level: u32) -> Self {
let mut health = Health::empty();
health.update_max_hp(Some(body), level);
health.set_to(health.maximum(), HealthSource::Revive);
health
}
pub fn empty() -> Self {
Health {
current: 0,
maximum: 0,
base_max: 0,
last_change: (0.0, HealthChange {
amount: 0,
cause: HealthSource::Revive,
}),
is_dead: false,
}
}
pub fn current(&self) -> u32 { self.current }
pub fn maximum(&self) -> u32 { self.maximum }
pub fn set_to(&mut self, amount: u32, cause: HealthSource) {
let amount = amount.min(self.maximum);
self.last_change = (0.0, HealthChange {
amount: amount as i32 - self.current as i32,
cause,
});
self.current = amount;
}
pub fn change_by(&mut self, change: HealthChange) {
self.current = ((self.current as i32 + change.amount).max(0) as u32).min(self.maximum);
self.last_change = (0.0, change);
}
// This function changes the modified max health value, not the base health
// value. The modified health value takes into account buffs and other temporary
// changes to max health.
pub fn set_maximum(&mut self, amount: u32) {
self.maximum = amount;
self.current = self.current.min(self.maximum);
}
// This is private because max hp is based on the level
fn set_base_max(&mut self, amount: u32) {
self.base_max = amount;
self.current = self.current.min(self.maximum);
}
pub fn reset_max(&mut self) { self.maximum = self.base_max; }
pub fn should_die(&self) -> bool { self.current == 0 }
pub fn revive(&mut self) {
self.set_to(self.maximum(), HealthSource::Revive);
self.is_dead = false;
}
// TODO: Delete this once stat points will be a thing
pub fn update_max_hp(&mut self, body: Option<Body>, level: u32) {
if let Some(body) = body {
self.set_base_max(body.base_health() + body.base_health_increase() * level);
self.set_maximum(body.base_health() + body.base_health_increase() * level);
}
}
pub fn with_max_health(mut self, amount: u32) -> Self {
self.maximum = amount;
self.current = amount;
self
}
}
impl Component for Health {
type Storage = FlaggedStorage<Self, IdvStorage<Self>>;
}
#[derive(Copy, Clone, Debug, Serialize, Deserialize)]
pub struct Dead {
pub cause: HealthSource,
}
impl Component for Dead {
type Storage = IdvStorage<Self>;
}

View File

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

View File

@ -9,6 +9,7 @@ pub mod chat;
mod controller;
mod energy;
pub mod group;
mod health;
mod inputs;
mod inventory;
mod last;
@ -43,8 +44,9 @@ pub use controller::{
Climb, ControlAction, ControlEvent, Controller, ControllerInputs, GroupManip, Input,
InventoryManip, MountState, Mounting,
};
pub use energy::{Energy, EnergySource};
pub use energy::{Energy, EnergyChange, EnergySource};
pub use group::Group;
pub use health::{Health, HealthChange, HealthSource};
pub use inputs::CanBuild;
pub use inventory::{
item,
@ -62,5 +64,5 @@ pub use player::Player;
pub use projectile::Projectile;
pub use shockwave::{Shockwave, ShockwaveHitEntities};
pub use skills::{Skill, SkillGroup, SkillGroupType, SkillSet};
pub use stats::{Exp, HealthChange, HealthSource, Level, Stats};
pub use stats::{Exp, Level, Stats};
pub use visual::{LightAnimation, LightEmitter};

View File

@ -1,4 +1,4 @@
use crate::{comp::Buff, sync::Uid, Damages, Explosion, Knockback};
use crate::{comp::Buff, sync::Uid, Damage, Explosion, GroupTarget, Knockback};
use serde::{Deserialize, Serialize};
use specs::{Component, FlaggedStorage};
use specs_idvs::IdvStorage;
@ -6,7 +6,7 @@ use std::time::Duration;
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub enum Effect {
Damages(Damages),
Damage(Option<GroupTarget>, Damage),
Knockback(Knockback),
RewardEnergy(u32),
Explode(Explosion),

View File

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

View File

@ -1,45 +1,12 @@
use crate::{
comp,
comp::{body::humanoid::Species, skills::SkillSet, Body},
sync::Uid,
};
use serde::{Deserialize, Serialize};
use specs::{Component, FlaggedStorage};
use specs_idvs::IdvStorage;
use std::{error::Error, fmt};
/// Specifies what and how much changed current health
#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq, Serialize, Deserialize)]
pub struct HealthChange {
pub amount: i32,
pub cause: HealthSource,
}
#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq, Serialize, Deserialize)]
pub enum HealthSource {
Attack { by: Uid }, // TODO: Implement weapon
Projectile { owner: Option<Uid> },
Explosion { owner: Option<Uid> },
Energy { owner: Option<Uid> },
Buff { owner: Option<Uid> },
Suicide,
World,
Revive,
Command,
LevelUp,
Item,
Healing { by: Option<Uid> },
Unknown,
}
#[derive(Clone, Copy, Debug, Serialize, Deserialize)]
pub struct Health {
base_max: u32,
current: u32,
maximum: u32,
pub last_change: (f64, HealthChange),
}
#[derive(Clone, Copy, Debug, Serialize, Deserialize)]
pub struct Exp {
current: u32,
@ -51,41 +18,6 @@ pub struct Level {
amount: u32,
}
impl Health {
pub fn current(&self) -> u32 { self.current }
pub fn maximum(&self) -> u32 { self.maximum }
pub fn set_to(&mut self, amount: u32, cause: HealthSource) {
let amount = amount.min(self.maximum);
self.last_change = (0.0, HealthChange {
amount: amount as i32 - self.current as i32,
cause,
});
self.current = amount;
}
pub fn change_by(&mut self, change: HealthChange) {
self.current = ((self.current as i32 + change.amount).max(0) as u32).min(self.maximum);
self.last_change = (0.0, change);
}
// This function changes the modified max health value, not the base health
// value. The modified health value takes into account buffs and other temporary
// changes to max health.
pub fn set_maximum(&mut self, amount: u32) {
self.maximum = amount;
self.current = self.current.min(self.maximum);
}
// This is private because max hp is based on the level
fn set_base_max(&mut self, amount: u32) {
self.base_max = amount;
self.current = self.current.min(self.maximum);
}
pub fn reset_max(&mut self) { self.maximum = self.base_max; }
}
#[derive(Debug)]
pub enum StatChangeError {
Underflow,
@ -139,35 +71,15 @@ impl Level {
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct Stats {
pub name: String,
pub health: Health,
pub level: Level,
pub exp: Exp,
pub skill_set: SkillSet,
pub endurance: u32,
pub fitness: u32,
pub willpower: u32,
pub is_dead: bool,
pub body_type: Body,
}
impl Stats {
pub fn should_die(&self) -> bool { self.health.current == 0 }
pub fn revive(&mut self) {
self.health
.set_to(self.health.maximum(), HealthSource::Revive);
self.is_dead = false;
}
// TODO: Delete this once stat points will be a thing
pub fn update_max_hp(&mut self, body: Body) {
self.health
.set_base_max(body.base_health() + body.base_health_increase() * self.level.amount);
self.health
.set_maximum(body.base_health() + body.base_health_increase() * self.level.amount);
}
}
impl Stats {
pub fn new(name: String, body: Body) -> Self {
let species = if let comp::Body::Humanoid(hbody) = body {
@ -189,17 +101,8 @@ impl Stats {
None => (0, 0, 0),
};
let mut stats = Self {
Self {
name,
health: Health {
current: 0,
maximum: 0,
base_max: 0,
last_change: (0.0, HealthChange {
amount: 0,
cause: HealthSource::Revive,
}),
},
level: Level { amount: 1 },
exp: Exp {
current: 0,
@ -209,17 +112,8 @@ impl Stats {
endurance,
fitness,
willpower,
is_dead: false,
body_type: body,
};
stats.update_max_hp(body);
stats
.health
.set_to(stats.health.maximum(), HealthSource::Revive);
stats
}
}
/// Creates an empty `Stats` instance - used during character loading from
@ -227,15 +121,6 @@ impl Stats {
pub fn empty() -> Self {
Self {
name: "".to_owned(),
health: Health {
current: 0,
maximum: 0,
base_max: 0,
last_change: (0.0, HealthChange {
amount: 0,
cause: HealthSource::Revive,
}),
},
level: Level { amount: 1 },
exp: Exp {
current: 0,
@ -245,27 +130,11 @@ impl Stats {
endurance: 0,
fitness: 0,
willpower: 0,
is_dead: false,
body_type: comp::Body::Humanoid(comp::body::humanoid::Body::random()),
}
}
pub fn with_max_health(mut self, amount: u32) -> Self {
self.health.maximum = amount;
self.health.current = amount;
self
}
}
impl Component for Stats {
type Storage = FlaggedStorage<Self, IdvStorage<Self>>;
}
#[derive(Copy, Clone, Debug, Serialize, Deserialize)]
pub struct Dying {
pub cause: HealthSource,
}
impl Component for Dying {
type Storage = IdvStorage<Self>;
}

View File

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

View File

@ -31,11 +31,10 @@ pub enum ServerEvent {
pos: Vec3<f32>,
explosion: Explosion,
owner: Option<Uid>,
friendly_damage: bool,
reagent: Option<Reagent>,
},
Damage {
uid: Uid,
entity: EcsEntity,
change: comp::HealthChange,
},
Destroy {
@ -93,6 +92,7 @@ pub enum ServerEvent {
CreateNpc {
pos: comp::Pos,
stats: comp::Stats,
health: comp::Health,
loadout: comp::Loadout,
body: comp::Body,
agent: Option<comp::Agent>,
@ -110,6 +110,10 @@ pub enum ServerEvent {
entity: EcsEntity,
buff_change: comp::BuffChange,
},
EnergyChange {
entity: EcsEntity,
change: comp::EnergyChange,
},
}
pub struct EventBus<E> {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,11 +1,11 @@
use crate::{
comp::{
BuffCategory, BuffChange, BuffEffect, BuffId, BuffSource, Buffs, HealthChange,
HealthSource, Loadout, ModifierKind, Stats,
BuffCategory, BuffChange, BuffEffect, BuffId, BuffSource, Buffs, Health, HealthChange,
HealthSource, Loadout, ModifierKind,
},
event::{EventBus, ServerEvent},
state::DeltaTime,
sync::Uid,
DamageSource,
};
use specs::{Entities, Join, Read, ReadStorage, System, WriteStorage};
use std::time::Duration;
@ -17,21 +17,20 @@ impl<'a> System<'a> for Sys {
Entities<'a>,
Read<'a, DeltaTime>,
Read<'a, EventBus<ServerEvent>>,
ReadStorage<'a, Uid>,
ReadStorage<'a, Loadout>,
WriteStorage<'a, Stats>,
WriteStorage<'a, Health>,
WriteStorage<'a, Buffs>,
);
fn run(
&mut self,
(entities, dt, server_bus, uids, loadouts, mut stats, mut buffs): Self::SystemData,
(entities, dt, server_bus, loadouts, mut healths, mut buffs): Self::SystemData,
) {
let mut server_emitter = server_bus.emitter();
// Set to false to avoid spamming server
buffs.set_event_emission(false);
stats.set_event_emission(false);
for (entity, buff_comp, uid, stat) in (&entities, &mut buffs, &uids, &mut stats).join() {
healths.set_event_emission(false);
for (entity, buff_comp, health) in (&entities, &mut buffs, &mut healths).join() {
let mut expired_buffs = Vec::<BuffId>::new();
for (id, buff) in buff_comp.buffs.iter_mut() {
// Tick the buff and subtract delta from it
@ -63,8 +62,8 @@ impl<'a> System<'a> for Sys {
}
}
// Call to reset stats to base values
stat.health.reset_max();
// Call to reset health to base values
health.reset_max();
// Iterator over the lists of buffs by kind
for buff_ids in buff_comp.kinds.values() {
@ -88,12 +87,15 @@ impl<'a> System<'a> for Sys {
|| buff.time.map_or(false, |dur| dur == Duration::default())
{
let cause = if *accumulated > 0.0 {
HealthSource::Healing { by: buff_owner }
HealthSource::Heal { by: buff_owner }
} else {
HealthSource::Buff { owner: buff_owner }
HealthSource::Damage {
kind: DamageSource::Other,
by: buff_owner,
}
};
server_emitter.emit(ServerEvent::Damage {
uid: *uid,
entity,
change: HealthChange {
amount: *accumulated as i32,
cause,
@ -104,14 +106,10 @@ impl<'a> System<'a> for Sys {
},
BuffEffect::MaxHealthModifier { value, kind } => match kind {
ModifierKind::Multiplicative => {
stat.health.set_maximum(
(stat.health.maximum() as f32 * *value) as u32,
);
health.set_maximum((health.maximum() as f32 * *value) as u32);
},
ModifierKind::Additive => {
stat.health.set_maximum(
(stat.health.maximum() as f32 + *value) as u32,
);
health.set_maximum((health.maximum() as f32 + *value) as u32);
},
},
};
@ -127,8 +125,8 @@ impl<'a> System<'a> for Sys {
});
}
// Remove stats that don't persist on death
if stat.is_dead {
// Remove buffs that don't persist on death
if health.is_dead {
server_emitter.emit(ServerEvent::Buff {
entity,
buff_change: BuffChange::RemoveByCategory {
@ -141,6 +139,6 @@ impl<'a> System<'a> for Sys {
}
// Turned back to true
buffs.set_event_emission(true);
stats.set_event_emission(true);
healths.set_event_emission(true);
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -4,12 +4,13 @@
use crate::{
settings::{BanRecord, EditableSetting},
Server, StateExt,
Server, SpawnPoint, StateExt,
};
use chrono::{NaiveTime, Timelike};
use common::{
cmd::{ChatCommand, CHAT_COMMANDS, CHAT_SHORTCUTS},
comp::{self, ChatType, Item, LightEmitter, WaypointArea},
effect::Effect,
event::{EventBus, ServerEvent},
msg::{DisconnectReason, Notification, PlayerListUpdate, ServerGeneral},
npc::{self, get_npc_name},
@ -18,7 +19,7 @@ use common::{
terrain::{Block, BlockKind, SpriteKind, TerrainChunkSize},
util::Dir,
vol::RectVolSize,
Explosion, LoadoutBuilder,
Damage, DamageSource, Explosion, LoadoutBuilder, RadiusEffect,
};
use rand::Rng;
use specs::{Builder, Entity as EcsEntity, Join, WorldExt};
@ -82,6 +83,7 @@ fn get_handler(cmd: &ChatCommand) -> CommandHandler {
ChatCommand::Group => handle_group,
ChatCommand::Health => handle_health,
ChatCommand::Help => handle_help,
ChatCommand::Home => handle_home,
ChatCommand::JoinFaction => handle_join_faction,
ChatCommand::Jump => handle_jump,
ChatCommand::Kick => handle_kick,
@ -359,6 +361,29 @@ fn handle_goto(
}
}
fn handle_home(
server: &mut Server,
client: EcsEntity,
target: EcsEntity,
_args: String,
_action: &ChatCommand,
) {
if server
.state
.read_component_copied::<comp::Pos>(target)
.is_some()
{
let home_pos = server.state.ecs().read_resource::<SpawnPoint>().0;
server.state.write_component(target, comp::Pos(home_pos));
server.state.write_component(target, comp::ForceUpdate);
} else {
server.notify_client(
client,
ChatType::CommandError.server_msg("You have no position."),
);
}
}
fn handle_kill(
server: &mut Server,
client: EcsEntity,
@ -369,16 +394,19 @@ fn handle_kill(
let reason = if client == target {
comp::HealthSource::Suicide
} else if let Some(uid) = server.state.read_storage::<Uid>().get(client) {
comp::HealthSource::Attack { by: *uid }
comp::HealthSource::Damage {
kind: DamageSource::Other,
by: Some(*uid),
}
} else {
comp::HealthSource::Command
};
server
.state
.ecs_mut()
.write_storage::<comp::Stats>()
.write_storage::<comp::Health>()
.get_mut(target)
.map(|s| s.health.set_to(0, reason));
.map(|h| h.set_to(0, reason));
}
fn handle_time(
@ -447,13 +475,13 @@ fn handle_health(
action: &ChatCommand,
) {
if let Ok(hp) = scan_fmt!(&args, &action.arg_fmt(), u32) {
if let Some(stats) = server
if let Some(health) = server
.state
.ecs()
.write_storage::<comp::Stats>()
.write_storage::<comp::Health>()
.get_mut(target)
{
stats.health.set_to(hp * 10, comp::HealthSource::Command);
health.set_to(hp * 10, comp::HealthSource::Command);
} else {
server.notify_client(
client,
@ -632,6 +660,7 @@ fn handle_spawn(
.create_npc(
pos,
comp::Stats::new(get_npc_name(id).into(), body),
comp::Health::new(body, 1),
LoadoutBuilder::build_loadout(body, alignment, None, false)
.build(),
body,
@ -738,9 +767,11 @@ fn handle_spawn_training_dummy(
// Level 0 will prevent exp gain from kill
stats.level.set_level(0);
let health = comp::Health::new(body, 0);
server
.state
.create_npc(pos, stats, comp::Loadout::default(), body)
.create_npc(pos, stats, health, comp::Loadout::default(), body)
.with(comp::Vel(vel))
.with(comp::MountState::Unmounted)
.build();
@ -900,12 +931,12 @@ fn handle_kill_npcs(
_action: &ChatCommand,
) {
let ecs = server.state.ecs();
let mut stats = ecs.write_storage::<comp::Stats>();
let mut healths = ecs.write_storage::<comp::Health>();
let players = ecs.read_storage::<comp::Player>();
let mut count = 0;
for (stats, ()) in (&mut stats, !&players).join() {
for (health, ()) in (&mut healths, !&players).join() {
count += 1;
stats.health.set_to(0, comp::HealthSource::Command);
health.set_to(0, comp::HealthSource::Command);
}
let text = if count > 0 {
format!("Destroyed {} NPCs.", count)
@ -1129,16 +1160,20 @@ fn handle_explosion(
.emit_now(ServerEvent::Explosion {
pos: pos.0,
explosion: Explosion {
effects: vec![
RadiusEffect::Entity(
None,
Effect::Damage(Damage {
source: DamageSource::Explosion,
value: 100.0 * power,
}),
),
RadiusEffect::TerrainDestruction(power),
],
radius: 3.0 * power,
max_damage: (100.0 * power) as u32,
min_damage: 0,
max_heal: 0,
min_heal: 0,
terrain_destruction_power: power,
energy_regen: 0,
},
owner: ecs.read_storage::<Uid>().get(target).copied(),
friendly_damage: true,
reagent: None,
})
},
@ -1671,6 +1706,8 @@ fn handle_set_level(
PlayerListUpdate::LevelChange(uid, lvl),
));
let body_type: Option<comp::Body>;
if let Some(stats) = server
.state
.ecs_mut()
@ -1678,13 +1715,20 @@ fn handle_set_level(
.get_mut(player)
{
stats.level.set_level(lvl);
stats.update_max_hp(stats.body_type);
stats
.health
.set_to(stats.health.maximum(), comp::HealthSource::LevelUp);
body_type = Some(stats.body_type);
} else {
error_msg = Some(ChatType::CommandError.server_msg("Player has no stats!"));
body_type = None;
}
if let Some(health) = server
.state
.ecs_mut()
.write_storage::<comp::Health>()
.get_mut(player)
{
health.update_max_hp(body_type, lvl);
health.set_to(health.maximum(), comp::HealthSource::LevelUp);
}
},
Err(e) => {

View File

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

View File

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

View File

@ -1,4 +1,8 @@
use crate::{client::Client, presence::RegionSubscription, Server};
use crate::{
client::Client,
presence::{Presence, RegionSubscription},
Server,
};
use common::{
comp::{self, item, Pos},
consts::MAX_MOUNT_RANGE,
@ -127,12 +131,16 @@ pub fn handle_possess(server: &Server, possessor_uid: Uid, possesse_uid: Uid) {
clients.insert(possesse, c).ok()?;
//optional entities
let mut players = ecs.write_storage::<comp::Player>();
let mut presence = ecs.write_storage::<Presence>();
let mut subscriptions = ecs.write_storage::<RegionSubscription>();
let mut admins = ecs.write_storage::<comp::Admin>();
let mut waypoints = ecs.write_storage::<comp::Waypoint>();
players
.remove(possessor)
.map(|p| players.insert(possesse, p).ok()?);
presence
.remove(possessor)
.map(|p| presence.insert(possesse, p).ok()?);
subscriptions
.remove(possessor)
.map(|s| subscriptions.insert(possesse, s).ok()?);

View File

@ -67,10 +67,10 @@ pub fn handle_inventory(server: &mut Server, entity: EcsEntity, manip: comp::Inv
return;
};
// Grab the stats from the player and check if the player is dead.
let stats = state.ecs().read_storage::<comp::Stats>();
if let Some(entity_stats) = stats.get(entity) {
if entity_stats.is_dead {
// Grab the health from the player and check if the player is dead.
let healths = state.ecs().read_storage::<comp::Health>();
if let Some(entity_health) = healths.get(entity) {
if entity_health.is_dead {
debug!("Failed to pick up item as the player is dead");
return; // If dead, don't continue
}
@ -349,7 +349,7 @@ pub fn handle_inventory(server: &mut Server, entity: EcsEntity, manip: comp::Inv
drop(inventories);
if let Some(effect) = maybe_effect {
state.apply_effect(entity, effect);
state.apply_effect(entity, effect, None);
}
if let Some(event) = event {
state.write_component(entity, comp::InventoryUpdate::new(event));

View File

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

View File

@ -318,11 +318,11 @@ pub fn convert_stats_from_database(stats: &Stats, alias: String) -> common::comp
new_stats.level.set_level(stats.level as u32);
new_stats.exp.update_maximum(stats.level as u32);
new_stats.exp.set_current(stats.exp as u32);
new_stats.update_max_hp(new_stats.body_type);
/*new_stats.update_max_hp(new_stats.body_type);
new_stats.health.set_to(
new_stats.health.maximum(),
common::comp::HealthSource::Revive,
);
);*/
new_stats.endurance = stats.endurance as u32;
new_stats.fitness = stats.fitness as u32;
new_stats.willpower = stats.willpower as u32;

View File

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

View File

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

View File

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

View File

@ -1,9 +1,9 @@
use super::SysTimer;
use common::{
comp::{
BeamSegment, Body, Buffs, CanBuild, CharacterState, Collider, Energy, Gravity, Group, Item,
LightEmitter, Loadout, Mass, MountState, Mounting, Ori, Player, Pos, Scale, Shockwave,
Stats, Sticky, Vel,
BeamSegment, Body, Buffs, CanBuild, CharacterState, Collider, Energy, Gravity, Group,
Health, Item, LightEmitter, Loadout, Mass, MountState, Mounting, Ori, Player, Pos, Scale,
Shockwave, Stats, Sticky, Vel,
},
msg::EcsCompPacket,
span,
@ -46,6 +46,7 @@ pub struct TrackedComps<'a> {
pub stats: ReadStorage<'a, Stats>,
pub buffs: ReadStorage<'a, Buffs>,
pub energy: ReadStorage<'a, Energy>,
pub health: ReadStorage<'a, Health>,
pub can_build: ReadStorage<'a, CanBuild>,
pub light_emitter: ReadStorage<'a, LightEmitter>,
pub item: ReadStorage<'a, Item>,
@ -94,6 +95,10 @@ impl<'a> TrackedComps<'a> {
.get(entity)
.cloned()
.map(|c| comps.push(c.into()));
self.health
.get(entity)
.cloned()
.map(|c| comps.push(c.into()));
self.can_build
.get(entity)
.cloned()
@ -164,6 +169,7 @@ pub struct ReadTrackers<'a> {
pub stats: ReadExpect<'a, UpdateTracker<Stats>>,
pub buffs: ReadExpect<'a, UpdateTracker<Buffs>>,
pub energy: ReadExpect<'a, UpdateTracker<Energy>>,
pub health: ReadExpect<'a, UpdateTracker<Health>>,
pub can_build: ReadExpect<'a, UpdateTracker<CanBuild>>,
pub light_emitter: ReadExpect<'a, UpdateTracker<LightEmitter>>,
pub item: ReadExpect<'a, UpdateTracker<Item>>,
@ -195,6 +201,7 @@ impl<'a> ReadTrackers<'a> {
.with_component(&comps.uid, &*self.stats, &comps.stats, filter)
.with_component(&comps.uid, &*self.buffs, &comps.buffs, filter)
.with_component(&comps.uid, &*self.energy, &comps.energy, filter)
.with_component(&comps.uid, &*self.health, &comps.health, filter)
.with_component(&comps.uid, &*self.can_build, &comps.can_build, filter)
.with_component(
&comps.uid,
@ -233,6 +240,7 @@ pub struct WriteTrackers<'a> {
stats: WriteExpect<'a, UpdateTracker<Stats>>,
buffs: WriteExpect<'a, UpdateTracker<Buffs>>,
energy: WriteExpect<'a, UpdateTracker<Energy>>,
health: WriteExpect<'a, UpdateTracker<Health>>,
can_build: WriteExpect<'a, UpdateTracker<CanBuild>>,
light_emitter: WriteExpect<'a, UpdateTracker<LightEmitter>>,
item: WriteExpect<'a, UpdateTracker<Item>>,
@ -258,6 +266,7 @@ fn record_changes(comps: &TrackedComps, trackers: &mut WriteTrackers) {
trackers.stats.record_changes(&comps.stats);
trackers.buffs.record_changes(&comps.buffs);
trackers.energy.record_changes(&comps.energy);
trackers.health.record_changes(&comps.health);
trackers.can_build.record_changes(&comps.can_build);
trackers.light_emitter.record_changes(&comps.light_emitter);
trackers.item.record_changes(&comps.item);
@ -296,6 +305,7 @@ fn record_changes(comps: &TrackedComps, trackers: &mut WriteTrackers) {
log_counts!(player, "Players");
log_counts!(stats, "Stats");
log_counts!(energy, "Energies");
log_vounts!(health, "Healths");
log_counts!(light_emitter, "Light emitters");
log_counts!(item, "Items");
log_counts!(scale, "Scales");
@ -319,6 +329,7 @@ pub fn register_trackers(world: &mut World) {
world.register_tracker::<Stats>();
world.register_tracker::<Buffs>();
world.register_tracker::<Energy>();
world.register_tracker::<Health>();
world.register_tracker::<CanBuild>();
world.register_tracker::<LightEmitter>();
world.register_tracker::<Item>();

View File

@ -146,11 +146,7 @@ impl<'a> System<'a> for Sys {
LoadoutBuilder::build_loadout(body, alignment, main_tool, entity.is_giant)
.build();
stats.update_max_hp(stats.body_type);
stats
.health
.set_to(stats.health.maximum(), comp::HealthSource::Revive);
let health = comp::Health::new(stats.body_type, stats.level.level());
let can_speak = match body {
comp::Body::Humanoid(_) => alignment == comp::Alignment::Npc,
@ -174,6 +170,7 @@ impl<'a> System<'a> for Sys {
server_emitter.emit(ServerEvent::CreateNpc {
pos: Pos(entity.pos),
stats,
health,
loadout,
agent: if entity.has_agency {
Some(comp::Agent::new(entity.pos, can_speak, &body))

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -20,7 +20,7 @@ use common::comp::{
tool::{Tool, ToolKind},
Hands, ItemKind,
},
Energy, Inventory, Loadout, Stats,
Energy, Health, Inventory, Loadout, Stats,
};
use conrod_core::{
color,
@ -124,6 +124,7 @@ pub struct Skillbar<'a> {
fonts: &'a ConrodVoxygenFonts,
rot_imgs: &'a ImgsRot,
stats: &'a Stats,
health: &'a Health,
loadout: &'a Loadout,
energy: &'a Energy,
// character_state: &'a CharacterState,
@ -148,6 +149,7 @@ impl<'a> Skillbar<'a> {
fonts: &'a ConrodVoxygenFonts,
rot_imgs: &'a ImgsRot,
stats: &'a Stats,
health: &'a Health,
loadout: &'a Loadout,
energy: &'a Energy,
// character_state: &'a CharacterState,
@ -167,6 +169,7 @@ impl<'a> Skillbar<'a> {
fonts,
rot_imgs,
stats,
health,
loadout,
energy,
common: widget::CommonBuilder::default(),
@ -216,11 +219,10 @@ impl<'a> Widget for Skillbar<'a> {
let exp_percentage = (self.stats.exp.current() as f64) / (self.stats.exp.maximum() as f64);
let mut hp_percentage =
self.stats.health.current() as f64 / self.stats.health.maximum() as f64 * 100.0;
let mut hp_percentage = self.health.current() as f64 / self.health.maximum() as f64 * 100.0;
let mut energy_percentage =
self.energy.current() as f64 / self.energy.maximum() as f64 * 100.0;
if self.stats.is_dead {
if self.health.is_dead {
hp_percentage = 0.0;
energy_percentage = 0.0;
};
@ -293,7 +295,7 @@ impl<'a> Widget for Skillbar<'a> {
.set(state.ids.level_down, ui);
}
// Death message
if self.stats.is_dead {
if self.health.is_dead {
if let Some(key) = self
.global_state
.settings
@ -400,12 +402,12 @@ impl<'a> Widget for Skillbar<'a> {
if let BarNumbers::Values = bar_values {
let mut hp_txt = format!(
"{}/{}",
(self.stats.health.current() / 10).max(1) as u32, /* Don't show 0 health for
* living players */
(self.stats.health.maximum() / 10) as u32
(self.health.current() / 10).max(1) as u32, /* Don't show 0 health for
* living players */
(self.health.maximum() / 10) as u32
);
let mut energy_txt = format!("{}", energy_percentage as u32);
if self.stats.is_dead {
if self.health.is_dead {
hp_txt = self.localized_strings.get("hud.group.dead").to_string();
energy_txt = self.localized_strings.get("hud.group.dead").to_string();
};
@ -438,7 +440,7 @@ impl<'a> Widget for Skillbar<'a> {
if let BarNumbers::Percent = bar_values {
let mut hp_txt = format!("{}%", hp_percentage as u32);
let mut energy_txt = format!("{}", energy_percentage as u32);
if self.stats.is_dead {
if self.health.is_dead {
hp_txt = self.localized_strings.get("hud.group.dead").to_string();
energy_txt = self.localized_strings.get("hud.group.dead").to_string();
};

View File

@ -26,8 +26,8 @@ use anim::{
use common::{
comp::{
item::{ItemKind, ToolKind},
Body, CharacterState, Item, Last, LightAnimation, LightEmitter, Loadout, Ori, PhysicsState,
Pos, Scale, Stats, Vel,
Body, CharacterState, Health, Item, Last, LightAnimation, LightEmitter, Loadout, Ori,
PhysicsState, Pos, Scale, Vel,
},
span,
state::{DeltaTime, State},
@ -545,7 +545,7 @@ impl FigureMgr {
character,
last_character,
physics,
stats,
health,
loadout,
item,
),
@ -559,7 +559,7 @@ impl FigureMgr {
ecs.read_storage::<CharacterState>().maybe(),
ecs.read_storage::<Last<CharacterState>>().maybe(),
&ecs.read_storage::<PhysicsState>(),
ecs.read_storage::<Stats>().maybe(),
ecs.read_storage::<Health>().maybe(),
ecs.read_storage::<Loadout>().maybe(),
ecs.read_storage::<Item>().maybe(),
)
@ -662,11 +662,11 @@ impl FigureMgr {
};
// Change in health as color!
let col = stats
.map(|s| {
let col = health
.map(|h| {
vek::Rgba::broadcast(1.0)
+ vek::Rgba::new(2.0, 2.0, 2., 0.00).map(|c| {
(c / (1.0 + DAMAGE_FADE_COEFFICIENT * s.health.last_change.0)) as f32
(c / (1.0 + DAMAGE_FADE_COEFFICIENT * h.last_change.0)) as f32
})
})
.unwrap_or(vek::Rgba::broadcast(1.0))
@ -2749,13 +2749,13 @@ impl FigureMgr {
&ecs.read_storage::<Pos>(),
ecs.read_storage::<Ori>().maybe(),
&ecs.read_storage::<Body>(),
ecs.read_storage::<Stats>().maybe(),
ecs.read_storage::<Health>().maybe(),
ecs.read_storage::<Loadout>().maybe(),
ecs.read_storage::<Scale>().maybe(),
)
.join()
// Don't render dead entities
.filter(|(_, _, _, _, stats, _, _)| stats.map_or(true, |s| !s.is_dead))
.filter(|(_, _, _, _, health, _, _)| health.map_or(true, |h| !h.is_dead))
.for_each(|(entity, pos, _, body, _, loadout, _)| {
if let Some((locals, bone_consts, model, _)) = self.get_model_for_render(
tick,
@ -2803,13 +2803,13 @@ impl FigureMgr {
&ecs.read_storage::<Pos>(),
ecs.read_storage::<Ori>().maybe(),
&ecs.read_storage::<Body>(),
ecs.read_storage::<Stats>().maybe(),
ecs.read_storage::<Health>().maybe(),
ecs.read_storage::<Loadout>().maybe(),
ecs.read_storage::<Scale>().maybe(),
)
.join()
// Don't render dead entities
.filter(|(_, _, _, _, stats, _, _)| stats.map_or(true, |s| !s.is_dead))
.filter(|(_, _, _, _, health, _, _)| health.map_or(true, |h| !h.is_dead))
{
let is_player = entity == player_entity;
@ -2853,10 +2853,9 @@ impl FigureMgr {
ecs.read_storage::<Pos>().get(player_entity),
ecs.read_storage::<Body>().get(player_entity),
) {
let stats_storage = state.read_storage::<Stats>();
let stats = stats_storage.get(player_entity);
if stats.map_or(false, |s| s.is_dead) {
let healths = state.read_storage::<Health>();
let health = healths.get(player_entity);
if health.map_or(false, |h| h.is_dead) {
return;
}

View File

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