mirror of
https://gitlab.com/veloren/veloren.git
synced 2024-08-30 18:12:32 +00:00
Piercing damage now ignores an amount of protection equal to the piercing damage value. Crushing damage now does poise damage equal to the amount of mitigated damage. When poise damage is dealt while in a poise state, poise damage is instead converted to damage.
363 lines
14 KiB
Rust
363 lines
14 KiB
Rust
use common::{
|
|
combat::{self, AttackOptions, AttackSource, AttackerInfo, TargetInfo},
|
|
comp::{
|
|
agent::{Sound, SoundKind},
|
|
Alignment, Body, CharacterState, Combo, Energy, Group, Health, Inventory, Ori,
|
|
PhysicsState, Player, Pos, Scale, Shockwave, ShockwaveHitEntities, Stats,
|
|
},
|
|
event::{EventBus, ServerEvent},
|
|
outcome::Outcome,
|
|
resources::{DeltaTime, Time},
|
|
uid::{Uid, UidAllocator},
|
|
util::Dir,
|
|
GroupTarget,
|
|
};
|
|
use common_ecs::{Job, Origin, Phase, System};
|
|
use rand::{thread_rng, Rng};
|
|
use specs::{
|
|
saveload::MarkerAllocator, shred::ResourceId, Entities, Join, Read, ReadStorage, SystemData,
|
|
World, Write, WriteStorage,
|
|
};
|
|
use vek::*;
|
|
|
|
#[derive(SystemData)]
|
|
pub struct ReadData<'a> {
|
|
entities: Entities<'a>,
|
|
server_bus: Read<'a, EventBus<ServerEvent>>,
|
|
time: Read<'a, Time>,
|
|
players: ReadStorage<'a, Player>,
|
|
dt: Read<'a, DeltaTime>,
|
|
uid_allocator: Read<'a, UidAllocator>,
|
|
uids: ReadStorage<'a, Uid>,
|
|
positions: ReadStorage<'a, Pos>,
|
|
orientations: ReadStorage<'a, Ori>,
|
|
alignments: ReadStorage<'a, Alignment>,
|
|
scales: ReadStorage<'a, Scale>,
|
|
bodies: ReadStorage<'a, Body>,
|
|
healths: ReadStorage<'a, Health>,
|
|
inventories: ReadStorage<'a, Inventory>,
|
|
groups: ReadStorage<'a, Group>,
|
|
physics_states: ReadStorage<'a, PhysicsState>,
|
|
energies: ReadStorage<'a, Energy>,
|
|
stats: ReadStorage<'a, Stats>,
|
|
combos: ReadStorage<'a, Combo>,
|
|
character_states: ReadStorage<'a, CharacterState>,
|
|
}
|
|
|
|
/// This system is responsible for handling accepted inputs like moving or
|
|
/// attacking
|
|
#[derive(Default)]
|
|
pub struct Sys;
|
|
impl<'a> System<'a> for Sys {
|
|
type SystemData = (
|
|
ReadData<'a>,
|
|
WriteStorage<'a, Shockwave>,
|
|
WriteStorage<'a, ShockwaveHitEntities>,
|
|
Write<'a, Vec<Outcome>>,
|
|
);
|
|
|
|
const NAME: &'static str = "shockwave";
|
|
const ORIGIN: Origin = Origin::Common;
|
|
const PHASE: Phase = Phase::Create;
|
|
|
|
fn run(
|
|
_job: &mut Job<Self>,
|
|
(read_data, mut shockwaves, mut shockwave_hit_lists, mut outcomes): Self::SystemData,
|
|
) {
|
|
let mut server_emitter = read_data.server_bus.emitter();
|
|
|
|
let time = read_data.time.0;
|
|
let dt = read_data.dt.0;
|
|
|
|
// Shockwaves
|
|
for (entity, pos, ori, shockwave, shockwave_hit_list) in (
|
|
&read_data.entities,
|
|
&read_data.positions,
|
|
&read_data.orientations,
|
|
&shockwaves,
|
|
&mut shockwave_hit_lists,
|
|
)
|
|
.join()
|
|
{
|
|
let creation_time = match shockwave.creation {
|
|
Some(time) => time,
|
|
// Skip newly created shockwaves
|
|
None => continue,
|
|
};
|
|
|
|
let end_time = creation_time + shockwave.duration.as_secs_f64();
|
|
|
|
let shockwave_owner = shockwave
|
|
.owner
|
|
.and_then(|uid| read_data.uid_allocator.retrieve_entity_internal(uid.into()));
|
|
|
|
let mut rng = thread_rng();
|
|
if rng.gen_bool(0.05) {
|
|
server_emitter.emit(ServerEvent::Sound {
|
|
sound: Sound::new(SoundKind::Shockwave, pos.0, 16.0, time),
|
|
});
|
|
}
|
|
|
|
// 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 time > end_time {
|
|
server_emitter.emit(ServerEvent::Delete(entity));
|
|
continue;
|
|
}
|
|
|
|
// 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 - dt)).max(0.0);
|
|
let frame_end_dist = (shockwave.speed * time_since_creation).max(frame_start_dist);
|
|
let pos2 = Vec2::from(pos.0);
|
|
let look_dir = ori.look_dir();
|
|
|
|
// From one frame to the next a shockwave travels over a strip of an arc
|
|
// This is used for collision detection
|
|
let arc_strip = ArcStrip {
|
|
origin: pos2,
|
|
// TODO: make sure this is not Vec2::new(0.0, 0.0)
|
|
dir: look_dir.xy(),
|
|
angle: shockwave.angle,
|
|
start: frame_start_dist,
|
|
end: frame_end_dist,
|
|
};
|
|
|
|
// Group to ignore collisions with
|
|
// Might make this more nuanced if shockwaves are used for non damage effects
|
|
let group = shockwave_owner.and_then(|e| read_data.groups.get(e));
|
|
|
|
// Go through all other effectable entities
|
|
for (target, uid_b, pos_b, health_b, body_b, physics_state_b) in (
|
|
&read_data.entities,
|
|
&read_data.uids,
|
|
&read_data.positions,
|
|
&read_data.healths,
|
|
&read_data.bodies,
|
|
&read_data.physics_states,
|
|
)
|
|
.join()
|
|
{
|
|
// Check to see if entity has already been hit
|
|
if shockwave_hit_list
|
|
.hit_entities
|
|
.iter()
|
|
.any(|&uid| uid == *uid_b)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
// 2D versions
|
|
let pos_b2 = pos_b.0.xy();
|
|
|
|
// Scales
|
|
let scale_b = read_data.scales.get(target).map_or(1.0, |s| s.0);
|
|
// TODO: use Capsule Prism instead of Cylinder
|
|
let rad_b = body_b.max_radius() * scale_b;
|
|
|
|
// Angle checks
|
|
let pos_b_ground = Vec3::new(pos_b.0.x, pos_b.0.y, pos.0.z);
|
|
let max_angle = shockwave.vertical_angle.to_radians();
|
|
|
|
// 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) == shockwave.owner);
|
|
|
|
let target_group = if same_group {
|
|
GroupTarget::InGroup
|
|
} else {
|
|
GroupTarget::OutOfGroup
|
|
};
|
|
|
|
// Check if it is a hit
|
|
let hit = entity != target
|
|
&& !health_b.is_dead
|
|
&& (pos_b.0 - pos.0).magnitude() < frame_end_dist + rad_b
|
|
// Collision shapes
|
|
&& {
|
|
// 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(Disk::new(pos_b2, rad_b))
|
|
}
|
|
&& (pos_b_ground - pos.0).angle_between(pos_b.0 - pos.0) < max_angle
|
|
&& (!shockwave.requires_ground || physics_state_b.on_ground.is_some());
|
|
|
|
if hit {
|
|
let dir = Dir::from_unnormalized(pos_b.0 - pos.0).unwrap_or(look_dir);
|
|
|
|
let attacker_info =
|
|
shockwave_owner
|
|
.zip(shockwave.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),
|
|
});
|
|
|
|
let target_info = TargetInfo {
|
|
entity: target,
|
|
uid: *uid_b,
|
|
inventory: read_data.inventories.get(target),
|
|
stats: read_data.stats.get(target),
|
|
health: read_data.healths.get(target),
|
|
pos: pos_b.0,
|
|
ori: read_data.orientations.get(target),
|
|
char_state: read_data.character_states.get(target),
|
|
energy: read_data.energies.get(target),
|
|
};
|
|
|
|
// PvP check
|
|
let may_harm = combat::may_harm(
|
|
&read_data.alignments,
|
|
&read_data.players,
|
|
&read_data.uid_allocator,
|
|
shockwave_owner,
|
|
target,
|
|
);
|
|
let attack_options = AttackOptions {
|
|
// Trying roll during earthquake isn't the best idea
|
|
target_dodging: false,
|
|
may_harm,
|
|
target_group,
|
|
};
|
|
|
|
shockwave.properties.attack.apply_attack(
|
|
attacker_info,
|
|
target_info,
|
|
dir,
|
|
attack_options,
|
|
1.0,
|
|
AttackSource::Shockwave,
|
|
*read_data.time,
|
|
|e| server_emitter.emit(e),
|
|
|o| outcomes.push(o),
|
|
);
|
|
|
|
shockwave_hit_list.hit_entities.push(*uid_b);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Set start time on new shockwaves
|
|
// This change doesn't need to be recorded as it is not sent to the client
|
|
shockwaves.set_event_emission(false);
|
|
(&mut shockwaves).join().for_each(|mut shockwave| {
|
|
if shockwave.creation.is_none() {
|
|
shockwave.creation = Some(time);
|
|
}
|
|
});
|
|
shockwaves.set_event_emission(true);
|
|
}
|
|
}
|
|
|
|
#[derive(Clone, Copy)]
|
|
struct ArcStrip {
|
|
origin: Vec2<f32>,
|
|
/// Normalizable direction
|
|
dir: Vec2<f32>,
|
|
/// Angle in degrees
|
|
angle: f32,
|
|
/// Start radius
|
|
start: f32,
|
|
/// End radius
|
|
end: f32,
|
|
}
|
|
|
|
impl ArcStrip {
|
|
fn collides_with_circle(self, d: Disk<f32, f32>) -> bool {
|
|
// Quit if aabb's don't collide
|
|
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(d.center);
|
|
let half_angle = self.angle.to_radians() / 2.0;
|
|
|
|
if dist > self.end + d.radius || dist + d.radius < self.start {
|
|
// Completely inside or outside full ring
|
|
return false;
|
|
}
|
|
|
|
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;
|
|
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();
|
|
let segment_in_circle = |angle| {
|
|
let dir = midpoint.rotated_z(angle);
|
|
let side = LineSegment2 {
|
|
start: dir * self.start + self.origin,
|
|
end: dir * self.end + self.origin,
|
|
};
|
|
d.contains_point(side.projected_point(d.center))
|
|
};
|
|
segment_in_circle(half_angle) || segment_in_circle(-half_angle)
|
|
};
|
|
|
|
if dist > self.end {
|
|
// Circle center is outside ring
|
|
// Check intersection with line segments
|
|
arc_segment_in_circle() || {
|
|
// Check angle of intersection points on outside edge of ring
|
|
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
|
|
}
|
|
} else if dist < self.start {
|
|
// Circle center is inside ring
|
|
// Check angle of intersection points on inside edge of ring
|
|
// Check if circle contains one of the inner points of the arc
|
|
inner_corner_in_circle()
|
|
|| (
|
|
// Check that the circles aren't identical
|
|
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 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 = (d.radius / dist).asin();
|
|
self.dir.angle_between(d.center - self.origin) < half_angle + extra_angle
|
|
}
|
|
}
|
|
}
|
|
|
|
// 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<f32, f32>,
|
|
disk2: Disk<f32, f32>,
|
|
dist: f32,
|
|
) -> (Vec2<f32>, Vec2<f32>) {
|
|
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)
|
|
}
|