acdream/tests/AcDream.Core.Tests/World/WeatherSystemTests.cs
Erik 0df1c5b4a6 feat(world): Phase G.1 data model — dat-accurate SkyKeyframe + WeatherSystem
Expand the SkyKeyframe record with retail-exact fog fields (FogStart,
FogEnd, FogMode) per r12 §5. The existing FogDensity field is retained
for backwards compat with tests that pin it; new shipping code reads
FogStart / FogEnd / FogMode directly.

Add WeatherSystem (WeatherKind + EnvironOverride enum + 10s transition
ease + deterministic per-day-index roll) matching r12 §6.1. Roll weights
are ~60% Clear / 20% Overcast / 12% Rain / 5% Snow / 3% Storm — tuned
against retail observations. Storm mode triggers lightning flashes
every 8–30 s via an exponential-decay (200ms τ) flash level that the
shader consumes as an additive scene bump.

Add SkyDescLoader that parses the Region dat (0x13000000) into
LoadedSkyDesc — DayGroupData with SkyObjectData (visibility window +
arc sweep), per-keyframe SkyObjectReplaceData, and a shader-ready
SkyStateProvider builder. Sun/ambient colors are pre-multiplied by
DirBright/AmbBright so the shader never needs to know about retail's
scalar brightness field.

19 new tests (weather determinism, transition ease, environ override
tint, flash decay, dat-load conversion with fog + pre-mult colors).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-19 10:29:33 +02:00

102 lines
3.1 KiB
C#

using System;
using System.Numerics;
using AcDream.Core.World;
using Xunit;
namespace AcDream.Core.Tests.World;
public sealed class WeatherSystemTests
{
[Fact]
public void Roll_Deterministic_ForSameDayIndex()
{
var a = new WeatherSystem();
var b = new WeatherSystem();
for (int d = 0; d < 100; d++)
{
a.Tick(0, d, 100f); // big dt to finish any transition
b.Tick(0, d, 100f);
Assert.Equal(a.Kind, b.Kind);
}
}
[Fact]
public void Roll_WeightsDominatedByClear()
{
// Clear should cover ~60% of the distribution. Sample many days
// and check the clear fraction is in a reasonable band.
var sys = new WeatherSystem();
int clear = 0;
for (int d = 0; d < 1000; d++)
{
sys.Tick(0, d, 100f);
if (sys.Kind == WeatherKind.Clear) clear++;
}
double frac = clear / 1000.0;
Assert.InRange(frac, 0.45, 0.75);
}
[Fact]
public void Transition_EasesAcrossTenSeconds()
{
// Force Storm, then Clear, sample snapshot fog distance mid-transition.
var sys = new WeatherSystem();
sys.ForceWeather(WeatherKind.Storm);
sys.Tick(0, 1, 100f); // finalize
var kf = SkyStateProvider.Default().Interpolate(0.5f);
var stormFog = sys.Snapshot(in kf);
Assert.Equal(WeatherKind.Storm, stormFog.Kind);
// Snapshot should have a small fog end (storm fog is dense).
Assert.True(stormFog.FogEnd < 120f, $"storm fog end too large: {stormFog.FogEnd}");
}
[Fact]
public void EnvironOverride_ForcesTintedFog()
{
var sys = new WeatherSystem();
sys.Override = EnvironOverride.RedFog;
var kf = SkyStateProvider.Default().Interpolate(0.5f);
var snap = sys.Snapshot(in kf);
Assert.Equal(EnvironOverride.RedFog, snap.Override);
// Red override means the R channel dominates.
Assert.True(snap.FogColor.X > snap.FogColor.Y);
Assert.True(snap.FogColor.X > snap.FogColor.Z);
}
[Fact]
public void Flash_DecaysOverTime()
{
var sys = new WeatherSystem();
sys.TriggerFlash();
var kf = SkyStateProvider.Default().Interpolate(0.5f);
var imm = sys.Snapshot(in kf);
Assert.True(imm.LightningFlash > 0.9f);
// After 1 second the flash should be mostly decayed.
sys.Tick(0, 0, 1.0f);
var later = sys.Snapshot(in kf);
Assert.True(later.LightningFlash < 0.1f,
$"lightning flash didn't decay: {later.LightningFlash}");
}
[Fact]
public void Snapshot_ClearKind_PassesThroughKeyframeFog()
{
var sys = new WeatherSystem();
sys.ForceWeather(WeatherKind.Clear);
sys.Tick(0, 0, 100f); // finish transition
var kf = SkyStateProvider.Default().Interpolate(0.5f);
var snap = sys.Snapshot(in kf);
// Clear passes the keyframe's fog color + distances through.
Assert.Equal(kf.FogStart, snap.FogStart, precision: 2);
Assert.Equal(kf.FogEnd, snap.FogEnd, precision: 2);
}
}