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