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, }
25
26#[derive(Clone, Debug)]
27pub struct Projectile {
28 pub hit_solid: Vec<Effect>,
30 pub hit_entity: Vec<Effect>,
31 pub time_left: Duration,
33 pub owner: Option<Uid>,
34 pub ignore_group: bool,
37 pub is_sticky: bool,
39 pub is_point: bool,
41}
42
43impl Component for Projectile {
44 type Storage = specs::DenseVecStorage<Self>;
45}
46
47#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)]
48pub struct ProjectileConstructor {
49 pub kind: ProjectileConstructorKind,
50 pub attack: Option<ProjectileAttack>,
51 pub scaled: Option<Scaled>,
52}
53
54#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)]
55pub struct Scaled {
56 damage: f32,
57 knockback: Option<f32>,
58 energy: f32,
59}
60
61#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)]
62pub struct ProjectileAttack {
63 pub damage: f32,
64 pub knockback: Option<f32>,
65 pub energy: f32,
66 pub buff: Option<CombatBuff>,
67}
68
69#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)]
70pub enum ProjectileConstructorKind {
71 Pointed,
73 Blunt,
74 Explosive {
75 radius: f32,
76 min_falloff: f32,
77 reagent: Option<Reagent>,
78 terrain: Option<(f32, ColorPreset)>,
79 },
80 Possess,
81 Hazard {
82 is_sticky: bool,
83 duration: Secs,
84 },
85 ExplosiveHazard {
86 radius: f32,
87 min_falloff: f32,
88 reagent: Option<Reagent>,
89 terrain: Option<(f32, ColorPreset)>,
90 is_sticky: bool,
91 duration: Secs,
92 },
93}
94
95impl ProjectileConstructor {
96 pub fn create_projectile(
97 self,
98 owner: Option<Uid>,
99 precision_mult: f32,
100 damage_effect: Option<CombatEffect>,
101 ) -> Projectile {
102 if self.scaled.is_some() {
103 dev_panic!(
104 "Attempted to create a projectile that had a provided scaled value without \
105 scaling the projectile."
106 )
107 }
108
109 let instance = rand::random();
110 let attack = self.attack.map(|a| {
111 let knockback = a.knockback.map(|kb| {
112 AttackEffect::new(
113 Some(GroupTarget::OutOfGroup),
114 CombatEffect::Knockback(Knockback {
115 strength: kb,
116 direction: KnockbackDir::Away,
117 }),
118 )
119 .with_requirement(CombatRequirement::AnyDamage)
120 });
121
122 let energy = AttackEffect::new(None, CombatEffect::EnergyReward(a.energy))
123 .with_requirement(CombatRequirement::AnyDamage);
124
125 let buff = a.buff.map(CombatEffect::Buff);
126
127 let (damage_source, damage_kind) = match self.kind {
128 ProjectileConstructorKind::Pointed | ProjectileConstructorKind::Hazard { .. } => {
129 (DamageSource::Projectile, DamageKind::Piercing)
130 },
131 ProjectileConstructorKind::Blunt => {
132 (DamageSource::Projectile, DamageKind::Crushing)
133 },
134 ProjectileConstructorKind::Explosive { .. }
135 | ProjectileConstructorKind::ExplosiveHazard { .. } => {
136 (DamageSource::Explosion, DamageKind::Energy)
137 },
138 ProjectileConstructorKind::Possess => {
139 dev_panic!("This should be unreachable");
140 (DamageSource::Projectile, DamageKind::Piercing)
141 },
142 };
143
144 let mut damage = AttackDamage::new(
145 Damage {
146 source: damage_source,
147 kind: damage_kind,
148 value: a.damage,
149 },
150 Some(GroupTarget::OutOfGroup),
151 instance,
152 );
153
154 if let Some(buff) = buff {
155 damage = damage.with_effect(buff);
156 }
157
158 if let Some(damage_effect) = damage_effect {
159 damage = damage.with_effect(damage_effect);
160 }
161
162 let mut attack = Attack::default()
163 .with_damage(damage)
164 .with_precision(precision_mult)
165 .with_effect(energy)
166 .with_combo_increment();
167
168 if let Some(knockback) = knockback {
169 attack = attack.with_effect(knockback);
170 }
171
172 attack
173 });
174
175 match self.kind {
176 ProjectileConstructorKind::Pointed | ProjectileConstructorKind::Blunt => {
177 let mut hit_entity = vec![Effect::Vanish];
178
179 if let Some(attack) = attack {
180 hit_entity.push(Effect::Attack(attack));
181 }
182
183 Projectile {
184 hit_solid: vec![Effect::Stick, Effect::Bonk],
185 hit_entity,
186 time_left: Duration::from_secs(15),
187 owner,
188 ignore_group: true,
189 is_sticky: true,
190 is_point: true,
191 }
192 },
193 ProjectileConstructorKind::Hazard {
194 is_sticky,
195 duration,
196 } => {
197 let mut hit_entity = vec![Effect::Vanish];
198
199 if let Some(attack) = attack {
200 hit_entity.push(Effect::Attack(attack));
201 }
202
203 Projectile {
204 hit_solid: vec![Effect::Stick, Effect::Bonk],
205 hit_entity,
206 time_left: Duration::from_secs_f64(duration.0),
207 owner,
208 ignore_group: true,
209 is_sticky,
210 is_point: false,
211 }
212 },
213 ProjectileConstructorKind::Explosive {
214 radius,
215 min_falloff,
216 reagent,
217 terrain,
218 } => {
219 let terrain =
220 terrain.map(|(pow, col)| RadiusEffect::TerrainDestruction(pow, col.to_rgb()));
221
222 let mut effects = Vec::new();
223
224 if let Some(attack) = attack {
225 effects.push(RadiusEffect::Attack(attack));
226 }
227
228 if let Some(terrain) = terrain {
229 effects.push(terrain);
230 }
231
232 let explosion = Explosion {
233 effects,
234 radius,
235 reagent,
236 min_falloff,
237 };
238
239 Projectile {
240 hit_solid: vec![Effect::Explode(explosion.clone()), Effect::Vanish],
241 hit_entity: vec![Effect::Explode(explosion), Effect::Vanish],
242 time_left: Duration::from_secs(10),
243 owner,
244 ignore_group: true,
245 is_sticky: true,
246 is_point: true,
247 }
248 },
249 ProjectileConstructorKind::ExplosiveHazard {
250 radius,
251 min_falloff,
252 reagent,
253 terrain,
254 is_sticky,
255 duration,
256 } => {
257 let terrain =
258 terrain.map(|(pow, col)| RadiusEffect::TerrainDestruction(pow, col.to_rgb()));
259
260 let mut effects = Vec::new();
261
262 if let Some(attack) = attack {
263 effects.push(RadiusEffect::Attack(attack));
264 }
265
266 if let Some(terrain) = terrain {
267 effects.push(terrain);
268 }
269
270 let explosion = Explosion {
271 effects,
272 radius,
273 reagent,
274 min_falloff,
275 };
276
277 Projectile {
278 hit_solid: Vec::new(),
279 hit_entity: vec![Effect::Explode(explosion), Effect::Vanish],
280 time_left: Duration::from_secs_f64(duration.0),
281 owner,
282 ignore_group: true,
283 is_sticky,
284 is_point: false,
285 }
286 },
287 ProjectileConstructorKind::Possess => Projectile {
288 hit_solid: vec![Effect::Stick],
289 hit_entity: vec![Effect::Stick, Effect::Possess],
290 time_left: Duration::from_secs(10),
291 owner,
292 ignore_group: false,
293 is_sticky: true,
294 is_point: true,
295 },
296 }
297 }
298
299 pub fn handle_scaling(mut self, scaling: f32) -> Self {
300 let scale_values = |a, b| a + b * scaling;
301
302 if let Some(scaled) = self.scaled {
303 if let Some(ref mut attack) = self.attack {
304 attack.damage = scale_values(attack.damage, scaled.damage);
305 attack.energy = scale_values(attack.energy, scaled.energy);
306 if let Some(s_kb) = scaled.knockback {
307 attack.knockback = Some(scale_values(attack.knockback.unwrap_or(0.0), s_kb));
308 }
309 } else {
310 dev_panic!("Attempted to scale on a projectile that has no attack to scale.")
311 }
312 } else {
313 dev_panic!("Attempted to scale on a projectile that has no provided scaling value.")
314 }
315
316 self.scaled = None;
317
318 self
319 }
320
321 pub fn adjusted_by_stats(mut self, stats: tool::Stats) -> Self {
322 self.attack = self.attack.map(|mut a| {
323 a.damage *= stats.power;
324 a.knockback = a.knockback.map(|kb| kb * stats.effect_power);
325 a.buff = a.buff.map(|mut b| {
326 b.strength *= stats.buff_strength;
327 b
328 });
329 a
330 });
331
332 self.scaled = self.scaled.map(|mut s| {
333 s.damage *= stats.power;
334 s.knockback = s.knockback.map(|kb| kb * stats.effect_power);
335 s
336 });
337
338 match self.kind {
339 ProjectileConstructorKind::Pointed
340 | ProjectileConstructorKind::Blunt
341 | ProjectileConstructorKind::Possess
342 | ProjectileConstructorKind::Hazard { .. } => {},
343 ProjectileConstructorKind::Explosive { ref mut radius, .. }
344 | ProjectileConstructorKind::ExplosiveHazard { ref mut radius, .. } => {
345 *radius *= stats.range;
346 },
347 }
348
349 self
350 }
351
352 pub fn legacy_modified_by_skills(
355 mut self,
356 power: f32,
357 regen: f32,
358 range: f32,
359 kb: f32,
360 ) -> Self {
361 self.attack = self.attack.map(|mut a| {
362 a.damage *= power;
363 a.knockback = a.knockback.map(|k| k * kb);
364 a.energy *= regen;
365 a
366 });
367 self.scaled = self.scaled.map(|mut s| {
368 s.damage *= power;
369 s.knockback = s.knockback.map(|k| k * kb);
370 s.energy *= regen;
371 s
372 });
373 if let ProjectileConstructorKind::Explosive { ref mut radius, .. } = self.kind {
374 *radius *= range;
375 }
376 self
377 }
378
379 pub fn is_explosive(&self) -> bool {
380 match self.kind {
381 ProjectileConstructorKind::Pointed
382 | ProjectileConstructorKind::Blunt
383 | ProjectileConstructorKind::Possess
384 | ProjectileConstructorKind::Hazard { .. } => false,
385 ProjectileConstructorKind::Explosive { .. }
386 | ProjectileConstructorKind::ExplosiveHazard { .. } => true,
387 }
388 }
389}