1 #include <iosfwd>
2 #include <list>
3 #include <memory>
4 
5 #include "avatar.h"
6 #include "calendar.h"
7 #include "catch/catch.hpp"
8 #include "creature.h"
9 #include "flag.h"
10 #include "game.h"
11 #include "item.h"
12 #include "map_helpers.h"
13 #include "monster.h"
14 #include "mtype.h"
15 #include "player_helpers.h"
16 #include "point.h"
17 #include "type_id.h"
18 
19 // The test cases below cover polymorphic functions related to melee hit and dodge rates
20 // for the Character, player, and monster classes, including:
21 //
22 // - Character::get_hit_base, monster::get_hit_base
23 // - Character::get_dodge_base, monster::get_dodge_base
24 // - player::get_dodge, monster::get_dodge
25 
26 // Return the avatar's `get_hit_base` with a given DEX stat.
hit_base_with_dex(avatar & dummy,int dexterity)27 static float hit_base_with_dex( avatar &dummy, int dexterity )
28 {
29     clear_character( dummy );
30     dummy.dex_max = dexterity;
31 
32     return dummy.get_hit_base();
33 }
34 
35 // Return the avatar's `get_dodge_base` with the given DEX stat and dodge skill.
dodge_base_with_dex_and_skill(avatar & dummy,int dexterity,int dodge_skill)36 static float dodge_base_with_dex_and_skill( avatar &dummy, int dexterity, int dodge_skill )
37 {
38     clear_character( dummy );
39     dummy.dex_max = dexterity;
40     dummy.set_skill_level( skill_id( "dodge" ), dodge_skill );
41 
42     return dummy.get_dodge_base();
43 }
44 
45 // Return the Creature's `get_dodge` with the given effect.
dodge_with_effect(Creature & critter,const std::string & effect_name)46 static float dodge_with_effect( Creature &critter, const std::string &effect_name )
47 {
48     // Set one effect and leave other attributes alone
49     critter.clear_effects();
50     critter.add_effect( efftype_id( effect_name ), 1_hours );
51 
52     return critter.get_dodge();
53 }
54 
55 // Return the avatar's `get_dodge` while wearing a single item of clothing.
dodge_wearing_item(avatar & dummy,item & clothing)56 static float dodge_wearing_item( avatar &dummy, item &clothing )
57 {
58     // Get nekkid and wear just this one item
59     std::list<item> temp;
60     while( dummy.takeoff( dummy.i_at( -2 ), &temp ) ) {}
61     dummy.wear_item( clothing );
62 
63     return dummy.get_dodge();
64 }
65 
66 TEST_CASE( "monster::get_hit_base", "[monster][melee][hit]" )
67 {
68     clear_map();
69 
70     SECTION( "monster get_hit_base is equal to melee skill level" ) {
71         monster zed( mtype_id( "mon_zombie" ) );
72         CHECK( zed.get_hit_base() == zed.type->melee_skill );
73     }
74 }
75 
76 TEST_CASE( "Character::get_hit_base", "[character][melee][hit][dex]" )
77 {
78     clear_map();
79 
80     avatar &dummy = get_avatar();
81     clear_character( dummy );
82     dummy.dodges_left = 1;
83 
84     SECTION( "character get_hit_base increases by 1/4 for each point of DEX" ) {
85         CHECK( hit_base_with_dex( dummy, 1 ) == 0.25f );
86         CHECK( hit_base_with_dex( dummy, 2 ) == 0.5f );
87         CHECK( hit_base_with_dex( dummy, 4 ) == 1.0f );
88         CHECK( hit_base_with_dex( dummy, 6 ) == 1.5f );
89         CHECK( hit_base_with_dex( dummy, 8 ) == 2.0f );
90         CHECK( hit_base_with_dex( dummy, 10 ) == 2.5f );
91         CHECK( hit_base_with_dex( dummy, 12 ) == 3.0f );
92     }
93 }
94 
95 TEST_CASE( "monster::get_dodge_base", "[monster][melee][dodge]" )
96 {
97     clear_map();
98 
99     SECTION( "monster get_dodge_base is equal to dodge skill level" ) {
100         monster smoker( mtype_id( "mon_zombie_smoker" ) );
101         CHECK( smoker.get_dodge_base() == smoker.type->sk_dodge );
102     }
103 }
104 
105 TEST_CASE( "Character::get_dodge_base", "[character][melee][dodge][dex][skill]" )
106 {
107     clear_map();
108 
109     avatar &dummy = get_avatar();
110     clear_character( dummy );
111 
112     // Character::get_dodge_base is simply DEXTERITY / 2 + DODGE_SKILL
113     // Even with average dexterity, you can become really good at dodging
114     GIVEN( "character has 8 DEX and no dodge skill" ) {
115         THEN( "base dodge is 4" ) {
116             CHECK( dodge_base_with_dex_and_skill( dummy, 8, 0 ) == 4.0f );
117         }
118 
119         WHEN( "their dodge skill increases to 2" ) {
120             THEN( "base dodge is 6" ) {
121                 CHECK( dodge_base_with_dex_and_skill( dummy, 8, 2 ) == 6.0f );
122             }
123         }
124 
125         AND_WHEN( "their dodge skill increases to 8" ) {
126             THEN( "base dodge is 12" ) {
127                 CHECK( dodge_base_with_dex_and_skill( dummy, 8, 8 ) == 12.0f );
128             }
129         }
130     }
131 
132     // More generally
133 
134     SECTION( "character get_dodge_base increases by 1/2 for each point of DEX" ) {
135         CHECK( dodge_base_with_dex_and_skill( dummy, 1, 0 ) == 0.5f );
136         CHECK( dodge_base_with_dex_and_skill( dummy, 2, 0 ) == 1.0f );
137         CHECK( dodge_base_with_dex_and_skill( dummy, 3, 0 ) == 1.5f );
138         CHECK( dodge_base_with_dex_and_skill( dummy, 4, 0 ) == 2.0f );
139         CHECK( dodge_base_with_dex_and_skill( dummy, 5, 0 ) == 2.5f );
140         CHECK( dodge_base_with_dex_and_skill( dummy, 6, 0 ) == 3.0f );
141         CHECK( dodge_base_with_dex_and_skill( dummy, 7, 0 ) == 3.5f );
142         CHECK( dodge_base_with_dex_and_skill( dummy, 8, 0 ) == 4.0f );
143         CHECK( dodge_base_with_dex_and_skill( dummy, 9, 0 ) == 4.5f );
144         CHECK( dodge_base_with_dex_and_skill( dummy, 10, 0 ) == 5.0f );
145     }
146 
147     SECTION( "character get_dodge_base increases by 1 for each level of dodge skill" ) {
148         CHECK( dodge_base_with_dex_and_skill( dummy, 8, 0 ) == 4.0f );
149         CHECK( dodge_base_with_dex_and_skill( dummy, 8, 1 ) == 5.0f );
150         CHECK( dodge_base_with_dex_and_skill( dummy, 8, 2 ) == 6.0f );
151         CHECK( dodge_base_with_dex_and_skill( dummy, 8, 3 ) == 7.0f );
152         CHECK( dodge_base_with_dex_and_skill( dummy, 8, 4 ) == 8.0f );
153 
154         CHECK( dodge_base_with_dex_and_skill( dummy, 6, 2 ) == 5.0f );
155         CHECK( dodge_base_with_dex_and_skill( dummy, 6, 4 ) == 7.0f );
156         CHECK( dodge_base_with_dex_and_skill( dummy, 6, 6 ) == 9.0f );
157         CHECK( dodge_base_with_dex_and_skill( dummy, 6, 8 ) == 11.0f );
158 
159         CHECK( dodge_base_with_dex_and_skill( dummy, 10, 7 ) == 12.0f );
160         CHECK( dodge_base_with_dex_and_skill( dummy, 10, 8 ) == 13.0f );
161         CHECK( dodge_base_with_dex_and_skill( dummy, 10, 9 ) == 14.0f );
162         CHECK( dodge_base_with_dex_and_skill( dummy, 10, 10 ) == 15.0f );
163     }
164 }
165 
166 TEST_CASE( "monster::get_dodge with effects", "[monster][melee][dodge][effect]" )
167 {
168     clear_map();
169 
170     monster zombie( mtype_id( "mon_zombie_smoker" ) );
171 
172     const float base_dodge = zombie.get_dodge_base();
173     REQUIRE( base_dodge > 0 );
174 
175     // Monsters don't have all the status effects of characters and can't be grabbed,
176     // but a few things may affect their ability to dodge.
177 
178     SECTION( "no effects or bonuses: base dodge" ) {
179         zombie.clear_effects();
180         CHECK( zombie.get_dodge() == base_dodge );
181     }
182 
183     SECTION( "downed: cannot dodge" ) {
184         CHECK( dodge_with_effect( zombie, "downed" ) == 0.0f );
185     }
186 
187     SECTION( "trapped or tied: 1/2 dodge" ) {
188         CHECK( dodge_with_effect( zombie, "beartrap" ) == base_dodge / 2 );
189         CHECK( dodge_with_effect( zombie, "tied" ) == base_dodge / 2 );
190     }
191 
192     SECTION( "unstable footing: 1/4 dodge" ) {
193         CHECK( dodge_with_effect( zombie, "bouldering" ) == base_dodge / 4 );
194     }
195 }
196 
197 TEST_CASE( "player::get_dodge", "[player][melee][dodge]" )
198 {
199     clear_map();
200 
201     avatar &dummy = get_avatar();
202     clear_character( dummy );
203 
204     const float base_dodge = dummy.get_dodge_base();
205 
206     SECTION( "speed below 100 linearly decreases dodge" ) {
207         dummy.set_speed_base( 90 );
208         CHECK( dummy.get_dodge() == Approx( 0.9 * base_dodge ) );
209         dummy.set_speed_base( 75 );
210         CHECK( dummy.get_dodge() == Approx( 0.75 * base_dodge ) );
211         dummy.set_speed_base( 50 );
212         CHECK( dummy.get_dodge() == Approx( 0.5 * base_dodge ) );
213         dummy.set_speed_base( 25 );
214         CHECK( dummy.get_dodge() == Approx( 0.25 * base_dodge ) );
215     }
216 }
217 
218 TEST_CASE( "player::get_dodge with effects", "[player][melee][dodge][effect]" )
219 {
220     clear_map();
221 
222     avatar &dummy = get_avatar();
223     clear_character( dummy );
224 
225     // Compare all effects against base dodge ability
226     const float base_dodge = dummy.get_dodge_base();
227 
228     SECTION( "no effects or bonuses: base dodge" ) {
229         dummy.clear_effects();
230         CHECK( dummy.get_dodge() == base_dodge );
231     }
232 
233     SECTION( "unconscious or winded: cannot dodge" ) {
234         CHECK( dodge_with_effect( dummy, "sleep" ) == 0.0f );
235         CHECK( dodge_with_effect( dummy, "lying_down" ) == 0.0f );
236         CHECK( dodge_with_effect( dummy, "npc_suspend" ) == 0.0f );
237         CHECK( dodge_with_effect( dummy, "narcosis" ) == 0.0f );
238         CHECK( dodge_with_effect( dummy, "winded" ) == 0.0f );
239     }
240 
241     SECTION( "trapped: 1/2 dodge" ) {
242         CHECK( dodge_with_effect( dummy, "beartrap" ) == base_dodge / 2 );
243     }
244 
245     SECTION( "unstable footing: 1/4 dodge" ) {
246         CHECK( dodge_with_effect( dummy, "bouldering" ) == base_dodge / 4 );
247     }
248 
249     SECTION( "skating: amateur or pro?" ) {
250         item skates( "rollerskates" );
251         item blades( "roller_blades" );
252         item heelys( "roller_shoes_on" );
253 
254         REQUIRE( skates.has_flag( flag_ROLLER_QUAD ) );
255         REQUIRE( blades.has_flag( flag_ROLLER_INLINE ) );
256         REQUIRE( heelys.has_flag( flag_ROLLER_ONE ) );
257 
258         SECTION( "amateur skater: 1/5 dodge" ) {
259             REQUIRE_FALSE( dummy.has_trait( trait_id( "PROF_SKATER" ) ) );
260 
261             CHECK( dodge_wearing_item( dummy, skates ) == base_dodge / 5 );
262             CHECK( dodge_wearing_item( dummy, blades ) == base_dodge / 5 );
263             CHECK( dodge_wearing_item( dummy, heelys ) == base_dodge / 5 );
264         }
265 
266         SECTION( "professional skater: 1/2 dodge" ) {
267             dummy.toggle_trait( trait_id( "PROF_SKATER" ) );
268             REQUIRE( dummy.has_trait( trait_id( "PROF_SKATER" ) ) );
269 
270             CHECK( dodge_wearing_item( dummy, skates ) == base_dodge / 2 );
271             CHECK( dodge_wearing_item( dummy, blades ) == base_dodge / 2 );
272             CHECK( dodge_wearing_item( dummy, heelys ) == base_dodge / 2 );
273         }
274     }
275 }
276 
277 TEST_CASE( "player::get_dodge while grabbed", "[player][melee][dodge][grab]" )
278 {
279     clear_map();
280 
281     avatar &dummy = get_avatar();
282     clear_character( dummy );
283 
284     // Base dodge rate when not grabbed
285     const float base_dodge = dummy.get_dodge_base();
286 
287     // Four nearby spots
288     tripoint mon1_pos = dummy.pos() + tripoint_north;
289     tripoint mon2_pos = dummy.pos() + tripoint_east;
290     tripoint mon3_pos = dummy.pos() + tripoint_south;
291     tripoint mon4_pos = dummy.pos() + tripoint_west;
292 
293     // Surrounded by zombies!
294     monster *zed1 = g->place_critter_at( mtype_id( "debug_mon" ), mon1_pos );
295     monster *zed2 = g->place_critter_at( mtype_id( "debug_mon" ), mon2_pos );
296     monster *zed3 = g->place_critter_at( mtype_id( "debug_mon" ), mon3_pos );
297     monster *zed4 = g->place_critter_at( mtype_id( "debug_mon" ), mon4_pos );
298 
299     // Make sure zombies are in their places
300     REQUIRE( g->critter_at<monster>( mon1_pos ) );
301     REQUIRE( g->critter_at<monster>( mon2_pos ) );
302     REQUIRE( g->critter_at<monster>( mon3_pos ) );
303     REQUIRE( g->critter_at<monster>( mon4_pos ) );
304 
305     // Get grabbed
306     dummy.add_effect( efftype_id( "grabbed" ), 1_minutes );
307     REQUIRE( dummy.has_effect( efftype_id( "grabbed" ) ) );
308 
309     // When grabbed, dodge skill reduces for each additional grab
310 
311     SECTION( "1 grab: 1/2 dodge" ) {
312         zed1->add_effect( efftype_id( "grabbing" ), 1_minutes );
313         REQUIRE( zed1->has_effect( efftype_id( "grabbing" ) ) );
314 
315         CHECK( dummy.get_dodge() == base_dodge / 2 );
316     }
317 
318     SECTION( "2 grabs: 1/3 dodge" ) {
319         zed1->add_effect( efftype_id( "grabbing" ), 1_minutes );
320         zed2->add_effect( efftype_id( "grabbing" ), 1_minutes );
321         REQUIRE( zed1->has_effect( efftype_id( "grabbing" ) ) );
322         REQUIRE( zed2->has_effect( efftype_id( "grabbing" ) ) );
323 
324         CHECK( dummy.get_dodge() == base_dodge / 3 );
325     }
326 
327     SECTION( "3 grabs: 1/4 dodge" ) {
328         zed1->add_effect( efftype_id( "grabbing" ), 1_minutes );
329         zed2->add_effect( efftype_id( "grabbing" ), 1_minutes );
330         zed3->add_effect( efftype_id( "grabbing" ), 1_minutes );
331         REQUIRE( zed1->has_effect( efftype_id( "grabbing" ) ) );
332         REQUIRE( zed2->has_effect( efftype_id( "grabbing" ) ) );
333         REQUIRE( zed3->has_effect( efftype_id( "grabbing" ) ) );
334 
335         CHECK( dummy.get_dodge() == base_dodge / 4 );
336     }
337 
338     SECTION( "4 grabs: 1/5 dodge" ) {
339         zed1->add_effect( efftype_id( "grabbing" ), 1_minutes );
340         zed2->add_effect( efftype_id( "grabbing" ), 1_minutes );
341         zed3->add_effect( efftype_id( "grabbing" ), 1_minutes );
342         zed4->add_effect( efftype_id( "grabbing" ), 1_minutes );
343         REQUIRE( zed1->has_effect( efftype_id( "grabbing" ) ) );
344         REQUIRE( zed2->has_effect( efftype_id( "grabbing" ) ) );
345         REQUIRE( zed3->has_effect( efftype_id( "grabbing" ) ) );
346         REQUIRE( zed4->has_effect( efftype_id( "grabbing" ) ) );
347 
348         CHECK( dummy.get_dodge() == base_dodge / 5 );
349     }
350 }
351 
352 TEST_CASE( "player::get_dodge stamina effects", "[player][melee][dodge][stamina]" )
353 {
354     avatar &dummy = get_avatar();
355     clear_character( dummy );
356 
357     SECTION( "8/8/8/8, no skills, unencumbered" ) {
358         const int stamina_max = dummy.get_stamina_max();
359 
360         SECTION( "100% stamina" ) {
361             CHECK( dummy.get_dodge() == Approx( 4.0f ).margin( 0.001 ) );
362         }
363 
364         SECTION( "75% stamina" ) {
365             dummy.set_stamina( .75 * stamina_max );
366             CHECK( dummy.get_dodge() == Approx( 4.0f ).margin( 0.001 ) );
367         }
368 
369         SECTION( "50% stamina" ) {
370             dummy.set_stamina( .5 * stamina_max );
371             CHECK( dummy.get_dodge() == Approx( 4.0f ).margin( 0.001 ) );
372         }
373 
374         SECTION( "40% stamina" ) {
375             dummy.set_stamina( .4 * stamina_max );
376             CHECK( dummy.get_dodge() == Approx( 3.2f ).margin( 0.001 ) );
377         }
378 
379         SECTION( "30% stamina" ) {
380             dummy.set_stamina( .3 * stamina_max );
381             CHECK( dummy.get_dodge() == Approx( 2.4f ).margin( 0.001 ) );
382         }
383 
384         SECTION( "20% stamina" ) {
385             dummy.set_stamina( .2 * stamina_max );
386             CHECK( dummy.get_dodge() == Approx( 1.6f ).margin( 0.001 ) );
387         }
388 
389         SECTION( "10% stamina" ) {
390             dummy.set_stamina( .1 * stamina_max );
391             CHECK( dummy.get_dodge() == Approx( 0.8f ).margin( 0.001 ) );
392         }
393 
394         SECTION( "0% stamina" ) {
395             dummy.set_stamina( 0 );
396             CHECK( dummy.get_dodge() == Approx( 0.0f ).margin( 0.001 ) );
397         }
398     }
399 }
400