namespace MrGameEng.Core; /// The four seasons, in calendar order from the start of the year. public enum Season { /// First quarter of the year — warming. Spring, /// Second quarter — warmest. Summer, /// Third quarter — cooling. Autumn, /// Last quarter — coldest. Winter, } /// /// Tuning for . A year spans in-game days; temperature /// follows a seasonal cosine peaking on , plus a daily swing (cooler at night). /// public readonly record struct ClimateSettings { /// In-game days in one year (e.g. 60 = four 15-day seasons). public int DaysPerYear { get; init; } /// Yearly mean temperature. public float MeanTemperature { get; init; } /// Peak deviation from the mean across the seasons (summer high / winter low). public float SeasonalAmplitude { get; init; } /// Peak deviation from the seasonal mean across one day (warmer at noon, cooler at night). public float DailyAmplitude { get; init; } /// Day of the year (0-based) with the highest temperature; defaults to mid-summer. public int WarmestDay { get; init; } /// Temperate defaults: a 60-day year, mean 12°, ±14° seasonal, ±5° daily, warmest mid-summer. public static ClimateSettings Default => new() { DaysPerYear = 60, MeanTemperature = 12f, SeasonalAmplitude = 14f, DailyAmplitude = 5f, WarmestDay = 22, // ~middle of the summer quarter of a 60-day year }; } /// /// Continuous climate layered over : a temperature that varies smoothly with the /// season and the time of day, plus the current season and year. The whole curve is derived from the /// calendar's elapsed time, so it slows or stops with the game clock. Pure read-side, GPU-free and /// deterministic; registered as a service via . /// public sealed class Climate { private readonly Calendar _calendar; private readonly ClimateSettings _settings; /// Creates a climate reading with the given . public Climate(Calendar calendar, ClimateSettings settings) { _calendar = calendar ?? throw new ArgumentNullException(nameof(calendar)); if (settings.DaysPerYear <= 0) { throw new ArgumentOutOfRangeException( nameof(settings), "Days per year must be positive." ); } _settings = settings; } /// In-game days per year. public int DaysPerYear => _settings.DaysPerYear; /// Continuous position within the current year in [0, 1). public double YearProgress { get { var years = _calendar.TotalDays / _settings.DaysPerYear; return years - Math.Floor(years); } } /// The current year, counting from 1. public int Year => (int)(_calendar.TotalDays / _settings.DaysPerYear) + 1; /// Day within the current year, 0-based. public int DayOfYear => (int)(YearProgress * _settings.DaysPerYear); /// The current season, derived from the quarter of the year. public Season Season => (Season)Math.Clamp((int)(YearProgress * 4.0), 0, 3); /// Current temperature: seasonal cosine peaking on the warmest day, plus a daily swing. public float Temperature { get { var warmFraction = _settings.WarmestDay / (float)_settings.DaysPerYear; var seasonal = _settings.MeanTemperature + _settings.SeasonalAmplitude * MathF.Cos(MathF.Tau * ((float)YearProgress - warmFraction)); var daily = _settings.DailyAmplitude * (2f * Daylight() - 1f); return seasonal + daily; } } // Daytime factor 0..1 (0 at midnight, 1 at noon). Mirrors the day/night curve without a // Graphics dependency — Core owns the daily temperature swing. private float Daylight() { var value = -MathF.Cos(MathF.Tau * _calendar.DayProgress); return value > 0f ? value : 0f; } } /// Wires the climate model into the engine. public static class ClimateEngineExtensions { /// /// Creates a bound to the context's service and /// registers it. Call once per world, after . /// public static Climate UseClimate(this EngineContext context, ClimateSettings settings) { var climate = new Climate(context.Services.Get(), settings); context.Services.Add(climate); return climate; } }