Enhance Calendar and Climate systems with start day offsets
CI / build-test (push) Successful in 1m13s

- Added a `startDay` parameter to the `Calendar` class to allow for fractional day offsets at clock initialization, enabling more flexible time settings.
- Updated `TotalDays` calculation to include the `startDay` offset.
- Introduced `StartDayOfYear` in `ClimateSettings` to shift the seasonal phase, allowing worlds to begin at a specific time of year.
- Adjusted `YearProgress` and `Year` calculations in the `Climate` class to account for the new `StartDayOfYear`.
- Added unit tests to verify the functionality of the new start day features in both `Calendar` and `Climate` classes.
This commit is contained in:
Leonid Pershin
2026-06-13 06:55:09 +03:00
parent 08381703f7
commit 38bdfbbb81
4 changed files with 53 additions and 8 deletions
+16 -6
View File
@@ -10,16 +10,21 @@ namespace MrGameEng.Core;
public sealed class Calendar public sealed class Calendar
{ {
private readonly GameClock _clock; private readonly GameClock _clock;
private readonly double _startDay;
private float _secondsPerDay; private float _secondsPerDay;
/// <summary> /// <summary>
/// Creates a calendar reading <paramref name="clock"/>; one day spans /// Creates a calendar reading <paramref name="clock"/>; one day spans
/// <paramref name="secondsPerDay"/> seconds of scaled time (must be positive). /// <paramref name="secondsPerDay"/> seconds of scaled time (must be positive).
/// <paramref name="startDay"/> offsets the calendar by (fractional) days at clock 0 — e.g.
/// <c>7.0 / 24</c> starts the world at 07:00 instead of midnight. It shifts the time of day and
/// the day/night phase that reads <see cref="DayProgress"/>, without touching the clock itself.
/// </summary> /// </summary>
public Calendar(GameClock clock, float secondsPerDay) public Calendar(GameClock clock, float secondsPerDay, double startDay = 0.0)
{ {
_clock = clock ?? throw new ArgumentNullException(nameof(clock)); _clock = clock ?? throw new ArgumentNullException(nameof(clock));
SecondsPerDay = secondsPerDay; SecondsPerDay = secondsPerDay;
_startDay = startDay;
} }
/// <summary>Scaled seconds per in-game day. Must be positive; raising it slows the calendar.</summary> /// <summary>Scaled seconds per in-game day. Must be positive; raising it slows the calendar.</summary>
@@ -36,8 +41,8 @@ public sealed class Calendar
); );
} }
/// <summary>Total elapsed days as a continuous value (e.g. 3.5 = midday of day 4).</summary> /// <summary>Total elapsed days as a continuous value (e.g. 3.5 = midday of day 4), incl. the start offset.</summary>
public double TotalDays => _clock.TotalTime / _secondsPerDay; public double TotalDays => _clock.TotalTime / _secondsPerDay + _startDay;
/// <summary>The current day number, counting from 1.</summary> /// <summary>The current day number, counting from 1.</summary>
public int Day => (int)TotalDays + 1; public int Day => (int)TotalDays + 1;
@@ -68,11 +73,16 @@ public static class CalendarEngineExtensions
/// <summary> /// <summary>
/// Creates a <see cref="Calendar"/> bound to the context's clock and registers it as a /// Creates a <see cref="Calendar"/> bound to the context's clock and registers it as a
/// service. Call once per world. <paramref name="secondsPerDay"/> is the scaled-time length /// service. Call once per world. <paramref name="secondsPerDay"/> is the scaled-time length
/// of one in-game day (must be positive). /// of one in-game day (must be positive); <paramref name="startDay"/> offsets the starting
/// time of day in fractional days (e.g. <c>7.0 / 24</c> begins the world at 07:00).
/// </summary> /// </summary>
public static Calendar UseCalendar(this EngineContext context, float secondsPerDay) public static Calendar UseCalendar(
this EngineContext context,
float secondsPerDay,
double startDay = 0.0
)
{ {
var calendar = new Calendar(context.Clock, secondsPerDay); var calendar = new Calendar(context.Clock, secondsPerDay, startDay);
context.Services.Add(calendar); context.Services.Add(calendar);
return calendar; return calendar;
} }
+12 -2
View File
@@ -37,6 +37,13 @@ public readonly record struct ClimateSettings
/// <summary>Day of the year (0-based) with the highest temperature; defaults to mid-summer.</summary> /// <summary>Day of the year (0-based) with the highest temperature; defaults to mid-summer.</summary>
public int WarmestDay { get; init; } public int WarmestDay { get; init; }
/// <summary>
/// Day of the year (0-based) that the calendar's day 0 maps to. Shifts the whole seasonal phase
/// (season and temperature together) so a world can begin in a chosen part of the year — e.g. a
/// warm late spring instead of the cold turn of the year. Defaults to 0 (year begins at day 0).
/// </summary>
public int StartDayOfYear { get; init; }
/// <summary>Temperate defaults: a 60-day year, mean 12°, ±14° seasonal, ±5° daily, warmest mid-summer.</summary> /// <summary>Temperate defaults: a 60-day year, mean 12°, ±14° seasonal, ±5° daily, warmest mid-summer.</summary>
public static ClimateSettings Default => public static ClimateSettings Default =>
new() new()
@@ -78,18 +85,21 @@ public sealed class Climate
/// <summary>In-game days per year.</summary> /// <summary>In-game days per year.</summary>
public int DaysPerYear => _settings.DaysPerYear; public int DaysPerYear => _settings.DaysPerYear;
/// <summary>Elapsed days shifted by <see cref="ClimateSettings.StartDayOfYear"/> — the seasonal clock.</summary>
private double YearDays => _calendar.TotalDays + _settings.StartDayOfYear;
/// <summary>Continuous position within the current year in <c>[0, 1)</c>.</summary> /// <summary>Continuous position within the current year in <c>[0, 1)</c>.</summary>
public double YearProgress public double YearProgress
{ {
get get
{ {
var years = _calendar.TotalDays / _settings.DaysPerYear; var years = YearDays / _settings.DaysPerYear;
return years - Math.Floor(years); return years - Math.Floor(years);
} }
} }
/// <summary>The current year, counting from 1.</summary> /// <summary>The current year, counting from 1.</summary>
public int Year => (int)(_calendar.TotalDays / _settings.DaysPerYear) + 1; public int Year => (int)(YearDays / _settings.DaysPerYear) + 1;
/// <summary>Day within the current year, 0-based.</summary> /// <summary>Day within the current year, 0-based.</summary>
public int DayOfYear => (int)(YearProgress * _settings.DaysPerYear); public int DayOfYear => (int)(YearProgress * _settings.DaysPerYear);
@@ -54,6 +54,20 @@ public class CalendarTests
Assert.Equal(14 * 60 + 30, calendar.MinuteOfDay); Assert.Equal(14 * 60 + 30, calendar.MinuteOfDay);
} }
[Fact]
public void StartDay_OffsetsTheTimeOfDay()
{
var clock = new GameClock();
var calendar = new Calendar(clock, secondsPerDay: 10f, startDay: 7.0 / 24); // begin at 07:00
Assert.Equal(1, calendar.Day);
Assert.Equal(7, calendar.Hour);
Assert.Equal(0, calendar.Minute);
clock.Advance(5f); // half a day later → 19:00
Assert.Equal(19, calendar.Hour);
}
[Fact] [Fact]
public void Pause_DoesNotAdvanceTheCalendar() public void Pause_DoesNotAdvanceTheCalendar()
{ {
@@ -81,4 +81,15 @@ public class ClimateTests
Assert.Equal(2, climate.Year); Assert.Equal(2, climate.Year);
Assert.Equal(0, climate.DayOfYear); Assert.Equal(0, climate.DayOfYear);
} }
[Fact]
public void StartDayOfYear_ShiftsTheSeasonalPhase()
{
// Begin the world already at the warmest day (15): the curve peaks at clock 0.
var (_, climate) = Make(Seasonal with { StartDayOfYear = 15 });
Assert.Equal(30f, climate.Temperature, 2); // mean 10 + amplitude 20, at the peak
Assert.Equal(15, climate.DayOfYear); // day-of-year reflects the offset
Assert.Equal(Season.Summer, climate.Season); // day 15 of a 60-day year = start of summer
}
} }