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, 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/// This system is responsible for handling projectile effect triggers
92#[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        // Attacks
117        '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            // Hit entity
139            for (&other, &pos_hit_other) in physics.touch_entities.iter() {
140                let same_group = projectile_owner
141                    // Note: somewhat inefficient since we do the lookup for every touching
142                    // entity, but if we pull this out of the loop we would want to do it only
143                    // if there is at least one touching entity
144                    .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                // Skip if in the same group
150                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                // Skip if projectile has already hit this entity
178                if projectile.hit_entities.contains(&other) {
179                    continue;
180                }
181
182                // Skip if another projectile from the attacker has already hit this entity, and
183                // if this projectile limits the number of hits per ability
184                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                // Don't hit if there is terrain between the projectile and where the entity was
199                // supposed to be hit by it.
200
201                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                // This hit entities list prevents a single projectile from hitting the same
220                // target multiple times
221                projectile.hit_entities.push(other);
222
223                // This hit entities list prevents multiple projectiles fired from the same
224                // attacker hitting the same target multiple times (if the ability calls for
225                // this behavior)
226                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                            // We offset position a little back on the way,
284                            // so if we hit non-exploadable block
285                            // we still can affect blocks around it.
286                            //
287                            // TODO: orientation of fallen projectile is
288                            // fragile heuristic for direction, find more
289                            // robust method.
290                            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                            // If the projectile splits, the original projectile should vanish
351                            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                    // We want the homing to be weaker when projectile first fired
388                    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                // Check this to avoid needless cloning as we need to clone projectile in some
403                // timeout events
404                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                                    // Note that if the expected fireworks per firework is > 1, this
414                                    // will eventually cause
415                                    // enough server lag that more players can't log in.
416                                    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        // Hit entities lists
533        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            // TODO: Is it possible to have projectile without body??
622            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            // PvP check
637            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                // This performs a cylinder and line segment intersection check.
661                //
662                // The cylinder is the upper N% of an entity's dimensions.
663                // The line segment is from the projectile's positions on the
664                // current and previous tick.
665                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 projectile from bottom, do not award precision damage
702                    // because it trivial to get from up close.
703                    //
704                    // If projectile from top, reduce precision damage to
705                    // mitigate cheesing benefits.
706                    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        // Remove split from effects here to avoid projectile infinitely
840        // splitting
841        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    /// Do not allow too many projectiles to be generated. Otherwise, some
854    /// weapons (such as the admin bow) can produce tens of thousands of
855    /// projectiles and halt the server/client. The limit may need to be tweaked
856    /// in the future and was chosen purely for keeping game performance in
857    /// check. The number itself is somewhat arbitrary and no game
858    /// design/balancing decisions should be oriented around it, so please feel
859    /// free to increase or decrease as needed.
860    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, /* TODO: Do we check original for
885                           * projectile's marker? */
886        })
887    }
888}