1use common::{
2 Damage, DamageKind, 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,
10 projectile::{self, ProjectileHitEntities, SplitOptions},
11 },
12 effect,
13 event::{
14 ArcingEvent, BonkEvent, BuffEvent, ComboChangeEvent, CreateNpcEvent, DeleteEvent, EmitExt,
15 Emitter, EnergyChangeEvent, EntityAttackedHookEvent, EventBus, ExplosionEvent,
16 HealthChangeEvent, KnockbackEvent, NpcBuilder, ParryHookEvent, PoiseChangeEvent,
17 PossessEvent, ShootEvent, SoundEvent, TransformEvent,
18 },
19 event_emitters,
20 outcome::Outcome,
21 resources::{DeltaTime, Secs, Time},
22 uid::{IdMaps, Uid},
23 util::Dir,
24};
25
26use common::vol::ReadVol;
27use common_ecs::{Job, Origin, Phase, System};
28use itertools::Either;
29use rand::Rng;
30use specs::{
31 Entities, Entity as EcsEntity, Join, Read, ReadExpect, ReadStorage, SystemData, WriteStorage,
32 shred,
33};
34use std::{
35 f32::consts::{PI, TAU},
36 time::Duration,
37};
38use vek::*;
39
40use common::terrain::TerrainGrid;
41
42event_emitters! {
43 struct Events[Emitters] {
44 sound: SoundEvent,
45 delete: DeleteEvent,
46 explosion: ExplosionEvent,
47 health_change: HealthChangeEvent,
48 energy_change: EnergyChangeEvent,
49 poise_change: PoiseChangeEvent,
50 parry_hook: ParryHookEvent,
51 knockback: KnockbackEvent,
52 entity_attack_hook: EntityAttackedHookEvent,
53 shoot: ShootEvent,
54 create_npc: CreateNpcEvent,
55 combo_change: ComboChangeEvent,
56 buff: BuffEvent,
57 bonk: BonkEvent,
58 possess: PossessEvent,
59 arc: ArcingEvent,
60 transform: TransformEvent,
61 }
62}
63
64#[derive(SystemData)]
65pub struct ReadData<'a> {
66 time: Read<'a, Time>,
67 entities: Entities<'a>,
68 players: ReadStorage<'a, Player>,
69 dt: Read<'a, DeltaTime>,
70 id_maps: Read<'a, IdMaps>,
71 events: Events<'a>,
72 uids: ReadStorage<'a, Uid>,
73 positions: ReadStorage<'a, Pos>,
74 alignments: ReadStorage<'a, Alignment>,
75 physics_states: ReadStorage<'a, PhysicsState>,
76 inventories: ReadStorage<'a, Inventory>,
77 groups: ReadStorage<'a, Group>,
78 energies: ReadStorage<'a, Energy>,
79 stats: ReadStorage<'a, Stats>,
80 combos: ReadStorage<'a, Combo>,
81 healths: ReadStorage<'a, Health>,
82 bodies: ReadStorage<'a, Body>,
83 character_states: ReadStorage<'a, CharacterState>,
84 terrain: ReadExpect<'a, TerrainGrid>,
85 buffs: ReadStorage<'a, Buffs>,
86 entered_auras: ReadStorage<'a, EnteredAuras>,
87 masses: ReadStorage<'a, Mass>,
88}
89
90#[derive(Default)]
92pub struct Sys;
93impl<'a> System<'a> for Sys {
94 type SystemData = (
95 ReadData<'a>,
96 WriteStorage<'a, Ori>,
97 WriteStorage<'a, Projectile>,
98 Read<'a, EventBus<Outcome>>,
99 WriteStorage<'a, Vel>,
100 WriteStorage<'a, ProjectileHitEntities>,
101 );
102
103 const NAME: &'static str = "projectile";
104 const ORIGIN: Origin = Origin::Common;
105 const PHASE: Phase = Phase::Create;
106
107 fn run(
108 _job: &mut Job<Self>,
109 (read_data, mut orientations, mut projectiles, outcomes, mut velocities, mut hit_entities): Self::SystemData,
110 ) {
111 let mut emitters = read_data.events.get_emitters();
112 let mut outcomes_emitter = outcomes.emitter();
113 let mut rng = rand::rng();
114
115 'projectile_loop: for (entity, pos, physics, body, projectile) in (
117 &read_data.entities,
118 &read_data.positions,
119 &read_data.physics_states,
120 &read_data.bodies,
121 &mut projectiles,
122 )
123 .join()
124 {
125 let projectile_owner = projectile
126 .owner
127 .and_then(|uid| read_data.id_maps.uid_entity(uid));
128
129 if physics.on_surface().is_none() && rng.random_bool(0.05) {
130 emitters.emit(SoundEvent {
131 sound: Sound::new(SoundKind::Projectile, pos.0, 4.0, read_data.time.0),
132 });
133 }
134
135 let mut projectile_vanished: bool = false;
136
137 for (&other, &pos_hit_other) in physics.touch_entities.iter() {
139 let same_group = projectile_owner
140 .and_then(|e| read_data.groups.get(e)).is_some_and(|owner_group|
144 Some(owner_group) == read_data.id_maps
145 .uid_entity(other)
146 .and_then(|e| read_data.groups.get(e)));
147
148 let target_group = if same_group {
150 GroupTarget::InGroup
151 } else {
152 GroupTarget::OutOfGroup
153 };
154
155 if projectile.ignore_group
156 && same_group
157 && projectile
158 .owner
159 .and_then(|owner| {
160 read_data
161 .id_maps
162 .uid_entity(owner)
163 .zip(read_data.id_maps.uid_entity(other))
164 })
165 .is_none_or(|(owner, other)| {
166 !combat::allow_friendly_fire(&read_data.entered_auras, owner, other)
167 })
168 {
169 continue;
170 }
171
172 if projectile.owner == Some(other) {
173 continue;
174 }
175
176 if projectile.hit_entities.contains(&other) {
178 continue;
179 }
180
181 if projectile.limit_per_ability
184 && projectile
185 .owner
186 .and_then(|owner| read_data.id_maps.uid_entity(owner))
187 .and_then(|owner| hit_entities.get(owner))
188 .is_some_and(|h_e| h_e.hit_entities.iter().any(|(hit, _)| *hit == other))
189 {
190 continue;
191 }
192
193 let projectile = &mut *projectile;
194
195 let entity_of = |uid: Uid| read_data.id_maps.uid_entity(uid);
196
197 if physics.on_surface().is_some() {
201 let projectile_direction = orientations
202 .get(entity)
203 .map_or_else(Vec3::zero, |ori| ori.look_vec());
204 let pos_wall = pos.0 - 0.2 * projectile_direction;
205 if !matches!(
206 read_data
207 .terrain
208 .ray(pos_wall, pos_hit_other)
209 .until(|b| b.is_filled())
210 .cast()
211 .1,
212 Ok(None)
213 ) {
214 continue;
215 }
216 }
217
218 projectile.hit_entities.push(other);
221
222 if let Some(owner) = projectile
226 .owner
227 .and_then(|owner| read_data.id_maps.uid_entity(owner))
228 && projectile.limit_per_ability
229 && let Some(hit_entities) = hit_entities.get_mut(owner)
230 {
231 hit_entities.hit_entities.push((other, *read_data.time))
232 }
233
234 let effects = if projectile.pierce_entities {
235 Either::Left(projectile.hit_entity.clone().into_iter())
236 } else {
237 Either::Right(projectile.hit_entity.drain(..))
238 };
239
240 for effect in effects {
241 let owner = projectile.owner.and_then(entity_of);
242 let projectile_info = ProjectileInfo {
243 entity,
244 effect,
245 owner_uid: projectile.owner,
246 owner,
247 ori: orientations.get(entity),
248 pos,
249 vel: velocities.get(entity).copied().unwrap_or(Vel(Vec3::zero())),
250 };
251
252 let target = entity_of(other);
253 let projectile_target_info = ProjectileTargetInfo {
254 uid: other,
255 entity: target,
256 target_group,
257 ori: target.and_then(|target| orientations.get(target)),
258 };
259
260 dispatch_hit(
261 projectile_info,
262 projectile_target_info,
263 &read_data,
264 &mut projectile_vanished,
265 &mut outcomes_emitter,
266 &mut emitters,
267 &mut rng,
268 );
269 }
270
271 if projectile_vanished {
272 continue 'projectile_loop;
273 }
274 }
275
276 if physics.on_surface().is_some() {
277 let init_projectile = projectile.clone();
278 let projectile = &mut *projectile;
279 for effect in projectile.hit_solid.drain(..) {
280 match effect {
281 projectile::Effect::Explode(e) => {
282 let projectile_direction = orientations
290 .get(entity)
291 .map_or_else(Vec3::zero, |ori| ori.look_vec());
292 let offset = -0.2 * projectile_direction;
293 emitters.emit(ExplosionEvent {
294 pos: pos.0 + offset,
295 explosion: e,
296 owner: projectile.owner,
297 });
298 },
299 projectile::Effect::Vanish => {
300 emitters.emit(DeleteEvent(entity));
301 projectile_vanished = true;
302 },
303 projectile::Effect::Bonk => {
304 emitters.emit(BonkEvent {
305 pos: pos.0,
306 owner: projectile.owner,
307 target: None,
308 });
309 },
310 projectile::Effect::SurpriseEgg => {
311 outcomes_emitter.emit(Outcome::SurpriseEgg { pos: pos.0 });
312 },
313 projectile::Effect::TrainingDummy => {
314 let body = Body::Object(object::Body::TrainingDummy);
315 emitters.emit(CreateNpcEvent {
316 pos: *pos,
317 ori: Ori::default(),
318 npc: NpcBuilder::new(
319 Stats::new(
320 Content::with_attr("name-custom-village-dummy", "neut"),
321 body,
322 ),
323 body,
324 Alignment::Npc,
325 )
326 .with_health(Health::new(body))
327 .with_poise(Poise::new(body)),
328 });
329 },
330 projectile::Effect::Split(split) => {
331 let init_dir = physics.on_surface().map(|d| -d).unwrap_or_default();
332
333 let new_pos = Pos(pos.0 - physics.on_surface().unwrap_or_default());
334
335 let speed = 10.0;
336
337 handle_split_effect(
338 &split,
339 &init_projectile,
340 init_dir,
341 new_pos,
342 &mut rng,
343 projectile_owner,
344 body,
345 speed,
346 &mut emitters,
347 );
348
349 projectile_vanished = true;
351 },
352 _ => {},
353 }
354 }
355
356 if projectile_vanished {
357 continue 'projectile_loop;
358 }
359 } else {
360 if let Some(ori) = orientations.get_mut(entity)
361 && let Some(dir) = velocities
362 .get(entity)
363 .and_then(|v| Dir::from_unnormalized(v.0))
364 {
365 *ori = dir.into();
366 }
367
368 if let Some(vel) = velocities.get_mut(entity)
369 && let Some((tgt_uid, rate)) = projectile.homing
370 && let Some(tgt_pos) = read_data
371 .id_maps
372 .uid_entity(tgt_uid)
373 .and_then(|e| read_data.positions.get(e))
374 && let Some((init_dir, tgt_dir)) = Dir::from_unnormalized(vel.0).zip(
375 Dir::from_unnormalized(tgt_pos.0.with_z(tgt_pos.0.z + 1.0) - pos.0),
376 )
377 {
378 let time_factor = (projectile.init_time.0 as f32
380 - projectile.time_left.as_secs_f32())
381 .min(1.0);
382 let factor = (rate * read_data.dt.0 / init_dir.angle_between(*tgt_dir)
383 * time_factor)
384 .min(1.0);
385 let new_dir = init_dir.slerped_to(tgt_dir, factor);
386 *vel = Vel(*new_dir * vel.0.magnitude());
387 }
388 }
389
390 if projectile.time_left == Duration::ZERO {
391 emitters.emit(DeleteEvent(entity));
392
393 if !projectile.timeout.is_empty() {
396 let init_projectile = projectile.clone();
397 for effect in projectile.timeout.drain(..) {
398 match effect {
399 projectile::Effect::Firework(reagent) => {
400 const ENABLE_RECURSIVE_FIREWORKS: bool = true;
401 if ENABLE_RECURSIVE_FIREWORKS {
402 use common::{comp::LightEmitter, event::ShootEvent};
403 use std::f32::consts::PI;
404 let thresholds: &[(f32, usize)] = &[(0.25, 2), (0.7, 1)];
408 let expected = {
409 let mut total = 0.0;
410 let mut cumulative_probability = 0.0;
411 for (p, n) in thresholds {
412 total += (p - cumulative_probability) * *n as f32;
413 cumulative_probability += p;
414 }
415 total
416 };
417 assert!(expected < 1.0);
418 let num_fireworks = (|| {
419 let x = rng.random_range(0.0..1.0);
420 for (p, n) in thresholds {
421 if x < *p {
422 return *n;
423 }
424 }
425 0
426 })();
427 for _ in 0..num_fireworks {
428 let speed: f32 = rng.random_range(40.0..80.0);
429 let theta: f32 = rng.random_range(0.0..2.0 * PI);
430 let phi: f32 = rng.random_range(0.25 * PI..0.5 * PI);
431 let dir = Dir::from_unnormalized(Vec3::new(
432 theta.cos(),
433 theta.sin(),
434 phi.sin(),
435 ))
436 .expect("nonzero vector should normalize");
437 emitters.emit(ShootEvent {
438 entity: Some(entity),
439 source_vel: velocities.get(entity).copied(),
440 pos: *pos,
441 dir,
442 body: *body,
443 light: Some(LightEmitter {
444 animated: true,
445 flicker: 2.0,
446 strength: 2.0,
447 col: Rgb::new(1.0, 1.0, 0.0),
448 dir: None,
449 }),
450 projectile: Projectile {
451 hit_solid: Vec::new(),
452 hit_entity: Vec::new(),
453 timeout: vec![projectile::Effect::Firework(
454 reagent,
455 )],
456 time_left: Duration::from_secs(1),
457 init_time: Secs(1.0),
458 ignore_group: true,
459 is_sticky: true,
460 is_point: true,
461 owner: projectile.owner,
462 homing: None,
463 pierce_entities: false,
464 hit_entities: Vec::new(),
465 limit_per_ability: false,
466 override_collider: None,
467 },
468 speed,
469 object: None,
470 marker: None,
471 });
472 }
473 }
474 emitters.emit(ExplosionEvent {
475 pos: pos.0,
476 explosion: Explosion {
477 effects: vec![
478 RadiusEffect::Entity(effect::Effect::Damage(Damage {
479 kind: DamageKind::Energy,
480 value: 5.0,
481 })),
482 RadiusEffect::Entity(effect::Effect::Poise(-40.0)),
483 RadiusEffect::TerrainDestruction(4.0, Rgb::black()),
484 ],
485 radius: 12.0,
486 reagent: Some(reagent),
487 min_falloff: 0.0,
488 },
489 owner: projectile.owner,
490 });
491 },
492 projectile::Effect::Split(split) if physics.on_surface().is_none() => {
493 let init_dir = velocities
494 .get(entity)
495 .and_then(|v| Dir::from_unnormalized(v.0))
496 .unwrap_or_default();
497
498 let speed = velocities.get(entity).map_or(0.0, |v| v.0.magnitude());
499
500 handle_split_effect(
501 &split,
502 &init_projectile,
503 *init_dir,
504 *pos,
505 &mut rng,
506 projectile_owner,
507 body,
508 speed,
509 &mut emitters,
510 );
511 },
512 _ => {},
513 }
514 }
515 }
516 }
517 projectile.time_left = projectile
518 .time_left
519 .checked_sub(Duration::from_secs_f32(read_data.dt.0))
520 .unwrap_or_default();
521 }
522
523 for hit_entities_list in (&mut hit_entities).join() {
525 hit_entities_list
526 .hit_entities
527 .retain(|(_, time)| read_data.time.0 < time.0 + 1.0)
528 }
529 }
530}
531
532struct ProjectileInfo<'a> {
533 entity: EcsEntity,
534 effect: projectile::Effect,
535 owner_uid: Option<Uid>,
536 owner: Option<EcsEntity>,
537 ori: Option<&'a Ori>,
538 pos: &'a Pos,
539 vel: Vel,
540}
541
542struct ProjectileTargetInfo<'a> {
543 uid: Uid,
544 entity: Option<EcsEntity>,
545 target_group: GroupTarget,
546 ori: Option<&'a Ori>,
547}
548
549fn dispatch_hit(
550 projectile_info: ProjectileInfo,
551 projectile_target_info: ProjectileTargetInfo,
552 read_data: &ReadData,
553 projectile_vanished: &mut bool,
554 outcomes_emitter: &mut Emitter<Outcome>,
555 emitters: &mut Emitters,
556 rng: &mut rand::rngs::ThreadRng,
557) {
558 match projectile_info.effect {
559 projectile::Effect::Attack(attack) => {
560 let target_uid = projectile_target_info.uid;
561 let target = if let Some(entity) = projectile_target_info.entity {
562 entity
563 } else {
564 return;
565 };
566
567 let (target_pos, projectile_dir) = {
568 let target_pos = read_data.positions.get(target);
569 let projectile_ori = projectile_info.ori;
570 match target_pos.zip(projectile_ori) {
571 Some((tgt_pos, proj_ori)) => {
572 let Pos(tgt_pos) = tgt_pos;
573 (*tgt_pos, proj_ori.look_dir())
574 },
575 None => return,
576 }
577 };
578
579 let owner = projectile_info.owner;
580 let projectile_entity = projectile_info.entity;
581
582 let attacker_info =
583 owner
584 .zip(projectile_info.owner_uid)
585 .map(|(entity, uid)| AttackerInfo {
586 entity,
587 uid,
588 group: read_data.groups.get(entity),
589 energy: read_data.energies.get(entity),
590 combo: read_data.combos.get(entity),
591 inventory: read_data.inventories.get(entity),
592 stats: read_data.stats.get(entity),
593 mass: read_data.masses.get(entity),
594 pos: read_data.positions.get(entity).map(|p| p.0),
595 });
596
597 let target_info = TargetInfo {
598 entity: target,
599 uid: target_uid,
600 inventory: read_data.inventories.get(target),
601 stats: read_data.stats.get(target),
602 health: read_data.healths.get(target),
603 pos: target_pos,
604 ori: projectile_target_info.ori,
605 char_state: read_data.character_states.get(target),
606 energy: read_data.energies.get(target),
607 buffs: read_data.buffs.get(target),
608 mass: read_data.masses.get(target),
609 player: read_data.players.get(target),
610 };
611
612 if let Some(&body) = read_data.bodies.get(projectile_entity) {
614 outcomes_emitter.emit(Outcome::ProjectileHit {
615 pos: target_pos,
616 body,
617 vel: projectile_info.vel.0,
618 source: projectile_info.owner_uid,
619 target: read_data.uids.get(target).copied(),
620 });
621 }
622
623 let allow_friendly_fire = owner.is_some_and(|owner| {
624 combat::allow_friendly_fire(&read_data.entered_auras, owner, target)
625 });
626
627 let permit_pvp = combat::permit_pvp(
629 &read_data.alignments,
630 &read_data.players,
631 &read_data.entered_auras,
632 &read_data.id_maps,
633 owner,
634 target,
635 );
636
637 let target_dodging = read_data
638 .character_states
639 .get(target)
640 .and_then(|cs| cs.roll_attack_immunities())
641 .is_some_and(|i| i.projectiles);
642
643 let precision_from_flank = combat::precision_mult_from_flank(
644 *projectile_dir,
645 target_info.ori,
646 Default::default(),
647 false,
648 );
649
650 let precision_from_head = {
651 let curr_pos = projectile_info.pos.0;
655 let last_pos = projectile_info.pos.0 - projectile_info.vel.0 * read_data.dt.0;
656 let vel = projectile_info.vel;
657 let (target_height, target_radius) = read_data
658 .bodies
659 .get(target)
660 .map_or((0.0, 0.0), |b| (b.height(), b.max_radius()));
661 let head_top_pos = target_pos.with_z(target_pos.z + target_height);
662 let head_bottom_pos = head_top_pos.with_z(
663 head_top_pos.z - target_height * combat::PROJECTILE_HEADSHOT_PROPORTION,
664 );
665 if (curr_pos.z < head_bottom_pos.z && last_pos.z < head_bottom_pos.z)
666 || (curr_pos.z > head_top_pos.z && last_pos.z > head_top_pos.z)
667 {
668 None
669 } else if curr_pos.z > head_top_pos.z
670 || curr_pos.z < head_bottom_pos.z
671 || last_pos.z > head_top_pos.z
672 || last_pos.z < head_bottom_pos.z
673 {
674 let proj_top_intersection = {
675 let t = (head_top_pos.z - last_pos.z) / vel.0.z;
676 last_pos + vel.0 * t
677 };
678 let proj_bottom_intersection = {
679 let t = (head_bottom_pos.z - last_pos.z) / vel.0.z;
680 last_pos + vel.0 * t
681 };
682 let intersected_bottom = head_bottom_pos
683 .distance_squared(proj_bottom_intersection)
684 < target_radius.powi(2);
685 let intersected_top = head_top_pos.distance_squared(proj_top_intersection)
686 < target_radius.powi(2);
687 let hit_head = intersected_bottom || intersected_top;
688 let hit_from_bottom = last_pos.z < head_bottom_pos.z && intersected_bottom;
689 let hit_from_top = last_pos.z > head_top_pos.z && intersected_top;
690 if !hit_head || hit_from_bottom {
694 None
695 } else if hit_from_top {
696 Some(combat::MAX_TOP_HEADSHOT_PRECISION)
697 } else {
698 Some(combat::MAX_HEADSHOT_PRECISION)
699 }
700 } else {
701 let trajectory = LineSegment3 {
702 start: last_pos,
703 end: curr_pos,
704 };
705 let head_middle_pos = head_bottom_pos.with_z(
706 head_bottom_pos.z
707 + target_height * combat::PROJECTILE_HEADSHOT_PROPORTION * 0.5,
708 );
709 if trajectory.distance_to_point(head_middle_pos) < target_radius {
710 Some(combat::MAX_HEADSHOT_PRECISION)
711 } else {
712 None
713 }
714 }
715 };
716
717 let precision_mult = match (precision_from_flank, precision_from_head) {
718 (Some(a), Some(b)) => Some(a.max(b)),
719 (Some(a), None) | (None, Some(a)) => Some(a),
720 (None, None) => None,
721 };
722
723 let attack_options = AttackOptions {
724 target_dodging,
725 permit_pvp,
726 allow_friendly_fire,
727 target_group: projectile_target_info.target_group,
728 precision_mult,
729 };
730
731 attack.apply_attack(
732 attacker_info,
733 &target_info,
734 projectile_dir,
735 attack_options,
736 1.0,
737 AttackSource::Projectile,
738 *read_data.time,
739 emitters,
740 |o| outcomes_emitter.emit(o),
741 rng,
742 0,
743 );
744 },
745 projectile::Effect::Explode(e) => {
746 let Pos(pos) = *projectile_info.pos;
747 let owner_uid = projectile_info.owner_uid;
748 emitters.emit(ExplosionEvent {
749 pos,
750 explosion: e,
751 owner: owner_uid,
752 });
753 },
754 projectile::Effect::Arc(a) => {
755 emitters.emit(ArcingEvent {
756 arc: a,
757 owner: projectile_info.owner_uid,
758 target: projectile_target_info.uid,
759 pos: *projectile_info.pos,
760 });
761 },
762 projectile::Effect::Bonk => {
763 let Pos(pos) = *projectile_info.pos;
764 let owner_uid = projectile_info.owner_uid;
765 emitters.emit(BonkEvent {
766 pos,
767 owner: owner_uid,
768 target: Some(projectile_target_info.uid),
769 });
770 },
771 projectile::Effect::Vanish => {
772 let entity = projectile_info.entity;
773 emitters.emit(DeleteEvent(entity));
774 *projectile_vanished = true;
775 },
776 projectile::Effect::Possess => {
777 let target_uid = projectile_target_info.uid;
778 let owner_uid = projectile_info.owner_uid;
779 if let Some(owner_uid) = owner_uid
780 && target_uid != owner_uid
781 {
782 emitters.emit(PossessEvent(owner_uid, target_uid));
783 }
784 },
785 projectile::Effect::Stick => {},
786 projectile::Effect::Firework(_) => {},
787 projectile::Effect::SurpriseEgg => {
788 let Pos(pos) = *projectile_info.pos;
789 outcomes_emitter.emit(Outcome::SurpriseEgg { pos });
790 },
791 projectile::Effect::TrainingDummy => emitters.emit(CreateNpcEvent {
792 pos: *projectile_info.pos,
793 ori: Ori::default(),
794 npc: NpcBuilder::new(
795 Stats::new(
796 Content::with_attr("name-custom-village-dummy", "neut"),
797 Body::Object(object::Body::TrainingDummy),
798 ),
799 Body::Object(object::Body::TrainingDummy),
800 Alignment::Npc,
801 ),
802 }),
803 projectile::Effect::Split(_) => {},
804 }
805}
806
807fn handle_split_effect(
808 split: &SplitOptions,
809 init_projectile: &Projectile,
810 init_dir: Vec3<f32>,
811 pos: Pos,
812 rng: &mut impl Rng,
813 projectile_owner: Option<EcsEntity>,
814 body: &Body,
815 speed: f32,
816 emitters: &mut impl EmitExt<ShootEvent>,
817) {
818 let split_projectile = || {
819 let mut projectile = init_projectile.clone();
820 projectile
823 .timeout
824 .retain(|e| !matches!(e, projectile::Effect::Split(_)));
825 projectile
826 .hit_solid
827 .retain(|e| !matches!(e, projectile::Effect::Split(_)));
828 projectile.time_left = Duration::from_secs_f64(split.new_lifetime.0);
829 projectile.init_time = split.new_lifetime;
830 projectile.override_collider = split.override_collider;
831 projectile
832 };
833
834 const MAX_PROJECTILES: u32 = 100;
842 for _ in 0..u32::min(split.amount, MAX_PROJECTILES) {
843 let dir = {
844 let theta = rng.random_range(0.0..TAU);
845 let phi = rng.random_range(0.0..PI);
846 let offset = {
847 let x = theta.sin() * phi.sin();
848 let y = theta.cos() * phi.sin();
849 let z = phi.cos();
850 Vec3::new(x, y, z) * split.spread
851 };
852 Dir::from_unnormalized(init_dir + offset).unwrap_or_default()
853 };
854
855 emitters.emit(ShootEvent {
856 entity: projectile_owner,
857 source_vel: None,
858 pos,
859 dir,
860 body: *body,
861 light: None,
862 projectile: split_projectile(),
863 speed,
864 object: None,
865 marker: None, })
868 }
869}