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