veloren_common_systems/
projectile.rs

1use common::{
2    GroupTarget,
3    combat::{self, AttackOptions, AttackSource, AttackerInfo, TargetInfo},
4    comp::{
5        Alignment, Body, Buffs, CharacterState, Combo, Energy, Group, Health, Inventory, Mass, Ori,
6        PhysicsState, Player, Pos, Projectile, Stats, Vel,
7        agent::{Sound, SoundKind},
8        aura::EnteredAuras,
9        projectile,
10    },
11    event::{
12        BonkEvent, BuffEvent, ComboChangeEvent, DeleteEvent, EmitExt, Emitter, EnergyChangeEvent,
13        EntityAttackedHookEvent, EventBus, ExplosionEvent, HealthChangeEvent, KnockbackEvent,
14        ParryHookEvent, PoiseChangeEvent, PossessEvent, SoundEvent,
15    },
16    event_emitters,
17    outcome::Outcome,
18    resources::{DeltaTime, Time},
19    uid::{IdMaps, Uid},
20    util::Dir,
21};
22
23use common::vol::ReadVol;
24use common_ecs::{Job, Origin, Phase, System};
25use rand::Rng;
26use specs::{
27    Entities, Entity as EcsEntity, Join, Read, ReadExpect, ReadStorage, SystemData, WriteStorage,
28    shred,
29};
30use std::time::Duration;
31use vek::*;
32
33use common::terrain::TerrainGrid;
34
35event_emitters! {
36    struct Events[Emitters] {
37        sound: SoundEvent,
38        delete: DeleteEvent,
39        explosion: ExplosionEvent,
40        health_change: HealthChangeEvent,
41        energy_change: EnergyChangeEvent,
42        poise_change: PoiseChangeEvent,
43        parry_hook: ParryHookEvent,
44        knockback: KnockbackEvent,
45        entity_attack_hoow: EntityAttackedHookEvent,
46        combo_change: ComboChangeEvent,
47        buff: BuffEvent,
48        bonk: BonkEvent,
49        possess: PossessEvent,
50    }
51}
52
53#[derive(SystemData)]
54pub struct ReadData<'a> {
55    time: Read<'a, Time>,
56    entities: Entities<'a>,
57    players: ReadStorage<'a, Player>,
58    dt: Read<'a, DeltaTime>,
59    id_maps: Read<'a, IdMaps>,
60    events: Events<'a>,
61    uids: ReadStorage<'a, Uid>,
62    positions: ReadStorage<'a, Pos>,
63    alignments: ReadStorage<'a, Alignment>,
64    physics_states: ReadStorage<'a, PhysicsState>,
65    velocities: ReadStorage<'a, Vel>,
66    inventories: ReadStorage<'a, Inventory>,
67    groups: ReadStorage<'a, Group>,
68    energies: ReadStorage<'a, Energy>,
69    stats: ReadStorage<'a, Stats>,
70    combos: ReadStorage<'a, Combo>,
71    healths: ReadStorage<'a, Health>,
72    bodies: ReadStorage<'a, Body>,
73    character_states: ReadStorage<'a, CharacterState>,
74    terrain: ReadExpect<'a, TerrainGrid>,
75    buffs: ReadStorage<'a, Buffs>,
76    entered_auras: ReadStorage<'a, EnteredAuras>,
77    masses: ReadStorage<'a, Mass>,
78}
79
80/// This system is responsible for handling projectile effect triggers
81#[derive(Default)]
82pub struct Sys;
83impl<'a> System<'a> for Sys {
84    type SystemData = (
85        ReadData<'a>,
86        WriteStorage<'a, Ori>,
87        WriteStorage<'a, Projectile>,
88        Read<'a, EventBus<Outcome>>,
89    );
90
91    const NAME: &'static str = "projectile";
92    const ORIGIN: Origin = Origin::Common;
93    const PHASE: Phase = Phase::Create;
94
95    fn run(
96        _job: &mut Job<Self>,
97        (read_data, mut orientations, mut projectiles, outcomes): Self::SystemData,
98    ) {
99        let mut emitters = read_data.events.get_emitters();
100        let mut outcomes_emitter = outcomes.emitter();
101        let mut rng = rand::thread_rng();
102
103        // Attacks
104        'projectile_loop: for (entity, pos, physics, vel, projectile) in (
105            &read_data.entities,
106            &read_data.positions,
107            &read_data.physics_states,
108            &read_data.velocities,
109            &mut projectiles,
110        )
111            .join()
112        {
113            let projectile_owner = projectile
114                .owner
115                .and_then(|uid| read_data.id_maps.uid_entity(uid));
116
117            if physics.on_surface().is_none() && rng.gen_bool(0.05) {
118                emitters.emit(SoundEvent {
119                    sound: Sound::new(SoundKind::Projectile, pos.0, 4.0, read_data.time.0),
120                });
121            }
122
123            let mut projectile_vanished: bool = false;
124
125            // Hit entity
126            for (&other, &pos_hit_other) in physics.touch_entities.iter() {
127                let same_group = projectile_owner
128                    // Note: somewhat inefficient since we do the lookup for every touching
129                    // entity, but if we pull this out of the loop we would want to do it only
130                    // if there is at least one touching entity
131                    .and_then(|e| read_data.groups.get(e)).is_some_and(|owner_group|
132                        Some(owner_group) == read_data.id_maps
133                        .uid_entity(other)
134                        .and_then(|e| read_data.groups.get(e)));
135
136                // Skip if in the same group
137                let target_group = if same_group {
138                    GroupTarget::InGroup
139                } else {
140                    GroupTarget::OutOfGroup
141                };
142
143                if projectile.ignore_group
144                    && same_group
145                    && projectile
146                        .owner
147                        .and_then(|owner| {
148                            read_data
149                                .id_maps
150                                .uid_entity(owner)
151                                .zip(read_data.id_maps.uid_entity(other))
152                        })
153                        .is_none_or(|(owner, other)| {
154                            !combat::allow_friendly_fire(&read_data.entered_auras, owner, other)
155                        })
156                {
157                    continue;
158                }
159
160                if projectile.owner == Some(other) {
161                    continue;
162                }
163
164                let projectile = &mut *projectile;
165
166                let entity_of = |uid: Uid| read_data.id_maps.uid_entity(uid);
167
168                // Don't hit if there is terrain between the projectile and where the entity was
169                // supposed to be hit by it.
170
171                if physics.on_surface().is_some() {
172                    let projectile_direction = orientations
173                        .get(entity)
174                        .map_or_else(Vec3::zero, |ori| ori.look_vec());
175                    let pos_wall = pos.0 - 0.2 * projectile_direction;
176                    if !matches!(
177                        read_data
178                            .terrain
179                            .ray(pos_wall, pos_hit_other)
180                            .until(|b| b.is_filled())
181                            .cast()
182                            .1,
183                        Ok(None)
184                    ) {
185                        continue;
186                    }
187                }
188
189                for effect in projectile.hit_entity.drain(..) {
190                    let owner = projectile.owner.and_then(entity_of);
191                    let projectile_info = ProjectileInfo {
192                        entity,
193                        effect,
194                        owner_uid: projectile.owner,
195                        owner,
196                        ori: orientations.get(entity),
197                        pos,
198                        vel,
199                    };
200
201                    let target = entity_of(other);
202                    let projectile_target_info = ProjectileTargetInfo {
203                        uid: other,
204                        entity: target,
205                        target_group,
206                        ori: target.and_then(|target| orientations.get(target)),
207                    };
208
209                    dispatch_hit(
210                        projectile_info,
211                        projectile_target_info,
212                        &read_data,
213                        &mut projectile_vanished,
214                        &mut outcomes_emitter,
215                        &mut emitters,
216                        &mut rng,
217                    );
218                }
219
220                if projectile_vanished {
221                    continue 'projectile_loop;
222                }
223            }
224
225            if physics.on_surface().is_some() {
226                let projectile = &mut *projectile;
227                for effect in projectile.hit_solid.drain(..) {
228                    match effect {
229                        projectile::Effect::Explode(e) => {
230                            // We offset position a little back on the way,
231                            // so if we hit non-exploadable block
232                            // we still can affect blocks around it.
233                            //
234                            // TODO: orientation of fallen projectile is
235                            // fragile heuristic for direction, find more
236                            // robust method.
237                            let projectile_direction = orientations
238                                .get(entity)
239                                .map_or_else(Vec3::zero, |ori| ori.look_vec());
240                            let offset = -0.2 * projectile_direction;
241                            emitters.emit(ExplosionEvent {
242                                pos: pos.0 + offset,
243                                explosion: e,
244                                owner: projectile.owner,
245                            });
246                        },
247                        projectile::Effect::Vanish => {
248                            emitters.emit(DeleteEvent(entity));
249                            projectile_vanished = true;
250                        },
251                        projectile::Effect::Bonk => {
252                            emitters.emit(BonkEvent {
253                                pos: pos.0,
254                                owner: projectile.owner,
255                                target: None,
256                            });
257                        },
258                        _ => {},
259                    }
260                }
261
262                if projectile_vanished {
263                    continue 'projectile_loop;
264                }
265            } else if let Some(ori) = orientations.get_mut(entity) {
266                if let Some(dir) = Dir::from_unnormalized(vel.0) {
267                    *ori = dir.into();
268                }
269            }
270
271            if projectile.time_left == Duration::default() {
272                emitters.emit(DeleteEvent(entity));
273            }
274            projectile.time_left = projectile
275                .time_left
276                .checked_sub(Duration::from_secs_f32(read_data.dt.0))
277                .unwrap_or_default();
278        }
279    }
280}
281
282struct ProjectileInfo<'a> {
283    entity: EcsEntity,
284    effect: projectile::Effect,
285    owner_uid: Option<Uid>,
286    owner: Option<EcsEntity>,
287    ori: Option<&'a Ori>,
288    pos: &'a Pos,
289    vel: &'a Vel,
290}
291
292struct ProjectileTargetInfo<'a> {
293    uid: Uid,
294    entity: Option<EcsEntity>,
295    target_group: GroupTarget,
296    ori: Option<&'a Ori>,
297}
298
299fn dispatch_hit(
300    projectile_info: ProjectileInfo,
301    projectile_target_info: ProjectileTargetInfo,
302    read_data: &ReadData,
303    projectile_vanished: &mut bool,
304    outcomes_emitter: &mut Emitter<Outcome>,
305    emitters: &mut Emitters,
306    rng: &mut rand::rngs::ThreadRng,
307) {
308    match projectile_info.effect {
309        projectile::Effect::Attack(attack) => {
310            let target_uid = projectile_target_info.uid;
311            let target = if let Some(entity) = projectile_target_info.entity {
312                entity
313            } else {
314                return;
315            };
316
317            let (target_pos, projectile_dir) = {
318                let target_pos = read_data.positions.get(target);
319                let projectile_ori = projectile_info.ori;
320                match target_pos.zip(projectile_ori) {
321                    Some((tgt_pos, proj_ori)) => {
322                        let Pos(tgt_pos) = tgt_pos;
323                        (*tgt_pos, proj_ori.look_dir())
324                    },
325                    None => return,
326                }
327            };
328
329            let owner = projectile_info.owner;
330            let projectile_entity = projectile_info.entity;
331
332            let attacker_info =
333                owner
334                    .zip(projectile_info.owner_uid)
335                    .map(|(entity, uid)| AttackerInfo {
336                        entity,
337                        uid,
338                        group: read_data.groups.get(entity),
339                        energy: read_data.energies.get(entity),
340                        combo: read_data.combos.get(entity),
341                        inventory: read_data.inventories.get(entity),
342                        stats: read_data.stats.get(entity),
343                        mass: read_data.masses.get(entity),
344                    });
345
346            let target_info = TargetInfo {
347                entity: target,
348                uid: target_uid,
349                inventory: read_data.inventories.get(target),
350                stats: read_data.stats.get(target),
351                health: read_data.healths.get(target),
352                pos: target_pos,
353                ori: projectile_target_info.ori,
354                char_state: read_data.character_states.get(target),
355                energy: read_data.energies.get(target),
356                buffs: read_data.buffs.get(target),
357                mass: read_data.masses.get(target),
358            };
359
360            // TODO: Is it possible to have projectile without body??
361            if let Some(&body) = read_data.bodies.get(projectile_entity) {
362                outcomes_emitter.emit(Outcome::ProjectileHit {
363                    pos: target_pos,
364                    body,
365                    vel: read_data
366                        .velocities
367                        .get(projectile_entity)
368                        .map_or(Vec3::zero(), |v| v.0),
369                    source: projectile_info.owner_uid,
370                    target: read_data.uids.get(target).copied(),
371                });
372            }
373
374            let allow_friendly_fire = owner.is_some_and(|owner| {
375                combat::allow_friendly_fire(&read_data.entered_auras, owner, target)
376            });
377
378            // PvP check
379            let permit_pvp = combat::permit_pvp(
380                &read_data.alignments,
381                &read_data.players,
382                &read_data.entered_auras,
383                &read_data.id_maps,
384                owner,
385                target,
386            );
387
388            let target_dodging = read_data
389                .character_states
390                .get(target)
391                .and_then(|cs| cs.roll_attack_immunities())
392                .is_some_and(|i| i.projectiles);
393
394            let precision_from_flank = combat::precision_mult_from_flank(
395                *projectile_dir,
396                target_info.ori,
397                Default::default(),
398                false,
399            );
400
401            let precision_from_head = {
402                // This performs a cylinder and line segment intersection check. The cylinder is
403                // the upper 10% of an entity's dimensions. The line segment is from the
404                // projectile's positions on the current and previous tick.
405                let curr_pos = projectile_info.pos.0;
406                let last_pos = projectile_info.pos.0 - projectile_info.vel.0 * read_data.dt.0;
407                let vel = projectile_info.vel.0;
408                let (target_height, target_radius) = read_data
409                    .bodies
410                    .get(target)
411                    .map_or((0.0, 0.0), |b| (b.height(), b.max_radius()));
412                let head_top_pos = target_pos.with_z(target_pos.z + target_height);
413                let head_bottom_pos = head_top_pos.with_z(
414                    head_top_pos.z - target_height * combat::PROJECTILE_HEADSHOT_PROPORTION,
415                );
416                if (curr_pos.z < head_bottom_pos.z && last_pos.z < head_bottom_pos.z)
417                    || (curr_pos.z > head_top_pos.z && last_pos.z > head_top_pos.z)
418                {
419                    None
420                } else if curr_pos.z > head_top_pos.z
421                    || curr_pos.z < head_bottom_pos.z
422                    || last_pos.z > head_top_pos.z
423                    || last_pos.z < head_bottom_pos.z
424                {
425                    let proj_top_intersection = {
426                        let t = (head_top_pos.z - last_pos.z) / vel.z;
427                        last_pos + vel * t
428                    };
429                    let proj_bottom_intersection = {
430                        let t = (head_bottom_pos.z - last_pos.z) / vel.z;
431                        last_pos + vel * t
432                    };
433                    let intersected_bottom = head_bottom_pos
434                        .distance_squared(proj_bottom_intersection)
435                        < target_radius.powi(2);
436                    let intersected_top = head_top_pos.distance_squared(proj_top_intersection)
437                        < target_radius.powi(2);
438                    let hit_head = intersected_bottom || intersected_top;
439                    let hit_from_bottom = last_pos.z < head_bottom_pos.z && intersected_bottom;
440                    let hit_from_top = last_pos.z > head_top_pos.z && intersected_top;
441                    // If projectile from bottom, do not award precision damage because it trivial
442                    // to get from up close If projectile from top, reduce
443                    // precision damage to mitigate cheesing benefits
444                    if !hit_head || hit_from_bottom {
445                        None
446                    } else if hit_from_top {
447                        Some(combat::MAX_TOP_HEADSHOT_PRECISION)
448                    } else {
449                        Some(combat::MAX_HEADSHOT_PRECISION)
450                    }
451                } else {
452                    let trajectory = LineSegment3 {
453                        start: last_pos,
454                        end: curr_pos,
455                    };
456                    let head_middle_pos = head_bottom_pos.with_z(
457                        head_bottom_pos.z
458                            + target_height * combat::PROJECTILE_HEADSHOT_PROPORTION * 0.5,
459                    );
460                    if trajectory.distance_to_point(head_middle_pos) < target_radius {
461                        Some(combat::MAX_HEADSHOT_PRECISION)
462                    } else {
463                        None
464                    }
465                }
466            };
467
468            let precision_mult = match (precision_from_flank, precision_from_head) {
469                (Some(a), Some(b)) => Some(a.max(b)),
470                (Some(a), None) | (None, Some(a)) => Some(a),
471                (None, None) => None,
472            };
473
474            let attack_options = AttackOptions {
475                target_dodging,
476                permit_pvp,
477                allow_friendly_fire,
478                target_group: projectile_target_info.target_group,
479                precision_mult,
480            };
481
482            attack.apply_attack(
483                attacker_info,
484                &target_info,
485                projectile_dir,
486                attack_options,
487                1.0,
488                AttackSource::Projectile,
489                *read_data.time,
490                emitters,
491                |o| outcomes_emitter.emit(o),
492                rng,
493                0,
494            );
495        },
496        projectile::Effect::Explode(e) => {
497            let Pos(pos) = *projectile_info.pos;
498            let owner_uid = projectile_info.owner_uid;
499            emitters.emit(ExplosionEvent {
500                pos,
501                explosion: e,
502                owner: owner_uid,
503            });
504        },
505        projectile::Effect::Bonk => {
506            let Pos(pos) = *projectile_info.pos;
507            let owner_uid = projectile_info.owner_uid;
508            emitters.emit(BonkEvent {
509                pos,
510                owner: owner_uid,
511                target: Some(projectile_target_info.uid),
512            });
513        },
514        projectile::Effect::Vanish => {
515            let entity = projectile_info.entity;
516            emitters.emit(DeleteEvent(entity));
517            *projectile_vanished = true;
518        },
519        projectile::Effect::Possess => {
520            let target_uid = projectile_target_info.uid;
521            let owner_uid = projectile_info.owner_uid;
522            if let Some(owner_uid) = owner_uid {
523                if target_uid != owner_uid {
524                    emitters.emit(PossessEvent(owner_uid, target_uid));
525                }
526            }
527        },
528        projectile::Effect::Stick => {},
529    }
530}