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, Firework(Reagent),
25 SurpriseEgg,
26 TrainingDummy,
27}
28
29#[derive(Clone, Debug)]
30pub struct Projectile {
31 pub hit_solid: Vec<Effect>,
33 pub hit_entity: Vec<Effect>,
34 pub timeout: Vec<Effect>,
35 pub time_left: Duration,
37 pub owner: Option<Uid>,
38 pub ignore_group: bool,
41 pub is_sticky: bool,
43 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 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 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}