mirror of
https://gitlab.com/veloren/veloren.git
synced 2024-08-30 18:12:32 +00:00
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:
parent
4b3aa7e8fa
commit
022c1417b6
@ -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
|
||||
|
||||
|
@ -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())
|
||||
|
@ -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),
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
@ -13,6 +13,7 @@
|
||||
type_alias_impl_trait,
|
||||
extend_one
|
||||
)]
|
||||
#![feature(hash_drain_filter)]
|
||||
|
||||
/// Re-exported crates
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
|
@ -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.
|
||||
|
@ -268,12 +268,16 @@ fn retrieve_action(
|
||||
RetrieveError::EcsAccessError(EcsAccessError::EcsEntityNotFound(e)),
|
||||
)?;
|
||||
Ok(RetrieveResult::GetEntityHealth(
|
||||
*world.health.get(player).ok_or_else(|| {
|
||||
RetrieveError::EcsAccessError(EcsAccessError::EcsComponentNotFound(
|
||||
e,
|
||||
"Health".to_owned(),
|
||||
))
|
||||
})?,
|
||||
world
|
||||
.health
|
||||
.get(player)
|
||||
.ok_or_else(|| {
|
||||
RetrieveError::EcsAccessError(EcsAccessError::EcsComponentNotFound(
|
||||
e,
|
||||
"Health".to_owned(),
|
||||
))
|
||||
})?
|
||||
.clone(),
|
||||
))
|
||||
},
|
||||
}
|
||||
|
@ -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),
|
||||
);
|
||||
|
@ -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;
|
||||
};
|
||||
|
@ -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),
|
||||
);
|
||||
|
@ -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),
|
||||
);
|
||||
|
@ -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),
|
||||
);
|
||||
|
@ -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;
|
||||
|
@ -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()
|
||||
/// {
|
||||
|
@ -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(())
|
||||
|
@ -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,63 +239,134 @@ 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 members_in_range = (
|
||||
&state.ecs().entities(),
|
||||
&groups,
|
||||
&positions,
|
||||
alignments.maybe(),
|
||||
&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;
|
||||
}
|
||||
|
||||
(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);
|
||||
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,
|
||||
&positions,
|
||||
alignments.maybe(),
|
||||
&uids,
|
||||
)
|
||||
.join()
|
||||
.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
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
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
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if let (Some(mut attacker_skill_set), Some(attacker_uid), Some(attacker_inventory)) = (
|
||||
skill_set.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,
|
||||
&mut attacker_skill_set,
|
||||
attacker_uid,
|
||||
&mut outcomes,
|
||||
);
|
||||
}
|
||||
}
|
||||
}).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_sets.get_mut(attacker),
|
||||
uids.get(attacker),
|
||||
inventories.get(attacker),
|
||||
) {
|
||||
handle_exp_gain(
|
||||
exp_reward,
|
||||
attacker_inventory,
|
||||
&mut attacker_skill_set,
|
||||
attacker_uid,
|
||||
&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),
|
||||
);
|
||||
|
@ -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>()
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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;
|
||||
|
@ -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(),
|
||||
});
|
||||
},
|
||||
|
@ -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))
|
||||
|
Loading…
Reference in New Issue
Block a user