From f99005f354a3d737a9af813019859186958f834c Mon Sep 17 00:00:00 2001 From: John Bytheway Date: Sat, 15 May 2021 16:13:06 -0400 Subject: [PATCH] Base sunrise and sunset on astronomical maths This is the first change that allows the sun angle calculations to actually influence the game. We switch the sunrise and sunset times to match the sun positions as calculated by the astronomical equations surrounding the location of the sun. This requires changing a few other places in the code, and updating a lot of tests. Heavinly inspired by Hirmuolio's work in #47570, but reimplemented rather than cherry-picked. Co-authored-by: Hirmuolio --- src/calendar.cpp | 455 ++++++++++++++++++++------------------ src/calendar.h | 23 +- src/character.cpp | 9 +- src/editmap.cpp | 4 +- src/game.cpp | 7 +- src/units.h | 5 + src/weather.cpp | 2 +- tests/char_sight_test.cpp | 8 +- tests/moon_test.cpp | 57 ++--- tests/sun_test.cpp | 438 +++++++++++++++++++----------------- 10 files changed, 526 insertions(+), 482 deletions(-) diff --git a/src/calendar.cpp b/src/calendar.cpp index e7580933523f9..16c13d41be64f 100644 --- a/src/calendar.cpp +++ b/src/calendar.cpp @@ -8,6 +8,7 @@ #include #include "cata_assert.h" +#include "cata_utility.h" #include "debug.h" #include "enum_conversions.h" #include "line.h" @@ -34,29 +35,18 @@ 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 +// 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 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; - -/** 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() { @@ -103,149 +93,282 @@ 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() ); +static std::pair sun_ra_declination( + time_point t, time_duration timezone ) +{ + // This derivation mostly from + // https://en.wikipedia.org/wiki/Position_of_the_Sun + // https://en.wikipedia.org/wiki/Celestial_coordinate_system#Notes_on_conversion - const double start_hour = start_hours[season]; - const double end_hour = start_hours[( season + 1 ) % 4]; + // The computation is inspired by the derivation based on J2000 (Greenwich + // noon, 2000-01-01), but because we want to handle a different year length + // than the real Earth, we don't use the same exact values. + // Instead we use as our epoch 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 - calendar::turn_zero - timezone ); - 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; + // The angle per day the Earth moves around the Sun + const units::angle angle_per_day = 360_degrees / to_days( calendar::year_length() ); - const time_point midnight = p - time_past_midnight( p ); - return midnight + time_duration::from_minutes( static_cast( time * 60 ) ); + // 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 ); + + // 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 RA = atan2( rot.xy() ); + const units::angle declination = units::asin( rot.z ); + return { RA, declination }; } -time_point sunset( const time_point &p ) +static units::angle sidereal_time_at( time_point t, units::angle longitude, time_duration timezone ) +{ + // Repeat some calculations from sun_ra_declination + const double days_since_epoch = to_days( 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; +} + +std::pair sun_azimuth_altitude( + time_point t, lat_long location, time_duration timezone ) +{ + units::angle RA; + units::angle declination; + std::tie( RA, declination ) = sun_ra_declination( t, timezone ); + const units::angle SIDTIME = sidereal_time_at( t, location.longitude, timezone ); + + const units::angle hour_angle = SIDTIME - RA; + + const rl_vec3d intermediate( + cos( hour_angle ) * cos( declination ), + sin( hour_angle ) * cos( declination ), + sin( declination ) ); + + const rl_vec3d hor( + -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( hor.xy() ) + 180_degrees ); + const units::angle altitude = units::asin( hor.z ); + + /*printf( + "\n" + "days_since_j2000 = %f, ecliptic_longitude = %f\n" + "RA = %f, declination = %f\n" + "SIDTIME = %f, hour_angle = %f\n" + "aziumth = %f, altitude = %f\n", + days_since_j2000, to_degrees( ecliptic_longitude ), + to_degrees( RA ), to_degrees( declination ), + to_degrees( SIDTIME ), to_degrees( hour_angle ), + to_degrees( azimuth ), to_degrees( altitude ) );*/ + + return std::make_pair( azimuth, altitude ); +} + +static units::angle sun_altitude( time_point t, lat_long location, time_duration timezone ) { - static_assert( static_cast( SPRING ) == 0, - "Expected spring to be the first season. If not, code below will use wrong index into array" ); + return sun_azimuth_altitude( t, location, timezone ).second; +} + +static units::angle sun_altitude( time_point t ) +{ + return sun_altitude( t, location_boston, timezone_boston ); +} + +cata::optional sunlight_angle( const time_point &t, lat_long location ) +{ + units::angle azimuth, altitude; + std::tie( azimuth, altitude ) = sun_azimuth_altitude( t, location, timezone_boston ); + if( altitude <= 0_degrees ) { + 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(); +} - 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() ); +cata::optional sunlight_angle( const time_point &t ) +{ + return sunlight_angle( t, location_boston ); +} - const double start_hour = start_hours[season]; - const double end_hour = start_hours[( season + 1 ) % 4]; +static time_point solar_noon_near( const time_point &t ) +{ + constexpr time_duration longitude_hours = angle_to_time( location_boston.longitude ); + const time_point prior_midnight = t - time_past_midnight( t ); + return prior_midnight + 12_hours - longitude_hours + timezone_boston; +} - 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; +static units::angle offset_to_sun_altitude( const units::angle altitude, + const time_point approx_time, const bool evening ) +{ + units::angle ra; + units::angle declination; + std::tie( ra, declination ) = sun_ra_declination( approx_time, timezone_boston ); + 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_boston ); + return normalize( target_sidereal_time - sidereal_time_at_approx_time ); +} - const time_point midnight = p - time_past_midnight( p ); - return midnight + time_duration::from_minutes( static_cast( time * 60 ) ); +static time_point sun_at_altitude( const units::angle altitude, 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, 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, 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; +} + +time_point sunrise( const time_point &p ) +{ + return sun_at_altitude( 0_degrees, p, false ); +} + +time_point sunset( const time_point &p ) +{ + return sun_at_altitude( 0_degrees, p, true ); } time_point night_time( const time_point &p ) { - return sunset( p ) + twilight_duration; + return sun_at_altitude( min_sun_angle_for_day, 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, 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 ) { @@ -635,107 +758,3 @@ time_point::time_point() { turn_ = 0; } - -std::pair sun_azimuth_altitude( - time_point t, lat_long location, float timezone ) -{ - // This derivation 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 handle a different year length - // than the real Earth, we don't use the same exact values. - // Instead we use as our epoch 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 - calendar::turn_zero ) + ( -timezone ) / 24.0; - - // The angle per day the Earth moves around the Sun - const units::angle angle_per_day = 360_degrees / to_days( calendar::year_length() ); - - // 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 ); - - // 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 RA = atan2( rot.xy() ); - const units::angle declination = units::asin( rot.z ); - - // 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; - const units::angle SIDTIME = L0 + L1 * days_since_epoch + location.longitude; - - const units::angle hour_angle = SIDTIME - RA; - - const rl_vec3d intermediate( - cos( hour_angle ) * cos( declination ), - sin( hour_angle ) * cos( declination ), - sin( declination ) ); - - const rl_vec3d hor( - -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( hor.xy() ) + 180_degrees ); - const units::angle altitude = units::asin( hor.z ); - - /*printf( - "\n" - "days_since_j2000 = %f, ecliptic_longitude = %f\n" - "RA = %f, declination = %f\n" - "SIDTIME = %f, hour_angle = %f\n" - "aziumth = %f, altitude = %f\n", - days_since_j2000, to_degrees( ecliptic_longitude ), - to_degrees( RA ), to_degrees( declination ), - to_degrees( SIDTIME ), to_degrees( hour_angle ), - to_degrees( azimuth ), to_degrees( altitude ) );*/ - - return std::make_pair( azimuth, altitude ); -} - -cata::optional sunlight_angle( const time_point &p, lat_long location ) -{ - constexpr float timezone = -5; - units::angle azimuth, altitude; - std::tie( azimuth, altitude ) = sun_azimuth_altitude( p, location, timezone ); - if( altitude <= 0_degrees ) { - 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 &p ) -{ - return sunlight_angle( p, location_boston ); -} diff --git a/src/calendar.h b/src/calendar.h index 368164f3637dc..b53eab4747665 100644 --- a/src/calendar.h +++ b/src/calendar.h @@ -574,18 +574,23 @@ 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, float timezone ); + time_point, lat_long, time_duration timezone ); /** 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. @@ -595,6 +600,8 @@ std::pair sun_azimuth_altitude( cata::optional sunlight_angle( const time_point &, lat_long ); cata::optional sunlight_angle( const time_point & ); +constexpr time_duration timezone_boston = -5_hours; + enum class weekdays : int { SUNDAY = 0, MONDAY, diff --git a/src/character.cpp b/src/character.cpp index 3c7655e57c8fa..c17201148a7e3 100644 --- a/src/character.cpp +++ b/src/character.cpp @@ -11947,14 +11947,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 e658635e93d46..967c2f0ba02fd 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 022e1f27532b8..657567b9f52f6 100644 --- a/src/game.cpp +++ b/src/game.cpp @@ -4023,7 +4023,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(); @@ -7646,8 +7646,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/units.h b/src/units.h index 36224785a28c5..fe494bbc97757 100644 --- a/src/units.h +++ b/src/units.h @@ -865,6 +865,11 @@ 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/weather.cpp b/src/weather.cpp index e78ce253da33b..391a168e26233 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/sun_test.cpp b/tests/sun_test.cpp index d5d63d0d5924d..ec996375964ea 100644 --- a/tests/sun_test.cpp +++ b/tests/sun_test.cpp @@ -43,101 +43,93 @@ 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) @@ -145,65 +137,60 @@ 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; @@ -212,28 +199,33 @@ TEST_CASE( "current daylight level", "[sun][daylight][equinox][solstice]" ) static 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]" ) { @@ -246,86 +238,85 @@ TEST_CASE( "sunrise and sunset", "[sun][sunrise][sunset][equinox][solstice]" ) static const time_point autumn = summer + one_season; static 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 5:47:36 AM" == to_string( sunrise( spring ) ) ); + CHECK( "Year 1, Spring, day 1 5:54:55 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:13:02 AM" == to_string( sunrise( summer ) ) ); + CHECK( "Year 1, Summer, day 1 7:19:24 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 ) ) ); - } - 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 ) ) ); - } + // Actual sunrise and sunset on September 22nd 2001 are 0531 and 1741 + CHECK( "Year 1, Autumn, day 1 5:35:13 AM" == to_string( sunrise( autumn ) ) ); + CHECK( "Year 1, Autumn, day 1 5:38:27 PM" == to_string( sunset( autumn ) ) ); } 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 ) ) ); - } - 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 ) ) ); - } + // Actual sunrise and sunset on December 21st 2001 are 0710 and 1614 + CHECK( "Year 1, Winter, day 1 7:15:44 AM" == to_string( sunrise( winter ) ) ); + CHECK( "Year 1, Winter, day 1 4:09:37 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 ) ); } } @@ -391,6 +382,43 @@ TEST_CASE( "noon_sun_doesn't_move_much", "[sun]" ) } } +TEST_CASE( "sunrise_sunset_consistency", "[sun]" ) +{ + for( int i = 1; i < 1000; ++i ) { + CAPTURE( i ); + { + const time_point later_noon = first_noon + i * 1_days; + const time_point this_sunrise = sunrise( later_noon ); + CHECK( this_sunrise < later_noon ); + units::angle azimuth; + units::angle altitude; + std::tie( azimuth, altitude ) = + sun_azimuth_altitude( this_sunrise, location_boston, timezone_boston ); + CHECK( to_degrees( altitude ) == Approx( 0 ).margin( 0.01 ) ); + } + { + const time_point later_noon = first_noon + i * 1_days; + const time_point this_sunset = sunset( later_noon ); + CHECK( this_sunset > later_noon ); + units::angle azimuth; + units::angle altitude; + std::tie( azimuth, altitude ) = + sun_azimuth_altitude( this_sunset, location_boston, timezone_boston ); + CHECK( to_degrees( altitude ) == Approx( 0 ).margin( 0.01 ) ); + } + { + const time_point later_noon = first_noon + i * 1_days; + const time_point this_daylight = daylight_time( later_noon ); + CHECK( this_daylight < later_noon ); + units::angle azimuth; + units::angle altitude; + std::tie( azimuth, altitude ) = + sun_azimuth_altitude( this_daylight, location_boston, timezone_boston ); + CHECK( to_degrees( altitude ) == Approx( -12 ).margin( 0.01 ) ); + } + } +} + using PointSet = std::unordered_set, cata::tuple_hash>; static PointSet sun_positions_regular( time_point start, time_point end, time_duration interval, @@ -402,7 +430,7 @@ static PointSet sun_positions_regular( time_point start, time_point end, time_du for( time_point t = start; t < end; t += interval ) { CAPTURE( to_minutes( t - start ) ); units::angle azimuth, altitude; - std::tie( azimuth, altitude ) = sun_azimuth_altitude( t, location_boston, -5 ); + std::tie( azimuth, altitude ) = sun_azimuth_altitude( t, location_boston, timezone_boston ); if( altitude < 0_degrees ) { continue; }