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