Skip to main content

veloren_common/comp/
projectile.rs

1use crate::{
2    combat::{
3        Attack, AttackDamage, AttackEffect, CombatBuff, CombatEffect, CombatRequirement, Damage,
4        DamageKind, GroupTarget, Knockback, KnockbackDir,
5    },
6    comp::{
7        ArcProperties, CapsulePrism, FrontendMarker, Stats,
8        ability::Dodgeable,
9        item::{Reagent, tool},
10        pool::PoolProperties,
11    },
12    consts::GRAVITY,
13    explosion::{ColorPreset, Explosion, RadiusEffect},
14    resources::{Secs, Time},
15    states::utils::AbilityInfo,
16    uid::Uid,
17    util::Dir,
18};
19use common_base::dev_panic;
20use serde::{Deserialize, Serialize};
21use specs::Component;
22use std::time::Duration;
23use vek::*;
24
25#[derive(Clone, Debug, Serialize, Deserialize)]
26pub enum Effect {
27    Attack(Attack),
28    Explode(Explosion),
29    Vanish,
30    Possess,
31    Bonk, // Knock/dislodge/change objects on hit
32    Firework(Reagent),
33    SurpriseEgg,
34    TrainingDummy,
35    Arc(ArcProperties),
36    Split(SplitOptions),
37    Pool(PoolProperties),
38}
39
40#[derive(Clone, Debug)]
41pub struct Projectile {
42    // TODO: use SmallVec for these effects
43    pub hit_solid: Vec<Effect>,
44    pub hit_entity: Vec<Effect>,
45    pub timeout: Vec<Effect>,
46    /// Time left until the projectile will despawn
47    pub time_left: Duration,
48    /// Max duration of projectile (should be equal to time_left when projectile
49    /// is created)
50    pub init_time: Secs,
51    pub owner: Option<Uid>,
52    /// Whether projectile collides with entities in the same group as its
53    /// owner
54    pub ignore_group: bool,
55    /// Whether the projectile is sticky
56    pub is_sticky: bool,
57    /// Whether the projectile should use a point collider
58    pub is_point: bool,
59    /// Whether the projectile should home towards a target entity and at what
60    /// rate (in deg/s)
61    pub homing: Option<(Uid, f32)>,
62    /// Whether the projectile should hit and apply its effects to multiple
63    /// entities
64    pub pierce_entities: bool,
65    /// Entities that the projectile has hit (only relevant for projectiles that
66    /// can pierce entities)
67    pub hit_entities: Vec<Uid>,
68    /// Whether to limit the number of projectiles from from an ability can
69    /// damage the target in a short duration
70    pub limit_per_ability: bool,
71    /// Whether to override the collider used by the projectile for detecting
72    /// hit entities
73    pub override_collider: Option<CapsulePrism>,
74}
75
76impl Component for Projectile {
77    type Storage = specs::DenseVecStorage<Self>;
78}
79
80impl Projectile {
81    pub fn is_blockable(&self) -> bool {
82        !self.hit_entity.iter().any(|effect| {
83            matches!(
84                effect,
85                Effect::Attack(Attack {
86                    blockable: false,
87                    ..
88                })
89            )
90        })
91    }
92}
93
94#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
95#[serde(deny_unknown_fields)]
96pub struct ProjectileConstructor {
97    pub kind: ProjectileConstructorKind,
98    pub attack: Option<ProjectileAttack>,
99    pub scaled: Option<Scaled>,
100    /// In degrees per second
101    pub homing_rate: Option<f32>,
102    pub split: Option<SplitOptions>,
103    pub lifetime_override: Option<Secs>,
104    #[serde(default)]
105    pub limit_per_ability: bool,
106    pub override_collider: Option<CapsulePrism>,
107    #[serde(default)]
108    pub pierce_entities: bool,
109    #[serde(default = "default_true")]
110    pub is_point: bool,
111    #[serde(default = "default_true")]
112    pub is_sticky: bool,
113    #[serde(default)]
114    pub hazard: bool,
115}
116
117#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)]
118#[serde(deny_unknown_fields)]
119pub struct SplitOptions {
120    pub split_on_terrain: bool,
121    pub amount: u32,
122    pub spread: f32,
123    pub new_lifetime: Secs,
124    /// If this is used, it will only apply to projectiles created after the
125    /// split, and will also override this option on the parent projectile
126    pub override_collider: Option<CapsulePrism>,
127}
128
129#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)]
130#[serde(deny_unknown_fields)]
131pub struct Scaled {
132    damage: f32,
133    poise: Option<f32>,
134    knockback: Option<f32>,
135    energy: Option<f32>,
136    damage_effect: Option<f32>,
137}
138
139fn default_true() -> bool { true }
140
141#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
142#[serde(deny_unknown_fields)]
143pub struct ProjectileAttack {
144    pub damage: f32,
145    pub poise: Option<f32>,
146    pub knockback: Option<f32>,
147    pub energy: Option<f32>,
148    pub buff: Option<CombatBuff>,
149    #[serde(default)]
150    pub friendly_fire: bool,
151    #[serde(default = "default_true")]
152    pub blockable: bool,
153    pub damage_effect: Option<CombatEffect>,
154    pub attack_effect: Option<(CombatEffect, CombatRequirement)>,
155    #[serde(default)]
156    pub without_combo: bool,
157    pub damage_kind: DamageKind,
158}
159
160#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)]
161pub struct ProjectileArcingProperties {
162    pub distance: f32,
163    pub arcs: u32,
164    pub min_delay: Secs,
165    pub max_delay: Secs,
166    #[serde(default)]
167    pub targets_owner: bool,
168}
169
170#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
171pub enum ProjectileConstructorEffectKind {
172    AttackEffect(AttackEffect),
173    ConvertKindToArcing(ProjectileArcingProperties),
174    Marker(FrontendMarker),
175}
176
177#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
178pub struct ProjectileConstructorEffect {
179    pub kind: ProjectileConstructorEffectKind,
180    pub tool_filter: Option<tool::ToolKind>,
181}
182
183fn default_both() -> ProjectileExplosionTarget { ProjectileExplosionTarget::Both }
184
185#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)]
186#[serde(deny_unknown_fields)]
187pub enum ProjectileConstructorKind {
188    // I want a better name for 'Pointed' and 'Blunt'
189    Simple,
190    Explosive {
191        radius: f32,
192        min_falloff: f32,
193        reagent: Option<Reagent>,
194        terrain: Option<(f32, ColorPreset)>,
195        #[serde(default = "default_both")]
196        target: ProjectileExplosionTarget,
197    },
198    Arcing(ProjectileArcingProperties),
199    Possess,
200    Firework(Reagent),
201    SurpriseEgg,
202    TrainingDummy,
203    Pool {
204        radius: f32,
205        tick_dur: Secs,
206        duration: Secs,
207        #[serde(default)]
208        dodgeable: Dodgeable,
209    },
210}
211
212impl ProjectileConstructor {
213    pub fn create_projectile(
214        self,
215        owner: Option<Uid>,
216        precision_mult: f32,
217        ability_info: Option<AbilityInfo>,
218        attacker_stats: Option<&Stats>,
219    ) -> (Projectile, Option<FrontendMarker>) {
220        if self.scaled.is_some() {
221            dev_panic!(
222                "Attempted to create a projectile that had a provided scaled value without \
223                 scaling the projectile."
224            )
225        }
226
227        let instance = rand::random();
228        let marker = None;
229        let attack = self.attack.map(|a| {
230            let target = if a.friendly_fire {
231                Some(GroupTarget::All)
232            } else {
233                Some(GroupTarget::OutOfGroup)
234            };
235
236            let poise = a.poise.map(|poise| {
237                AttackEffect::new(target, CombatEffect::Poise(poise))
238                    .with_requirement(CombatRequirement::AnyDamage)
239            });
240
241            let knockback = a.knockback.map(|kb| {
242                AttackEffect::new(
243                    target,
244                    CombatEffect::Knockback(Knockback {
245                        strength: kb,
246                        direction: KnockbackDir::Away,
247                    }),
248                )
249                .with_requirement(CombatRequirement::AnyDamage)
250            });
251
252            let energy = a.energy.map(|energy| {
253                AttackEffect::new(None, CombatEffect::EnergyReward(energy))
254                    .with_requirement(CombatRequirement::AnyDamage)
255            });
256
257            let buff = a.buff.map(CombatEffect::Buff);
258
259            let mut damage = AttackDamage::new(
260                Damage {
261                    kind: a.damage_kind,
262                    value: a.damage,
263                },
264                target,
265                instance,
266            );
267
268            if let Some(buff) = buff {
269                damage = damage.with_effect(buff);
270            }
271
272            if let Some(damage_effect) = a.damage_effect {
273                damage = damage.with_effect(damage_effect);
274            }
275
276            let mut attack = Attack::new(ability_info)
277                .with_damage(damage)
278                .with_precision(
279                    precision_mult
280                        * ability_info
281                            .and_then(|ai| ai.ability_meta.precision_power_mult)
282                            .unwrap_or(1.0),
283                )
284                .with_blockable(a.blockable);
285
286            if !a.without_combo {
287                attack = attack.with_combo_increment();
288            }
289
290            if let Some(poise) = poise {
291                attack = attack.with_effect(poise);
292            }
293
294            if let Some(knockback) = knockback {
295                attack = attack.with_effect(knockback);
296            }
297
298            if let Some(energy) = energy {
299                attack = attack.with_effect(energy);
300            }
301
302            if let Some((effect, requirement)) = a.attack_effect {
303                let effect = AttackEffect::new(Some(GroupTarget::OutOfGroup), effect)
304                    .with_requirement(requirement);
305                attack = attack.with_effect(effect);
306            }
307
308            attack
309        });
310
311        let (proj_kind, attack, marker) = {
312            let mut proj_kind = self.kind;
313            let mut attack = attack;
314            let mut marker = marker;
315
316            for effect in attacker_stats
317                .iter()
318                .flat_map(|s| s.projectile_constructor_effects.iter())
319            {
320                if effect
321                    .tool_filter
322                    .is_none_or(|tk| Some(tk) == ability_info.and_then(|ai| ai.tool))
323                {
324                    match &effect.kind {
325                        ProjectileConstructorEffectKind::ConvertKindToArcing(arc) => {
326                            proj_kind = ProjectileConstructorKind::Arcing(*arc);
327                        },
328                        ProjectileConstructorEffectKind::AttackEffect(effect) => {
329                            attack = attack.map(|a| a.with_effect(effect.clone()));
330                        },
331                        ProjectileConstructorEffectKind::Marker(mark) => {
332                            marker = Some(*mark);
333                        },
334                    }
335                }
336            }
337
338            (proj_kind, attack, marker)
339        };
340
341        let homing = ability_info
342            .and_then(|a| a.input_attr)
343            .and_then(|i| i.target_entity)
344            .zip(self.homing_rate);
345
346        let mut timeout = Vec::new();
347        let mut hit_solid = Vec::new();
348
349        if let Some(split) = self.split {
350            timeout.push(Effect::Split(split));
351            if split.split_on_terrain {
352                hit_solid.push(Effect::Split(split));
353            }
354        }
355
356        let default_lifetime = Secs(match proj_kind {
357            ProjectileConstructorKind::Firework(_) => 3.0,
358            _ => 15.0,
359        });
360
361        let lifetime = self.lifetime_override.unwrap_or(default_lifetime);
362
363        let projectile = match proj_kind {
364            ProjectileConstructorKind::Simple => {
365                hit_solid.push(Effect::Bonk);
366
367                let mut hit_entity = Vec::new();
368
369                if !self.pierce_entities {
370                    hit_entity.push(Effect::Vanish);
371                }
372
373                if let Some(attack) = attack {
374                    hit_entity.push(Effect::Attack(attack));
375                }
376
377                Projectile {
378                    hit_solid,
379                    hit_entity,
380                    timeout,
381                    time_left: Duration::from_secs_f64(lifetime.0),
382                    init_time: lifetime,
383                    owner,
384                    ignore_group: true,
385                    is_sticky: self.is_sticky,
386                    is_point: self.is_point,
387                    homing,
388                    pierce_entities: self.pierce_entities,
389                    hit_entities: Vec::new(),
390                    limit_per_ability: self.limit_per_ability,
391                    override_collider: self.override_collider,
392                }
393            },
394            ProjectileConstructorKind::Explosive {
395                radius,
396                min_falloff,
397                reagent,
398                terrain,
399                target,
400            } => {
401                let mut hit_entity = Vec::new();
402
403                let terrain =
404                    terrain.map(|(pow, col)| RadiusEffect::TerrainDestruction(pow, col.to_rgb()));
405
406                let mut effects = Vec::new();
407
408                if let Some(attack) = attack {
409                    if matches!(target, ProjectileExplosionTarget::SolidOnlyEntityAttack) {
410                        hit_entity.push(Effect::Attack(attack.clone()));
411                    }
412                    effects.push(RadiusEffect::Attack {
413                        attack,
414                        dodgeable: Dodgeable::Roll,
415                    });
416                }
417
418                if let Some(terrain) = terrain {
419                    effects.push(terrain);
420                }
421
422                let explosion = Explosion {
423                    effects,
424                    radius,
425                    reagent,
426                    min_falloff,
427                };
428
429                match target {
430                    ProjectileExplosionTarget::EntityOnly => {
431                        hit_entity.push(Effect::Explode(explosion));
432                    },
433                    ProjectileExplosionTarget::SolidOnly
434                    | ProjectileExplosionTarget::SolidOnlyEntityAttack => {
435                        hit_solid.push(Effect::Explode(explosion));
436                    },
437                    ProjectileExplosionTarget::Both => {
438                        hit_entity.push(Effect::Explode(explosion.clone()));
439                        hit_solid.push(Effect::Explode(explosion));
440                    },
441                }
442
443                if !self.hazard {
444                    hit_solid.push(Effect::Vanish);
445                }
446                hit_entity.push(Effect::Vanish);
447
448                Projectile {
449                    hit_solid,
450                    hit_entity,
451                    timeout,
452                    time_left: Duration::from_secs_f64(lifetime.0),
453                    init_time: lifetime,
454                    owner,
455                    ignore_group: true,
456                    is_sticky: self.is_sticky,
457                    is_point: self.is_point,
458                    homing,
459                    pierce_entities: self.pierce_entities,
460                    hit_entities: Vec::new(),
461                    limit_per_ability: self.limit_per_ability,
462                    override_collider: self.override_collider,
463                }
464            },
465            ProjectileConstructorKind::Arcing(ProjectileArcingProperties {
466                distance,
467                arcs,
468                min_delay,
469                max_delay,
470                targets_owner,
471            }) => {
472                let mut hit_entity = vec![Effect::Vanish];
473
474                if let Some(attack) = attack {
475                    hit_entity.push(Effect::Attack(attack.clone()));
476
477                    let arc = ArcProperties {
478                        attack,
479                        distance,
480                        arcs,
481                        min_delay,
482                        max_delay,
483                        targets_owner,
484                    };
485
486                    hit_entity.push(Effect::Arc(arc));
487                }
488
489                Projectile {
490                    hit_solid,
491                    hit_entity,
492                    timeout,
493                    time_left: Duration::from_secs_f64(lifetime.0),
494                    init_time: lifetime,
495                    owner,
496                    ignore_group: true,
497                    is_sticky: self.is_sticky,
498                    is_point: self.is_point,
499                    homing,
500                    pierce_entities: self.pierce_entities,
501                    hit_entities: Vec::new(),
502                    limit_per_ability: self.limit_per_ability,
503                    override_collider: self.override_collider,
504                }
505            },
506            ProjectileConstructorKind::Possess => Projectile {
507                hit_solid,
508                hit_entity: vec![Effect::Possess],
509                timeout,
510                time_left: Duration::from_secs_f64(lifetime.0),
511                init_time: lifetime,
512                owner,
513                ignore_group: false,
514                is_sticky: self.is_sticky,
515                is_point: self.is_point,
516                homing,
517                pierce_entities: self.pierce_entities,
518                hit_entities: Vec::new(),
519                limit_per_ability: self.limit_per_ability,
520                override_collider: self.override_collider,
521            },
522            ProjectileConstructorKind::Firework(reagent) => {
523                timeout.push(Effect::Firework(reagent));
524
525                Projectile {
526                    hit_solid,
527                    hit_entity: Vec::new(),
528                    timeout,
529                    time_left: Duration::from_secs_f64(lifetime.0),
530                    init_time: lifetime,
531                    owner,
532                    ignore_group: true,
533                    is_sticky: self.is_sticky,
534                    is_point: self.is_point,
535                    homing,
536                    pierce_entities: self.pierce_entities,
537                    hit_entities: Vec::new(),
538                    limit_per_ability: self.limit_per_ability,
539                    override_collider: self.override_collider,
540                }
541            },
542            ProjectileConstructorKind::SurpriseEgg => {
543                hit_solid.push(Effect::SurpriseEgg);
544                hit_solid.push(Effect::Vanish);
545
546                Projectile {
547                    hit_solid,
548                    hit_entity: vec![Effect::SurpriseEgg, Effect::Vanish],
549                    timeout,
550                    time_left: Duration::from_secs_f64(lifetime.0),
551                    init_time: lifetime,
552                    owner,
553                    ignore_group: true,
554                    is_sticky: self.is_sticky,
555                    is_point: self.is_point,
556                    homing,
557                    pierce_entities: self.pierce_entities,
558                    hit_entities: Vec::new(),
559                    limit_per_ability: self.limit_per_ability,
560                    override_collider: self.override_collider,
561                }
562            },
563            ProjectileConstructorKind::Pool {
564                radius,
565                tick_dur,
566                duration,
567                dodgeable,
568            } => {
569                let pool_props = attack.map(|atk| PoolProperties {
570                    attack: atk,
571                    radius,
572                    tick_dur,
573                    duration,
574                    dodgeable,
575                });
576
577                let lifetime = self.lifetime_override.unwrap_or(Secs(10.0));
578
579                let mut hit_entity = vec![Effect::Vanish];
580
581                if let Some(props) = pool_props {
582                    hit_solid.push(Effect::Pool(props.clone()));
583                    hit_solid.push(Effect::Vanish);
584                    hit_entity.push(Effect::Pool(props));
585                }
586
587                Projectile {
588                    hit_solid,
589                    hit_entity,
590                    timeout,
591                    time_left: Duration::from_secs_f64(lifetime.0),
592                    init_time: lifetime,
593                    owner,
594                    ignore_group: true,
595                    is_sticky: true,
596                    is_point: true,
597                    homing,
598                    pierce_entities: false,
599                    hit_entities: Vec::new(),
600                    limit_per_ability: self.limit_per_ability,
601                    override_collider: self.override_collider,
602                }
603            },
604            ProjectileConstructorKind::TrainingDummy => {
605                hit_solid.push(Effect::TrainingDummy);
606                hit_solid.push(Effect::Vanish);
607
608                timeout.push(Effect::TrainingDummy);
609
610                Projectile {
611                    hit_solid,
612                    hit_entity: vec![Effect::TrainingDummy, Effect::Vanish],
613                    timeout,
614                    time_left: Duration::from_secs_f64(lifetime.0),
615                    init_time: lifetime,
616                    owner,
617                    ignore_group: true,
618                    is_sticky: self.is_sticky,
619                    is_point: self.is_point,
620                    homing,
621                    pierce_entities: self.pierce_entities,
622                    hit_entities: Vec::new(),
623                    limit_per_ability: self.limit_per_ability,
624                    override_collider: self.override_collider,
625                }
626            },
627        };
628        (projectile, marker)
629    }
630
631    pub fn handle_scaling(mut self, scaling: f32) -> Self {
632        let scale_values = |a, b| a + b * scaling;
633
634        if let Some(scaled) = self.scaled {
635            if let Some(ref mut attack) = self.attack {
636                attack.damage = scale_values(attack.damage, scaled.damage);
637                if let Some(s_poise) = scaled.poise {
638                    attack.poise = Some(scale_values(attack.poise.unwrap_or(0.0), s_poise));
639                }
640                if let Some(s_kb) = scaled.knockback {
641                    attack.knockback = Some(scale_values(attack.knockback.unwrap_or(0.0), s_kb));
642                }
643                if let Some(s_energy) = scaled.energy {
644                    attack.energy = Some(scale_values(attack.energy.unwrap_or(0.0), s_energy));
645                }
646                if let Some(s_dmg_eff) = scaled.damage_effect {
647                    if attack.damage_effect.is_some() {
648                        attack.damage_effect =
649                            attack.damage_effect.as_ref().cloned().map(|dmg_eff| {
650                                dmg_eff.apply_multiplier(scale_values(1.0, s_dmg_eff))
651                            });
652                    } else {
653                        dev_panic!(
654                            "Attempted to scale damage effect on a projectile that doesn't have a \
655                             damage effect."
656                        )
657                    }
658                }
659            } else {
660                dev_panic!("Attempted to scale on a projectile that has no attack to scale.")
661            }
662        } else {
663            dev_panic!("Attempted to scale on a projectile that has no provided scaling value.")
664        }
665
666        self.scaled = None;
667
668        self
669    }
670
671    pub fn adjusted_by_stats(mut self, stats: tool::Stats) -> Self {
672        self.attack = self.attack.map(|mut a| {
673            a.damage *= stats.power;
674            a.poise = a.poise.map(|poise| poise * stats.effect_power);
675            a.knockback = a.knockback.map(|kb| kb * stats.effect_power);
676            a.buff = a.buff.map(|mut b| {
677                b.strength *= stats.buff_strength;
678                b
679            });
680            a.damage_effect = a.damage_effect.map(|de| de.adjusted_by_stats(stats));
681            a.attack_effect = a
682                .attack_effect
683                .map(|(e, r)| (e.adjusted_by_stats(stats), r));
684            a
685        });
686
687        self.scaled = self.scaled.map(|mut s| {
688            s.damage *= stats.power;
689            s.poise = s.poise.map(|poise| poise * stats.effect_power);
690            s.knockback = s.knockback.map(|kb| kb * stats.effect_power);
691            s
692        });
693
694        match self.kind {
695            ProjectileConstructorKind::Simple
696            | ProjectileConstructorKind::Possess
697            | ProjectileConstructorKind::Firework(_)
698            | ProjectileConstructorKind::SurpriseEgg
699            | ProjectileConstructorKind::TrainingDummy => {},
700            ProjectileConstructorKind::Explosive { ref mut radius, .. }
701            | ProjectileConstructorKind::Pool { ref mut radius, .. } => {
702                *radius *= stats.range;
703            },
704            ProjectileConstructorKind::Arcing(ProjectileArcingProperties {
705                ref mut distance,
706                ..
707            }) => {
708                *distance *= stats.range;
709            },
710        }
711
712        self.split = self.split.map(|mut s| {
713            s.amount = (s.amount as f32 * stats.effect_power).ceil().max(0.0) as u32;
714            s
715        });
716
717        self
718    }
719
720    // Remove this function after skill tree overhaul completed for bow and fire
721    // staff
722    pub fn legacy_modified_by_skills(
723        mut self,
724        power: f32,
725        regen: f32,
726        range: f32,
727        kb: f32,
728    ) -> Self {
729        self.attack = self.attack.map(|mut a| {
730            a.damage *= power;
731            a.knockback = a.knockback.map(|k| k * kb);
732            a.energy = a.energy.map(|e| e * regen);
733            a
734        });
735        self.scaled = self.scaled.map(|mut s| {
736            s.damage *= power;
737            s.knockback = s.knockback.map(|k| k * kb);
738            s.energy = s.energy.map(|e| e * regen);
739            s
740        });
741        if let ProjectileConstructorKind::Explosive { ref mut radius, .. } = self.kind {
742            *radius *= range;
743        }
744        self
745    }
746
747    pub fn is_explosive(&self) -> bool {
748        match self.kind {
749            ProjectileConstructorKind::Simple
750            | ProjectileConstructorKind::Possess
751            | ProjectileConstructorKind::Firework(_)
752            | ProjectileConstructorKind::SurpriseEgg
753            | ProjectileConstructorKind::TrainingDummy
754            | ProjectileConstructorKind::Arcing(_)
755            | ProjectileConstructorKind::Pool { .. } => false,
756            ProjectileConstructorKind::Explosive { .. } => true,
757        }
758    }
759
760    pub fn agent_aim_z_offset(&self, tgt_eye_offset: f32) -> f32 {
761        if self.hazard || matches!(self.kind, ProjectileConstructorKind::Explosive { .. }) {
762            0.0
763        } else {
764            tgt_eye_offset
765        }
766    }
767}
768
769/// Projectile motion: Returns the direction to aim for the projectile to reach
770/// target position. Does not take any forces but gravity into account.
771pub fn aim_projectile(speed: f32, pos: Vec3<f32>, tgt: Vec3<f32>, high_arc: bool) -> Option<Dir> {
772    let mut to_tgt = tgt - pos;
773    let dist_sqrd = to_tgt.xy().magnitude_squared();
774    let u_sqrd = speed.powi(2);
775    if high_arc {
776        to_tgt.z = (u_sqrd
777            + (u_sqrd.powi(2) - GRAVITY * (GRAVITY * dist_sqrd + 2.0 * to_tgt.z * u_sqrd))
778                .sqrt()
779                .max(0.0))
780            / GRAVITY;
781    } else {
782        to_tgt.z = (u_sqrd
783            - (u_sqrd.powi(2) - GRAVITY * (GRAVITY * dist_sqrd + 2.0 * to_tgt.z * u_sqrd))
784                .sqrt()
785                .max(0.0))
786            / GRAVITY;
787    }
788    Dir::from_unnormalized(to_tgt)
789}
790
791#[derive(Clone, Debug, Default)]
792pub struct ProjectileHitEntities {
793    pub hit_entities: Vec<(Uid, Time)>,
794}
795
796impl Component for ProjectileHitEntities {
797    type Storage = specs::DenseVecStorage<Self>;
798}
799
800#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)]
801pub enum ProjectileExplosionTarget {
802    EntityOnly,
803    SolidOnly,
804    SolidOnlyEntityAttack,
805    Both,
806}