diff --git a/client/src/lib.rs b/client/src/lib.rs index f7c748ea6a..e74ed9bd9d 100644 --- a/client/src/lib.rs +++ b/client/src/lib.rs @@ -1427,13 +1427,13 @@ impl Client { ServerMsg::Outcomes(outcomes) => { frontend_events.extend(outcomes.into_iter().map(Event::Outcome)) }, - ServerMsg::Knockback(force) => { + ServerMsg::Knockback(impulse) => { self.state .ecs() .read_resource::>() - .emit_now(LocalEvent::ApplyForce { + .emit_now(LocalEvent::ApplyImpulse { entity: self.entity, - force, + impulse, }); }, } diff --git a/common/src/comp/ability.rs b/common/src/comp/ability.rs index 843bea5933..4c328be14d 100644 --- a/common/src/comp/ability.rs +++ b/common/src/comp/ability.rs @@ -121,6 +121,7 @@ pub enum CharacterAbility { shockwave_angle: f32, shockwave_speed: f32, shockwave_duration: Duration, + requires_ground: bool, }, } @@ -417,6 +418,7 @@ impl From<&CharacterAbility> for CharacterState { shockwave_angle, shockwave_speed, shockwave_duration, + requires_ground, } => CharacterState::GroundShockwave(ground_shockwave::Data { exhausted: false, buildup_duration: *buildup_duration, @@ -426,6 +428,7 @@ impl From<&CharacterAbility> for CharacterState { shockwave_angle: *shockwave_angle, shockwave_speed: *shockwave_speed, shockwave_duration: *shockwave_duration, + requires_ground: *requires_ground, }), } } diff --git a/common/src/comp/inventory/item/tool.rs b/common/src/comp/inventory/item/tool.rs index 58e8489058..58cb8aae6a 100644 --- a/common/src/comp/inventory/item/tool.rs +++ b/common/src/comp/inventory/item/tool.rs @@ -369,6 +369,7 @@ impl Tool { shockwave_angle: 90.0, shockwave_speed: 20.0, shockwave_duration: Duration::from_millis(2000), + requires_ground: true, }, ] } else { diff --git a/common/src/event.rs b/common/src/event.rs index c7568bdce7..f5bd89c79d 100644 --- a/common/src/event.rs +++ b/common/src/event.rs @@ -11,8 +11,11 @@ use vek::*; pub enum LocalEvent { /// Applies upward force to entity's `Vel` Jump(EcsEntity), - /// Applies the `force` to `entity`'s `Vel` - ApplyForce { entity: EcsEntity, force: Vec3 }, + /// Applies the `impulse` to `entity`'s `Vel` + ApplyImpulse { + entity: EcsEntity, + impulse: Vec3, + }, /// Applies leaping force to `entity`'s `Vel` away from `wall_dir` direction WallLeap { entity: EcsEntity, @@ -58,7 +61,7 @@ pub enum ServerEvent { }, Knockback { entity: EcsEntity, - force: Vec3, + impulse: Vec3, }, LandOnGround { entity: EcsEntity, diff --git a/common/src/state.rs b/common/src/state.rs index bc203820cb..3e8e1e62a2 100644 --- a/common/src/state.rs +++ b/common/src/state.rs @@ -390,11 +390,9 @@ impl State { vel.0.z = HUMANOID_JUMP_ACCEL; } }, - LocalEvent::ApplyForce { entity, force } => { - // TODO: this sets the velocity directly to the value of `force`, consider - // renaming the event or changing the behavior + LocalEvent::ApplyImpulse { entity, impulse } => { if let Some(vel) = velocities.get_mut(entity) { - vel.0 = force; + vel.0 = impulse; } }, LocalEvent::WallLeap { entity, wall_dir } => { diff --git a/common/src/states/ground_shockwave.rs b/common/src/states/ground_shockwave.rs index 10041aec84..81038fc58b 100644 --- a/common/src/states/ground_shockwave.rs +++ b/common/src/states/ground_shockwave.rs @@ -25,6 +25,8 @@ pub struct Data { pub shockwave_speed: f32, /// How long the shockwave travels for pub shockwave_duration: Duration, + /// Whether the shockwave requires the target to be on the ground + pub requires_ground: bool, } impl CharacterBehavior for Data { @@ -47,6 +49,7 @@ impl CharacterBehavior for Data { shockwave_angle: self.shockwave_angle, shockwave_speed: self.shockwave_speed, shockwave_duration: self.shockwave_duration, + requires_ground: self.requires_ground, }); } else if !self.exhausted { // Attack @@ -56,7 +59,7 @@ impl CharacterBehavior for Data { duration: self.shockwave_duration, damage: self.damage, knockback: self.knockback, - requires_ground: true, + requires_ground: self.requires_ground, owner: Some(*data.uid), }; update.server_events.push_front(ServerEvent::Shockwave { @@ -74,6 +77,7 @@ impl CharacterBehavior for Data { shockwave_angle: self.shockwave_angle, shockwave_speed: self.shockwave_speed, shockwave_duration: self.shockwave_duration, + requires_ground: self.requires_ground, }); } else if self.recover_duration != Duration::default() { // Recovery @@ -89,6 +93,7 @@ impl CharacterBehavior for Data { shockwave_angle: self.shockwave_angle, shockwave_speed: self.shockwave_speed, shockwave_duration: self.shockwave_duration, + requires_ground: self.requires_ground, }); } else { // Done diff --git a/common/src/sys/combat.rs b/common/src/sys/combat.rs index 54a414f002..7e022f1463 100644 --- a/common/src/sys/combat.rs +++ b/common/src/sys/combat.rs @@ -155,7 +155,7 @@ impl<'a> System<'a> for Sys { if attack.knockback != 0.0 && damage.healthchange != 0.0 { server_emitter.emit(ServerEvent::Knockback { entity: b, - force: attack.knockback + impulse: attack.knockback * *Dir::slerp(ori.0, Dir::new(Vec3::new(0.0, 0.0, 1.0)), 0.5), }); } diff --git a/common/src/sys/projectile.rs b/common/src/sys/projectile.rs index a662eeb0f4..c44822c9a3 100644 --- a/common/src/sys/projectile.rs +++ b/common/src/sys/projectile.rs @@ -117,9 +117,9 @@ impl<'a> System<'a> for Sys { if let Some(entity) = uid_allocator.retrieve_entity_internal(other.into()) { - local_emitter.emit(LocalEvent::ApplyForce { + local_emitter.emit(LocalEvent::ApplyImpulse { entity, - force: knockback + impulse: knockback * *Dir::slerp(ori.0, Dir::new(Vec3::unit_z()), 0.5), }); } diff --git a/common/src/sys/shockwave.rs b/common/src/sys/shockwave.rs index dd27e4d496..ced2d336e1 100644 --- a/common/src/sys/shockwave.rs +++ b/common/src/sys/shockwave.rs @@ -83,22 +83,17 @@ impl<'a> System<'a> for Sys { // If shockwave 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 { + if time > end_time { server_emitter.emit(ServerEvent::Destroy { entity, cause: HealthSource::World, }); - } - - // Determine area that was covered by the shockwave in the last tick - let frame_time = dt.min((end_time - time) as f32); - if frame_time <= 0.0 { continue; } - // Note: min() probably uneeded + // Determine area that was covered by the shockwave in the last tick let time_since_creation = (time - creation_time) as f32; - let frame_start_dist = (shockwave.speed * (time_since_creation - frame_time)).max(0.0); + let frame_start_dist = (shockwave.speed * (time_since_creation - dt)).max(0.0); let frame_end_dist = (shockwave.speed * time_since_creation).max(frame_start_dist); let pos2 = Vec2::from(pos.0); @@ -159,6 +154,11 @@ impl<'a> System<'a> for Sys { let pos_b_ground = Vec3::new(pos_b.0.x, pos_b.0.y, pos.0.z); let max_angle = 15.0_f32.to_radians(); + // See if entities are in the same group + let same_group = group + .map(|group_a| Some(group_a) == groups.get(b)) + .unwrap_or(Some(*uid_b) == shockwave.owner); + // Check if it is a hit let hit = entity != b && !stats_b.is_dead @@ -166,78 +166,54 @@ impl<'a> System<'a> for Sys { && { // TODO: write code to collide rect with the arc strip so that we can do // more complete collision detection for rapidly moving entities - arc_strip.collides_with_circle(Circle { - pos: pos_b2, - radius: rad_b, - }) || last_pos_b2_maybe.map_or(false, |pos| { - arc_strip.collides_with_circle(Circle { pos, radius: rad_b }) + arc_strip.collides_with_circle(Disk::new(pos_b2, rad_b)) || last_pos_b2_maybe.map_or(false, |pos| { + arc_strip.collides_with_circle(Disk::new(pos, rad_b)) }) } && (pos_b_ground - pos.0).angle_between(pos_b.0 - pos.0) < max_angle - && (!shockwave.requires_ground || physics_state_b.on_ground); + && (!shockwave.requires_ground || physics_state_b.on_ground) + && !same_group; if hit { - // See if entities are in the same group - let same_group = group - .map(|group_a| Some(group_a) == groups.get(b)) - .unwrap_or(Some(*uid_b) == shockwave.owner); - - // Don't damage in the same group - if same_group { - continue; - } - - // Weapon gives base damage - let source = DamageSource::Shockwave; - let mut damage = Damage { healthchange: -(shockwave.damage as f32), - source, + source: DamageSource::Shockwave, }; let block = character_b.map(|c_b| c_b.is_block()).unwrap_or(false) - // TODO: investigate whether this calculation is proper for shockwaves && ori_b.0.angle_between(pos.0 - pos_b.0) < BLOCK_ANGLE.to_radians() / 2.0; if let Some(loadout) = loadouts.get(b) { damage.modify_damage(block, loadout); } - if damage.healthchange < 0.0 { + if damage.healthchange != 0.0 { + let cause = if damage.healthchange < 0.0 { + HealthSource::Attack { + by: shockwave.owner.unwrap_or(*uid), + } + } else { + HealthSource::Healing { + by: Some(shockwave.owner.unwrap_or(*uid)), + } + }; server_emitter.emit(ServerEvent::Damage { uid: *uid_b, change: HealthChange { amount: damage.healthchange as i32, - cause: HealthSource::Attack { - by: shockwave.owner.unwrap_or(*uid), - }, - }, - }); - } else if damage.healthchange > 0.0 { - server_emitter.emit(ServerEvent::Damage { - uid: *uid_b, - change: HealthChange { - amount: damage.healthchange as i32, - cause: HealthSource::Healing { - by: Some(shockwave.owner.unwrap_or(*uid)), - }, + cause, }, }); } if shockwave.knockback != 0.0 && damage.healthchange != 0.0 { - if shockwave.knockback < 0.0 { - server_emitter.emit(ServerEvent::Knockback { - entity: b, - force: shockwave.knockback - * *Dir::slerp(ori.0, Dir::new(Vec3::new(0.0, 0.0, -1.0)), 0.85), - }); + let impulse = if shockwave.knockback < 0.0 { + shockwave.knockback + * *Dir::slerp(ori.0, Dir::new(Vec3::new(0.0, 0.0, -1.0)), 0.85) } else { - server_emitter.emit(ServerEvent::Knockback { - entity: b, - force: shockwave.knockback - * *Dir::slerp(ori.0, Dir::new(Vec3::new(0.0, 0.0, 1.0)), 0.5), - }); - } + shockwave.knockback + * *Dir::slerp(ori.0, Dir::new(Vec3::new(0.0, 0.0, 1.0)), 0.5) + }; + server_emitter.emit(ServerEvent::Knockback { entity: b, impulse }); } } } @@ -269,34 +245,28 @@ struct ArcStrip { } impl ArcStrip { - fn collides_with_circle(self, c: Circle) -> bool { + fn collides_with_circle(self, d: Disk) -> bool { // Quit if aabb's don't collide - if (self.origin.x - c.pos.x).abs() > self.end + c.radius - || (self.origin.y - c.pos.y).abs() > self.end + c.radius + if (self.origin.x - d.center.x).abs() > self.end + d.radius + || (self.origin.y - d.center.y).abs() > self.end + d.radius { return false; } - let dist = self.origin.distance(c.pos); + let dist = self.origin.distance(d.center); let half_angle = self.angle.to_radians() / 2.0; - if dist > self.end + c.radius || dist + c.radius < self.start { + if dist > self.end + d.radius || dist + d.radius < self.start { // Completely inside or outside full ring return false; } - let inside_edge = Circle { - pos: self.origin, - radius: self.start, - }; - let outside_edge = Circle { - pos: self.origin, - radius: self.end, - }; + let inside_edge = Disk::new(self.origin, self.start); + let outside_edge = Disk::new(self.origin, self.end); let inner_corner_in_circle = || { let midpoint = self.dir.normalized() * self.start; - c.contains_point(midpoint.rotated_z(half_angle) + self.origin) - || c.contains_point(midpoint.rotated_z(-half_angle) + self.origin) + d.contains_point(midpoint.rotated_z(half_angle) + self.origin) + || d.contains_point(midpoint.rotated_z(-half_angle) + self.origin) }; let arc_segment_in_circle = || { let midpoint = self.dir.normalized(); @@ -306,7 +276,7 @@ impl ArcStrip { start: dir * self.start + self.origin, end: dir * self.end + self.origin, }; - c.contains_point(side.projected_point(c.pos)) + d.contains_point(side.projected_point(d.center)) }; segment_in_circle(half_angle) || segment_in_circle(-half_angle) }; @@ -316,7 +286,7 @@ impl ArcStrip { // Check intersection with line segments arc_segment_in_circle() || { // Check angle of intersection points on outside edge of ring - let (p1, p2) = outside_edge.intersection_points(c, dist); + let (p1, p2) = intersection_points(outside_edge, d, dist); self.dir.angle_between(p1 - self.origin) < half_angle || self.dir.angle_between(p2 - self.origin) < half_angle } @@ -327,54 +297,43 @@ impl ArcStrip { inner_corner_in_circle() || ( // Check that the circles aren't identical - !inside_edge.is_approx_eq(c) && { - let (p1, p2) = inside_edge.intersection_points(c, dist); + inside_edge != d && { + let (p1, p2) = intersection_points(inside_edge, d, dist); self.dir.angle_between(p1 - self.origin) < half_angle || self.dir.angle_between(p2 - self.origin) < half_angle } ) - } else if c.radius > dist { + } else if d.radius > dist { // Circle center inside ring // but center of ring is inside the circle so we can't calculate the angle inner_corner_in_circle() } else { // Circle center inside ring // Calculate extra angle to account for circle radius - let extra_angle = (c.radius / dist).asin(); - self.dir.angle_between(c.pos - self.origin) < half_angle + extra_angle + let extra_angle = (d.radius / dist).asin(); + self.dir.angle_between(d.center - self.origin) < half_angle + extra_angle } } } -#[derive(Clone, Copy)] -struct Circle { - pos: Vec2, - radius: f32, -} -impl Circle { - // Assumes an intersection is occuring at 2 points - // Uses precalculated distance - // https://www.xarg.org/2016/07/calculate-the-intersection-points-of-two-circles/ - fn intersection_points(self, other: Self, dist: f32) -> (Vec2, Vec2) { - let e = (other.pos - self.pos) / dist; - - let x = (self.radius.powi(2) - other.radius.powi(2) + dist.powi(2)) / (2.0 * dist); - let y = (self.radius.powi(2) - x.powi(2)).sqrt(); - - let pxe = self.pos + x * e; - let eyx = e.yx(); - - let p1 = pxe + Vec2::new(-y, y) * eyx; - let p2 = pxe + Vec2::new(y, -y) * eyx; - - (p1, p2) - } - - fn contains_point(self, point: Vec2) -> bool { - point.distance_squared(self.pos) < self.radius.powi(2) - } - - fn is_approx_eq(self, other: Self) -> bool { - (self.pos - other.pos).is_approx_zero() && self.radius - other.radius < 0.001 - } +// Assumes an intersection is occuring at 2 points +// Uses precalculated distance +// https://www.xarg.org/2016/07/calculate-the-intersection-points-of-two-circles/ +fn intersection_points( + disk1: Disk, + disk2: Disk, + dist: f32, +) -> (Vec2, Vec2) { + let e = (disk2.center - disk1.center) / dist; + + let x = (disk1.radius.powi(2) - disk2.radius.powi(2) + dist.powi(2)) / (2.0 * dist); + let y = (disk1.radius.powi(2) - x.powi(2)).sqrt(); + + let pxe = disk1.center + x * e; + let eyx = e.yx(); + + let p1 = pxe + Vec2::new(-y, y) * eyx; + let p2 = pxe + Vec2::new(y, -y) * eyx; + + (p1, p2) } diff --git a/server/src/events/entity_creation.rs b/server/src/events/entity_creation.rs index 022f007b3c..4c9e3d048a 100644 --- a/server/src/events/entity_creation.rs +++ b/server/src/events/entity_creation.rs @@ -2,8 +2,9 @@ use crate::{sys, Server, StateExt}; use common::{ character::CharacterId, comp::{ - self, humanoid::DEFAULT_HUMANOID_EYE_HEIGHT, shockwave, Agent, Alignment, Body, Gravity, Item, - ItemDrop, LightEmitter, Loadout, Ori, Pos, Projectile, Scale, Stats, Vel, WaypointArea, + self, humanoid::DEFAULT_HUMANOID_EYE_HEIGHT, shockwave, Agent, Alignment, Body, Gravity, + Item, ItemDrop, LightEmitter, Loadout, Ori, Pos, Projectile, Scale, Stats, Vel, + WaypointArea, }, outcome::Outcome, util::Dir, diff --git a/server/src/events/entity_manipulation.rs b/server/src/events/entity_manipulation.rs index 37331df6b0..ea1cdabb6b 100644 --- a/server/src/events/entity_manipulation.rs +++ b/server/src/events/entity_manipulation.rs @@ -32,16 +32,15 @@ pub fn handle_damage(server: &Server, uid: Uid, change: HealthChange) { } } -pub fn handle_knockback(server: &Server, entity: EcsEntity, force: Vec3) { +pub fn handle_knockback(server: &Server, entity: EcsEntity, impulse: Vec3) { let state = &server.state; - let ecs = state.ecs(); - let mut velocities = ecs.write_storage::(); + let mut velocities = state.ecs().write_storage::(); if let Some(vel) = velocities.get_mut(entity) { - vel.0 = force; + vel.0 = impulse; } let mut clients = state.ecs().write_storage::(); if let Some(client) = clients.get_mut(entity) { - client.notify(ServerMsg::Knockback(force)); + client.notify(ServerMsg::Knockback(impulse)); } } diff --git a/server/src/events/mod.rs b/server/src/events/mod.rs index 224abd1b4b..8bbcf9926e 100644 --- a/server/src/events/mod.rs +++ b/server/src/events/mod.rs @@ -75,7 +75,9 @@ impl Server { pos, ori, } => handle_shockwave(self, properties, pos, ori), - ServerEvent::Knockback { entity, force } => handle_knockback(&self, entity, force), + ServerEvent::Knockback { entity, impulse } => { + handle_knockback(&self, entity, impulse) + }, ServerEvent::Damage { uid, change } => handle_damage(&self, uid, change), ServerEvent::Destroy { entity, cause } => handle_destroy(self, entity, cause), ServerEvent::InventoryManip(entity, manip) => handle_inventory(self, entity, manip), diff --git a/voxygen/src/scene/particle.rs b/voxygen/src/scene/particle.rs index 426274ef79..3424783e01 100644 --- a/voxygen/src/scene/particle.rs +++ b/voxygen/src/scene/particle.rs @@ -447,28 +447,24 @@ impl ParticleMgr { // 1 / 3 the size of terrain voxel let scale = 1.0 / 3.0; - let freqency_millis = shockwave.properties.speed * scale; + let scaled_speed = shockwave.properties.speed * scale; for heartbeat in 0..self .scheduler - .heartbeats(Duration::from_millis(freqency_millis as u64)) + .heartbeats(Duration::from_millis(scaled_speed as u64)) { - let sub_tick_interpolation = freqency_millis * 1000.0 * heartbeat as f32; + let sub_tick_interpolation = scaled_speed * 1000.0 * heartbeat as f32; let distance = shockwave.properties.speed * (elapsed as f32 - sub_tick_interpolation); for d in 0..((distance / scale) as i32) { - let arc_position = (theta - (radians / 2.0)) + (dtheta * d as f32 * scale); + let arc_position = theta - radians / 2.0 + dtheta * d as f32 * scale; - let position = pos.0 - + Vec3::new( - distance * arc_position.cos(), - distance * arc_position.sin(), - 0.0, - ); + let position = + pos.0 + distance * Vec3::new(arc_position.cos(), arc_position.sin(), 0.0); - let position_snapped = (position / scale).round() * scale; + let position_snapped = ((position / scale).floor() + 0.5) * scale; self.particles.push(Particle::new( Duration::from_millis(250),