diff --git a/src/MrGameEng.Core/Calendar.cs b/src/MrGameEng.Core/Calendar.cs
index f79a987..55f564b 100644
--- a/src/MrGameEng.Core/Calendar.cs
+++ b/src/MrGameEng.Core/Calendar.cs
@@ -10,16 +10,21 @@ namespace MrGameEng.Core;
public sealed class Calendar
{
private readonly GameClock _clock;
+ private readonly double _startDay;
private float _secondsPerDay;
///
/// Creates a calendar reading ; one day spans
/// seconds of scaled time (must be positive).
+ /// offsets the calendar by (fractional) days at clock 0 — e.g.
+ /// 7.0 / 24 starts the world at 07:00 instead of midnight. It shifts the time of day and
+ /// the day/night phase that reads , without touching the clock itself.
///
- public Calendar(GameClock clock, float secondsPerDay)
+ public Calendar(GameClock clock, float secondsPerDay, double startDay = 0.0)
{
_clock = clock ?? throw new ArgumentNullException(nameof(clock));
SecondsPerDay = secondsPerDay;
+ _startDay = startDay;
}
/// Scaled seconds per in-game day. Must be positive; raising it slows the calendar.
@@ -36,8 +41,8 @@ public sealed class Calendar
);
}
- /// Total elapsed days as a continuous value (e.g. 3.5 = midday of day 4).
- public double TotalDays => _clock.TotalTime / _secondsPerDay;
+ /// Total elapsed days as a continuous value (e.g. 3.5 = midday of day 4), incl. the start offset.
+ public double TotalDays => _clock.TotalTime / _secondsPerDay + _startDay;
/// The current day number, counting from 1.
public int Day => (int)TotalDays + 1;
@@ -68,11 +73,16 @@ public static class CalendarEngineExtensions
///
/// Creates a bound to the context's clock and registers it as a
/// service. Call once per world. is the scaled-time length
- /// of one in-game day (must be positive).
+ /// of one in-game day (must be positive); offsets the starting
+ /// time of day in fractional days (e.g. 7.0 / 24 begins the world at 07:00).
///
- 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);
return calendar;
}
diff --git a/src/MrGameEng.Core/Climate.cs b/src/MrGameEng.Core/Climate.cs
index 781599d..a746542 100644
--- a/src/MrGameEng.Core/Climate.cs
+++ b/src/MrGameEng.Core/Climate.cs
@@ -37,6 +37,13 @@ public readonly record struct ClimateSettings
/// Day of the year (0-based) with the highest temperature; defaults to mid-summer.
public int WarmestDay { get; init; }
+ ///
+ /// 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).
+ ///
+ public int StartDayOfYear { get; init; }
+
/// Temperate defaults: a 60-day year, mean 12°, ±14° seasonal, ±5° daily, warmest mid-summer.
public static ClimateSettings Default =>
new()
@@ -78,18 +85,21 @@ public sealed class Climate
/// In-game days per year.
public int DaysPerYear => _settings.DaysPerYear;
+ /// Elapsed days shifted by — the seasonal clock.
+ private double YearDays => _calendar.TotalDays + _settings.StartDayOfYear;
+
/// Continuous position within the current year in [0, 1).
public double YearProgress
{
get
{
- var years = _calendar.TotalDays / _settings.DaysPerYear;
+ var years = YearDays / _settings.DaysPerYear;
return years - Math.Floor(years);
}
}
/// The current year, counting from 1.
- public int Year => (int)(_calendar.TotalDays / _settings.DaysPerYear) + 1;
+ public int Year => (int)(YearDays / _settings.DaysPerYear) + 1;
/// Day within the current year, 0-based.
public int DayOfYear => (int)(YearProgress * _settings.DaysPerYear);
diff --git a/tests/MrGameEng.Core.Tests/CalendarTests.cs b/tests/MrGameEng.Core.Tests/CalendarTests.cs
index 63b0fbd..bc6d8f8 100644
--- a/tests/MrGameEng.Core.Tests/CalendarTests.cs
+++ b/tests/MrGameEng.Core.Tests/CalendarTests.cs
@@ -54,6 +54,20 @@ public class CalendarTests
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]
public void Pause_DoesNotAdvanceTheCalendar()
{
diff --git a/tests/MrGameEng.Core.Tests/ClimateTests.cs b/tests/MrGameEng.Core.Tests/ClimateTests.cs
index 42fce5c..bd20821 100644
--- a/tests/MrGameEng.Core.Tests/ClimateTests.cs
+++ b/tests/MrGameEng.Core.Tests/ClimateTests.cs
@@ -81,4 +81,15 @@ public class ClimateTests
Assert.Equal(2, climate.Year);
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
+ }
}