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