acdream/tests/AcDream.Core.Tests/World/WeatherSystemTests.cs
Erik 5f9df4d620 sky(phase-3e): drive WeatherSystem from DayGroup name — no more rogue rain
User reported rain in acdream while retail showed a clear sunny sky
after Phase 3d landed. Root cause: two independent weather systems
running in parallel.

  1. Retail DayGroup picker (FUN_00501990 port, Phase 3c/3c.1) —
     selected DayGroup[6] "Sunny" correctly.
  2. WeatherSystem.Tick (legacy stub from pre-decompile era) —
     kept rolling its own hardcoded PDF every day (60% Clear, 20%
     Overcast, 12% Rain, 5% Snow, 3% Storm), independent of the
     DayGroup picker. Its output drove the rain/snow particle
     emitters via UpdateWeatherParticles. If its hash happened to
     land on Rain for today's dayIndex, rain rendered even on a
     Sunny DayGroup day.

Retail has ONE source of truth for weather: the DayGroup roll. There
is no separate weather state machine — rain/snow/storm are implied by
the DayGroup name and its per-keyframe SkyObjectReplace settings.

Fix (Phase 3e):
- WeatherSystem.SetKindFromDayGroupName(string?) — loose substring
  match on the retail DayGroup name: "storm" → Storm, "snow" → Snow,
  "rain" → Rain, "cloud"/"overcast"/"dark"/"fog" → Overcast, else
  Clear. Case-insensitive. Covers the names observed in the live
  Dereth dat dump (Sunny, Clear, Cloudy, Rainy + inferred variants).
- WeatherSystem._externallyDriven flag disables the internal
  RollKind auto-roll once SetKindFromDayGroupName has been called at
  least once. Tests that drive Tick() directly keep the legacy
  hash-roll behavior (offline fallback). ForceWeather still works
  for debug overrides.
- GameWindow.RefreshSkyForCurrentDay calls
  Weather.SetKindFromDayGroupName(grp.Name) right after it installs
  the new SkyStateProvider. Logs the resulting WeatherKind on the
  same line as the DayGroup pick for correlation.
- New WeatherSystemTests.SetKindFromDayGroupName_MapsRetailNames
  (theory, 14 cases) + SetKindFromDayGroupName_DisablesInternalRoll.

Expected effect: Sunny/Clear DayGroups → no rain emitter. Rainy/Stormy
DayGroups → rain emitter active. The user's specific scenario
(DayGroup[6] "Sunny") now correctly maps to WeatherKind.Clear and no
particles spawn.

Build + 733 tests green (+16 new).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-24 09:32:27 +02:00

145 lines
4.8 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);
}
[Theory]
[InlineData("Sunny", WeatherKind.Clear)]
[InlineData("SUNNY", WeatherKind.Clear)]
[InlineData("Clear", WeatherKind.Clear)]
[InlineData("Cloudy", WeatherKind.Overcast)]
[InlineData("Overcast", WeatherKind.Overcast)]
[InlineData("Dark skies", WeatherKind.Overcast)]
[InlineData("Fog", WeatherKind.Overcast)]
[InlineData("Rainy", WeatherKind.Rain)]
[InlineData("heavy rain", WeatherKind.Rain)]
[InlineData("Snowy", WeatherKind.Snow)]
[InlineData("Blizzard", WeatherKind.Clear)] // no matcher — default
[InlineData("Stormy", WeatherKind.Storm)]
[InlineData("Thunderstorm", WeatherKind.Storm)] // "storm" wins over no match
[InlineData("", WeatherKind.Clear)]
[InlineData(null, WeatherKind.Clear)]
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
}
}
}