EXP on kill is now shared between damage contributors. A "damage contributor" is either an individual entity, or a group - depending if the attacker is in a group. This means that not only does the "killing blow" no longer get 100% of EXP, but multiple groups and individuals all receive their fair share of EXP on death (assuming they are still within range of the entity when it dies).

Damage from a given individual or group only counts towards a kill for 10 minutes since that individual or group's last damage to the entity - after this period their damage contribution is removed. This avoids the list of damage contributors growing excessively large for an entity that does a lot of combat but never dies.

EXP sharing within groups is unchanged - the difference is simply that the input to this calculation may be less than 100% of the base EXP reward for the kill if other individuals or groups contributed damage.
This commit is contained in:
Ben Wallis 2021-11-13 20:46:45 +00:00
parent 4b3aa7e8fa
commit 022c1417b6
23 changed files with 512 additions and 156 deletions

View File

@ -40,6 +40,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Tweaked critical chance of legendary weapons - Tweaked critical chance of legendary weapons
- Agents using fireball projectiles aim at the feet instead of the eyes - Agents using fireball projectiles aim at the feet instead of the eyes
- Explosions can now have a nonzero minimum falloff - Explosions can now have a nonzero minimum falloff
- EXP on kill is now shared based on damage contribution
### Removed ### Removed

View File

@ -53,6 +53,11 @@ where
.add_directive("veloren_common::trade=info".parse().unwrap()) .add_directive("veloren_common::trade=info".parse().unwrap())
.add_directive("veloren_world::sim=info".parse().unwrap()) .add_directive("veloren_world::sim=info".parse().unwrap())
.add_directive("veloren_world::civ=info".parse().unwrap()) .add_directive("veloren_world::civ=info".parse().unwrap())
.add_directive(
"veloren_server::events::entity_manipulation=info"
.parse()
.unwrap(),
)
.add_directive("hyper=info".parse().unwrap()) .add_directive("hyper=info".parse().unwrap())
.add_directive("prometheus_hyper=info".parse().unwrap()) .add_directive("prometheus_hyper=info".parse().unwrap())
.add_directive("mio::pool=info".parse().unwrap()) .add_directive("mio::pool=info".parse().unwrap())

View File

