1 #include <cstddef>
2 #include <functional>
3 #include <memory>
4 #include <sstream>
5 #include <vector>
6 
7 #include "avatar.h"
8 #include "catch/catch.hpp"
9 #include "creature.h"
10 #include "explosion.h"
11 #include "game.h"
12 #include "item.h"
13 #include "itype.h"
14 #include "line.h"
15 #include "map.h"
16 #include "map_helpers.h"
17 #include "monster.h"
18 #include "point.h"
19 #include "test_statistics.h"
20 #include "type_id.h"
21 #include "units.h"
22 #include "veh_type.h"
23 #include "vehicle.h"
24 #include "vpart_position.h"
25 #include "vpart_range.h"
26 
27 enum class outcome_type {
28     Kill, Casualty
29 };
30 
check_lethality(const std::string & explosive_id,const int range,float lethality,float margin,outcome_type expected_outcome)31 static void check_lethality( const std::string &explosive_id, const int range, float lethality,
32                              float margin, outcome_type expected_outcome )
33 {
34     const epsilon_threshold target_lethality{ lethality, margin };
35     int num_survivors = 0;
36     int num_subjects = 0;
37     int num_wounded = 0;
38     statistics<bool> victims;
39     std::stringstream survivor_stats;
40     int total_hp = 0;
41     clear_map_and_put_player_underground();
42     do {
43         clear_creatures();
44         // Spawn some monsters in a circle.
45         tripoint origin( 30, 30, 0 );
46         int num_subjects_this_time = 0;
47         for( const tripoint &monster_position : closest_points_first( origin, range ) ) {
48             if( rl_dist( monster_position, origin ) != range ) {
49                 continue;
50             }
51             num_subjects++;
52             num_subjects_this_time++;
53             monster &new_monster = spawn_test_monster( "mon_zombie", monster_position );
54             new_monster.no_extra_death_drops = true;
55         }
56         // Set off an explosion
57         item grenade( explosive_id );
58         grenade.charges = 0;
59         grenade.type->invoke( get_avatar(), grenade, origin );
60         explosion_handler::process_explosions();
61         // see how many monsters survive
62         std::vector<Creature *> survivors = g->get_creatures_if( []( const Creature & critter ) {
63             return critter.is_monster();
64         } );
65         num_survivors += survivors.size();
66         for( Creature *survivor : survivors ) {
67             survivor_stats << survivor->pos() << " " << survivor->get_hp() << ", ";
68             bool wounded = survivor->get_hp() < survivor->get_hp_max();
69             num_wounded += wounded ? 1 : 0;
70             total_hp += survivor->get_hp();
71             if( expected_outcome == outcome_type::Casualty && wounded ) {
72                 victims.add( true );
73             } else {
74                 victims.add( false );
75             }
76         }
77         if( !survivors.empty() ) {
78             survivor_stats << std::endl;
79         }
80         for( int i = survivors.size(); i < num_subjects_this_time; ++i ) {
81             victims.add( true );
82         }
83     } while( victims.uncertain_about( target_lethality ) );
84     CAPTURE( margin );
85     INFO( explosive_id );
86     INFO( "range " << range );
87     INFO( num_survivors << " survivors out of " << num_subjects << " targets." );
88     INFO( survivor_stats.str() );
89     INFO( "Wounded survivors: " << num_wounded );
90     const int average_hp = num_survivors ? total_hp / num_survivors : 0;
91     INFO( "average hp of survivors: " << average_hp );
92     CHECK( victims.avg() == Approx( lethality ).margin( margin ) );
93 }
94 
get_part_hp(vehicle * veh)95 static std::vector<int> get_part_hp( vehicle *veh )
96 {
97     std::vector<int> part_hp;
98     part_hp.reserve( veh->part_count() );
99     for( const vpart_reference &vpr : veh->get_all_parts() ) {
100         part_hp.push_back( vpr.part().hp() );
101     }
102     return part_hp;
103 }
104 
check_vehicle_damage(const std::string & explosive_id,const std::string & vehicle_id,const int range,const double damage_lower_bound,const double damage_upper_bound=1.0)105 static void check_vehicle_damage( const std::string &explosive_id, const std::string &vehicle_id,
106                                   const int range, const double damage_lower_bound, const double damage_upper_bound = 1.0 )
107 {
108     // Clear map
109     clear_map_and_put_player_underground();
110     tripoint origin( 30, 30, 0 );
111 
112     vehicle *target_vehicle = get_map().add_vehicle( vproto_id( vehicle_id ), origin, 0_degrees,
113                               -1, 0 );
114     std::vector<int> before_hp = get_part_hp( target_vehicle );
115 
116     while( get_map().veh_at( origin ) ) {
117         origin.x++;
118     }
119     origin.x += range;
120 
121     // Set off an explosion
122     item grenade( explosive_id );
123     grenade.charges = 0;
124     grenade.type->invoke( get_avatar(), grenade, origin );
125     explosion_handler::process_explosions();
126 
127     std::vector<int> after_hp = get_part_hp( target_vehicle );
128 
129     // running sums of all parts, so we can do tests for hp range for the whole vehicle
130     int before_hp_total = 0;
131     int after_hp_total = 0;
132 
133     // We don't expect any destroyed parts.
134     REQUIRE( before_hp.size() == after_hp.size() );
135     for( size_t i = 0; i < before_hp.size(); ++i ) {
136         before_hp_total += before_hp[ i ];
137         after_hp_total += after_hp[ i ];
138     }
139     CAPTURE( before_hp_total );
140     INFO( vehicle_id );
141     CHECK( after_hp_total >= floor( before_hp_total * damage_lower_bound ) );
142     CHECK( after_hp_total <= ceil( before_hp_total * damage_upper_bound ) );
143 }
144 
145 TEST_CASE( "grenade_lethality", "[grenade],[explosion],[balance],[slow]" )
146 {
147     check_lethality( "grenade_act", 5, 0.95, 0.06, outcome_type::Kill );
148     check_lethality( "grenade_act", 15, 0.40, 0.06, outcome_type::Casualty );
149 }
150 
151 TEST_CASE( "grenade_vs_vehicle", "[grenade],[explosion],[balance]" )
152 {
153     /* as of test writing, car hp is 17653. 0.998 of that means the grenade
154      * has to do more than 36 points of damage to 'fail', which isn't remotely
155      * enough to disable a car even if it all happens to a single component
156      *
157      * also as of test writing:
158      * motorcycle = 3173 hp, 0.997 is 3163 (so <= 10 points of damage)
159      * at a range of 0, we expect motorcycles to take damage from a grenade.
160      * should be between 3093 and 3166 (or 7 to 80 damage)
161      *
162      * humvee absolutely does not believe in a grenade damaging it.
163      * this might change if we adjust the default armor loadout of a humvee,
164      * but i would still expect less damage than with the car due to
165      * heavy duty frames.
166      */
167     for( size_t i = 0; i <= 20 ; ++i ) {
168         check_vehicle_damage( "grenade_act", "car", 5, 0.998 );
169         check_vehicle_damage( "grenade_act", "motorcycle", 5, 0.997 );
170         check_vehicle_damage( "grenade_act", "motorcycle", 0, 0.975, 0.9975 );
171         check_vehicle_damage( "grenade_act", "humvee", 5, 1 );
172     }
173 }
174