diff --git a/common/net/src/synced_components.rs b/common/net/src/synced_components.rs index d8d5bf858b..4011bcecbc 100755 --- a/common/net/src/synced_components.rs +++ b/common/net/src/synced_components.rs @@ -43,7 +43,7 @@ macro_rules! synced_components { character_state: CharacterState, character_activity: CharacterActivity, shockwave: Shockwave, - beam_segment: BeamSegment, + beam: Beam, alignment: Alignment, stance: Stance, // TODO: change this to `SyncFrom::ClientEntity` and sync the bare minimum @@ -214,7 +214,7 @@ impl NetSync for Shockwave { const SYNC_FROM: SyncFrom = SyncFrom::AnyEntity; } -impl NetSync for BeamSegment { +impl NetSync for Beam { const SYNC_FROM: SyncFrom = SyncFrom::AnyEntity; } diff --git a/common/src/comp/ability.rs b/common/src/comp/ability.rs index aea3d75b57..06ae998cc4 100644 --- a/common/src/comp/ability.rs +++ b/common/src/comp/ability.rs @@ -902,7 +902,7 @@ pub enum CharacterAbility { BasicBeam { buildup_duration: f32, recover_duration: f32, - beam_duration: f32, + beam_duration: f64, damage: f32, tick_rate: f32, range: f32, @@ -1536,7 +1536,7 @@ impl CharacterAbility { *tick_rate *= stats.speed; *range *= stats.range; // Duration modified to keep velocity constant - *beam_duration *= stats.range; + *beam_duration *= stats.range as f64; *energy_drain /= stats.energy_efficiency; *damage_effect = damage_effect.map(|de| de.adjusted_by_stats(stats)); }, @@ -2127,7 +2127,7 @@ impl CharacterAbility { let range_mod = modifiers.range.powi(level.into()); *range *= range_mod; // Duration modified to keep velocity constant - *beam_duration *= range_mod; + *beam_duration *= range_mod as f64; } if let Ok(level) = skillset.skill_level(Staff(FDrain)) { *energy_drain *= modifiers.energy_drain.powi(level.into()); @@ -2135,7 +2135,7 @@ impl CharacterAbility { if let Ok(level) = skillset.skill_level(Staff(FVelocity)) { let velocity_increase = modifiers.velocity.powi(level.into()); let duration_mod = 1.0 / (1.0 + velocity_increase); - *beam_duration *= duration_mod; + *beam_duration *= duration_mod as f64; } }, CharacterAbility::Shockwave { @@ -2185,7 +2185,7 @@ impl CharacterAbility { let range_mod = modifiers.range.powi(level.into()); *range *= range_mod; // Duration modified to keep velocity constant - *beam_duration *= range_mod; + *beam_duration *= range_mod as f64; } if let Ok(level) = skillset.skill_level(Sceptre(LRegen)) { *energy_regen *= modifiers.energy_regen.powi(level.into()); @@ -2736,11 +2736,11 @@ impl From<(&CharacterAbility, AbilityInfo, &JoinData<'_>)> for CharacterState { static_data: basic_beam::StaticData { buildup_duration: Duration::from_secs_f32(*buildup_duration), recover_duration: Duration::from_secs_f32(*recover_duration), - beam_duration: Duration::from_secs_f32(*beam_duration), + beam_duration: Secs(*beam_duration), damage: *damage, tick_rate: *tick_rate, range: *range, - max_angle: *max_angle, + end_radius: max_angle.to_radians().tan() * *range, damage_effect: *damage_effect, energy_regen: *energy_regen, energy_drain: *energy_drain, @@ -2750,6 +2750,8 @@ impl From<(&CharacterAbility, AbilityInfo, &JoinData<'_>)> for CharacterState { }, timer: Duration::default(), stage_section: StageSection::Buildup, + aim_dir: data.ori.look_dir(), + beam_offset: data.pos.0, }), CharacterAbility::BasicAura { buildup_duration, diff --git a/common/src/comp/beam.rs b/common/src/comp/beam.rs index 701a4b9c84..20f5147996 100644 --- a/common/src/comp/beam.rs +++ b/common/src/comp/beam.rs @@ -1,48 +1,23 @@ -use crate::{combat::Attack, uid::Uid}; +use crate::{combat::Attack, resources::Secs}; use serde::{Deserialize, Serialize}; -use specs::{Component, DerefFlaggedStorage}; -use std::time::Duration; +use specs::{Component, DerefFlaggedStorage, Entity as EcsEntity}; +use vek::*; #[derive(Clone, Debug, Serialize, Deserialize)] -pub struct Properties { - pub attack: Attack, - pub angle: f32, - pub speed: f32, - pub duration: Duration, - pub owner: Option, - pub specifier: FrontendSpecifier, -} - -// TODO: Separate components out for cheaper network syncing -#[derive(Clone, Debug, Serialize, Deserialize)] -pub struct BeamSegment { - pub properties: Properties, - #[serde(skip)] - /// Time that the beam segment was created at - /// Used to calculate beam propagation - /// Deserialized from the network as `None` - pub creation: Option, -} - -impl Component for BeamSegment { - type Storage = DerefFlaggedStorage>; -} - -impl std::ops::Deref for BeamSegment { - type Target = Properties; - - fn deref(&self) -> &Properties { &self.properties } -} - -#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] pub struct Beam { - pub hit_entities: Vec, - pub tick_dur: Duration, - pub timer: Duration, + pub attack: Attack, + pub end_radius: f32, + pub range: f32, + pub duration: Secs, + pub tick_dur: Secs, + pub specifier: FrontendSpecifier, + pub bezier: QuadraticBezier3, + #[serde(skip)] + pub hit_entities: Vec, } impl Component for Beam { - type Storage = specs::DenseVecStorage; + type Storage = DerefFlaggedStorage>; } #[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Eq)] diff --git a/common/src/comp/character_state.rs b/common/src/comp/character_state.rs index a368210804..134810a01b 100644 --- a/common/src/comp/character_state.rs +++ b/common/src/comp/character_state.rs @@ -391,6 +391,8 @@ impl CharacterState { matches!(self.attack_kind(), Some(AttackSource::Melee)) } + pub fn is_beam_attack(&self) -> bool { matches!(self.attack_kind(), Some(AttackSource::Beam)) } + pub fn can_perform_mounted(&self) -> bool { matches!( self, diff --git a/common/src/comp/mod.rs b/common/src/comp/mod.rs index fac2ac29e8..675499e265 100644 --- a/common/src/comp/mod.rs +++ b/common/src/comp/mod.rs @@ -50,7 +50,7 @@ pub use self::{ }, anchor::Anchor, aura::{Aura, AuraChange, AuraKind, Auras}, - beam::{Beam, BeamSegment}, + beam::Beam, body::{ arthropod, biped_large, biped_small, bird_large, bird_medium, dragon, fish_medium, fish_small, golem, humanoid, item_drop, object, quadruped_low, quadruped_medium, diff --git a/common/src/event.rs b/common/src/event.rs index 94601da3ea..bcfd379015 100644 --- a/common/src/event.rs +++ b/common/src/event.rs @@ -182,11 +182,6 @@ pub enum ServerEvent { entity: EcsEntity, impulse: Vec3, }, - BeamSegment { - properties: comp::beam::Properties, - pos: Pos, - ori: Ori, - }, LandOnGround { entity: EcsEntity, vel: Vec3, diff --git a/common/src/states/basic_beam.rs b/common/src/states/basic_beam.rs index eac043a41e..216a79e2fe 100644 --- a/common/src/states/basic_beam.rs +++ b/common/src/states/basic_beam.rs @@ -5,16 +5,16 @@ use crate::{ }, comp::{ beam, body::biped_large, character_state::OutputEvents, object::Body::Flamethrower, Body, - CharacterState, Ori, Pos, StateUpdate, + CharacterState, Ori, StateUpdate, }, - event::{LocalEvent, ServerEvent}, + event::LocalEvent, outcome::Outcome, + resources::Secs, states::{ behavior::{CharacterBehavior, JoinData}, utils::*, }, terrain::Block, - uid::Uid, util::Dir, }; use serde::{Deserialize, Serialize}; @@ -28,16 +28,17 @@ pub struct StaticData { pub buildup_duration: Duration, /// How long the state has until exiting pub recover_duration: Duration, - /// How long each beam segment persists for - pub beam_duration: Duration, + /// Time required for beam to travel from start pos to end pos + pub beam_duration: Secs, /// Base damage per tick pub damage: f32, /// Ticks per second pub tick_rate: f32, /// Max range pub range: f32, - /// Max angle (45.0 will give you a 90.0 angle window) - pub max_angle: f32, + /// The radius at the far distance of the beam. Radius linearly increases + /// from 0 moving from start pos to end po. + pub end_radius: f32, /// Adds an effect onto the main damage of the attack pub damage_effect: Option, /// Energy regenerated per tick @@ -61,6 +62,10 @@ pub struct Data { pub timer: Duration, /// What section the character stage is in pub stage_section: StageSection, + /// Direction that beam should be aimed in + pub aim_dir: Dir, + /// Offset for beam start pos + pub beam_offset: Vec3, } impl CharacterBehavior for Data { @@ -93,11 +98,47 @@ impl CharacterBehavior for Data { } }; } else { + let attack = { + let energy = AttackEffect::new( + None, + CombatEffect::EnergyReward(self.static_data.energy_regen), + ) + .with_requirement(CombatRequirement::AnyDamage); + let mut damage = AttackDamage::new( + Damage { + source: DamageSource::Energy, + kind: DamageKind::Energy, + value: self.static_data.damage, + }, + Some(GroupTarget::OutOfGroup), + rand::random(), + ); + if let Some(effect) = self.static_data.damage_effect { + damage = damage.with_effect(effect); + } + let (crit_chance, crit_mult) = + get_crit_data(data, self.static_data.ability_info); + Attack::default() + .with_damage(damage) + .with_crit(crit_chance, crit_mult) + .with_effect(energy) + .with_combo_increment() + }; + // Creates beam data.updater.insert(data.entity, beam::Beam { - hit_entities: Vec::::new(), - tick_dur: Duration::from_secs_f32(1.0 / self.static_data.tick_rate), - timer: Duration::default(), + attack, + end_radius: self.static_data.end_radius, + range: self.static_data.range, + duration: self.static_data.beam_duration, + tick_dur: Secs(1.0 / self.static_data.tick_rate as f64), + hit_entities: Vec::new(), + specifier: self.static_data.specifier, + bezier: QuadraticBezier3 { + start: data.pos.0, + ctrl: data.pos.0, + end: data.pos.0, + }, }); // Build up update.character = CharacterState::BasicBeam(Data { @@ -112,42 +153,6 @@ impl CharacterBehavior for Data { && (self.static_data.energy_drain <= f32::EPSILON || update.energy.current() > 0.0) { - let speed = - self.static_data.range / self.static_data.beam_duration.as_secs_f32(); - - let energy = AttackEffect::new( - None, - CombatEffect::EnergyReward(self.static_data.energy_regen), - ) - .with_requirement(CombatRequirement::AnyDamage); - let mut damage = AttackDamage::new( - Damage { - source: DamageSource::Energy, - kind: DamageKind::Energy, - value: self.static_data.damage, - }, - Some(GroupTarget::OutOfGroup), - rand::random(), - ); - if let Some(effect) = self.static_data.damage_effect { - damage = damage.with_effect(effect); - } - let (crit_chance, crit_mult) = - get_crit_data(data, self.static_data.ability_info); - let attack = Attack::default() - .with_damage(damage) - .with_crit(crit_chance, crit_mult) - .with_effect(energy) - .with_combo_increment(); - - let properties = beam::Properties { - attack, - angle: self.static_data.max_angle.to_radians(), - speed, - duration: self.static_data.beam_duration, - owner: Some(*data.uid), - specifier: self.static_data.specifier, - }; let beam_ori = { // We want Beam to use Ori of owner. // But we also want beam to use Z part of where owner looks. @@ -184,15 +189,10 @@ impl CharacterBehavior for Data { rel_vel, data.physics.on_ground, ); - let pos = Pos(data.pos.0 + body_offsets); - // Create beam segment - output_events.emit_server(ServerEvent::BeamSegment { - properties, - pos, - ori: beam_ori, - }); update.character = CharacterState::BasicBeam(Data { + beam_offset: body_offsets, + aim_dir: beam_ori.look_dir(), timer: tick_attack_or_default(data, self.timer, None), ..*self }); diff --git a/common/state/src/state.rs b/common/state/src/state.rs index b06c6fe5c3..415f6afec9 100644 --- a/common/state/src/state.rs +++ b/common/state/src/state.rs @@ -213,7 +213,7 @@ impl State { ecs.register::(); ecs.register::(); ecs.register::(); - ecs.register::(); + ecs.register::(); ecs.register::(); ecs.register::(); ecs.register::(); @@ -261,7 +261,6 @@ impl State { ecs.register::(); ecs.register::(); ecs.register::(); - ecs.register::(); ecs.register::(); // Register synced resources used by the ECS. diff --git a/common/systems/src/beam.rs b/common/systems/src/beam.rs index c8893c8512..7f097a7590 100644 --- a/common/systems/src/beam.rs +++ b/common/systems/src/beam.rs @@ -2,8 +2,8 @@ use common::{ combat::{self, AttackOptions, AttackSource, AttackerInfo, TargetInfo}, comp::{ agent::{Sound, SoundKind}, - Alignment, Beam, BeamSegment, Body, Buffs, CharacterState, Combo, Energy, Group, Health, - Inventory, Ori, Player, Pos, Scale, Stats, + Alignment, Beam, Body, Buffs, CharacterState, Combo, Energy, Group, Health, Inventory, Ori, + Player, Pos, Scale, Stats, }, event::{EventBus, ServerEvent}, outcome::Outcome, @@ -17,10 +17,9 @@ use common_ecs::{Job, Origin, ParMode, Phase, System}; use rand::Rng; use rayon::iter::ParallelIterator; use specs::{ - shred::ResourceId, Entities, Join, LendJoin, ParJoin, Read, ReadExpect, ReadStorage, - SystemData, World, WriteStorage, + shred::ResourceId, Entities, LendJoin, ParJoin, Read, ReadExpect, ReadStorage, SystemData, + World, WriteStorage, }; -use std::time::Duration; use vek::*; #[derive(SystemData)] @@ -47,32 +46,52 @@ pub struct ReadData<'a> { combos: ReadStorage<'a, Combo>, character_states: ReadStorage<'a, CharacterState>, buffs: ReadStorage<'a, Buffs>, + outcomes: Read<'a, EventBus>, } /// This system is responsible for handling beams that heal or do damage #[derive(Default)] pub struct Sys; impl<'a> System<'a> for Sys { - type SystemData = ( - ReadData<'a>, - WriteStorage<'a, BeamSegment>, - WriteStorage<'a, Beam>, - Read<'a, EventBus>, - ); + type SystemData = (ReadData<'a>, WriteStorage<'a, Beam>); const NAME: &'static str = "beam"; const ORIGIN: Origin = Origin::Common; const PHASE: Phase = Phase::Create; - fn run( - job: &mut Job, - (read_data, mut beam_segments, mut beams, outcomes): Self::SystemData, - ) { + fn run(job: &mut Job, (read_data, mut beams): Self::SystemData) { let mut server_emitter = read_data.server_bus.emitter(); - let mut outcomes_emitter = outcomes.emitter(); + let mut outcomes_emitter = read_data.outcomes.emitter(); - let time = read_data.time.0; - let dt = read_data.dt.0; + ( + &read_data.positions, + &read_data.orientations, + &read_data.character_states, + &mut beams, + ) + .lend_join() + .for_each(|(pos, ori, char_state, mut beam)| { + // Clear hit entities list if list should be cleared + if read_data.time.0 % beam.tick_dur.0 < read_data.dt.0 as f64 { + beam.hit_entities.clear(); + } + // Update start, end, and control positions of beam bezier + let (offset, target_dir) = if let CharacterState::BasicBeam(c) = char_state { + (c.beam_offset, c.aim_dir) + } else { + (Vec3::zero(), ori.look_dir()) + }; + beam.bezier.start = pos.0 + offset; + const REL_CTRL_DIST: f32 = 0.3; + let target_ctrl = beam.bezier.start + *target_dir * beam.range * REL_CTRL_DIST; + let ctrl_translate = (target_ctrl - beam.bezier.ctrl) * read_data.dt.0 + / (beam.duration.0 as f32 * REL_CTRL_DIST); + beam.bezier.ctrl += ctrl_translate; + let target_end = beam.bezier.start + *target_dir * beam.range; + let end_translate = + (target_end - beam.bezier.end) * read_data.dt.0 / beam.duration.0 as f32; + beam.bezier.end += end_translate; + }); job.cpu_stats.measure(ParMode::Rayon); @@ -81,66 +100,32 @@ impl<'a> System<'a> for Sys { &read_data.entities, &read_data.positions, &read_data.orientations, - &beam_segments, + &read_data.uids, + &beams, ) .par_join() .fold( || (Vec::new(), Vec::new(), Vec::new()), |(mut server_events, mut add_hit_entities, mut outcomes), - (entity, pos, ori, beam_segment)| { - let creation_time = match beam_segment.creation { - Some(time) => time, - // Skip newly created beam segments - None => return (server_events, add_hit_entities, outcomes), - }; - let end_time = creation_time + beam_segment.duration.as_secs_f64(); - - let beam_owner = beam_segment - .owner - .and_then(|uid| read_data.id_maps.uid_entity(uid)); - + (entity, pos, ori, uid, beam)| { // Note: rayon makes it difficult to hold onto a thread-local RNG, if grabbing // this becomes a bottleneck we can look into alternatives. let mut rng = rand::thread_rng(); if rng.gen_bool(0.005) { server_events.push(ServerEvent::Sound { - sound: Sound::new(SoundKind::Beam, pos.0, 13.0, time), + sound: Sound::new(SoundKind::Beam, pos.0, 13.0, read_data.time.0), }); } - // If beam segment is out of time emit destroy event but still continue since it - // may have traveled and produced effects a bit before reaching its end point - if end_time < time { - server_events.push(ServerEvent::Delete(entity)); - } - - // Determine area that was covered by the beam in the last tick - let frame_time = dt.min((end_time - time) as f32); - if frame_time <= 0.0 { - return (server_events, add_hit_entities, outcomes); - } - // Note: min() probably unneeded - let time_since_creation = (time - creation_time) as f32; - let frame_start_dist = - (beam_segment.speed * (time_since_creation - frame_time)).max(0.0); - let frame_end_dist = - (beam_segment.speed * time_since_creation).max(frame_start_dist); - // Group to ignore collisions with // Might make this more nuanced if beams are used for non damage effects - let group = beam_owner.and_then(|e| read_data.groups.get(e)); - - let hit_entities = if let Some(beam) = beam_owner.and_then(|e| beams.get(e)) { - &beam.hit_entities - } else { - return (server_events, add_hit_entities, outcomes); - }; + let group = read_data.groups.get(entity); // Go through all affectable entities by querying the spatial grid let target_iter = read_data .cached_spatial_grid .0 - .in_circle_aabr(pos.0.xy(), frame_end_dist - frame_start_dist) + .in_circle_aabr(beam.bezier.start.xy(), beam.range) .filter_map(|target| { read_data .positions @@ -154,7 +139,7 @@ impl<'a> System<'a> for Sys { }); target_iter.for_each(|(target, uid_b, pos_b, health_b, body_b)| { // Check to see if entity has already been hit recently - if hit_entities.iter().any(|&uid| uid == *uid_b) { + if beam.hit_entities.iter().any(|&e| e == target) { return; } @@ -167,12 +152,10 @@ impl<'a> System<'a> for Sys { // TODO: use Capsule Prism instead of cylinder let hit = entity != target && !health_b.is_dead - && sphere_wedge_cylinder_collision( - pos.0, - frame_start_dist, - frame_end_dist, - *ori.look_dir(), - beam_segment.angle, + && conical_bezier_cylinder_collision( + beam.bezier, + beam.end_radius, + beam.range, pos_b.0, rad_b, height_b, @@ -195,7 +178,7 @@ impl<'a> System<'a> for Sys { // See if entities are in the same group let same_group = group .map(|group_a| Some(group_a) == read_data.groups.get(target)) - .unwrap_or(Some(*uid_b) == beam_segment.owner); + .unwrap_or(false); let target_group = if same_group { GroupTarget::InGroup @@ -203,23 +186,15 @@ impl<'a> System<'a> for Sys { GroupTarget::OutOfGroup }; - // If owner, shouldn't heal or damage - if Some(*uid_b) == beam_segment.owner { - return; - } - - let attacker_info = - beam_owner.zip(beam_segment.owner).map(|(entity, uid)| { - AttackerInfo { - entity, - uid, - group: read_data.groups.get(entity), - energy: read_data.energies.get(entity), - combo: read_data.combos.get(entity), - inventory: read_data.inventories.get(entity), - stats: read_data.stats.get(entity), - } - }); + let attacker_info = Some(AttackerInfo { + entity, + uid: *uid, + group: read_data.groups.get(entity), + energy: read_data.energies.get(entity), + combo: read_data.combos.get(entity), + inventory: read_data.inventories.get(entity), + stats: read_data.stats.get(entity), + }); let target_info = TargetInfo { entity: target, @@ -244,7 +219,7 @@ impl<'a> System<'a> for Sys { &read_data.alignments, &read_data.players, &read_data.id_maps, - beam_owner, + Some(entity), target, ); let attack_options = AttackOptions { @@ -253,7 +228,7 @@ impl<'a> System<'a> for Sys { target_group, }; - beam_segment.properties.attack.apply_attack( + beam.attack.apply_attack( attacker_info, &target_info, ori.look_dir(), @@ -267,7 +242,7 @@ impl<'a> System<'a> for Sys { 0, ); - add_hit_entities.push((beam_owner, *uid_b)); + add_hit_entities.push((entity, target)); } }); (server_events, add_hit_entities, outcomes) @@ -286,144 +261,42 @@ impl<'a> System<'a> for Sys { job.cpu_stats.measure(ParMode::Single); outcomes_emitter.emit_many(new_outcomes); + server_emitter.emit_many(server_events); - for event in server_events { - server_emitter.emit(event); - } - - for (owner, hit_entity) in add_hit_entities { - if let Some(ref mut beam) = owner.and_then(|e| beams.get_mut(e)) { + for (entity, hit_entity) in add_hit_entities { + if let Some(ref mut beam) = beams.get_mut(entity) { beam.hit_entities.push(hit_entity); } } - - for beam in (&mut beams).join() { - beam.timer = beam - .timer - .checked_add(Duration::from_secs_f32(dt)) - .unwrap_or(beam.tick_dur); - if beam.timer >= beam.tick_dur { - beam.hit_entities.clear(); - beam.timer = beam.timer.checked_sub(beam.tick_dur).unwrap_or_default(); - } - } - - // Set start time on new beams - // This change doesn't need to be recorded as it is not sent to the client - beam_segments.set_event_emission(false); - (&mut beam_segments) - .lend_join() - .for_each(|mut beam_segment| { - if beam_segment.creation.is_none() { - beam_segment.creation = Some(time); - } - }); - beam_segments.set_event_emission(true); } } /// Assumes upright cylinder -/// See page 12 of https://citeseerx.ist.psu.edu/viewdoc/download?doi=10.1.1.396.7952&rep=rep1&type=pdf -fn sphere_wedge_cylinder_collision( +fn conical_bezier_cylinder_collision( // Values for spherical wedge - real_pos: Vec3, - min_rad: f32, // Distance from beam origin to inner section of beam - max_rad: f32, //Distance from beam origin to outer section of beam - ori: Vec3, - angle: f32, + bezier: QuadraticBezier3, + max_rad: f32, // Radius at end_pos (radius is 0 at start_pos) + range: f32, // Used to decide number of steps in bezier function // Values for cylinder bottom_pos_b: Vec3, // Position of bottom of cylinder rad_b: f32, length_b: f32, ) -> bool { - // Converts all coordinates so that the new origin is in the center of the - // cylinder - let center_pos_b = Vec3::new( - bottom_pos_b.x, - bottom_pos_b.y, - bottom_pos_b.z + length_b / 2.0, - ); - let pos = real_pos - center_pos_b; - let pos_b = Vec3::zero(); - if pos.distance_squared(pos_b) > (max_rad + rad_b + length_b).powi(2) { - // Does quick check if entity is too far (I'm not sure if necessary, but - // probably makes detection more efficient) - false - } else if pos.z.abs() <= length_b / 2.0 { - // Checks case 1: center of sphere is on same z-height as cylinder - let pos2 = Vec2::::from(pos); - let ori2 = Vec2::from(ori); - let distance = pos2.distance(Vec2::zero()); - let in_range = distance < max_rad && distance > min_rad; - // Done so that if distance = 0, atan() can still be calculated https://play.rust-lang.org/?version=stable&mode=debug&edition=2018&gist=6d2221bb9454debdfca8f9c52d1edb29 - let tangent_value1: f32 = rad_b / distance; - let tangent_value2: f32 = length_b / 2.0 / distance; - let in_angle = pos2.angle_between(-ori2) < angle + (tangent_value1).atan().abs() - && pos.angle_between(-ori) < angle + (tangent_value2).atan().abs(); - in_range && in_angle - } else { - // Checks case 2: if sphere collides with top/bottom of cylinder, doesn't use - // paper. Logic used here is it checks if line between centers passes through - // either cap, then if the cap is within range, then if withing angle of beam. - // If line - let sign = if pos.z > 0.0 { 1.0 } else { -1.0 }; - let height = sign * length_b / 2.0; - let (in_range, in_angle): (bool, bool); - // Gets relatively how far along the line (between sphere and cylinder centers) - // the endcap of the cylinder is, is between 0 and 1 when sphere center is not - // in cylinder - let intersect_frac = (length_b / 2.0 / pos.z).abs(); - // Gets the position of the cylinder edge closest to the sphere center - let edge_pos = if let Some(vec) = Vec3::new(pos.x, pos.y, 0.0).try_normalized() { - vec * rad_b - } else { - // Returns an arbitrary location that is still guaranteed to be on the cylinder - // edge. This case should only happen when the sphere is directly above the - // cylinder, in which case all positions on edge are equally close. - Vec3::new(rad_b, 0.0, 0.0) - }; - // Gets position on opposite edge of same endcap - let opp_end_edge_pos = Vec3::new(-edge_pos.x, -edge_pos.y, height); - // Gets position on same edge of opposite endcap - let bot_end_edge_pos = Vec3::new(edge_pos.x, edge_pos.y, -height); - // Gets point on line between sphere and cylinder centers that the z value is - // equal to the endcap z location - let intersect_point = Vec2::new(pos.x * intersect_frac, pos.y * intersect_frac); - // Checks if line between sphere and cylinder center passes through cap of - // cylinder - if intersect_point.distance_squared(Vec2::zero()) <= rad_b.powi(2) { - let distance_squared = - Vec3::new(intersect_point.x, intersect_point.y, height).distance_squared(pos); - in_range = distance_squared < max_rad.powi(2) && distance_squared > min_rad.powi(2); - // Angle between (line between centers of cylinder and sphere) and either (line - // between opposite edge of endcap and sphere center) or (line between close - // edge of endcap on bottom of cylinder and sphere center). Whichever angle is - // largest is used. - let angle2 = (pos_b - pos) - .angle_between(opp_end_edge_pos - pos) - .max((pos_b - pos).angle_between(bot_end_edge_pos - pos)); - in_angle = pos.angle_between(-ori) < angle + angle2; - } else { - // TODO: Handle collision for this case more accurately - // For this case, the nearest point will be the edge of the endcap - let endcap_edge_pos = Vec3::new(edge_pos.x, edge_pos.y, height); - let distance_squared = endcap_edge_pos.distance_squared(pos); - in_range = distance_squared > min_rad.powi(2) && distance_squared < max_rad.powi(2); - // Gets side positions on same endcap - let side_end_edge_pos_1 = Vec3::new(edge_pos.y, -edge_pos.x, height); - let side_end_edge_pos_2 = Vec3::new(-edge_pos.y, edge_pos.x, height); - // Gets whichever angle is bigger, between sphere center and opposite edge, - // sphere center and bottom edge, or half of sphere center and both the side - // edges - let angle2 = (pos_b - pos).angle_between(opp_end_edge_pos - pos).max( - (pos_b - pos).angle_between(bot_end_edge_pos - pos).max( - (side_end_edge_pos_1 - pos).angle_between(side_end_edge_pos_2 - pos) / 2.0, - ), - ); - // Will be somewhat inaccurate, tends towards hitting when it shouldn't - // Checks angle between orientation and line between sphere and cylinder centers - in_angle = pos.angle_between(-ori) < angle + angle2; - } - in_range && in_angle - } + // This algorithm first determines the nearest point on the bezier to the point + // in the middle of the cylinder. It then checks that the bezier cone's radius + // at this point could allow it to be in the z bounds of the cylinder and within + // the cylinder's radius. + let center_pos_b = bottom_pos_b.with_z(bottom_pos_b.z + length_b / 2.0); + let (t, closest_pos) = + bezier.binary_search_point_by_steps(center_pos_b, (range * 5.0) as u16, 0.1); + let bezier_rad = t * max_rad; + let z_check = { + let dist = (closest_pos.z - center_pos_b.z).abs(); + dist < bezier_rad + length_b / 2.0 + }; + let rad_check = { + let dist_sqrd = closest_pos.xy().distance_squared(center_pos_b.xy()); + dist_sqrd < (bezier_rad + rad_b).powi(2) + }; + z_check && rad_check } diff --git a/common/systems/src/character_behavior.rs b/common/systems/src/character_behavior.rs index ca0823eb45..4bf9c95863 100644 --- a/common/systems/src/character_behavior.rs +++ b/common/systems/src/character_behavior.rs @@ -156,6 +156,14 @@ impl<'a> System<'a> for Sys { return; } + // Remove components that entity should not have if not in relevant char state + if !char_state.is_melee_attack() { + read_data.lazy_update.remove::(entity); + } + if !char_state.is_beam_attack() { + read_data.lazy_update.remove::(entity); + } + // Enter stunned state if poise damage is enough if let Some(mut poise) = poises.get_mut(entity) { let was_wielded = char_state.is_wield(); diff --git a/server/src/events/entity_creation.rs b/server/src/events/entity_creation.rs index 61ce93980b..4976a3236f 100644 --- a/server/src/events/entity_creation.rs +++ b/server/src/events/entity_creation.rs @@ -7,7 +7,6 @@ use common::{ comp::{ self, aura::{Aura, AuraKind, AuraTarget}, - beam, buff::{BuffCategory, BuffData, BuffKind, BuffSource}, misc::PortalData, ship::figuredata::VOXEL_COLLIDER_MANIFEST, @@ -367,17 +366,6 @@ pub fn handle_shockwave( state.create_shockwave(properties, pos, ori).build(); } -pub fn handle_beam(server: &mut Server, properties: beam::Properties, pos: Pos, ori: Ori) { - let state = server.state_mut(); - let ecs = state.ecs(); - ecs.read_resource::>() - .emit_now(Outcome::Beam { - pos: pos.0, - specifier: properties.specifier, - }); - state.create_beam(properties, pos, ori).build(); -} - pub fn handle_create_waypoint(server: &mut Server, pos: Vec3) { let time = server.state.get_time(); server diff --git a/server/src/events/entity_manipulation.rs b/server/src/events/entity_manipulation.rs index 3bc58b6e69..a9a37d8b59 100644 --- a/server/src/events/entity_manipulation.rs +++ b/server/src/events/entity_manipulation.rs @@ -140,6 +140,10 @@ pub fn handle_destroy(server: &mut Server, entity: EcsEntity, last_change: Healt return; } + // Remove components that should not persist across death + state.ecs().write_storage::().remove(entity); + state.ecs().write_storage::().remove(entity); + 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) { diff --git a/server/src/events/mod.rs b/server/src/events/mod.rs index c284753bdf..3d1520d430 100644 --- a/server/src/events/mod.rs +++ b/server/src/events/mod.rs @@ -11,9 +11,8 @@ use crate::{ use common::event::{EventBus, ServerEvent, ServerEventDiscriminants}; use common_base::span; use entity_creation::{ - handle_beam, handle_create_npc, handle_create_ship, handle_create_waypoint, - handle_initialize_character, handle_initialize_spectator, handle_loaded_character_data, - handle_shockwave, handle_shoot, + handle_create_npc, handle_create_ship, handle_create_waypoint, handle_initialize_character, + handle_initialize_spectator, handle_loaded_character_data, handle_shockwave, handle_shoot, }; use entity_manipulation::{ handle_aura, handle_bonk, handle_buff, handle_change_ability, handle_change_body, @@ -107,11 +106,6 @@ impl Server { pos, ori, } => handle_shockwave(self, properties, pos, ori), - ServerEvent::BeamSegment { - properties, - pos, - ori, - } => handle_beam(self, properties, pos, ori), ServerEvent::Knockback { entity, impulse } => { handle_knockback(self, entity, impulse) }, diff --git a/server/src/state_ext.rs b/server/src/state_ext.rs index 94af2adaed..5b146beff4 100644 --- a/server/src/state_ext.rs +++ b/server/src/state_ext.rs @@ -91,13 +91,6 @@ pub trait StateExt { pos: comp::Pos, ori: comp::Ori, ) -> EcsEntityBuilder; - /// Build a beam entity - fn create_beam( - &mut self, - properties: comp::beam::Properties, - pos: comp::Pos, - ori: comp::Ori, - ) -> EcsEntityBuilder; /// Creates a safezone fn create_safezone(&mut self, range: Option, pos: comp::Pos) -> EcsEntityBuilder; fn create_wiring( @@ -505,22 +498,6 @@ impl StateExt for State { }) } - fn create_beam( - &mut self, - properties: comp::beam::Properties, - pos: comp::Pos, - ori: comp::Ori, - ) -> EcsEntityBuilder { - self.ecs_mut() - .create_entity_synced() - .with(pos) - .with(ori) - .with(comp::BeamSegment { - properties, - creation: None, - }) - } - fn create_safezone(&mut self, range: Option, pos: comp::Pos) -> EcsEntityBuilder { use comp::{ aura::{Aura, AuraKind, AuraTarget, Auras}, diff --git a/voxygen/src/scene/particle.rs b/voxygen/src/scene/particle.rs index cec34b2daf..1fe2d61148 100644 --- a/voxygen/src/scene/particle.rs +++ b/voxygen/src/scene/particle.rs @@ -15,7 +15,7 @@ use common::{ item::Reagent, object, shockwave::{self, ShockwaveDodgeable}, - BeamSegment, Body, CharacterState, Ori, Pos, Scale, Shockwave, Vel, + Beam, Body, CharacterState, Ori, Pos, Scale, Shockwave, Vel, }, figure::Segment, outcome::Outcome, @@ -1030,35 +1030,28 @@ impl ParticleMgr { let state = scene_data.state; let ecs = state.ecs(); let time = state.get_time(); - let dt = scene_data.state.ecs().fetch::().0; - - for (interp, pos, ori, beam) in ( - ecs.read_storage::().maybe(), - &ecs.read_storage::(), - &ecs.read_storage::(), - &ecs.read_storage::(), - ) - .join() - .filter(|(_, _, _, b)| b.creation.map_or(true, |c| (c + dt as f64) >= time)) - { - let pos = interp.map_or(pos.0, |i| i.pos); - let ori = interp.map_or(*ori, |i| i.ori); + for (beam, ori) in (&ecs.read_storage::(), &ecs.read_storage::()).join() { // TODO: Handle this less hackily. Done this way as beam segments are created // every server tick, which is approximately 33 ms. Heartbeat scheduler used to // account for clients with less than 30 fps because they start the creation // time when the segments are received and could receive 2 at once + // TODO: Above limitation no longer exists. Evaluate changing below behavior + // later to allow for better beam particles. let beam_tick_count = 33.max(self.scheduler.heartbeats(Duration::from_millis(1))); - let range = beam.properties.speed * beam.properties.duration.as_secs_f32(); - match beam.properties.specifier { + let angle = (beam.end_radius / beam.range).atan(); + let beam_dir = (beam.bezier.ctrl - beam.bezier.start) + .try_normalized() + .unwrap_or(*ori.look_dir()); + match beam.specifier { beam::FrontendSpecifier::Flamethrower => { let mut rng = thread_rng(); - let (from, to) = (Vec3::::unit_z(), *ori.look_dir()); + let (from, to) = (Vec3::::unit_z(), beam_dir); let m = Mat3::::rotation_from_to_3d(from, to); // Emit a light when using flames if scene_data.flashing_lights_enabled { lights.push(Light::new( - pos, + beam.bezier.start, Rgb::new(1.0, 0.25, 0.05).map(|e| e * rng.gen_range(0.8..1.2)), 2.0, )); @@ -1066,7 +1059,7 @@ impl ParticleMgr { self.particles.resize_with( self.particles.len() + usize::from(beam_tick_count) / 2, || { - let phi: f32 = rng.gen_range(0.0..beam.properties.angle); + let phi: f32 = rng.gen_range(0.0..angle); let theta: f32 = rng.gen_range(0.0..2.0 * PI); let offset_z = Vec3::new( phi.sin() * theta.cos(), @@ -1075,23 +1068,23 @@ impl ParticleMgr { ); let random_ori = offset_z * m * Vec3::new(-1.0, -1.0, 1.0); Particle::new_directed( - beam.properties.duration, + Duration::from_secs_f64(beam.duration.0), time, ParticleMode::FlameThrower, - pos, - pos + random_ori * range, + beam.bezier.start, + beam.bezier.start + random_ori * beam.range, ) }, ); }, beam::FrontendSpecifier::Cultist => { let mut rng = thread_rng(); - let (from, to) = (Vec3::::unit_z(), *ori.look_dir()); + let (from, to) = (Vec3::::unit_z(), beam_dir); let m = Mat3::::rotation_from_to_3d(from, to); // Emit a light when using flames if scene_data.flashing_lights_enabled { lights.push(Light::new( - pos, + beam.bezier.start, Rgb::new(1.0, 0.0, 1.0).map(|e| e * rng.gen_range(0.5..1.0)), 2.0, )); @@ -1099,7 +1092,7 @@ impl ParticleMgr { self.particles.resize_with( self.particles.len() + usize::from(beam_tick_count) / 2, || { - let phi: f32 = rng.gen_range(0.0..beam.properties.angle); + let phi: f32 = rng.gen_range(0.0..angle); let theta: f32 = rng.gen_range(0.0..2.0 * PI); let offset_z = Vec3::new( phi.sin() * theta.cos(), @@ -1108,11 +1101,11 @@ impl ParticleMgr { ); let random_ori = offset_z * m * Vec3::new(-1.0, -1.0, 1.0); Particle::new_directed( - beam.properties.duration, + Duration::from_secs_f64(beam.duration.0), time, ParticleMode::CultistFlame, - pos, - pos + random_ori * range, + beam.bezier.start, + beam.bezier.start + random_ori * beam.range, ) }, ); @@ -1120,49 +1113,49 @@ impl ParticleMgr { beam::FrontendSpecifier::LifestealBeam => { // Emit a light when using lifesteal beam if scene_data.flashing_lights_enabled { - lights.push(Light::new(pos, Rgb::new(0.8, 1.0, 0.5), 1.0)); + lights.push(Light::new(beam.bezier.start, Rgb::new(0.8, 1.0, 0.5), 1.0)); } self.particles.reserve(beam_tick_count as usize); for i in 0..beam_tick_count { self.particles.push(Particle::new_directed( - beam.properties.duration, + Duration::from_secs_f64(beam.duration.0), time + i as f64 / 1000.0, ParticleMode::LifestealBeam, - pos, - pos + *ori.look_dir() * range, + beam.bezier.start, + beam.bezier.start + beam_dir * beam.range, )); } }, beam::FrontendSpecifier::ClayGolem => { self.particles.resize_with(self.particles.len() + 2, || { Particle::new_directed( - beam.properties.duration, + Duration::from_secs_f64(beam.duration.0), time, ParticleMode::Laser, - pos, - pos + *ori.look_dir() * range, + beam.bezier.start, + beam.bezier.start + beam_dir * beam.range, ) }) }, beam::FrontendSpecifier::WebStrand => { self.particles.resize_with(self.particles.len() + 1, || { Particle::new_directed( - beam.properties.duration, + Duration::from_secs_f64(beam.duration.0), time, ParticleMode::WebStrand, - pos, - pos + *ori.look_dir() * range, + beam.bezier.start, + beam.bezier.start + beam_dir * beam.range, ) }) }, beam::FrontendSpecifier::Bubbles => { let mut rng = thread_rng(); - let (from, to) = (Vec3::::unit_z(), *ori.look_dir()); + let (from, to) = (Vec3::::unit_z(), beam_dir); let m = Mat3::::rotation_from_to_3d(from, to); self.particles.resize_with( self.particles.len() + usize::from(beam_tick_count) / 15, || { - let phi: f32 = rng.gen_range(0.0..beam.properties.angle); + let phi: f32 = rng.gen_range(0.0..angle); let theta: f32 = rng.gen_range(0.0..2.0 * PI); let offset_z = Vec3::new( phi.sin() * theta.cos(), @@ -1171,23 +1164,23 @@ impl ParticleMgr { ); let random_ori = offset_z * m * Vec3::new(-1.0, -1.0, 1.0); Particle::new_directed( - beam.properties.duration, + Duration::from_secs_f64(beam.duration.0), time, ParticleMode::Bubbles, - pos, - pos + random_ori * range, + beam.bezier.start, + beam.bezier.start + random_ori * beam.range, ) }, ); }, beam::FrontendSpecifier::Poison => { let mut rng = thread_rng(); - let (from, to) = (Vec3::::unit_z(), *ori.look_dir()); + let (from, to) = (Vec3::::unit_z(), beam_dir); let m = Mat3::::rotation_from_to_3d(from, to); self.particles.resize_with( self.particles.len() + usize::from(beam_tick_count) / 15, || { - let phi: f32 = rng.gen_range(0.0..beam.properties.angle); + let phi: f32 = rng.gen_range(0.0..angle); let theta: f32 = rng.gen_range(0.0..2.0 * PI); let offset_z = Vec3::new( phi.sin() * theta.cos(), @@ -1196,23 +1189,23 @@ impl ParticleMgr { ); let random_ori = offset_z * m * Vec3::new(-1.0, -1.0, 1.0); Particle::new_directed( - beam.properties.duration, + Duration::from_secs_f64(beam.duration.0), time, ParticleMode::CultistFlame, - pos, - pos + random_ori * range, + beam.bezier.start, + beam.bezier.start + random_ori * beam.range, ) }, ); }, beam::FrontendSpecifier::Ink => { let mut rng = thread_rng(); - let (from, to) = (Vec3::::unit_z(), *ori.look_dir()); + let (from, to) = (Vec3::::unit_z(), beam_dir); let m = Mat3::::rotation_from_to_3d(from, to); self.particles.resize_with( self.particles.len() + usize::from(beam_tick_count) / 15, || { - let phi: f32 = rng.gen_range(0.0..beam.properties.angle); + let phi: f32 = rng.gen_range(0.0..angle); let theta: f32 = rng.gen_range(0.0..2.0 * PI); let offset_z = Vec3::new( phi.sin() * theta.cos(), @@ -1221,23 +1214,23 @@ impl ParticleMgr { ); let random_ori = offset_z * m * Vec3::new(-1.0, -1.0, 1.0); Particle::new_directed( - beam.properties.duration, + Duration::from_secs_f64(beam.duration.0), time, ParticleMode::Ink, - pos, - pos + random_ori * range, + beam.bezier.start, + beam.bezier.start + random_ori * beam.range, ) }, ); }, beam::FrontendSpecifier::Steam => { let mut rng = thread_rng(); - let (from, to) = (Vec3::::unit_z(), *ori.look_dir()); + let (from, to) = (Vec3::::unit_z(), beam_dir); let m = Mat3::::rotation_from_to_3d(from, to); self.particles.resize_with( self.particles.len() + usize::from(beam_tick_count) / 15, || { - let phi: f32 = rng.gen_range(0.0..beam.properties.angle); + let phi: f32 = rng.gen_range(0.0..angle); let theta: f32 = rng.gen_range(0.0..2.0 * PI); let offset_z = Vec3::new( phi.sin() * theta.cos(), @@ -1246,11 +1239,11 @@ impl ParticleMgr { ); let random_ori = offset_z * m * Vec3::new(-1.0, -1.0, 1.0); Particle::new_directed( - beam.properties.duration, + Duration::from_secs_f64(beam.duration.0), time, ParticleMode::Steam, - pos, - pos + random_ori * range, + beam.bezier.start, + beam.bezier.start + random_ori * beam.range, ) }, ); @@ -1258,22 +1251,22 @@ impl ParticleMgr { beam::FrontendSpecifier::Lightning => { self.particles.resize_with(self.particles.len() + 2, || { Particle::new_directed( - beam.properties.duration, + Duration::from_secs_f64(beam.duration.0), time, ParticleMode::Lightning, - pos, - pos + *ori.look_dir() * range, + beam.bezier.start, + beam.bezier.start + beam_dir * beam.range, ) }) }, beam::FrontendSpecifier::Frost => { let mut rng = thread_rng(); - let (from, to) = (Vec3::::unit_z(), *ori.look_dir()); + let (from, to) = (Vec3::::unit_z(), beam_dir); let m = Mat3::::rotation_from_to_3d(from, to); self.particles.resize_with( self.particles.len() + usize::from(beam_tick_count) / 4, || { - let phi: f32 = rng.gen_range(0.0..beam.properties.angle); + let phi: f32 = rng.gen_range(0.0..angle); let theta: f32 = rng.gen_range(0.0..2.0 * PI); let offset_z = Vec3::new( phi.sin() * theta.cos(), @@ -1282,11 +1275,11 @@ impl ParticleMgr { ); let random_ori = offset_z * m * Vec3::new(-1.0, -1.0, 1.0); Particle::new_directed( - beam.properties.duration, + Duration::from_secs_f64(beam.duration.0), time, ParticleMode::Ice, - pos, - pos + random_ori * range, + beam.bezier.start, + beam.bezier.start + random_ori * beam.range, ) }, );