veloren_common/comp/
projectile.rs

1use crate::{
2    combat::{
3        Attack, AttackDamage, AttackEffect, CombatBuff, CombatEffect, CombatRequirement, Damage,
4        DamageKind, DamageSource, GroupTarget, Knockback, KnockbackDir,
5    },
6    comp::item::{Reagent, tool},
7    explosion::{ColorPreset, Explosion, RadiusEffect},
8    resources::Secs,
9    uid::Uid,
10};
11use common_base::dev_panic;
12use serde::{Deserialize, Serialize};
13use specs::Component;
14use std::time::Duration;
15
16#[derive(Clone, Debug, Serialize, Deserialize)]
17pub enum Effect {
18    Attack(Attack),
19    Explode(Explosion),
20    Vanish,
21    Stick,
22    Possess,
23    Bonk, // Knock/dislodge/change objects on hit
24}
25
26#[derive(Clone, Debug)]
27pub struct Projectile {
28    // TODO: use SmallVec for these effects
29    pub hit_solid: Vec<Effect>,
30    pub hit_entity: Vec<Effect>,
31    /// Time left until the projectile will despawn
32    pub time_left: Duration,
33    pub owner: Option<Uid>,
34    /// Whether projectile collides with entities in the same group as its
35    /// owner
36    pub ignore_group: bool,
37    /// Whether the projectile is sticky
38    pub is_sticky: bool,
39    /// Whether the projectile should use a point collider
40    pub is_point: bool,
41}
42
43impl Component for Projectile {
44    type Storage = specs::DenseVecStorage<Self>;
45}
46
47#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)]
48pub struct ProjectileConstructor {
49    pub kind: ProjectileConstructorKind,
50    pub attack: Option<ProjectileAttack>,
51    pub scaled: Option<Scaled>,
52}
53
54#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)]
55pub struct Scaled {
56    damage: f32,
57    knockback: Option<f32>,
58    energy: f32,
59}
60
61#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)]
62pub struct ProjectileAttack {
63    pub damage: f32,
64    pub knockback: Option<f32>,
65    pub energy: f32,
66    pub buff: Option<CombatBuff>,
67}
68
69#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)]
70pub enum ProjectileConstructorKind {
71    // I want a better name for 'Pointed' and 'Blunt'
72    Pointed,
73    Blunt,
74    Explosive {
75        radius: f32,
76        min_falloff: f32,
77        reagent: Option<Reagent>,
78        terrain: Option<(f32, ColorPreset)>,
79    },
80    Possess,
81    Hazard {
82        is_sticky: bool,
83        duration: Secs,
84    },
85    ExplosiveHazard {
86        radius: f32,
87        min_falloff: f32,
88        reagent: Option<Reagent>,
89        terrain: Option<(f32, ColorPreset)>,
90        is_sticky: bool,
91        duration: Secs,
92    },
93}
94
95impl ProjectileConstructor {
96    pub fn create_projectile(
97        self,
98        owner: Option<Uid>,
99        precision_mult: f32,
100        damage_effect: Option<CombatEffect>,
101    ) -> Projectile {
102        if self.scaled.is_some() {
103            dev_panic!(
104                "Attempted to create a projectile that had a provided scaled value without \
105                 scaling the projectile."
106            )
107        }
108
109        let instance = rand::random();
110        let attack = self.attack.map(|a| {
111            let knockback = a.knockback.map(|kb| {
112                AttackEffect::new(
113                    Some(GroupTarget::OutOfGroup),
114                    CombatEffect::Knockback(Knockback {
115                        strength: kb,
116                        direction: KnockbackDir::Away,
117                    }),
118                )
119                .with_requirement(CombatRequirement::AnyDamage)
120            });
121
122            let energy = AttackEffect::new(None, CombatEffect::EnergyReward(a.energy))
123                .with_requirement(CombatRequirement::AnyDamage);
124
125            let buff = a.buff.map(CombatEffect::Buff);
126
127            let (damage_source, damage_kind) = match self.kind {
128                ProjectileConstructorKind::Pointed | ProjectileConstructorKind::Hazard { .. } => {
129                    (DamageSource::Projectile, DamageKind::Piercing)
130                },
131                ProjectileConstructorKind::Blunt => {
132                    (DamageSource::Projectile, DamageKind::Crushing)
133                },
134                ProjectileConstructorKind::Explosive { .. }
135                | ProjectileConstructorKind::ExplosiveHazard { .. } => {
136                    (DamageSource::Explosion, DamageKind::Energy)
137                },
138                ProjectileConstructorKind::Possess => {
139                    dev_panic!("This should be unreachable");
140                    (DamageSource::Projectile, DamageKind::Piercing)
141                },
142            };
143
144            let mut damage = AttackDamage::new(
145                Damage {
146                    source: damage_source,
147                    kind: damage_kind,
148                    value: a.damage,
149                },
150                Some(GroupTarget::OutOfGroup),
151                instance,
152            );
153
154            if let Some(buff) = buff {
155                damage = damage.with_effect(buff);
156            }
157
158            if let Some(damage_effect) = damage_effect {
159                damage = damage.with_effect(damage_effect);
160            }
161
162            let mut attack = Attack::default()
163                .with_damage(damage)
164                .with_precision(precision_mult)
165                .with_effect(energy)
166                .with_combo_increment();
167
168            if let Some(knockback) = knockback {
169                attack = attack.with_effect(knockback);
170            }
171
172            attack
173        });
174
175        match self.kind {
176            ProjectileConstructorKind::Pointed | ProjectileConstructorKind::Blunt => {
177                let mut hit_entity = vec![Effect::Vanish];
178
179                if let Some(attack) = attack {
180                    hit_entity.push(Effect::Attack(attack));
181                }
182
183                Projectile {
184                    hit_solid: vec![Effect::Stick, Effect::Bonk],
185                    hit_entity,
186                    time_left: Duration::from_secs(15),
187                    owner,
188                    ignore_group: true,
189                    is_sticky: true,
190                    is_point: true,
191                }
192            },
193            ProjectileConstructorKind::Hazard {
194                is_sticky,
195                duration,
196            } => {
197                let mut hit_entity = vec![Effect::Vanish];
198
199                if let Some(attack) = attack {
200                    hit_entity.push(Effect::Attack(attack));
201                }
202
203                Projectile {
204                    hit_solid: vec![Effect::Stick, Effect::Bonk],
205                    hit_entity,
206                    time_left: Duration::from_secs_f64(duration.0),
207                    owner,
208                    ignore_group: true,
209                    is_sticky,
210                    is_point: false,
211                }
212            },
213            ProjectileConstructorKind::Explosive {
214                radius,
215                min_falloff,
216                reagent,
217                terrain,
218            } => {
219                let terrain =
220                    terrain.map(|(pow, col)| RadiusEffect::TerrainDestruction(pow, col.to_rgb()));
221
222                let mut effects = Vec::new();
223
224                if let Some(attack) = attack {
225                    effects.push(RadiusEffect::Attack(attack));
226                }
227
228                if let Some(terrain) = terrain {
229                    effects.push(terrain);
230                }
231
232                let explosion = Explosion {
233                    effects,
234                    radius,
235                    reagent,
236                    min_falloff,
237                };
238
239                Projectile {
240                    hit_solid: vec![Effect::Explode(explosion.clone()), Effect::Vanish],
241                    hit_entity: vec![Effect::Explode(explosion), Effect::Vanish],
242                    time_left: Duration::from_secs(10),
243                    owner,
244                    ignore_group: true,
245                    is_sticky: true,
246                    is_point: true,
247                }
248            },
249            ProjectileConstructorKind::ExplosiveHazard {
250                radius,
251                min_falloff,
252                reagent,
253                terrain,
254                is_sticky,
255                duration,
256            } => {
257                let terrain =
258                    terrain.map(|(pow, col)| RadiusEffect::TerrainDestruction(pow, col.to_rgb()));
259
260                let mut effects = Vec::new();
261
262                if let Some(attack) = attack {
263                    effects.push(RadiusEffect::Attack(attack));
264                }
265
266                if let Some(terrain) = terrain {
267                    effects.push(terrain);
268                }
269
270                let explosion = Explosion {
271                    effects,
272                    radius,
273                    reagent,
274                    min_falloff,
275                };
276
277                Projectile {
278                    hit_solid: Vec::new(),
279                    hit_entity: vec![Effect::Explode(explosion), Effect::Vanish],
280                    time_left: Duration::from_secs_f64(duration.0),
281                    owner,
282                    ignore_group: true,
283                    is_sticky,
284                    is_point: false,
285                }
286            },
287            ProjectileConstructorKind::Possess => Projectile {
288                hit_solid: vec![Effect::Stick],
289                hit_entity: vec![Effect::Stick, Effect::Possess],
290                time_left: Duration::from_secs(10),
291                owner,
292                ignore_group: false,
293                is_sticky: true,
294                is_point: true,
295            },
296        }
297    }
298
299    pub fn handle_scaling(mut self, scaling: f32) -> Self {
300        let scale_values = |a, b| a + b * scaling;
301
302        if let Some(scaled) = self.scaled {
303            if let Some(ref mut attack) = self.attack {
304                attack.damage = scale_values(attack.damage, scaled.damage);
305                attack.energy = scale_values(attack.energy, scaled.energy);
306                if let Some(s_kb) = scaled.knockback {
307                    attack.knockback = Some(scale_values(attack.knockback.unwrap_or(0.0), s_kb));
308                }
309            } else {
310                dev_panic!("Attempted to scale on a projectile that has no attack to scale.")
311            }
312        } else {
313            dev_panic!("Attempted to scale on a projectile that has no provided scaling value.")
314        }
315
316        self.scaled = None;
317
318        self
319    }
320
321    pub fn adjusted_by_stats(mut self, stats: tool::Stats) -> Self {
322        self.attack = self.attack.map(|mut a| {
323            a.damage *= stats.power;
324            a.knockback = a.knockback.map(|kb| kb * stats.effect_power);
325            a.buff = a.buff.map(|mut b| {
326                b.strength *= stats.buff_strength;
327                b
328            });
329            a
330        });
331
332        self.scaled = self.scaled.map(|mut s| {
333            s.damage *= stats.power;
334            s.knockback = s.knockback.map(|kb| kb * stats.effect_power);
335            s
336        });
337
338        match self.kind {
339            ProjectileConstructorKind::Pointed
340            | ProjectileConstructorKind::Blunt
341            | ProjectileConstructorKind::Possess
342            | ProjectileConstructorKind::Hazard { .. } => {},
343            ProjectileConstructorKind::Explosive { ref mut radius, .. }
344            | ProjectileConstructorKind::ExplosiveHazard { ref mut radius, .. } => {
345                *radius *= stats.range;
346            },
347        }
348
349        self
350    }
351
352    // Remove this function after skill tree overhaul completed for bow and fire
353    // staff
354    pub fn legacy_modified_by_skills(
355        mut self,
356        power: f32,
357        regen: f32,
358        range: f32,
359        kb: f32,
360    ) -> Self {
361        self.attack = self.attack.map(|mut a| {
362            a.damage *= power;
363            a.knockback = a.knockback.map(|k| k * kb);
364            a.energy *= regen;
365            a
366        });
367        self.scaled = self.scaled.map(|mut s| {
368            s.damage *= power;
369            s.knockback = s.knockback.map(|k| k * kb);
370            s.energy *= regen;
371            s
372        });
373        if let ProjectileConstructorKind::Explosive { ref mut radius, .. } = self.kind {
374            *radius *= range;
375        }
376        self
377    }
378
379    pub fn is_explosive(&self) -> bool {
380        match self.kind {
381            ProjectileConstructorKind::Pointed
382            | ProjectileConstructorKind::Blunt
383            | ProjectileConstructorKind::Possess
384            | ProjectileConstructorKind::Hazard { .. } => false,
385            ProjectileConstructorKind::Explosive { .. }
386            | ProjectileConstructorKind::ExplosiveHazard { .. } => true,
387        }
388    }
389}