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