veloren/server/src/events/entity_manipulation.rs

1227 lines
49 KiB
Rust
Raw Normal View History

2020-10-07 02:23:20 +00:00
use crate::{
client::Client,
comp::{
2021-08-27 18:49:58 +00:00
agent::{Agent, AgentEvent, Sound, SoundKind},
skills::SkillGroupKind,
2021-09-23 18:43:31 +00:00
BuffKind, BuffSource, PhysicsState,
},
rtsim::RtSim,
sys::terrain::SAFE_ZONE_RADIUS,
2020-11-23 15:39:03 +00:00
Server, SpawnPoint, StateExt,
2020-10-07 02:23:20 +00:00
};
2020-02-16 20:04:06 +00:00
use common::{
combat,
combat::DamageContributor,
2020-07-25 23:57:04 +00:00
comp::{
2020-12-04 22:24:56 +00:00
self, aura, buff,
chat::{KillSource, KillType},
inventory::item::MaterialStatManifest,
object, Alignment, Auras, Body, CharacterState, Energy, Group, Health, HealthChange,
Inventory, Player, Poise, Pos, SkillSet, Stats,
2020-07-25 23:57:04 +00:00
},
2021-01-30 22:35:00 +00:00
event::{EventBus, ServerEvent},
2020-07-31 17:16:20 +00:00
outcome::Outcome,
2021-02-27 19:55:06 +00:00
resources::Time,
2020-11-23 15:39:03 +00:00
rtsim::RtSimEntity,
terrain::{Block, BlockKind, TerrainGrid},
uid::{Uid, UidAllocator},
2021-01-30 22:35:00 +00:00
util::Dir,
2020-09-21 20:10:32 +00:00
vol::ReadVol,
Damage, DamageKind, DamageSource, Explosion, GroupTarget, RadiusEffect,
2020-02-16 20:04:06 +00:00
};
use common_net::{msg::ServerGeneral, sync::WorldSyncExt};
2021-04-06 15:47:03 +00:00
use common_state::BlockChange;
use comp::chat::GenericChatMsg;
2020-12-13 17:21:51 +00:00
use hashbrown::HashSet;
2021-08-10 16:53:39 +00:00
use rand::Rng;
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};
2020-03-22 19:39:50 +00:00
#[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) {
// Entity is invincible to poise change during stunned/staggered character state
if !character_state.is_stunned() {
if let Some(mut poise) = ecs.write_storage::<Poise>().get_mut(entity) {
poise.change_by(change, knockback_dir);
}
}
2020-02-16 20:04:06 +00:00
}
}
2020-12-16 23:30:33 +00:00
pub fn handle_health_change(server: &Server, entity: EcsEntity, change: HealthChange) {
2020-12-06 02:29:46 +00:00
let ecs = &server.state.ecs();
2021-01-13 03:26:51 +00:00
if let Some(mut health) = ecs.write_storage::<Health>().get_mut(entity) {
2020-12-06 02:29:46 +00:00
health.change_by(change);
}
2021-06-17 05:49:09 +00:00
// This if statement filters out anything under 5 damage, for DOT ticks
// TODO: Find a better way to separate direct damage from DOT here
2021-09-10 19:20:14 +00:00
let damage = -change.amount;
if damage > -5.0 {
2021-06-17 05:49:09 +00:00
if let Some(agent) = ecs.write_storage::<Agent>().get_mut(entity) {
agent.inbox.push_front(AgentEvent::Hurt);
}
}
2020-12-06 02:29:46 +00:00
}
2020-02-16 20:04:06 +00:00
2020-09-19 16:55:31 +00:00
pub fn handle_knockback(server: &Server, entity: EcsEntity, impulse: Vec3<f32>) {
2020-11-07 18:00:07 +00:00
let ecs = &server.state.ecs();
let clients = ecs.read_storage::<Client>();
if let Some(physics) = ecs.read_storage::<PhysicsState>().get(entity) {
//Check if the entity is on a surface. If it is not, reduce knockback.
let mut impulse = impulse
2020-11-07 18:00:07 +00:00
* if physics.on_surface().is_some() {
1.0
} else {
0.4
};
if let Some(mass) = ecs.read_storage::<comp::Mass>().get(entity) {
// we go easy on the little ones (because they fly so far)
impulse /= mass.0.max(40.0);
}
2020-11-07 18:00:07 +00:00
let mut velocities = ecs.write_storage::<comp::Vel>();
if let Some(vel) = velocities.get_mut(entity) {
vel.0 += impulse;
2020-11-07 18:00:07 +00:00
}
if let Some(client) = clients.get(entity) {
client.send_fallible(ServerGeneral::Knockback(impulse));
}
}
}
2020-07-03 17:33:37 +00:00
/// Handle an entity dying. If it is a player, it will send a message to all
/// other players. If the entity that killed it had stats, then give it exp for
/// the kill. Experience given is equal to the level of the entity that was
/// killed times 10.
2021-12-05 17:59:02 +00:00
// NOTE: Clippy incorrectly warns about a needless collect here because it does
// not understand that the pet count (which is computed during the first
// iteration over the members in range) is actually used by the second iteration
// over the members in range; since we have no way of knowing the pet count
// before the first loop finishes, we definitely need at least two loops. Then
// (currently) our only options are to store the member list in temporary space
// (e.g. by collecting to a vector), or to repeat the loop; but repeating the
// loop would currently be very inefficient since it has to rescan every entity
// on the server again.
pub fn handle_destroy(server: &mut Server, entity: EcsEntity, last_change: HealthChange) {
2020-02-16 20:04:06 +00:00
let state = server.state_mut();
// TODO: Investigate duplicate `Destroy` events (but don't remove this).
// If the entity was already deleted, it can't be destroyed again.
if !state.ecs().is_alive(entity) {
return;
}
2020-02-16 20:04:06 +00:00
let get_attacker_name = |cause_of_death: KillType, by: Uid| -> KillSource {
// Get attacker entity
if let Some(char_entity) = state.ecs().entity_from_uid(by.into()) {
// Check if attacker is another player or entity with stats (npc)
if state
.ecs()
.read_storage::<Player>()
.get(char_entity)
.is_some()
{
KillSource::Player(by, cause_of_death)
} else if let Some(stats) = state.ecs().read_storage::<Stats>().get(char_entity) {
KillSource::NonPlayer(stats.name.clone(), cause_of_death)
} else {
KillSource::NonExistent(cause_of_death)
}
} else {
KillSource::NonExistent(cause_of_death)
}
};
// Push an outcome if entity is has a character state (entities that don't have
// one, we probably don't care about emitting death outcome)
if state
.ecs()
.read_storage::<comp::CharacterState>()
.get(entity)
.is_some()
{
if let Some(pos) = state.ecs().read_storage::<Pos>().get(entity) {
state
.ecs()
.write_resource::<Vec<Outcome>>()
.push(Outcome::Death { pos: pos.0 });
}
}
2020-02-16 20:04:06 +00:00
// Chat message
// 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.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)
},
(Some(DamageSource::Explosion), Some(by)) => {
get_attacker_name(KillType::Explosion, by)
},
(Some(DamageSource::Energy), Some(by)) => get_attacker_name(KillType::Energy, by),
(Some(DamageSource::Buff(buff_kind)), Some(by)) => {
get_attacker_name(KillType::Buff(buff_kind), by)
},
(Some(DamageSource::Other), Some(by)) => get_attacker_name(KillType::Other, by),
(Some(DamageSource::Falling), _) => KillSource::FallDamage,
// HealthSource::Suicide => KillSource::Suicide,
_ => KillSource::Other,
};
state.send_chat(GenericChatMsg {
chat_type: comp::ChatType::Kill(kill_source, *uid),
message: "".to_string(),
});
2020-02-16 20:04:06 +00:00
}
}
// 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_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 positions = state.ecs().read_storage::<Pos>();
let groups = state.ecs().read_storage::<Group>();
2021-09-26 14:52:16 +00:00
let (
entity_skill_set,
entity_health,
entity_energy,
entity_inventory,
entity_body,
entity_poise,
entity_pos,
2021-09-26 14:52:16 +00:00
) = match (|| {
Some((
skill_sets.get(entity)?,
2021-09-26 14:52:16 +00:00
healths.get(entity)?,
energies.get(entity)?,
inventories.get(entity)?,
bodies.get(entity)?,
poises.get(entity)?,
positions.get(entity)?,
2021-09-26 14:52:16 +00:00
))
})() {
Some(comps) => comps,
None => return,
};
// Calculate the total EXP award for the kill
let msm = state.ecs().read_resource::<MaterialStatManifest>();
let exp_reward = combat::combat_rating(
entity_inventory,
entity_health,
entity_energy,
entity_poise,
entity_skill_set,
*entity_body,
&msm,
) * 20.0;
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>();
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
}
}
}).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,
);
}
});
})();
2020-02-16 20:04:06 +00:00
let should_delete = if state
2020-02-16 20:04:06 +00:00
.ecs()
.write_storage::<Client>()
.get_mut(entity)
.is_some()
{
state
.ecs()
.write_storage()
.insert(entity, comp::Vel(Vec3::zero()))
.err()
.map(|e| error!(?e, ?entity, "Failed to set zero vel on dead client"));
2020-02-16 20:04:06 +00:00
state
.ecs()
.write_storage()
.insert(entity, comp::ForceUpdate)
.err()
.map(|e| error!(?e, ?entity, "Failed to insert ForceUpdate on dead client"));
2020-02-16 20:04:06 +00:00
state
.ecs()
.write_storage::<comp::Energy>()
.get_mut(entity)
.map(|mut energy| {
let energy = &mut *energy;
energy.refresh()
});
2020-02-16 20:04:06 +00:00
let _ = state
.ecs()
.write_storage::<comp::CharacterState>()
.insert(entity, comp::CharacterState::default());
false
} else if state.ecs().read_storage::<comp::Agent>().contains(entity)
&& !matches!(
state.ecs().read_storage::<comp::Alignment>().get(entity),
Some(comp::Alignment::Owned(_))
)
{
2021-09-27 20:21:57 +00:00
// Only drop loot if entity has agency (not a player),
// and if it is not owned by another entity (not a pet)
// Decide for a loot drop before turning into a lootbag
let old_body = state.ecs().write_storage::<Body>().remove(entity);
2021-09-27 20:21:57 +00:00
let item = {
let mut item_drop = state.ecs().write_storage::<comp::ItemDrop>();
item_drop.remove(entity).map(|comp::ItemDrop(item)| item)
2021-09-22 02:25:14 +00:00
};
2021-09-27 20:21:57 +00:00
if let Some(item) = item {
2021-09-01 23:17:36 +00:00
let pos = state.ecs().read_storage::<comp::Pos>().get(entity).cloned();
let vel = state.ecs().read_storage::<comp::Vel>().get(entity).cloned();
if let Some(pos) = pos {
// TODO: This should only be temporary as you'd eventually want to actually
// render the items on the ground, rather than changing the texture depending on
// the body type
2021-09-01 23:17:36 +00:00
let _ = state
.create_object(comp::Pos(pos.0 + Vec3::unit_z() * 0.25), match old_body {
Some(common::comp::Body::Humanoid(_)) => object::Body::Pouch,
Some(common::comp::Body::BipedSmall(_))
| Some(common::comp::Body::BipedLarge(_)) => object::Body::Pouch,
2021-09-01 23:17:36 +00:00
Some(common::comp::Body::Golem(_)) => object::Body::Chest,
Some(common::comp::Body::QuadrupedSmall(_)) => object::Body::SmallMeat,
Some(common::comp::Body::FishMedium(_))
| Some(common::comp::Body::FishSmall(_)) => object::Body::FishMeat,
Some(common::comp::Body::QuadrupedMedium(_)) => object::Body::BeastMeat,
Some(common::comp::Body::QuadrupedLow(_)) => object::Body::ToughMeat,
2021-09-01 23:17:36 +00:00
Some(common::comp::Body::BirdLarge(_))
| Some(common::comp::Body::BirdMedium(_)) => object::Body::BirdMeat,
Some(common::comp::Body::Theropod(_)) => object::Body::BeastMeat,
Some(common::comp::Body::Dragon(_)) => object::Body::BeastMeat,
Some(common::comp::Body::Object(_)) => object::Body::Chest,
_ => object::Body::Pouch,
2021-09-01 23:17:36 +00:00
})
.maybe_with(vel)
.with(item)
.build();
} else {
error!(
?entity,
"Entity doesn't have a position, no bag is being dropped"
)
}
}
true
} else {
true
};
if should_delete {
2020-11-23 15:39:03 +00:00
if let Some(rtsim_entity) = state
.ecs()
.read_storage::<RtSimEntity>()
.get(entity)
.copied()
{
state
.ecs()
.write_resource::<RtSim>()
.destroy_entity(rtsim_entity.0);
}
let _ = state
.delete_entity_recorded(entity)
.map_err(|e| error!(?e, ?entity, "Failed to delete destroyed entity"));
}
2020-03-18 19:37:11 +00:00
// TODO: Add Delete(time_left: Duration) component
/*
// If not a player delete the entity
if let Err(err) = state.delete_entity_recorded(entity) {
error!(?e, "Failed to delete destroyed entity");
2020-02-16 20:04:06 +00:00
}
*/
2020-02-16 20:04:06 +00:00
}
2020-11-11 11:42:22 +00:00
/// Delete an entity without any special actions (this is generally used for
/// temporarily unloading an entity when it leaves the view distance). As much
/// as possible, this function should simply make an entity cease to exist.
pub fn handle_delete(server: &mut Server, entity: EcsEntity) {
let _ = server
.state_mut()
.delete_entity_recorded(entity)
.map_err(|e| error!(?e, ?entity, "Failed to delete destroyed entity"));
}
2020-02-16 20:04:06 +00:00
pub fn handle_land_on_ground(server: &Server, entity: EcsEntity, vel: Vec3<f32>) {
let ecs = server.state.ecs();
2020-04-08 12:40:17 +00:00
if vel.z <= -30.0 {
let mass = ecs
.read_storage::<comp::Mass>()
.get(entity)
.copied()
.unwrap_or_default();
let impact_energy = mass.0 * vel.z.powi(2) / 2.0;
2021-09-14 10:53:01 +00:00
let falldmg = impact_energy / 1000.0;
let inventories = ecs.read_storage::<Inventory>();
let stats = ecs.read_storage::<Stats>();
// Handle health change
if let Some(mut health) = ecs.write_storage::<comp::Health>().get_mut(entity) {
let damage = Damage {
source: DamageSource::Falling,
kind: DamageKind::Crushing,
value: falldmg,
};
let damage_reduction = Damage::compute_damage_reduction(
inventories.get(entity),
stats.get(entity),
Some(DamageKind::Crushing),
);
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
if let Some(mut poise) = ecs.write_storage::<comp::Poise>().get_mut(entity) {
let poise_damage = -(mass.0 * vel.magnitude_squared() / 1500.0);
let poise_change = Poise::apply_poise_reduction(poise_damage, inventories.get(entity));
poise.change_by(poise_change, Vec3::unit_z());
2020-02-16 20:04:06 +00:00
}
}
}
pub fn handle_respawn(server: &Server, entity: EcsEntity) {
let state = &server.state;
// Only clients can respawn
if state
.ecs()
.write_storage::<Client>()
.get_mut(entity)
.is_some()
{
let respawn_point = state
2020-08-23 20:29:40 +00:00
.read_component_copied::<comp::Waypoint>(entity)
2020-02-16 20:04:06 +00:00
.map(|wp| wp.get_pos())
.unwrap_or(state.ecs().read_resource::<SpawnPoint>().0);
state
.ecs()
.write_storage::<comp::Health>()
2020-02-16 20:04:06 +00:00
.get_mut(entity)
.map(|mut health| health.revive());
2021-07-04 15:14:40 +00:00
state
.ecs()
.write_storage::<comp::Combo>()
.get_mut(entity)
.map(|mut combo| combo.reset());
2020-02-16 20:04:06 +00:00
state
.ecs()
.write_storage::<comp::Pos>()
.get_mut(entity)
.map(|pos| pos.0 = respawn_point);
state
.ecs()
.write_storage()
.insert(entity, comp::ForceUpdate)
.err()
.map(|e| {
2020-02-16 20:04:06 +00:00
error!(
?e,
"Error inserting ForceUpdate component when respawning client"
2020-02-16 20:04:06 +00:00
)
});
}
}
2021-02-20 20:38:27 +00:00
pub fn handle_explosion(server: &Server, pos: Vec3<f32>, explosion: Explosion, owner: Option<Uid>) {
2020-08-08 12:53:07 +00:00
// Go through all other entities
2020-03-22 19:39:50 +00:00
let ecs = &server.state.ecs();
let server_eventbus = ecs.read_resource::<EventBus<ServerEvent>>();
let time = ecs.read_resource::<Time>();
2021-07-14 07:40:43 +00:00
let owner_entity = owner.and_then(|uid| {
ecs.read_resource::<UidAllocator>()
.retrieve_entity_internal(uid.into())
});
let explosion_volume = 2.5 * explosion.radius;
2021-11-03 13:26:34 +00:00
let mut emitter = server_eventbus.emitter();
emitter.emit(ServerEvent::Sound {
sound: Sound::new(SoundKind::Explosion, pos, explosion_volume, time.0),
});
2020-07-31 17:16:20 +00:00
// Add an outcome
2021-02-16 05:18:05 +00:00
// Uses radius as outcome power for now
let outcome_power = explosion.radius;
2021-04-04 03:04:02 +00:00
let mut outcomes = ecs.write_resource::<Vec<Outcome>>();
outcomes.push(Outcome::Explosion {
pos,
power: outcome_power,
radius: explosion.radius,
is_attack: explosion
.effects
.iter()
.any(|e| matches!(e, RadiusEffect::Attack(_))),
reagent: explosion.reagent,
});
2020-08-08 12:53:07 +00:00
let groups = ecs.read_storage::<comp::Group>();
// Used to get strength of explosion effects as they falloff over distance
fn cylinder_sphere_strength(
sphere_pos: Vec3<f32>,
radius: f32,
2021-10-27 19:42:11 +00:00
min_falloff: f32,
cyl_pos: Vec3<f32>,
cyl_body: Body,
) -> f32 {
// 2d check
let horiz_dist = Vec2::<f32>::from(sphere_pos - cyl_pos).distance(Vec2::default())
- cyl_body.max_radius();
// z check
let half_body_height = cyl_body.height() / 2.0;
let vert_distance =
(sphere_pos.z - (cyl_pos.z + half_body_height)).abs() - half_body_height;
2021-10-27 19:55:25 +00:00
// Use whichever gives maximum distance as that closer to real value. Sets
// minimum to 0 as negative values would indicate inside entity.
let distance = horiz_dist.max(vert_distance).max(0.0);
if distance > radius {
// If further than exploion radius, no strength
0.0
} else {
// Falloff inversely proportional to radius
2021-10-29 21:12:57 +00:00
let fall_off = ((distance / radius).min(1.0) - 1.0).abs();
let min_falloff = min_falloff.clamp(0.0, 1.0);
2021-10-27 19:55:25 +00:00
min_falloff + fall_off * (1.0 - min_falloff)
}
}
2021-08-10 16:53:39 +00:00
// TODO: Faster RNG?
let mut rng = rand::thread_rng();
'effects: for effect in explosion.effects {
match effect {
RadiusEffect::TerrainDestruction(power) => {
const RAYS: usize = 500;
let spatial_grid = ecs.read_resource::<common::CachedSpatialGrid>();
let auras = ecs.read_storage::<Auras>();
let positions = ecs.read_storage::<Pos>();
// Prevent block colour changes within the radius of a safe zone aura
if spatial_grid
.0
.in_circle_aabr(pos.xy(), SAFE_ZONE_RADIUS)
.filter_map(|entity| {
auras
.get(entity)
.and_then(|entity_auras| {
positions.get(entity).map(|pos| (entity_auras, pos))
})
.and_then(|(entity_auras, pos)| {
entity_auras
.auras
.iter()
.find(|(_, aura)| {
matches!(aura.aura_kind, aura::AuraKind::Buff {
kind: BuffKind::Invulnerability,
source: BuffSource::World,
..
})
})
.map(|(_, aura)| (*pos, aura.radius))
})
})
.any(|(aura_pos, aura_radius)| {
pos.distance_squared(aura_pos.0) < aura_radius.powi(2)
})
{
continue 'effects;
}
// Color terrain
let mut touched_blocks = Vec::new();
let color_range = power * 2.7;
for _ in 0..RAYS {
let dir = Vec3::new(
2021-08-10 16:53:39 +00:00
rng.gen::<f32>() - 0.5,
rng.gen::<f32>() - 0.5,
rng.gen::<f32>() - 0.5,
)
.normalized();
let _ = ecs
.read_resource::<TerrainGrid>()
.ray(pos, pos + dir * color_range)
2021-08-10 16:53:39 +00:00
.until(|_| rng.gen::<f32>() < 0.05)
.for_each(|_: &Block, pos| touched_blocks.push(pos))
.cast();
}
2020-02-16 20:04:06 +00:00
let terrain = ecs.read_resource::<TerrainGrid>();
let mut block_change = ecs.write_resource::<BlockChange>();
for block_pos in touched_blocks {
if let Ok(block) = terrain.get(block_pos) {
if !matches!(block.kind(), BlockKind::Lava | BlockKind::GlowingRock) {
let diff2 = block_pos.map(|b| b as f32).distance_squared(pos);
let fade = (1.0 - diff2 / color_range.powi(2)).max(0.0);
if let Some(mut color) = block.get_color() {
let r = color[0] as f32
+ (fade * (color[0] as f32 * 0.5 - color[0] as f32));
let g = color[1] as f32
+ (fade * (color[1] as f32 * 0.3 - color[1] as f32));
let b = color[2] as f32
+ (fade * (color[2] as f32 * 0.3 - color[2] as f32));
// Darken blocks, but not too much
color[0] = (r as u8).max(30);
color[1] = (g as u8).max(30);
color[2] = (b as u8).max(30);
block_change.set(block_pos, Block::new(block.kind(), color));
}
}
2021-11-03 13:26:34 +00:00
if block.is_bonkable() {
emitter.emit(ServerEvent::Bonk {
pos: block_pos.map(|e| e as f32 + 0.5),
owner,
target: None,
});
}
}
}
2020-08-28 21:43:33 +00:00
// Destroy terrain
for _ in 0..RAYS {
let dir = Vec3::new(
2021-08-10 16:53:39 +00:00
rng.gen::<f32>() - 0.5,
rng.gen::<f32>() - 0.5,
rng.gen::<f32>() - 0.15,
)
.normalized();
let mut ray_energy = power;
let terrain = ecs.read_resource::<TerrainGrid>();
2021-08-10 16:53:39 +00:00
let from = pos;
let to = pos + dir * power;
let _ = terrain
2021-08-10 16:53:39 +00:00
.ray(from, to)
.until(|block: &Block| {
2021-08-10 16:53:39 +00:00
// Stop if:
// 1) Block is liquid
// 2) Consumed all energy
// 3) Can't explode block (for example we hit stone wall)
let stop = block.is_liquid()
|| block.explode_power().is_none()
|| ray_energy <= 0.0;
ray_energy -=
block.explode_power().unwrap_or(0.0) + rng.gen::<f32>() * 0.1;
stop
})
.for_each(|block: &Block, pos| {
if block.explode_power().is_some() {
block_change.set(pos, block.into_vacant());
}
})
.cast();
}
},
2021-01-30 22:35:00 +00:00
RadiusEffect::Attack(attack) => {
let energies = &ecs.read_storage::<comp::Energy>();
let combos = &ecs.read_storage::<comp::Combo>();
2021-05-19 01:42:14 +00:00
let inventories = &ecs.read_storage::<comp::Inventory>();
2021-08-27 12:52:52 +00:00
let alignments = &ecs.read_storage::<Alignment>();
let uid_allocator = &ecs.read_resource::<UidAllocator>();
let players = &ecs.read_storage::<comp::Player>();
2021-04-10 03:40:20 +00:00
for (
entity_b,
pos_b,
health_b,
2021-05-19 01:42:14 +00:00
(body_b_maybe, stats_b_maybe, ori_b_maybe, char_state_b_maybe, uid_b),
2021-04-10 03:40:20 +00:00
) in (
&ecs.entities(),
&ecs.read_storage::<comp::Pos>(),
&ecs.read_storage::<comp::Health>(),
2021-04-10 03:40:20 +00:00
(
ecs.read_storage::<comp::Body>().maybe(),
ecs.read_storage::<comp::Stats>().maybe(),
ecs.read_storage::<comp::Ori>().maybe(),
ecs.read_storage::<comp::CharacterState>().maybe(),
&ecs.read_storage::<Uid>(),
2021-04-10 03:40:20 +00:00
),
)
.join()
2021-04-10 03:40:20 +00:00
.filter(|(_, _, h, _)| !h.is_dead)
2020-11-01 17:15:46 +00:00
{
2021-01-30 22:35:00 +00:00
// Check if it is a hit
let strength = if let Some(body) = body_b_maybe {
2021-10-27 19:42:11 +00:00
cylinder_sphere_strength(
pos,
explosion.radius,
explosion.min_falloff,
pos_b.0,
*body,
)
} else {
let distance_squared = pos.distance_squared(pos_b.0);
1.0 - distance_squared / explosion.radius.powi(2)
};
2021-01-30 22:35:00 +00:00
if strength > 0.0 {
// See if entities are in the same group
let same_group = owner_entity
.and_then(|e| groups.get(e))
.map(|group_a| Some(group_a) == groups.get(entity_b))
.unwrap_or(Some(entity_b) == owner_entity);
let target_group = if same_group {
GroupTarget::InGroup
} else {
GroupTarget::OutOfGroup
};
let dir = Dir::new(
(pos_b.0 - pos)
.try_normalized()
.unwrap_or_else(Vec3::unit_z),
);
2021-02-02 18:02:40 +00:00
let attacker_info =
owner_entity
.zip(owner)
.map(|(entity, uid)| combat::AttackerInfo {
entity,
uid,
group: groups.get(entity),
2021-02-02 18:02:40 +00:00
energy: energies.get(entity),
combo: combos.get(entity),
2021-05-19 01:42:14 +00:00
inventory: inventories.get(entity),
2021-02-02 18:02:40 +00:00
});
let target_info = combat::TargetInfo {
entity: entity_b,
uid: *uid_b,
2021-05-19 01:42:14 +00:00
inventory: inventories.get(entity_b),
stats: stats_b_maybe,
health: Some(health_b),
pos: pos_b.0,
2021-04-10 03:40:20 +00:00
ori: ori_b_maybe,
char_state: char_state_b_maybe,
};
2021-08-27 12:52:52 +00:00
// PvP check
let may_harm = combat::may_harm(
2021-08-27 18:49:58 +00:00
alignments,
players,
uid_allocator,
owner_entity,
entity_b,
);
let attack_options = combat::AttackOptions {
// cool guyz maybe don't look at explosions
// but they still got hurt, it's not Hollywood
target_dodging: false,
may_harm,
2021-01-30 22:35:00 +00:00
target_group,
};
let time = server.state.ecs().read_resource::<Time>();
attack.apply_attack(
2021-02-02 18:02:40 +00:00
attacker_info,
target_info,
2021-01-30 22:35:00 +00:00
dir,
attack_options,
strength,
2021-04-10 03:40:20 +00:00
combat::AttackSource::Explosion,
*time,
2021-11-03 13:26:34 +00:00
|e| emitter.emit(e),
2021-04-04 03:04:02 +00:00
|o| outcomes.push(o),
2021-01-30 22:35:00 +00:00
);
}
2021-01-30 22:35:00 +00:00
}
},
RadiusEffect::Entity(mut effect) => {
2021-08-27 12:52:52 +00:00
let alignments = &ecs.read_storage::<Alignment>();
let uid_allocator = &ecs.read_resource::<UidAllocator>();
let players = &ecs.read_storage::<comp::Player>();
for (entity_b, pos_b, body_b_maybe) in (
&ecs.entities(),
&ecs.read_storage::<comp::Pos>(),
ecs.read_storage::<comp::Body>().maybe(),
)
.join()
2021-01-30 22:35:00 +00:00
{
let strength = if let Some(body) = body_b_maybe {
2021-10-27 19:42:11 +00:00
cylinder_sphere_strength(
pos,
explosion.radius,
explosion.min_falloff,
pos_b.0,
*body,
)
} else {
let distance_squared = pos.distance_squared(pos_b.0);
1.0 - distance_squared / explosion.radius.powi(2)
};
2021-08-27 18:49:58 +00:00
// Player check only accounts for PvP/PvE flag, but bombs
// are intented to do friendly fire.
//
2021-08-27 18:49:58 +00:00
// What exactly is friendly fire is subject to discussion.
// As we probably want to minimize possibility of being dick
// even to your group members, the only exception is when
// you want to harm yourself.
//
// This can be changed later.
let may_harm = || {
combat::may_harm(alignments, players, uid_allocator, owner_entity, entity_b)
|| owner_entity.map_or(true, |entity_a| entity_a == entity_b)
};
if strength > 0.0 {
2021-02-03 05:41:19 +00:00
let is_alive = ecs
.read_storage::<comp::Health>()
.get(entity_b)
.map_or(true, |h| !h.is_dead);
2021-02-03 05:41:19 +00:00
if is_alive {
effect.modify_strength(strength);
if !effect.is_harm() || may_harm() {
server.state().apply_effect(entity_b, effect.clone(), owner);
}
2021-02-03 05:41:19 +00:00
}
2020-11-01 17:15:46 +00:00
}
}
},
}
2020-02-16 20:04:06 +00:00
}
}
2020-06-01 09:21:33 +00:00
2021-11-03 11:15:20 +00:00
pub fn handle_bonk(server: &mut Server, pos: Vec3<f32>, owner: Option<Uid>, target: Option<Uid>) {
2021-08-30 15:02:13 +00:00
let ecs = &server.state.ecs();
let terrain = ecs.read_resource::<TerrainGrid>();
let mut block_change = ecs.write_resource::<BlockChange>();
if let Some(_target) = target {
// TODO: bonk entities but do no damage?
} else {
use common::terrain::SpriteKind;
let pos = pos.map(|e| e.floor() as i32);
if let Some(block) = terrain.get(pos).ok().copied().filter(|b| b.is_bonkable()) {
if let Some(item) = comp::Item::try_reclaim_from_block(block) {
if block_change
.try_set(pos, block.with_sprite(SpriteKind::Empty))
.is_some()
{
drop(terrain);
drop(block_change);
server
2021-08-30 15:02:13 +00:00
.state
2021-09-10 08:34:01 +00:00
.create_object(Default::default(), match block.get_sprite() {
// Create different containers depending on the original sprite
Some(SpriteKind::Apple) => comp::object::Body::Apple,
Some(SpriteKind::Beehive) => comp::object::Body::Hive,
Some(SpriteKind::Coconut) => comp::object::Body::Coconut,
2021-11-03 11:15:20 +00:00
Some(SpriteKind::Bomb) => comp::object::Body::Bomb,
2021-09-10 08:34:01 +00:00
_ => comp::object::Body::Pouch,
})
2021-08-30 15:02:13 +00:00
.with(comp::Pos(pos.map(|e| e as f32) + Vec3::new(0.5, 0.5, 0.0)))
.with(item)
.maybe_with(match block.get_sprite() {
Some(SpriteKind::Bomb) => Some(comp::Object::Bomb { owner }),
_ => None,
})
.build();
2021-08-30 15:02:13 +00:00
}
2021-09-10 08:34:01 +00:00
};
2021-08-30 15:02:13 +00:00
}
}
}
2020-12-04 22:24:56 +00:00
pub fn handle_aura(server: &mut Server, entity: EcsEntity, aura_change: aura::AuraChange) {
let ecs = &server.state.ecs();
let mut auras_all = ecs.write_storage::<comp::Auras>();
if let Some(mut auras) = auras_all.get_mut(entity) {
2020-12-04 22:24:56 +00:00
use aura::AuraChange;
match aura_change {
AuraChange::Add(new_aura) => {
auras.insert(new_aura);
},
AuraChange::RemoveByKey(keys) => {
for key in keys {
auras.remove(key);
}
},
}
}
}
pub fn handle_buff(server: &mut Server, entity: EcsEntity, buff_change: buff::BuffChange) {
let ecs = &server.state.ecs();
let mut buffs_all = ecs.write_storage::<comp::Buffs>();
let bodies = ecs.read_storage::<comp::Body>();
if let Some(mut buffs) = buffs_all.get_mut(entity) {
use buff::BuffChange;
match buff_change {
BuffChange::Add(new_buff) => {
if !bodies
.get(entity)
.map_or(false, |body| body.immune_to(new_buff.kind))
2021-07-02 15:58:08 +00:00
&& ecs
.read_component::<Health>()
.get(entity)
.map_or(true, |h| !h.is_dead)
{
buffs.insert(new_buff);
}
},
BuffChange::RemoveById(ids) => {
for id in ids {
buffs.remove(id);
}
},
BuffChange::RemoveByKind(kind) => {
buffs.remove_kind(kind);
},
BuffChange::RemoveFromController(kind) => {
if kind.is_buff() {
buffs.remove_kind(kind);
}
},
BuffChange::RemoveByCategory {
all_required,
any_required,
none_required,
} => {
let mut ids_to_remove = Vec::new();
for (id, buff) in buffs.buffs.iter() {
let mut required_met = true;
for required in &all_required {
if !buff.cat_ids.iter().any(|cat| cat == required) {
required_met = false;
break;
}
}
let mut any_met = any_required.is_empty();
for any in &any_required {
if buff.cat_ids.iter().any(|cat| cat == any) {
any_met = true;
break;
}
}
let mut none_met = true;
for none in &none_required {
if buff.cat_ids.iter().any(|cat| cat == none) {
none_met = false;
break;
}
}
if required_met && any_met && none_met {
ids_to_remove.push(*id);
}
}
for id in ids_to_remove {
buffs.remove(id);
}
},
}
}
}
2020-10-30 21:49:58 +00:00
pub fn handle_energy_change(server: &Server, entity: EcsEntity, change: f32) {
2020-10-30 21:49:58 +00:00
let ecs = &server.state.ecs();
if let Some(mut energy) = ecs.write_storage::<Energy>().get_mut(entity) {
energy.change_by(change);
2020-10-30 21:49:58 +00:00
}
}
fn handle_exp_gain(
exp_reward: f32,
inventory: &Inventory,
skill_set: &mut SkillSet,
uid: &Uid,
outcomes: &mut Vec<Outcome>,
) {
use comp::inventory::{item::ItemKind, slot::EquipSlot};
// Create hash set of xp pools to consider splitting xp amongst
let mut xp_pools = HashSet::<SkillGroupKind>::new();
// Insert general pool since it is always accessible
xp_pools.insert(SkillGroupKind::General);
// Closure to add xp pool corresponding to weapon type equipped in a particular
// EquipSlot
let mut add_tool_from_slot = |equip_slot| {
2021-06-09 05:14:20 +00:00
let tool_kind = inventory
.equipped(equip_slot)
.and_then(|i| match &i.kind() {
ItemKind::Tool(tool) if tool.kind.gains_combat_xp() => Some(tool.kind),
_ => None,
});
if let Some(weapon) = tool_kind {
// Only adds to xp pools if entity has that skill group available
if skill_set.contains_skill_group(SkillGroupKind::Weapon(weapon)) {
xp_pools.insert(SkillGroupKind::Weapon(weapon));
}
}
};
// Add weapons to xp pools considered
add_tool_from_slot(EquipSlot::ActiveMainhand);
add_tool_from_slot(EquipSlot::ActiveOffhand);
add_tool_from_slot(EquipSlot::InactiveMainhand);
add_tool_from_slot(EquipSlot::InactiveOffhand);
let num_pools = xp_pools.len() as f32;
for pool in xp_pools.iter() {
skill_set.change_experience(*pool, (exp_reward / num_pools).ceil() as i32);
}
outcomes.push(Outcome::ExpChange {
uid: *uid,
exp: exp_reward as i32,
xp_pools,
});
}
2021-02-27 19:55:06 +00:00
pub fn handle_combo_change(server: &Server, entity: EcsEntity, change: i32) {
let ecs = &server.state.ecs();
if let Some(mut combo) = ecs.write_storage::<comp::Combo>().get_mut(entity) {
2021-03-04 20:43:58 +00:00
let time = ecs.read_resource::<Time>();
let mut outcomes = ecs.write_resource::<Vec<Outcome>>();
combo.change_by(change, time.0);
if let Some(uid) = ecs.read_storage::<Uid>().get(entity) {
outcomes.push(Outcome::ComboChange {
uid: *uid,
combo: combo.counter(),
});
2021-02-27 19:55:06 +00:00
}
}
}
pub fn handle_parry(server: &Server, entity: EcsEntity, energy_cost: f32) {
let ecs = &server.state.ecs();
if let Some(mut character) = ecs.write_storage::<comp::CharacterState>().get_mut(entity) {
2021-10-05 00:15:58 +00:00
*character =
CharacterState::Wielding(common::states::wielding::Data { is_sneaking: false });
};
if let Some(mut energy) = ecs.write_storage::<Energy>().get_mut(entity) {
energy.change_by(energy_cost);
}
}
pub fn handle_teleport_to(server: &Server, entity: EcsEntity, target: Uid, max_range: Option<f32>) {
let ecs = &server.state.ecs();
let mut positions = ecs.write_storage::<Pos>();
let target_pos = ecs
.entity_from_uid(target.into())
.and_then(|e| positions.get(e))
.copied();
if let (Some(pos), Some(target_pos)) = (positions.get_mut(entity), target_pos) {
if max_range.map_or(true, |r| pos.0.distance_squared(target_pos.0) < r.powi(2)) {
*pos = target_pos;
ecs.write_storage()
.insert(entity, comp::ForceUpdate)
.err()
.map(|e| {
error!(
?e,
"Error inserting ForceUpdate component when teleporting client"
)
});
}
}
}
/// Intended to handle things that should happen for any successful attack,
/// regardless of the damages and effects specific to that attack
pub fn handle_entity_attacked_hook(server: &Server, entity: EcsEntity) {
let ecs = &server.state.ecs();
let server_eventbus = ecs.read_resource::<EventBus<ServerEvent>>();
let mut outcomes = ecs.write_resource::<Vec<Outcome>>();
if let (Some(mut char_state), Some(mut poise), Some(pos)) = (
ecs.write_storage::<CharacterState>().get_mut(entity),
ecs.write_storage::<Poise>().get_mut(entity),
ecs.read_storage::<Pos>().get(entity),
) {
// Interrupt sprite interaction and item use if any attack is applied to entity
if matches!(
*char_state,
CharacterState::SpriteInteract(_) | CharacterState::UseItem(_)
) {
let poise_state = comp::poise::PoiseState::Dazed;
let was_wielded = char_state.is_wield();
if let (Some(stunned_state), impulse_strength) = poise_state.poise_effect(was_wielded) {
// Reset poise if there is some stunned state to apply
poise.reset();
*char_state = stunned_state;
outcomes.push(Outcome::PoiseChange {
pos: pos.0,
state: poise_state,
});
if let Some(impulse_strength) = impulse_strength {
server_eventbus.emit_now(ServerEvent::Knockback {
entity,
impulse: impulse_strength * *poise.knockback(),
});
}
}
}
}
// Remove potion/saturation buff if attacked
server_eventbus.emit_now(ServerEvent::Buff {
entity,
buff_change: buff::BuffChange::RemoveByKind(buff::BuffKind::Potion),
});
server_eventbus.emit_now(ServerEvent::Buff {
entity,
buff_change: buff::BuffChange::RemoveByKind(buff::BuffKind::Saturation),
});
}