use crate::{ comp::{ group, Attacking, Body, CharacterState, Damage, DamageSource, HealthChange, HealthSource, Loadout, Ori, Pos, Scale, Stats, }, event::{EventBus, LocalEvent, ServerEvent}, metrics::SysMetrics, span, sync::Uid, util::Dir, }; use specs::{Entities, Join, Read, ReadExpect, ReadStorage, System, WriteStorage}; use vek::*; pub const BLOCK_EFFICIENCY: f32 = 0.9; pub const BLOCK_ANGLE: f32 = 180.0; /// This system is responsible for handling accepted inputs like moving or /// attacking pub struct Sys; impl<'a> System<'a> for Sys { #[allow(clippy::type_complexity)] type SystemData = ( Entities<'a>, Read<'a, EventBus>, Read<'a, EventBus>, ReadExpect<'a, SysMetrics>, ReadStorage<'a, Uid>, ReadStorage<'a, Pos>, ReadStorage<'a, Ori>, ReadStorage<'a, Scale>, ReadStorage<'a, Body>, ReadStorage<'a, Stats>, ReadStorage<'a, Loadout>, ReadStorage<'a, group::Group>, ReadStorage<'a, CharacterState>, WriteStorage<'a, Attacking>, ); fn run( &mut self, ( entities, server_bus, local_bus, sys_metrics, uids, positions, orientations, scales, bodies, stats, loadouts, groups, character_states, mut attacking_storage, ): Self::SystemData, ) { let start_time = std::time::Instant::now(); span!(_guard, "run", "combat::Sys::run"); let mut server_emitter = server_bus.emitter(); let mut _local_emitter = local_bus.emitter(); // Attacks for (entity, uid, pos, ori, scale_maybe, attack) in ( &entities, &uids, &positions, &orientations, scales.maybe(), &mut attacking_storage, ) .join() { if attack.applied { continue; } attack.applied = true; // Go through all other entities for (b, uid_b, pos_b, ori_b, scale_b_maybe, character_b, stats_b, body_b) in ( &entities, &uids, &positions, &orientations, scales.maybe(), character_states.maybe(), &stats, &bodies, ) .join() { // Scales let scale = scale_maybe.map_or(1.0, |s| s.0); let scale_b = scale_b_maybe.map_or(1.0, |s| s.0); let rad_b = body_b.radius() * scale_b; // Check if it is a damaging hit if entity != b && !stats_b.is_dead && ((attack.is_melee && cylindrical_hit_detection( *pos, *ori, *pos_b, rad_b, scale, attack.range, attack.max_angle, )) || (!attack.is_melee && spherical_hit_detection( *pos, *pos_b, rad_b, scale, attack.range, attack.max_angle, attack.look_dir.unwrap_or(*ori.0), ))) { // See if entities are in the same group let same_group = groups .get(entity) .map(|group_a| Some(group_a) == groups.get(b)) .unwrap_or(false); // Don't heal if outside group // Don't damage in the same group let (mut is_heal, mut is_damage) = (false, false); if !same_group && (attack.base_damage > 0) { is_damage = true; } if same_group && (attack.base_heal > 0) { is_heal = true; } if !is_heal && !is_damage { continue; } // Weapon gives base damage let source = if is_heal { DamageSource::Healing } else if attack.is_melee { DamageSource::Melee } else { DamageSource::Energy }; let healthchange = if is_heal { attack.base_heal as f32 } else { -(attack.base_damage as f32) }; let mut damage = Damage { healthchange, source, }; let block = character_b.map(|c_b| c_b.is_block()).unwrap_or(false) && ori_b.0.angle_between(pos.0 - pos_b.0) < BLOCK_ANGLE.to_radians() / 2.0; if let Some(loadout) = loadouts.get(b) { damage.modify_damage(block, loadout); } if damage.healthchange != 0.0 { if is_damage || stats_b.health.current() != stats_b.health.maximum() { let cause = if is_heal { HealthSource::Healing { by: Some(*uid) } } else { HealthSource::Attack { by: *uid } }; server_emitter.emit(ServerEvent::Damage { uid: *uid_b, change: HealthChange { amount: damage.healthchange as i32, cause, }, }); if attack.lifesteal_eff > 0.0 && is_damage { server_emitter.emit(ServerEvent::Damage { uid: *uid, change: HealthChange { amount: (-damage.healthchange * attack.lifesteal_eff) as i32, cause: HealthSource::Healing { by: Some(*uid) }, }, }); } attack.hit_count += 1; } } if attack.knockback != 0.0 && damage.healthchange != 0.0 { let kb_dir = Dir::new((pos_b.0 - pos.0).try_normalized().unwrap_or(*ori.0)); server_emitter.emit(ServerEvent::Knockback { entity: b, impulse: attack.knockback * *Dir::slerp(kb_dir, Dir::new(Vec3::new(0.0, 0.0, 1.0)), 0.5), }); } } } } sys_metrics.combat_ns.store( start_time.elapsed().as_nanos() as i64, std::sync::atomic::Ordering::Relaxed, ); } } fn cylindrical_hit_detection( pos: Pos, ori: Ori, pos_b: Pos, rad_b: f32, scale: f32, range: f32, angle: f32, ) -> bool { // 2D versions let pos2 = Vec2::from(pos.0); let pos_b2 = Vec2::::from(pos_b.0); let ori2 = Vec2::from(*ori.0); return pos.0.distance_squared(pos_b.0) < (rad_b + scale * range).powi(2) && ori2.angle_between(pos_b2 - pos2) < angle + (rad_b / pos2.distance(pos_b2)).atan(); } fn spherical_hit_detection( pos: Pos, pos_b: Pos, rad_b: f32, scale: f32, range: f32, angle: f32, ori: Vec3, ) -> bool { return pos.0.distance_squared(pos_b.0) < (rad_b + scale * range).powi(2) && ori.angle_between(pos_b.0 - pos.0) < angle + (rad_b / pos.0.distance(pos_b.0)).atan(); }