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 Snapshot_AlwaysPassesKeyframeFog_RegardlessOfKind() { // Phase 7: retail DOES NOT override fog by WeatherKind — Storm // doesn't produce denser fog, Overcast doesn't shrink distance. // Every Kind renders the keyframe's fog directly. This test // replaces the old "Transition_EasesAcrossTenSeconds" which // codified the invented per-Kind fog behaviour. var kf = SkyStateProvider.Default().Interpolate(0.5f); foreach (var kind in new[] { WeatherKind.Clear, WeatherKind.Overcast, WeatherKind.Rain, WeatherKind.Snow, WeatherKind.Storm, }) { var sys = new WeatherSystem(); sys.ForceWeather(kind); sys.Tick(0, 1, 100f); // finalize any transition var snap = sys.Snapshot(in kf); Assert.Equal(kind, snap.Kind); Assert.Equal(kf.FogStart, snap.FogStart, precision: 2); Assert.Equal(kf.FogEnd, snap.FogEnd, precision: 2); Assert.Equal(kf.FogColor, snap.FogColor); } } [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); } [Theory] [InlineData("Sunny", WeatherKind.Clear)] [InlineData("SUNNY", WeatherKind.Clear)] [InlineData("Clear", WeatherKind.Clear)] [InlineData("", WeatherKind.Clear)] [InlineData(null, WeatherKind.Clear)] // All "weathery" names map to Overcast. Retail does NOT spawn rain / // snow / lightning from the DayGroup name — verified by the 2026-04-23 // PhysicsScript + sky-PES decompile audits (see WeatherState.cs). Any // future particle rain must come from the camera-attached weather // subsystem, NOT from name string matching. [InlineData("Cloudy", WeatherKind.Overcast)] [InlineData("Overcast", WeatherKind.Overcast)] [InlineData("Dark skies", WeatherKind.Overcast)] [InlineData("Fog", WeatherKind.Overcast)] [InlineData("Rainy", WeatherKind.Overcast)] [InlineData("heavy rain", WeatherKind.Overcast)] [InlineData("Snowy", WeatherKind.Overcast)] [InlineData("Blizzard", WeatherKind.Clear)] // no matcher — default [InlineData("Stormy", WeatherKind.Overcast)] [InlineData("Thunderstorm", WeatherKind.Overcast)] public void SetKindFromDayGroupName_MapsRetailNames(string? name, WeatherKind expected) { var sys = new WeatherSystem(); sys.SetKindFromDayGroupName(name); sys.Tick(0, 0, 100f); // finalize transition Assert.Equal(expected, sys.Kind); } [Fact] public void SetKindFromDayGroupName_DisablesInternalRoll() { // Once driven externally, advancing dayIndex must NOT re-roll // to a different kind via the internal RollKind hash. var sys = new WeatherSystem(); sys.SetKindFromDayGroupName("Sunny"); sys.Tick(0, 0, 100f); var clearKind = sys.Kind; Assert.Equal(WeatherKind.Clear, clearKind); for (int d = 1; d < 50; d++) { sys.Tick(0, d, 100f); Assert.Equal(clearKind, sys.Kind); // stays put — no auto-roll } } }