Implement climate and weather features in world simulation; enhance API with weather retrieval and climate selection options, update UI to support climate selection during world creation, and improve weather display in the game interface.
This commit is contained in:
@@ -100,13 +100,113 @@ public sealed class WorldSimulationTests
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void OverlayForApi_strips_last_ticked_at()
|
||||
public void OverlayForApi_strips_storage_only_state_and_adds_live_weather()
|
||||
{
|
||||
using var simulation = WorldSimulation.Create(ReadySummary(), catchUp: false);
|
||||
var overlaid = simulation.OverlayForApi(ReadySummary());
|
||||
|
||||
Assert.NotNull(overlaid.Clock);
|
||||
Assert.Null(overlaid.LastTickedAt);
|
||||
Assert.Null(overlaid.WeatherState);
|
||||
|
||||
Assert.Equal(ClimateKind.CentralEuropean, overlaid.Climate);
|
||||
Assert.NotNull(overlaid.Weather);
|
||||
Assert.InRange(overlaid.Weather.Humidity, 0, 1);
|
||||
Assert.InRange(overlaid.Weather.CloudCover, 0, 1);
|
||||
Assert.InRange(overlaid.Weather.WindDirectionDeg, 0, 360);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Climate_defaults_to_the_latitude_when_the_world_never_picked_one()
|
||||
{
|
||||
var summary = ReadySummary() with { Latitude = 78.22, Climate = null };
|
||||
using var simulation = WorldSimulation.Create(summary, catchUp: false);
|
||||
|
||||
Assert.Equal(ClimateKind.Tundra, simulation.Climate);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ApplyTo_persists_the_climate_and_the_drifting_pressure_systems()
|
||||
{
|
||||
using var simulation = WorldSimulation.Create(ReadySummary(), catchUp: false);
|
||||
simulation.Tick(TimeSpan.FromSeconds(30));
|
||||
|
||||
var stored = simulation.ApplyTo(ReadySummary());
|
||||
|
||||
Assert.Equal(ClimateKind.CentralEuropean, stored.Climate);
|
||||
Assert.NotNull(stored.WeatherState);
|
||||
Assert.NotEmpty(stored.WeatherState.Systems);
|
||||
Assert.NotEqual(0ul, stored.WeatherState.RngState);
|
||||
// Weather itself is derived, so it has no business in the file.
|
||||
Assert.Null(stored.Weather);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void A_restart_resumes_the_sky_it_had_rather_than_rolling_a_new_one()
|
||||
{
|
||||
using var before = WorldSimulation.Create(ReadySummary(), catchUp: false);
|
||||
before.Tick(TimeSpan.FromSeconds(45));
|
||||
|
||||
var persisted = before.ApplyTo(ReadySummary());
|
||||
var weatherBefore = before.SnapshotWeather();
|
||||
|
||||
using var after = WorldSimulation.Create(persisted with { LastTickedAt = null }, catchUp: false);
|
||||
var weatherAfter = after.SnapshotWeather();
|
||||
|
||||
Assert.Equal(weatherBefore.PressureHpa, weatherAfter.PressureHpa, 1);
|
||||
Assert.Equal(weatherBefore.Condition, weatherAfter.Condition);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void A_long_absence_rolls_a_fresh_sky_instead_of_stepping_through_it()
|
||||
{
|
||||
// Six real hours is roughly two and a half game months - the systems that were drifting are long gone.
|
||||
var summary = ReadySummary() with { LastTickedAt = DateTimeOffset.UtcNow - TimeSpan.FromHours(6) };
|
||||
|
||||
using var simulation = WorldSimulation.Create(summary, catchUp: true);
|
||||
var stored = simulation.ApplyTo(summary);
|
||||
|
||||
Assert.NotNull(stored.WeatherState);
|
||||
Assert.Equal(ClimateCatalog.CentralEuropean.Storminess, stored.WeatherState.Systems.Count);
|
||||
|
||||
// A reseeded pool is caught mid-life over the map, not parked at age zero off the edge.
|
||||
Assert.Contains(stored.WeatherState.Systems, static system => system.AgeHours > 0f);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void The_weather_field_covers_the_whole_map_and_varies_across_it()
|
||||
{
|
||||
using var simulation = WorldSimulation.Create(ReadySummary(), catchUp: false);
|
||||
|
||||
var field = simulation.SnapshotWeatherField();
|
||||
|
||||
Assert.Equal(ClimateKind.CentralEuropean, field.Climate);
|
||||
Assert.Equal(WorldSimulation.WeatherGridSize, field.Size);
|
||||
Assert.Equal(field.Size * field.Size, field.Nodes.Count);
|
||||
|
||||
// A drifting pressure system means the corners cannot all read the same pressure.
|
||||
var pressures = field.Nodes.Select(static node => node.PressureHpa).Distinct().Count();
|
||||
Assert.True(pressures > 1, "The field is uniform - the pressure systems are not being sampled.");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Neighbouring_field_nodes_stay_close_together()
|
||||
{
|
||||
using var simulation = WorldSimulation.Create(ReadySummary(), catchUp: false);
|
||||
var field = simulation.SnapshotWeatherField();
|
||||
|
||||
// Gaussian bumps are smooth, so a coarse grid is safe to interpolate between on the client.
|
||||
for (var row = 0; row < field.Size; row++)
|
||||
{
|
||||
for (var column = 1; column < field.Size; column++)
|
||||
{
|
||||
var left = field.Nodes[(row * field.Size) + column - 1];
|
||||
var right = field.Nodes[(row * field.Size) + column];
|
||||
Assert.True(
|
||||
Math.Abs(left.TemperatureC - right.TemperatureC) < 6,
|
||||
$"Nodes {column - 1} and {column} of row {row} jump by more than six degrees.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static WorldSummaryDto ReadySummary() => new()
|
||||
@@ -119,6 +219,7 @@ public sealed class WorldSimulationTests
|
||||
Status = WorldStatus.Ready,
|
||||
CreatedAt = DateTimeOffset.UtcNow,
|
||||
Clock = WorldSimulation.DefaultClock(),
|
||||
Climate = ClimateKind.CentralEuropean,
|
||||
LastTickedAt = DateTimeOffset.UtcNow,
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user