2020-12-01 00:28:00 +00:00
use common ::{
2021-02-28 20:02:03 +00:00
combat ::{ AttackerInfo , TargetInfo } ,
2020-09-05 16:27:36 +00:00
comp ::{
2021-03-17 21:24:39 +00:00
Beam , BeamSegment , Body , Combo , Energy , Group , Health , HealthSource , Inventory , Ori , Pos ,
Scale , Stats ,
2020-09-05 16:27:36 +00:00
} ,
event ::{ EventBus , ServerEvent } ,
2020-12-01 00:28:00 +00:00
resources ::{ DeltaTime , Time } ,
2021-03-22 12:32:27 +00:00
terrain ::TerrainGrid ,
2020-12-13 17:11:55 +00:00
uid ::{ Uid , UidAllocator } ,
2021-03-22 12:32:27 +00:00
vol ::ReadVol ,
2020-11-04 02:01:12 +00:00
GroupTarget ,
2020-09-05 16:27:36 +00:00
} ;
2021-03-19 20:18:36 +00:00
use common_ecs ::{ Job , Origin , ParMode , Phase , System } ;
use rayon ::iter ::ParallelIterator ;
2021-02-22 18:57:56 +00:00
use specs ::{
2021-03-22 12:32:27 +00:00
saveload ::MarkerAllocator , shred ::ResourceId , Entities , Join , ParJoin , Read , ReadExpect ,
ReadStorage , SystemData , World , WriteStorage ,
2021-02-22 18:57:56 +00:00
} ;
2020-09-24 02:02:30 +00:00
use std ::time ::Duration ;
2020-09-05 16:27:36 +00:00
use vek ::* ;
2021-02-22 18:57:56 +00:00
#[ derive(SystemData) ]
2021-02-22 21:02:37 +00:00
pub struct ReadData < ' a > {
2021-02-22 18:57:56 +00:00
entities : Entities < ' a > ,
server_bus : Read < ' a , EventBus < ServerEvent > > ,
time : Read < ' a , Time > ,
dt : Read < ' a , DeltaTime > ,
2021-03-22 12:32:27 +00:00
terrain : ReadExpect < ' a , TerrainGrid > ,
2021-02-22 18:57:56 +00:00
uid_allocator : Read < ' a , UidAllocator > ,
uids : ReadStorage < ' a , Uid > ,
positions : ReadStorage < ' a , Pos > ,
orientations : ReadStorage < ' a , Ori > ,
scales : ReadStorage < ' a , Scale > ,
bodies : ReadStorage < ' a , Body > ,
healths : ReadStorage < ' a , Health > ,
inventories : ReadStorage < ' a , Inventory > ,
groups : ReadStorage < ' a , Group > ,
energies : ReadStorage < ' a , Energy > ,
2021-02-28 20:02:03 +00:00
stats : ReadStorage < ' a , Stats > ,
2021-03-02 22:19:38 +00:00
combos : ReadStorage < ' a , Combo > ,
2021-02-22 18:57:56 +00:00
}
2020-09-24 02:02:30 +00:00
/// This system is responsible for handling beams that heal or do damage
2021-03-04 14:00:16 +00:00
#[ derive(Default) ]
2020-09-05 16:27:36 +00:00
pub struct Sys ;
2021-03-08 11:13:59 +00:00
impl < ' a > System < ' a > for Sys {
2020-09-05 16:27:36 +00:00
type SystemData = (
2021-02-22 21:02:37 +00:00
ReadData < ' a > ,
2020-09-24 02:02:30 +00:00
WriteStorage < ' a , BeamSegment > ,
2020-09-05 16:27:36 +00:00
WriteStorage < ' a , Beam > ,
) ;
2021-03-04 14:00:16 +00:00
const NAME : & 'static str = " beam " ;
const ORIGIN : Origin = Origin ::Common ;
const PHASE : Phase = Phase ::Create ;
2021-03-19 20:18:36 +00:00
fn run ( job : & mut Job < Self > , ( read_data , mut beam_segments , mut beams ) : Self ::SystemData ) {
2021-02-22 21:02:37 +00:00
let mut server_emitter = read_data . server_bus . emitter ( ) ;
2021-02-22 18:57:56 +00:00
2021-02-22 21:02:37 +00:00
let time = read_data . time . 0 ;
let dt = read_data . dt . 0 ;
2020-09-05 16:27:36 +00:00
2021-03-19 20:18:36 +00:00
job . cpu_stats . measure ( ParMode ::Rayon ) ;
2020-09-05 16:27:36 +00:00
// Beams
2021-03-19 20:18:36 +00:00
let ( server_events , add_hit_entities ) = (
2021-02-22 21:02:37 +00:00
& read_data . entities ,
& read_data . positions ,
& read_data . orientations ,
2021-02-22 18:57:56 +00:00
& beam_segments ,
)
2021-03-19 20:18:36 +00:00
. par_join ( )
2021-03-22 14:55:24 +00:00
. fold ( | | ( Vec ::new ( ) , Vec ::new ( ) ) , | ( mut server_events , mut add_hit_entities ) , ( entity , pos , ori , beam_segment ) |
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
2021-03-19 20:18:36 +00:00
None = > return ( server_events , add_hit_entities ) ,
2020-09-05 16:27:36 +00:00
} ;
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 {
2021-03-19 20:18:36 +00:00
server_events . push ( ServerEvent ::Destroy {
2020-09-05 16:27:36 +00:00
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 {
2021-03-19 20:18:36 +00:00
return ( server_events , add_hit_entities ) ;
2020-09-05 16:27:36 +00:00
}
// 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 ) ;
2021-02-22 21:02:37 +00:00
let beam_owner = beam_segment
. owner
. and_then ( | uid | read_data . 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
2021-02-22 21:02:37 +00:00
let group = beam_owner . and_then ( | e | read_data . groups . get ( e ) ) ;
2020-09-24 02:02:30 +00:00
2021-03-19 20:18:36 +00:00
let hit_entities = if let Some ( beam ) = beam_owner . and_then ( | e | beams . get ( e ) ) {
& beam . hit_entities
2020-09-24 02:02:30 +00:00
} else {
2021-03-19 20:18:36 +00:00
return ( server_events , add_hit_entities ) ;
2020-09-24 02:02:30 +00:00
} ;
2020-09-05 16:27:36 +00:00
// Go through all other effectable entities
2021-02-22 18:57:56 +00:00
for ( target , uid_b , pos_b , health_b , body_b ) in (
2021-02-22 21:02:37 +00:00
& read_data . entities ,
& read_data . uids ,
& read_data . positions ,
& read_data . healths ,
& read_data . bodies ,
2020-09-05 16:27:36 +00:00
)
. 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
2021-02-22 21:02:37 +00:00
let scale_b = read_data . scales . get ( target ) . map_or ( 1.0 , | s | s . 0 ) ;
2020-09-05 16:27:36 +00:00
let rad_b = body_b . radius ( ) * scale_b ;
let height_b = body_b . height ( ) * scale_b ;
// Check if it is a hit
2021-02-22 18:57:56 +00:00
let hit = entity ! = target
2020-10-31 22:34:08 +00:00
& & ! health_b . is_dead
2020-09-05 16:27:36 +00:00
// Collision shapes
2021-03-22 14:55:24 +00:00
& & sphere_wedge_cylinder_collision ( pos . 0 , frame_start_dist , frame_end_dist , * ori . look_dir ( ) , beam_segment . angle , pos_b . 0 , rad_b , height_b ) ;
2021-03-22 12:32:27 +00:00
// Finally, ensure that a hit has actually occurred by performing a raycast. We do this last because
// it's likely to be the most expensive operation.
2021-03-22 14:55:24 +00:00
let tgt_dist = pos . 0. distance ( pos_b . 0 ) ;
2021-03-22 12:32:27 +00:00
let hit = hit & & read_data . terrain
2021-03-22 14:55:24 +00:00
. ray ( pos . 0 , pos . 0 + * ori . look_dir ( ) * ( tgt_dist + 1.0 ) )
2021-03-22 12:32:27 +00:00
. until ( | b | b . is_filled ( ) )
. cast ( ) . 0 > = tgt_dist ;
2020-09-05 16:27:36 +00:00
if hit {
// See if entities are in the same group
let same_group = group
2021-02-22 21:02:37 +00:00
. map ( | group_a | Some ( group_a ) = = read_data . groups . get ( target ) )
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-02-02 18:02:40 +00:00
let attacker_info =
beam_owner
. zip ( beam_segment . owner )
. map ( | ( entity , uid ) | AttackerInfo {
entity ,
uid ,
2021-02-22 21:02:37 +00:00
energy : read_data . energies . get ( entity ) ,
2021-03-02 22:19:38 +00:00
combo : read_data . combos . get ( entity ) ,
2021-02-02 18:02:40 +00:00
} ) ;
2021-02-28 20:02:03 +00:00
let target_info = TargetInfo {
entity : target ,
inventory : read_data . inventories . get ( target ) ,
stats : read_data . stats . get ( target ) ,
2021-03-20 20:26:10 +00:00
health : read_data . healths . get ( target ) ,
2021-02-28 20:02:03 +00:00
} ;
2021-02-02 18:02:40 +00:00
beam_segment . properties . attack . apply_attack (
2021-02-01 02:43:26 +00:00
target_group ,
2021-02-02 18:02:40 +00:00
attacker_info ,
2021-02-28 20:02:03 +00:00
target_info ,
2021-02-04 09:17:38 +00:00
ori . look_dir ( ) ,
2021-02-01 02:43:26 +00:00
false ,
1.0 ,
2021-03-19 20:18:36 +00:00
| e | server_events . push ( e ) ,
2021-02-01 02:43:26 +00:00
) ;
2021-03-19 20:18:36 +00:00
add_hit_entities . push ( ( beam_owner , * uid_b ) ) ;
2020-09-05 16:27:36 +00:00
}
}
2021-03-19 20:18:36 +00:00
( server_events , add_hit_entities )
} ) . reduce ( | | ( Vec ::new ( ) , Vec ::new ( ) ) , | ( mut events_a , mut hit_entities_a ) , ( mut events_b , mut hit_entities_b ) | {
events_a . append ( & mut events_b ) ;
hit_entities_a . append ( & mut hit_entities_b ) ;
( events_a , hit_entities_a )
} ) ;
job . cpu_stats . measure ( ParMode ::Single ) ;
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 ) ) {
beam . hit_entities . push ( hit_entity ) ;
}
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
}
}