@ -1,6 +1,7 @@
use crate::sync; use crate::sync;
use common::comp; use common::{comp, resources::Time};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use specs::WorldExt;
use std::marker::PhantomData; use std::marker::PhantomData;
use sum_type::sum_type; use sum_type::sum_type;
@ -92,7 +93,12 @@ impl sync::CompPacket for EcsCompPacket {
EcsCompPacket::Auras(comp) => sync::handle_insert(comp, entity, world), EcsCompPacket::Auras(comp) => sync::handle_insert(comp, entity, world),
EcsCompPacket::Energy(comp) => sync::handle_insert(comp, entity, world), EcsCompPacket::Energy(comp) => sync::handle_insert(comp, entity, world),
EcsCompPacket::Combo(comp) => sync::handle_insert(comp, entity, world), EcsCompPacket::Combo(comp) => sync::handle_insert(comp, entity, world),
EcsCompPacket::Health(comp) => sync::handle_insert(comp, entity, world), EcsCompPacket::Health(mut comp) => {
// Time isn't synced between client and server so replace the Time from the
// server with the Client's local Time to enable accurate comparison.
comp.last_change.time = *world.read_resource::<Time>();
sync::handle_insert(comp, entity, world)
},
EcsCompPacket::Poise(comp) => sync::handle_insert(comp, entity, world), EcsCompPacket::Poise(comp) => sync::handle_insert(comp, entity, world),
EcsCompPacket::LightEmitter(comp) => sync::handle_insert(comp, entity, world), EcsCompPacket::LightEmitter(comp) => sync::handle_insert(comp, entity, world),
EcsCompPacket::Inventory(comp) => sync::handle_insert(comp, entity, world), EcsCompPacket::Inventory(comp) => sync::handle_insert(comp, entity, world),
@ -132,7 +138,12 @@ impl sync::CompPacket for EcsCompPacket {
EcsCompPacket::Auras(comp) => sync::handle_modify(comp, entity, world), EcsCompPacket::Auras(comp) => sync::handle_modify(comp, entity, world),
EcsCompPacket::Energy(comp) => sync::handle_modify(comp, entity, world), EcsCompPacket::Energy(comp) => sync::handle_modify(comp, entity, world),
EcsCompPacket::Combo(comp) => sync::handle_modify(comp, entity, world), EcsCompPacket::Combo(comp) => sync::handle_modify(comp, entity, world),
EcsCompPacket::Health(comp) => sync::handle_modify(comp, entity, world), EcsCompPacket::Health(mut comp) => {
// Time isn't synced between client and server so replace the Time from the
// server with the Client's local Time to enable accurate comparison.
comp.last_change.time = *world.read_resource::<Time>();
sync::handle_modify(comp, entity, world)
},
EcsCompPacket::Poise(comp) => sync::handle_modify(comp, entity, world), EcsCompPacket::Poise(comp) => sync::handle_modify(comp, entity, world),
EcsCompPacket::LightEmitter(comp) => sync::handle_modify(comp, entity, world), EcsCompPacket::LightEmitter(comp) => sync::handle_modify(comp, entity, world),
EcsCompPacket::Inventory(comp) => sync::handle_modify(comp, entity, world), EcsCompPacket::Inventory(comp) => sync::handle_modify(comp, entity, world),

View File

@ -26,6 +26,7 @@ use rand::{thread_rng, Rng};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use crate::{comp::Group, resources::Time};
#[cfg(not(target_arch = "wasm32"))] #[cfg(not(target_arch = "wasm32"))]
use specs::{saveload::MarkerAllocator, Entity as EcsEntity, ReadStorage}; use specs::{saveload::MarkerAllocator, Entity as EcsEntity, ReadStorage};
#[cfg(not(target_arch = "wasm32"))] #[cfg(not(target_arch = "wasm32"))]
@ -53,6 +54,7 @@ pub enum AttackSource {
pub struct AttackerInfo<'a> { pub struct AttackerInfo<'a> {
pub entity: EcsEntity, pub entity: EcsEntity,
pub uid: Uid, pub uid: Uid,
pub group: Option<&'a Group>,
pub energy: Option<&'a Energy>, pub energy: Option<&'a Energy>,
pub combo: Option<&'a Combo>, pub combo: Option<&'a Combo>,
pub inventory: Option<&'a Inventory>, pub inventory: Option<&'a Inventory>,
@ -180,6 +182,7 @@ impl Attack {
// maybe look into modifying strength of other effects? // maybe look into modifying strength of other effects?
strength_modifier: f32, strength_modifier: f32,
attack_source: AttackSource, attack_source: AttackSource,
time: Time,
mut emit: impl FnMut(ServerEvent), mut emit: impl FnMut(ServerEvent),
mut emit_outcome: impl FnMut(Outcome), mut emit_outcome: impl FnMut(Outcome),
) -> bool { ) -> bool {
@ -222,10 +225,11 @@ impl Attack {
); );
let change = damage.damage.calculate_health_change( let change = damage.damage.calculate_health_change(
damage_reduction, damage_reduction,
attacker.map(|a| a.uid), attacker.map(|x| x.into()),
is_crit, is_crit,
self.crit_multiplier, self.crit_multiplier,
strength_modifier, strength_modifier,
time,
); );
let applied_damage = -change.amount; let applied_damage = -change.amount;
accumulated_damage += applied_damage; accumulated_damage += applied_damage;
@ -273,8 +277,9 @@ impl Attack {
if let Some(attacker_entity) = attacker.map(|a| a.entity) { if let Some(attacker_entity) = attacker.map(|a| a.entity) {
let change = HealthChange { let change = HealthChange {
amount: applied_damage * l, amount: applied_damage * l,
by: attacker.map(|a| a.uid), by: attacker.map(|a| a.into()),
cause: None, cause: None,
time,
}; };
if change.amount.abs() > Health::HEALTH_EPSILON { if change.amount.abs() > Health::HEALTH_EPSILON {
emit(ServerEvent::HealthChange { emit(ServerEvent::HealthChange {
@ -298,8 +303,9 @@ impl Attack {
CombatEffect::Heal(h) => { CombatEffect::Heal(h) => {
let change = HealthChange { let change = HealthChange {
amount: *h * strength_modifier, amount: *h * strength_modifier,
by: attacker.map(|a| a.uid), by: attacker.map(|a| a.into()),
cause: None, cause: None,
time,
}; };
if change.amount.abs() > Health::HEALTH_EPSILON { if change.amount.abs() > Health::HEALTH_EPSILON {
emit(ServerEvent::HealthChange { emit(ServerEvent::HealthChange {
@ -408,8 +414,9 @@ impl Attack {
if let Some(attacker_entity) = attacker.map(|a| a.entity) { if let Some(attacker_entity) = attacker.map(|a| a.entity) {
let change = HealthChange { let change = HealthChange {
amount: accumulated_damage * l, amount: accumulated_damage * l,
by: attacker.map(|a| a.uid), by: attacker.map(|a| a.into()),
cause: None, cause: None,
time,
}; };
if change.amount.abs() > Health::HEALTH_EPSILON { if change.amount.abs() > Health::HEALTH_EPSILON {
emit(ServerEvent::HealthChange { emit(ServerEvent::HealthChange {
@ -433,8 +440,9 @@ impl Attack {
CombatEffect::Heal(h) => { CombatEffect::Heal(h) => {
let change = HealthChange { let change = HealthChange {
amount: h * strength_modifier, amount: h * strength_modifier,
by: attacker.map(|a| a.uid), by: attacker.map(|a| a.into()),
cause: None, cause: None,
time,
}; };
if change.amount.abs() > Health::HEALTH_EPSILON { if change.amount.abs() > Health::HEALTH_EPSILON {
emit(ServerEvent::HealthChange { emit(ServerEvent::HealthChange {
@ -588,6 +596,41 @@ pub enum CombatRequirement {
Combo(u32), Combo(u32),
} }
#[derive(Clone, Copy, Debug, Hash, Eq, PartialEq, Serialize, Deserialize)]
pub enum DamageContributor {
Solo(Uid),
Group { entity_uid: Uid, group: Group },
}
impl DamageContributor {
pub fn new(uid: Uid, group: Option<Group>) -> Self {
if let Some(group) = group {
DamageContributor::Group {
entity_uid: uid,
group,
}
} else {
DamageContributor::Solo(uid)
}
}
pub fn uid(&self) -> Uid {
match self {
DamageContributor::Solo(uid) => *uid,
DamageContributor::Group {
entity_uid,
group: _,
} => *entity_uid,
}
}
}
impl From<AttackerInfo<'_>> for DamageContributor {
fn from(attacker_info: AttackerInfo) -> Self {
DamageContributor::new(attacker_info.uid, attacker_info.group.copied())
}
}
#[derive(Copy, Clone, Debug, Hash, PartialEq, Eq, Serialize, Deserialize)] #[derive(Copy, Clone, Debug, Hash, PartialEq, Eq, Serialize, Deserialize)]
pub enum DamageSource { pub enum DamageSource {
Buff(BuffKind), Buff(BuffKind),
@ -673,10 +716,11 @@ impl Damage {
pub fn calculate_health_change( pub fn calculate_health_change(
self, self,
damage_reduction: f32, damage_reduction: f32,
uid: Option<Uid>, damage_contributor: Option<DamageContributor>,
is_crit: bool, is_crit: bool,
crit_mult: f32, crit_mult: f32,
damage_modifier: f32, damage_modifier: f32,
time: Time,
) -> HealthChange { ) -> HealthChange {
let mut damage = self.value * damage_modifier; let mut damage = self.value * damage_modifier;
let critdamage = if is_crit { let critdamage = if is_crit {
@ -697,8 +741,9 @@ impl Damage {
HealthChange { HealthChange {
amount: -damage, amount: -damage,
by: uid, by: damage_contributor,
cause: Some(self.source), cause: Some(self.source),
time,
} }
}, },
DamageSource::Falling => { DamageSource::Falling => {
@ -710,12 +755,14 @@ impl Damage {
amount: -damage, amount: -damage,
by: None, by: None,
cause: Some(self.source), cause: Some(self.source),
time,
} }
}, },
DamageSource::Buff(_) | DamageSource::Other => HealthChange { DamageSource::Buff(_) | DamageSource::Other => HealthChange {
amount: -damage, amount: -damage,
by: None, by: None,
cause: Some(self.source), cause: Some(self.source),
time,
}, },
} }
} }
@ -913,22 +960,22 @@ pub fn combat_rating(
const SKILLS_WEIGHT: f32 = 1.0; const SKILLS_WEIGHT: f32 = 1.0;
const POISE_WEIGHT: f32 = 0.5; const POISE_WEIGHT: f32 = 0.5;
const CRIT_WEIGHT: f32 = 0.5; const CRIT_WEIGHT: f32 = 0.5;
// Normalzied with a standard max health of 100 // Normalized with a standard max health of 100
let health_rating = health.base_max() let health_rating = health.base_max()
/ 100.0 / 100.0
/ (1.0 - Damage::compute_damage_reduction(Some(inventory), None, None)).max(0.00001); / (1.0 - Damage::compute_damage_reduction(Some(inventory), None, None)).max(0.00001);
// Normalzied with a standard max energy of 100 and energy reward multiplier of // Normalized with a standard max energy of 100 and energy reward multiplier of
// x1 // x1
let energy_rating = (energy.base_max() + compute_max_energy_mod(Some(inventory))) / 100.0 let energy_rating = (energy.base_max() + compute_max_energy_mod(Some(inventory))) / 100.0
* compute_energy_reward_mod(Some(inventory)); * compute_energy_reward_mod(Some(inventory));
// Normalzied with a standard max poise of 100 // Normalized with a standard max poise of 100
let poise_rating = poise.base_max() as f32 let poise_rating = poise.base_max() as f32
/ 100.0 / 100.0
/ (1.0 - Poise::compute_poise_damage_reduction(inventory)).max(0.00001); / (1.0 - Poise::compute_poise_damage_reduction(inventory)).max(0.00001);
// Normalzied with a standard crit multiplier of 1.2 // Normalized with a standard crit multiplier of 1.2
let crit_rating = compute_crit_mult(Some(inventory)) / 1.2; let crit_rating = compute_crit_mult(Some(inventory)) / 1.2;
// Assumes a standard person has earned 20 skill points in the general skill // Assumes a standard person has earned 20 skill points in the general skill

View File

@ -15,7 +15,7 @@ use tracing::{error, warn};
// - clients don't know which pets are theirs (could be easy to solve by // - clients don't know which pets are theirs (could be easy to solve by
// putting owner uid in Role::Pet) // putting owner uid in Role::Pet)
#[derive(Copy, Clone, Debug, PartialEq, Serialize, Deserialize)] #[derive(Copy, Clone, Debug, Eq, Hash, PartialEq, Serialize, Deserialize)]
pub struct Group(u32); pub struct Group(u32);
// TODO: Hack // TODO: Hack

View File

@ -1,8 +1,11 @@
#[cfg(not(target_arch = "wasm32"))] #[cfg(not(target_arch = "wasm32"))]
use crate::comp; use crate::comp;
use crate::{uid::Uid, DamageSource}; use crate::DamageSource;
use hashbrown::HashMap;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::convert::TryFrom;
use crate::{combat::DamageContributor, resources::Time};
#[cfg(not(target_arch = "wasm32"))] #[cfg(not(target_arch = "wasm32"))]
use specs::{Component, DerefFlaggedStorage}; use specs::{Component, DerefFlaggedStorage};
#[cfg(not(target_arch = "wasm32"))] #[cfg(not(target_arch = "wasm32"))]
@ -12,16 +15,24 @@ use std::ops::Mul;
/// Specifies what and how much changed current health /// Specifies what and how much changed current health
#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq)] #[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq)]
pub struct HealthChange { pub struct HealthChange {
/// The amount of the health change, negative is damage, positive is healing
pub amount: f32, pub amount: f32,
pub by: Option<Uid>, /// The the individual or group who caused the health change (None if the
/// damage wasn't caused by an entity)
pub by: Option<DamageContributor>,
/// The category of action that resulted in the health change
pub cause: Option<DamageSource>, pub cause: Option<DamageSource>,
/// The time that the health change occurred at
pub time: Time,
} }
impl HealthChange { impl HealthChange {
pub fn damage_by(&self) -> Option<Uid> { self.cause.is_some().then_some(self.by).flatten() } pub fn damage_by(&self) -> Option<DamageContributor> {
self.cause.is_some().then_some(self.by).flatten()
}
} }
#[derive(Clone, Copy, Debug, Serialize, Deserialize)] #[derive(Clone, Debug, Serialize, Deserialize)]
/// Health is represented by u32s within the module, but treated as a float by /// Health is represented by u32s within the module, but treated as a float by
/// the rest of the game. /// the rest of the game.
// As a general rule, all input and output values to public functions should be // As a general rule, all input and output values to public functions should be
@ -41,11 +52,14 @@ pub struct Health {
/// Maximum is the amount of health the entity has after temporary modifiers /// Maximum is the amount of health the entity has after temporary modifiers
/// are considered /// are considered
maximum: u32, maximum: u32,
// Time since last change and what the last change was /// The last change to health
// TODO: Remove the time since last change, either convert to time of last change or just emit pub last_change: HealthChange,
// an outcome where appropriate. Is currently just used for frontend.
pub last_change: (f64, HealthChange),
pub is_dead: bool, pub is_dead: bool,
/// Keeps track of damage per DamageContributor and the last time they
/// caused damage, used for EXP sharing
#[serde(skip)]
damage_contributors: HashMap<DamageContributor, (u64, Time)>,
} }
impl Health { impl Health {
@ -98,12 +112,14 @@ impl Health {
current: health, current: health,
base_max: health, base_max: health,
maximum: health, maximum: health,
last_change: (0.0, HealthChange { last_change: HealthChange {
amount: 0.0, amount: 0.0,
by: None, by: None,
cause: None, cause: None,
}), time: Time(0.0),
},
is_dead: false, is_dead: false,
damage_contributors: HashMap::new(),
} }
} }
@ -120,10 +136,39 @@ impl Health {
#[cfg(not(target_arch = "wasm32"))] #[cfg(not(target_arch = "wasm32"))]
pub fn change_by(&mut self, change: HealthChange) { pub fn change_by(&mut self, change: HealthChange) {
let prev_health = i64::from(self.current);
self.current = (((self.current() + change.amount).clamp(0.0, f32::from(Self::MAX_HEALTH)) self.current = (((self.current() + change.amount).clamp(0.0, f32::from(Self::MAX_HEALTH))
* Self::SCALING_FACTOR_FLOAT) as u32) * Self::SCALING_FACTOR_FLOAT) as u32)
.min(self.maximum); .min(self.maximum);
self.last_change = (0.0, change); let delta = i64::from(self.current) - prev_health;
self.last_change = change;
// If damage is applied by an entity, update the damage contributors
if delta < 0 {
if let Some(attacker) = change.by {
let entry = self
.damage_contributors
.entry(attacker)
.or_insert((0, change.time));
(*entry).0 += u64::try_from(-delta).unwrap_or(0);
(*entry).1 = change.time
}
// Prune any damage contributors who haven't contributed damage for over the
// threshold - this enforces a maximum period that an entity will receive EXP
// for a kill after they last damaged the killed entity.
const DAMAGE_CONTRIB_PRUNE_SECS: f64 = 600.0;
self.damage_contributors.retain(|_, (_, last_damage_time)| {
(change.time.0 - last_damage_time.0) < DAMAGE_CONTRIB_PRUNE_SECS
});
}
}
pub fn damage_contributions(&self) -> impl Iterator<Item = (&DamageContributor, &u64)> {
self.damage_contributors
.iter()
.map(|(damage_contrib, (damage, _))| (damage_contrib, damage))
} }
pub fn should_die(&self) -> bool { self.current == 0 } pub fn should_die(&self) -> bool { self.current == 0 }
@ -139,20 +184,20 @@ impl Health {
self.is_dead = false; self.is_dead = false;
} }
// Function only exists for plugin tests, do not use anywhere else #[cfg(test)]
// TODO: Remove somehow later
#[deprecated]
pub fn empty() -> Self { pub fn empty() -> Self {
Health { Health {
current: 0, current: 0,
base_max: 0, base_max: 0,
maximum: 0, maximum: 0,
last_change: (0.0, HealthChange { last_change: HealthChange {
amount: 0.0, amount: 0.0,
by: None, by: None,
cause: None, cause: None,
}), time: Time(0.0),
},
is_dead: false, is_dead: false,
damage_contributors: HashMap::new(),
} }
} }
} }
@ -161,3 +206,123 @@ impl Health {
impl Component for Health { impl Component for Health {
type Storage = DerefFlaggedStorage<Self, IdvStorage<Self>>; type Storage = DerefFlaggedStorage<Self, IdvStorage<Self>>;
} }
#[cfg(test)]
mod tests {
use crate::{
combat::DamageContributor,
comp::{Health, HealthChange},
resources::Time,
uid::Uid,
};
#[test]
fn test_change_by_negative_health_change_adds_to_damage_contributors() {
let mut health = Health::empty();
health.current = 100 * Health::SCALING_FACTOR_INT;
health.maximum = health.current;
let damage_contrib = DamageContributor::Solo(Uid(0));
let health_change = HealthChange {
amount: -5.0,
time: Time(123.0),
by: Some(damage_contrib),
cause: None,
};
health.change_by(health_change);
let (damage, time) = health.damage_contributors.get(&damage_contrib).unwrap();
assert_eq!(
health_change.amount.abs() as u64 * Health::SCALING_FACTOR_INT as u64,
*damage
);
assert_eq!(health_change.time, *time);
}
#[test]
fn test_change_by_positive_health_change_does_not_add_damage_contributor() {
let mut health = Health::empty();
health.maximum = 100 * Health::SCALING_FACTOR_INT;
health.current = (health.maximum as f32 * 0.5) as u32;
let damage_contrib = DamageContributor::Solo(Uid(0));
let health_change = HealthChange {
amount: 20.0,
time: Time(123.0),
by: Some(damage_contrib),
cause: None,
};
health.change_by(health_change);
assert!(health.damage_contributors.is_empty());
}
#[test]
fn test_change_by_multiple_damage_from_same_damage_contributor() {
let mut health = Health::empty();
health.current = 100 * Health::SCALING_FACTOR_INT;
health.maximum = health.current;
let damage_contrib = DamageContributor::Solo(Uid(0));
let health_change = HealthChange {
amount: -5.0,
time: Time(123.0),
by: Some(damage_contrib),
cause: None,
};
health.change_by(health_change);
health.change_by(health_change);
let (damage, _) = health.damage_contributors.get(&damage_contrib).unwrap();
assert_eq!(
(health_change.amount.abs() * 2.0) as u64 * Health::SCALING_FACTOR_INT as u64,
*damage
);
assert_eq!(1, health.damage_contributors.len());
}
#[test]
fn test_change_by_damage_contributor_pruning() {
let mut health = Health::empty();
health.current = 100 * Health::SCALING_FACTOR_INT;
health.maximum = health.current;
let damage_contrib1 = DamageContributor::Solo(Uid(0));
let health_change = HealthChange {
amount: -5.0,
time: Time(10.0),
by: Some(damage_contrib1),
cause: None,
};
health.change_by(health_change);
let damage_contrib2 = DamageContributor::Solo(Uid(1));
let health_change = HealthChange {
amount: -5.0,
time: Time(100.0),
by: Some(damage_contrib2),
cause: None,
};
health.change_by(health_change);
assert!(health.damage_contributors.contains_key(&damage_contrib1));
assert!(health.damage_contributors.contains_key(&damage_contrib2));
// Apply damage 610 seconds after the damage from damage_contrib1 - this should
// result in the damage from damage_contrib1 being pruned.
let health_change = HealthChange {
amount: -5.0,
time: Time(620.0),
by: Some(damage_contrib2),
cause: None,
};
health.change_by(health_change);
assert!(!health.damage_contributors.contains_key(&damage_contrib1));
assert!(health.damage_contributors.contains_key(&damage_contrib2));
}
}

View File

@ -13,6 +13,7 @@
type_alias_impl_trait, type_alias_impl_trait,
extend_one extend_one
)] )]
#![feature(hash_drain_filter)]
/// Re-exported crates /// Re-exported crates
#[cfg(not(target_arch = "wasm32"))] #[cfg(not(target_arch = "wasm32"))]

View File

@ -9,7 +9,7 @@ use specs::Entity;
pub struct TimeOfDay(pub f64); pub struct TimeOfDay(pub f64);
/// A resource that stores the tick (i.e: physics) time. /// A resource that stores the tick (i.e: physics) time.
#[derive(Copy, Clone, Debug, Default, Serialize, Deserialize)] #[derive(Copy, Clone, Debug, Default, Serialize, Deserialize, PartialEq)]
pub struct Time(pub f64); pub struct Time(pub f64);
/// A resource that stores the time since the previous tick. /// A resource that stores the time since the previous tick.

View File

@ -268,12 +268,16 @@ fn retrieve_action(
RetrieveError::EcsAccessError(EcsAccessError::EcsEntityNotFound(e)), RetrieveError::EcsAccessError(EcsAccessError::EcsEntityNotFound(e)),
)?; )?;
Ok(RetrieveResult::GetEntityHealth( Ok(RetrieveResult::GetEntityHealth(
*world.health.get(player).ok_or_else(|| { world
.health
.get(player)
.ok_or_else(|| {
RetrieveError::EcsAccessError(EcsAccessError::EcsComponentNotFound( RetrieveError::EcsAccessError(EcsAccessError::EcsComponentNotFound(
e, e,
"Health".to_owned(), "Health".to_owned(),
)) ))
})?, })?
.clone(),
)) ))
}, },
} }

View File

@ -210,6 +210,7 @@ impl<'a> System<'a> for Sys {
AttackerInfo { AttackerInfo {
entity, entity,
uid, uid,
group: read_data.groups.get(entity),
energy: read_data.energies.get(entity), energy: read_data.energies.get(entity),
combo: read_data.combos.get(entity), combo: read_data.combos.get(entity),
inventory: read_data.inventories.get(entity), inventory: read_data.inventories.get(entity),
@ -249,6 +250,7 @@ impl<'a> System<'a> for Sys {
attack_options, attack_options,
1.0, 1.0,
AttackSource::Beam, AttackSource::Beam,
*read_data.time,
|e| server_events.push(e), |e| server_events.push(e),
|o| outcomes.push(o), |o| outcomes.push(o),
); );

View File

@ -1,4 +1,5 @@
use common::{ use common::{
combat::DamageContributor,
comp::{ comp::{
body::{object, Body}, body::{object, Body},
buff::{ buff::{
@ -6,17 +7,19 @@ use common::{
Buffs, Buffs,
}, },
fluid_dynamics::{Fluid, LiquidKind}, fluid_dynamics::{Fluid, LiquidKind},
Health, HealthChange, Inventory, LightEmitter, ModifierKind, PhysicsState, Stats, Group, Health, HealthChange, Inventory, LightEmitter, ModifierKind, PhysicsState, Stats,
}, },
event::{EventBus, ServerEvent}, event::{EventBus, ServerEvent},
resources::DeltaTime, resources::{DeltaTime, Time},
terrain::SpriteKind, terrain::SpriteKind,
uid::UidAllocator,
Damage, DamageSource, Damage, DamageSource,
}; };
use common_ecs::{Job, Origin, Phase, System}; use common_ecs::{Job, Origin, Phase, System};
use hashbrown::HashMap; use hashbrown::HashMap;
use specs::{ use specs::{
shred::ResourceId, Entities, Join, Read, ReadStorage, SystemData, World, WriteStorage, saveload::MarkerAllocator, shred::ResourceId, Entities, Join, Read, ReadStorage, SystemData,
World, WriteStorage,
}; };
use std::time::Duration; use std::time::Duration;
@ -28,6 +31,9 @@ pub struct ReadData<'a> {
inventories: ReadStorage<'a, Inventory>, inventories: ReadStorage<'a, Inventory>,
healths: ReadStorage<'a, Health>, healths: ReadStorage<'a, Health>,
physics_states: ReadStorage<'a, PhysicsState>, physics_states: ReadStorage<'a, PhysicsState>,
groups: ReadStorage<'a, Group>,
uid_allocator: Read<'a, UidAllocator>,
time: Read<'a, Time>,
} }
#[derive(Default)] #[derive(Default)]
@ -209,9 +215,27 @@ impl<'a> System<'a> for Sys {
ModifierKind::Additive => *accumulated, ModifierKind::Additive => *accumulated,
ModifierKind::Fractional => health.maximum() * *accumulated, ModifierKind::Fractional => health.maximum() * *accumulated,
}; };
let damage_contributor = by
.map(|uid| {
read_data
.uid_allocator
.retrieve_entity_internal(uid.0)
.map(|entity| {
DamageContributor::new(
uid,
read_data.groups.get(entity).cloned(),
)
})
})
.flatten();
server_emitter.emit(ServerEvent::HealthChange { server_emitter.emit(ServerEvent::HealthChange {
entity, entity,
change: HealthChange { amount, by, cause }, change: HealthChange {
amount,
by: damage_contributor,
cause,
time: *read_data.time,
},
}); });
*accumulated = 0.0; *accumulated = 0.0;
}; };

View File

@ -149,6 +149,7 @@ impl<'a> System<'a> for Sys {
let attacker_info = Some(AttackerInfo { let attacker_info = Some(AttackerInfo {
entity: attacker, entity: attacker,
uid: *uid, uid: *uid,
group: read_data.groups.get(attacker),
energy: read_data.energies.get(attacker), energy: read_data.energies.get(attacker),
combo: read_data.combos.get(attacker), combo: read_data.combos.get(attacker),
inventory: read_data.inventories.get(attacker), inventory: read_data.inventories.get(attacker),
@ -187,6 +188,7 @@ impl<'a> System<'a> for Sys {
attack_options, attack_options,
1.0, 1.0,
AttackSource::Melee, AttackSource::Melee,
*read_data.time,
|e| server_emitter.emit(e), |e| server_emitter.emit(e),
|o| outcomes.push(o), |o| outcomes.push(o),
); );

View File

@ -264,6 +264,7 @@ fn dispatch_hit(
.map(|(entity, uid)| AttackerInfo { .map(|(entity, uid)| AttackerInfo {
entity, entity,
uid, uid,
group: read_data.groups.get(entity),
energy: read_data.energies.get(entity), energy: read_data.energies.get(entity),
combo: read_data.combos.get(entity), combo: read_data.combos.get(entity),
inventory: read_data.inventories.get(entity), inventory: read_data.inventories.get(entity),
@ -318,6 +319,7 @@ fn dispatch_hit(
attack_options, attack_options,
1.0, 1.0,
AttackSource::Projectile, AttackSource::Projectile,
*read_data.time,
|e| server_emitter.emit(e), |e| server_emitter.emit(e),
|o| outcomes.push(o), |o| outcomes.push(o),
); );

View File

@ -192,6 +192,7 @@ impl<'a> System<'a> for Sys {
.map(|(entity, uid)| AttackerInfo { .map(|(entity, uid)| AttackerInfo {
entity, entity,
uid, uid,
group: read_data.groups.get(entity),
energy: read_data.energies.get(entity), energy: read_data.energies.get(entity),
combo: read_data.combos.get(entity), combo: read_data.combos.get(entity),
inventory: read_data.inventories.get(entity), inventory: read_data.inventories.get(entity),
@ -230,6 +231,7 @@ impl<'a> System<'a> for Sys {
attack_options, attack_options,
1.0, 1.0,
AttackSource::Shockwave, AttackSource::Shockwave,
*read_data.time,
|e| server_emitter.emit(e), |e| server_emitter.emit(e),
|o| outcomes.push(o), |o| outcomes.push(o),
); );

View File

@ -73,13 +73,6 @@ impl<'a> System<'a> for Sys {
let mut server_event_emitter = read_data.server_bus.emitter(); let mut server_event_emitter = read_data.server_bus.emitter();
let dt = read_data.dt.0; let dt = read_data.dt.0;
// Increment last change timer
healths.set_event_emission(false); // avoid unnecessary syncing
for mut health in (&mut healths).join() {
health.last_change.0 += f64::from(dt);
}
healths.set_event_emission(true);
// Update stats // Update stats
for (entity, uid, stats, mut skill_set, mut health, pos, mut energy, inventory) in ( for (entity, uid, stats, mut skill_set, mut health, pos, mut energy, inventory) in (
&read_data.entities, &read_data.entities,
@ -100,7 +93,7 @@ impl<'a> System<'a> for Sys {
entities_died_last_tick.0.push(cloned_entity); entities_died_last_tick.0.push(cloned_entity);
server_event_emitter.emit(ServerEvent::Destroy { server_event_emitter.emit(ServerEvent::Destroy {
entity, entity,
cause: health.last_change.1, cause: health.last_change,
}); });
health.is_dead = true; health.is_dead = true;

View File

@ -50,9 +50,11 @@ pub enum Action {
/// ``` /// ```
/// Over this one: /// Over this one:
/// ```rust /// ```rust
/// # use common::comp::Body;
/// # use common::comp::body::humanoid;
/// # use veloren_plugin_api::*; /// # use veloren_plugin_api::*;
/// # let entityid = Uid(0); /// # let entityid = Uid(0);
/// # fn retrieve_action(r: &Retrieve) -> Result<RetrieveResult, RetrieveError> { Ok(RetrieveResult::GetEntityHealth(Health::empty()))} /// # fn retrieve_action(r: &Retrieve) -> Result<RetrieveResult, RetrieveError> { Ok(RetrieveResult::GetEntityHealth(Health::new(Body::Humanoid(humanoid::Body::random()), 1))) }
/// let life = if let RetrieveResult::GetEntityHealth(e) = /// let life = if let RetrieveResult::GetEntityHealth(e) =
/// retrieve_action(&Retrieve::GetEntityHealth(entityid)).unwrap() /// retrieve_action(&Retrieve::GetEntityHealth(entityid)).unwrap()
/// { /// {
@ -75,9 +77,11 @@ pub enum Retrieve {
/// ///
/// Example: /// Example:
/// ```rust /// ```rust
/// # use common::comp::Body;
/// # use common::comp::body::humanoid;
/// # use veloren_plugin_api::*; /// # use veloren_plugin_api::*;
/// # let entityid = Uid(0); /// # let entityid = Uid(0);
/// # fn retrieve_action(r: &Retrieve) -> Result<RetrieveResult, RetrieveError> { Ok(RetrieveResult::GetEntityHealth(Health::empty()))} /// # fn retrieve_action(r: &Retrieve) -> Result<RetrieveResult, RetrieveError> { Ok(RetrieveResult::GetEntityHealth(Health::new(Body::Humanoid(humanoid::Body::random()), 1)))}
/// let life = if let RetrieveResult::GetEntityHealth(e) = /// let life = if let RetrieveResult::GetEntityHealth(e) =
/// retrieve_action(&Retrieve::GetEntityHealth(entityid)).unwrap() /// retrieve_action(&Retrieve::GetEntityHealth(entityid)).unwrap()
/// { /// {

View File

@ -1021,10 +1021,12 @@ fn handle_health(
.write_storage::<comp::Health>() .write_storage::<comp::Health>()
.get_mut(target) .get_mut(target)
{ {
let time = server.state.ecs().read_resource::<Time>();
let change = comp::HealthChange { let change = comp::HealthChange {
amount: hp - health.current(), amount: hp - health.current(),
by: None, by: None,
cause: None, cause: None,
time: *time,
}; };
health.change_by(change); health.change_by(change);
Ok(()) Ok(())

View File

@ -11,6 +11,7 @@ use crate::{
}; };
use common::{ use common::{
combat, combat,
combat::DamageContributor,
comp::{ comp::{
self, aura, buff, self, aura, buff,
chat::{KillSource, KillType}, chat::{KillSource, KillType},
@ -33,10 +34,20 @@ use common_state::BlockChange;
use comp::chat::GenericChatMsg; use comp::chat::GenericChatMsg;
use hashbrown::HashSet; use hashbrown::HashSet;
use rand::Rng; use rand::Rng;
use specs::{join::Join, saveload::MarkerAllocator, Builder, Entity as EcsEntity, WorldExt}; use specs::{
use tracing::error; join::Join, saveload::MarkerAllocator, Builder, Entity as EcsEntity, Entity, WorldExt,
};
use std::{collections::HashMap, iter};
use tracing::{debug, error};
use vek::{Vec2, Vec3}; use vek::{Vec2, Vec3};
#[derive(Hash, Eq, PartialEq)]
enum DamageContrib {
Solo(EcsEntity),
Group(Group),
NotFound,
}
pub fn handle_poise(server: &Server, entity: EcsEntity, change: f32, knockback_dir: Vec3<f32>) { pub fn handle_poise(server: &Server, entity: EcsEntity, change: f32, knockback_dir: Vec3<f32>) {
let ecs = &server.state.ecs(); let ecs = &server.state.ecs();
if let Some(character_state) = ecs.read_storage::<CharacterState>().get(entity) { if let Some(character_state) = ecs.read_storage::<CharacterState>().get(entity) {
@ -153,7 +164,7 @@ pub fn handle_destroy(server: &mut Server, entity: EcsEntity, last_change: Healt
// If it was a player that died // If it was a player that died
if let Some(_player) = state.ecs().read_storage::<Player>().get(entity) { if let Some(_player) = state.ecs().read_storage::<Player>().get(entity) {
if let Some(uid) = state.ecs().read_storage::<Uid>().get(entity) { if let Some(uid) = state.ecs().read_storage::<Uid>().get(entity) {
let kill_source = match (last_change.cause, last_change.by) { let kill_source = match (last_change.cause, last_change.by.map(|x| x.uid())) {
(Some(DamageSource::Melee), Some(by)) => get_attacker_name(KillType::Melee, by), (Some(DamageSource::Melee), Some(by)) => get_attacker_name(KillType::Melee, by),
(Some(DamageSource::Projectile), Some(by)) => { (Some(DamageSource::Projectile), Some(by)) => {
get_attacker_name(KillType::Projectile, by) get_attacker_name(KillType::Projectile, by)
@ -178,25 +189,20 @@ pub fn handle_destroy(server: &mut Server, entity: EcsEntity, last_change: Healt
} }
} }
// Give EXP to the killer if entity had stats // Award EXP to damage contributors
//
// NOTE: Debug logging is disabled by default for this module - to enable it add
// veloren_server::events::entity_manipulation=debug to RUST_LOG
(|| { (|| {
let mut skill_set = state.ecs().write_storage::<SkillSet>(); let mut skill_sets = state.ecs().write_storage::<SkillSet>();
let healths = state.ecs().read_storage::<Health>(); let healths = state.ecs().read_storage::<Health>();
let energies = state.ecs().read_storage::<Energy>(); let energies = state.ecs().read_storage::<Energy>();
let inventories = state.ecs().read_storage::<Inventory>(); let inventories = state.ecs().read_storage::<Inventory>();
let players = state.ecs().read_storage::<Player>(); let players = state.ecs().read_storage::<Player>();
let bodies = state.ecs().read_storage::<Body>(); let bodies = state.ecs().read_storage::<Body>();
let poises = state.ecs().read_storage::<comp::Poise>(); let poises = state.ecs().read_storage::<comp::Poise>();
let by = if let Some(by) = last_change.by { let positions = state.ecs().read_storage::<Pos>();
by let groups = state.ecs().read_storage::<Group>();
} else {
return;
};
let attacker = if let Some(attacker) = state.ecs().entity_from_uid(by.into()) {
attacker
} else {
return;
};
let ( let (
entity_skill_set, entity_skill_set,
@ -205,37 +211,25 @@ pub fn handle_destroy(server: &mut Server, entity: EcsEntity, last_change: Healt
entity_inventory, entity_inventory,
entity_body, entity_body,
entity_poise, entity_poise,
entity_pos,
) = match (|| { ) = match (|| {
Some(( Some((
skill_set.get(entity)?, skill_sets.get(entity)?,
healths.get(entity)?, healths.get(entity)?,
energies.get(entity)?, energies.get(entity)?,
inventories.get(entity)?, inventories.get(entity)?,
bodies.get(entity)?, bodies.get(entity)?,
poises.get(entity)?, poises.get(entity)?,
positions.get(entity)?,
)) ))
})() { })() {
Some(comps) => comps, Some(comps) => comps,
None => return, None => return,
}; };
let groups = state.ecs().read_storage::<Group>(); // Calculate the total EXP award for the kill
let attacker_group = groups.get(attacker);
let destroyed_group = groups.get(entity);
// Don't give exp if attacker destroyed themselves or one of their group
// members, or a pvp kill
if (attacker_group.is_some() && attacker_group == destroyed_group)
|| attacker == entity
|| (players.get(entity).is_some() && players.get(attacker).is_some())
{
return;
}
// Maximum distance for other group members to receive exp
const MAX_EXP_DIST: f32 = 150.0;
// TODO: Scale xp from skillset rather than health, when NPCs have their own
// skillsets
let msm = state.ecs().read_resource::<MaterialStatManifest>(); let msm = state.ecs().read_resource::<MaterialStatManifest>();
let mut exp_reward = combat::combat_rating( let exp_reward = combat::combat_rating(
entity_inventory, entity_inventory,
entity_health, entity_health,
entity_energy, entity_energy,
@ -245,16 +239,88 @@ pub fn handle_destroy(server: &mut Server, entity: EcsEntity, last_change: Healt
&msm, &msm,
) * 20.0; ) * 20.0;
// Distribute EXP to group let mut damage_contributors = HashMap::<DamageContrib, (u64, f32)>::new();
let positions = state.ecs().read_storage::<Pos>(); for (damage_contributor, damage) in entity_health.damage_contributions() {
match damage_contributor {
DamageContributor::Solo(uid) => {
if let Some(attacker) = state.ecs().entity_from_uid(uid.0) {
damage_contributors.insert(DamageContrib::Solo(attacker), (*damage, 0.0));
} else {
// An entity who was not in a group contributed damage but is now either
// dead or offline. Add a placeholder to ensure that the contributor's exp
// is discarded, not distributed between the other contributors
damage_contributors.insert(DamageContrib::NotFound, (*damage, 0.0));
}
},
DamageContributor::Group {
entity_uid: _,
group,
} => {
// Damage made by entities who were in a group at the time of attack is
// attributed to their group rather than themselves. This allows for all
// members of a group to receive EXP, not just the damage dealers.
let entry = damage_contributors
.entry(DamageContrib::Group(*group))
.or_insert((0, 0.0));
(*entry).0 += damage;
},
}
}
// Calculate the percentage of total damage that each DamageContributor
// contributed
let total_damage: f64 = damage_contributors
.values()
.map(|(damage, _)| *damage as f64)
.sum();
damage_contributors
.iter_mut()
.for_each(|(_, (damage, percentage))| {
*percentage = (*damage as f64 / total_damage) as f32
});
let alignments = state.ecs().read_storage::<Alignment>(); let alignments = state.ecs().read_storage::<Alignment>();
let uids = state.ecs().read_storage::<Uid>(); let uids = state.ecs().read_storage::<Uid>();
let mut outcomes = state.ecs().write_resource::<Vec<Outcome>>(); let mut outcomes = state.ecs().write_resource::<Vec<Outcome>>();
let inventories = state.ecs().read_storage::<comp::Inventory>(); let inventories = state.ecs().read_storage::<comp::Inventory>();
if let (Some(attacker_group), Some(pos)) = (attacker_group, positions.get(entity)) {
// TODO: rework if change to groups makes it easier to iterate entities in a let destroyed_group = groups.get(entity);
// group
let mut non_pet_group_members_in_range = 1; let within_range = |attacker_pos: &Pos| {
// Maximum distance that an attacker must be from an entity at the time of its
// death to receive EXP for the kill
const MAX_EXP_DIST: f32 = 150.0;
entity_pos.0.distance_squared(attacker_pos.0) < MAX_EXP_DIST.powi(2)
};
let is_pvp_kill =
|attacker: Entity| players.get(entity).is_some() && players.get(attacker).is_some();
// Iterate through all contributors of damage for the killed entity, calculating
// how much EXP each contributor should be awarded based on their
// percentage of damage contribution
damage_contributors.iter().filter_map(|(damage_contributor, (_, damage_percent))| {
let contributor_exp = exp_reward * damage_percent;
match damage_contributor {
DamageContrib::Solo(attacker) => {
// No exp for self kills or PvP
if *attacker == entity || is_pvp_kill(*attacker) { return None; }
// Only give EXP to the attacker if they are within EXP range of the killed entity
positions.get(*attacker).and_then(|attacker_pos| {
if within_range(attacker_pos) {
debug!("Awarding {} exp to individual {:?} who contributed {}% damage to the kill of {:?}", contributor_exp, attacker, *damage_percent * 100.0, entity);
Some(iter::once((*attacker, contributor_exp)).collect())
} else {
None
}
})
},
DamageContrib::Group(group) => {
// Don't give EXP to members in the destroyed entity's group
if destroyed_group == Some(group) { return None; }
// Only give EXP to members of the group that are within EXP range of the killed entity and aren't a pet
let members_in_range = ( let members_in_range = (
&state.ecs().entities(), &state.ecs().entities(),
&groups, &groups,
@ -263,37 +329,35 @@ pub fn handle_destroy(server: &mut Server, entity: EcsEntity, last_change: Healt
&uids, &uids,
) )
.join() .join()
.filter(|(entity, group, member_pos, _, _)| { .filter_map(|(member_entity, member_group, member_pos, alignment, uid)| {
// Check if: in group, not main attacker, and in range if *member_group == *group && within_range(member_pos) && !is_pvp_kill(member_entity) && !matches!(alignment, Some(Alignment::Owned(owner)) if owner != uid) {
*group == attacker_group Some(member_entity)
&& *entity != attacker } else {
&& pos.0.distance_squared(member_pos.0) < MAX_EXP_DIST.powi(2) None
})
.map(|(entity, _, _, alignment, uid)| {
if !matches!(alignment, Some(Alignment::Owned(owner)) if owner != uid) {
non_pet_group_members_in_range += 1;
} }
(entity, uid)
}) })
.collect::<Vec<_>>(); .collect::<Vec<_>>();
// Divides exp reward by square root of number of people in group
exp_reward /= (non_pet_group_members_in_range as f32).sqrt();
members_in_range.into_iter().for_each(|(e, uid)| {
if let (Some(inventory), Some(mut skill_set)) =
(inventories.get(e), skill_set.get_mut(e))
{
handle_exp_gain(exp_reward, inventory, &mut skill_set, uid, &mut outcomes);
}
});
}
if members_in_range.is_empty() { return None; }
// Divide EXP reward by square root of number of people in group for group EXP scaling
let exp_per_member = contributor_exp / (members_in_range.len() as f32).sqrt();
debug!("Awarding {} exp per member of group ID {:?} with {} members which contributed {}% damage to the kill of {:?}", exp_per_member, group, members_in_range.len(), *damage_percent * 100.0, entity);
Some(members_in_range.into_iter().map(|entity| (entity, exp_per_member)).collect::<Vec<(Entity, f32)>>())
},
DamageContrib::NotFound => {
// Discard exp for dead/offline individual damage contributors
None
}
}
}).flatten().for_each(|(attacker, exp_reward)| {
// Process the calculated EXP rewards
if let (Some(mut attacker_skill_set), Some(attacker_uid), Some(attacker_inventory)) = ( if let (Some(mut attacker_skill_set), Some(attacker_uid), Some(attacker_inventory)) = (
skill_set.get_mut(attacker), skill_sets.get_mut(attacker),
uids.get(attacker), uids.get(attacker),
inventories.get(attacker), inventories.get(attacker),
) { ) {
// TODO: Discuss whether we should give EXP by Player Killing or not.
handle_exp_gain( handle_exp_gain(
exp_reward, exp_reward,
attacker_inventory, attacker_inventory,
@ -302,6 +366,7 @@ pub fn handle_destroy(server: &mut Server, entity: EcsEntity, last_change: Healt
&mut outcomes, &mut outcomes,
); );
} }
});
})(); })();
let should_delete = if state let should_delete = if state
@ -458,7 +523,9 @@ pub fn handle_land_on_ground(server: &Server, entity: EcsEntity, vel: Vec3<f32>)
stats.get(entity), stats.get(entity),
Some(DamageKind::Crushing), Some(DamageKind::Crushing),
); );
let change = damage.calculate_health_change(damage_reduction, None, false, 0.0, 1.0); let time = server.state.ecs().read_resource::<Time>();
let change =
damage.calculate_health_change(damage_reduction, None, false, 0.0, 1.0, *time);
health.change_by(change); health.change_by(change);
} }
// Handle poise change // Handle poise change
@ -774,6 +841,7 @@ pub fn handle_explosion(server: &Server, pos: Vec3<f32>, explosion: Explosion, o
.map(|(entity, uid)| combat::AttackerInfo { .map(|(entity, uid)| combat::AttackerInfo {
entity, entity,
uid, uid,
group: groups.get(entity),
energy: energies.get(entity), energy: energies.get(entity),
combo: combos.get(entity), combo: combos.get(entity),
inventory: inventories.get(entity), inventory: inventories.get(entity),
@ -806,6 +874,7 @@ pub fn handle_explosion(server: &Server, pos: Vec3<f32>, explosion: Explosion, o
target_group, target_group,
}; };
let time = server.state.ecs().read_resource::<Time>();
attack.apply_attack( attack.apply_attack(
attacker_info, attacker_info,
target_info, target_info,
@ -813,6 +882,7 @@ pub fn handle_explosion(server: &Server, pos: Vec3<f32>, explosion: Explosion, o
attack_options, attack_options,
strength, strength,
combat::AttackSource::Explosion, combat::AttackSource::Explosion,
*time,
|e| emitter.emit(e), |e| emitter.emit(e),
|o| outcomes.push(o), |o| outcomes.push(o),
); );

View File

@ -10,13 +10,14 @@ use crate::{
use common::{ use common::{
character::CharacterId, character::CharacterId,
combat, combat,
combat::DamageContributor,
comp::{ comp::{
self, self,
skills::{GeneralSkill, Skill}, skills::{GeneralSkill, Skill},
Group, Inventory, Poise, Group, Inventory, Poise,
}, },
effect::Effect, effect::Effect,
resources::TimeOfDay, resources::{Time, TimeOfDay},
slowjob::SlowJobPool, slowjob::SlowJobPool,
uid::{Uid, UidAllocator}, uid::{Uid, UidAllocator},
}; };
@ -127,16 +128,27 @@ impl StateExt for State {
Effect::Damage(damage) => { Effect::Damage(damage) => {
let inventories = self.ecs().read_storage::<Inventory>(); let inventories = self.ecs().read_storage::<Inventory>();
let stats = self.ecs().read_storage::<comp::Stats>(); let stats = self.ecs().read_storage::<comp::Stats>();
let groups = self.ecs().read_storage::<comp::Group>();
let damage_contributor = source
.map(|uid| {
self.ecs().entity_from_uid(uid.0).map(|attacker_entity| {
DamageContributor::new(uid, groups.get(attacker_entity).cloned())
})
})
.flatten();
let time = self.ecs().read_resource::<Time>();
let change = damage.calculate_health_change( let change = damage.calculate_health_change(
combat::Damage::compute_damage_reduction( combat::Damage::compute_damage_reduction(
inventories.get(entity), inventories.get(entity),
stats.get(entity), stats.get(entity),
Some(damage.kind), Some(damage.kind),
), ),
source, damage_contributor,
false, false,
0.0, 0.0,
1.0, 1.0,
*time,
); );
self.ecs() self.ecs()
.write_storage::<comp::Health>() .write_storage::<comp::Health>()

View File

@ -473,10 +473,13 @@ impl<'a> System<'a> for Sys {
// Target an entity that's attacking us if the attack // Target an entity that's attacking us if the attack
// was recent and we have a health component // was recent and we have a health component
match health { match health {
Some(health) if health.last_change.0 < DAMAGE_MEMORY_DURATION => { Some(health)
if let Some(by) = health.last_change.1.damage_by() { if read_data.time.0 - health.last_change.time.0
< DAMAGE_MEMORY_DURATION =>
{
if let Some(by) = health.last_change.damage_by() {
if let Some(attacker) = if let Some(attacker) =
read_data.uid_allocator.retrieve_entity_internal(by.id()) read_data.uid_allocator.retrieve_entity_internal(by.uid().0)
{ {
// If the target is dead or in a safezone, remove the // If the target is dead or in a safezone, remove the
// target and idle. // target and idle.
@ -550,7 +553,7 @@ impl<'a> System<'a> for Sys {
} }
} }
} }
}, }
_ => {}, _ => {},
} }
@ -1581,17 +1584,19 @@ impl<'a> AgentData<'a> {
let guard_duty = |e_health: &Health, e_alignment: Option<&Alignment>| { let guard_duty = |e_health: &Health, e_alignment: Option<&Alignment>| {
// I'm a guard and a villager is in distress // I'm a guard and a villager is in distress
let other_is_npc = matches!(e_alignment, Some(Alignment::Npc)); let other_is_npc = matches!(e_alignment, Some(Alignment::Npc));
let remembers_damage = e_health.last_change.0 < 5.0; let remembers_damage = read_data.time.0 - e_health.last_change.time.0 < 5.0;
let need_help = read_data.stats.get(*self.entity).map_or(false, |stats| { let need_help = read_data.stats.get(*self.entity).map_or(false, |stats| {
stats.name == "Guard" && other_is_npc && remembers_damage stats.name == "Guard" && other_is_npc && remembers_damage
}); });
let attacker_of = |health: &Health| health.last_change.1.damage_by(); let attacker_of = |health: &Health| health.last_change.damage_by();
need_help need_help
.then(|| { .then(|| {
attacker_of(e_health) attacker_of(e_health)
.and_then(|attacker_uid| get_entity_by_id(attacker_uid.id(), read_data)) .and_then(|damage_contributor| {
get_entity_by_id(damage_contributor.uid().0, read_data)
})
.and_then(|attacker| { .and_then(|attacker| {
read_data read_data
.positions .positions
@ -4217,8 +4222,8 @@ impl<'a> AgentData<'a> {
) { ) {
if let Some(Target { target, .. }) = agent.target { if let Some(Target { target, .. }) = agent.target {
if let Some(tgt_health) = read_data.healths.get(target) { if let Some(tgt_health) = read_data.healths.get(target) {
if let Some(by) = tgt_health.last_change.1.damage_by() { if let Some(by) = tgt_health.last_change.damage_by() {
if let Some(attacker) = get_entity_by_id(by.id(), read_data) { if let Some(attacker) = get_entity_by_id(by.uid().0, read_data) {
if agent.target.is_none() { if agent.target.is_none() {
controller.push_event(ControlEvent::Utterance(UtteranceKind::Angry)); controller.push_event(ControlEvent::Utterance(UtteranceKind::Angry));
} }
@ -4483,7 +4488,8 @@ fn decrement_awareness(agent: &mut Agent) {
fn entity_was_attacked(entity: EcsEntity, read_data: &ReadData) -> bool { fn entity_was_attacked(entity: EcsEntity, read_data: &ReadData) -> bool {
if let Some(entity_health) = read_data.healths.get(entity) { if let Some(entity_health) = read_data.healths.get(entity) {
entity_health.last_change.0 < 5.0 && entity_health.last_change.1.amount < 0.0 read_data.time.0 - entity_health.last_change.time.0 < 5.0
&& entity_health.last_change.amount < 0.0
} else { } else {
false false
} }

View File

@ -54,7 +54,7 @@ use hashbrown::HashMap;
use rand::{prelude::SliceRandom, thread_rng, Rng}; use rand::{prelude::SliceRandom, thread_rng, Rng};
use serde::Deserialize; use serde::Deserialize;
use std::time::Instant; use std::time::Instant;
use tracing::{debug, warn}; use tracing::{debug, trace, warn};
/// Collection of all the tracks /// Collection of all the tracks
#[derive(Debug, Deserialize)] #[derive(Debug, Deserialize)]
@ -289,9 +289,10 @@ impl MusicMgr {
if interrupt { if interrupt {
self.last_interrupt = Instant::now(); self.last_interrupt = Instant::now();
} }
debug!( trace!(
"pre-play_random_track: {:?} {:?}", "pre-play_random_track: {:?} {:?}",
self.last_activity, music_state self.last_activity,
music_state
); );
if let Ok(next_activity) = self.play_random_track(audio, state, client, &music_state) { if let Ok(next_activity) = self.play_random_track(audio, state, client, &music_state) {
self.last_activity = next_activity; self.last_activity = next_activity;

View File

@ -71,7 +71,7 @@ impl<'a> System<'a> for Sys {
// would just be a transient glitch in the display of these damage numbers) // would just be a transient glitch in the display of these damage numbers)
// (maybe health changes could be sent to the client as a list // (maybe health changes could be sent to the client as a list
// of events) // of events)
if match health.last_change.1.by { if match health.last_change.by.map(|x| x.uid()) {
// HealthSource::Damage { by: Some(by), .. } // HealthSource::Damage { by: Some(by), .. }
// | HealthSource::Heal { by: Some(by) } => { // | HealthSource::Heal { by: Some(by) } => {
// let by_me = my_uid.map_or(false, |&uid| by == uid); // let by_me = my_uid.map_or(false, |&uid| by == uid);
@ -101,12 +101,12 @@ impl<'a> System<'a> for Sys {
match last_floater { match last_floater {
Some(f) if f.timer < HP_ACCUMULATETIME => { Some(f) if f.timer < HP_ACCUMULATETIME => {
//TODO: Add "jumping" animation on floater when it changes its value //TODO: Add "jumping" animation on floater when it changes its value
f.hp_change += health.last_change.1.amount; f.hp_change += health.last_change.amount;
}, },
_ => { _ => {
hp_floater_list.floaters.push(HpFloater { hp_floater_list.floaters.push(HpFloater {
timer: 0.0, timer: 0.0,
hp_change: health.last_change.1.amount, hp_change: health.last_change.amount,
rand: rand::random(), rand: rand::random(),
}); });
}, },

View File

@ -35,7 +35,7 @@ use common::{
Body, CharacterState, Collider, Controller, Health, Inventory, Item, Last, LightAnimation, Body, CharacterState, Collider, Controller, Health, Inventory, Item, Last, LightAnimation,
LightEmitter, Mounting, Ori, PhysicsState, PoiseState, Pos, Scale, Vel, LightEmitter, Mounting, Ori, PhysicsState, PoiseState, Pos, Scale, Vel,
}, },
resources::DeltaTime, resources::{DeltaTime, Time},
states::{equipping, idle, utils::StageSection, wielding}, states::{equipping, idle, utils::StageSection, wielding},
terrain::TerrainChunk, terrain::TerrainChunk,
uid::UidAllocator, uid::UidAllocator,
@ -752,9 +752,11 @@ impl FigureMgr {
// Change in health as color! // Change in health as color!
let col = health let col = health
.map(|h| { .map(|h| {
let time = scene_data.state.ecs().read_resource::<Time>();
let time_since_health_change = time.0 - h.last_change.time.0;
vek::Rgba::broadcast(1.0) vek::Rgba::broadcast(1.0)
+ vek::Rgba::new(10.0, 10.0, 10.0, 0.0).map(|c| { + vek::Rgba::new(10.0, 10.0, 10.0, 0.0).map(|c| {
(c / (1.0 + DAMAGE_FADE_COEFFICIENT * h.last_change.0)) as f32 (c / (1.0 + DAMAGE_FADE_COEFFICIENT * time_since_health_change)) as f32
}) })
}) })
.unwrap_or_else(|| vek::Rgba::broadcast(1.0)) .unwrap_or_else(|| vek::Rgba::broadcast(1.0))