1 #include <list> 2 #include <memory> 3 4 #include "calendar.h" 5 #include "catch/catch.hpp" 6 #include "character.h" 7 #include "flag.h" 8 #include "game.h" 9 #include "item.h" 10 #include "lightmap.h" 11 #include "map.h" 12 #include "map_helpers.h" 13 #include "player_helpers.h" 14 #include "type_id.h" 15 16 // Tests of Character vision and sight 17 // 18 // Functions tested: 19 // Character::recalc_sight_limits 20 // Character::unimpaired_range 21 // Character::sight_impaired 22 // Character::fine_detail_vision_mod 23 // 24 // Other related / supporting functions: 25 // game::reset_light_level 26 // game::is_in_sunlight 27 // map::build_map_cache 28 // map::ambient_light_at 29 30 // Character::fine_detail_vision_mod() returns a floating-point number that acts as a multiplier for 31 // the time taken to perform tasks that require detail vision. 1.0 is ideal lighting conditions, 32 // while greater than 4.0 means these activities cannot be performed at all. 33 // 34 // According to the function docs: 35 // Returned values range from 1.0 (unimpeded vision) to 11.0 (totally blind). 36 // 1.0 is LIGHT_AMBIENT_LIT or brighter 37 // 4.0 is a dark clear night, barely bright enough for reading and crafting 38 // 6.0 is LIGHT_AMBIENT_DIM 39 // 7.3 is LIGHT_AMBIENT_MINIMAL, a dark cloudy night, unlit indoors 40 // 11.0 is zero light or blindness 41 // 42 // TODO: Test 'pos' (position) parameter to fine_detail_vision_mod 43 // 44 TEST_CASE( "light and fine_detail_vision_mod", "[character][sight][light][vision]" ) 45 { 46 Character &dummy = get_player_character(); 47 map &here = get_map(); 48 49 clear_avatar(); 50 clear_map(); 51 g->reset_light_level(); 52 53 SECTION( "full daylight" ) { 54 // Set clock to noon 55 calendar::turn = calendar::turn_zero + 12_hours; 56 // Build map cache including lightmap 57 here.build_map_cache( 0, false ); 58 REQUIRE( g->is_in_sunlight( dummy.pos() ) ); 59 // ambient_light_at is 100.0 in full sun (this fails if lightmap cache is not built) 60 REQUIRE( here.ambient_light_at( dummy.pos() ) == Approx( 100.0f ) ); 61 62 // 1.0 is LIGHT_AMBIENT_LIT or brighter 63 CHECK( dummy.fine_detail_vision_mod() == Approx( 1.0f ) ); 64 } 65 66 SECTION( "wielding a bright lamp" ) { 67 item lamp( "atomic_lamp" ); 68 dummy.wield( lamp ); 69 REQUIRE( dummy.active_light() == Approx( 15.0f ) ); 70 71 // 1.0 is LIGHT_AMBIENT_LIT or brighter 72 CHECK( dummy.fine_detail_vision_mod() == Approx( 1.0f ) ); 73 } 74 75 // TODO: 76 // 4.0 is a dark clear night, barely bright enough for reading and crafting 77 // 6.0 is LIGHT_AMBIENT_DIM 78 79 SECTION( "midnight with a new moon" ) { 80 calendar::turn = calendar::turn_zero; 81 here.build_map_cache( 0, false ); 82 REQUIRE_FALSE( g->is_in_sunlight( dummy.pos() ) ); 83 REQUIRE( here.ambient_light_at( dummy.pos() ) == Approx( LIGHT_AMBIENT_MINIMAL ) ); 84 85 // 7.3 is LIGHT_AMBIENT_MINIMAL, a dark cloudy night, unlit indoors 86 CHECK( dummy.fine_detail_vision_mod() == Approx( 7.3f ) ); 87 } 88 89 SECTION( "blindfolded" ) { 90 dummy.wear_item( item( "blindfold" ) ); 91 REQUIRE( dummy.worn_with_flag( flag_BLIND ) ); 92 93 // 11.0 is zero light or blindness 94 CHECK( dummy.fine_detail_vision_mod() == Approx( 11.0f ) ); 95 } 96 } 97 98 // Sight limits 99 // 100 // Character::unimpaired_range() returns the maximum sight range, factoring in the effects of 101 // clothing (blindfold or corrective lenses), mutations (nearsightedness or ursine eyes), effects 102 // (boomered or darkness), or being underwater without suitable eye protection. 103 // 104 // Character::sight_impaired() returns true if sight is thus restricted. 105 // 106 TEST_CASE( "character sight limits", "[character][sight][vision]" ) 107 { 108 Character &dummy = get_player_character(); 109 map &here = get_map(); 110 111 clear_avatar(); 112 clear_map(); 113 g->reset_light_level(); 114 115 GIVEN( "it is midnight with a new moon" ) { 116 calendar::turn = calendar::turn_zero; 117 here.build_map_cache( 0, false ); 118 REQUIRE_FALSE( g->is_in_sunlight( dummy.pos() ) ); 119 120 THEN( "sight limit is 60 tiles away" ) { 121 dummy.recalc_sight_limits(); 122 CHECK( dummy.unimpaired_range() == 60 ); 123 } 124 } 125 126 WHEN( "blindfolded" ) { 127 dummy.wear_item( item( "blindfold" ) ); 128 REQUIRE( dummy.worn_with_flag( flag_BLIND ) ); 129 130 THEN( "impaired sight, with 0 tiles of range" ) { 131 dummy.recalc_sight_limits(); 132 CHECK( dummy.sight_impaired() ); 133 CHECK( dummy.unimpaired_range() == 0 ); 134 } 135 } 136 137 WHEN( "boomered" ) { 138 dummy.add_effect( efftype_id( "boomered" ), 1_minutes ); 139 REQUIRE( dummy.has_effect( efftype_id( "boomered" ) ) ); 140 141 THEN( "impaired sight, with 1 tile of range" ) { 142 dummy.recalc_sight_limits(); 143 CHECK( dummy.sight_impaired() ); 144 CHECK( dummy.unimpaired_range() == 1 ); 145 } 146 } 147 148 WHEN( "nearsighted" ) { 149 dummy.toggle_trait( trait_id( "MYOPIC" ) ); 150 REQUIRE( dummy.has_trait( trait_id( "MYOPIC" ) ) ); 151 152 WHEN( "without glasses" ) { 153 dummy.worn.clear(); 154 REQUIRE_FALSE( dummy.worn_with_flag( flag_FIX_NEARSIGHT ) ); 155 156 THEN( "impaired sight, with 4 tiles of range" ) { 157 dummy.recalc_sight_limits(); 158 CHECK( dummy.sight_impaired() ); 159 CHECK( dummy.unimpaired_range() == 4 ); 160 } 161 } 162 163 WHEN( "wearing glasses" ) { 164 dummy.wear_item( item( "glasses_eye" ) ); 165 REQUIRE( dummy.worn_with_flag( flag_FIX_NEARSIGHT ) ); 166 167 THEN( "unimpaired sight, with 60 tiles of range" ) { 168 dummy.recalc_sight_limits(); 169 CHECK_FALSE( dummy.sight_impaired() ); 170 CHECK( dummy.unimpaired_range() == 60 ); 171 } 172 } 173 } 174 175 GIVEN( "darkness effect" ) { 176 dummy.add_effect( efftype_id( "darkness" ), 1_minutes ); 177 REQUIRE( dummy.has_effect( efftype_id( "darkness" ) ) ); 178 179 THEN( "impaired sight, with 10 tiles of range" ) { 180 dummy.recalc_sight_limits(); 181 CHECK( dummy.sight_impaired() ); 182 CHECK( dummy.unimpaired_range() == 10 ); 183 } 184 } 185 } 186 187 // Special case of impaired sight - URSINE_EYES mutation causes severely reduced daytime vision 188 // equivalent to being nearsighted, which can be corrected with glasses. However, they have a 189 // nighttime vision range that exceeds that of normal characters. 190 // 191 // Contrary to its name, the range returned by unimpaired_range() represents maximum visibility WITH 192 // IMPAIRMENTS (that is, affected by the same things that cause sight_impaired() to return true). 193 // 194 // The sight_max computed by recalc_sight_limits does not include is the Beer-Lambert light 195 // attenuation of a given light level; this is handled by sight_range(), which returns a value from 196 // [1 .. sight_max]. 197 // 198 // FIXME: Rename unimpaired_range() to impaired_range() 199 // (it specifically includes all the things that impair visibility) 200 // 201 TEST_CASE( "ursine vision", "[character][ursine][vision]" ) 202 { 203 Character &dummy = get_player_character(); 204 map &here = get_map(); 205 206 clear_avatar(); 207 clear_map(); 208 g->reset_light_level(); 209 210 float light_here = 0.0f; 211 212 GIVEN( "character with ursine eyes and no eyeglasses" ) { 213 dummy.toggle_trait( trait_id( "URSINE_EYE" ) ); 214 REQUIRE( dummy.has_trait( trait_id( "URSINE_EYE" ) ) ); 215 216 dummy.worn.clear(); 217 REQUIRE_FALSE( dummy.worn_with_flag( flag_FIX_NEARSIGHT ) ); 218 219 WHEN( "under a new moon" ) { 220 calendar::turn = calendar::turn_zero; 221 here.build_map_cache( 0, false ); 222 light_here = here.ambient_light_at( dummy.pos() ); 223 REQUIRE( light_here == Approx( LIGHT_AMBIENT_MINIMAL ) ); 224 225 THEN( "unimpaired sight, with 4 tiles of range" ) { 226 dummy.recalc_sight_limits(); 227 CHECK_FALSE( dummy.sight_impaired() ); 228 CHECK( dummy.unimpaired_range() == 60 ); 229 CHECK( dummy.sight_range( light_here ) == 4 ); 230 } 231 } 232 233 WHEN( "under a half moon" ) { 234 calendar::turn = calendar::turn_zero + 7_days; 235 here.build_map_cache( 0, false ); 236 light_here = here.ambient_light_at( dummy.pos() ); 237 REQUIRE( light_here == Approx( LIGHT_AMBIENT_DIM ).margin( 1.0f ) ); 238 239 THEN( "unimpaired sight, with 10 tiles of range" ) { 240 dummy.recalc_sight_limits(); 241 CHECK_FALSE( dummy.sight_impaired() ); 242 CHECK( dummy.unimpaired_range() == 60 ); 243 CHECK( dummy.sight_range( light_here ) == 10 ); 244 } 245 } 246 247 WHEN( "under a full moon" ) { 248 calendar::turn = calendar::turn_zero + 14_days; 249 here.build_map_cache( 0, false ); 250 light_here = here.ambient_light_at( dummy.pos() ); 251 REQUIRE( light_here == Approx( LIGHT_AMBIENT_LIT ) ); 252 253 THEN( "unimpaired sight, with 27 tiles of range" ) { 254 dummy.recalc_sight_limits(); 255 CHECK_FALSE( dummy.sight_impaired() ); 256 CHECK( dummy.unimpaired_range() == 60 ); 257 CHECK( dummy.sight_range( light_here ) == 27 ); 258 } 259 } 260 261 WHEN( "under the noonday sun" ) { 262 calendar::turn = calendar::turn_zero + 12_hours; 263 here.build_map_cache( 0, false ); 264 light_here = here.ambient_light_at( dummy.pos() ); 265 REQUIRE( g->is_in_sunlight( dummy.pos() ) ); 266 REQUIRE( light_here == Approx( 100.0f ) ); 267 268 THEN( "impaired sight, with 4 tiles of range" ) { 269 dummy.recalc_sight_limits(); 270 CHECK( dummy.sight_impaired() ); 271 CHECK( dummy.unimpaired_range() == 4 ); 272 CHECK( dummy.sight_range( light_here ) == 4 ); 273 } 274 275 // Glasses can correct Ursine Vision in bright light 276 AND_WHEN( "wearing glasses" ) { 277 dummy.wear_item( item( "glasses_eye" ) ); 278 REQUIRE( dummy.worn_with_flag( flag_FIX_NEARSIGHT ) ); 279 280 THEN( "unimpaired sight, with 87 tiles of range" ) { 281 dummy.recalc_sight_limits(); 282 CHECK_FALSE( dummy.sight_impaired() ); 283 CHECK( dummy.unimpaired_range() == 60 ); 284 CHECK( dummy.sight_range( light_here ) == 87 ); 285 } 286 } 287 } 288 } 289 } 290 291