1use common::{
2 Damage, DamageKind, DamageSource, Explosion, GroupTarget, RadiusEffect,
3 combat::{self, AttackOptions, AttackSource, AttackerInfo, TargetInfo},
4 comp::{
5 Alignment, Body, Buffs, CharacterState, Combo, Content, Energy, Group, Health, Inventory,
6 Mass, Ori, PhysicsState, Player, Poise, Pos, Projectile, Stats, Vel,
7 agent::{Sound, SoundKind},
8 aura::EnteredAuras,
9 object, projectile,
10 },
11 effect,
12 event::{
13 BonkEvent, BuffEvent, ComboChangeEvent, CreateNpcEvent, DeleteEvent, EmitExt, Emitter,
14 EnergyChangeEvent, EntityAttackedHookEvent, EventBus, ExplosionEvent, HealthChangeEvent,
15 KnockbackEvent, NpcBuilder, ParryHookEvent, PoiseChangeEvent, PossessEvent, ShootEvent,
16 SoundEvent,
17 },
18 event_emitters,
19 outcome::Outcome,
20 resources::{DeltaTime, Time},
21 uid::{IdMaps, Uid},
22 util::Dir,
23};
24
25use common::vol::ReadVol;
26use common_ecs::{Job, Origin, Phase, System};
27use rand::Rng;
28use specs::{
29 Entities, Entity as EcsEntity, Join, Read, ReadExpect, ReadStorage, SystemData, WriteStorage,
30 shred,
31};
32use std::time::Duration;
33use vek::*;
34
35use common::terrain::TerrainGrid;
36
37event_emitters! {
38 struct Events[Emitters] {
39 sound: SoundEvent,
40 delete: DeleteEvent,
41 explosion: ExplosionEvent,
42 health_change: HealthChangeEvent,
43 energy_change: EnergyChangeEvent,
44 poise_change: PoiseChangeEvent,
45 parry_hook: ParryHookEvent,
46 knockback: KnockbackEvent,
47 entity_attack_hook: EntityAttackedHookEvent,
48 shoot: ShootEvent,
49 create_npc: CreateNpcEvent,
50 combo_change: ComboChangeEvent,
51 buff: BuffEvent,
52 bonk: BonkEvent,
53 possess: PossessEvent,
54 }
55}
56
57#[derive(SystemData)]
58pub struct ReadData<'a> {
59 time: Read<'a, Time>,
60 entities: Entities<'a>,
61 players: ReadStorage<'a, Player>,
62 dt: Read<'a, DeltaTime>,
63 id_maps: Read<'a, IdMaps>,
64 events: Events<'a>,
65 uids: ReadStorage<'a, Uid>,
66 positions: ReadStorage<'a, Pos>,
67 alignments: ReadStorage<'a, Alignment>,
68 physics_states: ReadStorage<'a, PhysicsState>,
69 velocities: ReadStorage<'a, Vel>,
70 inventories: ReadStorage<'a, Inventory>,
71 groups: ReadStorage<'a, Group>,
72 energies: ReadStorage<'a, Energy>,
73 stats: ReadStorage<'a, Stats>,
74 combos: ReadStorage<'a, Combo>,
75 healths: ReadStorage<'a, Health>,
76 bodies: ReadStorage<'a, Body>,
77 character_states: ReadStorage<'a, CharacterState>,
78 terrain: ReadExpect<'a, TerrainGrid>,
79 buffs: ReadStorage<'a, Buffs>,
80 entered_auras: ReadStorage<'a, EnteredAuras>,
81 masses: ReadStorage<'a, Mass>,
82}
83
84#[derive(Default)]
86pub struct Sys;
87impl<'a> System<'a> for Sys {
88 type SystemData = (
89 ReadData<'a>,
90 WriteStorage<'a, Ori>,
91 WriteStorage<'a, Projectile>,
92 Read<'a, EventBus<Outcome>>,
93 );
94
95 const NAME: &'static str = "projectile";
96 const ORIGIN: Origin = Origin::Common;
97 const PHASE: Phase = Phase::Create;
98
99 fn run(
100 _job: &mut Job<Self>,
101 (read_data, mut orientations, mut projectiles, outcomes): Self::SystemData,
102 ) {
103 let mut emitters = read_data.events.get_emitters();
104 let mut outcomes_emitter = outcomes.emitter();
105 let mut rng = rand::thread_rng();
106
107 'projectile_loop: for (entity, pos, physics, vel, body, projectile) in (
109 &read_data.entities,
110 &read_data.positions,
111 &read_data.physics_states,
112 &read_data.velocities,
113 &read_data.bodies,
114 &mut projectiles,
115 )
116 .join()
117 {
118 let projectile_owner = projectile
119 .owner
120 .and_then(|uid| read_data.id_maps.uid_entity(uid));
121
122 if physics.on_surface().is_none() && rng.gen_bool(0.05) {
123 emitters.emit(SoundEvent {
124 sound: Sound::new(SoundKind::Projectile, pos.0, 4.0, read_data.time.0),
125 });
126 }
127
128 let mut projectile_vanished: bool = false;
129
130 for (&other, &pos_hit_other) in physics.touch_entities.iter() {
132 let same_group = projectile_owner
133 .and_then(|e| read_data.groups.get(e)).is_some_and(|owner_group|
137 Some(owner_group) == read_data.id_maps
138 .uid_entity(other)
139 .and_then(|e| read_data.groups.get(e)));
140
141 let target_group = if same_group {
143 GroupTarget::InGroup
144 } else {
145 GroupTarget::OutOfGroup
146 };
147
148 if projectile.ignore_group
149 && same_group
150 && projectile
151 .owner
152 .and_then(|owner| {
153 read_data
154 .id_maps
155 .uid_entity(owner)
156 .zip(read_data.id_maps.uid_entity(other))
157 })
158 .is_none_or(|(owner, other)| {
159 !combat::allow_friendly_fire(&read_data.entered_auras, owner, other)
160 })
161 {
162 continue;
163 }
164
165 if projectile.owner == Some(other) {
166 continue;
167 }
168
169 let projectile = &mut *projectile;
170
171 let entity_of = |uid: Uid| read_data.id_maps.uid_entity(uid);
172
173 if physics.on_surface().is_some() {
177 let projectile_direction = orientations
178 .get(entity)
179 .map_or_else(Vec3::zero, |ori| ori.look_vec());
180 let pos_wall = pos.0 - 0.2 * projectile_direction;
181 if !matches!(
182 read_data
183 .terrain
184 .ray(pos_wall, pos_hit_other)
185 .until(|b| b.is_filled())
186 .cast()
187 .1,
188 Ok(None)
189 ) {
190 continue;
191 }
192 }
193
194 for effect in projectile.hit_entity.drain(..) {
195 let owner = projectile.owner.and_then(entity_of);
196 let projectile_info = ProjectileInfo {
197 entity,
198 effect,
199 owner_uid: projectile.owner,
200 owner,
201 ori: orientations.get(entity),
202 pos,
203 vel,
204 };
205
206 let target = entity_of(other);
207 let projectile_target_info = ProjectileTargetInfo {
208 uid: other,
209 entity: target,
210 target_group,
211 ori: target.and_then(|target| orientations.get(target)),
212 };
213
214 dispatch_hit(
215 projectile_info,
216 projectile_target_info,
217 &read_data,
218 &mut projectile_vanished,
219 &mut outcomes_emitter,
220 &mut emitters,
221 &mut rng,
222 );
223 }
224
225 if projectile_vanished {
226 continue 'projectile_loop;
227 }
228 }
229
230 if physics.on_surface().is_some() {
231 let projectile = &mut *projectile;
232 for effect in projectile.hit_solid.drain(..) {
233 match effect {
234 projectile::Effect::Explode(e) => {
235 let projectile_direction = orientations
243 .get(entity)
244 .map_or_else(Vec3::zero, |ori| ori.look_vec());
245 let offset = -0.2 * projectile_direction;
246 emitters.emit(ExplosionEvent {
247 pos: pos.0 + offset,
248 explosion: e,
249 owner: projectile.owner,
250 });
251 },
252 projectile::Effect::Vanish => {
253 emitters.emit(DeleteEvent(entity));
254 projectile_vanished = true;
255 },
256 projectile::Effect::Bonk => {
257 emitters.emit(BonkEvent {
258 pos: pos.0,
259 owner: projectile.owner,
260 target: None,
261 });
262 },
263 projectile::Effect::SurpriseEgg => {
264 outcomes_emitter.emit(Outcome::SurpriseEgg { pos: pos.0 });
265 },
266 projectile::Effect::TrainingDummy => {
267 let body = Body::Object(object::Body::TrainingDummy);
268 emitters.emit(CreateNpcEvent {
269 pos: *pos,
270 ori: Ori::default(),
271 npc: NpcBuilder::new(
272 Stats::new(
273 Content::with_attr("name-custom-village-dummy", "neut"),
274 body,
275 ),
276 body,
277 Alignment::Npc,
278 )
279 .with_health(Health::new(body))
280 .with_poise(Poise::new(body)),
281 });
282 },
283 _ => {},
284 }
285 }
286
287 if projectile_vanished {
288 continue 'projectile_loop;
289 }
290 } else if let Some(ori) = orientations.get_mut(entity) {
291 if let Some(dir) = Dir::from_unnormalized(vel.0) {
292 *ori = dir.into();
293 }
294 }
295
296 if projectile.time_left == Duration::ZERO {
297 emitters.emit(DeleteEvent(entity));
298
299 for effect in projectile.timeout.drain(..) {
300 if let projectile::Effect::Firework(reagent) = effect {
301 const ENABLE_RECURSIVE_FIREWORKS: bool = true;
302 if ENABLE_RECURSIVE_FIREWORKS {
303 use common::{comp::LightEmitter, event::ShootEvent};
304 use std::f32::consts::PI;
305 let thresholds: &[(f32, usize)] = &[(0.25, 2), (0.7, 1)];
309 let expected = {
310 let mut total = 0.0;
311 let mut cumulative_probability = 0.0;
312 for (p, n) in thresholds {
313 total += (p - cumulative_probability) * *n as f32;
314 cumulative_probability += p;
315 }
316 total
317 };
318 assert!(expected < 1.0);
319 let num_fireworks = (|| {
320 let x = rng.gen_range(0.0..1.0);
321 for (p, n) in thresholds {
322 if x < *p {
323 return *n;
324 }
325 }
326 0
327 })();
328 for _ in 0..num_fireworks {
329 let speed: f32 = rng.gen_range(40.0..80.0);
330 let theta: f32 = rng.gen_range(0.0..2.0 * PI);
331 let phi: f32 = rng.gen_range(0.25 * PI..0.5 * PI);
332 let dir = Dir::from_unnormalized(Vec3::new(
333 theta.cos(),
334 theta.sin(),
335 phi.sin(),
336 ))
337 .expect("nonzero vector should normalize");
338 emitters.emit(ShootEvent {
339 entity: Some(entity),
340 pos: *pos,
341 dir,
342 body: *body,
343 light: Some(LightEmitter {
344 animated: true,
345 flicker: 2.0,
346 strength: 2.0,
347 col: Rgb::new(1.0, 1.0, 0.0),
348 }),
349 projectile: Projectile {
350 hit_solid: Vec::new(),
351 hit_entity: Vec::new(),
352 timeout: vec![projectile::Effect::Firework(reagent)],
353 time_left: Duration::from_secs(1),
354 ignore_group: true,
355 is_sticky: true,
356 is_point: true,
357 owner: projectile.owner,
358 },
359 speed,
360 object: None,
361 });
362 }
363 }
364 emitters.emit(DeleteEvent(entity));
365 emitters.emit(ExplosionEvent {
366 pos: pos.0,
367 explosion: Explosion {
368 effects: vec![
369 RadiusEffect::Entity(effect::Effect::Damage(Damage {
370 source: DamageSource::Explosion,
371 kind: DamageKind::Energy,
372 value: 5.0,
373 })),
374 RadiusEffect::Entity(effect::Effect::Poise(-40.0)),
375 RadiusEffect::TerrainDestruction(4.0, Rgb::black()),
376 ],
377 radius: 12.0,
378 reagent: Some(reagent),
379 min_falloff: 0.0,
380 },
381 owner: projectile.owner,
382 });
383 }
384 }
385 }
386 projectile.time_left = projectile
387 .time_left
388 .checked_sub(Duration::from_secs_f32(read_data.dt.0))
389 .unwrap_or_default();
390 }
391 }
392}
393
394struct ProjectileInfo<'a> {
395 entity: EcsEntity,
396 effect: projectile::Effect,
397 owner_uid: Option<Uid>,
398 owner: Option<EcsEntity>,
399 ori: Option<&'a Ori>,
400 pos: &'a Pos,
401 vel: &'a Vel,
402}
403
404struct ProjectileTargetInfo<'a> {
405 uid: Uid,
406 entity: Option<EcsEntity>,
407 target_group: GroupTarget,
408 ori: Option<&'a Ori>,
409}
410
411fn dispatch_hit(
412 projectile_info: ProjectileInfo,
413 projectile_target_info: ProjectileTargetInfo,
414 read_data: &ReadData,
415 projectile_vanished: &mut bool,
416 outcomes_emitter: &mut Emitter<Outcome>,
417 emitters: &mut Emitters,
418 rng: &mut rand::rngs::ThreadRng,
419) {
420 match projectile_info.effect {
421 projectile::Effect::Attack(attack) => {
422 let target_uid = projectile_target_info.uid;
423 let target = if let Some(entity) = projectile_target_info.entity {
424 entity
425 } else {
426 return;
427 };
428
429 let (target_pos, projectile_dir) = {
430 let target_pos = read_data.positions.get(target);
431 let projectile_ori = projectile_info.ori;
432 match target_pos.zip(projectile_ori) {
433 Some((tgt_pos, proj_ori)) => {
434 let Pos(tgt_pos) = tgt_pos;
435 (*tgt_pos, proj_ori.look_dir())
436 },
437 None => return,
438 }
439 };
440
441 let owner = projectile_info.owner;
442 let projectile_entity = projectile_info.entity;
443
444 let attacker_info =
445 owner
446 .zip(projectile_info.owner_uid)
447 .map(|(entity, uid)| AttackerInfo {
448 entity,
449 uid,
450 group: read_data.groups.get(entity),
451 energy: read_data.energies.get(entity),
452 combo: read_data.combos.get(entity),
453 inventory: read_data.inventories.get(entity),
454 stats: read_data.stats.get(entity),
455 mass: read_data.masses.get(entity),
456 });
457
458 let target_info = TargetInfo {
459 entity: target,
460 uid: target_uid,
461 inventory: read_data.inventories.get(target),
462 stats: read_data.stats.get(target),
463 health: read_data.healths.get(target),
464 pos: target_pos,
465 ori: projectile_target_info.ori,
466 char_state: read_data.character_states.get(target),
467 energy: read_data.energies.get(target),
468 buffs: read_data.buffs.get(target),
469 mass: read_data.masses.get(target),
470 };
471
472 if let Some(&body) = read_data.bodies.get(projectile_entity) {
474 outcomes_emitter.emit(Outcome::ProjectileHit {
475 pos: target_pos,
476 body,
477 vel: read_data
478 .velocities
479 .get(projectile_entity)
480 .map_or(Vec3::zero(), |v| v.0),
481 source: projectile_info.owner_uid,
482 target: read_data.uids.get(target).copied(),
483 });
484 }
485
486 let allow_friendly_fire = owner.is_some_and(|owner| {
487 combat::allow_friendly_fire(&read_data.entered_auras, owner, target)
488 });
489
490 let permit_pvp = combat::permit_pvp(
492 &read_data.alignments,
493 &read_data.players,
494 &read_data.entered_auras,
495 &read_data.id_maps,
496 owner,
497 target,
498 );
499
500 let target_dodging = read_data
501 .character_states
502 .get(target)
503 .and_then(|cs| cs.roll_attack_immunities())
504 .is_some_and(|i| i.projectiles);
505
506 let precision_from_flank = combat::precision_mult_from_flank(
507 *projectile_dir,
508 target_info.ori,
509 Default::default(),
510 false,
511 );
512
513 let precision_from_head = {
514 let curr_pos = projectile_info.pos.0;
518 let last_pos = projectile_info.pos.0 - projectile_info.vel.0 * read_data.dt.0;
519 let vel = projectile_info.vel.0;
520 let (target_height, target_radius) = read_data
521 .bodies
522 .get(target)
523 .map_or((0.0, 0.0), |b| (b.height(), b.max_radius()));
524 let head_top_pos = target_pos.with_z(target_pos.z + target_height);
525 let head_bottom_pos = head_top_pos.with_z(
526 head_top_pos.z - target_height * combat::PROJECTILE_HEADSHOT_PROPORTION,
527 );
528 if (curr_pos.z < head_bottom_pos.z && last_pos.z < head_bottom_pos.z)
529 || (curr_pos.z > head_top_pos.z && last_pos.z > head_top_pos.z)
530 {
531 None
532 } else if curr_pos.z > head_top_pos.z
533 || curr_pos.z < head_bottom_pos.z
534 || last_pos.z > head_top_pos.z
535 || last_pos.z < head_bottom_pos.z
536 {
537 let proj_top_intersection = {
538 let t = (head_top_pos.z - last_pos.z) / vel.z;
539 last_pos + vel * t
540 };
541 let proj_bottom_intersection = {
542 let t = (head_bottom_pos.z - last_pos.z) / vel.z;
543 last_pos + vel * t
544 };
545 let intersected_bottom = head_bottom_pos
546 .distance_squared(proj_bottom_intersection)
547 < target_radius.powi(2);
548 let intersected_top = head_top_pos.distance_squared(proj_top_intersection)
549 < target_radius.powi(2);
550 let hit_head = intersected_bottom || intersected_top;
551 let hit_from_bottom = last_pos.z < head_bottom_pos.z && intersected_bottom;
552 let hit_from_top = last_pos.z > head_top_pos.z && intersected_top;
553 if !hit_head || hit_from_bottom {
557 None
558 } else if hit_from_top {
559 Some(combat::MAX_TOP_HEADSHOT_PRECISION)
560 } else {
561 Some(combat::MAX_HEADSHOT_PRECISION)
562 }
563 } else {
564 let trajectory = LineSegment3 {
565 start: last_pos,
566 end: curr_pos,
567 };
568 let head_middle_pos = head_bottom_pos.with_z(
569 head_bottom_pos.z
570 + target_height * combat::PROJECTILE_HEADSHOT_PROPORTION * 0.5,
571 );
572 if trajectory.distance_to_point(head_middle_pos) < target_radius {
573 Some(combat::MAX_HEADSHOT_PRECISION)
574 } else {
575 None
576 }
577 }
578 };
579
580 let precision_mult = match (precision_from_flank, precision_from_head) {
581 (Some(a), Some(b)) => Some(a.max(b)),
582 (Some(a), None) | (None, Some(a)) => Some(a),
583 (None, None) => None,
584 };
585
586 let attack_options = AttackOptions {
587 target_dodging,
588 permit_pvp,
589 allow_friendly_fire,
590 target_group: projectile_target_info.target_group,
591 precision_mult,
592 };
593
594 attack.apply_attack(
595 attacker_info,
596 &target_info,
597 projectile_dir,
598 attack_options,
599 1.0,
600 AttackSource::Projectile,
601 *read_data.time,
602 emitters,
603 |o| outcomes_emitter.emit(o),
604 rng,
605 0,
606 );
607 },
608 projectile::Effect::Explode(e) => {
609 let Pos(pos) = *projectile_info.pos;
610 let owner_uid = projectile_info.owner_uid;
611 emitters.emit(ExplosionEvent {
612 pos,
613 explosion: e,
614 owner: owner_uid,
615 });
616 },
617 projectile::Effect::Bonk => {
618 let Pos(pos) = *projectile_info.pos;
619 let owner_uid = projectile_info.owner_uid;
620 emitters.emit(BonkEvent {
621 pos,
622 owner: owner_uid,
623 target: Some(projectile_target_info.uid),
624 });
625 },
626 projectile::Effect::Vanish => {
627 let entity = projectile_info.entity;
628 emitters.emit(DeleteEvent(entity));
629 *projectile_vanished = true;
630 },
631 projectile::Effect::Possess => {
632 let target_uid = projectile_target_info.uid;
633 let owner_uid = projectile_info.owner_uid;
634 if let Some(owner_uid) = owner_uid {
635 if target_uid != owner_uid {
636 emitters.emit(PossessEvent(owner_uid, target_uid));
637 }
638 }
639 },
640 projectile::Effect::Stick => {},
641 projectile::Effect::Firework(_) => {},
642 projectile::Effect::SurpriseEgg => {
643 let Pos(pos) = *projectile_info.pos;
644 outcomes_emitter.emit(Outcome::SurpriseEgg { pos });
645 },
646 projectile::Effect::TrainingDummy => emitters.emit(CreateNpcEvent {
647 pos: *projectile_info.pos,
648 ori: Ori::default(),
649 npc: NpcBuilder::new(
650 Stats::new(
651 Content::with_attr("name-custom-village-dummy", "neut"),
652 Body::Object(object::Body::TrainingDummy),
653 ),
654 Body::Object(object::Body::TrainingDummy),
655 Alignment::Npc,
656 ),
657 }),
658 }
659}