acdream/tests/AcDream.Core.Tests/World/SkyStateTests.cs

182 lines
6.3 KiB
C#
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

using System.Numerics;
using AcDream.Core.World;
using Xunit;
namespace AcDream.Core.Tests.World;
[Collection(DerethDateTimeCollection.Name)]
public sealed class SkyStateTests
{
[Fact]
public void Default_Has4Keyframes()
{
var sky = SkyStateProvider.Default();
Assert.Equal(4, sky.KeyframeCount);
}
[Fact]
public void Interpolate_AtExactKeyframe_ReturnsThatFrameData()
{
var sky = SkyStateProvider.Default();
var noon = sky.Interpolate(0.5f); // noon keyframe
// Noon sky color should be near white (1.0 ish).
Assert.InRange(noon.SunColor.X, 0.9f, 1.1f);
Assert.InRange(noon.SunColor.Y, 0.9f, 1.1f);
}
[Fact]
public void Interpolate_BetweenKeyframes_LerpsRawInputs()
{
var sky = SkyStateProvider.Default();
var dawn = sky.Interpolate(0.25f);
var noon = sky.Interpolate(0.5f);
var midPt = sky.Interpolate(0.375f);
// The RAW per-channel inputs (DirColor, AmbColor, brightness scalars)
// lerp linearly between adjacent keyframes — that's the retail-faithful
// separate-channel interpolation. The composite SunColor / AmbientColor
// properties intentionally do NOT lerp linearly (their magnitude
// depends nonlinearly on heading/pitch/brightness via the retail
// sun-vector formula), so we assert on the raw inputs here.
float low = System.Math.Min(dawn.DirColor.Y, noon.DirColor.Y);
float high = System.Math.Max(dawn.DirColor.Y, noon.DirColor.Y);
Assert.InRange(midPt.DirColor.Y, low, high);
}
[Fact]
public void RetailSunVector_AtZenith_HasMagnitudeEqualToBrightness()
{
// Sun straight up (P=90°): cos(P)=0, sin(P)=1.
// sunVec = (sin(H)×B×0, 0, B×1) = (0, 0, B)
// |sunVec| = B
var kf = new SkyKeyframe(
Begin: 0.5f,
SunHeadingDeg: 0f,
SunPitchDeg: 90f,
DirColor: Vector3.One,
DirBright: 1.5f,
AmbColor: Vector3.One,
AmbBright: 0.3f,
FogColor: Vector3.One,
FogDensity: 0f);
var v = SkyStateProvider.RetailSunVector(kf);
Assert.InRange(v.Length(), 1.49f, 1.51f);
}
[Fact]
public void RetailSunVector_AtHorizonNorth_MagnitudeIsOne()
{
// Sun on horizon to the north (H=0°, P=0°): cos(P)=1, sin(P)=0.
// sunVec = (sin(0)×B×1, 1, B×0) = (0, 1, 0)
// |sunVec| = 1 regardless of B (because Y is unscaled by B)
var kf = new SkyKeyframe(
Begin: 0f,
SunHeadingDeg: 0f,
SunPitchDeg: 0f,
DirColor: Vector3.One,
DirBright: 2.0f, // anything
AmbColor: Vector3.One,
AmbBright: 1f,
FogColor: Vector3.One,
FogDensity: 0f);
var v = SkyStateProvider.RetailSunVector(kf);
Assert.InRange(v.Length(), 0.99f, 1.01f);
}
[Fact]
public void SunColor_UsesRetailMagnitudeNotDirBrightDirectly()
{
// At sun pitch 90° (zenith) with H=0, B=2: |sunVec| = 2.
// SunColor = DirColor × |sunVec| = (0.5, 0.5, 0.5) × 2 = (1, 1, 1).
var kf = new SkyKeyframe(
Begin: 0.5f,
SunHeadingDeg: 0f,
SunPitchDeg: 90f,
DirColor: new Vector3(0.5f, 0.5f, 0.5f),
DirBright: 2.0f,
AmbColor: Vector3.One,
AmbBright: 0.3f,
FogColor: Vector3.One,
FogDensity: 0f);
Assert.InRange(kf.SunColor.X, 0.99f, 1.01f);
Assert.InRange(kf.SunColor.Y, 0.99f, 1.01f);
Assert.InRange(kf.SunColor.Z, 0.99f, 1.01f);
}
[Fact]
public void AmbientColor_BoostsByTwentyPercentOfSunVectorLength()
{
// |sunVec| = 1 (horizon north), AmbBright = 0.4, AmbColor = (1,1,1).
// AmbientColor = AmbColor × (AmbBright + 0.2 × |sunVec|)
// = (1,1,1) × (0.4 + 0.2) = (0.6, 0.6, 0.6).
var kf = new SkyKeyframe(
Begin: 0f,
SunHeadingDeg: 0f,
SunPitchDeg: 0f,
DirColor: Vector3.One,
DirBright: 1f,
AmbColor: Vector3.One,
AmbBright: 0.4f,
FogColor: Vector3.One,
FogDensity: 0f);
Assert.InRange(kf.AmbientColor.X, 0.59f, 0.61f);
}
[Fact]
public void Interpolate_Wraps_AcrossMidnight()
{
var sky = SkyStateProvider.Default();
var justAfterMidnight = sky.Interpolate(0.01f);
// Should return finite valid state (not NaN).
Assert.False(float.IsNaN(justAfterMidnight.SunColor.X));
Assert.False(float.IsNaN(justAfterMidnight.AmbientColor.X));
}
[Fact]
public void SunDirectionFromKeyframe_ReturnsUnitVector()
{
var kf = new SkyKeyframe(
Begin: 0.5f,
SunHeadingDeg: 180f, // south
SunPitchDeg: 70f,
DirColor: Vector3.One,
DirBright: 1f,
AmbColor: Vector3.One,
AmbBright: 1f,
FogColor: Vector3.One,
FogDensity: 0.001f);
var dir = SkyStateProvider.SunDirectionFromKeyframe(kf);
Assert.InRange(dir.Length(), 0.99f, 1.01f);
}
[Fact]
public void WorldTimeService_SyncFromServer_SetsTicks()
{
var service = new WorldTimeService(SkyStateProvider.Default());
service.SyncFromServer(12345.0);
// NowTicks advances by real elapsed time; but immediately after
// sync it should be at or very close to the synced value.
Assert.InRange(service.NowTicks, 12345.0, 12346.0);
}
[Fact]
public void WorldTimeService_DayFraction_RespectsSync()
{
var service = new WorldTimeService(SkyStateProvider.Default());
// Need to aim for dayFraction 0.5 (Gloaming-and-Half, slot 15 since tick 0 = slot 7).
// Sync to (0.5 - 7/16) * DayTicks = (1/16) * DayTicks — 1 slot past Morntide-and-Half = Midsong.
// Actually simpler: target fraction 7/16 (slot 7 = Morntide-and-Half) by syncing to tick 0.
service.SyncFromServer(0);
Assert.InRange(service.DayFraction, 0.43, 0.44); // 7/16 = 0.4375
Assert.True(service.IsDaytime);
}
}