1use common::{
2 GroupTarget,
3 combat::{self, AttackOptions, AttackerInfo, TargetInfo},
4 comp::{
5 Alignment, Body, Buffs, CharacterState, Combo, Energy, Group, Health, Inventory, Mass, Ori,
6 PhysicsState, Player, Pos, Scale, Shockwave, ShockwaveHitEntities, Stats,
7 ability::Dodgeable,
8 agent::{Sound, SoundKind},
9 aura::EnteredAuras,
10 },
11 event::{
12 BuffEvent, ComboChangeEvent, DeleteEvent, EmitExt, EnergyChangeEvent,
13 EntityAttackedHookEvent, EventBus, HealthChangeEvent, KnockbackEvent, MineBlockEvent,
14 ParryHookEvent, PoiseChangeEvent, SoundEvent,
15 },
16 event_emitters,
17 outcome::Outcome,
18 resources::{DeltaTime, Time},
19 uid::{IdMaps, Uid},
20 util::Dir,
21};
22use common_ecs::{Job, Origin, Phase, System};
23use rand::Rng;
24use specs::{Entities, Join, LendJoin, Read, ReadStorage, SystemData, WriteStorage, shred};
25use vek::*;
26
27event_emitters! {
28 struct Events[Emitters] {
29 health_change: HealthChangeEvent,
30 energy_change: EnergyChangeEvent,
31 poise_change: PoiseChangeEvent,
32 sound: SoundEvent,
33 mine_block: MineBlockEvent,
34 parry_hook: ParryHookEvent,
35 knockback: KnockbackEvent,
36 entity_attack_hoow: EntityAttackedHookEvent,
37 combo_change: ComboChangeEvent,
38 buff: BuffEvent,
39 delete: DeleteEvent,
40 }
41}
42
43#[derive(SystemData)]
44pub struct ReadData<'a> {
45 entities: Entities<'a>,
46 events: Events<'a>,
47 time: Read<'a, Time>,
48 players: ReadStorage<'a, Player>,
49 dt: Read<'a, DeltaTime>,
50 id_maps: Read<'a, IdMaps>,
51 uids: ReadStorage<'a, Uid>,
52 positions: ReadStorage<'a, Pos>,
53 orientations: ReadStorage<'a, Ori>,
54 alignments: ReadStorage<'a, Alignment>,
55 scales: ReadStorage<'a, Scale>,
56 bodies: ReadStorage<'a, Body>,
57 healths: ReadStorage<'a, Health>,
58 inventories: ReadStorage<'a, Inventory>,
59 groups: ReadStorage<'a, Group>,
60 physics_states: ReadStorage<'a, PhysicsState>,
61 energies: ReadStorage<'a, Energy>,
62 stats: ReadStorage<'a, Stats>,
63 combos: ReadStorage<'a, Combo>,
64 character_states: ReadStorage<'a, CharacterState>,
65 buffs: ReadStorage<'a, Buffs>,
66 entered_auras: ReadStorage<'a, EnteredAuras>,
67 masses: ReadStorage<'a, Mass>,
68}
69
70#[derive(Default)]
73pub struct Sys;
74impl<'a> System<'a> for Sys {
75 type SystemData = (
76 ReadData<'a>,
77 WriteStorage<'a, Shockwave>,
78 WriteStorage<'a, ShockwaveHitEntities>,
79 Read<'a, EventBus<Outcome>>,
80 );
81
82 const NAME: &'static str = "shockwave";
83 const ORIGIN: Origin = Origin::Common;
84 const PHASE: Phase = Phase::Create;
85
86 fn run(
87 _job: &mut Job<Self>,
88 (read_data, mut shockwaves, mut shockwave_hit_lists, outcomes): Self::SystemData,
89 ) {
90 let mut emitters = read_data.events.get_emitters();
91 let mut outcomes_emitter = outcomes.emitter();
92 let mut rng = rand::thread_rng();
93
94 let time = read_data.time.0;
95 let dt = read_data.dt.0;
96
97 for (entity, pos, ori, shockwave, shockwave_hit_list) in (
99 &read_data.entities,
100 &read_data.positions,
101 &read_data.orientations,
102 &shockwaves,
103 &mut shockwave_hit_lists,
104 )
105 .join()
106 {
107 let creation_time = match shockwave.creation {
108 Some(time) => time,
109 None => continue,
111 };
112
113 let end_time = creation_time + shockwave.duration.as_secs_f64();
114
115 let shockwave_owner = shockwave
116 .owner
117 .and_then(|uid| read_data.id_maps.uid_entity(uid));
118
119 if rng.gen_bool(0.05) {
120 emitters.emit(SoundEvent {
121 sound: Sound::new(SoundKind::Shockwave, pos.0, 40.0, time),
122 });
123 }
124
125 if time > end_time {
128 emitters.emit(DeleteEvent(entity));
129 continue;
130 }
131
132 let time_since_creation = (time - creation_time) as f32;
134 let frame_start_dist = (shockwave.speed * (time_since_creation - dt)).max(0.0);
135 let frame_end_dist = (shockwave.speed * time_since_creation).max(frame_start_dist);
136 let pos2 = Vec2::from(pos.0);
137 let look_dir = ori.look_dir();
138
139 let arc_strip = ArcStrip {
142 origin: pos2,
143 dir: look_dir.xy(),
145 angle: shockwave.angle,
146 start: frame_start_dist,
147 end: frame_end_dist,
148 };
149
150 let group = shockwave_owner.and_then(|e| read_data.groups.get(e));
153
154 for (target, uid_b, pos_b, health_b, body_b, physics_state_b) in (
156 &read_data.entities,
157 &read_data.uids,
158 &read_data.positions,
159 &read_data.healths,
160 &read_data.bodies,
161 &read_data.physics_states,
162 )
163 .join()
164 {
165 if shockwave_hit_list
167 .hit_entities
168 .iter()
169 .any(|&uid| uid == *uid_b)
170 {
171 continue;
172 }
173
174 let pos_b2 = pos_b.0.xy();
176
177 let scale_b = read_data.scales.get(target).map_or(1.0, |s| s.0);
179 let rad_b = body_b.max_radius() * scale_b;
181
182 let pos_b_ground = Vec3::new(pos_b.0.x, pos_b.0.y, pos.0.z);
184 let max_angle = shockwave.vertical_angle.to_radians();
185
186 let same_group = group
188 .map(|group_a| Some(group_a) == read_data.groups.get(target))
189 .unwrap_or(Some(*uid_b) == shockwave.owner);
190
191 let target_group = if same_group {
192 GroupTarget::InGroup
193 } else {
194 GroupTarget::OutOfGroup
195 };
196
197 let hit = entity != target
205 && (shockwave_owner != Some(target))
206 && !health_b.is_dead
207 && (pos_b.0 - pos.0).magnitude() < frame_end_dist + rad_b
208 && {
210 arc_strip.collides_with_circle(Disk::new(pos_b2, rad_b))
213 }
214 && (pos_b_ground - pos.0).angle_between(pos_b.0 - pos.0) < max_angle
215 && match shockwave.dodgeable {
216 Dodgeable::Roll | Dodgeable::No => true,
217 Dodgeable::Jump => physics_state_b.on_ground.is_some()
218 };
219
220 if hit {
221 let allow_friendly_fire = shockwave_owner.is_some_and(|entity| {
222 combat::allow_friendly_fire(&read_data.entered_auras, entity, target)
223 });
224 let dir = Dir::from_unnormalized(pos_b.0 - pos.0).unwrap_or(look_dir);
225
226 let attacker_info =
227 shockwave_owner
228 .zip(shockwave.owner)
229 .map(|(entity, uid)| AttackerInfo {
230 entity,
231 uid,
232 group: read_data.groups.get(entity),
233 energy: read_data.energies.get(entity),
234 combo: read_data.combos.get(entity),
235 inventory: read_data.inventories.get(entity),
236 stats: read_data.stats.get(entity),
237 mass: read_data.masses.get(entity),
238 });
239
240 let target_info = TargetInfo {
241 entity: target,
242 uid: *uid_b,
243 inventory: read_data.inventories.get(target),
244 stats: read_data.stats.get(target),
245 health: read_data.healths.get(target),
246 pos: pos_b.0,
247 ori: read_data.orientations.get(target),
248 char_state: read_data.character_states.get(target),
249 energy: read_data.energies.get(target),
250 buffs: read_data.buffs.get(target),
251 mass: read_data.masses.get(target),
252 };
253
254 let target_dodging = read_data
255 .character_states
256 .get(target)
257 .and_then(|cs| cs.roll_attack_immunities())
258 .is_some_and(|i| match shockwave.dodgeable {
259 Dodgeable::Roll => i.air_shockwaves,
260 Dodgeable::Jump => i.ground_shockwaves,
261 Dodgeable::No => false,
262 });
263 let permit_pvp = combat::permit_pvp(
265 &read_data.alignments,
266 &read_data.players,
267 &read_data.entered_auras,
268 &read_data.id_maps,
269 shockwave_owner,
270 target,
271 );
272 let precision_mult = None;
274 let attack_options = AttackOptions {
275 target_dodging,
276 permit_pvp,
277 allow_friendly_fire,
278 target_group,
279 precision_mult,
280 };
281
282 shockwave.properties.attack.apply_attack(
283 attacker_info,
284 &target_info,
285 dir,
286 attack_options,
287 1.0,
288 shockwave.dodgeable.shockwave_attack_source(),
289 *read_data.time,
290 &mut emitters,
291 |o| outcomes_emitter.emit(o),
292 &mut rng,
293 0,
294 );
295
296 shockwave_hit_list.hit_entities.push(*uid_b);
297 }
298 }
299 }
300
301 shockwaves.set_event_emission(false);
304 (&mut shockwaves).lend_join().for_each(|mut shockwave| {
305 if shockwave.creation.is_none() {
306 shockwave.creation = Some(time);
307 }
308 });
309 shockwaves.set_event_emission(true);
310 }
311}
312
313#[derive(Clone, Copy)]
314struct ArcStrip {
315 origin: Vec2<f32>,
316 dir: Vec2<f32>,
318 angle: f32,
320 start: f32,
322 end: f32,
324}
325
326impl ArcStrip {
327 fn collides_with_circle(self, d: Disk<f32, f32>) -> bool {
328 if (self.origin.x - d.center.x).abs() > self.end + d.radius
330 || (self.origin.y - d.center.y).abs() > self.end + d.radius
331 {
332 return false;
333 }
334
335 let dist = self.origin.distance(d.center);
336 let half_angle = self.angle.to_radians() / 2.0;
337
338 if dist > self.end + d.radius || dist + d.radius < self.start {
339 return false;
341 }
342
343 let inside_edge = Disk::new(self.origin, self.start);
344 let outside_edge = Disk::new(self.origin, self.end);
345 let inner_corner_in_circle = || {
346 let midpoint = self.dir.normalized() * self.start;
347 d.contains_point(midpoint.rotated_z(half_angle) + self.origin)
348 || d.contains_point(midpoint.rotated_z(-half_angle) + self.origin)
349 };
350 let arc_segment_in_circle = || {
351 let midpoint = self.dir.normalized();
352 let segment_in_circle = |angle| {
353 let dir = midpoint.rotated_z(angle);
354 let side = LineSegment2 {
355 start: dir * self.start + self.origin,
356 end: dir * self.end + self.origin,
357 };
358 d.contains_point(side.projected_point(d.center))
359 };
360 segment_in_circle(half_angle) || segment_in_circle(-half_angle)
361 };
362
363 if dist > self.end {
364 arc_segment_in_circle() || {
367 let (p1, p2) = intersection_points(outside_edge, d, dist);
369 self.dir.angle_between(p1 - self.origin) < half_angle
370 || self.dir.angle_between(p2 - self.origin) < half_angle
371 }
372 } else if dist < self.start {
373 inner_corner_in_circle()
377 || (
378 inside_edge != d && {
380 let (p1, p2) = intersection_points(inside_edge, d, dist);
381 self.dir.angle_between(p1 - self.origin) < half_angle
382 || self.dir.angle_between(p2 - self.origin) < half_angle
383 }
384 )
385 } else if d.radius > dist {
386 inner_corner_in_circle()
389 } else {
390 let extra_angle = (d.radius / dist).asin();
393 self.dir.angle_between(d.center - self.origin) < half_angle + extra_angle
394 }
395 }
396}
397
398fn intersection_points(
402 disk1: Disk<f32, f32>,
403 disk2: Disk<f32, f32>,
404 dist: f32,
405) -> (Vec2<f32>, Vec2<f32>) {
406 let e = (disk2.center - disk1.center) / dist;
407
408 let x = (disk1.radius.powi(2) - disk2.radius.powi(2) + dist.powi(2)) / (2.0 * dist);
409 let y = (disk1.radius.powi(2) - x.powi(2)).sqrt();
410
411 let pxe = disk1.center + x * e;
412 let eyx = e.yx();
413
414 let p1 = pxe + Vec2::new(-y, y) * eyx;
415 let p2 = pxe + Vec2::new(y, -y) * eyx;
416
417 (p1, p2)
418}