From 38bdfbbb81fa7e283d2908e097c94d16853bc0de Mon Sep 17 00:00:00 2001 From: Leonid Pershin Date: Sat, 13 Jun 2026 06:55:09 +0300 Subject: [PATCH] Enhance Calendar and Climate systems with start day offsets - 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. --- src/MrGameEng.Core/Calendar.cs | 22 +++++++++++++++------ src/MrGameEng.Core/Climate.cs | 14 +++++++++++-- tests/MrGameEng.Core.Tests/CalendarTests.cs | 14 +++++++++++++ tests/MrGameEng.Core.Tests/ClimateTests.cs | 11 +++++++++++ 4 files changed, 53 insertions(+), 8 deletions(-) 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 + } }