2020-12-01 00:28:00 +00:00
use common ::{
2020-09-05 16:27:36 +00:00
comp ::{
2020-11-04 02:01:12 +00:00
group , Beam , BeamSegment , Body , Energy , EnergyChange , EnergySource , Health , HealthChange ,
2021-01-13 03:26:51 +00:00
HealthSource , Inventory , Last , Ori , Pos , Scale ,
2020-09-05 16:27:36 +00:00
} ,
event ::{ EventBus , ServerEvent } ,
2020-12-01 00:28:00 +00:00
resources ::{ DeltaTime , Time } ,
2020-12-13 17:11:55 +00:00
uid ::{ Uid , UidAllocator } ,
2020-11-04 02:01:12 +00:00
GroupTarget ,
2020-09-05 16:27:36 +00:00
} ;
use specs ::{ saveload ::MarkerAllocator , Entities , Join , Read , ReadStorage , System , WriteStorage } ;
2020-09-24 02:02:30 +00:00
use std ::time ::Duration ;
2020-09-05 16:27:36 +00:00
use vek ::* ;
2020-09-24 02:02:30 +00:00
/// This system is responsible for handling beams that heal or do damage
2020-09-05 16:27:36 +00:00
pub struct Sys ;
impl < ' a > System < ' a > for Sys {
#[ allow(clippy::type_complexity) ]
type SystemData = (
Entities < ' a > ,
Read < ' a , EventBus < ServerEvent > > ,
Read < ' a , Time > ,
Read < ' a , DeltaTime > ,
Read < ' a , UidAllocator > ,
ReadStorage < ' a , Uid > ,
ReadStorage < ' a , Pos > ,
ReadStorage < ' a , Last < Pos > > ,
ReadStorage < ' a , Ori > ,
ReadStorage < ' a , Scale > ,
ReadStorage < ' a , Body > ,
2020-10-31 22:34:08 +00:00
ReadStorage < ' a , Health > ,
2021-01-08 19:12:09 +00:00
ReadStorage < ' a , Inventory > ,
2020-09-05 16:27:36 +00:00
ReadStorage < ' a , group ::Group > ,
2020-10-30 21:49:58 +00:00
ReadStorage < ' a , Energy > ,
2020-09-24 02:02:30 +00:00
WriteStorage < ' a , BeamSegment > ,
2020-09-05 16:27:36 +00:00
WriteStorage < ' a , Beam > ,
) ;
fn run (
& mut self ,
(
entities ,
server_bus ,
time ,
dt ,
uid_allocator ,
uids ,
positions ,
last_positions ,
orientations ,
scales ,
bodies ,
2020-10-31 22:34:08 +00:00
healths ,
2021-01-08 19:12:09 +00:00
inventories ,
2020-09-05 16:27:36 +00:00
groups ,
2020-10-30 21:49:58 +00:00
energies ,
2020-09-24 02:02:30 +00:00
mut beam_segments ,
2020-09-05 16:27:36 +00:00
mut beams ,
) : Self ::SystemData ,
) {
let mut server_emitter = server_bus . emitter ( ) ;
let time = time . 0 ;
let dt = dt . 0 ;
// Beams
2020-11-02 00:26:01 +00:00
for ( entity , pos , ori , beam_segment ) in
( & entities , & positions , & orientations , & beam_segments ) . join ( )
2020-09-05 16:27:36 +00:00
{
2020-09-24 02:02:30 +00:00
let creation_time = match beam_segment . creation {
2020-09-05 16:27:36 +00:00
Some ( time ) = > time ,
// Skip newly created beam segments
None = > continue ,
} ;
2020-09-24 02:02:30 +00:00
let end_time = creation_time + beam_segment . duration . as_secs_f64 ( ) ;
2020-09-05 16:27:36 +00:00
// 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 it's
// end point
if end_time < time {
server_emitter . emit ( ServerEvent ::Destroy {
entity ,
cause : HealthSource ::World ,
} ) ;
}
// 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 {
continue ;
}
// Note: min() probably uneeded
let time_since_creation = ( time - creation_time ) as f32 ;
2020-09-24 02:02:30 +00:00
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 ) ;
let beam_owner = beam_segment
. owner
. and_then ( | uid | uid_allocator . retrieve_entity_internal ( uid . into ( ) ) ) ;
2020-09-05 16:27:36 +00:00
// Group to ignore collisions with
// Might make this more nuanced if beams are used for non damage effects
2020-09-24 02:02:30 +00:00
let group = beam_owner . and_then ( | e | groups . get ( e ) ) ;
let hit_entities = if let Some ( beam ) = beam_owner . and_then ( | e | beams . get_mut ( e ) ) {
& mut beam . hit_entities
} else {
continue ;
} ;
2020-09-05 16:27:36 +00:00
// Go through all other effectable entities
2020-11-04 02:01:12 +00:00
for ( b , uid_b , pos_b , last_pos_b_maybe , scale_b_maybe , health_b , body_b ) in (
2020-09-05 16:27:36 +00:00
& entities ,
& uids ,
& positions ,
// TODO: make sure that these are maintained on the client and remove `.maybe()`
last_positions . maybe ( ) ,
scales . maybe ( ) ,
2020-10-31 22:34:08 +00:00
& healths ,
2020-09-05 16:27:36 +00:00
& bodies ,
)
. join ( )
{
2020-09-24 02:02:30 +00:00
// Check to see if entity has already been hit recently
if hit_entities . iter ( ) . any ( | & uid | uid = = * uid_b ) {
continue ;
}
2020-09-05 16:27:36 +00:00
// Scales
let scale_b = scale_b_maybe . map_or ( 1.0 , | s | s . 0 ) ;
let rad_b = body_b . radius ( ) * scale_b ;
let height_b = body_b . height ( ) * scale_b ;
// Check if it is a hit
let hit = entity ! = b
2020-10-31 22:34:08 +00:00
& & ! health_b . is_dead
2020-09-05 16:27:36 +00:00
// Collision shapes
2020-09-24 02:02:30 +00:00
& & ( sphere_wedge_cylinder_collision ( pos . 0 , frame_start_dist , frame_end_dist , * ori . 0 , beam_segment . angle , pos_b . 0 , rad_b , height_b )
| | last_pos_b_maybe . map_or ( false , | pos_maybe | { sphere_wedge_cylinder_collision ( pos . 0 , frame_start_dist , frame_end_dist , * ori . 0 , beam_segment . angle , ( pos_maybe . 0 ) . 0 , rad_b , height_b ) } ) ) ;
2020-09-05 16:27:36 +00:00
if hit {
// See if entities are in the same group
let same_group = group
. map ( | group_a | Some ( group_a ) = = groups . get ( b ) )
2020-09-24 02:02:30 +00:00
. unwrap_or ( Some ( * uid_b ) = = beam_segment . owner ) ;
2020-09-05 16:27:36 +00:00
2020-11-02 00:26:01 +00:00
let target_group = if same_group {
GroupTarget ::InGroup
} else {
GroupTarget ::OutOfGroup
} ;
2020-09-06 21:01:56 +00:00
// If owner, shouldn't heal or damage
2020-09-24 02:02:30 +00:00
if Some ( * uid_b ) = = beam_segment . owner {
2020-09-06 21:01:56 +00:00
continue ;
}
2020-09-05 16:27:36 +00:00
2021-01-27 03:05:13 +00:00
for ( target , damage ) in beam_segment . damages . iter ( ) {
2020-11-04 02:01:12 +00:00
if let Some ( target ) = target {
if * target ! = target_group {
continue ;
2020-11-02 00:26:01 +00:00
}
2020-09-06 21:01:56 +00:00
}
2020-11-04 02:01:12 +00:00
// Modify damage
2021-01-26 03:53:52 +00:00
let change = damage . modify_damage (
inventories . get ( b ) ,
beam_segment . owner ,
false ,
0.0 ,
) ;
2020-11-04 02:01:12 +00:00
match target {
Some ( GroupTarget ::OutOfGroup ) = > {
server_emitter . emit ( ServerEvent ::Damage { entity : b , change } ) ;
if let Some ( entity ) = beam_owner {
server_emitter . emit ( ServerEvent ::Damage {
entity ,
2020-12-06 02:29:46 +00:00
change : HealthChange {
amount : ( - change . amount as f32
* beam_segment . lifesteal_eff )
as i32 ,
cause : HealthSource ::Heal {
by : beam_segment . owner ,
2020-11-04 02:01:12 +00:00
} ,
2020-12-06 02:29:46 +00:00
} ,
} ) ;
2020-11-04 02:01:12 +00:00
server_emitter . emit ( ServerEvent ::EnergyChange {
entity ,
change : EnergyChange {
amount : beam_segment . energy_regen as i32 ,
source : EnergySource ::HitEnemy ,
} ,
} ) ;
}
} ,
Some ( GroupTarget ::InGroup ) = > {
if let Some ( energy ) = beam_owner . and_then ( | o | energies . get ( o ) ) {
if energy . current ( ) > beam_segment . energy_cost {
server_emitter . emit ( ServerEvent ::EnergyChange {
entity : beam_owner . unwrap ( ) , /* If it's able to get an energy
* component , the entity exists * /
change : EnergyChange {
amount : - ( beam_segment . energy_cost as i32 ) , // Stamina use
source : EnergySource ::Ability ,
} ,
} ) ;
server_emitter
. emit ( ServerEvent ::Damage { entity : b , change } ) ;
}
}
} ,
None = > { } ,
2020-09-06 21:01:56 +00:00
}
2020-11-04 02:01:12 +00:00
// Adds entities that were hit to the hit_entities list on the beam, sees if
// it needs to purge the hit_entities list
hit_entities . push ( * uid_b ) ;
2020-09-05 16:27:36 +00:00
}
}
}
}
2020-09-24 02:02:30 +00:00
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 ( ) ;
}
}
2020-09-05 16:27:36 +00:00
// Set start time on new beams
// This change doesn't need to be recorded as it is not sent to the client
2020-09-24 02:02:30 +00:00
beam_segments . set_event_emission ( false ) ;
2021-01-07 20:25:12 +00:00
( & mut beam_segments ) . join ( ) . for_each ( | mut beam_segment | {
2020-09-24 02:02:30 +00:00
if beam_segment . creation . is_none ( ) {
beam_segment . creation = Some ( time ) ;
2020-09-05 16:27:36 +00:00
}
} ) ;
2020-09-24 02:02:30 +00:00
beam_segments . set_event_emission ( true ) ;
2020-09-05 16:27:36 +00:00
}
}
/// Assumes upright cylinder
/// See page 12 of https://citeseerx.ist.psu.edu/viewdoc/download?doi=10.1.1.396.7952&rep=rep1&type=pdf
2020-09-07 21:46:25 +00:00
#[ allow(clippy::too_many_arguments) ]
2020-09-05 16:27:36 +00:00
fn sphere_wedge_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 ,
// 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
2020-09-05 23:45:36 +00:00
let center_pos_b = Vec3 ::new (
bottom_pos_b . x ,
bottom_pos_b . y ,
bottom_pos_b . z + length_b / 2.0 ,
) ;
2020-09-05 16:27:36 +00:00
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 ;
2020-09-05 23:45:36 +00:00
// 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 ( ) ;
2020-09-05 16:27:36 +00:00
in_range & & in_angle
} else {
// Checks case 2: if sphere collides with top/bottom of cylinder, doesn't use
2020-09-05 23:45:36 +00:00
// 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
2020-09-05 16:27:36 +00:00
let sign = if pos . z > 0.0 { 1.0 } else { - 1.0 } ;
2020-09-05 23:45:36 +00:00
let height = sign * length_b / 2.0 ;
2020-09-05 16:27:36 +00:00
let ( in_range , in_angle ) : ( bool , bool ) ;
2020-09-05 23:45:36 +00:00
// 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
2020-09-05 16:27:36 +00:00
let intersect_frac = ( length_b / 2.0 / pos . z ) . abs ( ) ;
2020-09-05 23:45:36 +00:00
// Gets the position of the cylinder edge closest to the sphere center
2020-09-06 19:39:23 +00:00
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 )
} ;
2020-09-23 00:18:00 +00:00
// 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 ) ;
2020-09-05 23:45:36 +00:00
// 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
2020-09-05 16:27:36 +00:00
if intersect_point . distance_squared ( Vec2 ::zero ( ) ) < = rad_b . powi ( 2 ) {
2020-09-06 19:39:23 +00:00
let distance_squared =
Vec3 ::new ( intersect_point . x , intersect_point . y , height ) . distance_squared ( pos ) ;
2020-09-05 16:27:36 +00:00
in_range = distance_squared < max_rad . powi ( 2 ) & & distance_squared > min_rad . powi ( 2 ) ;
2020-09-23 00:18:00 +00:00
// 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 ;
2020-09-05 16:27:36 +00:00
} else {
2020-09-05 23:45:36 +00:00
// 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 ) ;
2020-09-05 16:27:36 +00:00
let distance_squared = endcap_edge_pos . distance_squared ( pos ) ;
2020-09-05 23:45:36 +00:00
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 ) ;
2020-09-23 00:18:00 +00:00
// 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 ,
) ,
) ;
2020-09-05 23:45:36 +00:00
// 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 ;
2020-09-05 16:27:36 +00:00
}
in_range & & in_angle
}
}