215 lines
7.5 KiB
C#
215 lines
7.5 KiB
C#
namespace Jovian.Calendar {
|
|
/// <summary>
|
|
/// Core calendar clock. Feed it a 0-1 normalized day float each tick.
|
|
/// Internally accumulates world-minutes and rolls them into the calendar.
|
|
/// </summary>
|
|
public class WorldClock {
|
|
/// <summary>The calendar configuration driving this clock.</summary>
|
|
public CalendarSettings Settings { get; }
|
|
|
|
/// <summary>The current world date and time.</summary>
|
|
public WorldDateTime Now { get; private set; }
|
|
|
|
/// <summary>
|
|
/// Total in-world minutes elapsed since the start date.
|
|
/// This is the single source of truth. WorldDateTime is derived from it.
|
|
/// </summary>
|
|
public long TotalElapsedMinutes { get; private set; }
|
|
|
|
/// <summary>The normalized time (0-1) from the last tick.</summary>
|
|
public float LastNormalizedTime { get; private set; }
|
|
|
|
private float prevNormalized;
|
|
private bool initialized;
|
|
private float fractionalMinutes;
|
|
private readonly long startOffsetMinutes;
|
|
|
|
/// <summary>
|
|
/// Creates a new world clock from the given calendar settings.
|
|
/// Validates settings and initializes to the configured start date.
|
|
/// </summary>
|
|
public WorldClock(CalendarSettings settings) {
|
|
settings.Validate();
|
|
Settings = settings;
|
|
|
|
startOffsetMinutes = ComputeMinuteOffset(
|
|
settings, settings.startYear, settings.startMonth,
|
|
settings.startDay, settings.startHour, settings.startMinute
|
|
);
|
|
|
|
TotalElapsedMinutes = 0;
|
|
prevNormalized = -1f;
|
|
initialized = false;
|
|
|
|
Now = MakeDateTime(startOffsetMinutes);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Call every frame with the current normalized day progress (0..1).
|
|
/// The clock detects the delta since last tick and advances time accordingly.
|
|
/// Handles wrap-around (1 to 0 = new day crossing).
|
|
/// </summary>
|
|
public void Tick(float normalizedTime) {
|
|
normalizedTime = Clamp01(normalizedTime);
|
|
LastNormalizedTime = normalizedTime;
|
|
|
|
if(!initialized) {
|
|
prevNormalized = normalizedTime;
|
|
initialized = true;
|
|
return;
|
|
}
|
|
|
|
var delta = normalizedTime - prevNormalized;
|
|
|
|
if(delta < 0f) {
|
|
delta += 1f;
|
|
}
|
|
|
|
if(delta < 1e-7f) {
|
|
prevNormalized = normalizedTime;
|
|
return;
|
|
}
|
|
|
|
var minutesAdvanced = delta * Settings.MinutesPerDay;
|
|
fractionalMinutes += minutesAdvanced;
|
|
|
|
var wholeMinutes = (int)fractionalMinutes;
|
|
if(wholeMinutes > 0) {
|
|
fractionalMinutes -= wholeMinutes;
|
|
TotalElapsedMinutes += wholeMinutes;
|
|
Now = MakeDateTime(startOffsetMinutes + TotalElapsedMinutes);
|
|
}
|
|
|
|
prevNormalized = normalizedTime;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Manually advance the clock by a number of in-world minutes.
|
|
/// Useful for sleep/rest skips, cutscenes, etc.
|
|
/// </summary>
|
|
public void AdvanceMinutes(int minutes) {
|
|
if(minutes <= 0) {
|
|
return;
|
|
}
|
|
TotalElapsedMinutes += minutes;
|
|
Now = MakeDateTime(startOffsetMinutes + TotalElapsedMinutes);
|
|
|
|
var dayFraction = (float)((Now.hour * Settings.minutesPerHour) + Now.minute) / Settings.MinutesPerDay;
|
|
prevNormalized = dayFraction;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Converts a total-minutes-from-epoch value into a WorldDateTime.
|
|
/// </summary>
|
|
private WorldDateTime MakeDateTime(long totalMinutes) {
|
|
if(totalMinutes < 0) {
|
|
totalMinutes = 0;
|
|
}
|
|
|
|
var minutesPerDay = Settings.MinutesPerDay;
|
|
var minutesPerHour = Settings.minutesPerHour;
|
|
|
|
var totalDays = totalMinutes / minutesPerDay;
|
|
var remainderMinutes = (int)(totalMinutes % minutesPerDay);
|
|
|
|
var hour = remainderMinutes / minutesPerHour;
|
|
var minute = remainderMinutes % minutesPerHour;
|
|
|
|
var daysInYear = Settings.TotalDaysInYear;
|
|
|
|
var year = (int)(totalDays / daysInYear);
|
|
var remainingDays = (int)(totalDays % daysInYear);
|
|
|
|
var month = 0;
|
|
for(var m = 0; m < Settings.daysPerMonth.Length; m++) {
|
|
if(remainingDays < Settings.daysPerMonth[m]) {
|
|
month = m;
|
|
break;
|
|
}
|
|
remainingDays -= Settings.daysPerMonth[m];
|
|
|
|
if(m == Settings.daysPerMonth.Length - 1) {
|
|
month = m;
|
|
break;
|
|
}
|
|
}
|
|
|
|
return new WorldDateTime {
|
|
year = year,
|
|
month = month,
|
|
day = remainingDays,
|
|
hour = hour,
|
|
minute = minute
|
|
};
|
|
}
|
|
|
|
/// <summary>
|
|
/// Inverse of MakeDateTime: given a calendar date, produce total minutes from epoch.
|
|
/// </summary>
|
|
private static long ComputeMinuteOffset(
|
|
CalendarSettings s, int year, int month, int day, int hour, int minute) {
|
|
var totalDays = (long)year * s.TotalDaysInYear;
|
|
for(var m = 0; m < month; m++) {
|
|
totalDays += s.daysPerMonth[m];
|
|
}
|
|
totalDays += day;
|
|
|
|
var totalMinutes = totalDays * s.MinutesPerDay;
|
|
totalMinutes += (long)hour * s.minutesPerHour;
|
|
totalMinutes += minute;
|
|
return totalMinutes;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Get the current day-of-week (0-indexed), or -1 if weeks are disabled.
|
|
/// </summary>
|
|
public int DayOfWeek {
|
|
get {
|
|
if(Settings.daysPerWeek <= 0) {
|
|
return -1;
|
|
}
|
|
var totalDays = (startOffsetMinutes + TotalElapsedMinutes) / Settings.MinutesPerDay;
|
|
return (int)(totalDays % Settings.daysPerWeek);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Normalized time of day as 0-1 float, derived from the current WorldDateTime.
|
|
/// Useful for lighting, skybox lerp, etc.
|
|
/// </summary>
|
|
public float NormalizedTimeOfDay {
|
|
get {
|
|
float currentMinute = (Now.hour * Settings.minutesPerHour) + Now.minute;
|
|
return currentMinute / Settings.MinutesPerDay;
|
|
}
|
|
}
|
|
|
|
/// <summary>Returns the name of the current month from settings.</summary>
|
|
public string GetMonthName() {
|
|
var month = Now.month;
|
|
if(Settings.monthNames == null || month < 0 || month >= Settings.monthNames.Length) {
|
|
return "???";
|
|
}
|
|
return Settings.monthNames[month];
|
|
}
|
|
|
|
/// <summary>Returns a full display string with named month and ordinal day.</summary>
|
|
public string FullStringNamed() {
|
|
var displayDay = Now.day;
|
|
var postFix = displayDay switch {
|
|
1 => "st",
|
|
2 => "nd",
|
|
3 => "rd",
|
|
_ => "th"
|
|
};
|
|
var year = Now.year;
|
|
var timeString = Now.TimeString;
|
|
return $"{timeString}, {displayDay}{postFix} day of {GetMonthName()}, \nyear {year}";
|
|
}
|
|
|
|
private static float Clamp01(float v) {
|
|
return v < 0f ? 0f : v > 1f ? 1f : v;
|
|
}
|
|
}
|
|
}
|