Converted beam system from spherical shell wedges to quadratic beziers

This commit is contained in:
Sam 2023-10-14 21:46:28 -04:00
parent 32b0b33abe
commit ef5e37a64d
15 changed files with 239 additions and 429 deletions

View File

@ -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;
}

View File

@ -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,

View File

@ -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<Uid>,
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<f64>,
}
impl Component for BeamSegment {
type Storage = DerefFlaggedStorage<Self, specs::DenseVecStorage<Self>>;
}
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<Uid>,
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<f32>,
#[serde(skip)]
pub hit_entities: Vec<EcsEntity>,
}
impl Component for Beam {
type Storage = specs::DenseVecStorage<Self>;
type Storage = DerefFlaggedStorage<Self, specs::DenseVecStorage<Self>>;
}
#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]

View File

@ -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,

View File

@ -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,

View File

@ -182,11 +182,6 @@ pub enum ServerEvent {
entity: EcsEntity,
impulse: Vec3<f32>,
},
BeamSegment {
properties: comp::beam::Properties,
pos: Pos,
ori: Ori,
},
LandOnGround {
entity: EcsEntity,
vel: Vec3<f32>,

View File

@ -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<CombatEffect>,
/// 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<f32>,
}
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::<Uid>::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
});

View File

@ -213,7 +213,7 @@ impl State {
ecs.register::<comp::Group>();
ecs.register::<comp::Shockwave>();
ecs.register::<comp::ShockwaveHitEntities>();
ecs.register::<comp::BeamSegment>();
ecs.register::<comp::Beam>();
ecs.register::<comp::Alignment>();
ecs.register::<comp::LootOwner>();
ecs.register::<comp::Admin>();
@ -261,7 +261,6 @@ impl State {
ecs.register::<comp::Faction>();
ecs.register::<comp::invite::Invite>();
ecs.register::<comp::invite::PendingInvites>();
ecs.register::<comp::Beam>();
ecs.register::<VolumeRiders>();
// Register synced resources used by the ECS.

View File

@ -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<Outcome>>,
}
/// 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<Outcome>>,
);
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<Self>,
(read_data, mut beam_segments, mut beams, outcomes): Self::SystemData,
) {
fn run(job: &mut Job<Self>, (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<f32>,
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<f32>,
angle: f32,
bezier: QuadraticBezier3<f32>,
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<f32>, // 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::<f32>::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
}

View File

@ -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::<Melee>(entity);
}
if !char_state.is_beam_attack() {
read_data.lazy_update.remove::<Beam>(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();

View File

@ -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::<EventBus<Outcome>>()
.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<f32>) {
let time = server.state.get_time();
server

View File

@ -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::<comp::Melee>().remove(entity);
state.ecs().write_storage::<comp::Beam>().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) {

View File

@ -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)
},

View File

@ -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<f32>, 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<f32>, pos: comp::Pos) -> EcsEntityBuilder {
use comp::{
aura::{Aura, AuraKind, AuraTarget, Auras},

View File

@ -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::<DeltaTime>().0;
for (interp, pos, ori, beam) in (
ecs.read_storage::<Interpolated>().maybe(),
&ecs.read_storage::<Pos>(),
&ecs.read_storage::<Ori>(),
&ecs.read_storage::<BeamSegment>(),
)
.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::<Beam>(), &ecs.read_storage::<Ori>()).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::<f32>::unit_z(), *ori.look_dir());
let (from, to) = (Vec3::<f32>::unit_z(), beam_dir);
let m = Mat3::<f32>::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::<f32>::unit_z(), *ori.look_dir());
let (from, to) = (Vec3::<f32>::unit_z(), beam_dir);
let m = Mat3::<f32>::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::<f32>::unit_z(), *ori.look_dir());
let (from, to) = (Vec3::<f32>::unit_z(), beam_dir);
let m = Mat3::<f32>::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::<f32>::unit_z(), *ori.look_dir());
let (from, to) = (Vec3::<f32>::unit_z(), beam_dir);
let m = Mat3::<f32>::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::<f32>::unit_z(), *ori.look_dir());
let (from, to) = (Vec3::<f32>::unit_z(), beam_dir);
let m = Mat3::<f32>::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::<f32>::unit_z(), *ori.look_dir());
let (from, to) = (Vec3::<f32>::unit_z(), beam_dir);
let m = Mat3::<f32>::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::<f32>::unit_z(), *ori.look_dir());
let (from, to) = (Vec3::<f32>::unit_z(), beam_dir);
let m = Mat3::<f32>::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,
)
},
);