Utils/DayNightController.cs
using System;
using HC3.Persistence;
using Sandbox.Diagnostics;
public interface ITimeOfDayEvents : ISceneEvent<ITimeOfDayEvents>
{
/// <summary>
/// Called when there's a new day (this is only called on the host.)
/// </summary>
public void OnNewDay();
}
[EditorHandle( Icon = "🌞" )]
public sealed class DayNightController : Component, ISaveDataProperty<DayNightController.SaveData>
{
public static DayNightController Instance { get; private set; }
[ConVar( "hc3.locktod", ConVarFlags.Cheat | ConVarFlags.Replicated )]
public static bool LockTimeOfDay { get; set; } = false;
[Property, Sync( SyncFlags.FromHost )]
public float TimeOfDay { get; private set; } = 12f;
[Sync( SyncFlags.FromHost )]
public float CurrentTimeScale { get; private set; } = 1f;
[Sync( SyncFlags.FromHost )]
public int Day { get; private set; } = 1;
[Sync( SyncFlags.FromHost )]
public int Month { get; private set; } = 1;
[Sync( SyncFlags.FromHost )]
public int Year { get; private set; } = 2025;
public bool IsPaused => CurrentTimeScale.AlmostEqual( 0 );
[Property] private float TimeScale { get; set; } = 0.05f;
[Property] Gradient ColorGradient { get; set; } = new Gradient();
[Property] Gradient FogGradient { get; set; } = new Gradient();
[Property] Curve FogDensity { get; set; } = new Curve();
DirectionalLight SunLight { get; set; }
[RequireComponent] GradientFog Fog { get; set; }
/// <summary>
/// When does the park close?
/// </summary>
[ConVar( "hc3.park.close_time" )]
public static float ParkCloseTime { get; set; } = 20f;
/// <summary>
/// When does the park open?
/// </summary>
[ConVar( "hc3.park.open_time" )]
public static float ParkOpenTime { get; set; } = 6f;
/// <summary>
/// Is the park open?
/// </summary>
/// <returns></returns>
public static bool IsParkOpen()
{
if ( Instance?.TimeOfDay < ParkOpenTime ) return false;
if ( Instance?.TimeOfDay > ParkCloseTime ) return false;
return true;
}
/// <summary>
/// Are we in peak?
/// </summary>
/// <returns></returns>
public static bool IsPeakSeason()
{
// April -> September
return Instance?.Month is < 9 and > 4;
}
private static readonly int[] DaysInMonth =
[
31, 28, 31, 30, 31, 30,
31, 31, 30, 31, 30, 31
];
private static readonly string[] MonthNames =
[
"Jan", "Feb", "Mar", "Apr", "May", "Jun",
"Jul", "Aug", "Sep", "Oct", "Nov", "Dec"
];
private static bool IsLeapYear( int year )
{
return (year % 4 == 0 && year % 100 != 0) || (year % 400 == 0);
}
private void AdvanceDate()
{
Day++;
var daysThisMonth = DaysInMonth[Month - 1];
if ( Month == 2 && IsLeapYear( Year ) )
{
daysThisMonth = 29;
}
if ( Day > daysThisMonth )
{
Day = 1;
Month++;
if ( Month > 12 )
{
Month = 1;
Year++;
}
}
}
[Rpc.Host]
public void TogglePause()
{
if ( Scene.TimeScale > 0f )
{
CurrentTimeScale = 0f;
}
else
{
CurrentTimeScale = 1;
}
}
[Rpc.Host]
public void ResetSpeed()
{
CurrentTimeScale = 1f;
}
[Rpc.Host]
public void SetSpeed( float newSpeed )
{
CurrentTimeScale = newSpeed;
if ( CurrentTimeScale < 0 )
CurrentTimeScale = 0;
if ( CurrentTimeScale > 5 )
CurrentTimeScale = 5;
}
protected override void OnAwake()
{
Instance = this;
}
protected override void OnStart()
{
SunLight = Scene.GetAll<DirectionalLight>().FirstOrDefault();
}
float GetTimeScale()
{
if ( LockTimeOfDay ) return 0;
return TimeScale;
}
[Property]
public float LightPower { get; set; } = 1f;
protected override void OnUpdate()
{
if ( Networking.IsHost )
{
TimeOfDay += GetTimeScale() * Time.Delta;
if ( TimeOfDay >= 24f )
{
StartNewDay();
}
}
Scene.TimeScale = CurrentTimeScale;
var sunrise = 6f;
var sunset = 20f;
float targetAngle;
if ( TimeOfDay >= sunrise && TimeOfDay <= sunset )
{
float t = (TimeOfDay - sunrise) / (sunset - sunrise);
targetAngle = t * 180f;
}
else
{
float t = (TimeOfDay < sunrise ? TimeOfDay + 24f : TimeOfDay - sunset) / (24f - (sunset - sunrise));
targetAngle = 180f + t * 180f;
}
float duration = 1f;
float fadeIn = Math.Clamp( (TimeOfDay - sunrise) / duration, 0f, 1f );
float fadeOut = Math.Clamp( (sunset - TimeOfDay) / duration, 0f, 1f );
// day = 1, night = 0 fades across [duration] hours before sunrise and after sunset (so sunrise/sunset is 0.5)
float frac = MathF.Min( fadeIn, fadeOut );
var color = ColorGradient.Evaluate( frac );
// sun / ambient
SunLight.LightColor = Color.Lerp( Color.Black, color, frac ) * LightPower;
SunLight.SkyColor = color * 0.4f;
SunLight.GameObject.WorldRotation = Rotation.From( targetAngle, 60f, 0f );
// fog
var fogColor = FogGradient.Evaluate( frac );
Fog.Color = fogColor;
Fog.Height = FogDensity.Evaluate( frac );
}
public string GetTimeString()
{
var hours = (int)TimeOfDay;
var minutes = (int)((TimeOfDay - hours) * 60);
return $"{hours:D2}:{minutes:D2}";
}
public string GetDateString()
{
return $"{Day:D2}/{Month:D2}/{Year}";
}
public string GetLongDateString()
{
return $"{Day:D2} {MonthNames[Month - 1]}, {Year}";
}
private void StartNewDay()
{
Assert.True( Networking.IsHost );
TimeOfDay = 0f;
AdvanceDate();
ITimeOfDayEvents.Post( x => x.OnNewDay() );
}
string ISaveDataProperty.PropertyName => "DateTime";
int ISaveDataProperty.PropertyOrder => -6_000;
private sealed record SaveData( float TimeOfDay, int Day, int Month, int Year );
SaveData ISaveDataProperty<SaveData>.WriteValue( Scene scene ) =>
new( TimeOfDay, Day, Month, Year );
void ISaveDataProperty<SaveData>.ReadValue( Scene scene, SaveData model )
{
TimeOfDay = model.TimeOfDay;
Day = model.Day;
Month = model.Month;
Year = model.Year;
}
}