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::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.random_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.hit_entities.contains(uid_b) {
167 continue;
168 }
169
170 let pos_b2 = pos_b.0.xy();
172
173 let scale_b = read_data.scales.get(target).map_or(1.0, |s| s.0);
175 let rad_b = body_b.max_radius() * scale_b;
177
178 let pos_b_ground = Vec3::new(pos_b.0.x, pos_b.0.y, pos.0.z);
180 let max_angle = shockwave.vertical_angle.to_radians();
181
182 let same_group = group
184 .map(|group_a| Some(group_a) == read_data.groups.get(target))
185 .unwrap_or(Some(*uid_b) == shockwave.owner);
186
187 let target_group = if same_group {
188 GroupTarget::InGroup
189 } else {
190 GroupTarget::OutOfGroup
191 };
192
193 let hit = entity != target
201 && (shockwave_owner != Some(target))
202 && !health_b.is_dead
203 && (pos_b.0 - pos.0).magnitude() < frame_end_dist + rad_b
204 && {
206 arc_strip.collides_with_circle(Disk::new(pos_b2, rad_b))
209 }
210 && (pos_b_ground - pos.0).angle_between(pos_b.0 - pos.0) < max_angle
211 && match shockwave.dodgeable {
212 Dodgeable::Roll | Dodgeable::No => true,
213 Dodgeable::Jump => physics_state_b.on_ground.is_some()
214 };
215
216 if hit {
217 let allow_friendly_fire = shockwave_owner.is_some_and(|entity| {
218 combat::allow_friendly_fire(&read_data.entered_auras, entity, target)
219 });
220 let dir = Dir::from_unnormalized(pos_b.0 - pos.0).unwrap_or(look_dir);
221
222 let attacker_info =
223 shockwave_owner
224 .zip(shockwave.owner)
225 .map(|(entity, uid)| AttackerInfo {
226 entity,
227 uid,
228 group: read_data.groups.get(entity),
229 energy: read_data.energies.get(entity),
230 combo: read_data.combos.get(entity),
231 inventory: read_data.inventories.get(entity),
232 stats: read_data.stats.get(entity),
233 mass: read_data.masses.get(entity),
234 });
235
236 let target_info = TargetInfo {
237 entity: target,
238 uid: *uid_b,
239 inventory: read_data.inventories.get(target),
240 stats: read_data.stats.get(target),
241 health: read_data.healths.get(target),
242 pos: pos_b.0,
243 ori: read_data.orientations.get(target),
244 char_state: read_data.character_states.get(target),
245 energy: read_data.energies.get(target),
246 buffs: read_data.buffs.get(target),
247 mass: read_data.masses.get(target),
248 };
249
250 let target_dodging = read_data
251 .character_states
252 .get(target)
253 .and_then(|cs| cs.roll_attack_immunities())
254 .is_some_and(|i| match shockwave.dodgeable {
255 Dodgeable::Roll => i.air_shockwaves,
256 Dodgeable::Jump => i.ground_shockwaves,
257 Dodgeable::No => false,
258 });
259 let permit_pvp = combat::permit_pvp(
261 &read_data.alignments,
262 &read_data.players,
263 &read_data.entered_auras,
264 &read_data.id_maps,
265 shockwave_owner,
266 target,
267 );
268 let precision_mult = None;
270 let attack_options = AttackOptions {
271 target_dodging,
272 permit_pvp,
273 allow_friendly_fire,
274 target_group,
275 precision_mult,
276 };
277
278 shockwave.properties.attack.apply_attack(
279 attacker_info,
280 &target_info,
281 dir,
282 attack_options,
283 1.0,
284 shockwave.dodgeable.shockwave_attack_source(),
285 *read_data.time,
286 &mut emitters,
287 |o| outcomes_emitter.emit(o),
288 &mut rng,
289 0,
290 );
291
292 shockwave_hit_list.hit_entities.push(*uid_b);
293 }
294 }
295 }
296
297 shockwaves.set_event_emission(false);
300 (&mut shockwaves).lend_join().for_each(|mut shockwave| {
301 if shockwave.creation.is_none() {
302 shockwave.creation = Some(time);
303 }
304 });
305 shockwaves.set_event_emission(true);
306 }
307}
308
309#[derive(Clone, Copy)]
310struct ArcStrip {
311 origin: Vec2<f32>,
312 dir: Vec2<f32>,
314 angle: f32,
316 start: f32,
318 end: f32,
320}
321
322impl ArcStrip {
323 fn collides_with_circle(self, d: Disk<f32, f32>) -> bool {
324 if (self.origin.x - d.center.x).abs() > self.end + d.radius
326 || (self.origin.y - d.center.y).abs() > self.end + d.radius
327 {
328 return false;
329 }
330
331 let dist = self.origin.distance(d.center);
332 let half_angle = self.angle.to_radians() / 2.0;
333
334 if dist > self.end + d.radius || dist + d.radius < self.start {
335 return false;
337 }
338
339 let inside_edge = Disk::new(self.origin, self.start);
340 let outside_edge = Disk::new(self.origin, self.end);
341 let inner_corner_in_circle = || {
342 let midpoint = self.dir.normalized() * self.start;
343 d.contains_point(midpoint.rotated_z(half_angle) + self.origin)
344 || d.contains_point(midpoint.rotated_z(-half_angle) + self.origin)
345 };
346 let arc_segment_in_circle = || {
347 let midpoint = self.dir.normalized();
348 let segment_in_circle = |angle| {
349 let dir = midpoint.rotated_z(angle);
350 let side = LineSegment2 {
351 start: dir * self.start + self.origin,
352 end: dir * self.end + self.origin,
353 };
354 d.contains_point(side.projected_point(d.center))
355 };
356 segment_in_circle(half_angle) || segment_in_circle(-half_angle)
357 };
358
359 if dist > self.end {
360 arc_segment_in_circle() || {
363 let (p1, p2) = intersection_points(outside_edge, d, dist);
365 self.dir.angle_between(p1 - self.origin) < half_angle
366 || self.dir.angle_between(p2 - self.origin) < half_angle
367 }
368 } else if dist < self.start {
369 inner_corner_in_circle()
373 || (
374 inside_edge != d && {
376 let (p1, p2) = intersection_points(inside_edge, d, dist);
377 self.dir.angle_between(p1 - self.origin) < half_angle
378 || self.dir.angle_between(p2 - self.origin) < half_angle
379 }
380 )
381 } else if d.radius > dist {
382 inner_corner_in_circle()
385 } else {
386 let extra_angle = (d.radius / dist).asin();
389 self.dir.angle_between(d.center - self.origin) < half_angle + extra_angle
390 }
391 }
392}
393
394fn intersection_points(
398 disk1: Disk<f32, f32>,
399 disk2: Disk<f32, f32>,
400 dist: f32,
401) -> (Vec2<f32>, Vec2<f32>) {
402 let e = (disk2.center - disk1.center) / dist;
403
404 let x = (disk1.radius.powi(2) - disk2.radius.powi(2) + dist.powi(2)) / (2.0 * dist);
405 let y = (disk1.radius.powi(2) - x.powi(2)).sqrt();
406
407 let pxe = disk1.center + x * e;
408 let eyx = e.yx();
409
410 let p1 = pxe + Vec2::new(-y, y) * eyx;
411 let p2 = pxe + Vec2::new(y, -y) * eyx;
412
413 (p1, p2)
414}