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