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>
86 lines
2.6 KiB
C#
86 lines
2.6 KiB
C#
using AcDream.Core.World;
|
|
using Xunit;
|
|
|
|
namespace AcDream.Core.Tests.World;
|
|
|
|
public sealed class DerethDateTimeTests
|
|
{
|
|
[Fact]
|
|
public void DayFraction_AtMidnight_IsZero()
|
|
{
|
|
Assert.Equal(0.0, DerethDateTime.DayFraction(0));
|
|
}
|
|
|
|
[Fact]
|
|
public void DayFraction_AtHalfDay_IsHalf()
|
|
{
|
|
Assert.Equal(0.5, DerethDateTime.DayFraction(DerethDateTime.DayTicks / 2), 4);
|
|
}
|
|
|
|
[Fact]
|
|
public void DayFraction_WrapsAfterOneDay()
|
|
{
|
|
Assert.Equal(0.0, DerethDateTime.DayFraction(DerethDateTime.DayTicks), 6);
|
|
Assert.Equal(0.25, DerethDateTime.DayFraction(DerethDateTime.DayTicks * 1.25), 4);
|
|
}
|
|
|
|
[Fact]
|
|
public void CurrentHour_AtMidnight_IsDarktide()
|
|
{
|
|
Assert.Equal(DerethDateTime.HourName.Darktide, DerethDateTime.CurrentHour(0));
|
|
}
|
|
|
|
[Fact]
|
|
public void CurrentHour_AtDawn_IsDawnsong()
|
|
{
|
|
// Dawnsong is the 5th slot (0-indexed slot 4) — day-fraction 4/16 = 0.25.
|
|
double ticks = DerethDateTime.DayTicks * (4.0 / 16.0);
|
|
Assert.Equal(DerethDateTime.HourName.Dawnsong, DerethDateTime.CurrentHour(ticks));
|
|
}
|
|
|
|
[Fact]
|
|
public void CurrentHour_AtNoon_IsMidsong()
|
|
{
|
|
// Midsong is 0-indexed slot 8 → day-fraction 0.5.
|
|
double ticks = DerethDateTime.DayTicks * 0.5;
|
|
Assert.Equal(DerethDateTime.HourName.Midsong, DerethDateTime.CurrentHour(ticks));
|
|
}
|
|
|
|
[Fact]
|
|
public void IsDaytime_Dawn_True()
|
|
{
|
|
double ticks = DerethDateTime.DayTicks * (4.0 / 16.0); // Dawnsong start
|
|
Assert.True(DerethDateTime.IsDaytime(ticks));
|
|
}
|
|
|
|
[Fact]
|
|
public void IsDaytime_Night_False()
|
|
{
|
|
double ticks = DerethDateTime.DayTicks * (1.0 / 16.0); // Darktide-and-Half
|
|
Assert.False(DerethDateTime.IsDaytime(ticks));
|
|
}
|
|
|
|
[Fact]
|
|
public void ToCalendar_PY0Day1_Snowreap()
|
|
{
|
|
var cal = DerethDateTime.ToCalendar(0);
|
|
Assert.Equal(0, cal.Year);
|
|
Assert.Equal(DerethDateTime.MonthName.Snowreap, cal.Month);
|
|
Assert.Equal(1, cal.Day);
|
|
}
|
|
|
|
[Fact]
|
|
public void ToCalendar_AdvancesCorrectly()
|
|
{
|
|
// One year from start → PY 1, Snowreap 1.
|
|
var cal = DerethDateTime.ToCalendar(DerethDateTime.YearTicks);
|
|
Assert.Equal(1, cal.Year);
|
|
Assert.Equal(DerethDateTime.MonthName.Snowreap, cal.Month);
|
|
Assert.Equal(1, cal.Day);
|
|
|
|
// One month into year 1.
|
|
var cal2 = DerethDateTime.ToCalendar(DerethDateTime.YearTicks + DerethDateTime.MonthTicks);
|
|
Assert.Equal(1, cal2.Year);
|
|
Assert.Equal(DerethDateTime.MonthName.ColdMeet, cal2.Month);
|
|
}
|
|
}
|