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
- Agents using fireball projectiles aim at the feet instead of the eyes
- Explosions can now have a nonzero minimum falloff
- EXP on kill is now shared based on damage contribution
### Removed

View File

@ -53,6 +53,11 @@ where
.add_directive("veloren_common::trade=info".parse().unwrap())
.add_directive("veloren_world::sim=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("prometheus_hyper=info".parse().unwrap())
.add_directive("mio::pool=info".parse().unwrap())

View File

@ -1,6 +1,7 @@
use crate::sync;
use common::comp;
use common::{comp, resources::Time};
use serde::{Deserialize, Serialize};
use specs::WorldExt;
use std::marker::PhantomData;
use sum_type::sum_type;
@ -92,7 +93,12 @@ impl sync::CompPacket for EcsCompPacket {
EcsCompPacket::Auras(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::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::LightEmitter(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::Energy(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::LightEmitter(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 crate::{comp::Group, resources::Time};
#[cfg(not(target_arch = "wasm32"))]
use specs::{saveload::MarkerAllocator, Entity as EcsEntity, ReadStorage};
#[cfg(not(target_arch = "wasm32"))]
@ -53,6 +54,7 @@ pub enum AttackSource {
pub struct AttackerInfo<'a> {
pub entity: EcsEntity,
pub uid: Uid,
pub group: Option<&'a Group>,
pub energy: Option<&'a Energy>,
pub combo: Option<&'a Combo>,
pub inventory: Option<&'a Inventory>,
@ -180,6 +182,7 @@ impl Attack {
// maybe look into modifying strength of other effects?
strength_modifier: f32,
attack_source: AttackSource,
time: Time,
mut emit: impl FnMut(ServerEvent),
mut emit_outcome: impl FnMut(Outcome),
) -> bool {
@ -222,10 +225,11 @@ impl Attack {
);
let change = damage.damage.calculate_health_change(
damage_reduction,
attacker.map(|a| a.uid),
attacker.map(|x| x.into()),
is_crit,
self.crit_multiplier,
strength_modifier,
time,
);
let applied_damage = -change.amount;
accumulated_damage += applied_damage;
@ -273,8 +277,9 @@ impl Attack {
if let Some(attacker_entity) = attacker.map(|a| a.entity) {
let change = HealthChange {
amount: applied_damage * l,
by: attacker.map(|a| a.uid),
by: attacker.map(|a| a.into()),
cause: None,
time,
};
if change.amount.abs() > Health::HEALTH_EPSILON {
emit(ServerEvent::HealthChange {
@ -298,8 +303,9 @@ impl Attack {
CombatEffect::Heal(h) => {
let change = HealthChange {
amount: *h * strength_modifier,
by: attacker.map(|a| a.uid),
by: attacker.map(|a| a.into()),
cause: None,
time,
};
if change.amount.abs() > Health::HEALTH_EPSILON {
emit(ServerEvent::HealthChange {
@ -408,8 +414,9 @@ impl Attack {
if let Some(attacker_entity) = attacker.map(|a| a.entity) {
let change = HealthChange {
amount: accumulated_damage * l,
by: attacker.map(|a| a.uid),
by: attacker.map(|a| a.into()),
cause: None,
time,
};
if change.amount.abs() > Health::HEALTH_EPSILON {
emit(ServerEvent::HealthChange {
@ -433,8 +440,9 @@ impl Attack {
CombatEffect::Heal(h) => {
let change = HealthChange {
amount: h * strength_modifier,
by: attacker.map(|a| a.uid),
by: attacker.map(|a| a.into()),
cause: None,
time,
};
if change.amount.abs() > Health::HEALTH_EPSILON {
emit(ServerEvent::HealthChange {
@ -588,6 +596,41 @@ pub enum CombatRequirement {
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)]
pub enum DamageSource {
Buff(BuffKind),
@ -673,10 +716,11 @@ impl Damage {
pub fn calculate_health_change(
self,
damage_reduction: f32,
uid: Option<Uid>,
damage_contributor: Option<DamageContributor>,
is_crit: bool,
crit_mult: f32,
damage_modifier: f32,
time: Time,
) -> HealthChange {
let mut damage = self.value * damage_modifier;
let critdamage = if is_crit {
@ -697,8 +741,9 @@ impl Damage {
HealthChange {
amount: -damage,
by: uid,
by: damage_contributor,
cause: Some(self.source),
time,
}
},
DamageSource::Falling => {
@ -710,12 +755,14 @@ impl Damage {
amount: -damage,
by: None,
cause: Some(self.source),
time,
}
},
DamageSource::Buff(_) | DamageSource::Other => HealthChange {
amount: -damage,
by: None,
cause: Some(self.source),
time,
},
}
}
@ -913,22 +960,22 @@ pub fn combat_rating(
const SKILLS_WEIGHT: f32 = 1.0;
const POISE_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()
/ 100.0
/ (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
let energy_rating = (energy.base_max() + compute_max_energy_mod(Some(inventory))) / 100.0
* 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
/ 100.0
/ (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;
// 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
// 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);
// TODO: Hack

View File

@ -1,8 +1,11 @@
#[cfg(not(target_arch = "wasm32"))]
use crate::comp;
use crate::{uid::Uid, DamageSource};
use crate::DamageSource;
use hashbrown::HashMap;
use serde::{Deserialize, Serialize};
use std::convert::TryFrom;
use crate::{combat::DamageContributor, resources::Time};
#[cfg(not(target_arch = "wasm32"))]
use specs::{Component, DerefFlaggedStorage};
#[cfg(not(target_arch = "wasm32"))]
@ -12,16 +15,24 @@ use std::ops::Mul;
/// Specifies what and how much changed current health
#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq)]
pub struct HealthChange {
/// The amount of the health change, negative is damage, positive is healing
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>,
/// The time that the health change occurred at
pub time: Time,
}
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
/// the rest of the game.
// 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
/// are considered
maximum: u32,
// Time since last change and what the last change was
// TODO: Remove the time since last change, either convert to time of last change or just emit
// an outcome where appropriate. Is currently just used for frontend.
pub last_change: (f64, HealthChange),
/// The last change to health
pub last_change: HealthChange,
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 {
@ -98,12 +112,14 @@ impl Health {
current: health,
base_max: health,
maximum: health,
last_change: (0.0, HealthChange {
last_change: HealthChange {
amount: 0.0,
by: None,
cause: None,
}),
time: Time(0.0),
},
is_dead: false,
damage_contributors: HashMap::new(),
}
}
@ -120,10 +136,39 @@ impl Health {
#[cfg(not(target_arch = "wasm32"))]
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::SCALING_FACTOR_FLOAT) as u32)
.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 }
@ -139,20 +184,20 @@ impl Health {
self.is_dead = false;
}
// Function only exists for plugin tests, do not use anywhere else
// TODO: Remove somehow later
#[deprecated]
#[cfg(test)]
pub fn empty() -> Self {
Health {
current: 0,
base_max: 0,
maximum: 0,
last_change: (0.0, HealthChange {
last_change: HealthChange {
amount: 0.0,
by: None,
cause: None,
}),
time: Time(0.0),
},
is_dead: false,
damage_contributors: HashMap::new(),
}
}
}
@ -161,3 +206,123 @@ impl Health {
impl Component for Health {
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,
extend_one
)]
#![feature(hash_drain_filter)]
/// Re-exported crates
#[cfg(not(target_arch = "wasm32"))]

View File

@ -9,7 +9,7 @@ use specs::Entity;
pub struct TimeOfDay(pub f64);
/// 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);
/// A resource that stores the time since the previous tick.

View File

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

View File

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

View File

@ -1,4 +1,5 @@
use common::{
combat::DamageContributor,
comp::{
body::{object, Body},
buff::{
@ -6,17 +7,19 @@ use common::{
Buffs,
},
fluid_dynamics::{Fluid, LiquidKind},
Health, HealthChange, Inventory, LightEmitter, ModifierKind, PhysicsState, Stats,
Group, Health, HealthChange, Inventory, LightEmitter, ModifierKind, PhysicsState, Stats,
},
event::{EventBus, ServerEvent},
resources::DeltaTime,
resources::{DeltaTime, Time},
terrain::SpriteKind,
uid::UidAllocator,
Damage, DamageSource,
};
use common_ecs::{Job, Origin, Phase, System};
use hashbrown::HashMap;
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;
@ -28,6 +31,9 @@ pub struct ReadData<'a> {
inventories: ReadStorage<'a, Inventory>,
healths: ReadStorage<'a, Health>,
physics_states: ReadStorage<'a, PhysicsState>,
groups: ReadStorage<'a, Group>,
uid_allocator: Read<'a, UidAllocator>,
time: Read<'a, Time>,
}
#[derive(Default)]
@ -209,9 +215,27 @@ impl<'a> System<'a> for Sys {
ModifierKind::Additive => *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 {
entity,
change: HealthChange { amount, by, cause },
change: HealthChange {
amount,
by: damage_contributor,
cause,
time: *read_data.time,
},
});
*accumulated = 0.0;
};

View File

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

View File

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

View File

@ -192,6 +192,7 @@ impl<'a> System<'a> for Sys {
.map(|(entity, uid)| AttackerInfo {
entity,
uid,
group: read_data.groups.get(entity),
energy: read_data.energies.get(entity),
combo: read_data.combos.get(entity),
inventory: read_data.inventories.get(entity),
@ -230,6 +231,7 @@ impl<'a> System<'a> for Sys {
attack_options,
1.0,
AttackSource::Shockwave,
*read_data.time,
|e| server_emitter.emit(e),
|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 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
for (entity, uid, stats, mut skill_set, mut health, pos, mut energy, inventory) in (
&read_data.entities,
@ -100,7 +93,7 @@ impl<'a> System<'a> for Sys {
entities_died_last_tick.0.push(cloned_entity);
server_event_emitter.emit(ServerEvent::Destroy {
entity,
cause: health.last_change.1,
cause: health.last_change,
});
health.is_dead = true;

View File

@ -50,9 +50,11 @@ pub enum Action {
/// ```
/// Over this one:
/// ```rust
/// # use common::comp::Body;
/// # use common::comp::body::humanoid;
/// # use veloren_plugin_api::*;
/// # 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) =
/// retrieve_action(&Retrieve::GetEntityHealth(entityid)).unwrap()
/// {
@ -75,9 +77,11 @@ pub enum Retrieve {
///
/// Example:
/// ```rust
/// # use common::comp::Body;
/// # use common::comp::body::humanoid;
/// # use veloren_plugin_api::*;
/// # 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) =
/// retrieve_action(&Retrieve::GetEntityHealth(entityid)).unwrap()
/// {

View File

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

View File

@ -11,6 +11,7 @@ use crate::{
};
use common::{
combat,
combat::DamageContributor,
comp::{
self, aura, buff,
chat::{KillSource, KillType},
@ -33,10 +34,20 @@ use common_state::BlockChange;
use comp::chat::GenericChatMsg;
use hashbrown::HashSet;
use rand::Rng;
use specs::{join::Join, saveload::MarkerAllocator, Builder, Entity as EcsEntity, WorldExt};
use tracing::error;
use specs::{
join::Join, saveload::MarkerAllocator, Builder, Entity as EcsEntity, Entity, WorldExt,
};
use std::{collections::HashMap, iter};
use tracing::{debug, error};
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>) {
let ecs = &server.state.ecs();
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 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 (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::Projectile), Some(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 energies = state.ecs().read_storage::<Energy>();
let inventories = state.ecs().read_storage::<Inventory>();
let players = state.ecs().read_storage::<Player>();
let bodies = state.ecs().read_storage::<Body>();
let poises = state.ecs().read_storage::<comp::Poise>();
let by = if let Some(by) = last_change.by {
by
} else {
return;
};
let attacker = if let Some(attacker) = state.ecs().entity_from_uid(by.into()) {
attacker
} else {
return;
};
let positions = state.ecs().read_storage::<Pos>();
let groups = state.ecs().read_storage::<Group>();
let (
entity_skill_set,
@ -205,37 +211,25 @@ pub fn handle_destroy(server: &mut Server, entity: EcsEntity, last_change: Healt
entity_inventory,
entity_body,
entity_poise,
entity_pos,
) = match (|| {
Some((
skill_set.get(entity)?,
skill_sets.get(entity)?,
healths.get(entity)?,
energies.get(entity)?,
inventories.get(entity)?,
bodies.get(entity)?,
poises.get(entity)?,
positions.get(entity)?,
))
})() {
Some(comps) => comps,
None => return,
};
let groups = state.ecs().read_storage::<Group>();
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
// Calculate the total EXP award for the kill
let msm = state.ecs().read_resource::<MaterialStatManifest>();
let mut exp_reward = combat::combat_rating(
let exp_reward = combat::combat_rating(
entity_inventory,
entity_health,
entity_energy,
@ -245,16 +239,88 @@ pub fn handle_destroy(server: &mut Server, entity: EcsEntity, last_change: Healt
&msm,
) * 20.0;
// Distribute EXP to group
let positions = state.ecs().read_storage::<Pos>();
let mut damage_contributors = HashMap::<DamageContrib, (u64, f32)>::new();
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 uids = state.ecs().read_storage::<Uid>();
let mut outcomes = state.ecs().write_resource::<Vec<Outcome>>();
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
// group
let mut non_pet_group_members_in_range = 1;
let destroyed_group = groups.get(entity);
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 = (
&state.ecs().entities(),
&groups,
@ -263,37 +329,35 @@ pub fn handle_destroy(server: &mut Server, entity: EcsEntity, last_change: Healt
&uids,
)
.join()
.filter(|(entity, group, member_pos, _, _)| {
// Check if: in group, not main attacker, and in range
*group == attacker_group
&& *entity != attacker
&& pos.0.distance_squared(member_pos.0) < MAX_EXP_DIST.powi(2)
})
.map(|(entity, _, _, alignment, uid)| {
if !matches!(alignment, Some(Alignment::Owned(owner)) if owner != uid) {
non_pet_group_members_in_range += 1;
.filter_map(|(member_entity, member_group, member_pos, alignment, uid)| {
if *member_group == *group && within_range(member_pos) && !is_pvp_kill(member_entity) && !matches!(alignment, Some(Alignment::Owned(owner)) if owner != uid) {
Some(member_entity)
} else {
None
}
(entity, uid)
})
.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)) = (
skill_set.get_mut(attacker),
skill_sets.get_mut(attacker),
uids.get(attacker),
inventories.get(attacker),
) {
// TODO: Discuss whether we should give EXP by Player Killing or not.
handle_exp_gain(
exp_reward,
attacker_inventory,
@ -302,6 +366,7 @@ pub fn handle_destroy(server: &mut Server, entity: EcsEntity, last_change: Healt
&mut outcomes,
);
}
});
})();
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),
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);
}
// Handle poise change
@ -774,6 +841,7 @@ pub fn handle_explosion(server: &Server, pos: Vec3<f32>, explosion: Explosion, o
.map(|(entity, uid)| combat::AttackerInfo {
entity,
uid,
group: groups.get(entity),
energy: energies.get(entity),
combo: combos.get(entity),
inventory: inventories.get(entity),
@ -806,6 +874,7 @@ pub fn handle_explosion(server: &Server, pos: Vec3<f32>, explosion: Explosion, o
target_group,
};
let time = server.state.ecs().read_resource::<Time>();
attack.apply_attack(
attacker_info,
target_info,
@ -813,6 +882,7 @@ pub fn handle_explosion(server: &Server, pos: Vec3<f32>, explosion: Explosion, o
attack_options,
strength,
combat::AttackSource::Explosion,
*time,
|e| emitter.emit(e),
|o| outcomes.push(o),
);

View File

@ -10,13 +10,14 @@ use crate::{
use common::{
character::CharacterId,
combat,
combat::DamageContributor,
comp::{
self,
skills::{GeneralSkill, Skill},
Group, Inventory, Poise,
},
effect::Effect,
resources::TimeOfDay,
resources::{Time, TimeOfDay},
slowjob::SlowJobPool,
uid::{Uid, UidAllocator},
};
@ -127,16 +128,27 @@ impl StateExt for State {
Effect::Damage(damage) => {
let inventories = self.ecs().read_storage::<Inventory>();
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(
combat::Damage::compute_damage_reduction(
inventories.get(entity),
stats.get(entity),
Some(damage.kind),
),
source,
damage_contributor,
false,
0.0,
1.0,
*time,
);
self.ecs()
.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
// was recent and we have a health component
match health {
Some(health) if health.last_change.0 < DAMAGE_MEMORY_DURATION => {
if let Some(by) = health.last_change.1.damage_by() {
Some(health)
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) =
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
// 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>| {
// I'm a guard and a villager is in distress
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| {
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
.then(|| {
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| {
read_data
.positions
@ -4217,8 +4222,8 @@ impl<'a> AgentData<'a> {
) {
if let Some(Target { target, .. }) = agent.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(attacker) = get_entity_by_id(by.id(), read_data) {
if let Some(by) = tgt_health.last_change.damage_by() {
if let Some(attacker) = get_entity_by_id(by.uid().0, read_data) {
if agent.target.is_none() {
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 {
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 {
false
}

View File

@ -54,7 +54,7 @@ use hashbrown::HashMap;
use rand::{prelude::SliceRandom, thread_rng, Rng};
use serde::Deserialize;
use std::time::Instant;
use tracing::{debug, warn};
use tracing::{debug, trace, warn};
/// Collection of all the tracks
#[derive(Debug, Deserialize)]
@ -289,9 +289,10 @@ impl MusicMgr {
if interrupt {
self.last_interrupt = Instant::now();
}
debug!(
trace!(
"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) {
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)
// (maybe health changes could be sent to the client as a list
// 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::Heal { by: Some(by) } => {
// let by_me = my_uid.map_or(false, |&uid| by == uid);
@ -101,12 +101,12 @@ impl<'a> System<'a> for Sys {
match last_floater {
Some(f) if f.timer < HP_ACCUMULATETIME => {
//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 {
timer: 0.0,
hp_change: health.last_change.1.amount,
hp_change: health.last_change.amount,
rand: rand::random(),
});
},

View File

@ -35,7 +35,7 @@ use common::{
Body, CharacterState, Collider, Controller, Health, Inventory, Item, Last, LightAnimation,
LightEmitter, Mounting, Ori, PhysicsState, PoiseState, Pos, Scale, Vel,
},
resources::DeltaTime,
resources::{DeltaTime, Time},
states::{equipping, idle, utils::StageSection, wielding},
terrain::TerrainChunk,
uid::UidAllocator,
@ -752,9 +752,11 @@ impl FigureMgr {
// Change in health as color!
let col = health
.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::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))