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