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
@@ -29,12 +29,17 @@ Engine libraries (each feature is a namespaced subfolder of its host):
|
||||
|
||||
- **`Core`** — game loop, ECS world, scenes, time (`GameClock` with `TimeScale`; `GameSpeed`
|
||||
for discrete pause/1×/3×/6× speed control over the clock, `context.UseGameSpeed(...)`;
|
||||
`Calendar` turning scaled time into in-game days, `context.UseCalendar(secondsPerDay)`),
|
||||
`Calendar` turning scaled time into in-game days, `context.UseCalendar(secondsPerDay)`;
|
||||
`Climate` — continuous seasonal/daily temperature and season over the calendar,
|
||||
`context.UseClimate(settings)`),
|
||||
plus **Input** (`MrGameEng.Input`: `InputManager`, `ActionMap`, `InputSystem`, in
|
||||
`Core/Input/`). Depends only on MonoGame and Friflo.Engine.ECS.
|
||||
- **`Graphics`** — custom batched renderer, camera, sprites, plus **Tilemaps**
|
||||
(`MrGameEng.Tilemaps`: code-built tile grids rendered through the batcher,
|
||||
`scene.UseTilemaps()` after `UseRenderer2D()`, in `Graphics/Tilemaps/`). → `Core`.
|
||||
`scene.UseTilemaps()` after `UseRenderer2D()`, in `Graphics/Tilemaps/`) and **Lighting**
|
||||
(`MrGameEng.Lighting`, `Graphics/Lighting/`: day/night ambient over the `Calendar` driving
|
||||
`Renderer2D.AmbientLight` on world layers, `scene.UseDayNight(renderer)`; a sampleable
|
||||
`DayNight.SampleAt` for the simulation — point lights and shadows land here later). → `Core`.
|
||||
- **`Audio`** — ogg playback (NVorbis); `AudioManager` with `SoundVolume`/`MasterVolume`
|
||||
(one knob for effects + music). → `Core`.
|
||||
- **`Content`** — the asset/content pipeline: **Assets** (`MrGameEng.Assets`: runtime
|
||||
|
||||
@@ -33,8 +33,8 @@
|
||||
|
||||
| Библиотека (сборка) | Фичи (неймспейсы) и ответственность |
|
||||
|------------------------------|------------------------------------------------------------|
|
||||
| `MrGameEng.Core` | Игровой цикл (хост над `Game`), `EntityStore`, `SystemRoot`, сцены, время (`GameClock.TimeScale`; `GameSpeed` — дискретная скорость пауза/1×/3×/6× поверх часов; `Calendar` — игровые дни поверх масштабированного времени, `context.UseCalendar(...)`), жизненный цикл. **Input** (`MrGameEng.Input`, `Core/Input/`): клавиатура, мышь, геймпад, action maps |
|
||||
| `MrGameEng.Graphics` | Собственный батчер-рендерер (см. «Рендеринг»), камера, спрайты, анимации, слои. **Tilemaps** (`MrGameEng.Tilemaps`, `Graphics/Tilemaps/`): тайловые карты кодом — `TileGrid` + `TileSet` + компонент `Tilemap`, отрисовка видимых клеток через батчер |
|
||||
| `MrGameEng.Core` | Игровой цикл (хост над `Game`), `EntityStore`, `SystemRoot`, сцены, время (`GameClock.TimeScale`; `GameSpeed` — дискретная скорость пауза/1×/3×/6× поверх часов; `Calendar` — игровые дни поверх масштабированного времени, `context.UseCalendar(...)`; `Climate` — непрерывная сезонная/суточная температура и сезон поверх календаря, `context.UseClimate(...)`), жизненный цикл. **Input** (`MrGameEng.Input`, `Core/Input/`): клавиатура, мышь, геймпад, action maps |
|
||||
| `MrGameEng.Graphics` | Собственный батчер-рендерер (см. «Рендеринг»), камера, спрайты, анимации, слои. **Tilemaps** (`MrGameEng.Tilemaps`, `Graphics/Tilemaps/`): тайловые карты кодом — `TileGrid` + `TileSet` + компонент `Tilemap`, отрисовка видимых клеток через батчер. **Lighting** (`MrGameEng.Lighting`, `Graphics/Lighting/`): амбиент день/ночь поверх `Calendar` → `Renderer2D.AmbientLight` на World-слоях, `scene.UseDayNight(renderer)`, сэмплируемый `DayNight.SampleAt` для симуляции |
|
||||
| `MrGameEng.Audio` | Звуковые эффекты и музыка (NVorbis); `AudioManager` с `SoundVolume`/`MasterVolume` (одна ручка на эффекты и музыку) |
|
||||
| `MrGameEng.Content` | Пайплайн контента. **Assets** (`MrGameEng.Assets`): runtime-загрузка без Content Pipeline, кэш, `AssetRef<T>`. **Atlases** (`MrGameEng.Atlases`): текстурные атласы — сборка из дерева картинок (`AtlasBuilder`) и рантайм-загрузка (`TextureAtlas`), CLI `tools/MrGameEng.AtlasTool`. **Mods** (`MrGameEng.Mods`): система модов — порядок загрузки, JSON-дефы, локализация, слияние деревьев контента |
|
||||
| `MrGameEng.Simulation` | Детерминированные геймплей-примитивы без данных мира. **Pathfinding** (`MrGameEng.Pathfinding`): A*, Dijkstra, BFS, flow fields по гриду. **AI** (`MrGameEng.AI`): utility-ИИ — кривые отклика, соображения, действия, выбор (`UtilityAi<TContext>`), `Blackboard`. **Collisions** (`MrGameEng.Collisions`): компонент `Collider`, spatial hash, пары/запросы/raycast |
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
using Microsoft.Xna.Framework;
|
||||
using MrGameEng.Core;
|
||||
|
||||
namespace MrGameEng.Lighting;
|
||||
|
||||
/// <summary>Colors for the day/night ambient: the tint at deep night and at midday.</summary>
|
||||
public readonly record struct DayNightSettings
|
||||
{
|
||||
/// <summary>Ambient tint at midnight (a dim, cool night).</summary>
|
||||
public Color NightColor { get; init; }
|
||||
|
||||
/// <summary>Ambient tint at noon (white = no tint).</summary>
|
||||
public Color DayColor { get; init; }
|
||||
|
||||
/// <summary>Sensible defaults: a dim blue night, untinted day.</summary>
|
||||
public static DayNightSettings Default =>
|
||||
new() { NightColor = new Color(45, 55, 95), DayColor = Color.White };
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Day/night ambient driven by <see cref="Calendar.DayProgress"/>: a daylight factor (0 at midnight,
|
||||
/// 1 at noon) and an ambient color lerped from night to day. <see cref="SampleAt"/> is the sampleable
|
||||
/// light interface read by both the renderer (scene darkening) and the simulation (plant light);
|
||||
/// it returns the global daylight today and will return locally-shadowed light once point lights and
|
||||
/// occlusion land. Pure read-side, GPU-free and deterministic.
|
||||
/// </summary>
|
||||
public sealed class DayNight
|
||||
{
|
||||
private readonly Calendar _calendar;
|
||||
private readonly DayNightSettings _settings;
|
||||
|
||||
/// <summary>Creates the cycle reading <paramref name="calendar"/> with the given <paramref name="settings"/>.</summary>
|
||||
public DayNight(Calendar calendar, DayNightSettings settings)
|
||||
{
|
||||
_calendar = calendar ?? throw new ArgumentNullException(nameof(calendar));
|
||||
_settings = settings;
|
||||
}
|
||||
|
||||
/// <summary>Daylight factor in <c>[0, 1]</c> — 0 at midnight, 1 at noon, 0 again at midnight.</summary>
|
||||
public float Daylight
|
||||
{
|
||||
get
|
||||
{
|
||||
var value = -MathF.Cos(MathF.Tau * _calendar.DayProgress);
|
||||
return value > 0f ? value : 0f;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Global light intensity in <c>[0, 1]</c>; same as <see cref="Daylight"/> today.</summary>
|
||||
public float Intensity => Daylight;
|
||||
|
||||
/// <summary>Light intensity at a world point in <c>[0, 1]</c>. Global today; local (with shadows) later.</summary>
|
||||
public float SampleAt(Vector2 world) => Daylight;
|
||||
|
||||
/// <summary>Ambient tint for the scene: night color at night, day color at noon, eased between.</summary>
|
||||
public Color Ambient
|
||||
{
|
||||
get
|
||||
{
|
||||
var t = Smoothstep(Daylight);
|
||||
return Color.Lerp(_settings.NightColor, _settings.DayColor, t);
|
||||
}
|
||||
}
|
||||
|
||||
private static float Smoothstep(float x) => x * x * (3f - 2f * x);
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
using Friflo.Engine.ECS.Systems;
|
||||
using MrGameEng.Core;
|
||||
using MrGameEng.Graphics;
|
||||
|
||||
namespace MrGameEng.Lighting;
|
||||
|
||||
/// <summary>
|
||||
/// Update-phase system that pushes the current <see cref="DayNight.Ambient"/> into the renderer's
|
||||
/// <see cref="Renderer2D.AmbientLight"/> each frame, so the world darkens toward night and brightens
|
||||
/// toward noon. Runs in the update phase, before the draw phase consumes the value.
|
||||
/// </summary>
|
||||
public sealed class DayNightSystem : BaseSystem
|
||||
{
|
||||
private readonly DayNight _dayNight;
|
||||
private readonly Renderer2D _renderer;
|
||||
|
||||
internal DayNightSystem(DayNight dayNight, Renderer2D renderer)
|
||||
{
|
||||
_dayNight = dayNight;
|
||||
_renderer = renderer;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void OnUpdateGroup() => _renderer.AmbientLight = _dayNight.Ambient;
|
||||
}
|
||||
|
||||
/// <summary>Wires the day/night ambient cycle into a <see cref="Scene"/>.</summary>
|
||||
public static class SceneLightingExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Creates a <see cref="DayNight"/> bound to the scene's <see cref="Calendar"/> service, registers
|
||||
/// it, and drives <paramref name="renderer"/>'s ambient light from it. Call from <c>OnLoad</c>
|
||||
/// after <see cref="CalendarEngineExtensions.UseCalendar"/> and <c>UseRenderer2D</c>.
|
||||
/// </summary>
|
||||
public static DayNight UseDayNight(
|
||||
this Scene scene,
|
||||
Renderer2D renderer,
|
||||
DayNightSettings? settings = null
|
||||
)
|
||||
{
|
||||
var dayNight = new DayNight(
|
||||
scene.Context.Services.Get<Calendar>(),
|
||||
settings ?? DayNightSettings.Default
|
||||
);
|
||||
scene.Context.Services.Add(dayNight);
|
||||
scene.UpdateSystems.Add(new DayNightSystem(dayNight, renderer));
|
||||
return dayNight;
|
||||
}
|
||||
}
|
||||
@@ -27,6 +27,13 @@ public sealed class Renderer2D : IDisposable
|
||||
/// <summary>Camera state of the current frame. Valid between BeginFrame and the next BeginFrame.</summary>
|
||||
public CameraState Camera { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Global ambient light multiplied into every <see cref="LayerSpace.World"/> sprite — the engine
|
||||
/// hook a day/night cycle drives. <see cref="Color.White"/> (default) leaves the scene unchanged;
|
||||
/// screen-space layers (HUD overlays) are never tinted.
|
||||
/// </summary>
|
||||
public Color AmbientLight { get; set; } = Color.White;
|
||||
|
||||
/// <summary>Draw calls issued by the last <see cref="EndFrame"/>.</summary>
|
||||
public int DrawCalls { get; private set; }
|
||||
|
||||
@@ -338,6 +345,12 @@ public sealed class Renderer2D : IDisposable
|
||||
// высоты с origin «в ногах» сортируются по ногам, как принято в top-down.
|
||||
var depth = layer.SortMode == LayerSortMode.YSort ? transform.Position.Y : sprite.Depth;
|
||||
|
||||
// Ambient light tints world sprites (day/night); screen-space overlays stay at full brightness.
|
||||
var color =
|
||||
layer.Space == LayerSpace.World && AmbientLight != Color.White
|
||||
? new Color(sprite.Color.ToVector4() * AmbientLight.ToVector4())
|
||||
: sprite.Color;
|
||||
|
||||
instance = new SpriteInstance
|
||||
{
|
||||
Region = region,
|
||||
@@ -346,7 +359,7 @@ public sealed class Renderer2D : IDisposable
|
||||
new Vector2(region.Width * transform.Scale.X, region.Height * transform.Scale.Y)
|
||||
/ 2f,
|
||||
Rotation = transform.Rotation,
|
||||
Color = sprite.Color,
|
||||
Color = color,
|
||||
Flip = sprite.Flip,
|
||||
Layer = sprite.Layer.Value,
|
||||
};
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
using MrGameEng.Core;
|
||||
using Xunit;
|
||||
|
||||
namespace MrGameEng.Core.Tests;
|
||||
|
||||
public class ClimateTests
|
||||
{
|
||||
private static (GameClock Clock, Climate Climate) Make(ClimateSettings settings)
|
||||
{
|
||||
var clock = new GameClock();
|
||||
var calendar = new Calendar(clock, secondsPerDay: 10f);
|
||||
return (clock, new Climate(calendar, settings));
|
||||
}
|
||||
|
||||
private static ClimateSettings Seasonal =>
|
||||
new()
|
||||
{
|
||||
DaysPerYear = 60,
|
||||
MeanTemperature = 10f,
|
||||
SeasonalAmplitude = 20f,
|
||||
DailyAmplitude = 0f,
|
||||
WarmestDay = 15,
|
||||
};
|
||||
|
||||
[Fact]
|
||||
public void Constructor_NonPositiveYear_Throws()
|
||||
{
|
||||
Assert.Throws<ArgumentOutOfRangeException>(() =>
|
||||
new Climate(new Calendar(new GameClock(), 10f), Seasonal with { DaysPerYear = 0 })
|
||||
);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Temperature_PeaksOnWarmestDay_AndBottomsHalfYearLater()
|
||||
{
|
||||
var (clock, climate) = Make(Seasonal);
|
||||
|
||||
clock.Advance(15 * 10f); // day 15 = warmest
|
||||
Assert.Equal(30f, climate.Temperature, 2); // mean 10 + amplitude 20
|
||||
|
||||
clock.Advance(30 * 10f); // +30 days = half a 60-day year later
|
||||
Assert.Equal(-10f, climate.Temperature, 2); // mean 10 - amplitude 20
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DailySwing_WarmerAtNoonThanMidnight()
|
||||
{
|
||||
var settings = Seasonal with { SeasonalAmplitude = 0f, DailyAmplitude = 6f };
|
||||
var (clock, climate) = Make(settings);
|
||||
|
||||
clock.Advance(5f); // day 0, noon (DayProgress 0.5)
|
||||
Assert.Equal(16f, climate.Temperature, 2); // mean 10 + daily 6
|
||||
|
||||
clock.Advance(5f); // day 1, midnight (DayProgress 0)
|
||||
Assert.Equal(4f, climate.Temperature, 2); // mean 10 - daily 6
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Season_FollowsTheQuarterOfTheYear()
|
||||
{
|
||||
var (clock, climate) = Make(Seasonal); // 60-day year → 15 days per season
|
||||
|
||||
Assert.Equal(Season.Spring, climate.Season);
|
||||
clock.Advance(20 * 10f);
|
||||
Assert.Equal(Season.Summer, climate.Season);
|
||||
clock.Advance(15 * 10f);
|
||||
Assert.Equal(Season.Autumn, climate.Season);
|
||||
clock.Advance(15 * 10f);
|
||||
Assert.Equal(Season.Winter, climate.Season);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void YearAndDayOfYear_RollOver()
|
||||
{
|
||||
var (clock, climate) = Make(Seasonal);
|
||||
|
||||
Assert.Equal(1, climate.Year);
|
||||
Assert.Equal(0, climate.DayOfYear);
|
||||
|
||||
clock.Advance(60 * 10f); // exactly one year
|
||||
Assert.Equal(2, climate.Year);
|
||||
Assert.Equal(0, climate.DayOfYear);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
using MrGameEng.Core;
|
||||
using MrGameEng.Lighting;
|
||||
using Xunit;
|
||||
|
||||
namespace MrGameEng.Lighting.Tests;
|
||||
|
||||
public class DayNightTests
|
||||
{
|
||||
private static (GameClock Clock, DayNight DayNight) Make()
|
||||
{
|
||||
var clock = new GameClock();
|
||||
var calendar = new Calendar(clock, secondsPerDay: 24f); // 1 second = 1 in-game hour
|
||||
return (clock, new DayNight(calendar, DayNightSettings.Default));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Daylight_ZeroAtMidnight_OneAtNoon()
|
||||
{
|
||||
var (clock, dayNight) = Make();
|
||||
|
||||
Assert.Equal(0f, dayNight.Daylight, 3); // 00:00
|
||||
|
||||
clock.Advance(12f); // 12:00
|
||||
Assert.Equal(1f, dayNight.Daylight, 3);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Daylight_RisesFromDawnToNoon()
|
||||
{
|
||||
var (clock, dayNight) = Make();
|
||||
|
||||
clock.Advance(6f); // 06:00
|
||||
var dawn = dayNight.Daylight;
|
||||
clock.Advance(3f); // 09:00
|
||||
var mid = dayNight.Daylight;
|
||||
clock.Advance(3f); // 12:00
|
||||
var noon = dayNight.Daylight;
|
||||
|
||||
Assert.True(dawn < mid && mid < noon, "daylight should rise from dawn to noon");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Ambient_DarkerAtNightThanAtNoon()
|
||||
{
|
||||
var (clock, dayNight) = Make();
|
||||
|
||||
var night = dayNight.Ambient; // 00:00
|
||||
clock.Advance(12f); // 12:00
|
||||
var noon = dayNight.Ambient;
|
||||
|
||||
Assert.True(night.R < noon.R && night.G < noon.G && night.B < noon.B);
|
||||
Assert.Equal(255, noon.R); // day color is white at noon
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user