diff --git a/src/calendar.cpp b/src/calendar.cpp index c207f1111c91c..e71c63904607c 100644 --- a/src/calendar.cpp +++ b/src/calendar.cpp @@ -8,11 +8,17 @@ #include #include "cata_assert.h" +#include "cata_utility.h" #include "debug.h" +#include "enum_conversions.h" +#include "line.h" +#include "optional.h" #include "options.h" #include "rng.h" #include "string_formatter.h" #include "translations.h" +#include "units.h" +#include "units_utility.h" /** How much light moon provides per lit-up quarter (Full-moon light is four times this value) */ static constexpr double moonlight_per_quarter = 2.25; @@ -24,37 +30,23 @@ const time_duration calendar::INDEFINITELY_LONG_DURATION( static bool is_eternal_season = false; static int cur_season_length = 1; -const time_point calendar::before_time_starts = time_point::from_turn( -1 ); -const time_point calendar::turn_zero = time_point::from_turn( 0 ); - time_point calendar::start_of_cataclysm = calendar::turn_zero; time_point calendar::start_of_game = calendar::turn_zero; time_point calendar::turn = calendar::turn_zero; season_type calendar::initial_season = SPRING; -// Internal constants, not part of the calendar interface. -// Times for sunrise, sunset at equinoxes - -/** Hour of sunrise at winter solstice */ -static constexpr int sunrise_winter = 7; - -/** Hour of sunrise at summer solstice */ -static constexpr int sunrise_summer = 5; - -/** Hour of sunrise at fall and spring equinox */ -static constexpr int sunrise_equinox = ( sunrise_summer + sunrise_winter ) / 2; - -/** Hour of sunset at winter solstice */ -static constexpr int sunset_winter = 17; +// The solar altitudes at which light changes in various ways +static constexpr units::angle min_sun_angle_for_day = -12_degrees; +static constexpr units::angle max_sun_angle_for_night = 0_degrees; +static constexpr units::angle min_sun_angle_for_twilight = -18_degrees; +static constexpr units::angle max_sun_angle_for_twilight = 1_degrees; -/** Hour of sunset at summer solstice */ -static constexpr int sunset_summer = 21; - -/** Hour of sunset at fall and spring equinox */ -static constexpr int sunset_equinox = ( sunset_summer + sunset_winter ) / 2; - -// How long, does sunrise/sunset last? -static const time_duration twilight_duration = 1_hours; +static_assert( min_sun_angle_for_day <= max_sun_angle_for_night, "day and night must overlap" ); +static_assert( min_sun_angle_for_day <= max_sun_angle_for_twilight, + "day and twilight must overlap" ); +static_assert( min_sun_angle_for_twilight <= max_sun_angle_for_night, + "twilight and night must overlap" ); +static_assert( min_sun_angle_for_twilight <= max_sun_angle_for_twilight, "twilight must exist" ); double default_daylight_level() { @@ -101,149 +93,319 @@ moon_phase get_moon_phase( const time_point &p ) return static_cast( current_phase ); } -// TODO: Refactor sunrise / sunset -// The only difference between them is the start_hours array -time_point sunrise( const time_point &p ) +static constexpr time_duration angle_to_time( const units::angle a ) { - static_assert( static_cast( SPRING ) == 0, - "Expected spring to be the first season. If not, code below will use wrong index into array" ); + return a / 15.0_degrees * 1_hours; +} - static const std::array start_hours = { { sunrise_equinox, sunrise_summer, sunrise_equinox, sunrise_winter, } }; - const size_t season = static_cast( season_of_year( p ) ); - cata_assert( season < start_hours.size() ); +// To support the eternal season option we create a strong typedef of timepoint +// which is a solar_effective_time. This converts a regular time to a time +// which would be relevant for sun position calculations. Normally the two +// times are the same, but when eternal seasons are used the effective time is +// always set to the same day, so that the sun position doesn't change from day +// to day. +struct solar_effective_time { + explicit solar_effective_time( const time_point &t_ ) + : t( t_ ) { + if( calendar::eternal_season() ) { + const time_point start_midnight = + calendar::start_of_game - time_past_midnight( calendar::start_of_game ); + t = start_midnight + time_past_midnight( t_ ); + } + } + time_point t; +}; - const double start_hour = start_hours[season]; - const double end_hour = start_hours[( season + 1 ) % 4]; +static std::pair sun_ra_declination( + solar_effective_time t, time_duration timezone ) +{ + // This derivation is mostly from + // https://en.wikipedia.org/wiki/Position_of_the_Sun + // https://en.wikipedia.org/wiki/Celestial_coordinate_system#Notes_on_conversion + + // The computation is inspired by the derivation based on J2000 (Greenwich + // noon, 2000-01-01), but because we want to be capable of handling a + // different year length than the real Earth, we don't use the same exact + // values. + // Instead we use as our epoch a point that won't change arbitrarily with a + // different year length - Greenwich midnight on the vernal equinox + // (note that the vernal equinox happens to be Spring day 1 in the game + // calendar, which is convenient). + const double days_since_epoch = to_days( t.t - calendar::turn_zero - timezone ); + + // The angle per day the Earth moves around the Sun + const units::angle angle_per_day = 360_degrees / to_days( calendar::year_length() ); - const double into_month = static_cast( day_of_season( p ) ) / to_days - ( calendar::season_length() ); - const double time = start_hour * ( 1.0 - into_month ) + end_hour * into_month; + // It turns out that we want mean longitude to be zero at the vernal + // equinox, which simplifies the calculations. + const units::angle mean_long = angle_per_day * days_since_epoch; + // Roughly 77 degrees offset between mean longitude and mean anomaly at + // J2000, so use that as our offset too. The relative drift is slow, so we + // neglect it. + const units::angle mean_anomaly = 77_degrees + mean_long; + // The two arbitrary constants in the caclulation of ecliptic longitude + // relate to the non-circularity of the Earth's orbit. + const units::angle ecliptic_longitude = + mean_long + 1.915_degrees * sin( mean_anomaly ) + 0.020_degrees * sin( 2 * mean_anomaly ); - const time_point midnight = p - time_past_midnight( p ); - return midnight + time_duration::from_minutes( static_cast( time * 60 ) ); + // Obliquity does vary slightly, but for simplicity we'll keep it fixed at + // its J2000 value. + static constexpr units::angle obliquity = 23.439279_degrees; + + // ecliptic rectangular coordinates + const rl_vec2d eclip( cos( ecliptic_longitude ), sin( ecliptic_longitude ) ); + // rotate to equatorial coordinates + const rl_vec3d rot( eclip.x, eclip.y * cos( obliquity ), eclip.y * sin( obliquity ) ); + const units::angle right_ascension = atan2( rot.xy() ); + const units::angle declination = units::asin( rot.z ); + return { right_ascension, declination }; } -time_point sunset( const time_point &p ) +static units::angle sidereal_time_at( solar_effective_time t, units::angle longitude, + time_duration timezone ) +{ + // Repeat some calculations from sun_ra_declination + const double days_since_epoch = to_days( t.t - calendar::turn_zero - timezone ); + const units::angle angle_per_day = 360_degrees / to_days( calendar::year_length() ); + + // Sidereal Time + // + // For the origin of sidereal time consider that at the epoch at Greenwich, + // it's midnight on the vernal equinox so sidereal time should be 180°. + // Timezone and longitude are both zero here, so L0 = 180°. + const units::angle L0 = 180_degrees; + // Sidereal time advances by 360° per day plus an additional 360° per year + const units::angle L1 = 360_degrees + angle_per_day; + return L0 + L1 * days_since_epoch + longitude; +} + +static std::pair sun_azimuth_altitude( + solar_effective_time t, lat_long location ) +{ + units::angle right_ascension; + units::angle declination; + time_duration timezone = angle_to_time( location.longitude ); + std::tie( right_ascension, declination ) = sun_ra_declination( t, timezone ); + const units::angle sidereal_time = sidereal_time_at( t, location.longitude, timezone ); + + const units::angle hour_angle = sidereal_time - right_ascension; + + // Use a two-step transformation to convert equatorial coordinates to + // horizontal. + // https://en.wikipedia.org/wiki/Celestial_coordinate_system#Equatorial_%E2%86%94_horizontal + const rl_vec3d intermediate( + cos( hour_angle ) * cos( declination ), + sin( hour_angle ) * cos( declination ), + sin( declination ) ); + + const rl_vec3d horizontal( + -intermediate.x * sin( location.latitude ) + + intermediate.z * cos( location.latitude ), + intermediate.y, + intermediate.x * cos( location.latitude ) + + intermediate.z * sin( location.latitude ) + ); + + // Azimuth is from the South, turning positive to the west + const units::angle azimuth = normalize( -atan2( horizontal.xy() ) + 180_degrees ); + const units::angle altitude = units::asin( horizontal.z ); + + /*printf( + "\n" + "right_ascension = %f, declination = %f\n" + "sidereal_time = %f, hour_angle = %f\n" + "aziumth = %f, altitude = %f\n", + to_degrees( right_ascension ), to_degrees( declination ), + to_degrees( sidereal_time ), to_degrees( hour_angle ), + to_degrees( azimuth ), to_degrees( altitude ) );*/ + + return std::make_pair( azimuth, altitude ); +} + +std::pair sun_azimuth_altitude( time_point t, lat_long location ) +{ + return sun_azimuth_altitude( solar_effective_time( t ), location ); +} + +static units::angle sun_altitude( time_point t, lat_long location ) +{ + return sun_azimuth_altitude( t, location ).second; +} + +static units::angle sun_altitude( time_point t ) +{ + return sun_altitude( t, location_boston ); +} + +cata::optional sunlight_angle( const time_point &t, lat_long location ) +{ + units::angle azimuth; + units::angle altitude; + std::tie( azimuth, altitude ) = sun_azimuth_altitude( t, location ); + if( altitude <= 0_degrees ) { + // Sun below horizon + return cata::nullopt; + } + rl_vec2d horizontal_direction( -sin( azimuth ), cos( azimuth ) ); + rl_vec3d direction( horizontal_direction * cos( altitude ), sin( altitude ) ); + direction /= -direction.z; + return direction.xy(); +} + +cata::optional sunlight_angle( const time_point &t ) +{ + return sunlight_angle( t, location_boston ); +} + +static time_point solar_noon_near( const time_point &t ) { - static_assert( static_cast( SPRING ) == 0, - "Expected spring to be the first season. If not, code below will use wrong index into array" ); + const time_point prior_midnight = t - time_past_midnight( t ); + return prior_midnight + 12_hours; + // If we were using a timezone rather than local solar time this would be + // calculated as follows: + //constexpr time_duration longitude_hours = angle_to_time( location_boston.longitude ); + //return prior_midnight + 12_hours - longitude_hours + timezone; +} - static const std::array start_hours = { { sunset_equinox, sunset_summer, sunset_equinox, sunset_winter, } }; - const size_t season = static_cast( season_of_year( p ) ); - cata_assert( season < start_hours.size() ); +static units::angle offset_to_sun_altitude( + const units::angle altitude, const units::angle longitude, + const solar_effective_time approx_time, const bool evening ) +{ + units::angle ra; + units::angle declination; + time_duration timezone = angle_to_time( longitude ); + std::tie( ra, declination ) = sun_ra_declination( approx_time, timezone ); + double cos_hour_angle = + ( sin( altitude ) - sin( location_boston.latitude ) * sin( declination ) ) / + cos( location_boston.latitude ) / cos( declination ); + if( std::abs( cos_hour_angle ) > 1 ) { + // It doesn't actually reach that angle, so we pretend that it does at + // its maximum possible angle + cos_hour_angle = cos_hour_angle > 0 ? 1 : -1; + } + units::angle hour_angle = units::acos( cos_hour_angle ); + if( !evening ) { + hour_angle = -hour_angle; + } + const units::angle target_sidereal_time = hour_angle + ra; + const units::angle sidereal_time_at_approx_time = + sidereal_time_at( approx_time, location_boston.longitude, timezone ); + return normalize( target_sidereal_time - sidereal_time_at_approx_time ); +} - const double start_hour = start_hours[season]; - const double end_hour = start_hours[( season + 1 ) % 4]; +static time_point sun_at_altitude( const units::angle altitude, const units::angle longitude, + const time_point t, const bool evening ) +{ + const time_point solar_noon = solar_noon_near( t ); + units::angle initial_offset = + offset_to_sun_altitude( altitude, longitude, solar_effective_time( solar_noon ), evening ); + if( !evening ) { + initial_offset -= 360_degrees; + } + const time_duration initial_offset_time = angle_to_time( initial_offset ); + const time_point initial_approximation = solar_noon + initial_offset_time; + // Now we should have the correct time to within a few minutes; iterate to + // get a more precise estimate + units::angle correction_offset = + offset_to_sun_altitude( altitude, longitude, solar_effective_time( initial_approximation ), + evening ); + if( correction_offset > 180_degrees ) { + correction_offset -= 360_degrees; + } + const time_duration correction_offset_time = angle_to_time( correction_offset ); + return initial_approximation + correction_offset_time; +} - const double into_month = static_cast( day_of_season( p ) ) / to_days - ( calendar::season_length() ); - const double time = start_hour * ( 1.0 - into_month ) + end_hour * into_month; +time_point sunrise( const time_point &p ) +{ + return sun_at_altitude( 0_degrees, location_boston.longitude, p, false ); +} - const time_point midnight = p - time_past_midnight( p ); - return midnight + time_duration::from_minutes( static_cast( time * 60 ) ); +time_point sunset( const time_point &p ) +{ + return sun_at_altitude( 0_degrees, location_boston.longitude, p, true ); } time_point night_time( const time_point &p ) { - return sunset( p ) + twilight_duration; + return sun_at_altitude( min_sun_angle_for_day, location_boston.longitude, p, true ); } time_point daylight_time( const time_point &p ) { - // TODO: Actual daylight should start 18 degrees before sunrise - return sunrise( p ) + 15_minutes; + return sun_at_altitude( min_sun_angle_for_day, location_boston.longitude, p, false ); } bool is_night( const time_point &p ) { - const time_duration now = time_past_midnight( p ); - const time_duration sunrise = time_past_midnight( ::sunrise( p ) ); - const time_duration sunset = time_past_midnight( ::sunset( p ) ); - - return now >= sunset + twilight_duration || now <= sunrise; + return sun_altitude( p ) <= min_sun_angle_for_day; } bool is_day( const time_point &p ) { - const time_duration now = time_past_midnight( p ); - const time_duration sunrise = time_past_midnight( ::sunrise( p ) ); - const time_duration sunset = time_past_midnight( ::sunset( p ) ); + return sun_altitude( p ) >= 0_degrees; +} - return now >= sunrise + twilight_duration && now <= sunset; +static bool is_twilight( const time_point &p ) +{ + units::angle altitude = sun_altitude( p ); + return altitude >= min_sun_angle_for_twilight && altitude <= max_sun_angle_for_twilight; } bool is_dusk( const time_point &p ) { const time_duration now = time_past_midnight( p ); - const time_duration sunset = time_past_midnight( ::sunset( p ) ); - - return now >= sunset && now <= sunset + twilight_duration; + return now > 12_hours && is_twilight( p ); } bool is_dawn( const time_point &p ) { const time_duration now = time_past_midnight( p ); - const time_duration sunrise = time_past_midnight( ::sunrise( p ) ); - - return now >= sunrise && now <= sunrise + twilight_duration; -} - -double current_daylight_level( const time_point &p ) -{ - const double percent = static_cast( day_of_season( p ) ) / to_days - ( calendar::season_length() ); - double modifier = 1.0; - // For ~Boston: solstices are +/- 25% sunlight intensity from equinoxes - static double deviation = 0.25; - - switch( season_of_year( p ) ) { - case SPRING: - modifier = 1. + ( percent * deviation ); - break; - case SUMMER: - modifier = ( 1. + deviation ) - ( percent * deviation ); - break; - case AUTUMN: - modifier = 1. - ( percent * deviation ); - break; - case WINTER: - modifier = ( 1. - deviation ) + ( percent * deviation ); - break; - default: - debugmsg( "Invalid season" ); - } - - return modifier * default_daylight_level(); + return now < 12_hours && is_twilight( p ); } -float sunlight( const time_point &p, const bool vision ) +static float moon_light_at( const time_point &p ) { - const time_duration now = time_past_midnight( p ); - - const double daylight = current_daylight_level( p ); - int current_phase = static_cast( get_moon_phase( p ) ); if( current_phase > static_cast( MOON_PHASE_MAX ) / 2 ) { current_phase = static_cast( MOON_PHASE_MAX ) - current_phase; } - const double moonlight = vision ? 1. + moonlight_per_quarter * current_phase : 0.; - - if( is_night( p ) ) { - return moonlight; - } else if( is_dawn( p ) ) { - const time_duration sunrise = time_past_midnight( ::sunrise( p ) ); - const double percent = ( now - sunrise ) / twilight_duration; - return moonlight * ( 1. - percent ) + daylight * percent; - } else if( is_dusk( p ) ) { - const time_duration sunset = time_past_midnight( ::sunset( p ) ); - const double percent = ( now - sunset ) / twilight_duration; - return daylight * ( 1. - percent ) + moonlight * percent; + return 1. + moonlight_per_quarter * current_phase; +} + +float sun_light_at( const time_point &p ) +{ + const units::angle solar_alt = sun_altitude( p ); + // For ~Boston: solstices are +/- 25% sunlight intensity from equinoxes. + // These values yield roughly that range (see sun_test.cpp) + static constexpr double light_at_zero_altitude = 60; + static constexpr double max_light = 125; + + if( solar_alt < min_sun_angle_for_twilight ) { + return 0; + } else if( solar_alt <= max_sun_angle_for_night ) { + // Sunlight rises exponentially from 0 to 60 as sun rises from -18° to 0° + return light_at_zero_altitude * + ( std::exp2( 1 - solar_alt / min_sun_angle_for_twilight ) - 1 ); } else { - return daylight; + // Linear increase from 0° to 70° degrees light increases from 60 to 125 brightness. + const double lerp_param = solar_alt / 70_degrees; + return lerp_clamped( light_at_zero_altitude, max_light, lerp_param ); } } +float sun_moon_light_at( const time_point &p ) +{ + return sun_light_at( p ) + moon_light_at( p ); +} + +double sun_moon_light_at_noon_near( const time_point &p ) +{ + const time_point solar_noon = solar_noon_near( p ); + return sun_moon_light_at( solar_noon ); +} + static std::string to_string_clipped( const int num, const clipped_unit type, const clipped_align align ) { diff --git a/src/calendar.h b/src/calendar.h index f9849e51ef922..8a9cde2fab3ba 100644 --- a/src/calendar.h +++ b/src/calendar.h @@ -6,12 +6,23 @@ #include #include +#include "units_fwd.h" + class JsonIn; class JsonOut; +struct lat_long; +struct rl_vec2d; class time_duration; class time_point; template struct enum_traits; +namespace cata +{ +template +class optional; +} // namespace cata + + /** Real world seasons */ enum season_type { SPRING = 0, @@ -109,18 +120,6 @@ extern time_point start_of_game; extern time_point turn; extern season_type initial_season; -/** - * A time point that is always before the current turn, even when the game has - * just started. This implies `before_time_starts < calendar::turn` is always - * true. It can be used to initialize `time_point` values that denote that last - * time a cache was update. - */ -extern const time_point before_time_starts; -/** - * Represents time point 0. - * TODO: flesh out the documentation - */ -extern const time_point turn_zero; } // namespace calendar template @@ -504,6 +503,25 @@ class time_point // TODO: implement minutes_of_hour and so on and use it. }; +namespace calendar +{ + +/** + * A time point that is always before the current turn, even when the game has + * just started. This implies `before_time_starts < calendar::turn` is always + * true. It can be used to initialize `time_point` values that denote the last + * time a cache was update. + */ +constexpr time_point before_time_starts = time_point::from_turn( -1 ); +/** + * Represents time point 0. + * TODO: flesh out the documentation + */ + +constexpr time_point turn_zero = time_point::from_turn( 0 ); + +} // namespace calendar + inline time_duration time_past_midnight( const time_point &p ) { return ( p - calendar::turn_zero ) % 1_days; @@ -560,15 +578,30 @@ bool is_day( const time_point &p ); bool is_dusk( const time_point &p ); /** Returns true if it's currently dawn - between sunrise and twilight_duration after sunrise. */ bool is_dawn( const time_point &p ); -/** Returns the current seasonally-adjusted maximum daylight level */ -double current_daylight_level( const time_point &p ); /** How much light is provided in full daylight */ double default_daylight_level(); -/** Returns the current sunlight or moonlight level through the preceding functions. - * By default, returns sunlight level for vision, with moonlight providing a measurable amount - * of light. with vision == false, returns sunlight for solar panel purposes, and moonlight - * provides 0 light */ -float sunlight( const time_point &p, bool vision = true ); +/** Returns the current sunlight. + * Based entirely on astronomical circumstances; does not account for e.g. + * weather. + * For most situations you actually want to call the below function which also + * includes moonlight. */ +float sun_light_at( const time_point &p ); +/** Returns the current sunlight plus moonlight level. + * Based entirely on astronomical circumstances; does not account for e.g. + * weather. */ +float sun_moon_light_at( const time_point &p ); +/** How much light is provided at the solar noon nearest to given time */ +double sun_moon_light_at_noon_near( const time_point &p ); + +std::pair sun_azimuth_altitude( time_point, lat_long ); + +/** Returns the offset by which a ray of sunlight would move when shifting down + * one z-level, or nullopt if the sun is below the horizon. + * + * If lat_long not provided it defaults to Boston. + */ +cata::optional sunlight_angle( const time_point &, lat_long ); +cata::optional sunlight_angle( const time_point & ); enum class weekdays : int { SUNDAY = 0, diff --git a/src/character.cpp b/src/character.cpp index bec02bfbbbb29..2577bde6df01b 100644 --- a/src/character.cpp +++ b/src/character.cpp @@ -11995,14 +11995,17 @@ bool Character::sees_with_infrared( const Creature &critter ) const } map &here = get_map(); + // Use range based on default daylight, not actual current light, since + // we're seeing in infra red not via light. + int range = sight_range( default_daylight_level() ); + if( is_player() || critter.is_player() ) { // Players should not use map::sees // Likewise, players should not be "looked at" with map::sees, not to break symmetry - return here.pl_line_of_sight( critter.pos(), - sight_range( current_daylight_level( calendar::turn ) ) ); + return here.pl_line_of_sight( critter.pos(), range ); } - return here.sees( pos(), critter.pos(), sight_range( current_daylight_level( calendar::turn ) ) ); + return here.sees( pos(), critter.pos(), range ); } bool Character::is_visible_in_range( const Creature &critter, const int range ) const diff --git a/src/editmap.cpp b/src/editmap.cpp index 8c420e6a0bc32..5c01d7832891b 100644 --- a/src/editmap.cpp +++ b/src/editmap.cpp @@ -744,9 +744,9 @@ void editmap::update_view_with_help( const std::string &txt, const std::string & const std::string u_see_msg = player_character.sees( target ) ? _( "yes" ) : _( "no" ); mvwprintw( w_info, point( 1, off++ ), _( "dist: %d u_see: %s veh: %s scent: %d" ), rl_dist( player_character.pos(), target ), u_see_msg, veh_msg, get_scent().get( target ) ); - mvwprintw( w_info, point( 1, off++ ), _( "sight_range: %d, daylight_sight_range: %d," ), + mvwprintw( w_info, point( 1, off++ ), _( "sight_range: %d, noon_sight_range: %d," ), player_character.sight_range( g->light_level( player_character.posz() ) ), - player_character.sight_range( current_daylight_level( calendar::turn ) ) ); + player_character.sight_range( sun_moon_light_at_noon_near( calendar::turn ) ) ); mvwprintw( w_info, point( 1, off++ ), _( "cache{transp:%.4f seen:%.4f cam:%.4f}" ), map_cache.transparency_cache[target.x][target.y], map_cache.seen_cache[target.x][target.y], diff --git a/src/game.cpp b/src/game.cpp index e7456a56cd51b..cada907afaeb3 100644 --- a/src/game.cpp +++ b/src/game.cpp @@ -4033,7 +4033,7 @@ float game::natural_light_level( const int zlev ) const // Sunlight/moonlight related stuff if( !weather.lightning_active ) { - ret = sunlight( calendar::turn ); + ret = sun_moon_light_at( calendar::turn ); } else { // Recent lightning strike has lit the area ret = default_daylight_level(); @@ -7656,8 +7656,9 @@ void game::reset_item_list_state( const catacurses::window &window, int height, void game::list_items_monsters() { - std::vector mons = u.get_visible_creatures( current_daylight_level( calendar::turn ) ); - // whole reality bubble + // Search whole reality bubble because each function internally verifies + // the visibilty of the items / monsters in question. + std::vector mons = u.get_visible_creatures( 60 ); const std::vector items = find_nearby_items( 60 ); if( mons.empty() && items.empty() ) { diff --git a/src/line.cpp b/src/line.cpp index 4c3959731b03f..4ce5ee8821612 100644 --- a/src/line.cpp +++ b/src/line.cpp @@ -276,6 +276,11 @@ units::angle atan2( const point &p ) return units::atan2( p.y, p.x ); } +units::angle atan2( const rl_vec2d &p ) +{ + return units::atan2( p.y, p.x ); +} + // This more general version of this function gives correct values for larger values. unsigned make_xyz( const tripoint &p ) { @@ -562,6 +567,11 @@ std::vector squares_in_direction( const point &p1, const point &p2 ) return adjacent_squares; } +rl_vec2d rl_vec3d::xy() const +{ + return rl_vec2d( x, y ); +} + float rl_vec2d::magnitude() const { return std::sqrt( x * x + y * y ); @@ -661,13 +671,17 @@ rl_vec2d rl_vec2d::operator*( const float rhs ) const return ret; } +rl_vec3d &rl_vec3d::operator*=( const float rhs ) +{ + x *= rhs; + y *= rhs; + z *= rhs; + return *this; +} + rl_vec3d rl_vec3d::operator*( const float rhs ) const { - rl_vec3d ret; - ret.x = x * rhs; - ret.y = y * rhs; - ret.z = z * rhs; - return ret; + return rl_vec3d( *this ) *= rhs; } // subtract @@ -731,13 +745,17 @@ rl_vec2d rl_vec2d::operator/( const float rhs ) const return ret; } +rl_vec3d &rl_vec3d::operator/=( const float rhs ) +{ + x /= rhs; + y /= rhs; + z /= rhs; + return *this; +} + rl_vec3d rl_vec3d::operator/( const float rhs ) const { - rl_vec3d ret; - ret.x = x / rhs; - ret.y = y / rhs; - ret.z = z / rhs; - return ret; + return rl_vec3d( *this ) /= rhs; } void calc_ray_end( units::angle angle, const int range, const tripoint &p, tripoint &out ) diff --git a/src/line.h b/src/line.h index 7605c281cd6df..2fb900cd4d880 100644 --- a/src/line.h +++ b/src/line.h @@ -11,6 +11,8 @@ #include "point.h" #include "units_fwd.h" +struct rl_vec2d; + extern bool trigdist; /** @@ -231,6 +233,7 @@ int manhattan_dist( const point &loc1, const point &loc2 ); // get angle of direction represented by point units::angle atan2( const point & ); +units::angle atan2( const rl_vec2d & ); // Get the magnitude of the slope ranging from 0.0 to 1.0 float get_normalized_angle( const point &start, const point &end ); @@ -282,8 +285,11 @@ struct rl_vec3d { float z; explicit rl_vec3d( float x = 0, float y = 0, float z = 0 ) : x( x ), y( y ), z( z ) {} + explicit rl_vec3d( const rl_vec2d &xy, float z = 0 ) : x( xy.x ), y( xy.y ), z( z ) {} explicit rl_vec3d( const tripoint &p ) : x( p.x ), y( p.y ), z( p.z ) {} + rl_vec2d xy() const; + float magnitude() const; rl_vec3d normalized() const; rl_vec3d rotated( float angle ) const; @@ -293,13 +299,15 @@ struct rl_vec3d { tripoint as_point() const; // scale. - rl_vec3d operator* ( float rhs ) const; - rl_vec3d operator/ ( float rhs ) const; + rl_vec3d &operator*=( float rhs ); + rl_vec3d &operator/=( float rhs ); + rl_vec3d operator*( float rhs ) const; + rl_vec3d operator/( float rhs ) const; // subtract - rl_vec3d operator- ( const rl_vec3d &rhs ) const; + rl_vec3d operator-( const rl_vec3d &rhs ) const; // unary negation - rl_vec3d operator- () const; - rl_vec3d operator+ ( const rl_vec3d &rhs ) const; + rl_vec3d operator-() const; + rl_vec3d operator+( const rl_vec3d &rhs ) const; }; #endif // CATA_SRC_LINE_H diff --git a/src/units.h b/src/units.h index 86b8828e1c426..fe494bbc97757 100644 --- a/src/units.h +++ b/src/units.h @@ -860,6 +860,16 @@ inline units::angle atan2( double y, double x ) return from_radians( std::atan2( y, x ) ); } +inline units::angle asin( double x ) +{ + return from_radians( std::asin( x ) ); +} + +inline units::angle acos( double x ) +{ + return from_radians( std::acos( x ) ); +} + static const std::vector> energy_units = { { { "mJ", 1_mJ }, { "J", 1_J }, diff --git a/src/units_utility.h b/src/units_utility.h index d16869e434311..50b5f277935f3 100644 --- a/src/units_utility.h +++ b/src/units_utility.h @@ -31,6 +31,13 @@ units::quantity round_to_multiple_of( units::quantity val, units::qu return multiple * of; } +struct lat_long { + units::angle latitude; + units::angle longitude; +}; + +constexpr lat_long location_boston{ 42.36_degrees, -71.06_degrees }; + /** * Create a units label for a weight value. * diff --git a/src/weather.cpp b/src/weather.cpp index ff04584a1674a..119eb8d2a87df 100644 --- a/src/weather.cpp +++ b/src/weather.cpp @@ -130,7 +130,7 @@ void glare( const weather_type_id &w ) int incident_sunlight( const weather_type_id &wtype, const time_point &t ) { - return std::max( 0.0f, sunlight( t, false ) + wtype->light_modifier ); + return std::max( 0.0f, sun_light_at( t ) + wtype->light_modifier ); } static inline void proc_weather_sum( const weather_type_id &wtype, weather_sum &data, diff --git a/tests/char_sight_test.cpp b/tests/char_sight_test.cpp index fe788b1bec57e..9514cc6185e85 100644 --- a/tests/char_sight_test.cpp +++ b/tests/char_sight_test.cpp @@ -56,8 +56,8 @@ TEST_CASE( "light and fine_detail_vision_mod", "[character][sight][light][vision // Build map cache including lightmap here.build_map_cache( 0, false ); REQUIRE( g->is_in_sunlight( dummy.pos() ) ); - // ambient_light_at is 100.0 in full sun (this fails if lightmap cache is not built) - REQUIRE( here.ambient_light_at( dummy.pos() ) == Approx( 100.0f ) ); + // ambient_light_at is ~100.0 in full sun (this fails if lightmap cache is not built) + REQUIRE( here.ambient_light_at( dummy.pos() ) == Approx( 100.0f ).margin( 10 ) ); // 1.0 is LIGHT_AMBIENT_LIT or brighter CHECK( dummy.fine_detail_vision_mod() == Approx( 1.0f ) ); @@ -263,7 +263,7 @@ TEST_CASE( "ursine vision", "[character][ursine][vision]" ) here.build_map_cache( 0, false ); light_here = here.ambient_light_at( dummy.pos() ); REQUIRE( g->is_in_sunlight( dummy.pos() ) ); - REQUIRE( light_here == Approx( 100.0f ) ); + REQUIRE( light_here == Approx( 100.0f ).margin( 10 ) ); THEN( "impaired sight, with 4 tiles of range" ) { dummy.recalc_sight_limits(); @@ -281,7 +281,7 @@ TEST_CASE( "ursine vision", "[character][ursine][vision]" ) dummy.recalc_sight_limits(); CHECK_FALSE( dummy.sight_impaired() ); CHECK( dummy.unimpaired_range() == 60 ); - CHECK( dummy.sight_range( light_here ) == 87 ); + CHECK( dummy.sight_range( light_here ) == 88 ); } } } diff --git a/tests/moon_test.cpp b/tests/moon_test.cpp index 55ef7e7bd8f3a..74b08f1d26bb7 100644 --- a/tests/moon_test.cpp +++ b/tests/moon_test.cpp @@ -216,30 +216,20 @@ TEST_CASE( "moonlight at dawn and dusk", "[calendar][moon][moonlight][dawn][dusk time_point new_sunset = sunset( new_midnight ); time_point new_noon = new_midnight + 12_hours; - // Daylight level should be 100 at first new moon - float daylight_level = current_daylight_level( new_noon ); - float half_twilight = ( daylight_level + 1.0f ) / 2.0f; + // Daylight level should be ~100 at first new moon + float daylight_level = sun_moon_light_at( new_noon ); + CHECK( daylight_level == Approx( 100 ).margin( 10 ) ); float moonlight_level = 1.0f; THEN( "at night, light is only moonlight" ) { - CHECK( sunlight( new_sunset + 61_minutes ) == moonlight_level ); - CHECK( sunlight( new_midnight ) == moonlight_level ); - CHECK( sunlight( new_sunrise - 1_minutes ) == moonlight_level ); + CHECK( sun_moon_light_at( new_sunset + 2_hours ) == moonlight_level ); + CHECK( sun_moon_light_at( new_midnight ) == moonlight_level ); + CHECK( sun_moon_light_at( new_sunrise - 2_hours ) == moonlight_level ); } THEN( "at dawn, light increases from moonlight to daylight" ) { - CHECK( sunlight( new_sunrise ) == moonlight_level ); - CHECK( sunlight( new_sunrise + 30_minutes ) == Approx( half_twilight ) ); - CHECK( sunlight( new_sunrise + 1_hours ) == daylight_level ); - } - THEN( "after dawn, until dusk, light is full daylight" ) { - CHECK( sunlight( new_sunrise + 61_minutes ) == daylight_level ); - CHECK( sunlight( new_noon ) == daylight_level ); - CHECK( sunlight( new_sunset - 1_minutes ) == daylight_level ); - } - THEN( "at dusk, light decreases from daylight to moonlight" ) { - CHECK( sunlight( new_sunset ) == daylight_level ); - CHECK( sunlight( new_sunset + 30_minutes ) == Approx( half_twilight ) ); - CHECK( sunlight( new_sunset + 1_hours ) == moonlight_level ); + CHECK( sun_moon_light_at( new_sunrise ) > sun_moon_light_at( new_sunrise - 2_hours ) ); + CHECK( sun_moon_light_at( new_sunrise + 1_hours ) == + sun_light_at( new_sunrise + 1_hours ) + moonlight_level ); } } @@ -252,29 +242,20 @@ TEST_CASE( "moonlight at dawn and dusk", "[calendar][moon][moonlight][dawn][dusk time_point full_noon = full_midnight + 12_hours; // Daylight level is higher, later in the season (~104 at first full moon) - float daylight_level = current_daylight_level( full_noon ); - float half_twilight = ( daylight_level + 10.0f ) / 2.0f; + float daylight_level = sun_moon_light_at( full_noon ); + CHECK( daylight_level == Approx( 120 ).margin( 10 ) ); float moonlight_level = 10.0f; THEN( "at night, light is only moonlight" ) { - CHECK( sunlight( full_sunset + 61_minutes ) == moonlight_level ); - CHECK( sunlight( full_midnight ) == moonlight_level ); - CHECK( sunlight( full_sunrise - 1_minutes ) == moonlight_level ); + CHECK( sun_moon_light_at( full_sunset + 2_hours ) == moonlight_level ); + CHECK( sun_moon_light_at( full_midnight ) == moonlight_level ); + CHECK( sun_moon_light_at( full_sunrise - 2_hours ) == moonlight_level ); } THEN( "at dawn, light increases from moonlight to daylight" ) { - CHECK( sunlight( full_sunrise ) == moonlight_level ); - CHECK( sunlight( full_sunrise + 30_minutes ) == Approx( half_twilight ) ); - CHECK( sunlight( full_sunrise + 1_hours ) == daylight_level ); - } - THEN( "after dawn, until dusk, light is full daylight" ) { - CHECK( sunlight( full_sunrise + 61_minutes ) == daylight_level ); - CHECK( sunlight( full_noon ) == daylight_level ); - CHECK( sunlight( full_sunset - 1_minutes ) == daylight_level ); - } - THEN( "at dusk, light decreases from daylight to moonlight" ) { - CHECK( sunlight( full_sunset ) == daylight_level ); - CHECK( sunlight( full_sunset + 30_minutes ) == Approx( half_twilight ) ); - CHECK( sunlight( full_sunset + 1_hours ) == moonlight_level ); + CHECK( sun_moon_light_at( full_sunrise ) > + sun_moon_light_at( full_sunrise - 2_hours ) ); + CHECK( sun_moon_light_at( full_sunrise + 1_hours ) == + sun_light_at( full_sunrise + 1_hours ) + moonlight_level ); } } } @@ -314,7 +295,7 @@ static float phase_moonlight( const float phase_scale, const moon_phase expect_p expect_phase_enum ) ); // Finally, get the amount of moonlight - return sunlight( this_night ); + return sun_moon_light_at( this_night ); } // Moonlight level varies with moon phase, from 1.0 at new moon to 10.0 at full moon. diff --git a/tests/stringmaker.h b/tests/stringmaker.h index d0eac3c374539..1cfd99232c790 100644 --- a/tests/stringmaker.h +++ b/tests/stringmaker.h @@ -41,6 +41,13 @@ struct StringMaker { } }; +template<> +struct StringMaker { + static std::string convert( const rl_vec2d &p ) { + return string_format( "rl_vec2d( %f, %f )", p.x, p.y ); + } +}; + template struct StringMaker> { static std::string convert( const rectangle &r ) { @@ -70,6 +77,14 @@ struct StringMaker { } }; +template<> +struct StringMaker { + static std::string convert( const time_point &d ) { + return string_format( + "time_point( %d ) [%s]", to_turns( d - calendar::turn_zero ), to_string( d ) ); + } +}; + template<> struct StringMaker { static std::string convert( const talk_response &r ) { diff --git a/tests/sun_test.cpp b/tests/sun_test.cpp index 5ffbe45abb807..197c94f072137 100644 --- a/tests/sun_test.cpp +++ b/tests/sun_test.cpp @@ -1,134 +1,123 @@ #include "cata_catch.h" #include "calendar.h" // IWYU pragma: associated +#include #include +#include + +#include "hash_utils.h" +#include "line.h" +#include "options_helpers.h" +#include "optional.h" +#include "output.h" +#include "stringmaker.h" +#include "units_utility.h" // SUN TESTS -// The 24-hour solar cycle is divided into four parts, as returned by four calendar.cpp functions: -// -// is_night : from the end of dusk, until the following sunrise (start of dawn) -// is_dawn : begins at sunrise, lasts twilight_duration (1 hour) -// is_day : from the end of dawn, until sunset (start of dusk) -// is_dusk : begins at sunset, lasts twilight_duration (1 hour) +// The 24-hour solar cycle has four overlapping parts, as defined by four calendar.cpp functions: // -// These are inclusive at their endpoints; in other words, each overlaps with the next like so: +// is_night : While the Sun is below the horizon +// is_day : While the Sun is above -12° altitude +// is_dawn, is_dusk : While the Sun is near the horizon at the appropriate end +// of the day // -// 00:00 is_night -// : is_night -// 06:00 is_night && is_dawn ( sunrise ) -// : is_dawn ( sunrise + twilight ) -// 07:00 is_dawn && is_day -// : is_day -// 19:00 is_day && is_dusk ( sunset ) -// : is_dusk ( sunset + twilight ) -// 20:00 is_dusk && is_night -// : is_night -// 23:59 is_night +// Day and night overlap, and dawn and dusk both overlap with both day and +// night. // // The times of sunrise and sunset will naturally depend on the current time of year; this aspect is // covered by the "sunrise and sunset" and solstice/equinox tests later in this file. Here we simply // use the first day of spring as a baseline. -// -// This test covers is_night, is_dawn, is_day, is_dusk, and their behavior in relation to time of day. TEST_CASE( "daily solar cycle", "[sun][night][dawn][day][dusk]" ) { // Use sunrise/sunset on the first day (spring equinox) - static const time_point midnight = calendar::turn_zero; - static const time_point noon = calendar::turn_zero + 12_hours; - static const time_point today_sunrise = sunrise( midnight ); - static const time_point today_sunset = sunset( midnight ); - - REQUIRE( "Year 1, Spring, day 1 6:00:00 AM" == to_string( today_sunrise ) ); - REQUIRE( "Year 1, Spring, day 1 7:00:00 PM" == to_string( today_sunset ) ); - - // First, at the risk of stating the obvious - CHECK( is_night( midnight ) ); - // And while we're at it - CHECK_FALSE( is_day( midnight ) ); - CHECK_FALSE( is_dawn( midnight ) ); - CHECK_FALSE( is_dusk( midnight ) ); - - // Yep, still dark - CHECK( is_night( midnight + 1_seconds ) ); - CHECK( is_night( midnight + 2_hours ) ); - CHECK( is_night( midnight + 3_hours ) ); - CHECK( is_night( midnight + 4_hours ) ); - - // The point of sunrise is both "night" and "dawn" - CHECK( is_night( today_sunrise ) ); - CHECK( is_dawn( today_sunrise ) ); - - // Dawn - CHECK_FALSE( is_night( today_sunrise + 1_seconds ) ); - CHECK( is_dawn( today_sunrise + 1_seconds ) ); - CHECK( is_dawn( today_sunrise + 30_minutes ) ); - CHECK( is_dawn( today_sunrise + 1_hours - 1_seconds ) ); - CHECK_FALSE( is_day( today_sunrise + 1_hours - 1_seconds ) ); - - // The endpoint of dawn is both "dawn" and "day" - CHECK( is_dawn( today_sunrise + 1_hours ) ); - CHECK( is_day( today_sunrise + 1_hours ) ); - - // Breakfast - CHECK_FALSE( is_dawn( today_sunrise + 1_hours + 1_seconds ) ); - CHECK( is_day( today_sunrise + 1_hours + 1_seconds ) ); - CHECK( is_day( today_sunrise + 2_hours ) ); - // Second breakfast - CHECK( is_day( today_sunrise + 3_hours ) ); - CHECK( is_day( today_sunrise + 4_hours ) ); - // Luncheon - CHECK( is_day( noon - 3_hours ) ); - CHECK( is_day( noon - 2_hours ) ); - // Elevenses - CHECK( is_day( noon - 1_hours ) ); - - // Noon - CHECK( is_day( noon ) ); - CHECK_FALSE( is_dawn( noon ) ); - CHECK_FALSE( is_dusk( noon ) ); - CHECK_FALSE( is_night( noon ) ); - - // Afternoon tea - CHECK( is_day( noon + 1_hours ) ); - CHECK( is_day( noon + 2_hours ) ); - // Dinner - CHECK( is_day( noon + 3_hours ) ); - CHECK( is_day( today_sunset - 2_hours ) ); - // Supper - CHECK( is_day( today_sunset - 1_hours ) ); - CHECK( is_day( today_sunset - 1_seconds ) ); - CHECK_FALSE( is_dusk( today_sunset - 1_seconds ) ); - - // The beginning of sunset is both "day" and "dusk" - CHECK( is_day( today_sunset ) ); - CHECK( is_dusk( today_sunset ) ); - - // Dusk - CHECK_FALSE( is_day( today_sunset + 1_seconds ) ); - CHECK( is_dusk( today_sunset + 1_seconds ) ); - CHECK( is_dusk( today_sunset + 30_minutes ) ); - CHECK( is_dusk( today_sunset + 1_hours - 1_seconds ) ); - CHECK_FALSE( is_night( today_sunset + 1_hours - 1_seconds ) ); - - // The point when dusk ends is both "dusk" and "night" - CHECK( is_dusk( today_sunset + 1_hours ) ); - CHECK( is_night( today_sunset + 1_hours ) ); - - // Night again - CHECK( is_night( today_sunset + 1_hours + 1_seconds ) ); - CHECK( is_night( today_sunset + 2_hours ) ); - CHECK( is_night( today_sunset + 3_hours ) ); - CHECK( is_night( today_sunset + 4_hours ) ); + const time_point midnight = calendar::turn_zero; + const time_point noon = calendar::turn_zero + 12_hours; + const time_point today_sunrise = sunrise( midnight ); + const time_point today_sunset = sunset( midnight ); + + CAPTURE( to_string( today_sunrise ) ); + CAPTURE( to_string( today_sunset ) ); + + SECTION( "Night" ) { + // First, at the risk of stating the obvious + CHECK( is_night( midnight ) ); + // And while we're at it + CHECK_FALSE( is_day( midnight ) ); + CHECK_FALSE( is_dawn( midnight ) ); + CHECK_FALSE( is_dusk( midnight ) ); + + // Yep, still dark + CHECK( is_night( midnight + 1_seconds ) ); + CHECK( is_night( midnight + 2_hours ) ); + CHECK( is_night( midnight + 3_hours ) ); + CHECK( is_night( midnight + 4_hours ) ); + } + + SECTION( "Dawn" ) { + CHECK_FALSE( is_night( today_sunrise ) ); + CHECK( is_dawn( today_sunrise - 1_seconds ) ); + CHECK( is_dawn( today_sunrise - 30_minutes ) ); + + // Dawn stops at 1 degrees + CHECK_FALSE( is_dawn( today_sunrise + 7_minutes ) ); + } + SECTION( "Day" ) { + // Due to roundings the day may start few seconds later than expected + CHECK( is_day( today_sunrise + 2_seconds ) ); + CHECK( is_day( today_sunrise + 2_hours ) ); + // Second breakfast + CHECK( is_day( today_sunrise + 3_hours ) ); + CHECK( is_day( today_sunrise + 4_hours ) ); + // Luncheon + CHECK( is_day( noon - 3_hours ) ); + CHECK( is_day( noon - 2_hours ) ); + // Elevenses + CHECK( is_day( noon - 1_hours ) ); + // Noon + CHECK( is_day( noon ) ); + CHECK_FALSE( is_dawn( noon ) ); + CHECK_FALSE( is_dusk( noon ) ); + CHECK_FALSE( is_night( noon ) ); + // Afternoon tea + CHECK( is_day( noon + 1_hours ) ); + CHECK( is_day( noon + 2_hours ) ); + // Dinner + CHECK( is_day( noon + 3_hours ) ); + CHECK( is_day( today_sunset - 2_hours ) ); + // Supper + CHECK( is_day( today_sunset - 1_hours ) ); + CHECK( is_day( today_sunset - 1_seconds ) ); + } + + SECTION( "Dusk" ) { + // Sun setting down is both "day" and "dusk" + CHECK( is_day( today_sunset ) ); + CHECK( is_dusk( today_sunset ) ); + + // Dusk + CHECK_FALSE( is_day( today_sunset + 1_seconds ) ); + CHECK( is_dusk( today_sunset + 1_seconds ) ); + CHECK( is_dusk( today_sunset + 30_minutes ) ); + CHECK( is_dusk( today_sunset + 1_hours - 1_seconds ) ); + } + + SECTION( "Night again" ) { + CHECK( is_night( today_sunset + 2_hours ) ); + CHECK( is_night( today_sunset + 3_hours ) ); + CHECK( is_night( today_sunset + 4_hours ) ); + } } // The calendar `sunlight` function returns light level for both sun and moon. TEST_CASE( "sunlight and moonlight", "[sun][sunlight][moonlight]" ) { // Use sunrise/sunset on the first day (spring equinox) - static const time_point midnight = calendar::turn_zero; - static const time_point today_sunrise = sunrise( midnight ); - static const time_point today_sunset = sunset( midnight ); + const time_point midnight = calendar::turn_zero; + const time_point today_sunrise = sunrise( midnight ); + const time_point today_sunset = sunset( midnight ); + CHECK( today_sunset > today_sunrise ); + CHECK( today_sunrise > midnight ); // Expected numbers below assume 100.0f maximum daylight level // (maximum daylight is different at other times of year - see [daylight] tests) @@ -136,187 +125,528 @@ TEST_CASE( "sunlight and moonlight", "[sun][sunlight][moonlight]" ) SECTION( "sunlight" ) { // Before dawn - CHECK( 1.0f == sunlight( midnight ) ); - CHECK( 1.0f == sunlight( today_sunrise ) ); + CHECK( 1.0f == sun_moon_light_at( midnight ) ); // Dawn - CHECK( 1.0275f == sunlight( today_sunrise + 1_seconds ) ); - CHECK( 2.65f == sunlight( today_sunrise + 1_minutes ) ); - CHECK( 25.75f == sunlight( today_sunrise + 15_minutes ) ); - CHECK( 50.50f == sunlight( today_sunrise + 30_minutes ) ); - CHECK( 75.25f == sunlight( today_sunrise + 45_minutes ) ); - // 1 second before full daylight - CHECK( 99.9725f == sunlight( today_sunrise + 1_hours - 1_seconds ) ); - CHECK( 100.0f == sunlight( today_sunrise + 1_hours ) ); - // End of dawn, full light all day - CHECK( 100.0f == sunlight( today_sunrise + 2_hours ) ); - CHECK( 100.0f == sunlight( today_sunrise + 3_hours ) ); + CHECK( sun_moon_light_at( today_sunrise - 2_hours ) == 1.0f ); + CHECK( sun_moon_light_at( today_sunrise - 1_hours ) == Approx( 20 ).margin( 2 ) ); + CHECK( sun_moon_light_at( today_sunrise ) == Approx( 60 ).margin( 2 ) ); + CHECK( sun_moon_light_at( today_sunrise + 1_hours ) == Approx( 70 ).margin( 2 ) ); + // Light gets brighter towards noon + CHECK( sun_moon_light_at( today_sunrise + 2_hours ) > + sun_moon_light_at( today_sunrise + 1_hours ) ); + CHECK( sun_moon_light_at( today_sunrise + 3_hours ) > + sun_moon_light_at( today_sunrise + 2_hours ) ); // Noon - CHECK( 100.0f == sunlight( midnight + 12_hours ) ); - CHECK( 100.0f == sunlight( midnight + 13_hours ) ); - CHECK( 100.0f == sunlight( midnight + 14_hours ) ); + CHECK( sun_moon_light_at( midnight + 12_hours ) == Approx( 100 ).margin( 10 ) ); + CHECK( sun_moon_light_at( midnight + 13_hours ) < + sun_moon_light_at( midnight + 12_hours ) ); + CHECK( sun_moon_light_at( midnight + 14_hours ) < + sun_moon_light_at( midnight + 13_hours ) ); // Dusk begins - CHECK( 100.0f == sunlight( today_sunset ) ); - // 1 second after dusk begins - CHECK( 99.9725f == sunlight( today_sunset + 1_seconds ) ); - CHECK( 75.25f == sunlight( today_sunset + 15_minutes ) ); - CHECK( 50.50f == sunlight( today_sunset + 30_minutes ) ); - CHECK( 25.75f == sunlight( today_sunset + 45_minutes ) ); - // 1 second before full night - CHECK( 1.0275f == sunlight( today_sunset + 1_hours - 1_seconds ) ); - CHECK( 1.0f == sunlight( today_sunset + 1_hours ) ); - // After dusk - CHECK( 1.0f == sunlight( today_sunset + 2_hours ) ); - CHECK( 1.0f == sunlight( today_sunset + 3_hours ) ); + CHECK( sun_moon_light_at( today_sunset - 1_hours ) == + Approx( sun_moon_light_at( today_sunrise + 1_hours ) ).margin( 1 ) ); + CHECK( sun_moon_light_at( today_sunset ) == + Approx( sun_moon_light_at( today_sunrise ) ).margin( 1 ) ); + CHECK( sun_moon_light_at( today_sunset + 1_hours ) == + Approx( sun_moon_light_at( today_sunrise - 1_hours ) ).margin( 1 ) ); } // This moonlight test is intentionally simple, only checking new moon (minimal light) and full // moon (maximum moonlight). More detailed tests of moon phase and light should be expressed in - // `moon_test.cpp`. Including here simply to check that `sunlight` also calculates moonlight. + // `moon_test.cpp`. Including here simply to check that `sun_moon_light_at` also calculates + // moonlight. SECTION( "moonlight" ) { - static const time_duration phase_time = calendar::season_length() / 6; - static const time_point new_moon = calendar::turn_zero; - static const time_point full_moon = new_moon + phase_time; + const time_duration phase_time = calendar::season_length() / 6; + const time_point new_moon = calendar::turn_zero; + const time_point full_moon = new_moon + phase_time; + const time_point full_moon_midnight = full_moon - time_past_midnight( full_moon ); WHEN( "the moon is new" ) { REQUIRE( get_moon_phase( new_moon ) == MOON_NEW ); THEN( "moonlight is 1.0" ) { - CHECK( 1.0f == sunlight( new_moon ) ); + CHECK( 1.0f == sun_moon_light_at( new_moon ) ); } } WHEN( "the moon is full" ) { - REQUIRE( get_moon_phase( full_moon ) == MOON_FULL ); + REQUIRE( get_moon_phase( full_moon_midnight ) == MOON_FULL ); THEN( "moonlight is 10.0" ) { - CHECK( 10.0f == sunlight( full_moon ) ); + CHECK( 10.0f == sun_moon_light_at( full_moon_midnight ) ); } } } } -// current_daylight_level returns seasonally-adjusted maximum daylight level -TEST_CASE( "current daylight level", "[sun][daylight][equinox][solstice]" ) +// sanity-check seasonally-adjusted maximum daylight level +TEST_CASE( "noon sunlight levels", "[sun][daylight][equinox][solstice]" ) { - static const time_duration one_season = calendar::season_length(); - static const time_point spring = calendar::turn_zero; - static const time_point summer = spring + one_season; - static const time_point autumn = summer + one_season; - static const time_point winter = autumn + one_season; + const time_duration one_season = calendar::season_length(); + const time_point spring = calendar::turn_zero; + const time_point summer = spring + one_season; + const time_point autumn = summer + one_season; + const time_point winter = autumn + one_season; SECTION( "baseline 100 daylight on the spring and autumn equinoxes" ) { - CHECK( 100.0f == current_daylight_level( spring ) ); - CHECK( 100.0f == current_daylight_level( autumn ) ); + float spring_light = sun_light_at( spring + 12_hours ); + CHECK( spring_light == Approx( 100.0f ).margin( 10 ) ); + CHECK( sun_light_at( autumn + 12_hours ) == Approx( spring_light ).margin( 1 ) ); } SECTION( "25 percent more daylight on the summer solstice" ) { - CHECK( 125.0f == current_daylight_level( summer ) ); + CHECK( sun_light_at( summer + 12_hours ) == 125.0f ); } SECTION( "25 percent less daylight on the winter solstice" ) { - CHECK( 75.0f == current_daylight_level( winter ) ); + CHECK( sun_light_at( winter + 12_hours ) == Approx( 75.0f ).margin( 10 ) ); } // Many other times of day have peak daylight level, but noon is for sure SECTION( "noon is peak daylight level" ) { - CHECK( 100.0f == sunlight( spring + 12_hours ) ); - CHECK( 125.0f == sunlight( summer + 12_hours ) ); - CHECK( 100.0f == sunlight( autumn + 12_hours ) ); - CHECK( 75.0f == sunlight( winter + 12_hours ) ); + CHECK( sun_moon_light_at( spring + 12_hours ) == + Approx( sun_moon_light_at_noon_near( spring ) ).margin( 3 ) ); + CHECK( sun_moon_light_at( summer + 12_hours ) == + Approx( sun_moon_light_at_noon_near( summer ) ).margin( 3 ) ); + CHECK( sun_moon_light_at( autumn + 12_hours ) == + Approx( sun_moon_light_at_noon_near( autumn ) ).margin( 3 ) ); + CHECK( sun_moon_light_at( winter + 12_hours ) == + Approx( sun_moon_light_at_noon_near( winter ) ).margin( 3 ) ); } } -// The times of sunrise and sunset vary throughout the year. For simplicity, equinoxes occur on the +// The times of sunrise and sunset vary throughout the year. Equinoxes occur on the // first day of spring and autumn, and solstices occur on the first day of summer and winter. TEST_CASE( "sunrise and sunset", "[sun][sunrise][sunset][equinox][solstice]" ) { // Due to the "NN_days" math below, this test requires a default 91-day season length REQUIRE( calendar::season_from_default_ratio() == Approx( 1.0f ) ); - static const time_duration one_season = calendar::season_length(); - static const time_point spring = calendar::turn_zero; - static const time_point summer = spring + one_season; - static const time_point autumn = summer + one_season; - static const time_point winter = autumn + one_season; + const time_duration one_season = calendar::season_length(); + const time_point spring = calendar::turn_zero; + const time_point summer = spring + one_season; + const time_point autumn = summer + one_season; + const time_point winter = autumn + one_season; - // The expected sunrise/sunset times depend on internal values in `calendar.cpp` including: - // - sunrise_winter, sunrise_summer, sunrise_equinox - // - sunset_winter, sunset_summer, sunset_equinox - // These being constants based on the default game setting in New England, planet Earth. - // Were these to become variable, the tests would need to adapt. + auto const sunrise_in_day = []( const time_point & t ) { + return time_past_midnight( sunrise( t ) ); + }; + + auto const sunset_in_day = []( const time_point & t ) { + return time_past_midnight( sunset( t ) ); + }; + + // The expected sunrise/sunset times depend on calculations in `calendar.cpp` + // They should approximately match Boston in the year 2000. There is minor + // variation due to our year length being 364 days and some other minor + // simplifications in the formulae. SECTION( "spring equinox is day 1 of spring" ) { - // 11 hours of daylight - CHECK( "Year 1, Spring, day 1 6:00:00 AM" == to_string( sunrise( spring ) ) ); - CHECK( "Year 1, Spring, day 1 7:00:00 PM" == to_string( sunset( spring ) ) ); - - THEN( "sunrise gets earlier" ) { - CHECK( "6:00:00 AM" == to_string_time_of_day( sunrise( spring ) ) ); - CHECK( "5:40:00 AM" == to_string_time_of_day( sunrise( spring + 30_days ) ) ); - CHECK( "5:20:00 AM" == to_string_time_of_day( sunrise( spring + 60_days ) ) ); - CHECK( "5:00:00 AM" == to_string_time_of_day( sunrise( spring + 90_days ) ) ); - } - THEN( "sunset gets later" ) { - CHECK( "7:00:00 PM" == to_string_time_of_day( sunset( spring ) ) ); - CHECK( "7:39:00 PM" == to_string_time_of_day( sunset( spring + 30_days ) ) ); - CHECK( "8:19:00 PM" == to_string_time_of_day( sunset( spring + 60_days ) ) ); - CHECK( "8:58:00 PM" == to_string_time_of_day( sunset( spring + 90_days ) ) ); - } + // Actual sunrise and sunset on March 21st 2001 are 0545 and 1757 + CHECK( "Year 1, Spring, day 1 6:03:22 AM" == to_string( sunrise( spring ) ) ); + CHECK( "Year 1, Spring, day 1 6:10:41 PM" == to_string( sunset( spring ) ) ); } SECTION( "summer solstice is day 1 of summer" ) { - // 14 hours of daylight - CHECK( "Year 1, Summer, day 1 5:00:00 AM" == to_string( sunrise( summer ) ) ); - CHECK( "Year 1, Summer, day 1 9:00:00 PM" == to_string( sunset( summer ) ) ); - - THEN( "sunrise gets later" ) { - CHECK( "5:00:00 AM" == to_string_time_of_day( sunrise( summer ) ) ); - CHECK( "5:19:00 AM" == to_string_time_of_day( sunrise( summer + 30_days ) ) ); - CHECK( "5:39:00 AM" == to_string_time_of_day( sunrise( summer + 60_days ) ) ); - CHECK( "5:59:00 AM" == to_string_time_of_day( sunrise( summer + 90_days ) ) ); - } - THEN( "sunset gets earlier" ) { - CHECK( "9:00:00 PM" == to_string_time_of_day( sunset( summer ) ) ); - CHECK( "8:20:00 PM" == to_string_time_of_day( sunset( summer + 30_days ) ) ); - CHECK( "7:40:00 PM" == to_string_time_of_day( sunset( summer + 60_days ) ) ); - CHECK( "7:01:00 PM" == to_string_time_of_day( sunset( summer + 90_days ) ) ); - } + // Actual sunrise and sunset on June 21st 2001 are 0407 and 1924 + CHECK( "Year 1, Summer, day 1 4:28:48 AM" == to_string( sunrise( summer ) ) ); + CHECK( "Year 1, Summer, day 1 7:35:10 PM" == to_string( sunset( summer ) ) ); } SECTION( "autumn equinox is day 1 of autumn" ) { - // 11 hours of daylight - CHECK( "Year 1, Autumn, day 1 6:00:00 AM" == to_string( sunrise( autumn ) ) ); - CHECK( "Year 1, Autumn, day 1 7:00:00 PM" == to_string( sunset( autumn ) ) ); - - THEN( "sunrise gets later" ) { - CHECK( "6:00:00 AM" == to_string_time_of_day( sunrise( autumn ) ) ); - CHECK( "6:19:00 AM" == to_string_time_of_day( sunrise( autumn + 30_days ) ) ); - CHECK( "6:39:00 AM" == to_string_time_of_day( sunrise( autumn + 60_days ) ) ); - CHECK( "6:59:00 AM" == to_string_time_of_day( sunrise( autumn + 90_days ) ) ); + // Actual sunrise and sunset on September 22nd 2001 are 0531 and 1741 + CHECK( "Year 1, Autumn, day 1 5:50:59 AM" == to_string( sunrise( autumn ) ) ); + CHECK( "Year 1, Autumn, day 1 5:54:13 PM" == to_string( sunset( autumn ) ) ); + } + + SECTION( "winter solstice is day 1 of winter" ) { + // Actual sunrise and sunset on December 21st 2001 are 0710 and 1614 + CHECK( "Year 1, Winter, day 1 7:31:30 AM" == to_string( sunrise( winter ) ) ); + CHECK( "Year 1, Winter, day 1 4:25:23 PM" == to_string( sunset( winter ) ) ); + } + + SECTION( "spring sunrise gets earlier" ) { + CHECK( sunrise_in_day( spring + 30_days ) < sunrise_in_day( spring ) ); + CHECK( sunrise_in_day( spring + 60_days ) < sunrise_in_day( spring + 30_days ) ); + CHECK( sunrise_in_day( spring + 90_days ) < sunrise_in_day( spring + 60_days ) ); + } + SECTION( "spring sunset gets later" ) { + CHECK( sunset_in_day( spring + 30_days ) > sunset_in_day( spring ) ); + CHECK( sunset_in_day( spring + 60_days ) > sunset_in_day( spring + 30_days ) ); + CHECK( sunset_in_day( spring + 90_days ) > sunset_in_day( spring + 60_days ) ); + } + + SECTION( "summer sunrise gets later" ) { + CHECK( sunrise_in_day( summer + 30_days ) > sunrise_in_day( summer ) ); + CHECK( sunrise_in_day( summer + 60_days ) > sunrise_in_day( summer + 30_days ) ); + CHECK( sunrise_in_day( summer + 90_days ) > sunrise_in_day( summer + 60_days ) ); + } + SECTION( "summer sunset gets earlier" ) { + CHECK( sunset_in_day( summer + 30_days ) < sunset_in_day( summer ) ); + CHECK( sunset_in_day( summer + 60_days ) < sunset_in_day( summer + 30_days ) ); + CHECK( sunset_in_day( summer + 90_days ) < sunset_in_day( summer + 60_days ) ); + } + + SECTION( "autumn sunrise gets later" ) { + CHECK( sunrise_in_day( autumn + 30_days ) > sunrise_in_day( autumn ) ); + CHECK( sunrise_in_day( autumn + 60_days ) > sunrise_in_day( autumn + 30_days ) ); + CHECK( sunrise_in_day( autumn + 90_days ) > sunrise_in_day( autumn + 60_days ) ); + } + SECTION( "autumn sunset gets earlier" ) { + CHECK( sunset_in_day( autumn + 30_days ) < sunset_in_day( autumn ) ); + CHECK( sunset_in_day( autumn + 60_days ) < sunset_in_day( autumn + 30_days ) ); + CHECK( sunset_in_day( autumn + 90_days ) < sunset_in_day( autumn + 60_days ) ); + } + + SECTION( "winter sunrise gets earlier" ) { + CHECK( sunrise_in_day( winter + 30_days ) < sunrise_in_day( winter ) ); + CHECK( sunrise_in_day( winter + 60_days ) < sunrise_in_day( winter + 30_days ) ); + CHECK( sunrise_in_day( winter + 90_days ) < sunrise_in_day( winter + 60_days ) ); + } + SECTION( "winter sunset gets later" ) { + CHECK( sunset_in_day( winter + 30_days ) > sunset_in_day( winter ) ); + CHECK( sunset_in_day( winter + 60_days ) > sunset_in_day( winter + 30_days ) ); + CHECK( sunset_in_day( winter + 90_days ) > sunset_in_day( winter + 60_days ) ); + } +} + +static rl_vec2d checked_sunlight_angle( const time_point &t ) +{ + const cata::optional opt_angle = sunlight_angle( t ); + REQUIRE( opt_angle ); + return *opt_angle; +} + +static constexpr time_point first_midnight = calendar::turn_zero; +static constexpr time_point first_noon = first_midnight + 12_hours; + +TEST_CASE( "sun_highest_at_noon", "[sun]" ) +{ + for( int i = 0; i < 100; ++i ) { + CAPTURE( i ); + + const time_point midnight = first_midnight + i * 1_days; + CHECK_FALSE( sunlight_angle( midnight ) ); + + const time_point noon = first_noon + i * 1_days; + const time_point before_noon = noon - 2_hours; + const time_point after_noon = noon + 2_hours; + + const rl_vec2d before_noon_angle = checked_sunlight_angle( before_noon ); + const rl_vec2d noon_angle = checked_sunlight_angle( noon ); + const rl_vec2d after_noon_angle = checked_sunlight_angle( after_noon ); + + CAPTURE( before_noon_angle ); + CAPTURE( noon_angle ); + CAPTURE( after_noon_angle ); + // Sun should be highest around noon + CHECK( noon_angle.magnitude() < before_noon_angle.magnitude() ); + CHECK( noon_angle.magnitude() < after_noon_angle.magnitude() ); + + // Sun should always be in the South, meaning angle points North + // (negative) + CHECK( before_noon_angle.y < 0 ); + CHECK( noon_angle.y < 0 ); + CHECK( after_noon_angle.y < 0 ); + + // Sun should be moving westwards across the sky, so its angle points + // more eastwards, which means it's increasing + CHECK( noon_angle.x > before_noon_angle.x ); + CHECK( after_noon_angle.x > noon_angle.x ); + + CHECK( before_noon_angle.magnitude() == + Approx( after_noon_angle.magnitude() ).epsilon( 0.25 ) ); + } +} + +TEST_CASE( "noon_sun_doesn't_move_much", "[sun]" ) +{ + rl_vec2d noon_angle = checked_sunlight_angle( first_noon ); + for( int i = 1; i < 1000; ++i ) { + CAPTURE( i ); + const time_point later_noon = first_noon + i * 1_days; + const rl_vec2d later_noon_angle = checked_sunlight_angle( later_noon ); + CHECK( noon_angle.x == Approx( later_noon_angle.x ).margin( 0.01 ) ); + CHECK( noon_angle.y == Approx( later_noon_angle.y ).epsilon( 0.05 ) ); + noon_angle = later_noon_angle; + } +} + +TEST_CASE( "dawn_dusk_fixed_during_eternal_season", "[sun]" ) +{ + on_out_of_scope restore_eternal_season( []() { + calendar::set_eternal_season( false ); + } ); + calendar::set_eternal_season( true ); + override_option override_eternal_season( "ETERNAL_SEASON", "true" ); + + const time_point first_sunrise = sunrise( first_noon ); + const time_point first_sunset = sunset( first_noon ); + + for( int i = 1; i < 1000; ++i ) { + CAPTURE( i ); + const time_point this_noon = first_noon + i * 1_days; + const time_point this_sunrise = sunrise( this_noon ); + const time_point this_sunset = sunset( this_noon ); + + CHECK( this_sunrise < this_noon ); + CHECK( this_sunset > this_noon ); + + CHECK( time_past_midnight( this_sunrise ) == time_past_midnight( first_sunrise ) ); + CHECK( time_past_midnight( this_sunset ) == time_past_midnight( first_sunset ) ); + } +} + +TEST_CASE( "sunrise_sunset_consistency", "[sun]" ) +{ + bool set_eternal = GENERATE( false, true ); + on_out_of_scope restore_eternal_season( []() { + calendar::set_eternal_season( false ); + } ); + calendar::set_eternal_season( set_eternal ); + + for( int i = 1; i < 1000; ++i ) { + CAPTURE( i ); + const time_point this_noon = first_noon + i * 1_days; + { + const time_point this_sunrise = sunrise( this_noon ); + CHECK( this_sunrise < this_noon ); + units::angle azimuth; + units::angle altitude; + std::tie( azimuth, altitude ) = + sun_azimuth_altitude( this_sunrise, location_boston ); + CHECK( to_degrees( altitude ) == Approx( 0 ).margin( 0.01 ) ); + } + { + const time_point this_sunset = sunset( this_noon ); + CHECK( this_sunset > this_noon ); + units::angle azimuth; + units::angle altitude; + std::tie( azimuth, altitude ) = + sun_azimuth_altitude( this_sunset, location_boston ); + CHECK( to_degrees( altitude ) == Approx( 0 ).margin( 0.01 ) ); } - THEN( "sunset gets earlier" ) { - CHECK( "7:00:00 PM" == to_string_time_of_day( sunset( autumn ) ) ); - CHECK( "6:20:00 PM" == to_string_time_of_day( sunset( autumn + 30_days ) ) ); - CHECK( "5:40:00 PM" == to_string_time_of_day( sunset( autumn + 60_days ) ) ); - CHECK( "5:01:00 PM" == to_string_time_of_day( sunset( autumn + 90_days ) ) ); + { + const time_point this_daylight = daylight_time( this_noon ); + CHECK( this_daylight < this_noon ); + units::angle azimuth; + units::angle altitude; + std::tie( azimuth, altitude ) = + sun_azimuth_altitude( this_daylight, location_boston ); + CHECK( to_degrees( altitude ) == Approx( -12 ).margin( 0.01 ) ); } } +} - SECTION( "winter solstice is day 1 of winter" ) { - // 10 hours of daylight - CHECK( "Year 1, Winter, day 1 7:00:00 AM" == to_string( sunrise( winter ) ) ); - CHECK( "Year 1, Winter, day 1 5:00:00 PM" == to_string( sunset( winter ) ) ); - - THEN( "sunrise gets earlier" ) { - CHECK( "7:00:00 AM" == to_string_time_of_day( sunrise( winter ) ) ); - CHECK( "6:40:00 AM" == to_string_time_of_day( sunrise( winter + 30_days ) ) ); - CHECK( "6:20:00 AM" == to_string_time_of_day( sunrise( winter + 60_days ) ) ); - CHECK( "6:00:00 AM" == to_string_time_of_day( sunrise( winter + 90_days ) ) ); +using PointSet = std::unordered_set, cata::tuple_hash>; + +static PointSet sun_positions_regular( time_point start, time_point end, time_duration interval, + int azimuth_scale ) +{ + CAPTURE( to_days( start - calendar::turn_zero ) ); + std::unordered_set, cata::tuple_hash> plot_points; + + for( time_point t = start; t < end; t += interval ) { + CAPTURE( to_minutes( t - start ) ); + units::angle azimuth; + units::angle altitude; + std::tie( azimuth, altitude ) = sun_azimuth_altitude( t, location_boston ); + if( altitude < 0_degrees ) { + continue; } - THEN( "sunset gets later" ) { - CHECK( "5:00:00 PM" == to_string_time_of_day( sunset( winter ) ) ); - CHECK( "5:39:00 PM" == to_string_time_of_day( sunset( winter + 30_days ) ) ); - CHECK( "6:19:00 PM" == to_string_time_of_day( sunset( winter + 60_days ) ) ); - CHECK( "6:58:00 PM" == to_string_time_of_day( sunset( winter + 90_days ) ) ); + // Convert to ASCII-art plot + // x-axis is azimuth, 4/azimuth_scale degrees per column + // y-axis is altitude, 3 degrees per column + azimuth = normalize( azimuth + 180_degrees ); + // Scale azimuth away from 180 by specified scale + azimuth = 180_degrees + ( azimuth - 180_degrees ) * azimuth_scale; + REQUIRE( azimuth >= 0_degrees ); + REQUIRE( azimuth <= 360_degrees ); + REQUIRE( altitude >= 0_degrees ); + REQUIRE( altitude <= 90_degrees ); + plot_points.emplace( azimuth / 4_degrees, altitude / 3_degrees ); + } + + return plot_points; +} + +static PointSet sun_throughout_day( time_point day_start ) +{ + REQUIRE( time_past_midnight( day_start ) == 0_seconds ); + // Calculate the Sun's position every few minutes thourhgout the day + time_point day_end = day_start + 1_days; + return sun_positions_regular( day_start, day_end, 5_minutes, 1 ); +} + +static PointSet sun_throughout_year( time_point day_start ) +{ + REQUIRE( time_past_midnight( day_start ) == 0_seconds ); + // Calculate the Sun's position every noon throughout the year + time_point first_noon = day_start + 1_days / 2; + time_point last_noon = first_noon + calendar::year_length(); + return sun_positions_regular( first_noon, last_noon, 1_days, 4 ); +} + +static void check_sun_plot( const std::vector &points, const std::string &reference ) +{ + static constexpr std::array symbols = { { '#', '*', '-' } }; + REQUIRE( points.size() <= symbols.size() ); + + std::ostringstream os; + os << "Altitude\n"; + + for( int rough_altitude = 30; rough_altitude >= 0; --rough_altitude ) { + for( int rough_azimuth = 0; rough_azimuth <= 90; ++rough_azimuth ) { + std::pair p{ rough_azimuth, rough_altitude }; + char c = ' '; + for( size_t i = 0; i < points.size(); ++i ) { + if( points[i].count( p ) ) { + c = symbols[i]; + break; + } + } + os << c; + } + os << '\n'; + } + os << std::setw( 92 ) << "Azimuth\n"; + std::string result = os.str(); + CHECK( result == reference ); + // When the test fails, print out something to copy-paste as a new + // reference output: + if( result != reference ) { + result.pop_back(); + for( const std::string &line : string_split( result, '\n' ) ) { + printf( R"("%s\n")" "\n", line.c_str() ); } } } +TEST_CASE( "movement_of_sun_through_day", "[sun]" ) +{ + PointSet equinox_points = sun_throughout_day( calendar::turn_zero ); + PointSet summer_points = + sun_throughout_day( calendar::turn_zero + calendar::season_length() ); + PointSet winter_points = + sun_throughout_day( calendar::turn_zero + calendar::season_length() * 3 ); + std::string reference = +// *INDENT-OFF* +"Altitude\n" +" \n" +" \n" +" \n" +" \n" +" \n" +" \n" +" \n" +" ################ \n" +" #### #### \n" +" #### ## \n" +" ## ## \n" +" ## ### \n" +" ## ## \n" +" ## ## \n" +" ## ******* ## \n" +" ## **** ***** ## \n" +" # *** *** ## \n" +" # *** *** # \n" +" ## ** ** ## \n" +" ## ** ** ## \n" +" # ** * # \n" +" ## * * ## \n" +" ## * ---- * # \n" +" # ** ----- ----- * # \n" +" ## ** --- --- * ## \n" +" ## ** --- -- ** ## \n" +" # * -- -- ** # \n" +" ## * -- -- * ## \n" +" # ** -- -- ** # \n" +" # ** -- -- ** # \n" +" ## * -- -- * # \n" +" Azimuth\n"; +// *INDENT-ON* + check_sun_plot( { summer_points, equinox_points, winter_points }, reference ); +} + +TEST_CASE( "movement_of_noon_through_year", "[sun]" ) +{ + PointSet points = sun_throughout_year( calendar::turn_zero ); + std::string reference = +// *INDENT-OFF* +// This should yield an analemma +"Altitude\n" +" \n" +" \n" +" \n" +" \n" +" \n" +" \n" +" \n" +" ###### \n" +" ## ## \n" +" ## ## \n" +" ## ## \n" +" ### \n" +" ### \n" +" # ## \n" +" # ## \n" +" ## ## \n" +" # ## \n" +" ## # \n" +" # # \n" +" # # \n" +" ## # \n" +" ## ## \n" +" ####### \n" +" \n" +" \n" +" \n" +" \n" +" \n" +" \n" +" \n" +" \n" +" Azimuth\n"; +// *INDENT-ON* + check_sun_plot( { points }, reference ); +} + +TEST_CASE( "noon_rises_towards_summer_solsitice_and_falls_towards_winter", "[sun]" ) +{ + int old_season_length = to_days( calendar::season_length() ); + on_out_of_scope restore_season_length( [ = ]() { + calendar::set_season_length( old_season_length ); + } ); + int this_season_length = GENERATE( 5, 14, 91, 1000 ); + CAPTURE( this_season_length ); + calendar::set_season_length( this_season_length ); + + const time_point summer_solstice = calendar::turn_zero + calendar::season_length(); + const time_point winter_solstice = calendar::turn_zero + 3 * calendar::season_length(); + + // Make some allowance and don't check the days within this range of the + // solstice. + const time_duration allowance = calendar::season_length() / 100; + + rl_vec2d last_noon_angle; + + for( time_point noon = first_noon; noon < winter_solstice; noon += 1_days ) { + CAPTURE( to_days( noon - first_noon ) ); + + const rl_vec2d noon_angle = checked_sunlight_angle( noon ); + + if( last_noon_angle.magnitude() != 0 ) { + CAPTURE( last_noon_angle ); + CAPTURE( noon_angle ); + + if( noon < summer_solstice - allowance ) { + // Sun should be higher than yesterday until summer solstice + CHECK( noon_angle.magnitude() < last_noon_angle.magnitude() ); + } else if( noon >= summer_solstice + allowance && + noon <= winter_solstice - allowance ) { + // ...and then lower than yesterday until winter solstice + CHECK( noon_angle.magnitude() > last_noon_angle.magnitude() ); + } + } + + last_noon_angle = noon_angle; + } +}