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, projectile) in (
109 &read_data.entities,
110 &read_data.positions,
111 &read_data.physics_states,
112 &read_data.velocities,
113 &mut projectiles,
114 )
115 .join()
116 {
117 let projectile_owner = projectile
118 .owner
119 .and_then(|uid| read_data.id_maps.uid_entity(uid));
120
121 if physics.on_surface().is_none() && rng.gen_bool(0.05) {
122 emitters.emit(SoundEvent {
123 sound: Sound::new(SoundKind::Projectile, pos.0, 4.0, read_data.time.0),
124 });
125 }
126
127 let mut projectile_vanished: bool = false;
128
129 for (&other, &pos_hit_other) in physics.touch_entities.iter() {
131 let same_group = projectile_owner
132 .and_then(|e| read_data.groups.get(e)).is_some_and(|owner_group|
136 Some(owner_group) == read_data.id_maps
137 .uid_entity(other)
138 .and_then(|e| read_data.groups.get(e)));
139
140 let target_group = if same_group {
142 GroupTarget::InGroup
143 } else {
144 GroupTarget::OutOfGroup
145 };
146
147 if projectile.ignore_group
148 && same_group
149 && projectile
150 .owner
151 .and_then(|owner| {
152 read_data
153 .id_maps
154 .uid_entity(owner)
155 .zip(read_data.id_maps.uid_entity(other))
156 })
157 .is_none_or(|(owner, other)| {
158 !combat::allow_friendly_fire(&read_data.entered_auras, owner, other)
159 })
160 {
161 continue;
162 }
163
164 if projectile.owner == Some(other) {
165 continue;
166 }
167
168 let projectile = &mut *projectile;
169
170 let entity_of = |uid: Uid| read_data.id_maps.uid_entity(uid);
171
172 if physics.on_surface().is_some() {
176 let projectile_direction = orientations
177 .get(entity)
178 .map_or_else(Vec3::zero, |ori| ori.look_vec());
179 let pos_wall = pos.0 - 0.2 * projectile_direction;
180 if !matches!(
181 read_data
182 .terrain
183 .ray(pos_wall, pos_hit_other)
184 .until(|b| b.is_filled())
185 .cast()
186 .1,
187 Ok(None)
188 ) {
189 continue;
190 }
191 }
192
193 for effect in projectile.hit_entity.drain(..) {
194 let owner = projectile.owner.and_then(entity_of);
195 let projectile_info = ProjectileInfo {
196 entity,
197 effect,
198 owner_uid: projectile.owner,
199 owner,
200 ori: orientations.get(entity),
201 pos,
202 vel,
203 };
204
205 let target = entity_of(other);
206 let projectile_target_info = ProjectileTargetInfo {
207 uid: other,
208 entity: target,
209 target_group,
210 ori: target.and_then(|target| orientations.get(target)),
211 };
212
213 dispatch_hit(
214 projectile_info,
215 projectile_target_info,
216 &read_data,
217 &mut projectile_vanished,
218 &mut outcomes_emitter,
219 &mut emitters,
220 &mut rng,
221 );
222 }
223
224 if projectile_vanished {
225 continue 'projectile_loop;
226 }
227 }
228
229 if physics.on_surface().is_some() {
230 let projectile = &mut *projectile;
231 for effect in projectile.hit_solid.drain(..) {
232 match effect {
233 projectile::Effect::Explode(e) => {
234 let projectile_direction = orientations
242 .get(entity)
243 .map_or_else(Vec3::zero, |ori| ori.look_vec());
244 let offset = -0.2 * projectile_direction;
245 emitters.emit(ExplosionEvent {
246 pos: pos.0 + offset,
247 explosion: e,
248 owner: projectile.owner,
249 });
250 },
251 projectile::Effect::Vanish => {
252 emitters.emit(DeleteEvent(entity));
253 projectile_vanished = true;
254 },
255 projectile::Effect::Bonk => {
256 emitters.emit(BonkEvent {
257 pos: pos.0,
258 owner: projectile.owner,
259 target: None,
260 });
261 },
262 projectile::Effect::SurpriseEgg => {
263 outcomes_emitter.emit(Outcome::SurpriseEgg { pos: pos.0 });
264 },
265 projectile::Effect::TrainingDummy => {
266 let body = Body::Object(object::Body::TrainingDummy);
267 emitters.emit(CreateNpcEvent {
268 pos: *pos,
269 ori: Ori::default(),
270 npc: NpcBuilder::new(
271 Stats::new(
272 Content::with_attr("name-custom-village-dummy", "neut"),
273 body,
274 ),
275 body,
276 Alignment::Npc,
277 )
278 .with_health(Health::new(body))
279 .with_poise(Poise::new(body)),
280 });
281 },
282 _ => {},
283 }
284 }
285
286 if projectile_vanished {
287 continue 'projectile_loop;
288 }
289 } else if let Some(ori) = orientations.get_mut(entity) {
290 if let Some(dir) = Dir::from_unnormalized(vel.0) {
291 *ori = dir.into();
292 }
293 }
294
295 if projectile.time_left == Duration::ZERO {
296 emitters.emit(DeleteEvent(entity));
297
298 for effect in projectile.timeout.drain(..) {
299 if let projectile::Effect::Firework(reagent) = effect {
300 const ENABLE_RECURSIVE_FIREWORKS: bool = true;
301 if ENABLE_RECURSIVE_FIREWORKS {
302 use common::{
303 comp::{LightEmitter, object},
304 event::ShootEvent,
305 };
306 use std::f32::consts::PI;
307 let thresholds: &[(f32, usize)] = &[(0.25, 2), (0.7, 1)];
311 let expected = {
312 let mut total = 0.0;
313 let mut cumulative_probability = 0.0;
314 for (p, n) in thresholds {
315 total += (p - cumulative_probability) * *n as f32;
316 cumulative_probability += p;
317 }
318 total
319 };
320 assert!(expected < 1.0);
321 let num_fireworks = (|| {
322 let x = rng.gen_range(0.0..1.0);
323 for (p, n) in thresholds {
324 if x < *p {
325 return *n;
326 }
327 }
328 0
329 })();
330 for _ in 0..num_fireworks {
331 let speed: f32 = rng.gen_range(40.0..80.0);
332 let theta: f32 = rng.gen_range(0.0..2.0 * PI);
333 let phi: f32 = rng.gen_range(0.25 * PI..0.5 * PI);
334 let dir = Dir::from_unnormalized(Vec3::new(
335 theta.cos(),
336 theta.sin(),
337 phi.sin(),
338 ))
339 .expect("nonzero vector should normalize");
340 emitters.emit(ShootEvent {
341 entity: Some(entity),
342 pos: *pos,
343 dir,
344 body: Body::Object(object::Body::for_firework(reagent)),
345 light: Some(LightEmitter {
346 animated: true,
347 flicker: 2.0,
348 strength: 2.0,
349 col: Rgb::new(1.0, 1.0, 0.0),
350 }),
351 projectile: Projectile {
352 hit_solid: Vec::new(),
353 hit_entity: Vec::new(),
354 timeout: vec![projectile::Effect::Firework(reagent)],
355 time_left: Duration::from_secs(1),
356 ignore_group: true,
357 is_sticky: true,
358 is_point: true,
359 owner: projectile.owner,
360 },
361 speed,
362 object: None,
363 });
364 }
365 }
366 emitters.emit(DeleteEvent(entity));
367 emitters.emit(ExplosionEvent {
368 pos: pos.0,
369 explosion: Explosion {
370 effects: vec![
371 RadiusEffect::Entity(effect::Effect::Damage(Damage {
372 source: DamageSource::Explosion,
373 kind: DamageKind::Energy,
374 value: 5.0,
375 })),
376 RadiusEffect::Entity(effect::Effect::Poise(-40.0)),
377 RadiusEffect::TerrainDestruction(4.0, Rgb::black()),
378 ],
379 radius: 12.0,
380 reagent: Some(reagent),
381 min_falloff: 0.0,
382 },
383 owner: projectile.owner,
384 });
385 }
386 }
387 }
388 projectile.time_left = projectile
389 .time_left
390 .checked_sub(Duration::from_secs_f32(read_data.dt.0))
391 .unwrap_or_default();
392 }
393 }
394}
395
396struct ProjectileInfo<'a> {
397 entity: EcsEntity,
398 effect: projectile::Effect,
399 owner_uid: Option<Uid>,
400 owner: Option<EcsEntity>,
401 ori: Option<&'a Ori>,
402 pos: &'a Pos,
403 vel: &'a Vel,
404}
405
406struct ProjectileTargetInfo<'a> {
407 uid: Uid,
408 entity: Option<EcsEntity>,
409 target_group: GroupTarget,
410 ori: Option<&'a Ori>,
411}
412
413fn dispatch_hit(
414 projectile_info: ProjectileInfo,
415 projectile_target_info: ProjectileTargetInfo,
416 read_data: &ReadData,
417 projectile_vanished: &mut bool,
418 outcomes_emitter: &mut Emitter<Outcome>,
419 emitters: &mut Emitters,
420 rng: &mut rand::rngs::ThreadRng,
421) {
422 match projectile_info.effect {
423 projectile::Effect::Attack(attack) => {
424 let target_uid = projectile_target_info.uid;
425 let target = if let Some(entity) = projectile_target_info.entity {
426 entity
427 } else {
428 return;
429 };
430
431 let (target_pos, projectile_dir) = {
432 let target_pos = read_data.positions.get(target);
433 let projectile_ori = projectile_info.ori;
434 match target_pos.zip(projectile_ori) {
435 Some((tgt_pos, proj_ori)) => {
436 let Pos(tgt_pos) = tgt_pos;
437 (*tgt_pos, proj_ori.look_dir())
438 },
439 None => return,
440 }
441 };
442
443 let owner = projectile_info.owner;
444 let projectile_entity = projectile_info.entity;
445
446 let attacker_info =
447 owner
448 .zip(projectile_info.owner_uid)
449 .map(|(entity, uid)| AttackerInfo {
450 entity,
451 uid,
452 group: read_data.groups.get(entity),
453 energy: read_data.energies.get(entity),
454 combo: read_data.combos.get(entity),
455 inventory: read_data.inventories.get(entity),
456 stats: read_data.stats.get(entity),
457 mass: read_data.masses.get(entity),
458 });
459
460 let target_info = TargetInfo {
461 entity: target,
462 uid: target_uid,
463 inventory: read_data.inventories.get(target),
464 stats: read_data.stats.get(target),
465 health: read_data.healths.get(target),
466 pos: target_pos,
467 ori: projectile_target_info.ori,
468 char_state: read_data.character_states.get(target),
469 energy: read_data.energies.get(target),
470 buffs: read_data.buffs.get(target),
471 mass: read_data.masses.get(target),
472 };
473
474 if let Some(&body) = read_data.bodies.get(projectile_entity) {
476 outcomes_emitter.emit(Outcome::ProjectileHit {
477 pos: target_pos,
478 body,
479 vel: read_data
480 .velocities
481 .get(projectile_entity)
482 .map_or(Vec3::zero(), |v| v.0),
483 source: projectile_info.owner_uid,
484 target: read_data.uids.get(target).copied(),
485 });
486 }
487
488 let allow_friendly_fire = owner.is_some_and(|owner| {
489 combat::allow_friendly_fire(&read_data.entered_auras, owner, target)
490 });
491
492 let permit_pvp = combat::permit_pvp(
494 &read_data.alignments,
495 &read_data.players,
496 &read_data.entered_auras,
497 &read_data.id_maps,
498 owner,
499 target,
500 );
501
502 let target_dodging = read_data
503 .character_states
504 .get(target)
505 .and_then(|cs| cs.roll_attack_immunities())
506 .is_some_and(|i| i.projectiles);
507
508 let precision_from_flank = combat::precision_mult_from_flank(
509 *projectile_dir,
510 target_info.ori,
511 Default::default(),
512 false,
513 );
514
515 let precision_from_head = {
516 let curr_pos = projectile_info.pos.0;
520 let last_pos = projectile_info.pos.0 - projectile_info.vel.0 * read_data.dt.0;
521 let vel = projectile_info.vel.0;
522 let (target_height, target_radius) = read_data
523 .bodies
524 .get(target)
525 .map_or((0.0, 0.0), |b| (b.height(), b.max_radius()));
526 let head_top_pos = target_pos.with_z(target_pos.z + target_height);
527 let head_bottom_pos = head_top_pos.with_z(
528 head_top_pos.z - target_height * combat::PROJECTILE_HEADSHOT_PROPORTION,
529 );
530 if (curr_pos.z < head_bottom_pos.z && last_pos.z < head_bottom_pos.z)
531 || (curr_pos.z > head_top_pos.z && last_pos.z > head_top_pos.z)
532 {
533 None
534 } else if curr_pos.z > head_top_pos.z
535 || curr_pos.z < head_bottom_pos.z
536 || last_pos.z > head_top_pos.z
537 || last_pos.z < head_bottom_pos.z
538 {
539 let proj_top_intersection = {
540 let t = (head_top_pos.z - last_pos.z) / vel.z;
541 last_pos + vel * t
542 };
543 let proj_bottom_intersection = {
544 let t = (head_bottom_pos.z - last_pos.z) / vel.z;
545 last_pos + vel * t
546 };
547 let intersected_bottom = head_bottom_pos
548 .distance_squared(proj_bottom_intersection)
549 < target_radius.powi(2);
550 let intersected_top = head_top_pos.distance_squared(proj_top_intersection)
551 < target_radius.powi(2);
552 let hit_head = intersected_bottom || intersected_top;
553 let hit_from_bottom = last_pos.z < head_bottom_pos.z && intersected_bottom;
554 let hit_from_top = last_pos.z > head_top_pos.z && intersected_top;
555 if !hit_head || hit_from_bottom {
559 None
560 } else if hit_from_top {
561 Some(combat::MAX_TOP_HEADSHOT_PRECISION)
562 } else {
563 Some(combat::MAX_HEADSHOT_PRECISION)
564 }
565 } else {
566 let trajectory = LineSegment3 {
567 start: last_pos,
568 end: curr_pos,
569 };
570 let head_middle_pos = head_bottom_pos.with_z(
571 head_bottom_pos.z
572 + target_height * combat::PROJECTILE_HEADSHOT_PROPORTION * 0.5,
573 );
574 if trajectory.distance_to_point(head_middle_pos) < target_radius {
575 Some(combat::MAX_HEADSHOT_PRECISION)
576 } else {
577 None
578 }
579 }
580 };
581
582 let precision_mult = match (precision_from_flank, precision_from_head) {
583 (Some(a), Some(b)) => Some(a.max(b)),
584 (Some(a), None) | (None, Some(a)) => Some(a),
585 (None, None) => None,
586 };
587
588 let attack_options = AttackOptions {
589 target_dodging,
590 permit_pvp,
591 allow_friendly_fire,
592 target_group: projectile_target_info.target_group,
593 precision_mult,
594 };
595
596 attack.apply_attack(
597 attacker_info,
598 &target_info,
599 projectile_dir,
600 attack_options,
601 1.0,
602 AttackSource::Projectile,
603 *read_data.time,
604 emitters,
605 |o| outcomes_emitter.emit(o),
606 rng,
607 0,
608 );
609 },
610 projectile::Effect::Explode(e) => {
611 let Pos(pos) = *projectile_info.pos;
612 let owner_uid = projectile_info.owner_uid;
613 emitters.emit(ExplosionEvent {
614 pos,
615 explosion: e,
616 owner: owner_uid,
617 });
618 },
619 projectile::Effect::Bonk => {
620 let Pos(pos) = *projectile_info.pos;
621 let owner_uid = projectile_info.owner_uid;
622 emitters.emit(BonkEvent {
623 pos,
624 owner: owner_uid,
625 target: Some(projectile_target_info.uid),
626 });
627 },
628 projectile::Effect::Vanish => {
629 let entity = projectile_info.entity;
630 emitters.emit(DeleteEvent(entity));
631 *projectile_vanished = true;
632 },
633 projectile::Effect::Possess => {
634 let target_uid = projectile_target_info.uid;
635 let owner_uid = projectile_info.owner_uid;
636 if let Some(owner_uid) = owner_uid {
637 if target_uid != owner_uid {
638 emitters.emit(PossessEvent(owner_uid, target_uid));
639 }
640 }
641 },
642 projectile::Effect::Stick => {},
643 projectile::Effect::Firework(_) => {},
644 projectile::Effect::SurpriseEgg => {
645 let Pos(pos) = *projectile_info.pos;
646 outcomes_emitter.emit(Outcome::SurpriseEgg { pos });
647 },
648 projectile::Effect::TrainingDummy => emitters.emit(CreateNpcEvent {
649 pos: *projectile_info.pos,
650 ori: Ori::default(),
651 npc: NpcBuilder::new(
652 Stats::new(
653 Content::with_attr("name-custom-village-dummy", "neut"),
654 Body::Object(object::Body::TrainingDummy),
655 ),
656 Body::Object(object::Body::TrainingDummy),
657 Alignment::Npc,
658 ),
659 }),
660 }
661}