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, TransformEvent,
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_hook: EntityAttackedHookEvent,
37 combo_change: ComboChangeEvent,
38 buff: BuffEvent,
39 delete: DeleteEvent,
40 transform: TransformEvent,
41 }
42}
43
44#[derive(SystemData)]
45pub struct ReadData<'a> {
46 entities: Entities<'a>,
47 events: Events<'a>,
48 time: Read<'a, Time>,
49 players: ReadStorage<'a, Player>,
50 dt: Read<'a, DeltaTime>,
51 id_maps: Read<'a, IdMaps>,
52 uids: ReadStorage<'a, Uid>,
53 positions: ReadStorage<'a, Pos>,
54 orientations: ReadStorage<'a, Ori>,
55 alignments: ReadStorage<'a, Alignment>,
56 scales: ReadStorage<'a, Scale>,
57 bodies: ReadStorage<'a, Body>,
58 healths: ReadStorage<'a, Health>,
59 inventories: ReadStorage<'a, Inventory>,
60 groups: ReadStorage<'a, Group>,
61 physics_states: ReadStorage<'a, PhysicsState>,
62 energies: ReadStorage<'a, Energy>,
63 stats: ReadStorage<'a, Stats>,
64 combos: ReadStorage<'a, Combo>,
65 character_states: ReadStorage<'a, CharacterState>,
66 buffs: ReadStorage<'a, Buffs>,
67 entered_auras: ReadStorage<'a, EnteredAuras>,
68 masses: ReadStorage<'a, Mass>,
69}
70
71#[derive(Default)]
74pub struct Sys;
75impl<'a> System<'a> for Sys {
76 type SystemData = (
77 ReadData<'a>,
78 WriteStorage<'a, Shockwave>,
79 WriteStorage<'a, ShockwaveHitEntities>,
80 Read<'a, EventBus<Outcome>>,
81 );
82
83 const NAME: &'static str = "shockwave";
84 const ORIGIN: Origin = Origin::Common;
85 const PHASE: Phase = Phase::Create;
86
87 fn run(
88 _job: &mut Job<Self>,
89 (read_data, mut shockwaves, mut shockwave_hit_lists, outcomes): Self::SystemData,
90 ) {
91 let mut emitters = read_data.events.get_emitters();
92 let mut outcomes_emitter = outcomes.emitter();
93 let mut rng = rand::rng();
94
95 let time = read_data.time.0;
96 let dt = read_data.dt.0;
97
98 for (entity, pos, ori, shockwave, shockwave_hit_list) in (
100 &read_data.entities,
101 &read_data.positions,
102 &read_data.orientations,
103 &shockwaves,
104 &mut shockwave_hit_lists,
105 )
106 .join()
107 {
108 let creation_time = match shockwave.creation {
109 Some(time) => time,
110 None => continue,
112 };
113
114 let end_time = creation_time + shockwave.duration.as_secs_f64();
115
116 let shockwave_owner = shockwave
117 .owner
118 .and_then(|uid| read_data.id_maps.uid_entity(uid));
119
120 if rng.random_bool(0.05) {
121 emitters.emit(SoundEvent {
122 sound: Sound::new(SoundKind::Shockwave, pos.0, 40.0, time),
123 });
124 }
125
126 if time > end_time {
129 emitters.emit(DeleteEvent(entity));
130 continue;
131 }
132
133 let time_since_creation = (time - creation_time) as f32;
135 let frame_start_dist = (shockwave.speed * (time_since_creation - dt)).max(0.0);
136 let frame_end_dist = (shockwave.speed * time_since_creation).max(frame_start_dist);
137 let pos2 = Vec2::from(pos.0);
138 let look_dir = ori.look_dir();
139
140 let arc_strip = ArcStrip {
143 origin: pos2,
144 dir: look_dir.xy(),
146 angle: shockwave.angle,
147 start: frame_start_dist,
148 end: frame_end_dist,
149 };
150
151 let group = shockwave_owner.and_then(|e| read_data.groups.get(e));
154
155 for (target, uid_b, pos_b, health_b, body_b, physics_state_b) in (
157 &read_data.entities,
158 &read_data.uids,
159 &read_data.positions,
160 &read_data.healths,
161 &read_data.bodies,
162 &read_data.physics_states,
163 )
164 .join()
165 {
166 if shockwave_hit_list.hit_entities.contains(uid_b) {
168 continue;
169 }
170
171 let pos_b2 = pos_b.0.xy();
173
174 let scale_b = read_data.scales.get(target).map_or(1.0, |s| s.0);
176 let rad_b = body_b.max_radius() * scale_b;
178
179 let pos_b_ground = Vec3::new(pos_b.0.x, pos_b.0.y, pos.0.z);
181 let max_angle = shockwave.vertical_angle.to_radians();
182
183 let same_group = group
185 .map(|group_a| Some(group_a) == read_data.groups.get(target))
186 .unwrap_or(Some(*uid_b) == shockwave.owner);
187
188 let target_group = if same_group {
189 GroupTarget::InGroup
190 } else {
191 GroupTarget::OutOfGroup
192 };
193
194 let hit = entity != target
202 && (shockwave_owner != Some(target))
203 && !health_b.is_dead
204 && (pos_b.0 - pos.0).magnitude() < frame_end_dist + rad_b
205 && {
207 arc_strip.collides_with_circle(Disk::new(pos_b2, rad_b))
210 }
211 && (pos_b_ground - pos.0).angle_between(pos_b.0 - pos.0) < max_angle
212 && match shockwave.dodgeable {
213 Dodgeable::Roll | Dodgeable::No => true,
214 Dodgeable::Jump => physics_state_b.on_ground.is_some()
215 };
216
217 if hit {
218 let allow_friendly_fire = shockwave_owner.is_some_and(|entity| {
219 combat::allow_friendly_fire(&read_data.entered_auras, entity, target)
220 });
221 let dir = Dir::from_unnormalized(pos_b.0 - pos.0).unwrap_or(look_dir);
222
223 let attacker_info =
224 shockwave_owner
225 .zip(shockwave.owner)
226 .map(|(entity, uid)| AttackerInfo {
227 entity,
228 uid,
229 group: read_data.groups.get(entity),
230 energy: read_data.energies.get(entity),
231 combo: read_data.combos.get(entity),
232 inventory: read_data.inventories.get(entity),
233 stats: read_data.stats.get(entity),
234 mass: read_data.masses.get(entity),
235 pos: Some(pos.0),
236 });
237
238 let target_info = TargetInfo {
239 entity: target,
240 uid: *uid_b,
241 inventory: read_data.inventories.get(target),
242 stats: read_data.stats.get(target),
243 health: read_data.healths.get(target),
244 pos: pos_b.0,
245 ori: read_data.orientations.get(target),
246 char_state: read_data.character_states.get(target),
247 energy: read_data.energies.get(target),
248 buffs: read_data.buffs.get(target),
249 mass: read_data.masses.get(target),
250 player: read_data.players.get(target),
251 };
252
253 let target_dodging = read_data
254 .character_states
255 .get(target)
256 .and_then(|cs| cs.roll_attack_immunities())
257 .is_some_and(|i| match shockwave.dodgeable {
258 Dodgeable::Roll => i.air_shockwaves,
259 Dodgeable::Jump => i.ground_shockwaves,
260 Dodgeable::No => false,
261 });
262 let permit_pvp = combat::permit_pvp(
264 &read_data.alignments,
265 &read_data.players,
266 &read_data.entered_auras,
267 &read_data.id_maps,
268 shockwave_owner,
269 target,
270 );
271 let precision_mult = None;
273 let attack_options = AttackOptions {
274 target_dodging,
275 permit_pvp,
276 allow_friendly_fire,
277 target_group,
278 precision_mult,
279 };
280
281 shockwave.properties.attack.apply_attack(
282 attacker_info,
283 &target_info,
284 dir,
285 attack_options,
286 1.0,
287 shockwave.dodgeable.shockwave_attack_source(),
288 *read_data.time,
289 &mut emitters,
290 |o| outcomes_emitter.emit(o),
291 &mut rng,
292 0,
293 );
294
295 shockwave_hit_list.hit_entities.push(*uid_b);
296 }
297 }
298 }
299
300 shockwaves.set_event_emission(false);
303 (&mut shockwaves).lend_join().for_each(|mut shockwave| {
304 if shockwave.creation.is_none() {
305 shockwave.creation = Some(time);
306 }
307 });
308 shockwaves.set_event_emission(true);
309 }
310}
311
312#[derive(Clone, Copy)]
313struct ArcStrip {
314 origin: Vec2<f32>,
315 dir: Vec2<f32>,
317 angle: f32,
319 start: f32,
321 end: f32,
323}
324
325impl ArcStrip {
326 fn collides_with_circle(self, d: Disk<f32, f32>) -> bool {
327 if (self.origin.x - d.center.x).abs() > self.end + d.radius
329 || (self.origin.y - d.center.y).abs() > self.end + d.radius
330 {
331 return false;
332 }
333
334 let dist = self.origin.distance(d.center);
335 let half_angle = self.angle.to_radians() / 2.0;
336
337 if dist > self.end + d.radius || dist + d.radius < self.start {
338 return false;
340 }
341
342 let inside_edge = Disk::new(self.origin, self.start);
343 let outside_edge = Disk::new(self.origin, self.end);
344 let inner_corner_in_circle = || {
345 let midpoint = self.dir.normalized() * self.start;
346 d.contains_point(midpoint.rotated_z(half_angle) + self.origin)
347 || d.contains_point(midpoint.rotated_z(-half_angle) + self.origin)
348 };
349 let arc_segment_in_circle = || {
350 let midpoint = self.dir.normalized();
351 let segment_in_circle = |angle| {
352 let dir = midpoint.rotated_z(angle);
353 let side = LineSegment2 {
354 start: dir * self.start + self.origin,
355 end: dir * self.end + self.origin,
356 };
357 d.contains_point(side.projected_point(d.center))
358 };
359 segment_in_circle(half_angle) || segment_in_circle(-half_angle)
360 };
361
362 if dist > self.end {
363 arc_segment_in_circle() || {
366 let (p1, p2) = intersection_points(outside_edge, d, dist);
368 self.dir.angle_between(p1 - self.origin) < half_angle
369 || self.dir.angle_between(p2 - self.origin) < half_angle
370 }
371 } else if dist < self.start {
372 inner_corner_in_circle()
376 || (
377 inside_edge != d && {
379 let (p1, p2) = intersection_points(inside_edge, d, dist);
380 self.dir.angle_between(p1 - self.origin) < half_angle
381 || self.dir.angle_between(p2 - self.origin) < half_angle
382 }
383 )
384 } else if d.radius > dist {
385 inner_corner_in_circle()
388 } else {
389 let extra_angle = (d.radius / dist).asin();
392 self.dir.angle_between(d.center - self.origin) < half_angle + extra_angle
393 }
394 }
395}
396
397fn intersection_points(
401 disk1: Disk<f32, f32>,
402 disk2: Disk<f32, f32>,
403 dist: f32,
404) -> (Vec2<f32>, Vec2<f32>) {
405 let e = (disk2.center - disk1.center) / dist;
406
407 let x = (disk1.radius.powi(2) - disk2.radius.powi(2) + dist.powi(2)) / (2.0 * dist);
408 let y = (disk1.radius.powi(2) - x.powi(2)).sqrt();
409
410 let pxe = disk1.center + x * e;
411 let eyx = e.yx();
412
413 let p1 = pxe + Vec2::new(-y, y) * eyx;
414 let p2 = pxe + Vec2::new(y, -y) * eyx;
415
416 (p1, p2)
417}