veloren_common_systems/
projectile.rs

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/// This system is responsible for handling projectile effect triggers
91#[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        // Attacks
116        '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            // Hit entity
138            for (&other, &pos_hit_other) in physics.touch_entities.iter() {
139                let same_group = projectile_owner
140                    // Note: somewhat inefficient since we do the lookup for every touching
141                    // entity, but if we pull this out of the loop we would want to do it only
142                    // if there is at least one touching entity
143                    .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                // Skip if in the same group
149                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                // Skip if projectile has already hit this entity
177                if projectile.hit_entities.contains(&other) {
178                    continue;
179                }
180
181                // Skip if another projectile from the attacker has already hit this entity, and
182                // if this projectile limits the number of hits per ability
183                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                // Don't hit if there is terrain between the projectile and where the entity was
198                // supposed to be hit by it.
199
200                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                // This hit entities list prevents a single projectile from hitting the same
219                // target multiple times
220                projectile.hit_entities.push(other);
221
222                // This hit entities list prevents multiple projectiles fired from the same
223                // attacker hitting the same target multiple times (if the ability calls for
224                // this behavior)
225                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                            // We offset position a little back on the way,
283                            // so if we hit non-exploadable block
284                            // we still can affect blocks around it.
285                            //
286                            // TODO: orientation of fallen projectile is
287                            // fragile heuristic for direction, find more
288                            // robust method.
289                            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                            // If the projectile splits, the original projectile should vanish
350                            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                    // We want the homing to be weaker when projectile first fired
379                    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                // Check this to avoid needless cloning as we need to clone projectile in some
394                // timeout events
395                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                                    // Note that if the expected fireworks per firework is > 1, this
405                                    // will eventually cause
406                                    // enough server lag that more players can't log in.
407                                    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        // Hit entities lists
524        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            // TODO: Is it possible to have projectile without body??
613            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            // PvP check
628            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                // This performs a cylinder and line segment intersection check.
652                //
653                // The cylinder is the upper N% of an entity's dimensions.
654                // The line segment is from the projectile's positions on the
655                // current and previous tick.
656                let curr_pos = projectile_info.pos.0;
657                let last_pos = projectile_info.pos.0 - projectile_info.vel.0 * read_data.dt.0;
658                let vel = projectile_info.vel;
659                let (target_height, head_ratio, target_radius) =
660                    read_data.bodies.get(target).map_or((0.0, 0.0, 0.0), |b| {
661                        (b.height(), b.top_ratio(), b.max_radius())
662                    });
663
664                let head_top_pos = target_pos.with_z(target_pos.z + target_height);
665                let head_bottom_pos =
666                    head_top_pos.with_z(head_top_pos.z - target_height * head_ratio);
667                if (curr_pos.z < head_bottom_pos.z && last_pos.z < head_bottom_pos.z)
668                    || (curr_pos.z > head_top_pos.z && last_pos.z > head_top_pos.z)
669                {
670                    None
671                } else if curr_pos.z > head_top_pos.z
672                    || curr_pos.z < head_bottom_pos.z
673                    || last_pos.z > head_top_pos.z
674                    || last_pos.z < head_bottom_pos.z
675                {
676                    let proj_top_intersection = {
677                        let t = (head_top_pos.z - last_pos.z) / vel.0.z;
678                        last_pos + vel.0 * t
679                    };
680                    let proj_bottom_intersection = {
681                        let t = (head_bottom_pos.z - last_pos.z) / vel.0.z;
682                        last_pos + vel.0 * t
683                    };
684                    let intersected_bottom = head_bottom_pos
685                        .distance_squared(proj_bottom_intersection)
686                        < target_radius.powi(2);
687                    let intersected_top = head_top_pos.distance_squared(proj_top_intersection)
688                        < target_radius.powi(2);
689                    let hit_head = intersected_bottom || intersected_top;
690                    let hit_from_bottom = last_pos.z < head_bottom_pos.z && intersected_bottom;
691                    let hit_from_top = last_pos.z > head_top_pos.z && intersected_top;
692                    // If projectile from bottom, do not award precision damage
693                    // because it trivial to get from up close.
694                    //
695                    // If projectile from top, reduce precision damage to
696                    // mitigate cheesing benefits.
697                    if !hit_head || hit_from_bottom {
698                        None
699                    } else if hit_from_top {
700                        Some(combat::MAX_TOP_HEADSHOT_PRECISION)
701                    } else {
702                        Some(combat::MAX_HEADSHOT_PRECISION)
703                    }
704                } else {
705                    let trajectory = LineSegment3 {
706                        start: last_pos,
707                        end: curr_pos,
708                    };
709                    let head_middle_pos = head_bottom_pos
710                        .with_z(head_bottom_pos.z + target_height * head_ratio * 0.5);
711                    if trajectory.distance_to_point(head_middle_pos) < target_radius {
712                        Some(combat::MAX_HEADSHOT_PRECISION)
713                    } else {
714                        None
715                    }
716                }
717            };
718
719            let precision_mult = match (precision_from_flank, precision_from_head) {
720                (Some(a), Some(b)) => Some(a.max(b)),
721                (Some(a), None) | (None, Some(a)) => Some(a),
722                (None, None) => None,
723            };
724
725            let attack_options = AttackOptions {
726                target_dodging,
727                permit_pvp,
728                allow_friendly_fire,
729                target_group: projectile_target_info.target_group,
730                precision_mult,
731            };
732
733            attack.apply_attack(
734                attacker_info,
735                &target_info,
736                projectile_dir,
737                attack_options,
738                1.0,
739                AttackSource::Projectile,
740                *read_data.time,
741                emitters,
742                |o| outcomes_emitter.emit(o),
743                rng,
744                0,
745            );
746        },
747        projectile::Effect::Explode(e) => {
748            let Pos(pos) = *projectile_info.pos;
749            let owner_uid = projectile_info.owner_uid;
750            emitters.emit(ExplosionEvent {
751                pos,
752                explosion: e,
753                owner: owner_uid,
754            });
755        },
756        projectile::Effect::Arc(a) => {
757            emitters.emit(ArcingEvent {
758                arc: a,
759                owner: projectile_info.owner_uid,
760                target: projectile_target_info.uid,
761                pos: *projectile_info.pos,
762            });
763        },
764        projectile::Effect::Bonk => {
765            let Pos(pos) = *projectile_info.pos;
766            let owner_uid = projectile_info.owner_uid;
767            emitters.emit(BonkEvent {
768                pos,
769                owner: owner_uid,
770                target: Some(projectile_target_info.uid),
771            });
772        },
773        projectile::Effect::Vanish => {
774            let entity = projectile_info.entity;
775            emitters.emit(DeleteEvent(entity));
776            *projectile_vanished = true;
777        },
778        projectile::Effect::Possess => {
779            let target_uid = projectile_target_info.uid;
780            let owner_uid = projectile_info.owner_uid;
781            if let Some(owner_uid) = owner_uid
782                && target_uid != owner_uid
783            {
784                emitters.emit(PossessEvent(owner_uid, target_uid));
785            }
786        },
787        projectile::Effect::Stick => {},
788        projectile::Effect::Firework(_) => {},
789        projectile::Effect::SurpriseEgg => {
790            let Pos(pos) = *projectile_info.pos;
791            outcomes_emitter.emit(Outcome::SurpriseEgg { pos });
792        },
793        projectile::Effect::TrainingDummy => emitters.emit(CreateNpcEvent {
794            pos: *projectile_info.pos,
795            ori: Ori::default(),
796            npc: NpcBuilder::new(
797                Stats::new(
798                    Content::with_attr("name-custom-village-dummy", "neut"),
799                    Body::Object(object::Body::TrainingDummy),
800                ),
801                Body::Object(object::Body::TrainingDummy),
802                Alignment::Npc,
803            ),
804        }),
805        projectile::Effect::Split(_) => {},
806    }
807}
808
809fn handle_split_effect(
810    split: &SplitOptions,
811    init_projectile: &Projectile,
812    init_dir: Vec3<f32>,
813    pos: Pos,
814    rng: &mut impl Rng,
815    projectile_owner: Option<EcsEntity>,
816    body: &Body,
817    speed: f32,
818    emitters: &mut impl EmitExt<ShootEvent>,
819) {
820    let split_projectile = || {
821        let mut projectile = init_projectile.clone();
822        // Remove split from effects here to avoid projectile infinitely
823        // splitting
824        projectile
825            .timeout
826            .retain(|e| !matches!(e, projectile::Effect::Split(_)));
827        projectile
828            .hit_solid
829            .retain(|e| !matches!(e, projectile::Effect::Split(_)));
830        projectile.time_left = Duration::from_secs_f64(split.new_lifetime.0);
831        projectile.init_time = split.new_lifetime;
832        projectile.override_collider = split.override_collider;
833        projectile
834    };
835
836    /// Do not allow too many projectiles to be generated. Otherwise, some
837    /// weapons (such as the admin bow) can produce tens of thousands of
838    /// projectiles and halt the server/client. The limit may need to be tweaked
839    /// in the future and was chosen purely for keeping game performance in
840    /// check. The number itself is somewhat arbitrary and no game
841    /// design/balancing decisions should be oriented around it, so please feel
842    /// free to increase or decrease as needed.
843    const MAX_PROJECTILES: u32 = 100;
844    for _ in 0..u32::min(split.amount, MAX_PROJECTILES) {
845        let dir = {
846            let theta = rng.random_range(0.0..TAU);
847            let phi = rng.random_range(0.0..PI);
848            let offset = {
849                let x = theta.sin() * phi.sin();
850                let y = theta.cos() * phi.sin();
851                let z = phi.cos();
852                Vec3::new(x, y, z) * split.spread
853            };
854            Dir::from_unnormalized(init_dir + offset).unwrap_or_default()
855        };
856
857        emitters.emit(ShootEvent {
858            entity: projectile_owner,
859            source_vel: None,
860            pos,
861            dir,
862            body: *body,
863            light: None,
864            projectile: split_projectile(),
865            speed,
866            object: None,
867            marker: None, /* TODO: Do we check original for
868                           * projectile's marker? */
869        })
870    }
871}