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>
102 lines
3.1 KiB
C#
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);
|
|
}
|
|
}
|