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, Firework(Reagent),
33 SurpriseEgg,
34 TrainingDummy,
35 Arc(ArcProperties),
36 Split(SplitOptions),
37}
38
39#[derive(Clone, Debug)]
40pub struct Projectile {
41 pub hit_solid: Vec<Effect>,
43 pub hit_entity: Vec<Effect>,
44 pub timeout: Vec<Effect>,
45 pub time_left: Duration,
47 pub init_time: Secs,
50 pub owner: Option<Uid>,
51 pub ignore_group: bool,
54 pub is_sticky: bool,
56 pub is_point: bool,
58 pub homing: Option<(Uid, f32)>,
61 pub pierce_entities: bool,
64 pub hit_entities: Vec<Uid>,
67 pub limit_per_ability: bool,
70 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 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 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 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 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
783pub 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}