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
|
- 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
|
||||||
|
|
||||||
|
@ -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())
|
||||||
|
@ -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),
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -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"))]
|
||||||
|
@ -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.
|
||||||
|
@ -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(),
|
||||||
))
|
))
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
@ -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),
|
||||||
);
|
);
|
||||||
|
@ -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;
|
||||||
};
|
};
|
||||||
|
@ -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),
|
||||||
);
|
);
|
||||||
|
@ -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),
|
||||||
);
|
);
|
||||||
|
@ -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),
|
||||||
);
|
);
|
||||||
|
@ -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;
|
||||||
|
@ -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()
|
||||||
/// {
|
/// {
|
||||||
|
@ -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(())
|
||||||
|
@ -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),
|
||||||
);
|
);
|
||||||
|
@ -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>()
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
|
@ -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;
|
||||||
|
@ -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(),
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
@ -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))
|
||||||
|
Loading…
Reference in New Issue
Block a user