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