Client-side deterministic sky + weather + day/night system per R12. Retail's model is 95% client-side: the server just delivers its current PortalYearTicks (double, seconds since boot-seed) at login and in TimeSync packets; the client computes everything else locally from the constants in r12 §1.2 + ACE DerethDateTime.cs. Core layer (AcDream.Core/World): - DerethDateTime: retail-exact calendar (16 hours/day, 30 days/month, 12 months/year, 7620 ticks/day, 2,743,200 ticks/year). HourName enum covers all 16 named half-hour slots (Darktide → GloamingAndHalf); MonthName covers the 12 Derethian months (Snowreap → Frostfell). DayFraction, CurrentHour, IsDaytime, ToCalendar. - SkyKeyframe + SkyStateProvider: 4-keyframe default day/dawn/noon/dusk with linear color + angular-wrap heading interpolation + slerp-like shortest-arc lerp so heading wraps 350° → 10° don't tween backwards through 180°. Default keyframe colors tuned to retail screenshots (sunrise warm, noon white, sunset red, midnight deep blue). - WorldTimeService: owns the live clock. SyncFromServer(ticks) sets baseline; NowTicks advances by real-time elapsed. Exposes DayFraction, CurrentSky, CurrentSunDirection, IsDaytime for the render thread. This is the foundation Phase G.2 (dynamic lighting) consumes: lighting uniforms are fed from CurrentSky's SunColor / AmbientColor / sun direction, varying smoothly across the day. Tests (16 new): - DerethDateTime: midnight, half-day, wrap, Dawnsong, Midsong, day/night flag at dawn vs Darktide-Half, year rollover, month advance. - SkyState: 4-default keyframes, noon-exact matches frame data, midpoint lerps between neighbours, wrap across midnight doesn't produce NaN, sun direction returns unit vector, WorldTimeService sync + DayFraction at noon. Build green, 587 tests pass (up from 570). Ref: r12 §1 (Portal Year math), §2 (sky objects), §4 (color lerp). Ref: ACE DerethDateTime.cs + NetworkSession TimeSync handler. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
89 lines
2.8 KiB
C#
89 lines
2.8 KiB
C#
using System.Numerics;
|
|
using AcDream.Core.World;
|
|
using Xunit;
|
|
|
|
namespace AcDream.Core.Tests.World;
|
|
|
|
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_LerpsColors()
|
|
{
|
|
var sky = SkyStateProvider.Default();
|
|
var dawn = sky.Interpolate(0.25f);
|
|
var noon = sky.Interpolate(0.5f);
|
|
var midPt = sky.Interpolate(0.375f);
|
|
|
|
// Midpoint should fall between dawn & noon for sun color Y (green channel).
|
|
float low = System.Math.Min(dawn.SunColor.Y, noon.SunColor.Y);
|
|
float high = System.Math.Max(dawn.SunColor.Y, noon.SunColor.Y);
|
|
Assert.InRange(midPt.SunColor.Y, low, high);
|
|
}
|
|
|
|
[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,
|
|
SunColor: Vector3.One,
|
|
AmbientColor: Vector3.One,
|
|
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());
|
|
// Sync to exactly noon of day 0.
|
|
service.SyncFromServer(DerethDateTime.DayTicks * 0.5);
|
|
|
|
Assert.InRange(service.DayFraction, 0.499, 0.501);
|
|
Assert.True(service.IsDaytime);
|
|
}
|
|
}
|