use crate::{ client::Client, comp::{ ability, agent::{Agent, AgentEvent, Sound, SoundKind}, loot_owner::LootOwner, skillset::SkillGroupKind, BuffKind, BuffSource, PhysicsState, }, rtsim::RtSim, sys::terrain::SAFE_ZONE_RADIUS, Server, SpawnPoint, StateExt, }; use authc::Uuid; use common::{ combat, combat::DamageContributor, comp::{ self, aura, buff, chat::{KillSource, KillType}, inventory::item::MaterialStatManifest, loot_owner::LootOwnerKind, Alignment, Auras, Body, CharacterState, Energy, Group, Health, HealthChange, Inventory, Player, Poise, Pos, SkillSet, Stats, }, event::{EventBus, ServerEvent}, outcome::{HealthChangeInfo, Outcome}, resources::{Secs, Time}, rtsim::RtSimEntity, states::utils::{AbilityInfo, StageSection}, terrain::{Block, BlockKind, TerrainGrid}, uid::{Uid, UidAllocator}, util::Dir, vol::ReadVol, Damage, DamageKind, DamageSource, Explosion, GroupTarget, RadiusEffect, }; use common_net::{msg::ServerGeneral, sync::WorldSyncExt}; use common_state::BlockChange; use comp::chat::GenericChatMsg; use hashbrown::HashSet; use rand::{distributions::WeightedIndex, Rng}; use rand_distr::Distribution; use specs::{ join::Join, saveload::MarkerAllocator, Builder, Entity as EcsEntity, Entity, WorldExt, }; use std::{collections::HashMap, iter, ops::Mul, time::Duration}; use tracing::{debug, error}; use vek::{Vec2, Vec3}; #[derive(Hash, Eq, PartialEq)] enum DamageContrib { Solo(EcsEntity), Group(Group), NotFound, } pub fn handle_poise(server: &Server, entity: EcsEntity, change: comp::PoiseChange) { let ecs = &server.state.ecs(); if let Some(character_state) = ecs.read_storage::().get(entity) { // Entity is invincible to poise change during stunned character state if !matches!(character_state, CharacterState::Stunned(_)) { if let Some(mut poise) = ecs.write_storage::().get_mut(entity) { poise.change(change); } } } } pub fn handle_health_change(server: &Server, entity: EcsEntity, change: HealthChange) { let ecs = &server.state.ecs(); if let Some(mut health) = ecs.write_storage::().get_mut(entity) { // If the change amount was not zero let changed = health.change_by(change); if let (Some(pos), Some(uid)) = ( ecs.read_storage::().get(entity), ecs.read_storage::().get(entity), ) { if changed { let outcomes = ecs.write_resource::>(); outcomes.emit_now(Outcome::HealthChange { pos: pos.0, info: HealthChangeInfo { amount: change.amount, by: change.by, target: *uid, cause: change.cause, crit: change.crit, instance: change.instance, }, }); } } } // This if statement filters out anything under 5 damage, for DOT ticks // TODO: Find a better way to separate direct damage from DOT here let damage = -change.amount; if damage > 5.0 { if let Some(agent) = ecs.write_storage::().get_mut(entity) { agent.inbox.push_back(AgentEvent::Hurt); } } } pub fn handle_knockback(server: &Server, entity: EcsEntity, impulse: Vec3) { let ecs = &server.state.ecs(); let clients = ecs.read_storage::(); if let Some(physics) = ecs.read_storage::().get(entity) { //Check if the entity is on a surface. If it is not, reduce knockback. let mut impulse = impulse * if physics.on_surface().is_some() { 1.0 } else { 0.4 }; if let Some(mass) = ecs.read_storage::().get(entity) { // we go easy on the little ones (because they fly so far) impulse /= mass.0.max(40.0); } let mut velocities = ecs.write_storage::(); if let Some(vel) = velocities.get_mut(entity) { vel.0 += impulse; } if let Some(client) = clients.get(entity) { client.send_fallible(ServerGeneral::Knockback(impulse)); } } } /// 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. pub fn handle_destroy(server: &mut Server, entity: EcsEntity, last_change: HealthChange) { 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; } 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::() .get(char_entity) .is_some() { KillSource::Player(by, cause_of_death) } else if let Some(stats) = state.ecs().read_storage::().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::() .get(entity) .is_some() { if let Some(pos) = state.ecs().read_storage::().get(entity) { state .ecs() .read_resource::>() .emit_now(Outcome::Death { pos: pos.0 }); } } // Chat message // If it was a player that died if let Some(_player) = state.ecs().read_storage::().get(entity) { if let Some(uid) = state.ecs().read_storage::().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(), }); } } let mut exp_awards = Vec::<(Entity, f32, Option)>::new(); // 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::(); let healths = state.ecs().read_storage::(); let energies = state.ecs().read_storage::(); let inventories = state.ecs().read_storage::(); let players = state.ecs().read_storage::(); let bodies = state.ecs().read_storage::(); let poises = state.ecs().read_storage::(); let positions = state.ecs().read_storage::(); let groups = state.ecs().read_storage::(); let ( entity_skill_set, entity_health, entity_energy, entity_inventory, entity_body, entity_poise, entity_pos, ) = match (|| { Some(( skill_sets.get(entity)?, healths.get(entity)?, energies.get(entity)?, inventories.get(entity)?, bodies.get(entity)?, poises.get(entity)?, positions.get(entity)?, )) })() { Some(comps) => comps, None => return, }; // Calculate the total EXP award for the kill let msm = state.ecs().read_resource::(); 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::::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::(); let uids = state.ecs().read_storage::(); let mut outcomes = state.ecs().write_resource::>(); let inventories = state.ecs().read_storage::(); 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 exp_awards = 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, None)).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::>(); 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, Some(*group))).collect::)>>()) }, DamageContrib::NotFound => { // Discard exp for dead/offline individual damage contributors None } } }).flatten().collect::)>>(); exp_awards.iter().for_each(|(attacker, exp_reward, _)| { // Process the calculated EXP rewards if let (Some(mut attacker_skill_set), Some(attacker_uid), Some(attacker_inventory)) = ( skill_sets.get_mut(*attacker), uids.get(*attacker), inventories.get(*attacker), ) { handle_exp_gain( *exp_reward, attacker_inventory, &mut attacker_skill_set, attacker_uid, &mut outcomes, ); } }); })(); let should_delete = if state .ecs() .write_storage::() .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")); state .ecs() .write_storage::() .get_mut(entity) .map(|force_update| force_update.update()); state .ecs() .write_storage::() .get_mut(entity) .map(|mut energy| { let energy = &mut *energy; energy.refresh() }); let _ = state .ecs() .write_storage::() .insert(entity, CharacterState::default()); false } else if state.ecs().read_storage::().contains(entity) && !matches!( state.ecs().read_storage::().get(entity), Some(comp::Alignment::Owned(_)) ) { // 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 item = { let mut item_drop = state.ecs().write_storage::(); item_drop.remove(entity).map(|comp::ItemDrop(item)| item) }; if let Some(item) = item { let pos = state.ecs().read_storage::().get(entity).cloned(); let vel = state.ecs().read_storage::().get(entity).cloned(); if let Some(pos) = pos { // Remove entries where zero exp was awarded - this happens because some // entities like Object bodies don't give EXP. let _ = exp_awards.drain_filter(|(_, exp, _)| *exp < f32::EPSILON); let winner = if exp_awards.is_empty() { None } else { // Use the awarded exp per entity as the weight distribution for drop chance // Creating the WeightedIndex can only fail if there are weights <= 0 or no // weights, which shouldn't ever happen let dist = WeightedIndex::new(exp_awards.iter().map(|x| x.1)) .expect("Failed to create WeightedIndex for loot drop chance"); let mut rng = rand::thread_rng(); let winner = exp_awards .get(dist.sample(&mut rng)) .expect("Loot distribution failed to find a winner"); let (winner, group) = (winner.0, winner.2); if let Some(group) = group { Some(LootOwnerKind::Group(group)) } else { let uid = state .ecs() .read_storage::() .get(winner) .and_then(|body| { // Only humanoids are awarded loot ownership - if the winner // was a // non-humanoid NPC the loot will be free-for-all if matches!(body, Body::Humanoid(_)) { Some(state.ecs().read_storage::().get(winner).cloned()) } else { None } }) .flatten(); uid.map(LootOwnerKind::Player) } }; let item_drop_entity = state .create_item_drop(Pos(pos.0 + Vec3::unit_z() * 0.25), item) .maybe_with(vel) .build(); // If there was a loot winner, assign them as the owner of the loot. There will // not be a loot winner when an entity dies to environment damage and such so // the loot will be free-for-all. if let Some(uid) = winner { debug!("Assigned UID {:?} as the winner for the loot drop", uid); state .ecs() .write_storage::() .insert(item_drop_entity, LootOwner::new(uid)) .unwrap(); } } else { error!( ?entity, "Entity doesn't have a position, no bag is being dropped" ) } } true } else { true }; if should_delete { if let Some(rtsim_entity) = state .ecs() .read_storage::() .get(entity) .copied() { state .ecs() .write_resource::() .destroy_entity(rtsim_entity.0); } if let Err(e) = state.delete_entity_recorded(entity) { error!(?e, ?entity, "Failed to delete destroyed entity"); } } } /// 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")); } pub fn handle_land_on_ground(server: &Server, entity: EcsEntity, vel: Vec3) { let ecs = server.state.ecs(); if vel.z <= -30.0 { let char_states = ecs.read_storage::(); let reduced_vel_z = if let Some(CharacterState::DiveMelee(c)) = char_states.get(entity) { (vel.z + c.static_data.vertical_speed).min(0.0) } else { vel.z }; let mass = ecs .read_storage::() .get(entity) .copied() .unwrap_or_default(); let impact_energy = mass.0 * reduced_vel_z.powi(2) / 2.0; let falldmg = impact_energy / 1000.0; let inventories = ecs.read_storage::(); let stats = ecs.read_storage::(); let time = ecs.read_resource::