1use common::{
2 GroupTarget,
3 combat::{self, AttackOptions, AttackSource, AttackerInfo, TargetInfo},
4 comp::{
5 Alignment, Body, Buffs, CharacterState, Combo, Energy, Group, Health, Inventory, Mass, Ori,
6 PhysicsState, Player, Pos, Projectile, Stats, Vel,
7 agent::{Sound, SoundKind},
8 aura::EnteredAuras,
9 projectile,
10 },
11 event::{
12 BonkEvent, BuffEvent, ComboChangeEvent, DeleteEvent, EmitExt, Emitter, EnergyChangeEvent,
13 EntityAttackedHookEvent, EventBus, ExplosionEvent, HealthChangeEvent, KnockbackEvent,
14 ParryHookEvent, PoiseChangeEvent, PossessEvent, SoundEvent,
15 },
16 event_emitters,
17 outcome::Outcome,
18 resources::{DeltaTime, Time},
19 uid::{IdMaps, Uid},
20 util::Dir,
21};
22
23use common::vol::ReadVol;
24use common_ecs::{Job, Origin, Phase, System};
25use rand::Rng;
26use specs::{
27 Entities, Entity as EcsEntity, Join, Read, ReadExpect, ReadStorage, SystemData, WriteStorage,
28 shred,
29};
30use std::time::Duration;
31use vek::*;
32
33use common::terrain::TerrainGrid;
34
35event_emitters! {
36 struct Events[Emitters] {
37 sound: SoundEvent,
38 delete: DeleteEvent,
39 explosion: ExplosionEvent,
40 health_change: HealthChangeEvent,
41 energy_change: EnergyChangeEvent,
42 poise_change: PoiseChangeEvent,
43 parry_hook: ParryHookEvent,
44 knockback: KnockbackEvent,
45 entity_attack_hoow: EntityAttackedHookEvent,
46 combo_change: ComboChangeEvent,
47 buff: BuffEvent,
48 bonk: BonkEvent,
49 possess: PossessEvent,
50 }
51}
52
53#[derive(SystemData)]
54pub struct ReadData<'a> {
55 time: Read<'a, Time>,
56 entities: Entities<'a>,
57 players: ReadStorage<'a, Player>,
58 dt: Read<'a, DeltaTime>,
59 id_maps: Read<'a, IdMaps>,
60 events: Events<'a>,
61 uids: ReadStorage<'a, Uid>,
62 positions: ReadStorage<'a, Pos>,
63 alignments: ReadStorage<'a, Alignment>,
64 physics_states: ReadStorage<'a, PhysicsState>,
65 velocities: ReadStorage<'a, Vel>,
66 inventories: ReadStorage<'a, Inventory>,
67 groups: ReadStorage<'a, Group>,
68 energies: ReadStorage<'a, Energy>,
69 stats: ReadStorage<'a, Stats>,
70 combos: ReadStorage<'a, Combo>,
71 healths: ReadStorage<'a, Health>,
72 bodies: ReadStorage<'a, Body>,
73 character_states: ReadStorage<'a, CharacterState>,
74 terrain: ReadExpect<'a, TerrainGrid>,
75 buffs: ReadStorage<'a, Buffs>,
76 entered_auras: ReadStorage<'a, EnteredAuras>,
77 masses: ReadStorage<'a, Mass>,
78}
79
80#[derive(Default)]
82pub struct Sys;
83impl<'a> System<'a> for Sys {
84 type SystemData = (
85 ReadData<'a>,
86 WriteStorage<'a, Ori>,
87 WriteStorage<'a, Projectile>,
88 Read<'a, EventBus<Outcome>>,
89 );
90
91 const NAME: &'static str = "projectile";
92 const ORIGIN: Origin = Origin::Common;
93 const PHASE: Phase = Phase::Create;
94
95 fn run(
96 _job: &mut Job<Self>,
97 (read_data, mut orientations, mut projectiles, outcomes): Self::SystemData,
98 ) {
99 let mut emitters = read_data.events.get_emitters();
100 let mut outcomes_emitter = outcomes.emitter();
101 let mut rng = rand::thread_rng();
102
103 'projectile_loop: for (entity, pos, physics, vel, projectile) in (
105 &read_data.entities,
106 &read_data.positions,
107 &read_data.physics_states,
108 &read_data.velocities,
109 &mut projectiles,
110 )
111 .join()
112 {
113 let projectile_owner = projectile
114 .owner
115 .and_then(|uid| read_data.id_maps.uid_entity(uid));
116
117 if physics.on_surface().is_none() && rng.gen_bool(0.05) {
118 emitters.emit(SoundEvent {
119 sound: Sound::new(SoundKind::Projectile, pos.0, 4.0, read_data.time.0),
120 });
121 }
122
123 let mut projectile_vanished: bool = false;
124
125 for (&other, &pos_hit_other) in physics.touch_entities.iter() {
127 let same_group = projectile_owner
128 .and_then(|e| read_data.groups.get(e)).is_some_and(|owner_group|
132 Some(owner_group) == read_data.id_maps
133 .uid_entity(other)
134 .and_then(|e| read_data.groups.get(e)));
135
136 let target_group = if same_group {
138 GroupTarget::InGroup
139 } else {
140 GroupTarget::OutOfGroup
141 };
142
143 if projectile.ignore_group
144 && same_group
145 && projectile
146 .owner
147 .and_then(|owner| {
148 read_data
149 .id_maps
150 .uid_entity(owner)
151 .zip(read_data.id_maps.uid_entity(other))
152 })
153 .is_none_or(|(owner, other)| {
154 !combat::allow_friendly_fire(&read_data.entered_auras, owner, other)
155 })
156 {
157 continue;
158 }
159
160 if projectile.owner == Some(other) {
161 continue;
162 }
163
164 let projectile = &mut *projectile;
165
166 let entity_of = |uid: Uid| read_data.id_maps.uid_entity(uid);
167
168 if physics.on_surface().is_some() {
172 let projectile_direction = orientations
173 .get(entity)
174 .map_or_else(Vec3::zero, |ori| ori.look_vec());
175 let pos_wall = pos.0 - 0.2 * projectile_direction;
176 if !matches!(
177 read_data
178 .terrain
179 .ray(pos_wall, pos_hit_other)
180 .until(|b| b.is_filled())
181 .cast()
182 .1,
183 Ok(None)
184 ) {
185 continue;
186 }
187 }
188
189 for effect in projectile.hit_entity.drain(..) {
190 let owner = projectile.owner.and_then(entity_of);
191 let projectile_info = ProjectileInfo {
192 entity,
193 effect,
194 owner_uid: projectile.owner,
195 owner,
196 ori: orientations.get(entity),
197 pos,
198 vel,
199 };
200
201 let target = entity_of(other);
202 let projectile_target_info = ProjectileTargetInfo {
203 uid: other,
204 entity: target,
205 target_group,
206 ori: target.and_then(|target| orientations.get(target)),
207 };
208
209 dispatch_hit(
210 projectile_info,
211 projectile_target_info,
212 &read_data,
213 &mut projectile_vanished,
214 &mut outcomes_emitter,
215 &mut emitters,
216 &mut rng,
217 );
218 }
219
220 if projectile_vanished {
221 continue 'projectile_loop;
222 }
223 }
224
225 if physics.on_surface().is_some() {
226 let projectile = &mut *projectile;
227 for effect in projectile.hit_solid.drain(..) {
228 match effect {
229 projectile::Effect::Explode(e) => {
230 let projectile_direction = orientations
238 .get(entity)
239 .map_or_else(Vec3::zero, |ori| ori.look_vec());
240 let offset = -0.2 * projectile_direction;
241 emitters.emit(ExplosionEvent {
242 pos: pos.0 + offset,
243 explosion: e,
244 owner: projectile.owner,
245 });
246 },
247 projectile::Effect::Vanish => {
248 emitters.emit(DeleteEvent(entity));
249 projectile_vanished = true;
250 },
251 projectile::Effect::Bonk => {
252 emitters.emit(BonkEvent {
253 pos: pos.0,
254 owner: projectile.owner,
255 target: None,
256 });
257 },
258 _ => {},
259 }
260 }
261
262 if projectile_vanished {
263 continue 'projectile_loop;
264 }
265 } else if let Some(ori) = orientations.get_mut(entity) {
266 if let Some(dir) = Dir::from_unnormalized(vel.0) {
267 *ori = dir.into();
268 }
269 }
270
271 if projectile.time_left == Duration::default() {
272 emitters.emit(DeleteEvent(entity));
273 }
274 projectile.time_left = projectile
275 .time_left
276 .checked_sub(Duration::from_secs_f32(read_data.dt.0))
277 .unwrap_or_default();
278 }
279 }
280}
281
282struct ProjectileInfo<'a> {
283 entity: EcsEntity,
284 effect: projectile::Effect,
285 owner_uid: Option<Uid>,
286 owner: Option<EcsEntity>,
287 ori: Option<&'a Ori>,
288 pos: &'a Pos,
289 vel: &'a Vel,
290}
291
292struct ProjectileTargetInfo<'a> {
293 uid: Uid,
294 entity: Option<EcsEntity>,
295 target_group: GroupTarget,
296 ori: Option<&'a Ori>,
297}
298
299fn dispatch_hit(
300 projectile_info: ProjectileInfo,
301 projectile_target_info: ProjectileTargetInfo,
302 read_data: &ReadData,
303 projectile_vanished: &mut bool,
304 outcomes_emitter: &mut Emitter<Outcome>,
305 emitters: &mut Emitters,
306 rng: &mut rand::rngs::ThreadRng,
307) {
308 match projectile_info.effect {
309 projectile::Effect::Attack(attack) => {
310 let target_uid = projectile_target_info.uid;
311 let target = if let Some(entity) = projectile_target_info.entity {
312 entity
313 } else {
314 return;
315 };
316
317 let (target_pos, projectile_dir) = {
318 let target_pos = read_data.positions.get(target);
319 let projectile_ori = projectile_info.ori;
320 match target_pos.zip(projectile_ori) {
321 Some((tgt_pos, proj_ori)) => {
322 let Pos(tgt_pos) = tgt_pos;
323 (*tgt_pos, proj_ori.look_dir())
324 },
325 None => return,
326 }
327 };
328
329 let owner = projectile_info.owner;
330 let projectile_entity = projectile_info.entity;
331
332 let attacker_info =
333 owner
334 .zip(projectile_info.owner_uid)
335 .map(|(entity, uid)| AttackerInfo {
336 entity,
337 uid,
338 group: read_data.groups.get(entity),
339 energy: read_data.energies.get(entity),
340 combo: read_data.combos.get(entity),
341 inventory: read_data.inventories.get(entity),
342 stats: read_data.stats.get(entity),
343 mass: read_data.masses.get(entity),
344 });
345
346 let target_info = TargetInfo {
347 entity: target,
348 uid: target_uid,
349 inventory: read_data.inventories.get(target),
350 stats: read_data.stats.get(target),
351 health: read_data.healths.get(target),
352 pos: target_pos,
353 ori: projectile_target_info.ori,
354 char_state: read_data.character_states.get(target),
355 energy: read_data.energies.get(target),
356 buffs: read_data.buffs.get(target),
357 mass: read_data.masses.get(target),
358 };
359
360 if let Some(&body) = read_data.bodies.get(projectile_entity) {
362 outcomes_emitter.emit(Outcome::ProjectileHit {
363 pos: target_pos,
364 body,
365 vel: read_data
366 .velocities
367 .get(projectile_entity)
368 .map_or(Vec3::zero(), |v| v.0),
369 source: projectile_info.owner_uid,
370 target: read_data.uids.get(target).copied(),
371 });
372 }
373
374 let allow_friendly_fire = owner.is_some_and(|owner| {
375 combat::allow_friendly_fire(&read_data.entered_auras, owner, target)
376 });
377
378 let permit_pvp = combat::permit_pvp(
380 &read_data.alignments,
381 &read_data.players,
382 &read_data.entered_auras,
383 &read_data.id_maps,
384 owner,
385 target,
386 );
387
388 let target_dodging = read_data
389 .character_states
390 .get(target)
391 .and_then(|cs| cs.roll_attack_immunities())
392 .is_some_and(|i| i.projectiles);
393
394 let precision_from_flank = combat::precision_mult_from_flank(
395 *projectile_dir,
396 target_info.ori,
397 Default::default(),
398 false,
399 );
400
401 let precision_from_head = {
402 let curr_pos = projectile_info.pos.0;
406 let last_pos = projectile_info.pos.0 - projectile_info.vel.0 * read_data.dt.0;
407 let vel = projectile_info.vel.0;
408 let (target_height, target_radius) = read_data
409 .bodies
410 .get(target)
411 .map_or((0.0, 0.0), |b| (b.height(), b.max_radius()));
412 let head_top_pos = target_pos.with_z(target_pos.z + target_height);
413 let head_bottom_pos = head_top_pos.with_z(
414 head_top_pos.z - target_height * combat::PROJECTILE_HEADSHOT_PROPORTION,
415 );
416 if (curr_pos.z < head_bottom_pos.z && last_pos.z < head_bottom_pos.z)
417 || (curr_pos.z > head_top_pos.z && last_pos.z > head_top_pos.z)
418 {
419 None
420 } else if curr_pos.z > head_top_pos.z
421 || curr_pos.z < head_bottom_pos.z
422 || last_pos.z > head_top_pos.z
423 || last_pos.z < head_bottom_pos.z
424 {
425 let proj_top_intersection = {
426 let t = (head_top_pos.z - last_pos.z) / vel.z;
427 last_pos + vel * t
428 };
429 let proj_bottom_intersection = {
430 let t = (head_bottom_pos.z - last_pos.z) / vel.z;
431 last_pos + vel * t
432 };
433 let intersected_bottom = head_bottom_pos
434 .distance_squared(proj_bottom_intersection)
435 < target_radius.powi(2);
436 let intersected_top = head_top_pos.distance_squared(proj_top_intersection)
437 < target_radius.powi(2);
438 let hit_head = intersected_bottom || intersected_top;
439 let hit_from_bottom = last_pos.z < head_bottom_pos.z && intersected_bottom;
440 let hit_from_top = last_pos.z > head_top_pos.z && intersected_top;
441 if !hit_head || hit_from_bottom {
445 None
446 } else if hit_from_top {
447 Some(combat::MAX_TOP_HEADSHOT_PRECISION)
448 } else {
449 Some(combat::MAX_HEADSHOT_PRECISION)
450 }
451 } else {
452 let trajectory = LineSegment3 {
453 start: last_pos,
454 end: curr_pos,
455 };
456 let head_middle_pos = head_bottom_pos.with_z(
457 head_bottom_pos.z
458 + target_height * combat::PROJECTILE_HEADSHOT_PROPORTION * 0.5,
459 );
460 if trajectory.distance_to_point(head_middle_pos) < target_radius {
461 Some(combat::MAX_HEADSHOT_PRECISION)
462 } else {
463 None
464 }
465 }
466 };
467
468 let precision_mult = match (precision_from_flank, precision_from_head) {
469 (Some(a), Some(b)) => Some(a.max(b)),
470 (Some(a), None) | (None, Some(a)) => Some(a),
471 (None, None) => None,
472 };
473
474 let attack_options = AttackOptions {
475 target_dodging,
476 permit_pvp,
477 allow_friendly_fire,
478 target_group: projectile_target_info.target_group,
479 precision_mult,
480 };
481
482 attack.apply_attack(
483 attacker_info,
484 &target_info,
485 projectile_dir,
486 attack_options,
487 1.0,
488 AttackSource::Projectile,
489 *read_data.time,
490 emitters,
491 |o| outcomes_emitter.emit(o),
492 rng,
493 0,
494 );
495 },
496 projectile::Effect::Explode(e) => {
497 let Pos(pos) = *projectile_info.pos;
498 let owner_uid = projectile_info.owner_uid;
499 emitters.emit(ExplosionEvent {
500 pos,
501 explosion: e,
502 owner: owner_uid,
503 });
504 },
505 projectile::Effect::Bonk => {
506 let Pos(pos) = *projectile_info.pos;
507 let owner_uid = projectile_info.owner_uid;
508 emitters.emit(BonkEvent {
509 pos,
510 owner: owner_uid,
511 target: Some(projectile_target_info.uid),
512 });
513 },
514 projectile::Effect::Vanish => {
515 let entity = projectile_info.entity;
516 emitters.emit(DeleteEvent(entity));
517 *projectile_vanished = true;
518 },
519 projectile::Effect::Possess => {
520 let target_uid = projectile_target_info.uid;
521 let owner_uid = projectile_info.owner_uid;
522 if let Some(owner_uid) = owner_uid {
523 if target_uid != owner_uid {
524 emitters.emit(PossessEvent(owner_uid, target_uid));
525 }
526 }
527 },
528 projectile::Effect::Stick => {},
529 }
530}