namespace HSchool.Simulation;
///
/// In-game calendar of one school. Time only moves while is set, and it
/// moves by whole fixed steps — never by wall-clock deltas — so the same tick count always
/// produces the same date.
///
public sealed class GameClock
{
/// Earliest date a school may start at; anything below is a typo, not a design choice.
public static readonly DateTime MinStartDate = new(1900, 1, 1, 0, 0, 0, DateTimeKind.Utc);
public static readonly DateTime MaxStartDate = new(2999, 12, 31, 23, 59, 59, DateTimeKind.Utc);
private int _speedIndex = ClockSpeed.DefaultIndex;
public GameClock(DateTime startDate)
{
if (!IsValidStartDate(startDate))
{
throw new ArgumentOutOfRangeException(nameof(startDate), startDate, "Start date is outside the supported range.");
}
// The game calendar is not tied to a real time zone; UTC keeps serialization unambiguous.
Time = DateTime.SpecifyKind(startDate, DateTimeKind.Utc);
}
public DateTime Time { get; private set; }
///
/// Schools live on their own: a new calendar starts running and only the player's pause
/// button stops it. Leaving for the menu does not.
///
public bool IsRunning { get; set; } = true;
/// Index into ; invalid values are ignored.
public int SpeedIndex
{
get => _speedIndex;
set
{
if (ClockSpeed.IsValid(value))
{
_speedIndex = value;
}
}
}
public double Multiplier => ClockSpeed.MultiplierAt(_speedIndex);
public static bool IsValidStartDate(DateTime date) => date >= MinStartDate && date <= MaxStartDate;
/// Empty-time skip. Not a tick — the calendar jumps to an instant already known to be legal.
public void JumpTo(DateTime time)
{
if (!IsValidStartDate(time))
{
throw new ArgumentOutOfRangeException(nameof(time), time, "Jump target is outside the supported range.");
}
Time = DateTime.SpecifyKind(time, DateTimeKind.Utc);
}
///
/// Advances the calendar by one fixed step of , scaled by the
/// base rate and the current speed. Does nothing while paused.
///
/// Game minutes actually added; zero while paused.
public double Advance(double realSeconds, double gameMinutesPerRealSecond)
{
if (!IsRunning)
{
return 0;
}
var gameMinutes = realSeconds * gameMinutesPerRealSecond * Multiplier;
Time = Time.AddMinutes(gameMinutes);
return gameMinutes;
}
}