Final pre-decompile-era invention cleanup. Snapshot() now returns
the keyframe's fog (color, start, end) directly in all cases.
AdminEnvirons override replaces fog COLOR only; distances stay at
the keyframe's MinWorldFog/MaxWorldFog.
Removed:
- FogForKind(kind, kf): the per-WeatherKind fog table with
invented constants (Overcast 40-150m grey, Storm 25-90m dark,
Rain 40-150m blue, Snow 60-200m white). Retail has no such
logic — Agent #3's decompile scan found zero per-Kind fog
manipulation in chunk_005* / chunk_006*. The SkyTimeOfDay
keyframe interp (FUN_00501860) does all fog value selection.
- OvercastFogStart/End, StormFogStart/End constants.
- Storm-kind random lightning timer + _strikeJitter. Retail's
lightning is server-driven via PlayScript (Phase 6), not a
client timer — Agents #3 + #5 both rule this out.
- Per-Kind cross-fade (_transitionT and TransitionSeconds-based
lerp). Retail has a different crossfade — SkyTimeOfDay step
blending via LightTickSize gating (_DAT_008427b8 + _DAT_007c7208)
— which is the deferred Phase 5c "polish" item.
Result:
- Clear: keyframe fog passthrough — unchanged behaviour.
- Overcast / Rain / Snow / Storm: now ALSO keyframe passthrough.
Previously these clobbered the keyframe with the invented
constants, producing a grey-wall sky that extended no further
than ~150m. User observation 2026-04-23: "retail sky extends
all the way into the horizon, we cap at a grey wall." Fixed.
- EnvironOverride (AdminEnvirons RedFog, BlueFog, etc):
substitutes the fog COLOR preset, keeps keyframe distances.
WeatherKind enum retained as purely informational (debug overlay,
telemetry). Internal RollKind fallback retained for offline tests
that drive Tick() directly without SetKindFromDayGroupName.
TriggerFlash()/flash decay retained as a test-only hook for the
UBO's lightning-flash channel — production flash stays 0 since
retail drives lightning visuals through particle emitters, not
through a UBO uniform.
Tests updated: `Transition_EasesAcrossTenSeconds` deleted (codified
the Storm=dense-fog invention we just removed) and replaced by
`Snapshot_AlwaysPassesKeyframeFog_RegardlessOfKind` which asserts
every WeatherKind returns the keyframe fog directly.
Build + 742 tests green.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
161 lines
5.6 KiB
C#
161 lines
5.6 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 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
|
|
}
|
|
}
|
|
}
|