use crate::{ client::Client, comp::{ agent::{Sound, SoundKind}, biped_large, quadruped_low, quadruped_medium, quadruped_small, skills::SkillGroupKind, theropod, PhysicsState, }, rtsim::RtSim, Server, SpawnPoint, StateExt, }; use common::{ assets::AssetExt, combat, comp::{ self, aura, buff, chat::{KillSource, KillType}, inventory::item::MaterialStatManifest, object, Alignment, Body, CharacterState, Energy, EnergyChange, Group, Health, HealthChange, HealthSource, Inventory, Player, Poise, PoiseChange, PoiseSource, Pos, SkillSet, Stats, }, event::{EventBus, ServerEvent}, lottery::{LootSpec, Lottery}, outcome::Outcome, resources::Time, rtsim::RtSimEntity, terrain::{Block, 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 specs::{join::Join, saveload::MarkerAllocator, Entity as EcsEntity, WorldExt}; use tracing::error; use vek::{Vec2, Vec3}; pub fn handle_poise( server: &Server, entity: EcsEntity, change: PoiseChange, knockback_dir: Vec3, ) { let ecs = &server.state.ecs(); if let Some(character_state) = ecs.read_storage::().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::().get_mut(entity) { poise.change_by(change, knockback_dir); } } } } pub fn handle_damage(server: &Server, entity: EcsEntity, change: HealthChange) { let ecs = &server.state.ecs(); if let Some(mut health) = ecs.write_storage::().get_mut(entity) { health.change_by(change); } } 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. // 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. #[allow(clippy::needless_collect)] pub fn handle_destroy(server: &mut Server, entity: EcsEntity, cause: HealthSource) { 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) } }; // 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 cause { HealthSource::Damage { kind: DamageSource::Melee, by: Some(by), } => get_attacker_name(KillType::Melee, by), HealthSource::Damage { kind: DamageSource::Projectile, by: Some(by), } => { // TODO: add names to projectiles and send in message get_attacker_name(KillType::Projectile, by) }, HealthSource::Damage { kind: DamageSource::Explosion, by: Some(by), } => get_attacker_name(KillType::Explosion, by), HealthSource::Damage { kind: DamageSource::Energy, by: Some(by), } => get_attacker_name(KillType::Energy, by), HealthSource::Damage { kind: DamageSource::Buff(buff_kind), by: Some(by), } => get_attacker_name(KillType::Buff(buff_kind), by), HealthSource::Damage { kind: DamageSource::Other, by: Some(by), } => get_attacker_name(KillType::Other, by), HealthSource::World => KillSource::FallDamage, HealthSource::Suicide => KillSource::Suicide, HealthSource::Damage { .. } | HealthSource::Revive | HealthSource::Command | HealthSource::LevelUp | HealthSource::Item | HealthSource::Heal { by: _ } | HealthSource::Unknown => KillSource::Other, }; state.send_chat(GenericChatMsg { chat_type: comp::ChatType::Kill(kill_source, *uid), message: "".to_string(), }); } } // Give EXP to the killer if entity had stats (|| { let mut skill_set = state.ecs().write_storage::(); let healths = state.ecs().read_storage::(); let inventories = state.ecs().read_storage::(); let players = state.ecs().read_storage::(); let bodies = state.ecs().read_storage::(); let by = if let HealthSource::Damage { by: Some(by), .. } = cause { by } else { return; }; let attacker = if let Some(attacker) = state.ecs().entity_from_uid(by.into()) { attacker } else { return; }; let (entity_skill_set, entity_health, entity_inventory, entity_body) = if let ( Some(entity_skill_set), Some(entity_health), Some(entity_inventory), Some(entity_body), ) = ( skill_set.get(entity), healths.get(entity), inventories.get(entity), bodies.get(entity), ) { ( entity_skill_set, entity_health, entity_inventory, entity_body, ) } else { return; }; let groups = state.ecs().read_storage::(); 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::(); let mut exp_reward = combat::combat_rating( entity_inventory, entity_health, entity_skill_set, *entity_body, &msm, ) * 2.5; // Distribute EXP to group let positions = state.ecs().read_storage::(); 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::(); if let (Some(attacker_group), Some(pos)) = (attacker_group, positions.get(entity)) { // TODO: rework if change to groups makes it easier to iterate entities in a // group let mut non_pet_group_members_in_range = 1; let members_in_range = ( &state.ecs().entities(), &groups, &positions, alignments.maybe(), &uids, ) .join() .filter(|(entity, group, member_pos, _, _)| { // Check if: in group, not main attacker, and in range *group == attacker_group && *entity != attacker && pos.0.distance_squared(member_pos.0) < MAX_EXP_DIST.powi(2) }) .map(|(entity, _, _, alignment, uid)| { if !matches!(alignment, Some(Alignment::Owned(owner)) if owner != uid) { non_pet_group_members_in_range += 1; } (entity, uid) }) .collect::>(); // 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 let (Some(mut attacker_skill_set), Some(attacker_uid), Some(attacker_inventory)) = ( skill_set.get_mut(attacker), uids.get(attacker), inventories.get(attacker), ) { // TODO: Discuss whether we should give EXP by Player Killing or not. handle_exp_gain( exp_reward, attacker_inventory, &mut attacker_skill_set, attacker_uid, &mut outcomes, ); } })(); 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() .insert(entity, comp::ForceUpdate) .err() .map(|e| error!(?e, ?entity, "Failed to insert ForceUpdate on dead client")); state .ecs() .write_storage::() .remove(entity); state .ecs() .write_storage::() .get_mut(entity) .map(|mut energy| { let energy = &mut *energy; energy.set_to(energy.maximum(), comp::EnergySource::Revive) }); let _ = state .ecs() .write_storage::() .insert(entity, comp::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) use specs::Builder; // Decide for a loot drop before turning into a lootbag let old_body = state.ecs().write_storage::().remove(entity); let lottery = || { Lottery::::load_expect(match old_body { Some(common::comp::Body::Humanoid(_)) => "common.loot_tables.creature.humanoid", Some(common::comp::Body::QuadrupedSmall(quadruped_small)) => { match quadruped_small.species { quadruped_small::Species::Dodarock => { "common.loot_tables.creature.quad_small.dodarock" }, _ => "common.loot_tables.creature.quad_small.default", } }, Some(Body::QuadrupedMedium(quadruped_medium)) => match quadruped_medium.species { quadruped_medium::Species::Frostfang | quadruped_medium::Species::Roshwalr => { "common.loot_tables.creature.quad_medium.ice" }, _ => "common.loot_tables.creature.quad_medium.default", }, Some(common::comp::Body::BirdMedium(_)) => { "common.loot_tables.creature.bird_medium" }, Some(common::comp::Body::BirdLarge(_)) => "common.loot_tables.creature.bird_medium", Some(common::comp::Body::FishMedium(_)) => "common.loot_tables.creature.fish", Some(common::comp::Body::FishSmall(_)) => "common.loot_tables.creature.fish", Some(common::comp::Body::BipedLarge(biped_large)) => match biped_large.species { biped_large::Species::Wendigo => { "common.loot_tables.creature.biped_large.wendigo" }, biped_large::Species::Troll => "common.loot_tables.creature.biped_large.troll", biped_large::Species::Occultsaurok | biped_large::Species::Mightysaurok | biped_large::Species::Slysaurok => { "common.loot_tables.creature.biped_large.saurok" }, _ => "common.loot_tables.creature.biped_large.default", }, Some(common::comp::Body::Golem(_)) => "common.loot_tables.creature.golem", Some(common::comp::Body::Theropod(theropod)) => match theropod.species { theropod::Species::Sandraptor | theropod::Species::Snowraptor | theropod::Species::Woodraptor => { "common.loot_tables.creature.theropod.raptor" }, _ => "common.loot_tables.creature.theropod.default", }, Some(common::comp::Body::Dragon(_)) => "common.loot_tables.creature.dragon", Some(common::comp::Body::QuadrupedLow(quadruped_low)) => { match quadruped_low.species { quadruped_low::Species::Maneater => { "common.loot_tables.creature.quad_low.maneater" }, _ => "common.loot_tables.creature.quad_low.default", } }, _ => "common.loot_tables.fallback", }) }; let item = { let mut item_drops = state.ecs().write_storage::(); item_drops.remove(entity).map_or_else( || lottery().read().choose().to_item(), |item_drop| item_drop.0, ) }; let pos = state.ecs().read_storage::().get(entity).cloned(); let vel = state.ecs().read_storage::().get(entity).cloned(); if let Some(pos) = pos { 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(_)) => object::Body::Pouch, Some(common::comp::Body::Golem(_)) => object::Body::Chest, Some(common::comp::Body::BipedLarge(_)) | Some(common::comp::Body::QuadrupedLow(_)) => object::Body::MeatDrop, _ => object::Body::Steak, }) .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 { if let Some(rtsim_entity) = state .ecs() .read_storage::() .get(entity) .copied() { state .ecs() .write_resource::() .destroy_entity(rtsim_entity.0); } let _ = state .delete_entity_recorded(entity) .map_err(|e| error!(?e, ?entity, "Failed to delete destroyed entity")); } // 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"); } */ } /// 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 mass = ecs .read_storage::() .get(entity) .copied() .unwrap_or_default(); let impact_energy = mass.0 * vel.z.powi(2) / 2.0; let falldmg = impact_energy / 100.0; let inventories = ecs.read_storage::(); let stats = ecs.read_storage::(); // Handle health change if let Some(mut health) = ecs.write_storage::().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 change = damage.calculate_health_change(damage_reduction, None, false, 0.0, 1.0); health.change_by(change); } // Handle poise change if let Some(mut poise) = ecs.write_storage::().get_mut(entity) { let poise_damage = PoiseChange { amount: -(mass.0 * vel.magnitude_squared() / 1500.0) as i32, source: PoiseSource::Falling, }; let poise_change = poise_damage.modify_poise_damage(inventories.get(entity)); poise.change_by(poise_change, Vec3::unit_z()); } } } pub fn handle_respawn(server: &Server, entity: EcsEntity) { let state = &server.state; // Only clients can respawn if state .ecs() .write_storage::() .get_mut(entity) .is_some() { let respawn_point = state .read_component_copied::(entity) .map(|wp| wp.get_pos()) .unwrap_or(state.ecs().read_resource::().0); state .ecs() .write_storage::() .get_mut(entity) .map(|mut health| health.revive()); state .ecs() .write_storage::() .get_mut(entity) .map(|pos| pos.0 = respawn_point); state .ecs() .write_storage() .insert(entity, comp::ForceUpdate) .err() .map(|e| { error!( ?e, "Error inserting ForceUpdate component when respawning client" ) }); } } #[allow(clippy::blocks_in_if_conditions)] pub fn handle_explosion(server: &Server, pos: Vec3, explosion: Explosion, owner: Option) { // Go through all other entities let ecs = &server.state.ecs(); let server_eventbus = ecs.read_resource::>(); let time = ecs.read_resource::