1 #include "ballistics.h"
2 
3 #include <algorithm>
4 #include <cmath>
5 #include <cstddef>
6 #include <functional>
7 #include <iosfwd>
8 #include <memory>
9 #include <set>
10 #include <vector>
11 
12 #include "calendar.h"
13 #include "creature.h"
14 #include "damage.h"
15 #include "debug.h"
16 #include "dispersion.h"
17 #include "enums.h"
18 #include "explosion.h"
19 #include "game.h"
20 #include "item.h"
21 #include "itype.h"
22 #include "line.h"
23 #include "map.h"
24 #include "messages.h"
25 #include "monster.h"
26 #include "optional.h"
27 #include "options.h"
28 #include "point.h"
29 #include "projectile.h"
30 #include "rng.h"
31 #include "sounds.h"
32 #include "translations.h"
33 #include "trap.h"
34 #include "type_id.h"
35 #include "units.h"
36 #include "visitable.h"
37 #include "vpart_position.h"
38 
39 static const efftype_id effect_bounced( "bounced" );
40 
41 static const std::string flag_LIQUID( "LIQUID" );
42 
drop_or_embed_projectile(const dealt_projectile_attack & attack)43 static void drop_or_embed_projectile( const dealt_projectile_attack &attack )
44 {
45     const auto &proj = attack.proj;
46     const item &drop_item = proj.get_drop();
47     const auto &effects = proj.proj_effects;
48     if( drop_item.is_null() ) {
49         return;
50     }
51 
52     const tripoint &pt = attack.end_point;
53 
54     if( effects.count( "SHATTER_SELF" ) ) {
55         // Drop the contents, not the thrown item
56         add_msg_if_player_sees( pt, _( "The %s shatters!" ), drop_item.tname() );
57 
58         // copies the drop item to spill the contents
59         item( drop_item ).spill_contents( pt );
60 
61         // TODO: Non-glass breaking
62         // TODO: Wine glass breaking vs. entire sheet of glass breaking
63         sounds::sound( pt, 16, sounds::sound_t::combat, _( "glass breaking!" ), false, "bullet_hit",
64                        "hit_glass" );
65 
66         const units::mass shard_mass = itype_id( "glass_shard" )->weight;
67         const int max_nb_of_shards = floor( to_gram( drop_item.type->weight ) / to_gram( shard_mass ) );
68         //between half and max_nb_of_shards-1 will be usable
69         const int nb_of_dropped_shard = std::max( 0, rng( max_nb_of_shards / 2, max_nb_of_shards - 1 ) );
70         //feel free to remove this msg_debug
71         /*add_msg_debug( "Shattered %s dropped %i shards out of a max of %i, based on mass %i g",
72                        drop_item.tname(), nb_of_dropped_shard, max_nb_of_shards - 1, to_gram( drop_item.type->weight ) );*/
73 
74         for( int i = 0; i < nb_of_dropped_shard; ++i ) {
75             item shard( "glass_shard" );
76             //actual dropping of shards
77             get_map().add_item_or_charges( pt, shard );
78         }
79 
80         return;
81     }
82 
83     if( effects.count( "BURST" ) ) {
84         // Drop the contents, not the thrown item
85         add_msg_if_player_sees( pt, _( "The %s bursts!" ), drop_item.tname() );
86 
87         // copies the drop item to spill the contents
88         item( drop_item ).spill_contents( pt );
89 
90         // TODO: Sound
91         return;
92     }
93 
94     // Copy the item
95     item dropped_item = drop_item;
96 
97     monster *mon = dynamic_cast<monster *>( attack.hit_critter );
98 
99     // We can only embed in monsters
100     bool mon_there = mon != nullptr && !mon->is_dead_state();
101     // And if we actually want to embed
102     bool embed = mon_there && effects.count( "NO_EMBED" ) == 0 && effects.count( "TANGLE" ) == 0;
103     // Don't embed in small creatures
104     if( embed ) {
105         const creature_size critter_size = mon->get_size();
106         const units::volume vol = dropped_item.volume();
107         embed = embed && ( critter_size > creature_size::tiny || vol < 250_ml );
108         embed = embed && ( critter_size > creature_size::small || vol < 500_ml );
109         // And if we deal enough damage
110         // Item volume bumps up the required damage too
111         embed = embed &&
112                 ( attack.dealt_dam.type_damage( damage_type::CUT ) / 2 ) +
113                 attack.dealt_dam.type_damage( damage_type::STAB ) >
114                 attack.dealt_dam.type_damage( damage_type::BASH ) +
115                 vol * 3 / 250_ml + rng( 0, 5 );
116     }
117 
118     if( embed ) {
119         mon->add_item( dropped_item );
120         add_msg_if_player_sees( pt, _( "The %1$s embeds in %2$s!" ),
121                                 dropped_item.tname(), mon->disp_name() );
122     } else {
123         bool do_drop = true;
124         // monsters that are able to be tied up will store the item another way
125         // see monexamine.cpp tie_or_untie()
126         // if they aren't friendly they will try and break out of the net/bolas/lasso
127         // players and NPCs just get the downed effect, and item is dropped.
128         // TODO: storing the item on player until they recover from downed
129         if( effects.count( "TANGLE" ) && mon_there ) {
130             do_drop = false;
131         }
132         if( effects.count( "ACT_ON_RANGED_HIT" ) ) {
133             // Don't drop if it exploded
134             do_drop = !dropped_item.activate_thrown( attack.end_point );
135         }
136 
137         map &here = get_map();
138         if( do_drop ) {
139             here.add_item_or_charges( attack.end_point, dropped_item );
140         }
141 
142         if( effects.count( "HEAVY_HIT" ) ) {
143             if( here.has_flag( flag_LIQUID, pt ) ) {
144                 sounds::sound( pt, 10, sounds::sound_t::combat, _( "splash!" ), false, "bullet_hit", "hit_water" );
145             } else {
146                 sounds::sound( pt, 8, sounds::sound_t::combat, _( "thud." ), false, "bullet_hit", "hit_wall" );
147             }
148         }
149 
150         const trap &tr = here.tr_at( pt );
151         if( tr.triggered_by_item( dropped_item ) ) {
152             tr.trigger( pt, dropped_item );
153         }
154     }
155 }
156 
blood_trail_len(int damage)157 static size_t blood_trail_len( int damage )
158 {
159     if( damage > 50 ) {
160         return 3;
161     } else if( damage > 20 ) {
162         return 2;
163     } else if( damage > 0 ) {
164         return 1;
165     }
166     return 0;
167 }
168 
projectile_attack_roll(const dispersion_sources & dispersion,double range,double target_size)169 projectile_attack_aim projectile_attack_roll( const dispersion_sources &dispersion, double range,
170         double target_size )
171 {
172     projectile_attack_aim aim;
173 
174     // dispersion is a measure of the dispersion of shots due to the gun + shooter characteristics
175     // i.e. it is independent of any particular shot
176 
177     // shot_dispersion is the actual dispersion for this particular shot, i.e.
178     // the error angle between where the shot was aimed and where this one actually went
179     // NB: some cases pass dispersion == 0 for a "never misses" shot e.g. bio_magnet,
180     aim.dispersion = dispersion.roll();
181 
182     // an isosceles triangle is formed by the intended and actual target tiles
183     aim.missed_by_tiles = iso_tangent( range, units::from_arcmin( aim.dispersion ) );
184 
185     // fraction we missed a monster target by (0.0 = perfect hit, 1.0 = miss)
186     if( target_size > 0.0 ) {
187         aim.missed_by = std::min( 1.0, aim.missed_by_tiles / target_size );
188     } else {
189         // Special case 0 size targets, just to be safe from 0.0/0.0 NaNs
190         aim.missed_by = 1.0;
191     }
192 
193     return aim;
194 }
195 
projectile_attack(const projectile & proj_arg,const tripoint & source,const tripoint & target_arg,const dispersion_sources & dispersion,Creature * origin,const vehicle * in_veh)196 dealt_projectile_attack projectile_attack( const projectile &proj_arg, const tripoint &source,
197         const tripoint &target_arg, const dispersion_sources &dispersion,
198         Creature *origin, const vehicle *in_veh )
199 {
200     const bool do_animation = get_option<bool>( "ANIMATION_PROJECTILES" );
201 
202     double range = rl_dist( source, target_arg );
203 
204     Creature *target_critter = g->critter_at( target_arg );
205     map &here = get_map();
206     double target_size = target_critter != nullptr ?
207                          target_critter->ranged_target_size() :
208                          here.ranged_target_size( target_arg );
209     projectile_attack_aim aim = projectile_attack_roll( dispersion, range, target_size );
210 
211     // TODO: move to-hit roll back in here
212 
213     dealt_projectile_attack attack {
214         proj_arg, nullptr, dealt_damage_instance(), source, aim.missed_by
215     };
216 
217     // No suicidal shots
218     if( source == target_arg ) {
219         debugmsg( "Projectile_attack targeted own square." );
220         return attack;
221     }
222 
223     projectile &proj = attack.proj;
224     const auto &proj_effects = proj.proj_effects;
225 
226     const bool stream = proj_effects.count( "STREAM" ) > 0 ||
227                         proj_effects.count( "STREAM_BIG" ) > 0 ||
228                         proj_effects.count( "JET" ) > 0;
229     const char bullet = stream ? '#' : '*';
230     const bool no_item_damage = proj_effects.count( "NO_ITEM_DAMAGE" ) > 0;
231     const bool do_draw_line = proj_effects.count( "DRAW_AS_LINE" ) > 0;
232     const bool null_source = proj_effects.count( "NULL_SOURCE" ) > 0;
233     // Determines whether it can penetrate obstacles
234     const bool is_bullet = proj_arg.speed >= 200 && !proj_effects.count( "NO_PENETRATE_OBSTACLES" );
235 
236     // If we were targeting a tile rather than a monster, don't overshoot
237     // Unless the target was a wall, then we are aiming high enough to overshoot
238     const bool no_overshoot = proj_effects.count( "NO_OVERSHOOT" ) ||
239                               ( g->critter_at( target_arg ) == nullptr && here.passable( target_arg ) );
240 
241     double extend_to_range = no_overshoot ? range : proj_arg.range;
242 
243     tripoint target = target_arg;
244     std::vector<tripoint> trajectory;
245     if( aim.missed_by_tiles >= 1.0 ) {
246         // We missed enough to target a different tile
247         double dx = target_arg.x - source.x;
248         double dy = target_arg.y - source.y;
249         units::angle rad = units::atan2( dy, dx );
250 
251         // cap wild misses at +/- 30 degrees
252         units::angle dispersion_angle =
253             std::min( units::from_arcmin( aim.dispersion ), 30_degrees );
254         rad += ( one_in( 2 ) ? 1 : -1 ) * dispersion_angle;
255 
256         // TODO: This should also represent the miss on z axis
257         const int offset = std::min<int>( range, std::sqrt( aim.missed_by_tiles ) );
258         int new_range = no_overshoot ?
259                         range + rng( -offset, offset ) :
260                         rng( range - offset, proj_arg.range );
261         new_range = std::max( new_range, 1 );
262 
263         target.x = source.x + roll_remainder( new_range * cos( rad ) );
264         target.y = source.y + roll_remainder( new_range * sin( rad ) );
265 
266         if( target == source ) {
267             target.x = source.x + sgn( dx );
268             target.y = source.y + sgn( dy );
269         }
270 
271         // Don't extend range further, miss here can mean hitting the ground near the target
272         range = rl_dist( source, target );
273         extend_to_range = range;
274 
275         sfx::play_variant_sound( "bullet_hit", "hit_wall", sfx::get_heard_volume( target ),
276                                  sfx::get_heard_angle( target ) );
277         // TODO: Z dispersion
278         // If we missed, just draw a straight line.
279         trajectory = line_to( source, target );
280     } else {
281         // Go around obstacles a little if we're on target.
282         trajectory = here.find_clear_path( source, target );
283     }
284 
285     add_msg_debug( "missed_by_tiles: %.2f; missed_by: %.2f; target (orig/hit): %d,%d,%d/%d,%d,%d",
286                    aim.missed_by_tiles, aim.missed_by,
287                    target_arg.x, target_arg.y, target_arg.z,
288                    target.x, target.y, target.z );
289 
290     // Trace the trajectory, doing damage in order
291     tripoint &tp = attack.end_point;
292     tripoint prev_point = source;
293 
294     // Add the first point to the trajectory
295     trajectory.insert( trajectory.begin(), source );
296 
297     static emit_id muzzle_smoke( "emit_smaller_smoke_plume" );
298     if( proj_effects.count( "MUZZLE_SMOKE" ) ) {
299         here.emit_field( trajectory.front(), muzzle_smoke );
300     }
301 
302     if( !no_overshoot && range < extend_to_range ) {
303         // Continue line is very "stiff" when the original range is short
304         // TODO: Make it use a more distant point for more realistic extended lines
305         std::vector<tripoint> trajectory_extension = continue_line( trajectory,
306                 extend_to_range - range );
307         trajectory.reserve( trajectory.size() + trajectory_extension.size() );
308         trajectory.insert( trajectory.end(), trajectory_extension.begin(), trajectory_extension.end() );
309     }
310     // Range can be 0
311     size_t traj_len = trajectory.size();
312     while( traj_len > 0 && rl_dist( source, trajectory[traj_len - 1] ) > proj_arg.range ) {
313         --traj_len;
314     }
315 
316     const float projectile_skip_multiplier = 0.1f;
317     // Randomize the skip so that bursts look nicer
318     int projectile_skip_calculation = range * projectile_skip_multiplier;
319     int projectile_skip_current_frame = rng( 0, projectile_skip_calculation );
320     bool has_momentum = true;
321 
322     for( size_t i = 1; i < traj_len && ( has_momentum || stream ); ++i ) {
323         prev_point = tp;
324         tp = trajectory[i];
325 
326         if( ( tp.z > prev_point.z && here.has_floor( tp ) ) ||
327             ( tp.z < prev_point.z && here.has_floor( prev_point ) ) ) {
328             // Currently strictly no shooting through floor
329             // TODO: Bash the floor
330             tp = prev_point;
331             traj_len = --i;
332             break;
333         }
334         // Drawing the bullet uses player g->u, and not player p, because it's drawn
335         // relative to YOUR position, which may not be the gunman's position.
336         if( do_animation && !do_draw_line ) {
337             // TODO: Make this draw thrown item/launched grenade/arrow
338             if( projectile_skip_current_frame >= projectile_skip_calculation ) {
339                 g->draw_bullet( tp, static_cast<int>( i ), trajectory, bullet );
340                 projectile_skip_current_frame = 0;
341                 // If we missed recalculate the skip factor so they spread out.
342                 projectile_skip_calculation =
343                     std::max( static_cast<size_t>( range ), i ) * projectile_skip_multiplier;
344             } else {
345                 projectile_skip_current_frame++;
346             }
347         }
348 
349         if( in_veh != nullptr ) {
350             const optional_vpart_position other = here.veh_at( tp );
351             if( in_veh == veh_pointer_or_null( other ) && other->is_inside() ) {
352                 // Turret is on the roof and can't hit anything inside
353                 continue;
354             }
355         }
356 
357         Creature *critter = g->critter_at( tp );
358         if( origin == critter ) {
359             // No hitting self with "weird" attacks.
360             critter = nullptr;
361         }
362 
363         monster *mon = dynamic_cast<monster *>( critter );
364         // ignore non-point-blank digging targets (since they are underground)
365         if( mon != nullptr && mon->digging() &&
366             rl_dist( source, tp ) > 1 ) {
367             critter = nullptr;
368             mon = nullptr;
369         }
370 
371         // Reset hit critter from the last iteration
372         attack.hit_critter = nullptr;
373 
374         // If we shot us a monster...
375         // TODO: add size effects to accuracy
376         // If there's a monster in the path of our bullet, and either our aim was true,
377         //  OR it's not the monster we were aiming at and we were lucky enough to hit it
378         double cur_missed_by = aim.missed_by;
379 
380         // unintentional hit on something other than our actual target
381         // don't re-roll for the actual target, we already decided on a missed_by value for that
382         // at the start, misses should stay as misses
383         if( critter != nullptr && tp != target_arg ) {
384             // Unintentional hit
385             cur_missed_by = std::max( rng_float( 0.1, 1.5 - aim.missed_by ) /
386                                       critter->ranged_target_size(), 0.4 );
387         }
388 
389         if( critter != nullptr && cur_missed_by < 1.0 ) {
390             if( in_veh != nullptr && veh_pointer_or_null( here.veh_at( tp ) ) == in_veh &&
391                 critter->is_player() ) {
392                 // Turret either was aimed by the player (who is now ducking) and shoots from above
393                 // Or was just IFFing, giving lots of warnings and time to get out of the line of fire
394                 continue;
395             }
396             attack.missed_by = cur_missed_by;
397             critter->deal_projectile_attack( null_source ? nullptr : origin, attack );
398             // Critter can still dodge the projectile
399             // In this case hit_critter won't be set
400             if( attack.hit_critter != nullptr ) {
401                 const size_t bt_len = blood_trail_len( attack.dealt_dam.total_damage() );
402                 if( bt_len > 0 ) {
403                     const tripoint &dest = move_along_line( tp, trajectory, bt_len );
404                     here.add_splatter_trail( critter->bloodType(), tp, dest );
405                 }
406                 sfx::do_projectile_hit( *attack.hit_critter );
407                 has_momentum = false;
408             } else {
409                 attack.missed_by = aim.missed_by;
410             }
411         } else if( in_veh != nullptr && veh_pointer_or_null( here.veh_at( tp ) ) == in_veh ) {
412             // Don't do anything, especially don't call map::shoot as this would damage the vehicle
413         } else {
414             here.shoot( tp, proj, !no_item_damage && tp == target );
415             has_momentum = proj.impact.total_damage() > 0;
416         }
417 
418         if( ( !has_momentum || !is_bullet ) && here.impassable( tp ) ) {
419             // Don't let flamethrowers go through walls
420             // TODO: Let them go through bars
421             traj_len = i;
422             break;
423         }
424     }
425     // Done with the trajectory!
426     if( do_animation && do_draw_line && traj_len > 2 ) {
427         trajectory.erase( trajectory.begin() );
428         trajectory.resize( traj_len-- );
429         g->draw_line( tp, trajectory );
430         g->draw_bullet( tp, static_cast<int>( traj_len-- ), trajectory, bullet );
431     }
432 
433     if( here.impassable( tp ) ) {
434         tp = prev_point;
435     }
436 
437     drop_or_embed_projectile( attack );
438 
439     apply_ammo_effects( tp, proj.proj_effects );
440     const auto &expl = proj.get_custom_explosion();
441     if( expl.power > 0.0f ) {
442         explosion_handler::explosion( tp, proj.get_custom_explosion() );
443     }
444 
445     // TODO: Move this outside now that we have hit point in return values?
446     if( proj.proj_effects.count( "BOUNCE" ) ) {
447         // Add effect so the shooter is not targeted itself.
448         if( origin && !origin->has_effect( effect_bounced ) ) {
449             origin->add_effect( effect_bounced, 1_turns );
450         }
451         Creature *mon_ptr = g->get_creature_if( [&]( const Creature & z ) {
452             // search for creatures in radius 4 around impact site
453             if( rl_dist( z.pos(), tp ) <= 4 &&
454                 here.sees( z.pos(), tp, -1 ) ) {
455                 // don't hit targets that have already been hit
456                 if( !z.has_effect( effect_bounced ) ) {
457                     return true;
458                 }
459             }
460             return false;
461         } );
462         if( mon_ptr ) {
463             Creature &z = *mon_ptr;
464             add_msg( _( "The attack bounced to %s!" ), z.get_name() );
465             z.add_effect( effect_bounced, 1_turns );
466             projectile_attack( proj, tp, z.pos(), dispersion, origin, in_veh );
467             sfx::play_variant_sound( "fire_gun", "bio_lightning_tail",
468                                      sfx::get_heard_volume( z.pos() ), sfx::get_heard_angle( z.pos() ) );
469         }
470     }
471 
472     return attack;
473 }
474