Climate (Core) layers a continuous seasonal-plus-daily temperature and the current season over the Calendar, registered via context.UseClimate. A new Lighting module (Graphics) adds DayNight: a daylight factor and ambient color from the calendar's time of day, pushed into the renderer's new AmbientLight (multiplied into world-space sprites only, so the scene darkens at night while screen-space overlays stay readable) and exposed as a sampleable SampleAt for the simulation. Both deterministic and GPU-free, covered by tests; docs updated. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
f39ee9e787
commit
a684c23cd9
@@ -0,0 +1,137 @@
|
||||
namespace MrGameEng.Core;
|
||||
|
||||
/// <summary>The four seasons, in calendar order from the start of the year.</summary>
|
||||
public enum Season
|
||||
{
|
||||
/// <summary>First quarter of the year — warming.</summary>
|
||||
Spring,
|
||||
|
||||
/// <summary>Second quarter — warmest.</summary>
|
||||
Summer,
|
||||
|
||||
/// <summary>Third quarter — cooling.</summary>
|
||||
Autumn,
|
||||
|
||||
/// <summary>Last quarter — coldest.</summary>
|
||||
Winter,
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tuning for <see cref="Climate"/>. A year spans <see cref="DaysPerYear"/> in-game days; temperature
|
||||
/// follows a seasonal cosine peaking on <see cref="WarmestDay"/>, plus a daily swing (cooler at night).
|
||||
/// </summary>
|
||||
public readonly record struct ClimateSettings
|
||||
{
|
||||
/// <summary>In-game days in one year (e.g. 60 = four 15-day seasons).</summary>
|
||||
public int DaysPerYear { get; init; }
|
||||
|
||||
/// <summary>Yearly mean temperature.</summary>
|
||||
public float MeanTemperature { get; init; }
|
||||
|
||||
/// <summary>Peak deviation from the mean across the seasons (summer high / winter low).</summary>
|
||||
public float SeasonalAmplitude { get; init; }
|
||||
|
||||
/// <summary>Peak deviation from the seasonal mean across one day (warmer at noon, cooler at night).</summary>
|
||||
public float DailyAmplitude { get; init; }
|
||||
|
||||
/// <summary>Day of the year (0-based) with the highest temperature; defaults to mid-summer.</summary>
|
||||
public int WarmestDay { get; init; }
|
||||
|
||||
/// <summary>Temperate defaults: a 60-day year, mean 12°, ±14° seasonal, ±5° daily, warmest mid-summer.</summary>
|
||||
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
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Continuous climate layered over <see cref="Calendar"/>: 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 <see cref="ClimateEngineExtensions.UseClimate"/>.
|
||||
/// </summary>
|
||||
public sealed class Climate
|
||||
{
|
||||
private readonly Calendar _calendar;
|
||||
private readonly ClimateSettings _settings;
|
||||
|
||||
/// <summary>Creates a climate reading <paramref name="calendar"/> with the given <paramref name="settings"/>.</summary>
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>In-game days per year.</summary>
|
||||
public int DaysPerYear => _settings.DaysPerYear;
|
||||
|
||||
/// <summary>Continuous position within the current year in <c>[0, 1)</c>.</summary>
|
||||
public double YearProgress
|
||||
{
|
||||
get
|
||||
{
|
||||
var years = _calendar.TotalDays / _settings.DaysPerYear;
|
||||
return years - Math.Floor(years);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>The current year, counting from 1.</summary>
|
||||
public int Year => (int)(_calendar.TotalDays / _settings.DaysPerYear) + 1;
|
||||
|
||||
/// <summary>Day within the current year, 0-based.</summary>
|
||||
public int DayOfYear => (int)(YearProgress * _settings.DaysPerYear);
|
||||
|
||||
/// <summary>The current season, derived from the quarter of the year.</summary>
|
||||
public Season Season => (Season)Math.Clamp((int)(YearProgress * 4.0), 0, 3);
|
||||
|
||||
/// <summary>Current temperature: seasonal cosine peaking on the warmest day, plus a daily swing.</summary>
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Wires the climate model into the engine.</summary>
|
||||
public static class ClimateEngineExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Creates a <see cref="Climate"/> bound to the context's <see cref="Calendar"/> service and
|
||||
/// registers it. Call once per world, after <see cref="CalendarEngineExtensions.UseCalendar"/>.
|
||||
/// </summary>
|
||||
public static Climate UseClimate(this EngineContext context, ClimateSettings settings)
|
||||
{
|
||||
var climate = new Climate(context.Services.Get<Calendar>(), settings);
|
||||
context.Services.Add(climate);
|
||||
return climate;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user