acdream/tests/AcDream.Core.Tests/World/DerethDateTimeTests.cs
Erik 6850d716a2 feat(world): Phase G.1 DerethDateTime + SkyStateProvider + WorldTimeService
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>
2026-04-18 17:07:26 +02:00

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);
}
}